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