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