mirror of
https://github.com/StarCitizenToolBox/app.git
synced 2026-01-13 11:40:27 +00:00
feat: Optimize structure
This commit is contained in:
parent
b11603d68c
commit
79e1256759
@ -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
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
16
rust/src/webview/mod.rs
Normal 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::*;
|
||||
512
rust/src/webview/webview_impl.rs
Normal file
512
rust/src/webview/webview_impl.rs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
39
rust/src/webview/webview_stub.rs
Normal file
39
rust/src/webview/webview_stub.rs
Normal 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))
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user