fix: .gitignore + add 44 language JSON files

This commit is contained in:
shuiyu1123 committed 2026-07-14 01:17:33 +08:00
1 parent e8fd68e27b
commit 8de545dd2c
227 files changed
+22603

No files matched your search

+8
View File
@@ -0,0 +1,8 @@
[package]
name = "lity3d-editor"
version = "1.0.0"
edition = "2021"
[dependencies]
eframe = { version = "0.31", features = ["default"] }
rfd = "0.15"
+348
View File
@@ -0,0 +1,348 @@
// lity_editor.c - Lity3D Editor (Win32 Unicode + JSON i18n)
#include <windows.h>
#include <commctrl.h>
#include <commdlg.h>
#include <shellapi.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define IDC_TREE 1001
#define IDC_EDIT 1002
#define IDC_CONSOLE 1003
#define IDM_NEW 2001
#define IDM_OPEN 2002
#define IDM_SAVE 2003
#define IDM_RUN 2004
#define IDM_BUILD 2005
#define IDM_PACKAGE 2006
#define IDM_ABOUT 2007
#define IDM_LANG_BASE 3000
static HWND g_hwnd, g_tree, g_edit, g_console;
static WCHAR g_current_file[MAX_PATH] = L"";
static WCHAR g_engine_dir[MAX_PATH] = L"";
static int g_lang_count = 0;
static WCHAR g_lang_codes[128][32];
static WCHAR g_lang_names[128][128];
static WCHAR g_cur_lang[32] = L"en";
// Translation keys
static WCHAR g_t[256][256]; // up to 256 keys
static int g_t_count = 0;
static WCHAR g_t_keys[256][64];
static WCHAR* T(const char* key) {
for (int i = 0; i < g_t_count; i++) {
char kbuf[64];
WideCharToMultiByte(CP_UTF8, 0, g_t_keys[i], -1, kbuf, sizeof(kbuf), NULL, NULL);
if (strcmp(kbuf, key) == 0) return g_t[i];
}
// fallback to English defaults
static const WCHAR* defs[][2] = {
{L"app_title",L"Lity3D Editor"},{L"file",L"File"},{L"build",L"Build"},
{L"scene",L"Scene"},{L"help",L"Help"},{L"lang",L"Language"},
{L"new",L"New"},{L"open",L"Open"},{L"save",L"Save"},{L"exit",L"Exit"},
{L"run",L"Run"},{L"compile",L"Build Engine"},{L"package",L"Package"},
{L"explorer",L"Explorer"},{L"output",L"Output"},{L"ready",L"Ready"},
{L"about",L"About Lity3D"},
};
for (int i = 0; i < 17; i++) {
char kbuf2[64];
WideCharToMultiByte(CP_UTF8, 0, defs[i][0], -1, kbuf2, sizeof(kbuf2), NULL, NULL);
if (strcmp(kbuf2, key) == 0) return defs[i][1];
}
static WCHAR fb[128];
MultiByteToWideChar(CP_UTF8, 0, key, -1, fb, 128);
return fb;
}
static void parse_json_value(const char* json, const char* key, WCHAR* out, int max) {
char pattern[128];
sprintf(pattern, "\"%s\"", key);
const char* p = strstr(json, pattern);
if (!p) { out[0] = 0; return; }
p = strchr(p + strlen(pattern), ':');
if (!p) { out[0] = 0; return; }
p++; while (*p == ' ') p++;
if (*p == '"') {
p++; const char* end = strchr(p, '"'); if (!end) end = p + strlen(p);
int len = (int)(end - p);
char tmp[512] = {0}; if (len > 511) len = 511;
memcpy(tmp, p, len);
MultiByteToWideChar(CP_UTF8, 0, tmp, -1, out, max);
}
}
static void load_lang(const WCHAR* code) {
WCHAR path[MAX_PATH];
wsprintfW(path, L"%s\\languages\\%s.json", g_engine_dir, code);
HANDLE hf = CreateFileW(path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
if (hf == INVALID_HANDLE_VALUE) {
// try English fallback
wsprintfW(path, L"%s\\languages\\en.json", g_engine_dir);
hf = CreateFileW(path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
if (hf == INVALID_HANDLE_VALUE) return;
}
DWORD sz = GetFileSize(hf, NULL);
char* buf = malloc(sz + 1); DWORD rd; ReadFile(hf, buf, sz, &rd, NULL); buf[sz] = '\0'; CloseHandle(hf);
g_t_count = 0;
const char* keys[] = {"app_title","file","build","scene","help","lang","new","open","save","exit",
"run","compile","package","explorer","output","ready","running","saved","opened",
"build_first","kill","packaged","about"};
for (int i = 0; i < 23; i++) {
MultiByteToWideChar(CP_UTF8, 0, keys[i], -1, g_t_keys[g_t_count], 64);
parse_json_value(buf, keys[i], g_t[g_t_count], 256);
g_t_count++;
}
free(buf);
wcscpy(g_cur_lang, code);
}
static void discover_languages(void) {
g_lang_count = 0;
WCHAR pattern[MAX_PATH];
wsprintfW(pattern, L"%s\\languages\\*.json", g_engine_dir);
WIN32_FIND_DATAW fd;
HANDLE h = FindFirstFileW(pattern, &fd);
if (h == INVALID_HANDLE_VALUE) return;
do {
WCHAR name[MAX_PATH]; wcscpy(name, fd.cFileName);
WCHAR* dot = wcsrchr(name, L'.'); if (dot) *dot = 0;
wcscpy(g_lang_codes[g_lang_count], name);
// Read language name from JSON
WCHAR path[MAX_PATH]; wsprintfW(path, L"%s\\languages\\%s", g_engine_dir, fd.cFileName);
HANDLE hf = CreateFileW(path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
if (hf != INVALID_HANDLE_VALUE) {
DWORD sz = GetFileSize(hf, NULL); char* buf = malloc(sz + 1); DWORD rd;
ReadFile(hf, buf, sz, &rd, NULL); buf[sz] = '\0'; CloseHandle(hf);
WCHAR title[256]; parse_json_value(buf, "app_title", title, 256);
wsprintfW(g_lang_names[g_lang_count], L"%s (%s)", title, name);
free(buf);
}
g_lang_count++;
} while (FindNextFileW(h, &fd));
FindClose(h);
}
static void console_msg(const char* msg) {
int len = GetWindowTextLengthA(g_console);
SendMessageA(g_console, EM_SETSEL, len, len);
SendMessageA(g_console, EM_REPLACESEL, FALSE, (LPARAM)msg);
}
static void populate_tree(void) {
TreeView_DeleteAllItems(g_tree);
TVINSERTSTRUCTW tvi = {0};
tvi.hParent = TVI_ROOT; tvi.hInsertAfter = TVI_LAST;
tvi.item.mask = TVIF_TEXT;
tvi.item.pszText = (WCHAR*)L"lity3d";
HTREEITEM root = TreeView_InsertItem(g_tree, &tvi);
WIN32_FIND_DATAW fd; WCHAR pat[MAX_PATH];
wsprintfW(pat, L"%s\\*", g_engine_dir);
HANDLE hFind = FindFirstFileW(pat, &fd);
if (hFind == INVALID_HANDLE_VALUE) return;
do {
if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
if (!wcscmp(fd.cFileName,L".")||!wcscmp(fd.cFileName,L"..")) continue;
if (!wcscmp(fd.cFileName,L".git")||!wcscmp(fd.cFileName,L"build")||!wcscmp(fd.cFileName,L"target")) continue;
tvi.hParent = root; tvi.item.pszText = fd.cFileName;
HTREEITEM d = TreeView_InsertItem(g_tree, &tvi);
WCHAR sub[MAX_PATH]; wsprintfW(sub, L"%s\\%s\\*", g_engine_dir, fd.cFileName);
WIN32_FIND_DATAW fd2; HANDLE h2 = FindFirstFileW(sub, &fd2);
if (h2 != INVALID_HANDLE_VALUE) {
do { if (fd2.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) continue;
tvi.hParent = d; tvi.item.pszText = fd2.cFileName;
TreeView_InsertItem(g_tree, &tvi);
} while (FindNextFileW(h2, &fd2));
FindClose(h2);
}
} else {
if (wcsstr(fd.cFileName,L".py")||wcsstr(fd.cFileName,L".c")||wcsstr(fd.cFileName,L".h")||
wcsstr(fd.cFileName,L".asm")||wcsstr(fd.cFileName,L".md")||wcsstr(fd.cFileName,L".txt")||
wcsstr(fd.cFileName,L".json")||wcsstr(fd.cFileName,L".toml")||wcsstr(fd.cFileName,L".bat")) {
tvi.hParent = root; tvi.item.pszText = fd.cFileName;
TreeView_InsertItem(g_tree, &tvi);
}
}
} while (FindNextFileW(hFind, &fd));
FindClose(hFind);
}
static void open_file_at(WCHAR* path) {
HANDLE hf = CreateFileW(path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
if (hf == INVALID_HANDLE_VALUE) return;
DWORD sz = GetFileSize(hf, NULL); char* buf = malloc(sz + 1);
DWORD rd; ReadFile(hf, buf, sz, &rd, NULL); buf[sz] = '\0'; CloseHandle(hf);
SetWindowTextA(g_edit, buf); free(buf);
wcscpy(g_current_file, path);
WCHAR title[MAX_PATH]; wsprintfW(title, L"%s - %s", T("app_title"), path);
SetWindowTextW(g_hwnd, title);
}
static void open_file_dialog(void) {
WCHAR file[MAX_PATH] = L""; OPENFILENAMEW of = {sizeof(of)};
of.lpstrFilter = L"All\0*.*\0Python\0*.py\0C\0*.c;*.h\0";
of.lpstrFile = file; of.nMaxFile = MAX_PATH; of.Flags = OFN_FILEMUSTEXIST;
if (GetOpenFileNameW(&of)) open_file_at(file);
}
static void save_file(void) {
if (!g_current_file[0]) { open_file_dialog(); return; }
int len = GetWindowTextLengthA(g_edit); char* buf = malloc(len + 1);
GetWindowTextA(g_edit, buf, len + 1);
HANDLE hf = CreateFileW(g_current_file, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, 0, NULL);
if (hf != INVALID_HANDLE_VALUE) { DWORD wr; WriteFile(hf, buf, len, &wr, NULL); CloseHandle(hf); }
free(buf); console_msg("[OK] Saved.\n");
}
static void run_game(void) {
WCHAR p[MAX_PATH]; wsprintfW(p, L"%s\\build\\lity.exe", g_engine_dir);
if (GetFileAttributesW(p) == INVALID_FILE_ATTRIBUTES) { console_msg("[!] Build engine first\n"); return; }
ShellExecuteW(NULL, L"open", p, NULL, g_engine_dir, SW_SHOWNORMAL);
console_msg("[>] Run game\n");
}
static void build_engine(void) {
WCHAR cmd[MAX_PATH*2];
wsprintfW(cmd, L"/c set PATH=C:\\mingw64\\bin;%%PATH%%&& cd /d %s\\build && cmake --build . -j4", g_engine_dir);
ShellExecuteW(NULL, L"open", L"cmd.exe", cmd, NULL, SW_SHOWNORMAL);
console_msg("[>] Build engine...\n");
}
static void package_exe(void) {
WCHAR src[MAX_PATH], dst[MAX_PATH] = L"";
wsprintfW(src, L"%s\\build\\lity.exe", g_engine_dir);
OPENFILENAMEW of = {sizeof(of)}; of.lpstrFilter = L"EXE\0*.exe\0";
of.lpstrFile = dst; of.nMaxFile = MAX_PATH; of.lpstrTitle = L"Package"; of.Flags = OFN_OVERWRITEPROMPT;
if (GetSaveFileNameW(&of)) { CopyFileW(src, dst, FALSE); console_msg("[OK] Packaged.\n"); }
}
static void rebuild_lang_menu(void) {
HMENU hBar = GetMenu(g_hwnd);
HMENU hLang = CreatePopupMenu();
for (int i = 0; i < g_lang_count; i++) {
UINT flags = (!wcscmp(g_lang_codes[i], g_cur_lang)) ? (MF_STRING|MF_CHECKED) : MF_STRING;
AppendMenuW(hLang, flags, IDM_LANG_BASE + i, g_lang_names[i]);
}
// Replace old Language menu
HMENU hOld = GetSubMenu(hBar, 3); // Language is 4th menu
if (hOld) { RemoveMenu(hBar, 3, MF_BYPOSITION); DestroyMenu(hOld); }
// Insert before Help
InsertMenuW(hBar, 3, MF_BYPOSITION | MF_POPUP, (UINT_PTR)hLang, T("lang"));
DrawMenuBar(g_hwnd);
}
static LRESULT CALLBACK WP(HWND h, UINT m, WPARAM w, LPARAM l) {
switch (m) {
case WM_COMMAND:
if (LOWORD(w) == IDM_NEW) { SetWindowTextA(g_edit, ""); g_current_file[0] = 0; }
else if (LOWORD(w) == IDM_OPEN) open_file_dialog();
else if (LOWORD(w) == IDM_SAVE) save_file();
else if (LOWORD(w) == IDM_RUN) run_game();
else if (LOWORD(w) == IDM_BUILD) build_engine();
else if (LOWORD(w) == IDM_PACKAGE) package_exe();
else if (LOWORD(w) == IDM_ABOUT) MessageBoxW(h, L"Lity3D Engine v1.0\nC + MASM x64\nD3D11 + Python Scripting\n\nhttps://gitee.com/shuiyu1123/lity3d", T("about"), MB_OK|MB_ICONINFORMATION);
else if (LOWORD(w) >= IDM_LANG_BASE && LOWORD(w) < IDM_LANG_BASE + g_lang_count) {
int idx = LOWORD(w) - IDM_LANG_BASE;
load_lang(g_lang_codes[idx]);
rebuild_lang_menu();
SetWindowTextW(g_hwnd, T("app_title"));
populate_tree();
}
break;
case WM_NOTIFY: {
NMHDR* nm = (NMHDR*)l;
if (nm->idFrom == IDC_TREE && nm->code == TVN_SELCHANGED) {
TVITEMW ti = {0}; ti.mask = TVIF_TEXT|TVIF_HANDLE;
ti.hItem = TreeView_GetSelection(g_tree);
WCHAR name[MAX_PATH]; ti.pszText = name; ti.cchTextMax = MAX_PATH;
TreeView_GetItem(g_tree, &ti);
if (!ti.hItem) break;
HTREEITEM par = TreeView_GetParent(g_tree, ti.hItem);
WCHAR full[MAX_PATH];
if (par) { WCHAR parent[MAX_PATH]={0}; TVITEMW pti={0};
pti.mask=TVIF_TEXT; pti.hItem=par; pti.pszText=parent; pti.cchTextMax=MAX_PATH;
TreeView_GetItem(g_tree, &pti);
wsprintfW(full, L"%s\\%s\\%s", g_engine_dir, parent, name);
} else wsprintfW(full, L"%s\\%s", g_engine_dir, name);
DWORD attr = GetFileAttributesW(full);
if (attr != INVALID_FILE_ATTRIBUTES && !(attr & FILE_ATTRIBUTE_DIRECTORY))
open_file_at(full);
}
break; }
case WM_SIZE: {
int w2 = LOWORD(l), h2 = HIWORD(l);
if (w2<100||h2<100) break;
int tw=220, ch=160;
MoveWindow(g_tree,0,0,tw,h2-ch,TRUE);
MoveWindow(g_edit,tw+4,0,w2-tw-4,h2-ch,TRUE);
MoveWindow(g_console,0,h2-ch,w2,ch-24,TRUE);
break; }
case WM_DESTROY: PostQuitMessage(0); return 0;
}
return DefWindowProcW(h, m, w, l);
}
int WINAPI wWinMain(HINSTANCE hI, HINSTANCE hP, LPWSTR nC, int nS) {
(void)hP; (void)nC; (void)nS;
GetModuleFileNameW(NULL, g_engine_dir, MAX_PATH);
WCHAR* sl = wcsrchr(g_engine_dir, L'\\'); if (sl) *sl = 0;
// Editor.exe is in lity3d/Editor/, engine root is one level up
sl = wcsrchr(g_engine_dir, L'\\'); if (sl) *sl = 0;
INITCOMMONCONTROLSEX icc = {sizeof(icc), ICC_TREEVIEW_CLASSES}; InitCommonControlsEx(&icc);
WNDCLASSEXW wc = {sizeof(wc)}; wc.style=CS_HREDRAW|CS_VREDRAW; wc.lpfnWndProc=WP;
wc.hInstance=hI; wc.hbrBackground=(HBRUSH)(COLOR_BTNFACE+1); wc.lpszClassName=L"LityEditor";
wc.hCursor=LoadCursor(NULL,IDC_ARROW); wc.hIcon=LoadIcon(NULL,IDI_APPLICATION);
RegisterClassExW(&wc);
// Discover and load languages
discover_languages();
// Auto-detect system language
WCHAR sysLang[10]; int n = GetLocaleInfoW(LOCALE_USER_DEFAULT, LOCALE_SABBREVLANGNAME, sysLang, 10);
if (n > 0) {
for (int i = 0; i < g_lang_count; i++) {
if (!_wcsicmp(g_lang_codes[i], sysLang)) { load_lang(g_lang_codes[i]); goto lang_done; }
}
}
load_lang(L"en");
lang_done:
g_hwnd = CreateWindowExW(0, L"LityEditor", T("app_title"),
WS_OVERLAPPEDWINDOW|WS_CLIPCHILDREN, 100,100,1400,900, NULL, NULL, hI, NULL);
g_tree = CreateWindowExW(WS_EX_CLIENTEDGE, WC_TREEVIEWW, L"",
WS_CHILD|WS_VISIBLE|TVS_HASLINES|TVS_HASBUTTONS|TVS_LINESATROOT,
0,0,220,600, g_hwnd, (HMENU)IDC_TREE, hI, NULL);
g_edit = CreateWindowExW(WS_EX_CLIENTEDGE, L"EDIT", L"",
WS_CHILD|WS_VISIBLE|WS_VSCROLL|WS_HSCROLL|ES_MULTILINE|ES_AUTOVSCROLL,
224,0,1176,600, g_hwnd, (HMENU)IDC_EDIT, hI, NULL);
SendMessageW(g_edit, WM_SETFONT, (WPARAM)GetStockObject(DEFAULT_GUI_FONT), TRUE);
g_console = CreateWindowExW(WS_EX_CLIENTEDGE, L"EDIT", L"",
WS_CHILD|WS_VISIBLE|WS_VSCROLL|ES_MULTILINE|ES_READONLY,
0,600,1400,136, g_hwnd, (HMENU)IDC_CONSOLE, hI, NULL);
HFONT hf = CreateFontW(14,0,0,0,FW_NORMAL,0,0,0,DEFAULT_CHARSET,0,0,0,FIXED_PITCH,L"Consolas");
SendMessageW(g_console, WM_SETFONT, (WPARAM)hf, TRUE);
// Build menu
HMENU hBar = CreateMenu();
HMENU hFile = CreatePopupMenu();
AppendMenuW(hFile,MF_STRING,IDM_NEW,T("new")); AppendMenuW(hFile,MF_STRING,IDM_OPEN,T("open"));
AppendMenuW(hFile,MF_STRING,IDM_SAVE,T("save")); AppendMenuW(hFile,MF_SEPARATOR,0,NULL);
AppendMenuW(hFile,MF_STRING,IDM_ABOUT,T("about"));
AppendMenuW(hBar,MF_POPUP,(UINT_PTR)hFile,T("file"));
HMENU hBuild = CreatePopupMenu();
AppendMenuW(hBuild,MF_STRING,IDM_RUN,T("run")); AppendMenuW(hBuild,MF_SEPARATOR,0,NULL);
AppendMenuW(hBuild,MF_STRING,IDM_BUILD,T("compile")); AppendMenuW(hBuild,MF_STRING,IDM_PACKAGE,T("package"));
AppendMenuW(hBar,MF_POPUP,(UINT_PTR)hBuild,T("build"));
HMENU hLang = CreatePopupMenu();
for (int i=0;i<g_lang_count;i++) {
UINT flags=(!wcscmp(g_lang_codes[i],g_cur_lang))?(MF_STRING|MF_CHECKED):MF_STRING;
AppendMenuW(hLang,flags,IDM_LANG_BASE+i,g_lang_names[i]);
}
AppendMenuW(hBar,MF_POPUP,(UINT_PTR)hLang,T("lang"));
HMENU hHelp=CreatePopupMenu(); AppendMenuW(hHelp,MF_STRING,IDM_ABOUT,T("about"));
AppendMenuW(hBar,MF_POPUP,(UINT_PTR)hHelp,T("help"));
SetMenu(g_hwnd,hBar);
populate_tree();
console_msg("Lity3D Editor ready.\n");
ShowWindow(g_hwnd,SW_SHOW);
MSG msg;
while(GetMessageW(&msg,NULL,0,0)){TranslateMessage(&msg);DispatchMessageW(&msg);}
return (int)msg.wParam;
}
+464
View File
@@ -0,0 +1,464 @@
use eframe::egui;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::sync::{Arc, Mutex};
use std::thread;
const SUPPORTED_LANGS: &[(&str, &str)] = &[
("zh", "中文"), ("en", "English"), ("ja", "日本語"), ("ko", "한국어"),
("es", "Español"), ("fr", "Français"), ("de", "Deutsch"), ("ru", "Русский"),
("pt", "Português"), ("it", "Italiano"), ("tr", "Türkçe"),
];
struct LityEditor {
engine_dir: PathBuf,
code: String,
file_path: Option<PathBuf>,
file_tree: Vec<FileNode>,
tree_expanded: std::collections::HashSet<String>,
console: String,
current_lang: String,
process_running: bool,
process_output: Arc<Mutex<Vec<String>>>,
needs_refresh: bool,
show_about: bool,
}
#[derive(Clone)]
struct FileNode {
name: String,
path: PathBuf,
is_dir: bool,
children: Vec<FileNode>,
}
impl LityEditor {
fn new(cc: &eframe::CreationContext) -> Self {
let mut style = (*cc.egui_ctx.style()).clone();
style.spacing.item_spacing = egui::vec2(4.0, 4.0);
cc.egui_ctx.set_style(style);
let engine_dir = PathBuf::from(std::env::var("ENGINE_DIR")
.unwrap_or_else(|_| {
let exe = std::env::current_exe().unwrap_or_default();
exe.parent().unwrap_or(Path::new(".")).to_path_buf()
}));
let mut editor = Self {
engine_dir,
code: String::new(),
file_path: None,
file_tree: Vec::new(),
tree_expanded: std::collections::HashSet::new(),
console: String::new(),
current_lang: "zh".to_string(),
process_running: false,
process_output: Arc::new(Mutex::new(Vec::new())),
needs_refresh: true,
show_about: false,
};
editor.refresh_tree();
editor
}
fn t(&self, key: &str) -> String {
let lang = &self.current_lang;
match (lang.as_str(), key) {
("zh", k) => match k {
"app_title" => "Lity3D 编辑器".into(),
"menu_file" => "文件".into(),
"menu_edit" => "编辑".into(),
"menu_build" => "构建".into(),
"menu_scene" => "场景".into(),
"menu_help" => "帮助".into(),
"menu_view" => "视图".into(),
"menu_lang" => "语言".into(),
"new" => "新建".into(),
"open" => "打开".into(),
"save" => "保存".into(),
"run" => "运行".into(),
"stop" => "停止".into(),
"build" => "编译".into(),
"package" => "打包".into(),
"explorer" => "资源管理器".into(),
"output" => "输出".into(),
"ready" => "就绪".into(),
"running" => "运行中...".into(),
"about" => "关于".into(),
"save_ok" => "已保存".into(),
"open_ok" => "已打开".into(),
"build_first" => "请先编译引擎".into(),
_ => k.into(),
},
("en", k) => match k {
"app_title" => "Lity3D Editor".into(),
"menu_file" => "File".into(),
"menu_edit" => "Edit".into(),
"menu_build" => "Build".into(),
"menu_scene" => "Scene".into(),
"menu_help" => "Help".into(),
"menu_view" => "View".into(),
"menu_lang" => "Language".into(),
"new" => "New".into(),
"open" => "Open".into(),
"save" => "Save".into(),
"run" => "Run".into(),
"stop" => "Stop".into(),
"build" => "Build".into(),
"package" => "Package".into(),
"explorer" => "Explorer".into(),
"output" => "Output".into(),
"ready" => "Ready".into(),
"running" => "Running...".into(),
"about" => "About".into(),
"save_ok" => "Saved".into(),
"open_ok" => "Opened".into(),
"build_first" => "Build engine first".into(),
_ => k.into(),
},
_ => {
// Fallback to English
match key {
"app_title" => "Lity3D Editor".into(),
_ => key.into(),
}
}
}
}
fn refresh_tree(&mut self) {
self.file_tree.clear();
self.file_tree = Self::build_tree(&self.engine_dir, 0);
self.needs_refresh = false;
}
fn build_tree(dir: &Path, depth: usize) -> Vec<FileNode> {
if depth > 3 { return Vec::new(); }
let skip = [".git", "build", "__pycache__", "target", ".vs", ".vscode", "obj"];
let mut nodes = Vec::new();
if let Ok(entries) = fs::read_dir(dir) {
let mut entries: Vec<_> = entries.filter_map(|e| e.ok()).collect();
entries.sort_by(|a, b| a.file_name().cmp(&b.file_name()));
for entry in entries {
let name = entry.file_name().to_string_lossy().to_string();
if name.starts_with('.') || skip.contains(&name.as_str()) { continue; }
let path = entry.path();
if path.is_dir() {
let children = Self::build_tree(&path, depth + 1);
nodes.push(FileNode { name, path, is_dir: true, children });
} else if name.ends_with(".py") || name.ends_with(".c") || name.ends_with(".h")
|| name.ends_with(".asm") || name.ends_with(".txt") || name.ends_with(".json")
|| name.ends_with(".md") || name.ends_with(".toml") || name.ends_with(".bat") {
nodes.push(FileNode { name, path, is_dir: false, children: Vec::new() });
}
}
}
nodes
}
fn open_file(&mut self, path: PathBuf) {
if let Ok(content) = fs::read_to_string(&path) {
self.code = content;
self.file_path = Some(path);
}
}
fn save_file(&mut self) {
if let Some(ref path) = self.file_path {
if fs::write(path, &self.code).is_ok() {
self.console.push_str(&format!("[OK] {}\n", self.t("save_ok")));
}
}
}
fn run_process(&mut self, cmd: Vec<String>) {
self.process_running = true;
self.console.push_str(&format!("[>] {}\n", cmd.join(" ")));
let output = self.process_output.clone();
let cmd_clone = cmd.clone();
thread::spawn(move || {
let result = Command::new(&cmd_clone[0])
.args(&cmd_clone[1..])
.current_dir(std::env::current_dir().unwrap_or_default())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output();
match result {
Ok(out) => {
let mut buf = output.lock().unwrap();
if let Ok(s) = String::from_utf8(out.stdout) { buf.push(s); }
if let Ok(s) = String::from_utf8(out.stderr) { buf.push(s); }
buf.push(format!("[exit: {}]\n", out.status.code().unwrap_or(-1)));
}
Err(e) => {
output.lock().unwrap().push(format!("[ERROR] {}\n", e));
}
}
});
}
fn build(&mut self) {
let build_dir = self.engine_dir.join("build");
let _ = fs::create_dir_all(&build_dir);
self.run_process(vec![
"cmake".into(), "--build".into(),
build_dir.to_string_lossy().to_string(),
]);
}
fn run_game(&mut self) {
let exe = self.engine_dir.join("build").join("lity.exe");
if exe.exists() {
self.run_process(vec![exe.to_string_lossy().to_string()]);
} else {
self.console.push_str(&format!("[!] {}\n", self.t("build_first")));
}
}
fn ui_tree_node(ui: &mut egui::Ui, nodes: &[FileNode], expanded: &mut std::collections::HashSet<String>, selected: &mut Option<PathBuf>) {
for node in nodes {
let id = egui::Id::new(&node.path);
if node.is_dir {
let header = ui.horizontal(|ui| {
let icon = if expanded.contains(&node.name) { "▼" } else { "▶" };
if ui.selectable_label(false, format!("{} 📁 {}", icon, node.name)).clicked() {
if expanded.contains(&node.name) {
expanded.remove(&node.name);
} else {
expanded.insert(node.name.clone());
}
}
});
if expanded.contains(&node.name) {
ui.indent(id, |ui| {
Self::ui_tree_node(ui, &node.children, expanded, selected);
});
}
} else {
let ext = node.name.rsplit('.').next().unwrap_or("");
let icon = match ext {
"py" => "🐍", "c" | "h" => "⚙", "asm" => "🔧",
"md" => "📝", "json" => "📋", _ => "📄",
};
if ui.selectable_label(false, format!("{} {}", icon, node.name)).clicked() {
*selected = Some(node.path.clone());
}
}
}
}
}
impl eframe::App for LityEditor {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
// Collect process output
{
let mut buf = self.process_output.lock().unwrap();
if !buf.is_empty() {
for line in buf.drain(..) {
self.console.push_str(&line);
if !line.ends_with('\n') { self.console.push('\n'); }
}
}
}
egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| {
egui::menu::bar(ui, |ui| {
ui.menu_button(self.t("menu_file"), |ui| {
if ui.button(self.t("new")).clicked() {
self.code.clear();
self.file_path = None;
ui.close_menu();
}
if ui.button(self.t("open")).clicked() {
if let Some(path) = rfd::FileDialog::new()
.set_title(&self.t("open"))
.add_filter("Code", &["py", "c", "h", "asm", "txt", "json"])
.pick_file() {
self.open_file(path);
}
ui.close_menu();
}
if ui.button(self.t("save")).clicked() {
self.save_file();
ui.close_menu();
}
ui.separator();
if ui.button("Exit").clicked() {
std::process::exit(0);
}
});
ui.menu_button(self.t("menu_build"), |ui| {
if ui.button(format!("▶ {}", self.t("run"))).clicked() {
self.run_game();
ui.close_menu();
}
if ui.button(format!("⏹ {}", self.t("stop"))).clicked() {
self.process_running = false;
ui.close_menu();
}
ui.separator();
if ui.button(format!("🔨 {}", self.t("build"))).clicked() {
self.build();
ui.close_menu();
}
if ui.button(format!("📦 {}", self.t("package"))).clicked() {
if let Some(path) = rfd::FileDialog::new()
.set_title(&self.t("package"))
.add_filter("EXE", &["exe"])
.save_file() {
let exe = self.engine_dir.join("build").join("lity.exe");
if exe.exists() {
let _ = fs::copy(&exe, &path);
self.console.push_str(&format!("[OK] Packaged to {}\n", path.display()));
} else {
self.console.push_str(&format!("[!] {}\n", self.t("build_first")));
}
}
ui.close_menu();
}
});
ui.menu_button(self.t("menu_lang"), |ui| {
for (code, name) in SUPPORTED_LANGS {
if ui.selectable_label(self.current_lang == *code, *name).clicked() {
self.current_lang = code.to_string();
ui.close_menu();
}
}
});
ui.menu_button(self.t("menu_help"), |ui| {
if ui.button(self.t("about")).clicked() {
self.show_about = true;
ui.close_menu();
}
});
});
});
egui::TopBottomPanel::bottom("toolbar").show(ctx, |ui| {
ui.horizontal(|ui| {
if ui.button("📂 Open").on_hover_text("Ctrl+O").clicked() {
if let Some(path) = rfd::FileDialog::new().pick_file() {
self.open_file(path);
}
}
if ui.button("💾 Save").on_hover_text("Ctrl+S").clicked() {
self.save_file();
}
ui.separator();
let run_text = if self.process_running { "⏹ Stop" } else { "▶ Run (F5)" };
if ui.button(run_text).clicked() {
if self.process_running {
self.process_running = false;
self.console.push_str("[!] Stopped\n");
} else {
self.run_game();
}
}
if ui.button("🔨 Build (F7)").clicked() {
self.build();
}
ui.separator();
if ui.button("📁 Refresh").clicked() {
self.needs_refresh = true;
}
if let Some(ref p) = self.file_path {
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
ui.label(egui::RichText::new(p.file_name().unwrap_or_default().to_string_lossy().to_string())
.color(egui::Color32::GRAY).small());
});
}
});
});
egui::SidePanel::left("explorer").resizable(true).default_width(220.0).show(ctx, |ui| {
ui.heading(self.t("explorer"));
ui.separator();
if self.needs_refresh { self.refresh_tree(); }
egui::ScrollArea::vertical().show(ui, |ui| {
let mut selected = None;
Self::ui_tree_node(ui, &self.file_tree, &mut self.tree_expanded, &mut selected);
if let Some(path) = selected {
self.open_file(path);
}
});
});
egui::TopBottomPanel::bottom("console").resizable(true).default_height(150.0).show(ctx, |ui| {
ui.heading(self.t("output"));
ui.separator();
egui::ScrollArea::vertical().stick_to_bottom(true).show(ui, |ui| {
ui.add(egui::TextEdit::multiline(&mut self.console.as_str())
.desired_width(f32::INFINITY)
.font(egui::TextStyle::Monospace));
});
});
egui::CentralPanel::default().show(ctx, |ui| {
ui.horizontal(|ui| {
let title = self.file_path.as_ref()
.map(|p| p.file_name().unwrap_or_default().to_string_lossy().to_string())
.unwrap_or_else(|| "untitled".into());
ui.heading(title);
if self.file_path.is_some() {
ui.label(egui::RichText::new("●").color(egui::Color32::from_rgb(255, 200, 0)));
}
});
ui.separator();
egui::ScrollArea::both().show(ui, |ui| {
let output = ui.add(egui::TextEdit::multiline(&mut self.code)
.desired_width(f32::INFINITY)
.desired_rows(40)
.font(egui::TextStyle::Monospace)
.code_editor());
});
});
if self.show_about {
egui::Window::new(self.t("about")).collapsible(false).resizable(false).show(ctx, |ui| {
ui.label("Lity3D Engine v1.0");
ui.label("C + MASM x64 Game Engine");
ui.label("D3D11 Rendering + Python Scripting");
ui.label("11 Languages Supported");
ui.separator();
ui.label("https://gitee.com/shuiyu1123/lity3d");
if ui.button("OK").clicked() {
self.show_about = false;
}
});
}
// Request repaint when process is running
if self.process_running {
ctx.request_repaint_after(std::time::Duration::from_millis(100));
}
}
}
fn main() -> eframe::Result<()> {
let engine_dir = std::env::current_exe()
.ok()
.and_then(|p| p.parent().map(|p| p.to_path_buf()))
.unwrap_or_else(|| PathBuf::from("."));
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_inner_size([1400.0, 900.0])
.with_title("Lity3D Editor"),
..Default::default()
};
eframe::run_native(
"Lity3D Editor",
options,
Box::new(move |cc| {
let mut editor = LityEditor::new(cc);
editor.engine_dir = engine_dir;
editor.refresh_tree();
Ok(Box::new(editor))
}),
)
}