/* * 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" #include "konpack.h" #include "stb_image.h" #if !defined(KON_MODULE_UI) || !defined(KON_MODULE_FONT) || !defined(KON_MODULE_TOML) || !defined(KON_MODULE_FS) || !defined(KON_MODULE_PACKW) || !defined(KON_MODULE_IMAGE) #error "build with: ./build.sh -d konedit" #endif #define PATH 512 #define SIDE_WIDTH 260 #define TABS 6 #define PACK_SHOWN 300 /*** 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[128]; char pack[128]; char key[128]; } project_t; /* what the Assets tab shows of the file picked on the left, kept so it is not read again every frame */ typedef struct preview { char path[PATH]; long long size; kon_image *image; int width, height; char text[1600]; int isText; } preview_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; preview_t preview; kon_fsList_t packList; char packInfo[200], packResult[200]; int packTreeOpen; 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", "Pack", "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"; } static int isTextKind(const char *kind) { return strcmp(kind, "text") == 0 || strcmp(kind, "settings") == 0 || strcmp(kind, "sector map") == 0; } /* a path from project.toml is from the project folder, unless it starts at a root */ static void projectPath(const project_t *p, const char *rel, char *out, size_t size) { if (kon_fsIsAbsolute(rel)) copy(out, size, rel); else kon_fsJoin(out, size, p->dir, rel); } static long long fileSize(const char *path) { FILE *f = fopen(path, "rb"); long long size = -1; if (!f) return -1; if (fseek(f, 0, SEEK_END) == 0) size = (long long)ftell(f); fclose(f); return size; } /*** 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; } /*** preview ***/ static void previewFree(preview_t *v) { if (v->image) kon_freeImage(v->image); memset(v, 0, sizeof(*v)); } static void previewLoad(preview_t *v, const char *path, const char *kind, long long size) { previewFree(v); copy(v->path, sizeof(v->path), path); v->size = size; if (strcmp(kind, "image") == 0) { int w, h, n; unsigned char *pixels = size < (64LL << 20) ? stbi_load(path, &w, &h, &n, 4) : NULL; if (pixels) { v->image = kon_loadImage(pixels, w, h, konFormatRGBA8); v->width = w; v->height = h; stbi_image_free(pixels); } } else if (isTextKind(kind)) { FILE *f = fopen(path, "rb"); if (f) { size_t got = fread(v->text, 1, sizeof(v->text) - 1, f), i, out = 0; fclose(f); for (i = 0; i < got; i++) { unsigned char c = (unsigned char)v->text[i]; if (c == '\r') continue; v->text[out++] = (char)(c == '\n' || (c >= 32 && c < 127) ? c : (c == '\t' ? ' ' : '.')); } v->text[out] = 0; v->isText = 1; } } } /* the size a picture is shown at: small ones are blown up whole times, big ones shrink to fit */ static void previewSize(int w, int h, int maxW, int maxH, int *outW, int *outH) { int k; if (maxW < 16) maxW = 16; if (w > maxW || h > maxH) { if ((long long)w * maxH > (long long)h * maxW) { *outW = maxW; *outH = (int)((long long)h * maxW / w); } else { *outH = maxH; *outW = (int)((long long)w * maxH / h); } if (*outW < 1) *outW = 1; if (*outH < 1) *outH = 1; return; } k = maxW / w < maxH / h ? maxW / w : maxH / h; if (k > 8) k = 8; if (k < 1) k = 1; *outW = w * k; *outH = h * k; } /*** 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")); copy(p->pack, sizeof(p->pack), kon_tomlGetString(toml, "pack", "output", "data.kpk")); copy(p->key, sizeof(p->key), kon_tomlGetString(toml, "pack", "key", "")); 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); fputs("\n\n[pack]\noutput = ", f); writeString(f, p->pack); fputs("\nkey = ", f); writeString(f, p->key); 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"); copy(p->pack, sizeof(p->pack), "data.kpk"); 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; } /*** pack ***/ static void scanPack(state_t *state) { project_t *p = &state->project; char assets[PATH], out[PATH]; long long total = 0, onDisk; size_t i; kon_fsFree(&state->packList); state->packInfo[0] = 0; if (!p->open) return; projectPath(p, p->assets, assets, sizeof(assets)); if (!kon_fsWalk(assets, &state->packList)) { snprintf(state->packInfo, sizeof(state->packInfo), "no folder %s", tail(assets, 60)); return; } for (i = 0; i < state->packList.count; i++) total += state->packList.items[i].size; projectPath(p, p->pack, out, sizeof(out)); onDisk = fileSize(out); if (onDisk >= 0) { snprintf(state->packInfo, sizeof(state->packInfo), "%d files, %lld bytes in the assets folder. the pack on disk is %lld bytes", (int)state->packList.count, total, onDisk); } else { snprintf(state->packInfo, sizeof(state->packInfo), "%d files, %lld bytes in the assets folder. no pack built yet", (int)state->packList.count, total); } } /* 1 for a good key, 0 when it is not 32 bytes, -1 when it cannot be opened */ static int readKey(const char *path, uint8_t key[KON_PACK_KEY_SIZE]) { FILE *f = fopen(path, "rb"); size_t got; uint8_t extra; int ok; if (!f) return -1; got = fread(key, 1, KON_PACK_KEY_SIZE, f); ok = got == KON_PACK_KEY_SIZE && fread(&extra, 1, 1, f) == 0; fclose(f); return ok; } static void buildPack(state_t *state) { project_t *p = &state->project; char assets[PATH], out[PATH], keyPath[PATH]; uint8_t key[KON_PACK_KEY_SIZE]; const uint8_t *useKey = NULL; char **paths; int i, n = 0, count, failed = 0; kon_pack_t *check; state->packResult[0] = 0; scanPack(state); count = (int)state->packList.count; if (count == 0) { snprintf(state->packResult, sizeof(state->packResult), "nothing to pack"); return; } projectPath(p, p->assets, assets, sizeof(assets)); projectPath(p, p->pack, out, sizeof(out)); if (p->key[0]) { int keyState; projectPath(p, p->key, keyPath, sizeof(keyPath)); keyState = readKey(keyPath, key); if (keyState < 0) { snprintf(state->packResult, sizeof(state->packResult), "cannot read the key file %s", tail(keyPath, 40)); return; } if (keyState == 0) { snprintf(state->packResult, sizeof(state->packResult), "the key file has to be exactly %d bytes: %s", KON_PACK_KEY_SIZE, tail(keyPath, 40)); return; } useKey = key; } paths = calloc((size_t)count, sizeof(char *)); if (!paths) { snprintf(state->packResult, sizeof(state->packResult), "out of memory"); return; } for (i = 0; i < count; i++) { char full[PATH]; kon_fsJoin(full, sizeof(full), assets, state->packList.items[i].name); if (strcmp(full, out) == 0) continue; paths[n] = malloc(strlen(full) + 1); if (!paths[n]) { failed = 1; break; } strcpy(paths[n], full); n++; } if (failed) { snprintf(state->packResult, sizeof(state->packResult), "out of memory"); } else if (n == 0) { snprintf(state->packResult, sizeof(state->packResult), "nothing to pack"); } else if (kon_packWrite(out, useKey, assets, (const char *const *)paths, n) != 0) { snprintf(state->packResult, sizeof(state->packResult), "%s", kon_packError()); } else if ((check = kon_packOpen(out, useKey)) == NULL) { snprintf(state->packResult, sizeof(state->packResult), "built it but cannot read it back: %s", kon_packError()); } else { snprintf(state->packResult, sizeof(state->packResult), "built %s: %d files, %lld bytes%s", kon_fsBaseName(out), kon_packCount(check), fileSize(out), useKey ? ", encrypted" : ""); kon_packClose(check); } for (i = 0; i < n; i++) free(paths[i]); free(paths); scanPack(state); setStatus(state, "%s", state->packResult); } /*** project actions ***/ 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; state->packResult[0] = 0; previewFree(&state->preview); scanPack(state); } 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); previewFree(&state->preview); kon_fsFree(&state->packList); state->packInfo[0] = 0; state->packResult[0] = 0; 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); scanPack(state); } } 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); } /* an input for a text of the project that marks it as unsaved when it changes */ static void projectInput(state_t *state, const char *title, const char *id, char *value, size_t size) { char before[128]; kon_uiLabel(state->ui, title); copy(before, sizeof(before), value); kon_uiTextInput(state->ui, id, value, size); if (strcmp(before, value) != 0) state->project.dirty = 1; } static void drawProjectTab(state_t *state) { kon_ui_t *ui = state->ui; project_t *p = &state->project; projectInput(state, "name", "project name##name", p->name, sizeof(p->name)); projectInput(state, "assets folder", "assets folder##assets", p->assets, sizeof(p->assets)); 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; preview_t *v = &state->preview; const kon_fsEntry_t *found = NULL; const char *kind; char path[PATH]; 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"); return; } kind = kindOf(found->name); kon_fsJoin(path, sizeof(path), b->path, found->name); if (strcmp(v->path, path) != 0 || v->size != found->size) previewLoad(v, path, kind, found->size); kon_uiLabel(ui, found->name); kon_uiLabelf(ui, "%s, %lld bytes", kind, found->size); kon_uiLabel(ui, tail(b->path, 60)); kon_uiSeparator(ui); if (strcmp(kind, "image") == 0) { if (v->image) { int w, h; previewSize(v->width, v->height, state->mainArea.w - 60, 320, &w, &h); kon_uiLabelf(ui, "%d x %d", v->width, v->height); kon_uiImage(ui, v->image, w, h); } else { kon_uiLabel(ui, "cannot read this image"); } } else if (v->isText) { const char *at = v->text; int lines = 0; while (*at && lines < 24) { char line[120]; size_t n = strcspn(at, "\n"); if (n > sizeof(line) - 1) n = sizeof(line) - 1; memcpy(line, at, n); line[n] = 0; kon_uiLabel(ui, line[0] ? line : " "); at += strcspn(at, "\n"); if (*at == '\n') at++; lines++; } if (*at) kon_uiLabel(ui, "..."); } } static void drawPackTab(state_t *state) { kon_ui_t *ui = state->ui; project_t *p = &state->project; size_t i, shown; projectInput(state, "pack file", "pack file##pack", p->pack, sizeof(p->pack)); projectInput(state, "key file, 32 bytes, or empty for no encryption", "key file##key", p->key, sizeof(p->key)); kon_uiSeparator(ui); kon_uiNextWidth(ui, 110); if (kon_uiButton(ui, "Scan")) scanPack(state); kon_uiSameLine(ui); kon_uiNextWidth(ui, 110); if (kon_uiButton(ui, "Build Pack")) buildPack(state); if (p->dirty) { kon_uiSameLine(ui); kon_uiLabel(ui, "unsaved"); } if (state->packInfo[0]) kon_uiLabel(ui, state->packInfo); if (state->packResult[0]) kon_uiLabel(ui, state->packResult); kon_uiSeparator(ui); kon_uiTreeNode(ui, "what goes in the pack", &state->packTreeOpen); if (!state->packTreeOpen) return; kon_uiIndent(ui, 8); shown = state->packList.count < PACK_SHOWN ? state->packList.count : PACK_SHOWN; for (i = 0; i < shown; i++) kon_uiLabelf(ui, "%s (%lld)", state->packList.items[i].name, state->packList.items[i].size); if (shown < state->packList.count) kon_uiLabelf(ui, "and %d more", (int)(state->packList.count - shown)); kon_uiIndent(ui, -8); } 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 if (state->tab == 2) drawPackTab(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); scanPack(state); } } 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); previewFree(&state->preview); kon_fsFree(&state->packList); 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); }