1#![cfg_attr(
2 not(any(
3 target_os = "android",
4 target_os = "ios",
5 target_os = "macos",
6 target_os = "windows",
7 all(target_os = "linux", target_env = "ohos")
8 )),
9 allow(dead_code)
10)]
11
12use serde::{Deserialize, Serialize};
13use std::collections::{HashMap, HashSet};
14use std::future::Future;
15use std::pin::Pin;
16use std::sync::mpsc::{SyncSender, sync_channel};
17use std::sync::{Arc, Mutex, OnceLock, RwLock};
18use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
19use tokio::sync::watch;
20
21#[cfg(target_os = "android")]
22use crate::android::WebViewInner;
23
24#[cfg(any(target_os = "ios", target_os = "macos"))]
25use crate::apple::WebViewInner;
26
27#[cfg(all(target_os = "linux", target_env = "ohos"))]
28use crate::harmony::WebViewInner;
29
30#[cfg(target_os = "windows")]
31use crate::windows::WebViewInner;
32
33#[cfg(not(any(
34 target_os = "android",
35 target_os = "ios",
36 target_os = "macos",
37 target_os = "windows",
38 all(target_os = "linux", target_env = "ohos")
39)))]
40pub(crate) struct WebViewInner {
41 webtag: WebTag,
42}
43
44use crate::traits::{
45 AsyncSchemeHandler, ClickOptions, DownloadHandler, DownloadRequest, FileChooserRequest,
46 FileChooserResponse, FillOptions, NavigationHandler, NavigationPolicy, NavigationRequest,
47 NewWindowHandler, NewWindowPolicy, PressOptions, SchemeOutcome, ScrollOptions, TypeOptions,
48 UserAgentOverride, WebViewInputController,
49};
50use crate::{
51 ClearSiteDataOptions, ClearSiteDataResult, LoadDataRequest, NetworkCaptureSnapshot,
52 WebResourceResponse, WebViewController, WebViewCookie, WebViewCookieSetRequest,
53 WebViewDelegate, WebViewError, WebViewInputError, WebViewScriptError,
54};
55use async_trait::async_trait;
56
57const APPLE_INTERNAL_SCHEME: &str = "lx-apple";
58
59#[cfg(not(any(
60 target_os = "android",
61 target_os = "ios",
62 target_os = "macos",
63 target_os = "windows",
64 all(target_os = "linux", target_env = "ohos")
65)))]
66fn unsupported_webview_error(action: &str) -> WebViewError {
67 WebViewError::Unsupported(action.to_string())
68}
69
70#[cfg(not(any(
71 target_os = "android",
72 target_os = "ios",
73 target_os = "macos",
74 target_os = "windows",
75 all(target_os = "linux", target_env = "ohos")
76)))]
77impl WebViewInner {
78 pub(crate) fn create(
79 appid: &str,
80 path: &str,
81 session_id: Option<u64>,
82 _effective_options: EffectiveWebViewCreateOptions,
83 sender: WebViewCreateSender,
84 ) {
85 let _webtag = WebTag::new(appid, path, session_id);
86 sender.fail(
87 WebViewCreateStage::Requested,
88 unsupported_webview_error("webview creation"),
89 );
90 }
91}
92
93#[cfg(not(any(
94 target_os = "android",
95 target_os = "ios",
96 target_os = "macos",
97 target_os = "windows",
98 all(target_os = "linux", target_env = "ohos")
99)))]
100#[async_trait]
101impl WebViewController for WebViewInner {
102 fn load_url(&self, _url: &str) -> Result<(), WebViewError> {
103 Err(unsupported_webview_error("load_url"))
104 }
105
106 fn load_data(&self, _request: LoadDataRequest<'_>) -> Result<(), WebViewError> {
107 Err(unsupported_webview_error("load_data"))
108 }
109
110 fn exec_js(&self, _js: &str) -> Result<(), WebViewError> {
111 Err(unsupported_webview_error("exec_js"))
112 }
113
114 async fn eval_js(&self, _js: &str) -> Result<serde_json::Value, WebViewScriptError> {
115 Err(WebViewScriptError::Unsupported(
116 "JavaScript evaluation is not supported on this platform",
117 ))
118 }
119
120 fn post_message(&self, _message: &str) -> Result<(), WebViewError> {
121 Err(unsupported_webview_error("post_message"))
122 }
123
124 fn clear_browsing_data(&self) -> Result<(), WebViewError> {
125 Err(unsupported_webview_error("clear_browsing_data"))
126 }
127
128 fn set_user_agent_override(&self, _user_agent: UserAgentOverride) -> Result<(), WebViewError> {
129 Err(unsupported_webview_error("set_user_agent_override"))
130 }
131}
132
133fn lock_or_recover<'a, T>(mutex: &'a Mutex<T>, name: &str) -> std::sync::MutexGuard<'a, T> {
134 match mutex.lock() {
135 Ok(guard) => guard,
136 Err(poisoned) => {
137 log::error!("Mutex poisoned at {}, recovering inner value", name);
138 poisoned.into_inner()
139 }
140 }
141}
142
143fn scheme_waker_from_sender(sender: SyncSender<()>) -> Waker {
144 unsafe { Waker::from_raw(scheme_raw_waker(Arc::new(sender))) }
146}
147
148fn scheme_raw_waker(sender: Arc<SyncSender<()>>) -> RawWaker {
149 RawWaker::new(Arc::into_raw(sender) as *const (), &SCHEME_WAKER_VTABLE)
150}
151
152unsafe fn scheme_waker_clone(data: *const ()) -> RawWaker {
153 let arc = unsafe { Arc::<SyncSender<()>>::from_raw(data as *const SyncSender<()>) };
155 let cloned = Arc::clone(&arc);
156 let _ = Arc::into_raw(arc);
157 scheme_raw_waker(cloned)
158}
159
160unsafe fn scheme_waker_wake(data: *const ()) {
161 let arc = unsafe { Arc::<SyncSender<()>>::from_raw(data as *const SyncSender<()>) };
163 let _ = arc.try_send(());
164}
165
166unsafe fn scheme_waker_wake_by_ref(data: *const ()) {
167 let arc = unsafe { Arc::<SyncSender<()>>::from_raw(data as *const SyncSender<()>) };
169 let _ = arc.try_send(());
170 let _ = Arc::into_raw(arc);
171}
172
173unsafe fn scheme_waker_drop(data: *const ()) {
174 let _ = unsafe { Arc::<SyncSender<()>>::from_raw(data as *const SyncSender<()>) };
176}
177
178static SCHEME_WAKER_VTABLE: RawWakerVTable = RawWakerVTable::new(
179 scheme_waker_clone,
180 scheme_waker_wake,
181 scheme_waker_wake_by_ref,
182 scheme_waker_drop,
183);
184
185fn block_on_scheme_future<F>(future: F) -> F::Output
186where
187 F: Future,
188{
189 let (tx, rx) = sync_channel::<()>(1);
190 let waker = scheme_waker_from_sender(tx);
191 let mut context = Context::from_waker(&waker);
192 let mut future = Box::pin(future);
193
194 loop {
195 match Pin::as_mut(&mut future).poll(&mut context) {
196 Poll::Ready(value) => return value,
197 Poll::Pending => {
198 if rx.recv().is_err() {
199 std::thread::yield_now();
200 }
201 }
202 }
203 }
204}
205
206#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
208#[serde(rename_all = "snake_case")]
209pub(crate) enum SecurityProfile {
210 StrictDefault,
211 BrowserRelaxed,
212}
213
214#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
219#[serde(rename_all = "snake_case")]
220pub enum WebViewDataMode {
221 #[default]
224 ProfileDefault,
225 Ephemeral,
228}
229
230pub(crate) type FileChooserFuture =
231 Pin<Box<dyn Future<Output = FileChooserResponse> + Send + 'static>>;
232pub(crate) type FileChooserHandler =
233 Box<dyn Fn(FileChooserRequest) -> FileChooserFuture + Send + Sync>;
234
235pub(crate) struct WebViewCreateOptions {
237 pub(crate) profile: SecurityProfile,
238 pub(crate) data_mode: WebViewDataMode,
239 pub(crate) scheme_handlers: HashMap<String, AsyncSchemeHandler>,
240 pub(crate) navigation_handler: Option<NavigationHandler>,
241 pub(crate) new_window_handler: Option<NewWindowHandler>,
242 pub(crate) download_handler: Option<DownloadHandler>,
243 pub(crate) file_chooser_handler: Option<FileChooserHandler>,
244 pub(crate) delegate: Option<Arc<dyn WebViewDelegate>>,
245 pub(crate) surface_owned: bool,
248}
249
250impl std::fmt::Debug for WebViewCreateOptions {
251 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
252 f.debug_struct("WebViewCreateOptions")
253 .field("profile", &self.profile)
254 .field("data_mode", &self.data_mode)
255 .field(
256 "scheme_handlers",
257 &self.scheme_handlers.keys().collect::<Vec<_>>(),
258 )
259 .field("has_navigation_handler", &self.navigation_handler.is_some())
260 .field("has_new_window_handler", &self.new_window_handler.is_some())
261 .field("has_download_handler", &self.download_handler.is_some())
262 .field(
263 "has_file_chooser_handler",
264 &self.file_chooser_handler.is_some(),
265 )
266 .field("has_delegate", &self.delegate.is_some())
267 .finish()
268 }
269}
270
271#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
273pub struct ProxyConfig {
274 pub host: String,
275 pub port: u16,
276 #[serde(default)]
277 pub bypass: Vec<String>,
278}
279
280impl ProxyConfig {
281 pub fn new(host: impl Into<String>, port: u16) -> Result<Self, WebViewError> {
282 let cfg = Self {
283 host: host.into(),
284 port,
285 bypass: Vec::new(),
286 };
287 cfg.validate()
288 }
289
290 pub fn with_bypass<I, S>(mut self, bypass: I) -> Self
291 where
292 I: IntoIterator<Item = S>,
293 S: Into<String>,
294 {
295 self.bypass = bypass.into_iter().map(Into::into).collect();
296 self
297 }
298
299 fn validate(self) -> Result<Self, WebViewError> {
300 let host = self.host.trim().to_string();
301 if host.is_empty() {
302 return Err(WebViewError::InvalidCreateOptions(
303 "proxy host cannot be empty".to_string(),
304 ));
305 }
306 if host.contains(char::is_whitespace) {
307 return Err(WebViewError::InvalidCreateOptions(
308 "proxy host cannot contain whitespace".to_string(),
309 ));
310 }
311 if self.port == 0 {
312 return Err(WebViewError::InvalidCreateOptions(
313 "proxy port must be greater than 0".to_string(),
314 ));
315 }
316
317 let mut seen = HashSet::new();
318 let mut bypass = Vec::new();
319 for raw in self.bypass {
320 let rule = raw.trim();
321 if rule.is_empty() {
322 continue;
323 }
324 let key = rule.to_ascii_lowercase();
325 if seen.insert(key) {
326 bypass.push(rule.to_string());
327 }
328 }
329
330 Ok(Self {
331 host,
332 bypass,
333 ..self
334 })
335 }
336}
337
338#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
339#[serde(rename_all = "snake_case")]
340pub enum ProxyApplyStatus {
341 Applied,
342 Cleared,
343 Unsupported,
344}
345
346#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
347#[serde(rename_all = "snake_case")]
348pub enum ProxyActivation {
349 EffectiveNow,
350 NewWebViewsOnly,
351 EngineRecreateRequired,
352 NotApplied,
353}
354
355#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
356pub struct ProxyApplyReport {
357 pub status: ProxyApplyStatus,
358 pub activation: ProxyActivation,
359 #[serde(default, skip_serializing_if = "Option::is_none")]
360 pub detail: Option<String>,
361}
362
363impl ProxyApplyReport {
364 pub fn applied(activation: ProxyActivation) -> Self {
365 Self {
366 status: ProxyApplyStatus::Applied,
367 activation,
368 detail: None,
369 }
370 }
371
372 pub fn cleared(activation: ProxyActivation) -> Self {
373 Self {
374 status: ProxyApplyStatus::Cleared,
375 activation,
376 detail: None,
377 }
378 }
379
380 pub fn unsupported(detail: impl Into<String>) -> Self {
381 Self {
382 status: ProxyApplyStatus::Unsupported,
383 activation: ProxyActivation::NotApplied,
384 detail: Some(detail.into()),
385 }
386 }
387}
388
389#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
391pub(crate) struct EffectiveWebViewCreateOptions {
392 pub(crate) profile: SecurityProfile,
393 #[serde(default)]
395 pub(crate) data_mode: WebViewDataMode,
396 #[serde(default)]
398 pub(crate) registered_schemes: Vec<String>,
399 #[serde(default)]
400 pub(crate) has_navigation_handler: bool,
401 #[serde(default)]
402 pub(crate) has_new_window_handler: bool,
403 #[serde(default)]
404 pub(crate) has_download_handler: bool,
405 #[serde(default)]
406 pub(crate) has_file_chooser_handler: bool,
407 #[serde(default)]
408 pub(crate) has_delegate: bool,
409 #[serde(default)]
410 pub(crate) surface_owned: bool,
411}
412
413impl Default for WebViewCreateOptions {
414 fn default() -> Self {
415 Self::strict()
416 }
417}
418
419impl WebViewCreateOptions {
420 fn strict() -> Self {
421 Self {
422 profile: SecurityProfile::StrictDefault,
423 data_mode: WebViewDataMode::ProfileDefault,
424 scheme_handlers: HashMap::new(),
425 navigation_handler: None,
426 new_window_handler: None,
427 download_handler: None,
428 file_chooser_handler: None,
429 delegate: None,
430 surface_owned: false,
431 }
432 }
433
434 fn browser() -> Self {
435 Self {
436 profile: SecurityProfile::BrowserRelaxed,
437 data_mode: WebViewDataMode::ProfileDefault,
438 scheme_handlers: HashMap::new(),
439 navigation_handler: None,
440 new_window_handler: None,
441 download_handler: None,
442 file_chooser_handler: None,
443 delegate: None,
444 surface_owned: false,
445 }
446 }
447
448 fn on_scheme<F, Fut>(mut self, scheme: &str, handler: F) -> Self
458 where
459 F: Fn(http::Request<Vec<u8>>) -> Fut + Send + Sync + 'static,
460 Fut: std::future::Future<Output = SchemeOutcome> + Send + 'static,
461 {
462 let normalized = scheme.trim().to_ascii_lowercase();
463 if !normalized.is_empty() {
464 self.scheme_handlers.insert(
465 normalized,
466 Arc::new(move |req| {
467 let fut = handler(req);
468 Box::pin(fut)
469 }),
470 );
471 }
472 self
473 }
474
475 fn on_navigation<F>(mut self, handler: F) -> Self
478 where
479 F: Fn(&NavigationRequest) -> NavigationPolicy + Send + Sync + 'static,
480 {
481 self.navigation_handler = Some(Box::new(handler));
482 self
483 }
484
485 fn on_new_window<F>(mut self, handler: F) -> Self
488 where
489 F: Fn(&str) -> NewWindowPolicy + Send + Sync + 'static,
490 {
491 self.new_window_handler = Some(Box::new(handler));
492 self
493 }
494
495 fn on_download<F>(mut self, handler: F) -> Self
505 where
506 F: Fn(DownloadRequest) + Send + Sync + 'static,
507 {
508 self.download_handler = Some(Box::new(handler));
509 self
510 }
511
512 fn on_file_chooser<F, Fut>(mut self, handler: F) -> Self
513 where
514 F: Fn(FileChooserRequest) -> Fut + Send + Sync + 'static,
515 Fut: Future<Output = FileChooserResponse> + Send + 'static,
516 {
517 self.file_chooser_handler = Some(Box::new(move |request| Box::pin(handler(request))));
518 self
519 }
520
521 fn delegate(mut self, delegate: Arc<dyn WebViewDelegate>) -> Self {
522 self.delegate = Some(delegate);
523 self
524 }
525
526 fn data_mode(mut self, data_mode: WebViewDataMode) -> Self {
527 self.data_mode = data_mode;
528 self
529 }
530
531 fn surface_owned(mut self, surface_owned: bool) -> Self {
532 self.surface_owned = surface_owned;
533 self
534 }
535
536 pub(crate) fn normalize(
537 self,
538 ) -> Result<(EffectiveWebViewCreateOptions, PendingCallbacks), WebViewError> {
539 if self.profile != SecurityProfile::BrowserRelaxed && self.download_handler.is_some() {
540 return Err(WebViewError::InvalidCreateOptions(
541 "download callback is only supported in browser profile; use WebViewBuilder::browser(webtag).on_download(...).create()".to_string(),
542 ));
543 }
544 if self.scheme_handlers.contains_key(APPLE_INTERNAL_SCHEME) {
545 return Err(WebViewError::InvalidCreateOptions(format!(
546 "scheme '{APPLE_INTERNAL_SCHEME}' is reserved for LingXia Apple bridge transport"
547 )));
548 }
549 let mut registered_schemes: Vec<String> = self.scheme_handlers.keys().cloned().collect();
550 registered_schemes.sort_unstable();
551 registered_schemes.dedup();
552 let effective = EffectiveWebViewCreateOptions {
553 profile: self.profile,
554 data_mode: self.data_mode,
555 registered_schemes,
556 has_navigation_handler: self.navigation_handler.is_some(),
557 has_new_window_handler: self.new_window_handler.is_some(),
558 has_download_handler: self.download_handler.is_some(),
559 has_file_chooser_handler: self.file_chooser_handler.is_some(),
560 has_delegate: self.delegate.is_some(),
561 surface_owned: self.surface_owned,
562 };
563 let pending = PendingCallbacks {
564 scheme_handlers: self.scheme_handlers,
565 navigation_handler: self.navigation_handler,
566 new_window_handler: self.new_window_handler,
567 download_handler: self.download_handler,
568 file_chooser_handler: self.file_chooser_handler,
569 delegate: self.delegate,
570 };
571 Ok((effective, pending))
572 }
573}
574
575pub struct WebViewBuilder;
583
584#[must_use = "call .create() to start WebView creation"]
585pub struct StrictWebViewBuilder {
586 webtag: WebTag,
587 options: WebViewCreateOptions,
588}
589
590#[must_use = "call .create() to start WebView creation"]
591pub struct BrowserWebViewBuilder {
592 webtag: WebTag,
593 options: WebViewCreateOptions,
594}
595
596impl WebViewBuilder {
597 #[must_use = "call .create() to start WebView creation"]
599 pub fn strict(webtag: WebTag) -> StrictWebViewBuilder {
600 StrictWebViewBuilder {
601 webtag,
602 options: WebViewCreateOptions::strict(),
603 }
604 }
605
606 #[must_use = "call .create() to start WebView creation"]
608 pub fn browser(webtag: WebTag) -> BrowserWebViewBuilder {
609 BrowserWebViewBuilder {
610 webtag,
611 options: WebViewCreateOptions::browser(),
612 }
613 }
614}
615
616impl StrictWebViewBuilder {
617 pub fn delegate(mut self, delegate: Arc<dyn WebViewDelegate>) -> Self {
621 self.options = self.options.delegate(delegate);
622 self
623 }
624
625 pub fn data_mode(mut self, data_mode: WebViewDataMode) -> Self {
627 self.options = self.options.data_mode(data_mode);
628 self
629 }
630
631 pub fn surface_owned(mut self, surface_owned: bool) -> Self {
634 self.options = self.options.surface_owned(surface_owned);
635 self
636 }
637
638 pub fn on_scheme<F, Fut>(mut self, scheme: &str, handler: F) -> Self
639 where
640 F: Fn(http::Request<Vec<u8>>) -> Fut + Send + Sync + 'static,
641 Fut: std::future::Future<Output = SchemeOutcome> + Send + 'static,
642 {
643 self.options = self.options.on_scheme(scheme, handler);
644 self
645 }
646
647 pub fn on_navigation<F>(mut self, handler: F) -> Self
648 where
649 F: Fn(&NavigationRequest) -> NavigationPolicy + Send + Sync + 'static,
650 {
651 self.options = self.options.on_navigation(handler);
652 self
653 }
654
655 pub fn on_new_window<F>(mut self, handler: F) -> Self
656 where
657 F: Fn(&str) -> NewWindowPolicy + Send + Sync + 'static,
658 {
659 self.options = self.options.on_new_window(handler);
660 self
661 }
662
663 pub fn on_file_chooser<F, Fut>(mut self, handler: F) -> Self
664 where
665 F: Fn(FileChooserRequest) -> Fut + Send + Sync + 'static,
666 Fut: Future<Output = FileChooserResponse> + Send + 'static,
667 {
668 self.options = self.options.on_file_chooser(handler);
669 self
670 }
671
672 pub fn create(self) -> WebViewSession {
679 create_webview_session(self.webtag, self.options)
680 }
681}
682
683impl BrowserWebViewBuilder {
684 pub fn delegate(mut self, delegate: Arc<dyn WebViewDelegate>) -> Self {
688 self.options = self.options.delegate(delegate);
689 self
690 }
691
692 pub fn on_scheme<F, Fut>(mut self, scheme: &str, handler: F) -> Self
693 where
694 F: Fn(http::Request<Vec<u8>>) -> Fut + Send + Sync + 'static,
695 Fut: std::future::Future<Output = SchemeOutcome> + Send + 'static,
696 {
697 self.options = self.options.on_scheme(scheme, handler);
698 self
699 }
700
701 pub fn on_navigation<F>(mut self, handler: F) -> Self
702 where
703 F: Fn(&NavigationRequest) -> NavigationPolicy + Send + Sync + 'static,
704 {
705 self.options = self.options.on_navigation(handler);
706 self
707 }
708
709 pub fn on_new_window<F>(mut self, handler: F) -> Self
710 where
711 F: Fn(&str) -> NewWindowPolicy + Send + Sync + 'static,
712 {
713 self.options = self.options.on_new_window(handler);
714 self
715 }
716
717 pub fn on_download<F>(mut self, handler: F) -> Self
722 where
723 F: Fn(DownloadRequest) + Send + Sync + 'static,
724 {
725 self.options = self.options.on_download(handler);
726 self
727 }
728
729 pub fn data_mode(mut self, data_mode: WebViewDataMode) -> Self {
731 self.options = self.options.data_mode(data_mode);
732 self
733 }
734
735 pub fn on_file_chooser<F, Fut>(mut self, handler: F) -> Self
736 where
737 F: Fn(FileChooserRequest) -> Fut + Send + Sync + 'static,
738 Fut: Future<Output = FileChooserResponse> + Send + 'static,
739 {
740 self.options = self.options.on_file_chooser(handler);
741 self
742 }
743
744 pub fn create(self) -> WebViewSession {
751 create_webview_session(self.webtag, self.options)
752 }
753}
754
755pub(crate) struct PendingCallbacks {
758 pub(crate) scheme_handlers: HashMap<String, AsyncSchemeHandler>,
759 pub(crate) navigation_handler: Option<NavigationHandler>,
760 pub(crate) new_window_handler: Option<NewWindowHandler>,
761 pub(crate) download_handler: Option<DownloadHandler>,
762 pub(crate) file_chooser_handler: Option<FileChooserHandler>,
763 pub(crate) delegate: Option<Arc<dyn WebViewDelegate>>,
764}
765
766impl PendingCallbacks {
767 fn has_any(&self) -> bool {
768 !self.scheme_handlers.is_empty()
769 || self.navigation_handler.is_some()
770 || self.new_window_handler.is_some()
771 || self.download_handler.is_some()
772 || self.file_chooser_handler.is_some()
773 || self.delegate.is_some()
774 }
775}
776
777pub struct WebView {
779 pub(crate) inner: WebViewInner,
780 effective_options: EffectiveWebViewCreateOptions,
781 delegate: RwLock<Option<Arc<dyn WebViewDelegate>>>,
783 scheme_handlers: RwLock<HashMap<String, AsyncSchemeHandler>>,
785 navigation_handler: RwLock<Option<NavigationHandler>>,
786 new_window_handler: RwLock<Option<NewWindowHandler>>,
787 download_handler: RwLock<Option<DownloadHandler>>,
788 file_chooser_handler: RwLock<Option<FileChooserHandler>>,
789}
790
791impl WebView {
792 pub(crate) fn new(
793 inner: WebViewInner,
794 effective_options: EffectiveWebViewCreateOptions,
795 ) -> Self {
796 Self {
797 inner,
798 effective_options,
799 delegate: RwLock::new(None),
800 scheme_handlers: RwLock::new(HashMap::new()),
801 navigation_handler: RwLock::new(None),
802 new_window_handler: RwLock::new(None),
803 download_handler: RwLock::new(None),
804 file_chooser_handler: RwLock::new(None),
805 }
806 }
807
808 pub fn appid(&self) -> String {
810 self.inner.webtag.extract_appid()
811 }
812
813 pub fn path(&self) -> String {
815 self.inner.webtag.extract_parts().1
816 }
817
818 pub fn webtag(&self) -> WebTag {
820 self.inner.webtag.clone()
821 }
822
823 pub(crate) fn effective_options(&self) -> &EffectiveWebViewCreateOptions {
824 &self.effective_options
825 }
826
827 pub(crate) fn get_delegate(&self) -> Option<Arc<dyn WebViewDelegate>> {
829 self.delegate.read().ok().and_then(|guard| guard.clone())
830 }
831
832 pub(crate) fn remove_delegate(&self) {
834 if let Ok(mut guard) = self.delegate.write() {
835 *guard = None;
836 }
837 }
838
839 pub(crate) fn install_callbacks(&self, callbacks: PendingCallbacks) {
841 if let Some(delegate) = callbacks.delegate
842 && let Ok(mut guard) = self.delegate.write()
843 {
844 *guard = Some(delegate);
845 }
846 if let Ok(mut guard) = self.scheme_handlers.write() {
847 *guard = callbacks.scheme_handlers;
848 }
849 if let Some(handler) = callbacks.navigation_handler
850 && let Ok(mut guard) = self.navigation_handler.write()
851 {
852 *guard = Some(handler);
853 }
854 if let Some(handler) = callbacks.new_window_handler
855 && let Ok(mut guard) = self.new_window_handler.write()
856 {
857 *guard = Some(handler);
858 }
859 if let Some(handler) = callbacks.download_handler
860 && let Ok(mut guard) = self.download_handler.write()
861 {
862 *guard = Some(handler);
863 }
864 if let Some(handler) = callbacks.file_chooser_handler
865 && let Ok(mut guard) = self.file_chooser_handler.write()
866 {
867 *guard = Some(handler);
868 }
869 }
870
871 pub fn has_scheme_handler(&self, scheme: &str) -> bool {
873 self.scheme_handlers
874 .read()
875 .ok()
876 .is_some_and(|guard| guard.contains_key(scheme))
877 }
878
879 pub(crate) fn handle_scheme_request(
882 &self,
883 scheme: &str,
884 request: http::Request<Vec<u8>>,
885 ) -> Option<WebResourceResponse> {
886 #[cfg(any(target_os = "ios", target_os = "macos"))]
887 if let Some(response) = self.inner.handle_internal_bridge_request(&request) {
888 return Some(response);
889 }
890
891 let guard = self.scheme_handlers.read().ok()?;
892 let handler = guard.get(scheme)?;
893 let outcome = block_on_scheme_future(handler(request));
894 match outcome {
895 SchemeOutcome::Handled(response) => Some(response),
896 SchemeOutcome::PassThrough => None,
897 }
898 }
899
900 pub fn handle_navigation(&self, request: &NavigationRequest) -> NavigationPolicy {
905 if crate::url_callback::dispatch(&request.url) {
906 return NavigationPolicy::Cancel;
907 }
908 if let Ok(guard) = self.navigation_handler.read()
909 && let Some(handler) = guard.as_ref()
910 {
911 return handler(request);
912 }
913 NavigationPolicy::Allow
914 }
915
916 pub fn has_new_window_handler(&self) -> bool {
918 self.new_window_handler
919 .read()
920 .ok()
921 .is_some_and(|guard| guard.is_some())
922 }
923
924 pub fn handle_new_window(&self, url: &str) -> NewWindowPolicy {
929 if crate::url_callback::dispatch(url) {
930 return NewWindowPolicy::Cancel;
931 }
932 if let Ok(guard) = self.new_window_handler.read()
933 && let Some(handler) = guard.as_ref()
934 {
935 return handler(url);
936 }
937 NewWindowPolicy::Cancel
938 }
939
940 pub(crate) fn handle_download(&self, request: DownloadRequest) {
942 if let Ok(guard) = self.download_handler.read()
943 && let Some(handler) = guard.as_ref()
944 {
945 handler(request);
946 }
947 }
948
949 #[cfg_attr(not(target_os = "windows"), allow(dead_code))]
951 pub(crate) fn has_download_handler(&self) -> bool {
952 self.download_handler
953 .read()
954 .ok()
955 .is_some_and(|guard| guard.is_some())
956 }
957
958 #[cfg_attr(target_os = "windows", allow(dead_code))]
959 pub(crate) fn handle_file_chooser<C>(&self, request: FileChooserRequest, completion: C) -> bool
960 where
961 C: FnOnce(FileChooserResponse) + Send + 'static,
962 {
963 let Some(future) = self.make_file_chooser_future(request) else {
964 return false;
965 };
966 std::thread::spawn(move || {
967 completion(block_on_scheme_future(future));
968 });
969 true
970 }
971
972 #[cfg_attr(target_os = "windows", allow(dead_code))]
973 fn make_file_chooser_future(&self, request: FileChooserRequest) -> Option<FileChooserFuture> {
974 let Ok(guard) = self.file_chooser_handler.read() else {
975 return None;
976 };
977 let handler = guard.as_ref()?;
978 Some(handler(request))
979 }
980
981 #[cfg(target_os = "macos")]
983 pub fn toggle_devtools(&self) {
984 self.inner.toggle_devtools();
985 }
986
987 #[cfg(target_os = "macos")]
989 pub fn toggle_devtools_detached(&self) {
990 self.inner.toggle_devtools_detached();
991 }
992
993 #[cfg(any(target_os = "ios", target_os = "macos"))]
995 pub fn get_swift_webview_ptr(&self) -> usize {
996 self.inner.get_swift_webview_ptr()
997 }
998
999 #[cfg(target_os = "android")]
1001 pub fn get_java_webview(&self) -> &jni::objects::Global<jni::objects::JObject<'static>> {
1002 self.inner.get_java_webview()
1003 }
1004
1005 pub async fn evaluate_javascript(
1006 &self,
1007 js: &str,
1008 ) -> Result<serde_json::Value, crate::WebViewScriptError> {
1009 self.inner.eval_js(js).await
1010 }
1011
1012 #[cfg(any(
1020 target_os = "ios",
1021 target_os = "android",
1022 all(feature = "webview-input", target_os = "macos"),
1023 all(target_os = "linux", target_env = "ohos")
1024 ))]
1025 async fn run_js_action(&self, script: &str) -> Result<(), WebViewInputError> {
1026 let result = self
1027 .inner
1028 .eval_js(script)
1029 .await
1030 .map_err(WebViewInputError::Script)?;
1031 if result.get("ok").and_then(|v| v.as_bool()) == Some(true) {
1032 return Ok(());
1033 }
1034 let err_msg = result
1035 .get("error")
1036 .and_then(|v| v.as_str())
1037 .unwrap_or("input action failed")
1038 .to_string();
1039 if result.get("interactable").and_then(|v| v.as_bool()) == Some(false) {
1040 Err(WebViewInputError::ElementNotInteractable(err_msg))
1041 } else {
1042 Err(WebViewInputError::ElementNotFound(err_msg))
1043 }
1044 }
1045
1046 #[cfg(any(
1052 target_os = "ios",
1053 all(feature = "webview-input", target_os = "macos"),
1054 all(target_os = "linux", target_env = "ohos")
1055 ))]
1056 pub(crate) async fn click_via_js(
1057 &self,
1058 selector: &str,
1059 index: Option<usize>,
1060 ) -> Result<(), WebViewInputError> {
1061 let selector_json = serde_json::to_string(selector)
1062 .map_err(|err| WebViewInputError::Platform(format!("Invalid selector: {err}")))?;
1063 let idx = index.unwrap_or(0);
1064 let script = format!(
1065 "((sel, i) => {{ \
1066 const els = document.querySelectorAll(sel); \
1067 if (!els.length || i < 0 || i >= els.length) return {{ ok:false, error:'no match', count:els.length }}; \
1068 const el = els[i]; \
1069 try {{ el.scrollIntoView({{block:'center', inline:'center'}}); }} catch(_e) {{}} \
1070 const rect = el.getBoundingClientRect(); \
1071 const style = window.getComputedStyle(el); \
1072 const disabled = !!el.disabled || el.getAttribute('aria-disabled') === 'true'; \
1073 const visible = rect.width > 0 && rect.height > 0 && rect.bottom > 0 && rect.right > 0 && \
1074 rect.top < window.innerHeight && rect.left < window.innerWidth && \
1075 style.visibility !== 'hidden' && style.display !== 'none' && Number(style.opacity || '1') !== 0; \
1076 if (!visible) return {{ ok:false, error:'not visible', interactable:false, count:els.length }}; \
1077 if (disabled) return {{ ok:false, error:'not enabled', interactable:false, count:els.length }}; \
1078 const tag = (el.tagName || '').toLowerCase(); \
1079 if (tag.indexOf('lx-') === 0) {{ \
1080 el.setAttribute('focus', 'true'); \
1081 if (typeof el.syncNativeProps === 'function') {{ try {{ el.syncNativeProps(); }} catch(_e) {{}} }} \
1082 return {{ ok:true, count:els.length, native:true }}; \
1083 }} \
1084 if (typeof el.focus === 'function') {{ try {{ el.focus({{preventScroll:true}}); }} catch(_e) {{ try {{ el.focus(); }} catch(__){{}} }} }} \
1085 const opts = {{ bubbles:true, cancelable:true, view:window, clientX: rect.left + rect.width/2, clientY: rect.top + rect.height/2 }}; \
1086 try {{ if (window.PointerEvent) el.dispatchEvent(new PointerEvent('pointerdown', Object.assign({{pointerId:1, isPrimary:true, pointerType:'mouse'}}, opts))); }} catch(_e) {{}} \
1087 try {{ el.dispatchEvent(new MouseEvent('mousedown', opts)); }} catch(_e) {{}} \
1088 try {{ if (window.PointerEvent) el.dispatchEvent(new PointerEvent('pointerup', Object.assign({{pointerId:1, isPrimary:true, pointerType:'mouse'}}, opts))); }} catch(_e) {{}} \
1089 try {{ el.dispatchEvent(new MouseEvent('mouseup', opts)); }} catch(_e) {{}} \
1090 try {{ el.dispatchEvent(new MouseEvent('click', opts)); }} catch(_e) {{}} \
1091 return {{ ok:true, count:els.length }}; \
1092 }})({selector_json}, {idx})"
1093 );
1094 self.run_js_action(&script).await
1095 }
1096
1097 #[cfg(any(
1101 target_os = "ios",
1102 target_os = "android",
1103 all(feature = "webview-input", target_os = "macos"),
1104 all(target_os = "linux", target_env = "ohos")
1105 ))]
1106 pub(crate) async fn type_via_js(
1107 &self,
1108 selector: &str,
1109 index: Option<usize>,
1110 text: &str,
1111 replace: bool,
1112 ) -> Result<(), WebViewInputError> {
1113 let selector_json = serde_json::to_string(selector)
1114 .map_err(|err| WebViewInputError::Platform(format!("Invalid selector: {err}")))?;
1115 let text_json = serde_json::to_string(text)
1116 .map_err(|err| WebViewInputError::Platform(format!("Invalid text: {err}")))?;
1117 let idx = index.unwrap_or(0);
1118 let script = format!(
1119 "((sel, i, text, replace) => {{ \
1120 const els = document.querySelectorAll(sel); \
1121 if (!els.length || i < 0 || i >= els.length) return {{ ok:false, error:'no match', count:els.length }}; \
1122 const el = els[i]; \
1123 try {{ el.scrollIntoView({{block:'center', inline:'center'}}); }} catch(_e) {{}} \
1124 if (typeof el.focus === 'function') {{ try {{ el.focus({{preventScroll:true}}); }} catch(_e) {{ try {{ el.focus(); }} catch(__){{}} }} }} \
1125 const tag = (el.tagName || '').toLowerCase(); \
1126 if (tag === 'input' || tag === 'textarea') {{ \
1127 const proto = tag === 'textarea' ? window.HTMLTextAreaElement.prototype : window.HTMLInputElement.prototype; \
1128 const desc = Object.getOwnPropertyDescriptor(proto, 'value'); \
1129 const next = (replace ? '' : (el.value || '')) + text; \
1130 if (desc && desc.set) {{ desc.set.call(el, next); }} else {{ el.value = next; }} \
1131 el.dispatchEvent(new InputEvent('input', {{ bubbles:true, cancelable:true, data:text, inputType:'insertText' }})); \
1132 el.dispatchEvent(new Event('change', {{ bubbles:true }})); \
1133 return {{ ok:true, count:els.length }}; \
1134 }} \
1135 if (el.isContentEditable) {{ \
1136 el.textContent = (replace ? '' : (el.textContent || '')) + text; \
1137 el.dispatchEvent(new InputEvent('input', {{ bubbles:true, data:text, inputType:'insertText' }})); \
1138 return {{ ok:true, count:els.length }}; \
1139 }} \
1140 if (tag.indexOf('lx-') === 0) {{ \
1141 try {{ el.value = (replace ? '' : (el.value || '')) + text; }} catch(_e) {{}} \
1142 if (typeof el.syncNativeProps === 'function') {{ try {{ el.syncNativeProps(); }} catch(_e) {{}} }} \
1143 el.dispatchEvent(new Event('input', {{ bubbles:true }})); \
1144 return {{ ok:true, count:els.length, native:true }}; \
1145 }} \
1146 return {{ ok:false, error:'not editable', interactable:false, count:els.length }}; \
1147 }})({selector_json}, {idx}, {text_json}, {replace})"
1148 );
1149 self.run_js_action(&script).await
1150 }
1151
1152 #[cfg(any(
1154 target_os = "ios",
1155 target_os = "android",
1156 all(feature = "webview-input", target_os = "macos"),
1157 all(target_os = "linux", target_env = "ohos")
1158 ))]
1159 pub(crate) async fn press_via_js(
1160 &self,
1161 key: &str,
1162 selector: Option<&str>,
1163 index: Option<usize>,
1164 ) -> Result<(), WebViewInputError> {
1165 let key_json = serde_json::to_string(key)
1166 .map_err(|err| WebViewInputError::Platform(format!("Invalid key: {err}")))?;
1167 let selector_json = serde_json::to_string(&selector)
1168 .map_err(|err| WebViewInputError::Platform(format!("Invalid selector: {err}")))?;
1169 let idx = index.unwrap_or(0);
1170 let script = format!(
1171 "((key, sel, i) => {{ \
1172 const els = sel === null ? null : document.querySelectorAll(sel); \
1173 if (els && (!els.length || i < 0 || i >= els.length)) return {{ ok:false, error:'no match', count:els.length }}; \
1174 const el = els ? els[i] : (document.activeElement || document.body); \
1175 if (els) {{ \
1176 try {{ el.scrollIntoView({{block:'center', inline:'center'}}); }} catch(_e) {{}} \
1177 if (typeof el.focus === 'function') {{ try {{ el.focus({{preventScroll:true}}); }} catch(_e) {{ try {{ el.focus(); }} catch(__){{}} }} }} \
1178 }} \
1179 const map = {{ enter:'Enter', 'return':'Enter', tab:'Tab', esc:'Escape', escape:'Escape', backspace:'Backspace', 'delete':'Delete', forwarddelete:'Delete', space:' ', up:'ArrowUp', down:'ArrowDown', left:'ArrowLeft', right:'ArrowRight', arrowup:'ArrowUp', arrowdown:'ArrowDown', arrowleft:'ArrowLeft', arrowright:'ArrowRight', home:'Home', end:'End', pageup:'PageUp', pagedown:'PageDown' }}; \
1180 const norm = String(key).toLowerCase(); \
1181 const k = map[norm] || key; \
1182 const opts = {{ bubbles:true, cancelable:true, composed:true, key:k, view:window }}; \
1183 el.dispatchEvent(new KeyboardEvent('keydown', opts)); \
1184 el.dispatchEvent(new KeyboardEvent('keyup', opts)); \
1185 return {{ ok:true }}; \
1186 }})({key_json}, {selector_json}, {idx})"
1187 );
1188 self.run_js_action(&script).await
1189 }
1190
1191 #[cfg(any(
1201 target_os = "ios",
1202 all(feature = "webview-input", target_os = "macos"),
1203 all(target_os = "linux", target_env = "ohos")
1204 ))]
1205 pub(crate) async fn scroll_via_js(
1206 &self,
1207 at: Option<(f64, f64)>,
1208 dx: f64,
1209 dy: f64,
1210 ) -> Result<(), WebViewInputError> {
1211 let (px, py) = at.unwrap_or((-1.0, -1.0));
1212 let script = format!(
1213 "((px, py, dx, dy) => {{ \
1214 const overflows = (v) => (/(auto|scroll|overlay)/).test(v); \
1215 const ancestor = (node) => {{ \
1216 while (node && node !== document.body && node !== document.documentElement) {{ \
1217 const s = window.getComputedStyle(node); \
1218 if ((overflows(s.overflowY) && node.scrollHeight > node.clientHeight) || \
1219 (overflows(s.overflowX) && node.scrollWidth > node.clientWidth)) return node; \
1220 node = node.parentElement; \
1221 }} \
1222 return null; \
1223 }}; \
1224 const largest = () => {{ \
1225 let best = null, range = 0; \
1226 const all = document.querySelectorAll('*'); \
1227 for (let k = 0; k < all.length; k++) {{ \
1228 const n = all[k], s = window.getComputedStyle(n); \
1229 const ry = overflows(s.overflowY) ? (n.scrollHeight - n.clientHeight) : 0; \
1230 const rx = overflows(s.overflowX) ? (n.scrollWidth - n.clientWidth) : 0; \
1231 const r = ry > rx ? ry : rx; \
1232 if (r > range) {{ range = r; best = n; }} \
1233 }} \
1234 return best; \
1235 }}; \
1236 const vw = window.innerWidth || document.documentElement.clientWidth || 0; \
1237 const vh = window.innerHeight || document.documentElement.clientHeight || 0; \
1238 let target = null; \
1239 if (px >= 0 && py >= 0) target = ancestor(document.elementFromPoint(px, py) || document.body); \
1240 else if (vw > 0 && vh > 0) target = ancestor(document.elementFromPoint(vw >> 1, vh >> 1) || document.body); \
1241 if (!target) {{ \
1242 const se = document.scrollingElement || document.documentElement; \
1243 target = (se && se.scrollHeight > se.clientHeight) ? se : (largest() || se); \
1244 }} \
1245 target.scrollLeft += dx; target.scrollTop += dy; \
1246 return {{ ok:true }}; \
1247 }})({px}, {py}, {dx}, {dy})"
1248 );
1249 self.run_js_action(&script).await
1250 }
1251
1252 #[cfg(any(
1254 target_os = "ios",
1255 all(feature = "webview-input", target_os = "macos"),
1256 all(target_os = "linux", target_env = "ohos")
1257 ))]
1258 pub(crate) async fn scroll_to_via_js(
1259 &self,
1260 selector: &str,
1261 index: Option<usize>,
1262 ) -> Result<(), WebViewInputError> {
1263 let selector_json = serde_json::to_string(selector)
1264 .map_err(|err| WebViewInputError::Platform(format!("Invalid selector: {err}")))?;
1265 let idx = index.unwrap_or(0);
1266 let script = format!(
1267 "((sel, i) => {{ \
1268 const els = document.querySelectorAll(sel); \
1269 if (!els.length || i < 0 || i >= els.length) return {{ ok:false, error:'no match', count:els.length }}; \
1270 try {{ els[i].scrollIntoView({{ block:'center', inline:'center' }}); }} catch(_e) {{ els[i].scrollIntoView(); }} \
1271 return {{ ok:true, count:els.length }}; \
1272 }})({selector_json}, {idx})"
1273 );
1274 self.run_js_action(&script).await
1275 }
1276
1277 pub async fn current_url(&self) -> Result<Option<String>, WebViewError> {
1278 self.inner.current_url().await
1279 }
1280
1281 pub fn reload(&self) -> Result<(), WebViewError> {
1282 self.inner.reload()
1283 }
1284
1285 pub fn go_back(&self) -> Result<(), WebViewError> {
1286 self.inner.go_back()
1287 }
1288
1289 pub fn go_forward(&self) -> Result<(), WebViewError> {
1290 self.inner.go_forward()
1291 }
1292
1293 pub async fn list_cookies(&self) -> Result<Vec<WebViewCookie>, WebViewError> {
1294 self.inner.list_cookies().await
1295 }
1296
1297 pub async fn set_cookie(&self, request: WebViewCookieSetRequest) -> Result<(), WebViewError> {
1298 self.inner.set_cookie(request).await
1299 }
1300
1301 pub async fn delete_cookie(
1302 &self,
1303 name: &str,
1304 domain: &str,
1305 path: &str,
1306 ) -> Result<(), WebViewError> {
1307 self.inner.delete_cookie(name, domain, path).await
1308 }
1309
1310 pub async fn clear_cookies(&self) -> Result<(), WebViewError> {
1311 self.inner.clear_cookies().await
1312 }
1313
1314 pub async fn start_network_capture(&self) -> Result<(), WebViewError> {
1315 self.inner.start_network_capture().await
1316 }
1317
1318 pub async fn stop_network_capture(&self) -> Result<(), WebViewError> {
1319 self.inner.stop_network_capture().await
1320 }
1321
1322 pub async fn network_entries(&self) -> Result<NetworkCaptureSnapshot, WebViewError> {
1323 self.inner.network_entries().await
1324 }
1325
1326 pub async fn clear_network_capture(&self) -> Result<(), WebViewError> {
1327 self.inner.clear_network_capture().await
1328 }
1329
1330 pub async fn take_screenshot(&self) -> Result<Vec<u8>, WebViewError> {
1331 self.inner.take_screenshot().await
1332 }
1333
1334 pub async fn click(
1335 &self,
1336 selector: &str,
1337 options: ClickOptions,
1338 ) -> Result<(), WebViewInputError> {
1339 <Self as WebViewInputController>::click(self, selector, options).await
1340 }
1341
1342 pub async fn type_text(
1343 &self,
1344 selector: &str,
1345 text: &str,
1346 options: TypeOptions,
1347 ) -> Result<(), WebViewInputError> {
1348 <Self as WebViewInputController>::type_text(self, selector, text, options).await
1349 }
1350
1351 pub async fn fill(
1352 &self,
1353 selector: &str,
1354 text: &str,
1355 options: FillOptions,
1356 ) -> Result<(), WebViewInputError> {
1357 <Self as WebViewInputController>::fill(self, selector, text, options).await
1358 }
1359
1360 pub async fn press(&self, key: &str, options: PressOptions) -> Result<(), WebViewInputError> {
1361 <Self as WebViewInputController>::press(self, key, options).await
1362 }
1363
1364 pub async fn scroll(
1365 &self,
1366 dx: f64,
1367 dy: f64,
1368 options: ScrollOptions,
1369 ) -> Result<(), WebViewInputError> {
1370 <Self as WebViewInputController>::scroll(self, dx, dy, options).await
1371 }
1372
1373 pub async fn scroll_to(
1374 &self,
1375 selector: &str,
1376 options: ScrollOptions,
1377 ) -> Result<(), WebViewInputError> {
1378 <Self as WebViewInputController>::scroll_to(self, selector, options).await
1379 }
1380}
1381
1382#[async_trait]
1383impl WebViewController for WebView {
1384 fn load_url(&self, url: &str) -> Result<(), WebViewError> {
1385 self.inner.load_url(url)
1386 }
1387
1388 fn load_data(&self, request: LoadDataRequest<'_>) -> Result<(), WebViewError> {
1389 self.inner.load_data(request)
1390 }
1391
1392 fn exec_js(&self, js: &str) -> Result<(), WebViewError> {
1393 self.inner.exec_js(js)
1394 }
1395
1396 async fn eval_js(&self, js: &str) -> Result<serde_json::Value, WebViewScriptError> {
1397 self.inner.eval_js(js).await
1398 }
1399
1400 async fn current_url(&self) -> Result<Option<String>, WebViewError> {
1401 self.inner.current_url().await
1402 }
1403
1404 fn post_message(&self, message: &str) -> Result<(), WebViewError> {
1405 self.inner.post_message(message)
1406 }
1407
1408 fn clear_browsing_data(&self) -> Result<(), WebViewError> {
1409 self.inner.clear_browsing_data()
1410 }
1411
1412 fn set_user_agent_override(&self, user_agent: UserAgentOverride) -> Result<(), WebViewError> {
1413 user_agent.validate()?;
1414 self.inner.set_user_agent_override(user_agent)
1415 }
1416
1417 fn reload(&self) -> Result<(), WebViewError> {
1418 self.inner.reload()
1419 }
1420
1421 fn go_back(&self) -> Result<(), WebViewError> {
1422 self.inner.go_back()
1423 }
1424
1425 fn go_forward(&self) -> Result<(), WebViewError> {
1426 self.inner.go_forward()
1427 }
1428
1429 async fn list_cookies(&self) -> Result<Vec<WebViewCookie>, WebViewError> {
1430 self.inner.list_cookies().await
1431 }
1432
1433 async fn set_cookie(&self, request: WebViewCookieSetRequest) -> Result<(), WebViewError> {
1434 self.inner.set_cookie(request).await
1435 }
1436
1437 async fn delete_cookie(
1438 &self,
1439 name: &str,
1440 domain: &str,
1441 path: &str,
1442 ) -> Result<(), WebViewError> {
1443 self.inner.delete_cookie(name, domain, path).await
1444 }
1445
1446 async fn clear_cookies(&self) -> Result<(), WebViewError> {
1447 self.inner.clear_cookies().await
1448 }
1449
1450 async fn clear_site_data(
1451 &self,
1452 url: &str,
1453 options: ClearSiteDataOptions,
1454 ) -> Result<ClearSiteDataResult, WebViewError> {
1455 self.inner.clear_site_data(url, options).await
1456 }
1457
1458 async fn take_screenshot(&self) -> Result<Vec<u8>, WebViewError> {
1463 self.inner.take_screenshot().await
1464 }
1465
1466 async fn start_network_capture(&self) -> Result<(), WebViewError> {
1467 self.inner.start_network_capture().await
1468 }
1469
1470 async fn stop_network_capture(&self) -> Result<(), WebViewError> {
1471 self.inner.stop_network_capture().await
1472 }
1473
1474 async fn network_entries(&self) -> Result<NetworkCaptureSnapshot, WebViewError> {
1475 self.inner.network_entries().await
1476 }
1477
1478 async fn clear_network_capture(&self) -> Result<(), WebViewError> {
1479 self.inner.clear_network_capture().await
1480 }
1481}
1482
1483#[async_trait]
1484impl WebViewInputController for WebView {
1485 async fn click(
1486 &self,
1487 _selector: &str,
1488 _options: ClickOptions,
1489 ) -> Result<(), WebViewInputError> {
1490 #[cfg(all(feature = "webview-input", target_os = "macos"))]
1496 {
1497 return self.click_via_js(_selector, _options.index).await;
1498 }
1499 #[cfg(all(feature = "webview-input", target_os = "windows"))]
1500 {
1501 return self.inner.click_inner(_selector, _options).await;
1502 }
1503 #[cfg(target_os = "android")]
1504 {
1505 return self.inner.click_inner(_selector, _options).await;
1506 }
1507 #[cfg(any(target_os = "ios", all(target_os = "linux", target_env = "ohos")))]
1508 {
1509 return self.click_via_js(_selector, _options.index).await;
1510 }
1511 #[allow(unreachable_code)]
1512 Err(WebViewInputError::Unsupported(
1513 "input control is not implemented for this platform",
1514 ))
1515 }
1516
1517 async fn type_text(
1518 &self,
1519 _selector: &str,
1520 _text: &str,
1521 _options: TypeOptions,
1522 ) -> Result<(), WebViewInputError> {
1523 #[cfg(all(feature = "webview-input", target_os = "macos"))]
1524 {
1525 if self.inner.is_window_attached().await {
1526 return self.inner.type_text_inner(_selector, _text, _options).await;
1527 }
1528 return self
1529 .type_via_js(_selector, _options.index, _text, _options.replace)
1530 .await;
1531 }
1532 #[cfg(all(feature = "webview-input", target_os = "windows"))]
1533 {
1534 return self.inner.type_text_inner(_selector, _text, _options).await;
1535 }
1536 #[cfg(any(
1537 target_os = "ios",
1538 target_os = "android",
1539 all(target_os = "linux", target_env = "ohos")
1540 ))]
1541 {
1542 return self
1543 .type_via_js(_selector, _options.index, _text, _options.replace)
1544 .await;
1545 }
1546 #[allow(unreachable_code)]
1547 Err(WebViewInputError::Unsupported(
1548 "input control is not implemented for this platform",
1549 ))
1550 }
1551
1552 async fn fill(
1553 &self,
1554 _selector: &str,
1555 _text: &str,
1556 _options: FillOptions,
1557 ) -> Result<(), WebViewInputError> {
1558 #[cfg(all(feature = "webview-input", target_os = "macos"))]
1559 {
1560 return self
1565 .type_via_js(_selector, _options.index, _text, true)
1566 .await;
1567 }
1568 #[cfg(all(feature = "webview-input", target_os = "windows"))]
1569 {
1570 return self
1571 .inner
1572 .type_text_inner(
1573 _selector,
1574 _text,
1575 TypeOptions {
1576 index: _options.index,
1577 replace: true,
1578 },
1579 )
1580 .await;
1581 }
1582 #[cfg(any(
1583 target_os = "ios",
1584 target_os = "android",
1585 all(target_os = "linux", target_env = "ohos")
1586 ))]
1587 {
1588 return self
1589 .type_via_js(_selector, _options.index, _text, true)
1590 .await;
1591 }
1592 #[allow(unreachable_code)]
1593 Err(WebViewInputError::Unsupported(
1594 "input control is not implemented for this platform",
1595 ))
1596 }
1597
1598 async fn press(&self, _key: &str, _options: PressOptions) -> Result<(), WebViewInputError> {
1599 if _options.index.is_some() && _options.selector.is_none() {
1600 return Err(WebViewInputError::Platform(
1601 "press index requires a selector".to_string(),
1602 ));
1603 }
1604 #[cfg(all(feature = "webview-input", target_os = "macos"))]
1605 {
1606 if self.inner.is_window_attached().await {
1607 return self.inner.press_inner(_key, _options).await;
1608 }
1609 return self
1610 .press_via_js(_key, _options.selector.as_deref(), _options.index)
1611 .await;
1612 }
1613 #[cfg(all(feature = "webview-input", target_os = "windows"))]
1614 {
1615 return self.inner.press_inner(_key, _options).await;
1616 }
1617 #[cfg(any(
1618 target_os = "ios",
1619 target_os = "android",
1620 all(target_os = "linux", target_env = "ohos")
1621 ))]
1622 {
1623 return self
1624 .press_via_js(_key, _options.selector.as_deref(), _options.index)
1625 .await;
1626 }
1627 #[allow(unreachable_code)]
1628 Err(WebViewInputError::Unsupported(
1629 "input control is not implemented for this platform",
1630 ))
1631 }
1632
1633 async fn scroll(
1634 &self,
1635 _dx: f64,
1636 _dy: f64,
1637 _options: ScrollOptions,
1638 ) -> Result<(), WebViewInputError> {
1639 #[cfg(all(feature = "webview-input", target_os = "macos"))]
1642 {
1643 if self.inner.is_window_attached().await {
1644 return self.inner.scroll_inner(_dx, _dy, _options).await;
1645 }
1646 return self.scroll_via_js(None, _dx, _dy).await;
1647 }
1648 #[cfg(all(feature = "webview-input", target_os = "windows"))]
1649 {
1650 return self.inner.scroll_inner(_dx, _dy, _options).await;
1651 }
1652 #[cfg(target_os = "android")]
1655 {
1656 return self.inner.scroll_inner(_dx, _dy, _options).await;
1657 }
1658 #[cfg(any(target_os = "ios", all(target_os = "linux", target_env = "ohos")))]
1660 {
1661 return self.scroll_via_js(None, _dx, _dy).await;
1662 }
1663 #[allow(unreachable_code)]
1664 Err(WebViewInputError::Unsupported(
1665 "input control is not implemented for this platform",
1666 ))
1667 }
1668
1669 async fn scroll_to(
1670 &self,
1671 _selector: &str,
1672 _options: ScrollOptions,
1673 ) -> Result<(), WebViewInputError> {
1674 #[cfg(all(feature = "webview-input", target_os = "macos"))]
1675 {
1676 if self.inner.is_window_attached().await {
1677 return self.inner.scroll_to_inner(_selector, _options).await;
1678 }
1679 return self.scroll_to_via_js(_selector, None).await;
1680 }
1681 #[cfg(all(feature = "webview-input", target_os = "windows"))]
1682 {
1683 return self.inner.scroll_to_inner(_selector, _options).await;
1684 }
1685 #[cfg(target_os = "android")]
1686 {
1687 return self.inner.scroll_to_inner(_selector, _options).await;
1688 }
1689 #[cfg(any(target_os = "ios", all(target_os = "linux", target_env = "ohos")))]
1690 {
1691 return self.scroll_to_via_js(_selector, None).await;
1692 }
1693 #[allow(unreachable_code)]
1694 Err(WebViewInputError::Unsupported(
1695 "input control is not implemented for this platform",
1696 ))
1697 }
1698}
1699
1700type WebViewInstancesMap = Arc<Mutex<HashMap<String, Arc<WebView>>>>;
1702
1703#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1704#[serde(rename_all = "snake_case")]
1705pub enum WebViewCreateStage {
1706 Requested,
1707 NativeCreated,
1708 ControllerAttached,
1709 Ready,
1710 Destroyed,
1711}
1712
1713#[derive(Debug, Clone, PartialEq, Eq)]
1714pub enum WebViewEvent {
1715 Stage(WebViewCreateStage),
1716 Failed {
1717 stage: WebViewCreateStage,
1718 error: WebViewError,
1719 },
1720}
1721
1722type WebViewReadyState = Option<Result<Arc<WebView>, WebViewError>>;
1723
1724#[derive(Clone)]
1725pub struct WebViewEventSubscription {
1726 rx: watch::Receiver<WebViewEvent>,
1727}
1728
1729impl WebViewEventSubscription {
1730 pub fn current(&self) -> WebViewEvent {
1731 self.rx.borrow().clone()
1732 }
1733
1734 pub async fn changed(&mut self) -> Result<WebViewEvent, WebViewError> {
1735 self.rx.changed().await.map_err(|_| {
1736 WebViewError::WebView("webview event channel unexpectedly closed".to_string())
1737 })?;
1738 Ok(self.current())
1739 }
1740}
1741
1742#[derive(Clone)]
1743pub struct WebViewSession {
1744 webtag: WebTag,
1745 event_rx: watch::Receiver<WebViewEvent>,
1746 ready_rx: watch::Receiver<WebViewReadyState>,
1747 signals: Arc<WebViewSessionSignals>,
1748}
1749
1750impl WebViewSession {
1751 pub fn webtag(&self) -> &WebTag {
1752 &self.webtag
1753 }
1754
1755 pub fn subscribe_events(&self) -> WebViewEventSubscription {
1756 WebViewEventSubscription {
1757 rx: self.event_rx.clone(),
1758 }
1759 }
1760
1761 pub fn current_event(&self) -> WebViewEvent {
1762 self.event_rx.borrow().clone()
1763 }
1764
1765 pub async fn wait_ready(&self) -> Result<Arc<WebView>, WebViewError> {
1766 let mut rx = self.ready_rx.clone();
1767 loop {
1768 if let Some(result) = self.signals.terminal_result() {
1769 return result;
1770 }
1771 if let Some(result) = rx.borrow().clone() {
1772 return result;
1773 }
1774 if rx.changed().await.is_err() {
1775 if let Some(result) = self.signals.terminal_result() {
1776 return result;
1777 }
1778 return Err(WebViewError::WebView(
1779 "webview ready channel unexpectedly closed".to_string(),
1780 ));
1781 }
1782 }
1783 }
1784}
1785
1786struct WebViewSessionSignals {
1787 event_tx: watch::Sender<WebViewEvent>,
1788 ready_tx: watch::Sender<WebViewReadyState>,
1789 state: Mutex<WebViewSessionState>,
1790}
1791
1792#[derive(Default)]
1793struct WebViewSessionState {
1794 terminal_result: Option<Result<Arc<WebView>, WebViewError>>,
1795 destroyed: bool,
1796}
1797
1798impl WebViewSessionSignals {
1799 fn new() -> Arc<Self> {
1800 let (event_tx, _event_rx) =
1801 watch::channel(WebViewEvent::Stage(WebViewCreateStage::Requested));
1802 let (ready_tx, _ready_rx) = watch::channel(None);
1803 Arc::new(Self {
1804 event_tx,
1805 ready_tx,
1806 state: Mutex::new(WebViewSessionState::default()),
1807 })
1808 }
1809
1810 fn subscribe(self: &Arc<Self>, webtag: WebTag) -> WebViewSession {
1811 WebViewSession {
1812 webtag,
1813 event_rx: self.event_tx.subscribe(),
1814 ready_rx: self.ready_tx.subscribe(),
1815 signals: Arc::clone(self),
1816 }
1817 }
1818
1819 fn terminal_result(&self) -> Option<Result<Arc<WebView>, WebViewError>> {
1820 let state = lock_or_recover(&self.state, "webview_session_state.terminal_result");
1821 state.terminal_result.clone()
1822 }
1823
1824 #[cfg_attr(not(any(target_os = "macos", target_os = "ios")), allow(dead_code))]
1826 fn is_destroyed(&self) -> bool {
1827 let state = lock_or_recover(&self.state, "webview_session_state.is_destroyed");
1828 state.destroyed
1829 }
1830
1831 fn publish_result(
1832 &self,
1833 result: Result<Arc<WebView>, WebViewError>,
1834 stage_on_error: WebViewCreateStage,
1835 ) {
1836 let mut state = lock_or_recover(&self.state, "webview_session_state.publish_result");
1837 if state.destroyed || state.terminal_result.is_some() {
1838 return;
1839 }
1840 state.terminal_result = Some(result.clone());
1841 drop(state);
1842
1843 match result {
1844 Ok(webview) => {
1845 self.event_tx
1846 .send_replace(WebViewEvent::Stage(WebViewCreateStage::NativeCreated));
1847 self.event_tx
1848 .send_replace(WebViewEvent::Stage(WebViewCreateStage::ControllerAttached));
1849 self.ready_tx.send_replace(Some(Ok(webview)));
1850 self.event_tx
1851 .send_replace(WebViewEvent::Stage(WebViewCreateStage::Ready));
1852 }
1853 Err(error) => {
1854 self.ready_tx.send_replace(Some(Err(error.clone())));
1855 self.event_tx.send_replace(WebViewEvent::Failed {
1856 stage: stage_on_error,
1857 error,
1858 });
1859 }
1860 }
1861 }
1862
1863 fn publish_destroyed(&self) {
1864 let mut state = lock_or_recover(&self.state, "webview_session_state.publish_destroyed");
1865 if state.destroyed {
1866 return;
1867 }
1868 state.destroyed = true;
1869 if state.terminal_result.is_none() {
1870 state.terminal_result = Some(Err(WebViewError::WebView(
1871 "webview destroyed before ready".to_string(),
1872 )));
1873 }
1874 let terminal_result = state.terminal_result.clone();
1875 drop(state);
1876
1877 self.event_tx
1878 .send_replace(WebViewEvent::Stage(WebViewCreateStage::Destroyed));
1879 if let Some(result) = terminal_result {
1880 self.ready_tx.send_replace(Some(result));
1881 }
1882 }
1883}
1884
1885pub(crate) struct WebViewCreateSender {
1886 webtag: WebTag,
1887 signals: Arc<WebViewSessionSignals>,
1888}
1889
1890impl WebViewCreateSender {
1891 fn new(webtag: WebTag, signals: Arc<WebViewSessionSignals>) -> Self {
1892 Self { webtag, signals }
1893 }
1894
1895 pub(crate) fn succeed(self, webview: Arc<WebView>) {
1896 self.signals
1897 .publish_result(Ok(webview), WebViewCreateStage::Requested);
1898 }
1899
1900 pub(crate) fn fail(self, stage: WebViewCreateStage, error: WebViewError) {
1901 if remove_session_signals_if_matches(&self.webtag, &self.signals) {
1902 crate::events::normalizer::destroy(&self.webtag);
1903 }
1904 self.signals.publish_result(Err(error), stage);
1905 }
1906
1907 #[cfg_attr(not(target_os = "android"), allow(dead_code))]
1911 pub(crate) fn cancel_superseded(self) {
1912 if remove_session_signals_if_matches(&self.webtag, &self.signals) {
1913 crate::events::normalizer::destroy(&self.webtag);
1914 }
1915 self.signals.publish_destroyed();
1916 }
1917
1918 #[cfg_attr(
1923 not(any(target_os = "macos", target_os = "ios", target_os = "windows")),
1924 allow(dead_code)
1925 )]
1926 pub(crate) fn is_destroyed(&self) -> bool {
1927 self.signals.is_destroyed()
1928 }
1929}
1930
1931static WEBVIEW_INSTANCES: OnceLock<WebViewInstancesMap> = OnceLock::new();
1933
1934struct PendingCallbacksEntry {
1937 #[cfg(target_os = "android")]
1938 signals: Arc<WebViewSessionSignals>,
1939 callbacks: PendingCallbacks,
1940}
1941
1942static PENDING_CALLBACKS: OnceLock<Mutex<HashMap<String, PendingCallbacksEntry>>> = OnceLock::new();
1943static WEBVIEW_SESSIONS: OnceLock<Mutex<HashMap<String, Arc<WebViewSessionSignals>>>> =
1944 OnceLock::new();
1945#[cfg(target_os = "windows")]
1946static WEBVIEW_CREATE_LOCKS: OnceLock<Mutex<HashMap<String, std::sync::Weak<Mutex<()>>>>> =
1947 OnceLock::new();
1948static DESIRED_PROXY_FOR_NEW_WEBVIEWS: OnceLock<RwLock<Option<ProxyConfig>>> = OnceLock::new();
1949static PROXY_APPLY_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
1950
1951fn apply_http_proxy_platform(
1952 config: Option<&ProxyConfig>,
1953) -> Result<ProxyApplyReport, WebViewError> {
1954 #[cfg(target_os = "android")]
1955 {
1956 crate::android::apply_http_proxy(config)
1957 }
1958
1959 #[cfg(any(target_os = "ios", target_os = "macos"))]
1960 {
1961 crate::apple::apply_http_proxy(config)
1962 }
1963
1964 #[cfg(all(target_os = "linux", target_env = "ohos"))]
1965 {
1966 crate::harmony::apply_http_proxy(config)
1967 }
1968
1969 #[cfg(not(any(
1970 target_os = "android",
1971 target_os = "ios",
1972 target_os = "macos",
1973 all(target_os = "linux", target_env = "ohos")
1974 )))]
1975 {
1976 let _ = config;
1977 Ok(ProxyApplyReport::unsupported(
1978 "proxy is not supported on this platform",
1979 ))
1980 }
1981}
1982
1983pub fn configure_proxy_for_new_webviews(config: Option<ProxyConfig>) -> Result<(), WebViewError> {
1988 let apply_lock = PROXY_APPLY_LOCK.get_or_init(|| Mutex::new(()));
1989 let _guard = lock_or_recover(apply_lock, "webview_proxy_apply_lock");
1990
1991 let normalized_config = match config {
1992 Some(cfg) => Some(cfg.validate()?),
1993 None => None,
1994 };
1995
1996 let state = DESIRED_PROXY_FOR_NEW_WEBVIEWS.get_or_init(|| RwLock::new(None));
1997 match state.write() {
1998 Ok(mut guard) => {
1999 *guard = normalized_config;
2000 }
2001 Err(poisoned) => {
2002 log::error!("RwLock poisoned at webview_desired_proxy.write, recovering");
2003 *poisoned.into_inner() = normalized_config;
2004 }
2005 }
2006 Ok(())
2007}
2008
2009pub fn apply_proxy_to_current_runtime(
2014 config: Option<ProxyConfig>,
2015) -> Result<ProxyApplyReport, WebViewError> {
2016 let apply_lock = PROXY_APPLY_LOCK.get_or_init(|| Mutex::new(()));
2017 let _guard = lock_or_recover(apply_lock, "webview_proxy_apply_lock");
2018
2019 let normalized_config = match config {
2020 Some(cfg) => Some(cfg.validate()?),
2021 None => None,
2022 };
2023
2024 let report = apply_http_proxy_platform(normalized_config.as_ref())?;
2025
2026 if matches!(
2027 report.status,
2028 ProxyApplyStatus::Applied | ProxyApplyStatus::Cleared
2029 ) {
2030 let state = DESIRED_PROXY_FOR_NEW_WEBVIEWS.get_or_init(|| RwLock::new(None));
2031 match state.write() {
2032 Ok(mut guard) => {
2033 *guard = normalized_config;
2034 }
2035 Err(poisoned) => {
2036 log::error!("RwLock poisoned at webview_desired_proxy.write, recovering");
2037 *poisoned.into_inner() = normalized_config;
2038 }
2039 }
2040 }
2041
2042 Ok(report)
2043}
2044
2045pub fn configured_proxy_for_new_webviews() -> Option<ProxyConfig> {
2047 let state = DESIRED_PROXY_FOR_NEW_WEBVIEWS.get()?;
2048 match state.read() {
2049 Ok(guard) => guard.clone(),
2050 Err(poisoned) => {
2051 log::error!("RwLock poisoned at webview_desired_proxy.read, recovering");
2052 poisoned.into_inner().clone()
2053 }
2054 }
2055}
2056
2057fn clear_pending_callbacks(webtag: &WebTag) {
2058 if let Some(pending) = PENDING_CALLBACKS.get()
2059 && let Ok(mut map) = pending.lock()
2060 {
2061 map.remove(webtag.key());
2062 }
2063}
2064
2065fn replace_session_signals(webtag: &WebTag, signals: Arc<WebViewSessionSignals>) {
2066 let sessions = WEBVIEW_SESSIONS.get_or_init(|| Mutex::new(HashMap::new()));
2067 let mut guard = lock_or_recover(sessions, "webview_sessions.replace");
2068 guard.insert(webtag.key().to_string(), signals);
2069}
2070
2071fn remove_session_signals(webtag: &WebTag) -> Option<Arc<WebViewSessionSignals>> {
2072 let sessions = WEBVIEW_SESSIONS.get()?;
2073 let mut guard = lock_or_recover(sessions, "webview_sessions.remove");
2074 guard.remove(webtag.key())
2075}
2076
2077fn remove_session_signals_if_matches(
2078 webtag: &WebTag,
2079 expected: &Arc<WebViewSessionSignals>,
2080) -> bool {
2081 let Some(sessions) = WEBVIEW_SESSIONS.get() else {
2082 return false;
2083 };
2084 let mut guard = lock_or_recover(sessions, "webview_sessions.remove_if_matches");
2085 if guard
2086 .get(webtag.key())
2087 .is_some_and(|current| Arc::ptr_eq(current, expected))
2088 {
2089 guard.remove(webtag.key());
2090 true
2091 } else {
2092 false
2093 }
2094}
2095
2096#[derive(Debug, Clone, PartialEq, Eq, Hash)]
2099pub struct WebTag(String);
2100
2101impl std::fmt::Display for WebTag {
2102 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2103 write!(f, "{}", self.0)
2104 }
2105}
2106
2107impl WebTag {
2108 pub fn new(appid: &str, path: &str, session_id: Option<u64>) -> Self {
2109 let mut tag = format!("{}:{}", appid, path);
2110 if let Some(session) = session_id {
2111 tag.push('#');
2112 tag.push_str(&session.to_string());
2113 }
2114 Self(tag)
2115 }
2116
2117 pub fn as_str(&self) -> &str {
2118 &self.0
2119 }
2120
2121 pub fn key(&self) -> &str {
2125 &self.0
2126 }
2127
2128 pub fn extract_appid(&self) -> String {
2130 self.0.split(':').next().unwrap_or("").to_string()
2131 }
2132
2133 pub fn extract_parts(&self) -> (String, String) {
2136 if let Some((appid, path_with_session)) = self.0.split_once(':') {
2137 let path = path_with_session
2138 .split('#')
2139 .next()
2140 .unwrap_or(path_with_session);
2141 (appid.to_string(), path.to_string())
2142 } else {
2143 log::error!("Invalid webtag format: {}", self.0);
2144 ("".to_string(), self.0.clone())
2145 }
2146 }
2147
2148 pub fn session_id(&self) -> Option<u64> {
2150 self.0
2151 .split('#')
2152 .next_back()
2153 .and_then(|raw| raw.parse::<u64>().ok())
2154 }
2155
2156 #[cfg_attr(
2160 any(not(target_os = "windows"), target_os = "windows"),
2161 allow(dead_code)
2162 )]
2163 pub(crate) fn group_key(&self) -> String {
2164 let Some((appid, path_with_session)) = self.0.split_once(':') else {
2165 return self.0.clone();
2166 };
2167 let session = path_with_session
2168 .rsplit_once('#')
2169 .and_then(|(_, suffix)| suffix.parse::<u64>().ok())
2170 .map(|session| session.to_string())
2171 .unwrap_or_else(|| "0".to_string());
2172 format!("{appid}#{session}")
2173 }
2174
2175 fn key_path(&self) -> String {
2176 let Some((_, path_with_suffix)) = self.0.split_once(':') else {
2177 return self.0.clone();
2178 };
2179 if self.session_id().is_some()
2180 && let Some((path, _)) = path_with_suffix.rsplit_once('#')
2181 {
2182 return path.to_string();
2183 }
2184 path_with_suffix.to_string()
2185 }
2186}
2187
2188impl From<&str> for WebTag {
2189 fn from(webtag_str: &str) -> Self {
2190 Self(webtag_str.to_string())
2191 }
2192}
2193
2194fn request_create_webview(
2195 webtag: &WebTag,
2196 sender: WebViewCreateSender,
2197 options: WebViewCreateOptions,
2198) {
2199 let (appid, _) = webtag.extract_parts();
2200 let (effective_options, pending_callbacks) = match options.normalize() {
2201 Ok(value) => value,
2202 Err(error) => {
2203 sender.fail(WebViewCreateStage::Requested, error);
2204 return;
2205 }
2206 };
2207
2208 log::info!(
2209 "Creating WebView for key={} profile={:?} data_mode={:?} schemes={:?}",
2210 webtag.key(),
2211 effective_options.profile,
2212 effective_options.data_mode,
2213 effective_options.registered_schemes,
2214 );
2215
2216 let instances = WEBVIEW_INSTANCES.get_or_init(|| Arc::new(Mutex::new(HashMap::new())));
2218
2219 if let Ok(webviews) = instances.lock()
2224 && let Some(existing_webview) = webviews.get(webtag.key())
2225 {
2226 if existing_webview.effective_options() != &effective_options {
2227 sender.fail(
2228 WebViewCreateStage::Requested,
2229 WebViewError::InvalidCreateOptions(format!(
2230 "webview already exists with different options: key={} existing={:?} requested={:?}",
2231 webtag.key(),
2232 existing_webview.effective_options(),
2233 effective_options
2234 )),
2235 );
2236 return;
2237 }
2238
2239 if pending_callbacks.has_any() {
2240 sender.fail(
2241 WebViewCreateStage::Requested,
2242 WebViewError::InvalidCreateOptions(format!(
2243 "webview already exists and callback registrations are immutable: key={} options={:?}",
2244 webtag.key(),
2245 existing_webview.effective_options()
2246 )),
2247 );
2248 log::warn!(
2249 "Rejected recreate with callbacks for existing webview key={} options={:?}",
2250 webtag.key(),
2251 existing_webview.effective_options()
2252 );
2253 return;
2254 }
2255
2256 log::info!("WebView already exists, reusing: {}", webtag.key());
2257 sender.succeed(existing_webview.clone());
2258 return;
2259 }
2260
2261 clear_pending_callbacks(webtag);
2263
2264 if pending_callbacks.has_any() {
2266 let pending = PENDING_CALLBACKS.get_or_init(|| Mutex::new(HashMap::new()));
2267 if let Ok(mut map) = pending.lock() {
2268 map.insert(
2269 webtag.key().to_string(),
2270 PendingCallbacksEntry {
2271 #[cfg(target_os = "android")]
2272 signals: Arc::clone(&sender.signals),
2273 callbacks: pending_callbacks,
2274 },
2275 );
2276 }
2277 }
2278
2279 WebViewInner::create(
2281 &appid,
2282 &webtag.key_path(),
2283 webtag.session_id(),
2284 effective_options,
2285 sender,
2286 );
2287}
2288
2289fn create_webview_session(webtag: WebTag, options: WebViewCreateOptions) -> WebViewSession {
2290 #[cfg(target_os = "windows")]
2295 let create_lock = windows_webview_create_lock(webtag.key());
2296 #[cfg(target_os = "windows")]
2297 let _create_guard = lock_or_recover(&create_lock, "windows_webview_create_lock");
2298
2299 let signals = WebViewSessionSignals::new();
2300 let session = signals.subscribe(webtag.clone());
2301 let sender = WebViewCreateSender::new(webtag.clone(), signals.clone());
2302 replace_session_signals(&webtag, signals);
2303 crate::events::normalizer::begin(&webtag);
2304 request_create_webview(&webtag, sender, options);
2305 session
2306}
2307
2308#[cfg(target_os = "windows")]
2309fn windows_webview_create_lock(webtag_key: &str) -> Arc<Mutex<()>> {
2310 let locks = WEBVIEW_CREATE_LOCKS.get_or_init(|| Mutex::new(HashMap::new()));
2311 let mut locks = lock_or_recover(locks, "windows_webview_create_locks");
2312 locks.retain(|_, lock| lock.strong_count() > 0);
2313 if let Some(lock) = locks.get(webtag_key).and_then(std::sync::Weak::upgrade) {
2314 return lock;
2315 }
2316 let lock = Arc::new(Mutex::new(()));
2317 locks.insert(webtag_key.to_string(), Arc::downgrade(&lock));
2318 lock
2319}
2320
2321#[cfg_attr(target_os = "android", allow(dead_code))]
2322pub(crate) fn register_webview(webview: Arc<WebView>) {
2323 let webtag = webview.webtag();
2324
2325 if let Some(pending) = PENDING_CALLBACKS.get()
2327 && let Ok(mut map) = pending.lock()
2328 && let Some(entry) = map.remove(webtag.key())
2329 {
2330 let callbacks = entry.callbacks;
2331 log::info!(
2332 "Installing callbacks for {} (schemes={}, nav={}, new_window={}, download={}, file_chooser={}, delegate={})",
2333 webtag.key(),
2334 callbacks.scheme_handlers.len(),
2335 callbacks.navigation_handler.is_some(),
2336 callbacks.new_window_handler.is_some(),
2337 callbacks.download_handler.is_some(),
2338 callbacks.file_chooser_handler.is_some(),
2339 callbacks.delegate.is_some()
2340 );
2341 webview.install_callbacks(callbacks);
2342 }
2343
2344 if let Some(instances) = WEBVIEW_INSTANCES.get()
2345 && let Ok(mut webviews) = instances.lock()
2346 {
2347 webviews.insert(webtag.key().to_string(), webview.clone());
2348 log::info!("WebView created and stored: {}", webtag.key());
2349 }
2350}
2351
2352#[cfg(target_os = "android")]
2353pub(crate) fn register_android_webview_if_current(
2354 webview: Arc<WebView>,
2355 sender: &WebViewCreateSender,
2356) -> bool {
2357 let webtag = webview.webtag();
2358 let sessions = WEBVIEW_SESSIONS.get_or_init(|| Mutex::new(HashMap::new()));
2359 let session_guard = lock_or_recover(sessions, "webview_sessions.register_android");
2360 if !session_guard
2361 .get(webtag.key())
2362 .is_some_and(|current| Arc::ptr_eq(current, &sender.signals))
2363 {
2364 return false;
2365 }
2366
2367 if let Some(pending) = PENDING_CALLBACKS.get()
2368 && let Ok(mut map) = pending.lock()
2369 && map
2370 .get(webtag.key())
2371 .is_some_and(|entry| Arc::ptr_eq(&entry.signals, &sender.signals))
2372 && let Some(entry) = map.remove(webtag.key())
2373 {
2374 webview.install_callbacks(entry.callbacks);
2375 }
2376
2377 let instances = WEBVIEW_INSTANCES.get_or_init(|| Arc::new(Mutex::new(HashMap::new())));
2378 let mut webviews = lock_or_recover(instances, "webview_instances.register_android");
2379 webviews.insert(webtag.key().to_string(), webview);
2380 true
2381}
2382
2383pub(crate) fn find_webview(webtag: &WebTag) -> Option<Arc<WebView>> {
2385 if let Some(instances) = WEBVIEW_INSTANCES.get() {
2386 if let Ok(webviews) = instances.lock() {
2387 webviews.get(webtag.key()).cloned()
2388 } else {
2389 None
2390 }
2391 } else {
2392 None
2393 }
2394}
2395
2396#[cfg(target_os = "windows")]
2397pub(crate) fn first_browser_webview() -> Option<Arc<WebView>> {
2398 WEBVIEW_INSTANCES
2399 .get()
2400 .and_then(|instances| instances.lock().ok())
2401 .and_then(|webviews| {
2402 webviews
2403 .values()
2404 .find(|webview| {
2405 webview.effective_options.profile == SecurityProfile::BrowserRelaxed
2406 })
2407 .cloned()
2408 })
2409}
2410
2411pub(crate) fn list_webviews() -> Vec<WebTag> {
2412 if let Some(instances) = WEBVIEW_INSTANCES.get()
2413 && let Ok(webviews) = instances.lock()
2414 {
2415 let mut tags: Vec<WebTag> = webviews.values().map(|webview| webview.webtag()).collect();
2416 tags.sort_by(|a, b| a.as_str().cmp(b.as_str()));
2417 return tags;
2418 }
2419 Vec::new()
2420}
2421
2422pub(crate) fn find_webview_delegate(webtag: &WebTag) -> Option<Arc<dyn WebViewDelegate>> {
2423 find_webview(webtag).and_then(|webview| webview.get_delegate())
2424}
2425
2426fn remove_arc_if_matches<T>(
2427 entries: &mut HashMap<String, Arc<T>>,
2428 key: &str,
2429 expected: &Arc<T>,
2430) -> Option<Arc<T>> {
2431 entries
2432 .get(key)
2433 .is_some_and(|current| Arc::ptr_eq(current, expected))
2434 .then(|| entries.remove(key))
2435 .flatten()
2436}
2437
2438pub(crate) fn destroy_webview_if_matches(webtag: &WebTag, expected: &Arc<WebView>) -> bool {
2442 let removed = if let Some(instances) = WEBVIEW_INSTANCES.get()
2443 && let Ok(mut webviews) = instances.lock()
2444 {
2445 remove_arc_if_matches(&mut webviews, webtag.key(), expected)
2446 } else {
2447 None
2448 };
2449 if let Some(webview) = removed {
2450 #[cfg(target_os = "windows")]
2451 {
2452 let _ = webview.inner.set_content_visible(false);
2453 webview.inner.request_shutdown();
2454 }
2455 webview.remove_delegate();
2456 true
2457 } else {
2458 false
2459 }
2460}
2461
2462pub(crate) fn destroy_webview(webtag: &WebTag) {
2464 crate::events::normalizer::destroy(webtag);
2467 if let Some(signals) = remove_session_signals(webtag) {
2473 signals.publish_destroyed();
2474 }
2475 let removed = if let Some(instances) = WEBVIEW_INSTANCES.get()
2476 && let Ok(mut webviews) = instances.lock()
2477 {
2478 webviews.remove(webtag.key())
2479 } else {
2480 None
2481 };
2482 if let Some(webview) = removed {
2483 #[cfg(target_os = "windows")]
2487 {
2488 let _ = webview.inner.set_content_visible(false);
2489 webview.inner.request_shutdown();
2493 }
2494 webview.remove_delegate();
2495 }
2496 clear_pending_callbacks(webtag);
2497}
2498
2499#[cfg(test)]
2500mod tests {
2501 use super::{
2502 WEBVIEW_SESSIONS, WebTag, WebViewCreateSender, WebViewSessionSignals,
2503 remove_arc_if_matches, remove_session_signals_if_matches, replace_session_signals,
2504 };
2505 use std::collections::HashMap;
2506 use std::sync::Arc;
2507
2508 #[test]
2509 fn conditional_instance_removal_uses_arc_identity() {
2510 let current = Arc::new(7_u8);
2511 let same_value_different_instance = Arc::new(7_u8);
2512 let mut entries = HashMap::from([("tab".to_string(), current.clone())]);
2513
2514 assert!(
2515 remove_arc_if_matches(&mut entries, "tab", &same_value_different_instance).is_none()
2516 );
2517 assert!(Arc::ptr_eq(entries.get("tab").unwrap(), ¤t));
2518
2519 let removed = remove_arc_if_matches(&mut entries, "tab", ¤t).unwrap();
2520 assert!(Arc::ptr_eq(&removed, ¤t));
2521 assert!(!entries.contains_key("tab"));
2522 }
2523
2524 #[test]
2525 fn superseded_sender_completes_without_removing_current_generation() {
2526 let webtag = WebTag::from("test:pages/superseded#9173");
2527 let superseded = WebViewSessionSignals::new();
2528 let current = WebViewSessionSignals::new();
2529 replace_session_signals(&webtag, current.clone());
2530
2531 WebViewCreateSender::new(webtag.clone(), superseded.clone()).cancel_superseded();
2532
2533 assert!(
2534 superseded
2535 .terminal_result()
2536 .is_some_and(|result| result.is_err())
2537 );
2538 assert!(current.terminal_result().is_none());
2539 let sessions = WEBVIEW_SESSIONS.get().unwrap().lock().unwrap();
2540 assert!(
2541 sessions
2542 .get(webtag.key())
2543 .is_some_and(|signals| Arc::ptr_eq(signals, ¤t))
2544 );
2545 drop(sessions);
2546 assert!(remove_session_signals_if_matches(&webtag, ¤t));
2547 }
2548}