use std::sync::Mutex; use tauri::{AppHandle, Emitter, LogicalPosition, LogicalSize, Manager, Runtime, WebviewBuilder, WebviewUrl}; const PREVIEW_LABEL: &str = "preview"; const PREVIEW_INIT_SCRIPT: &str = include_str!("../resources/preview-agent.js"); const PREVIEW_TAURI_BRIDGE_SCRIPT: &str = r#" window.__DESKTOP_PREVIEW_POST__ = function(raw) { var internals = window.__TAURI_INTERNALS__; if (internals && typeof internals.invoke === 'function') { internals.invoke('preview_message', { raw: raw }); } }; "#; #[derive(Default)] pub struct PreviewState(Mutex); #[derive(Default)] struct PreviewInner { created: bool, } #[derive(serde::Deserialize)] pub struct PreviewBounds { pub x: f64, pub y: f64, pub width: f64, pub height: f64, } /// 仅允许 http/https;其它(file:、javascript: 等)拒绝。返回规范化后的 url 字符串。 pub fn normalize_preview_url(input: &str) -> Result { let trimmed = input.trim(); if trimmed.is_empty() { return Err("empty url".to_string()); } let lower = trimmed.to_ascii_lowercase(); if lower.starts_with("http://") || lower.starts_with("https://") { Ok(trimmed.to_string()) } else { Err(format!("unsupported url scheme: {trimmed}")) } } #[tauri::command] pub async fn preview_open( app: AppHandle, state: tauri::State<'_, PreviewState>, url: String, bounds: PreviewBounds, ) -> Result<(), String> { let normalized = normalize_preview_url(&url)?; let target = tauri::Url::parse(&normalized).map_err(|e| e.to_string())?; if let Some(webview) = app.get_webview(PREVIEW_LABEL) { webview.navigate(target).map_err(|e| e.to_string())?; webview .set_position(LogicalPosition::new(bounds.x, bounds.y)) .map_err(|e| e.to_string())?; webview .set_size(LogicalSize::new(bounds.width, bounds.height)) .map_err(|e| e.to_string())?; return Ok(()); } // `add_child` lives on `Window`, not `WebviewWindow`; `get_window` // (unstable, enabled) returns the underlying `Window` for the same OS window. let main = app .get_window(crate::MAIN_WINDOW_LABEL) .ok_or_else(|| "main window not found".to_string())?; let init_script = format!("{PREVIEW_TAURI_BRIDGE_SCRIPT}\n{PREVIEW_INIT_SCRIPT}"); let builder = WebviewBuilder::new(PREVIEW_LABEL, WebviewUrl::External(target)) .initialization_script_for_all_frames(init_script); main.add_child( builder, LogicalPosition::new(bounds.x, bounds.y), LogicalSize::new(bounds.width, bounds.height), ) .map_err(|e| e.to_string())?; state.0.lock().unwrap().created = true; Ok(()) } #[tauri::command] pub async fn preview_navigate(app: AppHandle, url: String) -> Result<(), String> { let normalized = normalize_preview_url(&url)?; let target = tauri::Url::parse(&normalized).map_err(|e| e.to_string())?; let webview = app .get_webview(PREVIEW_LABEL) .ok_or_else(|| "preview not open".to_string())?; webview.navigate(target).map_err(|e| e.to_string()) } #[tauri::command] pub async fn preview_set_bounds( app: AppHandle, bounds: PreviewBounds, ) -> Result<(), String> { let Some(webview) = app.get_webview(PREVIEW_LABEL) else { return Ok(()); }; webview .set_position(LogicalPosition::new(bounds.x, bounds.y)) .map_err(|e| e.to_string())?; webview .set_size(LogicalSize::new(bounds.width, bounds.height)) .map_err(|e| e.to_string())?; Ok(()) } #[tauri::command] pub async fn preview_set_visible( app: AppHandle, visible: bool, ) -> Result<(), String> { let Some(webview) = app.get_webview(PREVIEW_LABEL) else { return Ok(()); }; if visible { webview.show().map_err(|e| e.to_string()) } else { webview.hide().map_err(|e| e.to_string()) } } #[tauri::command] pub async fn preview_close( app: AppHandle, state: tauri::State<'_, PreviewState>, ) -> Result<(), String> { if let Some(webview) = app.get_webview(PREVIEW_LABEL) { webview.close().map_err(|e| e.to_string())?; } state.0.lock().unwrap().created = false; Ok(()) } /// 页面 → 宿主:注入脚本经 IPC 调此命令;Rust 校验后转发给前端。 #[tauri::command] pub fn preview_message(app: AppHandle, raw: String) -> Result<(), String> { // 仅转发 JSON 字符串;前端做强类型解析(schema 校验在 TS 侧 protocol) serde_json::from_str::(&raw).map_err(|e| e.to_string())?; app.emit("preview://event", raw).map_err(|e| e.to_string()) } /// 宿主 → 页面:在子 webview 内 eval 一段 JS。 #[tauri::command] pub async fn preview_eval(app: AppHandle, js: String) -> Result<(), String> { let webview = app.get_webview(PREVIEW_LABEL).ok_or_else(|| "preview not open".to_string())?; webview.eval(&js).map_err(|e| e.to_string()) } #[cfg(test)] mod tests { use super::*; #[test] fn accepts_http_and_https() { assert_eq!(normalize_preview_url("http://localhost:5173/").unwrap(), "http://localhost:5173/"); assert_eq!(normalize_preview_url("https://example.com").unwrap(), "https://example.com"); } #[test] fn trims_whitespace() { assert_eq!(normalize_preview_url(" http://127.0.0.1:8080 ").unwrap(), "http://127.0.0.1:8080"); } #[test] fn rejects_non_http_schemes() { assert!(normalize_preview_url("file:///etc/passwd").is_err()); assert!(normalize_preview_url("javascript:alert(1)").is_err()); assert!(normalize_preview_url("").is_err()); } }