fix: .gitignore + add 44 language JSON files
This commit is contained in:
1 parent
e8fd68e27b
commit
8de545dd2c
227 files changed
+22603
No files matched your search
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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))
|
||||
}),
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user