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, NewWindowHandler,
47    NewWindowPolicy, PressOptions, SchemeOutcome, ScrollOptions, TypeOptions,
48    WebViewInputController,
49};
50use crate::{
51    LoadDataRequest, WebResourceResponse, WebViewController, WebViewCookie,
52    WebViewCookieSetRequest, WebViewDelegate, WebViewError, WebViewInputError, WebViewScriptError,
53};
54use async_trait::async_trait;
55
56const APPLE_INTERNAL_SCHEME: &str = "lx-apple";
57
58#[cfg(not(any(
59    target_os = "android",
60    target_os = "ios",
61    target_os = "macos",
62    target_os = "windows",
63    all(target_os = "linux", target_env = "ohos")
64)))]
65fn unsupported_webview_error(action: &str) -> WebViewError {
66    WebViewError::WebView(format!("{action} is not supported on this platform"))
67}
68
69#[cfg(not(any(
70    target_os = "android",
71    target_os = "ios",
72    target_os = "macos",
73    target_os = "windows",
74    all(target_os = "linux", target_env = "ohos")
75)))]
76impl WebViewInner {
77    pub(crate) fn create(
78        appid: &str,
79        path: &str,
80        session_id: Option<u64>,
81        _effective_options: EffectiveWebViewCreateOptions,
82        sender: WebViewCreateSender,
83    ) {
84        let _webtag = WebTag::new(appid, path, session_id);
85        sender.fail(
86            WebViewCreateStage::Requested,
87            unsupported_webview_error("webview creation"),
88        );
89    }
90}
91
92#[cfg(not(any(
93    target_os = "android",
94    target_os = "ios",
95    target_os = "macos",
96    target_os = "windows",
97    all(target_os = "linux", target_env = "ohos")
98)))]
99#[async_trait]
100impl WebViewController for WebViewInner {
101    fn load_url(&self, _url: &str) -> Result<(), WebViewError> {
102        Err(unsupported_webview_error("load_url"))
103    }
104
105    fn load_data(&self, _request: LoadDataRequest<'_>) -> Result<(), WebViewError> {
106        Err(unsupported_webview_error("load_data"))
107    }
108
109    fn exec_js(&self, _js: &str) -> Result<(), WebViewError> {
110        Err(unsupported_webview_error("exec_js"))
111    }
112
113    async fn eval_js(&self, _js: &str) -> Result<serde_json::Value, WebViewScriptError> {
114        Err(WebViewScriptError::Unsupported(
115            "JavaScript evaluation is not supported on this platform",
116        ))
117    }
118
119    fn post_message(&self, _message: &str) -> Result<(), WebViewError> {
120        Err(unsupported_webview_error("post_message"))
121    }
122
123    fn clear_browsing_data(&self) -> Result<(), WebViewError> {
124        Err(unsupported_webview_error("clear_browsing_data"))
125    }
126
127    fn set_user_agent(&self, _ua: &str) -> Result<(), WebViewError> {
128        Err(unsupported_webview_error("set_user_agent"))
129    }
130}
131
132fn lock_or_recover<'a, T>(mutex: &'a Mutex<T>, name: &str) -> std::sync::MutexGuard<'a, T> {
133    match mutex.lock() {
134        Ok(guard) => guard,
135        Err(poisoned) => {
136            log::error!("Mutex poisoned at {}, recovering inner value", name);
137            poisoned.into_inner()
138        }
139    }
140}
141
142fn scheme_waker_from_sender(sender: SyncSender<()>) -> Waker {
143    // SAFETY: RawWaker functions maintain Arc refcounts correctly.
144    unsafe { Waker::from_raw(scheme_raw_waker(Arc::new(sender))) }
145}
146
147fn scheme_raw_waker(sender: Arc<SyncSender<()>>) -> RawWaker {
148    RawWaker::new(Arc::into_raw(sender) as *const (), &SCHEME_WAKER_VTABLE)
149}
150
151unsafe fn scheme_waker_clone(data: *const ()) -> RawWaker {
152    // SAFETY: data is created from Arc<SyncSender<()>> in scheme_raw_waker.
153    let arc = unsafe { Arc::<SyncSender<()>>::from_raw(data as *const SyncSender<()>) };
154    let cloned = Arc::clone(&arc);
155    let _ = Arc::into_raw(arc);
156    scheme_raw_waker(cloned)
157}
158
159unsafe fn scheme_waker_wake(data: *const ()) {
160    // SAFETY: data is created from Arc<SyncSender<()>> in scheme_raw_waker.
161    let arc = unsafe { Arc::<SyncSender<()>>::from_raw(data as *const SyncSender<()>) };
162    let _ = arc.try_send(());
163}
164
165unsafe fn scheme_waker_wake_by_ref(data: *const ()) {
166    // SAFETY: data is created from Arc<SyncSender<()>> in scheme_raw_waker.
167    let arc = unsafe { Arc::<SyncSender<()>>::from_raw(data as *const SyncSender<()>) };
168    let _ = arc.try_send(());
169    let _ = Arc::into_raw(arc);
170}
171
172unsafe fn scheme_waker_drop(data: *const ()) {
173    // SAFETY: data is created from Arc<SyncSender<()>> in scheme_raw_waker.
174    let _ = unsafe { Arc::<SyncSender<()>>::from_raw(data as *const SyncSender<()>) };
175}
176
177static SCHEME_WAKER_VTABLE: RawWakerVTable = RawWakerVTable::new(
178    scheme_waker_clone,
179    scheme_waker_wake,
180    scheme_waker_wake_by_ref,
181    scheme_waker_drop,
182);
183
184fn block_on_scheme_future<F>(future: F) -> F::Output
185where
186    F: Future,
187{
188    let (tx, rx) = sync_channel::<()>(1);
189    let waker = scheme_waker_from_sender(tx);
190    let mut context = Context::from_waker(&waker);
191    let mut future = Box::pin(future);
192
193    loop {
194        match Pin::as_mut(&mut future).poll(&mut context) {
195            Poll::Ready(value) => return value,
196            Poll::Pending => {
197                if rx.recv().is_err() {
198                    std::thread::yield_now();
199                }
200            }
201        }
202    }
203}
204
205/// Security profile for WebView creation.
206#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
207#[serde(rename_all = "snake_case")]
208pub(crate) enum SecurityProfile {
209    StrictDefault,
210    BrowserRelaxed,
211}
212
213pub(crate) type FileChooserFuture =
214    Pin<Box<dyn Future<Output = FileChooserResponse> + Send + 'static>>;
215pub(crate) type FileChooserHandler =
216    Box<dyn Fn(FileChooserRequest) -> FileChooserFuture + Send + Sync>;
217
218/// Internal WebView creation options.
219pub(crate) struct WebViewCreateOptions {
220    pub(crate) profile: SecurityProfile,
221    pub(crate) scheme_handlers: HashMap<String, AsyncSchemeHandler>,
222    pub(crate) navigation_handler: Option<NavigationHandler>,
223    pub(crate) new_window_handler: Option<NewWindowHandler>,
224    pub(crate) download_handler: Option<DownloadHandler>,
225    pub(crate) file_chooser_handler: Option<FileChooserHandler>,
226    pub(crate) delegate: Option<Arc<dyn WebViewDelegate>>,
227}
228
229impl std::fmt::Debug for WebViewCreateOptions {
230    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
231        f.debug_struct("WebViewCreateOptions")
232            .field("profile", &self.profile)
233            .field(
234                "scheme_handlers",
235                &self.scheme_handlers.keys().collect::<Vec<_>>(),
236            )
237            .field("has_navigation_handler", &self.navigation_handler.is_some())
238            .field("has_new_window_handler", &self.new_window_handler.is_some())
239            .field("has_download_handler", &self.download_handler.is_some())
240            .field(
241                "has_file_chooser_handler",
242                &self.file_chooser_handler.is_some(),
243            )
244            .field("has_delegate", &self.delegate.is_some())
245            .finish()
246    }
247}
248
249/// Global HTTP proxy configuration shared by all WebViews in the process.
250#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
251pub struct ProxyConfig {
252    pub host: String,
253    pub port: u16,
254    #[serde(default)]
255    pub bypass: Vec<String>,
256}
257
258impl ProxyConfig {
259    pub fn new(host: impl Into<String>, port: u16) -> Result<Self, WebViewError> {
260        let cfg = Self {
261            host: host.into(),
262            port,
263            bypass: Vec::new(),
264        };
265        cfg.validate()
266    }
267
268    pub fn with_bypass<I, S>(mut self, bypass: I) -> Self
269    where
270        I: IntoIterator<Item = S>,
271        S: Into<String>,
272    {
273        self.bypass = bypass.into_iter().map(Into::into).collect();
274        self
275    }
276
277    fn validate(self) -> Result<Self, WebViewError> {
278        let host = self.host.trim().to_string();
279        if host.is_empty() {
280            return Err(WebViewError::InvalidCreateOptions(
281                "proxy host cannot be empty".to_string(),
282            ));
283        }
284        if host.contains(char::is_whitespace) {
285            return Err(WebViewError::InvalidCreateOptions(
286                "proxy host cannot contain whitespace".to_string(),
287            ));
288        }
289        if self.port == 0 {
290            return Err(WebViewError::InvalidCreateOptions(
291                "proxy port must be greater than 0".to_string(),
292            ));
293        }
294
295        let mut seen = HashSet::new();
296        let mut bypass = Vec::new();
297        for raw in self.bypass {
298            let rule = raw.trim();
299            if rule.is_empty() {
300                continue;
301            }
302            let key = rule.to_ascii_lowercase();
303            if seen.insert(key) {
304                bypass.push(rule.to_string());
305            }
306        }
307
308        Ok(Self {
309            host,
310            bypass,
311            ..self
312        })
313    }
314}
315
316#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
317#[serde(rename_all = "snake_case")]
318pub enum ProxyApplyStatus {
319    Applied,
320    Cleared,
321    Unsupported,
322}
323
324#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
325#[serde(rename_all = "snake_case")]
326pub enum ProxyActivation {
327    EffectiveNow,
328    NewWebViewsOnly,
329    EngineRecreateRequired,
330    NotApplied,
331}
332
333#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
334pub struct ProxyApplyReport {
335    pub status: ProxyApplyStatus,
336    pub activation: ProxyActivation,
337    #[serde(default, skip_serializing_if = "Option::is_none")]
338    pub detail: Option<String>,
339}
340
341impl ProxyApplyReport {
342    pub fn applied(activation: ProxyActivation) -> Self {
343        Self {
344            status: ProxyApplyStatus::Applied,
345            activation,
346            detail: None,
347        }
348    }
349
350    pub fn cleared(activation: ProxyActivation) -> Self {
351        Self {
352            status: ProxyApplyStatus::Cleared,
353            activation,
354            detail: None,
355        }
356    }
357
358    pub fn unsupported(detail: impl Into<String>) -> Self {
359        Self {
360            status: ProxyApplyStatus::Unsupported,
361            activation: ProxyActivation::NotApplied,
362            detail: Some(detail.into()),
363        }
364    }
365}
366
367/// Effective, normalized options actually applied to a concrete WebView instance.
368#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
369pub(crate) struct EffectiveWebViewCreateOptions {
370    pub(crate) profile: SecurityProfile,
371    /// Scheme names registered via `on_scheme` (serializable).
372    #[serde(default)]
373    pub(crate) registered_schemes: Vec<String>,
374    #[serde(default)]
375    pub(crate) has_navigation_handler: bool,
376    #[serde(default)]
377    pub(crate) has_new_window_handler: bool,
378    #[serde(default)]
379    pub(crate) has_download_handler: bool,
380    #[serde(default)]
381    pub(crate) has_file_chooser_handler: bool,
382    #[serde(default)]
383    pub(crate) has_delegate: bool,
384}
385
386impl Default for WebViewCreateOptions {
387    fn default() -> Self {
388        Self::strict()
389    }
390}
391
392impl WebViewCreateOptions {
393    fn strict() -> Self {
394        Self {
395            profile: SecurityProfile::StrictDefault,
396            scheme_handlers: HashMap::new(),
397            navigation_handler: None,
398            new_window_handler: None,
399            download_handler: None,
400            file_chooser_handler: None,
401            delegate: None,
402        }
403    }
404
405    fn browser() -> Self {
406        Self {
407            profile: SecurityProfile::BrowserRelaxed,
408            scheme_handlers: HashMap::new(),
409            navigation_handler: None,
410            new_window_handler: None,
411            download_handler: None,
412            file_chooser_handler: None,
413            delegate: None,
414        }
415    }
416
417    /// Register a scheme handler for a custom URL scheme.
418    ///
419    /// The handler is async by design.
420    ///
421    /// Usage:
422    /// - Async workload:
423    ///   `options.on_scheme("lx", |req| async move { ... })`
424    /// - Immediate response:
425    ///   `options.on_scheme("lx", |req| async move { immediate(req).into() })`
426    fn on_scheme<F, Fut>(mut self, scheme: &str, handler: F) -> Self
427    where
428        F: Fn(http::Request<Vec<u8>>) -> Fut + Send + Sync + 'static,
429        Fut: std::future::Future<Output = SchemeOutcome> + Send + 'static,
430    {
431        let normalized = scheme.trim().to_ascii_lowercase();
432        if !normalized.is_empty() {
433            self.scheme_handlers.insert(
434                normalized,
435                Arc::new(move |req| {
436                    let fut = handler(req);
437                    Box::pin(fut)
438                }),
439            );
440        }
441        self
442    }
443
444    /// Register a navigation handler that decides whether to allow or cancel navigations.
445    /// The handler receives the URL being navigated to and returns a `NavigationPolicy`.
446    fn on_navigation<F>(mut self, handler: F) -> Self
447    where
448        F: Fn(&str) -> NavigationPolicy + Send + Sync + 'static,
449    {
450        self.navigation_handler = Some(Box::new(handler));
451        self
452    }
453
454    /// Register a new-window handler for `target="_blank"` / `window.open()`.
455    /// The handler receives the URL and returns a `NewWindowPolicy`.
456    fn on_new_window<F>(mut self, handler: F) -> Self
457    where
458        F: Fn(&str) -> NewWindowPolicy + Send + Sync + 'static,
459    {
460        self.new_window_handler = Some(Box::new(handler));
461        self
462    }
463
464    /// Register a download handler for browser-mode downloads.
465    ///
466    /// The handler runs synchronously on the platform callback thread. Keep it fast and
467    /// spawn background work onto your runtime inside the closure.
468    ///
469    /// This callback is only valid for browser profile.
470    /// Public API: `WebViewBuilder::browser(webtag).on_download(...).create()`.
471    /// In this mode, download requests are routed to the callback path instead of in-WebView
472    /// download UI.
473    fn on_download<F>(mut self, handler: F) -> Self
474    where
475        F: Fn(DownloadRequest) + Send + Sync + 'static,
476    {
477        self.download_handler = Some(Box::new(handler));
478        self
479    }
480
481    fn on_file_chooser<F, Fut>(mut self, handler: F) -> Self
482    where
483        F: Fn(FileChooserRequest) -> Fut + Send + Sync + 'static,
484        Fut: Future<Output = FileChooserResponse> + Send + 'static,
485    {
486        self.file_chooser_handler = Some(Box::new(move |request| Box::pin(handler(request))));
487        self
488    }
489
490    fn delegate(mut self, delegate: Arc<dyn WebViewDelegate>) -> Self {
491        self.delegate = Some(delegate);
492        self
493    }
494
495    pub(crate) fn normalize(
496        self,
497    ) -> Result<(EffectiveWebViewCreateOptions, PendingCallbacks), WebViewError> {
498        if self.profile != SecurityProfile::BrowserRelaxed && self.download_handler.is_some() {
499            return Err(WebViewError::InvalidCreateOptions(
500                "download callback is only supported in browser profile; use WebViewBuilder::browser(webtag).on_download(...).create()".to_string(),
501            ));
502        }
503        if self.scheme_handlers.contains_key(APPLE_INTERNAL_SCHEME) {
504            return Err(WebViewError::InvalidCreateOptions(format!(
505                "scheme '{APPLE_INTERNAL_SCHEME}' is reserved for LingXia Apple bridge transport"
506            )));
507        }
508        let mut registered_schemes: Vec<String> = self.scheme_handlers.keys().cloned().collect();
509        registered_schemes.sort_unstable();
510        registered_schemes.dedup();
511        let effective = EffectiveWebViewCreateOptions {
512            profile: self.profile,
513            registered_schemes,
514            has_navigation_handler: self.navigation_handler.is_some(),
515            has_new_window_handler: self.new_window_handler.is_some(),
516            has_download_handler: self.download_handler.is_some(),
517            has_file_chooser_handler: self.file_chooser_handler.is_some(),
518            has_delegate: self.delegate.is_some(),
519        };
520        let pending = PendingCallbacks {
521            scheme_handlers: self.scheme_handlers,
522            navigation_handler: self.navigation_handler,
523            new_window_handler: self.new_window_handler,
524            download_handler: self.download_handler,
525            file_chooser_handler: self.file_chooser_handler,
526            delegate: self.delegate,
527        };
528        Ok((effective, pending))
529    }
530}
531
532/// Entry point for mode-specific WebView creation.
533///
534/// Typical usage:
535/// - Strict lxapp page:
536///   `WebViewBuilder::strict(tag).on_scheme(...).on_navigation(...).create()`
537/// - Browser page:
538///   `WebViewBuilder::browser(tag).on_new_window(...).on_download(...).create()`
539pub struct WebViewBuilder;
540
541#[must_use = "call .create() to start WebView creation"]
542pub struct StrictWebViewBuilder {
543    webtag: WebTag,
544    options: WebViewCreateOptions,
545}
546
547#[must_use = "call .create() to start WebView creation"]
548pub struct BrowserWebViewBuilder {
549    webtag: WebTag,
550    options: WebViewCreateOptions,
551}
552
553impl WebViewBuilder {
554    /// Start a strict-profile WebView builder.
555    #[must_use = "call .create() to start WebView creation"]
556    pub fn strict(webtag: WebTag) -> StrictWebViewBuilder {
557        StrictWebViewBuilder {
558            webtag,
559            options: WebViewCreateOptions::strict(),
560        }
561    }
562
563    /// Start a browser-profile WebView builder.
564    #[must_use = "call .create() to start WebView creation"]
565    pub fn browser(webtag: WebTag) -> BrowserWebViewBuilder {
566        BrowserWebViewBuilder {
567            webtag,
568            options: WebViewCreateOptions::browser(),
569        }
570    }
571}
572
573impl StrictWebViewBuilder {
574    /// Bind a `WebViewDelegate` during creation.
575    ///
576    /// This is the only supported way to configure delegate callbacks.
577    pub fn delegate(mut self, delegate: Arc<dyn WebViewDelegate>) -> Self {
578        self.options = self.options.delegate(delegate);
579        self
580    }
581
582    pub fn on_scheme<F, Fut>(mut self, scheme: &str, handler: F) -> Self
583    where
584        F: Fn(http::Request<Vec<u8>>) -> Fut + Send + Sync + 'static,
585        Fut: std::future::Future<Output = SchemeOutcome> + Send + 'static,
586    {
587        self.options = self.options.on_scheme(scheme, handler);
588        self
589    }
590
591    pub fn on_navigation<F>(mut self, handler: F) -> Self
592    where
593        F: Fn(&str) -> NavigationPolicy + Send + Sync + 'static,
594    {
595        self.options = self.options.on_navigation(handler);
596        self
597    }
598
599    pub fn on_new_window<F>(mut self, handler: F) -> Self
600    where
601        F: Fn(&str) -> NewWindowPolicy + Send + Sync + 'static,
602    {
603        self.options = self.options.on_new_window(handler);
604        self
605    }
606
607    pub fn on_file_chooser<F, Fut>(mut self, handler: F) -> Self
608    where
609        F: Fn(FileChooserRequest) -> Fut + Send + Sync + 'static,
610        Fut: Future<Output = FileChooserResponse> + Send + 'static,
611    {
612        self.options = self.options.on_file_chooser(handler);
613        self
614    }
615
616    /// Create a strict-profile WebView session.
617    ///
618    /// Re-creating with the same `webtag` follows strict rules:
619    /// - Different options => creation fails.
620    /// - Same options but new callback registrations => creation fails.
621    /// - Same options and no callbacks => existing instance is reused.
622    pub fn create(self) -> WebViewSession {
623        create_webview_session(self.webtag, self.options)
624    }
625}
626
627impl BrowserWebViewBuilder {
628    /// Bind a `WebViewDelegate` during creation.
629    ///
630    /// This is the only supported way to configure delegate callbacks.
631    pub fn delegate(mut self, delegate: Arc<dyn WebViewDelegate>) -> Self {
632        self.options = self.options.delegate(delegate);
633        self
634    }
635
636    pub fn on_scheme<F, Fut>(mut self, scheme: &str, handler: F) -> Self
637    where
638        F: Fn(http::Request<Vec<u8>>) -> Fut + Send + Sync + 'static,
639        Fut: std::future::Future<Output = SchemeOutcome> + Send + 'static,
640    {
641        self.options = self.options.on_scheme(scheme, handler);
642        self
643    }
644
645    pub fn on_navigation<F>(mut self, handler: F) -> Self
646    where
647        F: Fn(&str) -> NavigationPolicy + Send + Sync + 'static,
648    {
649        self.options = self.options.on_navigation(handler);
650        self
651    }
652
653    pub fn on_new_window<F>(mut self, handler: F) -> Self
654    where
655        F: Fn(&str) -> NewWindowPolicy + Send + Sync + 'static,
656    {
657        self.options = self.options.on_new_window(handler);
658        self
659    }
660
661    /// Register a download callback (browser profile only).
662    ///
663    /// The callback runs on the platform callback thread; keep it fast and offload
664    /// expensive work to your app runtime.
665    pub fn on_download<F>(mut self, handler: F) -> Self
666    where
667        F: Fn(DownloadRequest) + Send + Sync + 'static,
668    {
669        self.options = self.options.on_download(handler);
670        self
671    }
672
673    pub fn on_file_chooser<F, Fut>(mut self, handler: F) -> Self
674    where
675        F: Fn(FileChooserRequest) -> Fut + Send + Sync + 'static,
676        Fut: Future<Output = FileChooserResponse> + Send + 'static,
677    {
678        self.options = self.options.on_file_chooser(handler);
679        self
680    }
681
682    /// Create a browser-profile WebView session.
683    ///
684    /// Re-creating with the same `webtag` follows strict rules:
685    /// - Different options => creation fails.
686    /// - Same options but new callback registrations => creation fails.
687    /// - Same options and no callbacks => existing instance is reused.
688    pub fn create(self) -> WebViewSession {
689        create_webview_session(self.webtag, self.options)
690    }
691}
692
693/// Pending callbacks extracted from internal option normalization.
694/// Stored between session creation and `register_webview` installation.
695pub(crate) struct PendingCallbacks {
696    pub(crate) scheme_handlers: HashMap<String, AsyncSchemeHandler>,
697    pub(crate) navigation_handler: Option<NavigationHandler>,
698    pub(crate) new_window_handler: Option<NewWindowHandler>,
699    pub(crate) download_handler: Option<DownloadHandler>,
700    pub(crate) file_chooser_handler: Option<FileChooserHandler>,
701    pub(crate) delegate: Option<Arc<dyn WebViewDelegate>>,
702}
703
704impl PendingCallbacks {
705    fn has_any(&self) -> bool {
706        !self.scheme_handlers.is_empty()
707            || self.navigation_handler.is_some()
708            || self.new_window_handler.is_some()
709            || self.download_handler.is_some()
710            || self.file_chooser_handler.is_some()
711            || self.delegate.is_some()
712    }
713}
714
715/// WebView type that includes inner implementation and delegate
716pub struct WebView {
717    pub(crate) inner: WebViewInner,
718    effective_options: EffectiveWebViewCreateOptions,
719    // Hold a strong reference to the delegate; runtime destroy clears it to break cycles.
720    delegate: RwLock<Option<Arc<dyn WebViewDelegate>>>,
721    // Closure-based scheme handlers registered via builders.
722    scheme_handlers: RwLock<HashMap<String, AsyncSchemeHandler>>,
723    navigation_handler: RwLock<Option<NavigationHandler>>,
724    new_window_handler: RwLock<Option<NewWindowHandler>>,
725    download_handler: RwLock<Option<DownloadHandler>>,
726    file_chooser_handler: RwLock<Option<FileChooserHandler>>,
727}
728
729impl WebView {
730    pub(crate) fn new(
731        inner: WebViewInner,
732        effective_options: EffectiveWebViewCreateOptions,
733    ) -> Self {
734        Self {
735            inner,
736            effective_options,
737            delegate: RwLock::new(None),
738            scheme_handlers: RwLock::new(HashMap::new()),
739            navigation_handler: RwLock::new(None),
740            new_window_handler: RwLock::new(None),
741            download_handler: RwLock::new(None),
742            file_chooser_handler: RwLock::new(None),
743        }
744    }
745
746    /// Get the appid
747    pub fn appid(&self) -> String {
748        self.inner.webtag.extract_appid()
749    }
750
751    /// Get the path
752    pub fn path(&self) -> String {
753        self.inner.webtag.extract_parts().1
754    }
755
756    /// Get the webtag (computed from appid and path)
757    pub fn webtag(&self) -> WebTag {
758        self.inner.webtag.clone()
759    }
760
761    pub(crate) fn effective_options(&self) -> &EffectiveWebViewCreateOptions {
762        &self.effective_options
763    }
764
765    /// Get delegate for this WebView
766    pub(crate) fn get_delegate(&self) -> Option<Arc<dyn WebViewDelegate>> {
767        self.delegate.read().ok().and_then(|guard| guard.clone())
768    }
769
770    /// Remove delegate for this WebView
771    pub(crate) fn remove_delegate(&self) {
772        if let Ok(mut guard) = self.delegate.write() {
773            *guard = None;
774        }
775    }
776
777    /// Install all pending callbacks into this WebView (called once during creation).
778    pub(crate) fn install_callbacks(&self, callbacks: PendingCallbacks) {
779        if let Some(delegate) = callbacks.delegate
780            && let Ok(mut guard) = self.delegate.write()
781        {
782            *guard = Some(delegate);
783        }
784        if let Ok(mut guard) = self.scheme_handlers.write() {
785            *guard = callbacks.scheme_handlers;
786        }
787        if let Some(handler) = callbacks.navigation_handler
788            && let Ok(mut guard) = self.navigation_handler.write()
789        {
790            *guard = Some(handler);
791        }
792        if let Some(handler) = callbacks.new_window_handler
793            && let Ok(mut guard) = self.new_window_handler.write()
794        {
795            *guard = Some(handler);
796        }
797        if let Some(handler) = callbacks.download_handler
798            && let Ok(mut guard) = self.download_handler.write()
799        {
800            *guard = Some(handler);
801        }
802        if let Some(handler) = callbacks.file_chooser_handler
803            && let Ok(mut guard) = self.file_chooser_handler.write()
804        {
805            *guard = Some(handler);
806        }
807    }
808
809    /// Check if a scheme handler is registered for the given scheme.
810    pub fn has_scheme_handler(&self, scheme: &str) -> bool {
811        self.scheme_handlers
812            .read()
813            .ok()
814            .is_some_and(|guard| guard.contains_key(scheme))
815    }
816
817    /// Synchronously invoke the registered scheme handler for `scheme`.
818    /// Returns `None` if no handler is registered or the handler declines.
819    pub(crate) fn handle_scheme_request(
820        &self,
821        scheme: &str,
822        request: http::Request<Vec<u8>>,
823    ) -> Option<WebResourceResponse> {
824        #[cfg(any(target_os = "ios", target_os = "macos"))]
825        if let Some(response) = self.inner.handle_internal_bridge_request(&request) {
826            return Some(response);
827        }
828
829        let guard = self.scheme_handlers.read().ok()?;
830        let handler = guard.get(scheme)?;
831        let outcome = block_on_scheme_future(handler(request));
832        match outcome {
833            SchemeOutcome::Handled(response) => Some(response),
834            SchemeOutcome::PassThrough => None,
835        }
836    }
837
838    /// Call the navigation handler. Returns `Allow` if no handler is registered.
839    pub fn handle_navigation(&self, url: &str) -> NavigationPolicy {
840        if let Ok(guard) = self.navigation_handler.read()
841            && let Some(handler) = guard.as_ref()
842        {
843            return handler(url);
844        }
845        NavigationPolicy::Allow
846    }
847
848    /// Check if a new-window handler is registered.
849    pub fn has_new_window_handler(&self) -> bool {
850        self.new_window_handler
851            .read()
852            .ok()
853            .is_some_and(|guard| guard.is_some())
854    }
855
856    /// Call the new-window handler. Returns `Cancel` if no handler is registered.
857    pub fn handle_new_window(&self, url: &str) -> NewWindowPolicy {
858        if let Ok(guard) = self.new_window_handler.read()
859            && let Some(handler) = guard.as_ref()
860        {
861            return handler(url);
862        }
863        NewWindowPolicy::Cancel
864    }
865
866    /// Dispatch a download request to the registered handler.
867    pub(crate) fn handle_download(&self, request: DownloadRequest) {
868        if let Ok(guard) = self.download_handler.read()
869            && let Some(handler) = guard.as_ref()
870        {
871            handler(request);
872        }
873    }
874
875    // Consulted only by the Windows download-event path.
876    #[cfg_attr(not(target_os = "windows"), allow(dead_code))]
877    pub(crate) fn has_download_handler(&self) -> bool {
878        self.download_handler
879            .read()
880            .ok()
881            .is_some_and(|guard| guard.is_some())
882    }
883
884    #[cfg_attr(target_os = "windows", allow(dead_code))]
885    pub(crate) fn handle_file_chooser<C>(&self, request: FileChooserRequest, completion: C) -> bool
886    where
887        C: FnOnce(FileChooserResponse) + Send + 'static,
888    {
889        let Some(future) = self.make_file_chooser_future(request) else {
890            return false;
891        };
892        std::thread::spawn(move || {
893            completion(block_on_scheme_future(future));
894        });
895        true
896    }
897
898    #[cfg_attr(target_os = "windows", allow(dead_code))]
899    fn make_file_chooser_future(&self, request: FileChooserRequest) -> Option<FileChooserFuture> {
900        let Ok(guard) = self.file_chooser_handler.read() else {
901            return None;
902        };
903        let handler = guard.as_ref()?;
904        Some(handler(request))
905    }
906
907    /// Toggle docked DevTools (macOS only, uses private _inspector API)
908    #[cfg(target_os = "macos")]
909    pub fn toggle_devtools(&self) {
910        self.inner.toggle_devtools();
911    }
912
913    /// Toggle detached DevTools (macOS only, uses private _inspector API)
914    #[cfg(target_os = "macos")]
915    pub fn toggle_devtools_detached(&self) {
916        self.inner.toggle_devtools_detached();
917    }
918
919    /// Get platform-specific pointer for interop (Apple platforms only)
920    #[cfg(any(target_os = "ios", target_os = "macos"))]
921    pub fn get_swift_webview_ptr(&self) -> usize {
922        self.inner.get_swift_webview_ptr()
923    }
924
925    /// Get Java WebView reference (Android only)
926    #[cfg(target_os = "android")]
927    pub fn get_java_webview(&self) -> &jni::objects::Global<jni::objects::JObject<'static>> {
928        self.inner.get_java_webview()
929    }
930
931    pub async fn evaluate_javascript(
932        &self,
933        js: &str,
934    ) -> Result<serde_json::Value, crate::WebViewScriptError> {
935        self.inner.eval_js(js).await
936    }
937
938    /// Synthetic-event click for platforms that don't expose a native touch
939    /// injection API (iOS WKWebView, ArkWeb on Harmony). Looks up the
940    /// selector, scrolls it into view, and dispatches a synthetic
941    /// `MouseEvent` (or sets `focus="true"` for `<lx-*>` custom elements
942    /// that proxy focus to a native overlay).
943    #[cfg(any(target_os = "ios", all(target_os = "linux", target_env = "ohos")))]
944    pub(crate) async fn click_via_js(
945        &self,
946        selector: &str,
947        index: Option<usize>,
948    ) -> Result<(), WebViewInputError> {
949        let selector_json = serde_json::to_string(selector)
950            .map_err(|err| WebViewInputError::Platform(format!("Invalid selector: {err}")))?;
951        let idx = index.unwrap_or(0);
952        let script = format!(
953            "((sel, i) => {{ \
954              const els = document.querySelectorAll(sel); \
955              if (!els.length || i < 0 || i >= els.length) return {{ ok:false, error:'no match', count:els.length }}; \
956              const el = els[i]; \
957              try {{ el.scrollIntoView({{block:'center', inline:'center'}}); }} catch(_e) {{}} \
958              const rect = el.getBoundingClientRect(); \
959              const style = window.getComputedStyle(el); \
960              const disabled = !!el.disabled || el.getAttribute('aria-disabled') === 'true'; \
961              const visible = rect.width > 0 && rect.height > 0 && rect.bottom > 0 && rect.right > 0 && \
962                rect.top < window.innerHeight && rect.left < window.innerWidth && \
963                style.visibility !== 'hidden' && style.display !== 'none' && Number(style.opacity || '1') !== 0; \
964              if (!visible) return {{ ok:false, error:'not visible', interactable:false, count:els.length }}; \
965              if (disabled) return {{ ok:false, error:'not enabled', interactable:false, count:els.length }}; \
966              const tag = (el.tagName || '').toLowerCase(); \
967              if (tag.indexOf('lx-') === 0) {{ \
968                el.setAttribute('focus', 'true'); \
969                if (typeof el.syncNativeProps === 'function') {{ try {{ el.syncNativeProps(); }} catch(_e) {{}} }} \
970                return {{ ok:true, count:els.length, native:true }}; \
971              }} \
972              if (typeof el.focus === 'function') {{ try {{ el.focus({{preventScroll:true}}); }} catch(_e) {{ try {{ el.focus(); }} catch(__){{}} }} }} \
973              const opts = {{ bubbles:true, cancelable:true, view:window, clientX: rect.left + rect.width/2, clientY: rect.top + rect.height/2 }}; \
974              try {{ el.dispatchEvent(new MouseEvent('mousedown', opts)); }} catch(_e) {{}} \
975              try {{ el.dispatchEvent(new MouseEvent('mouseup', opts)); }} catch(_e) {{}} \
976              try {{ el.dispatchEvent(new MouseEvent('click', opts)); }} catch(_e) {{}} \
977              return {{ ok:true, count:els.length }}; \
978            }})({selector_json}, {idx})"
979        );
980        let result = self
981            .inner
982            .eval_js(&script)
983            .await
984            .map_err(WebViewInputError::Script)?;
985        if result.get("ok").and_then(|v| v.as_bool()) == Some(true) {
986            Ok(())
987        } else {
988            let err_msg = result
989                .get("error")
990                .and_then(|v| v.as_str())
991                .unwrap_or("click failed")
992                .to_string();
993            if result.get("interactable").and_then(|v| v.as_bool()) == Some(false) {
994                Err(WebViewInputError::ElementNotInteractable(err_msg))
995            } else {
996                Err(WebViewInputError::ElementNotFound(err_msg))
997            }
998        }
999    }
1000
1001    pub async fn current_url(&self) -> Result<Option<String>, WebViewError> {
1002        self.inner.current_url().await
1003    }
1004
1005    pub fn reload(&self) -> Result<(), WebViewError> {
1006        self.inner.reload()
1007    }
1008
1009    pub fn go_back(&self) -> Result<(), WebViewError> {
1010        self.inner.go_back()
1011    }
1012
1013    pub fn go_forward(&self) -> Result<(), WebViewError> {
1014        self.inner.go_forward()
1015    }
1016
1017    pub async fn list_cookies(&self) -> Result<Vec<WebViewCookie>, WebViewError> {
1018        self.inner.list_cookies().await
1019    }
1020
1021    pub async fn set_cookie(&self, request: WebViewCookieSetRequest) -> Result<(), WebViewError> {
1022        self.inner.set_cookie(request).await
1023    }
1024
1025    pub async fn delete_cookie(
1026        &self,
1027        name: &str,
1028        domain: &str,
1029        path: &str,
1030    ) -> Result<(), WebViewError> {
1031        self.inner.delete_cookie(name, domain, path).await
1032    }
1033
1034    pub async fn clear_cookies(&self) -> Result<(), WebViewError> {
1035        self.inner.clear_cookies().await
1036    }
1037
1038    pub async fn take_screenshot(&self) -> Result<Vec<u8>, WebViewError> {
1039        self.inner.take_screenshot().await
1040    }
1041
1042    pub async fn click(
1043        &self,
1044        selector: &str,
1045        options: ClickOptions,
1046    ) -> Result<(), WebViewInputError> {
1047        <Self as WebViewInputController>::click(self, selector, options).await
1048    }
1049
1050    pub async fn type_text(
1051        &self,
1052        selector: &str,
1053        text: &str,
1054        options: TypeOptions,
1055    ) -> Result<(), WebViewInputError> {
1056        <Self as WebViewInputController>::type_text(self, selector, text, options).await
1057    }
1058
1059    pub async fn fill(
1060        &self,
1061        selector: &str,
1062        text: &str,
1063        options: FillOptions,
1064    ) -> Result<(), WebViewInputError> {
1065        <Self as WebViewInputController>::fill(self, selector, text, options).await
1066    }
1067
1068    pub async fn press(&self, key: &str, options: PressOptions) -> Result<(), WebViewInputError> {
1069        <Self as WebViewInputController>::press(self, key, options).await
1070    }
1071
1072    pub async fn scroll(
1073        &self,
1074        dx: f64,
1075        dy: f64,
1076        options: ScrollOptions,
1077    ) -> Result<(), WebViewInputError> {
1078        <Self as WebViewInputController>::scroll(self, dx, dy, options).await
1079    }
1080
1081    pub async fn scroll_to(
1082        &self,
1083        selector: &str,
1084        options: ScrollOptions,
1085    ) -> Result<(), WebViewInputError> {
1086        <Self as WebViewInputController>::scroll_to(self, selector, options).await
1087    }
1088}
1089
1090#[async_trait]
1091impl WebViewController for WebView {
1092    fn load_url(&self, url: &str) -> Result<(), WebViewError> {
1093        self.inner.load_url(url)
1094    }
1095
1096    fn load_data(&self, request: LoadDataRequest<'_>) -> Result<(), WebViewError> {
1097        self.inner.load_data(request)
1098    }
1099
1100    fn exec_js(&self, js: &str) -> Result<(), WebViewError> {
1101        self.inner.exec_js(js)
1102    }
1103
1104    async fn eval_js(&self, js: &str) -> Result<serde_json::Value, WebViewScriptError> {
1105        self.inner.eval_js(js).await
1106    }
1107
1108    async fn current_url(&self) -> Result<Option<String>, WebViewError> {
1109        self.inner.current_url().await
1110    }
1111
1112    fn post_message(&self, message: &str) -> Result<(), WebViewError> {
1113        self.inner.post_message(message)
1114    }
1115
1116    fn clear_browsing_data(&self) -> Result<(), WebViewError> {
1117        self.inner.clear_browsing_data()
1118    }
1119
1120    fn set_user_agent(&self, ua: &str) -> Result<(), WebViewError> {
1121        self.inner.set_user_agent(ua)
1122    }
1123
1124    fn reload(&self) -> Result<(), WebViewError> {
1125        self.inner.reload()
1126    }
1127
1128    fn go_back(&self) -> Result<(), WebViewError> {
1129        self.inner.go_back()
1130    }
1131
1132    fn go_forward(&self) -> Result<(), WebViewError> {
1133        self.inner.go_forward()
1134    }
1135
1136    async fn list_cookies(&self) -> Result<Vec<WebViewCookie>, WebViewError> {
1137        self.inner.list_cookies().await
1138    }
1139
1140    async fn set_cookie(&self, request: WebViewCookieSetRequest) -> Result<(), WebViewError> {
1141        self.inner.set_cookie(request).await
1142    }
1143
1144    async fn delete_cookie(
1145        &self,
1146        name: &str,
1147        domain: &str,
1148        path: &str,
1149    ) -> Result<(), WebViewError> {
1150        self.inner.delete_cookie(name, domain, path).await
1151    }
1152
1153    async fn clear_cookies(&self) -> Result<(), WebViewError> {
1154        self.inner.clear_cookies().await
1155    }
1156}
1157
1158#[async_trait]
1159impl WebViewInputController for WebView {
1160    async fn click(
1161        &self,
1162        _selector: &str,
1163        _options: ClickOptions,
1164    ) -> Result<(), WebViewInputError> {
1165        #[cfg(all(feature = "webview-input", target_os = "macos"))]
1166        {
1167            return self.inner.click_inner(_selector, _options).await;
1168        }
1169        #[cfg(target_os = "android")]
1170        {
1171            return self.inner.click_inner(_selector, _options).await;
1172        }
1173        #[cfg(target_os = "ios")]
1174        {
1175            return self.click_via_js(_selector, _options.index).await;
1176        }
1177        #[cfg(all(target_os = "linux", target_env = "ohos"))]
1178        {
1179            return self.click_via_js(_selector, _options.index).await;
1180        }
1181        #[allow(unreachable_code)]
1182        Err(WebViewInputError::Unsupported(
1183            "input control is not implemented for this platform",
1184        ))
1185    }
1186
1187    async fn type_text(
1188        &self,
1189        _selector: &str,
1190        _text: &str,
1191        _options: TypeOptions,
1192    ) -> Result<(), WebViewInputError> {
1193        #[cfg(all(feature = "webview-input", target_os = "macos"))]
1194        {
1195            return self.inner.type_text_inner(_selector, _text, _options).await;
1196        }
1197        #[allow(unreachable_code)]
1198        Err(WebViewInputError::Unsupported(
1199            "input control is not implemented for this platform",
1200        ))
1201    }
1202
1203    async fn fill(
1204        &self,
1205        _selector: &str,
1206        _text: &str,
1207        _options: FillOptions,
1208    ) -> Result<(), WebViewInputError> {
1209        #[cfg(all(feature = "webview-input", target_os = "macos"))]
1210        {
1211            let _ = _options;
1212            return self
1213                .inner
1214                .type_text_inner(
1215                    _selector,
1216                    _text,
1217                    TypeOptions {
1218                        index: _options.index,
1219                        replace: true,
1220                    },
1221                )
1222                .await;
1223        }
1224        #[allow(unreachable_code)]
1225        Err(WebViewInputError::Unsupported(
1226            "input control is not implemented for this platform",
1227        ))
1228    }
1229
1230    async fn press(&self, _key: &str, _options: PressOptions) -> Result<(), WebViewInputError> {
1231        #[cfg(all(feature = "webview-input", target_os = "macos"))]
1232        {
1233            return self.inner.press_inner(_key, _options).await;
1234        }
1235        #[allow(unreachable_code)]
1236        Err(WebViewInputError::Unsupported(
1237            "input control is not implemented for this platform",
1238        ))
1239    }
1240
1241    async fn scroll(
1242        &self,
1243        _dx: f64,
1244        _dy: f64,
1245        _options: ScrollOptions,
1246    ) -> Result<(), WebViewInputError> {
1247        #[cfg(all(feature = "webview-input", target_os = "macos"))]
1248        {
1249            return self.inner.scroll_inner(_dx, _dy, _options).await;
1250        }
1251        #[allow(unreachable_code)]
1252        Err(WebViewInputError::Unsupported(
1253            "input control is not implemented for this platform",
1254        ))
1255    }
1256
1257    async fn scroll_to(
1258        &self,
1259        _selector: &str,
1260        _options: ScrollOptions,
1261    ) -> Result<(), WebViewInputError> {
1262        #[cfg(all(feature = "webview-input", target_os = "macos"))]
1263        {
1264            return self.inner.scroll_to_inner(_selector, _options).await;
1265        }
1266        #[allow(unreachable_code)]
1267        Err(WebViewInputError::Unsupported(
1268            "input control is not implemented for this platform",
1269        ))
1270    }
1271}
1272
1273/// Type alias for WebView instances storage to reduce complexity
1274type WebViewInstancesMap = Arc<Mutex<HashMap<String, Arc<WebView>>>>;
1275
1276#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1277#[serde(rename_all = "snake_case")]
1278pub enum WebViewCreateStage {
1279    Requested,
1280    NativeCreated,
1281    ControllerAttached,
1282    Ready,
1283    Destroyed,
1284}
1285
1286#[derive(Debug, Clone, PartialEq, Eq)]
1287pub enum WebViewEvent {
1288    Stage(WebViewCreateStage),
1289    Failed {
1290        stage: WebViewCreateStage,
1291        error: WebViewError,
1292    },
1293}
1294
1295type WebViewReadyState = Option<Result<Arc<WebView>, WebViewError>>;
1296
1297#[derive(Clone)]
1298pub struct WebViewEventSubscription {
1299    rx: watch::Receiver<WebViewEvent>,
1300}
1301
1302impl WebViewEventSubscription {
1303    pub fn current(&self) -> WebViewEvent {
1304        self.rx.borrow().clone()
1305    }
1306
1307    pub async fn changed(&mut self) -> Result<WebViewEvent, WebViewError> {
1308        self.rx.changed().await.map_err(|_| {
1309            WebViewError::WebView("webview event channel unexpectedly closed".to_string())
1310        })?;
1311        Ok(self.current())
1312    }
1313}
1314
1315#[derive(Clone)]
1316pub struct WebViewSession {
1317    webtag: WebTag,
1318    event_rx: watch::Receiver<WebViewEvent>,
1319    ready_rx: watch::Receiver<WebViewReadyState>,
1320    signals: Arc<WebViewSessionSignals>,
1321}
1322
1323impl WebViewSession {
1324    pub fn webtag(&self) -> &WebTag {
1325        &self.webtag
1326    }
1327
1328    pub fn subscribe_events(&self) -> WebViewEventSubscription {
1329        WebViewEventSubscription {
1330            rx: self.event_rx.clone(),
1331        }
1332    }
1333
1334    pub fn current_event(&self) -> WebViewEvent {
1335        self.event_rx.borrow().clone()
1336    }
1337
1338    pub async fn wait_ready(&self) -> Result<Arc<WebView>, WebViewError> {
1339        let mut rx = self.ready_rx.clone();
1340        loop {
1341            if let Some(result) = self.signals.terminal_result() {
1342                return result;
1343            }
1344            if let Some(result) = rx.borrow().clone() {
1345                return result;
1346            }
1347            if rx.changed().await.is_err() {
1348                if let Some(result) = self.signals.terminal_result() {
1349                    return result;
1350                }
1351                return Err(WebViewError::WebView(
1352                    "webview ready channel unexpectedly closed".to_string(),
1353                ));
1354            }
1355        }
1356    }
1357}
1358
1359struct WebViewSessionSignals {
1360    event_tx: watch::Sender<WebViewEvent>,
1361    ready_tx: watch::Sender<WebViewReadyState>,
1362    state: Mutex<WebViewSessionState>,
1363}
1364
1365#[derive(Default)]
1366struct WebViewSessionState {
1367    terminal_result: Option<Result<Arc<WebView>, WebViewError>>,
1368    destroyed: bool,
1369}
1370
1371impl WebViewSessionSignals {
1372    fn new() -> Arc<Self> {
1373        let (event_tx, _event_rx) =
1374            watch::channel(WebViewEvent::Stage(WebViewCreateStage::Requested));
1375        let (ready_tx, _ready_rx) = watch::channel(None);
1376        Arc::new(Self {
1377            event_tx,
1378            ready_tx,
1379            state: Mutex::new(WebViewSessionState::default()),
1380        })
1381    }
1382
1383    fn subscribe(self: &Arc<Self>, webtag: WebTag) -> WebViewSession {
1384        WebViewSession {
1385            webtag,
1386            event_rx: self.event_tx.subscribe(),
1387            ready_rx: self.ready_tx.subscribe(),
1388            signals: Arc::clone(self),
1389        }
1390    }
1391
1392    fn terminal_result(&self) -> Option<Result<Arc<WebView>, WebViewError>> {
1393        let state = lock_or_recover(&self.state, "webview_session_state.terminal_result");
1394        state.terminal_result.clone()
1395    }
1396
1397    // Only consulted by the Apple create path's registry-race guard.
1398    #[cfg_attr(not(any(target_os = "macos", target_os = "ios")), allow(dead_code))]
1399    fn is_destroyed(&self) -> bool {
1400        let state = lock_or_recover(&self.state, "webview_session_state.is_destroyed");
1401        state.destroyed
1402    }
1403
1404    fn publish_result(
1405        &self,
1406        result: Result<Arc<WebView>, WebViewError>,
1407        stage_on_error: WebViewCreateStage,
1408    ) {
1409        let mut state = lock_or_recover(&self.state, "webview_session_state.publish_result");
1410        if state.destroyed || state.terminal_result.is_some() {
1411            return;
1412        }
1413        state.terminal_result = Some(result.clone());
1414        drop(state);
1415
1416        match result {
1417            Ok(webview) => {
1418                self.event_tx
1419                    .send_replace(WebViewEvent::Stage(WebViewCreateStage::NativeCreated));
1420                self.event_tx
1421                    .send_replace(WebViewEvent::Stage(WebViewCreateStage::ControllerAttached));
1422                self.ready_tx.send_replace(Some(Ok(webview)));
1423                self.event_tx
1424                    .send_replace(WebViewEvent::Stage(WebViewCreateStage::Ready));
1425            }
1426            Err(error) => {
1427                self.ready_tx.send_replace(Some(Err(error.clone())));
1428                self.event_tx.send_replace(WebViewEvent::Failed {
1429                    stage: stage_on_error,
1430                    error,
1431                });
1432            }
1433        }
1434    }
1435
1436    fn publish_destroyed(&self) {
1437        let mut state = lock_or_recover(&self.state, "webview_session_state.publish_destroyed");
1438        if state.destroyed {
1439            return;
1440        }
1441        state.destroyed = true;
1442        if state.terminal_result.is_none() {
1443            state.terminal_result = Some(Err(WebViewError::WebView(
1444                "webview destroyed before ready".to_string(),
1445            )));
1446        }
1447        let terminal_result = state.terminal_result.clone();
1448        drop(state);
1449
1450        self.event_tx
1451            .send_replace(WebViewEvent::Stage(WebViewCreateStage::Destroyed));
1452        if let Some(result) = terminal_result {
1453            self.ready_tx.send_replace(Some(result));
1454        }
1455    }
1456}
1457
1458pub(crate) struct WebViewCreateSender {
1459    signals: Arc<WebViewSessionSignals>,
1460}
1461
1462impl WebViewCreateSender {
1463    fn new(signals: Arc<WebViewSessionSignals>) -> Self {
1464        Self { signals }
1465    }
1466
1467    pub(crate) fn succeed(self, webview: Arc<WebView>) {
1468        self.signals
1469            .publish_result(Ok(webview), WebViewCreateStage::Requested);
1470    }
1471
1472    pub(crate) fn fail(self, stage: WebViewCreateStage, error: WebViewError) {
1473        self.signals.publish_result(Err(error), stage);
1474    }
1475
1476    /// True if the session was destroyed (e.g. the tab was closed/discarded)
1477    /// while the native WebView was still being built. The platform create
1478    /// path checks this before registering, to avoid leaving a zombie in the
1479    /// global registry. Only the Apple create path consults it today.
1480    #[cfg_attr(not(any(target_os = "macos", target_os = "ios")), allow(dead_code))]
1481    pub(crate) fn is_destroyed(&self) -> bool {
1482        self.signals.is_destroyed()
1483    }
1484}
1485
1486/// Global WebView instances storage
1487static WEBVIEW_INSTANCES: OnceLock<WebViewInstancesMap> = OnceLock::new();
1488
1489/// Pending callbacks: keyed by webtag string -> callbacks struct.
1490/// Stored here between builder-based session creation and `register_webview`.
1491static PENDING_CALLBACKS: OnceLock<Mutex<HashMap<String, PendingCallbacks>>> = OnceLock::new();
1492static WEBVIEW_SESSIONS: OnceLock<Mutex<HashMap<String, Arc<WebViewSessionSignals>>>> =
1493    OnceLock::new();
1494static DESIRED_PROXY_FOR_NEW_WEBVIEWS: OnceLock<RwLock<Option<ProxyConfig>>> = OnceLock::new();
1495static PROXY_APPLY_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
1496
1497fn apply_http_proxy_platform(
1498    config: Option<&ProxyConfig>,
1499) -> Result<ProxyApplyReport, WebViewError> {
1500    #[cfg(target_os = "android")]
1501    {
1502        crate::android::apply_http_proxy(config)
1503    }
1504
1505    #[cfg(any(target_os = "ios", target_os = "macos"))]
1506    {
1507        crate::apple::apply_http_proxy(config)
1508    }
1509
1510    #[cfg(all(target_os = "linux", target_env = "ohos"))]
1511    {
1512        crate::harmony::apply_http_proxy(config)
1513    }
1514
1515    #[cfg(not(any(
1516        target_os = "android",
1517        target_os = "ios",
1518        target_os = "macos",
1519        all(target_os = "linux", target_env = "ohos")
1520    )))]
1521    {
1522        let _ = config;
1523        Ok(ProxyApplyReport::unsupported(
1524            "proxy is not supported on this platform",
1525        ))
1526    }
1527}
1528
1529/// Configure the proxy that should be used for newly created WebViews in this process.
1530///
1531/// This only updates the desired configuration kept in process memory. It does
1532/// not live-apply the proxy to currently active WebViews.
1533pub fn configure_proxy_for_new_webviews(config: Option<ProxyConfig>) -> Result<(), WebViewError> {
1534    let apply_lock = PROXY_APPLY_LOCK.get_or_init(|| Mutex::new(()));
1535    let _guard = lock_or_recover(apply_lock, "webview_proxy_apply_lock");
1536
1537    let normalized_config = match config {
1538        Some(cfg) => Some(cfg.validate()?),
1539        None => None,
1540    };
1541
1542    let state = DESIRED_PROXY_FOR_NEW_WEBVIEWS.get_or_init(|| RwLock::new(None));
1543    match state.write() {
1544        Ok(mut guard) => {
1545            *guard = normalized_config;
1546        }
1547        Err(poisoned) => {
1548            log::error!("RwLock poisoned at webview_desired_proxy.write, recovering");
1549            *poisoned.into_inner() = normalized_config;
1550        }
1551    }
1552    Ok(())
1553}
1554
1555/// Apply or clear process-level HTTP proxy for the current platform runtime now.
1556///
1557/// - `Some(config)`: set proxy
1558/// - `None`: clear proxy
1559pub fn apply_proxy_to_current_runtime(
1560    config: Option<ProxyConfig>,
1561) -> Result<ProxyApplyReport, WebViewError> {
1562    let apply_lock = PROXY_APPLY_LOCK.get_or_init(|| Mutex::new(()));
1563    let _guard = lock_or_recover(apply_lock, "webview_proxy_apply_lock");
1564
1565    let normalized_config = match config {
1566        Some(cfg) => Some(cfg.validate()?),
1567        None => None,
1568    };
1569
1570    let report = apply_http_proxy_platform(normalized_config.as_ref())?;
1571
1572    if matches!(
1573        report.status,
1574        ProxyApplyStatus::Applied | ProxyApplyStatus::Cleared
1575    ) {
1576        let state = DESIRED_PROXY_FOR_NEW_WEBVIEWS.get_or_init(|| RwLock::new(None));
1577        match state.write() {
1578            Ok(mut guard) => {
1579                *guard = normalized_config;
1580            }
1581            Err(poisoned) => {
1582                log::error!("RwLock poisoned at webview_desired_proxy.write, recovering");
1583                *poisoned.into_inner() = normalized_config;
1584            }
1585        }
1586    }
1587
1588    Ok(report)
1589}
1590
1591/// Get the configured proxy that will be used for newly created WebViews.
1592pub fn configured_proxy_for_new_webviews() -> Option<ProxyConfig> {
1593    let state = DESIRED_PROXY_FOR_NEW_WEBVIEWS.get()?;
1594    match state.read() {
1595        Ok(guard) => guard.clone(),
1596        Err(poisoned) => {
1597            log::error!("RwLock poisoned at webview_desired_proxy.read, recovering");
1598            poisoned.into_inner().clone()
1599        }
1600    }
1601}
1602
1603fn clear_pending_callbacks(webtag: &WebTag) {
1604    if let Some(pending) = PENDING_CALLBACKS.get()
1605        && let Ok(mut map) = pending.lock()
1606    {
1607        map.remove(webtag.key());
1608    }
1609}
1610
1611fn replace_session_signals(webtag: &WebTag, signals: Arc<WebViewSessionSignals>) {
1612    let sessions = WEBVIEW_SESSIONS.get_or_init(|| Mutex::new(HashMap::new()));
1613    let mut guard = lock_or_recover(sessions, "webview_sessions.replace");
1614    guard.insert(webtag.key().to_string(), signals);
1615}
1616
1617fn remove_session_signals(webtag: &WebTag) -> Option<Arc<WebViewSessionSignals>> {
1618    let sessions = WEBVIEW_SESSIONS.get()?;
1619    let mut guard = lock_or_recover(sessions, "webview_sessions.remove");
1620    guard.remove(webtag.key())
1621}
1622
1623/// WebView identifier combining appid, path, and optional session id.
1624/// Example: `appid:path#123`.
1625#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1626pub struct WebTag(String);
1627
1628impl std::fmt::Display for WebTag {
1629    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1630        write!(f, "{}", self.0)
1631    }
1632}
1633
1634impl WebTag {
1635    pub fn new(appid: &str, path: &str, session_id: Option<u64>) -> Self {
1636        let mut tag = format!("{}:{}", appid, path);
1637        if let Some(session) = session_id {
1638            tag.push('#');
1639            tag.push_str(&session.to_string());
1640        }
1641        Self(tag)
1642    }
1643
1644    pub fn as_str(&self) -> &str {
1645        &self.0
1646    }
1647
1648    /// Storage key for this tag.
1649    /// This preserves the optional `#session` suffix so instances are isolated
1650    /// per runtime session.
1651    pub fn key(&self) -> &str {
1652        &self.0
1653    }
1654
1655    /// Extract appid from the webtag
1656    pub fn extract_appid(&self) -> String {
1657        self.0.split(':').next().unwrap_or("").to_string()
1658    }
1659
1660    /// Extract appid and path from WebTag
1661    /// This will always succeed since WebTag is constructed with a valid format
1662    pub fn extract_parts(&self) -> (String, String) {
1663        if let Some((appid, path_with_session)) = self.0.split_once(':') {
1664            let path = path_with_session
1665                .split('#')
1666                .next()
1667                .unwrap_or(path_with_session);
1668            (appid.to_string(), path.to_string())
1669        } else {
1670            log::error!("Invalid webtag format: {}", self.0);
1671            ("".to_string(), self.0.clone())
1672        }
1673    }
1674
1675    /// Extract session id (if present) from the webtag
1676    pub fn session_id(&self) -> Option<u64> {
1677        self.0
1678            .split('#')
1679            .next_back()
1680            .and_then(|raw| raw.parse::<u64>().ok())
1681    }
1682
1683    /// Grouping key combining appid and session id (`appid#session`), with the
1684    /// session defaulting to `0` when the tag carries no `#session` suffix.
1685    /// Tags without an `appid:` prefix are returned unchanged.
1686    #[cfg_attr(
1687        any(not(target_os = "windows"), target_os = "windows"),
1688        allow(dead_code)
1689    )]
1690    pub(crate) fn group_key(&self) -> String {
1691        let Some((appid, path_with_session)) = self.0.split_once(':') else {
1692            return self.0.clone();
1693        };
1694        let session = path_with_session
1695            .rsplit_once('#')
1696            .and_then(|(_, suffix)| suffix.parse::<u64>().ok())
1697            .map(|session| session.to_string())
1698            .unwrap_or_else(|| "0".to_string());
1699        format!("{appid}#{session}")
1700    }
1701
1702    fn key_path(&self) -> String {
1703        let Some((_, path_with_suffix)) = self.0.split_once(':') else {
1704            return self.0.clone();
1705        };
1706        if self.session_id().is_some()
1707            && let Some((path, _)) = path_with_suffix.rsplit_once('#')
1708        {
1709            return path.to_string();
1710        }
1711        path_with_suffix.to_string()
1712    }
1713}
1714
1715impl From<&str> for WebTag {
1716    fn from(webtag_str: &str) -> Self {
1717        Self(webtag_str.to_string())
1718    }
1719}
1720
1721fn request_create_webview(
1722    webtag: &WebTag,
1723    sender: WebViewCreateSender,
1724    options: WebViewCreateOptions,
1725) {
1726    let (appid, _) = webtag.extract_parts();
1727    let (effective_options, pending_callbacks) = match options.normalize() {
1728        Ok(value) => value,
1729        Err(error) => {
1730            sender.fail(WebViewCreateStage::Requested, error);
1731            return;
1732        }
1733    };
1734
1735    log::info!(
1736        "Creating WebView for key={} profile={:?} schemes={:?}",
1737        webtag.key(),
1738        effective_options.profile,
1739        effective_options.registered_schemes,
1740    );
1741
1742    // Get or initialize the global instances map
1743    let instances = WEBVIEW_INSTANCES.get_or_init(|| Arc::new(Mutex::new(HashMap::new())));
1744
1745    // Existing instance policy:
1746    // - Different options: fail fast (do not silently reuse incompatible instance).
1747    // - Same options + callback registrations: fail fast because callbacks are immutable after first create.
1748    // - Same options + no callbacks: return existing instance.
1749    if let Ok(webviews) = instances.lock()
1750        && let Some(existing_webview) = webviews.get(webtag.key())
1751    {
1752        if existing_webview.effective_options() != &effective_options {
1753            sender.fail(
1754                WebViewCreateStage::Requested,
1755                WebViewError::InvalidCreateOptions(format!(
1756                    "webview already exists with different options: key={} existing={:?} requested={:?}",
1757                    webtag.key(),
1758                    existing_webview.effective_options(),
1759                    effective_options
1760                )),
1761            );
1762            return;
1763        }
1764
1765        if pending_callbacks.has_any() {
1766            sender.fail(
1767                WebViewCreateStage::Requested,
1768                WebViewError::InvalidCreateOptions(format!(
1769                    "webview already exists and callback registrations are immutable: key={} options={:?}",
1770                    webtag.key(),
1771                    existing_webview.effective_options()
1772                )),
1773            );
1774            log::warn!(
1775                "Rejected recreate with callbacks for existing webview key={} options={:?}",
1776                webtag.key(),
1777                existing_webview.effective_options()
1778            );
1779            return;
1780        }
1781
1782        log::info!("WebView already exists, reusing: {}", webtag.key());
1783        sender.succeed(existing_webview.clone());
1784        return;
1785    }
1786
1787    // Drop stale pending callbacks from previously failed create attempts.
1788    clear_pending_callbacks(webtag);
1789
1790    // Stash pending callbacks for install during register_webview()
1791    if pending_callbacks.has_any() {
1792        let pending = PENDING_CALLBACKS.get_or_init(|| Mutex::new(HashMap::new()));
1793        if let Ok(mut map) = pending.lock() {
1794            map.insert(webtag.key().to_string(), pending_callbacks);
1795        }
1796    }
1797
1798    // Delegate WebView creation to the platform-specific implementation
1799    WebViewInner::create(
1800        &appid,
1801        &webtag.key_path(),
1802        webtag.session_id(),
1803        effective_options,
1804        sender,
1805    );
1806}
1807
1808fn create_webview_session(webtag: WebTag, options: WebViewCreateOptions) -> WebViewSession {
1809    let signals = WebViewSessionSignals::new();
1810    let session = signals.subscribe(webtag.clone());
1811    let sender = WebViewCreateSender::new(signals.clone());
1812    replace_session_signals(&webtag, signals);
1813    request_create_webview(&webtag, sender, options);
1814    session
1815}
1816
1817pub(crate) fn register_webview(webview: Arc<WebView>) {
1818    let webtag = webview.webtag();
1819
1820    // Install any pending callbacks
1821    if let Some(pending) = PENDING_CALLBACKS.get()
1822        && let Ok(mut map) = pending.lock()
1823        && let Some(callbacks) = map.remove(webtag.key())
1824    {
1825        log::info!(
1826            "Installing callbacks for {} (schemes={}, nav={}, new_window={}, download={}, file_chooser={}, delegate={})",
1827            webtag.key(),
1828            callbacks.scheme_handlers.len(),
1829            callbacks.navigation_handler.is_some(),
1830            callbacks.new_window_handler.is_some(),
1831            callbacks.download_handler.is_some(),
1832            callbacks.file_chooser_handler.is_some(),
1833            callbacks.delegate.is_some()
1834        );
1835        webview.install_callbacks(callbacks);
1836    }
1837
1838    if let Some(instances) = WEBVIEW_INSTANCES.get()
1839        && let Ok(mut webviews) = instances.lock()
1840    {
1841        webviews.insert(webtag.key().to_string(), webview.clone());
1842        log::info!("WebView created and stored: {}", webtag.key());
1843    }
1844}
1845
1846/// Find WebView by WebTag.
1847pub(crate) fn find_webview(webtag: &WebTag) -> Option<Arc<WebView>> {
1848    if let Some(instances) = WEBVIEW_INSTANCES.get() {
1849        if let Ok(webviews) = instances.lock() {
1850            webviews.get(webtag.key()).cloned()
1851        } else {
1852            None
1853        }
1854    } else {
1855        None
1856    }
1857}
1858
1859pub(crate) fn list_webviews() -> Vec<WebTag> {
1860    if let Some(instances) = WEBVIEW_INSTANCES.get()
1861        && let Ok(webviews) = instances.lock()
1862    {
1863        let mut tags: Vec<WebTag> = webviews.values().map(|webview| webview.webtag()).collect();
1864        tags.sort_by(|a, b| a.as_str().cmp(b.as_str()));
1865        return tags;
1866    }
1867    Vec::new()
1868}
1869
1870#[cfg(any(
1871    target_os = "android",
1872    target_os = "ios",
1873    target_os = "macos",
1874    target_os = "windows",
1875    all(target_os = "linux", target_env = "ohos")
1876))]
1877pub(crate) fn find_webview_delegate(webtag: &WebTag) -> Option<Arc<dyn WebViewDelegate>> {
1878    find_webview(webtag).and_then(|webview| webview.get_delegate())
1879}
1880
1881/// Destroy a WebView instance by WebTag and remove it from global storage
1882pub(crate) fn destroy_webview(webtag: &WebTag) {
1883    // Mark the session destroyed FIRST. If a native create is still in flight
1884    // (built on the main thread but not yet registered), it observes this via
1885    // `WebViewCreateSender::is_destroyed()` after registering and tears the
1886    // instance back down — so a destroy that races ahead of registration can't
1887    // leave a zombie in the global registry.
1888    if let Some(signals) = remove_session_signals(webtag) {
1889        signals.publish_destroyed();
1890    }
1891    let removed = if let Some(instances) = WEBVIEW_INSTANCES.get()
1892        && let Ok(mut webviews) = instances.lock()
1893    {
1894        webviews.remove(webtag.key())
1895    } else {
1896        None
1897    };
1898    if let Some(webview) = removed {
1899        webview.remove_delegate();
1900    }
1901    clear_pending_callbacks(webtag);
1902}