e5424cbf7b
新增自绘标题栏、窗口控制和项目标题同步 将泥点账户入口接入标题栏并调整面板描边 收紧本地图片导入类型约束并同步契约文档 补充窗口外壳与泥点面板测试
245 lines
8.1 KiB
Rust
245 lines
8.1 KiB
Rust
use super::*;
|
|
|
|
#[cfg(windows)]
|
|
fn configure_windows_background_std_command_impl(
|
|
command: &mut std::process::Command,
|
|
create_process_group: bool,
|
|
) {
|
|
use std::os::windows::process::CommandExt;
|
|
|
|
const CREATE_NEW_PROCESS_GROUP: u32 = 0x0000_0200;
|
|
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
|
|
command.creation_flags(
|
|
CREATE_NO_WINDOW
|
|
| if create_process_group {
|
|
CREATE_NEW_PROCESS_GROUP
|
|
} else {
|
|
0
|
|
},
|
|
);
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
pub(crate) fn configure_windows_background_std_command(
|
|
command: &mut std::process::Command,
|
|
create_process_group: bool,
|
|
) {
|
|
configure_windows_background_std_command_impl(command, create_process_group);
|
|
}
|
|
|
|
#[cfg(not(windows))]
|
|
pub(crate) fn configure_windows_background_std_command(
|
|
_command: &mut std::process::Command,
|
|
_create_process_group: bool,
|
|
) {
|
|
}
|
|
|
|
pub(crate) fn new_windows_background_std_command<S>(program: S) -> std::process::Command
|
|
where
|
|
S: AsRef<std::ffi::OsStr>,
|
|
{
|
|
let mut command = std::process::Command::new(program);
|
|
configure_windows_background_std_command(&mut command, false);
|
|
command
|
|
}
|
|
|
|
pub(crate) fn configure_windows_background_tokio_command(
|
|
command: &mut tokio::process::Command,
|
|
create_process_group: bool,
|
|
) {
|
|
configure_windows_background_std_command(command.as_std_mut(), create_process_group);
|
|
}
|
|
|
|
#[cfg(all(test, windows))]
|
|
mod windows_background_command_tests {
|
|
use super::*;
|
|
use std::fs;
|
|
use std::process::Stdio;
|
|
|
|
const CHILD_ENV: &str = "GENARRATIVE_WINDOWS_NO_CONSOLE_CHILD";
|
|
const RESULT_ENV: &str = "GENARRATIVE_WINDOWS_NO_CONSOLE_RESULT";
|
|
const FIXTURE_TEST: &str =
|
|
"windows::windows_background_command_tests::background_command_console_fixture";
|
|
|
|
#[test]
|
|
#[ignore = "child-process fixture"]
|
|
fn background_command_console_fixture() {
|
|
if std::env::var_os(CHILD_ENV).is_none() {
|
|
return;
|
|
}
|
|
#[link(name = "kernel32")]
|
|
unsafe extern "system" {
|
|
fn GetConsoleWindow() -> windows_sys::Win32::Foundation::HWND;
|
|
}
|
|
let result_path = std::env::var_os(RESULT_ENV).expect("result path");
|
|
let has_console_window = unsafe { !GetConsoleWindow().is_null() };
|
|
fs::write(
|
|
result_path,
|
|
if has_console_window {
|
|
"console"
|
|
} else {
|
|
"hidden"
|
|
},
|
|
)
|
|
.expect("write console-window result");
|
|
}
|
|
|
|
#[test]
|
|
fn background_std_command_does_not_allocate_a_console_window() {
|
|
let directory = tempfile::tempdir().expect("create no-console test directory");
|
|
let result_path = directory.path().join("console-window.txt");
|
|
let mut command = new_windows_background_std_command(
|
|
std::env::current_exe().expect("current test binary"),
|
|
);
|
|
command
|
|
.args(["--exact", FIXTURE_TEST, "--ignored"])
|
|
.env(CHILD_ENV, "1")
|
|
.env(RESULT_ENV, &result_path)
|
|
.stdin(Stdio::null())
|
|
.stdout(Stdio::null())
|
|
.stderr(Stdio::null());
|
|
let status = command.status().expect("run no-console child fixture");
|
|
assert!(status.success(), "no-console child fixture must succeed");
|
|
assert_eq!(
|
|
fs::read_to_string(result_path).expect("read console-window result"),
|
|
"hidden",
|
|
"CREATE_NO_WINDOW must keep background command probes from flashing a console window"
|
|
);
|
|
}
|
|
}
|
|
|
|
pub(crate) fn workspace_window_url(project_path: &str) -> tauri::WebviewUrl {
|
|
tauri::WebviewUrl::App(PathBuf::from(format!(
|
|
"index.html?main&projectPath={}",
|
|
percent_encode_query_value(project_path)
|
|
)))
|
|
}
|
|
|
|
pub(crate) fn launcher_window_url() -> tauri::WebviewUrl {
|
|
tauri::WebviewUrl::App(PathBuf::from("index.html?launcher"))
|
|
}
|
|
|
|
pub(crate) fn supervisor_chat_window_url(project_path: &str) -> tauri::WebviewUrl {
|
|
tauri::WebviewUrl::App(PathBuf::from(format!(
|
|
"index.html?supervisor-chat&projectPath={}",
|
|
percent_encode_query_value(project_path)
|
|
)))
|
|
}
|
|
|
|
pub(crate) fn validate_workspace_window_project_path(project_path: &str) -> Result<&str, String> {
|
|
let project_path = project_path.trim();
|
|
if project_path.is_empty() {
|
|
return Err("请提供工作区绝对路径".to_string());
|
|
}
|
|
if !Path::new(project_path).is_absolute() {
|
|
return Err("工作区路径必须是绝对路径".to_string());
|
|
}
|
|
if project_path.chars().any(char::is_control) {
|
|
return Err("工作区路径不能包含控制字符".to_string());
|
|
}
|
|
Ok(project_path)
|
|
}
|
|
|
|
fn percent_encode_query_value(value: &str) -> String {
|
|
let mut encoded = String::new();
|
|
for byte in value.bytes() {
|
|
match byte {
|
|
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' => {
|
|
encoded.push(byte as char)
|
|
}
|
|
_ => encoded.push_str(&format!("%{byte:02X}")),
|
|
}
|
|
}
|
|
encoded
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn open_game_creator_workspace_window(
|
|
app: tauri::AppHandle,
|
|
window: tauri::Window,
|
|
project_path: String,
|
|
) -> Result<(), String> {
|
|
let project_path = validate_workspace_window_project_path(&project_path)?;
|
|
if let Some(existing) = app.get_webview_window("main") {
|
|
existing.close().map_err(|error| error.to_string())?;
|
|
}
|
|
tauri::WebviewWindowBuilder::new(&app, "main", workspace_window_url(project_path))
|
|
.title("陶泥儿")
|
|
.decorations(false)
|
|
.inner_size(1180.0, 820.0)
|
|
.min_inner_size(760.0, 560.0)
|
|
.build()
|
|
.map_err(|error| error.to_string())?;
|
|
window.close().map_err(|error| error.to_string())?;
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn open_game_creator_launcher_window(
|
|
app: tauri::AppHandle,
|
|
window: tauri::Window,
|
|
) -> Result<(), String> {
|
|
if let Some(existing) = app.get_webview_window("launcher") {
|
|
existing.set_focus().map_err(|error| error.to_string())?;
|
|
} else {
|
|
tauri::WebviewWindowBuilder::new(&app, "launcher", launcher_window_url())
|
|
.title("陶泥儿")
|
|
.decorations(false)
|
|
.inner_size(820.0, 640.0)
|
|
.min_inner_size(720.0, 520.0)
|
|
.build()
|
|
.map_err(|error| error.to_string())?;
|
|
}
|
|
window.close().map_err(|error| error.to_string())?;
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn open_project_supervisor_chat_window(
|
|
app: tauri::AppHandle,
|
|
project_path: String,
|
|
) -> Result<(), String> {
|
|
let project_path = validate_workspace_window_project_path(&project_path)?;
|
|
#[cfg(not(debug_assertions))]
|
|
{
|
|
let _ = app;
|
|
let _ = project_path;
|
|
return Err("项目总控对话窗口仅在开发构建中可用".to_string());
|
|
}
|
|
#[cfg(debug_assertions)]
|
|
{
|
|
if let Some(existing) = app.get_webview_window("supervisor-chat") {
|
|
let mut current_url = existing.url().map_err(|error| error.to_string())?;
|
|
let current_project_path = current_url
|
|
.query_pairs()
|
|
.find_map(|(key, value)| (key == "projectPath").then(|| value.into_owned()));
|
|
if current_project_path.as_deref() != Some(project_path) {
|
|
current_url.set_query(Some(&format!(
|
|
"supervisor-chat&projectPath={}",
|
|
percent_encode_query_value(project_path)
|
|
)));
|
|
current_url.set_fragment(None);
|
|
existing
|
|
.navigate(current_url)
|
|
.map_err(|error| error.to_string())?;
|
|
}
|
|
existing.show().map_err(|error| error.to_string())?;
|
|
existing.unminimize().map_err(|error| error.to_string())?;
|
|
existing.set_focus().map_err(|error| error.to_string())?;
|
|
return Ok(());
|
|
}
|
|
tauri::WebviewWindowBuilder::new(
|
|
&app,
|
|
"supervisor-chat",
|
|
supervisor_chat_window_url(project_path),
|
|
)
|
|
.title("项目总控 Agent 对话")
|
|
.decorations(false)
|
|
.inner_size(820.0, 720.0)
|
|
.min_inner_size(560.0, 480.0)
|
|
.build()
|
|
.map_err(|error| error.to_string())?;
|
|
Ok(())
|
|
}
|
|
}
|