commit 08c9c53b4c6b366ed3c6bd0ff8d39f92e9dae1d3
parent 540f40220ae4f484b1e5f6d5a77f514b86b40eb1
Author: bsandro <[email protected]>
Date: Sun, 29 Sep 2024 23:28:48 +0300
Basic split into different logic files
Diffstat:
10 files changed, 486 insertions(+), 414 deletions(-)
diff --git a/TODO b/TODO
@@ -1,3 +1,4 @@
+- Memory leak with file open/save dialog :(
- Add 'debug' and 'release' builds into makefile
- Wrap errors into sane messages and not just asserts
- Support drag-n-drop for GUI version
@@ -6,5 +7,12 @@
- i18n
- Status bar with service/info messages
- Fix preview display - pixels with alpha channel are borked
-- Better file open/save dialog
-- Split into files/headers
+- File open/save dialog: display current path on open
+- Better main window layout
+- Split into logical files/headers
+- Display/update resulting spritesheet dimensions in pixels
+- Unify path and filename fields (more canonical way)
+- `-> copy the full path+name into the unified field
+- M$ Windows build
+- Use system open/save dialog on Windows?
+
diff --git a/gui/animation.c b/gui/animation.c
@@ -1,6 +1,7 @@
#define _GNU_SOURCE
#include "animation.h"
+#include "fileops.h"
#include <libgen.h>
#include <stdlib.h>
#include <stdio.h>
@@ -54,8 +55,8 @@ int WebpRead(const char *filename, Animation *anim) {
WebPData webp_data;
WebPDataInit(&webp_data);
- if (ReadFile(filename, &webp_data.bytes, &webp_data.size) == -1) {
- fprintf(stderr, "ReadFile error\n");
+ if (FileRead(filename, &webp_data.bytes, &webp_data.size) == -1) {
+ fprintf(stderr, "FileRead() error\n");
return -1;
}
diff --git a/gui/animation.h b/gui/animation.h
@@ -1,3 +1,6 @@
+/* webp animations utility stuff */
+#pragma once
+
#include <stdbool.h>
#include <stdint.h>
@@ -22,3 +25,4 @@ typedef struct {
Animation * ImageLoad(const char *path);
void ImageUnload(Animation **img);
int WebpRead(const char *filename, Animation *anim);
+void AnimationCreate(Animation *img, uint32_t frame_count);
diff --git a/gui/fileops.c b/gui/fileops.c
@@ -0,0 +1,48 @@
+#include "webp/decode.h"
+#include "webp/encode.h"
+#include "webp/demux.h"
+#include "fileops.h"
+#include "spritesheet.h"
+
+int FileRead(const char *filename, const uint8_t **data, size_t *size) {
+ assert(data != NULL);
+ assert(size != NULL);
+
+ *data = NULL;
+ *size = 0;
+ FILE *infile = fopen(filename, "rb");
+ assert(infile != NULL);
+ fseek(infile, 0, SEEK_END);
+ size_t fsize = ftell(infile);
+ // printf("%s: %zu bytes\n", filename, fsize);
+ fseek(infile, 0, SEEK_SET);
+
+ uint8_t *fdata = malloc(fsize+1);
+ assert(fdata != NULL);
+ fdata[fsize] = '\0';
+ int ok = (fread(fdata, fsize, 1, infile)==1);
+ fclose(infile);
+
+ if (!ok) {
+ fprintf(stderr, "cannot read file %s (%d)\n", filename, ok);
+ free(fdata);
+ return -1;
+ }
+ *data = fdata;
+ *size = fsize;
+ return 0;
+}
+
+void WebpWrite(const char *path, Animation *img, int cols) {
+ Spritesheet ss = SpritesheetGen(img, cols);
+ uint8_t *out;
+ FILE *fp = fopen(path, "wb");
+ assert(fp!=NULL);
+ size_t encoded = WebPEncodeLosslessRGBA(ss.data, ss.width, ss.height, ss.stride, &out);
+ // printf("size: %zu, encoded: %zu\n", img->width*img->height*sizeof(uint32_t), encoded);
+ assert(encoded!=0);
+ size_t written = fwrite(out, sizeof(uint8_t), encoded, fp);
+ assert(written==encoded);
+ WebPFree(out);
+ free(ss.data);
+}
diff --git a/gui/fileops.h b/gui/fileops.h
@@ -0,0 +1,12 @@
+/* file i/o operations */
+#pragma once
+
+#include <stdint.h>
+#include <stddef.h>
+#include <assert.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include "animation.h"
+
+int FileRead(const char *filename, const uint8_t **data, size_t *size);
+void WebpWrite(const char *path, Animation *img, int cols);
diff --git a/gui/main.c b/gui/main.c
@@ -10,20 +10,20 @@
#define UI_IMPLEMENTATION
#include <stdio.h>
+//#include <stdbool.h>
+//#include <stdlib.h>
+//#include <libgen.h>
+//#include <string.h>
+//#include <strings.h>
+//#include <assert.h>
+//#include <dirent.h>
+#include <sys/prctl.h>
#include "webp/decode.h"
#include "webp/encode.h"
#include "webp/demux.h"
-#include <stdbool.h>
-#include <stdlib.h>
-#include <libgen.h>
-#include <string.h>
-#include <strings.h>
-#include <assert.h>
-#include <dirent.h>
-#include <sys/prctl.h>
-#include "luigi.h"
-
+//#include "luigi.h"
#include "animation.h"
+#include "ui.h"
#ifdef __OpenBSD__
#include <sys/syslimits.h>
@@ -33,17 +33,9 @@
#include <linux/limits.h>
#endif
-#ifdef _WIN32
-#define DIR_SEPARATOR '\\'
-#else
-#define DIR_SEPARATOR '/'
-#endif
-
#define WIN_WIDTH 750
#define WIN_HEIGHT 400
-#define UI_COLOR_FROM_RGBA(r, g, b, a) (((uint32_t) (r) << 16) | ((uint32_t) (g) << 8) | ((uint32_t) (b) << 0) | ((uint32_t) (a) << 24))
-
static void print_webp_version() {
int dec_ver = WebPGetDecoderVersion();
int demux_ver = WebPGetDemuxVersion();
@@ -52,375 +44,8 @@ static void print_webp_version() {
printf("webp demuxer version: %d.%d.%d\n", (demux_ver>>16)&0xff, (demux_ver>>8)&0xff, demux_ver&0xff);
}
-typedef struct {
- uint8_t *data;
- size_t len;
- int width, height, stride;
-} Spritesheet;
-
// let it be global variable for now
-static Animation *img = NULL;
-static int cols = 1;
-static volatile UIWindow *modal_win = NULL;
-static UIImageDisplay *img_disp = NULL;
-static UILabel *lbl_header = NULL;
-
-//@todo headers!
-//Animation * ImageLoad(const char *);
-//void ImageUnload(Animation **);
-void PreviewUpdate(Animation *, UIImageDisplay *);
-
-// @todo collision with ms windows function
-int ReadFile(const char *filename, const uint8_t **data, size_t *size) {
- assert(data != NULL);
- assert(size != NULL);
-
- *data = NULL;
- *size = 0;
- FILE *infile = fopen(filename, "rb");
- assert(infile != NULL);
- fseek(infile, 0, SEEK_END);
- size_t fsize = ftell(infile);
- // printf("%s: %zu bytes\n", filename, fsize);
- fseek(infile, 0, SEEK_SET);
-
- uint8_t *fdata = malloc(fsize+1);
- assert(fdata != NULL);
- fdata[fsize] = '\0';
- int ok = (fread(fdata, fsize, 1, infile)==1);
- fclose(infile);
-
- if (!ok) {
- fprintf(stderr, "cannot read file %s (%d)\n", filename, ok);
- free(fdata);
- return -1;
- }
- *data = fdata;
- *size = fsize;
- return 0;
-}
-
-Spritesheet GenSpritesheet(Animation *img, int cols) {
- int rows = (int)img->frame_count / cols;
- if ((int)img->frame_count % cols > 0) {
- ++rows;
- }
- size_t frame_size = img->width * img->height * sizeof(uint32_t);
- size_t line_size = img->width * sizeof(uint32_t);
- size_t full_line = line_size * cols;
- uint8_t *merged = calloc(rows * cols, frame_size);
- assert(merged!=NULL);
- uint8_t *merged_orig = merged;
- for (int row = 0; row < rows; ++row) {
- for (int y = 0; y < img->height; ++y) {
- for (int col = 0; col < cols; ++col) {
- uint32_t offset = row*cols+col;
- if (offset < img->frame_count) {
- memcpy(merged, img->frames[offset].rgba+y*line_size, line_size);
- }
- merged += line_size;
- }
- }
- }
- int stride = full_line;
- Spritesheet ss = {0};
- ss.data = merged_orig;
- ss.stride = stride;
- ss.width = cols * img->width;
- ss.height = rows * img->height;
- ss.len = ss.width * ss.height * sizeof(uint32_t);
-
- return ss;
-}
-
-void WebpWrite(const char *path, Animation *img) {
- Spritesheet ss = GenSpritesheet(img, cols);
- uint8_t *out;
- FILE *fp = fopen(path, "wb");
- assert(fp!=NULL);
- size_t encoded = WebPEncodeLosslessRGBA(ss.data, ss.width, ss.height, ss.stride, &out);
- // printf("size: %zu, encoded: %zu\n", img->width*img->height*sizeof(uint32_t), encoded);
- assert(encoded!=0);
- size_t written = fwrite(out, sizeof(uint8_t), encoded, fp);
- assert(written==encoded);
- WebPFree(out);
- free(ss.data);
-}
-
-int WinModalEvent(UIElement *element, UIMessage msg, int di, void *dp) {
- if (msg == UI_MSG_DESTROY) {
- // printf("modal bye\n");
- assert(element==modal_win);
- modal_win = NULL;
- }
- return 0;
-}
-
-int ButtonDialogSaveEvent(UIElement *element, UIMessage msg, int di, void *dp) {
- if (msg == UI_MSG_CLICKED) {
- // get values of path and filename inputs
- UITextbox *path_input = (UITextbox *)element->parent->children;
- UITextbox *filename_input = (UITextbox *)path_input->e.next;
- // printf("path_input: %p\nfilename_input: %p\n", path_input, filename_input);
- // that printf might work incorrectly because path_input->string might not contain valid C string with \0 at the end
- // printf("path_input: %s(%d)\nfilename_input: %s(%d)\n", path_input->string, path_input->bytes, filename_input->string, filename_input->bytes);
- int path_len = path_input->bytes + filename_input->bytes + 2; // DIR_SEPARATOR and '\0'
- // printf("path_len: %d\n", path_len);
- char out_name[path_len];
- out_name[path_input->bytes] = DIR_SEPARATOR;
- out_name[path_len-1] = '\0';
- memcpy(out_name, path_input->string, path_input->bytes);
- memcpy(out_name+path_input->bytes+1, filename_input->string, filename_input->bytes);
- // printf("strlen(out_name): %d\n", strlen(out_name));
- // printf("out_name: %s\n", out_name);
- WebpWrite(out_name, img);
- // printf("save dialog window close\n");
- assert(element->window==modal_win);
- UIElementDestroy(element->window);
- }
- return 0;
- }
-
-int ButtonDialogOpenEvent(UIElement *element, UIMessage msg, int di, void *dp) {
- if (msg == UI_MSG_CLICKED) {
- // printf("open dialog window close\n");
- UITextbox *path_input = (UITextbox *)element->parent->children;
- UITextbox *filename_input = (UITextbox *)path_input->e.next;
- // printf("path_input: %p\nfilename_input: %p\n", path_input, filename_input);
- // printf("path_input: %s(%d)\nfilename_input: %s(%d)\n", path_input->string, strlen(path_input->string), filename_input->string, strlen(filename_input->string));
-
- UIElementDestroy(element->window);
-
- int path_len = path_input->bytes + filename_input->bytes + 2; // DIR_SEPARATOR and '\0'
- char filepath[path_len];
- filepath[path_input->bytes] = DIR_SEPARATOR;
- filepath[path_len-1] = '\0';
- memcpy(filepath, path_input->string, path_input->bytes);
- memcpy(filepath+path_input->bytes+1, filename_input->string, filename_input->bytes);
-
- if (img != NULL) {
- ImageUnload(&img);
- assert(img==NULL);
- }
- img = ImageLoad(filepath);
- assert(img!=NULL);
- PreviewUpdate(img, img_disp);
- UILabelSetContent(lbl_header, img->path, -1);
- }
- return 0;
-}
-
-static int FilelistFilter(const struct dirent *f) {
- if (strncmp(f->d_name, ".", 255)==0) return 0;
- if (f->d_type==DT_DIR) return 1;
- //@todo not very efficient
- size_t len = strlen(f->d_name);
- // checking for the ".webp" at the very end
- if (len<6) return 0;
- return strncmp(f->d_name+len-5, ".webp", 255)==0;
-}
-
-static int FilelistCompare(const struct dirent **a, const struct dirent **b) {
- if ((*a)->d_type==DT_DIR && (*b)->d_type==DT_DIR) {
- if (strncmp((*a)->d_name, "..", 255)==0) return -1;
- else if (strncmp((*b)->d_name, "..", 255)==0) return 1;
- else return alphasort(a, b);
- }
- if ((*a)->d_type==DT_DIR) return -1;
- if ((*b)->d_type==DT_DIR) return 1;
- return alphasort(a, b);
-}
-
-int TableEvent(UIElement *element, UIMessage msg, int di, void *dp) {
- static int selected = -1;
- static struct dirent **filelist = NULL;
- char *dir_name = element->cp;
- UITable *table = (UITable *)element;
- if (filelist==NULL) {
- table->itemCount = scandir(dir_name, &filelist, FilelistFilter, FilelistCompare);
- // printf("populated dir %s with %d items\n", dir_name, table->itemCount);
- }
- if (msg==UI_MSG_TABLE_GET_ITEM) {
- UITableGetItem *m = (UITableGetItem *)dp;
- m->isSelected = selected==m->index;
- bool is_dir = filelist[m->index]->d_type==DT_DIR;
- //printf("render %s (%d)\n", filelist[m->index]->d_name, m->bufferBytes);
- int ret = snprintf(m->buffer, m->bufferBytes, is_dir?"[%s]":"%s", filelist[m->index]->d_name);
- //printf("%s - %d (%d)\n", filelist[m->index]->d_name, ret, m->bufferBytes);
- return ret;
- } else if (msg==UI_MSG_CLICKED) {
- int hit = UITableHitTest(table, element->window->cursorX, element->window->cursorY);
- if (hit!=-1 && selected==hit) {
- if (filelist[hit]->d_type==DT_DIR) {
- char newpath[PATH_MAX];
- bzero(newpath, PATH_MAX);
- strcpy(newpath, element->cp);
- strcat(newpath, "/");
- strcat(newpath, filelist[hit]->d_name);
- free(element->cp);
- element->cp = realpath(newpath, NULL);
- //free(filelist);
- filelist = NULL;
- selected = -1;
- //@todo duplicated code
- UIPanel *panel_out = (UIPanel *)element->parent;
- UILabel *label = (UILabel *)panel_out->e.children;
- UIPanel *panel_top = (UIPanel *)label->e.next;
- UITextbox *path_input = (UITextbox *)panel_top->e.children;
- UITextboxClear(path_input, false);
- UITextboxReplace(path_input, (char *)element->cp, -1, false);
- UIElementRepaint(path_input, NULL);
- UITableEnsureVisible(table, -1);
- }
- } else {
- selected = hit;
- if (!UITableEnsureVisible(table, selected)) {
- UIElementRepaint(element, NULL);
- }
- //@todo remove copypasta
- if (hit!=-1 && filelist[hit]->d_type!=DT_DIR) {
- UIPanel *panel_out = (UIPanel *)element->parent;
- UILabel *label = (UILabel *)panel_out->e.children;
- UIPanel *panel_top = (UIPanel *)label->e.next;
- UITextbox *path_input = (UITextbox *)panel_top->e.children;
- UITextbox *file_input = (UITextbox *)path_input->e.next;
- UITextboxClear(path_input, false);
- UITextboxClear(file_input, false);
- UITextboxReplace(path_input, (char *)element->cp, -1, false);
- UITextboxReplace(file_input, filelist[hit]->d_name, -1, false);
- UIElementRepaint(path_input, NULL);
- UIElementRepaint(file_input, NULL);
- }
- }
- } else if (msg==UI_MSG_DESTROY) {
- printf("destroy\n");
- // free(filelist);
- filelist = NULL;
- selected = -1;
- } else if (msg==UI_MSG_UPDATE) {
- UITableResizeColumns(table);
- }
- return 0;
-}
-
-int ButtonCloseEvent(UIElement *element, UIMessage msg, int di, void *dp) {
- if (msg == UI_MSG_CLICKED) {
- UIElementDestroy(element->window);
- }
- return 0;
-}
-
-typedef int (*CallbackFn)(struct UIElement *element, UIMessage message, int di, void *dp);
-
-void ShowModalWindow(UIWindow *parent, const char *def_dir, const char *def_file, CallbackFn cb) {
- if (modal_win == NULL) {
- // printf("create modal window\n");
- modal_win = UIWindowCreate(parent, NULL, def_file?"Save File":"Open File", WIN_WIDTH, WIN_HEIGHT);
- modal_win->e.messageUser = WinModalEvent;
- UIPanel *panel_out = UIPanelCreate(&modal_win->e, UI_PANEL_GRAY|UI_PANEL_EXPAND);
- UILabel *lbl_title = UILabelCreate(&panel_out->e, 0, def_file?"Save File":"Open File", -1);
- UIPanel *panel_top = UIPanelCreate(&panel_out->e, UI_PANEL_GRAY|UI_PANEL_MEDIUM_SPACING|UI_PANEL_HORIZONTAL);
- UITextbox *path_input = UITextboxCreate(&panel_top->e, UI_ELEMENT_DISABLED|UI_ELEMENT_H_FILL);
- if (def_dir != NULL) {
- UITextboxReplace(path_input, def_dir, -1, false);
- }
-
- UITextbox *filename_input = UITextboxCreate(&panel_top->e, UI_ELEMENT_TAB_STOP|UI_ELEMENT_H_FILL);
-
- printf("path_input: %p\nfilename_input: %p\n", path_input, filename_input);
- if (def_file != NULL) {
- UITextboxReplace(filename_input, def_file, -1, false);
- }
-
- UIButton *btn_save = UIButtonCreate(&panel_top->e, UI_ELEMENT_TAB_STOP, def_file?"Save":"Open", -1);
- btn_save->e.messageUser = cb;
-
- UIButton *btn_cancel = UIButtonCreate(&panel_top->e, UI_ELEMENT_TAB_STOP, "Cancel", -1);
- btn_cancel->e.messageUser = ButtonCloseEvent;
-
- UITable *table = UITableCreate(&panel_out->e, UI_ELEMENT_V_FILL, "Directory");
- table->itemCount = 1; // at least '..' element
- // @todo this way setting the initial directory is kinda trashcan
- char *init_path = (char *)malloc(2*sizeof(char));
- init_path[0] = '.';
- init_path[1] = '\0';
- table->e.cp = (void *)init_path;
- table->e.messageUser = TableEvent;
- UITableResizeColumns(table);
- } else {
- UIElementFocus(modal_win);
- }
-}
-
-int ButtonSaveEvent(UIElement *element, UIMessage msg, int di, void *dp) {
- if (msg == UI_MSG_CLICKED) {
- // printf("save button clicked\n");
- char fname[strlen(img->filename)+7];
- int n = snprintf(fname, NAME_MAX, "atlas_%s", img->filename);
- assert(n>0);
- ShowModalWindow(element->window, img->dirname, fname, ButtonDialogSaveEvent);
- }
- return 0;
-}
-
-int ButtonOpenEvent(UIElement *element, UIMessage msg, int di, void *dp) {
- if (msg == UI_MSG_CLICKED) {
- // printf("open button clicked\n");
- ShowModalWindow(element->window, getenv("HOME"), NULL, ButtonDialogOpenEvent);
- }
- return 0;
-}
-
-void PreviewUpdate(Animation *img, UIImageDisplay *img_disp) {
- // gen new spritesheet and refresh the preview
- Spritesheet ss = GenSpritesheet(img, cols);
- //printf("spritesheet width: %d, height: %d, stride: %d, len: %zu\n", ss.width, ss.height, ss.stride, ss.len);
-
- uint32_t *frame0 = calloc(ss.width*ss.height, sizeof(uint32_t));
- assert(frame0!=NULL);
-
- for (uint32_t i=0; i<ss.width*ss.height; ++i) {
- frame0[i] = UI_COLOR_FROM_RGBA(ss.data[i*4+0], ss.data[i*4+1], ss.data[i*4+2], ss.data[i*4+3]);
- }
- UIImageDisplaySetContent(img_disp, frame0, ss.width, ss.height, ss.stride);
- UIElementRefresh(img_disp->e.parent);
- UIElementRefresh(&img_disp->e);
-
- free(frame0);
- free(ss.data);
-
- UILabel *label = (UILabel *)img_disp->e.cp;
- char label_text[256] = {0};
- snprintf(&label_text, 255, "Width: %3d tiles", cols);
- UILabelSetContent(label, label_text, -1);
- UIElementRefresh(label->e.parent);
-}
-
-int SliderEvent(UIElement *element, UIMessage msg, int di, void *dp) {
- if (msg == UI_MSG_VALUE_CHANGED) {
- if (img == NULL) return 1;
-
- float slider_pos = ((UISlider *)element)->position;
- float step = 1.0f / (float)img->frame_count;
- int new_cols = (int)(slider_pos / step);
- if (new_cols > 0 && cols != new_cols) {
- //printf("new_cols: %d\n", new_cols);
- cols = new_cols;
- PreviewUpdate(img, element->cp);
- }
- }
-
- return 0;
-}
-
-int WinMainEvent(UIElement *element, UIMessage msg, int di, void *dp) {
- if (msg == UI_MSG_DESTROY) {
- // printf("bye\n");
- free(img);
- exit(0);
- }
- return 0;
-}
+//static Animation *img = NULL;
#ifdef UI_LINUX
int main(int argc, const char **argv) {
@@ -429,39 +54,20 @@ int WinMain(HINSTANCE instance, HINSTANCE previousInstance, LPSTR commandLine, i
#endif
// just some silly stuff, always wanted to try that
- prctl(PR_SET_NAME, "webp-anim-2-spritesheet");
- // printf("argv[0]: %s\n", argv[0]);
+ prctl(PR_SET_NAME, "webp-anim-to-spritesheet");
atexit(print_webp_version);
UIInitialise();
- UIWindow *win = UIWindowCreate(0, 0, "emote2ss", WIN_WIDTH, WIN_HEIGHT);
- win->e.messageUser = WinMainEvent;
- UIPanel *panelv = UIPanelCreate(&win->e, UI_PANEL_GRAY|UI_PANEL_MEDIUM_SPACING);
- lbl_header = UILabelCreate(&panelv->e, 0, "filename", -1);
-
- UIPanel *panelh = UIPanelCreate(&panelv->e, UI_PANEL_GRAY|UI_PANEL_MEDIUM_SPACING|UI_PANEL_HORIZONTAL);
- UILabel *label = UILabelCreate(&panelh->e, 0, "webp to spritesheet converter", -1);
- UISlider *slider = UISliderCreate(&panelh->e, 0);
- UIButton *btnopen = UIButtonCreate(&panelh->e, 0, "Open", -1);
- btnopen->e.messageUser = ButtonOpenEvent;
- UIButton *btnsave = UIButtonCreate(&panelh->e, 0, "Save", -1);
- btnsave->e.messageUser = ButtonSaveEvent;
- btnsave->e.cp = win;
- UIButton *btn_exit = UIButtonCreate(&panelh->e, 0, "Close", -1);
- btn_exit->e.messageUser = ButtonCloseEvent;
-
- img_disp = UIImageDisplayCreate(&panelv->e, UI_ELEMENT_V_FILL|UI_ELEMENT_H_FILL|UI_IMAGE_DISPLAY_INTERACTIVE|_UI_IMAGE_DISPLAY_ZOOM_FIT, NULL, 0, 0, 0);
- img_disp->e.cp = label;
- slider->e.messageUser = SliderEvent;
- slider->e.cp = img_disp;
+ UIWindow *win = MainWindowCreate("emote2ss", WIN_WIDTH, WIN_HEIGHT);
- if (argc > 1) {
+ // do I actually need this?
+ /*if (argc > 1) {
img = ImageLoad(argv[1]);
assert(img!=NULL);
PreviewUpdate(img, img_disp);
- }
+ }*/
return UIMessageLoop();
}
diff --git a/gui/spritesheet.c b/gui/spritesheet.c
@@ -0,0 +1,36 @@
+#include <assert.h>
+#include "spritesheet.h"
+
+Spritesheet SpritesheetGen(Animation *img, int cols) {
+ int rows = (int)img->frame_count / cols;
+ if ((int)img->frame_count % cols > 0) {
+ ++rows;
+ }
+ size_t frame_size = img->width * img->height * sizeof(uint32_t);
+ size_t line_size = img->width * sizeof(uint32_t);
+ size_t full_line = line_size * cols;
+ uint8_t *merged = calloc(rows * cols, frame_size);
+ assert(merged!=NULL);
+ uint8_t *merged_orig = merged;
+ for (int row = 0; row < rows; ++row) {
+ for (int y = 0; y < img->height; ++y) {
+ for (int col = 0; col < cols; ++col) {
+ uint32_t offset = row*cols+col;
+ if (offset < img->frame_count) {
+ memcpy(merged, img->frames[offset].rgba+y*line_size, line_size);
+ }
+ merged += line_size;
+ }
+ }
+ }
+ int stride = full_line;
+ Spritesheet ss = {0};
+ ss.data = merged_orig;
+ ss.stride = stride;
+ ss.width = cols * img->width;
+ ss.height = rows * img->height;
+ ss.len = ss.width * ss.height * sizeof(uint32_t);
+
+ return ss;
+}
+
diff --git a/gui/spritesheet.h b/gui/spritesheet.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include <stdint.h>
+#include <stddef.h>
+#include "animation.h"
+
+typedef struct {
+ uint8_t *data;
+ size_t len;
+ int width, height, stride;
+} Spritesheet;
+
+Spritesheet SpritesheetGen(Animation *img, int cols);
diff --git a/gui/ui.c b/gui/ui.c
@@ -0,0 +1,316 @@
+#define _DEFAULT_SOURCE
+
+#include <dirent.h>
+#include "fileops.h"
+#include "animation.h"
+#include "ui.h"
+#include "spritesheet.h"
+
+static int cols = 1;
+static volatile UIWindow *modal_win = NULL;
+static UIImageDisplay *img_disp = NULL;
+static UILabel *lbl_header = NULL;
+static Animation *img = NULL;
+
+static int FilelistFilter(const struct dirent *f) {
+ if (strncmp(f->d_name, ".", 255)==0) return 0;
+ if (f->d_type==DT_DIR) return 1;
+ //@todo not very efficient
+ size_t len = strlen(f->d_name);
+ // checking for the ".webp" at the very end
+ if (len<6) return 0;
+ return strncmp(f->d_name+len-5, ".webp", 255)==0;
+}
+
+static int FilelistCompare(const struct dirent **a, const struct dirent **b) {
+ if ((*a)->d_type==DT_DIR && (*b)->d_type==DT_DIR) {
+ if (strncmp((*a)->d_name, "..", 255)==0) return -1;
+ else if (strncmp((*b)->d_name, "..", 255)==0) return 1;
+ else return alphasort(a, b);
+ }
+ if ((*a)->d_type==DT_DIR) return -1;
+ if ((*b)->d_type==DT_DIR) return 1;
+ return alphasort(a, b);
+}
+
+UIWindow * MainWindowCreate(const char *wname, int w, int h) {
+ UIWindow *win = UIWindowCreate(0, 0, wname, w, h);
+ win->e.messageUser = WinMainEvent;
+ UIPanel *panelv = UIPanelCreate(&win->e, UI_PANEL_GRAY|UI_PANEL_MEDIUM_SPACING);
+ lbl_header = UILabelCreate(&panelv->e, 0, "filename", -1);
+
+ UIPanel *panelh = UIPanelCreate(&panelv->e, UI_PANEL_GRAY|UI_PANEL_MEDIUM_SPACING|UI_PANEL_HORIZONTAL);
+ UILabel *label = UILabelCreate(&panelh->e, 0, "webp to spritesheet converter", -1);
+ UISlider *slider = UISliderCreate(&panelh->e, 0);
+ UIButton *btnopen = UIButtonCreate(&panelh->e, 0, "Open", -1);
+ btnopen->e.messageUser = ButtonOpenEvent;
+ UIButton *btnsave = UIButtonCreate(&panelh->e, 0, "Save", -1);
+ btnsave->e.messageUser = ButtonSaveEvent;
+ btnsave->e.cp = win;
+ UIButton *btn_exit = UIButtonCreate(&panelh->e, 0, "Close", -1);
+ btn_exit->e.messageUser = ButtonCloseEvent;
+
+ img_disp = UIImageDisplayCreate(&panelv->e, UI_ELEMENT_V_FILL|UI_ELEMENT_H_FILL|UI_IMAGE_DISPLAY_INTERACTIVE|_UI_IMAGE_DISPLAY_ZOOM_FIT, NULL, 0, 0, 0);
+ img_disp->e.cp = label;
+ slider->e.messageUser = SliderEvent;
+ slider->e.cp = img_disp;
+ return win;
+}
+
+int WinModalEvent(UIElement *element, UIMessage msg, int di, void *dp) {
+ if (msg == UI_MSG_DESTROY) {
+ // printf("modal bye\n");
+ assert(element==modal_win);
+ modal_win = NULL;
+ }
+ return 0;
+}
+
+int ButtonDialogSaveEvent(UIElement *element, UIMessage msg, int di, void *dp) {
+ if (msg == UI_MSG_CLICKED) {
+ // get values of path and filename inputs
+ UITextbox *path_input = (UITextbox *)element->parent->children;
+ UITextbox *filename_input = (UITextbox *)path_input->e.next;
+ // printf("path_input: %p\nfilename_input: %p\n", path_input, filename_input);
+ // that printf might work incorrectly because path_input->string might not contain valid C string with \0 at the end
+ // printf("path_input: %s(%d)\nfilename_input: %s(%d)\n", path_input->string, path_input->bytes, filename_input->string, filename_input->bytes);
+ int path_len = path_input->bytes + filename_input->bytes + 2; // DIR_SEPARATOR and '\0'
+ // printf("path_len: %d\n", path_len);
+ char out_name[path_len];
+ out_name[path_input->bytes] = DIR_SEPARATOR;
+ out_name[path_len-1] = '\0';
+ memcpy(out_name, path_input->string, path_input->bytes);
+ memcpy(out_name+path_input->bytes+1, filename_input->string, filename_input->bytes);
+ // printf("strlen(out_name): %d\n", strlen(out_name));
+ // printf("out_name: %s\n", out_name);
+ WebpWrite(out_name, img, cols);
+ // printf("save dialog window close\n");
+ assert(element->window==modal_win);
+ UIElementDestroy(element->window);
+ }
+ return 0;
+ }
+
+int ButtonDialogOpenEvent(UIElement *element, UIMessage msg, int di, void *dp) {
+ if (msg == UI_MSG_CLICKED) {
+ // printf("open dialog window close\n");
+ UITextbox *path_input = (UITextbox *)element->parent->children;
+ UITextbox *filename_input = (UITextbox *)path_input->e.next;
+ // printf("path_input: %p\nfilename_input: %p\n", path_input, filename_input);
+ // printf("path_input: %s(%d)\nfilename_input: %s(%d)\n", path_input->string, strlen(path_input->string), filename_input->string, strlen(filename_input->string));
+
+ UIElementDestroy(element->window);
+
+ int path_len = path_input->bytes + filename_input->bytes + 2; // DIR_SEPARATOR and '\0'
+ char filepath[path_len];
+ filepath[path_input->bytes] = DIR_SEPARATOR;
+ filepath[path_len-1] = '\0';
+ memcpy(filepath, path_input->string, path_input->bytes);
+ memcpy(filepath+path_input->bytes+1, filename_input->string, filename_input->bytes);
+
+ if (img != NULL) {
+ ImageUnload(&img);
+ assert(img==NULL);
+ }
+ img = ImageLoad(filepath);
+ assert(img!=NULL);
+ PreviewUpdate(img, img_disp);
+ UILabelSetContent(lbl_header, img->path, -1);
+ }
+ return 0;
+}
+
+int TableEvent(UIElement *element, UIMessage msg, int di, void *dp) {
+ static int selected = -1;
+ static struct dirent **filelist = NULL;
+ char *dir_name = element->cp;
+ UITable *table = (UITable *)element;
+ if (filelist==NULL) {
+ table->itemCount = scandir(dir_name, &filelist, FilelistFilter, FilelistCompare);
+ // printf("populated dir %s with %d items\n", dir_name, table->itemCount);
+ }
+ if (msg==UI_MSG_TABLE_GET_ITEM) {
+ UITableGetItem *m = (UITableGetItem *)dp;
+ m->isSelected = selected==m->index;
+ bool is_dir = filelist[m->index]->d_type==DT_DIR;
+ //printf("render %s (%d)\n", filelist[m->index]->d_name, m->bufferBytes);
+ int ret = snprintf(m->buffer, m->bufferBytes, is_dir?"[%s]":"%s", filelist[m->index]->d_name);
+ //printf("%s - %d (%d)\n", filelist[m->index]->d_name, ret, m->bufferBytes);
+ return ret;
+ } else if (msg==UI_MSG_CLICKED) {
+ int hit = UITableHitTest(table, element->window->cursorX, element->window->cursorY);
+ if (hit!=-1 && selected==hit) {
+ if (filelist[hit]->d_type==DT_DIR) {
+ char newpath[PATH_MAX];
+ bzero(newpath, PATH_MAX);
+ strcpy(newpath, element->cp);
+ strcat(newpath, "/");
+ strcat(newpath, filelist[hit]->d_name);
+ free(element->cp);
+ element->cp = realpath(newpath, NULL);
+ //free(filelist);
+ filelist = NULL;
+ selected = -1;
+ //@todo duplicated code
+ UIPanel *panel_out = (UIPanel *)element->parent;
+ UILabel *label = (UILabel *)panel_out->e.children;
+ UIPanel *panel_top = (UIPanel *)label->e.next;
+ UITextbox *path_input = (UITextbox *)panel_top->e.children;
+ UITextboxClear(path_input, false);
+ UITextboxReplace(path_input, (char *)element->cp, -1, false);
+ UIElementRepaint(path_input, NULL);
+ UITableEnsureVisible(table, -1);
+ }
+ } else {
+ selected = hit;
+ if (!UITableEnsureVisible(table, selected)) {
+ UIElementRepaint(element, NULL);
+ }
+ //@todo remove copypasta
+ if (hit!=-1 && filelist[hit]->d_type!=DT_DIR) {
+ UIPanel *panel_out = (UIPanel *)element->parent;
+ UILabel *label = (UILabel *)panel_out->e.children;
+ UIPanel *panel_top = (UIPanel *)label->e.next;
+ UITextbox *path_input = (UITextbox *)panel_top->e.children;
+ UITextbox *file_input = (UITextbox *)path_input->e.next;
+ UITextboxClear(path_input, false);
+ UITextboxClear(file_input, false);
+ UITextboxReplace(path_input, (char *)element->cp, -1, false);
+ UITextboxReplace(file_input, filelist[hit]->d_name, -1, false);
+ UIElementRepaint(path_input, NULL);
+ UIElementRepaint(file_input, NULL);
+ }
+ }
+ } else if (msg==UI_MSG_DESTROY) {
+ free(filelist);
+ filelist = NULL;
+ selected = -1;
+ UIElement *el = table->e.children;
+ printf("child %p\n", el);
+ while (el!=NULL) {
+ printf("child %p\n", el);
+ }
+ } else if (msg==UI_MSG_UPDATE) {
+ UITableResizeColumns(table);
+ }
+ return 0;
+}
+
+int ButtonCloseEvent(UIElement *element, UIMessage msg, int di, void *dp) {
+ if (msg == UI_MSG_CLICKED) {
+ UIElementDestroy(element->window);
+ }
+ return 0;
+}
+
+void ShowModalWindow(UIWindow *parent, const char *def_dir, const char *def_file, CallbackFn cb) {
+ if (modal_win == NULL) {
+ // printf("create modal window\n");
+ int w = UIElementMessage(parent, UI_MSG_GET_WIDTH, 0, 0);
+ int h = UIElementMessage(parent, UI_MSG_GET_HEIGHT, 0, 0);
+ modal_win = UIWindowCreate(parent, NULL, def_file?"Save File":"Open File", w, h);
+ modal_win->e.messageUser = WinModalEvent;
+ UIPanel *panel_out = UIPanelCreate(&modal_win->e, UI_PANEL_GRAY|UI_PANEL_EXPAND);
+ UILabel *lbl_title = UILabelCreate(&panel_out->e, 0, def_file?"Save File":"Open File", -1);
+ UIPanel *panel_top = UIPanelCreate(&panel_out->e, UI_PANEL_GRAY|UI_PANEL_MEDIUM_SPACING|UI_PANEL_HORIZONTAL);
+ UITextbox *path_input = UITextboxCreate(&panel_top->e, UI_ELEMENT_DISABLED|UI_ELEMENT_H_FILL);
+ if (def_dir != NULL) {
+ UITextboxReplace(path_input, def_dir, -1, false);
+ }
+
+ UITextbox *filename_input = UITextboxCreate(&panel_top->e, UI_ELEMENT_TAB_STOP|UI_ELEMENT_H_FILL);
+
+ printf("path_input: %p\nfilename_input: %p\n", path_input, filename_input);
+ if (def_file != NULL) {
+ UITextboxReplace(filename_input, def_file, -1, false);
+ }
+
+ UIButton *btn_save = UIButtonCreate(&panel_top->e, UI_ELEMENT_TAB_STOP, def_file?"Save":"Open", -1);
+ btn_save->e.messageUser = cb;
+
+ UIButton *btn_cancel = UIButtonCreate(&panel_top->e, UI_ELEMENT_TAB_STOP, "Cancel", -1);
+ btn_cancel->e.messageUser = ButtonCloseEvent;
+
+ UITable *table = UITableCreate(&panel_out->e, UI_ELEMENT_V_FILL, "Directory");
+ table->itemCount = 1; // at least '..' element
+ // @todo this way setting the initial directory is kinda trashcan
+ char *init_path = (char *)malloc(2*sizeof(char));
+ init_path[0] = '.';
+ init_path[1] = '\0';
+ table->e.cp = (void *)init_path;
+ table->e.messageUser = TableEvent;
+ UITableResizeColumns(table);
+ } else {
+ UIElementFocus(modal_win);
+ }
+}
+
+int ButtonSaveEvent(UIElement *element, UIMessage msg, int di, void *dp) {
+ if (msg == UI_MSG_CLICKED) {
+ // printf("save button clicked\n");
+ char fname[strlen(img->filename)+7];
+ int n = snprintf(fname, NAME_MAX, "atlas_%s", img->filename);
+ assert(n>0);
+ ShowModalWindow(element->window, img->dirname, fname, ButtonDialogSaveEvent);
+ }
+ return 0;
+}
+
+int ButtonOpenEvent(UIElement *element, UIMessage msg, int di, void *dp) {
+ if (msg == UI_MSG_CLICKED) {
+ // printf("open button clicked\n");
+ ShowModalWindow(element->window, getenv("HOME"), NULL, ButtonDialogOpenEvent);
+ }
+ return 0;
+}
+
+int SliderEvent(UIElement *element, UIMessage msg, int di, void *dp) {
+ if (msg == UI_MSG_VALUE_CHANGED) {
+ if (img == NULL) return 1;
+
+ float slider_pos = ((UISlider *)element)->position;
+ float step = 1.0f / (float)img->frame_count;
+ int new_cols = (int)(slider_pos / step);
+ if (new_cols > 0 && cols != new_cols) {
+ //printf("new_cols: %d\n", new_cols);
+ cols = new_cols;
+ PreviewUpdate(img, element->cp);
+ }
+ }
+
+ return 0;
+}
+
+int WinMainEvent(UIElement *element, UIMessage msg, int di, void *dp) {
+ if (msg == UI_MSG_DESTROY) {
+ // printf("bye\n");
+ free(img);
+ exit(0);
+ }
+ return 0;
+}
+
+void PreviewUpdate(Animation *img, UIImageDisplay *img_disp) {
+ // gen new spritesheet and refresh the preview
+ Spritesheet ss = SpritesheetGen(img, cols);
+ //printf("spritesheet width: %d, height: %d, stride: %d, len: %zu\n", ss.width, ss.height, ss.stride, ss.len);
+
+ uint32_t *frame0 = calloc(ss.width*ss.height, sizeof(uint32_t));
+ assert(frame0!=NULL);
+
+ for (uint32_t i=0; i<ss.width*ss.height; ++i) {
+ frame0[i] = UI_COLOR_FROM_RGBA(ss.data[i*4+0], ss.data[i*4+1], ss.data[i*4+2], ss.data[i*4+3]);
+ }
+ UIImageDisplaySetContent(img_disp, frame0, ss.width, ss.height, ss.stride);
+ UIElementRefresh(img_disp->e.parent);
+ UIElementRefresh(&img_disp->e);
+
+ free(frame0);
+ free(ss.data);
+
+ UILabel *label = (UILabel *)img_disp->e.cp;
+ char label_text[256] = {0};
+ snprintf(&label_text, 255, "Width: %3d tiles", cols);
+ UILabelSetContent(label, label_text, -1);
+ UIElementRefresh(label->e.parent);
+}
diff --git a/gui/ui.h b/gui/ui.h
@@ -0,0 +1,28 @@
+/* user interface */
+#pragma once
+
+#ifdef _WIN32
+#define DIR_SEPARATOR '\\'
+#else
+#define DIR_SEPARATOR '/'
+#endif
+
+#include "luigi.h"
+
+#define UI_COLOR_FROM_RGBA(r, g, b, a) (((uint32_t) (r) << 16) | ((uint32_t) (g) << 8) | ((uint32_t) (b) << 0) | ((uint32_t) (a) << 24))
+
+typedef int (*CallbackFn)(struct UIElement *element, UIMessage message, int di, void *dp);
+
+UIWindow * MainWindowCreate(const char *wname, int w, int h);
+
+void PreviewUpdate(Animation *, UIImageDisplay *);
+void ShowModalWindow(UIWindow *parent, const char *def_dir, const char *def_file, CallbackFn cb);
+int WinMainEvent(UIElement *element, UIMessage msg, int di, void *dp);
+int WinModalEvent(UIElement *element, UIMessage msg, int di, void *dp);
+int SliderEvent(UIElement *element, UIMessage msg, int di, void *dp);
+int ButtonOpenEvent(UIElement *element, UIMessage msg, int di, void *dp);
+int ButtonSaveEvent(UIElement *element, UIMessage msg, int di, void *dp);
+int ButtonCloseEvent(UIElement *element, UIMessage msg, int di, void *dp);
+int ButtonDialogSaveEvent(UIElement *element, UIMessage msg, int di, void *dp);
+int ButtonDialogOpenEvent(UIElement *element, UIMessage msg, int di, void *dp);
+int TableEvent(UIElement *element, UIMessage msg, int di, void *dp);