diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 3612db0..63e0462 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -1,8 +1,6 @@ PODS: - desktop_multi_window (0.0.1): - FlutterMacOS - - desktop_webview_window (0.0.1): - - FlutterMacOS - device_info_plus (0.0.1): - FlutterMacOS - file_picker (0.0.1): @@ -10,7 +8,8 @@ PODS: - FlutterMacOS (1.0.0) - macos_window_utils (1.0.0): - FlutterMacOS - - objective_c (0.0.1): + - path_provider_foundation (0.0.1): + - Flutter - FlutterMacOS - rust_builder (0.0.1): - FlutterMacOS @@ -23,12 +22,11 @@ PODS: DEPENDENCIES: - desktop_multi_window (from `Flutter/ephemeral/.symlinks/plugins/desktop_multi_window/macos`) - - desktop_webview_window (from `Flutter/ephemeral/.symlinks/plugins/desktop_webview_window/macos`) - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) - file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/macos`) - FlutterMacOS (from `Flutter/ephemeral`) - macos_window_utils (from `Flutter/ephemeral/.symlinks/plugins/macos_window_utils/macos`) - - objective_c (from `Flutter/ephemeral/.symlinks/plugins/objective_c/macos`) + - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) - rust_builder (from `Flutter/ephemeral/.symlinks/plugins/rust_builder/macos`) - screen_retriever_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) @@ -37,8 +35,6 @@ DEPENDENCIES: EXTERNAL SOURCES: desktop_multi_window: :path: Flutter/ephemeral/.symlinks/plugins/desktop_multi_window/macos - desktop_webview_window: - :path: Flutter/ephemeral/.symlinks/plugins/desktop_webview_window/macos device_info_plus: :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos file_picker: @@ -47,8 +43,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral macos_window_utils: :path: Flutter/ephemeral/.symlinks/plugins/macos_window_utils/macos - objective_c: - :path: Flutter/ephemeral/.symlinks/plugins/objective_c/macos + path_provider_foundation: + :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin rust_builder: :path: Flutter/ephemeral/.symlinks/plugins/rust_builder/macos screen_retriever_macos: @@ -60,12 +56,11 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: desktop_multi_window: 93667594ccc4b88d91a97972fd3b1b89667fa80a - desktop_webview_window: 7e37af677d6d19294cb433d9b1d878ef78dffa4d device_info_plus: 4fb280989f669696856f8b129e4a5e3cd6c48f76 file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 macos_window_utils: 23f54331a0fd51eea9e0ed347253bf48fd379d1d - objective_c: ec13431e45ba099cb734eb2829a5c1cd37986cba + path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880 rust_builder: f99e810dace35ac9783428b039d73d1caa5bfbc1 screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd diff --git a/rust/Cargo.toml b/rust/Cargo.toml index e589df6..fa42165 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -22,7 +22,7 @@ once_cell = "1.21.3" reqwest = { version = "0.12.24", features = ["rustls-tls-webpki-roots", "cookies", "gzip", "json", "stream"] } hickory-resolver = { version = "0.25.2" } anyhow = "1.0.100" -scopeguard = "1.2.0" +scopeguard = "1.0" notify-rust = "4.11.7" asar = "0.3.0" walkdir = "2.5.0" @@ -32,11 +32,14 @@ ndarray = "0.17.1" serde_json = "1.0.145" serde = { version = "1.0.228", features = ["derive"] } unp4k_rs = { git = "https://github.com/StarCitizenToolBox/unp4k_rs", tag = "V0.0.2" } -wry = "0.53.5" -tao = { version = "0.34.5", features = ["serde"] } uuid = { version = "1.19.0", features = ["v4"] } parking_lot = "0.12.5" crossbeam-channel = "0.5.15" + +# WebView +[target.'cfg(not(target_os = "macos"))'.dependencies] +wry = "0.53.5" +tao = { version = "0.34.5", features = ["serde"] } image = { version = "0.25.9", default-features = false, features = ["ico"] } [target.'cfg(windows)'.dependencies] diff --git a/rust/src/api/webview_api.rs b/rust/src/api/webview_api.rs index 5a9dd58..3a1c538 100644 --- a/rust/src/api/webview_api.rs +++ b/rust/src/api/webview_api.rs @@ -1,32 +1,11 @@ // WebView API using wry + tao -// This module provides a cross-platform WebView implementation for Flutter -use std::collections::HashMap; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::Arc; +use std::sync::atomic::Ordering; -use crossbeam_channel::{bounded, Receiver, Sender}; use flutter_rust_bridge::frb; -use parking_lot::RwLock; use serde::{Deserialize, Serialize}; -use tao::dpi::{LogicalPosition, LogicalSize}; -use tao::event::{Event, WindowEvent}; -use tao::event_loop::{ControlFlow, EventLoop, EventLoopBuilder}; -use tao::platform::run_return::EventLoopExtRunReturn; -use tao::window::{Icon, Window, WindowBuilder}; -use wry::{PageLoadEvent, WebView, WebViewBuilder}; -// Platform-specific imports for running event loop on any thread -#[cfg(target_os = "windows")] -use tao::platform::windows::EventLoopBuilderExtWindows; - -use once_cell::sync::Lazy; - -// Embed the app icon at compile time -static APP_ICON_DATA: &[u8] = include_bytes!("../../../windows/runner/resources/app_icon.ico"); - -// Embed the init script at compile time -static INIT_SCRIPT: &str = include_str!("../assets/webview_init_script.js"); +use crate::webview::{WEBVIEW_INSTANCES, send_command}; // ============ Data Structures ============ @@ -107,135 +86,12 @@ pub enum WebViewCommand { SetWindowPosition(i32, i32), } -// ============ Global State ============ - -type WebViewId = String; - -/// Navigation history manager to track back/forward capability -#[derive(Debug, Clone, Default)] -struct NavigationHistory { - /// List of URLs in history (excluding about:blank) - urls: Vec, - /// Current position in history (0-based index) - current_index: i32, -} - -impl NavigationHistory { - fn new() -> Self { - Self { - urls: Vec::new(), - current_index: -1, - } - } - - /// Push a new URL to history (when navigating to a new page) - fn push(&mut self, url: &str) { - // Skip about:blank - if url == "about:blank" { - return; - } - - // If we're not at the end of history, truncate forward history - if self.current_index >= 0 && (self.current_index as usize) < self.urls.len().saturating_sub(1) { - self.urls.truncate((self.current_index + 1) as usize); - } - - // Don't add duplicate consecutive URLs - if self.urls.last().map(|s| s.as_str()) != Some(url) { - self.urls.push(url.to_string()); - } - self.current_index = (self.urls.len() as i32) - 1; - } - - /// Check if we can go back (not at first real page) - fn can_go_back(&self) -> bool { - self.current_index > 0 - } - - /// Check if we can go forward - fn can_go_forward(&self) -> bool { - self.current_index >= 0 && (self.current_index as usize) < self.urls.len().saturating_sub(1) - } - - /// Go back in history, returns true if successful - fn go_back(&mut self) -> bool { - if self.can_go_back() { - self.current_index -= 1; - true - } else { - false - } - } - - /// Go forward in history, returns true if successful - fn go_forward(&mut self) -> bool { - if self.can_go_forward() { - self.current_index += 1; - true - } else { - false - } - } - - /// Get current URL - fn current_url(&self) -> Option<&str> { - if self.current_index >= 0 && (self.current_index as usize) < self.urls.len() { - Some(&self.urls[self.current_index as usize]) - } else { - None - } - } -} - -struct WebViewInstance { - command_sender: Sender, - event_receiver: Receiver, - state: Arc>, - is_closed: Arc, -} - -static WEBVIEW_INSTANCES: Lazy>> = - Lazy::new(|| RwLock::new(HashMap::new())); - -// Custom event type for the event loop -#[derive(Debug, Clone)] -#[allow(dead_code)] -enum UserEvent { - Command(WebViewCommand), - Quit, -} - // ============ Public API ============ /// Create a new WebView window and return its ID #[frb(sync)] pub fn webview_create(config: WebViewConfiguration) -> Result { - let id = uuid::Uuid::new_v4().to_string(); - let id_clone = id.clone(); - - let (cmd_tx, cmd_rx) = bounded::(100); - let (event_tx, event_rx) = bounded::(100); - let state = Arc::new(RwLock::new(WebViewNavigationState::default())); - let state_clone = Arc::clone(&state); - let is_closed = Arc::new(AtomicBool::new(false)); - let is_closed_clone = Arc::clone(&is_closed); - - std::thread::spawn(move || { - run_webview_loop(id_clone, config, cmd_rx, event_tx, state_clone, is_closed_clone); - }); - - // Wait a moment for the window to be created - std::thread::sleep(std::time::Duration::from_millis(100)); - - let instance = WebViewInstance { - command_sender: cmd_tx, - event_receiver: event_rx, - state, - is_closed, - }; - - WEBVIEW_INSTANCES.write().insert(id.clone(), instance); - Ok(id) + crate::webview::create_webview(config) } /// Navigate to a URL @@ -351,381 +207,3 @@ pub fn webview_list_all() -> Vec { drop(instances); keys } - -// ============ Internal Implementation ============ - -fn send_command(id: &str, command: WebViewCommand) -> Result<(), String> { - let instances = WEBVIEW_INSTANCES.read(); - let instance = instances - .get(id) - .ok_or_else(|| format!("WebView instance not found: {}", id))?; - - if instance.is_closed.load(Ordering::SeqCst) { - return Err("WebView is closed".to_string()); - } - - instance - .command_sender - .send(command) - .map_err(|e| e.to_string()) -} - -fn run_webview_loop( - _id: String, - config: WebViewConfiguration, - cmd_rx: Receiver, - event_tx: Sender, - state: Arc>, - is_closed: Arc, -) { - // Create event loop with any_thread support for non-main thread execution - #[cfg(target_os = "windows")] - let mut event_loop: EventLoop = EventLoopBuilder::with_user_event() - .with_any_thread(true) - .build(); - - #[cfg(not(target_os = "windows"))] - let mut event_loop: EventLoop = EventLoopBuilder::with_user_event().build(); - - let proxy = event_loop.create_proxy(); - - // Load window icon from embedded ICO file - let window_icon = load_app_icon(); - - // Build the window with decorations (title bar provided by OS) - // Set dark background color matching app theme (#1a1a1a) - let mut window_builder = WindowBuilder::new() - .with_title(&config.title) - .with_inner_size(LogicalSize::new(config.width, config.height)) - .with_visible(true); - - // Set window icon if loaded successfully - if let Some(icon) = window_icon { - window_builder = window_builder.with_window_icon(Some(icon)); - } - - let window = window_builder.build(&event_loop) - .expect("Failed to create window"); - - let window = Arc::new(window); - - // Navigation history for tracking back/forward state - let nav_history = Arc::new(RwLock::new(NavigationHistory::new())); - - let event_tx_clone = event_tx.clone(); - let nav_history_ipc = Arc::clone(&nav_history); - let state_ipc = Arc::clone(&state); - - // Create web context with custom data directory if provided - let mut web_context = config - .user_data_folder - .as_ref() - .map(|path| wry::WebContext::new(Some(std::path::PathBuf::from(path)))) - .unwrap_or_default(); - - let mut builder = WebViewBuilder::new_with_web_context(&mut web_context) - .with_url("about:blank") - .with_devtools(config.enable_devtools) - .with_transparent(config.transparent) - .with_background_color((26, 26, 26, 255)) // Dark background #1a1a1a - .with_initialization_script(INIT_SCRIPT); - - // Set user agent if provided - if let Some(ref user_agent) = config.user_agent { - builder = builder.with_user_agent(user_agent); - } - - // Store proxy for IPC commands - let proxy_ipc = proxy.clone(); - - let webview = builder - .with_ipc_handler(move |message| { - let msg = message.body().to_string(); - - // Try to parse as navigation command from JS - if let Ok(parsed) = serde_json::from_str::(&msg) { - if let Some(msg_type) = parsed.get("type").and_then(|v| v.as_str()) { - match msg_type { - "nav_back" => { - // Check if we can go back (avoid about:blank) - let can_back = nav_history_ipc.read().can_go_back(); - if can_back { - let _ = proxy_ipc.send_event(UserEvent::Command(WebViewCommand::GoBack)); - } - return; - } - "nav_forward" => { - let can_forward = nav_history_ipc.read().can_go_forward(); - if can_forward { - let _ = proxy_ipc.send_event(UserEvent::Command(WebViewCommand::GoForward)); - } - return; - } - "nav_reload" => { - let _ = proxy_ipc.send_event(UserEvent::Command(WebViewCommand::Reload)); - return; - } - "get_nav_state" => { - // Send current state to JS - let state_guard = state_ipc.read(); - let history = nav_history_ipc.read(); - let state_json = serde_json::json!({ - "can_go_back": history.can_go_back(), - "can_go_forward": history.can_go_forward(), - "is_loading": state_guard.is_loading, - "url": state_guard.url - }); - let script = format!("window._sctUpdateNavState({})", state_json); - let _ = proxy_ipc.send_event(UserEvent::Command(WebViewCommand::ExecuteScript(script))); - return; - } - _ => {} - } - } - } - - // Forward other messages to Dart - let _ = event_tx_clone.send(WebViewEvent::WebMessage { message: msg }); - }) - .with_navigation_handler({ - let event_tx = event_tx.clone(); - let state = Arc::clone(&state); - let nav_history = Arc::clone(&nav_history); - move |uri| { - // Skip about:blank for navigation events - if uri == "about:blank" { - return true; - } - - { - let mut state_guard = state.write(); - state_guard.url = uri.clone(); - state_guard.is_loading = true; - // Update can_go_back/can_go_forward from history - let history = nav_history.read(); - state_guard.can_go_back = history.can_go_back(); - state_guard.can_go_forward = history.can_go_forward(); - } - let _ = event_tx.send(WebViewEvent::NavigationStarted { url: uri }); - true // Allow navigation - } - }) - .with_on_page_load_handler({ - let event_tx = event_tx.clone(); - let state = Arc::clone(&state); - let nav_history = Arc::clone(&nav_history); - let proxy = proxy.clone(); - move |event, url| { - // Skip about:blank - if url == "about:blank" { - return; - } - - match event { - PageLoadEvent::Started => { - let can_back; - let can_forward; - { - let mut state_guard = state.write(); - state_guard.url = url.clone(); - state_guard.is_loading = true; - let history = nav_history.read(); - can_back = history.can_go_back(); - can_forward = history.can_go_forward(); - state_guard.can_go_back = can_back; - state_guard.can_go_forward = can_forward; - } - // Send loading state to JS - let state_json = serde_json::json!({ - "can_go_back": can_back, - "can_go_forward": can_forward, - "is_loading": true, - "url": url - }); - let script = format!("window._sctUpdateNavState && window._sctUpdateNavState({})", state_json); - let _ = proxy.send_event(UserEvent::Command(WebViewCommand::ExecuteScript(script))); - } - PageLoadEvent::Finished => { - let can_back; - let can_forward; - { - // Add to history when page finishes loading - let mut history = nav_history.write(); - history.push(&url); - can_back = history.can_go_back(); - can_forward = history.can_go_forward(); - } - { - let mut state_guard = state.write(); - state_guard.url = url.clone(); - state_guard.is_loading = false; - state_guard.can_go_back = can_back; - state_guard.can_go_forward = can_forward; - } - let _ = event_tx.send(WebViewEvent::NavigationCompleted { url: url.clone() }); - - // Send completed state to JS - let state_json = serde_json::json!({ - "can_go_back": can_back, - "can_go_forward": can_forward, - "is_loading": false, - "url": url - }); - let script = format!("window._sctUpdateNavState && window._sctUpdateNavState({})", state_json); - let _ = proxy.send_event(UserEvent::Command(WebViewCommand::ExecuteScript(script))); - } - } - } - }) - .build(&window) - .expect("Failed to create webview"); - - let webview = Arc::new(webview); - let webview_cmd = Arc::clone(&webview); - - // Spawn command handler thread - let proxy_cmd = proxy.clone(); - std::thread::spawn(move || { - while let Ok(cmd) = cmd_rx.recv() { - if proxy_cmd.send_event(UserEvent::Command(cmd)).is_err() { - break; - } - } - }); - - // Run the event loop with run_return so we can properly cleanup - event_loop.run_return(|event, _, control_flow| { - *control_flow = ControlFlow::Wait; - - match event { - Event::WindowEvent { - event: WindowEvent::CloseRequested, - .. - } => { - is_closed.store(true, Ordering::SeqCst); - let _ = event_tx.send(WebViewEvent::WindowClosed); - *control_flow = ControlFlow::Exit; - } - Event::UserEvent(user_event) => match user_event { - UserEvent::Command(cmd) => { - handle_command(&webview_cmd, &window, cmd, &state, &nav_history, &event_tx, &is_closed, control_flow); - } - UserEvent::Quit => { - is_closed.store(true, Ordering::SeqCst); - *control_flow = ControlFlow::Exit; - } - }, - _ => {} - } - }); - - // Explicitly drop in correct order: webview first, then web_context, then window - drop(webview_cmd); - drop(web_context); - drop(window); -} - -fn handle_command( - webview: &Arc, - window: &Arc, - command: WebViewCommand, - state: &Arc>, - nav_history: &Arc>, - event_tx: &Sender, - is_closed: &Arc, - control_flow: &mut ControlFlow, -) { - match command { - WebViewCommand::Navigate(url) => { - { - let mut s = state.write(); - s.url = url.clone(); - s.is_loading = true; - } - let _ = webview.load_url(&url); - let _ = event_tx.send(WebViewEvent::NavigationStarted { url }); - } - WebViewCommand::GoBack => { - // Update history index before navigation - let can_go = { - let mut history = nav_history.write(); - history.go_back() - }; - if can_go { - let _ = webview.evaluate_script("history.back()"); - } - } - WebViewCommand::GoForward => { - // Update history index before navigation - let can_go = { - let mut history = nav_history.write(); - history.go_forward() - }; - if can_go { - let _ = webview.evaluate_script("history.forward()"); - } - } - WebViewCommand::Reload => { - let _ = webview.evaluate_script("location.reload()"); - } - WebViewCommand::Stop => { - let _ = webview.evaluate_script("window.stop()"); - } - WebViewCommand::ExecuteScript(script) => { - let _ = webview.evaluate_script(&script); - } - WebViewCommand::SetVisibility(visible) => { - window.set_visible(visible); - } - WebViewCommand::Close => { - // Properly close the window and exit the event loop - is_closed.store(true, Ordering::SeqCst); - let _ = event_tx.send(WebViewEvent::WindowClosed); - // Exit the event loop - this will cause cleanup - *control_flow = ControlFlow::Exit; - } - WebViewCommand::SetWindowSize(width, height) => { - let _ = window.set_inner_size(LogicalSize::new(width, height)); - } - WebViewCommand::SetWindowPosition(x, y) => { - window.set_outer_position(LogicalPosition::new(x, y)); - } - } -} - -/// Load the application icon from embedded ICO data -fn load_app_icon() -> Option { - use std::io::Cursor; - use image::ImageReader; - - // Parse the ICO file from embedded bytes - let cursor = Cursor::new(APP_ICON_DATA); - let reader = match ImageReader::new(cursor).with_guessed_format() { - Ok(r) => r, - Err(e) => { - eprintln!("[SCToolbox] Failed to create image reader: {}", e); - return None; - } - }; - - let img = match reader.decode() { - Ok(img) => img, - Err(e) => { - eprintln!("[SCToolbox] Failed to decode icon: {}", e); - return None; - } - }; - - // Convert to RGBA8 - let rgba = img.to_rgba8(); - let (width, height) = rgba.dimensions(); - let raw_data = rgba.into_raw(); - - match Icon::from_rgba(raw_data, width, height) { - Ok(icon) => Some(icon), - Err(e) => { - eprintln!("[SCToolbox] Failed to create icon: {:?}", e); - None - } - } -} diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 118a7c7..3e7ad6c 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -2,3 +2,4 @@ pub mod api; mod frb_generated; pub mod http_package; pub mod ort_models; +pub mod webview; diff --git a/rust/src/webview/mod.rs b/rust/src/webview/mod.rs new file mode 100644 index 0000000..51845de --- /dev/null +++ b/rust/src/webview/mod.rs @@ -0,0 +1,16 @@ +// WebView 独立模块 +// 此模块包含 wry + tao WebView 的完整实现 +// macOS 平台不支持 WebView,因为 tao 的 EventLoop 必须在主线程运行,与 Flutter 冲突 + +#[cfg(not(target_os = "macos"))] +mod webview_impl; + +#[cfg(not(target_os = "macos"))] +pub use webview_impl::*; + +// macOS 平台提供空实现 +#[cfg(target_os = "macos")] +mod webview_stub; + +#[cfg(target_os = "macos")] +pub use webview_stub::*; diff --git a/rust/src/webview/webview_impl.rs b/rust/src/webview/webview_impl.rs new file mode 100644 index 0000000..764e3c0 --- /dev/null +++ b/rust/src/webview/webview_impl.rs @@ -0,0 +1,512 @@ +// WebView 实现 (Windows/Linux) +// 使用 wry + tao 实现跨平台 WebView + +use std::collections::HashMap; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; + +use crossbeam_channel::{bounded, Receiver, Sender}; +use parking_lot::RwLock; +use once_cell::sync::Lazy; +use tao::dpi::{LogicalPosition, LogicalSize}; +use tao::event::{Event, WindowEvent}; +use tao::event_loop::{ControlFlow, EventLoop, EventLoopBuilder}; +use tao::platform::run_return::EventLoopExtRunReturn; +use tao::window::{Icon, Window, WindowBuilder}; +use wry::{PageLoadEvent, WebView, WebViewBuilder}; + +#[cfg(target_os = "windows")] +use tao::platform::windows::EventLoopBuilderExtWindows; + +use crate::api::webview_api::{ + WebViewConfiguration, WebViewNavigationState, WebViewEvent, WebViewCommand, +}; + +// Embed the app icon at compile time +static APP_ICON_DATA: &[u8] = include_bytes!("../../windows/runner/resources/app_icon.ico"); + +// Embed the init script at compile time +static INIT_SCRIPT: &str = include_str!("../assets/webview_init_script.js"); + +// ============ Types ============ + +pub type WebViewId = String; + +/// Navigation history manager to track back/forward capability +#[derive(Debug, Clone, Default)] +struct NavigationHistory { + urls: Vec, + current_index: i32, +} + +impl NavigationHistory { + fn new() -> Self { + Self { + urls: Vec::new(), + current_index: -1, + } + } + + fn push(&mut self, url: &str) { + if url == "about:blank" { + return; + } + + if self.current_index >= 0 && (self.current_index as usize) < self.urls.len().saturating_sub(1) { + self.urls.truncate((self.current_index + 1) as usize); + } + + if self.urls.last().map(|s| s.as_str()) != Some(url) { + self.urls.push(url.to_string()); + } + self.current_index = (self.urls.len() as i32) - 1; + } + + fn can_go_back(&self) -> bool { + self.current_index > 0 + } + + fn can_go_forward(&self) -> bool { + self.current_index >= 0 && (self.current_index as usize) < self.urls.len().saturating_sub(1) + } + + fn go_back(&mut self) -> bool { + if self.can_go_back() { + self.current_index -= 1; + true + } else { + false + } + } + + fn go_forward(&mut self) -> bool { + if self.can_go_forward() { + self.current_index += 1; + true + } else { + false + } + } + + #[allow(dead_code)] + fn current_url(&self) -> Option<&str> { + if self.current_index >= 0 && (self.current_index as usize) < self.urls.len() { + Some(&self.urls[self.current_index as usize]) + } else { + None + } + } +} + +pub struct WebViewInstance { + pub command_sender: Sender, + pub event_receiver: Receiver, + pub state: Arc>, + pub is_closed: Arc, +} + +pub static WEBVIEW_INSTANCES: Lazy>> = + Lazy::new(|| RwLock::new(HashMap::new())); + +// Custom event type for the event loop +#[derive(Debug, Clone)] +#[allow(dead_code)] +enum UserEvent { + Command(WebViewCommand), + Quit, +} + +// ============ Public Implementation ============ + +/// Create a new WebView window +pub fn create_webview(config: WebViewConfiguration) -> Result { + let id = uuid::Uuid::new_v4().to_string(); + let id_clone = id.clone(); + + let (cmd_tx, cmd_rx) = bounded::(100); + let (event_tx, event_rx) = bounded::(100); + let state = Arc::new(RwLock::new(WebViewNavigationState::default())); + let state_clone = Arc::clone(&state); + let is_closed = Arc::new(AtomicBool::new(false)); + let is_closed_clone = Arc::clone(&is_closed); + + std::thread::spawn(move || { + run_webview_loop(id_clone, config, cmd_rx, event_tx, state_clone, is_closed_clone); + }); + + // Wait a moment for the window to be created + std::thread::sleep(std::time::Duration::from_millis(100)); + + let instance = WebViewInstance { + command_sender: cmd_tx, + event_receiver: event_rx, + state, + is_closed, + }; + + WEBVIEW_INSTANCES.write().insert(id.clone(), instance); + Ok(id) +} + +pub fn send_command(id: &str, command: WebViewCommand) -> Result<(), String> { + let instances = WEBVIEW_INSTANCES.read(); + let instance = instances + .get(id) + .ok_or_else(|| format!("WebView instance not found: {}", id))?; + + if instance.is_closed.load(Ordering::SeqCst) { + return Err("WebView is closed".to_string()); + } + + instance + .command_sender + .send(command) + .map_err(|e| e.to_string()) +} + +// ============ Internal Implementation ============ + +fn run_webview_loop( + _id: String, + config: WebViewConfiguration, + cmd_rx: Receiver, + event_tx: Sender, + state: Arc>, + is_closed: Arc, +) { + // Create event loop with any_thread support for non-main thread execution + #[cfg(target_os = "windows")] + let mut event_loop: EventLoop = EventLoopBuilder::with_user_event() + .with_any_thread(true) + .build(); + + #[cfg(not(target_os = "windows"))] + let mut event_loop: EventLoop = EventLoopBuilder::with_user_event().build(); + + let proxy = event_loop.create_proxy(); + + // Load window icon from embedded ICO file + let window_icon = load_app_icon(); + + // Build the window with decorations (title bar provided by OS) + let mut window_builder = WindowBuilder::new() + .with_title(&config.title) + .with_inner_size(LogicalSize::new(config.width, config.height)) + .with_visible(true); + + // Set window icon if loaded successfully + if let Some(icon) = window_icon { + window_builder = window_builder.with_window_icon(Some(icon)); + } + + let window = window_builder.build(&event_loop) + .expect("Failed to create window"); + + let window = Arc::new(window); + + // Navigation history for tracking back/forward state + let nav_history = Arc::new(RwLock::new(NavigationHistory::new())); + + let event_tx_clone = event_tx.clone(); + let nav_history_ipc = Arc::clone(&nav_history); + let state_ipc = Arc::clone(&state); + + // Create web context with custom data directory if provided + let mut web_context = config + .user_data_folder + .as_ref() + .map(|path| wry::WebContext::new(Some(std::path::PathBuf::from(path)))) + .unwrap_or_default(); + + let mut builder = WebViewBuilder::new_with_web_context(&mut web_context) + .with_url("about:blank") + .with_devtools(config.enable_devtools) + .with_transparent(config.transparent) + .with_background_color((26, 26, 26, 255)) // Dark background #1a1a1a + .with_initialization_script(INIT_SCRIPT); + + // Set user agent if provided + if let Some(ref user_agent) = config.user_agent { + builder = builder.with_user_agent(user_agent); + } + + // Store proxy for IPC commands + let proxy_ipc = proxy.clone(); + + let webview = builder + .with_ipc_handler(move |message| { + let msg = message.body().to_string(); + + // Try to parse as navigation command from JS + if let Ok(parsed) = serde_json::from_str::(&msg) { + if let Some(msg_type) = parsed.get("type").and_then(|v| v.as_str()) { + match msg_type { + "nav_back" => { + let can_back = nav_history_ipc.read().can_go_back(); + if can_back { + let _ = proxy_ipc.send_event(UserEvent::Command(WebViewCommand::GoBack)); + } + return; + } + "nav_forward" => { + let can_forward = nav_history_ipc.read().can_go_forward(); + if can_forward { + let _ = proxy_ipc.send_event(UserEvent::Command(WebViewCommand::GoForward)); + } + return; + } + "nav_reload" => { + let _ = proxy_ipc.send_event(UserEvent::Command(WebViewCommand::Reload)); + return; + } + "get_nav_state" => { + let state_guard = state_ipc.read(); + let history = nav_history_ipc.read(); + let state_json = serde_json::json!({ + "can_go_back": history.can_go_back(), + "can_go_forward": history.can_go_forward(), + "is_loading": state_guard.is_loading, + "url": state_guard.url + }); + let script = format!("window._sctUpdateNavState({})", state_json); + let _ = proxy_ipc.send_event(UserEvent::Command(WebViewCommand::ExecuteScript(script))); + return; + } + _ => {} + } + } + } + + // Forward other messages to Dart + let _ = event_tx_clone.send(WebViewEvent::WebMessage { message: msg }); + }) + .with_navigation_handler({ + let event_tx = event_tx.clone(); + let state = Arc::clone(&state); + let nav_history = Arc::clone(&nav_history); + move |uri| { + if uri == "about:blank" { + return true; + } + + { + let mut state_guard = state.write(); + state_guard.url = uri.clone(); + state_guard.is_loading = true; + let history = nav_history.read(); + state_guard.can_go_back = history.can_go_back(); + state_guard.can_go_forward = history.can_go_forward(); + } + let _ = event_tx.send(WebViewEvent::NavigationStarted { url: uri }); + true + } + }) + .with_on_page_load_handler({ + let event_tx = event_tx.clone(); + let state = Arc::clone(&state); + let nav_history = Arc::clone(&nav_history); + let proxy = proxy.clone(); + move |event, url| { + if url == "about:blank" { + return; + } + + match event { + PageLoadEvent::Started => { + let can_back; + let can_forward; + { + let mut state_guard = state.write(); + state_guard.url = url.clone(); + state_guard.is_loading = true; + let history = nav_history.read(); + can_back = history.can_go_back(); + can_forward = history.can_go_forward(); + state_guard.can_go_back = can_back; + state_guard.can_go_forward = can_forward; + } + let state_json = serde_json::json!({ + "can_go_back": can_back, + "can_go_forward": can_forward, + "is_loading": true, + "url": url + }); + let script = format!("window._sctUpdateNavState && window._sctUpdateNavState({})", state_json); + let _ = proxy.send_event(UserEvent::Command(WebViewCommand::ExecuteScript(script))); + } + PageLoadEvent::Finished => { + let can_back; + let can_forward; + { + let mut history = nav_history.write(); + history.push(&url); + can_back = history.can_go_back(); + can_forward = history.can_go_forward(); + } + { + let mut state_guard = state.write(); + state_guard.url = url.clone(); + state_guard.is_loading = false; + state_guard.can_go_back = can_back; + state_guard.can_go_forward = can_forward; + } + let _ = event_tx.send(WebViewEvent::NavigationCompleted { url: url.clone() }); + + let state_json = serde_json::json!({ + "can_go_back": can_back, + "can_go_forward": can_forward, + "is_loading": false, + "url": url + }); + let script = format!("window._sctUpdateNavState && window._sctUpdateNavState({})", state_json); + let _ = proxy.send_event(UserEvent::Command(WebViewCommand::ExecuteScript(script))); + } + } + } + }) + .build(&window) + .expect("Failed to create webview"); + + let webview = Arc::new(webview); + let webview_cmd = Arc::clone(&webview); + + // Spawn command handler thread + let proxy_cmd = proxy.clone(); + std::thread::spawn(move || { + while let Ok(cmd) = cmd_rx.recv() { + if proxy_cmd.send_event(UserEvent::Command(cmd)).is_err() { + break; + } + } + }); + + // Run the event loop with run_return so we can properly cleanup + event_loop.run_return(|event, _, control_flow| { + *control_flow = ControlFlow::Wait; + + match event { + Event::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } => { + is_closed.store(true, Ordering::SeqCst); + let _ = event_tx.send(WebViewEvent::WindowClosed); + *control_flow = ControlFlow::Exit; + } + Event::UserEvent(user_event) => match user_event { + UserEvent::Command(cmd) => { + handle_command(&webview_cmd, &window, cmd, &state, &nav_history, &event_tx, &is_closed, control_flow); + } + UserEvent::Quit => { + is_closed.store(true, Ordering::SeqCst); + *control_flow = ControlFlow::Exit; + } + }, + _ => {} + } + }); + + // Explicitly drop in correct order + drop(webview_cmd); + drop(web_context); + drop(window); +} + +fn handle_command( + webview: &Arc, + window: &Arc, + command: WebViewCommand, + state: &Arc>, + nav_history: &Arc>, + event_tx: &Sender, + is_closed: &Arc, + control_flow: &mut ControlFlow, +) { + match command { + WebViewCommand::Navigate(url) => { + { + let mut s = state.write(); + s.url = url.clone(); + s.is_loading = true; + } + let _ = webview.load_url(&url); + let _ = event_tx.send(WebViewEvent::NavigationStarted { url }); + } + WebViewCommand::GoBack => { + let can_go = { + let mut history = nav_history.write(); + history.go_back() + }; + if can_go { + let _ = webview.evaluate_script("history.back()"); + } + } + WebViewCommand::GoForward => { + let can_go = { + let mut history = nav_history.write(); + history.go_forward() + }; + if can_go { + let _ = webview.evaluate_script("history.forward()"); + } + } + WebViewCommand::Reload => { + let _ = webview.evaluate_script("location.reload()"); + } + WebViewCommand::Stop => { + let _ = webview.evaluate_script("window.stop()"); + } + WebViewCommand::ExecuteScript(script) => { + let _ = webview.evaluate_script(&script); + } + WebViewCommand::SetVisibility(visible) => { + window.set_visible(visible); + } + WebViewCommand::Close => { + is_closed.store(true, Ordering::SeqCst); + let _ = event_tx.send(WebViewEvent::WindowClosed); + *control_flow = ControlFlow::Exit; + } + WebViewCommand::SetWindowSize(width, height) => { + let _ = window.set_inner_size(LogicalSize::new(width, height)); + } + WebViewCommand::SetWindowPosition(x, y) => { + window.set_outer_position(LogicalPosition::new(x, y)); + } + } +} + +/// Load the application icon from embedded ICO data +fn load_app_icon() -> Option { + use std::io::Cursor; + use image::ImageReader; + + let cursor = Cursor::new(APP_ICON_DATA); + let reader = match ImageReader::new(cursor).with_guessed_format() { + Ok(r) => r, + Err(e) => { + eprintln!("[SCToolbox] Failed to create image reader: {}", e); + return None; + } + }; + + let img = match reader.decode() { + Ok(img) => img, + Err(e) => { + eprintln!("[SCToolbox] Failed to decode icon: {}", e); + return None; + } + }; + + let rgba = img.to_rgba8(); + let (width, height) = rgba.dimensions(); + let raw_data = rgba.into_raw(); + + match Icon::from_rgba(raw_data, width, height) { + Ok(icon) => Some(icon), + Err(e) => { + eprintln!("[SCToolbox] Failed to create icon: {:?}", e); + None + } + } +} diff --git a/rust/src/webview/webview_stub.rs b/rust/src/webview/webview_stub.rs new file mode 100644 index 0000000..15e9089 --- /dev/null +++ b/rust/src/webview/webview_stub.rs @@ -0,0 +1,39 @@ +// macOS WebView 存根实现 +// macOS 平台不支持 WebView,因为 tao 的 EventLoop 必须在主线程运行,与 Flutter 冲突 + +use std::collections::HashMap; +use std::sync::Arc; +use std::sync::atomic::AtomicBool; + +use crossbeam_channel::{Receiver, Sender}; +use parking_lot::RwLock; +use once_cell::sync::Lazy; + +use crate::api::webview_api::{ + WebViewConfiguration, WebViewNavigationState, WebViewEvent, WebViewCommand, +}; + +// ============ Types ============ + +pub type WebViewId = String; + +pub struct WebViewInstance { + pub command_sender: Sender, + pub event_receiver: Receiver, + pub state: Arc>, + pub is_closed: Arc, +} + +pub static WEBVIEW_INSTANCES: Lazy>> = + Lazy::new(|| RwLock::new(HashMap::new())); + +// ============ macOS 存根实现 ============ + +/// macOS 上 WebView 不可用 +pub fn create_webview(_config: WebViewConfiguration) -> Result { + Err("WebView is not supported on macOS".to_string()) +} + +pub fn send_command(id: &str, _command: WebViewCommand) -> Result<(), String> { + Err(format!("WebView instance not found: {} (WebView not supported on macOS)", id)) +}