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 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 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 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 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 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#[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
210pub(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#[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#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
361pub(crate) struct EffectiveWebViewCreateOptions {
362 pub(crate) profile: SecurityProfile,
363 #[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 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 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 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 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
524pub 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 #[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 #[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 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 pub fn create(self) -> WebViewSession {
615 create_webview_session(self.webtag, self.options)
616 }
617}
618
619impl BrowserWebViewBuilder {
620 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 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 pub fn create(self) -> WebViewSession {
681 create_webview_session(self.webtag, self.options)
682 }
683}
684
685pub(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
707pub struct WebView {
709 pub(crate) inner: WebViewInner,
710 effective_options: EffectiveWebViewCreateOptions,
711 delegate: RwLock<Option<Arc<dyn WebViewDelegate>>>,
713 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 pub fn appid(&self) -> String {
740 self.inner.webtag.extract_appid()
741 }
742
743 pub fn path(&self) -> String {
745 self.inner.webtag.extract_parts().1
746 }
747
748 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 pub(crate) fn get_delegate(&self) -> Option<Arc<dyn WebViewDelegate>> {
759 self.delegate.read().ok().and_then(|guard| guard.clone())
760 }
761
762 pub(crate) fn remove_delegate(&self) {
764 if let Ok(mut guard) = self.delegate.write() {
765 *guard = None;
766 }
767 }
768
769 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 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 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 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 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 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 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 #[cfg(target_os = "macos")]
890 pub fn toggle_devtools(&self) {
891 self.inner.toggle_devtools();
892 }
893
894 #[cfg(target_os = "macos")]
896 pub fn toggle_devtools_detached(&self) {
897 self.inner.toggle_devtools_detached();
898 }
899
900 #[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 #[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 #[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
1254type 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
1451static WEBVIEW_INSTANCES: OnceLock<WebViewInstancesMap> = OnceLock::new();
1453
1454static 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
1494pub 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
1520pub 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
1556pub 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#[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 pub fn key(&self) -> &str {
1617 &self.0
1618 }
1619
1620 pub fn extract_appid(&self) -> String {
1622 self.0.split(':').next().unwrap_or("").to_string()
1623 }
1624
1625 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 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 let instances = WEBVIEW_INSTANCES.get_or_init(|| Arc::new(Mutex::new(HashMap::new())));
1690
1691 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 clear_pending_callbacks(webtag);
1735
1736 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 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 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
1792pub(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
1826pub(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}