Tauri 编辑器应用框架

This commit is contained in:
YHH
2025-10-14 22:53:26 +08:00
parent b20b2ae4ce
commit bd839cf431
18 changed files with 1702 additions and 5 deletions

View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ECS Framework Editor</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@@ -0,0 +1,31 @@
{
"name": "@esengine/editor-app",
"version": "1.0.0",
"description": "ECS Framework Editor Application - Cross-platform desktop editor",
"type": "module",
"private": true,
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"tauri": "tauri",
"tauri:dev": "tauri dev",
"tauri:build": "tauri build"
},
"dependencies": {
"@esengine/ecs-framework": "file:../core",
"@esengine/editor-core": "file:../editor-core",
"@tauri-apps/api": "^2.2.0",
"@tauri-apps/plugin-shell": "^2.0.0",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@tauri-apps/cli": "^2.2.0",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.3.4",
"typescript": "^5.8.3",
"vite": "^6.0.7"
}
}

View File

@@ -0,0 +1,29 @@
[package]
name = "ecs-editor"
version = "1.0.0"
description = "ECS Framework Editor - Cross-platform desktop editor"
authors = ["yhh"]
edition = "2021"
[lib]
name = "ecs_editor_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = [] }
tauri-plugin-shell = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
[profile.dev]
incremental = true
[profile.release]
codegen-units = 1
lto = true
opt-level = "s"
panic = "abort"
strip = true

View File

@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}

View File

