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/// New-window policy decision returned by the new-window handler.
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum NewWindowPolicy {
36    /// Load the URL in the current WebView (replaces current page).
37    LoadInSelf,
38    /// Cancel the new-window request without doing anything.
39    Cancel,
40}
41
42pub type NavigationHandler = Box<dyn Fn(&str) -> NavigationPolicy + Send + Sync>;
43pub type NewWindowHandler = Box<dyn Fn(&str) -> NewWindowPolicy + Send + Sync>;
44
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct DownloadRequest {
47    /// Final download URL reported by the platform callback.
48    pub url: String,
49    /// Request user-agent if available on this platform.
50    pub user_agent: Option<String>,
51    /// `Content-Disposition` response header if exposed by the platform.
52    pub content_disposition: Option<String>,
53    /// Response MIME type if exposed by the platform.
54    pub mime_type: Option<String>,
55    /// Response content length if known.
56    pub content_length: Option<u64>,
57    /// Platform-suggested filename (may be absent).
58    pub suggested_filename: Option<String>,
59    /// Source page URL that initiated the download when available.
60    pub source_page_url: Option<String>,
61    /// Cookie header string for `url` when available.
62    pub cookie: Option<String>,
63}
64
65/// Download callback.
66///
67/// In browser profile, registering this callback makes download requests flow through the host
68/// app callback path instead of in-WebView download UI.
69pub type DownloadHandler = Box<dyn Fn(DownloadRequest) + Send + Sync>;
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
72#[serde(rename_all = "lowercase")]
73pub enum WebViewCookieSameSite {
74    Lax,
75    Strict,
76    None,
77}
78
79impl WebViewCookieSameSite {
80    pub fn as_str(self) -> &'static str {
81        match self {
82            Self::Lax => "lax",
83            Self::Strict => "strict",
84            Self::None => "none",
85        }
86    }
87}
88
89#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
90pub struct WebViewCookie {
91    pub name: String,
92    pub value: String,
93    pub domain: String,
94    pub path: String,
95    #[serde(default, skip_serializing_if = "is_false")]
96    pub host_only: bool,
97    #[serde(default)]
98    pub secure: bool,
99    #[serde(default)]
100    pub http_only: bool,
101    #[serde(default)]
102    pub session: bool,
103    #[serde(default, skip_serializing_if = "Option::is_none")]
104    pub expires_unix_ms: Option<i64>,
105    #[serde(default, skip_serializing_if = "Option::is_none")]
106    pub same_site: Option<WebViewCookieSameSite>,
107}
108
109#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
110pub struct WebViewCookieSetRequest {
111    #[serde(default)]
112    pub url: String,
113    pub name: String,
114    pub value: String,
115    #[serde(default, skip_serializing_if = "Option::is_none")]
116    pub domain: Option<String>,
117    #[serde(default = "default_cookie_path")]
118    pub path: String,
119    #[serde(default)]
120    pub secure: bool,
121    #[serde(default)]
122    pub http_only: bool,
123    #[serde(default, skip_serializing_if = "Option::is_none")]
124    pub expires_unix_ms: Option<i64>,
125    #[serde(default, skip_serializing_if = "Option::is_none")]
126    pub same_site: Option<WebViewCookieSameSite>,
127}
128
129fn default_cookie_path() -> String {
130    "/".to_string()
131}
132
133fn is_false(value: &bool) -> bool {
134    !*value
135}
136
137#[derive(Debug, Clone, PartialEq, Eq)]
138pub struct FileChooserRequest {
139    /// Accepted MIME types / extensions requested by the page.
140    pub accept_types: Vec<String>,
141    /// Whether multiple files may be selected.
142    pub allow_multiple: bool,
143    /// Whether directories may be selected.
144    pub allow_directories: bool,
145    /// Whether the page requested capture/live media.
146    pub capture: bool,
147    /// Source page URL that initiated the chooser when available.
148    pub source_page_url: Option<String>,
149}
150
151#[derive(Debug, Clone, PartialEq, Eq)]
152pub struct FileChooserFile {
153    pub path: Option<String>,
154    pub uri: Option<String>,
155}
156
157#[derive(Debug, Clone, PartialEq, Eq)]
158pub enum FileChooserResponse {
159    Cancel,
160    Error(String),
161    Files(Vec<FileChooserFile>),
162}
163
164/// Body source for WebResourceResponse
165#[derive(Debug)]
166pub enum WebResourceBody {
167    /// Serve data from a regular file path on disk
168    Path(PathBuf),
169    /// Serve data from a system pipe (read end)
170    Pipe(SystemPipeReader),
171    /// Serve data directly from memory
172    Bytes(Vec<u8>),
173}
174
175/// Cross‑platform system pipe reader (read end)
176#[derive(Debug)]
177pub struct SystemPipeReader {
178    #[cfg(unix)]
179    fd: std::os::fd::RawFd,
180    #[cfg(windows)]
181    handle: std::os::windows::io::RawHandle,
182}
183
184impl SystemPipeReader {
185    /// Consume and return the raw file descriptor (Unix).
186    /// Caller becomes responsible for closing it.
187    #[cfg(unix)]
188    pub fn into_raw_fd(self) -> std::os::fd::RawFd {
189        self.fd
190    }
191
192    /// Construct from a raw file descriptor (Unix).
193    ///
194    /// # Safety
195    ///
196    /// Caller guarantees that `fd` is a valid read end of a pipe file descriptor.
197    #[cfg(unix)]
198    pub unsafe fn from_raw_fd(fd: std::os::fd::RawFd) -> Self {
199        Self { fd }
200    }
201
202    /// Convert into a File for reading (consumes self).
203    #[cfg(unix)]
204    pub fn into_file(self) -> std::fs::File {
205        use std::os::fd::FromRawFd;
206        unsafe { std::fs::File::from_raw_fd(self.into_raw_fd()) }
207    }
208
209    /// Consume and return the raw handle (Windows).
210    /// Caller becomes responsible for closing it.
211    #[cfg(windows)]
212    pub fn into_raw_handle(self) -> std::os::windows::io::RawHandle {
213        self.handle
214    }
215
216    /// Construct from a raw handle (Windows).
217    ///
218    /// # Safety
219    ///
220    /// Caller guarantees that `handle` is a valid readable OS handle.
221    #[cfg(windows)]
222    pub unsafe fn from_raw_handle(handle: std::os::windows::io::RawHandle) -> Self {
223        Self { handle }
224    }
225
226    /// Convert into a File for reading (consumes self).
227    #[cfg(windows)]
228    pub fn into_file(self) -> std::fs::File {
229        use std::os::windows::io::FromRawHandle;
230        unsafe { std::fs::File::from_raw_handle(self.into_raw_handle()) }
231    }
232}
233
234/// Interface for controlling WebView (100% copy from lxapp)
235#[async_trait]
236pub trait WebViewController: Send + Sync {
237    /// Load a URL in the WebView
238    fn load_url(&self, url: &str) -> Result<(), WebViewError>;
239
240    /// Load HTML data into the WebView.
241    fn load_data(&self, request: LoadDataRequest<'_>) -> Result<(), WebViewError>;
242
243    /// Execute JavaScript in the WebView without observing its return value.
244    fn exec_js(&self, js: &str) -> Result<(), WebViewError>;
245
246    /// Evaluate JavaScript in the WebView and return the decoded JSON value.
247    ///
248    /// Implementations are required to be both CSP-safe (no `(0,eval)` /
249    /// `new Function` — pages whose CSP omits `'unsafe-eval'` must still
250    /// work) and `await`-aware (top-level `await` in the user expression
251    /// resolves before the future returns). Platforms achieve this by
252    /// dispatching through the native await-capable API
253    /// (`callAsyncJavaScript:` on Apple, `LingXiaProxy.resolveEval` JS
254    /// bridge on Android/Harmony).
255    async fn eval_js(&self, js: &str) -> Result<serde_json::Value, WebViewScriptError>;
256
257    /// Return the platform WebView's current URL.
258    async fn current_url(&self) -> Result<Option<String>, WebViewError> {
259        Err(WebViewError::WebView(
260            "current_url is not implemented for this platform".to_string(),
261        ))
262    }
263
264    /// Post a message to the WebView
265    fn post_message(&self, message: &str) -> Result<(), WebViewError>;
266
267    /// Clear browsing data from the WebView
268    fn clear_browsing_data(&self) -> Result<(), WebViewError>;
269
270    /// Set the user agent string for the WebView
271    fn set_user_agent(&self, ua: &str) -> Result<(), WebViewError>;
272
273    /// Reload the current WebView document.
274    fn reload(&self) -> Result<(), WebViewError> {
275        Err(WebViewError::WebView(
276            "reload is not implemented for this platform".to_string(),
277        ))
278    }
279
280    /// Navigate back in WebView history.
281    fn go_back(&self) -> Result<(), WebViewError> {
282        Err(WebViewError::WebView(
283            "go_back is not implemented for this platform".to_string(),
284        ))
285    }
286
287    /// Navigate forward in WebView history.
288    fn go_forward(&self) -> Result<(), WebViewError> {
289        Err(WebViewError::WebView(
290            "go_forward is not implemented for this platform".to_string(),
291        ))
292    }
293
294    /// List HTTP cookies from the platform WebView cookie store.
295    async fn list_cookies(&self) -> Result<Vec<WebViewCookie>, WebViewError> {
296        Err(WebViewError::WebView(
297            "cookie store is not implemented for this platform".to_string(),
298        ))
299    }
300
301    /// Set an HTTP cookie through the platform WebView cookie store.
302    async fn set_cookie(&self, _request: WebViewCookieSetRequest) -> Result<(), WebViewError> {
303        Err(WebViewError::WebView(
304            "cookie store is not implemented for this platform".to_string(),
305        ))
306    }
307
308    /// Delete an HTTP cookie from the platform WebView cookie store.
309    async fn delete_cookie(
310        &self,
311        _name: &str,
312        _domain: &str,
313        _path: &str,
314    ) -> Result<(), WebViewError> {
315        Err(WebViewError::WebView(
316            "cookie store is not implemented for this platform".to_string(),
317        ))
318    }
319
320    /// Clear all HTTP cookies from the platform WebView cookie store.
321    async fn clear_cookies(&self) -> Result<(), WebViewError> {
322        Err(WebViewError::WebView(
323            "cookie store is not implemented for this platform".to_string(),
324        ))
325    }
326
327    /// Capture a PNG screenshot of the WebView's visible content.
328    /// Returns raw PNG-encoded bytes ready to be base64'd over the wire.
329    async fn take_screenshot(&self) -> Result<Vec<u8>, WebViewError> {
330        Err(WebViewError::WebView(
331            "screenshot is not implemented for this platform".to_string(),
332        ))
333    }
334}
335
336#[derive(Debug, Clone, Default, Serialize, Deserialize)]
337pub struct ClickOptions {
338    #[serde(default, skip_serializing_if = "Option::is_none")]
339    pub index: Option<usize>,
340}
341
342#[derive(Debug, Clone, Default, Serialize, Deserialize)]
343pub struct TypeOptions {
344    #[serde(default, skip_serializing_if = "Option::is_none")]
345    pub index: Option<usize>,
346    #[serde(default)]
347    pub replace: bool,
348}
349
350#[derive(Debug, Clone, Default, Serialize, Deserialize)]
351pub struct FillOptions {
352    #[serde(default, skip_serializing_if = "Option::is_none")]
353    pub index: Option<usize>,
354}
355
356#[derive(Debug, Clone, Default, Serialize, Deserialize)]
357pub struct PressOptions;
358
359#[derive(Debug, Clone, Default, Serialize, Deserialize)]
360pub struct ScrollOptions;
361
362#[async_trait]
363pub trait WebViewInputController: WebViewController {
364    async fn click(
365        &self,
366        _selector: &str,
367        _options: ClickOptions,
368    ) -> Result<(), WebViewInputError> {
369        Err(WebViewInputError::Unsupported(
370            "input control is not implemented for this platform",
371        ))
372    }
373
374    async fn type_text(
375        &self,
376        _selector: &str,
377        _text: &str,
378        _options: TypeOptions,
379    ) -> Result<(), WebViewInputError> {
380        Err(WebViewInputError::Unsupported(
381            "input control is not implemented for this platform",
382        ))
383    }
384
385    async fn fill(
386        &self,
387        _selector: &str,
388        _text: &str,
389        _options: FillOptions,
390    ) -> Result<(), WebViewInputError> {
391        Err(WebViewInputError::Unsupported(
392            "input control is not implemented for this platform",
393        ))
394    }
395
396    async fn press(&self, _key: &str, _options: PressOptions) -> Result<(), WebViewInputError> {
397        Err(WebViewInputError::Unsupported(
398            "input control is not implemented for this platform",
399        ))
400    }
401
402    async fn scroll(
403        &self,
404        _dx: f64,
405        _dy: f64,
406        _options: ScrollOptions,
407    ) -> Result<(), WebViewInputError> {
408        Err(WebViewInputError::Unsupported(
409            "input control is not implemented for this platform",
410        ))
411    }
412
413    async fn scroll_to(
414        &self,
415        _selector: &str,
416        _options: ScrollOptions,
417    ) -> Result<(), WebViewInputError> {
418        Err(WebViewInputError::Unsupported(
419            "input control is not implemented for this platform",
420        ))
421    }
422}
423
424#[derive(Debug, Clone, Copy)]
425pub struct LoadDataRequest<'a> {
426    pub data: &'a str,
427    pub base_url: &'a str,
428    pub history_url: Option<&'a str>,
429}
430
431impl<'a> LoadDataRequest<'a> {
432    pub fn new(data: &'a str, base_url: &'a str) -> Self {
433        Self {
434            data,
435            base_url,
436            history_url: None,
437        }
438    }
439
440    pub fn with_history_url(mut self, history_url: &'a str) -> Self {
441        self.history_url = Some(history_url);
442        self
443    }
444}
445
446/// Normalized category for a main-frame page load failure.
447#[derive(Debug, Clone, Copy, PartialEq, Eq)]
448pub enum LoadErrorKind {
449    Dns,
450    Network,
451    Timeout,
452    Security,
453    Cancelled,
454    InvalidUrl,
455    NotFound,
456    Unknown,
457}
458
459/// Error reported when a main-frame page load fails (DNS, network, TLS, etc.).
460///
461/// The webview crate is responsible only for delivering this event.
462/// What to display is entirely up to the caller.
463#[derive(Debug, Clone)]
464pub struct LoadError {
465    /// URL that failed to load, if the platform exposes it.
466    pub url: Option<String>,
467    /// Cross-platform error category for application logic and UI.
468    pub kind: LoadErrorKind,
469    /// Human-readable description from the platform.
470    pub description: String,
471}
472
473/// WebView delegate trait - focused on WebView events only
474pub trait WebViewDelegate: Send + Sync {
475    /// Called when the page starts loading
476    fn on_page_started(&self);
477
478    /// Called when the page finishes loading
479    fn on_page_finished(&self);
480
481    /// Called when a main-frame page load fails (e.g. DNS failure, network unreachable, TLS error).
482    ///
483    /// Only fires for the main document; sub-resource errors are ignored.
484    /// Default is a no-op so existing implementations do not need to change.
485    fn on_load_error(&self, _error: &LoadError) {}
486
487    /// Called when the document title changes (where the platform reports
488    /// it; currently Windows/WebView2). Default is a no-op so existing
489    /// implementations do not need to change.
490    fn on_title_changed(&self, _title: &str) {}
491
492    /// Called when the page favicon changes (where the platform reports it;
493    /// currently Windows/WebView2). `png_bytes` holds the favicon encoded
494    /// as PNG; an empty vector means the page has no favicon. Default is a
495    /// no-op so existing implementations do not need to change.
496    fn on_favicon_changed(&self, _png_bytes: Vec<u8>) {}
497
498    /// Handles a postMessage from the page View(WebView)
499    fn handle_post_message(&self, msg: String);
500
501    /// Handles a native-component message posted by the page through the
502    /// embedded-component channel (`window.NativeComponentBridge`), where
503    /// the platform routes it in-process (currently Windows/WebView2).
504    /// `message_json` is the raw component message (`component.mount`,
505    /// `component.update`, ...). Default is a no-op so existing
506    /// implementations do not need to change.
507    fn handle_native_component_message(&self, message_json: &str) {
508        let _ = message_json;
509    }
510
511    /// Receive log from WebView
512    fn log(&self, level: LogLevel, message: &str);
513}
514
515/// Represents an HTTP response whose body is provided by a file path, pipe, or in-memory bytes.
516#[derive(Debug)]
517pub struct WebResourceResponse {
518    parts: http::response::Parts,
519    body: WebResourceBody,
520}
521
522impl From<Option<WebResourceResponse>> for SchemeOutcome {
523    fn from(value: Option<WebResourceResponse>) -> Self {
524        match value {
525            Some(response) => SchemeOutcome::Handled(response),
526            None => SchemeOutcome::PassThrough,
527        }
528    }
529}
530
531impl WebResourceResponse {
532    /// Borrow the response parts (status, headers, etc.).
533    pub fn parts(&self) -> &http::response::Parts {
534        &self.parts
535    }
536
537    /// Consume the struct and return the owned parts and file path.
538    pub fn into_parts(self) -> (http::response::Parts, WebResourceBody) {
539        (self.parts, self.body)
540    }
541}
542
543/// Convenience conversion from (Parts, PathBuf)
544impl From<(http::response::Parts, PathBuf)> for WebResourceResponse {
545    fn from(value: (http::response::Parts, PathBuf)) -> Self {
546        WebResourceResponse {
547            parts: value.0,
548            body: WebResourceBody::Path(value.1),
549        }
550    }
551}
552
553/// Convenience conversion from (Parts, SystemPipeReader)
554impl From<(http::response::Parts, SystemPipeReader)> for WebResourceResponse {
555    fn from(value: (http::response::Parts, SystemPipeReader)) -> Self {
556        WebResourceResponse {
557            parts: value.0,
558            body: WebResourceBody::Pipe(value.1),
559        }
560    }
561}
562
563/// Convenience conversion from (Parts, Vec<u8>)
564impl From<(http::response::Parts, Vec<u8>)> for WebResourceResponse {
565    fn from(value: (http::response::Parts, Vec<u8>)) -> Self {
566        WebResourceResponse {
567            parts: value.0,
568            body: WebResourceBody::Bytes(value.1),
569        }
570    }
571}
572
573impl WebResourceResponse {
574    fn response_parts_with_status(status: u16) -> http::response::Parts {
575        let response = match http::Response::builder().status(status).body(()) {
576            Ok(response) => response,
577            Err(_) => http::Response::new(()),
578        };
579        let (parts, _) = response.into_parts();
580        parts
581    }
582
583    /// Create a response serving a file from disk (status 200).
584    pub fn file(path: impl Into<PathBuf>) -> Self {
585        let path = path.into();
586        let content_length = std::fs::metadata(&path).ok().map(|m| m.len());
587        let mut parts = Self::response_parts_with_status(200);
588        if let Some(len) = content_length {
589            parts
590                .headers
591                .insert(http::header::CONTENT_LENGTH, http::HeaderValue::from(len));
592        }
593        Self {
594            parts,
595            body: WebResourceBody::Path(path),
596        }
597    }
598
599    /// Create a response serving in-memory bytes (status 200).
600    pub fn bytes(data: impl Into<Vec<u8>>) -> Self {
601        let data = data.into();
602        let len = data.len();
603        let mut parts = Self::response_parts_with_status(200);
604        parts
605            .headers
606            .insert(http::header::CONTENT_LENGTH, http::HeaderValue::from(len));
607        Self {
608            parts,
609            body: WebResourceBody::Bytes(data),
610        }
611    }
612
613    /// Create a response serving data from a system pipe (status 200).
614    pub fn stream(reader: SystemPipeReader) -> Self {
615        let parts = Self::response_parts_with_status(200);
616        Self {
617            parts,
618            body: WebResourceBody::Pipe(reader),
619        }
620    }
621
622    /// Set the Content-Type header (builder pattern).
623    pub fn mime(mut self, content_type: &str) -> Self {
624        if let Ok(value) = http::HeaderValue::from_str(content_type) {
625            self.parts.headers.insert(http::header::CONTENT_TYPE, value);
626        }
627        self
628    }
629
630    /// Set the HTTP status code (builder pattern).
631    pub fn status(mut self, code: u16) -> Self {
632        self.parts.status = http::StatusCode::from_u16(code).unwrap_or(self.parts.status);
633        self
634    }
635
636    /// Add a response header (builder pattern).
637    pub fn header(mut self, name: &str, value: &str) -> Self {
638        if let (Ok(header_name), Ok(header_value)) = (
639            name.parse::<http::header::HeaderName>(),
640            http::HeaderValue::from_str(value),
641        ) {
642            self.parts.headers.insert(header_name, header_value);
643        }
644        self
645    }
646
647    /// Add CORS header `Access-Control-Allow-Origin: null` (builder pattern).
648    pub fn cors(self) -> Self {
649        self.header("access-control-allow-origin", "null")
650    }
651}