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}
181
182impl SystemPipeReader {
183    /// Consume and return the raw file descriptor (Unix).
184    /// Caller becomes responsible for closing it.
185    #[cfg(unix)]
186    pub fn into_raw_fd(self) -> std::os::fd::RawFd {
187        self.fd
188    }
189
190    /// Construct from a raw file descriptor (Unix).
191    ///
192    /// # Safety
193    ///
194    /// Caller guarantees that `fd` is a valid read end of a pipe file descriptor.
195    #[cfg(unix)]
196    pub unsafe fn from_raw_fd(fd: std::os::fd::RawFd) -> Self {
197        Self { fd }
198    }
199
200    /// Convert into a File for reading (consumes self).
201    #[cfg(unix)]
202    pub fn into_file(self) -> std::fs::File {
203        use std::os::fd::FromRawFd;
204        unsafe { std::fs::File::from_raw_fd(self.into_raw_fd()) }
205    }
206}
207
208/// Interface for controlling WebView (100% copy from lxapp)
209#[async_trait]
210pub trait WebViewController: Send + Sync {
211    /// Load a URL in the WebView
212    fn load_url(&self, url: &str) -> Result<(), WebViewError>;
213
214    /// Load HTML data into the WebView.
215    fn load_data(&self, request: LoadDataRequest<'_>) -> Result<(), WebViewError>;
216
217    /// Execute JavaScript in the WebView without observing its return value.
218    fn exec_js(&self, js: &str) -> Result<(), WebViewError>;
219
220    /// Evaluate JavaScript in the WebView and return the decoded JSON value.
221    ///
222    /// Implementations are required to be both CSP-safe (no `(0,eval)` /
223    /// `new Function` — pages whose CSP omits `'unsafe-eval'` must still
224    /// work) and `await`-aware (top-level `await` in the user expression
225    /// resolves before the future returns). Platforms achieve this by
226    /// dispatching through the native await-capable API
227    /// (`callAsyncJavaScript:` on Apple, `LingXiaProxy.resolveEval` JS
228    /// bridge on Android/Harmony).
229    async fn eval_js(&self, js: &str) -> Result<serde_json::Value, WebViewScriptError>;
230
231    /// Return the platform WebView's current URL.
232    async fn current_url(&self) -> Result<Option<String>, WebViewError> {
233        Err(WebViewError::WebView(
234            "current_url is not implemented for this platform".to_string(),
235        ))
236    }
237
238    /// Post a message to the WebView
239    fn post_message(&self, message: &str) -> Result<(), WebViewError>;
240
241    /// Clear browsing data from the WebView
242    fn clear_browsing_data(&self) -> Result<(), WebViewError>;
243
244    /// Set the user agent string for the WebView
245    fn set_user_agent(&self, ua: &str) -> Result<(), WebViewError>;
246
247    /// Reload the current WebView document.
248    fn reload(&self) -> Result<(), WebViewError> {
249        Err(WebViewError::WebView(
250            "reload is not implemented for this platform".to_string(),
251        ))
252    }
253
254    /// Navigate back in WebView history.
255    fn go_back(&self) -> Result<(), WebViewError> {
256        Err(WebViewError::WebView(
257            "go_back is not implemented for this platform".to_string(),
258        ))
259    }
260
261    /// Navigate forward in WebView history.
262    fn go_forward(&self) -> Result<(), WebViewError> {
263        Err(WebViewError::WebView(
264            "go_forward is not implemented for this platform".to_string(),
265        ))
266    }
267
268    /// List HTTP cookies from the platform WebView cookie store.
269    async fn list_cookies(&self) -> Result<Vec<WebViewCookie>, WebViewError> {
270        Err(WebViewError::WebView(
271            "cookie store is not implemented for this platform".to_string(),
272        ))
273    }
274
275    /// Set an HTTP cookie through the platform WebView cookie store.
276    async fn set_cookie(&self, _request: WebViewCookieSetRequest) -> Result<(), WebViewError> {
277        Err(WebViewError::WebView(
278            "cookie store is not implemented for this platform".to_string(),
279        ))
280    }
281
282    /// Delete an HTTP cookie from the platform WebView cookie store.
283    async fn delete_cookie(
284        &self,
285        _name: &str,
286        _domain: &str,
287        _path: &str,
288    ) -> Result<(), WebViewError> {
289        Err(WebViewError::WebView(
290            "cookie store is not implemented for this platform".to_string(),
291        ))
292    }
293
294    /// Clear all HTTP cookies from the platform WebView cookie store.
295    async fn clear_cookies(&self) -> Result<(), WebViewError> {
296        Err(WebViewError::WebView(
297            "cookie store is not implemented for this platform".to_string(),
298        ))
299    }
300
301    /// Capture a PNG screenshot of the WebView's visible content.
302    /// Returns raw PNG-encoded bytes ready to be base64'd over the wire.
303    async fn take_screenshot(&self) -> Result<Vec<u8>, WebViewError> {
304        Err(WebViewError::WebView(
305            "screenshot is not implemented for this platform".to_string(),
306        ))
307    }
308}
309
310#[derive(Debug, Clone, Default, Serialize, Deserialize)]
311pub struct ClickOptions {
312    #[serde(default, skip_serializing_if = "Option::is_none")]
313    pub index: Option<usize>,
314}
315
316#[derive(Debug, Clone, Default, Serialize, Deserialize)]
317pub struct TypeOptions {
318    #[serde(default, skip_serializing_if = "Option::is_none")]
319    pub index: Option<usize>,
320    #[serde(default)]
321    pub replace: bool,
322}
323
324#[derive(Debug, Clone, Default, Serialize, Deserialize)]
325pub struct FillOptions {
326    #[serde(default, skip_serializing_if = "Option::is_none")]
327    pub index: Option<usize>,
328}
329
330#[derive(Debug, Clone, Default, Serialize, Deserialize)]
331pub struct PressOptions;
332
333#[derive(Debug, Clone, Default, Serialize, Deserialize)]
334pub struct ScrollOptions;
335
336#[async_trait]
337pub trait WebViewInputController: WebViewController {
338    async fn click(
339        &self,
340        _selector: &str,
341        _options: ClickOptions,
342    ) -> Result<(), WebViewInputError> {
343        Err(WebViewInputError::Unsupported(
344            "input control is not implemented for this platform",
345        ))
346    }
347
348    async fn type_text(
349        &self,
350        _selector: &str,
351        _text: &str,
352        _options: TypeOptions,
353    ) -> Result<(), WebViewInputError> {
354        Err(WebViewInputError::Unsupported(
355            "input control is not implemented for this platform",
356        ))
357    }
358
359    async fn fill(
360        &self,
361        _selector: &str,
362        _text: &str,
363        _options: FillOptions,
364    ) -> Result<(), WebViewInputError> {
365        Err(WebViewInputError::Unsupported(
366            "input control is not implemented for this platform",
367        ))
368    }
369
370    async fn press(&self, _key: &str, _options: PressOptions) -> Result<(), WebViewInputError> {
371        Err(WebViewInputError::Unsupported(
372            "input control is not implemented for this platform",
373        ))
374    }
375
376    async fn scroll(
377        &self,
378        _dx: f64,
379        _dy: f64,
380        _options: ScrollOptions,
381    ) -> Result<(), WebViewInputError> {
382        Err(WebViewInputError::Unsupported(
383            "input control is not implemented for this platform",
384        ))
385    }
386
387    async fn scroll_to(
388        &self,
389        _selector: &str,
390        _options: ScrollOptions,
391    ) -> Result<(), WebViewInputError> {
392        Err(WebViewInputError::Unsupported(
393            "input control is not implemented for this platform",
394        ))
395    }
396}
397
398#[derive(Debug, Clone, Copy)]
399pub struct LoadDataRequest<'a> {
400    pub data: &'a str,
401    pub base_url: &'a str,
402    pub history_url: Option<&'a str>,
403}
404
405impl<'a> LoadDataRequest<'a> {
406    pub fn new(data: &'a str, base_url: &'a str) -> Self {
407        Self {
408            data,
409            base_url,
410            history_url: None,
411        }
412    }
413
414    pub fn with_history_url(mut self, history_url: &'a str) -> Self {
415        self.history_url = Some(history_url);
416        self
417    }
418}
419
420/// Normalized category for a main-frame page load failure.
421#[derive(Debug, Clone, Copy, PartialEq, Eq)]
422pub enum LoadErrorKind {
423    Dns,
424    Network,
425    Timeout,
426    Security,
427    Cancelled,
428    InvalidUrl,
429    NotFound,
430    Unknown,
431}
432
433/// Error reported when a main-frame page load fails (DNS, network, TLS, etc.).
434///
435/// The webview crate is responsible only for delivering this event.
436/// What to display is entirely up to the caller.
437#[derive(Debug, Clone)]
438pub struct LoadError {
439    /// URL that failed to load, if the platform exposes it.
440    pub url: Option<String>,
441    /// Cross-platform error category for application logic and UI.
442    pub kind: LoadErrorKind,
443    /// Human-readable description from the platform.
444    pub description: String,
445}
446
447/// WebView delegate trait - focused on WebView events only
448pub trait WebViewDelegate: Send + Sync {
449    /// Called when the page starts loading
450    fn on_page_started(&self);
451
452    /// Called when the page finishes loading
453    fn on_page_finished(&self);
454
455    /// Called when a main-frame page load fails (e.g. DNS failure, network unreachable, TLS error).
456    ///
457    /// Only fires for the main document; sub-resource errors are ignored.
458    /// Default is a no-op so existing implementations do not need to change.
459    fn on_load_error(&self, _error: &LoadError) {}
460
461    /// Handles a postMessage from the page View(WebView)
462    fn handle_post_message(&self, msg: String);
463
464    /// Receive log from WebView
465    fn log(&self, level: LogLevel, message: &str);
466}
467
468/// Represents an HTTP response whose body is provided by a file path, pipe, or in-memory bytes.
469#[derive(Debug)]
470pub struct WebResourceResponse {
471    parts: http::response::Parts,
472    body: WebResourceBody,
473}
474
475impl From<Option<WebResourceResponse>> for SchemeOutcome {
476    fn from(value: Option<WebResourceResponse>) -> Self {
477        match value {
478            Some(response) => SchemeOutcome::Handled(response),
479            None => SchemeOutcome::PassThrough,
480        }
481    }
482}
483
484impl WebResourceResponse {
485    /// Borrow the response parts (status, headers, etc.).
486    pub fn parts(&self) -> &http::response::Parts {
487        &self.parts
488    }
489
490    /// Consume the struct and return the owned parts and file path.
491    pub fn into_parts(self) -> (http::response::Parts, WebResourceBody) {
492        (self.parts, self.body)
493    }
494}
495
496/// Convenience conversion from (Parts, PathBuf)
497impl From<(http::response::Parts, PathBuf)> for WebResourceResponse {
498    fn from(value: (http::response::Parts, PathBuf)) -> Self {
499        WebResourceResponse {
500            parts: value.0,
501            body: WebResourceBody::Path(value.1),
502        }
503    }
504}
505
506/// Convenience conversion from (Parts, SystemPipeReader)
507impl From<(http::response::Parts, SystemPipeReader)> for WebResourceResponse {
508    fn from(value: (http::response::Parts, SystemPipeReader)) -> Self {
509        WebResourceResponse {
510            parts: value.0,
511            body: WebResourceBody::Pipe(value.1),
512        }
513    }
514}
515
516/// Convenience conversion from (Parts, Vec<u8>)
517impl From<(http::response::Parts, Vec<u8>)> for WebResourceResponse {
518    fn from(value: (http::response::Parts, Vec<u8>)) -> Self {
519        WebResourceResponse {
520            parts: value.0,
521            body: WebResourceBody::Bytes(value.1),
522        }
523    }
524}
525
526impl WebResourceResponse {
527    fn response_parts_with_status(status: u16) -> http::response::Parts {
528        let response = match http::Response::builder().status(status).body(()) {
529            Ok(response) => response,
530            Err(_) => http::Response::new(()),
531        };
532        let (parts, _) = response.into_parts();
533        parts
534    }
535
536    /// Create a response serving a file from disk (status 200).
537    pub fn file(path: impl Into<PathBuf>) -> Self {
538        let path = path.into();
539        let content_length = std::fs::metadata(&path).ok().map(|m| m.len());
540        let mut parts = Self::response_parts_with_status(200);
541        if let Some(len) = content_length {
542            parts
543                .headers
544                .insert(http::header::CONTENT_LENGTH, http::HeaderValue::from(len));
545        }
546        Self {
547            parts,
548            body: WebResourceBody::Path(path),
549        }
550    }
551
552    /// Create a response serving in-memory bytes (status 200).
553    pub fn bytes(data: impl Into<Vec<u8>>) -> Self {
554        let data = data.into();
555        let len = data.len();
556        let mut parts = Self::response_parts_with_status(200);
557        parts
558            .headers
559            .insert(http::header::CONTENT_LENGTH, http::HeaderValue::from(len));
560        Self {
561            parts,
562            body: WebResourceBody::Bytes(data),
563        }
564    }
565
566    /// Create a response serving data from a system pipe (status 200).
567    pub fn stream(reader: SystemPipeReader) -> Self {
568        let parts = Self::response_parts_with_status(200);
569        Self {
570            parts,
571            body: WebResourceBody::Pipe(reader),
572        }
573    }
574
575    /// Set the Content-Type header (builder pattern).
576    pub fn mime(mut self, content_type: &str) -> Self {
577        if let Ok(value) = http::HeaderValue::from_str(content_type) {
578            self.parts.headers.insert(http::header::CONTENT_TYPE, value);
579        }
580        self
581    }
582
583    /// Set the HTTP status code (builder pattern).
584    pub fn status(mut self, code: u16) -> Self {
585        self.parts.status = http::StatusCode::from_u16(code).unwrap_or(self.parts.status);
586        self
587    }
588
589    /// Add a response header (builder pattern).
590    pub fn header(mut self, name: &str, value: &str) -> Self {
591        if let (Ok(header_name), Ok(header_value)) = (
592            name.parse::<http::header::HeaderName>(),
593            http::HeaderValue::from_str(value),
594        ) {
595            self.parts.headers.insert(header_name, header_value);
596        }
597        self
598    }
599
600    /// Add CORS header `Access-Control-Allow-Origin: null` (builder pattern).
601    pub fn cors(self) -> Self {
602        self.header("access-control-allow-origin", "null")
603    }
604}