commit c32e91b7f412d2a06d1e020eb4be2bf44e600f96 Author: Lephenixnoir Date: Fri May 14 11:10:03 2021 +0200 first version - and it works too! diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b24b5b2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +# Build files +/build-fx +/build-cg +/*.g1a +/*.g3a + +# Development files +/assets-cg/icon.xcf + +# Python bytecode +__pycache__/ + +# Common IDE files +*.sublime-project +*.sublime-workspace +.vscode diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..0ecd459 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,53 @@ +# Configure with [fxsdk build-fx] or [fxsdk build-cg], which provide the +# toolchain file and module path of the fxSDK + +cmake_minimum_required(VERSION 3.15) +project(FxLibcTest) + +include(GenerateG1A) +include(GenerateG3A) +include(Fxconv) +find_package(Gint 2.4 REQUIRED) +find_package(JustUI 1.0 REQUIRED) + +# FXLIBC_INSTALL: Additional folders to read includes and libs from + +set(SOURCES + src/main.c + src/test.c + src/widgets/fbar.c + src/widgets/fbrowser.c + src/widgets/flist.c + src/widgets/gscreen.c + # ctype + src/ctype/trivialties.c + # string + src/string/memarray.c + src/string/core.c +) +# fx-9860G-only assets and fx-CG-50-only assets +set(ASSETS_fx +) +set(ASSETS_cg +) + +fxconv_declare_assets(${ASSETS} ${ASSETS_fx} ${ASSETS_cg} WITH_METADATA) + +add_executable(fxlibctest ${SOURCES} ${ASSETS} ${ASSETS_${FXSDK_PLATFORM}}) +target_compile_options(fxlibctest PRIVATE -Wall -Wextra -Os) +target_link_options(fxlibctest PRIVATE -Wl,-Map=map -Wl,--print-memory-usage) +target_include_directories(fxlibctest PRIVATE include/) +target_link_libraries(fxlibctest JustUI::JustUI Gint::Gint -lc) + +foreach(FOLDER IN LISTS FXLIBC_INSTALL) + target_include_directories(fxlibctest PRIVATE "${FOLDER}/include") + target_link_directories(fxlibctest PRIVATE "${FOLDER}/lib") +endforeach() + +if("${FXSDK_PLATFORM_LONG}" STREQUAL fx9860G) + generate_g1a(TARGET fxlibctest OUTPUT "FxLibcT.g1a" + NAME "FxLibc test" ICON assets-fx/icon.png) +elseif("${FXSDK_PLATFORM_LONG}" STREQUAL fxCG50) + generate_g3a(TARGET fxlibctest OUTPUT "FxLibcT.g3a" + NAME "FxLibc test" ICONS assets-cg/icon-uns.png assets-cg/icon-sel.png) +endif() diff --git a/assets-cg/icon-sel.png b/assets-cg/icon-sel.png new file mode 100644 index 0000000..44f0445 Binary files /dev/null and b/assets-cg/icon-sel.png differ diff --git a/assets-cg/icon-uns.png b/assets-cg/icon-uns.png new file mode 100644 index 0000000..3bee2ef Binary files /dev/null and b/assets-cg/icon-uns.png differ diff --git a/include/ft/all-tests.h b/include/ft/all-tests.h new file mode 100644 index 0000000..bf261b7 --- /dev/null +++ b/include/ft/all-tests.h @@ -0,0 +1,21 @@ +//--- +// ft.all-tests: List of all test structures defined by modules +//--- + +#ifndef _FT_ALL_TESTS_H_ +#define _FT_ALL_TESTS_H_ + +#include + +/* ctype */ +extern ft_test ft_ctype_trivial_success; +extern ft_test ft_ctype_trivial_failure; +extern ft_test ft_ctype_trivial_empty; + +/* string */ +extern ft_test ft_string_memset; +extern ft_test ft_string_memcpy; +extern ft_test ft_string_memmove; +extern ft_test ft_string_memcmp; + +#endif /* _FT_ALL_TESTS_H_ */ diff --git a/include/ft/test.h b/include/ft/test.h new file mode 100644 index 0000000..21bc3f8 --- /dev/null +++ b/include/ft/test.h @@ -0,0 +1,93 @@ +//--- +// ft.test: Testing tools and status reports +//--- + +#ifndef _FT_TEST_H_ +#define _FT_TEST_H_ + +#include +#include + +//--- +// Test framework +//--- + +/* ft_test: A series of assertions grouped together in a single function */ +typedef struct ft_test { + /* Test name */ + char const *name; + /* Test function */ + void (*function)(struct ft_test *t); + /* Optional visualization widget; called when the visualization is + requested. The widget can be interactive, but it must not accept EXIT or + MENU events. It is destroyed atfer use. */ + jwidget *(*widget)(struct ft_test *test); + /* Test log; this can be displayed by a scrolling widget */ + char *log; + /* Number of passed, skipped and failed assertions in the test */ + uint16_t passed; + uint16_t skipped; + uint16_t failed; +} ft_test; + +/* ft_test_init(): Init counters and logs for a test */ +void ft_test_init(ft_test *test); + +/* ft_test_run(): Reset and run a test */ +void ft_test_run(ft_test *test); + +/* ft_test_done(): Check whether a test is done */ +bool ft_test_done(ft_test *test); + +/* ft_assert(): Record an assertion in a test */ +void ft_assert(ft_test *test, int expression, + char const *file, int line, char const *expression_str); +#define ft_assert(test, expression) \ + ft_assert(test, expression, __FILE__, __LINE__, #expression) + +/* ft_log(): Write some data in a test's log */ +__attribute__((format(printf, 2, 3))) +void ft_log(ft_test *test, char const *format, ...); + +//--- +// Test lists (to organize by header) +//--- + +/* ft_list: A named list of tests with aggregating statistics */ +typedef struct { + /* List name (header name) */ + char const *name; + /* NULL-terminated list of children */ + ft_test **children; + /* Number of passed, skipped and failed assertions in the children */ + uint16_t passed; + uint16_t skipped; + uint16_t failed; +} ft_list; + +/* ft_list_init(): Init counters and logs for a NULL-terminated test list */ +void ft_list_init(ft_list *list); + +/* ft_list_run(): Reset and run a list of tests (aggregates automatically) */ +void ft_list_run(ft_list *list); + +/* ft_list_aggregate(): Update list results based on the children's results */ +void ft_list_aggregate(ft_list *l); + +//--- +// Utilities +//--- + +/* Colors for each test category */ +#define FT_TEST_EMPTY C_RGB(20,20,20) +#define FT_TEST_PASSED C_RGB(6,25,8) +#define FT_TEST_SKIPPED C_RGB(31,20,9) +#define FT_TEST_PENDING C_RGB(15,23,27) +#define FT_TEST_FAILED C_RGB(31,10,7) + +/* ft_test_color(): Color for the summary of a test */ +int ft_test_color(ft_test const *t); +/* ft_list_color(): Color for the summary of a list of tests */ +int ft_list_color(ft_list const *t); + +#endif /* _FT_TEST_H_ */ diff --git a/include/ft/util.h b/include/ft/util.h new file mode 100644 index 0000000..9422eb3 --- /dev/null +++ b/include/ft/util.h @@ -0,0 +1,16 @@ +//--- +// fxlibtest.util: Random utilities +//--- + +#ifndef _FT_UTIL_H_ +#define _FT_UTIL_H_ + +#ifdef FX9860G +#define _(fx,cg) (fx) +#endif + +#ifdef FXCG50 +#define _(fx, cg) (cg) +#endif + +#endif /* _FT_UTIL_H_ */ diff --git a/include/ft/widgets/fbar.h b/include/ft/widgets/fbar.h new file mode 100644 index 0000000..61bb88f --- /dev/null +++ b/include/ft/widgets/fbar.h @@ -0,0 +1,36 @@ +//--- +// ft.widgets.fbar: A colored bar summarizing test results +//--- + +#ifndef _FT_WIDGETS_FBAR_H_ +#define _FT_WIDGETS_FBAR_H_ + +#include +#include + +/* fbar: A bar summarizing test results with colored sections + + This widget is a simple display widget. It displays either a summary for a + list or a summary for a whole header set (list of lists). + + The length of the pending section is based on the proportion of pending + tests. The length of the other sections is based on the number of assertions + in each category. */ +typedef struct { + jwidget widget; + + /* Current set of tests or set of lists (only one can be non-NULL) */ + ft_test **tests; + ft_list *lists; +} fbar; + +/* fbar_create(): Create an fbar with no data tied */ +fbar *fbar_create(void *parent); + +/* fbar_set_tests(): Show statistics for a list */ +void fbar_set_tests(fbar *b, ft_test **tests); + +/* fbar_set_lists(): Show statistics for a full header set */ +void fbar_set_lists(fbar *b, ft_list *lists); + +#endif /* _FT_WIDGETS_FBAR_H_ */ diff --git a/include/ft/widgets/fbrowser.h b/include/ft/widgets/fbrowser.h new file mode 100644 index 0000000..e185ae8 --- /dev/null +++ b/include/ft/widgets/fbrowser.h @@ -0,0 +1,59 @@ +//--- +// ft.widgets.browser: A dual-list setup to browse categorized tests +//--- + +#ifndef _FT_WIDGETS_BROWSER_H_ +#define _FT_WIDGETS_BROWSER_H_ + +#include +#include +#include +#include + +/* fbrowser: A test browser that shows categorized tests and summaries + + This widget is the main frame of this application. It consists of list of + headers on the left, and a list of tests on the right. Summaries of all + headers and of all tests within a single header are shown. */ +typedef struct { + jwidget widget; + /* Header list and test list */ + flist *headers; + flist *tests; + /* Main bar at the top, and secondary bar above test list */ + fbar *bar_top; + fbar *bar_right; + /* Widgets that help with the structure */ + jwidget *rstack; + /* Current set of headers and tests */ + ft_list *data_headers; + ft_test **data_tests; +} fbrowser; + +/* A test entry has been validated */ +extern uint16_t FBROWSER_VALIDATED; + +/* fbrowser_create(): Create a test browser */ +fbrowser *fbrowser_create(void *parent); + +/* fbrowser_set_headers(): Select a set of headers + If (select = true), this also selects the first header.*/ +void fbrowser_set_headers(fbrowser *b, ft_list *list, bool select); + +/* fbrowser_set_tests(): Select a set of tests + You should not call this function; fbrowser synchronizes the set of tests + with the currently selected header. */ +void fbrowser_set_tests(fbrowser *b, ft_test **tests); + +/* fbrowser_current_header(): Currently-selected header + This function returns a pointer to the currently-selected list, which is + NULL if the summary is selected on the left panel, and non-NULL otherwise. + This is defined even when the focus is on the right panel. */ +ft_list *fbrowser_current_header(fbrowser *b); + +/* fbrowser_current_test(): Currently-selected test + This function returns a pointer to the currently-selected test, which is + non-NULL only when the focus is on the right panel. */ +ft_test *fbrowser_current_test(fbrowser *b); + +#endif /* _FT_WIDGETS_BROWSER_H_ */ diff --git a/include/ft/widgets/flist.h b/include/ft/widgets/flist.h new file mode 100644 index 0000000..0c198b2 --- /dev/null +++ b/include/ft/widgets/flist.h @@ -0,0 +1,79 @@ +//--- +// ft.widgets.flist: Scrolling list with selectable elements +//--- + +#ifndef _FT_WIDGETS_FLIST_H_ +#define _FT_WIDGETS_FLIST_H_ + +#include +#include +#include + +/* flist: A scrolling list with a selection cursor + + This widget is a list of horizontal entries of a fixed height. The rendering + of entries and data storage is delegated to caller code. This widget only + handles the geometry and has no idea about the contents of the entries. */ +typedef struct flist { + jwidget widget; + /* Renderer function */ + gint_call_t renderer; + /* Number of rows, number of rows visible on-screen, current top row, and + currently-selected row (-1 if none) */ + uint16_t rows; + uint16_t visible; + uint16_t top; + int16_t selected; + /* Row height */ + uint16_t row_height; + /* Parameters available during rendering */ + int16_t x, y; + int16_t row; +} flist; + +/* The cursor has moved to a different entry */ +extern uint16_t FLIST_SELECTED; +/* The current entry has been validated with EXE */ +extern uint16_t FLIST_VALIDATED; + +/* flist_create(): Create a scrolling list + Rows are dynamic, this function only sets the rendering function, which + should have the following prototype: + + void render(flist *f, ...); + + The GINT_CALL() for the renderer can use argument 2 to 4, the first will be + overridden by the flist code during the call. + + The renderer can access the dimensions and row_height setting of the flist + widget, as well as draw_x, draw_y, draw_row and selected. Note however that + a couple of pixels may be reserved on the right to render a scrollbar. */ +flist *flist_create(gint_call_t renderer, void *parent); + +/* flist_set_renderer(): Change renderer after creation */ +void flist_set_renderer(flist *l, gint_call_t renderer); + +//--- +// Configuration of rows +//--- + +/* flist_set_rows(): Set the number of rows */ +void flist_set_rows(flist *l, int rows); + +/* flist_set_row_height(): Fix the row height for every row */ +void flist_set_row_height(flist *l, int row_height); + +//--- +// Movement +//--- + +/* flist_scroll_to(): Scroll to the specified offset */ +void flist_scroll_to(flist *l, int offset); + +/* flist_select(): Select an entry and scroll to it if needed */ +void flist_select(flist *l, int entry); + +/* flist_end(): Offset of the end of the list */ +int flist_end(flist *l); + +#endif /* _FT_WIDGETS_FLIST_H_ */ diff --git a/include/ft/widgets/gscreen.h b/include/ft/widgets/gscreen.h new file mode 100644 index 0000000..d5e3f68 --- /dev/null +++ b/include/ft/widgets/gscreen.h @@ -0,0 +1,110 @@ +//--- +// 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; + /* Current function bar level */ + int8_t fkey_level; + +} gscreen; + +struct gscreen_tab { + 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 + +void gscreen_destroy(gscreen *s); + +//--- +// Function bar settings +//--- + +/* gscreen_set_fkeys_level(): Select the function key bar */ +void gscreen_set_fkeys_level(gscreen *s, int level); + +//--- +// Tab settings +//--- + +/* 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_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); + +//--- +// Tab navigation +//--- + +/* 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); + +//--- +// Focus management +//--- + +/* Set focus for the current tab */ +void gscreen_focus(gscreen *s, void *widget); + +#endif /* _GINTCTL_WIDGETS_GSCREEN */ diff --git a/src/ctype/trivialties.c b/src/ctype/trivialties.c new file mode 100644 index 0000000..9e3313f --- /dev/null +++ b/src/ctype/trivialties.c @@ -0,0 +1,28 @@ +#include +#include + +static void _ctype_trivial_success(ft_test *t) +{ + ft_assert(t, 1); + ft_assert(t, 1); +} + +ft_test ft_ctype_trivial_success = { + .name = "Trivialty (always succeeds)", + .function = _ctype_trivial_success, +}; + +static void _ctype_trivial_failure(ft_test *t) +{ + ft_assert(t, 0); +} + +ft_test ft_ctype_trivial_failure = { + .name = "Trivialty (always fails)", + .function = _ctype_trivial_failure, +}; + +ft_test ft_ctype_trivial_empty = { + .name = "Trivialty (no function)", + .function = NULL, +}; diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..740e523 --- /dev/null +++ b/src/main.c @@ -0,0 +1,142 @@ +#include +#include + +#include +#include +#include +#include +#include + +/* We don't initialize the test result fields below */ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wmissing-field-initializers" + +ft_list headers_libc[] = { + { "", (ft_test*[]){ + &ft_ctype_trivial_success, + &ft_ctype_trivial_failure, + &ft_ctype_trivial_empty, + NULL, + }}, + { "", NULL }, + { "", NULL }, + { "", NULL }, + { "", NULL },/*(ft_test[]){ + { "Formatted printing basics", NULL, NULL }, + { "Format types and options", NULL, NULL }, + { NULL }, + }}, */ + { "", NULL }, + { "", (ft_test*[]){ + &ft_string_memset, + &ft_string_memcpy, + &ft_string_memmove, + NULL, + }}, + { "", NULL }, + { "", NULL }, + { "", NULL }, + { "", NULL }, + { NULL } +}; +ft_list headers_posix[] = { + { "", NULL }, + { NULL } +}; + +#pragma GCC diagnostic pop + +int main(void) +{ + // Initialize test results + for(int i = 0; headers_libc[i].name; i++) + ft_list_init(&headers_libc[i]); + for(int i = 0; headers_posix[i].name; i++) + ft_list_init(&headers_posix[i]); + + // Create GUI + gscreen *scr = gscreen_create2("FxLibc tests", NULL, + "FxLibc regression and performance tests", "/LIBC;/POSIX;;;;#RUN"); + fbrowser *browser = fbrowser_create(NULL); + int selected_set = 0; + + gscreen_add_tab(scr, browser, NULL); + fbrowser_set_headers(browser, headers_libc, false); + + jwidget *results = jwidget_create(NULL); + gscreen_add_tab(scr, results, NULL); + gscreen_set_tab_fkeys_visible(scr, 1, false); + + // Event handling + while(1) { + jevent e = jscene_run(scr->scene); + int tab = gscreen_current_tab(scr); + + int key = 0; + if(e.type == JSCENE_KEY || e.key.type == KEYEV_DOWN) key = e.key.key; + + if(e.type == JSCENE_PAINT) { + dclear(C_WHITE); + jscene_render(scr->scene); + dupdate(); + } + + // Switch lists on F1/F2 + if(tab == 0 && key == KEY_F1 && selected_set != 0) { + fbrowser_set_headers(browser, headers_libc, false); + selected_set = 0; + } + if(tab == 0 && key == KEY_F2 && selected_set != 1) { + fbrowser_set_headers(browser, headers_posix, false); + selected_set = 1; + } + + // Run tests + if(tab == 0 && key == KEY_F6) { + ft_list *header = fbrowser_current_header(browser); + ft_test *test = fbrowser_current_test(browser); + + if(test) { + ft_test_run(test); + ft_list_aggregate(header); + browser->widget.update = 1; + } + else if(header) { + ft_list_run(header); + browser->widget.update = 1; + } + else { + for(int i = 0; headers_libc[i].name; i++) + ft_list_run(&headers_libc[i]); + for(int i = 0; headers_posix[i].name; i++) + ft_list_run(&headers_posix[i]); + browser->widget.update = 1; + } + } + + // Browse test results + if(tab == 0 && e.type == FBROWSER_VALIDATED && e.source == browser) { + ft_test *test = fbrowser_current_test(browser); + if(ft_test_done(test) && test->widget) { + jwidget *w = test->widget(test); + if(w) { + jwidget_add_child(results, w); + gscreen_show_tab(scr, 1); + jscene_set_focused_widget(scr->scene, w); + } + } + } + + // Close test results + if(tab == 1 && key == KEY_EXIT) { + jscene_set_focused_widget(scr->scene, browser->tests); + gscreen_show_tab(scr, 0); + while(results->child_count > 0) { + jwidget_destroy(results->children[0]); + } + } + } + + gscreen_destroy(scr); + return 1; +} diff --git a/src/string/core.c b/src/string/core.c new file mode 100644 index 0000000..e8ff5de --- /dev/null +++ b/src/string/core.c @@ -0,0 +1,168 @@ +#include +#include +#include +#include "memarray.h" + +//--- +// Utilities +//--- + +/* Fill buffer with non-zero and position-sensitive data */ +static void fill(uint8_t *buf, int size, int start) +{ + for(int i = 0; i < size; i++) buf[i] = start+i; +} +/* Clear buffer */ +static void clear(uint8_t *buf, int size) +{ + for(int i = 0; i < size; i++) buf[i] = 0; +} +/* Check buffer equality (returns zero on equal) */ +static int cmp(uint8_t *left, uint8_t *right, int size) +{ + for(int i = 0; i < size; i++) if(left[i] != right[i]) return 1; + return 0; +} + +//--- +// Naive functions (baseline) +//--- + +static void *naive_memcpy(void *_dst, void const *_src, size_t len) +{ + uint8_t *dst = _dst; + uint8_t const *src = _src; + + while(len--) *dst++ = *src++; + return _dst; +} +static void *naive_memset(void *_dst, int byte, size_t len) +{ + uint8_t *dst = _dst; + + while(len--) *dst++ = byte; + return _dst; +} +static void *naive_memmove(void *dst, void const *src, size_t len) +{ + uint8_t tmp[len]; + naive_memcpy(tmp, src, len); + naive_memcpy(dst, tmp, len); + return dst; +} +static int naive_memcmp(void const *_s1, void const *_s2, size_t len) +{ + uint8_t const *s1 = _s1, *s2 = _s2; + + for(size_t i = 0; i < len; i++) + { + if(s1[i] != s2[i]) return s1[i] - s2[i]; + } + return 0; +} + +//--- +// memset +//--- + +static int _string_memset_func(memarray_args_t const *args) +{ + fill(args->full_buf1, args->full_size, 0); + fill(args->full_buf2, args->full_size, 0); + + memset(args->buf1, 0, args->size); + naive_memset(args->buf2, 0, args->size); + + return cmp(args->full_buf1, args->full_buf2, args->full_size); +} + +uint8_t _string_memset_rc[MEMARRAY_RC_SINGLE]; + +static void _string_memset(ft_test *t) +{ + memarray_single(_string_memset_rc, _string_memset_func); + memarray_assert(_string_memset_rc, t); +} + +static jwidget *_string_memset_widget(GUNUSED ft_test *t) +{ + return memarray_widget(_string_memset_rc); +} + +ft_test ft_string_memset = { + .name = "Configurations of memset", + .function = _string_memset, + .widget = _string_memset_widget, +}; + +//--- +// memcpy +//--- + +static int _string_memcpy_func(memarray_args_t const *args) +{ + fill(args->full_left1, args->full_size, 0); + fill(args->full_left2, args->full_size, 0); + clear(args->full_right1, args->full_size); + clear(args->full_right2, args->full_size); + + memcpy(args->right1, args->left1, args->size); + naive_memcpy(args->right2, args->left2, args->size); + + return cmp(args->full_right1, args->full_right2, args->full_size); +} + +uint8_t _string_memcpy_rc[MEMARRAY_RC_DOUBLE]; + +static void _string_memcpy(ft_test *t) +{ + memarray_double(_string_memcpy_rc, false, _string_memcpy_func); + memarray_assert(_string_memcpy_rc, t); +} + +static jwidget *_string_memcpy_widget(GUNUSED ft_test *t) +{ + return memarray_widget(_string_memcpy_rc); +} + +ft_test ft_string_memcpy = { + .name = "Configurations of memcpy", + .function = _string_memcpy, + .widget = _string_memcpy_widget, +}; + +//--- +// memmove +//--- + +static int _string_memmove_func(memarray_args_t const *args) +{ + fill(args->full_left1, args->full_size, 0); + fill(args->full_left2, args->full_size, 0); + fill(args->full_right1, args->full_size, 0); + fill(args->full_right2, args->full_size, 0); + + memmove(args->right1, args->left1, args->size); + naive_memmove(args->right2, args->left2, args->size); + + return cmp(args->full_right1, args->full_right2, args->full_size); +} + +uint8_t _string_memmove_rc[MEMARRAY_RC_DOUBLE_OVERLAP]; + +static void _string_memmove(ft_test *t) +{ + memarray_double(_string_memmove_rc, true, _string_memmove_func); + memarray_assert(_string_memmove_rc, t); +} + +static jwidget *_string_memmove_widget(GUNUSED ft_test *t) +{ + return memarray_widget(_string_memmove_rc); +} + +ft_test ft_string_memmove = { + .name = "Configurations of memmove", + .function = _string_memmove, + .widget = _string_memmove_widget, +}; diff --git a/src/string/memarray.c b/src/string/memarray.c new file mode 100644 index 0000000..2deb3c1 --- /dev/null +++ b/src/string/memarray.c @@ -0,0 +1,199 @@ +#include +#include +#include +#include +#include +#include + +#include +#include +#include "memarray.h" + +//--- +// Utilities +//--- + +/* Code of exception that occurs during a memory access */ +static uint32_t exception = 0; +/* Exception-catching function */ +static int catch_exc(uint32_t code) +{ + if(code == 0x100 || code == 0x0e0) + { + exception = code; + gint_exc_skip(1); + return 0; + } + return 1; +} + +/* memory_protected_call(): Wrapper that protects against access errors */ +static int memory_protected_call(gint_call_t function) +{ + exception = 0; + gint_exc_catch(catch_exc); + int rc = gint_call(function); + gint_exc_catch(NULL); + return rc || (exception != 0); +} + +//--- +// Public API +//--- + +void memarray_set(uint8_t *rc, int position, int value) +{ + int index = 1 + (position >> 3); + uint8_t byte = 0x80 >> (position & 7); + + if(index >= rc[0]) return; + if(value) rc[index] |= byte; + else rc[index] &= ~byte; +} + +int memarray_get(uint8_t *rc, int position) +{ + int index = 1 + (position >> 3); + uint8_t byte = 0x80 >> (position & 7); + + if(index >= rc[0]) return -1; + return (rc[index] & byte) != 0; +} + +void memarray_assert(uint8_t *rc, ft_test *test) +{ + for(int i = 0; true; i++) { + int v = memarray_get(rc, i); + if(v < 0) return; + ft_assert(test, v == 0); + } +} + +void memarray_single(uint8_t *rc, memarray_func_t f) +{ + uint8_t buf1[128], buf2[128]; + + int counter = 0; + rc[0] = MEMARRAY_RC_SINGLE; + + memarray_args_t args = { + .full_buf1 = buf1, + .full_buf2 = buf2, + .full_size = 128, + }; + gint_call_t call = GINT_CALL(f, (void *)&args); + + for(int buf_al = 0; buf_al < 4; buf_al++) + for(int len_al = 0; len_al < 4; len_al++) + { + args.buf1 = buf1 + 8 + buf_al; + args.buf2 = buf2 + 8 + buf_al; + args.size = 12 + len_al; + memarray_set(rc, counter++, memory_protected_call(call)); + + args.buf1 = buf1 + 8 + buf_al; + args.buf2 = buf2 + 8 + buf_al; + args.size = 92 + len_al; + memarray_set(rc, counter++, memory_protected_call(call)); + } +} + +void memarray_double(uint8_t *rc, bool overlap, memarray_func_t f) +{ + uint8_t left1[128], left2[128], right1[128], right2[128]; + + int counter = 0; + rc[0] = overlap ? MEMARRAY_RC_DOUBLE_OVERLAP : MEMARRAY_RC_DOUBLE; + + memarray_args_t args = { + .full_left1 = left1, + .full_left2 = left2, + .full_right1 = right1, + .full_right2 = right2, + .full_size = 128, + }; + gint_call_t call = GINT_CALL(f, (void *)&args); + + for(int left_al = 0; left_al < 4; left_al++) + for(int right_al = 0; right_al < 4; right_al++) + for(int len_al = 0; len_al < 4; len_al++) + { + args.left1 = left1 + 8 + left_al; + args.left2 = left2 + 8 + left_al; + args.right1 = right1 + 8 + right_al; + args.right2 = right2 + 8 + right_al; + args.size = 12 + len_al; + memarray_set(rc, counter++, memory_protected_call(call)); + + args.left1 = left1 + 8 + left_al; + args.left2 = left2 + 8 + left_al; + args.right1 = right1 + 8 + right_al; + args.right2 = right2 + 8 + right_al; + args.size = 92 + len_al; + memarray_set(rc, counter++, memory_protected_call(call)); + + if(!overlap) continue; + + /* In overlap mode, only use left1 and left2 to create an overlap */ + args.left1 = left1 + left_al; + args.left2 = left2 + left_al; + args.right1 = left1 + 28 + right_al; + args.right2 = left2 + 28 + right_al; + args.size = 92 + len_al; + memarray_set(rc, counter++, memory_protected_call(call)); + + args.left1 = left1 + 28 + left_al; + args.left2 = left2 + 28 + left_al; + args.right1 = left1 + right_al; + args.right2 = left2 + right_al; + args.size = 92 + len_al; + memarray_set(rc, counter++, memory_protected_call(call)); + } +} + +//--- +// Visualization widget +//--- + +static void paint_results(int x, int y, uint8_t *rc) +{ + dprint_opt(x+222, y, C_BLACK, C_NONE, DTEXT_CENTER, DTEXT_TOP, + "Source align and destination align"); + for(int i = 0; i < 16; i++) + dprint(x + 74 + 18*i + 3*(i/4), y+12, C_BLACK, "%d%d", i/4, i & 3); + + int sizes = 0; + if(rc[0] == MEMARRAY_RC_SINGLE) sizes = 2; + if(rc[0] == MEMARRAY_RC_DOUBLE) sizes = 2; + if(rc[0] == MEMARRAY_RC_DOUBLE_OVERLAP) sizes = 4; + + char const *size_names[4] = { "Small", "Large", "Over-L", "Over-R" }; + for(int i = 0; i < 4; i++) { + dprint_opt(x+55, y + 43 + 42*i, C_BLACK, C_NONE, DTEXT_RIGHT, + DTEXT_MIDDLE, "%s", size_names[i]); + } + + for(int i = 0; i < 16; i++) + dprint_opt(x+64, y + 25 + 10*i + 2*(i/4), C_BLACK, C_NONE, + DTEXT_CENTER, DTEXT_TOP, "%d", i & 3); + + for(int row = 0; row < 16; row++) + for(int col = 0; col < 16; col++) + { + int x1 = x + 72 + 18*col + 3*(col/4); + int y1 = y + 24 + 10*row + 2*(row/4); + + int value = -1; + if(row < 4*sizes) value = memarray_get(rc, 4*sizes * col + row); + int fg = (value == -1) ? C_WHITE : (value == 0) ? C_GREEN : C_RED; + drect_border(x1, y1, x1+19, y1+10, fg, 1, C_BLACK); + } +} + +jwidget *memarray_widget(uint8_t *rc) +{ + jpainted *p = jpainted_create(paint_results, rc, 377, 185, NULL); + if(!p) return NULL; + jwidget_set_margin(p, 4, 4, 4, 4); + return &p->widget; +} diff --git a/src/string/memarray.h b/src/string/memarray.h new file mode 100644 index 0000000..02636eb --- /dev/null +++ b/src/string/memarray.h @@ -0,0 +1,96 @@ +//--- +// string.memarray: Alignment/size combination array for core memory functions +// +// This header provides tools to check memory functions in all size/alignment +// configurations. Up to two buffers can be involved (source and destination), +// and for each buffer all 4n + {0,1,2,3} alignments are tested. Different +// sizes are also tested, each with all possible 4n + {0,1,2,3} values. +// +// By default, two size configurations are tested: +// * 12..15 bytes (intended to use naive, minimum-logic methods) +// * 92..95 bytes (intended to use elaborate methods) +// If two buffers are involved, overlapping sections can also be requested: +// * (32-[64)-32] (approximately; alignment slightly alters values) +// * [32-(64]-32) (approximately; alignment slightly alters values) +// +// This header defines two functions memarray_single() and memarray_double() +// that repeatedly invoke a user-supplied test function with different +// combinations of alignments and sizes. The test function is guarded against +// misaligned memory accesses (which are detected and counted as failures) to +// avoid application crashes. +// +// When invoked, the test function is supplied with a memarray_args_t structure +// that gives buffer addresses and sizes for a single combination. Each buffer +// is provided twice ("name1" and "name2") so that tests requiring a baseline +// (eg. comparing optimized memcpy with naive memcpy) can run the baseline on +// the duplicate buffers. +// +// Test results are recorded in bitmaps based on the return value of the test +// function (0 for success, 1 for failure); this header provides memarray_set() +// and memarray_get() to use the bitmaps (although rarely needed), as well as +// memarray_assert() to record results into an ft_test and memarray_widget() to +// generate result visualizations. +//--- + +#ifndef _STRING_MEMARRAY_H +#define _STRING_MEMARRAY_H + +#include +#include +#include + +/* Size of result buffers to allocate for each configuration; one byte is + reserved to indicate the configuration for the bitmap functions */ + +/* Single buffer: 4 alignments * 8 sizes = 32 tests -> 4 bytes */ +#define MEMARRAY_RC_SINGLE 5 +/* Double buffer : 4 * 4 alignments * 8 sizes = 128 tests -> 16 bytes */ +#define MEMARRAY_RC_DOUBLE 17 +/* Double with overlap : 4 * 4 alignments * 16 sizes = 256 tests -> 32 bytes */ +#define MEMARRAY_RC_DOUBLE_OVERLAP 33 + +/* Arguments to test functions: + * In single mode, buf1 and buf2 are used + * In double mode, left1, left2, right1 and right2 are used */ +typedef struct { + /* Pointers with varying alignment, to test with */ + union { void *buf1, *left1; }; + union { void *buf2, *left2; }; + void *right1; + void *right2; + /* Size of the operation to perform */ + size_t size; + /* Pointers to the orignal buffers (which occupy a larger interval of + memory than the pointers above). This is useful to initialize and check + an area slightly larger than the tested area, to make sure that tested + functions don't write outside their assigned bounds */ + union { void *full_buf1, *full_left1; }; + union { void *full_buf2, *full_left2; }; + void *full_right1; + void *full_right2; + /* Size of the original buffers (128) */ + size_t full_size; + +} memarray_args_t; + +/* Type of test functions; should return 0 for success, 1 for failure */ +typedef int (*memarray_func_t)(memarray_args_t const *args); + +/* Run a test; the result buffer must have a suitable size allocated */ +void memarray_single(uint8_t *rc, memarray_func_t f); +void memarray_double(uint8_t *rc, bool do_overlaps, memarray_func_t f); + +/* Set a bit in the result buffer; if position is out of bounds, no-op */ +void memarray_set(uint8_t *rc, int position, int value); +/* Get a bit in the result buffer; if position is out of bounds, returns -1 */ +int memarray_get(uint8_t *rc, int position); + +/* Assert that every value in the result array is 0 */ +void memarray_assert(uint8_t *rc, ft_test *test); + +/* Create a widget to visualize the contents of a result buffer. The result + pointer is not tied to the widget so it will continue to exist even after + the widget is destroyed. */ +jwidget *memarray_widget(uint8_t *buffer); + +#endif /* _STRING_MEMARRAY_H */ diff --git a/src/test.c b/src/test.c new file mode 100644 index 0000000..1f388dc --- /dev/null +++ b/src/test.c @@ -0,0 +1,114 @@ +#include +#include +#undef ft_assert + +//--- +// Test framework +//--- + +void ft_test_init(ft_test *test) +{ + test->passed = 0; + test->skipped = 0; + test->failed = 0; +} + +void ft_test_run(ft_test *test) +{ + ft_test_init(test); + if(!test->function) return; + test->function(test); +} + +bool ft_test_done(ft_test *test) +{ + return test->passed + test->skipped + test->failed > 0; +} + +void ft_assert(ft_test *test, int expression, char const *file, int line, + char const *str) +{ + if(expression) { + test->passed++; + } + else { + test->failed++; + ft_log(test, "%s:%d: assertion '%s' failed", file, line, str); + } +} + +void ft_log(ft_test *test, char const *format, ...) +{ +} + +//--- +// Test lists (to organize by header) +//--- + +void ft_list_init(ft_list *list) +{ + list->passed = 0; + list->skipped = 0; + list->failed = 0; + + if(!list->children) return; + for(int i = 0; list->children[i]; i++) { + ft_test_init(list->children[i]); + } +} + +void ft_list_run(ft_list *list) +{ + ft_list_init(list); + + if(!list->children) return; + for(int i = 0; list->children[i]; i++) { + ft_test_run(list->children[i]); + } + + ft_list_aggregate(list); +} + +void ft_list_aggregate(ft_list *l) +{ + l->passed = 0; + l->skipped = 0; + l->failed = 0; + + if(!l->children) return; + for(int i = 0; l->children[i]; i++) { + ft_test const *t = l->children[i]; + l->passed += t->passed; + l->skipped += t->skipped; + l->failed += t->failed; + } +} + +//--- +// Utilities +//--- + +int ft_test_color(ft_test const *t) +{ + if(t->failed > 0) return FT_TEST_FAILED; + if(t->skipped > 0) return FT_TEST_SKIPPED; + if(t->passed > 0) return FT_TEST_PASSED; + if(t->function) return FT_TEST_PENDING; + return FT_TEST_EMPTY; +} + +int ft_list_color(ft_list const *l) +{ + if(l->failed > 0) return FT_TEST_FAILED; + if(l->skipped > 0) return FT_TEST_SKIPPED; + + if(l->children) { + for(int i = 0; l->children[i]; i++) { + if(l->children[i]->function && l->children[i]->passed == 0) + return FT_TEST_PENDING; + } + } + + if(l->passed > 0) return FT_TEST_PASSED; + return FT_TEST_EMPTY; +} diff --git a/src/widgets/fbar.c b/src/widgets/fbar.c new file mode 100644 index 0000000..08adc35 --- /dev/null +++ b/src/widgets/fbar.c @@ -0,0 +1,187 @@ +#include +#include +#include +#include + +/* Type identifier for fbar */ +static int fbar_type_id = -1; + +fbar *fbar_create(void *parent) +{ + if(fbar_type_id < 0) return NULL; + + fbar *b = malloc(sizeof *b); + if(!b) return NULL; + + jwidget_init(&b->widget, fbar_type_id, parent); + b->tests = NULL; + b->lists = NULL; + + jwidget_set_margin(b, 0, 8, 2, 8); + + return b; +} + +void fbar_set_tests(fbar *b, ft_test **tests) +{ + b->tests = tests; + b->lists = NULL; + b->widget.update = 1; +} + +void fbar_set_lists(fbar *b, ft_list *lists) +{ + b->tests = NULL; + b->lists = lists; + b->widget.update = 1; +} + +//--- +// Counting statistics +//--- + +struct stats { + /* Number of tests done, pending and empty (proportion of full bar) */ + int16_t done; + int16_t pending; + int16_t empty; + /* Number of assertions (proportion of the done section of the bar) */ + int16_t passed; + int16_t skipped; + int16_t failed; +}; + +static void stats_init(struct stats *st) +{ + memset(st, 0, sizeof *st); +} + +static void stats_add_test(struct stats *st, ft_test *t) +{ + st->passed += t->passed; + st->skipped += t->skipped; + st->failed += t->failed; + + if(!t->function) st->empty++; + else if(t->passed + t->skipped + t->failed == 0) st->pending++; + else st->done++; +} + +static void stats_add_list(struct stats *st, ft_list *l) +{ + if(!l->children) return; + for(int i = 0; l->children[i]; i++) + stats_add_test(st, l->children[i]); +} + +//--- +// Polymorphic widget operations +//--- + +static void fbar_poly_csize(void *b0) +{ + fbar *b = b0; + b->widget.w = 120; + b->widget.h = 13; +} + +static void fbar_poly_render(void *b0, int x, int y) +{ + fbar *b = b0; + int w = jwidget_content_width(b); + int h = jwidget_content_height(b); + + struct stats st, px; + stats_init(&st); + stats_init(&px); + + if(b->tests) for(int i = 0; b->tests[i]; i++) { + stats_add_test(&st, b->tests[i]); + } + else if(b->lists) for(int i = 0; b->lists[i].name; i++) { + stats_add_list(&st, &b->lists[i]); + } + + /* Width of bar for each of the 3 main sections */ + int main = st.done + st.pending + st.empty; + if(main == 0) { + drect_border(x, y, x+w-1, y+h-1, C_WHITE, 1, C_BLACK); + return; + } + + /* Try go guarantee at least 10 pixels per section */ + int guaranteeable_min = 10 * (!!st.done + !!st.pending + !!st.empty); + bool guaranteed = (w > guaranteeable_min); + + if(guaranteed) w -= guaranteeable_min; + px.done = (st.done * w) / main; + px.pending = (st.pending * w) / main; + px.empty = w - px.done - px.pending; + if(guaranteed) { + if(st.done) px.done += 10; + if(st.pending) px.pending += 10; + if(st.empty) px.empty += 10; + w += guaranteeable_min; + } + + /* Width of bar for each of the subsections of done */ + int assertions = st.passed + st.skipped + st.failed; + if(assertions == 0) assertions = 1; + + guaranteeable_min = 10 * (!!st.passed + !!st.skipped + !!st.failed); + guaranteed = (px.done > guaranteeable_min); + + if(guaranteed) px.done -= guaranteeable_min; + px.passed = (st.passed * px.done) / assertions; + px.skipped = (st.skipped * px.done) / assertions; + px.failed = px.done - px.passed - px.skipped; + if(guaranteed) { + if(st.passed) px.passed += 10; + if(st.skipped) px.skipped += 10; + if(st.failed) px.failed += 10; + px.done += guaranteeable_min; + } + + /* Draw */ + #define block(px, val, fg) if(px) { \ + drect(rx, y, rx+px-1, y+h-1, fg); \ + dprint_opt(rx+px/2, y+2, C_BLACK, C_NONE, DTEXT_CENTER, DTEXT_TOP, \ + "%d", val); \ + rx += px; \ + } + int rx = x; + block(px.passed, st.passed, FT_TEST_PASSED); + block(px.skipped, st.skipped, FT_TEST_SKIPPED); + block(px.failed, st.failed, FT_TEST_FAILED); + block(px.pending, st.pending, FT_TEST_PENDING); + block(px.empty, st.empty, FT_TEST_EMPTY); + #undef block + + /* Darken the border for a cool effect */ + for(int dx = 0; dx < w; dx++) { + int i1 = 396 * (y) + (x + dx); + int i2 = 396 * (y + h - 1) + (x + dx); + gint_vram[i1] = (gint_vram[i1] & 0xf7de) >> 1; + gint_vram[i2] = (gint_vram[i2] & 0xf7de) >> 1; + } + for(int dy = 1; dy < h-1; dy++) { + int i1 = 396 * (y + dy) + (x); + int i2 = 396 * (y + dy) + (x + w - 1); + gint_vram[i1] = (gint_vram[i1] & 0xf7de) >> 1; + gint_vram[i2] = (gint_vram[i2] & 0xf7de) >> 1; + } +} + +/* fbar type definition */ +static jwidget_poly type_fbar = { + .name = "fbar", + .csize = fbar_poly_csize, + .render = fbar_poly_render, +}; + +/* Type registration */ +__attribute__((constructor(2000))) +static void j_register_fbar(void) +{ + fbar_type_id = j_register_widget(&type_fbar, "jwidget"); +} diff --git a/src/widgets/fbrowser.c b/src/widgets/fbrowser.c new file mode 100644 index 0000000..08fd2ef --- /dev/null +++ b/src/widgets/fbrowser.c @@ -0,0 +1,273 @@ +#include +#include +#include +#include +#include + +/* Type identifier for fbrowser */ +static int fbrowser_type_id = -1; + +/* Events */ +uint16_t FBROWSER_VALIDATED; + +/* Header list and test list renderers */ +static void render_entries(flist *l, void *data, int is_headers); +/* Update whatever is displayed on the right panel */ +static void update_right_panel(fbrowser *b); + +fbrowser *fbrowser_create(void *parent) +{ + if(fbrowser_type_id < 0) return NULL; + jlabel *l; + + fbrowser *b = malloc(sizeof *b); + if(!b) return NULL; + + b->data_headers = NULL; + b->data_tests = NULL; + + jwidget_init(&b->widget, fbrowser_type_id, parent); + jlayout_set_hbox(b)->spacing = 4; + + // On the left, list of headers + + b->headers = flist_create(GINT_CALL_NULL, b); + flist_set_rows(b->headers, 1); + flist_select(b->headers, 0); + flist_set_row_height(b->headers, 13); + jwidget_set_fixed_width(b->headers, 120); + jwidget_set_stretch(b->headers, 0, 1, false); + + // On the right, either a summary of the whole application... + + b->rstack = jwidget_create(b); + jwidget_set_stretch(b->rstack, 1, 1, false); + jlayout_set_stack(b->rstack); + + jwidget *summary = jwidget_create(b->rstack); + jwidget_set_stretch(summary, 1, 1, false); + jlayout_set_vbox(summary)->spacing = 4; + + b->bar_top = fbar_create(summary); + jwidget_set_stretch(b->bar_top, 1, 0, false); + + l = jlabel_create( + "Summary here!\n\n" + "Green: Passed\n" + "Orange: Skipped\n" + "Red: Failed\n" + "Blue: Not tested\n" + "Gray: Empty\n\n" + "Run tests with F6:\n" + "- Here: runs all tests\n" + "- On a header: all tests in header\n" + "- On a test: just that test", + summary); + jlabel_set_block_alignment(l, J_ALIGN_LEFT, J_ALIGN_TOP); + jlabel_set_line_spacing(l, 3); + jwidget_set_stretch(l, 1, 1, false); + + // ... or a label for headers with no tests yet... + + l = jlabel_create("No test for this header.", b->rstack); + jlabel_set_block_alignment(l, J_ALIGN_LEFT, J_ALIGN_TOP); + jwidget_set_margin(l, 2, 0, 0, 0); + jwidget_set_stretch(l, 1, 1, false); + + // Or a header summary and a test list for the selected header + + jwidget *rvbox = jwidget_create(b->rstack); + jwidget_set_stretch(rvbox, 1, 1, false); + jlayout_set_vbox(rvbox)->spacing = 4; + + b->bar_right = fbar_create(rvbox); + jwidget_set_stretch(b->bar_right, 1, 0, false); + + b->tests = flist_create(GINT_CALL_NULL, rvbox); + flist_set_row_height(b->tests, 13); + jwidget_set_stretch(b->tests, 1, 1, false); + + return b; +} + +void fbrowser_set_headers(fbrowser *b, ft_list *headers, bool select) +{ + int rows = 0; + while(headers[rows].name) rows++; + + flist_set_renderer(b->headers, + GINT_CALL(render_entries, 0 /* Overridden */, (void *)headers, 1)); + flist_set_rows(b->headers, rows + 1); + flist_select(b->headers, select ? 1 : 0); + + jscene *scene = jscene_owning(b); + if(scene) jscene_set_focused_widget(scene, b->headers); + + b->data_headers = headers; + fbar_set_lists(b->bar_top, headers); + update_right_panel(b); + b->widget.update = 1; +} + +void fbrowser_set_tests(fbrowser *b, ft_test **tests) +{ + int rows = 0; + while(tests[rows]) rows++; + + flist_set_renderer(b->tests, + GINT_CALL(render_entries, 0 /* Overridden */, (void *)tests, 0)); + flist_set_rows(b->tests, rows); + flist_select(b->tests, -1); + + b->data_tests = tests; + fbar_set_tests(b->bar_right, tests); + b->widget.update = 1; +} + +ft_list *fbrowser_current_header(fbrowser *b) +{ + if(!b->data_headers || b->headers->selected < 1) return NULL; + return &b->data_headers[b->headers->selected - 1]; +} + +ft_test *fbrowser_current_test(fbrowser *b) +{ + if(!b->data_tests || b->tests->selected < 0) return NULL; + return b->data_tests[b->tests->selected]; +} + +//--- +// Rendering +//--- + +static void render_entries(flist *l, void *data, int is_headers) +{ + jscene *scene = jscene_owning(l); + + bool selected = (l->row == l->selected); + bool focused = (scene && jscene_focused_widget(scene) == l); + + int w = jwidget_content_width(l); + int h = l->row_height; + + if(selected && !focused) { + drect(l->x, l->y, l->x + w - 1, l->y + h - 1, C_RGB(24, 24, 24)); + } + + char const *name = "(null)"; + int color = C_BLACK; + + if(is_headers && l->row != 0) { + ft_list const *headers = data; + ft_list const *header = &headers[l->row - 1]; + color = ft_list_color(header); + name = header->name; + } + else { + ft_test const **tests = data; + ft_test const *test = tests[l->row]; + color = ft_test_color(test); + name = test->name; + } + + if(is_headers && l->row == 0) + dtext(l->x + 2, l->y + 2, C_BLACK, "Summary"); + else + dprint(l->x + 16, l->y + 2, C_BLACK, "%s", name); + + if(selected && focused) { + drect(l->x, l->y, l->x + w - 1, l->y + h - 1, C_INVERT); + } + + if(is_headers && l->row == 0) return; + for(int y = 0; y < 9; y++) + for(int x = 0; x < 9; x++) + { + if((x == 0 || x == 8) && (y == 0 || y == 8)) continue; + dpixel(l->x + 3 + x, l->y + 2 + y, color); + } +} + +//--- +// Polymorphic widget operations +//--- + +void update_right_panel(fbrowser *b) +{ + jlayout_stack *ls = jlayout_get_stack(b->rstack); + + ft_list *header = fbrowser_current_header(b); + ft_test **tests = header ? header->children : NULL; + + if(!header) { + ls->active = 0; + } + else if(!tests) { + ls->active = 1; + } + else { + fbrowser_set_tests(b, tests); + ls->active = 2; + } + + b->widget.update = 1; +} + +static bool fbrowser_poly_event(void *b0, jevent e) +{ + fbrowser *b = b0; + jscene *scene = jscene_owning(b); + + int key = (e.type==JSCENE_KEY || e.key.type==KEYEV_DOWN) ? e.key.key : 0; + void *f = scene ? jscene_focused_widget(scene) : NULL; + + /* Update right panel when moving in the header list */ + if(e.type == FLIST_SELECTED && e.source == b->headers) { + update_right_panel(b); + return true; + } + /* Move focus to the test list when validating a header */ + if(e.type == FLIST_VALIDATED && e.source == b->headers) { + ft_list *header = fbrowser_current_header(b); + + if(header && header->children) { + if(scene) jscene_set_focused_widget(scene, b->tests); + flist_select(b->tests, 0); + b->widget.update = 1; + } + return true; + } + /* Move focus back when pressing EXIT */ + if(key == KEY_EXIT && f == b->tests) { + jscene_set_focused_widget(scene, b->headers); + flist_select(b->tests, -1); + b->widget.update = 1; + return true; + } + + /* Emit validation when pressing EXE on a test */ + if(e.type == FLIST_VALIDATED && e.source == b->tests) { + ft_test *test = fbrowser_current_test(b); + if(test) { + jevent e = { .source = b, .type = FBROWSER_VALIDATED }; + jwidget_emit(b, e); + } + return true; + } + + return false; +} + +/* fbrowser type definition */ +static jwidget_poly type_fbrowser = { + .name = "fbrowser", + .event = fbrowser_poly_event, +}; + +/* Type registration */ +__attribute__((constructor(2000))) +static void j_register_fbrowser(void) +{ + fbrowser_type_id = j_register_widget(&type_fbrowser, "jwidget"); + FBROWSER_VALIDATED = j_register_event(); +} diff --git a/src/widgets/flist.c b/src/widgets/flist.c new file mode 100644 index 0000000..5d6202b --- /dev/null +++ b/src/widgets/flist.c @@ -0,0 +1,217 @@ +#include +#include +#include +#include + +/* Type identifier for flist */ +static int flist_type_id = -1; + +/* Events */ +uint16_t FLIST_SELECTED; +uint16_t FLIST_VALIDATED; + +/* Update the count of visible items after a layout change */ +static void update_visible(flist *l); + +flist *flist_create(gint_call_t renderer, void *parent) +{ + if(flist_type_id < 0) return NULL; + + flist *l = malloc(sizeof *l); + if(!l) return NULL; + + jwidget_init(&l->widget, flist_type_id, parent); + + l->renderer = renderer; + l->rows = 0; + l->visible = 0; + l->top = 0; + l->selected = -1; + l->row_height = 3; + + return l; +} + +void flist_set_renderer(flist *l, gint_call_t renderer) +{ + l->renderer = renderer; +} + +//--- +// Configuration of rows +//--- + +void flist_set_rows(flist *l, int rows) +{ + if(l->rows == rows) return; + l->rows = max(rows, 0); + + if(l->selected < 0 && l->rows > 0) + l->selected = 0; + if(l->selected >= l->rows) + l->selected = l->rows - 1; + + l->widget.dirty = 1; +} + +void flist_set_row_height(flist *l, int row_height) +{ + if(l->row_height == row_height) return; + + l->row_height = row_height; + l->widget.dirty = 1; +} + +//--- +// Movement +//--- + +/* Guarantee that the positioning is valid */ +static int bound(flist *l, int pos) +{ + pos = max(pos, 0); + pos = min(pos, flist_end(l)); + return pos; +} + +void flist_scroll_to(flist *l, int offset) +{ + l->top = bound(l, offset); +} + +void flist_select(flist *l, int entry) +{ + if(entry < -1 || entry >= l->rows) return; + l->selected = entry; + l->widget.update = 1; + + /* TODO: flist_select(): Scroll to show selected entry */ +} + +int flist_end(flist *l) +{ + update_visible(l); + return max((int)l->rows - (int)l->visible, 0); +} + +//--- +// Polymorphic widget operations +//--- + +/* Recompute (visible) based on the current size */ +static void update_visible(flist *l) +{ + if(l->visible != 0) return; + l->visible = max(0, jwidget_content_height(l) / l->row_height); +} + +static void flist_poly_csize(void *l0) +{ + flist *l = l0; + l->widget.w = 64; + l->widget.h = 32; +} + +static void flist_poly_layout(void *l0) +{ + flist *l = l0; + update_visible(l); +} + +static void flist_poly_render(void *l0, int x, int base_y) +{ + flist *l = l0; + int cw = jwidget_content_width(l); + int y = base_y; + + for(uint i = 0; l->top + i < l->rows && i < l->visible; i++) { + l->x = x; + l->y = y; + l->row = l->top + i; + + l->renderer.args[0] = (gint_call_arg_t)(void *)l; + gint_call(l->renderer); + + y += l->row_height; + } + + /* Scrollbar */ + if(l->visible < l->rows) { + /* Area where the scroll bar lives */ + int area_w = _(1,2); + int area_x = x + cw - area_w; + int area_y = base_y; + int area_h = jwidget_content_height(l); + + /* Position and size of scroll bar within the area */ + int bar_y = (l->top * area_h + l->rows/2) / l->rows; + int bar_h = (l->visible * area_h + l->rows/2) / l->rows; + + drect(area_x, area_y, area_x+area_w-1, area_y+area_h-1, C_RGB(24,24,24)); + drect(area_x, area_y + bar_y, area_x + area_w - 1, + area_y + bar_y + bar_h, C_BLACK); + } +} + +static bool flist_poly_event(void *l0, jevent e) +{ + flist *l = l0; + update_visible(l); + + if(e.type != JWIDGET_KEY) return false; + if(e.key.type == KEYEV_UP) return false; + + if(e.key.key == KEY_DOWN && l->selected < l->rows - 1) { + if(e.key.shift) l->selected = l->rows - 1; + else l->selected++; + + /* Make sure the selected item remains visible */ + if(l->selected - l->top > l->visible - 1) + l->top = bound(l, l->selected - l->visible + 1); + + l->widget.update = 1; + + jevent e = { .source = l, .type = FLIST_SELECTED }; + jwidget_emit(l, e); + return true; + } + if(e.key.key == KEY_UP && l->selected > 0) { + if(e.key.shift) l->selected = 0; + else l->selected--; + + /* Same thing as before */ + if(l->selected < l->top) + l->top = bound(l, l->selected); + + jevent e = { .source = l, .type = FLIST_SELECTED }; + jwidget_emit(l, e); + l->widget.update = 1; + return true; + } + + if(e.key.key == KEY_EXE) { + jevent e = { .source = l, .type = FLIST_VALIDATED }; + jwidget_emit(l, e); + return true; + } + + return false; +} + +/* flist type definition */ +static jwidget_poly type_flist = { + .name = "flist", + .csize = flist_poly_csize, + .layout = flist_poly_layout, + .render = flist_poly_render, + .event = flist_poly_event, +}; + +/* Type registration */ +__attribute__((constructor(2000))) +static void j_register_flist(void) +{ + flist_type_id = j_register_widget(&type_flist, "jwidget"); + FLIST_SELECTED = j_register_event(); + FLIST_VALIDATED = j_register_event(); +} diff --git a/src/widgets/gscreen.c b/src/widgets/gscreen.c new file mode 100644 index 0000000..2b6b62b --- /dev/null +++ b/src/widgets/gscreen.c @@ -0,0 +1,188 @@ +#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; + g->fkey_level = 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_destroy(gscreen *s) +{ + if(s->scene) jwidget_destroy(s->scene); + free(s->tabs); + free(s); +} + +/* tab_stack(): Stacked widget where the tabs are located */ +static jwidget *tab_stack(gscreen *s) +{ + int index = (s->title != NULL) ? 1 : 0; + return s->scene->widget.children[index]; +} + +//--- +// Function bar settings +//--- + +void gscreen_set_fkeys_level(gscreen *s, int level) +{ + s->fkey_level = level; +} + +//--- +// Tab settings +//--- + +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_add_child(tab_stack(s), 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); +} + +void gscreen_set_tab_title_visible(gscreen *s, int tab, bool visible) +{ + if(!s->title || tab < 0 || tab >= s->tab_count) return; + s->tabs[tab].title_visible = visible; + + if(gscreen_current_tab(s) == tab) + jwidget_set_visible(s->title, visible); +} + +void gscreen_set_tab_fkeys_visible(gscreen *s, int tab, bool visible) +{ + if(!s->fkeys || tab < 0 || tab >= s->tab_count) return; + s->tabs[tab].fkeys_visible = visible; + + if(gscreen_current_tab(s) == tab) + jwidget_set_visible(s->fkeys, visible); +} + +//--- +// Tab navigation +//--- + +bool gscreen_show_tab(gscreen *s, int tab) +{ + jwidget *stack = tab_stack(s); + 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 = tab_stack(s); + jlayout_stack *l = jlayout_get_stack(stack); + return l->active; +} + +bool gscreen_in(gscreen *s, int tab) +{ + return gscreen_current_tab(s) == tab; +} + +//--- +// Focus management +//--- + +void gscreen_focus(gscreen *s, void *widget) +{ + return jscene_set_focused_widget(s->scene, widget); +}