Files
Genarrative/apps/desktop-shell/src-tauri/src/app.rs
T
kdletters 1b895370c7 移除桌面壳原生菜单栏
删除 Tauri 应用菜单注册与 shell/menu.rs 实现
更新桌面壳结构门禁,移除菜单栏必需片段
同步宿主壳方案文档和共享决策记录
2026-06-22 17:09:29 +08:00

242 lines
9.8 KiB
Rust

use crate::host_bridge::{DesktopShareState, HostBridgeReplayState};
use crate::shell::deep_link::{
register_desktop_deep_link_events, register_desktop_deep_link_schemes,
};
use crate::shell::tray::{
register_desktop_tray, register_desktop_window_close_events,
resolve_desktop_single_instance_action, show_main_window, DesktopSingleInstanceAction,
};
use crate::shell::webview::{
desktop_external_navigation_url, desktop_window_config_with_runtime_platform,
emit_current_desktop_lifecycle_event, handle_desktop_webview_download,
log_desktop_host_event_result, open_desktop_external_navigation,
open_normalized_desktop_external_url, register_desktop_file_drop_events,
register_desktop_lifecycle_events, register_desktop_navigation_events,
replay_desktop_webview_state, should_allow_desktop_webview_navigation,
should_replay_desktop_webview_state_on_page_load,
};
use crate::shell::window_state::desktop_window_state_plugin;
use tauri::webview::NewWindowResponse;
const DESKTOP_MAIN_WINDOW_LABEL: &str = "main";
fn desktop_main_window_config_from_windows(
windows: &[tauri::utils::config::WindowConfig],
) -> tauri::Result<tauri::utils::config::WindowConfig> {
windows
.iter()
.find(|window| window.label == DESKTOP_MAIN_WINDOW_LABEL)
.cloned()
.map(desktop_window_config_with_runtime_platform)
.ok_or(tauri::Error::WindowNotFound)
}
#[cfg(dev)]
fn apply_desktop_dev_url_to_main_window_config(
mut config: tauri::utils::config::WindowConfig,
dev_url: &tauri::Url,
) -> tauri::utils::config::WindowConfig {
config.url = tauri::WebviewUrl::External(dev_url.clone());
desktop_window_config_with_runtime_platform(config)
}
fn desktop_main_window_config(
app: &tauri::App,
) -> tauri::Result<tauri::utils::config::WindowConfig> {
let config = desktop_main_window_config_from_windows(&app.config().app.windows)?;
#[cfg(dev)]
if let Some(dev_url) = app.config().build.dev_url.as_ref() {
return Ok(apply_desktop_dev_url_to_main_window_config(config, dev_url));
}
Ok(config)
}
fn desktop_new_window_external_url(url: &tauri::Url) -> Option<String> {
desktop_external_navigation_url(url)
}
fn desktop_new_window_response<R: tauri::Runtime>() -> NewWindowResponse<R> {
NewWindowResponse::Deny
}
pub(crate) fn run() {
tauri::Builder::default()
.manage(DesktopShareState::default())
.manage(HostBridgeReplayState::default())
.plugin(tauri_plugin_single_instance::init(|app, _argv, _cwd| {
match resolve_desktop_single_instance_action() {
DesktopSingleInstanceAction::ShowMainWindow => {
log_desktop_host_event_result("single_instance.show", show_main_window(app));
}
}
}))
.plugin(desktop_window_state_plugin())
.plugin(tauri_plugin_deep_link::init())
.plugin(tauri_plugin_clipboard_manager::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_notification::init())
.plugin(tauri_plugin_opener::init())
.setup(|app| {
let tray_registered = match register_desktop_tray(app) {
Ok(()) => true,
Err(_error) => {
eprintln!("desktop tray registration failed");
false
}
};
let config = desktop_main_window_config(app)?;
let app_handle = app.handle().clone();
let new_window_app_handle = app.handle().clone();
let window = tauri::WebviewWindowBuilder::from_config(app.handle(), &config)?
.on_navigation(move |url| {
if should_allow_desktop_webview_navigation(url) {
true
} else {
open_desktop_external_navigation(&app_handle, url);
false
}
})
.on_new_window(move |url, _features| {
if let Some(external_url) = desktop_new_window_external_url(&url) {
log_desktop_host_event_result(
"webview.new_window.external",
open_normalized_desktop_external_url(
&new_window_app_handle,
external_url,
),
);
}
desktop_new_window_response::<tauri::Wry>()
})
.on_page_load(|window, payload| {
if should_replay_desktop_webview_state_on_page_load(payload.event()) {
replay_desktop_webview_state(&window);
}
})
.on_download(|_webview, event| handle_desktop_webview_download(&event))
.build()?;
register_desktop_window_close_events(&window, tray_registered);
register_desktop_lifecycle_events(&window);
log_desktop_host_event_result(
"app.lifecycle",
emit_current_desktop_lifecycle_event(&window),
);
register_desktop_navigation_events(&window)?;
register_desktop_file_drop_events(&window);
register_desktop_deep_link_events(app)?;
register_desktop_deep_link_schemes(app);
Ok(())
})
.invoke_handler(tauri::generate_handler![
crate::host_bridge::host_bridge_request
])
.run(tauri::generate_context!())
.expect("failed to run Genarrative desktop shell");
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn desktop_main_window_config_uses_labeled_main_window() {
let mut secondary_window = tauri::utils::config::WindowConfig::default();
secondary_window.label = "secondary".to_string();
secondary_window.url = tauri::WebviewUrl::App(PathBuf::from("secondary.html"));
let mut main_window = tauri::utils::config::WindowConfig::default();
main_window.label = "main".to_string();
main_window.url = tauri::WebviewUrl::App(PathBuf::from("index.html"));
let config = desktop_main_window_config_from_windows(&[secondary_window, main_window])
.expect("main window config");
assert_eq!(config.label, "main");
match config.url {
tauri::WebviewUrl::App(path) => {
let path = path.to_string_lossy();
assert!(path.starts_with("index.html?"));
assert!(path.contains("clientRuntime=native_app"));
assert!(path.contains("clientType=native_app"));
assert!(path.contains("hostShell=tauri_desktop"));
assert!(path.contains("hostPlatform="));
assert!(path.contains(&format!("hostVersion={}", env!("CARGO_PKG_VERSION"))));
assert!(path.contains("bridgeVersion="));
assert!(path.contains("hostCapabilities="));
}
other => panic!("unexpected main window url {other:?}"),
}
}
#[cfg(dev)]
#[test]
fn desktop_main_window_config_uses_dev_url_in_dev_builds() {
let mut main_window = tauri::utils::config::WindowConfig::default();
main_window.label = "main".to_string();
main_window.url = tauri::WebviewUrl::App(PathBuf::from("index.html"));
let mut config = tauri::utils::config::Config::default();
config.app.windows = vec![main_window];
config.build.dev_url =
Some(tauri::Url::parse("http://127.0.0.1:3000/").expect("desktop dev url"));
let mut resolved = desktop_main_window_config_from_windows(&config.app.windows)
.expect("main window config");
if let Some(dev_url) = config.build.dev_url.as_ref() {
resolved = apply_desktop_dev_url_to_main_window_config(resolved, dev_url);
}
match resolved.url {
tauri::WebviewUrl::External(url) => {
assert_eq!(url.origin().ascii_serialization(), "http://127.0.0.1:3000");
assert_eq!(url.path(), "/");
assert_eq!(
url.query_pairs()
.find(|(key, _)| key == "hostShell")
.map(|(_, value)| value.into_owned()),
Some("tauri_desktop".to_string())
);
}
other => panic!("unexpected dev main window url {other:?}"),
}
}
#[test]
fn desktop_main_window_config_rejects_missing_main_window() {
let mut secondary_window = tauri::utils::config::WindowConfig::default();
secondary_window.label = "secondary".to_string();
let error = desktop_main_window_config_from_windows(&[secondary_window])
.expect_err("missing main window should fail startup");
assert!(matches!(error, tauri::Error::WindowNotFound));
}
#[test]
fn desktop_new_window_policy_denies_embedded_windows() {
assert!(matches!(
desktop_new_window_response::<tauri::Wry>(),
NewWindowResponse::Deny
));
}
#[test]
fn desktop_new_window_policy_opens_only_safe_external_urls() {
let external_url = tauri::Url::parse("https://example.com/share")
.expect("external desktop new window url");
assert_eq!(
desktop_new_window_external_url(&external_url),
Some("https://example.com/share".to_string())
);
let same_origin_url = tauri::Url::parse("https://www.genarrative.world/works/detail")
.expect("same-origin desktop new window url");
assert_eq!(desktop_new_window_external_url(&same_origin_url), None);
let unsafe_url =
tauri::Url::parse("javascript:alert(1)").expect("unsafe desktop new window url");
assert_eq!(desktop_new_window_external_url(&unsafe_url), None);
}
}