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/// Outcome of handling a scheme request.
10#[derive(Debug)]
11pub enum SchemeOutcome {
12    /// Handler produced a response.
13    Handled(WebResourceResponse),
14    /// Handler intentionally declined the request.
15    PassThrough,
16}
17
18/// Async scheme handler signature.
19pub(crate) type AsyncSchemeFuture = Pin<Box<dyn Future<Output = SchemeOutcome> + Send + 'static>>;
20pub(crate) type AsyncSchemeHandler =
21    Arc<dyn Fn(http::Request<Vec<u8>>) -> AsyncSchemeFuture + Send + Sync>;
22
23/// Navigation policy decision returned by the navigation handler.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum NavigationPolicy {
26    /// Allow the WebView to navigate to this URL.
27    Allow,
28    /// Cancel the navigation. The handler is responsible for any side effects
29    /// (e.g., opening the URL externally via `AppRuntime::open_url()`).
30    Cancel,
31}
32
33/// A platform navigation request passed to the registered policy handler.
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct NavigationRequest {
36    pub url: String,
37    pub has_user_gesture: bool,
38    pub is_main_frame: bool,
39}
40
41impl NavigationRequest {
42    pub fn new(url: impl Into<String>, has_user_gesture: bool, is_main_frame: bool) -> Self {
43        Self {
44            url: url.into(),
45            has_user_gesture,
46            is_main_frame,
47        }
48    }
49}
50
51/// New-window policy decision returned by the new-window handler.
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum NewWindowPolicy {
54    /// Load the URL in the current WebView (replaces current page).
55    LoadInSelf,
56    /// Cancel the new-window request without doing anything.
57    Cancel,
58}
59
60pub type NavigationHandler = Box<dyn Fn(&NavigationRequest) -> NavigationPolicy + Send + Sync>;
61pub type NewWindowHandler = Box<dyn Fn(&str) -> NewWindowPolicy + Send + Sync>;
62
63/// Per-WebView user-agent override.
64#[derive(Debug, Clone, PartialEq, Eq)]
65pub enum UserAgentOverride {
66    /// Restore the user agent supplied by the platform WebView engine.
67    Default,
68    /// Replace the complete user-agent string. The value must be non-empty and
69    /// engine-compatible. This does not emulate other browser capabilities or
70    /// synchronize User-Agent Client Hints.
71    Custom(String),
72}
73
74impl UserAgentOverride {
75    /// Validate a complete override before applying or persisting it.
76    pub fn validate(&self) -> Result<(), WebViewError> {
77        if let Self::Custom(value) = self {
78            if value.trim().is_empty() {
79                return Err(WebViewError::WebView(
80                    "custom user-agent override must not be empty".to_string(),
81                ));
82            }
83            if value.contains(['\r', '\n', '\0']) {
84                return Err(WebViewError::WebView(
85                    "custom user-agent override must not contain CR, LF, or NUL".to_string(),
86                ));
87            }
88        }
89        Ok(())
90    }
91}
92
93#[cfg(test)]
94mod user_agent_override_tests {
95    use super::*;
96
97    #[test]
98    fn custom_user_agent_must_not_be_blank() {
99        assert!(UserAgentOverride::Custom(String::new()).validate().is_err());
100        assert!(UserAgentOverride::Custom("   ".into()).validate().is_err());
101        assert!(
102            UserAgentOverride::Custom("Mozilla/5.0 valid".into())
103                .validate()
104                .is_ok()
105        );
106        for invalid in [
107            "Mozilla/5.0\rInjected",
108            "Mozilla/5.0\nInjected",
109            "Mozilla\0/5.0",
110        ] {
111            assert!(
112                UserAgentOverride::Custom(invalid.into())
113                    .validate()
114                    .is_err()
115            );
116        }
117        assert!(UserAgentOverride::Default.validate().is_ok());
118    }
119}
120
121#[derive(Debug, Clone, PartialEq, Eq)]
122pub struct DownloadRequest {
123    /// Final download URL reported by the platform callback.
124    pub url: String,
125    /// Request user-agent if available on this platform.
126    pub user_agent: Option<String>,
127    /// `Content-Disposition` response header if exposed by the platform.
128    pub content_disposition: Option<String>,
129    /// Response MIME type if exposed by the platform.
130    pub mime_type: Option<String>,
131    /// Response content length if known.
132    pub content_length: Option<u64>,
133    /// Platform-suggested filename (may be absent).
134    pub suggested_filename: Option<String>,
135    /// Source page URL that initiated the download when available.
136    pub source_page_url: Option<String>,
137    /// Cookie header string for `url` when available.
138    pub cookie: Option<String>,
139}
140
141/// Download callback.
142///
143/// In browser profile, registering this callback makes download requests flow through the host
144/// app callback path instead of in-WebView download UI.
145pub type DownloadHandler = Box<dyn Fn(DownloadRequest) + Send + Sync>;
146
147#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
148#[serde(rename_all = "lowercase")]
149pub enum WebViewCookieSameSite {
150    Lax,
151    Strict,
152    None,
153}
154
155impl WebViewCookieSameSite {
156    pub fn as_str(self) -> &'static str {
157        match self {
158            Self::Lax => "lax",
159            Self::Strict => "strict",
160            Self::None => "none",
161        }
162    }
163}
164
165#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
166pub struct WebViewCookie {
167    pub name: String,
168    pub value: String,
169    pub domain: String,
170    pub path: String,
171    #[serde(default, skip_serializing_if = "is_false")]
172    pub host_only: bool,
173    #[serde(default)]
174    pub secure: bool,
175    #[serde(default)]
176    pub http_only: bool,
177    #[serde(default)]
178    pub session: bool,
179    #[serde(default, skip_serializing_if = "Option::is_none")]
180    pub expires_unix_ms: Option<i64>,
181    #[serde(default, skip_serializing_if = "Option::is_none")]
182    pub same_site: Option<WebViewCookieSameSite>,
183}
184
185#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
186pub struct WebViewCookieSetRequest {
187    #[serde(default)]
188    pub url: String,
189    pub name: String,
190    pub value: String,
191    #[serde(default, skip_serializing_if = "Option::is_none")]
192    pub domain: Option<String>,
193    #[serde(default = "default_cookie_path")]
194    pub path: String,
195    #[serde(default)]
196    pub secure: bool,
197    #[serde(default)]
198    pub http_only: bool,
199    #[serde(default, skip_serializing_if = "Option::is_none")]
200    pub expires_unix_ms: Option<i64>,
201    #[serde(default, skip_serializing_if = "Option::is_none")]
202    pub same_site: Option<WebViewCookieSameSite>,
203}
204
205fn default_cookie_path() -> String {
206    "/".to_string()
207}
208
209fn is_false(value: &bool) -> bool {
210    !*value
211}
212
213#[derive(Debug, Clone, PartialEq, Eq)]
214pub struct FileChooserRequest {
215    /// Accepted MIME types / extensions requested by the page.
216    pub accept_types: Vec<String>,
217    /// Whether multiple files may be selected.
218    pub allow_multiple: bool,
219    /// Whether directories may be selected.
220    pub allow_directories: bool,
221    /// Whether the page requested capture/live media.
222    pub capture: bool,
223    /// Source page URL that initiated the chooser when available.
224    pub source_page_url: Option<String>,
225}
226
227#[derive(Debug, Clone, PartialEq, Eq)]
228pub struct FileChooserFile {
229    pub path: Option<String>,
230    pub uri: Option<String>,
231}
232
233#[derive(Debug, Clone, PartialEq, Eq)]
234pub enum FileChooserResponse {
235    Cancel,
236    Error(String),
237    Files(Vec<FileChooserFile>),
238}
239
240/// Body source for WebResourceResponse
241#[derive(Debug)]
242pub enum WebResourceBody {
243    /// Serve data from a regular file path on disk
244    Path(PathBuf),
245    /// Serve data from a system pipe (read end)
246    Pipe(SystemPipeReader),
247    /// Serve data directly from memory
248    Bytes(Vec<u8>),
249}
250
251/// Cross‑platform system pipe reader (read end)
252#[derive(Debug)]
253pub struct SystemPipeReader {
254    #[cfg(unix)]
255    fd: std::os::fd::RawFd,
256    #[cfg(windows)]
257    handle: std::os::windows::io::RawHandle,
258}
259
260impl SystemPipeReader {
261    /// Consume and return the raw file descriptor (Unix).
262    /// Caller becomes responsible for closing it.
263    #[cfg(unix)]
264    pub fn into_raw_fd(self) -> std::os::fd::RawFd {
265        self.fd
266    }
267
268    /// Construct from a raw file descriptor (Unix).
269    ///
270    /// # Safety
271    ///
272    /// Caller guarantees that `fd` is a valid read end of a pipe file descriptor.
273    #[cfg(unix)]
274    pub unsafe fn from_raw_fd(fd: std::os::fd::RawFd) -> Self {
275        Self { fd }
276    }
277
278    /// Convert into a File for reading (consumes self).
279    #[cfg(unix)]
280    pub fn into_file(self) -> std::fs::File {
281        use std::os::fd::FromRawFd;
282        unsafe { std::fs::File::from_raw_fd(self.into_raw_fd()) }
283    }
284
285    /// Consume and return the raw handle (Windows).
286    /// Caller becomes responsible for closing it.
287    #[cfg(windows)]
288    pub fn into_raw_handle(self) -> std::os::windows::io::RawHandle {
289        self.handle
290    }
291
292    /// Construct from a raw handle (Windows).
293    ///
294    /// # Safety
295    ///
296    /// Caller guarantees that `handle` is a valid readable OS handle.
297    #[cfg(windows)]
298    pub unsafe fn from_raw_handle(handle: std::os::windows::io::RawHandle) -> Self {
299        Self { handle }
300    }
301
302    /// Convert into a File for reading (consumes self).
303    #[cfg(windows)]
304    pub fn into_file(self) -> std::fs::File {
305        use std::os::windows::io::FromRawHandle;
306        unsafe { std::fs::File::from_raw_handle(self.into_raw_handle()) }
307    }
308}
309
310/// Interface for controlling WebView (100% copy from lxapp)
311#[async_trait]
312pub trait WebViewController: Send + Sync {
313    /// Load a URL in the WebView
314    fn load_url(&self, url: &str) -> Result<(), WebViewError>;
315
316    /// Load HTML data into the WebView.
317    fn load_data(&self, request: LoadDataRequest<'_>) -> Result<(), WebViewError>;
318
319    /// Execute JavaScript in the WebView without observing its return value.
320    fn exec_js(&self, js: &str) -> Result<(), WebViewError>;
321
322    /// Evaluate JavaScript in the WebView and return the decoded JSON value.
323    ///
324    /// Implementations are required to be both CSP-safe (no `(0,eval)` /
325    /// `new Function` — pages whose CSP omits `'unsafe-eval'` must still
326    /// work) and `await`-aware (top-level `await` in the user expression
327    /// resolves before the future returns). Platforms achieve this by
328    /// dispatching through the native await-capable API
329    /// (`callAsyncJavaScript:` on Apple, `LingXiaProxy.resolveEval` JS
330    /// bridge on Android/Harmony).
331    async fn eval_js(&self, js: &str) -> Result<serde_json::Value, WebViewScriptError>;
332
333    /// Return the platform WebView's current URL.
334    async fn current_url(&self) -> Result<Option<String>, WebViewError> {
335        Err(WebViewError::WebView(
336            "current_url is not implemented for this platform".to_string(),
337        ))
338    }
339
340    /// Post a message to the WebView
341    fn post_message(&self, message: &str) -> Result<(), WebViewError>;
342
343    /// Clear browsing data from the WebView
344    fn clear_browsing_data(&self) -> Result<(), WebViewError>;
345
346    /// Override or restore the WebView user agent.
347    fn set_user_agent_override(&self, user_agent: UserAgentOverride) -> Result<(), WebViewError>;
348
349    /// Reload the current WebView document.
350    fn reload(&self) -> Result<(), WebViewError> {
351        Err(WebViewError::WebView(
352            "reload is not implemented for this platform".to_string(),
353        ))
354    }
355
356    /// Navigate back in WebView history.
357    fn go_back(&self) -> Result<(), WebViewError> {
358        Err(WebViewError::WebView(
359            "go_back is not implemented for this platform".to_string(),
360        ))
361    }
362
363    /// Navigate forward in WebView history.
364    fn go_forward(&self) -> Result<(), WebViewError> {
365        Err(WebViewError::WebView(
366            "go_forward is not implemented for this platform".to_string(),
367        ))
368    }
369
370    /// List HTTP cookies from the platform WebView cookie store.
371    async fn list_cookies(&self) -> Result<Vec<WebViewCookie>, WebViewError> {
372        Err(WebViewError::WebView(
373            "cookie store is not implemented for this platform".to_string(),
374        ))
375    }
376
377    /// Set an HTTP cookie through the platform WebView cookie store.
378    async fn set_cookie(&self, _request: WebViewCookieSetRequest) -> Result<(), WebViewError> {
379        Err(WebViewError::WebView(
380            "cookie store is not implemented for this platform".to_string(),
381        ))
382    }
383
384    /// Delete an HTTP cookie from the platform WebView cookie store.
385    async fn delete_cookie(
386        &self,
387        _name: &str,
388        _domain: &str,
389        _path: &str,
390    ) -> Result<(), WebViewError> {
391        Err(WebViewError::WebView(
392            "cookie store is not implemented for this platform".to_string(),
393        ))
394    }
395
396    /// Clear all HTTP cookies from the platform WebView cookie store.
397    async fn clear_cookies(&self) -> Result<(), WebViewError> {
398        Err(WebViewError::WebView(
399            "cookie store is not implemented for this platform".to_string(),
400        ))
401    }
402
403    /// Clear data owned by the current website without clearing the shared
404    /// browser profile. Platforms report whether their network cache supports
405    /// site-scoped removal.
406    async fn clear_site_data(
407        &self,
408        _url: &str,
409        _options: ClearSiteDataOptions,
410    ) -> Result<ClearSiteDataResult, WebViewError> {
411        Err(WebViewError::WebView(
412            "site-scoped data clearing is not implemented for this platform".to_string(),
413        ))
414    }
415
416    /// Capture a PNG screenshot of the WebView's visible content.
417    /// Returns raw PNG-encoded bytes ready to be base64'd over the wire.
418    async fn take_screenshot(&self) -> Result<Vec<u8>, WebViewError> {
419        Err(WebViewError::WebView(
420            "screenshot is not implemented for this platform".to_string(),
421        ))
422    }
423
424    /// Begin recording network requests/responses into a bounded per-webview
425    /// buffer, retrievable via [`Self::network_entries`]. Dev-tooling only;
426    /// implemented on platforms whose WebView exposes an inspection protocol
427    /// (currently Windows/WebView2 via the Chrome DevTools Protocol).
428    async fn start_network_capture(&self) -> Result<(), WebViewError> {
429        Err(WebViewError::WebView(
430            "network capture is not implemented for this platform".to_string(),
431        ))
432    }
433
434    /// Stop recording network traffic. Captured entries are kept until
435    /// [`Self::clear_network_capture`] or the webview is torn down.
436    async fn stop_network_capture(&self) -> Result<(), WebViewError> {
437        Err(WebViewError::WebView(
438            "network capture is not implemented for this platform".to_string(),
439        ))
440    }
441
442    /// Snapshot the captured network entries (oldest first). `dropped` counts
443    /// entries evicted from the ring buffer since the last clear.
444    async fn network_entries(&self) -> Result<NetworkCaptureSnapshot, WebViewError> {
445        Err(WebViewError::WebView(
446            "network capture is not implemented for this platform".to_string(),
447        ))
448    }
449
450    /// Drop all captured entries (leaves capture enabled if it was on).
451    async fn clear_network_capture(&self) -> Result<(), WebViewError> {
452        Err(WebViewError::WebView(
453            "network capture is not implemented for this platform".to_string(),
454        ))
455    }
456}
457
458/// Data categories to remove for one site via
459/// [`WebViewController::clear_site_data`].
460#[derive(Debug, Clone, Copy)]
461pub struct ClearSiteDataOptions {
462    pub cache: bool,
463    pub site_data: bool,
464}
465
466/// Outcome of [`WebViewController::clear_site_data`]. Each flag means "this
467/// category was requested AND the platform fully honored it" — `false` both
468/// when the category was not requested and when it could not be fully cleared.
469///
470/// Windows caveat: WebView2 clears the site's Cache Storage/appcache but
471/// cannot site-scope the shared HTTP cache, so it reports
472/// `cache_cleared: false` even when cache clearing was requested.
473#[derive(Debug, Clone, Copy)]
474pub struct ClearSiteDataResult {
475    pub cache_cleared: bool,
476    pub site_data_cleared: bool,
477}
478
479/// One captured network request and its response (when it completed).
480#[derive(Debug, Clone, Serialize, Deserialize)]
481pub struct NetworkEntry {
482    /// Protocol request id, stable across the request/response events.
483    pub request_id: String,
484    pub url: String,
485    pub method: String,
486    /// Resource kind reported by the engine (document, xhr, fetch, script,
487    /// image, ...), when available.
488    pub resource_type: Option<String>,
489    pub request_headers: Vec<(String, String)>,
490    /// Request payload (POST body) as reported by the engine, when present.
491    pub request_body: Option<String>,
492    pub status: Option<u16>,
493    pub response_headers: Vec<(String, String)>,
494    pub mime_type: Option<String>,
495    pub response_body: NetworkBody,
496    pub from_cache: bool,
497    /// Populated when the request failed (engine error text) instead of
498    /// producing a response.
499    pub failed: Option<String>,
500    /// Wall-clock start time (Unix epoch seconds), when the engine reports it.
501    pub wall_time: Option<f64>,
502    /// Monotonic engine timestamps (seconds), for ordering and durations.
503    pub started: f64,
504    pub finished: Option<f64>,
505}
506
507impl NetworkEntry {
508    /// Request duration in milliseconds, once the response has completed.
509    pub fn duration_ms(&self) -> Option<f64> {
510        self.finished
511            .filter(|finished| *finished >= self.started)
512            .map(|finished| (finished - self.started) * 1000.0)
513    }
514}
515
516/// Response body of a captured entry.
517#[derive(Debug, Clone, Default, Serialize, Deserialize)]
518#[serde(tag = "kind", rename_all = "snake_case")]
519pub enum NetworkBody {
520    /// No body captured yet (in flight) or the response had none.
521    #[default]
522    None,
523    /// UTF-8 text body.
524    Text { text: String },
525    /// Base64-encoded binary body.
526    Base64 { base64: String },
527    /// Body deliberately not captured (e.g. over the size cap, or evicted
528    /// before it could be read); `reason` says which.
529    Skipped { reason: String },
530}
531
532/// A point-in-time view of the capture buffer.
533#[derive(Debug, Clone, Default, Serialize, Deserialize)]
534pub struct NetworkCaptureSnapshot {
535    pub entries: Vec<NetworkEntry>,
536    /// Entries evicted from the ring buffer since the last clear (buffer
537    /// full). Surfaced so truncation is never silent.
538    pub dropped: u64,
539}
540
541#[derive(Debug, Clone, Default, Serialize, Deserialize)]
542pub struct ClickOptions {
543    #[serde(default, skip_serializing_if = "Option::is_none")]
544    pub index: Option<usize>,
545}
546
547#[derive(Debug, Clone, Default, Serialize, Deserialize)]
548pub struct TypeOptions {
549    #[serde(default, skip_serializing_if = "Option::is_none")]
550    pub index: Option<usize>,
551    #[serde(default)]
552    pub replace: bool,
553}
554
555#[derive(Debug, Clone, Default, Serialize, Deserialize)]
556pub struct FillOptions {
557    #[serde(default, skip_serializing_if = "Option::is_none")]
558    pub index: Option<usize>,
559}
560
561#[derive(Debug, Clone, Default, Serialize, Deserialize)]
562pub struct PressOptions {
563    #[serde(default, skip_serializing_if = "Option::is_none")]
564    pub selector: Option<String>,
565    #[serde(default, skip_serializing_if = "Option::is_none")]
566    pub index: Option<usize>,
567}
568
569#[derive(Debug, Clone, Default, Serialize, Deserialize)]
570pub struct ScrollOptions;
571
572#[async_trait]
573pub trait WebViewInputController: WebViewController {
574    async fn click(
575        &self,
576        _selector: &str,
577        _options: ClickOptions,
578    ) -> Result<(), WebViewInputError> {
579        Err(WebViewInputError::Unsupported(
580            "input control is not implemented for this platform",
581        ))
582    }
583
584    async fn type_text(
585        &self,
586        _selector: &str,
587        _text: &str,
588        _options: TypeOptions,
589    ) -> Result<(), WebViewInputError> {
590        Err(WebViewInputError::Unsupported(
591            "input control is not implemented for this platform",
592        ))
593    }
594
595    async fn fill(
596        &self,
597        _selector: &str,
598        _text: &str,
599        _options: FillOptions,
600    ) -> Result<(), WebViewInputError> {
601        Err(WebViewInputError::Unsupported(
602            "input control is not implemented for this platform",
603        ))
604    }
605
606    async fn press(&self, _key: &str, _options: PressOptions) -> Result<(), WebViewInputError> {
607        Err(WebViewInputError::Unsupported(
608            "input control is not implemented for this platform",
609        ))
610    }
611
612    async fn scroll(
613        &self,
614        _dx: f64,
615        _dy: f64,
616        _options: ScrollOptions,
617    ) -> Result<(), WebViewInputError> {
618        Err(WebViewInputError::Unsupported(
619            "input control is not implemented for this platform",
620        ))
621    }
622
623    async fn scroll_to(
624        &self,
625        _selector: &str,
626        _options: ScrollOptions,
627    ) -> Result<(), WebViewInputError> {
628        Err(WebViewInputError::Unsupported(
629            "input control is not implemented for this platform",
630        ))
631    }
632}
633
634#[derive(Debug, Clone, Copy)]
635pub struct LoadDataRequest<'a> {
636    pub data: &'a str,
637    pub base_url: &'a str,
638    pub history_url: Option<&'a str>,
639}
640
641impl<'a> LoadDataRequest<'a> {
642    pub fn new(data: &'a str, base_url: &'a str) -> Self {
643        Self {
644            data,
645            base_url,
646            history_url: None,
647        }
648    }
649
650    pub fn with_history_url(mut self, history_url: &'a str) -> Self {
651        self.history_url = Some(history_url);
652        self
653    }
654}
655
656/// Normalized category for a main-frame page load failure.
657///
658/// Cancellation is deliberately not a kind: a cancelled navigation is control
659/// flow and terminates as `NavigationEvent::Cancelled`, never as a load error.
660#[derive(Debug, Clone, Copy, PartialEq, Eq)]
661pub enum LoadErrorKind {
662    Dns,
663    Network,
664    Timeout,
665    Security,
666    InvalidUrl,
667    NotFound,
668    Unknown,
669}
670
671/// Error reported when a main-frame page load fails (DNS, network, TLS, etc.).
672///
673/// `kind` is the stable value for program logic; `description` is platform
674/// diagnostic text for logs and must not be parsed or shown directly as
675/// localized product copy.
676#[derive(Debug, Clone, PartialEq, Eq)]
677pub struct LoadError {
678    /// URL that failed to load, if the platform exposes it.
679    pub failing_url: Option<String>,
680    /// Cross-platform error category for application logic and UI.
681    pub kind: LoadErrorKind,
682    /// Human-readable description from the platform.
683    pub description: String,
684}
685
686/// WebView delegate: typed navigation lifecycle, observable state, page
687/// messaging, and logging for one WebView. Exactly one owner per WebView
688/// (an lxapp `PageInstance` or a browser tab delegate); read-only watchers
689/// use [`crate::events::normalizer::add_observer`]-registered observers.
690///
691/// Delivery contract (enforced by the event normalizer):
692/// - events arrive by value, serially, synchronously on the submitting
693///   thread, flattened FIFO — a callback is never re-entered for the same
694///   WebView;
695/// - callbacks may arrive on the WebView's own UI thread; fire-and-forget
696///   commands (`exec_js`) are safe there, but result-awaiting APIs must not
697///   block the callback thread;
698/// - every `Started` gets exactly one terminal event; success owns a
699///   non-empty final URL; cancellation is control flow, never a load error;
700/// - state changes are snapshots, not lifecycle: `Location` alone is never
701///   evidence of a successful visit, and `None` clears title/favicon.
702///
703/// Fold navigation through [`crate::events::NavigationProgress`] and state
704/// through [`crate::events::ObservedWebViewState`] instead of hand-rolling
705/// attempt correlation:
706///
707/// ```ignore
708/// fn on_navigation_event(&self, event: NavigationEvent) {
709///     let mut progress = self.progress.lock().unwrap();
710///     progress.apply(&event);
711///     if let NavigationEvent::Succeeded { id, final_url } = &event
712///         && progress.is_current(*id)
713///     {
714///         self.loaded(final_url);
715///     }
716/// }
717/// ```
718pub trait WebViewDelegate: Send + Sync {
719    /// One correlated top-level navigation lifecycle event.
720    ///
721    /// Required: after the typed-event migration every delegate must decide
722    /// how it handles the lifecycle — a silent default would lose page loads.
723    fn on_navigation_event(&self, event: crate::events::NavigationEvent);
724
725    /// One observable-state snapshot (location, title, favicon,
726    /// back/forward availability), coalesced and generation-scoped by the
727    /// normalizer.
728    fn on_webview_state_change(&self, _change: crate::events::WebViewStateChange) {}
729
730    /// Handles a postMessage from the page View(WebView)
731    fn handle_post_message(&self, msg: String);
732
733    /// Handles a native-component message posted by the page through the
734    /// embedded-component channel (`window.NativeComponentBridge`), where
735    /// the platform routes it in-process (currently Windows/WebView2).
736    /// `message_json` is the raw component message (`component.mount`,
737    /// `component.update`, ...).
738    fn handle_native_component_message(&self, _message_json: String) {}
739
740    /// Receive log from WebView
741    fn log(&self, level: LogLevel, message: &str);
742}
743
744/// Represents an HTTP response whose body is provided by a file path, pipe, or in-memory bytes.
745#[derive(Debug)]
746pub struct WebResourceResponse {
747    parts: http::response::Parts,
748    body: WebResourceBody,
749}
750
751impl From<Option<WebResourceResponse>> for SchemeOutcome {
752    fn from(value: Option<WebResourceResponse>) -> Self {
753        match value {
754            Some(response) => SchemeOutcome::Handled(response),
755            None => SchemeOutcome::PassThrough,
756        }
757    }
758}
759
760impl WebResourceResponse {
761    /// Borrow the response parts (status, headers, etc.).
762    pub fn parts(&self) -> &http::response::Parts {
763        &self.parts
764    }
765
766    /// Consume the struct and return the owned parts and file path.
767    pub fn into_parts(self) -> (http::response::Parts, WebResourceBody) {
768        (self.parts, self.body)
769    }
770}
771
772/// Convenience conversion from (Parts, PathBuf)
773impl From<(http::response::Parts, PathBuf)> for WebResourceResponse {
774    fn from(value: (http::response::Parts, PathBuf)) -> Self {
775        WebResourceResponse {
776            parts: value.0,
777            body: WebResourceBody::Path(value.1),
778        }
779    }
780}
781
782/// Convenience conversion from (Parts, SystemPipeReader)
783impl From<(http::response::Parts, SystemPipeReader)> for WebResourceResponse {
784    fn from(value: (http::response::Parts, SystemPipeReader)) -> Self {
785        WebResourceResponse {
786            parts: value.0,
787            body: WebResourceBody::Pipe(value.1),
788        }
789    }
790}
791
792/// Convenience conversion from (Parts, Vec<u8>)
793impl From<(http::response::Parts, Vec<u8>)> for WebResourceResponse {
794    fn from(value: (http::response::Parts, Vec<u8>)) -> Self {
795        WebResourceResponse {
796            parts: value.0,
797            body: WebResourceBody::Bytes(value.1),
798        }
799    }
800}
801
802impl WebResourceResponse {
803    fn response_parts_with_status(status: u16) -> http::response::Parts {
804        let response = match http::Response::builder().status(status).body(()) {
805            Ok(response) => response,
806            Err(_) => http::Response::new(()),
807        };
808        let (parts, _) = response.into_parts();
809        parts
810    }
811
812    /// Create a response serving a file from disk (status 200).
813    pub fn file(path: impl Into<PathBuf>) -> Self {
814        let path = path.into();
815        let content_length = std::fs::metadata(&path).ok().map(|m| m.len());
816        let mut parts = Self::response_parts_with_status(200);
817        if let Some(len) = content_length {
818            parts
819                .headers
820                .insert(http::header::CONTENT_LENGTH, http::HeaderValue::from(len));
821        }
822        Self {
823            parts,
824            body: WebResourceBody::Path(path),
825        }
826    }
827
828    /// Create a response serving in-memory bytes (status 200).
829    pub fn bytes(data: impl Into<Vec<u8>>) -> Self {
830        let data = data.into();
831        let len = data.len();
832        let mut parts = Self::response_parts_with_status(200);
833        parts
834            .headers
835            .insert(http::header::CONTENT_LENGTH, http::HeaderValue::from(len));
836        Self {
837            parts,
838            body: WebResourceBody::Bytes(data),
839        }
840    }
841
842    /// Create a response serving data from a system pipe (status 200).
843    pub fn stream(reader: SystemPipeReader) -> Self {
844        let parts = Self::response_parts_with_status(200);
845        Self {
846            parts,
847            body: WebResourceBody::Pipe(reader),
848        }
849    }
850
851    /// Set the Content-Type header (builder pattern).
852    pub fn mime(mut self, content_type: &str) -> Self {
853        if let Ok(value) = http::HeaderValue::from_str(content_type) {
854            self.parts.headers.insert(http::header::CONTENT_TYPE, value);
855        }
856        self
857    }
858
859    /// Set the HTTP status code (builder pattern).
860    pub fn status(mut self, code: u16) -> Self {
861        self.parts.status = http::StatusCode::from_u16(code).unwrap_or(self.parts.status);
862        self
863    }
864
865    /// Add a response header (builder pattern).
866    pub fn header(mut self, name: &str, value: &str) -> Self {
867        if let (Ok(header_name), Ok(header_value)) = (
868            name.parse::<http::header::HeaderName>(),
869            http::HeaderValue::from_str(value),
870        ) {
871            self.parts.headers.insert(header_name, header_value);
872        }
873        self
874    }
875
876    /// Add CORS header `Access-Control-Allow-Origin: null` (builder pattern).
877    pub fn cors(self) -> Self {
878        self.header("access-control-allow-origin", "null")
879    }
880}