diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index f67c91b7..dfefe961 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -2159,6 +2159,7 @@ pub fn run() { .manage(AdapterState::default()) .manage(TerminalState::default()) .manage(AppExitState::default()) + .manage(webview_panel::PreviewState::default()) .plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_process::init()) @@ -2183,7 +2184,12 @@ pub fn run() { get_app_mode, set_app_mode, detect_portable_dir, - set_app_zoom + set_app_zoom, + webview_panel::preview_open, + webview_panel::preview_navigate, + webview_panel::preview_set_bounds, + webview_panel::preview_set_visible, + webview_panel::preview_close ]); // macOS: native menu bar (traffic-light overlay style) diff --git a/desktop/src-tauri/src/webview_panel.rs b/desktop/src-tauri/src/webview_panel.rs index f6aec4ac..79c44648 100644 --- a/desktop/src-tauri/src/webview_panel.rs +++ b/desktop/src-tauri/src/webview_panel.rs @@ -1,3 +1,27 @@ +use std::sync::Mutex; +use tauri::{AppHandle, LogicalPosition, LogicalSize, Manager, Runtime, WebviewBuilder, WebviewUrl}; + +const PREVIEW_LABEL: &str = "preview"; + +/// M1 placeholder init script; replaced by the real preview-agent bundle in a later milestone. +const PREVIEW_INIT_SCRIPT: &str = "window.__PREVIEW_AGENT_READY__ = true;"; + +#[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(); @@ -12,6 +36,99 @@ pub fn normalize_preview_url(input: &str) -> Result { } } +#[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 builder = WebviewBuilder::new(PREVIEW_LABEL, WebviewUrl::External(target)) + .initialization_script(PREVIEW_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(()) +} + #[cfg(test)] mod tests { use super::*;