diff --git a/CMakeLists.txt b/CMakeLists.txt index c007d94..5f98d48 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,6 +10,7 @@ include(Fxconv) find_package(Gint 2.1 REQUIRED) find_package(LibProf 2.1 REQUIRED) find_package(LibImg 2.1 REQUIRED) +find_package(JustUI 1.0 REQUIRED) set(SOURCES src/gintctl.c @@ -32,6 +33,7 @@ set(SOURCES src/gint/timer_callbacks.c src/gint/tlb.c src/gint/topti.c + src/libs/justui.c src/libs/libimg.c src/libs/memory.c src/libs/openlibm.c @@ -46,10 +48,13 @@ set(SOURCES src/perf/memory.s src/perf/render.c src/regs/regs.c + src/widgets/gscreen.c + src/widgets/gtable.c ) set(ASSETS_fx assets-fx/fonts/hexa.png assets-fx/fonts/mini.png + assets-fx/fonts/title.png assets-fx/fonts/uf5x7 assets-fx/img/bopti_1col.png assets-fx/img/bopti_2col.png @@ -69,6 +74,7 @@ set(ASSETS_fx assets-fx/img/opt_gint_timer_callbacks.png assets-fx/img/opt_gint_timers.png assets-fx/img/opt_gint_tlb.png + assets-fx/img/opt_libs_jui.png assets-fx/img/opt_main.png assets-fx/img/opt_mem.png assets-fx/img/opt_perf_libprof.png @@ -117,7 +123,7 @@ target_include_directories(gintctl PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/include" "${FXSDK_COMPILER_INSTALL}/include/openlibm") target_link_libraries(gintctl - Gint::Gint LibProf::LibProf LibImg::LibImg -lopenlibm) + Gint::Gint LibProf::LibProf LibImg::LibImg JustUI::JustUI -lopenlibm) if("${FXSDK_PLATFORM_LONG}" STREQUAL fx9860G) generate_g1a(TARGET gintctl OUTPUT "gintctl.g1a" diff --git a/assets-fx/fonts/fxconv-metadata.txt b/assets-fx/fonts/fxconv-metadata.txt index 1ada7a3..74437e4 100644 --- a/assets-fx/fonts/fxconv-metadata.txt +++ b/assets-fx/fonts/fxconv-metadata.txt @@ -14,6 +14,13 @@ mini.png: grid.size: 5x6 grid.padding: 1 +title.png: + type: font + name: font_title + charset: print + grid.size: 5x6 + grid.padding: 1 + uf5x7: type: font name: font_uf5x7 diff --git a/assets-fx/fonts/title.png b/assets-fx/fonts/title.png new file mode 100644 index 0000000..edf8139 Binary files /dev/null and b/assets-fx/fonts/title.png differ diff --git a/assets-fx/img/opt_libs_jui.png b/assets-fx/img/opt_libs_jui.png new file mode 100644 index 0000000..81ada0d Binary files /dev/null and b/assets-fx/img/opt_libs_jui.png differ diff --git a/include/gintctl/libs.h b/include/gintctl/libs.h index 0ce0bff..f431077 100644 --- a/include/gintctl/libs.h +++ b/include/gintctl/libs.h @@ -20,4 +20,7 @@ void gintctl_libs_openlibm(void); /* gintctl_libs_libimg(): libimg-based rendering and image transform */ void gintctl_libs_libimg(void); +/* gintctl_libs_justui(): Just User Interfaces */ +void gintctl_libs_justui(void); + #endif /* GINTCTL_LIBS */ diff --git a/include/gintctl/widgets/gscreen.h b/include/gintctl/widgets/gscreen.h new file mode 100644 index 0000000..e9d2177 --- /dev/null +++ b/include/gintctl/widgets/gscreen.h @@ -0,0 +1,92 @@ +//--- +// gintctl.widgets.gscreen: gintctl's extended standard scene +//--- + +#ifndef _GINTCTL_WIDGETS_GSCREEN +#define _GINTCTL_WIDGETS_GSCREEN + +#include +#include +#include +#include +#include + +struct gscreen_tab; + +/* gscreen: gintctl's standard screen. + + The gscreen is a scene with a stylized title bar, an optional function key + bar at the bottom, and a stacked layout of tabs in the middle. Each tab can + have specific properties set, and the gscreen handles some of the focus + management caused by hiding and showing a lot of widgets in tabs. + + The scene of a gscreen can be accessed with (the_gscreen->scene). */ +typedef struct { + jscene *scene; + /* Number of tabs */ + int tab_count; + /* Information on tabs */ + struct gscreen_tab *tabs; + /* Fixed widets */ + jlabel *title; + jfkeys *fkeys; + +} gscreen; + +struct gscreen_tab { + /* TODO: gscreen: Hide title bar and/or status bar */ + bool title_visible, fkeys_visible; + /* Most recent focused widget one */ + jwidget *focus; +}; + +/* gscreen_create(): Set up a standard scene. + If (title = NULL), the scene has no title bar, if (fkeys = NULL) it has no + function bar. To show a title/function bar on some tabs but not all, create + one here and use gscreen_set_tab_{title,fkeys}_visible(). */ + +#ifdef FX9860G +gscreen *gscreen_create(char const *title, bopti_image_t const *fkeys); +#define gscreen_create2(short, img, long, fkeys) gscreen_create(short, img) +#endif + +#ifdef FXCG50 +gscreen *gscreen_create(char const *title, char const *fkeys); +#define gscreen_create2(short, img, long, fkeys) gscreen_create(long, fkeys) +#endif + +/* gscreen_add_tab(): Add a tab to the stacked layout + The child's parent will be changed. The widget will also have a stretch of + (1, 1, false) set by default. If not NULL, the last parameter indicates + which widget to focus when the tab is first shown. */ +void gscreen_add_tab(gscreen *scene, void *widget, void *focus); + +/* gcreen_add_tabs(): Add several tabs at once, with no special parameters */ +void gscreen_add_tabs(gscreen *s, ...); +#define gscreen_add_tabs(...) gscreen_add_tabs(__VA_ARGS__, NULL) + +/* gscreen_show_tab(): Show a tab from the stack + Returns true if the tab changed, false if it was already active or if it is + an invalid tab widget. */ +bool gscreen_show_tab(gscreen *s, int tab); + +/* gscreen_current_tab(): Give the currently visible tab */ +int gscreen_current_tab(gscreen *s); + +/* gscreen_in(): Check if we're in a specific tab */ +bool gscreen_in(gscreen *s, int tab); + +/* gscreen_set_tab_title_visible(): Set whether title bar is shown on a tab */ +void gscreen_set_tab_title_visible(gscreen *s, int tab, bool visible); + +/* gscreen_set_tab_fkeys_visible(): Set whether fkeys are shown on a tab */ +void gscreen_set_tab_fkeys_visible(gscreen *s, int tab, bool visible); + +//--- +// Focus management +//--- + +/* Set focus for the current tab */ +void gscreen_focus(gscreen *s, void *widget); + +#endif /* _GINTCTL_WIDGETS_GSCREEN */ diff --git a/include/gintctl/widgets/gtable.h b/include/gintctl/widgets/gtable.h new file mode 100644 index 0000000..9a2a4ed --- /dev/null +++ b/include/gintctl/widgets/gtable.h @@ -0,0 +1,125 @@ +//--- +// gintctl.widgets.gtable: gintctl's scrolling tables +//--- + +#ifndef _GINTCTL_WIDGETS_GTABLE +#define _GINTCTL_WIDGETS_GTABLE + +#include +#include + +/* gtable: A dynamic scrolling table + + This widget is a table with a header and a set of rows, that scrolls + vertically to show more rows than there is space available on-screen. + + The columns all have their own width, label, and font. During rendering, the + data is provided to the table through a user-provided function that + generates strings for every column in a chosen row. */ +typedef struct gtable { + jwidget widget; + /* Column details */ + struct gtable_column *meta; + /* Function to generate the strings for a row */ + void (*generator)(struct gtable *g, int row, j_arg_t arg); + j_arg_t arg; + /* Number of columns, number of rows visible on-screen, number of rows, + and current top row */ + uint8_t columns; + uint8_t visible; + uint16_t rows; + uint16_t offset; + /* Row height */ + uint8_t row_height; + /* Additional row spacing */ + uint8_t row_spacing; + + /* Coordinates during rendering */ + int16_t x, y; + +} gtable; + +/* gtable_create(): Create a scrolling table + + The number of columns of the table must be fixed, while the number of rows + can be set and changed later with gtable_set_rows(). + + The generator is a function that will be called during rendering to generate + strings for each row. It should have the following prototype, where the last + argument is an optional object of a type listed in j_arg_t (essentially an + integer or a pointer). + + void generator(gtable *g, int row [, ]) + + The generator should return all the strings for the row by a call to + gtable_provide(). */ +gtable *gtable_create(int columns, void *generator, j_arg_t arg, void *parent); + +/* gtable_provide(): Pass strings to display on a given row + + The strings should be passed as variable arguments. A NULL terminator is + added automatically. The strings will not be used after the call finishes, + so they can be allocated statically or on the stack of the caller. */ +void gtable_provide(gtable *g, ...); +#define gtable_provide(...) gtable_provide(__VA_ARGS__, NULL) + +//--- +// Configuration of columns +//--- + +/* gtable_set_column_title(): Set the title of a column */ +void gtable_set_column_title(gtable *t, int column, char const *title); + +/* gtable_set_column_font(): Set the font of a column */ +void gtable_set_column_font(gtable *t, int column, font_t const *font); + +/* gtable_set_column_size(): Set the size of a column + This function sets the size of a column in relative units. During layout, + the width of the table will be split, and each column will receive space + proportional to their size setting. The default is 1 for all columns. */ +void gtable_set_column_size(gtable *t, int column, uint size); + +/* The previous functions have group-setting functions to set properties for + all columns at once. Simply list the headers/fonts/widths all at once; + columns 0 through the number of args will be assigned. */ + +void gtable_set_column_titles(gtable *t, ...); +#define gtable_set_column_titles(...) \ + gtable_set_column_titles(__VA_ARGS__, NULL) + +void gtable_set_column_sizes(gtable *t, ...); +#define gtable_set_column_sizes(...) \ + gtable_set_column_sizes(__VA_ARGS__, 0) + +void gtable_set_column_fonts(gtable *t, ...); +#define gtable_set_column_fonts(...) \ + gtable_set_column_fonts(__VA_ARGS__, NULL) + +/* gtable_set_font(): Set the font for all columns */ +void gtable_set_font(gtable *t, font_t const *font); + +//--- +// Configuration of rows +//--- + +/* gtable_set_rows(): Set the number of rows */ +void gtable_set_rows(gtable *t, int rows); + +/* gtable_set_row_height(): Fix row height instead of guessing from font + Setting 0 will reset to the default behavior. */ +void gtable_set_row_height(gtable *t, int row_height); + +/* gtable_set_row_spacing(): Set additional row spacing */ +void gtable_set_row_spacing(gtable *t, int row_spacing); + +//--- +// Movement +//--- + +/* gtable_scroll_to(): Scroll to the specified offset (if acceptable) */ +void gtable_scroll_to(gtable *t, int offset); + +/* gtable_end(): Offset of the end of the table */ +int gtable_end(gtable *t); + +#endif /* _GINTCTL_WIDGETS_GTABLE */ diff --git a/src/gintctl.c b/src/gintctl.c index 71bc1ed..b521dbb 100644 --- a/src/gintctl.c +++ b/src/gintctl.c @@ -92,6 +92,8 @@ struct menu menu_libs = { gintctl_libs_openlibm, 0 }, { "libimg" _("",": Image transforms"), gintctl_libs_libimg, 0 }, + { "JustUI widgets", + gintctl_libs_justui, 0 }, { NULL, NULL, 0 }, }}; diff --git a/src/libs/justui.c b/src/libs/justui.c new file mode 100644 index 0000000..4144016 --- /dev/null +++ b/src/libs/justui.c @@ -0,0 +1,175 @@ +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +static int recursive_widget_count(void *w0) +{ + J_CAST(w) + + int total = 1; + for(int k = 0; k < w->child_count; k++) + total += recursive_widget_count(w->children[k]); + + return total; +} + +static int nth_child(void *w0, int n, int l, jwidget **result, int *level) +{ + J_CAST(w) + if(n-- == 0) + { + *result = w; + *level = l; + return n; + } + + for(int k = 0; k < w->child_count && n >= 0; k++) + n = nth_child(w->children[k], n, l+1, result, level); + + return n; +} + +static void widget_tree_gen(gtable *t, int row, jwidget *root) +{ + jwidget *w = NULL; + int indent; + nth_child(root, row, 0, &w, &indent); + if(!w) return; + + char c1[32], c2[16], c3[16]; + for(int i = 0; i < indent; i++) c1[i] = ' '; + sprintf(c1 + indent, "%s", jwidget_type(w)); + sprintf(c2, "%dx%d", jwidget_full_width(w), jwidget_full_height(w)); + sprintf(c3, "%dx%d", jwidget_content_width(w), jwidget_content_height(w)); + + gtable_provide(t, c1, c2, c3); +} + +static void paint_pattern(int x, int y) +{ + for(int dx = 0; dx < 12; dx++) + for(int dy = 0; dy < 12; dy++) + { + if(((x + dx) ^ (y + dy)) & 1) dpixel(x + dx, y + dy, C_BLACK); + } +} + +static void table_gen(gtable *t, int row) +{ + char c1[16], c2[16], c3[16]; + sprintf(c1, "%d:1", row); + sprintf(c2, "%d:2", row); + sprintf(c3, "%d:%d", row, t->visible); + gtable_provide(t, c1, c2, c3); +} + +/* gintctl_libs_justui(): Just User Interfaces */ +void gintctl_libs_justui(void) +{ + gscreen *scr = gscreen_create2("JustUI Widgets", &img_opt_libs_jui, + "JustUI graphical interfaces", "/SCENE;/TREE;;;;"); + + // Sample GUI + + jwidget *tab1 = jwidget_create(NULL); + jlayout_set_vbox(tab1)->spacing = _(1,2); + + jwidget *c = jwidget_create(tab1); + jlabel *c1 = jlabel_create("", c); + jpainted_create(paint_pattern, NULL, 12, 12, c); + gtable *c3 = gtable_create(3, table_gen, NULL, c); + + jwidget_set_border(c, J_BORDER_SOLID, 1, C_BLACK); + jwidget_set_padding(c, 1, 1, 1, 1); + jwidget_set_stretch(c, 1, 1, false); + jlayout_set_hbox(c)->spacing = _(1,2); + + jlabel_set_font(c1, _(&font_uf5x7, dfont_default())); + jlabel_set_alignment(c1, J_ALIGN_CENTER); + jlabel_asprintf(c1, "Test\nlĪ±bel\nxy=%d,%d", 7, 8); + jwidget_set_border(c1, J_BORDER_SOLID, 1, C_BLACK); + jwidget_set_stretch(c1, 1, 1, false); + + jwidget_set_stretch(c3, 2, 0, false); + gtable_set_rows(c3, 5); + gtable_set_column_titles(c3, "C1", "C2", "Column 3"); + gtable_set_column_sizes(c3, 1, 1, 3); + gtable_set_font(c3, &font_mini); + + jinput *input = jinput_create("Prompt:" _(," "), 12, tab1); + jwidget_set_stretch(input, 1, 0, false); + jinput_set_font(input, _(&font_uf5x7, dfont_default())); + + // Widget tree visualisation + + gtable *tree = gtable_create(3, widget_tree_gen, scr->scene, NULL); + gtable_set_column_titles(tree, "Type", "Size", "Content"); + gtable_set_column_sizes(tree, 3, 2, 2); + gtable_set_row_spacing(tree, 2); + gtable_set_font(tree, &font_mini); + + // Scene setup + + gscreen_add_tab(scr, tab1, c3); + gscreen_add_tab(scr, tree, tree); + jscene_set_focused_widget(scr->scene, c3); + gtable_set_rows(tree, recursive_widget_count(scr->scene)); + gscreen_set_tab_title_visible(scr, 1, false); + + jevent e; + key_event_t k; + int key = 0; + + while(key != KEY_EXIT) + { + jscene_run(scr->scene, &e, &k); + + if(e.type == JSCENE_PAINT) + { + dclear(C_WHITE); + jscene_render(scr->scene); + dupdate(); + } + + if(e.type == JINPUT_VALIDATED && e.source == input) + { + gscreen_focus(scr, c3); + jlabel_snprintf(c1, 20, "New!\n%s", jinput_value(input)); + } + if(e.type == JINPUT_CANCELED && e.source == input) + { + gscreen_focus(scr, c3); + } + + if(k.type != KEYEV_DOWN) continue; + key = k.key; + + if(key == KEY_F3 && gscreen_in(scr, 0)) + { + bool input_focused = (jscene_focused_widget(scr->scene) == input); + gscreen_focus(scr, input_focused ? (void *)c3 : (void *)input); + } + + if(key == KEY_F1) gscreen_show_tab(scr, 0); + if(key == KEY_F2) gscreen_show_tab(scr, 1); + + #ifdef FX9860G + if(key == KEY_F6) screen_mono(u"\\\\fls0\\justui.bin"); + #endif + } + + jwidget_destroy(scr->scene); +} diff --git a/src/widgets/gscreen.c b/src/widgets/gscreen.c new file mode 100644 index 0000000..772fac1 --- /dev/null +++ b/src/widgets/gscreen.c @@ -0,0 +1,147 @@ +#include +#include +#include +#include + +#include +#include +#include + +#ifdef FX9860G +gscreen *gscreen_create(char const *name, bopti_image_t const *img) +#endif +#ifdef FXCG50 +gscreen *gscreen_create(char const *name, char const *labels) +#endif +{ + gscreen *g = malloc(sizeof *g); + if(!g) return NULL; + + jscene *s = jscene_create_fullscreen(NULL); + if(!s) { free(g); return NULL; } + + g->scene = s; + g->tabs = NULL; + g->tab_count = 0; + + jlabel *title = name ? jlabel_create(name, s) : NULL; + jwidget *stack = jwidget_create(s); + jfkeys *fkeys = _(img,labels) ? jfkeys_create(_(img,labels), s) : NULL; + + if((name && !title) || !stack || (_(img,labels) && !fkeys)) { + jwidget_destroy(s); + return NULL; + } + + g->title = title; + g->fkeys = fkeys; + + jlayout_set_vbox(s)->spacing = _(1,3); + jlayout_set_stack(stack); + + if(title) { + jwidget_set_background(title, C_BLACK); + jlabel_set_text_color(title, C_WHITE); + jlabel_set_font(title, _(&font_title, dfont_default())); + jwidget_set_stretch(title, 1, 0, false); + + #ifdef FX9860G + jwidget_set_padding(title, 1, 1, 0, 1); + jwidget_set_margin(title, 0, 0, 1, 0); + #endif + + #ifdef FXCG50 + jwidget_set_padding(title, 3, 6, 3, 6); + #endif + } + + #ifdef FXCG50 + jwidget_set_padding(stack, 1, 3, 1, 3); + #endif + + jwidget_set_stretch(stack, 1, 1, false); + return g; +} + +void gscreen_add_tab(gscreen *s, void *widget, void *focus) +{ + struct gscreen_tab *t = realloc(s->tabs, (s->tab_count+1) * sizeof *t); + if(!t) return; + + s->tabs = t; + s->tabs[s->tab_count].title_visible = (s->title != NULL); + s->tabs[s->tab_count].fkeys_visible = (s->fkeys != NULL); + s->tabs[s->tab_count].focus = focus; + s->tab_count++; + + jwidget *stack = s->scene->widget.children[1]; + jwidget_add_child(stack, widget); + jwidget_set_stretch(widget, 1, 1, false); +} + +#undef gscreen_add_tabs +void gscreen_add_tabs(gscreen *s, ...) +{ + va_list args; + va_start(args, s); + jwidget *w; + + while((w = va_arg(args, jwidget *))) + gscreen_add_tab(s, w, NULL); + + va_end(args); +} + +bool gscreen_show_tab(gscreen *s, int tab) +{ + jwidget *stack = s->scene->widget.children[1]; + jlayout_stack *l = jlayout_get_stack(stack); + + /* Find widget ID in the stack + int i = 0; + while(i < stack->child_count && stack->children[i] != widget) i++; + if(i >= stack->child_count || l->active == i) return false; */ + if(tab < 0 || tab >= stack->child_count) return false; + + /* Update keyboard focus */ + s->tabs[l->active].focus = jscene_focused_widget(s->scene); + jscene_set_focused_widget(s->scene, s->tabs[tab].focus); + + l->active = tab; + stack->update = 1; + + /* Hide or show title and function key bar as needed */ + jwidget_set_visible(s->title, s->tabs[tab].title_visible); + if(s->fkeys) jwidget_set_visible(s->fkeys, s->tabs[tab].fkeys_visible); + + return true; +} + +int gscreen_current_tab(gscreen *s) +{ + jwidget *stack = s->scene->widget.children[1]; + jlayout_stack *l = jlayout_get_stack(stack); + return l->active; +} + +bool gscreen_in(gscreen *s, int tab) +{ + return gscreen_current_tab(s) == tab; +} + +void gscreen_set_tab_title_visible(gscreen *s, int tab, bool visible) +{ + if(tab < 0 || tab >= s->tab_count) return; + s->tabs[tab].title_visible = visible; +} + +void gscreen_set_tab_fkeys_visible(gscreen *s, int tab, bool visible) +{ + if(tab < 0 || tab >= s->tab_count) return; + s->tabs[tab].fkeys_visible = visible; +} + +void gscreen_focus(gscreen *s, void *widget) +{ + return jscene_set_focused_widget(s->scene, widget); +} diff --git a/src/widgets/gtable.c b/src/widgets/gtable.c new file mode 100644 index 0000000..12b6330 --- /dev/null +++ b/src/widgets/gtable.c @@ -0,0 +1,354 @@ +#include +#include +#include +#include +#include + +struct gtable_column { + char const *title; + font_t const *font; + uint16_t size; + uint16_t width; +}; + +/* Type identifier for gtable */ +static int gtable_type_id = -1; + +void update_visible(gtable *t); + +gtable *gtable_create(int columns, void *generator, j_arg_t arg, void *parent) +{ + if(gtable_type_id < 0) return NULL; + + gtable *t = malloc(sizeof *t); + if(!t) return NULL; + + t->meta = malloc(columns * sizeof *t->meta); + if(!t->meta) { free(t); return NULL; } + + jwidget_init(&t->widget, gtable_type_id, parent); + + t->generator = generator; + t->arg = arg; + + t->columns = columns; + t->rows = 0; + t->offset = 0; + t->visible = 0; + + t->row_height = 0; + t->row_spacing = 1; + + for(uint i = 0; i < t->columns; i++) { + t->meta[i].title = NULL; + t->meta[i].font = dfont_default(); + t->meta[i].width = 1; + } + + return t; +} + +//--- +// Configuration of columns +//--- + +void gtable_set_column_title(gtable *t, int column, char const *title) +{ + if(column < 0 || column >= t->columns) return; + t->meta[column].title = title; + t->widget.update = 1; +} + +void gtable_set_column_font(gtable *t, int column, font_t const *font) +{ + if(column < 0 || column >= t->columns) return; + t->meta[column].font = (font ? font : dfont_default()); + t->widget.update = 1; +} + +void gtable_set_column_size(gtable *t, int column, uint size) +{ + if(column < 0 || column >= t->columns) return; + t->meta[column].size = size; + t->widget.dirty = 1; +} + +#undef gtable_set_column_titles +void gtable_set_column_titles(gtable *t, ...) +{ + va_list args; + va_start(args, t); + + for(uint i = 0; i < t->columns; i++) + { + char const *title = va_arg(args, char const *); + if(!title) break; + gtable_set_column_title(t, i, title); + } + + va_end(args); +} + +#undef gtable_set_column_fonts +void gtable_set_column_fonts(gtable *t, ...) +{ + va_list args; + va_start(args, t); + + for(uint i = 0; i < t->columns; i++) + { + font_t const *font = va_arg(args, font_t const *); + if(!font) break; + gtable_set_column_font(t, i, font); + } + + va_end(args); +} + +#undef gtable_set_column_sizes +void gtable_set_column_sizes(gtable *t, ...) +{ + va_list args; + va_start(args, t); + + for(uint i = 0; i < t->columns; i++) + { + uint size = va_arg(args, uint); + if(!size) break; + gtable_set_column_size(t, i, size); + } + + va_end(args); +} + +void gtable_set_font(gtable *t, font_t const *font) +{ + for(uint i = 0; i < t->columns; i++) { + gtable_set_column_font(t, i, font); + } +} + +//--- +// Configuration of rows +//--- + +void gtable_set_rows(gtable *t, int rows) +{ + /* The re-layout will make sure we stay in the visible range */ + t->rows = max(rows, 0); + t->widget.dirty = 1; +} + +void gtable_set_row_height(gtable *t, int row_height) +{ + t->row_height = row_height; + t->visible = 0; + t->widget.dirty = 1; +} + +void gtable_set_row_spacing(gtable *t, int row_spacing) +{ + t->row_spacing = row_spacing; + t->visible = 0; + t->widget.dirty = 1; +} + +//--- +// Movement +//--- + +/* Guarantee that the current positioning is valid */ +static void bound(gtable *t) +{ + t->offset = min(t->offset, gtable_end(t)); +} + +void gtable_scroll_to(gtable *t, int offset) +{ + t->offset = offset; + bound(t); +} + +int gtable_end(gtable *t) +{ + update_visible(t); + return max((int)t->rows - (int)t->visible, 0); +} + +//--- +// Polymorphic widget operations +//--- + +int compute_row_height(gtable *t) +{ + int row_height = t->row_height; + if(row_height == 0) { + for(int i = 0; i < t->columns; i++) { + row_height = max(row_height, t->meta[i].font->line_height); + } + } + return row_height; +} + +/* Recompute (visible) based on the current size */ +void update_visible(gtable *t) +{ + if(t->visible == 0) { + int row_height = compute_row_height(t); + int header_height = row_height + t->row_spacing + 1; + + int space = jwidget_content_height(t) - header_height; + t->visible = max(0, space / (row_height + t->row_spacing)); + } +} + +static void gtable_poly_csize(void *t0) +{ + gtable *t = t0; + t->widget.w = 64; + t->widget.h = 32; +} + +static void gtable_poly_layout(void *t0) +{ + gtable *t = t0; + t->visible = 0; + update_visible(t); + bound(t); + + int cw = jwidget_content_width(t); + /* Leave space for a scrollbar */ + if(t->visible < t->rows) cw -= _(2,4); + + /* Compute the width of each column */ + uint total_size = 0; + for(uint i = 0; i < t->columns; i++) { + total_size += t->meta[i].size; + } + + uint space_left = cw; + for(uint i = 0; i < t->columns; i++) { + t->meta[i].width = (t->meta[i].size * cw) / total_size; + space_left -= t->meta[i].width; + } + + /* Grant space left to the first columns */ + for(uint i = 0; i < t->columns && i < space_left; i++) { + t->meta[i].width++; + } +} + +static void gtable_poly_render(void *t0, int base_x, int base_y) +{ + gtable *t = t0; + int row_height = compute_row_height(t); + int cw = jwidget_content_width(t); + int y = base_y; + + for(uint i=0, x=base_x; i < t->columns; i++) { + font_t const *old_font = dfont(t->meta[i].font); + dtext(x, y, C_BLACK, t->meta[i].title); + dfont(old_font); + + x += t->meta[i].width; + } + + y += row_height + t->row_spacing; + dline(base_x, y, base_x + cw - 1, y, C_BLACK); + y += t->row_spacing + 1; + int rows_y = y; + + for(uint i = 0; t->offset + i < t->rows && i < t->visible; i++) { + t->x = base_x; + t->y = y; + + t->generator(t, t->offset + i, t->arg); + y += row_height + t->row_spacing; + } + + /* Scrollbar */ + if(t->visible < t->rows) { + /* Area where the scroll bar lives */ + int area_w = _(1,2); + int area_x = base_x + cw - area_w; + int area_y = rows_y; + int area_h = jwidget_content_height(t) - (area_y - base_y); + + /* Position and size of scroll bar within the area */ + int bar_y = (t->offset * area_h + t->rows/2) / t->rows; + int bar_h = (t->visible * area_h + t->rows/2) / t->rows; + + drect(area_x, area_y + bar_y, area_x + area_w - 1, + area_y + bar_y + bar_h, C_BLACK); + } +} + +#undef gtable_provide +void gtable_provide(gtable *t, ...) +{ + va_list args; + va_start(args, t); + + for(uint i = 0; i < t->columns; i++) { + char const *str = va_arg(args, char const *); + if(!str) break; + + font_t const *old_font = dfont(t->meta[i].font); + dtext(t->x, t->y, C_BLACK, str); + dfont(old_font); + + t->x += t->meta[i].width; + } + + va_end(args); +} + +static bool gtable_poly_event(void *t0, jevent e) +{ + gtable *t = t0; + int end = gtable_end(t); + update_visible(t); + + if(e.type != JWIDGET_KEY) return false; + + key_event_t kev = e.data.key; + if(kev.type == KEYEV_UP) return false; + + if(kev.key == KEY_DOWN && t->offset < end) { + if(kev.shift) t->offset = end; + else t->offset++; + t->widget.update = 1; + return true; + } + if(kev.key == KEY_UP && t->offset > 0) { + if(kev.shift) t->offset = 0; + else t->offset--; + t->widget.update = 1; + return true; + } + + return false; +} + +static void gtable_poly_destroy(void *t0) +{ + gtable *t = t0; + free(t->meta); +} + +/* gtable type definition */ +static jwidget_poly type_gtable = { + .name = "gtable", + .csize = gtable_poly_csize, + .layout = gtable_poly_layout, + .render = gtable_poly_render, + .event = gtable_poly_event, + .destroy = gtable_poly_destroy, +}; + +/* Type registration */ +__attribute__((constructor(2000))) +static void j_register_gtable(void) +{ + gtable_type_id = j_register_widget(&type_gtable, "jwidget"); +}