feat: Optimize structure

This commit is contained in:
xkeyC 2025-12-05 10:12:06 +08:00
parent b11603d68c
commit 79e1256759
7 changed files with 583 additions and 539 deletions

View File

@ -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

View File

@ -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]

View File

@ -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<String>,
/// 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<WebViewCommand>,
event_receiver: Receiver<WebViewEvent>,
state: Arc<RwLock<WebViewNavigationState>>,
is_closed: Arc<AtomicBool>,
}
static WEBVIEW_INSTANCES: Lazy<RwLock<HashMap<WebViewId, WebViewInstance>>> =
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<String, String> {
let id = uuid::Uuid::new_v4().to_string();
let id_clone = id.clone();
let (cmd_tx, cmd_rx) = bounded::<WebViewCommand>(100);
let (event_tx, event_rx) = bounded::<WebViewEvent>(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<String> {
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<WebViewCommand>,
event_tx: Sender<WebViewEvent>,
state: Arc<RwLock<WebViewNavigationState>>,
is_closed: Arc<AtomicBool>,
) {
// Create event loop with any_thread support for non-main thread execution
#[cfg(target_os = "windows")]
let mut event_loop: EventLoop<UserEvent> = EventLoopBuilder::with_user_event()
.with_any_thread(true)
.build();
#[cfg(not(target_os = "windows"))]
let mut event_loop: EventLoop<UserEvent> = 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::<serde_json::Value>(&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<WebView>,
window: &Arc<Window>,
command: WebViewCommand,
state: &Arc<RwLock<WebViewNavigationState>>,
nav_history: &Arc<RwLock<NavigationHistory>>,
event_tx: &Sender<WebViewEvent>,
is_closed: &Arc<AtomicBool>,
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<Icon> {
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
}
}
}

View File

@ -2,3 +2,4 @@ pub mod api;
mod frb_generated;
pub mod http_package;
pub mod ort_models;
pub mod webview;

16
rust/src/webview/mod.rs Normal file
View File

@ -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::*;

View File

@ -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<String>,
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<WebViewCommand>,
pub event_receiver: Receiver<WebViewEvent>,
pub state: Arc<RwLock<WebViewNavigationState>>,
pub is_closed: Arc<AtomicBool>,
}
pub static WEBVIEW_INSTANCES: Lazy<RwLock<HashMap<WebViewId, WebViewInstance>>> =
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<String, String> {
let id = uuid::Uuid::new_v4().to_string();
let id_clone = id.clone();
let (cmd_tx, cmd_rx) = bounded::<WebViewCommand>(100);
let (event_tx, event_rx) = bounded::<WebViewEvent>(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<WebViewCommand>,
event_tx: Sender<WebViewEvent>,
state: Arc<RwLock<WebViewNavigationState>>,
is_closed: Arc<AtomicBool>,
) {
// Create event loop with any_thread support for non-main thread execution
#[cfg(target_os = "windows")]
let mut event_loop: EventLoop<UserEvent> = EventLoopBuilder::with_user_event()
.with_any_thread(true)
.build();
#[cfg(not(target_os = "windows"))]
let mut event_loop: EventLoop<UserEvent> = 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::<serde_json::Value>(&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<WebView>,
window: &Arc<Window>,
command: WebViewCommand,
state: &Arc<RwLock<WebViewNavigationState>>,
nav_history: &Arc<RwLock<NavigationHistory>>,
event_tx: &Sender<WebViewEvent>,
is_closed: &Arc<AtomicBool>,
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<Icon> {
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
}
}
}

View File

@ -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<WebViewCommand>,
pub event_receiver: Receiver<WebViewEvent>,
pub state: Arc<RwLock<WebViewNavigationState>>,
pub is_closed: Arc<AtomicBool>,
}
pub static WEBVIEW_INSTANCES: Lazy<RwLock<HashMap<WebViewId, WebViewInstance>>> =
Lazy::new(|| RwLock::new(HashMap::new()));
// ============ macOS 存根实现 ============
/// macOS 上 WebView 不可用
pub fn create_webview(_config: WebViewConfiguration) -> Result<String, String> {
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))
}