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}
246
247impl std::fmt::Debug for WebViewCreateOptions {
248 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
249 f.debug_struct("WebViewCreateOptions")
250 .field("profile", &self.profile)
251 .field("data_mode", &self.data_mode)
252 .field(
253 "scheme_handlers",
254 &self.scheme_handlers.keys().collect::<Vec<_>>(),
255 )
256 .field("has_navigation_handler", &self.navigation_handler.is_some())
257 .field("has_new_window_handler", &self.new_window_handler.is_some())
258 .field("has_download_handler", &self.download_handler.is_some())
259 .field(
260 "has_file_chooser_handler",
261 &self.file_chooser_handler.is_some(),
262 )
263 .field("has_delegate", &self.delegate.is_some())
264 .finish()
265 }
266}
267
268#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
270pub struct ProxyConfig {
271 pub host: String,
272 pub port: u16,
273 #[serde(default)]
274 pub bypass: Vec<String>,
275}
276
277impl ProxyConfig {
278 pub fn new(host: impl Into<String>, port: u16) -> Result<Self, WebViewError> {
279 let cfg = Self {
280 host: host.into(),
281 port,
282 bypass: Vec::new(),
283 };
284 cfg.validate()
285 }
286
287 pub fn with_bypass<I, S>(mut self, bypass: I) -> Self
288 where
289 I: IntoIterator<Item = S>,
290 S: Into<String>,
291 {
292 self.bypass = bypass.into_iter().map(Into::into).collect();
293 self
294 }
295
296 fn validate(self) -> Result<Self, WebViewError> {
297 let host = self.host.trim().to_string();
298 if host.is_empty() {
299 return Err(WebViewError::InvalidCreateOptions(
300 "proxy host cannot be empty".to_string(),
301 ));
302 }
303 if host.contains(char::is_whitespace) {
304 return Err(WebViewError::InvalidCreateOptions(
305 "proxy host cannot contain whitespace".to_string(),
306 ));
307 }
308 if self.port == 0 {
309 return Err(WebViewError::InvalidCreateOptions(
310 "proxy port must be greater than 0".to_string(),
311 ));
312 }
313
314 let mut seen = HashSet::new();
315 let mut bypass = Vec::new();
316 for raw in self.bypass {
317 let rule = raw.trim();
318 if rule.is_empty() {
319 continue;
320 }
321 let key = rule.to_ascii_lowercase();
322 if seen.insert(key) {
323 bypass.push(rule.to_string());
324 }
325 }
326
327 Ok(Self {
328 host,
329 bypass,
330 ..self
331 })
332 }
333}
334
335#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
336#[serde(rename_all = "snake_case")]
337pub enum ProxyApplyStatus {
338 Applied,
339 Cleared,
340 Unsupported,
341}
342
343#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
344#[serde(rename_all = "snake_case")]
345pub enum ProxyActivation {
346 EffectiveNow,
347 NewWebViewsOnly,
348 EngineRecreateRequired,
349 NotApplied,
350}
351
352#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
353pub struct ProxyApplyReport {
354 pub status: ProxyApplyStatus,
355 pub activation: ProxyActivation,
356 #[serde(default, skip_serializing_if = "Option::is_none")]
357 pub detail: Option<String>,
358}
359
360impl ProxyApplyReport {
361 pub fn applied(activation: ProxyActivation) -> Self {
362 Self {
363 status: ProxyApplyStatus::Applied,
364 activation,
365 detail: None,
366 }
367 }
368
369 pub fn cleared(activation: ProxyActivation) -> Self {
370 Self {
371 status: ProxyApplyStatus::Cleared,
372 activation,
373 detail: None,
374 }
375 }
376
377 pub fn unsupported(detail: impl Into<String>) -> Self {
378 Self {
379 status: ProxyApplyStatus::Unsupported,
380 activation: ProxyActivation::NotApplied,
381 detail: Some(detail.into()),
382 }
383 }
384}
385
386#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
388pub(crate) struct EffectiveWebViewCreateOptions {
389 pub(crate) profile: SecurityProfile,
390 #[serde(default)]
392 pub(crate) data_mode: WebViewDataMode,
393 #[serde(default)]
395 pub(crate) registered_schemes: Vec<String>,
396 #[serde(default)]
397 pub(crate) has_navigation_handler: bool,
398 #[serde(default)]
399 pub(crate) has_new_window_handler: bool,
400 #[serde(default)]
401 pub(crate) has_download_handler: bool,
402 #[serde(default)]
403 pub(crate) has_file_chooser_handler: bool,
404 #[serde(default)]
405 pub(crate) has_delegate: bool,
406}
407
408impl Default for WebViewCreateOptions {
409 fn default() -> Self {
410 Self::strict()
411 }
412}
413
414impl WebViewCreateOptions {
415 fn strict() -> Self {
416 Self {
417 profile: SecurityProfile::StrictDefault,
418 data_mode: WebViewDataMode::ProfileDefault,
419 scheme_handlers: HashMap::new(),
420 navigation_handler: None,
421 new_window_handler: None,
422 download_handler: None,
423 file_chooser_handler: None,
424 delegate: None,
425 }
426 }
427
428 fn browser() -> Self {
429 Self {
430 profile: SecurityProfile::BrowserRelaxed,
431 data_mode: WebViewDataMode::ProfileDefault,
432 scheme_handlers: HashMap::new(),
433 navigation_handler: None,
434 new_window_handler: None,
435 download_handler: None,
436 file_chooser_handler: None,
437 delegate: None,
438 }
439 }
440
441 fn on_scheme<F, Fut>(mut self, scheme: &str, handler: F) -> Self
451 where
452 F: Fn(http::Request<Vec<u8>>) -> Fut + Send + Sync + 'static,
453 Fut: std::future::Future<Output = SchemeOutcome> + Send + 'static,
454 {
455 let normalized = scheme.trim().to_ascii_lowercase();
456 if !normalized.is_empty() {
457 self.scheme_handlers.insert(
458 normalized,
459 Arc::new(move |req| {
460 let fut = handler(req);
461 Box::pin(fut)
462 }),
463 );
464 }
465 self
466 }
467
468 fn on_navigation<F>(mut self, handler: F) -> Self
471 where
472 F: Fn(&NavigationRequest) -> NavigationPolicy + Send + Sync + 'static,
473 {
474 self.navigation_handler = Some(Box::new(handler));
475 self
476 }
477
478 fn on_new_window<F>(mut self, handler: F) -> Self
481 where
482 F: Fn(&str) -> NewWindowPolicy + Send + Sync + 'static,
483 {
484 self.new_window_handler = Some(Box::new(handler));
485 self
486 }
487
488 fn on_download<F>(mut self, handler: F) -> Self
498 where
499 F: Fn(DownloadRequest) + Send + Sync + 'static,
500 {
501 self.download_handler = Some(Box::new(handler));
502 self
503 }
504
505 fn on_file_chooser<F, Fut>(mut self, handler: F) -> Self
506 where
507 F: Fn(FileChooserRequest) -> Fut + Send + Sync + 'static,
508 Fut: Future<Output = FileChooserResponse> + Send + 'static,
509 {
510 self.file_chooser_handler = Some(Box::new(move |request| Box::pin(handler(request))));
511 self
512 }
513
514 fn delegate(mut self, delegate: Arc<dyn WebViewDelegate>) -> Self {
515 self.delegate = Some(delegate);
516 self
517 }
518
519 fn data_mode(mut self, data_mode: WebViewDataMode) -> Self {
520 self.data_mode = data_mode;
521 self
522 }
523
524 pub(crate) fn normalize(
525 self,
526 ) -> Result<(EffectiveWebViewCreateOptions, PendingCallbacks), WebViewError> {
527 if self.profile != SecurityProfile::BrowserRelaxed && self.download_handler.is_some() {
528 return Err(WebViewError::InvalidCreateOptions(
529 "download callback is only supported in browser profile; use WebViewBuilder::browser(webtag).on_download(...).create()".to_string(),
530 ));
531 }
532 if self.scheme_handlers.contains_key(APPLE_INTERNAL_SCHEME) {
533 return Err(WebViewError::InvalidCreateOptions(format!(
534 "scheme '{APPLE_INTERNAL_SCHEME}' is reserved for LingXia Apple bridge transport"
535 )));
536 }
537 let mut registered_schemes: Vec<String> = self.scheme_handlers.keys().cloned().collect();
538 registered_schemes.sort_unstable();
539 registered_schemes.dedup();
540 let effective = EffectiveWebViewCreateOptions {
541 profile: self.profile,
542 data_mode: self.data_mode,
543 registered_schemes,
544 has_navigation_handler: self.navigation_handler.is_some(),
545 has_new_window_handler: self.new_window_handler.is_some(),
546 has_download_handler: self.download_handler.is_some(),
547 has_file_chooser_handler: self.file_chooser_handler.is_some(),
548 has_delegate: self.delegate.is_some(),
549 };
550 let pending = PendingCallbacks {
551 scheme_handlers: self.scheme_handlers,
552 navigation_handler: self.navigation_handler,
553 new_window_handler: self.new_window_handler,
554 download_handler: self.download_handler,
555 file_chooser_handler: self.file_chooser_handler,
556 delegate: self.delegate,
557 };
558 Ok((effective, pending))
559 }
560}
561
562pub struct WebViewBuilder;
570
571#[must_use = "call .create() to start WebView creation"]
572pub struct StrictWebViewBuilder {
573 webtag: WebTag,
574 options: WebViewCreateOptions,
575}
576
577#[must_use = "call .create() to start WebView creation"]
578pub struct BrowserWebViewBuilder {
579 webtag: WebTag,
580 options: WebViewCreateOptions,
581}
582
583impl WebViewBuilder {
584 #[must_use = "call .create() to start WebView creation"]
586 pub fn strict(webtag: WebTag) -> StrictWebViewBuilder {
587 StrictWebViewBuilder {
588 webtag,
589 options: WebViewCreateOptions::strict(),
590 }
591 }
592
593 #[must_use = "call .create() to start WebView creation"]
595 pub fn browser(webtag: WebTag) -> BrowserWebViewBuilder {
596 BrowserWebViewBuilder {
597 webtag,
598 options: WebViewCreateOptions::browser(),
599 }
600 }
601}
602
603impl StrictWebViewBuilder {
604 pub fn delegate(mut self, delegate: Arc<dyn WebViewDelegate>) -> Self {
608 self.options = self.options.delegate(delegate);
609 self
610 }
611
612 pub fn data_mode(mut self, data_mode: WebViewDataMode) -> Self {
614 self.options = self.options.data_mode(data_mode);
615 self
616 }
617
618 pub fn on_scheme<F, Fut>(mut self, scheme: &str, handler: F) -> Self
619 where
620 F: Fn(http::Request<Vec<u8>>) -> Fut + Send + Sync + 'static,
621 Fut: std::future::Future<Output = SchemeOutcome> + Send + 'static,
622 {
623 self.options = self.options.on_scheme(scheme, handler);
624 self
625 }
626
627 pub fn on_navigation<F>(mut self, handler: F) -> Self
628 where
629 F: Fn(&NavigationRequest) -> NavigationPolicy + Send + Sync + 'static,
630 {
631 self.options = self.options.on_navigation(handler);
632 self
633 }
634
635 pub fn on_new_window<F>(mut self, handler: F) -> Self
636 where
637 F: Fn(&str) -> NewWindowPolicy + Send + Sync + 'static,
638 {
639 self.options = self.options.on_new_window(handler);
640 self
641 }
642
643 pub fn on_file_chooser<F, Fut>(mut self, handler: F) -> Self
644 where
645 F: Fn(FileChooserRequest) -> Fut + Send + Sync + 'static,
646 Fut: Future<Output = FileChooserResponse> + Send + 'static,
647 {
648 self.options = self.options.on_file_chooser(handler);
649 self
650 }
651
652 pub fn create(self) -> WebViewSession {
659 create_webview_session(self.webtag, self.options)
660 }
661}
662
663impl BrowserWebViewBuilder {
664 pub fn delegate(mut self, delegate: Arc<dyn WebViewDelegate>) -> Self {
668 self.options = self.options.delegate(delegate);
669 self
670 }
671
672 pub fn on_scheme<F, Fut>(mut self, scheme: &str, handler: F) -> Self
673 where
674 F: Fn(http::Request<Vec<u8>>) -> Fut + Send + Sync + 'static,
675 Fut: std::future::Future<Output = SchemeOutcome> + Send + 'static,
676 {
677 self.options = self.options.on_scheme(scheme, handler);
678 self
679 }
680
681 pub fn on_navigation<F>(mut self, handler: F) -> Self
682 where
683 F: Fn(&NavigationRequest) -> NavigationPolicy + Send + Sync + 'static,
684 {
685 self.options = self.options.on_navigation(handler);
686 self
687 }
688
689 pub fn on_new_window<F>(mut self, handler: F) -> Self
690 where
691 F: Fn(&str) -> NewWindowPolicy + Send + Sync + 'static,
692 {
693 self.options = self.options.on_new_window(handler);
694 self
695 }
696
697 pub fn on_download<F>(mut self, handler: F) -> Self
702 where
703 F: Fn(DownloadRequest) + Send + Sync + 'static,
704 {
705 self.options = self.options.on_download(handler);
706 self
707 }
708
709 pub fn data_mode(mut self, data_mode: WebViewDataMode) -> Self {
711 self.options = self.options.data_mode(data_mode);
712 self
713 }
714
715 pub fn on_file_chooser<F, Fut>(mut self, handler: F) -> Self
716 where
717 F: Fn(FileChooserRequest) -> Fut + Send + Sync + 'static,
718 Fut: Future<Output = FileChooserResponse> + Send + 'static,
719 {
720 self.options = self.options.on_file_chooser(handler);
721 self
722 }
723
724 pub fn create(self) -> WebViewSession {
731 create_webview_session(self.webtag, self.options)
732 }
733}
734
735pub(crate) struct PendingCallbacks {
738 pub(crate) scheme_handlers: HashMap<String, AsyncSchemeHandler>,
739 pub(crate) navigation_handler: Option<NavigationHandler>,
740 pub(crate) new_window_handler: Option<NewWindowHandler>,
741 pub(crate) download_handler: Option<DownloadHandler>,
742 pub(crate) file_chooser_handler: Option<FileChooserHandler>,
743 pub(crate) delegate: Option<Arc<dyn WebViewDelegate>>,
744}
745
746impl PendingCallbacks {
747 fn has_any(&self) -> bool {
748 !self.scheme_handlers.is_empty()
749 || self.navigation_handler.is_some()
750 || self.new_window_handler.is_some()
751 || self.download_handler.is_some()
752 || self.file_chooser_handler.is_some()
753 || self.delegate.is_some()
754 }
755}
756
757pub struct WebView {
759 pub(crate) inner: WebViewInner,
760 effective_options: EffectiveWebViewCreateOptions,
761 delegate: RwLock<Option<Arc<dyn WebViewDelegate>>>,
763 scheme_handlers: RwLock<HashMap<String, AsyncSchemeHandler>>,
765 navigation_handler: RwLock<Option<NavigationHandler>>,
766 new_window_handler: RwLock<Option<NewWindowHandler>>,
767 download_handler: RwLock<Option<DownloadHandler>>,
768 file_chooser_handler: RwLock<Option<FileChooserHandler>>,
769}
770
771impl WebView {
772 pub(crate) fn new(
773 inner: WebViewInner,
774 effective_options: EffectiveWebViewCreateOptions,
775 ) -> Self {
776 Self {
777 inner,
778 effective_options,
779 delegate: RwLock::new(None),
780 scheme_handlers: RwLock::new(HashMap::new()),
781 navigation_handler: RwLock::new(None),
782 new_window_handler: RwLock::new(None),
783 download_handler: RwLock::new(None),
784 file_chooser_handler: RwLock::new(None),
785 }
786 }
787
788 pub fn appid(&self) -> String {
790 self.inner.webtag.extract_appid()
791 }
792
793 pub fn path(&self) -> String {
795 self.inner.webtag.extract_parts().1
796 }
797
798 pub fn webtag(&self) -> WebTag {
800 self.inner.webtag.clone()
801 }
802
803 pub(crate) fn effective_options(&self) -> &EffectiveWebViewCreateOptions {
804 &self.effective_options
805 }
806
807 pub(crate) fn get_delegate(&self) -> Option<Arc<dyn WebViewDelegate>> {
809 self.delegate.read().ok().and_then(|guard| guard.clone())
810 }
811
812 pub(crate) fn remove_delegate(&self) {
814 if let Ok(mut guard) = self.delegate.write() {
815 *guard = None;
816 }
817 }
818
819 pub(crate) fn install_callbacks(&self, callbacks: PendingCallbacks) {
821 if let Some(delegate) = callbacks.delegate
822 && let Ok(mut guard) = self.delegate.write()
823 {
824 *guard = Some(delegate);
825 }
826 if let Ok(mut guard) = self.scheme_handlers.write() {
827 *guard = callbacks.scheme_handlers;
828 }
829 if let Some(handler) = callbacks.navigation_handler
830 && let Ok(mut guard) = self.navigation_handler.write()
831 {
832 *guard = Some(handler);
833 }
834 if let Some(handler) = callbacks.new_window_handler
835 && let Ok(mut guard) = self.new_window_handler.write()
836 {
837 *guard = Some(handler);
838 }
839 if let Some(handler) = callbacks.download_handler
840 && let Ok(mut guard) = self.download_handler.write()
841 {
842 *guard = Some(handler);
843 }
844 if let Some(handler) = callbacks.file_chooser_handler
845 && let Ok(mut guard) = self.file_chooser_handler.write()
846 {
847 *guard = Some(handler);
848 }
849 }
850
851 pub fn has_scheme_handler(&self, scheme: &str) -> bool {
853 self.scheme_handlers
854 .read()
855 .ok()
856 .is_some_and(|guard| guard.contains_key(scheme))
857 }
858
859 pub(crate) fn handle_scheme_request(
862 &self,
863 scheme: &str,
864 request: http::Request<Vec<u8>>,
865 ) -> Option<WebResourceResponse> {
866 #[cfg(any(target_os = "ios", target_os = "macos"))]
867 if let Some(response) = self.inner.handle_internal_bridge_request(&request) {
868 return Some(response);
869 }
870
871 let guard = self.scheme_handlers.read().ok()?;
872 let handler = guard.get(scheme)?;
873 let outcome = block_on_scheme_future(handler(request));
874 match outcome {
875 SchemeOutcome::Handled(response) => Some(response),
876 SchemeOutcome::PassThrough => None,
877 }
878 }
879
880 pub fn handle_navigation(&self, request: &NavigationRequest) -> NavigationPolicy {
885 if crate::url_callback::dispatch(&request.url) {
886 return NavigationPolicy::Cancel;
887 }
888 if let Ok(guard) = self.navigation_handler.read()
889 && let Some(handler) = guard.as_ref()
890 {
891 return handler(request);
892 }
893 NavigationPolicy::Allow
894 }
895
896 pub fn has_new_window_handler(&self) -> bool {
898 self.new_window_handler
899 .read()
900 .ok()
901 .is_some_and(|guard| guard.is_some())
902 }
903
904 pub fn handle_new_window(&self, url: &str) -> NewWindowPolicy {
909 if crate::url_callback::dispatch(url) {
910 return NewWindowPolicy::Cancel;
911 }
912 if let Ok(guard) = self.new_window_handler.read()
913 && let Some(handler) = guard.as_ref()
914 {
915 return handler(url);
916 }
917 NewWindowPolicy::Cancel
918 }
919
920 pub(crate) fn handle_download(&self, request: DownloadRequest) {
922 if let Ok(guard) = self.download_handler.read()
923 && let Some(handler) = guard.as_ref()
924 {
925 handler(request);
926 }
927 }
928
929 #[cfg_attr(not(target_os = "windows"), allow(dead_code))]
931 pub(crate) fn has_download_handler(&self) -> bool {
932 self.download_handler
933 .read()
934 .ok()
935 .is_some_and(|guard| guard.is_some())
936 }
937
938 #[cfg_attr(target_os = "windows", allow(dead_code))]
939 pub(crate) fn handle_file_chooser<C>(&self, request: FileChooserRequest, completion: C) -> bool
940 where
941 C: FnOnce(FileChooserResponse) + Send + 'static,
942 {
943 let Some(future) = self.make_file_chooser_future(request) else {
944 return false;
945 };
946 std::thread::spawn(move || {
947 completion(block_on_scheme_future(future));
948 });
949 true
950 }
951
952 #[cfg_attr(target_os = "windows", allow(dead_code))]
953 fn make_file_chooser_future(&self, request: FileChooserRequest) -> Option<FileChooserFuture> {
954 let Ok(guard) = self.file_chooser_handler.read() else {
955 return None;
956 };
957 let handler = guard.as_ref()?;
958 Some(handler(request))
959 }
960
961 #[cfg(target_os = "macos")]
963 pub fn toggle_devtools(&self) {
964 self.inner.toggle_devtools();
965 }
966
967 #[cfg(target_os = "macos")]
969 pub fn toggle_devtools_detached(&self) {
970 self.inner.toggle_devtools_detached();
971 }
972
973 #[cfg(any(target_os = "ios", target_os = "macos"))]
975 pub fn get_swift_webview_ptr(&self) -> usize {
976 self.inner.get_swift_webview_ptr()
977 }
978
979 #[cfg(target_os = "android")]
981 pub fn get_java_webview(&self) -> &jni::objects::Global<jni::objects::JObject<'static>> {
982 self.inner.get_java_webview()
983 }
984
985 pub async fn evaluate_javascript(
986 &self,
987 js: &str,
988 ) -> Result<serde_json::Value, crate::WebViewScriptError> {
989 self.inner.eval_js(js).await
990 }
991
992 #[cfg(any(
1000 target_os = "ios",
1001 target_os = "android",
1002 all(feature = "webview-input", target_os = "macos"),
1003 all(target_os = "linux", target_env = "ohos")
1004 ))]
1005 async fn run_js_action(&self, script: &str) -> Result<(), WebViewInputError> {
1006 let result = self
1007 .inner
1008 .eval_js(script)
1009 .await
1010 .map_err(WebViewInputError::Script)?;
1011 if result.get("ok").and_then(|v| v.as_bool()) == Some(true) {
1012 return Ok(());
1013 }
1014 let err_msg = result
1015 .get("error")
1016 .and_then(|v| v.as_str())
1017 .unwrap_or("input action failed")
1018 .to_string();
1019 if result.get("interactable").and_then(|v| v.as_bool()) == Some(false) {
1020 Err(WebViewInputError::ElementNotInteractable(err_msg))
1021 } else {
1022 Err(WebViewInputError::ElementNotFound(err_msg))
1023 }
1024 }
1025
1026 #[cfg(any(
1032 target_os = "ios",
1033 all(feature = "webview-input", target_os = "macos"),
1034 all(target_os = "linux", target_env = "ohos")
1035 ))]
1036 pub(crate) async fn click_via_js(
1037 &self,
1038 selector: &str,
1039 index: Option<usize>,
1040 ) -> Result<(), WebViewInputError> {
1041 let selector_json = serde_json::to_string(selector)
1042 .map_err(|err| WebViewInputError::Platform(format!("Invalid selector: {err}")))?;
1043 let idx = index.unwrap_or(0);
1044 let script = format!(
1045 "((sel, i) => {{ \
1046 const els = document.querySelectorAll(sel); \
1047 if (!els.length || i < 0 || i >= els.length) return {{ ok:false, error:'no match', count:els.length }}; \
1048 const el = els[i]; \
1049 try {{ el.scrollIntoView({{block:'center', inline:'center'}}); }} catch(_e) {{}} \
1050 const rect = el.getBoundingClientRect(); \
1051 const style = window.getComputedStyle(el); \
1052 const disabled = !!el.disabled || el.getAttribute('aria-disabled') === 'true'; \
1053 const visible = rect.width > 0 && rect.height > 0 && rect.bottom > 0 && rect.right > 0 && \
1054 rect.top < window.innerHeight && rect.left < window.innerWidth && \
1055 style.visibility !== 'hidden' && style.display !== 'none' && Number(style.opacity || '1') !== 0; \
1056 if (!visible) return {{ ok:false, error:'not visible', interactable:false, count:els.length }}; \
1057 if (disabled) return {{ ok:false, error:'not enabled', interactable:false, count:els.length }}; \
1058 const tag = (el.tagName || '').toLowerCase(); \
1059 if (tag.indexOf('lx-') === 0) {{ \
1060 el.setAttribute('focus', 'true'); \
1061 if (typeof el.syncNativeProps === 'function') {{ try {{ el.syncNativeProps(); }} catch(_e) {{}} }} \
1062 return {{ ok:true, count:els.length, native:true }}; \
1063 }} \
1064 if (typeof el.focus === 'function') {{ try {{ el.focus({{preventScroll:true}}); }} catch(_e) {{ try {{ el.focus(); }} catch(__){{}} }} }} \
1065 const opts = {{ bubbles:true, cancelable:true, view:window, clientX: rect.left + rect.width/2, clientY: rect.top + rect.height/2 }}; \
1066 try {{ if (window.PointerEvent) el.dispatchEvent(new PointerEvent('pointerdown', Object.assign({{pointerId:1, isPrimary:true, pointerType:'mouse'}}, opts))); }} catch(_e) {{}} \
1067 try {{ el.dispatchEvent(new MouseEvent('mousedown', opts)); }} catch(_e) {{}} \
1068 try {{ if (window.PointerEvent) el.dispatchEvent(new PointerEvent('pointerup', Object.assign({{pointerId:1, isPrimary:true, pointerType:'mouse'}}, opts))); }} catch(_e) {{}} \
1069 try {{ el.dispatchEvent(new MouseEvent('mouseup', opts)); }} catch(_e) {{}} \
1070 try {{ el.dispatchEvent(new MouseEvent('click', opts)); }} catch(_e) {{}} \
1071 return {{ ok:true, count:els.length }}; \
1072 }})({selector_json}, {idx})"
1073 );
1074 self.run_js_action(&script).await
1075 }
1076
1077 #[cfg(any(
1081 target_os = "ios",
1082 target_os = "android",
1083 all(feature = "webview-input", target_os = "macos"),
1084 all(target_os = "linux", target_env = "ohos")
1085 ))]
1086 pub(crate) async fn type_via_js(
1087 &self,
1088 selector: &str,
1089 index: Option<usize>,
1090 text: &str,
1091 replace: bool,
1092 ) -> Result<(), WebViewInputError> {
1093 let selector_json = serde_json::to_string(selector)
1094 .map_err(|err| WebViewInputError::Platform(format!("Invalid selector: {err}")))?;
1095 let text_json = serde_json::to_string(text)
1096 .map_err(|err| WebViewInputError::Platform(format!("Invalid text: {err}")))?;
1097 let idx = index.unwrap_or(0);
1098 let script = format!(
1099 "((sel, i, text, replace) => {{ \
1100 const els = document.querySelectorAll(sel); \
1101 if (!els.length || i < 0 || i >= els.length) return {{ ok:false, error:'no match', count:els.length }}; \
1102 const el = els[i]; \
1103 try {{ el.scrollIntoView({{block:'center', inline:'center'}}); }} catch(_e) {{}} \
1104 if (typeof el.focus === 'function') {{ try {{ el.focus({{preventScroll:true}}); }} catch(_e) {{ try {{ el.focus(); }} catch(__){{}} }} }} \
1105 const tag = (el.tagName || '').toLowerCase(); \
1106 if (tag === 'input' || tag === 'textarea') {{ \
1107 const proto = tag === 'textarea' ? window.HTMLTextAreaElement.prototype : window.HTMLInputElement.prototype; \
1108 const desc = Object.getOwnPropertyDescriptor(proto, 'value'); \
1109 const next = (replace ? '' : (el.value || '')) + text; \
1110 if (desc && desc.set) {{ desc.set.call(el, next); }} else {{ el.value = next; }} \
1111 el.dispatchEvent(new InputEvent('input', {{ bubbles:true, cancelable:true, data:text, inputType:'insertText' }})); \
1112 el.dispatchEvent(new Event('change', {{ bubbles:true }})); \
1113 return {{ ok:true, count:els.length }}; \
1114 }} \
1115 if (el.isContentEditable) {{ \
1116 el.textContent = (replace ? '' : (el.textContent || '')) + text; \
1117 el.dispatchEvent(new InputEvent('input', {{ bubbles:true, data:text, inputType:'insertText' }})); \
1118 return {{ ok:true, count:els.length }}; \
1119 }} \
1120 if (tag.indexOf('lx-') === 0) {{ \
1121 try {{ el.value = (replace ? '' : (el.value || '')) + text; }} catch(_e) {{}} \
1122 if (typeof el.syncNativeProps === 'function') {{ try {{ el.syncNativeProps(); }} catch(_e) {{}} }} \
1123 el.dispatchEvent(new Event('input', {{ bubbles:true }})); \
1124 return {{ ok:true, count:els.length, native:true }}; \
1125 }} \
1126 return {{ ok:false, error:'not editable', interactable:false, count:els.length }}; \
1127 }})({selector_json}, {idx}, {text_json}, {replace})"
1128 );
1129 self.run_js_action(&script).await
1130 }
1131
1132 #[cfg(any(
1134 target_os = "ios",
1135 target_os = "android",
1136 all(feature = "webview-input", target_os = "macos"),
1137 all(target_os = "linux", target_env = "ohos")
1138 ))]
1139 pub(crate) async fn press_via_js(
1140 &self,
1141 key: &str,
1142 selector: Option<&str>,
1143 index: Option<usize>,
1144 ) -> Result<(), WebViewInputError> {
1145 let key_json = serde_json::to_string(key)
1146 .map_err(|err| WebViewInputError::Platform(format!("Invalid key: {err}")))?;
1147 let selector_json = serde_json::to_string(&selector)
1148 .map_err(|err| WebViewInputError::Platform(format!("Invalid selector: {err}")))?;
1149 let idx = index.unwrap_or(0);
1150 let script = format!(
1151 "((key, sel, i) => {{ \
1152 const els = sel === null ? null : document.querySelectorAll(sel); \
1153 if (els && (!els.length || i < 0 || i >= els.length)) return {{ ok:false, error:'no match', count:els.length }}; \
1154 const el = els ? els[i] : (document.activeElement || document.body); \
1155 if (els) {{ \
1156 try {{ el.scrollIntoView({{block:'center', inline:'center'}}); }} catch(_e) {{}} \
1157 if (typeof el.focus === 'function') {{ try {{ el.focus({{preventScroll:true}}); }} catch(_e) {{ try {{ el.focus(); }} catch(__){{}} }} }} \
1158 }} \
1159 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' }}; \
1160 const norm = String(key).toLowerCase(); \
1161 const k = map[norm] || key; \
1162 const opts = {{ bubbles:true, cancelable:true, composed:true, key:k, view:window }}; \
1163 el.dispatchEvent(new KeyboardEvent('keydown', opts)); \
1164 el.dispatchEvent(new KeyboardEvent('keyup', opts)); \
1165 return {{ ok:true }}; \
1166 }})({key_json}, {selector_json}, {idx})"
1167 );
1168 self.run_js_action(&script).await
1169 }
1170
1171 #[cfg(any(
1181 target_os = "ios",
1182 all(feature = "webview-input", target_os = "macos"),
1183 all(target_os = "linux", target_env = "ohos")
1184 ))]
1185 pub(crate) async fn scroll_via_js(
1186 &self,
1187 at: Option<(f64, f64)>,
1188 dx: f64,
1189 dy: f64,
1190 ) -> Result<(), WebViewInputError> {
1191 let (px, py) = at.unwrap_or((-1.0, -1.0));
1192 let script = format!(
1193 "((px, py, dx, dy) => {{ \
1194 const overflows = (v) => (/(auto|scroll|overlay)/).test(v); \
1195 const ancestor = (node) => {{ \
1196 while (node && node !== document.body && node !== document.documentElement) {{ \
1197 const s = window.getComputedStyle(node); \
1198 if ((overflows(s.overflowY) && node.scrollHeight > node.clientHeight) || \
1199 (overflows(s.overflowX) && node.scrollWidth > node.clientWidth)) return node; \
1200 node = node.parentElement; \
1201 }} \
1202 return null; \
1203 }}; \
1204 const largest = () => {{ \
1205 let best = null, range = 0; \
1206 const all = document.querySelectorAll('*'); \
1207 for (let k = 0; k < all.length; k++) {{ \
1208 const n = all[k], s = window.getComputedStyle(n); \
1209 const ry = overflows(s.overflowY) ? (n.scrollHeight - n.clientHeight) : 0; \
1210 const rx = overflows(s.overflowX) ? (n.scrollWidth - n.clientWidth) : 0; \
1211 const r = ry > rx ? ry : rx; \
1212 if (r > range) {{ range = r; best = n; }} \
1213 }} \
1214 return best; \
1215 }}; \
1216 const vw = window.innerWidth || document.documentElement.clientWidth || 0; \
1217 const vh = window.innerHeight || document.documentElement.clientHeight || 0; \
1218 let target = null; \
1219 if (px >= 0 && py >= 0) target = ancestor(document.elementFromPoint(px, py) || document.body); \
1220 else if (vw > 0 && vh > 0) target = ancestor(document.elementFromPoint(vw >> 1, vh >> 1) || document.body); \
1221 if (!target) {{ \
1222 const se = document.scrollingElement || document.documentElement; \
1223 target = (se && se.scrollHeight > se.clientHeight) ? se : (largest() || se); \
1224 }} \
1225 target.scrollLeft += dx; target.scrollTop += dy; \
1226 return {{ ok:true }}; \
1227 }})({px}, {py}, {dx}, {dy})"
1228 );
1229 self.run_js_action(&script).await
1230 }
1231
1232 #[cfg(any(
1234 target_os = "ios",
1235 all(feature = "webview-input", target_os = "macos"),
1236 all(target_os = "linux", target_env = "ohos")
1237 ))]
1238 pub(crate) async fn scroll_to_via_js(
1239 &self,
1240 selector: &str,
1241 index: Option<usize>,
1242 ) -> Result<(), WebViewInputError> {
1243 let selector_json = serde_json::to_string(selector)
1244 .map_err(|err| WebViewInputError::Platform(format!("Invalid selector: {err}")))?;
1245 let idx = index.unwrap_or(0);
1246 let script = format!(
1247 "((sel, i) => {{ \
1248 const els = document.querySelectorAll(sel); \
1249 if (!els.length || i < 0 || i >= els.length) return {{ ok:false, error:'no match', count:els.length }}; \
1250 try {{ els[i].scrollIntoView({{ block:'center', inline:'center' }}); }} catch(_e) {{ els[i].scrollIntoView(); }} \
1251 return {{ ok:true, count:els.length }}; \
1252 }})({selector_json}, {idx})"
1253 );
1254 self.run_js_action(&script).await
1255 }
1256
1257 pub async fn current_url(&self) -> Result<Option<String>, WebViewError> {
1258 self.inner.current_url().await
1259 }
1260
1261 pub fn reload(&self) -> Result<(), WebViewError> {
1262 self.inner.reload()
1263 }
1264
1265 pub fn go_back(&self) -> Result<(), WebViewError> {
1266 self.inner.go_back()
1267 }
1268
1269 pub fn go_forward(&self) -> Result<(), WebViewError> {
1270 self.inner.go_forward()
1271 }
1272
1273 pub async fn list_cookies(&self) -> Result<Vec<WebViewCookie>, WebViewError> {
1274 self.inner.list_cookies().await
1275 }
1276
1277 pub async fn set_cookie(&self, request: WebViewCookieSetRequest) -> Result<(), WebViewError> {
1278 self.inner.set_cookie(request).await
1279 }
1280
1281 pub async fn delete_cookie(
1282 &self,
1283 name: &str,
1284 domain: &str,
1285 path: &str,
1286 ) -> Result<(), WebViewError> {
1287 self.inner.delete_cookie(name, domain, path).await
1288 }
1289
1290 pub async fn clear_cookies(&self) -> Result<(), WebViewError> {
1291 self.inner.clear_cookies().await
1292 }
1293
1294 pub async fn start_network_capture(&self) -> Result<(), WebViewError> {
1295 self.inner.start_network_capture().await
1296 }
1297
1298 pub async fn stop_network_capture(&self) -> Result<(), WebViewError> {
1299 self.inner.stop_network_capture().await
1300 }
1301
1302 pub async fn network_entries(&self) -> Result<NetworkCaptureSnapshot, WebViewError> {
1303 self.inner.network_entries().await
1304 }
1305
1306 pub async fn clear_network_capture(&self) -> Result<(), WebViewError> {
1307 self.inner.clear_network_capture().await
1308 }
1309
1310 pub async fn take_screenshot(&self) -> Result<Vec<u8>, WebViewError> {
1311 self.inner.take_screenshot().await
1312 }
1313
1314 pub async fn click(
1315 &self,
1316 selector: &str,
1317 options: ClickOptions,
1318 ) -> Result<(), WebViewInputError> {
1319 <Self as WebViewInputController>::click(self, selector, options).await
1320 }
1321
1322 pub async fn type_text(
1323 &self,
1324 selector: &str,
1325 text: &str,
1326 options: TypeOptions,
1327 ) -> Result<(), WebViewInputError> {
1328 <Self as WebViewInputController>::type_text(self, selector, text, options).await
1329 }
1330
1331 pub async fn fill(
1332 &self,
1333 selector: &str,
1334 text: &str,
1335 options: FillOptions,
1336 ) -> Result<(), WebViewInputError> {
1337 <Self as WebViewInputController>::fill(self, selector, text, options).await
1338 }
1339
1340 pub async fn press(&self, key: &str, options: PressOptions) -> Result<(), WebViewInputError> {
1341 <Self as WebViewInputController>::press(self, key, options).await
1342 }
1343
1344 pub async fn scroll(
1345 &self,
1346 dx: f64,
1347 dy: f64,
1348 options: ScrollOptions,
1349 ) -> Result<(), WebViewInputError> {
1350 <Self as WebViewInputController>::scroll(self, dx, dy, options).await
1351 }
1352
1353 pub async fn scroll_to(
1354 &self,
1355 selector: &str,
1356 options: ScrollOptions,
1357 ) -> Result<(), WebViewInputError> {
1358 <Self as WebViewInputController>::scroll_to(self, selector, options).await
1359 }
1360}
1361
1362#[async_trait]
1363impl WebViewController for WebView {
1364 fn load_url(&self, url: &str) -> Result<(), WebViewError> {
1365 self.inner.load_url(url)
1366 }
1367
1368 fn load_data(&self, request: LoadDataRequest<'_>) -> Result<(), WebViewError> {
1369 self.inner.load_data(request)
1370 }
1371
1372 fn exec_js(&self, js: &str) -> Result<(), WebViewError> {
1373 self.inner.exec_js(js)
1374 }
1375
1376 async fn eval_js(&self, js: &str) -> Result<serde_json::Value, WebViewScriptError> {
1377 self.inner.eval_js(js).await
1378 }
1379
1380 async fn current_url(&self) -> Result<Option<String>, WebViewError> {
1381 self.inner.current_url().await
1382 }
1383
1384 fn post_message(&self, message: &str) -> Result<(), WebViewError> {
1385 self.inner.post_message(message)
1386 }
1387
1388 fn clear_browsing_data(&self) -> Result<(), WebViewError> {
1389 self.inner.clear_browsing_data()
1390 }
1391
1392 fn set_user_agent_override(&self, user_agent: UserAgentOverride) -> Result<(), WebViewError> {
1393 user_agent.validate()?;
1394 self.inner.set_user_agent_override(user_agent)
1395 }
1396
1397 fn reload(&self) -> Result<(), WebViewError> {
1398 self.inner.reload()
1399 }
1400
1401 fn go_back(&self) -> Result<(), WebViewError> {
1402 self.inner.go_back()
1403 }
1404
1405 fn go_forward(&self) -> Result<(), WebViewError> {
1406 self.inner.go_forward()
1407 }
1408
1409 async fn list_cookies(&self) -> Result<Vec<WebViewCookie>, WebViewError> {
1410 self.inner.list_cookies().await
1411 }
1412
1413 async fn set_cookie(&self, request: WebViewCookieSetRequest) -> Result<(), WebViewError> {
1414 self.inner.set_cookie(request).await
1415 }
1416
1417 async fn delete_cookie(
1418 &self,
1419 name: &str,
1420 domain: &str,
1421 path: &str,
1422 ) -> Result<(), WebViewError> {
1423 self.inner.delete_cookie(name, domain, path).await
1424 }
1425
1426 async fn clear_cookies(&self) -> Result<(), WebViewError> {
1427 self.inner.clear_cookies().await
1428 }
1429
1430 async fn clear_site_data(
1431 &self,
1432 url: &str,
1433 options: ClearSiteDataOptions,
1434 ) -> Result<ClearSiteDataResult, WebViewError> {
1435 self.inner.clear_site_data(url, options).await
1436 }
1437
1438 async fn take_screenshot(&self) -> Result<Vec<u8>, WebViewError> {
1443 self.inner.take_screenshot().await
1444 }
1445
1446 async fn start_network_capture(&self) -> Result<(), WebViewError> {
1447 self.inner.start_network_capture().await
1448 }
1449
1450 async fn stop_network_capture(&self) -> Result<(), WebViewError> {
1451 self.inner.stop_network_capture().await
1452 }
1453
1454 async fn network_entries(&self) -> Result<NetworkCaptureSnapshot, WebViewError> {
1455 self.inner.network_entries().await
1456 }
1457
1458 async fn clear_network_capture(&self) -> Result<(), WebViewError> {
1459 self.inner.clear_network_capture().await
1460 }
1461}
1462
1463#[async_trait]
1464impl WebViewInputController for WebView {
1465 async fn click(
1466 &self,
1467 _selector: &str,
1468 _options: ClickOptions,
1469 ) -> Result<(), WebViewInputError> {
1470 #[cfg(all(feature = "webview-input", target_os = "macos"))]
1476 {
1477 return self.click_via_js(_selector, _options.index).await;
1478 }
1479 #[cfg(all(feature = "webview-input", target_os = "windows"))]
1480 {
1481 return self.inner.click_inner(_selector, _options).await;
1482 }
1483 #[cfg(target_os = "android")]
1484 {
1485 return self.inner.click_inner(_selector, _options).await;
1486 }
1487 #[cfg(any(target_os = "ios", all(target_os = "linux", target_env = "ohos")))]
1488 {
1489 return self.click_via_js(_selector, _options.index).await;
1490 }
1491 #[allow(unreachable_code)]
1492 Err(WebViewInputError::Unsupported(
1493 "input control is not implemented for this platform",
1494 ))
1495 }
1496
1497 async fn type_text(
1498 &self,
1499 _selector: &str,
1500 _text: &str,
1501 _options: TypeOptions,
1502 ) -> Result<(), WebViewInputError> {
1503 #[cfg(all(feature = "webview-input", target_os = "macos"))]
1504 {
1505 if self.inner.is_window_attached().await {
1506 return self.inner.type_text_inner(_selector, _text, _options).await;
1507 }
1508 return self
1509 .type_via_js(_selector, _options.index, _text, _options.replace)
1510 .await;
1511 }
1512 #[cfg(all(feature = "webview-input", target_os = "windows"))]
1513 {
1514 return self.inner.type_text_inner(_selector, _text, _options).await;
1515 }
1516 #[cfg(any(
1517 target_os = "ios",
1518 target_os = "android",
1519 all(target_os = "linux", target_env = "ohos")
1520 ))]
1521 {
1522 return self
1523 .type_via_js(_selector, _options.index, _text, _options.replace)
1524 .await;
1525 }
1526 #[allow(unreachable_code)]
1527 Err(WebViewInputError::Unsupported(
1528 "input control is not implemented for this platform",
1529 ))
1530 }
1531
1532 async fn fill(
1533 &self,
1534 _selector: &str,
1535 _text: &str,
1536 _options: FillOptions,
1537 ) -> Result<(), WebViewInputError> {
1538 #[cfg(all(feature = "webview-input", target_os = "macos"))]
1539 {
1540 return self
1545 .type_via_js(_selector, _options.index, _text, true)
1546 .await;
1547 }
1548 #[cfg(all(feature = "webview-input", target_os = "windows"))]
1549 {
1550 return self
1551 .inner
1552 .type_text_inner(
1553 _selector,
1554 _text,
1555 TypeOptions {
1556 index: _options.index,
1557 replace: true,
1558 },
1559 )
1560 .await;
1561 }
1562 #[cfg(any(
1563 target_os = "ios",
1564 target_os = "android",
1565 all(target_os = "linux", target_env = "ohos")
1566 ))]
1567 {
1568 return self
1569 .type_via_js(_selector, _options.index, _text, true)
1570 .await;
1571 }
1572 #[allow(unreachable_code)]
1573 Err(WebViewInputError::Unsupported(
1574 "input control is not implemented for this platform",
1575 ))
1576 }
1577
1578 async fn press(&self, _key: &str, _options: PressOptions) -> Result<(), WebViewInputError> {
1579 if _options.index.is_some() && _options.selector.is_none() {
1580 return Err(WebViewInputError::Platform(
1581 "press index requires a selector".to_string(),
1582 ));
1583 }
1584 #[cfg(all(feature = "webview-input", target_os = "macos"))]
1585 {
1586 if self.inner.is_window_attached().await {
1587 return self.inner.press_inner(_key, _options).await;
1588 }
1589 return self
1590 .press_via_js(_key, _options.selector.as_deref(), _options.index)
1591 .await;
1592 }
1593 #[cfg(all(feature = "webview-input", target_os = "windows"))]
1594 {
1595 return self.inner.press_inner(_key, _options).await;
1596 }
1597 #[cfg(any(
1598 target_os = "ios",
1599 target_os = "android",
1600 all(target_os = "linux", target_env = "ohos")
1601 ))]
1602 {
1603 return self
1604 .press_via_js(_key, _options.selector.as_deref(), _options.index)
1605 .await;
1606 }
1607 #[allow(unreachable_code)]
1608 Err(WebViewInputError::Unsupported(
1609 "input control is not implemented for this platform",
1610 ))
1611 }
1612
1613 async fn scroll(
1614 &self,
1615 _dx: f64,
1616 _dy: f64,
1617 _options: ScrollOptions,
1618 ) -> Result<(), WebViewInputError> {
1619 #[cfg(all(feature = "webview-input", target_os = "macos"))]
1622 {
1623 if self.inner.is_window_attached().await {
1624 return self.inner.scroll_inner(_dx, _dy, _options).await;
1625 }
1626 return self.scroll_via_js(None, _dx, _dy).await;
1627 }
1628 #[cfg(all(feature = "webview-input", target_os = "windows"))]
1629 {
1630 return self.inner.scroll_inner(_dx, _dy, _options).await;
1631 }
1632 #[cfg(target_os = "android")]
1635 {
1636 return self.inner.scroll_inner(_dx, _dy, _options).await;
1637 }
1638 #[cfg(any(target_os = "ios", all(target_os = "linux", target_env = "ohos")))]
1640 {
1641 return self.scroll_via_js(None, _dx, _dy).await;
1642 }
1643 #[allow(unreachable_code)]
1644 Err(WebViewInputError::Unsupported(
1645 "input control is not implemented for this platform",
1646 ))
1647 }
1648
1649 async fn scroll_to(
1650 &self,
1651 _selector: &str,
1652 _options: ScrollOptions,
1653 ) -> Result<(), WebViewInputError> {
1654 #[cfg(all(feature = "webview-input", target_os = "macos"))]
1655 {
1656 if self.inner.is_window_attached().await {
1657 return self.inner.scroll_to_inner(_selector, _options).await;
1658 }
1659 return self.scroll_to_via_js(_selector, None).await;
1660 }
1661 #[cfg(all(feature = "webview-input", target_os = "windows"))]
1662 {
1663 return self.inner.scroll_to_inner(_selector, _options).await;
1664 }
1665 #[cfg(target_os = "android")]
1666 {
1667 return self.inner.scroll_to_inner(_selector, _options).await;
1668 }
1669 #[cfg(any(target_os = "ios", all(target_os = "linux", target_env = "ohos")))]
1670 {
1671 return self.scroll_to_via_js(_selector, None).await;
1672 }
1673 #[allow(unreachable_code)]
1674 Err(WebViewInputError::Unsupported(
1675 "input control is not implemented for this platform",
1676 ))
1677 }
1678}
1679
1680type WebViewInstancesMap = Arc<Mutex<HashMap<String, Arc<WebView>>>>;
1682
1683#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1684#[serde(rename_all = "snake_case")]
1685pub enum WebViewCreateStage {
1686 Requested,
1687 NativeCreated,
1688 ControllerAttached,
1689 Ready,
1690 Destroyed,
1691}
1692
1693#[derive(Debug, Clone, PartialEq, Eq)]
1694pub enum WebViewEvent {
1695 Stage(WebViewCreateStage),
1696 Failed {
1697 stage: WebViewCreateStage,
1698 error: WebViewError,
1699 },
1700}
1701
1702type WebViewReadyState = Option<Result<Arc<WebView>, WebViewError>>;
1703
1704#[derive(Clone)]
1705pub struct WebViewEventSubscription {
1706 rx: watch::Receiver<WebViewEvent>,
1707}
1708
1709impl WebViewEventSubscription {
1710 pub fn current(&self) -> WebViewEvent {
1711 self.rx.borrow().clone()
1712 }
1713
1714 pub async fn changed(&mut self) -> Result<WebViewEvent, WebViewError> {
1715 self.rx.changed().await.map_err(|_| {
1716 WebViewError::WebView("webview event channel unexpectedly closed".to_string())
1717 })?;
1718 Ok(self.current())
1719 }
1720}
1721
1722#[derive(Clone)]
1723pub struct WebViewSession {
1724 webtag: WebTag,
1725 event_rx: watch::Receiver<WebViewEvent>,
1726 ready_rx: watch::Receiver<WebViewReadyState>,
1727 signals: Arc<WebViewSessionSignals>,
1728}
1729
1730impl WebViewSession {
1731 pub fn webtag(&self) -> &WebTag {
1732 &self.webtag
1733 }
1734
1735 pub fn subscribe_events(&self) -> WebViewEventSubscription {
1736 WebViewEventSubscription {
1737 rx: self.event_rx.clone(),
1738 }
1739 }
1740
1741 pub fn current_event(&self) -> WebViewEvent {
1742 self.event_rx.borrow().clone()
1743 }
1744
1745 pub async fn wait_ready(&self) -> Result<Arc<WebView>, WebViewError> {
1746 let mut rx = self.ready_rx.clone();
1747 loop {
1748 if let Some(result) = self.signals.terminal_result() {
1749 return result;
1750 }
1751 if let Some(result) = rx.borrow().clone() {
1752 return result;
1753 }
1754 if rx.changed().await.is_err() {
1755 if let Some(result) = self.signals.terminal_result() {
1756 return result;
1757 }
1758 return Err(WebViewError::WebView(
1759 "webview ready channel unexpectedly closed".to_string(),
1760 ));
1761 }
1762 }
1763 }
1764}
1765
1766struct WebViewSessionSignals {
1767 event_tx: watch::Sender<WebViewEvent>,
1768 ready_tx: watch::Sender<WebViewReadyState>,
1769 state: Mutex<WebViewSessionState>,
1770}
1771
1772#[derive(Default)]
1773struct WebViewSessionState {
1774 terminal_result: Option<Result<Arc<WebView>, WebViewError>>,
1775 destroyed: bool,
1776}
1777
1778impl WebViewSessionSignals {
1779 fn new() -> Arc<Self> {
1780 let (event_tx, _event_rx) =
1781 watch::channel(WebViewEvent::Stage(WebViewCreateStage::Requested));
1782 let (ready_tx, _ready_rx) = watch::channel(None);
1783 Arc::new(Self {
1784 event_tx,
1785 ready_tx,
1786 state: Mutex::new(WebViewSessionState::default()),
1787 })
1788 }
1789
1790 fn subscribe(self: &Arc<Self>, webtag: WebTag) -> WebViewSession {
1791 WebViewSession {
1792 webtag,
1793 event_rx: self.event_tx.subscribe(),
1794 ready_rx: self.ready_tx.subscribe(),
1795 signals: Arc::clone(self),
1796 }
1797 }
1798
1799 fn terminal_result(&self) -> Option<Result<Arc<WebView>, WebViewError>> {
1800 let state = lock_or_recover(&self.state, "webview_session_state.terminal_result");
1801 state.terminal_result.clone()
1802 }
1803
1804 #[cfg_attr(not(any(target_os = "macos", target_os = "ios")), allow(dead_code))]
1806 fn is_destroyed(&self) -> bool {
1807 let state = lock_or_recover(&self.state, "webview_session_state.is_destroyed");
1808 state.destroyed
1809 }
1810
1811 fn publish_result(
1812 &self,
1813 result: Result<Arc<WebView>, WebViewError>,
1814 stage_on_error: WebViewCreateStage,
1815 ) {
1816 let mut state = lock_or_recover(&self.state, "webview_session_state.publish_result");
1817 if state.destroyed || state.terminal_result.is_some() {
1818 return;
1819 }
1820 state.terminal_result = Some(result.clone());
1821 drop(state);
1822
1823 match result {
1824 Ok(webview) => {
1825 self.event_tx
1826 .send_replace(WebViewEvent::Stage(WebViewCreateStage::NativeCreated));
1827 self.event_tx
1828 .send_replace(WebViewEvent::Stage(WebViewCreateStage::ControllerAttached));
1829 self.ready_tx.send_replace(Some(Ok(webview)));
1830 self.event_tx
1831 .send_replace(WebViewEvent::Stage(WebViewCreateStage::Ready));
1832 }
1833 Err(error) => {
1834 self.ready_tx.send_replace(Some(Err(error.clone())));
1835 self.event_tx.send_replace(WebViewEvent::Failed {
1836 stage: stage_on_error,
1837 error,
1838 });
1839 }
1840 }
1841 }
1842
1843 fn publish_destroyed(&self) {
1844 let mut state = lock_or_recover(&self.state, "webview_session_state.publish_destroyed");
1845 if state.destroyed {
1846 return;
1847 }
1848 state.destroyed = true;
1849 if state.terminal_result.is_none() {
1850 state.terminal_result = Some(Err(WebViewError::WebView(
1851 "webview destroyed before ready".to_string(),
1852 )));
1853 }
1854 let terminal_result = state.terminal_result.clone();
1855 drop(state);
1856
1857 self.event_tx
1858 .send_replace(WebViewEvent::Stage(WebViewCreateStage::Destroyed));
1859 if let Some(result) = terminal_result {
1860 self.ready_tx.send_replace(Some(result));
1861 }
1862 }
1863}
1864
1865pub(crate) struct WebViewCreateSender {
1866 signals: Arc<WebViewSessionSignals>,
1867}
1868
1869impl WebViewCreateSender {
1870 fn new(signals: Arc<WebViewSessionSignals>) -> Self {
1871 Self { signals }
1872 }
1873
1874 pub(crate) fn succeed(self, webview: Arc<WebView>) {
1875 self.signals
1876 .publish_result(Ok(webview), WebViewCreateStage::Requested);
1877 }
1878
1879 pub(crate) fn fail(self, stage: WebViewCreateStage, error: WebViewError) {
1880 self.signals.publish_result(Err(error), stage);
1881 }
1882
1883 #[cfg_attr(
1888 not(any(target_os = "macos", target_os = "ios", target_os = "windows")),
1889 allow(dead_code)
1890 )]
1891 pub(crate) fn is_destroyed(&self) -> bool {
1892 self.signals.is_destroyed()
1893 }
1894}
1895
1896static WEBVIEW_INSTANCES: OnceLock<WebViewInstancesMap> = OnceLock::new();
1898
1899static PENDING_CALLBACKS: OnceLock<Mutex<HashMap<String, PendingCallbacks>>> = OnceLock::new();
1902static WEBVIEW_SESSIONS: OnceLock<Mutex<HashMap<String, Arc<WebViewSessionSignals>>>> =
1903 OnceLock::new();
1904#[cfg(target_os = "windows")]
1905static WEBVIEW_CREATE_LOCKS: OnceLock<Mutex<HashMap<String, std::sync::Weak<Mutex<()>>>>> =
1906 OnceLock::new();
1907static DESIRED_PROXY_FOR_NEW_WEBVIEWS: OnceLock<RwLock<Option<ProxyConfig>>> = OnceLock::new();
1908static PROXY_APPLY_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
1909
1910fn apply_http_proxy_platform(
1911 config: Option<&ProxyConfig>,
1912) -> Result<ProxyApplyReport, WebViewError> {
1913 #[cfg(target_os = "android")]
1914 {
1915 crate::android::apply_http_proxy(config)
1916 }
1917
1918 #[cfg(any(target_os = "ios", target_os = "macos"))]
1919 {
1920 crate::apple::apply_http_proxy(config)
1921 }
1922
1923 #[cfg(all(target_os = "linux", target_env = "ohos"))]
1924 {
1925 crate::harmony::apply_http_proxy(config)
1926 }
1927
1928 #[cfg(not(any(
1929 target_os = "android",
1930 target_os = "ios",
1931 target_os = "macos",
1932 all(target_os = "linux", target_env = "ohos")
1933 )))]
1934 {
1935 let _ = config;
1936 Ok(ProxyApplyReport::unsupported(
1937 "proxy is not supported on this platform",
1938 ))
1939 }
1940}
1941
1942pub fn configure_proxy_for_new_webviews(config: Option<ProxyConfig>) -> Result<(), WebViewError> {
1947 let apply_lock = PROXY_APPLY_LOCK.get_or_init(|| Mutex::new(()));
1948 let _guard = lock_or_recover(apply_lock, "webview_proxy_apply_lock");
1949
1950 let normalized_config = match config {
1951 Some(cfg) => Some(cfg.validate()?),
1952 None => None,
1953 };
1954
1955 let state = DESIRED_PROXY_FOR_NEW_WEBVIEWS.get_or_init(|| RwLock::new(None));
1956 match state.write() {
1957 Ok(mut guard) => {
1958 *guard = normalized_config;
1959 }
1960 Err(poisoned) => {
1961 log::error!("RwLock poisoned at webview_desired_proxy.write, recovering");
1962 *poisoned.into_inner() = normalized_config;
1963 }
1964 }
1965 Ok(())
1966}
1967
1968pub fn apply_proxy_to_current_runtime(
1973 config: Option<ProxyConfig>,
1974) -> Result<ProxyApplyReport, WebViewError> {
1975 let apply_lock = PROXY_APPLY_LOCK.get_or_init(|| Mutex::new(()));
1976 let _guard = lock_or_recover(apply_lock, "webview_proxy_apply_lock");
1977
1978 let normalized_config = match config {
1979 Some(cfg) => Some(cfg.validate()?),
1980 None => None,
1981 };
1982
1983 let report = apply_http_proxy_platform(normalized_config.as_ref())?;
1984
1985 if matches!(
1986 report.status,
1987 ProxyApplyStatus::Applied | ProxyApplyStatus::Cleared
1988 ) {
1989 let state = DESIRED_PROXY_FOR_NEW_WEBVIEWS.get_or_init(|| RwLock::new(None));
1990 match state.write() {
1991 Ok(mut guard) => {
1992 *guard = normalized_config;
1993 }
1994 Err(poisoned) => {
1995 log::error!("RwLock poisoned at webview_desired_proxy.write, recovering");
1996 *poisoned.into_inner() = normalized_config;
1997 }
1998 }
1999 }
2000
2001 Ok(report)
2002}
2003
2004pub fn configured_proxy_for_new_webviews() -> Option<ProxyConfig> {
2006 let state = DESIRED_PROXY_FOR_NEW_WEBVIEWS.get()?;
2007 match state.read() {
2008 Ok(guard) => guard.clone(),
2009 Err(poisoned) => {
2010 log::error!("RwLock poisoned at webview_desired_proxy.read, recovering");
2011 poisoned.into_inner().clone()
2012 }
2013 }
2014}
2015
2016fn clear_pending_callbacks(webtag: &WebTag) {
2017 if let Some(pending) = PENDING_CALLBACKS.get()
2018 && let Ok(mut map) = pending.lock()
2019 {
2020 map.remove(webtag.key());
2021 }
2022}
2023
2024fn replace_session_signals(webtag: &WebTag, signals: Arc<WebViewSessionSignals>) {
2025 let sessions = WEBVIEW_SESSIONS.get_or_init(|| Mutex::new(HashMap::new()));
2026 let mut guard = lock_or_recover(sessions, "webview_sessions.replace");
2027 guard.insert(webtag.key().to_string(), signals);
2028}
2029
2030fn remove_session_signals(webtag: &WebTag) -> Option<Arc<WebViewSessionSignals>> {
2031 let sessions = WEBVIEW_SESSIONS.get()?;
2032 let mut guard = lock_or_recover(sessions, "webview_sessions.remove");
2033 guard.remove(webtag.key())
2034}
2035
2036#[derive(Debug, Clone, PartialEq, Eq, Hash)]
2039pub struct WebTag(String);
2040
2041impl std::fmt::Display for WebTag {
2042 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2043 write!(f, "{}", self.0)
2044 }
2045}
2046
2047impl WebTag {
2048 pub fn new(appid: &str, path: &str, session_id: Option<u64>) -> Self {
2049 let mut tag = format!("{}:{}", appid, path);
2050 if let Some(session) = session_id {
2051 tag.push('#');
2052 tag.push_str(&session.to_string());
2053 }
2054 Self(tag)
2055 }
2056
2057 pub fn as_str(&self) -> &str {
2058 &self.0
2059 }
2060
2061 pub fn key(&self) -> &str {
2065 &self.0
2066 }
2067
2068 pub fn extract_appid(&self) -> String {
2070 self.0.split(':').next().unwrap_or("").to_string()
2071 }
2072
2073 pub fn extract_parts(&self) -> (String, String) {
2076 if let Some((appid, path_with_session)) = self.0.split_once(':') {
2077 let path = path_with_session
2078 .split('#')
2079 .next()
2080 .unwrap_or(path_with_session);
2081 (appid.to_string(), path.to_string())
2082 } else {
2083 log::error!("Invalid webtag format: {}", self.0);
2084 ("".to_string(), self.0.clone())
2085 }
2086 }
2087
2088 pub fn session_id(&self) -> Option<u64> {
2090 self.0
2091 .split('#')
2092 .next_back()
2093 .and_then(|raw| raw.parse::<u64>().ok())
2094 }
2095
2096 #[cfg_attr(
2100 any(not(target_os = "windows"), target_os = "windows"),
2101 allow(dead_code)
2102 )]
2103 pub(crate) fn group_key(&self) -> String {
2104 let Some((appid, path_with_session)) = self.0.split_once(':') else {
2105 return self.0.clone();
2106 };
2107 let session = path_with_session
2108 .rsplit_once('#')
2109 .and_then(|(_, suffix)| suffix.parse::<u64>().ok())
2110 .map(|session| session.to_string())
2111 .unwrap_or_else(|| "0".to_string());
2112 format!("{appid}#{session}")
2113 }
2114
2115 fn key_path(&self) -> String {
2116 let Some((_, path_with_suffix)) = self.0.split_once(':') else {
2117 return self.0.clone();
2118 };
2119 if self.session_id().is_some()
2120 && let Some((path, _)) = path_with_suffix.rsplit_once('#')
2121 {
2122 return path.to_string();
2123 }
2124 path_with_suffix.to_string()
2125 }
2126}
2127
2128impl From<&str> for WebTag {
2129 fn from(webtag_str: &str) -> Self {
2130 Self(webtag_str.to_string())
2131 }
2132}
2133
2134fn request_create_webview(
2135 webtag: &WebTag,
2136 sender: WebViewCreateSender,
2137 options: WebViewCreateOptions,
2138) {
2139 let (appid, _) = webtag.extract_parts();
2140 let (effective_options, pending_callbacks) = match options.normalize() {
2141 Ok(value) => value,
2142 Err(error) => {
2143 sender.fail(WebViewCreateStage::Requested, error);
2144 return;
2145 }
2146 };
2147
2148 log::info!(
2149 "Creating WebView for key={} profile={:?} data_mode={:?} schemes={:?}",
2150 webtag.key(),
2151 effective_options.profile,
2152 effective_options.data_mode,
2153 effective_options.registered_schemes,
2154 );
2155
2156 let instances = WEBVIEW_INSTANCES.get_or_init(|| Arc::new(Mutex::new(HashMap::new())));
2158
2159 if let Ok(webviews) = instances.lock()
2164 && let Some(existing_webview) = webviews.get(webtag.key())
2165 {
2166 if existing_webview.effective_options() != &effective_options {
2167 sender.fail(
2168 WebViewCreateStage::Requested,
2169 WebViewError::InvalidCreateOptions(format!(
2170 "webview already exists with different options: key={} existing={:?} requested={:?}",
2171 webtag.key(),
2172 existing_webview.effective_options(),
2173 effective_options
2174 )),
2175 );
2176 return;
2177 }
2178
2179 if pending_callbacks.has_any() {
2180 sender.fail(
2181 WebViewCreateStage::Requested,
2182 WebViewError::InvalidCreateOptions(format!(
2183 "webview already exists and callback registrations are immutable: key={} options={:?}",
2184 webtag.key(),
2185 existing_webview.effective_options()
2186 )),
2187 );
2188 log::warn!(
2189 "Rejected recreate with callbacks for existing webview key={} options={:?}",
2190 webtag.key(),
2191 existing_webview.effective_options()
2192 );
2193 return;
2194 }
2195
2196 log::info!("WebView already exists, reusing: {}", webtag.key());
2197 sender.succeed(existing_webview.clone());
2198 return;
2199 }
2200
2201 clear_pending_callbacks(webtag);
2203
2204 if pending_callbacks.has_any() {
2206 let pending = PENDING_CALLBACKS.get_or_init(|| Mutex::new(HashMap::new()));
2207 if let Ok(mut map) = pending.lock() {
2208 map.insert(webtag.key().to_string(), pending_callbacks);
2209 }
2210 }
2211
2212 WebViewInner::create(
2214 &appid,
2215 &webtag.key_path(),
2216 webtag.session_id(),
2217 effective_options,
2218 sender,
2219 );
2220}
2221
2222fn create_webview_session(webtag: WebTag, options: WebViewCreateOptions) -> WebViewSession {
2223 #[cfg(target_os = "windows")]
2228 let create_lock = windows_webview_create_lock(webtag.key());
2229 #[cfg(target_os = "windows")]
2230 let _create_guard = lock_or_recover(&create_lock, "windows_webview_create_lock");
2231
2232 let signals = WebViewSessionSignals::new();
2233 let session = signals.subscribe(webtag.clone());
2234 let sender = WebViewCreateSender::new(signals.clone());
2235 replace_session_signals(&webtag, signals);
2236 request_create_webview(&webtag, sender, options);
2237 session
2238}
2239
2240#[cfg(target_os = "windows")]
2241fn windows_webview_create_lock(webtag_key: &str) -> Arc<Mutex<()>> {
2242 let locks = WEBVIEW_CREATE_LOCKS.get_or_init(|| Mutex::new(HashMap::new()));
2243 let mut locks = lock_or_recover(locks, "windows_webview_create_locks");
2244 locks.retain(|_, lock| lock.strong_count() > 0);
2245 if let Some(lock) = locks.get(webtag_key).and_then(std::sync::Weak::upgrade) {
2246 return lock;
2247 }
2248 let lock = Arc::new(Mutex::new(()));
2249 locks.insert(webtag_key.to_string(), Arc::downgrade(&lock));
2250 lock
2251}
2252
2253pub(crate) fn register_webview(webview: Arc<WebView>) {
2254 let webtag = webview.webtag();
2255
2256 if let Some(pending) = PENDING_CALLBACKS.get()
2258 && let Ok(mut map) = pending.lock()
2259 && let Some(callbacks) = map.remove(webtag.key())
2260 {
2261 log::info!(
2262 "Installing callbacks for {} (schemes={}, nav={}, new_window={}, download={}, file_chooser={}, delegate={})",
2263 webtag.key(),
2264 callbacks.scheme_handlers.len(),
2265 callbacks.navigation_handler.is_some(),
2266 callbacks.new_window_handler.is_some(),
2267 callbacks.download_handler.is_some(),
2268 callbacks.file_chooser_handler.is_some(),
2269 callbacks.delegate.is_some()
2270 );
2271 webview.install_callbacks(callbacks);
2272 }
2273
2274 if let Some(instances) = WEBVIEW_INSTANCES.get()
2275 && let Ok(mut webviews) = instances.lock()
2276 {
2277 webviews.insert(webtag.key().to_string(), webview.clone());
2278 log::info!("WebView created and stored: {}", webtag.key());
2279 }
2280}
2281
2282pub(crate) fn find_webview(webtag: &WebTag) -> Option<Arc<WebView>> {
2284 if let Some(instances) = WEBVIEW_INSTANCES.get() {
2285 if let Ok(webviews) = instances.lock() {
2286 webviews.get(webtag.key()).cloned()
2287 } else {
2288 None
2289 }
2290 } else {
2291 None
2292 }
2293}
2294
2295#[cfg(target_os = "windows")]
2296pub(crate) fn first_browser_webview() -> Option<Arc<WebView>> {
2297 WEBVIEW_INSTANCES
2298 .get()
2299 .and_then(|instances| instances.lock().ok())
2300 .and_then(|webviews| {
2301 webviews
2302 .values()
2303 .find(|webview| {
2304 webview.effective_options.profile == SecurityProfile::BrowserRelaxed
2305 })
2306 .cloned()
2307 })
2308}
2309
2310pub(crate) fn list_webviews() -> Vec<WebTag> {
2311 if let Some(instances) = WEBVIEW_INSTANCES.get()
2312 && let Ok(webviews) = instances.lock()
2313 {
2314 let mut tags: Vec<WebTag> = webviews.values().map(|webview| webview.webtag()).collect();
2315 tags.sort_by(|a, b| a.as_str().cmp(b.as_str()));
2316 return tags;
2317 }
2318 Vec::new()
2319}
2320
2321pub(crate) fn find_webview_delegate(webtag: &WebTag) -> Option<Arc<dyn WebViewDelegate>> {
2322 find_webview(webtag).and_then(|webview| webview.get_delegate())
2323}
2324
2325pub(crate) fn destroy_webview(webtag: &WebTag) {
2327 crate::events::normalizer::destroy(webtag);
2330 if let Some(signals) = remove_session_signals(webtag) {
2336 signals.publish_destroyed();
2337 }
2338 let removed = if let Some(instances) = WEBVIEW_INSTANCES.get()
2339 && let Ok(mut webviews) = instances.lock()
2340 {
2341 webviews.remove(webtag.key())
2342 } else {
2343 None
2344 };
2345 if let Some(webview) = removed {
2346 webview.remove_delegate();
2347 }
2348 clear_pending_callbacks(webtag);
2349}