diff options
| author | KonAki <konaki@anokonaki.org> | 2026-10-18 04:04:59 +0800 |
|---|---|---|
| committer | KonAki <konaki@anokonaki.org> | 2026-10-18 04:04:59 +0800 |
| commit | e5d3af2e9e6521727e2a6560bd66d2b5174a4ff9 (patch) | |
| tree | 4c6fe26fe2e974a1a819c2038e955fd4c1580d80 | |
| parent | 2e3f2eb4a4c2e65b6a2097658a75924e66302061 (diff) | |
konedit: menu bar, project files, tabs, new and open project
| -rw-r--r-- | editor/konedit.c | 647 |
1 files changed, 647 insertions, 0 deletions
diff --git a/editor/konedit.c b/editor/konedit.c new file mode 100644 index 0000000..ff4020b --- /dev/null +++ b/editor/konedit.c @@ -0,0 +1,647 @@ +/* + * konedit: the editor for konengine projects. it reads and writes the plain files the engine loads, and a project.toml in the + * project folder + * + * build: ./build.sh -d konedit + * run: obj/konedit [project folder] + * + * File > New Project or Open Project to start. the left side is the files of the project, the tabs are the editors + */ + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <stdarg.h> + +#include "konprogram.h" +#include "konui.h" +#include "kontoml.h" +#include "konfs.h" + +#if !defined(KON_MODULE_UI) || !defined(KON_MODULE_FONT) || !defined(KON_MODULE_TOML) || !defined(KON_MODULE_FS) +#error "build with: ./build.sh -d konedit" +#endif + +#define PATH 512 +#define SIDE_WIDTH 260 +#define TABS 5 + +/*** colors ***/ + +#define COL_BACKGROUND 0xFF1B1826u +#define COL_PANEL 0xFF26223Au +#define COL_TITLE 0xFF332E4Eu +#define COL_WIDGET 0xFF3A3558u +#define COL_HOVER 0xFF48426Au +#define COL_BORDER 0xFF4A4468u +#define COL_TEXT 0xFFE4DDF0u +#define COL_DIM 0xFF9A93B3u + +/*** definition ***/ + +typedef struct browser { + char path[PATH]; + char root[PATH]; + kon_fsList_t list; + char selected[256]; + int changed; +} browser_t; + +typedef struct project { + int open; + int dirty; + char dir[PATH]; + char name[64]; + char assets[64]; +} project_t; + +typedef enum dialog { + DIALOG_NONE, + DIALOG_OPEN, + DIALOG_NEW +} dialog_t; + +enum { + BROWSER_NONE, + BROWSER_DIR, + BROWSER_FILE +}; + +typedef struct state { + kon_ui_t *ui; + project_t project; + browser_t side, pick; + kon_uiArea_t sideArea, mainArea, statusArea, dialogTop, dialogList; + dialog_t dialog; + char pathInput[PATH]; + char newName[64]; + const char *startDir; + int tab; + int ctrl; + char status[200]; +} state_t; + +static const char *tabNames[TABS] = { "Project", "Assets", "Tilemap", "Script", "Level" }; + +static const char *tabAbout[TABS] = { + "", + "", + "the tilemap editor: paint tiles on layers, pick tiles from a sheet, save the map the engine loads", + "the dialogue and script editor: write konscript with a text box preview next to it", + "the sector level editor: draw rooms in 2d, see them in 3d, save a kmap" +}; + +/*** helpers ***/ + +static void setStatus(state_t *state, const char *format, ...) +{ + va_list args; + + va_start(args, format); + vsnprintf(state->status, sizeof(state->status), format, args); + va_end(args); +} + +static void copy(char *to, size_t size, const char *from) +{ + snprintf(to, size, "%s", from); +} + +static void slashes(char *path) +{ + for (; *path; path++) { + if (*path == '\\') *path = '/'; + } +} + +/* the end of a long path, so the part that matters fits */ +static const char *tail(const char *path, size_t keep) +{ + size_t n = strlen(path); + + return n > keep ? path + n - keep : path; +} + +static const char *kindOf(const char *name) +{ + static const struct { const char *ext, *kind; } kinds[] = { + { "png", "image" }, { "jpg", "image" }, { "bmp", "image" }, { "tga", "image" }, + { "wav", "sound" }, { "ogg", "sound" }, { "mp3", "sound" }, { "flac", "sound" }, + { "ttf", "font" }, { "glb", "model" }, { "gltf", "model" }, + { "kmap", "sector map" }, { "toml", "settings" }, { "kpk", "pack" }, { "txt", "text" } + }; + const char *dot = strrchr(name, '.'); + size_t i; + + if (!dot) return "file"; + for (i = 0; i < sizeof(kinds) / sizeof(kinds[0]); i++) { + const char *a = dot + 1, *b = kinds[i].ext; + + while (*a && *b && (*a | 32) == *b) { + a++; + b++; + } + if (!*a && !*b) return kinds[i].kind; + } + return "file"; +} + +/*** browser ***/ + +static int browserGo(browser_t *b, const char *path) +{ + kon_fsList_t list; + + if (!kon_fsRead(path, &list)) { + kon_fsFree(&list); + return 0; + } + kon_fsFree(&b->list); + b->list = list; + copy(b->path, sizeof(b->path), path); + b->selected[0] = 0; + b->changed = 1; + return 1; +} + +static void browserFree(browser_t *b) +{ + kon_fsFree(&b->list); + memset(b, 0, sizeof(*b)); +} + +/* draws the way up and the entries of the folder. say what happened with BROWSER_DIR when the folder is another one now, so the + scrolling can start over */ +static int browserDraw(state_t *state, browser_t *b) +{ + kon_ui_t *ui = state->ui; + int result = BROWSER_NONE; + size_t i; + + if (b->root[0] == 0 || strcmp(b->path, b->root) != 0) { + kon_uiNextWidth(ui, 40); + if (kon_uiButton(ui, "Up")) { + char up[PATH]; + + copy(up, sizeof(up), b->path); + if (kon_fsParent(up) && browserGo(b, up)) result = BROWSER_DIR; + } + kon_uiSameLine(ui); + } + kon_uiLabel(ui, b->path[0] ? tail(b->path, 28) : "drives"); + kon_uiSeparator(ui); + + for (i = 0; i < b->list.count && result == BROWSER_NONE; i++) { + const kon_fsEntry_t *e = &b->list.items[i]; + char label[280]; + + snprintf(label, sizeof(label), e->isDir ? "%s/" : "%s", e->name); + kon_uiPushId(ui, (int)i); + if (kon_uiSelectable(ui, label, !e->isDir && strcmp(e->name, b->selected) == 0)) { + if (e->isDir) { + char next[PATH]; + + kon_fsJoin(next, sizeof(next), b->path, e->name); + if (browserGo(b, next)) result = BROWSER_DIR; + } else { + copy(b->selected, sizeof(b->selected), e->name); + result = BROWSER_FILE; + } + } + kon_uiPopId(ui); + } + return result; +} + +/*** project ***/ + +static int projectLoad(project_t *p, const char *dir, char *error, size_t errorSize) +{ + char file[PATH]; + kon_toml_t *toml; + + kon_fsJoin(file, sizeof(file), dir, "project.toml"); + if (!kon_fsIsFile(file)) { + snprintf(error, errorSize, "no project.toml in %s", tail(dir, 60)); + return 0; + } + toml = kon_parseTomlFile(file); + if (!toml) { + snprintf(error, errorSize, "cannot read %s", tail(file, 60)); + return 0; + } + memset(p, 0, sizeof(*p)); + p->open = 1; + copy(p->dir, sizeof(p->dir), dir); + copy(p->name, sizeof(p->name), kon_tomlGetString(toml, "project", "name", kon_fsBaseName(dir))); + copy(p->assets, sizeof(p->assets), kon_tomlGetString(toml, "project", "assets", "assets")); + kon_freeToml(toml); + return 1; +} + +/* a quote or a backslash would need escaping, so they are left out */ +static void writeString(FILE *f, const char *s) +{ + fputc('"', f); + for (; *s; s++) fputc(*s == '"' || *s == '\\' ? '_' : *s, f); + fputc('"', f); +} + +static int projectSave(project_t *p) +{ + char file[PATH]; + FILE *f; + + kon_fsJoin(file, sizeof(file), p->dir, "project.toml"); + f = fopen(file, "w"); + if (!f) return 0; + fputs("[project]\nname = ", f); + writeString(f, p->name); + fputs("\nassets = ", f); + writeString(f, p->assets); + fputc('\n', f); + if (fclose(f) != 0) return 0; + p->dirty = 0; + return 1; +} + +static int projectCreate(project_t *p, const char *dir, const char *name, char *error, size_t errorSize) +{ + char file[PATH], assets[PATH]; + + kon_fsJoin(file, sizeof(file), dir, "project.toml"); + if (kon_fsIsFile(file)) { + snprintf(error, errorSize, "there is a project in %s already", tail(dir, 60)); + return 0; + } + memset(p, 0, sizeof(*p)); + p->open = 1; + copy(p->dir, sizeof(p->dir), dir); + copy(p->name, sizeof(p->name), name[0] ? name : kon_fsBaseName(dir)); + copy(p->assets, sizeof(p->assets), "assets"); + kon_fsJoin(assets, sizeof(assets), dir, p->assets); + if (!kon_fsMakeDir(assets) || !projectSave(p)) { + snprintf(error, errorSize, "cannot write in %s", tail(dir, 60)); + memset(p, 0, sizeof(*p)); + return 0; + } + return 1; +} + +static void projectShow(state_t *state) +{ + copy(state->side.root, sizeof(state->side.root), state->project.dir); + browserGo(&state->side, state->project.dir); + state->sideArea.scroll = 0; + state->mainArea.scroll = 0; + state->tab = 0; + state->dialog = DIALOG_NONE; +} + +static void openProject(state_t *state, const char *dir) +{ + char error[160]; + project_t p; + + if (!projectLoad(&p, dir, error, sizeof(error))) { + setStatus(state, "%s", error); + return; + } + state->project = p; + projectShow(state); + setStatus(state, "opened %s", state->project.name); +} + +static void createProject(state_t *state, const char *dir, const char *name) +{ + char error[160]; + project_t p; + + if (!projectCreate(&p, dir, name, error, sizeof(error))) { + setStatus(state, "%s", error); + return; + } + state->project = p; + projectShow(state); + setStatus(state, "made %s", state->project.name); +} + +static void saveProject(state_t *state) +{ + if (!state->project.open) return; + if (projectSave(&state->project)) setStatus(state, "saved %s", state->project.name); + else setStatus(state, "cannot write project.toml"); +} + +static void closeProject(state_t *state) +{ + if (!state->project.open) return; + setStatus(state, "closed %s", state->project.name); + memset(&state->project, 0, sizeof(state->project)); + browserFree(&state->side); + state->sideArea.scroll = 0; +} + +static void openDialog(state_t *state, dialog_t dialog) +{ + char cwd[PATH]; + + if (!kon_fsCwd(cwd, sizeof(cwd))) cwd[0] = 0; + slashes(cwd); + if (!browserGo(&state->pick, cwd)) browserGo(&state->pick, ""); + state->dialog = dialog; + state->newName[0] = 0; + state->dialogList.scroll = 0; +} + +/*** screens ***/ + +static void drawMenus(kon_program_t *program, state_t *state) +{ + kon_ui_t *ui = state->ui; + + if (!kon_uiBeginMenuBar(ui)) return; + if (kon_uiBeginMenu(ui, "File")) { + if (kon_uiMenuItem(ui, "New Project...", NULL)) openDialog(state, DIALOG_NEW); + if (kon_uiMenuItem(ui, "Open Project...", NULL)) openDialog(state, DIALOG_OPEN); + kon_uiMenuSeparator(ui); + if (kon_uiMenuItem(ui, "Save Project", "Ctrl+S")) saveProject(state); + if (kon_uiMenuItem(ui, "Close Project", NULL)) closeProject(state); + kon_uiMenuSeparator(ui); + if (kon_uiMenuItem(ui, "Quit", NULL)) kon_programQuit(program); + } + kon_uiEndMenu(ui); + if (kon_uiBeginMenu(ui, "View")) { + if (kon_uiMenuItem(ui, "Refresh files", "F5") && state->project.open) browserGo(&state->side, state->side.path); + } + kon_uiEndMenu(ui); + kon_uiEndMenuBar(ui); +} + +static void drawSide(state_t *state) +{ + kon_ui_t *ui = state->ui; + + if (!kon_uiBeginArea(ui, "files", &state->sideArea)) return; + if (!state->project.open) { + kon_uiLabel(ui, "no project open"); + } else { + kon_uiLabel(ui, state->project.name); + kon_uiSeparator(ui); + if (browserDraw(state, &state->side) == BROWSER_DIR) state->sideArea.scroll = 0; + } + kon_uiEndArea(ui); +} + +static void drawProjectTab(state_t *state) +{ + kon_ui_t *ui = state->ui; + project_t *p = &state->project; + char before[64]; + + kon_uiLabel(ui, "name"); + copy(before, sizeof(before), p->name); + kon_uiTextInput(ui, "project name##name", p->name, sizeof(p->name)); + if (strcmp(before, p->name) != 0) p->dirty = 1; + + kon_uiLabel(ui, "assets folder"); + copy(before, sizeof(before), p->assets); + kon_uiTextInput(ui, "assets folder##assets", p->assets, sizeof(p->assets)); + if (strcmp(before, p->assets) != 0) p->dirty = 1; + + kon_uiSeparator(ui); + kon_uiLabel(ui, tail(p->dir, 60)); + kon_uiNextWidth(ui, 80); + if (kon_uiButton(ui, "Save")) saveProject(state); + if (p->dirty) { + kon_uiSameLine(ui); + kon_uiLabel(ui, "unsaved"); + } +} + +static void drawAssetsTab(state_t *state) +{ + kon_ui_t *ui = state->ui; + browser_t *b = &state->side; + const kon_fsEntry_t *found = NULL; + size_t i; + + if (b->selected[0]) { + for (i = 0; i < b->list.count; i++) { + if (!b->list.items[i].isDir && strcmp(b->list.items[i].name, b->selected) == 0) found = &b->list.items[i]; + } + } + if (!found) { + kon_uiLabel(ui, "pick a file on the left"); + kon_uiLabel(ui, "the asset browser and the pack tool come here"); + return; + } + kon_uiLabel(ui, found->name); + kon_uiLabelf(ui, "kind: %s", kindOf(found->name)); + kon_uiLabelf(ui, "size: %lld bytes", found->size); + kon_uiLabel(ui, tail(b->path, 60)); +} + +static void drawMain(state_t *state) +{ + kon_ui_t *ui = state->ui; + + if (!kon_uiBeginArea(ui, "main", &state->mainArea)) return; + if (!state->project.open) { + kon_uiLabel(ui, "File > New Project to start a project,"); + kon_uiLabel(ui, "or File > Open Project for one you have"); + } else { + kon_uiTabs(ui, tabNames, TABS, &state->tab); + kon_uiSeparator(ui); + if (state->tab == 0) drawProjectTab(state); + else if (state->tab == 1) drawAssetsTab(state); + else kon_uiLabel(ui, tabAbout[state->tab]); + } + kon_uiEndArea(ui); +} + +static void drawStatus(state_t *state) +{ + if (!kon_uiBeginArea(state->ui, "status", &state->statusArea)) return; + kon_uiLabel(state->ui, state->status); + kon_uiEndArea(state->ui); +} + +static void goTo(state_t *state, char *path) +{ + slashes(path); + if (!browserGo(&state->pick, path)) setStatus(state, "cannot open %s", tail(path, 60)); + state->dialogList.scroll = 0; +} + +static void drawDialog(state_t *state, int width, int height) +{ + kon_ui_t *ui = state->ui; + browser_t *b = &state->pick; + int w = width - 40 < 480 ? width - 40 : 480; + int total = height - 40 < 420 ? height - 40 : 420; + int topH = state->dialogTop.contentH > 0 ? state->dialogTop.contentH : 160; + int result, accepted = 0; + + if (state->dialog == DIALOG_NONE || w < 200 || total < topH + 60) return; + + state->dialogTop.x = (width - w) / 2; + state->dialogTop.y = (height - total) / 2; + state->dialogTop.w = w; + state->dialogTop.h = topH; + state->dialogList.x = state->dialogTop.x; + state->dialogList.y = state->dialogTop.y + topH; + state->dialogList.w = w; + state->dialogList.h = total - topH; + + if (kon_uiBeginArea(ui, "dialog", &state->dialogTop)) { + kon_uiLabel(ui, state->dialog == DIALOG_OPEN ? "Open project" : "New project"); + kon_uiSeparator(ui); + if (state->dialog == DIALOG_NEW) { + kon_uiLabel(ui, "name"); + kon_uiTextInput(ui, "project name##new", state->newName, sizeof(state->newName)); + } + kon_uiLabel(ui, "folder"); + if (b->changed) { + copy(state->pathInput, sizeof(state->pathInput), b->path); + b->changed = 0; + } + if (kon_uiTextInput(ui, "folder##path", state->pathInput, sizeof(state->pathInput))) goTo(state, state->pathInput); + + kon_uiNextWidth(ui, 100); + if (kon_uiButton(ui, state->dialog == DIALOG_OPEN ? "Open" : "Create")) accepted = 1; + kon_uiSameLine(ui); + kon_uiNextWidth(ui, 100); + if (kon_uiButton(ui, "Cancel")) state->dialog = DIALOG_NONE; + } + kon_uiEndArea(ui); + + if (kon_uiBeginArea(ui, "dialog list", &state->dialogList)) { + result = browserDraw(state, b); + if (result == BROWSER_DIR) state->dialogList.scroll = 0; + if (result == BROWSER_FILE && state->dialog == DIALOG_OPEN && strcmp(b->selected, "project.toml") == 0) accepted = 1; + } + kon_uiEndArea(ui); + + if (accepted && state->dialog == DIALOG_OPEN) openProject(state, b->path); + else if (accepted && state->dialog == DIALOG_NEW) createProject(state, b->path, state->newName); +} + +/*** program ***/ + +static int event(kon_program_t *program, const kon_event_t *ev, void *user) +{ + state_t *state = user; + + (void)program; + kon_uiFeedEvent(state->ui, ev); + + if (ev->type == KON_EVENT_KEY_DOWN || ev->type == KON_EVENT_KEY_UP) { + int down = ev->type == KON_EVENT_KEY_DOWN; + + if (ev->key == KON_KEY_LEFT_CTRL || ev->key == KON_KEY_RIGHT_CTRL) state->ctrl = down; + if (down && ev->key == KON_KEY_S && state->ctrl) saveProject(state); + if (down && ev->key == KON_KEY_ESCAPE && !kon_uiWantsKeyboard(state->ui)) state->dialog = DIALOG_NONE; + if (down && ev->key == KON_KEY_F5 && state->project.open) browserGo(&state->side, state->side.path); + } + return kon_uiWantsMouse(state->ui) || kon_uiWantsKeyboard(state->ui); +} + +static void draw(kon_program_t *program, kon_framebuffer_t *game, void *user) +{ + (void)program; + (void)user; + kon_clearFramebuffer(game, COL_BACKGROUND); +} + +static void drawScreen(kon_program_t *program, kon_framebuffer_t *screen, void *user) +{ + state_t *state = user; + int menuH, statusH, pad = kon_uiTheme(state->ui)->padding; + + kon_uiBegin(state->ui, screen); + menuH = kon_uiMenuBarHeight(state->ui); + statusH = menuH + pad; + + state->sideArea.x = 0; + state->sideArea.y = menuH; + state->sideArea.w = SIDE_WIDTH; + state->sideArea.h = screen->height - menuH - statusH; + state->mainArea.x = SIDE_WIDTH; + state->mainArea.y = menuH; + state->mainArea.w = screen->width - SIDE_WIDTH; + state->mainArea.h = state->sideArea.h; + state->statusArea.x = 0; + state->statusArea.y = screen->height - statusH; + state->statusArea.w = screen->width; + state->statusArea.h = statusH; + + drawMenus(program, state); + drawSide(state); + drawMain(state); + drawStatus(state); + drawDialog(state, screen->width, screen->height); + + kon_uiEnd(state->ui); +} + +static int init(kon_program_t *program, void *user) +{ + state_t *state = user; + kon_uiTheme_t *theme; + + state->ui = kon_uiCreate(program->font); + if (!state->ui) return 0; + + theme = kon_uiTheme(state->ui); + theme->panel = COL_PANEL; + theme->title = COL_TITLE; + theme->text = COL_TEXT; + theme->textDim = COL_DIM; + theme->widget = COL_WIDGET; + theme->hover = COL_HOVER; + theme->active = 0xFF6B4F80u; + theme->accent = 0xFFF2A7C3u; + theme->border = COL_BORDER; + + setStatus(state, "ready"); + if (state->startDir) { + char dir[PATH]; + + copy(dir, sizeof(dir), state->startDir); + slashes(dir); + openProject(state, dir); + } + return 1; +} + +static void shutdown(kon_program_t *program, void *user) +{ + state_t *state = user; + + (void)program; + browserFree(&state->side); + browserFree(&state->pick); + kon_uiDestroy(state->ui); +} + +int main(int argc, char **argv) +{ + state_t state = {0}; + kon_programConfig_t config = {0}; + + state.startDir = argc > 1 ? argv[1] : NULL; + + config.title = "konedit"; + config.gameWidth = 640; + config.gameHeight = 360; + config.windowWidth = 1280; + config.windowHeight = 720; + config.user = &state; + config.init = init; + config.event = event; + config.draw = draw; + config.drawScreen = drawScreen; + config.shutdown = shutdown; + return kon_programRun(&config); +} |
