Skip to main content

lingxia_webview/
webview.rs

1#![cfg_attr(
2    not(any(
3        target_os = "android",
4        target_os = "ios",
5        target_os = "macos",
6        target_os = "windows",
7        all(target_os = "linux", target_env = "ohos")
8    )),
9    allow(dead_code)
10)]
11
12use serde::{Deserialize, Serialize};
13use std::collections::{HashMap, HashSet};
14use std::future::Future;
15use std::pin::Pin;
16use std::sync::mpsc::{SyncSender, sync_channel};
17use std::sync::{Arc, Mutex, OnceLock, RwLock};
18use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
19use tokio::sync::watch;
20
21#[cfg(target_os = "android")]
22use crate::android::WebViewInner;
23
24#[cfg(any(target_os = "ios", target_os = "macos"))]
25use crate::apple::WebViewInner;
26
27#[cfg(all(target_os = "linux", target_env = "ohos"))]
28use crate::harmony::WebViewInner;
29
30#[cfg(target_os = "windows")]
31use crate::windows::WebViewInner;
32
33#[cfg(not(any(
34    target_os = "android",
35    target_os = "ios",
36    target_os = "macos",
37    target_os = "windows",
38    all(target_os = "linux", target_env = "ohos")
39)))]
40pub(crate) struct WebViewInner {
41    webtag: WebTag,
42}
43
44use crate::traits::{
45    AsyncSchemeHandler, ClickOptions, DownloadHandler, DownloadRequest, FileChooserRequest,
46    FileChooserResponse, FillOptions, NavigationHandler, NavigationPolicy, NavigationRequest,
47    NewWindowHandler, NewWindowPolicy, PressOptions, SchemeOutcome, ScrollOptions, TypeOptions,
48    UserAgentOverride, WebViewInputController,
49};
50use crate::{
51    ClearSiteDataOptions, ClearSiteDataResult, LoadDataRequest, NetworkCaptureSnapshot,
52    WebResourceResponse, WebViewController, WebViewCookie, WebViewCookieSetRequest,
53    WebViewDelegate, WebViewError, WebViewInputError, WebViewScriptError,
54};
55use async_trait::async_trait;
56
57const APPLE_INTERNAL_SCHEME: &str = "lx-apple";
58
59#[cfg(not(any(
60    target_os = "android",
61    target_os = "ios",
62    target_os = "macos",
63    target_os = "windows",
64    all(target_os = "linux", target_env = "ohos")
65)))]
66fn unsupported_webview_error(action: &str) -> WebViewError {
67    WebViewError::Unsupported(action.to_string())
68}
69
70#[cfg(not(any(
71    target_os = "android",
72    target_os = "ios",
73    target_os = "macos",
74    target_os = "windows",
75    all(target_os = "linux", target_env = "ohos")
76)))]
77impl WebViewInner {
78    pub(crate) fn create(
79        appid: &str,
80        path: &str,
81        session_id: Option<u64>,
82        _effective_options: EffectiveWebViewCreateOptions,
83        sender: WebViewCreateSender,
84    ) {
85        let _webtag = WebTag::new(appid, path, session_id);
86        sender.fail(
87            WebViewCreateStage::Requested,
88            unsupported_webview_error("webview creation"),
89        );
90    }
91}
92
93#[cfg(not(any(
94    target_os = "android",
95    target_os = "ios",
96    target_os = "macos",
97    target_os = "windows",
98    all(target_os = "linux", target_env = "ohos")
99)))]
100#[async_trait]
101impl WebViewController for WebViewInner {
102    fn load_url(&self, _url: &str) -> Result<(), WebViewError> {
103        Err(unsupported_webview_error("load_url"))
104    }
105
106    fn load_data(&self, _request: LoadDataRequest<'_>) -> Result<(), WebViewError> {
107        Err(unsupported_webview_error("load_data"))
108    }
109
110    fn exec_js(&self, _js: &str) -> Result<(), WebViewError> {
111        Err(unsupported_webview_error("exec_js"))
112    }
113
114    async fn eval_js(&self, _js: &str) -> Result<serde_json::Value, WebViewScriptError> {
115        Err(WebViewScriptError::Unsupported(
116            "JavaScript evaluation is not supported on this platform",
117        ))
118    }
119
120    fn post_message(&self, _message: &str) -> Result<(), WebViewError> {
121        Err(unsupported_webview_error("post_message"))
122    }
123
124    fn clear_browsing_data(&self) -> Result<(), WebViewError> {
125        Err(unsupported_webview_error("clear_browsing_data"))
126    }
127
128    fn set_user_agent_override(&self, _user_agent: UserAgentOverride) -> Result<(), WebViewError> {
129        Err(unsupported_webview_error("set_user_agent_override"))
130    }
131}
132
133fn lock_or_recover<'a, T>(mutex: &'a Mutex<T>, name: &str) -> std::sync::MutexGuard<'a, T> {
134    match mutex.lock() {
135        Ok(guard) => guard,
136        Err(poisoned) => {
137            log::error!("Mutex poisoned at {}, recovering inner value", name);
138            poisoned.into_inner()
139        }
140    }
141}
142
143fn scheme_waker_from_sender(sender: SyncSender<()>) -> Waker {
144    // SAFETY: RawWaker functions maintain Arc refcounts correctly.
145    unsafe { Waker::from_raw(scheme_raw_waker(Arc::new(sender))) }
146}
147
148fn scheme_raw_waker(sender: Arc<SyncSender<()>>) -> RawWaker {
149    RawWaker::new(Arc::into_raw(sender) as *const (), &SCHEME_WAKER_VTABLE)
150}
151
152unsafe fn scheme_waker_clone(data: *const ()) -> RawWaker {
153    // SAFETY: data is created from Arc<SyncSender<()>> in scheme_raw_waker.
154    let arc = unsafe { Arc::<SyncSender<()>>::from_raw(data as *const SyncSender<()>) };
155    let cloned = Arc::clone(&arc);
156    let _ = Arc::into_raw(arc);
157    scheme_raw_waker(cloned)
158}
159
160unsafe fn scheme_waker_wake(data: *const ()) {
161    // SAFETY: data is created from Arc<SyncSender<()>> in scheme_raw_waker.
162    let arc = unsafe { Arc::<SyncSender<()>>::from_raw(data as *const SyncSender<()>) };
163    let _ = arc.try_send(());
164}
165
166unsafe fn scheme_waker_wake_by_ref(data: *const ()) {
167    // SAFETY: data is created from Arc<SyncSender<()>> in scheme_raw_waker.
168    let arc = unsafe { Arc::<SyncSender<()>>::from_raw(data as *const SyncSender<()>) };
169    let _ = arc.try_send(());
170    let _ = Arc::into_raw(arc);
171}
172
173unsafe fn scheme_waker_drop(data: *const ()) {
174    // SAFETY: data is created from Arc<SyncSender<()>> in scheme_raw_waker.
175    let _ = unsafe { Arc::<SyncSender<()>>::from_raw(data as *const SyncSender<()>) };
176}
177
178static SCHEME_WAKER_VTABLE: RawWakerVTable = RawWakerVTable::new(
179    scheme_waker_clone,
180    scheme_waker_wake,
181    scheme_waker_wake_by_ref,
182    scheme_waker_drop,
183);
184
185fn block_on_scheme_future<F>(future: F) -> F::Output
186where
187    F: Future,
188{
189    let (tx, rx) = sync_channel::<()>(1);
190    let waker = scheme_waker_from_sender(tx);
191    let mut context = Context::from_waker(&waker);
192    let mut future = Box::pin(future);
193
194    loop {
195        match Pin::as_mut(&mut future).poll(&mut context) {
196            Poll::Ready(value) => return value,
197            Poll::Pending => {
198                if rx.recv().is_err() {
199                    std::thread::yield_now();
200                }
201            }
202        }
203    }
204}
205
206/// Security profile for WebView creation.
207#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
208#[serde(rename_all = "snake_case")]
209pub(crate) enum SecurityProfile {
210    StrictDefault,
211    BrowserRelaxed,
212}
213
214/// Website-data lifetime for a WebView.
215///
216/// This is independent of the security profile: a browser-profile WebView can
217/// use an ephemeral data store without giving up browser navigation features.
218#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
219#[serde(rename_all = "snake_case")]
220pub enum WebViewDataMode {
221    /// Keep the platform behavior associated with the selected security
222    /// profile. Browser-profile WebViews use the shared persistent store.
223    #[default]
224    ProfileDefault,
225    /// Isolate cookies and site storage from persistent/shared browser data
226    /// and discard them when the WebView is destroyed.
227    Ephemeral,
228}
229
230pub(crate) type FileChooserFuture =
231    Pin<Box<dyn Future<Output = FileChooserResponse> + Send + 'static>>;
232pub(crate) type FileChooserHandler =
233    Box<dyn Fn(FileChooserRequest) -> FileChooserFuture + Send + Sync>;
234
235/// Internal WebView creation options.
236pub(crate) struct WebViewCreateOptions {
237    pub(crate) profile: SecurityProfile,
238    pub(crate) data_mode: WebViewDataMode,
239    pub(crate) scheme_handlers: HashMap<String, AsyncSchemeHandler>,
240    pub(crate) navigation_handler: Option<NavigationHandler>,
241    pub(crate) new_window_handler: Option<NewWindowHandler>,
242    pub(crate) download_handler: Option<DownloadHandler>,
243    pub(crate) file_chooser_handler: Option<FileChooserHandler>,
244    pub(crate) delegate: Option<Arc<dyn WebViewDelegate>>,
245    /// The webview belongs to a surface, not the app's page container; the
246    /// platform shell must not adopt it into stack-page presentation.
247    pub(crate) surface_owned: bool,
248}
249
250impl std::fmt::Debug for WebViewCreateOptions {
251    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
252        f.debug_struct("WebViewCreateOptions")
253            .field("profile", &self.profile)
254            .field("data_mode", &self.data_mode)
255            .field(
256                "scheme_handlers",
257                &self.scheme_handlers.keys().collect::<Vec<_>>(),
258            )
259            .field("has_navigation_handler", &self.navigation_handler.is_some())
260            .field("has_new_window_handler", &self.new_window_handler.is_some())
261            .field("has_download_handler", &self.download_handler.is_some())
262            .field(
263                "has_file_chooser_handler",
264                &self.file_chooser_handler.is_some(),
265            )
266            .field("has_delegate", &self.delegate.is_some())
267            .finish()
268    }
269}
270
271/// Global HTTP proxy configuration shared by all WebViews in the process.
272#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
273pub struct ProxyConfig {
274    pub host: String,
275    pub port: u16,
276    #[serde(default)]
277    pub bypass: Vec<String>,
278}
279
280impl ProxyConfig {
281    pub fn new(host: impl Into<String>, port: u16) -> Result<Self, WebViewError> {
282        let cfg = Self {
283            host: host.into(),
284            port,
285            bypass: Vec::new(),
286        };
287        cfg.validate()
288    }
289
290    pub fn with_bypass<I, S>(mut self, bypass: I) -> Self
291    where
292        I: IntoIterator<Item = S>,
293        S: Into<String>,
294    {
295        self.bypass = bypass.into_iter().map(Into::into).collect();
296        self
297    }
298
299    fn validate(self) -> Result<Self, WebViewError> {
300        let host = self.host.trim().to_string();
301        if host.is_empty() {
302            return Err(WebViewError::InvalidCreateOptions(
303                "proxy host cannot be empty".to_string(),
304            ));
305        }
306        if host.contains(char::is_whitespace) {
307            return Err(WebViewError::InvalidCreateOptions(
308                "proxy host cannot contain whitespace".to_string(),
309            ));
310        }
311        if self.port == 0 {
312            return Err(WebViewError::InvalidCreateOptions(
313                "proxy port must be greater than 0".to_string(),
314            ));
315        }
316
317        let mut seen = HashSet::new();
318        let mut bypass = Vec::new();
319        for raw in self.bypass {
320            let rule = raw.trim();
321            if rule.is_empty() {
322                continue;
323            }
324            let key = rule.to_ascii_lowercase();
325            if seen.insert(key) {
326                bypass.push(rule.to_string());
327            }
328        }
329
330        Ok(Self {
331            host,
332            bypass,
333            ..self
334        })
335    }
336}
337
338#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
339#[serde(rename_all = "snake_case")]
340pub enum ProxyApplyStatus {
341    Applied,
342    Cleared,
343    Unsupported,
344}
345
346#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
347#[serde(rename_all = "snake_case")]
348pub enum ProxyActivation {
349    EffectiveNow,
350    NewWebViewsOnly,
351    EngineRecreateRequired,
352    NotApplied,
353}
354
355#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
356pub struct ProxyApplyReport {
357    pub status: ProxyApplyStatus,
358    pub activation: ProxyActivation,
359    #[serde(default, skip_serializing_if = "Option::is_none")]
360    pub detail: Option<String>,
361}
362
363impl ProxyApplyReport {
364    pub fn applied(activation: ProxyActivation) -> Self {
365        Self {
366            status: ProxyApplyStatus::Applied,
367            activation,
368            detail: None,
369        }
370    }
371
372    pub fn cleared(activation: ProxyActivation) -> Self {
373        Self {
374            status: ProxyApplyStatus::Cleared,
375            activation,
376            detail: None,
377        }
378    }
379
380    pub fn unsupported(detail: impl Into<String>) -> Self {
381        Self {
382            status: ProxyApplyStatus::Unsupported,
383            activation: ProxyActivation::NotApplied,
384            detail: Some(detail.into()),
385        }
386    }
387}
388
389/// Effective, normalized options actually applied to a concrete WebView instance.
390#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
391pub(crate) struct EffectiveWebViewCreateOptions {
392    pub(crate) profile: SecurityProfile,
393    /// Website-data lifetime, independent of the security profile.
394    #[serde(default)]
395    pub(crate) data_mode: WebViewDataMode,
396    /// Scheme names registered via `on_scheme` (serializable).
397    #[serde(default)]
398    pub(crate) registered_schemes: Vec<String>,
399    #[serde(default)]
400    pub(crate) has_navigation_handler: bool,
401    #[serde(default)]
402    pub(crate) has_new_window_handler: bool,
403    #[serde(default)]
404    pub(crate) has_download_handler: bool,
405    #[serde(default)]
406    pub(crate) has_file_chooser_handler: bool,
407    #[serde(default)]
408    pub(crate) has_delegate: bool,
409    #[serde(default)]
410    pub(crate) surface_owned: bool,
411}
412
413impl Default for WebViewCreateOptions {
414    fn default() -> Self {
415        Self::strict()
416    }
417}
418
419impl WebViewCreateOptions {
420    fn strict() -> Self {
421        Self {
422            profile: SecurityProfile::StrictDefault,
423            data_mode: WebViewDataMode::ProfileDefault,
424            scheme_handlers: HashMap::new(),
425            navigation_handler: None,
426            new_window_handler: None,
427            download_handler: None,
428            file_chooser_handler: None,
429            delegate: None,
430            surface_owned: false,
431        }
432    }
433
434    fn browser() -> Self {
435        Self {
436            profile: SecurityProfile::BrowserRelaxed,
437            data_mode: WebViewDataMode::ProfileDefault,
438            scheme_handlers: HashMap::new(),
439            navigation_handler: None,
440            new_window_handler: None,
441            download_handler: None,
442            file_chooser_handler: None,
443            delegate: None,
444            surface_owned: false,
445        }
446    }
447
448    /// Register a scheme handler for a custom URL scheme.
449    ///
450    /// The handler is async by design.
451    ///
452    /// Usage:
453    /// - Async workload:
454    ///   `options.on_scheme("lx", |req| async move { ... })`
455    /// - Immediate response:
456    ///   `options.on_scheme("lx", |req| async move { immediate(req).into() })`
457    fn on_scheme<F, Fut>(mut self, scheme: &str, handler: F) -> Self
458    where
459        F: Fn(http::Request<Vec<u8>>) -> Fut + Send + Sync + 'static,
460        Fut: std::future::Future<Output = SchemeOutcome> + Send + 'static,
461    {
462        let normalized = scheme.trim().to_ascii_lowercase();
463        if !normalized.is_empty() {
464            self.scheme_handlers.insert(
465                normalized,
466                Arc::new(move |req| {
467                    let fut = handler(req);
468                    Box::pin(fut)
469                }),
470            );
471        }
472        self
473    }
474
475    /// Register a navigation handler that decides whether to allow or cancel navigations.
476    /// The handler receives the URL and available platform navigation metadata.
477    fn on_navigation<F>(mut self, handler: F) -> Self
478    where
479        F: Fn(&NavigationRequest) -> NavigationPolicy + Send + Sync + 'static,
480    {
481        self.navigation_handler = Some(Box::new(handler));
482        self
483    }
484
485    /// Register a new-window handler for `target="_blank"` / `window.open()`.
486    /// The handler receives the URL and returns a `NewWindowPolicy`.
487    fn on_new_window<F>(mut self, handler: F) -> Self
488    where
489        F: Fn(&str) -> NewWindowPolicy + Send + Sync + 'static,
490    {
491        self.new_window_handler = Some(Box::new(handler));
492        self
493    }
494
495    /// Register a download handler for browser-mode downloads.
496    ///
497    /// The handler runs synchronously on the platform callback thread. Keep it fast and
498    /// spawn background work onto your runtime inside the closure.
499    ///
500    /// This callback is only valid for browser profile.
501    /// Public API: `WebViewBuilder::browser(webtag).on_download(...).create()`.
502    /// In this mode, download requests are routed to the callback path instead of in-WebView
503    /// download UI.
504    fn on_download<F>(mut self, handler: F) -> Self
505    where
506        F: Fn(DownloadRequest) + Send + Sync + 'static,
507    {
508        self.download_handler = Some(Box::new(handler));
509        self
510    }
511
512    fn on_file_chooser<F, Fut>(mut self, handler: F) -> Self
513    where
514        F: Fn(FileChooserRequest) -> Fut + Send + Sync + 'static,
515        Fut: Future<Output = FileChooserResponse> + Send + 'static,
516    {
517        self.file_chooser_handler = Some(Box::new(move |request| Box::pin(handler(request))));
518        self
519    }
520
521    fn delegate(mut self, delegate: Arc<dyn WebViewDelegate>) -> Self {
522        self.delegate = Some(delegate);
523        self
524    }
525
526    fn data_mode(mut self, data_mode: WebViewDataMode) -> Self {
527        self.data_mode = data_mode;
528        self
529    }
530
531    fn surface_owned(mut self, surface_owned: bool) -> Self {
532        self.surface_owned = surface_owned;
533        self
534    }
535
536    pub(crate) fn normalize(
537        self,
538    ) -> Result<(EffectiveWebViewCreateOptions, PendingCallbacks), WebViewError> {
539        if self.profile != SecurityProfile::BrowserRelaxed && self.download_handler.is_some() {
540            return Err(WebViewError::InvalidCreateOptions(
541                "download callback is only supported in browser profile; use WebViewBuilder::browser(webtag).on_download(...).create()".to_string(),
542            ));
543        }
544        if self.scheme_handlers.contains_key(APPLE_INTERNAL_SCHEME) {
545            return Err(WebViewError::InvalidCreateOptions(format!(
546                "scheme '{APPLE_INTERNAL_SCHEME}' is reserved for LingXia Apple bridge transport"
547            )));
548        }
549        let mut registered_schemes: Vec<String> = self.scheme_handlers.keys().cloned().collect();
550        registered_schemes.sort_unstable();
551        registered_schemes.dedup();
552        let effective = EffectiveWebViewCreateOptions {
553            profile: self.profile,
554            data_mode: self.data_mode,
555            registered_schemes,
556            has_navigation_handler: self.navigation_handler.is_some(),
557            has_new_window_handler: self.new_window_handler.is_some(),
558            has_download_handler: self.download_handler.is_some(),
559            has_file_chooser_handler: self.file_chooser_handler.is_some(),
560            has_delegate: self.delegate.is_some(),
561            surface_owned: self.surface_owned,
562        };
563        let pending = PendingCallbacks {
564            scheme_handlers: self.scheme_handlers,
565            navigation_handler: self.navigation_handler,
566            new_window_handler: self.new_window_handler,
567            download_handler: self.download_handler,
568            file_chooser_handler: self.file_chooser_handler,
569            delegate: self.delegate,
570        };
571        Ok((effective, pending))
572    }
573}
574
575/// Entry point for mode-specific WebView creation.
576///
577/// Typical usage:
578/// - Strict lxapp page:
579///   `WebViewBuilder::strict(tag).on_scheme(...).on_navigation(...).create()`
580/// - Browser page:
581///   `WebViewBuilder::browser(tag).on_new_window(...).on_download(...).create()`
582pub struct WebViewBuilder;
583
584#[must_use = "call .create() to start WebView creation"]
585pub struct StrictWebViewBuilder {
586    webtag: WebTag,
587    options: WebViewCreateOptions,
588}
589
590#[must_use = "call .create() to start WebView creation"]
591pub struct BrowserWebViewBuilder {
592    webtag: WebTag,
593    options: WebViewCreateOptions,
594}
595
596impl WebViewBuilder {
597    /// Start a strict-profile WebView builder.
598    #[must_use = "call .create() to start WebView creation"]
599    pub fn strict(webtag: WebTag) -> StrictWebViewBuilder {
600        StrictWebViewBuilder {
601            webtag,
602            options: WebViewCreateOptions::strict(),
603        }
604    }
605
606    /// Start a browser-profile WebView builder.
607    #[must_use = "call .create() to start WebView creation"]
608    pub fn browser(webtag: WebTag) -> BrowserWebViewBuilder {
609        BrowserWebViewBuilder {
610            webtag,
611            options: WebViewCreateOptions::browser(),
612        }
613    }
614}
615
616impl StrictWebViewBuilder {
617    /// Bind a `WebViewDelegate` during creation.
618    ///
619    /// This is the only supported way to configure delegate callbacks.
620    pub fn delegate(mut self, delegate: Arc<dyn WebViewDelegate>) -> Self {
621        self.options = self.options.delegate(delegate);
622        self
623    }
624
625    /// Select the website-data lifetime independently of the security profile.
626    pub fn data_mode(mut self, data_mode: WebViewDataMode) -> Self {
627        self.options = self.options.data_mode(data_mode);
628        self
629    }
630
631    /// Mark the webview as surface-owned so platform shells leave its
632    /// presentation to the surface instead of the page container.
633    pub fn surface_owned(mut self, surface_owned: bool) -> Self {
634        self.options = self.options.surface_owned(surface_owned);
635        self
636    }
637
638    pub fn on_scheme<F, Fut>(mut self, scheme: &str, handler: F) -> Self
639    where
640        F: Fn(http::Request<Vec<u8>>) -> Fut + Send + Sync + 'static,
641        Fut: std::future::Future<Output = SchemeOutcome> + Send + 'static,
642    {
643        self.options = self.options.on_scheme(scheme, handler);
644        self
645    }
646
647    pub fn on_navigation<F>(mut self, handler: F) -> Self
648    where
649        F: Fn(&NavigationRequest) -> NavigationPolicy + Send + Sync + 'static,
650    {
651        self.options = self.options.on_navigation(handler);
652        self
653    }
654
655    pub fn on_new_window<F>(mut self, handler: F) -> Self
656    where
657        F: Fn(&str) -> NewWindowPolicy + Send + Sync + 'static,
658    {
659        self.options = self.options.on_new_window(handler);
660        self
661    }
662
663    pub fn on_file_chooser<F, Fut>(mut self, handler: F) -> Self
664    where
665        F: Fn(FileChooserRequest) -> Fut + Send + Sync + 'static,
666        Fut: Future<Output = FileChooserResponse> + Send + 'static,
667    {
668        self.options = self.options.on_file_chooser(handler);
669        self
670    }
671
672    /// Create a strict-profile WebView session.
673    ///
674    /// Re-creating with the same `webtag` follows strict rules:
675    /// - Different options => creation fails.
676    /// - Same options but new callback registrations => creation fails.
677    /// - Same options and no callbacks => existing instance is reused.
678    pub fn create(self) -> WebViewSession {
679        create_webview_session(self.webtag, self.options)
680    }
681}
682
683impl BrowserWebViewBuilder {
684    /// Bind a `WebViewDelegate` during creation.
685    ///
686    /// This is the only supported way to configure delegate callbacks.
687    pub fn delegate(mut self, delegate: Arc<dyn WebViewDelegate>) -> Self {
688        self.options = self.options.delegate(delegate);
689        self
690    }
691
692    pub fn on_scheme<F, Fut>(mut self, scheme: &str, handler: F) -> Self
693    where
694        F: Fn(http::Request<Vec<u8>>) -> Fut + Send + Sync + 'static,
695        Fut: std::future::Future<Output = SchemeOutcome> + Send + 'static,
696    {
697        self.options = self.options.on_scheme(scheme, handler);
698        self
699    }
700
701    pub fn on_navigation<F>(mut self, handler: F) -> Self
702    where
703        F: Fn(&NavigationRequest) -> NavigationPolicy + Send + Sync + 'static,
704    {
705        self.options = self.options.on_navigation(handler);
706        self
707    }
708
709    pub fn on_new_window<F>(mut self, handler: F) -> Self
710    where
711        F: Fn(&str) -> NewWindowPolicy + Send + Sync + 'static,
712    {
713        self.options = self.options.on_new_window(handler);
714        self
715    }
716
717    /// Register a download callback (browser profile only).
718    ///
719    /// The callback runs on the platform callback thread; keep it fast and offload
720    /// expensive work to your app runtime.
721    pub fn on_download<F>(mut self, handler: F) -> Self
722    where
723        F: Fn(DownloadRequest) + Send + Sync + 'static,
724    {
725        self.options = self.options.on_download(handler);
726        self
727    }
728
729    /// Select the website-data lifetime independently of the security profile.
730    pub fn data_mode(mut self, data_mode: WebViewDataMode) -> Self {
731        self.options = self.options.data_mode(data_mode);
732        self
733    }
734
735    pub fn on_file_chooser<F, Fut>(mut self, handler: F) -> Self
736    where
737        F: Fn(FileChooserRequest) -> Fut + Send + Sync + 'static,
738        Fut: Future<Output = FileChooserResponse> + Send + 'static,
739    {
740        self.options = self.options.on_file_chooser(handler);
741        self
742    }
743
744    /// Create a browser-profile WebView session.
745    ///
746    /// Re-creating with the same `webtag` follows strict rules:
747    /// - Different options => creation fails.
748    /// - Same options but new callback registrations => creation fails.
749    /// - Same options and no callbacks => existing instance is reused.
750    pub fn create(self) -> WebViewSession {
751        create_webview_session(self.webtag, self.options)
752    }
753}
754
755/// Pending callbacks extracted from internal option normalization.
756/// Stored between session creation and `register_webview` installation.
757pub(crate) struct PendingCallbacks {
758    pub(crate) scheme_handlers: HashMap<String, AsyncSchemeHandler>,
759    pub(crate) navigation_handler: Option<NavigationHandler>,
760    pub(crate) new_window_handler: Option<NewWindowHandler>,
761    pub(crate) download_handler: Option<DownloadHandler>,
762    pub(crate) file_chooser_handler: Option<FileChooserHandler>,
763    pub(crate) delegate: Option<Arc<dyn WebViewDelegate>>,
764}
765
766impl PendingCallbacks {
767    fn has_any(&self) -> bool {
768        !self.scheme_handlers.is_empty()
769            || self.navigation_handler.is_some()
770            || self.new_window_handler.is_some()
771            || self.download_handler.is_some()
772            || self.file_chooser_handler.is_some()
773            || self.delegate.is_some()
774    }
775}
776
777/// WebView type that includes inner implementation and delegate
778pub struct WebView {
779    pub(crate) inner: WebViewInner,
780    effective_options: EffectiveWebViewCreateOptions,
781    // Hold a strong reference to the delegate; runtime destroy clears it to break cycles.
782    delegate: RwLock<Option<Arc<dyn WebViewDelegate>>>,
783    // Closure-based scheme handlers registered via builders.
784    scheme_handlers: RwLock<HashMap<String, AsyncSchemeHandler>>,
785    navigation_handler: RwLock<Option<NavigationHandler>>,
786    new_window_handler: RwLock<Option<NewWindowHandler>>,
787    download_handler: RwLock<Option<DownloadHandler>>,
788    file_chooser_handler: RwLock<Option<FileChooserHandler>>,
789}
790
791impl WebView {
792    pub(crate) fn new(
793        inner: WebViewInner,
794        effective_options: EffectiveWebViewCreateOptions,
795    ) -> Self {
796        Self {
797            inner,
798            effective_options,
799            delegate: RwLock::new(None),
800            scheme_handlers: RwLock::new(HashMap::new()),
801            navigation_handler: RwLock::new(None),
802            new_window_handler: RwLock::new(None),
803            download_handler: RwLock::new(None),
804            file_chooser_handler: RwLock::new(None),
805        }
806    }
807
808    /// Get the appid
809    pub fn appid(&self) -> String {
810        self.inner.webtag.extract_appid()
811    }
812
813    /// Get the path
814    pub fn path(&self) -> String {
815        self.inner.webtag.extract_parts().1
816    }
817
818    /// Get the webtag (computed from appid and path)
819    pub fn webtag(&self) -> WebTag {
820        self.inner.webtag.clone()
821    }
822
823    pub(crate) fn effective_options(&self) -> &EffectiveWebViewCreateOptions {
824        &self.effective_options
825    }
826
827    /// Get delegate for this WebView
828    pub(crate) fn get_delegate(&self) -> Option<Arc<dyn WebViewDelegate>> {
829        self.delegate.read().ok().and_then(|guard| guard.clone())
830    }
831
832    /// Remove delegate for this WebView
833    pub(crate) fn remove_delegate(&self) {
834        if let Ok(mut guard) = self.delegate.write() {
835            *guard = None;
836        }
837    }
838
839    /// Install all pending callbacks into this WebView (called once during creation).
840    pub(crate) fn install_callbacks(&self, callbacks: PendingCallbacks) {
841        if let Some(delegate) = callbacks.delegate
842            && let Ok(mut guard) = self.delegate.write()
843        {
844            *guard = Some(delegate);
845        }
846        if let Ok(mut guard) = self.scheme_handlers.write() {
847            *guard = callbacks.scheme_handlers;
848        }
849        if let Some(handler) = callbacks.navigation_handler
850            && let Ok(mut guard) = self.navigation_handler.write()
851        {
852            *guard = Some(handler);
853        }
854        if let Some(handler) = callbacks.new_window_handler
855            && let Ok(mut guard) = self.new_window_handler.write()
856        {
857            *guard = Some(handler);
858        }
859        if let Some(handler) = callbacks.download_handler
860            && let Ok(mut guard) = self.download_handler.write()
861        {
862            *guard = Some(handler);
863        }
864        if let Some(handler) = callbacks.file_chooser_handler
865            && let Ok(mut guard) = self.file_chooser_handler.write()
866        {
867            *guard = Some(handler);
868        }
869    }
870
871    /// Check if a scheme handler is registered for the given scheme.
872    pub fn has_scheme_handler(&self, scheme: &str) -> bool {
873        self.scheme_handlers
874            .read()
875            .ok()
876            .is_some_and(|guard| guard.contains_key(scheme))
877    }
878
879    /// Synchronously invoke the registered scheme handler for `scheme`.
880    /// Returns `None` if no handler is registered or the handler declines.
881    pub(crate) fn handle_scheme_request(
882        &self,
883        scheme: &str,
884        request: http::Request<Vec<u8>>,
885    ) -> Option<WebResourceResponse> {
886        #[cfg(any(target_os = "ios", target_os = "macos"))]
887        if let Some(response) = self.inner.handle_internal_bridge_request(&request) {
888            return Some(response);
889        }
890
891        let guard = self.scheme_handlers.read().ok()?;
892        let handler = guard.get(scheme)?;
893        let outcome = block_on_scheme_future(handler(request));
894        match outcome {
895            SchemeOutcome::Handled(response) => Some(response),
896            SchemeOutcome::PassThrough => None,
897        }
898    }
899
900    /// Call the navigation handler. Returns `Allow` if no handler is registered.
901    ///
902    /// A URL matching an open [`crate::url_callback`] channel is delivered to
903    /// that channel and cancelled before any per-webview handler runs.
904    pub fn handle_navigation(&self, request: &NavigationRequest) -> NavigationPolicy {
905        if crate::url_callback::dispatch(&request.url) {
906            return NavigationPolicy::Cancel;
907        }
908        if let Ok(guard) = self.navigation_handler.read()
909            && let Some(handler) = guard.as_ref()
910        {
911            return handler(request);
912        }
913        NavigationPolicy::Allow
914    }
915
916    /// Check if a new-window handler is registered.
917    pub fn has_new_window_handler(&self) -> bool {
918        self.new_window_handler
919            .read()
920            .ok()
921            .is_some_and(|guard| guard.is_some())
922    }
923
924    /// Call the new-window handler. Returns `Cancel` if no handler is registered.
925    ///
926    /// A URL matching an open [`crate::url_callback`] channel is delivered to
927    /// that channel and cancelled before any per-webview handler runs.
928    pub fn handle_new_window(&self, url: &str) -> NewWindowPolicy {
929        if crate::url_callback::dispatch(url) {
930            return NewWindowPolicy::Cancel;
931        }
932        if let Ok(guard) = self.new_window_handler.read()
933            && let Some(handler) = guard.as_ref()
934        {
935            return handler(url);
936        }
937        NewWindowPolicy::Cancel
938    }
939
940    /// Dispatch a download request to the registered handler.
941    pub(crate) fn handle_download(&self, request: DownloadRequest) {
942        if let Ok(guard) = self.download_handler.read()
943            && let Some(handler) = guard.as_ref()
944        {
945            handler(request);
946        }
947    }
948
949    // Consulted only by the Windows download-event path.
950    #[cfg_attr(not(target_os = "windows"), allow(dead_code))]
951    pub(crate) fn has_download_handler(&self) -> bool {
952        self.download_handler
953            .read()
954            .ok()
955            .is_some_and(|guard| guard.is_some())
956    }
957
958    #[cfg_attr(target_os = "windows", allow(dead_code))]
959    pub(crate) fn handle_file_chooser<C>(&self, request: FileChooserRequest, completion: C) -> bool
960    where
961        C: FnOnce(FileChooserResponse) + Send + 'static,
962    {
963        let Some(future) = self.make_file_chooser_future(request) else {
964            return false;
965        };
966        std::thread::spawn(move || {
967            completion(block_on_scheme_future(future));
968        });
969        true
970    }
971
972    #[cfg_attr(target_os = "windows", allow(dead_code))]
973    fn make_file_chooser_future(&self, request: FileChooserRequest) -> Option<FileChooserFuture> {
974        let Ok(guard) = self.file_chooser_handler.read() else {
975            return None;
976        };
977        let handler = guard.as_ref()?;
978        Some(handler(request))
979    }
980
981    /// Toggle docked DevTools (macOS only, uses private _inspector API)
982    #[cfg(target_os = "macos")]
983    pub fn toggle_devtools(&self) {
984        self.inner.toggle_devtools();
985    }
986
987    /// Toggle detached DevTools (macOS only, uses private _inspector API)
988    #[cfg(target_os = "macos")]
989    pub fn toggle_devtools_detached(&self) {
990        self.inner.toggle_devtools_detached();
991    }
992
993    /// Get platform-specific pointer for interop (Apple platforms only)
994    #[cfg(any(target_os = "ios", target_os = "macos"))]
995    pub fn get_swift_webview_ptr(&self) -> usize {
996        self.inner.get_swift_webview_ptr()
997    }
998
999    /// Get Java WebView reference (Android only)
1000    #[cfg(target_os = "android")]
1001    pub fn get_java_webview(&self) -> &jni::objects::Global<jni::objects::JObject<'static>> {
1002        self.inner.get_java_webview()
1003    }
1004
1005    pub async fn evaluate_javascript(
1006        &self,
1007        js: &str,
1008    ) -> Result<serde_json::Value, crate::WebViewScriptError> {
1009        self.inner.eval_js(js).await
1010    }
1011
1012    /// Synthetic-event click for platforms that don't expose a native touch
1013    /// injection API (iOS WKWebView, ArkWeb on Harmony). Looks up the
1014    /// selector, scrolls it into view, and dispatches a synthetic
1015    /// `MouseEvent` (or sets `focus="true"` for `<lx-*>` custom elements
1016    /// that proxy focus to a native overlay).
1017    /// Run a page-input action script and decode its `{ok, error, interactable}`
1018    /// result.
1019    #[cfg(any(
1020        target_os = "ios",
1021        target_os = "android",
1022        all(feature = "webview-input", target_os = "macos"),
1023        all(target_os = "linux", target_env = "ohos")
1024    ))]
1025    async fn run_js_action(&self, script: &str) -> Result<(), WebViewInputError> {
1026        let result = self
1027            .inner
1028            .eval_js(script)
1029            .await
1030            .map_err(WebViewInputError::Script)?;
1031        if result.get("ok").and_then(|v| v.as_bool()) == Some(true) {
1032            return Ok(());
1033        }
1034        let err_msg = result
1035            .get("error")
1036            .and_then(|v| v.as_str())
1037            .unwrap_or("input action failed")
1038            .to_string();
1039        if result.get("interactable").and_then(|v| v.as_bool()) == Some(false) {
1040            Err(WebViewInputError::ElementNotInteractable(err_msg))
1041        } else {
1042            Err(WebViewInputError::ElementNotFound(err_msg))
1043        }
1044    }
1045
1046    /// Click an element by synthesizing DOM events. The shared input mechanism
1047    /// for platforms/hosts where native event dispatch cannot reach the page:
1048    /// iOS (no `UITouch` synthesis), OpenHarmony, and macOS when the WebView is
1049    /// detached (AppUI renders pages off-surface). `lx-` custom elements proxy
1050    /// focus to their native overlay instead of receiving mouse events.
1051    #[cfg(any(
1052        target_os = "ios",
1053        all(feature = "webview-input", target_os = "macos"),
1054        all(target_os = "linux", target_env = "ohos")
1055    ))]
1056    pub(crate) async fn click_via_js(
1057        &self,
1058        selector: &str,
1059        index: Option<usize>,
1060    ) -> Result<(), WebViewInputError> {
1061        let selector_json = serde_json::to_string(selector)
1062            .map_err(|err| WebViewInputError::Platform(format!("Invalid selector: {err}")))?;
1063        let idx = index.unwrap_or(0);
1064        let script = format!(
1065            "((sel, i) => {{ \
1066              const els = document.querySelectorAll(sel); \
1067              if (!els.length || i < 0 || i >= els.length) return {{ ok:false, error:'no match', count:els.length }}; \
1068              const el = els[i]; \
1069              try {{ el.scrollIntoView({{block:'center', inline:'center'}}); }} catch(_e) {{}} \
1070              const rect = el.getBoundingClientRect(); \
1071              const style = window.getComputedStyle(el); \
1072              const disabled = !!el.disabled || el.getAttribute('aria-disabled') === 'true'; \
1073              const visible = rect.width > 0 && rect.height > 0 && rect.bottom > 0 && rect.right > 0 && \
1074                rect.top < window.innerHeight && rect.left < window.innerWidth && \
1075                style.visibility !== 'hidden' && style.display !== 'none' && Number(style.opacity || '1') !== 0; \
1076              if (!visible) return {{ ok:false, error:'not visible', interactable:false, count:els.length }}; \
1077              if (disabled) return {{ ok:false, error:'not enabled', interactable:false, count:els.length }}; \
1078              const tag = (el.tagName || '').toLowerCase(); \
1079              if (tag.indexOf('lx-') === 0) {{ \
1080                el.setAttribute('focus', 'true'); \
1081                if (typeof el.syncNativeProps === 'function') {{ try {{ el.syncNativeProps(); }} catch(_e) {{}} }} \
1082                return {{ ok:true, count:els.length, native:true }}; \
1083              }} \
1084              if (typeof el.focus === 'function') {{ try {{ el.focus({{preventScroll:true}}); }} catch(_e) {{ try {{ el.focus(); }} catch(__){{}} }} }} \
1085              const opts = {{ bubbles:true, cancelable:true, view:window, clientX: rect.left + rect.width/2, clientY: rect.top + rect.height/2 }}; \
1086              try {{ if (window.PointerEvent) el.dispatchEvent(new PointerEvent('pointerdown', Object.assign({{pointerId:1, isPrimary:true, pointerType:'mouse'}}, opts))); }} catch(_e) {{}} \
1087              try {{ el.dispatchEvent(new MouseEvent('mousedown', opts)); }} catch(_e) {{}} \
1088              try {{ if (window.PointerEvent) el.dispatchEvent(new PointerEvent('pointerup', Object.assign({{pointerId:1, isPrimary:true, pointerType:'mouse'}}, opts))); }} catch(_e) {{}} \
1089              try {{ el.dispatchEvent(new MouseEvent('mouseup', opts)); }} catch(_e) {{}} \
1090              try {{ el.dispatchEvent(new MouseEvent('click', opts)); }} catch(_e) {{}} \
1091              return {{ ok:true, count:els.length }}; \
1092            }})({selector_json}, {idx})"
1093        );
1094        self.run_js_action(&script).await
1095    }
1096
1097    /// Type text into an editable element by synthesizing DOM events. Goes
1098    /// through the native value setter so framework-tracked inputs (React) fire
1099    /// their `onChange`. `lx-` custom elements set their value + sync native.
1100    #[cfg(any(
1101        target_os = "ios",
1102        target_os = "android",
1103        all(feature = "webview-input", target_os = "macos"),
1104        all(target_os = "linux", target_env = "ohos")
1105    ))]
1106    pub(crate) async fn type_via_js(
1107        &self,
1108        selector: &str,
1109        index: Option<usize>,
1110        text: &str,
1111        replace: bool,
1112    ) -> Result<(), WebViewInputError> {
1113        let selector_json = serde_json::to_string(selector)
1114            .map_err(|err| WebViewInputError::Platform(format!("Invalid selector: {err}")))?;
1115        let text_json = serde_json::to_string(text)
1116            .map_err(|err| WebViewInputError::Platform(format!("Invalid text: {err}")))?;
1117        let idx = index.unwrap_or(0);
1118        let script = format!(
1119            "((sel, i, text, replace) => {{ \
1120              const els = document.querySelectorAll(sel); \
1121              if (!els.length || i < 0 || i >= els.length) return {{ ok:false, error:'no match', count:els.length }}; \
1122              const el = els[i]; \
1123              try {{ el.scrollIntoView({{block:'center', inline:'center'}}); }} catch(_e) {{}} \
1124              if (typeof el.focus === 'function') {{ try {{ el.focus({{preventScroll:true}}); }} catch(_e) {{ try {{ el.focus(); }} catch(__){{}} }} }} \
1125              const tag = (el.tagName || '').toLowerCase(); \
1126              if (tag === 'input' || tag === 'textarea') {{ \
1127                const proto = tag === 'textarea' ? window.HTMLTextAreaElement.prototype : window.HTMLInputElement.prototype; \
1128                const desc = Object.getOwnPropertyDescriptor(proto, 'value'); \
1129                const next = (replace ? '' : (el.value || '')) + text; \
1130                if (desc && desc.set) {{ desc.set.call(el, next); }} else {{ el.value = next; }} \
1131                el.dispatchEvent(new InputEvent('input', {{ bubbles:true, cancelable:true, data:text, inputType:'insertText' }})); \
1132                el.dispatchEvent(new Event('change', {{ bubbles:true }})); \
1133                return {{ ok:true, count:els.length }}; \
1134              }} \
1135              if (el.isContentEditable) {{ \
1136                el.textContent = (replace ? '' : (el.textContent || '')) + text; \
1137                el.dispatchEvent(new InputEvent('input', {{ bubbles:true, data:text, inputType:'insertText' }})); \
1138                return {{ ok:true, count:els.length }}; \
1139              }} \
1140              if (tag.indexOf('lx-') === 0) {{ \
1141                try {{ el.value = (replace ? '' : (el.value || '')) + text; }} catch(_e) {{}} \
1142                if (typeof el.syncNativeProps === 'function') {{ try {{ el.syncNativeProps(); }} catch(_e) {{}} }} \
1143                el.dispatchEvent(new Event('input', {{ bubbles:true }})); \
1144                return {{ ok:true, count:els.length, native:true }}; \
1145              }} \
1146              return {{ ok:false, error:'not editable', interactable:false, count:els.length }}; \
1147            }})({selector_json}, {idx}, {text_json}, {replace})"
1148        );
1149        self.run_js_action(&script).await
1150    }
1151
1152    /// Press a key by synthesizing keydown/keyup on the selected or focused element.
1153    #[cfg(any(
1154        target_os = "ios",
1155        target_os = "android",
1156        all(feature = "webview-input", target_os = "macos"),
1157        all(target_os = "linux", target_env = "ohos")
1158    ))]
1159    pub(crate) async fn press_via_js(
1160        &self,
1161        key: &str,
1162        selector: Option<&str>,
1163        index: Option<usize>,
1164    ) -> Result<(), WebViewInputError> {
1165        let key_json = serde_json::to_string(key)
1166            .map_err(|err| WebViewInputError::Platform(format!("Invalid key: {err}")))?;
1167        let selector_json = serde_json::to_string(&selector)
1168            .map_err(|err| WebViewInputError::Platform(format!("Invalid selector: {err}")))?;
1169        let idx = index.unwrap_or(0);
1170        let script = format!(
1171            "((key, sel, i) => {{ \
1172              const els = sel === null ? null : document.querySelectorAll(sel); \
1173              if (els && (!els.length || i < 0 || i >= els.length)) return {{ ok:false, error:'no match', count:els.length }}; \
1174              const el = els ? els[i] : (document.activeElement || document.body); \
1175              if (els) {{ \
1176                try {{ el.scrollIntoView({{block:'center', inline:'center'}}); }} catch(_e) {{}} \
1177                if (typeof el.focus === 'function') {{ try {{ el.focus({{preventScroll:true}}); }} catch(_e) {{ try {{ el.focus(); }} catch(__){{}} }} }} \
1178              }} \
1179              const map = {{ enter:'Enter', 'return':'Enter', tab:'Tab', esc:'Escape', escape:'Escape', backspace:'Backspace', 'delete':'Delete', forwarddelete:'Delete', space:' ', up:'ArrowUp', down:'ArrowDown', left:'ArrowLeft', right:'ArrowRight', arrowup:'ArrowUp', arrowdown:'ArrowDown', arrowleft:'ArrowLeft', arrowright:'ArrowRight', home:'Home', end:'End', pageup:'PageUp', pagedown:'PageDown' }}; \
1180              const norm = String(key).toLowerCase(); \
1181              const k = map[norm] || key; \
1182              const opts = {{ bubbles:true, cancelable:true, composed:true, key:k, view:window }}; \
1183              el.dispatchEvent(new KeyboardEvent('keydown', opts)); \
1184              el.dispatchEvent(new KeyboardEvent('keyup', opts)); \
1185              return {{ ok:true }}; \
1186            }})({key_json}, {selector_json}, {idx})"
1187        );
1188        self.run_js_action(&script).await
1189    }
1190
1191    /// Scroll by `(dx, dy)` in the DOM. Walks up from the element at the given
1192    /// viewport point (default: center) to the nearest scrollable ancestor, so
1193    /// it scrolls internal scroll containers, not just the document. When a
1194    /// webview reports `innerWidth/Height` as 0, the center point is unusable,
1195    /// so it falls back to the largest scrollable element, then the document
1196    /// scroller. Uses direct `scrollTop`/`scrollLeft` assignment, not
1197    /// `scrollBy`: on iOS WKWebView `scrollBy` animates sub-scrollers and
1198    /// overshoots to 2x the delta. NB: the built script must contain no `//`
1199    /// line comments — the `\`-continued format string collapses to one line.
1200    #[cfg(any(
1201        target_os = "ios",
1202        all(feature = "webview-input", target_os = "macos"),
1203        all(target_os = "linux", target_env = "ohos")
1204    ))]
1205    pub(crate) async fn scroll_via_js(
1206        &self,
1207        at: Option<(f64, f64)>,
1208        dx: f64,
1209        dy: f64,
1210    ) -> Result<(), WebViewInputError> {
1211        let (px, py) = at.unwrap_or((-1.0, -1.0));
1212        let script = format!(
1213            "((px, py, dx, dy) => {{ \
1214              const overflows = (v) => (/(auto|scroll|overlay)/).test(v); \
1215              const ancestor = (node) => {{ \
1216                while (node && node !== document.body && node !== document.documentElement) {{ \
1217                  const s = window.getComputedStyle(node); \
1218                  if ((overflows(s.overflowY) && node.scrollHeight > node.clientHeight) || \
1219                      (overflows(s.overflowX) && node.scrollWidth > node.clientWidth)) return node; \
1220                  node = node.parentElement; \
1221                }} \
1222                return null; \
1223              }}; \
1224              const largest = () => {{ \
1225                let best = null, range = 0; \
1226                const all = document.querySelectorAll('*'); \
1227                for (let k = 0; k < all.length; k++) {{ \
1228                  const n = all[k], s = window.getComputedStyle(n); \
1229                  const ry = overflows(s.overflowY) ? (n.scrollHeight - n.clientHeight) : 0; \
1230                  const rx = overflows(s.overflowX) ? (n.scrollWidth - n.clientWidth) : 0; \
1231                  const r = ry > rx ? ry : rx; \
1232                  if (r > range) {{ range = r; best = n; }} \
1233                }} \
1234                return best; \
1235              }}; \
1236              const vw = window.innerWidth || document.documentElement.clientWidth || 0; \
1237              const vh = window.innerHeight || document.documentElement.clientHeight || 0; \
1238              let target = null; \
1239              if (px >= 0 && py >= 0) target = ancestor(document.elementFromPoint(px, py) || document.body); \
1240              else if (vw > 0 && vh > 0) target = ancestor(document.elementFromPoint(vw >> 1, vh >> 1) || document.body); \
1241              if (!target) {{ \
1242                const se = document.scrollingElement || document.documentElement; \
1243                target = (se && se.scrollHeight > se.clientHeight) ? se : (largest() || se); \
1244              }} \
1245              target.scrollLeft += dx; target.scrollTop += dy; \
1246              return {{ ok:true }}; \
1247            }})({px}, {py}, {dx}, {dy})"
1248        );
1249        self.run_js_action(&script).await
1250    }
1251
1252    /// Scroll an element into view (`scrollIntoView`).
1253    #[cfg(any(
1254        target_os = "ios",
1255        all(feature = "webview-input", target_os = "macos"),
1256        all(target_os = "linux", target_env = "ohos")
1257    ))]
1258    pub(crate) async fn scroll_to_via_js(
1259        &self,
1260        selector: &str,
1261        index: Option<usize>,
1262    ) -> Result<(), WebViewInputError> {
1263        let selector_json = serde_json::to_string(selector)
1264            .map_err(|err| WebViewInputError::Platform(format!("Invalid selector: {err}")))?;
1265        let idx = index.unwrap_or(0);
1266        let script = format!(
1267            "((sel, i) => {{ \
1268              const els = document.querySelectorAll(sel); \
1269              if (!els.length || i < 0 || i >= els.length) return {{ ok:false, error:'no match', count:els.length }}; \
1270              try {{ els[i].scrollIntoView({{ block:'center', inline:'center' }}); }} catch(_e) {{ els[i].scrollIntoView(); }} \
1271              return {{ ok:true, count:els.length }}; \
1272            }})({selector_json}, {idx})"
1273        );
1274        self.run_js_action(&script).await
1275    }
1276
1277    pub async fn current_url(&self) -> Result<Option<String>, WebViewError> {
1278        self.inner.current_url().await
1279    }
1280
1281    pub fn reload(&self) -> Result<(), WebViewError> {
1282        self.inner.reload()
1283    }
1284
1285    pub fn go_back(&self) -> Result<(), WebViewError> {
1286        self.inner.go_back()
1287    }
1288
1289    pub fn go_forward(&self) -> Result<(), WebViewError> {
1290        self.inner.go_forward()
1291    }
1292
1293    pub async fn list_cookies(&self) -> Result<Vec<WebViewCookie>, WebViewError> {
1294        self.inner.list_cookies().await
1295    }
1296
1297    pub async fn set_cookie(&self, request: WebViewCookieSetRequest) -> Result<(), WebViewError> {
1298        self.inner.set_cookie(request).await
1299    }
1300
1301    pub async fn delete_cookie(
1302        &self,
1303        name: &str,
1304        domain: &str,
1305        path: &str,
1306    ) -> Result<(), WebViewError> {
1307        self.inner.delete_cookie(name, domain, path).await
1308    }
1309
1310    pub async fn clear_cookies(&self) -> Result<(), WebViewError> {
1311        self.inner.clear_cookies().await
1312    }
1313
1314    pub async fn start_network_capture(&self) -> Result<(), WebViewError> {
1315        self.inner.start_network_capture().await
1316    }
1317
1318    pub async fn stop_network_capture(&self) -> Result<(), WebViewError> {
1319        self.inner.stop_network_capture().await
1320    }
1321
1322    pub async fn network_entries(&self) -> Result<NetworkCaptureSnapshot, WebViewError> {
1323        self.inner.network_entries().await
1324    }
1325
1326    pub async fn clear_network_capture(&self) -> Result<(), WebViewError> {
1327        self.inner.clear_network_capture().await
1328    }
1329
1330    pub async fn take_screenshot(&self) -> Result<Vec<u8>, WebViewError> {
1331        self.inner.take_screenshot().await
1332    }
1333
1334    pub async fn click(
1335        &self,
1336        selector: &str,
1337        options: ClickOptions,
1338    ) -> Result<(), WebViewInputError> {
1339        <Self as WebViewInputController>::click(self, selector, options).await
1340    }
1341
1342    pub async fn type_text(
1343        &self,
1344        selector: &str,
1345        text: &str,
1346        options: TypeOptions,
1347    ) -> Result<(), WebViewInputError> {
1348        <Self as WebViewInputController>::type_text(self, selector, text, options).await
1349    }
1350
1351    pub async fn fill(
1352        &self,
1353        selector: &str,
1354        text: &str,
1355        options: FillOptions,
1356    ) -> Result<(), WebViewInputError> {
1357        <Self as WebViewInputController>::fill(self, selector, text, options).await
1358    }
1359
1360    pub async fn press(&self, key: &str, options: PressOptions) -> Result<(), WebViewInputError> {
1361        <Self as WebViewInputController>::press(self, key, options).await
1362    }
1363
1364    pub async fn scroll(
1365        &self,
1366        dx: f64,
1367        dy: f64,
1368        options: ScrollOptions,
1369    ) -> Result<(), WebViewInputError> {
1370        <Self as WebViewInputController>::scroll(self, dx, dy, options).await
1371    }
1372
1373    pub async fn scroll_to(
1374        &self,
1375        selector: &str,
1376        options: ScrollOptions,
1377    ) -> Result<(), WebViewInputError> {
1378        <Self as WebViewInputController>::scroll_to(self, selector, options).await
1379    }
1380}
1381
1382#[async_trait]
1383impl WebViewController for WebView {
1384    fn load_url(&self, url: &str) -> Result<(), WebViewError> {
1385        self.inner.load_url(url)
1386    }
1387
1388    fn load_data(&self, request: LoadDataRequest<'_>) -> Result<(), WebViewError> {
1389        self.inner.load_data(request)
1390    }
1391
1392    fn exec_js(&self, js: &str) -> Result<(), WebViewError> {
1393        self.inner.exec_js(js)
1394    }
1395
1396    async fn eval_js(&self, js: &str) -> Result<serde_json::Value, WebViewScriptError> {
1397        self.inner.eval_js(js).await
1398    }
1399
1400    async fn current_url(&self) -> Result<Option<String>, WebViewError> {
1401        self.inner.current_url().await
1402    }
1403
1404    fn post_message(&self, message: &str) -> Result<(), WebViewError> {
1405        self.inner.post_message(message)
1406    }
1407
1408    fn clear_browsing_data(&self) -> Result<(), WebViewError> {
1409        self.inner.clear_browsing_data()
1410    }
1411
1412    fn set_user_agent_override(&self, user_agent: UserAgentOverride) -> Result<(), WebViewError> {
1413        user_agent.validate()?;
1414        self.inner.set_user_agent_override(user_agent)
1415    }
1416
1417    fn reload(&self) -> Result<(), WebViewError> {
1418        self.inner.reload()
1419    }
1420
1421    fn go_back(&self) -> Result<(), WebViewError> {
1422        self.inner.go_back()
1423    }
1424
1425    fn go_forward(&self) -> Result<(), WebViewError> {
1426        self.inner.go_forward()
1427    }
1428
1429    async fn list_cookies(&self) -> Result<Vec<WebViewCookie>, WebViewError> {
1430        self.inner.list_cookies().await
1431    }
1432
1433    async fn set_cookie(&self, request: WebViewCookieSetRequest) -> Result<(), WebViewError> {
1434        self.inner.set_cookie(request).await
1435    }
1436
1437    async fn delete_cookie(
1438        &self,
1439        name: &str,
1440        domain: &str,
1441        path: &str,
1442    ) -> Result<(), WebViewError> {
1443        self.inner.delete_cookie(name, domain, path).await
1444    }
1445
1446    async fn clear_cookies(&self) -> Result<(), WebViewError> {
1447        self.inner.clear_cookies().await
1448    }
1449
1450    async fn clear_site_data(
1451        &self,
1452        url: &str,
1453        options: ClearSiteDataOptions,
1454    ) -> Result<ClearSiteDataResult, WebViewError> {
1455        self.inner.clear_site_data(url, options).await
1456    }
1457
1458    // Callers reach this through the inherent method today, but the trait
1459    // impl must stay exhaustive: a missed forward silently resolves to the
1460    // trait's Err default for dyn/generic dispatch (how clear_site_data
1461    // shipped broken).
1462    async fn take_screenshot(&self) -> Result<Vec<u8>, WebViewError> {
1463        self.inner.take_screenshot().await
1464    }
1465
1466    async fn start_network_capture(&self) -> Result<(), WebViewError> {
1467        self.inner.start_network_capture().await
1468    }
1469
1470    async fn stop_network_capture(&self) -> Result<(), WebViewError> {
1471        self.inner.stop_network_capture().await
1472    }
1473
1474    async fn network_entries(&self) -> Result<NetworkCaptureSnapshot, WebViewError> {
1475        self.inner.network_entries().await
1476    }
1477
1478    async fn clear_network_capture(&self) -> Result<(), WebViewError> {
1479        self.inner.clear_network_capture().await
1480    }
1481}
1482
1483#[async_trait]
1484impl WebViewInputController for WebView {
1485    async fn click(
1486        &self,
1487        _selector: &str,
1488        _options: ClickOptions,
1489    ) -> Result<(), WebViewInputError> {
1490        // macOS uses DOM synthesis for selector clicks: AppKit does not expose
1491        // a reliable permission-free way to update WKWebView hit testing from
1492        // an in-process NSEvent. Text and key input still use native WebKit
1493        // editing paths below. iOS/OpenHarmony likewise have no native touch
1494        // synthesis.
1495        #[cfg(all(feature = "webview-input", target_os = "macos"))]
1496        {
1497            return self.click_via_js(_selector, _options.index).await;
1498        }
1499        #[cfg(all(feature = "webview-input", target_os = "windows"))]
1500        {
1501            return self.inner.click_inner(_selector, _options).await;
1502        }
1503        #[cfg(target_os = "android")]
1504        {
1505            return self.inner.click_inner(_selector, _options).await;
1506        }
1507        #[cfg(any(target_os = "ios", all(target_os = "linux", target_env = "ohos")))]
1508        {
1509            return self.click_via_js(_selector, _options.index).await;
1510        }
1511        #[allow(unreachable_code)]
1512        Err(WebViewInputError::Unsupported(
1513            "input control is not implemented for this platform",
1514        ))
1515    }
1516
1517    async fn type_text(
1518        &self,
1519        _selector: &str,
1520        _text: &str,
1521        _options: TypeOptions,
1522    ) -> Result<(), WebViewInputError> {
1523        #[cfg(all(feature = "webview-input", target_os = "macos"))]
1524        {
1525            if self.inner.is_window_attached().await {
1526                return self.inner.type_text_inner(_selector, _text, _options).await;
1527            }
1528            return self
1529                .type_via_js(_selector, _options.index, _text, _options.replace)
1530                .await;
1531        }
1532        #[cfg(all(feature = "webview-input", target_os = "windows"))]
1533        {
1534            return self.inner.type_text_inner(_selector, _text, _options).await;
1535        }
1536        #[cfg(any(
1537            target_os = "ios",
1538            target_os = "android",
1539            all(target_os = "linux", target_env = "ohos")
1540        ))]
1541        {
1542            return self
1543                .type_via_js(_selector, _options.index, _text, _options.replace)
1544                .await;
1545        }
1546        #[allow(unreachable_code)]
1547        Err(WebViewInputError::Unsupported(
1548            "input control is not implemented for this platform",
1549        ))
1550    }
1551
1552    async fn fill(
1553        &self,
1554        _selector: &str,
1555        _text: &str,
1556        _options: FillOptions,
1557    ) -> Result<(), WebViewInputError> {
1558        #[cfg(all(feature = "webview-input", target_os = "macos"))]
1559        {
1560            // `fill` is a framework-aware replacement operation. WebKit's
1561            // native InsertText command can report success before a controlled
1562            // React/Vue input observes the edit, leaving dependent controls in
1563            // their old state. `type` retains the native keyboard path.
1564            return self
1565                .type_via_js(_selector, _options.index, _text, true)
1566                .await;
1567        }
1568        #[cfg(all(feature = "webview-input", target_os = "windows"))]
1569        {
1570            return self
1571                .inner
1572                .type_text_inner(
1573                    _selector,
1574                    _text,
1575                    TypeOptions {
1576                        index: _options.index,
1577                        replace: true,
1578                    },
1579                )
1580                .await;
1581        }
1582        #[cfg(any(
1583            target_os = "ios",
1584            target_os = "android",
1585            all(target_os = "linux", target_env = "ohos")
1586        ))]
1587        {
1588            return self
1589                .type_via_js(_selector, _options.index, _text, true)
1590                .await;
1591        }
1592        #[allow(unreachable_code)]
1593        Err(WebViewInputError::Unsupported(
1594            "input control is not implemented for this platform",
1595        ))
1596    }
1597
1598    async fn press(&self, _key: &str, _options: PressOptions) -> Result<(), WebViewInputError> {
1599        if _options.index.is_some() && _options.selector.is_none() {
1600            return Err(WebViewInputError::Platform(
1601                "press index requires a selector".to_string(),
1602            ));
1603        }
1604        #[cfg(all(feature = "webview-input", target_os = "macos"))]
1605        {
1606            if self.inner.is_window_attached().await {
1607                return self.inner.press_inner(_key, _options).await;
1608            }
1609            return self
1610                .press_via_js(_key, _options.selector.as_deref(), _options.index)
1611                .await;
1612        }
1613        #[cfg(all(feature = "webview-input", target_os = "windows"))]
1614        {
1615            return self.inner.press_inner(_key, _options).await;
1616        }
1617        #[cfg(any(
1618            target_os = "ios",
1619            target_os = "android",
1620            all(target_os = "linux", target_env = "ohos")
1621        ))]
1622        {
1623            return self
1624                .press_via_js(_key, _options.selector.as_deref(), _options.index)
1625                .await;
1626        }
1627        #[allow(unreachable_code)]
1628        Err(WebViewInputError::Unsupported(
1629            "input control is not implemented for this platform",
1630        ))
1631    }
1632
1633    async fn scroll(
1634        &self,
1635        _dx: f64,
1636        _dy: f64,
1637        _options: ScrollOptions,
1638    ) -> Result<(), WebViewInputError> {
1639        // AppUI renders lxapp pages as native surfaces with the WKWebView
1640        // detached, so native scroll wheel events can't reach the DOM — use JS.
1641        #[cfg(all(feature = "webview-input", target_os = "macos"))]
1642        {
1643            if self.inner.is_window_attached().await {
1644                return self.inner.scroll_inner(_dx, _dy, _options).await;
1645            }
1646            return self.scroll_via_js(None, _dx, _dy).await;
1647        }
1648        #[cfg(all(feature = "webview-input", target_os = "windows"))]
1649        {
1650            return self.inner.scroll_inner(_dx, _dy, _options).await;
1651        }
1652        // Android scrolls page content in the native View layer (the DOM
1653        // document has no scroll extent), so drive WebView.scrollBy natively.
1654        #[cfg(target_os = "android")]
1655        {
1656            return self.inner.scroll_inner(_dx, _dy, _options).await;
1657        }
1658        // iOS has no native scroll synthesis; Harmony webview is always detached.
1659        #[cfg(any(target_os = "ios", all(target_os = "linux", target_env = "ohos")))]
1660        {
1661            return self.scroll_via_js(None, _dx, _dy).await;
1662        }
1663        #[allow(unreachable_code)]
1664        Err(WebViewInputError::Unsupported(
1665            "input control is not implemented for this platform",
1666        ))
1667    }
1668
1669    async fn scroll_to(
1670        &self,
1671        _selector: &str,
1672        _options: ScrollOptions,
1673    ) -> Result<(), WebViewInputError> {
1674        #[cfg(all(feature = "webview-input", target_os = "macos"))]
1675        {
1676            if self.inner.is_window_attached().await {
1677                return self.inner.scroll_to_inner(_selector, _options).await;
1678            }
1679            return self.scroll_to_via_js(_selector, None).await;
1680        }
1681        #[cfg(all(feature = "webview-input", target_os = "windows"))]
1682        {
1683            return self.inner.scroll_to_inner(_selector, _options).await;
1684        }
1685        #[cfg(target_os = "android")]
1686        {
1687            return self.inner.scroll_to_inner(_selector, _options).await;
1688        }
1689        #[cfg(any(target_os = "ios", all(target_os = "linux", target_env = "ohos")))]
1690        {
1691            return self.scroll_to_via_js(_selector, None).await;
1692        }
1693        #[allow(unreachable_code)]
1694        Err(WebViewInputError::Unsupported(
1695            "input control is not implemented for this platform",
1696        ))
1697    }
1698}
1699
1700/// Type alias for WebView instances storage to reduce complexity
1701type WebViewInstancesMap = Arc<Mutex<HashMap<String, Arc<WebView>>>>;
1702
1703#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1704#[serde(rename_all = "snake_case")]
1705pub enum WebViewCreateStage {
1706    Requested,
1707    NativeCreated,
1708    ControllerAttached,
1709    Ready,
1710    Destroyed,
1711}
1712
1713#[derive(Debug, Clone, PartialEq, Eq)]
1714pub enum WebViewEvent {
1715    Stage(WebViewCreateStage),
1716    Failed {
1717        stage: WebViewCreateStage,
1718        error: WebViewError,
1719    },
1720}
1721
1722type WebViewReadyState = Option<Result<Arc<WebView>, WebViewError>>;
1723
1724#[derive(Clone)]
1725pub struct WebViewEventSubscription {
1726    rx: watch::Receiver<WebViewEvent>,
1727}
1728
1729impl WebViewEventSubscription {
1730    pub fn current(&self) -> WebViewEvent {
1731        self.rx.borrow().clone()
1732    }
1733
1734    pub async fn changed(&mut self) -> Result<WebViewEvent, WebViewError> {
1735        self.rx.changed().await.map_err(|_| {
1736            WebViewError::WebView("webview event channel unexpectedly closed".to_string())
1737        })?;
1738        Ok(self.current())
1739    }
1740}
1741
1742#[derive(Clone)]
1743pub struct WebViewSession {
1744    webtag: WebTag,
1745    event_rx: watch::Receiver<WebViewEvent>,
1746    ready_rx: watch::Receiver<WebViewReadyState>,
1747    signals: Arc<WebViewSessionSignals>,
1748}
1749
1750impl WebViewSession {
1751    pub fn webtag(&self) -> &WebTag {
1752        &self.webtag
1753    }
1754
1755    pub fn subscribe_events(&self) -> WebViewEventSubscription {
1756        WebViewEventSubscription {
1757            rx: self.event_rx.clone(),
1758        }
1759    }
1760
1761    pub fn current_event(&self) -> WebViewEvent {
1762        self.event_rx.borrow().clone()
1763    }
1764
1765    pub async fn wait_ready(&self) -> Result<Arc<WebView>, WebViewError> {
1766        let mut rx = self.ready_rx.clone();
1767        loop {
1768            if let Some(result) = self.signals.terminal_result() {
1769                return result;
1770            }
1771            if let Some(result) = rx.borrow().clone() {
1772                return result;
1773            }
1774            if rx.changed().await.is_err() {
1775                if let Some(result) = self.signals.terminal_result() {
1776                    return result;
1777                }
1778                return Err(WebViewError::WebView(
1779                    "webview ready channel unexpectedly closed".to_string(),
1780                ));
1781            }
1782        }
1783    }
1784}
1785
1786struct WebViewSessionSignals {
1787    event_tx: watch::Sender<WebViewEvent>,
1788    ready_tx: watch::Sender<WebViewReadyState>,
1789    state: Mutex<WebViewSessionState>,
1790}
1791
1792#[derive(Default)]
1793struct WebViewSessionState {
1794    terminal_result: Option<Result<Arc<WebView>, WebViewError>>,
1795    destroyed: bool,
1796}
1797
1798impl WebViewSessionSignals {
1799    fn new() -> Arc<Self> {
1800        let (event_tx, _event_rx) =
1801            watch::channel(WebViewEvent::Stage(WebViewCreateStage::Requested));
1802        let (ready_tx, _ready_rx) = watch::channel(None);
1803        Arc::new(Self {
1804            event_tx,
1805            ready_tx,
1806            state: Mutex::new(WebViewSessionState::default()),
1807        })
1808    }
1809
1810    fn subscribe(self: &Arc<Self>, webtag: WebTag) -> WebViewSession {
1811        WebViewSession {
1812            webtag,
1813            event_rx: self.event_tx.subscribe(),
1814            ready_rx: self.ready_tx.subscribe(),
1815            signals: Arc::clone(self),
1816        }
1817    }
1818
1819    fn terminal_result(&self) -> Option<Result<Arc<WebView>, WebViewError>> {
1820        let state = lock_or_recover(&self.state, "webview_session_state.terminal_result");
1821        state.terminal_result.clone()
1822    }
1823
1824    // Only consulted by the Apple create path's registry-race guard.
1825    #[cfg_attr(not(any(target_os = "macos", target_os = "ios")), allow(dead_code))]
1826    fn is_destroyed(&self) -> bool {
1827        let state = lock_or_recover(&self.state, "webview_session_state.is_destroyed");
1828        state.destroyed
1829    }
1830
1831    fn publish_result(
1832        &self,
1833        result: Result<Arc<WebView>, WebViewError>,
1834        stage_on_error: WebViewCreateStage,
1835    ) {
1836        let mut state = lock_or_recover(&self.state, "webview_session_state.publish_result");
1837        if state.destroyed || state.terminal_result.is_some() {
1838            return;
1839        }
1840        state.terminal_result = Some(result.clone());
1841        drop(state);
1842
1843        match result {
1844            Ok(webview) => {
1845                self.event_tx
1846                    .send_replace(WebViewEvent::Stage(WebViewCreateStage::NativeCreated));
1847                self.event_tx
1848                    .send_replace(WebViewEvent::Stage(WebViewCreateStage::ControllerAttached));
1849                self.ready_tx.send_replace(Some(Ok(webview)));
1850                self.event_tx
1851                    .send_replace(WebViewEvent::Stage(WebViewCreateStage::Ready));
1852            }
1853            Err(error) => {
1854                self.ready_tx.send_replace(Some(Err(error.clone())));
1855                self.event_tx.send_replace(WebViewEvent::Failed {
1856                    stage: stage_on_error,
1857                    error,
1858                });
1859            }
1860        }
1861    }
1862
1863    fn publish_destroyed(&self) {
1864        let mut state = lock_or_recover(&self.state, "webview_session_state.publish_destroyed");
1865        if state.destroyed {
1866            return;
1867        }
1868        state.destroyed = true;
1869        if state.terminal_result.is_none() {
1870            state.terminal_result = Some(Err(WebViewError::WebView(
1871                "webview destroyed before ready".to_string(),
1872            )));
1873        }
1874        let terminal_result = state.terminal_result.clone();
1875        drop(state);
1876
1877        self.event_tx
1878            .send_replace(WebViewEvent::Stage(WebViewCreateStage::Destroyed));
1879        if let Some(result) = terminal_result {
1880            self.ready_tx.send_replace(Some(result));
1881        }
1882    }
1883}
1884
1885pub(crate) struct WebViewCreateSender {
1886    webtag: WebTag,
1887    signals: Arc<WebViewSessionSignals>,
1888}
1889
1890impl WebViewCreateSender {
1891    fn new(webtag: WebTag, signals: Arc<WebViewSessionSignals>) -> Self {
1892        Self { webtag, signals }
1893    }
1894
1895    pub(crate) fn succeed(self, webview: Arc<WebView>) {
1896        self.signals
1897            .publish_result(Ok(webview), WebViewCreateStage::Requested);
1898    }
1899
1900    pub(crate) fn fail(self, stage: WebViewCreateStage, error: WebViewError) {
1901        if remove_session_signals_if_matches(&self.webtag, &self.signals) {
1902            crate::events::normalizer::destroy(&self.webtag);
1903        }
1904        self.signals.publish_result(Err(error), stage);
1905    }
1906
1907    /// Complete only this create generation after a newer same-tag session
1908    /// replaced it. The current generation's registry and callbacks belong to
1909    /// a different signals identity and must remain untouched.
1910    #[cfg_attr(not(target_os = "android"), allow(dead_code))]
1911    pub(crate) fn cancel_superseded(self) {
1912        if remove_session_signals_if_matches(&self.webtag, &self.signals) {
1913            crate::events::normalizer::destroy(&self.webtag);
1914        }
1915        self.signals.publish_destroyed();
1916    }
1917
1918    /// True if the session was destroyed (e.g. the tab was closed/discarded)
1919    /// while the native WebView was still being built. The platform create
1920    /// path checks this before registering, to avoid leaving a zombie in the
1921    /// global registry. Apple and Windows consult it around native registration.
1922    #[cfg_attr(
1923        not(any(target_os = "macos", target_os = "ios", target_os = "windows")),
1924        allow(dead_code)
1925    )]
1926    pub(crate) fn is_destroyed(&self) -> bool {
1927        self.signals.is_destroyed()
1928    }
1929}
1930
1931/// Global WebView instances storage
1932static WEBVIEW_INSTANCES: OnceLock<WebViewInstancesMap> = OnceLock::new();
1933
1934/// Pending callbacks: keyed by webtag string -> callbacks struct.
1935/// Stored here between builder-based session creation and `register_webview`.
1936struct PendingCallbacksEntry {
1937    #[cfg(target_os = "android")]
1938    signals: Arc<WebViewSessionSignals>,
1939    callbacks: PendingCallbacks,
1940}
1941
1942static PENDING_CALLBACKS: OnceLock<Mutex<HashMap<String, PendingCallbacksEntry>>> = OnceLock::new();
1943static WEBVIEW_SESSIONS: OnceLock<Mutex<HashMap<String, Arc<WebViewSessionSignals>>>> =
1944    OnceLock::new();
1945#[cfg(target_os = "windows")]
1946static WEBVIEW_CREATE_LOCKS: OnceLock<Mutex<HashMap<String, std::sync::Weak<Mutex<()>>>>> =
1947    OnceLock::new();
1948static DESIRED_PROXY_FOR_NEW_WEBVIEWS: OnceLock<RwLock<Option<ProxyConfig>>> = OnceLock::new();
1949static PROXY_APPLY_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
1950
1951fn apply_http_proxy_platform(
1952    config: Option<&ProxyConfig>,
1953) -> Result<ProxyApplyReport, WebViewError> {
1954    #[cfg(target_os = "android")]
1955    {
1956        crate::android::apply_http_proxy(config)
1957    }
1958
1959    #[cfg(any(target_os = "ios", target_os = "macos"))]
1960    {
1961        crate::apple::apply_http_proxy(config)
1962    }
1963
1964    #[cfg(all(target_os = "linux", target_env = "ohos"))]
1965    {
1966        crate::harmony::apply_http_proxy(config)
1967    }
1968
1969    #[cfg(not(any(
1970        target_os = "android",
1971        target_os = "ios",
1972        target_os = "macos",
1973        all(target_os = "linux", target_env = "ohos")
1974    )))]
1975    {
1976        let _ = config;
1977        Ok(ProxyApplyReport::unsupported(
1978            "proxy is not supported on this platform",
1979        ))
1980    }
1981}
1982
1983/// Configure the proxy that should be used for newly created WebViews in this process.
1984///
1985/// This only updates the desired configuration kept in process memory. It does
1986/// not live-apply the proxy to currently active WebViews.
1987pub fn configure_proxy_for_new_webviews(config: Option<ProxyConfig>) -> Result<(), WebViewError> {
1988    let apply_lock = PROXY_APPLY_LOCK.get_or_init(|| Mutex::new(()));
1989    let _guard = lock_or_recover(apply_lock, "webview_proxy_apply_lock");
1990
1991    let normalized_config = match config {
1992        Some(cfg) => Some(cfg.validate()?),
1993        None => None,
1994    };
1995
1996    let state = DESIRED_PROXY_FOR_NEW_WEBVIEWS.get_or_init(|| RwLock::new(None));
1997    match state.write() {
1998        Ok(mut guard) => {
1999            *guard = normalized_config;
2000        }
2001        Err(poisoned) => {
2002            log::error!("RwLock poisoned at webview_desired_proxy.write, recovering");
2003            *poisoned.into_inner() = normalized_config;
2004        }
2005    }
2006    Ok(())
2007}
2008
2009/// Apply or clear process-level HTTP proxy for the current platform runtime now.
2010///
2011/// - `Some(config)`: set proxy
2012/// - `None`: clear proxy
2013pub fn apply_proxy_to_current_runtime(
2014    config: Option<ProxyConfig>,
2015) -> Result<ProxyApplyReport, WebViewError> {
2016    let apply_lock = PROXY_APPLY_LOCK.get_or_init(|| Mutex::new(()));
2017    let _guard = lock_or_recover(apply_lock, "webview_proxy_apply_lock");
2018
2019    let normalized_config = match config {
2020        Some(cfg) => Some(cfg.validate()?),
2021        None => None,
2022    };
2023
2024    let report = apply_http_proxy_platform(normalized_config.as_ref())?;
2025
2026    if matches!(
2027        report.status,
2028        ProxyApplyStatus::Applied | ProxyApplyStatus::Cleared
2029    ) {
2030        let state = DESIRED_PROXY_FOR_NEW_WEBVIEWS.get_or_init(|| RwLock::new(None));
2031        match state.write() {
2032            Ok(mut guard) => {
2033                *guard = normalized_config;
2034            }
2035            Err(poisoned) => {
2036                log::error!("RwLock poisoned at webview_desired_proxy.write, recovering");
2037                *poisoned.into_inner() = normalized_config;
2038            }
2039        }
2040    }
2041
2042    Ok(report)
2043}
2044
2045/// Get the configured proxy that will be used for newly created WebViews.
2046pub fn configured_proxy_for_new_webviews() -> Option<ProxyConfig> {
2047    let state = DESIRED_PROXY_FOR_NEW_WEBVIEWS.get()?;
2048    match state.read() {
2049        Ok(guard) => guard.clone(),
2050        Err(poisoned) => {
2051            log::error!("RwLock poisoned at webview_desired_proxy.read, recovering");
2052            poisoned.into_inner().clone()
2053        }
2054    }
2055}
2056
2057fn clear_pending_callbacks(webtag: &WebTag) {
2058    if let Some(pending) = PENDING_CALLBACKS.get()
2059        && let Ok(mut map) = pending.lock()
2060    {
2061        map.remove(webtag.key());
2062    }
2063}
2064
2065fn replace_session_signals(webtag: &WebTag, signals: Arc<WebViewSessionSignals>) {
2066    let sessions = WEBVIEW_SESSIONS.get_or_init(|| Mutex::new(HashMap::new()));
2067    let mut guard = lock_or_recover(sessions, "webview_sessions.replace");
2068    guard.insert(webtag.key().to_string(), signals);
2069}
2070
2071fn remove_session_signals(webtag: &WebTag) -> Option<Arc<WebViewSessionSignals>> {
2072    let sessions = WEBVIEW_SESSIONS.get()?;
2073    let mut guard = lock_or_recover(sessions, "webview_sessions.remove");
2074    guard.remove(webtag.key())
2075}
2076
2077fn remove_session_signals_if_matches(
2078    webtag: &WebTag,
2079    expected: &Arc<WebViewSessionSignals>,
2080) -> bool {
2081    let Some(sessions) = WEBVIEW_SESSIONS.get() else {
2082        return false;
2083    };
2084    let mut guard = lock_or_recover(sessions, "webview_sessions.remove_if_matches");
2085    if guard
2086        .get(webtag.key())
2087        .is_some_and(|current| Arc::ptr_eq(current, expected))
2088    {
2089        guard.remove(webtag.key());
2090        true
2091    } else {
2092        false
2093    }
2094}
2095
2096/// WebView identifier combining appid, path, and optional session id.
2097/// Example: `appid:path#123`.
2098#[derive(Debug, Clone, PartialEq, Eq, Hash)]
2099pub struct WebTag(String);
2100
2101impl std::fmt::Display for WebTag {
2102    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2103        write!(f, "{}", self.0)
2104    }
2105}
2106
2107impl WebTag {
2108    pub fn new(appid: &str, path: &str, session_id: Option<u64>) -> Self {
2109        let mut tag = format!("{}:{}", appid, path);
2110        if let Some(session) = session_id {
2111            tag.push('#');
2112            tag.push_str(&session.to_string());
2113        }
2114        Self(tag)
2115    }
2116
2117    pub fn as_str(&self) -> &str {
2118        &self.0
2119    }
2120
2121    /// Storage key for this tag.
2122    /// This preserves the optional `#session` suffix so instances are isolated
2123    /// per runtime session.
2124    pub fn key(&self) -> &str {
2125        &self.0
2126    }
2127
2128    /// Extract appid from the webtag
2129    pub fn extract_appid(&self) -> String {
2130        self.0.split(':').next().unwrap_or("").to_string()
2131    }
2132
2133    /// Extract appid and path from WebTag
2134    /// This will always succeed since WebTag is constructed with a valid format
2135    pub fn extract_parts(&self) -> (String, String) {
2136        if let Some((appid, path_with_session)) = self.0.split_once(':') {
2137            let path = path_with_session
2138                .split('#')
2139                .next()
2140                .unwrap_or(path_with_session);
2141            (appid.to_string(), path.to_string())
2142        } else {
2143            log::error!("Invalid webtag format: {}", self.0);
2144            ("".to_string(), self.0.clone())
2145        }
2146    }
2147
2148    /// Extract session id (if present) from the webtag
2149    pub fn session_id(&self) -> Option<u64> {
2150        self.0
2151            .split('#')
2152            .next_back()
2153            .and_then(|raw| raw.parse::<u64>().ok())
2154    }
2155
2156    /// Grouping key combining appid and session id (`appid#session`), with the
2157    /// session defaulting to `0` when the tag carries no `#session` suffix.
2158    /// Tags without an `appid:` prefix are returned unchanged.
2159    #[cfg_attr(
2160        any(not(target_os = "windows"), target_os = "windows"),
2161        allow(dead_code)
2162    )]
2163    pub(crate) fn group_key(&self) -> String {
2164        let Some((appid, path_with_session)) = self.0.split_once(':') else {
2165            return self.0.clone();
2166        };
2167        let session = path_with_session
2168            .rsplit_once('#')
2169            .and_then(|(_, suffix)| suffix.parse::<u64>().ok())
2170            .map(|session| session.to_string())
2171            .unwrap_or_else(|| "0".to_string());
2172        format!("{appid}#{session}")
2173    }
2174
2175    fn key_path(&self) -> String {
2176        let Some((_, path_with_suffix)) = self.0.split_once(':') else {
2177            return self.0.clone();
2178        };
2179        if self.session_id().is_some()
2180            && let Some((path, _)) = path_with_suffix.rsplit_once('#')
2181        {
2182            return path.to_string();
2183        }
2184        path_with_suffix.to_string()
2185    }
2186}
2187
2188impl From<&str> for WebTag {
2189    fn from(webtag_str: &str) -> Self {
2190        Self(webtag_str.to_string())
2191    }
2192}
2193
2194fn request_create_webview(
2195    webtag: &WebTag,
2196    sender: WebViewCreateSender,
2197    options: WebViewCreateOptions,
2198) {
2199    let (appid, _) = webtag.extract_parts();
2200    let (effective_options, pending_callbacks) = match options.normalize() {
2201        Ok(value) => value,
2202        Err(error) => {
2203            sender.fail(WebViewCreateStage::Requested, error);
2204            return;
2205        }
2206    };
2207
2208    log::info!(
2209        "Creating WebView for key={} profile={:?} data_mode={:?} schemes={:?}",
2210        webtag.key(),
2211        effective_options.profile,
2212        effective_options.data_mode,
2213        effective_options.registered_schemes,
2214    );
2215
2216    // Get or initialize the global instances map
2217    let instances = WEBVIEW_INSTANCES.get_or_init(|| Arc::new(Mutex::new(HashMap::new())));
2218
2219    // Existing instance policy:
2220    // - Different options: fail fast (do not silently reuse incompatible instance).
2221    // - Same options + callback registrations: fail fast because callbacks are immutable after first create.
2222    // - Same options + no callbacks: return existing instance.
2223    if let Ok(webviews) = instances.lock()
2224        && let Some(existing_webview) = webviews.get(webtag.key())
2225    {
2226        if existing_webview.effective_options() != &effective_options {
2227            sender.fail(
2228                WebViewCreateStage::Requested,
2229                WebViewError::InvalidCreateOptions(format!(
2230                    "webview already exists with different options: key={} existing={:?} requested={:?}",
2231                    webtag.key(),
2232                    existing_webview.effective_options(),
2233                    effective_options
2234                )),
2235            );
2236            return;
2237        }
2238
2239        if pending_callbacks.has_any() {
2240            sender.fail(
2241                WebViewCreateStage::Requested,
2242                WebViewError::InvalidCreateOptions(format!(
2243                    "webview already exists and callback registrations are immutable: key={} options={:?}",
2244                    webtag.key(),
2245                    existing_webview.effective_options()
2246                )),
2247            );
2248            log::warn!(
2249                "Rejected recreate with callbacks for existing webview key={} options={:?}",
2250                webtag.key(),
2251                existing_webview.effective_options()
2252            );
2253            return;
2254        }
2255
2256        log::info!("WebView already exists, reusing: {}", webtag.key());
2257        sender.succeed(existing_webview.clone());
2258        return;
2259    }
2260
2261    // Drop stale pending callbacks from previously failed create attempts.
2262    clear_pending_callbacks(webtag);
2263
2264    // Stash pending callbacks for install during register_webview()
2265    if pending_callbacks.has_any() {
2266        let pending = PENDING_CALLBACKS.get_or_init(|| Mutex::new(HashMap::new()));
2267        if let Ok(mut map) = pending.lock() {
2268            map.insert(
2269                webtag.key().to_string(),
2270                PendingCallbacksEntry {
2271                    #[cfg(target_os = "android")]
2272                    signals: Arc::clone(&sender.signals),
2273                    callbacks: pending_callbacks,
2274                },
2275            );
2276        }
2277    }
2278
2279    // Delegate WebView creation to the platform-specific implementation
2280    WebViewInner::create(
2281        &appid,
2282        &webtag.key_path(),
2283        webtag.session_id(),
2284        effective_options,
2285        sender,
2286    );
2287}
2288
2289fn create_webview_session(webtag: WebTag, options: WebViewCreateOptions) -> WebViewSession {
2290    // Windows creation blocks until its WebView2 UI thread registers the
2291    // native instance. Serialize the whole same-tag transaction, including
2292    // session replacement and pending callbacks, so a discard/reactivate race
2293    // cannot cross-wire two generations of callbacks.
2294    #[cfg(target_os = "windows")]
2295    let create_lock = windows_webview_create_lock(webtag.key());
2296    #[cfg(target_os = "windows")]
2297    let _create_guard = lock_or_recover(&create_lock, "windows_webview_create_lock");
2298
2299    let signals = WebViewSessionSignals::new();
2300    let session = signals.subscribe(webtag.clone());
2301    let sender = WebViewCreateSender::new(webtag.clone(), signals.clone());
2302    replace_session_signals(&webtag, signals);
2303    crate::events::normalizer::begin(&webtag);
2304    request_create_webview(&webtag, sender, options);
2305    session
2306}
2307
2308#[cfg(target_os = "windows")]
2309fn windows_webview_create_lock(webtag_key: &str) -> Arc<Mutex<()>> {
2310    let locks = WEBVIEW_CREATE_LOCKS.get_or_init(|| Mutex::new(HashMap::new()));
2311    let mut locks = lock_or_recover(locks, "windows_webview_create_locks");
2312    locks.retain(|_, lock| lock.strong_count() > 0);
2313    if let Some(lock) = locks.get(webtag_key).and_then(std::sync::Weak::upgrade) {
2314        return lock;
2315    }
2316    let lock = Arc::new(Mutex::new(()));
2317    locks.insert(webtag_key.to_string(), Arc::downgrade(&lock));
2318    lock
2319}
2320
2321#[cfg_attr(target_os = "android", allow(dead_code))]
2322pub(crate) fn register_webview(webview: Arc<WebView>) {
2323    let webtag = webview.webtag();
2324
2325    // Install any pending callbacks
2326    if let Some(pending) = PENDING_CALLBACKS.get()
2327        && let Ok(mut map) = pending.lock()
2328        && let Some(entry) = map.remove(webtag.key())
2329    {
2330        let callbacks = entry.callbacks;
2331        log::info!(
2332            "Installing callbacks for {} (schemes={}, nav={}, new_window={}, download={}, file_chooser={}, delegate={})",
2333            webtag.key(),
2334            callbacks.scheme_handlers.len(),
2335            callbacks.navigation_handler.is_some(),
2336            callbacks.new_window_handler.is_some(),
2337            callbacks.download_handler.is_some(),
2338            callbacks.file_chooser_handler.is_some(),
2339            callbacks.delegate.is_some()
2340        );
2341        webview.install_callbacks(callbacks);
2342    }
2343
2344    if let Some(instances) = WEBVIEW_INSTANCES.get()
2345        && let Ok(mut webviews) = instances.lock()
2346    {
2347        webviews.insert(webtag.key().to_string(), webview.clone());
2348        log::info!("WebView created and stored: {}", webtag.key());
2349    }
2350}
2351
2352#[cfg(target_os = "android")]
2353pub(crate) fn register_android_webview_if_current(
2354    webview: Arc<WebView>,
2355    sender: &WebViewCreateSender,
2356) -> bool {
2357    let webtag = webview.webtag();
2358    let sessions = WEBVIEW_SESSIONS.get_or_init(|| Mutex::new(HashMap::new()));
2359    let session_guard = lock_or_recover(sessions, "webview_sessions.register_android");
2360    if !session_guard
2361        .get(webtag.key())
2362        .is_some_and(|current| Arc::ptr_eq(current, &sender.signals))
2363    {
2364        return false;
2365    }
2366
2367    if let Some(pending) = PENDING_CALLBACKS.get()
2368        && let Ok(mut map) = pending.lock()
2369        && map
2370            .get(webtag.key())
2371            .is_some_and(|entry| Arc::ptr_eq(&entry.signals, &sender.signals))
2372        && let Some(entry) = map.remove(webtag.key())
2373    {
2374        webview.install_callbacks(entry.callbacks);
2375    }
2376
2377    let instances = WEBVIEW_INSTANCES.get_or_init(|| Arc::new(Mutex::new(HashMap::new())));
2378    let mut webviews = lock_or_recover(instances, "webview_instances.register_android");
2379    webviews.insert(webtag.key().to_string(), webview);
2380    true
2381}
2382
2383/// Find WebView by WebTag.
2384pub(crate) fn find_webview(webtag: &WebTag) -> Option<Arc<WebView>> {
2385    if let Some(instances) = WEBVIEW_INSTANCES.get() {
2386        if let Ok(webviews) = instances.lock() {
2387            webviews.get(webtag.key()).cloned()
2388        } else {
2389            None
2390        }
2391    } else {
2392        None
2393    }
2394}
2395
2396#[cfg(target_os = "windows")]
2397pub(crate) fn first_browser_webview() -> Option<Arc<WebView>> {
2398    WEBVIEW_INSTANCES
2399        .get()
2400        .and_then(|instances| instances.lock().ok())
2401        .and_then(|webviews| {
2402            webviews
2403                .values()
2404                .find(|webview| {
2405                    webview.effective_options.profile == SecurityProfile::BrowserRelaxed
2406                })
2407                .cloned()
2408        })
2409}
2410
2411pub(crate) fn list_webviews() -> Vec<WebTag> {
2412    if let Some(instances) = WEBVIEW_INSTANCES.get()
2413        && let Ok(webviews) = instances.lock()
2414    {
2415        let mut tags: Vec<WebTag> = webviews.values().map(|webview| webview.webtag()).collect();
2416        tags.sort_by(|a, b| a.as_str().cmp(b.as_str()));
2417        return tags;
2418    }
2419    Vec::new()
2420}
2421
2422pub(crate) fn find_webview_delegate(webtag: &WebTag) -> Option<Arc<dyn WebViewDelegate>> {
2423    find_webview(webtag).and_then(|webview| webview.get_delegate())
2424}
2425
2426fn remove_arc_if_matches<T>(
2427    entries: &mut HashMap<String, Arc<T>>,
2428    key: &str,
2429    expected: &Arc<T>,
2430) -> Option<Arc<T>> {
2431    entries
2432        .get(key)
2433        .is_some_and(|current| Arc::ptr_eq(current, expected))
2434        .then(|| entries.remove(key))
2435        .flatten()
2436}
2437
2438/// Remove one ready WebView only while it is still the instance registered for
2439/// its tag. Tag-scoped session, callback, and navigation state may already
2440/// belong to a newer create cycle and is deliberately left untouched.
2441pub(crate) fn destroy_webview_if_matches(webtag: &WebTag, expected: &Arc<WebView>) -> bool {
2442    let removed = if let Some(instances) = WEBVIEW_INSTANCES.get()
2443        && let Ok(mut webviews) = instances.lock()
2444    {
2445        remove_arc_if_matches(&mut webviews, webtag.key(), expected)
2446    } else {
2447        None
2448    };
2449    if let Some(webview) = removed {
2450        #[cfg(target_os = "windows")]
2451        {
2452            let _ = webview.inner.set_content_visible(false);
2453            webview.inner.request_shutdown();
2454        }
2455        webview.remove_delegate();
2456        true
2457    } else {
2458        false
2459    }
2460}
2461
2462/// Destroy a WebView instance by WebTag and remove it from global storage
2463pub(crate) fn destroy_webview(webtag: &WebTag) {
2464    // Drain active navigations as Cancelled(WebViewDestroyed) while the
2465    // delegate can still observe them, then drop the normalizer.
2466    crate::events::normalizer::destroy(webtag);
2467    // Mark the session destroyed FIRST. If a native create is still in flight
2468    // (built on the main thread but not yet registered), it observes this via
2469    // `WebViewCreateSender::is_destroyed()` after registering and tears the
2470    // instance back down — so a destroy that races ahead of registration can't
2471    // leave a zombie in the global registry.
2472    if let Some(signals) = remove_session_signals(webtag) {
2473        signals.publish_destroyed();
2474    }
2475    let removed = if let Some(instances) = WEBVIEW_INSTANCES.get()
2476        && let Ok(mut webviews) = instances.lock()
2477    {
2478        webviews.remove(webtag.key())
2479    } else {
2480        None
2481    };
2482    if let Some(webview) = removed {
2483        // Windows composition teardown is asynchronous. Hide the controller
2484        // synchronously while it is still callable so a closed browser tab or
2485        // surface cannot leave its last composed frame over the replacement.
2486        #[cfg(target_os = "windows")]
2487        {
2488            let _ = webview.inner.set_content_visible(false);
2489            // Other owners can keep the retired Arc alive after it leaves the
2490            // registry. Stop its native thread now so a same-tag replacement
2491            // can safely begin instead of waiting for the final Arc to drop.
2492            webview.inner.request_shutdown();
2493        }
2494        webview.remove_delegate();
2495    }
2496    clear_pending_callbacks(webtag);
2497}
2498
2499#[cfg(test)]
2500mod tests {
2501    use super::{
2502        WEBVIEW_SESSIONS, WebTag, WebViewCreateSender, WebViewSessionSignals,
2503        remove_arc_if_matches, remove_session_signals_if_matches, replace_session_signals,
2504    };
2505    use std::collections::HashMap;
2506    use std::sync::Arc;
2507
2508    #[test]
2509    fn conditional_instance_removal_uses_arc_identity() {
2510        let current = Arc::new(7_u8);
2511        let same_value_different_instance = Arc::new(7_u8);
2512        let mut entries = HashMap::from([("tab".to_string(), current.clone())]);
2513
2514        assert!(
2515            remove_arc_if_matches(&mut entries, "tab", &same_value_different_instance).is_none()
2516        );
2517        assert!(Arc::ptr_eq(entries.get("tab").unwrap(), &current));
2518
2519        let removed = remove_arc_if_matches(&mut entries, "tab", &current).unwrap();
2520        assert!(Arc::ptr_eq(&removed, &current));
2521        assert!(!entries.contains_key("tab"));
2522    }
2523
2524    #[test]
2525    fn superseded_sender_completes_without_removing_current_generation() {
2526        let webtag = WebTag::from("test:pages/superseded#9173");
2527        let superseded = WebViewSessionSignals::new();
2528        let current = WebViewSessionSignals::new();
2529        replace_session_signals(&webtag, current.clone());
2530
2531        WebViewCreateSender::new(webtag.clone(), superseded.clone()).cancel_superseded();
2532
2533        assert!(
2534            superseded
2535                .terminal_result()
2536                .is_some_and(|result| result.is_err())
2537        );
2538        assert!(current.terminal_result().is_none());
2539        let sessions = WEBVIEW_SESSIONS.get().unwrap().lock().unwrap();
2540        assert!(
2541            sessions
2542                .get(webtag.key())
2543                .is_some_and(|signals| Arc::ptr_eq(signals, &current))
2544        );
2545        drop(sessions);
2546        assert!(remove_session_signals_if_matches(&webtag, &current));
2547    }
2548}