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