From d0856d100b5029751d89d34f610d220da9a84f1d Mon Sep 17 00:00:00 2001 From: Lephenixnoir Date: Sun, 19 Jun 2022 22:36:39 +0100 Subject: [PATCH] jfileselect: add a new file selection widget --- CMakeLists.txt | 3 + include/justui/defs.h | 4 +- include/justui/jevent.h | 2 +- include/justui/jfileselect.h | 88 +++++++++ include/justui/jwidget-api.h | 2 +- src/jfileselect.c | 342 +++++++++++++++++++++++++++++++++++ 6 files changed, 437 insertions(+), 4 deletions(-) create mode 100644 include/justui/jfileselect.h create mode 100644 src/jfileselect.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 01aa56c..80903ff 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,6 +5,8 @@ project(JustUI VERSION 1.0 LANGUAGES C) find_package(Gint 2.1 REQUIRED) include(Fxconv) +set(CMAKE_INSTALL_MESSAGE LAZY) + configure_file(include/justui/config.h.in include/justui/config.h) set(ASSETS_fx @@ -26,6 +28,7 @@ add_library(${NAME} STATIC src/jinput.c src/jpainted.c src/jfkeys.c + src/jfileselect.c src/vec.c src/keymap.c ${ASSETS_${FXSDK_PLATFORM}} diff --git a/include/justui/defs.h b/include/justui/defs.h index e47fab8..645acc2 100644 --- a/include/justui/defs.h +++ b/include/justui/defs.h @@ -13,7 +13,7 @@ #include #include -/* j_dirs_t: Quadruplet with four directions */ +/* jdirs: Quadruplet with four directions */ typedef struct { uint8_t top; uint8_t right; @@ -21,7 +21,7 @@ typedef struct { uint8_t left; } jdirs; -/* j_align_t: Alignment options with both horizontal and vertical names */ +/* jalign: Alignment options with both horizontal and vertical names */ typedef enum { /* Horizontal names */ J_ALIGN_LEFT = 0, diff --git a/include/justui/jevent.h b/include/justui/jevent.h index 97a0624..7e89822 100644 --- a/include/justui/jevent.h +++ b/include/justui/jevent.h @@ -16,7 +16,7 @@ occurs in the GUI. These are mostly widget signaling state changes, validations, and other GUI specifics that might require attention. Events can either be reported to the user by the scene (upwards event) or notify - widgets of something occuring to them (downwards event). + widgets of something occurring to them (downwards event). JustUI tries hard to not invert flow control and leave the user to decide when to produce downwards events. In a normal situation, events from diff --git a/include/justui/jfileselect.h b/include/justui/jfileselect.h new file mode 100644 index 0000000..ea63cf5 --- /dev/null +++ b/include/justui/jfileselect.h @@ -0,0 +1,88 @@ +//--- +// JustUI.jfileselect: Basic file selector +//--- + +#ifndef _J_JFILESELECT +#define _J_JFILESELECT + +#include +#include +#include +#include + +/* jfileselect: Basic file selector + + This widget is used to browse the filesystem and select a file. Visually, it + only consists of a scrolling list of names showing a section of a folder's + entries. + + TODO: jfileselect: Select a new file to write to + + Events: + * JFILESELECT_LOADED when a folder is loaded into the view + * JFILESELECT_VALIDATED when a file has been selected + * JFILESELECT_CANCELED when if the user exits from the top-level folder */ +typedef struct { + jwidget widget; + + /* Folder currently being browsed */ + char *path; + /* Corresponding directory stream */ + DIR *dp; + /* Entry previously validated (with EXE) */ + char *selected_file; + + /* Number of entries in the current folder */ + int folder_entries; + /* Current cursor position (0 .. folder_entries-1) */ + int16_t cursor; + /* Current scroll position */ + int16_t scroll; + /* Number of visible lines */ + int8_t visible_lines; + + /* Additional pixels of spacing per line (base is font->height) */ + int8_t line_spacing; + /* Rendering font */ + font_t const *font; + +} jfileselect; + +/* Type IDs */ +extern uint16_t JFILESELECT_LOADED; +extern uint16_t JFILESELECT_VALIDATED; +extern uint16_t JFILESELECT_CANCELED; + +/* jfileselect_create(): Create a file selection interface + + There is no initial folder. The widget will not handle any events nor emit + any events in this state; a path must first be set before use. */ +jfileselect *jfileselect_create(void *parent); + +/* jfileselect_browse(): Browse a folder + + This function loads the specified folder and allows the user to select a + file. (Remember to give the widget focus.) A JFILESELECT_LOADED event is + emitted immediately, and further events are emitted based on user inputs. + + This function resets the selected file to NULL. + + Returns true on success, false if the path does not exist or cannot be + browsed (in that case, check errno). */ +bool jfileselect_browse(jfileselect *fs, char const *path); + +/* jfileselect_selected_file(): Get the path to the selected file + + The selected file is NULL until jfileselect_browse() is called and the user + selects a file in the interface. The returned pointer is owned by the + widget. */ +char const *jfileselect_selected_file(jfileselect *fs); + +/* jfileselect_current_folder(): Get the path to the current folder */ +char const *jfileselect_current_folder(jfileselect *fs); + +/* Trivial properties */ +void jfileselect_set_font(jfileselect *fs, font_t const *font); +void jfileselect_set_line_spacing(jfileselect *fs, int line_spacing); + +#endif /* _J_JFILESELECT */ diff --git a/include/justui/jwidget-api.h b/include/justui/jwidget-api.h index ccb78c9..643772c 100644 --- a/include/justui/jwidget-api.h +++ b/include/justui/jwidget-api.h @@ -63,7 +63,7 @@ typedef void jwidget_poly_render_t(void *w, int x, int y); key events. This function is somewhat of a catch-all function for dynamic occurrences. The widget should either accept the event, do something with it, and return true, or refuse the event, do nothing and return false. This - is influences events that propagate, such as key events. */ + influences events that propagate, such as key events. */ typedef bool jwidget_poly_event_t(void *w, jevent e); /* jwidget_poly_destroy_t: Destroy a widget's specific resources diff --git a/src/jfileselect.c b/src/jfileselect.c new file mode 100644 index 0000000..4a8fec8 --- /dev/null +++ b/src/jfileselect.c @@ -0,0 +1,342 @@ +#include +#include +#include + +#include +#include + +#include +#include +#include + +/* Type identifier for jfileselect */ +static int jfileselect_type_id = -1; + +/* Events */ +uint16_t JFILESELECT_LOADED; +uint16_t JFILESELECT_VALIDATED; +uint16_t JFILESELECT_CANCELED; + +jfileselect *jfileselect_create(void *parent) +{ + if(jfileselect_type_id < 0) return NULL; + + jfileselect *fs = malloc(sizeof *fs); + if(!fs) return NULL; + + jwidget_init(&fs->widget, jfileselect_type_id, parent); + + fs->path = NULL; + fs->dp = NULL; + fs->selected_file = NULL; + + fs->folder_entries = -1; + fs->cursor = -1; + fs->scroll = 0; + fs->visible_lines = 0; + + fs->line_spacing = 4; + fs->font = dfont_default(); + + return fs; +} + +static void count_visible_lines(jfileselect *fs) +{ + int ch = jwidget_content_height(fs); + int line_height = fs->font->line_height + fs->line_spacing; + fs->visible_lines = ch / line_height; +} + +//--- +// Getters and setters +//--- + +void jfileselect_set_font(jfileselect *fs, font_t const *font) +{ + fs->font = font ? font : dfont_default(); + count_visible_lines(fs); +} + +void jfileselect_set_line_spacing(jfileselect *fs, int line_spacing) +{ + fs->line_spacing = line_spacing; + count_visible_lines(fs); +} + +//--- +// Path and folder manipulation +//--- + +static char *path_down(char const *path, char const *name) +{ + char *child = malloc(strlen(path) + strlen(name) + 2); + if(!child) + return NULL; + + strcpy(child, path); + if(strcmp(path, "/") != 0) + strcat(child, "/"); + strcat(child, name); + + return child; +} + +static char *path_up(char const *path) +{ + char *parent = strdup(path); + if(!parent) + return NULL; + + char *p = strrchr(parent, '/'); + if(p == parent) + *(p+1) = 0; + else if(p) + *p = 0; + + return parent; +} + +static int accept_entry(struct dirent *ent) +{ + /* TODO: jfileselect: Programmable filter */ + if(!strcmp(ent->d_name, "@MainMem")) + return 0; + if(!strcmp(ent->d_name, "SAVE-F")) + return 0; + if(!strcmp(ent->d_name, ".")) + return 0; + if(!strcmp(ent->d_name, "..")) + return 0; + return 1; +} + +struct dirent *read_nth_dir_entry(DIR *dp, int n) +{ + struct dirent *entry = NULL; + rewinddir(dp); + + for(int i = 0; i <= n;) { + entry = readdir(dp); + if(!entry) + return NULL; + i += accept_entry(entry); + } + + return entry; +} + +static bool load_folder(jfileselect *fs, char *path) +{ + if(fs->dp) + closedir(fs->dp); + + fs->dp = (DIR *)gint_world_switch(GINT_CALL(opendir, path)); + if(!fs->dp) + return false; + + free(fs->path); + fs->path = path; + + fs->folder_entries = 0; + struct dirent *ent; + while ((ent = readdir(fs->dp))) + fs->folder_entries += accept_entry(ent); + + fs->widget.update = true; + + jwidget_emit(fs, (jevent){ .type = JFILESELECT_LOADED }); + return true; +} + +bool jfileselect_browse(jfileselect *fs, char const *path) +{ + char *path_copy = strdup(path); + if(!path_copy) + return false; + + if(!load_folder(fs, path_copy)) + return false; + + free(fs->selected_file); + fs->selected_file = NULL; + + fs->cursor = 0; + fs->scroll = 0; + return true; +} + +char const *jfileselect_selected_file(jfileselect *fs) +{ + return fs->selected_file; +} + +char const *jfileselect_current_folder(jfileselect *fs) +{ + return fs->path; +} + +//--- +// Polymorphic widget operations +//--- + +static void jfileselect_poly_csize(void *fs0) +{ + jfileselect *fs = fs0; + jwidget *w = &fs->widget; + + w->w = 128; + w->h = 6 * max(fs->font->line_height + fs->line_spacing, 0); +} + +static void jfileselect_poly_layout(void *fs0) +{ + jfileselect *fs = fs0; + count_visible_lines(fs); +} + +static void jfileselect_poly_render(void *fs0, int x, int y) +{ + jfileselect *fs = fs0; + if(!fs->path || !fs->dp) + return; + + font_t const *old_font = dfont(fs->font); + int line_height = fs->font->line_height + fs->line_spacing; + int cw = jwidget_content_width(fs); + + rewinddir(fs->dp); + struct dirent *ent; + char const *entry_name; + bool isfolder; + + for(int i = -fs->scroll; i < fs->visible_lines;) { + bool selected = (fs->cursor == fs->scroll + i); + + ent = readdir(fs->dp); + if(!ent) break; + if(!accept_entry(ent)) continue; + + entry_name = ent->d_name; + isfolder = (ent->d_type == DT_DIR); + + if(i < 0) { + i++; + continue; + } + + int line_y = y + line_height * i; + if(selected) + drect(x, line_y, x + cw - 1, line_y + line_height - 1, C_BLACK); + + /* Round `line_spacing / 2` down so there is more spacing below */ + dprint(x+2, line_y + (fs->line_spacing + 0) / 2, + selected ? C_WHITE : C_BLACK, + "%s%s", entry_name, isfolder ? "/" : ""); + i++; + } + + dfont(old_font); +} + +static bool jfileselect_poly_event(void *fs0, jevent e) +{ + jfileselect *fs = fs0; + if(!fs->path || !fs->dp) + return false; + + if(e.type == JWIDGET_KEY) { + key_event_t ev = e.key; + if(ev.type != KEYEV_DOWN && ev.type != KEYEV_HOLD) + return false; + int key = ev.key; + + bool moved = false; + + if(key == KEY_UP && fs->cursor > 0) { + fs->cursor = ev.shift ? 0 : fs->cursor - 1; + moved = true; + } + if(key == KEY_DOWN && fs->cursor < fs->folder_entries - 1) { + fs->cursor = ev.shift ? fs->folder_entries - 1 : fs->cursor + 1; + moved = true; + } + + if(fs->scroll > 0 && fs->cursor <= fs->scroll) + fs->scroll = max(fs->cursor - 1, 0); + if(fs->scroll + fs->visible_lines < fs->folder_entries + && fs->cursor >= fs->scroll + fs->visible_lines - 2) { + fs->scroll = min(fs->cursor - fs->visible_lines + 2, + fs->folder_entries - fs->visible_lines); + } + + if(moved) { + fs->widget.update = true; + return true; + } + + if(key == KEY_EXIT) { + if(!strcmp(fs->path, "/")) { + jwidget_emit(fs, (jevent){ .type = JFILESELECT_CANCELED }); + return true; + } + char *parent = path_up(fs->path); + if(parent) { + load_folder(fs, parent); + fs->cursor = 0; + fs->scroll = 0; + return true; + } + } + else if(key == KEY_EXE) { + struct dirent *ent = read_nth_dir_entry(fs->dp, fs->cursor); + if(ent->d_type == DT_DIR) { + char *child = path_down(fs->path, ent->d_name); + if(child) { + load_folder(fs, child); + fs->cursor = 0; + fs->scroll = 0; + return true; + } + } + else { + fs->selected_file = path_down(fs->path, ent->d_name); + if(fs->selected_file) { + jwidget_emit(fs,(jevent){ .type = JFILESELECT_VALIDATED }); + return true; + } + } + } + } + + return false; +} + +static void jfileselect_poly_destroy(void *fs0) +{ + jfileselect *fs = fs0; + + free(fs->path); + if(fs->dp) + closedir(fs->dp); + free(fs->selected_file); +} + +/* jfileselect type definition */ +static jwidget_poly type_jfileselect = { + .name = "jfileselect", + .csize = jfileselect_poly_csize, + .layout = jfileselect_poly_layout, + .render = jfileselect_poly_render, + .event = jfileselect_poly_event, + .destroy = jfileselect_poly_destroy, +}; + +__attribute__((constructor(1003))) +static void j_register_jfileselect(void) +{ + jfileselect_type_id = j_register_widget(&type_jfileselect, "jwidget"); + JFILESELECT_LOADED = j_register_event(); + JFILESELECT_VALIDATED = j_register_event(); + JFILESELECT_CANCELED = j_register_event(); +}