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