Files
esengine/packages/behavior-tree-editor/src/components/toolbar/EditorToolbar.tsx
YHH 3fb6f919f8 Feature/tilemap editor (#237)
* feat: 添加 Tilemap 编辑器插件和组件生命周期支持

* feat(editor-core): 添加声明式插件注册 API

* feat(editor-core): 改进tiledmap结构合并tileset进tiledmapeditor

* feat: 添加 editor-runtime SDK 和插件系统改进

* fix(ci): 修复SceneResourceManager里变量未使用问题
2025-11-25 22:23:19 +08:00

480 lines
19 KiB
TypeScript

import { React, Icons } from '@esengine/editor-runtime';
const { Play, Pause, Square, SkipForward, Undo, Redo, ZoomIn, Save, FolderOpen, Download, Clipboard, Home } = Icons;
type ExecutionMode = 'idle' | 'running' | 'paused';
interface EditorToolbarProps {
executionMode: ExecutionMode;
canUndo: boolean;
canRedo: boolean;
hasUnsavedChanges?: boolean;
onPlay: () => void;
onPause: () => void;
onStop: () => void;
onStep: () => void;
onReset: () => void;
onUndo: () => void;
onRedo: () => void;
onResetView: () => void;
onSave?: () => void;
onOpen?: () => void;
onExport?: () => void;
onCopyToClipboard?: () => void;
onGoToRoot?: () => void;
}
export const EditorToolbar: React.FC<EditorToolbarProps> = ({
executionMode,
canUndo,
canRedo,
hasUnsavedChanges = false,
onPlay,
onPause,
onStop,
onStep,
onReset,
onUndo,
onRedo,
onResetView,
onSave,
onOpen,
onExport,
onCopyToClipboard,
onGoToRoot
}) => {
return (
<div style={{
position: 'absolute',
top: '12px',
left: '50%',
transform: 'translateX(-50%)',
display: 'flex',
gap: '6px',
backgroundColor: '#2a2a2a',
padding: '6px',
borderRadius: '8px',
boxShadow: '0 4px 12px rgba(0,0,0,0.5)',
border: '1px solid #3f3f3f',
zIndex: 100
}}>
{/* 文件操作组 */}
<div style={{
display: 'flex',
gap: '4px',
padding: '2px',
backgroundColor: '#1e1e1e',
borderRadius: '6px'
}}>
{onOpen && (
<button
onClick={onOpen}
style={{
padding: '6px 8px',
backgroundColor: '#3c3c3c',
border: 'none',
borderRadius: '4px',
color: '#ccc',
cursor: 'pointer',
fontSize: '13px',
display: 'flex',
alignItems: 'center',
transition: 'all 0.15s'
}}
title="打开文件 (Ctrl+O)"
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#4a4a4a'}
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = '#3c3c3c'}
>
<FolderOpen size={14} />
</button>
)}
{onSave && (
<button
onClick={onSave}
style={{
padding: '6px 8px',
backgroundColor: hasUnsavedChanges ? '#2563eb' : '#3c3c3c',
border: 'none',
borderRadius: '4px',
color: hasUnsavedChanges ? '#fff' : '#ccc',
cursor: 'pointer',
fontSize: '13px',
display: 'flex',
alignItems: 'center',
transition: 'all 0.15s'
}}
title={`保存 (Ctrl+S)${hasUnsavedChanges ? ' - 有未保存的更改' : ''}`}
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = hasUnsavedChanges ? '#1d4ed8' : '#4a4a4a'}
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = hasUnsavedChanges ? '#2563eb' : '#3c3c3c'}
>
<Save size={14} />
</button>
)}
{onExport && (
<button
onClick={onExport}
style={{
padding: '6px 8px',
backgroundColor: '#3c3c3c',
border: 'none',
borderRadius: '4px',
color: '#ccc',
cursor: 'pointer',
fontSize: '13px',
display: 'flex',
alignItems: 'center',
transition: 'all 0.15s'
}}
title="导出运行时配置"
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#4a4a4a'}
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = '#3c3c3c'}
>
<Download size={14} />
</button>
)}
{onCopyToClipboard && (
<button
onClick={onCopyToClipboard}
style={{
padding: '6px 8px',
backgroundColor: '#3c3c3c',
border: 'none',
borderRadius: '4px',
color: '#ccc',
cursor: 'pointer',
fontSize: '13px',
display: 'flex',
alignItems: 'center',
transition: 'all 0.15s'
}}
title="复制JSON到剪贴板"
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#4a4a4a'}
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = '#3c3c3c'}
>
<Clipboard size={14} />
</button>
)}
</div>
{/* 分隔符 */}
<div style={{
width: '1px',
backgroundColor: '#444',
margin: '2px 0'
}} />
{/* 执行控制组 */}
<div style={{
display: 'flex',
gap: '4px',
padding: '2px',
backgroundColor: '#1e1e1e',
borderRadius: '6px'
}}>
{/* 播放按钮 */}
<button
onClick={onPlay}
disabled={executionMode === 'running'}
style={{
padding: '6px 10px',
backgroundColor: executionMode === 'running' ? '#2a2a2a' : '#16a34a',
border: 'none',
borderRadius: '4px',
color: executionMode === 'running' ? '#666' : '#fff',
cursor: executionMode === 'running' ? 'not-allowed' : 'pointer',
fontSize: '13px',
fontWeight: 500,
display: 'flex',
alignItems: 'center',
gap: '4px',
transition: 'all 0.15s'
}}
title="运行 (Play)"
onMouseEnter={(e) => {
if (executionMode !== 'running') {
e.currentTarget.style.backgroundColor = '#15803d';
}
}}
onMouseLeave={(e) => {
if (executionMode !== 'running') {
e.currentTarget.style.backgroundColor = '#16a34a';
}
}}
>
<Play size={14} fill="currentColor" />
</button>
{/* 暂停按钮 */}
<button
onClick={onPause}
disabled={executionMode === 'idle'}
style={{
padding: '6px 10px',
backgroundColor: executionMode === 'idle' ? '#2a2a2a' : '#f59e0b',
border: 'none',
borderRadius: '4px',
color: executionMode === 'idle' ? '#666' : '#fff',
cursor: executionMode === 'idle' ? 'not-allowed' : 'pointer',
fontSize: '13px',
display: 'flex',
alignItems: 'center',
transition: 'all 0.15s'
}}
title={executionMode === 'paused' ? '继续' : '暂停'}
onMouseEnter={(e) => {
if (executionMode !== 'idle') {
e.currentTarget.style.backgroundColor = '#d97706';
}
}}
onMouseLeave={(e) => {
if (executionMode !== 'idle') {
e.currentTarget.style.backgroundColor = '#f59e0b';
}
}}
>
{executionMode === 'paused' ? <Play size={14} fill="currentColor" /> : <Pause size={14} fill="currentColor" />}
</button>
{/* 停止按钮 */}
<button
onClick={onStop}
disabled={executionMode === 'idle'}
style={{
padding: '6px 10px',
backgroundColor: executionMode === 'idle' ? '#2a2a2a' : '#dc2626',
border: 'none',
borderRadius: '4px',
color: executionMode === 'idle' ? '#666' : '#fff',
cursor: executionMode === 'idle' ? 'not-allowed' : 'pointer',
fontSize: '13px',
display: 'flex',
alignItems: 'center',
transition: 'all 0.15s'
}}
title="停止"
onMouseEnter={(e) => {
if (executionMode !== 'idle') {
e.currentTarget.style.backgroundColor = '#b91c1c';
}
}}
onMouseLeave={(e) => {
if (executionMode !== 'idle') {
e.currentTarget.style.backgroundColor = '#dc2626';
}
}}
>
<Square size={14} fill="currentColor" />
</button>
{/* 单步执行按钮 */}
<button
onClick={onStep}
disabled={executionMode !== 'idle' && executionMode !== 'paused'}
style={{
padding: '6px 10px',
backgroundColor: (executionMode !== 'idle' && executionMode !== 'paused') ? '#2a2a2a' : '#3b82f6',
border: 'none',
borderRadius: '4px',
color: (executionMode !== 'idle' && executionMode !== 'paused') ? '#666' : '#fff',
cursor: (executionMode !== 'idle' && executionMode !== 'paused') ? 'not-allowed' : 'pointer',
fontSize: '13px',
display: 'flex',
alignItems: 'center',
transition: 'all 0.15s'
}}
title="单步执行"
onMouseEnter={(e) => {
if (executionMode === 'idle' || executionMode === 'paused') {
e.currentTarget.style.backgroundColor = '#2563eb';
}
}}
onMouseLeave={(e) => {
if (executionMode === 'idle' || executionMode === 'paused') {
e.currentTarget.style.backgroundColor = '#3b82f6';
}
}}
>
<SkipForward size={14} />
</button>
</div>
{/* 分隔符 */}
<div style={{
width: '1px',
backgroundColor: '#444',
margin: '2px 0'
}} />
{/* 视图控制 */}
<button
onClick={onResetView}
style={{
padding: '6px 10px',
backgroundColor: '#3c3c3c',
border: 'none',
borderRadius: '4px',
color: '#ccc',
cursor: 'pointer',
fontSize: '11px',
display: 'flex',
alignItems: 'center',
gap: '4px',
transition: 'all 0.15s'
}}
title="重置视图 (滚轮缩放, Alt+拖动平移)"
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#4a4a4a'}
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = '#3c3c3c'}
>
<ZoomIn size={13} />
<span>Reset View</span>
</button>
{/* 分隔符 */}
<div style={{
width: '1px',
backgroundColor: '#444',
margin: '2px 0'
}} />
{/* 历史控制组 */}
<div style={{
display: 'flex',
gap: '4px',
padding: '2px',
backgroundColor: '#1e1e1e',
borderRadius: '6px'
}}>
<button
onClick={onUndo}
disabled={!canUndo}
style={{
padding: '6px 8px',
backgroundColor: canUndo ? '#3c3c3c' : '#2a2a2a',
border: 'none',
borderRadius: '4px',
color: canUndo ? '#ccc' : '#666',
cursor: canUndo ? 'pointer' : 'not-allowed',
fontSize: '13px',
display: 'flex',
alignItems: 'center',
transition: 'all 0.15s'
}}
title="撤销 (Ctrl+Z)"
onMouseEnter={(e) => {
if (canUndo) {
e.currentTarget.style.backgroundColor = '#4a4a4a';
}
}}
onMouseLeave={(e) => {
if (canUndo) {
e.currentTarget.style.backgroundColor = '#3c3c3c';
}
}}
>
<Undo size={14} />
</button>
<button
onClick={onRedo}
disabled={!canRedo}
style={{
padding: '6px 8px',
backgroundColor: canRedo ? '#3c3c3c' : '#2a2a2a',
border: 'none',
borderRadius: '4px',
color: canRedo ? '#ccc' : '#666',
cursor: canRedo ? 'pointer' : 'not-allowed',
fontSize: '13px',
display: 'flex',
alignItems: 'center',
transition: 'all 0.15s'
}}
title="重做 (Ctrl+Shift+Z / Ctrl+Y)"
onMouseEnter={(e) => {
if (canRedo) {
e.currentTarget.style.backgroundColor = '#4a4a4a';
}
}}
onMouseLeave={(e) => {
if (canRedo) {
e.currentTarget.style.backgroundColor = '#3c3c3c';
}
}}
>
<Redo size={14} />
</button>
</div>
{/* 状态指示器 */}
<div style={{
padding: '6px 12px',
backgroundColor: '#1e1e1e',
borderRadius: '6px',
fontSize: '11px',
color: '#999',
display: 'flex',
alignItems: 'center',
gap: '6px',
fontWeight: 500,
minWidth: '70px'
}}>
<span style={{
width: '6px',
height: '6px',
borderRadius: '50%',
backgroundColor:
executionMode === 'running' ? '#16a34a' :
executionMode === 'paused' ? '#f59e0b' : '#666',
boxShadow: executionMode !== 'idle' ? `0 0 8px ${
executionMode === 'running' ? '#16a34a' :
executionMode === 'paused' ? '#f59e0b' : 'transparent'
}` : 'none',
transition: 'all 0.2s'
}} />
<span style={{
color: executionMode === 'running' ? '#16a34a' :
executionMode === 'paused' ? '#f59e0b' : '#888'
}}>
{executionMode === 'idle' ? 'Idle' :
executionMode === 'running' ? 'Running' : 'Paused'}
</span>
</div>
{onGoToRoot && (
<>
<div style={{
width: '1px',
backgroundColor: '#444',
margin: '2px 0'
}} />
<button
onClick={onGoToRoot}
style={{
padding: '6px 10px',
backgroundColor: '#3c3c3c',
border: 'none',
borderRadius: '4px',
color: '#ccc',
cursor: 'pointer',
fontSize: '11px',
display: 'flex',
alignItems: 'center',
gap: '4px',
transition: 'all 0.15s'
}}
title="回到根节点"
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#4a4a4a'}
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = '#3c3c3c'}
>
<Home size={13} />
<span>Root</span>
</button>
</>
)}
</div>
);
};