summaryrefslogtreecommitdiff
path: root/editor/konedit.c
diff options
context:
space:
mode:
Diffstat (limited to 'editor/konedit.c')
-rw-r--r--editor/konedit.c647
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);
+}