Skip to main content

lingxia_webview/
traits.rs

1use crate::{LogLevel, WebViewError, WebViewInputError, WebViewScriptError};
2use async_trait::async_trait;
3use serde::{Deserialize, Serialize};
4use std::future::Future;
5use std::path::PathBuf;
6use std::pin::Pin;
7use std::sync::Arc;
8
9/// Opaque identity for one native WebView instance.
10///
11/// A [`crate::WebTag`] is a logical lookup key and may be reused after a
12/// WebView is destroyed. This identity is allocated for one concrete native
13/// instance and is therefore the only identity suitable for message binding.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15pub struct NativeWebViewId(u64);
16
17impl NativeWebViewId {
18    pub(crate) const fn new(raw: u64) -> Self {
19        Self(raw)
20    }
21
22    /// Construct a synthetic native identity in downstream unit tests.
23    #[cfg(feature = "test-support")]
24    pub const fn for_test(raw: u64) -> Self {
25        Self(raw)
26    }
27
28    // Android's JNI binding consumes this only on its conditionally compiled target.
29    #[allow(dead_code)]
30    pub(crate) const fn raw(self) -> u64 {
31        self.0
32    }
33}
34
35/// Monotonic generation of a committed document within one native WebView.
36#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
37pub struct DocumentGeneration(u64);
38
39impl DocumentGeneration {
40    /// Constructed only by the navigation normalizer after reliable commit
41    /// evidence. Adapters never receive this constructor.
42    pub(crate) const fn new(raw: u64) -> Self {
43        Self(raw)
44    }
45
46    /// Construct a synthetic document generation in downstream unit tests.
47    #[cfg(feature = "test-support")]
48    pub const fn for_test(raw: u64) -> Self {
49        Self(raw)
50    }
51
52    /// The platform-owned ordinal, intended only for equality binding.
53    pub const fn get(self) -> u64 {
54        self.0
55    }
56}
57
58/// Opaque capability issued for one native-owned direct HTML load.
59///
60/// This is deliberately useful only for equality binding. It has no public
61/// constructor, raw representation, formatting implementation, or wire
62/// encoding: URL and HTML data are not authority, and neither is this token
63/// until the navigation normalizer attests it at a committed document.
64#[derive(Clone, Copy, PartialEq, Eq, Hash)]
65pub struct TrustedLoadIntent(u64);
66
67impl TrustedLoadIntent {
68    pub(crate) const fn new(raw: u64) -> Self {
69        Self(raw)
70    }
71}
72
73/// Non-forgeable evidence that a trusted native HTML load committed.
74///
75/// The normalizer creates this only after it correlates one issued
76/// [`TrustedLoadIntent`] with the exact native WebView, the platform-native
77/// navigation key, and an accepted navigation lifecycle.
78#[derive(Clone, Copy)]
79pub struct TrustedDocumentAdmission {
80    native_view: NativeWebViewId,
81    generation: DocumentGeneration,
82    navigation_id: crate::events::NavigationId,
83    intent: TrustedLoadIntent,
84}
85
86impl TrustedDocumentAdmission {
87    pub(crate) const fn new(
88        native_view: NativeWebViewId,
89        generation: DocumentGeneration,
90        navigation_id: crate::events::NavigationId,
91        intent: TrustedLoadIntent,
92    ) -> Self {
93        Self {
94            native_view,
95            generation,
96            navigation_id,
97            intent,
98        }
99    }
100
101    pub const fn native_view(&self) -> NativeWebViewId {
102        self.native_view
103    }
104
105    pub const fn generation(&self) -> DocumentGeneration {
106        self.generation
107    }
108
109    pub const fn navigation_id(&self) -> crate::events::NavigationId {
110        self.navigation_id
111    }
112
113    pub const fn intent(&self) -> TrustedLoadIntent {
114        self.intent
115    }
116}
117
118/// Whether a platform callback is bound to a committed document generation.
119#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
120pub enum DocumentBinding {
121    /// The adapter cannot prove which committed document emitted the message.
122    Unbound,
123    /// The adapter captured the native WebView's committed document generation.
124    Bound(DocumentGeneration),
125}
126
127/// A document-session authority check for one outbound native message.
128///
129/// Implementations must invoke `action` only while the document session is
130/// still active. The closure makes it possible for a registry-backed gate to
131/// keep its lock for the check and the native post as one operation.
132pub trait DocumentOutboundGate: Send + Sync {
133    fn with_active(&self, action: &mut dyn FnMut()) -> bool;
134}
135
136/// Platform proof of the emitting frame.
137///
138/// Authorization-sensitive consumers must accept only [`Self::TopLevel`].
139/// `Unproven` is deliberately distinct from `TopLevel` so an adapter cannot
140/// silently upgrade an unavailable platform signal into authority.
141#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
142pub enum WebMessageFrame {
143    TopLevel,
144    Subframe,
145    Unproven,
146}
147
148/// Native transport which delivered a WebView message.
149#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
150pub enum WebMessageTransport {
151    AppleScriptMessage,
152    AndroidMessagePort,
153    AndroidJavascriptInterface,
154    WindowsWebMessage,
155    HarmonyMessagePort,
156    Other,
157}
158
159/// Platform-reported source information for diagnostics and auditing.
160///
161/// It is intentionally not an authorization credential: URLs and origins can
162/// be stale, unavailable, or insufficient to bind a message to an app
163/// session. Authorization must use the native view identity, document binding,
164/// and frame proof in [`WebMessageContext`].
165#[derive(Debug, Clone, PartialEq, Eq, Default)]
166pub struct WebMessageSource {
167    reported_url: Option<String>,
168    reported_origin: Option<String>,
169}
170
171impl WebMessageSource {
172    pub const fn unavailable() -> Self {
173        Self {
174            reported_url: None,
175            reported_origin: None,
176        }
177    }
178
179    pub fn diagnostic_url(reported_url: Option<String>) -> Self {
180        Self {
181            reported_url,
182            reported_origin: None,
183        }
184    }
185
186    pub fn diagnostic_origin(reported_origin: Option<String>) -> Self {
187        Self {
188            reported_url: None,
189            reported_origin,
190        }
191    }
192
193    pub fn diagnostic(reported_url: Option<String>, reported_origin: Option<String>) -> Self {
194        Self {
195            reported_url,
196            reported_origin,
197        }
198    }
199
200    /// A platform-reported URL. This is diagnostic data, never authorization.
201    pub fn reported_url(&self) -> Option<&str> {
202        self.reported_url.as_deref()
203    }
204
205    /// A platform-reported origin. This is diagnostic data, never authorization.
206    pub fn reported_origin(&self) -> Option<&str> {
207        self.reported_origin.as_deref()
208    }
209}
210
211/// Non-forgeable platform context bound to an incoming WebView message.
212///
213/// Carrying this context does not by itself authorize bridge dispatch. The
214/// bridge admission layer must explicitly evaluate its binding and proof.
215#[derive(Debug, Clone, PartialEq, Eq)]
216pub struct WebMessageContext {
217    native_view: NativeWebViewId,
218    document: DocumentBinding,
219    frame: WebMessageFrame,
220    transport: WebMessageTransport,
221    source: WebMessageSource,
222}
223
224impl WebMessageContext {
225    pub(crate) const fn new(
226        native_view: NativeWebViewId,
227        document: DocumentBinding,
228        frame: WebMessageFrame,
229        transport: WebMessageTransport,
230        source: WebMessageSource,
231    ) -> Self {
232        Self {
233            native_view,
234            document,
235            frame,
236            transport,
237            source,
238        }
239    }
240
241    pub const fn native_view(&self) -> NativeWebViewId {
242        self.native_view
243    }
244
245    pub const fn document(&self) -> DocumentBinding {
246        self.document
247    }
248
249    pub const fn frame(&self) -> WebMessageFrame {
250        self.frame
251    }
252
253    pub const fn transport(&self) -> WebMessageTransport {
254        self.transport
255    }
256
257    pub fn source(&self) -> &WebMessageSource {
258        &self.source
259    }
260}
261
262/// A page-originated WebView message plus platform-attested context.
263#[derive(Debug, Clone, PartialEq, Eq)]
264pub struct IncomingWebMessage {
265    body: String,
266    context: WebMessageContext,
267}
268
269impl IncomingWebMessage {
270    pub(crate) fn new(body: String, context: WebMessageContext) -> Self {
271        Self { body, context }
272    }
273
274    pub fn body(&self) -> &str {
275        &self.body
276    }
277
278    pub fn context(&self) -> &WebMessageContext {
279        &self.context
280    }
281}
282
283/// Platform proof of which document scope initiated a scheme request.
284///
285/// `Unproven` is intentionally not treated as a top-level document. Platform
286/// adapters must preserve unavailable or ambiguous evidence rather than
287/// upgrading it at the Rust boundary.
288#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
289pub enum SchemeRequestFrame {
290    TopLevelDocument,
291    Subresource,
292    Unproven,
293}
294
295/// A scheme request plus the concrete native WebView and frame proof which
296/// delivered it.
297///
298/// Construction is crate-private: only platform adapters, after validating
299/// their callback's native identity, may attach this authority-relevant
300/// context. Consumers can inspect it but cannot forge a replacement.
301#[derive(Debug)]
302pub struct ContextualSchemeRequest {
303    request: http::Request<Vec<u8>>,
304    native_view: NativeWebViewId,
305    frame: SchemeRequestFrame,
306}
307
308impl ContextualSchemeRequest {
309    pub(crate) fn new(
310        request: http::Request<Vec<u8>>,
311        native_view: NativeWebViewId,
312        frame: SchemeRequestFrame,
313    ) -> Self {
314        Self {
315            request,
316            native_view,
317            frame,
318        }
319    }
320
321    pub fn request(&self) -> &http::Request<Vec<u8>> {
322        &self.request
323    }
324
325    pub fn into_request(self) -> http::Request<Vec<u8>> {
326        self.request
327    }
328
329    pub const fn native_view(&self) -> NativeWebViewId {
330        self.native_view
331    }
332
333    pub const fn frame(&self) -> SchemeRequestFrame {
334        self.frame
335    }
336}
337
338/// Outcome of handling a scheme request.
339#[derive(Debug)]
340pub enum SchemeOutcome {
341    /// Handler produced a response.
342    Handled(WebResourceResponse),
343    /// Handler intentionally declined the request.
344    PassThrough,
345}
346
347/// Async scheme handler signature.
348pub(crate) type AsyncSchemeFuture = Pin<Box<dyn Future<Output = SchemeOutcome> + Send + 'static>>;
349pub(crate) type AsyncSchemeHandler =
350    Arc<dyn Fn(ContextualSchemeRequest) -> AsyncSchemeFuture + Send + Sync>;
351
352/// Navigation policy decision returned by the navigation handler.
353#[derive(Debug, Clone, Copy, PartialEq, Eq)]
354pub enum NavigationPolicy {
355    /// Allow the WebView to navigate to this URL.
356    Allow,
357    /// Cancel the navigation. The handler is responsible for any side effects
358    /// (e.g., opening the URL externally via `AppRuntime::open_url()`).
359    Cancel,
360}
361
362/// A platform navigation request passed to the registered policy handler.
363#[derive(Debug, Clone, PartialEq, Eq)]
364pub struct NavigationRequest {
365    pub url: String,
366    pub has_user_gesture: bool,
367    pub is_main_frame: bool,
368}
369
370impl NavigationRequest {
371    pub fn new(url: impl Into<String>, has_user_gesture: bool, is_main_frame: bool) -> Self {
372        Self {
373            url: url.into(),
374            has_user_gesture,
375            is_main_frame,
376        }
377    }
378}
379
380/// New-window policy decision returned by the new-window handler.
381#[derive(Debug, Clone, Copy, PartialEq, Eq)]
382pub enum NewWindowPolicy {
383    /// Load the URL in the current WebView (replaces current page).
384    LoadInSelf,
385    /// Cancel the new-window request without doing anything.
386    Cancel,
387}
388
389pub type NavigationHandler = Box<dyn Fn(&NavigationRequest) -> NavigationPolicy + Send + Sync>;
390pub type NewWindowHandler = Box<dyn Fn(&str) -> NewWindowPolicy + Send + Sync>;
391
392/// Per-WebView user-agent override.
393#[derive(Debug, Clone, PartialEq, Eq)]
394pub enum UserAgentOverride {
395    /// Restore the user agent supplied by the platform WebView engine.
396    Default,
397    /// Replace the complete user-agent string. The value must be non-empty and
398    /// engine-compatible. This does not emulate other browser capabilities or
399    /// synchronize User-Agent Client Hints.
400    Custom(String),
401}
402
403impl UserAgentOverride {
404    /// Validate a complete override before applying or persisting it.
405    pub fn validate(&self) -> Result<(), WebViewError> {
406        if let Self::Custom(value) = self {
407            if value.trim().is_empty() {
408                return Err(WebViewError::WebView(
409                    "custom user-agent override must not be empty".to_string(),
410                ));
411            }
412            if value.contains(['\r', '\n', '\0']) {
413                return Err(WebViewError::WebView(
414                    "custom user-agent override must not contain CR, LF, or NUL".to_string(),
415                ));
416            }
417        }
418        Ok(())
419    }
420}
421
422#[cfg(test)]
423mod user_agent_override_tests {
424    use super::*;
425
426    #[test]
427    fn custom_user_agent_must_not_be_blank() {
428        assert!(UserAgentOverride::Custom(String::new()).validate().is_err());
429        assert!(UserAgentOverride::Custom("   ".into()).validate().is_err());
430        assert!(
431            UserAgentOverride::Custom("Mozilla/5.0 valid".into())
432                .validate()
433                .is_ok()
434        );
435        for invalid in [
436            "Mozilla/5.0\rInjected",
437            "Mozilla/5.0\nInjected",
438            "Mozilla\0/5.0",
439        ] {
440            assert!(
441                UserAgentOverride::Custom(invalid.into())
442                    .validate()
443                    .is_err()
444            );
445        }
446        assert!(UserAgentOverride::Default.validate().is_ok());
447    }
448}
449
450#[derive(Debug, Clone, PartialEq, Eq)]
451pub struct DownloadRequest {
452    /// Final download URL reported by the platform callback.
453    pub url: String,
454    /// Request user-agent if available on this platform.
455    pub user_agent: Option<String>,
456    /// `Content-Disposition` response header if exposed by the platform.
457    pub content_disposition: Option<String>,
458    /// Response MIME type if exposed by the platform.
459    pub mime_type: Option<String>,
460    /// Response content length if known.
461    pub content_length: Option<u64>,
462    /// Platform-suggested filename (may be absent).
463    pub suggested_filename: Option<String>,
464    /// Source page URL that initiated the download when available.
465    pub source_page_url: Option<String>,
466    /// Cookie header string for `url` when available.
467    pub cookie: Option<String>,
468}
469
470/// Download callback.
471///
472/// In browser profile, registering this callback makes download requests flow through the host
473/// app callback path instead of in-WebView download UI.
474pub type DownloadHandler = Box<dyn Fn(DownloadRequest) + Send + Sync>;
475
476#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
477#[serde(rename_all = "lowercase")]
478pub enum WebViewCookieSameSite {
479    Lax,
480    Strict,
481    None,
482}
483
484impl WebViewCookieSameSite {
485    pub fn as_str(self) -> &'static str {
486        match self {
487            Self::Lax => "lax",
488            Self::Strict => "strict",
489            Self::None => "none",
490        }
491    }
492}
493
494#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
495pub struct WebViewCookie {
496    pub name: String,
497    pub value: String,
498    pub domain: String,
499    pub path: String,
500    #[serde(default, skip_serializing_if = "is_false")]
501    pub host_only: bool,
502    #[serde(default)]
503    pub secure: bool,
504    #[serde(default)]
505    pub http_only: bool,
506    #[serde(default)]
507    pub session: bool,
508    #[serde(default, skip_serializing_if = "Option::is_none")]
509    pub expires_unix_ms: Option<i64>,
510    #[serde(default, skip_serializing_if = "Option::is_none")]
511    pub same_site: Option<WebViewCookieSameSite>,
512}
513
514#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
515pub struct WebViewCookieSetRequest {
516    #[serde(default)]
517    pub url: String,
518    pub name: String,
519    pub value: String,
520    #[serde(default, skip_serializing_if = "Option::is_none")]
521    pub domain: Option<String>,
522    #[serde(default = "default_cookie_path")]
523    pub path: String,
524    #[serde(default)]
525    pub secure: bool,
526    #[serde(default)]
527    pub http_only: bool,
528    #[serde(default, skip_serializing_if = "Option::is_none")]
529    pub expires_unix_ms: Option<i64>,
530    #[serde(default, skip_serializing_if = "Option::is_none")]
531    pub same_site: Option<WebViewCookieSameSite>,
532}
533
534fn default_cookie_path() -> String {
535    "/".to_string()
536}
537
538fn is_false(value: &bool) -> bool {
539    !*value
540}
541
542#[derive(Debug, Clone, PartialEq, Eq)]
543pub struct FileChooserRequest {
544    /// Accepted MIME types / extensions requested by the page.
545    pub accept_types: Vec<String>,
546    /// Whether multiple files may be selected.
547    pub allow_multiple: bool,
548    /// Whether directories may be selected.
549    pub allow_directories: bool,
550    /// Whether the page requested capture/live media.
551    pub capture: bool,
552    /// Source page URL that initiated the chooser when available.
553    pub source_page_url: Option<String>,
554}
555
556#[derive(Debug, Clone, PartialEq, Eq)]
557pub struct FileChooserFile {
558    pub path: Option<String>,
559    pub uri: Option<String>,
560}
561
562#[derive(Debug, Clone, PartialEq, Eq)]
563pub enum FileChooserResponse {
564    Cancel,
565    Error(String),
566    Files(Vec<FileChooserFile>),
567}
568
569/// Body source for WebResourceResponse
570#[derive(Debug)]
571pub enum WebResourceBody {
572    /// Serve data from a regular file path on disk
573    Path(PathBuf),
574    /// Serve data from a system pipe (read end)
575    Pipe(SystemPipeReader),
576    /// Serve data directly from memory
577    Bytes(Vec<u8>),
578}
579
580/// Cross‑platform system pipe reader (read end)
581#[derive(Debug)]
582pub struct SystemPipeReader {
583    #[cfg(unix)]
584    fd: std::os::fd::RawFd,
585    #[cfg(windows)]
586    handle: std::os::windows::io::RawHandle,
587}
588
589impl SystemPipeReader {
590    /// Consume and return the raw file descriptor (Unix).
591    /// Caller becomes responsible for closing it.
592    #[cfg(unix)]
593    pub fn into_raw_fd(self) -> std::os::fd::RawFd {
594        self.fd
595    }
596
597    /// Construct from a raw file descriptor (Unix).
598    ///
599    /// # Safety
600    ///
601    /// Caller guarantees that `fd` is a valid read end of a pipe file descriptor.
602    #[cfg(unix)]
603    pub unsafe fn from_raw_fd(fd: std::os::fd::RawFd) -> Self {
604        Self { fd }
605    }
606
607    /// Convert into a File for reading (consumes self).
608    #[cfg(unix)]
609    pub fn into_file(self) -> std::fs::File {
610        use std::os::fd::FromRawFd;
611        unsafe { std::fs::File::from_raw_fd(self.into_raw_fd()) }
612    }
613
614    /// Consume and return the raw handle (Windows).
615    /// Caller becomes responsible for closing it.
616    #[cfg(windows)]
617    pub fn into_raw_handle(self) -> std::os::windows::io::RawHandle {
618        self.handle
619    }
620
621    /// Construct from a raw handle (Windows).
622    ///
623    /// # Safety
624    ///
625    /// Caller guarantees that `handle` is a valid readable OS handle.
626    #[cfg(windows)]
627    pub unsafe fn from_raw_handle(handle: std::os::windows::io::RawHandle) -> Self {
628        Self { handle }
629    }
630
631    /// Convert into a File for reading (consumes self).
632    #[cfg(windows)]
633    pub fn into_file(self) -> std::fs::File {
634        use std::os::windows::io::FromRawHandle;
635        unsafe { std::fs::File::from_raw_handle(self.into_raw_handle()) }
636    }
637}
638
639/// Interface for controlling WebView (100% copy from lxapp)
640#[async_trait]
641pub trait WebViewController: Send + Sync {
642    /// Load a URL in the WebView
643    fn load_url(&self, url: &str) -> Result<(), WebViewError>;
644
645    /// Load HTML data into the WebView.
646    fn load_data(&self, request: LoadDataRequest<'_>) -> Result<(), WebViewError>;
647
648    /// Execute JavaScript in the WebView without observing its return value.
649    fn exec_js(&self, js: &str) -> Result<(), WebViewError>;
650
651    /// Evaluate JavaScript in the WebView and return the decoded JSON value.
652    ///
653    /// Implementations are required to be both CSP-safe (no `(0,eval)` /
654    /// `new Function` — pages whose CSP omits `'unsafe-eval'` must still
655    /// work) and `await`-aware (top-level `await` in the user expression
656    /// resolves before the future returns). Platforms achieve this by
657    /// dispatching through the native await-capable API
658    /// (`callAsyncJavaScript:` on Apple, `LingXiaProxy.resolveEval` JS
659    /// bridge on Android/Harmony).
660    async fn eval_js(&self, js: &str) -> Result<serde_json::Value, WebViewScriptError>;
661
662    /// Return the platform WebView's current URL.
663    async fn current_url(&self) -> Result<Option<String>, WebViewError> {
664        Err(WebViewError::WebView(
665            "current_url is not implemented for this platform".to_string(),
666        ))
667    }
668
669    /// Post a message to the WebView
670    fn post_message(&self, message: &str) -> Result<(), WebViewError>;
671
672    /// Post a message only if an exact committed document and its session gate
673    /// are still current.
674    ///
675    /// This is intentionally unsupported unless a platform can perform the
676    /// final identity check at its actual JavaScript execution point.
677    fn post_message_to_document(
678        &self,
679        _expected_generation: DocumentGeneration,
680        _gate: Arc<dyn DocumentOutboundGate>,
681        _message: &str,
682    ) -> Result<(), WebViewError> {
683        Err(WebViewError::Unsupported(
684            "document-bound message posting".to_string(),
685        ))
686    }
687
688    /// Clear browsing data from the WebView
689    fn clear_browsing_data(&self) -> Result<(), WebViewError>;
690
691    /// Override or restore the WebView user agent.
692    fn set_user_agent_override(&self, user_agent: UserAgentOverride) -> Result<(), WebViewError>;
693
694    /// Reload the current WebView document.
695    fn reload(&self) -> Result<(), WebViewError> {
696        Err(WebViewError::WebView(
697            "reload is not implemented for this platform".to_string(),
698        ))
699    }
700
701    /// Navigate back in WebView history.
702    fn go_back(&self) -> Result<(), WebViewError> {
703        Err(WebViewError::WebView(
704            "go_back is not implemented for this platform".to_string(),
705        ))
706    }
707
708    /// Navigate forward in WebView history.
709    fn go_forward(&self) -> Result<(), WebViewError> {
710        Err(WebViewError::WebView(
711            "go_forward is not implemented for this platform".to_string(),
712        ))
713    }
714
715    /// List HTTP cookies from the platform WebView cookie store.
716    async fn list_cookies(&self) -> Result<Vec<WebViewCookie>, WebViewError> {
717        Err(WebViewError::WebView(
718            "cookie store is not implemented for this platform".to_string(),
719        ))
720    }
721
722    /// Set an HTTP cookie through the platform WebView cookie store.
723    async fn set_cookie(&self, _request: WebViewCookieSetRequest) -> Result<(), WebViewError> {
724        Err(WebViewError::WebView(
725            "cookie store is not implemented for this platform".to_string(),
726        ))
727    }
728
729    /// Delete an HTTP cookie from the platform WebView cookie store.
730    async fn delete_cookie(
731        &self,
732        _name: &str,
733        _domain: &str,
734        _path: &str,
735    ) -> Result<(), WebViewError> {
736        Err(WebViewError::WebView(
737            "cookie store is not implemented for this platform".to_string(),
738        ))
739    }
740
741    /// Clear all HTTP cookies from the platform WebView cookie store.
742    async fn clear_cookies(&self) -> Result<(), WebViewError> {
743        Err(WebViewError::WebView(
744            "cookie store is not implemented for this platform".to_string(),
745        ))
746    }
747
748    /// Clear data owned by the current website without clearing the shared
749    /// browser profile. Platforms report whether their network cache supports
750    /// site-scoped removal.
751    async fn clear_site_data(
752        &self,
753        _url: &str,
754        _options: ClearSiteDataOptions,
755    ) -> Result<ClearSiteDataResult, WebViewError> {
756        Err(WebViewError::WebView(
757            "site-scoped data clearing is not implemented for this platform".to_string(),
758        ))
759    }
760
761    /// Capture a PNG screenshot of the WebView's visible content.
762    /// Returns raw PNG-encoded bytes ready to be base64'd over the wire.
763    async fn take_screenshot(&self) -> Result<Vec<u8>, WebViewError> {
764        Err(WebViewError::WebView(
765            "screenshot is not implemented for this platform".to_string(),
766        ))
767    }
768
769    /// Begin recording network requests/responses into a bounded per-webview
770    /// buffer, retrievable via [`Self::network_entries`]. Dev-tooling only;
771    /// implemented on platforms whose WebView exposes an inspection protocol
772    /// (currently Windows/WebView2 via the Chrome DevTools Protocol).
773    async fn start_network_capture(&self) -> Result<(), WebViewError> {
774        Err(WebViewError::WebView(
775            "network capture is not implemented for this platform".to_string(),
776        ))
777    }
778
779    /// Stop recording network traffic. Captured entries are kept until
780    /// [`Self::clear_network_capture`] or the webview is torn down.
781    async fn stop_network_capture(&self) -> Result<(), WebViewError> {
782        Err(WebViewError::WebView(
783            "network capture is not implemented for this platform".to_string(),
784        ))
785    }
786
787    /// Snapshot the captured network entries (oldest first). `dropped` counts
788    /// entries evicted from the ring buffer since the last clear.
789    async fn network_entries(&self) -> Result<NetworkCaptureSnapshot, WebViewError> {
790        Err(WebViewError::WebView(
791            "network capture is not implemented for this platform".to_string(),
792        ))
793    }
794
795    /// Drop all captured entries (leaves capture enabled if it was on).
796    async fn clear_network_capture(&self) -> Result<(), WebViewError> {
797        Err(WebViewError::WebView(
798            "network capture is not implemented for this platform".to_string(),
799        ))
800    }
801}
802
803/// Data categories to remove for one site via
804/// [`WebViewController::clear_site_data`].
805#[derive(Debug, Clone, Copy)]
806pub struct ClearSiteDataOptions {
807    pub cache: bool,
808    pub site_data: bool,
809}
810
811/// Outcome of [`WebViewController::clear_site_data`]. Each flag means "this
812/// category was requested AND the platform fully honored it" — `false` both
813/// when the category was not requested and when it could not be fully cleared.
814///
815/// Windows caveat: WebView2 clears the site's Cache Storage/appcache but
816/// cannot site-scope the shared HTTP cache, so it reports
817/// `cache_cleared: false` even when cache clearing was requested.
818#[derive(Debug, Clone, Copy)]
819pub struct ClearSiteDataResult {
820    pub cache_cleared: bool,
821    pub site_data_cleared: bool,
822}
823
824/// One captured network request and its response (when it completed).
825#[derive(Debug, Clone, Serialize, Deserialize)]
826pub struct NetworkEntry {
827    /// Protocol request id, stable across the request/response events.
828    pub request_id: String,
829    pub url: String,
830    pub method: String,
831    /// Resource kind reported by the engine (document, xhr, fetch, script,
832    /// image, ...), when available.
833    pub resource_type: Option<String>,
834    pub request_headers: Vec<(String, String)>,
835    /// Request payload (POST body) as reported by the engine, when present.
836    pub request_body: Option<String>,
837    pub status: Option<u16>,
838    pub response_headers: Vec<(String, String)>,
839    pub mime_type: Option<String>,
840    pub response_body: NetworkBody,
841    pub from_cache: bool,
842    /// Populated when the request failed (engine error text) instead of
843    /// producing a response.
844    pub failed: Option<String>,
845    /// Wall-clock start time (Unix epoch seconds), when the engine reports it.
846    pub wall_time: Option<f64>,
847    /// Monotonic engine timestamps (seconds), for ordering and durations.
848    pub started: f64,
849    pub finished: Option<f64>,
850}
851
852impl NetworkEntry {
853    /// Request duration in milliseconds, once the response has completed.
854    pub fn duration_ms(&self) -> Option<f64> {
855        self.finished
856            .filter(|finished| *finished >= self.started)
857            .map(|finished| (finished - self.started) * 1000.0)
858    }
859}
860
861/// Response body of a captured entry.
862#[derive(Debug, Clone, Default, Serialize, Deserialize)]
863#[serde(tag = "kind", rename_all = "snake_case")]
864pub enum NetworkBody {
865    /// No body captured yet (in flight) or the response had none.
866    #[default]
867    None,
868    /// UTF-8 text body.
869    Text { text: String },
870    /// Base64-encoded binary body.
871    Base64 { base64: String },
872    /// Body deliberately not captured (e.g. over the size cap, or evicted
873    /// before it could be read); `reason` says which.
874    Skipped { reason: String },
875}
876
877/// A point-in-time view of the capture buffer.
878#[derive(Debug, Clone, Default, Serialize, Deserialize)]
879pub struct NetworkCaptureSnapshot {
880    pub entries: Vec<NetworkEntry>,
881    /// Entries evicted from the ring buffer since the last clear (buffer
882    /// full). Surfaced so truncation is never silent.
883    pub dropped: u64,
884}
885
886#[derive(Debug, Clone, Default, Serialize, Deserialize)]
887pub struct ClickOptions {
888    #[serde(default, skip_serializing_if = "Option::is_none")]
889    pub index: Option<usize>,
890}
891
892#[derive(Debug, Clone, Default, Serialize, Deserialize)]
893pub struct TypeOptions {
894    #[serde(default, skip_serializing_if = "Option::is_none")]
895    pub index: Option<usize>,
896    #[serde(default)]
897    pub replace: bool,
898}
899
900#[derive(Debug, Clone, Default, Serialize, Deserialize)]
901pub struct FillOptions {
902    #[serde(default, skip_serializing_if = "Option::is_none")]
903    pub index: Option<usize>,
904}
905
906#[derive(Debug, Clone, Default, Serialize, Deserialize)]
907pub struct PressOptions {
908    #[serde(default, skip_serializing_if = "Option::is_none")]
909    pub selector: Option<String>,
910    #[serde(default, skip_serializing_if = "Option::is_none")]
911    pub index: Option<usize>,
912}
913
914#[derive(Debug, Clone, Default, Serialize, Deserialize)]
915pub struct ScrollOptions;
916
917#[async_trait]
918pub trait WebViewInputController: WebViewController {
919    async fn click(
920        &self,
921        _selector: &str,
922        _options: ClickOptions,
923    ) -> Result<(), WebViewInputError> {
924        Err(WebViewInputError::Unsupported(
925            "input control is not implemented for this platform",
926        ))
927    }
928
929    async fn type_text(
930        &self,
931        _selector: &str,
932        _text: &str,
933        _options: TypeOptions,
934    ) -> Result<(), WebViewInputError> {
935        Err(WebViewInputError::Unsupported(
936            "input control is not implemented for this platform",
937        ))
938    }
939
940    async fn fill(
941        &self,
942        _selector: &str,
943        _text: &str,
944        _options: FillOptions,
945    ) -> Result<(), WebViewInputError> {
946        Err(WebViewInputError::Unsupported(
947            "input control is not implemented for this platform",
948        ))
949    }
950
951    async fn press(&self, _key: &str, _options: PressOptions) -> Result<(), WebViewInputError> {
952        Err(WebViewInputError::Unsupported(
953            "input control is not implemented for this platform",
954        ))
955    }
956
957    async fn scroll(
958        &self,
959        _dx: f64,
960        _dy: f64,
961        _options: ScrollOptions,
962    ) -> Result<(), WebViewInputError> {
963        Err(WebViewInputError::Unsupported(
964            "input control is not implemented for this platform",
965        ))
966    }
967
968    async fn scroll_to(
969        &self,
970        _selector: &str,
971        _options: ScrollOptions,
972    ) -> Result<(), WebViewInputError> {
973        Err(WebViewInputError::Unsupported(
974            "input control is not implemented for this platform",
975        ))
976    }
977}
978
979#[derive(Debug, Clone, Copy)]
980pub struct LoadDataRequest<'a> {
981    pub data: &'a str,
982    pub base_url: &'a str,
983    pub history_url: Option<&'a str>,
984}
985
986impl<'a> LoadDataRequest<'a> {
987    pub fn new(data: &'a str, base_url: &'a str) -> Self {
988        Self {
989            data,
990            base_url,
991            history_url: None,
992        }
993    }
994
995    pub fn with_history_url(mut self, history_url: &'a str) -> Self {
996        self.history_url = Some(history_url);
997        self
998    }
999}
1000
1001/// Normalized category for a main-frame page load failure.
1002///
1003/// Cancellation is deliberately not a kind: a cancelled navigation is control
1004/// flow and terminates as `NavigationEvent::Cancelled`, never as a load error.
1005#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1006pub enum LoadErrorKind {
1007    Dns,
1008    Network,
1009    Timeout,
1010    Security,
1011    InvalidUrl,
1012    NotFound,
1013    Unknown,
1014}
1015
1016/// Error reported when a main-frame page load fails (DNS, network, TLS, etc.).
1017///
1018/// `kind` is the stable value for program logic; `description` is platform
1019/// diagnostic text for logs and must not be parsed or shown directly as
1020/// localized product copy.
1021#[derive(Debug, Clone, PartialEq, Eq)]
1022pub struct LoadError {
1023    /// URL that failed to load, if the platform exposes it.
1024    pub failing_url: Option<String>,
1025    /// Cross-platform error category for application logic and UI.
1026    pub kind: LoadErrorKind,
1027    /// Human-readable description from the platform.
1028    pub description: String,
1029}
1030
1031/// WebView delegate: typed navigation lifecycle, observable state, page
1032/// messaging, and logging for one WebView. Exactly one owner per WebView
1033/// (an lxapp `PageInstance` or a browser tab delegate); read-only watchers
1034/// use [`crate::events::normalizer::add_observer`]-registered observers.
1035///
1036/// Delivery contract (enforced by the event normalizer):
1037/// - events arrive by value, serially, synchronously on the submitting
1038///   thread, flattened FIFO — a callback is never re-entered for the same
1039///   WebView;
1040/// - callbacks may arrive on the WebView's own UI thread; fire-and-forget
1041///   commands (`exec_js`) are safe there, but result-awaiting APIs must not
1042///   block the callback thread;
1043/// - every `Started` gets exactly one terminal event; success owns a
1044///   non-empty final URL; cancellation is control flow, never a load error;
1045/// - state changes are snapshots, not lifecycle: `Location` alone is never
1046///   evidence of a successful visit, and `None` clears title/favicon.
1047///
1048/// Fold navigation through [`crate::events::NavigationProgress`] and state
1049/// through [`crate::events::ObservedWebViewState`] instead of hand-rolling
1050/// attempt correlation:
1051///
1052/// ```ignore
1053/// fn on_navigation_event(&self, event: NavigationEvent) {
1054///     let mut progress = self.progress.lock().unwrap();
1055///     progress.apply(&event);
1056///     if let NavigationEvent::Succeeded { id, final_url } = &event
1057///         && progress.is_current(*id)
1058///     {
1059///         self.loaded(final_url);
1060///     }
1061/// }
1062/// ```
1063pub trait WebViewDelegate: Send + Sync {
1064    /// One correlated top-level navigation lifecycle event.
1065    ///
1066    /// Required: after the typed-event migration every delegate must decide
1067    /// how it handles the lifecycle — a silent default would lose page loads.
1068    fn on_navigation_event(&self, event: crate::events::NavigationEvent);
1069
1070    /// One observable-state snapshot (location, title, favicon,
1071    /// back/forward availability), coalesced and generation-scoped by the
1072    /// normalizer.
1073    fn on_webview_state_change(&self, _change: crate::events::WebViewStateChange) {}
1074
1075    /// A top-level document binding minted from reliable, non-stale commit
1076    /// evidence. It is never emitted for duplicate or ambiguous commits.
1077    fn on_document_committed(
1078        &self,
1079        _native_view: NativeWebViewId,
1080        _generation: DocumentGeneration,
1081        _navigation_id: crate::events::NavigationId,
1082    ) {
1083    }
1084
1085    /// A trusted native HTML load that reached a reliably committed document.
1086    ///
1087    /// This is stricter than [`Self::on_document_committed`]: it is emitted
1088    /// only when the platform returned the exact native navigation key for a
1089    /// direct native load and the normalizer bound that key to this accepted
1090    /// navigation. The data and base URL used for the load are not authority.
1091    fn on_trusted_document_admitted(&self, _admission: TrustedDocumentAdmission) {}
1092
1093    /// The platform terminated this exact WebView's content process. Only
1094    /// adapters with native evidence emit it; consumers must revoke any
1095    /// document authority before allowing a replacement to load.
1096    fn on_web_content_process_terminated(&self, _native_view: NativeWebViewId) {}
1097
1098    /// A backend proved that a previously committed document was restored
1099    /// without a fresh native start/commit chain (for example from BFCache).
1100    /// The adapter revokes its generation before invoking this hook.
1101    fn on_document_restored(&self, _native_view: NativeWebViewId, _url: &str) {}
1102
1103    /// Handles a postMessage from the page View(WebView).
1104    ///
1105    /// The context is assembled only by the platform adapter and must travel
1106    /// with the payload through any bridge admission decision.
1107    fn handle_post_message(&self, message: IncomingWebMessage);
1108
1109    /// Handles a native-component message posted by the page through the
1110    /// embedded-component channel (`window.NativeComponentBridge`), where
1111    /// the platform routes it in-process (currently Windows/WebView2).
1112    /// `message_json` is the raw component message (`component.mount`,
1113    /// `component.update`, ...).
1114    fn handle_native_component_message(&self, _message_json: String) {}
1115
1116    /// Receive log from WebView
1117    fn log(&self, level: LogLevel, message: &str);
1118}
1119
1120/// Represents an HTTP response whose body is provided by a file path, pipe, or in-memory bytes.
1121#[derive(Debug)]
1122pub struct WebResourceResponse {
1123    parts: http::response::Parts,
1124    body: WebResourceBody,
1125}
1126
1127impl From<Option<WebResourceResponse>> for SchemeOutcome {
1128    fn from(value: Option<WebResourceResponse>) -> Self {
1129        match value {
1130            Some(response) => SchemeOutcome::Handled(response),
1131            None => SchemeOutcome::PassThrough,
1132        }
1133    }
1134}
1135
1136impl WebResourceResponse {
1137    /// Borrow the response parts (status, headers, etc.).
1138    pub fn parts(&self) -> &http::response::Parts {
1139        &self.parts
1140    }
1141
1142    /// Consume the struct and return the owned parts and file path.
1143    pub fn into_parts(self) -> (http::response::Parts, WebResourceBody) {
1144        (self.parts, self.body)
1145    }
1146}
1147
1148/// Convenience conversion from (Parts, PathBuf)
1149impl From<(http::response::Parts, PathBuf)> for WebResourceResponse {
1150    fn from(value: (http::response::Parts, PathBuf)) -> Self {
1151        WebResourceResponse {
1152            parts: value.0,
1153            body: WebResourceBody::Path(value.1),
1154        }
1155    }
1156}
1157
1158/// Convenience conversion from (Parts, SystemPipeReader)
1159impl From<(http::response::Parts, SystemPipeReader)> for WebResourceResponse {
1160    fn from(value: (http::response::Parts, SystemPipeReader)) -> Self {
1161        WebResourceResponse {
1162            parts: value.0,
1163            body: WebResourceBody::Pipe(value.1),
1164        }
1165    }
1166}
1167
1168/// Convenience conversion from (Parts, Vec<u8>)
1169impl From<(http::response::Parts, Vec<u8>)> for WebResourceResponse {
1170    fn from(value: (http::response::Parts, Vec<u8>)) -> Self {
1171        WebResourceResponse {
1172            parts: value.0,
1173            body: WebResourceBody::Bytes(value.1),
1174        }
1175    }
1176}
1177
1178impl WebResourceResponse {
1179    fn response_parts_with_status(status: u16) -> http::response::Parts {
1180        let response = match http::Response::builder().status(status).body(()) {
1181            Ok(response) => response,
1182            Err(_) => http::Response::new(()),
1183        };
1184        let (parts, _) = response.into_parts();
1185        parts
1186    }
1187
1188    /// Create a response serving a file from disk (status 200).
1189    pub fn file(path: impl Into<PathBuf>) -> Self {
1190        let path = path.into();
1191        let content_length = std::fs::metadata(&path).ok().map(|m| m.len());
1192        let mut parts = Self::response_parts_with_status(200);
1193        if let Some(len) = content_length {
1194            parts
1195                .headers
1196                .insert(http::header::CONTENT_LENGTH, http::HeaderValue::from(len));
1197        }
1198        Self {
1199            parts,
1200            body: WebResourceBody::Path(path),
1201        }
1202    }
1203
1204    /// Create a response serving in-memory bytes (status 200).
1205    pub fn bytes(data: impl Into<Vec<u8>>) -> Self {
1206        let data = data.into();
1207        let len = data.len();
1208        let mut parts = Self::response_parts_with_status(200);
1209        parts
1210            .headers
1211            .insert(http::header::CONTENT_LENGTH, http::HeaderValue::from(len));
1212        Self {
1213            parts,
1214            body: WebResourceBody::Bytes(data),
1215        }
1216    }
1217
1218    /// Create a response serving data from a system pipe (status 200).
1219    pub fn stream(reader: SystemPipeReader) -> Self {
1220        let parts = Self::response_parts_with_status(200);
1221        Self {
1222            parts,
1223            body: WebResourceBody::Pipe(reader),
1224        }
1225    }
1226
1227    /// Set the Content-Type header (builder pattern).
1228    pub fn mime(mut self, content_type: &str) -> Self {
1229        if let Ok(value) = http::HeaderValue::from_str(content_type) {
1230            self.parts.headers.insert(http::header::CONTENT_TYPE, value);
1231        }
1232        self
1233    }
1234
1235    /// Set the HTTP status code (builder pattern).
1236    pub fn status(mut self, code: u16) -> Self {
1237        self.parts.status = http::StatusCode::from_u16(code).unwrap_or(self.parts.status);
1238        self
1239    }
1240
1241    /// Add a response header (builder pattern).
1242    pub fn header(mut self, name: &str, value: &str) -> Self {
1243        if let (Ok(header_name), Ok(header_value)) = (
1244            name.parse::<http::header::HeaderName>(),
1245            http::HeaderValue::from_str(value),
1246        ) {
1247            self.parts.headers.insert(header_name, header_value);
1248        }
1249        self
1250    }
1251
1252    /// Add CORS header `Access-Control-Allow-Origin: null` (builder pattern).
1253    pub fn cors(self) -> Self {
1254        self.header("access-control-allow-origin", "null")
1255    }
1256}
1257
1258#[cfg(test)]
1259mod tests {
1260    use super::*;
1261
1262    #[test]
1263    fn contextual_scheme_request_preserves_platform_context() {
1264        let request = http::Request::builder()
1265            .uri("lx://app/index.html")
1266            .body(vec![1, 2, 3])
1267            .unwrap();
1268        let request = ContextualSchemeRequest::new(
1269            request,
1270            NativeWebViewId::new(91),
1271            SchemeRequestFrame::TopLevelDocument,
1272        );
1273
1274        assert_eq!(request.native_view(), NativeWebViewId::new(91));
1275        assert_eq!(request.frame(), SchemeRequestFrame::TopLevelDocument);
1276        assert_eq!(request.request().uri(), "lx://app/index.html");
1277        assert_eq!(request.into_request().into_body(), vec![1, 2, 3]);
1278    }
1279}