diff --git a/packages/editor-app/src-tauri/src/main.rs b/packages/editor-app/src-tauri/src/main.rs index 4733a5ca..8e488609 100644 --- a/packages/editor-app/src-tauri/src/main.rs +++ b/packages/editor-app/src-tauri/src/main.rs @@ -88,6 +88,61 @@ fn read_file_content(path: String) -> Result { .map_err(|e| format!("Failed to read file {}: {}", path, e)) } +#[derive(serde::Serialize)] +struct DirectoryEntry { + name: String, + path: String, + is_dir: bool, +} + +#[tauri::command] +fn list_directory(path: String) -> Result, String> { + use std::fs; + use std::path::Path; + + let dir_path = Path::new(&path); + if !dir_path.exists() { + return Err(format!("Directory does not exist: {}", path)); + } + + if !dir_path.is_dir() { + return Err(format!("Path is not a directory: {}", path)); + } + + let mut entries = Vec::new(); + + match fs::read_dir(dir_path) { + Ok(read_dir) => { + for entry in read_dir { + match entry { + Ok(entry) => { + let entry_path = entry.path(); + if let Some(name) = entry_path.file_name() { + entries.push(DirectoryEntry { + name: name.to_string_lossy().to_string(), + path: entry_path.to_string_lossy().to_string(), + is_dir: entry_path.is_dir(), + }); + } + } + Err(e) => eprintln!("Error reading directory entry: {}", e), + } + } + } + Err(e) => return Err(format!("Failed to read directory: {}", e)), + } + + entries.sort_by(|a, b| { + match (a.is_dir, b.is_dir) { + (true, false) => std::cmp::Ordering::Less, + (false, true) => std::cmp::Ordering::Greater, + _ => a.name.to_lowercase().cmp(&b.name.to_lowercase()), + } + }); + + Ok(entries) +} + fn main() { tauri::Builder::default() .plugin(tauri_plugin_shell::init()) @@ -108,7 +163,8 @@ fn main() { export_binary, open_project_dialog, scan_directory, - read_file_content + read_file_content, + list_directory ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/packages/editor-app/src/App.tsx b/packages/editor-app/src/App.tsx index 4bd334d3..17e27ba5 100644 --- a/packages/editor-app/src/App.tsx +++ b/packages/editor-app/src/App.tsx @@ -6,6 +6,7 @@ import { StartupPage } from './components/StartupPage'; import { SceneHierarchy } from './components/SceneHierarchy'; import { EntityInspector } from './components/EntityInspector'; import { AssetBrowser } from './components/AssetBrowser'; +import { ResizablePanel } from './components/ResizablePanel'; import { TauriAPI } from './api/tauri'; import { TransformComponent } from './example-components/TransformComponent'; import { SpriteComponent } from './example-components/SpriteComponent'; @@ -236,32 +237,60 @@ function App() {
-
- {entityStore && messageHub ? ( - - ) : ( -
Loading...
- )} -
- -
-
-

{t('viewport.title')}

-

{t('viewport.placeholder')}