@@ -0,0 +1,25 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct ProjectInfo {
pub name: String,
pub path: String,
pub version: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct EditorConfig {
pub theme: String,
pub auto_save: bool,
pub recent_projects: Vec<String>,
}
impl Default for EditorConfig {
fn default() -> Self {
Self {
theme: "dark".to_string(),
auto_save: true,
recent_projects: Vec::new(),
}
}
}

View File

@@ -0,0 +1,7 @@
// ECS Editor Library
pub mod commands;
pub mod project;
pub use commands::*;
pub use project::*;

View File

@@ -0,0 +1,54 @@
// Prevents additional console window on Windows in release
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use tauri::Manager;
// IPC Commands
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}! Welcome to ECS Framework Editor.", name)
}
#[tauri::command]
fn open_project(path: String) -> Result<String, String> {
// 项目打开逻辑
Ok(format!("Project opened: {}", path))
}
#[tauri::command]
fn save_project(path: String, data: String) -> Result<(), String> {
// 项目保存逻辑
std::fs::write(&path, data)
.map_err(|e| format!("Failed to save project: {}", e))?;
Ok(())
}
#[tauri::command]
fn export_binary(data: Vec<u8>, output_path: String) -> Result<(), String> {
// 二进制导出逻辑
std::fs::write(&output_path, data)
.map_err(|e| format!("Failed to export binary: {}", e))?;
Ok(())
}
fn main() {
tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
.setup(|app| {
// 应用启动时的初始化逻辑
#[cfg(debug_assertions)]
{
let window = app.get_webview_window("main").unwrap();
window.open_devtools();
}
Ok(())
})
.invoke_handler(tauri::generate_handler![
greet,
open_project,
save_project,
export_binary
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

View File

@@ -0,0 +1,42 @@
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct Project {
pub name: String,
pub path: PathBuf,
pub scenes: Vec<String>,
pub assets: Vec<String>,
}
impl Project {
pub fn new(name: String, path: PathBuf) -> Self {
Self {
name,
path,
scenes: Vec::new(),
assets: Vec::new(),
}
}
pub fn load(path: &PathBuf) -> Result<Self, String> {
let content = std::fs::read_to_string(path)
.map_err(|e| format!("Failed to read project file: {}", e))?;
serde_json::from_str(&content)
.map_err(|e| format!("Failed to parse project file: {}", e))
}
pub fn save(&self) -> Result<(), String> {
let mut project_file = self.path.clone();
project_file.push("project.json");
let content = serde_json::to_string_pretty(self)
.map_err(|e| format!("Failed to serialize project: {}", e))?;
std::fs::write(&project_file, content)
.map_err(|e| format!("Failed to write project file: {}", e))?;
Ok(())
}
}

View File

@@ -0,0 +1,59 @@
{
"productName": "ECS Framework Editor",
"version": "1.0.0",
"identifier": "com.esengine.editor",
"build": {
"beforeDevCommand": "npm run dev",
"devUrl": "http://localhost:5173",
"beforeBuildCommand": "npm run build",
"frontendDist": "../dist"
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"windows": {
"certificateThumbprint": null,
"digestAlgorithm": "sha256",
"timestampUrl": ""
},
"macOS": {
"entitlements": null,
"exceptionDomain": "",
"frameworks": [],
"providerShortName": null,
"signingIdentity": null
}
},
"app": {
"windows": [
{
"title": "ECS Framework Editor",
"width": 1280,
"height": 800,
"minWidth": 800,
"minHeight": 600,
"resizable": true,
"fullscreen": false,
"decorations": true,
"transparent": false,
"center": true,
"skipTaskbar": false
}
],
"security": {
"csp": null
}
},
"plugins": {
"shell": {
"open": true
}
}
}

View File

@@ -0,0 +1,89 @@
import { useState, useEffect } from 'react';
import { Core } from '@esengine/ecs-framework';
import { EditorPluginManager, UIRegistry, MessageHub, SerializerRegistry } from '@esengine/editor-core';
import { SceneInspectorPlugin } from './plugins/SceneInspectorPlugin';
import { TauriAPI } from './api/tauri';
import './styles/App.css';
function App() {
const [core, setCore] = useState<Core | null>(null);
const [pluginManager, setPluginManager] = useState<EditorPluginManager | null>(null);
const [status, setStatus] = useState('Initializing...');
useEffect(() => {
const initializeEditor = async () => {
try {
const coreInstance = Core.create({ debug: true });
const uiRegistry = new UIRegistry();
const messageHub = new MessageHub();
const serializerRegistry = new SerializerRegistry();
Core.services.registerInstance(UIRegistry, uiRegistry);
Core.services.registerInstance(MessageHub, messageHub);
Core.services.registerInstance(SerializerRegistry, serializerRegistry);
const pluginMgr = new EditorPluginManager();
pluginMgr.initialize(coreInstance, Core.services);
await pluginMgr.installEditor(new SceneInspectorPlugin());
const greeting = await TauriAPI.greet('Developer');
console.log(greeting);
setCore(coreInstance);
setPluginManager(pluginMgr);
setStatus('Editor Ready');
} catch (error) {
console.error('Failed to initialize editor:', error);
setStatus('Initialization Failed');
}
};
initializeEditor();
return () => {
Core.destroy();
};
}, []);
return (
<div className="editor-container">
<div className="editor-header">
<h1>ECS Framework Editor</h1>
<span className="status">{status}</span>
</div>
<div className="editor-content">
<div className="sidebar-left">
<h3>Hierarchy</h3>
<p>Scene hierarchy will appear here</p>
</div>
<div className="main-content">
<div className="viewport">
<h3>Viewport</h3>
<p>Scene viewport will appear here</p>
</div>
<div className="bottom-panel">
<h4>Console</h4>
<p>Console output will appear here</p>
</div>
</div>
<div className="sidebar-right">
<h3>Inspector</h3>
<p>Entity inspector will appear here</p>
</div>
</div>
<div className="editor-footer">
<span>Plugins: {pluginManager?.getAllEditorPlugins().length ?? 0}</span>
<span>Core: {core ? 'Active' : 'Inactive'}</span>
</div>
</div>
);
}
export default App;

View File

@@ -0,0 +1,55 @@
import { invoke } from '@tauri-apps/api/core';
/**
* Tauri IPC 通信层
*/
export class TauriAPI {
/**
* 打招呼(测试命令)
*/
static async greet(name: string): Promise<string> {
return await invoke<string>('greet', { name });
}
/**
* 打开项目
*/
static async openProject(path: string): Promise<string> {
return await invoke<string>('open_project', { path });
}
/**
* 保存项目
*/
static async saveProject(path: string, data: string): Promise<void> {
return await invoke<void>('save_project', { path, data });
}
/**
* 导出二进制数据
*/
static async exportBinary(data: Uint8Array, outputPath: string): Promise<void> {
return await invoke<void>('export_binary', {
data: Array.from(data),
outputPath
});
}
}
/**
* 项目信息
*/
export interface ProjectInfo {
name: string;
path: string;
version: string;
}
/**
* 编辑器配置
*/
export interface EditorConfig {
theme: string;
autoSave: boolean;
recentProjects: string[];
}

View File

@@ -0,0 +1,10 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './styles/index.css';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@@ -0,0 +1,130 @@
import type { Core, ServiceContainer } from '@esengine/ecs-framework';
import { IEditorPlugin, EditorPluginCategory, PanelPosition } from '@esengine/editor-core';
import type { MenuItem, ToolbarItem, PanelDescriptor, ISerializer } from '@esengine/editor-core';
/**
* Scene Inspector 插件
*
* 提供场景层级视图和实体检视功能
*/
export class SceneInspectorPlugin implements IEditorPlugin {
readonly name = '@esengine/scene-inspector';
readonly version = '1.0.0';
readonly displayName = 'Scene Inspector';
readonly category = EditorPluginCategory.Inspector;
readonly description = 'Scene hierarchy and entity inspector';
readonly icon = '🔍';
async install(_core: Core, _services: ServiceContainer): Promise<void> {
console.log('[SceneInspectorPlugin] Installed');
}
async uninstall(): Promise<void> {
console.log('[SceneInspectorPlugin] Uninstalled');
}
registerMenuItems(): MenuItem[] {
return [
{
id: 'view-scene-inspector',
label: 'Scene Inspector',
parentId: 'view',
onClick: () => {
console.log('Toggle Scene Inspector');
},
shortcut: 'Ctrl+Shift+I',
order: 100
},
{
id: 'scene-create-entity',
label: 'Create Entity',
parentId: 'scene',
onClick: () => {
console.log('Create new entity');
},
shortcut: 'Ctrl+N',
order: 10
}
];
}
registerToolbar(): ToolbarItem[] {
return [
{
id: 'toolbar-create-entity',
label: 'New Entity',
groupId: 'entity-tools',
icon: '',
onClick: () => {
console.log('Create entity from toolbar');
},
order: 10
},
{
id: 'toolbar-delete-entity',
label: 'Delete Entity',
groupId: 'entity-tools',
icon: '🗑️',
onClick: () => {
console.log('Delete entity from toolbar');
},
order: 20
}
];
}
registerPanels(): PanelDescriptor[] {
return [
{
id: 'panel-scene-hierarchy',
title: 'Scene Hierarchy',
position: PanelPosition.Left,
defaultSize: 250,
resizable: true,
closable: false,
icon: '📋',
order: 10
},
{
id: 'panel-entity-inspector',
title: 'Entity Inspector',
position: PanelPosition.Right,
defaultSize: 300,
resizable: true,
closable: false,
icon: '🔎',
order: 10
}
];
}
getSerializers(): ISerializer[] {
return [
{
serialize: (data: any) => {
const json = JSON.stringify(data);
const encoder = new TextEncoder();
return encoder.encode(json);
},
deserialize: (data: Uint8Array) => {
const decoder = new TextDecoder();
const json = decoder.decode(data);
return JSON.parse(json);
},
getSupportedType: () => 'scene'
}
];
}
async onEditorReady(): Promise<void> {
console.log('[SceneInspectorPlugin] Editor is ready');
}
async onProjectOpen(projectPath: string): Promise<void> {
console.log(`[SceneInspectorPlugin] Project opened: ${projectPath}`);
}
async onProjectClose(): Promise<void> {
console.log('[SceneInspectorPlugin] Project closed');
}
}

View File

@@ -0,0 +1,99 @@
.editor-container {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
background-color: #1e1e1e;
color: #cccccc;
}
.editor-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 16px;
background-color: #2d2d2d;
border-bottom: 1px solid #3e3e3e;
}
.editor-header h1 {
font-size: 16px;
font-weight: 600;
margin: 0;
}
.editor-header .status {
font-size: 12px;
color: #4ec9b0;
}
.editor-content {
display: flex;
flex: 1;
overflow: hidden;
}
.sidebar-left,
.sidebar-right {
width: 250px;
background-color: #252526;
border-right: 1px solid #3e3e3e;
padding: 12px;
overflow-y: auto;
}
.sidebar-right {
border-right: none;
border-left: 1px solid #3e3e3e;
}
.sidebar-left h3,
.sidebar-right h3 {
font-size: 14px;
margin-bottom: 12px;
color: #ffffff;
}
.main-content {
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
}
.viewport {
flex: 1;
background-color: #1e1e1e;
border-bottom: 1px solid #3e3e3e;
padding: 12px;
display: flex;
flex-direction: column;
}
.viewport h3 {
font-size: 14px;
margin-bottom: 12px;
color: #ffffff;
}
.bottom-panel {
height: 200px;
background-color: #252526;
padding: 12px;
overflow-y: auto;
}
.bottom-panel h4 {
font-size: 12px;
margin-bottom: 8px;
color: #ffffff;
}
.editor-footer {
display: flex;
justify-content: space-between;
padding: 4px 16px;
background-color: #007acc;
color: #ffffff;
font-size: 12px;
}

View File

@@ -0,0 +1,19 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
overflow: hidden;
}
#root {
width: 100vw;
height: 100vh;
}

View File

@@ -0,0 +1,23 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true
},
"include": ["src"]
}

View File

@@ -0,0 +1,27 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
const host = process.env.TAURI_DEV_HOST;
export default defineConfig({
plugins: [react()],
clearScreen: false,
server: {
host: host || false,
port: 5173,
strictPort: true,
hmr: host
? {
protocol: 'ws',
host,
port: 5183,
}
: undefined,
},
envPrefix: ['VITE_', 'TAURI_'],
build: {
target: 'es2021',
minify: !process.env.TAURI_DEBUG ? 'esbuild' : false,
sourcemap: !!process.env.TAURI_DEBUG,
},
});