#define ZPL_NO_WINDOWS_H #include "zpl.h" #include "editors/texed.h" #include "raylib.h" #include "utils/raylib_helpers.h" #include "cwpack/cwpack.h" #define RAYGUI_IMPLEMENTATION #define RAYGUI_SUPPORT_ICONS #include "raygui.h" #define GUI_FILE_DIALOG_IMPLEMENTATION #include "gui_file_dialog.h" static uint16_t screenWidth = 1280; static uint16_t screenHeight = 720; static float zoom = 4.0f; #define TD_DEFAULT_IMG_WIDTH 64 #define TD_DEFAULT_IMG_HEIGHT 64 #define TD_UI_PADDING 5.0f #define TD_UI_PREVIEW_BORDER 4.0f typedef enum { TPARAM_FLOAT, TPARAM_INT, TPARAM_COLOR, TPARAM_STRING, TPARAM_FORCE_UINT8 = UINT8_MAX } td_param_kind; typedef struct { td_param_kind kind; union { float number; int pix; char str[400]; Color color; }; } td_param; typedef enum { TOP_CLEAR, TOP_DRAW_RECT, TOP_DRAW_LINE, TOP_FORCE_UINT8 = UINT8_MAX } td_op_kind; typedef struct { td_op_kind kind; char const *name; bool is_hidden; uint8_t num_params; td_param *params; } td_op; #define OP(n) .kind = n, .name = #n typedef struct { char *filepath; Image img; Texture2D tex; GuiFileDialogState fileDialog; td_op *ops; //< zpl_array } td_ctx; static td_ctx ctx = {0}; static char filename[200]; static td_op default_ops[] = { { OP(TOP_CLEAR), .num_params = 1, .params = (td_param[]) { { .kind = TPARAM_COLOR, .color = WHITE } } }, { OP(TOP_DRAW_RECT), .num_params = 5, .params = (td_param[]) { { .kind = TPARAM_INT, .pix = 0 }, { .kind = TPARAM_INT, .pix = 0 }, { .kind = TPARAM_INT, .pix = 10 }, { .kind = TPARAM_INT, .pix = 10 }, { .kind = TPARAM_COLOR, .color = RED }, } } }; // NOTE(zaklaus): IMPORTANT !! keep these in sync static char const *add_op_list = "CLEAR SOLID;DRAW RECTANGLE;PLOT LINE;DITHER"; #define DEF_OPS_LEN (int)(sizeof(default_ops) / (sizeof(default_ops[0]))) void texed_new(int32_t w, int32_t h); void texed_destroy(void); void texed_load(void); void texed_save(void); void texed_repaint_preview(void); void texed_process_ops(void); void texed_add_op(int idx); void texed_rem_op(int idx); void texed_swp_op(int idx, int idx2); void texed_draw_oplist_pane(zpl_aabb2 r); void texed_draw_topbar(zpl_aabb2 r); static inline void DrawAABB(zpl_aabb2 rect, Color color) { DrawRectangleEco(rect.min.x, rect.min.y, rect.max.x-rect.min.x, rect.max.y-rect.min.y, color); } static inline Rectangle aabb2_ray(zpl_aabb2 r); void texed_run(void) { InitWindow(screenWidth, screenHeight, "eco2d - texture editor"); SetTargetFPS(60); texed_new(TD_DEFAULT_IMG_WIDTH, TD_DEFAULT_IMG_HEIGHT); zpl_aabb2 screen = { .min = (zpl_vec2) {.x = 0.0f, .y = 0.0f}, .max = (zpl_vec2) {.x = screenWidth, .y = screenHeight}, }; // TODO(zaklaus): TEMP texed_add_op(0); texed_add_op(1); zpl_aabb2 topbar = zpl_aabb2_cut_top(&screen, 25.0f); zpl_aabb2 oplist_pane = zpl_aabb2_cut_right(&screen, screenWidth / 2.0f); zpl_aabb2 preview_window = zpl_aabb2_cut_top(&screen, (screen.max.y-screen.min.y) / 2.0f); zpl_aabb2 property_pane = screen; // NOTE(zaklaus): contract all panes for a clean UI separation oplist_pane = zpl_aabb2_contract(&oplist_pane, TD_UI_PADDING); preview_window = zpl_aabb2_contract(&preview_window, TD_UI_PADDING); property_pane = zpl_aabb2_contract(&property_pane, TD_UI_PADDING); Rectangle preview_rect = aabb2_ray(preview_window); Image checkerboard = GenImageChecked(preview_rect.width, preview_rect.height, 16, 16, BLACK, ColorAlpha(GRAY, 0.2f)); Texture2D checker_tex = LoadTextureFromImage(checkerboard); UnloadImage(checkerboard); while (!WindowShouldClose()) { BeginDrawing(); ClearBackground(GetColor(0x222034)); { if (ctx.fileDialog.fileDialogActive) GuiLock(); DrawTextureEx(checker_tex, (Vector2){ preview_window.min.x, preview_window.min.y}, 0.0f, 1.0f, WHITE); DrawTextureEx(ctx.tex, (Vector2){ preview_window.min.x, preview_window.min.y}, 0.0f, zoom, WHITE); DrawAABB(topbar, RAYWHITE); DrawAABB(property_pane, GetColor(0x422060)); DrawAABB(oplist_pane, GetColor(0x425060)); texed_draw_topbar(topbar); texed_draw_oplist_pane(oplist_pane); if (ctx.fileDialog.fileDialogActive) GuiUnlock(); GuiFileDialog(&ctx.fileDialog); } EndDrawing(); } UnloadTexture(checker_tex); texed_destroy(); } void texed_new(int32_t w, int32_t h) { ctx.img = GenImageColor(w, h, WHITE); ctx.filepath = NULL; zpl_array_init(ctx.ops, zpl_heap()); texed_repaint_preview(); ctx.fileDialog = InitGuiFileDialog(420, 310, zpl_bprintf("%s/art", GetWorkingDirectory()), false); //zpl_strcpy(ctx.fileDialog.filterExt, ".ecotex"); } void texed_destroy(void) { UnloadTexture(ctx.tex); UnloadImage(ctx.img); zpl_array_free(ctx.ops); } void texed_repaint_preview(void) { UnloadTexture(ctx.tex); ImageClearBackground(&ctx.img, ColorAlpha(BLACK, 0.0f)); texed_process_ops(); ctx.tex = LoadTextureFromImage(ctx.img); } void texed_add_op(int idx) { assert(idx >= 0 && idx < DEF_OPS_LEN); td_op *dop = &default_ops[idx]; td_op op = { .kind = dop->kind, .name = dop->name, .num_params = dop->num_params, .params = (td_param*)zpl_malloc(sizeof(td_param)*dop->num_params) }; zpl_memcopy(op.params, dop->params, sizeof(td_param)*dop->num_params); zpl_array_append(ctx.ops, op); texed_repaint_preview(); } void texed_rem_op(int idx) { assert(idx >= 0 && idx < (int)zpl_array_count(ctx.ops)); zpl_mfree(ctx.ops[idx].params); zpl_array_remove_at(ctx.ops, idx); texed_repaint_preview(); } static bool is_add_op_dropbox_open = false; static int add_op_dropbox_selected = 0; void texed_draw_oplist_pane(zpl_aabb2 r) { zpl_aabb2 oplist_header = zpl_aabb2_cut_top(&r, 40.0f); zpl_aabb2 add_op_r = zpl_aabb2_cut_left(&oplist_header, 120.0f); if (!is_add_op_dropbox_open && GuiButton(aabb2_ray(add_op_r), "ADD OPERATION")) { is_add_op_dropbox_open = true; } GuiSetState(ctx.filepath ? GUI_STATE_NORMAL : GUI_STATE_DISABLED); zpl_aabb2 export_code_r = zpl_aabb2_cut_left(&oplist_header, 120.0f); if (GuiButton(aabb2_ray(export_code_r), "BUILD TEXTURE")) { } zpl_aabb2 export_img_r = zpl_aabb2_cut_left(&oplist_header, 120.0f); if (GuiButton(aabb2_ray(export_img_r), "EXPORT AS IMAGE")) { } GuiSetState(GUI_STATE_NORMAL); // NOTE(zaklaus): operator list for (int i = 0; i < zpl_array_count(ctx.ops); i += 1) { zpl_aabb2 op_item_r = zpl_aabb2_cut_top(&r, 45.0f); zpl_aabb2_cut_top(&op_item_r, 2.5f); zpl_aabb2_cut_bottom(&op_item_r, 2.5f); Rectangle list_item = aabb2_ray(op_item_r); DrawRectangleRec(list_item, ColorAlpha(RED, 0.4f)); zpl_aabb2 swap_r = zpl_aabb2_cut_left(&op_item_r, 50.0f); Rectangle list_text = aabb2_ray(op_item_r); zpl_aabb2_cut_right(&swap_r, 5.0f); zpl_aabb2 swap_top = zpl_aabb2_cut_top(&swap_r, 20.0f); zpl_aabb2 swap_bottom = swap_r; if (i > 0 && GuiButton(aabb2_ray(swap_top), "UP")) { texed_swp_op(i, i-1); } if (i+1 < zpl_array_count(ctx.ops) && GuiButton(aabb2_ray(swap_bottom), "DOWN")) { texed_swp_op(i, i+1); } zpl_aabb2 remove_r = zpl_aabb2_cut_right(&op_item_r, 60.0f); if (GuiButton(aabb2_ray(remove_r), "REMOVE")) { texed_rem_op(i); } zpl_aabb2 hidden_r = zpl_aabb2_cut_right(&op_item_r, 60.0f); if (GuiButton(aabb2_ray(hidden_r), ctx.ops[i].is_hidden ? "SHOW" : "HIDE")) { ctx.ops[i].is_hidden = !ctx.ops[i].is_hidden; texed_repaint_preview(); } GuiDrawText(ctx.ops[i].name, GetTextBounds(LABEL, list_text), GuiGetStyle(LABEL, TEXT_ALIGNMENT), Fade(RAYWHITE, guiAlpha)); } if (is_add_op_dropbox_open && GuiDropdownBox(aabb2_ray(add_op_r), add_op_list, &add_op_dropbox_selected, true)) { is_add_op_dropbox_open = false; texed_add_op(add_op_dropbox_selected); } } static inline Rectangle aabb2_ray(zpl_aabb2 r) { return (Rectangle) { .x = r.min.x, .y = r.min.y, .width = r.max.x-r.min.x, .height = r.max.y-r.min.y }; } void texed_process_ops(void) { for (int i = 0; i < zpl_array_count(ctx.ops); i += 1) { td_op *op = &ctx.ops[i]; if (op->is_hidden) continue; zpl_printf("processing op: %s ... \n", op->name); switch (op->kind) { case TOP_CLEAR: { ImageClearBackground(&ctx.img, op->params[0].color); }break; case TOP_DRAW_RECT: { ImageDrawRectangle(&ctx.img, op->params[0].pix, op->params[1].pix, op->params[2].pix, op->params[3].pix, op->params[4].color); }break; default: { zpl_printf("%s\n", "unsupported op!"); }break; } } } void texed_draw_topbar(zpl_aabb2 r) { zpl_aabb2 zoom_ctrl_r = zpl_aabb2_cut_left(&r, 150.0f); zoom = GuiSlider(aabb2_ray(zoom_ctrl_r), "zoom: ", zpl_bprintf("%.02f x", zoom), zoom, 1.0f, 16.0f); zpl_aabb2_cut_left(&r, 100.0f); zpl_aabb2 new_prj_r = zpl_aabb2_cut_left(&r, 60.0f); if (GuiButton(aabb2_ray(new_prj_r), "NEW")) { texed_destroy(); texed_new(TD_DEFAULT_IMG_WIDTH, TD_DEFAULT_IMG_HEIGHT); // TODO(zaklaus): show res panel } zpl_aabb2 load_prj_r = zpl_aabb2_cut_left(&r, 60.0f); static bool load_pending = false; if (GuiButton(aabb2_ray(load_prj_r), "LOAD")) { load_pending = true; ctx.fileDialog.fileDialogActive = true; } if (ctx.fileDialog.SelectFilePressed && load_pending) { ctx.fileDialog.SelectFilePressed = false; if (IsFileExtension(ctx.fileDialog.fileNameText, ".ecotex")) { zpl_strcpy(filename, ctx.fileDialog.fileNameText); ctx.filepath = filename; load_pending = false; texed_load(); } else { ctx.fileDialog.fileDialogActive = true; } } zpl_aabb2 save_prj_r = zpl_aabb2_cut_left(&r, 60.0f); static bool save_as_pending = false; if (GuiButton(aabb2_ray(save_prj_r), "SAVE")) { if (ctx.filepath == NULL) { save_as_pending = true; ctx.fileDialog.fileDialogActive = true; } else { texed_save(); } } zpl_aabb2 save_as_prj_r = zpl_aabb2_cut_left(&r, 60.0f); if (GuiButton(aabb2_ray(save_as_prj_r), "SAVE AS")) { save_as_pending = true; ctx.fileDialog.fileDialogActive = true; } if (ctx.fileDialog.SelectFilePressed && save_as_pending) { ctx.fileDialog.SelectFilePressed = false; if (!IsFileExtension(ctx.fileDialog.fileNameText, ".ecotex")) { zpl_strcpy(ctx.fileDialog.fileNameText, zpl_bprintf("%s.ecotex", ctx.fileDialog.fileNameText)); } zpl_strcpy(filename, ctx.fileDialog.fileNameText); ctx.filepath = filename; save_as_pending = false; texed_save(); } zpl_aabb2 prj_name_r = zpl_aabb2_cut_right(&r, 200.0f); GuiDrawText(zpl_bprintf("Project: %s", ctx.filepath ? ctx.filepath : "(unnamed)"), GetTextBounds(LABEL, aabb2_ray(prj_name_r)), GuiGetStyle(LABEL, TEXT_ALIGNMENT), Fade(BLACK, guiAlpha)); } void texed_swp_op(int idx, int idx2) { assert(idx >= 0 && idx < (int)zpl_array_count(ctx.ops)); assert(idx2 >= 0 && idx2 < (int)zpl_array_count(ctx.ops)); td_op tmp = ctx.ops[idx2]; ctx.ops[idx2] = ctx.ops[idx]; ctx.ops[idx] = tmp; texed_repaint_preview(); } #define ECOTEX_VERSION 1 #define UNPACK(kind) cw_unpack_next(&uc); assert(uc.item.type == kind); void texed_load(void) { assert(ctx.filepath); zpl_printf("Loading %s ...\n", ctx.filepath); zpl_array_clear(ctx.ops); uint32_t size = 0; uint8_t *databuf = LoadFileData(zpl_bprintf("art/%s", ctx.filepath), &size); cw_unpack_context uc; cw_unpack_context_init(&uc, databuf, (size_t)size, NULL); UNPACK(CWP_ITEM_POSITIVE_INTEGER); assert(uc.item.as.u64 == ECOTEX_VERSION); UNPACK(CWP_ITEM_ARRAY); int arrsize = (int)uc.item.as.array.size; for (int i = 0; i < arrsize; i += 1) { UNPACK(CWP_ITEM_POSITIVE_INTEGER); int kind = (int)uc.item.as.u64; texed_add_op(kind); td_op *op = zpl_array_end(ctx.ops); UNPACK(CWP_ITEM_BOOLEAN); op->is_hidden = uc.item.as.boolean; UNPACK(CWP_ITEM_POSITIVE_INTEGER); op->num_params = uc.item.as.u64; op->params = zpl_malloc(sizeof(td_param)*op->num_params); UNPACK(CWP_ITEM_BIN); zpl_memcopy(op->params, uc.item.as.bin.start, uc.item.as.bin.length); } assert(uc.return_code == CWP_RC_OK); cw_unpack_next(&uc); assert(uc.return_code == CWP_RC_END_OF_INPUT); texed_repaint_preview(); UnloadFileData(databuf); } void texed_save(void) { assert(ctx.filepath); zpl_printf("Saving %s ...\n", ctx.filepath); static uint8_t databuf[400000] = {0}; cw_pack_context pc; cw_pack_context_init(&pc, databuf, sizeof(databuf), NULL); cw_pack_unsigned(&pc, ECOTEX_VERSION); cw_pack_array_size(&pc, zpl_array_count(ctx.ops)); for (int i = 0; i < zpl_array_count(ctx.ops); i += 1) { td_op *op = &ctx.ops[i]; cw_pack_unsigned(&pc, op->kind); cw_pack_boolean(&pc, (bool)op->is_hidden); cw_pack_unsigned(&pc, op->num_params); cw_pack_bin(&pc, op->params, sizeof(td_param)*op->num_params); } SaveFileData(zpl_bprintf("art/%s", ctx.filepath), databuf, pc.current - pc.start); }