-
- -
- -
-
- -
- {entityStore && messageHub ? ( - - ) : ( -
Loading...
- )} -
+ + {entityStore && messageHub ? ( + + ) : ( +
Loading...
+ )} +
+ } + rightOrBottom={ + +
+

{t('viewport.title')}

+

{t('viewport.placeholder')}

+
+ + } + rightOrBottom={ +
+ +
+ } + /> + } + rightOrBottom={ +
+ {entityStore && messageHub ? ( + + ) : ( +
Loading...
+ )} +
+ } + /> + } + />
diff --git a/packages/editor-app/src/api/tauri.ts b/packages/editor-app/src/api/tauri.ts index 578bf0ef..13e5b440 100644 --- a/packages/editor-app/src/api/tauri.ts +++ b/packages/editor-app/src/api/tauri.ts @@ -49,6 +49,19 @@ export class TauriAPI { static async readFileContent(path: string): Promise { return await invoke('read_file_content', { path }); } + + /** + * 列出目录内容 + */ + static async listDirectory(path: string): Promise { + return await invoke('list_directory', { path }); + } +} + +export interface DirectoryEntry { + name: string; + path: string; + is_dir: boolean; } /** diff --git a/packages/editor-app/src/components/AssetBrowser.tsx b/packages/editor-app/src/components/AssetBrowser.tsx index 6a79d12d..8af28f95 100644 --- a/packages/editor-app/src/components/AssetBrowser.tsx +++ b/packages/editor-app/src/components/AssetBrowser.tsx @@ -1,5 +1,5 @@ import { useState, useEffect } from 'react'; -import { TauriAPI } from '../api/tauri'; +import { TauriAPI, DirectoryEntry } from '../api/tauri'; import '../styles/AssetBrowser.css'; interface AssetItem { @@ -58,27 +58,20 @@ export function AssetBrowser({ projectPath, locale }: AssetBrowserProps) { const loadAssets = async (path: string) => { setLoading(true); try { - const files = await TauriAPI.scanDirectory(path, '*'); + const entries = await TauriAPI.listDirectory(path); - const assetItems: AssetItem[] = files.map(filePath => { - const name = filePath.split(/[\\/]/).pop() || ''; - const extension = name.includes('.') ? name.split('.').pop() : undefined; + const assetItems: AssetItem[] = entries.map((entry: DirectoryEntry) => { + const extension = entry.is_dir ? undefined : + (entry.name.includes('.') ? entry.name.split('.').pop() : undefined); return { - name, - path: filePath, - type: 'file' as const, + name: entry.name, + path: entry.path, + type: entry.is_dir ? 'folder' as const : 'file' as const, extension }; }); - assetItems.sort((a, b) => { - if (a.type === b.type) { - return a.name.localeCompare(b.name); - } - return a.type === 'folder' ? -1 : 1; - }); - setAssets(assetItems); } catch (error) { console.error('Failed to load assets:', error); diff --git a/packages/editor-app/src/components/ResizablePanel.tsx b/packages/editor-app/src/components/ResizablePanel.tsx new file mode 100644 index 00000000..57f36388 --- /dev/null +++ b/packages/editor-app/src/components/ResizablePanel.tsx @@ -0,0 +1,151 @@ +import { useState, useRef, useEffect, ReactNode } from 'react'; +import '../styles/ResizablePanel.css'; + +interface ResizablePanelProps { + direction: 'horizontal' | 'vertical'; + leftOrTop: ReactNode; + rightOrBottom: ReactNode; + defaultSize?: number; + minSize?: number; + maxSize?: number; + side?: 'left' | 'right' | 'top' | 'bottom'; +} + +export function ResizablePanel({ + direction, + leftOrTop, + rightOrBottom, + defaultSize = 250, + minSize = 150, + maxSize = 600, + side = 'left' +}: ResizablePanelProps) { + const [size, setSize] = useState(defaultSize); + const [isDragging, setIsDragging] = useState(false); + const containerRef = useRef(null); + + useEffect(() => { + if (!isDragging) return; + + const handleMouseMove = (e: MouseEvent) => { + if (!containerRef.current) return; + + const rect = containerRef.current.getBoundingClientRect(); + let newSize: number; + + if (direction === 'horizontal') { + if (side === 'right') { + newSize = rect.right - e.clientX; + } else { + newSize = e.clientX - rect.left; + } + } else { + if (side === 'bottom') { + newSize = rect.bottom - e.clientY; + } else { + newSize = e.clientY - rect.top; + } + } + + newSize = Math.max(minSize, Math.min(maxSize, newSize)); + setSize(newSize); + }; + + const handleMouseUp = () => { + setIsDragging(false); + }; + + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleMouseUp); + + return () => { + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); + }; + }, [isDragging, direction, minSize, maxSize, side]); + + const handleMouseDown = () => { + setIsDragging(true); + }; + + const className = `resizable-panel resizable-panel-${direction}`; + const resizerClassName = `resizer resizer-${direction}`; + + if (direction === 'horizontal') { + if (side === 'right') { + return ( +
+
+ {leftOrTop} +
+
+
+
+
+ {rightOrBottom} +
+
+ ); + } else { + return ( +
+
+ {leftOrTop} +
+
+
+
+
+ {rightOrBottom} +
+
+ ); + } + } else { + if (side === 'bottom') { + return ( +
+
+ {leftOrTop} +
+
+
+
+
+ {rightOrBottom} +
+
+ ); + } else { + return ( +
+
+ {leftOrTop} +
+
+
+
+
+ {rightOrBottom} +
+
+ ); + } + } +} diff --git a/packages/editor-app/src/styles/App.css b/packages/editor-app/src/styles/App.css index a97d32d2..5860fc78 100644 --- a/packages/editor-app/src/styles/App.css +++ b/packages/editor-app/src/styles/App.css @@ -63,17 +63,11 @@ .sidebar-left, .sidebar-right { - width: 250px; + height: 100%; background-color: #252526; - border-right: 1px solid #3e3e3e; overflow: hidden; } -.sidebar-right { - border-right: none; - border-left: 1px solid #3e3e3e; -} - .sidebar-left h3, .sidebar-right h3 { font-size: 14px; @@ -91,16 +85,13 @@ } .main-content { - display: flex; - flex-direction: column; - flex: 1; + height: 100%; overflow: hidden; } .viewport { - flex: 1; + height: 100%; background-color: #1e1e1e; - border-bottom: 1px solid #3e3e3e; padding: 12px; display: flex; flex-direction: column; @@ -113,9 +104,8 @@ } .bottom-panel { - height: 200px; + height: 100%; background-color: #252526; - padding: 12px; overflow-y: auto; } diff --git a/packages/editor-app/src/styles/ResizablePanel.css b/packages/editor-app/src/styles/ResizablePanel.css new file mode 100644 index 00000000..70a03d25 --- /dev/null +++ b/packages/editor-app/src/styles/ResizablePanel.css @@ -0,0 +1,77 @@ +.resizable-panel { + display: flex; + width: 100%; + height: 100%; + position: relative; +} + +.resizable-panel-horizontal { + flex-direction: row; +} + +.resizable-panel-vertical { + flex-direction: column; +} + +.panel-section { + overflow: hidden; + position: relative; +} + +.resizer { + background: #252526; + position: relative; + z-index: 10; + transition: background-color 0.2s ease; +} + +.resizer:hover { + background: #094771; +} + +.resizer-horizontal { + width: 4px; + cursor: col-resize; + flex-shrink: 0; +} + +.resizer-vertical { + height: 4px; + cursor: row-resize; + flex-shrink: 0; +} + +.resizer-handle { + position: absolute; + background: transparent; +} + +.resizer-horizontal .resizer-handle { + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 12px; + height: 40px; +} + +.resizer-vertical .resizer-handle { + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 40px; + height: 12px; +} + +.resizer:active { + background: #0e6caa; +} + +body.resizing { + cursor: col-resize !important; + user-select: none !important; +} + +body.resizing-vertical { + cursor: row-resize !important; + user-select: none !important; +}