use super::*;
use async_trait::async_trait;
pub(crate) const WM_LINGXIA_COMMAND: u32 = WM_APP + 0x154;
pub(crate) const WEBVIEW_SCREENSHOT_TIMEOUT: Duration = Duration::from_secs(4);
const BROWSER_EMULATION_TIMEOUT: Duration = Duration::from_secs(4);
pub(crate) enum UiCommand {
LoadUrl {
url: String,
resp: Sender<StdResult<()>>,
},
LoadHtml {
html: String,
base_url: String,
history_url: Option<String>,
resp: Sender<StdResult<()>>,
},
ExecJs {
js: String,
resp: Sender<StdResult<()>>,
},
EvalJs {
js: String,
resp: Sender<std::result::Result<serde_json::Value, WebViewScriptError>>,
},
PostMessage {
message: String,
resp: Sender<StdResult<()>>,
},
SetUserAgentOverride {
user_agent: UserAgentOverride,
resp: Sender<StdResult<()>>,
},
SetBrowserEmulationProfile {
profile: WindowsBrowserEmulationProfile,
resp: Sender<StdResult<String>>,
},
ClearBrowsingData {
resp: Sender<StdResult<()>>,
},
ClearProfileData {
kind: super::data_store::BrowsingDataKind,
since_unix_ms: Option<u64>,
resp: Sender<StdResult<()>>,
},
CurrentUrl {
resp: Sender<StdResult<Option<String>>>,
},
Reload {
resp: Sender<StdResult<()>>,
},
GoBack {
resp: Sender<StdResult<()>>,
},
GoForward {
resp: Sender<StdResult<()>>,
},
TakeScreenshot {
resp: Sender<StdResult<Vec<u8>>>,
},
ListCookies {
resp: Sender<StdResult<Vec<WebViewCookie>>>,
},
SetCookie {
request: WebViewCookieSetRequest,
resp: Sender<StdResult<()>>,
},
DeleteCookie {
name: String,
domain: String,
path: String,
resp: Sender<StdResult<()>>,
},
ClearCookies {
resp: Sender<StdResult<()>>,
},
StartNetworkCapture {
resp: Sender<StdResult<()>>,
},
StopNetworkCapture {
resp: Sender<StdResult<()>>,
},
NetworkEntries {
resp: Sender<StdResult<NetworkCaptureSnapshot>>,
},
ClearNetworkCapture {
resp: Sender<StdResult<()>>,
},
CallDevToolsProtocol {
method: String,
params: String,
resp: Sender<StdResult<String>>,
},
OpenDevTools {
resp: Sender<StdResult<()>>,
},
SetContentBounds {
bounds: RECT,
resp: Sender<StdResult<()>>,
},
SetContentGeometry {
bounds: RECT,
radii: [i32; 4],
corner_color: u32,
resp: Sender<StdResult<()>>,
},
SetContentVisible {
visible: bool,
resp: Sender<StdResult<()>>,
},
SetParentWindow {
window: isize,
resp: Sender<StdResult<()>>,
},
NotifyParentPositionChanged {
resp: Sender<StdResult<()>>,
},
SetRasterizationScale {
scale: f64,
resp: Sender<StdResult<()>>,
},
Shutdown,
}
pub(crate) struct UiState {
pub(crate) controller: ICoreWebView2Controller,
pub(crate) webview: ICoreWebView2,
pub(crate) hosting: HostingMode,
pub(crate) hwnd: HWND,
pub(crate) native_view: WindowsWebViewNativeView,
pub(crate) webtag_key: String,
pub(crate) memory_pages: Arc<Mutex<HashMap<String, Vec<u8>>>>,
pub(crate) ephemeral_user_data_dir: Option<PathBuf>,
pub(crate) network_log: Arc<Mutex<network::NetworkLog>>,
pub(crate) network_receivers: Vec<(ICoreWebView2DevToolsProtocolEventReceiver, i64)>,
pub(crate) _console_receivers: Vec<(ICoreWebView2DevToolsProtocolEventReceiver, i64)>,
pub(crate) default_user_agent: String,
pub(crate) browser_emulation_configured: bool,
}
impl UiState {
pub(crate) fn notify_parent_position_changed(&self) {
unsafe {
let _ = self.controller.NotifyParentWindowPositionChanged();
}
}
}
pub struct WebViewInner {
command_tx: Sender<UiCommand>,
thread_id: u32,
join_handle: Mutex<Option<JoinHandle<()>>>,
pub(crate) webtag: WebTag,
pub(crate) native_view: isize,
pub(crate) composition_hosted: bool,
}
impl std::fmt::Debug for WebViewInner {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("WebViewInner")
.field("thread_id", &self.thread_id)
.field("webtag", &self.webtag)
.finish()
}
}
impl WebViewInner {
pub(crate) fn create(
appid: &str,
path: &str,
session_id: Option<u64>,
effective_options: EffectiveWebViewCreateOptions,
sender: WebViewCreateSender,
) {
let webtag = WebTag::new(appid, path, session_id);
let webtag_for_thread = webtag.clone();
let effective_options_for_thread = effective_options.clone();
let (startup_tx, startup_rx) = mpsc::channel();
let join_handle = thread::Builder::new()
.name(format!("lingxia-webview-{}", webtag.as_str()))
.spawn(move || {
if let Err(err) =
run_ui_thread(webtag_for_thread, effective_options_for_thread, startup_tx)
{
log::error!("Windows WebView UI thread failed: {}", err);
}
});
let join_handle = match join_handle {
Ok(handle) => handle,
Err(err) => {
sender.fail(
WebViewCreateStage::Requested,
WebViewError::WebView(format!(
"Failed to spawn Windows WebView thread: {}",
err
)),
);
return;
}
};
match startup_rx.recv() {
Ok(Ok((command_tx, thread_id, native_view, composition_hosted))) => {
let webview = Arc::new(crate::WebView::new(
WebViewInner {
command_tx,
thread_id,
join_handle: Mutex::new(Some(join_handle)),
webtag,
native_view,
composition_hosted,
},
effective_options,
));
if sender.is_destroyed() {
log::info!(
"Windows WebView for {} was destroyed during creation; discarding",
webview.webtag().key()
);
return;
}
register_webview(webview.clone());
if sender.is_destroyed() {
log::info!(
"Windows WebView for {} was destroyed during registration; discarding",
webview.webtag().key()
);
crate::webview::destroy_webview(&webview.webtag());
return;
}
sender.succeed(webview);
}
Ok(Err(err)) => {
sender.fail(WebViewCreateStage::Requested, err);
let _ = join_handle.join();
}
Err(err) => {
sender.fail(
WebViewCreateStage::Requested,
WebViewError::WebView(format!(
"Windows WebView startup channel failed: {}",
err
)),
);
let _ = join_handle.join();
}
}
}
pub(super) fn dispatch_ui<T>(
&self,
make: impl FnOnce(Sender<T>) -> UiCommand,
timeout: Option<Duration>,
) -> std::result::Result<T, UiDispatchError> {
if unsafe { Threading::GetCurrentThreadId() } == self.thread_id {
return Err(UiDispatchError::SameThread);
}
let (resp_tx, resp_rx) = mpsc::channel();
self.command_tx
.send(make(resp_tx))
.map_err(|_| UiDispatchError::Unavailable)?;
self.wake_ui_thread();
recv_reply_pumping(&resp_rx, timeout).map_err(|err| match err {
mpsc::RecvTimeoutError::Timeout => UiDispatchError::NoReply(Some(err.to_string())),
mpsc::RecvTimeoutError::Disconnected => UiDispatchError::NoReply(None),
})
}
fn dispatch_command(
&self,
command: impl FnOnce(Sender<StdResult<()>>) -> UiCommand,
) -> StdResult<()> {
self.dispatch_ui(command, None)
.map_err(|err| err.into_webview_error("run synchronous WebView command"))?
}
fn dispatch_command_same_thread_safe(
&self,
command: impl FnOnce(Sender<StdResult<()>>) -> UiCommand,
) -> StdResult<()> {
let (resp_tx, resp_rx) = mpsc::channel();
self.command_tx
.send(command(resp_tx))
.map_err(|_| WebViewError::WebView("WebView UI thread is unavailable".to_string()))?;
self.wake_ui_thread();
if unsafe { Threading::GetCurrentThreadId() } == self.thread_id {
return Ok(());
}
recv_reply_pumping(&resp_rx, None)
.map_err(|_| WebViewError::WebView("WebView UI thread did not reply".to_string()))?
.map_err(|err| WebViewError::WebView(format!("run WebView command: {err}")))
}
pub(crate) fn set_content_bounds(&self, bounds: RECT) -> StdResult<()> {
self.dispatch_command_same_thread_safe(|resp| UiCommand::SetContentBounds { bounds, resp })
}
pub(crate) fn set_browser_emulation_profile(
&self,
profile: WindowsBrowserEmulationProfile,
) -> StdResult<()> {
self.dispatch_ui(
|resp| UiCommand::SetBrowserEmulationProfile { profile, resp },
Some(BROWSER_EMULATION_TIMEOUT),
)
.map_err(|err| err.into_webview_error("set browser emulation profile"))??;
Ok(())
}
pub(crate) fn set_content_geometry(
&self,
bounds: RECT,
radii: [i32; 4],
corner_color: u32,
) -> StdResult<()> {
self.dispatch_command_same_thread_safe(|resp| UiCommand::SetContentGeometry {
bounds,
radii,
corner_color,
resp,
})
}
pub(crate) fn set_content_visible(&self, visible: bool) -> StdResult<()> {
self.dispatch_command_same_thread_safe(|resp| UiCommand::SetContentVisible {
visible,
resp,
})
}
pub(crate) fn set_parent_window(&self, window: isize) -> StdResult<()> {
self.dispatch_command_same_thread_safe(|resp| UiCommand::SetParentWindow { window, resp })
}
pub(crate) fn dispatch_screenshot_command(&self) -> StdResult<Vec<u8>> {
self.dispatch_ui(
|resp| UiCommand::TakeScreenshot { resp },
Some(WEBVIEW_SCREENSHOT_TIMEOUT),
)
.map_err(|err| match err {
UiDispatchError::NoReply(Some(detail)) => {
WebViewError::WebView(format!("WebView screenshot timed out: {detail}"))
}
err => err.into_webview_error("capture WebView screenshot"),
})?
}
pub(crate) fn open_devtools(&self) -> StdResult<()> {
self.dispatch_command(|resp| UiCommand::OpenDevTools { resp })
}
pub(crate) fn notify_parent_position_changed(&self) -> StdResult<()> {
self.dispatch_command_same_thread_safe(|resp| UiCommand::NotifyParentPositionChanged {
resp,
})
}
pub(crate) fn set_rasterization_scale(&self, scale: f64) -> StdResult<()> {
self.dispatch_command_same_thread_safe(|resp| UiCommand::SetRasterizationScale {
scale,
resp,
})
}
fn wake_ui_thread(&self) {
let posted = unsafe {
WindowsAndMessaging::PostMessageW(
Some(hwnd_from_handle(self.native_view)),
WM_LINGXIA_COMMAND,
WPARAM::default(),
LPARAM::default(),
)
.is_ok()
};
if !posted {
unsafe {
let _ = WindowsAndMessaging::PostThreadMessageW(
self.thread_id,
WM_LINGXIA_COMMAND,
WPARAM::default(),
LPARAM::default(),
);
}
}
}
pub(super) fn dispatch_eval_command(
&self,
js: String,
) -> std::result::Result<serde_json::Value, WebViewScriptError> {
self.dispatch_ui(|resp| UiCommand::EvalJs { js, resp }, None)
.map_err(|err| match err {
UiDispatchError::SameThread => WebViewScriptError::Platform(
"Cannot evaluate JavaScript from WebView UI thread".to_string(),
),
UiDispatchError::Unavailable | UiDispatchError::NoReply(_) => {
WebViewScriptError::Destroyed
}
})?
}
fn dispatch_current_url(&self) -> StdResult<Option<String>> {
self.dispatch_ui(|resp| UiCommand::CurrentUrl { resp }, None)
.map_err(|err| err.into_webview_error("read current WebView URL"))?
}
fn dispatch_list_cookies(&self) -> StdResult<Vec<WebViewCookie>> {
self.dispatch_ui(|resp| UiCommand::ListCookies { resp }, None)
.map_err(|err| err.into_webview_error("list WebView cookies"))?
}
fn dispatch_network_entries(&self) -> StdResult<NetworkCaptureSnapshot> {
self.dispatch_ui(|resp| UiCommand::NetworkEntries { resp }, None)
.map_err(|err| err.into_webview_error("read network capture"))?
}
pub(super) fn dispatch_cdp_command(
&self,
method: &str,
params: serde_json::Value,
) -> StdResult<String> {
let (method, params) = (method.to_string(), params.to_string());
self.dispatch_ui(
|resp| UiCommand::CallDevToolsProtocol {
method,
params,
resp,
},
Some(Duration::from_secs(4)),
)
.map_err(|err| err.into_webview_error("run CDP command"))?
}
}
#[derive(Debug)]
pub(crate) enum UiDispatchError {
SameThread,
Unavailable,
NoReply(Option<String>),
}
impl UiDispatchError {
fn into_webview_error(self, action: &str) -> WebViewError {
WebViewError::WebView(match self {
UiDispatchError::SameThread => {
format!("Cannot {action} from WebView UI thread")
}
UiDispatchError::Unavailable => "WebView UI thread is unavailable".to_string(),
UiDispatchError::NoReply(detail) => detail
.map(|detail| format!("WebView UI thread did not reply: {detail}"))
.unwrap_or_else(|| "WebView UI thread did not reply".to_string()),
})
}
}
#[async_trait]
impl WebViewController for WebViewInner {
fn load_url(&self, url: &str) -> StdResult<()> {
self.dispatch_command(|resp| UiCommand::LoadUrl {
url: url.to_string(),
resp,
})
}
fn load_data(&self, request: LoadDataRequest<'_>) -> StdResult<()> {
self.dispatch_command_same_thread_safe(|resp| UiCommand::LoadHtml {
html: request.data.to_string(),
base_url: request.base_url.to_string(),
history_url: request.history_url.map(str::to_string),
resp,
})
}
fn exec_js(&self, js: &str) -> StdResult<()> {
self.dispatch_command_same_thread_safe(|resp| UiCommand::ExecJs {
js: js.to_string(),
resp,
})
}
async fn eval_js(
&self,
js: &str,
) -> std::result::Result<serde_json::Value, WebViewScriptError> {
self.dispatch_eval_command(js.to_string())
}
fn post_message(&self, message: &str) -> StdResult<()> {
self.dispatch_command(|resp| UiCommand::PostMessage {
message: message.to_string(),
resp,
})
}
fn clear_browsing_data(&self) -> StdResult<()> {
self.dispatch_command(|resp| UiCommand::ClearBrowsingData { resp })
}
fn set_user_agent_override(&self, user_agent: UserAgentOverride) -> StdResult<()> {
self.dispatch_command(|resp| UiCommand::SetUserAgentOverride { user_agent, resp })
}
async fn current_url(&self) -> StdResult<Option<String>> {
self.dispatch_current_url()
}
fn reload(&self) -> StdResult<()> {
self.dispatch_command(|resp| UiCommand::Reload { resp })
}
fn go_back(&self) -> StdResult<()> {
self.dispatch_command(|resp| UiCommand::GoBack { resp })
}
fn go_forward(&self) -> StdResult<()> {
self.dispatch_command(|resp| UiCommand::GoForward { resp })
}
async fn take_screenshot(&self) -> StdResult<Vec<u8>> {
self.dispatch_screenshot_command()
}
async fn list_cookies(&self) -> StdResult<Vec<WebViewCookie>> {
self.dispatch_list_cookies()
}
async fn set_cookie(&self, request: WebViewCookieSetRequest) -> StdResult<()> {
self.dispatch_command(|resp| UiCommand::SetCookie { request, resp })
}
async fn delete_cookie(&self, name: &str, domain: &str, path: &str) -> StdResult<()> {
let (name, domain, path) = (name.to_string(), domain.to_string(), path.to_string());
self.dispatch_command(|resp| UiCommand::DeleteCookie {
name,
domain,
path,
resp,
})
}
async fn clear_cookies(&self) -> StdResult<()> {
self.dispatch_command(|resp| UiCommand::ClearCookies { resp })
}
async fn clear_site_data(
&self,
url: &str,
options: ClearSiteDataOptions,
) -> StdResult<ClearSiteDataResult> {
let uri = url
.parse::<http::Uri>()
.map_err(|_| WebViewError::WebView("site data URL is invalid".to_string()))?;
let host = uri
.host()
.filter(|host| !host.is_empty())
.ok_or_else(|| WebViewError::WebView("site data URL has no host".to_string()))?
.trim_start_matches('.')
.to_ascii_lowercase();
if !matches!(uri.scheme_str(), Some("http" | "https")) {
return Err(WebViewError::WebView(
"site data URL must use HTTP or HTTPS".to_string(),
));
}
let current = self.current_url().await?.ok_or_else(|| {
WebViewError::WebView("current WebView has no website URL".to_string())
})?;
let current_host = current
.parse::<http::Uri>()
.ok()
.and_then(|uri| uri.host().map(str::to_ascii_lowercase));
if current_host.as_deref() != Some(host.as_str()) {
return Err(WebViewError::WebView(
"current website changed before its data could be cleared".to_string(),
));
}
if options.site_data {
for cookie in self.list_cookies().await? {
let domain = cookie.domain.trim_start_matches('.').to_ascii_lowercase();
let matches = if cookie.host_only {
domain == host
} else {
host == domain
|| host
.strip_suffix(&domain)
.is_some_and(|prefix| prefix.ends_with('.'))
};
if matches {
self.delete_cookie(&cookie.name, &cookie.domain, &cookie.path)
.await?;
}
}
}
let mut storage_types = Vec::new();
if options.site_data {
storage_types.extend([
"file_systems",
"indexeddb",
"local_storage",
"websql",
"service_workers",
]);
}
if options.cache {
storage_types.extend(["appcache", "cache_storage"]);
}
if !storage_types.is_empty() {
let scheme = uri.scheme_str().unwrap_or("https");
let authority = uri.authority().ok_or_else(|| {
WebViewError::WebView("site data URL has no authority".to_string())
})?;
self.dispatch_cdp_command(
"Storage.clearDataForOrigin",
serde_json::json!({
"origin": format!("{scheme}://{authority}"),
"storageTypes": storage_types.join(","),
}),
)?;
}
Ok(ClearSiteDataResult {
cache_cleared: false,
site_data_cleared: options.site_data,
})
}
async fn start_network_capture(&self) -> StdResult<()> {
self.dispatch_command(|resp| UiCommand::StartNetworkCapture { resp })
}
async fn stop_network_capture(&self) -> StdResult<()> {
self.dispatch_command(|resp| UiCommand::StopNetworkCapture { resp })
}
async fn network_entries(&self) -> StdResult<NetworkCaptureSnapshot> {
self.dispatch_network_entries()
}
async fn clear_network_capture(&self) -> StdResult<()> {
self.dispatch_command(|resp| UiCommand::ClearNetworkCapture { resp })
}
}
impl WebViewInner {
pub(crate) fn clear_profile_data(
&self,
kind: super::data_store::BrowsingDataKind,
since_unix_ms: Option<u64>,
) -> StdResult<()> {
self.dispatch_command(|resp| UiCommand::ClearProfileData {
kind,
since_unix_ms,
resp,
})
}
}
impl Drop for WebViewInner {
fn drop(&mut self) {
let _ = self.command_tx.send(UiCommand::Shutdown);
self.wake_ui_thread();
if let Ok(mut guard) = self.join_handle.lock() {
drop(guard.take());
}
}
}
pub(crate) type WebViewStartup = (Sender<UiCommand>, u32, isize, bool);
pub(crate) fn run_ui_thread(
webtag: WebTag,
effective_options: EffectiveWebViewCreateOptions,
startup_tx: Sender<StdResult<WebViewStartup>>,
) -> StdResult<()> {
unsafe {
windows::Win32::System::Ole::OleInitialize(None)
.map_err(|err| WebViewError::WebView(format!("OleInitialize failed: {err}")))?;
}
let result = run_ui_thread_inner(webtag, effective_options, startup_tx);
unsafe {
windows::Win32::System::Ole::OleUninitialize();
}
result
}
type WebViewControllerSetup = (
ICoreWebView2Controller,
HostingMode,
ICoreWebView2,
Arc<Mutex<HashMap<String, Vec<u8>>>>,
EphemeralProfileGuard,
);
struct EphemeralProfileGuard(Option<PathBuf>);
impl EphemeralProfileGuard {
fn take(&mut self) -> Option<PathBuf> {
self.0.take()
}
}
impl Drop for EphemeralProfileGuard {
fn drop(&mut self) {
if let Some(dir) = self.0.take() {
schedule_ephemeral_profile_cleanup(dir);
}
}
}
pub(crate) fn run_ui_thread_inner(
webtag: WebTag,
effective_options: EffectiveWebViewCreateOptions,
startup_tx: Sender<StdResult<WebViewStartup>>,
) -> StdResult<()> {
ensure_message_queue();
let native_view = create_webview_parent(&webtag)?;
let hwnd = hwnd_from_handle(native_view.window);
let webtag_key = webtag.key().to_string();
let setup = (|| -> StdResult<WebViewControllerSetup> {
let (env, ephemeral_user_data_dir) = create_environment(&webtag, &effective_options)?;
let ephemeral_profile = EphemeralProfileGuard(ephemeral_user_data_dir);
let (controller, hosting) = create_hosting_controller(&env, hwnd)?;
let webview = unsafe {
controller
.CoreWebView2()
.map_err(|err| WebViewError::WebView(format!("CoreWebView2 failed: {err}")))?
};
let bounds = webview_parent_bounds(native_view)?;
unsafe {
controller
.SetBounds(bounds)
.map_err(|err| WebViewError::WebView(format!("SetBounds failed: {err}")))?;
}
configure_controller(&controller)?;
configure_settings(&webview, &effective_options)?;
let menu_appid = webtag.extract_appid();
let menu_path = webtag.extract_parts().1;
configure_context_menu(&webview, &env, &menu_appid, &menu_path, &effective_options)?;
let inject_platform_baseline = effective_options.profile != SecurityProfile::BrowserRelaxed;
install_document_scripts(&webview, inject_platform_baseline)?;
let memory_pages = Arc::new(Mutex::new(HashMap::new()));
register_event_handlers(
&env,
&webview,
webtag.clone(),
&effective_options.registered_schemes,
memory_pages.clone(),
)?;
Ok((
controller,
hosting,
webview,
memory_pages,
ephemeral_profile,
))
})();
let (controller, hosting, webview, memory_pages, mut ephemeral_profile) = match setup {
Ok(parts) => parts,
Err(err) => {
let _ = startup_tx.send(Err(err.clone()));
destroy_webview_parent(webtag.key(), native_view);
return Err(err);
}
};
let (command_tx, command_rx) = mpsc::channel();
let console_receivers = match console::subscribe(&webview, &webtag) {
Ok(receivers) => {
console::enable(&webview);
receivers
}
Err(err) => {
log::warn!("console log capture unavailable: {err}");
Vec::new()
}
};
let default_user_agent = match user_agent(&webview) {
Ok(user_agent) => user_agent,
Err(err) => {
let _ = startup_tx.send(Err(err.clone()));
unsafe {
let _ = controller.Close();
}
if let HostingMode::Composition(surface) = &hosting {
surface.destroy();
}
destroy_webview_parent(webtag.key(), native_view);
return Err(err);
}
};
let configured_profile = browser_emulation::configured_profile();
let mut state = UiState {
controller,
webview,
hosting,
hwnd,
native_view,
webtag_key,
memory_pages,
ephemeral_user_data_dir: ephemeral_profile.take(),
network_log: Arc::new(Mutex::new(network::NetworkLog::default())),
network_receivers: Vec::new(),
_console_receivers: console_receivers,
default_user_agent,
browser_emulation_configured: configured_profile.is_some(),
};
let bootstrap = (|| -> StdResult<()> {
if let Some(profile) = configured_profile
&& profile != WindowsBrowserEmulationProfile::Desktop
{
let (profile_tx, profile_rx) = mpsc::channel();
browser_emulation::apply_profile(
&state.webview,
&state.default_user_agent,
profile,
profile_tx,
);
wait_for_ui_reply(&profile_rx, BROWSER_EMULATION_TIMEOUT)??;
}
Ok(())
})();
if let Err(err) = bootstrap {
let _ = startup_tx.send(Err(err.clone()));
cleanup_state(&mut state);
return Err(err);
}
if startup_tx
.send(Ok((
command_tx,
unsafe { Threading::GetCurrentThreadId() },
native_view.window,
matches!(state.hosting, HostingMode::Composition(_)),
)))
.is_err()
{
cleanup_state(&mut state);
return Err(WebViewError::WebView(
"Failed to publish WebView startup".to_string(),
));
}
message_loop(&mut state, command_rx)
}
fn wait_for_ui_reply<T>(resp_rx: &Receiver<T>, timeout: Duration) -> StdResult<T> {
let deadline = std::time::Instant::now() + timeout;
loop {
match resp_rx.try_recv() {
Ok(reply) => return Ok(reply),
Err(mpsc::TryRecvError::Disconnected) => {
return Err(WebViewError::WebView(
"WebView2 callback channel disconnected".to_string(),
));
}
Err(mpsc::TryRecvError::Empty) => {}
}
if std::time::Instant::now() >= deadline {
return Err(WebViewError::WebView(
"WebView2 browser emulation timed out".to_string(),
));
}
let mut dispatched = false;
unsafe {
let mut msg = MSG::default();
while WindowsAndMessaging::PeekMessageW(
&mut msg,
None,
0,
0,
WindowsAndMessaging::PM_REMOVE,
)
.as_bool()
{
dispatched = true;
if msg.message == WindowsAndMessaging::WM_QUIT {
return Err(WebViewError::WebView(
"WebView thread quit during browser emulation".to_string(),
));
}
if msg.message != WM_LINGXIA_COMMAND {
let _ = WindowsAndMessaging::TranslateMessage(&msg);
WindowsAndMessaging::DispatchMessageW(&msg);
}
}
}
if !dispatched {
std::thread::sleep(Duration::from_millis(1));
}
}
}
fn recv_reply_pumping<T>(
resp_rx: &Receiver<T>,
timeout: Option<Duration>,
) -> std::result::Result<T, mpsc::RecvTimeoutError> {
const PUMP_SLICE: Duration = Duration::from_millis(10);
let deadline = timeout.map(|timeout| std::time::Instant::now() + timeout);
loop {
let slice = match deadline {
Some(deadline) => {
let Some(left) = deadline.checked_duration_since(std::time::Instant::now()) else {
return Err(mpsc::RecvTimeoutError::Timeout);
};
PUMP_SLICE.min(left)
}
None => PUMP_SLICE,
};
match resp_rx.recv_timeout(slice) {
Err(mpsc::RecvTimeoutError::Timeout) => unsafe {
let mut msg = MSG::default();
let _ = WindowsAndMessaging::PeekMessageW(
&mut msg,
None,
0,
0,
WindowsAndMessaging::PM_NOREMOVE,
);
},
reply => return reply,
}
}
}
pub(crate) fn ensure_message_queue() {
let mut msg = MSG::default();
unsafe {
let _ = WindowsAndMessaging::PeekMessageW(
&mut msg,
None,
0,
0,
WindowsAndMessaging::PM_NOREMOVE,
);
}
}
pub(crate) fn message_loop(state: &mut UiState, command_rx: Receiver<UiCommand>) -> StdResult<()> {
let mut msg = MSG::default();
loop {
while let Ok(command) = command_rx.try_recv() {
if handle_command(state, command)? {
cleanup_state(state);
return Ok(());
}
}
let status = unsafe { WindowsAndMessaging::GetMessageW(&mut msg, None, 0, 0).0 };
match status {
-1 => {
cleanup_state(state);
return Err(WebViewError::WebView(
"GetMessageW failed in WebView loop".to_string(),
));
}
0 => {
cleanup_state(state);
return Ok(());
}
_ => {
if msg.message != WM_LINGXIA_COMMAND {
unsafe {
let _ = WindowsAndMessaging::TranslateMessage(&msg);
WindowsAndMessaging::DispatchMessageW(&msg);
}
}
}
}
}
}
pub(crate) fn handle_command(state: &mut UiState, command: UiCommand) -> StdResult<bool> {
match command {
UiCommand::LoadUrl { url, resp } => {
clear_memory_pages(&state.memory_pages);
let result = unsafe {
let url = CoTaskMemPWSTR::from(url.as_str());
state
.webview
.Navigate(*url.as_ref().as_pcwstr())
.map_err(|err| WebViewError::WebView(format!("Navigate failed: {err}")))
};
let _ = resp.send(result);
}
UiCommand::LoadHtml {
html,
base_url,
history_url,
resp,
} => {
let navigation_url = history_url.unwrap_or_else(|| base_url.clone());
clear_memory_pages(&state.memory_pages);
store_memory_page(
&state.memory_pages,
&navigation_url,
prepare_navigation_html(&html, &base_url, &navigation_url),
);
if navigation_url != base_url {
store_memory_page(&state.memory_pages, &base_url, html.into_bytes());
}
let result = unsafe {
let url = CoTaskMemPWSTR::from(navigation_url.as_str());
state
.webview
.Navigate(*url.as_ref().as_pcwstr())
.map_err(|err| WebViewError::WebView(format!("Navigate failed: {err}")))
};
let _ = resp.send(result);
}
UiCommand::ExecJs { js, resp } => {
start_execute_script(&state.webview, &js, resp, |result| {
result
.map(|_| ())
.map_err(|err| WebViewError::WebView(format!("ExecuteScript failed: {err}")))
});
}
UiCommand::EvalJs { js, resp } => {
start_execute_script(&state.webview, &js, resp, |result| {
result.and_then(|json| decode_script_result(&json))
});
}
UiCommand::PostMessage { message, resp } => {
let result = unsafe {
let message = CoTaskMemPWSTR::from(message.as_str());
state
.webview
.PostWebMessageAsString(*message.as_ref().as_pcwstr())
.map_err(|err| {
WebViewError::WebView(format!("PostWebMessageAsString failed: {err}"))
})
};
let _ = resp.send(result);
}
UiCommand::SetUserAgentOverride { user_agent, resp } => {
if state.browser_emulation_configured {
let _ = resp.send(Err(WebViewError::WebView(
"per-WebView user-agent overrides conflict with host browser emulation"
.to_string(),
)));
return Ok(false);
}
let user_agent = match user_agent {
UserAgentOverride::Default => state.default_user_agent.as_str(),
UserAgentOverride::Custom(ref user_agent) => user_agent.as_str(),
};
let result = set_user_agent_override(&state.webview, user_agent);
let _ = resp.send(result);
}
UiCommand::SetBrowserEmulationProfile { profile, resp } => {
if state.browser_emulation_configured {
browser_emulation::apply_profile(
&state.webview,
&state.default_user_agent,
profile,
resp,
);
} else {
let _ = resp.send(Err(WebViewError::WebView(
"browser emulation was not configured before WebView creation".to_string(),
)));
}
}
UiCommand::ClearBrowsingData { resp } => {
if let Err(err) = begin_clear_browsing_data(&state.webview, resp.clone()) {
let _ = resp.send(Err(err));
}
}
UiCommand::ClearProfileData {
kind,
since_unix_ms,
resp,
} => {
if let Err(err) =
begin_clear_profile_data(&state.webview, kind, since_unix_ms, resp.clone())
{
let _ = resp.send(Err(err));
}
}
UiCommand::CurrentUrl { resp } => {
let result = current_url(&state.webview);
let _ = resp.send(result);
}
UiCommand::Reload { resp } => {
let result = unsafe {
state
.webview
.Reload()
.map_err(|err| WebViewError::WebView(format!("Reload failed: {err}")))
};
let _ = resp.send(result);
}
UiCommand::GoBack { resp } => {
let result = go_history(&state.webview, HistoryDirection::Back);
let _ = resp.send(result);
}
UiCommand::GoForward { resp } => {
let result = go_history(&state.webview, HistoryDirection::Forward);
let _ = resp.send(result);
}
UiCommand::TakeScreenshot { resp } => {
start_capture_preview_png(&state.webview, resp);
}
UiCommand::ListCookies { resp } => {
start_list_cookies(&state.webview, resp);
}
UiCommand::SetCookie { request, resp } => {
let result = set_cookie(&state.webview, &request);
let _ = resp.send(result);
}
UiCommand::DeleteCookie {
name,
domain,
path,
resp,
} => {
let result = delete_cookie(&state.webview, &name, &domain, &path);
let _ = resp.send(result);
}
UiCommand::ClearCookies { resp } => {
let result = clear_cookies(&state.webview);
let _ = resp.send(result);
}
UiCommand::StartNetworkCapture { resp } => {
if !state.network_receivers.is_empty() {
let _ = resp.send(Ok(())); } else {
match network::subscribe(&state.webview, &state.network_log) {
Ok(receivers) => {
state.network_receivers = receivers;
network::enable_domain(&state.webview, resp);
}
Err(err) => {
let _ = resp.send(Err(err));
}
}
}
}
UiCommand::StopNetworkCapture { resp } => {
network::stop_capture(&state.webview, &mut state.network_receivers);
let _ = resp.send(Ok(()));
}
UiCommand::NetworkEntries { resp } => {
let snapshot = state
.network_log
.lock()
.map(|log| log.snapshot())
.unwrap_or_default();
let _ = resp.send(Ok(snapshot));
}
UiCommand::ClearNetworkCapture { resp } => {
if let Ok(mut log) = state.network_log.lock() {
log.clear();
}
let _ = resp.send(Ok(()));
}
UiCommand::CallDevToolsProtocol {
method,
params,
resp,
} => {
start_call_devtools_protocol(&state.webview, &method, ¶ms, resp);
}
UiCommand::OpenDevTools { resp } => {
let result = unsafe {
state.webview.OpenDevToolsWindow().map_err(|err| {
WebViewError::WebView(format!("OpenDevToolsWindow failed: {err}"))
})
};
let _ = resp.send(result);
}
UiCommand::SetContentBounds { bounds, resp } => {
let result = set_content_geometry(state, bounds, None);
let _ = resp.send(result);
}
UiCommand::SetContentGeometry {
bounds,
radii,
corner_color,
resp,
} => {
let result = set_content_geometry(state, bounds, Some((radii, corner_color)));
let _ = resp.send(result);
}
UiCommand::SetContentVisible { visible, resp } => {
let result = set_controller_visible(state, visible);
let _ = resp.send(result);
}
UiCommand::SetParentWindow { window, resp } => {
let hwnd = hwnd_from_handle(window);
let composition_hosted = matches!(&state.hosting, HostingMode::Composition(_));
if !should_update_parent(composition_hosted, state.hwnd == hwnd) {
let _ = resp.send(Ok(()));
return Ok(false);
}
let result = match &mut state.hosting {
HostingMode::Windowed => unsafe {
state.controller.SetParentWindow(hwnd).map_err(|err| {
WebViewError::WebView(format!("SetParentWindow failed: {err}"))
})
},
HostingMode::Composition(surface) => surface.set_parent(&state.controller, hwnd),
};
if result.is_ok() {
state.hwnd = hwnd;
}
let _ = resp.send(result);
}
UiCommand::NotifyParentPositionChanged { resp } => {
state.notify_parent_position_changed();
let _ = resp.send(Ok(()));
}
UiCommand::SetRasterizationScale { scale, resp } => {
let result = state
.controller
.cast::<ICoreWebView2Controller3>()
.map_err(|err| WebViewError::WebView(format!("Controller3 cast failed: {err}")))
.and_then(|controller3| unsafe {
controller3
.SetRasterizationScale(scale.max(0.1))
.map_err(|err| {
WebViewError::WebView(format!("SetRasterizationScale failed: {err}"))
})
});
let _ = resp.send(result);
}
UiCommand::Shutdown => return Ok(true),
}
Ok(false)
}
fn should_update_parent(composition_hosted: bool, same_parent: bool) -> bool {
composition_hosted || !same_parent
}
pub(crate) fn cleanup_state(state: &mut UiState) {
unsafe {
let _ = state.controller.Close();
}
if let HostingMode::Composition(surface) = &state.hosting {
surface.destroy();
}
destroy_webview_parent(&state.webtag_key, state.native_view);
if let Some(dir) = state.ephemeral_user_data_dir.take() {
schedule_ephemeral_profile_cleanup(dir);
}
}
fn schedule_ephemeral_profile_cleanup(dir: PathBuf) {
let _ = std::thread::Builder::new()
.name("lingxia-webview-profile-cleanup".to_string())
.spawn(move || {
for attempt in 0..20 {
if !dir.exists() || std::fs::remove_dir_all(&dir).is_ok() {
return;
}
std::thread::sleep(std::time::Duration::from_millis(50 + attempt * 25));
}
log::warn!("failed to remove ephemeral WebView2 profile {dir:?}");
});
}
pub(crate) fn set_controller_visible(state: &mut UiState, visible: bool) -> StdResult<()> {
match &mut state.hosting {
HostingMode::Windowed => unsafe {
state
.controller
.SetIsVisible(visible)
.map_err(|err| WebViewError::WebView(format!("SetIsVisible failed: {err}")))
},
HostingMode::Composition(surface) => surface.set_visible(&state.controller, visible),
}
}
fn set_content_geometry(
state: &mut UiState,
bounds: RECT,
corners: Option<([i32; 4], u32)>,
) -> StdResult<()> {
match &mut state.hosting {
HostingMode::Windowed => unsafe {
state
.controller
.SetBounds(bounds)
.map_err(|err| WebViewError::WebView(format!("SetBounds failed: {err}")))
},
HostingMode::Composition(surface) => {
surface.set_geometry(&state.controller, bounds, corners)
}
}
}
#[cfg(test)]
mod tests {
use super::should_update_parent;
#[test]
fn composition_rechecks_a_reused_parent_handle() {
assert!(should_update_parent(true, true));
assert!(!should_update_parent(false, true));
assert!(should_update_parent(false, false));
}
}