/* * 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 #include #include #include #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); }