From c4d2a5dc88ee999cab7a8ccde66a14520e975ae4 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 6 Sep 2026 17:35:09 +0100 Subject: [PATCH 01/98] fix(wuss): keep parent menu rows highlit while a submenu is open A single global wuss->hover_icon meant moving the pointer from a parent menu row into its submenu window switched the highlight to the submenu row and dropped it from the parent, unlike the RISC OS feel where every ancestor row on the open-submenu path stays lit. wuss__icon_set_hover now consults a new wuss__menu_row_pinned() before dropping the previous hover icon's highlight: a row that is some open chain level's open_index with a live child keeps its highlight. When a re-entering move closes a submenu, menu.c clears the ex-parent row's highlight explicitly unless the pointer landed on that row. Covers arbitrary nesting (walks the whole chain) and borrowed-window submenus (keyed off open_index/child). WUSS_ICONS can build without WUSS_MENUS, so the call site is #ifdef WUSS_MENUS guarded. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/icon/set-hover.c | 7 ++ libraries/wuss/menu.h | 6 ++ libraries/wuss/menu/menu.c | 29 ++++++ libraries/wuss/test/wuss-test.c | 156 ++++++++++++++++++++++++++++++++ 4 files changed, 198 insertions(+) diff --git a/libraries/wuss/icon/set-hover.c b/libraries/wuss/icon/set-hover.c index 1c55d1f..090c91d 100644 --- a/libraries/wuss/icon/set-hover.c +++ b/libraries/wuss/icon/set-hover.c @@ -20,7 +20,14 @@ void wuss__icon_set_hover(wuss_t *wuss, wuss_icon_t *icon) if (prev == icon) return; + /* Every ancestor row on an open menu chain's open-submenu path keeps its + * highlight while the pointer is down in a deeper level (RISC OS feel), so + * don't drop `prev`'s highlight if it is one of those rows. */ +#ifdef WUSS_MENUS + if (prev != NULL && !wuss__menu_row_pinned(wuss, prev)) +#else if (prev != NULL) +#endif { wuss__icon_set_state(prev, wuss_ICON_STATE_HOVERED, 0); if (wuss__icon_hover_visible(prev)) diff --git a/libraries/wuss/menu.h b/libraries/wuss/menu.h index 482b392..2c0e451 100644 --- a/libraries/wuss/menu.h +++ b/libraries/wuss/menu.h @@ -57,4 +57,10 @@ struct wuss__menu * and return 1 (the click is spent on dismissal). Returns 0 otherwise. */ int wuss__menu_click_outside(wuss_t *wuss, const wuss_window_t *hit); +/* True if `icon` is a parent row on an open menu chain whose submenu is open -- + * i.e. some chain level's `open_index` row. Such a row keeps its hover + * highlight while the pointer is in a deeper level; wuss__icon_set_hover asks + * before dropping a row's highlight. */ +int wuss__menu_row_pinned(const wuss_t *wuss, const wuss_icon_t *icon); + #endif /* WUSS_MENU_IMPL_H */ diff --git a/libraries/wuss/menu/menu.c b/libraries/wuss/menu/menu.c index ea245eb..2f7099f 100644 --- a/libraries/wuss/menu/menu.c +++ b/libraries/wuss/menu/menu.c @@ -268,9 +268,23 @@ static result_t wuss__menu_handle(wuss_window_t *window, * this row's text off the arrow -- closes the child it opened. */ if (self->child != NULL && !(self->open_index == index && on_arrow)) { + wuss_icon_t *was_parent; + + was_parent = (self->open_index >= 0) ? self->icons[self->open_index] + : NULL; + wuss__menu_close_from(self->child); self->child = NULL; self->open_index = -1; + + /* The ex-parent row was held highlit while its submenu was open (see + * wuss__menu_row_pinned). Now the submenu is gone, drop that highlight + * unless the pointer has landed back on that very row. */ + if (was_parent != NULL && was_parent != icon) + { + wuss__icon_set_state(was_parent, wuss_ICON_STATE_HOVERED, 0); + wuss__icon_invalidate(was_parent); + } } if (self->child != NULL) @@ -926,6 +940,21 @@ void wuss_menu_set_item_ticked(wuss_menu_handle_t handle, /* ----------------------------------------------------------------------- */ +int wuss__menu_row_pinned(const wuss_t *wuss, const wuss_icon_t *icon) +{ + const struct wuss__menu *node; + + for (node = wuss->menu_chain; node != NULL; node = node->child) + { + if (node->borrowed || node->child == NULL || node->open_index < 0) + continue; + if (node->icons[node->open_index] == icon) + return 1; + } + + return 0; +} + int wuss__menu_click_outside(wuss_t *wuss, const wuss_window_t *hit) { struct wuss__menu *node; diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index c56d158..1129ace 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -302,6 +302,32 @@ static void flash_pick_row(wuss_t *wuss, wuss_mouse_click(wuss, at, button, wuss_MOUSE_UP, NULL); } +/* Move the pointer over row `row` of menu level `level`: to the text column + * (`over_arrow` == 0) or into the submenu-arrow gutter at the row's right edge + * (`over_arrow` == 1). Used to drive submenu open/close from the tests. */ +static void menu_move_over_row(wuss_t *wuss, + struct wuss__menu *level, + int row, + int over_arrow) +{ + box_t content; + box_t bbox; + box_t screen_box; + point_t scroll; + point_t at; + + wuss_window_get_content_bounds(level->window, &content); + wuss_window_get_scroll(level->window, &scroll); + wuss_icon_get_bbox(level->icons[row], &bbox); + wuss__icon_box_to_screen(&content, scroll, &bbox, &screen_box); + + at.x = over_arrow ? screen_box.x1 - 4 + : screen_box.x0 + 4; + at.y = (screen_box.y0 + screen_box.y1) / 2; + + wuss_mouse_move(wuss, at, NULL); +} + /* A task that opens its own menu on a MENU press over its window, the way a * real content task (e.g. greeble) does. task_data points to a menu_task_t * so the test can see the press arrived and which menu it raised. */ @@ -4251,6 +4277,136 @@ ColourMenuOK: ; return result_TEST_FAILED; } + printf("test: a parent menu row keeps its highlight while its submenu is " + "open, and loses it once the submenu closes\n"); + { + static const wuss_menu_item_t sm_sub_items[] = + { + { "Sub-A", wuss_MENU_ITEM_NONE, NULL }, + { "Sub-B", wuss_MENU_ITEM_NONE, NULL } + }; + static const wuss_menu_t sm_sub = + { + "Sub", sm_sub_items, NELEMS(sm_sub_items) + }; + static const wuss_menu_item_t sm_items[] = + { + { "Plain", wuss_MENU_ITEM_NONE, NULL }, + { "More", wuss_MENU_ITEM_NONE, &sm_sub }, /* row 1 owns the submenu */ + { "Bottom", wuss_MENU_ITEM_NONE, NULL } + }; + static const wuss_menu_t sm_menu = + { + "Root", sm_items, NELEMS(sm_items) + }; + + const char *fontfile; + bmfont_t *font = NULL; + wuss_font_desc_t fdesc; + screen_t sscr; + bitmap_t sbm; + void *spixels; + wuss_t *swuss; + test_task_t stc; + wuss_task_t *sowner; + struct wuss__menu *root; + struct wuss__menu *sub; + + fontfile = path_join_filename(resources, 3, "resources", "bmfonts", + path_join_leafname("Tiny", "png")); + rc = bmfont_create(fontfile, &font); + if (rc != result_OK) + { + printf("wuss_test: submenu-highlight test could not load %s\n", fontfile); + goto Failure; + } + + spixels = malloc((size_t) rowbytes * 200); + if (spixels == NULL) { rc = result_OOM; goto SubHiFail; } + rc = bitmap_init(&sbm, SIZE2D(200, 200), pixelfmt_bgrx8888, rowbytes, + NULL, spixels); + if (rc != result_OK) goto SubHiFailFree; + screen_for_bitmap(&sscr, &sbm); + + fdesc.font = font; + fdesc.font_class = wuss_FONT_CLASS_NONE; + fdesc.name = NULL; + rc = wuss_create(&sscr, &fdesc, 1, NULL, 0, NULL, NULL, &swuss); + if (rc != result_OK) goto SubHiFailFree; + + memset(&stc, 0, sizeof(stc)); + sowner = mk_task(swuss, test_handle, &stc); + if (sowner == NULL) { rc = result_OOM; goto SubHiDestroy; } + + rc = wuss_menu_open(sowner, &sm_menu, POINT(40, 40), NULL); + if (rc != result_OK) goto SubHiDestroy; + + root = swuss->menu_chain; + if (root == NULL || root->menu != &sm_menu) goto SubHiCheckFail; + + /* pointer onto row 1's arrow gutter: its submenu opens */ + menu_move_over_row(swuss, root, 1, 1); + if (root->child == NULL) goto SubHiCheckFail; + if (root->open_index != 1) goto SubHiCheckFail; + sub = root->child; + if (sub->menu != &sm_sub) goto SubHiCheckFail; + if (!wuss__icon_hovered(root->icons[1])) goto SubHiCheckFail; /* parent lit */ + + /* pointer travels into the submenu, onto its row 0. The parent row that + * spawned it must keep its highlight; the submenu row gets one too. */ + menu_move_over_row(swuss, sub, 0, 0); + if (root->child != sub) goto SubHiCheckFail; /* still open */ + if (!wuss__icon_hovered(root->icons[1])) goto SubHiCheckFail; /* retained */ + if (!wuss__icon_hovered(sub->icons[0])) goto SubHiCheckFail; + + /* and while parked over the submenu's own title strip (no row under the + * pointer, so no ICON event) the parent highlight still holds */ + { + box_t cb; + point_t p; + + wuss_window_get_content_bounds(sub->window, &cb); + p.x = (cb.x0 + cb.x1) / 2; + p.y = cb.y0 - 3; /* just above the content: the titlebar */ + wuss_mouse_move(swuss, p, NULL); + if (!wuss__icon_hovered(root->icons[1])) goto SubHiCheckFail; + } + + /* pointer re-enters the parent on a different row (row 2, off the arrow): + * the submenu closes and row 1 must go dark, row 2 lit */ + menu_move_over_row(swuss, root, 2, 0); + if (root->child != NULL) goto SubHiCheckFail; /* closed */ + if (root->open_index != -1) goto SubHiCheckFail; + if (wuss__icon_hovered(root->icons[1])) goto SubHiCheckFail; /* dropped */ + if (!wuss__icon_hovered(root->icons[2])) goto SubHiCheckFail; + + /* re-open, then re-enter the parent on the *same* row's text (off the + * arrow): submenu closes, and row 1 stays lit because the pointer is on it */ + menu_move_over_row(swuss, root, 1, 1); + if (root->child == NULL) goto SubHiCheckFail; + menu_move_over_row(swuss, root, 1, 0); + if (root->child != NULL) goto SubHiCheckFail; + if (!wuss__icon_hovered(root->icons[1])) goto SubHiCheckFail; + + wuss_menu_close(root); + rc = result_OK; + goto SubHiDestroy; + +SubHiCheckFail: + printf("wuss_test: submenu-highlight check failed\n"); + rc = result_TEST_FAILED; + +SubHiDestroy: + reap_test_tasks(); + wuss_destroy(swuss); +SubHiFailFree: + free(spixels); +SubHiFail: + bmfont_destroy(font); + if (rc != result_OK) + return result_TEST_FAILED; + } + printf("test: a MENU press over another window closes the open menu and " "reaches that window's task so it opens its own menu\n"); { From 8816c8048738d470940585d7b0ea295347e12900 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 6 Sep 2026 18:08:04 +0100 Subject: [PATCH 02/98] refactor(wuss): move the descriptor menu demo into the image task Drop the Test submenu (Menu / Menu-desc exercisers) and its static menu tree, the hidden Details window and the print-only fallback from the demo task launcher. The launcher menu is now just Launch / Quit Wuss. The descriptor-built menu (wuss_menu_create_from_desc) moves to the image task, opened on a MENU-button click, rebuilt on each open and freed at QUIT. image_task_t gains wuss / delegate / menu fields for it. tasks_teardown is gone with the last thing it freed; its call in main.c and declaration in tasks.h go too, along with the now-unused stdio.h, geom/box.h and wuss/menu-desc.h includes in tasks.c. Co-Authored-By: Claude Sonnet 5 --- apps/wuss/main.c | 2 - apps/wuss/tasks.c | 147 +----------------------------- apps/wuss/tasks.h | 10 +- libraries/wuss/test/tasks/image.c | 55 ++++++++++- libraries/wuss/test/tasks/image.h | 6 ++ 5 files changed, 66 insertions(+), 154 deletions(-) diff --git a/apps/wuss/main.c b/apps/wuss/main.c index 235f6ba..940af95 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -382,8 +382,6 @@ static result_t run_wuss(const char *resources) * block. Harmless at process exit. */ wuss_destroy(wuss); /* also sweeps g.menu_task and closes any open chain */ - tasks_teardown(); /* frees the descriptor-built menu, now the chain is gone */ - for (i = 0; i < nfonts; i++) bmfont_destroy(fonts[i]); diff --git a/apps/wuss/tasks.c b/apps/wuss/tasks.c index 9a6e515..bcab8c3 100644 --- a/apps/wuss/tasks.c +++ b/apps/wuss/tasks.c @@ -1,6 +1,5 @@ /* wuss/tasks.c -- Wuss demo task launcher and menu wiring */ -#include #include #include @@ -9,13 +8,11 @@ #include "base/utils.h" #include "framebuf/bitmap.h" #include "framebuf/colour.h" -#include "geom/box.h" #include "io/path.h" #include "wuss/task.h" #include "wuss/wuss.h" #include "wuss/window.h" #include "wuss/menu.h" -#include "wuss/menu-desc.h" #include "frontend.h" #include "tasks.h" @@ -262,108 +259,6 @@ static result_t spawn_porter_duff(void) return result_OK; } -/* A static demo menu tree for the pop-up helper: a submenu, a couple of - * ticked rows and a standalone dashed rule row above the final entry. - * wuss_menu_open never mutates it. */ -static const wuss_menu_item_t g_menu_export_items[] = -{ - { "As PNG", wuss_MENU_ITEM_NONE, NULL }, - { "As JPEG", wuss_MENU_ITEM_NONE, NULL }, - { "As GIF", wuss_MENU_ITEM_DISABLED, NULL } -}; - -static const wuss_menu_t g_menu_export = -{ - "Export", g_menu_export_items, NELEMS(g_menu_export_items) -}; - -/* A caller-owned window wired as a menu item's `.window`: hovering "Details" - * shows it where a submenu would open, and moving off the row (or dismissing - * the menu) hides it again. Created once, lazily, by spawn_menu. */ -static wuss_window_t *g_menu_details_window; - -static wuss_menu_item_t g_menu_items[] = -{ - { "Open", wuss_MENU_ITEM_NONE, NULL, NULL }, - { "Show grid", wuss_MENU_ITEM_TICKED, NULL, NULL }, - { "Wireframe", wuss_MENU_ITEM_TICKED, NULL, NULL }, - { "Export", wuss_MENU_ITEM_NONE, &g_menu_export, NULL }, - { "Details", wuss_MENU_ITEM_NONE, NULL, NULL }, /* .window set in spawn_menu */ - { "Quit", wuss_MENU_ITEM_DASHED, NULL, NULL } -}; - -static const wuss_menu_t g_menu = -{ - "Display", g_menu_items, NELEMS(g_menu_items) -}; - -/* index of the "Details" row in g_menu_items */ -#define G_MENU_DETAILS_INDEX 4 - -static result_t spawn_menu(void) -{ - if (g_menu_details_window == NULL) - { - box_t content; - result_t rc; - - /* a small hidden window; wuss fills its background, no task needed */ - content.x0 = 0; - content.y0 = 0; - content.x1 = 180; - content.y1 = 120; - rc = wuss_window_create(g.menu_task, &content, "Details", - wuss_WINDOW_NO_CLOSE | wuss_WINDOW_NO_BACK - | wuss_WINDOW_NO_TOGGLE_SIZE - | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL - | wuss_WINDOW_NO_RESIZE | wuss_WINDOW_HIDDEN, - wuss_BACKDROP_COLOUR(1), - SIZE2D(180, 120), SIZE2D(0, 0), - &g_menu_details_window); - if (rc != result_OK) - return rc; - g_menu_items[G_MENU_DETAILS_INDEX].window = g_menu_details_window; - } - - return wuss_menu_open(g.menu_task, &g_menu, wuss_get_pointer(g.wuss), NULL); -} - -/* Same menu shape built from a descriptor string, to exercise - * wuss_menu_create_from_desc. The tree must outlive the open chain, so it is - * kept here and rebuilt (previous one freed) on each open. Freed for good in - * tasks_teardown. */ -static wuss_menu_t *g_menu_desc; - -static const wuss_menu_item_t g_menu_desc_export_items[] = -{ - { "As PNG", wuss_MENU_ITEM_NONE, NULL }, - { "As JPEG", wuss_MENU_ITEM_NONE, NULL }, - { "As GIF", wuss_MENU_ITEM_DISABLED, NULL } -}; - -static const wuss_menu_t g_menu_desc_export = -{ - "Export", g_menu_desc_export_items, NELEMS(g_menu_desc_export_items) -}; - -static result_t spawn_menu_desc(void) -{ - wuss_menu_t *m; - result_t rc; - - rc = wuss_menu_create_from_desc(&m, - "Display, Open, !Show grid, !Wireframe, >Export, |Quit", - &g_menu_desc_export); - if (rc != result_OK) - return rc; - - wuss_menu_destroy(g_menu_desc); - g_menu_desc = m; - - return wuss_menu_open(g.menu_task, g_menu_desc, wuss_get_pointer(g.wuss), - NULL); -} - /* The task launcher is a MENU-button pop-up over the backdrop rather than a * window of buttons. Each leaf menu pairs a *_items table with a *_spawn table * in lock-step: picking row i of that menu calls its spawn[i]. */ @@ -410,34 +305,15 @@ static result_t spawn_quit(void) return result_OK; } -/* "Test" submenu: the menu-system exercisers. */ -static const wuss_menu_item_t g_test_items[] = -{ - { "Menu", wuss_MENU_ITEM_NONE, NULL }, - { "Menu (desc)", wuss_MENU_ITEM_NONE, NULL } -}; - -static const task_spawn_fn_t g_test_spawn[] = -{ - spawn_menu, spawn_menu_desc -}; - -static const wuss_menu_t g_test_menu = -{ - "Test", g_test_items, NELEMS(g_test_items) -}; - static const wuss_menu_item_t g_task_items[] = { { "Launch", wuss_MENU_ITEM_NONE, &g_launch_menu, NULL }, - { "Test", wuss_MENU_ITEM_DASHED, &g_test_menu, NULL }, { "Quit Wuss", wuss_MENU_ITEM_DASHED, NULL, NULL } }; static const task_spawn_fn_t g_task_spawn[] = { NULL, /* "Launch" -> submenu g_launch_menu */ - NULL, /* "Test" -> submenu g_test_menu */ spawn_quit }; @@ -446,10 +322,9 @@ static const wuss_menu_t g_task_menu = "Tasks", g_task_items, NELEMS(g_task_items) }; -/* g_task_menu / g_launch_menu / g_test_menu picks are dispatched by index; - * g_menu / g_menu_desc picks just print. Every menu is opened by g.menu_task, - * so one handler sees every wuss_EVENT_MENU_SELECT and tells them apart by - * data.menu_select.menu. */ +/* g_task_menu / g_launch_menu picks are dispatched by index. Every menu is + * opened by g.menu_task, so one handler sees every wuss_EVENT_MENU_SELECT and + * tells them apart by data.menu_select.menu. */ result_t task_handle_event(wuss_window_t *window, const wuss_event_t *event, void *task_data) @@ -488,13 +363,6 @@ result_t task_handle_event(wuss_window_t *window, return result_OK; } - if (menu == &g_test_menu) - { - if (index >= 0 && index < (int) NELEMS(g_test_spawn)) - (void) g_test_spawn[index](); - return result_OK; - } - if (menu == &g_task_menu) { if (index >= 0 && index < (int) NELEMS(g_task_spawn) && g_task_spawn[index]) @@ -502,8 +370,6 @@ result_t task_handle_event(wuss_window_t *window, return result_OK; } - printf("menu: picked \"%s\"\n", - menu->items[index].text ? menu->items[index].text : "(sep)"); return result_OK; } @@ -511,10 +377,3 @@ result_t tasks_open_launcher(point_t pos) { return wuss_menu_open(g.menu_task, &g_task_menu, pos, NULL); } - -void tasks_teardown(void) -{ - /* menus are only safe to free once wuss_destroy has torn down any chain - * that was still borrowing them */ - wuss_menu_destroy(g_menu_desc); -} diff --git a/apps/wuss/tasks.h b/apps/wuss/tasks.h index 3907f97..08aa8c5 100644 --- a/apps/wuss/tasks.h +++ b/apps/wuss/tasks.h @@ -22,7 +22,7 @@ extern struct wuss_app_tasks { wuss_t *wuss; - wuss_task_t *menu_task; /* owns the menus and the hidden Details window */ + wuss_task_t *menu_task; /* owns the task launcher menus */ colour_t *palette; int npalette; const char *palette_name; /* startup *.hex leafname, for ticking the @@ -44,12 +44,8 @@ result_t task_handle_event(wuss_window_t *window, const wuss_event_t *event, void *task_data); -/* open the top-level task launcher (Launch / Test / Quit Wuss) at pos; called - * on a MENU click over bare backdrop */ +/* open the top-level task launcher (Launch / Quit Wuss) at pos; called on a + * MENU click over bare backdrop */ result_t tasks_open_launcher(point_t pos); -/* free the descriptor-built menu tree kept alive across opens; call in - * run_wuss's teardown after wuss_destroy */ -void tasks_teardown(void); - #endif /* WUSS_APP_TASKS_H */ diff --git a/libraries/wuss/test/tasks/image.c b/libraries/wuss/test/tasks/image.c index 2f80cda..193a8ab 100644 --- a/libraries/wuss/test/tasks/image.c +++ b/libraries/wuss/test/tasks/image.c @@ -2,6 +2,7 @@ #ifdef WUSS_APP +#include #include #include @@ -15,6 +16,8 @@ #include "geom/box.h" #include "io/dirscan.h" #include "io/path.h" +#include "wuss/menu.h" +#include "wuss/menu-desc.h" #include "image.h" @@ -57,9 +60,12 @@ result_t image_create(wuss_t *wuss, result_t rc; size2d_t sz; + task->wuss = wuss; + task->delegate = NULL; task->resources = resources; task->index = 0; task->nnames = 0; + task->menu = NULL; images_dir = path_join_filename(resources, 2, "resources", "images"); rc = dirscan_walk(images_dir, image__scan_entry, task); @@ -96,6 +102,7 @@ result_t image_create(wuss_t *wuss, free(task); /* nothing registered yet; the spawner will not free it */ return rc; } + task->delegate = delegate; wuss_task_set_autoclose(delegate, 1); sz.w = task->bitmap.size.w + NINEPATCHSZ * 2; @@ -181,11 +188,47 @@ static result_t image_click(wuss_window_t *window, return wuss_window_set_doc(window, sz); } +/* Same menu shape built from a descriptor string, to exercise + * wuss_menu_create_from_desc. The tree must outlive the open chain, so it is + * kept on the task and rebuilt (previous one freed) on each open. Freed for + * good in the QUIT handler. */ +static const wuss_menu_item_t image_menu_export_items[] = +{ + { "As PNG", wuss_MENU_ITEM_NONE, NULL }, + { "As JPEG", wuss_MENU_ITEM_NONE, NULL }, + { "As GIF", wuss_MENU_ITEM_DISABLED, NULL } +}; + +static const wuss_menu_t image_menu_export = +{ + "Export", image_menu_export_items, NELEMS(image_menu_export_items) +}; + +static result_t image_open_menu(image_task_t *ic) +{ + wuss_menu_t *m; + result_t rc; + + rc = wuss_menu_create_from_desc(&m, + "Display, Open, !Show grid, !Wireframe, >Export, |Quit", + &image_menu_export); + if (rc != result_OK) + return rc; + + wuss_menu_destroy(ic->menu); + ic->menu = m; + + return wuss_menu_open(ic->delegate, ic->menu, wuss_get_pointer(ic->wuss), + NULL); +} + result_t image_handle(wuss_window_t *window, const wuss_event_t *event, void *task_data) { - image_task_t *ic; + image_task_t *ic; + const wuss_menu_t *menu; + int index; ic = task_data; @@ -201,9 +244,19 @@ result_t image_handle(wuss_window_t *window, return image_click(window, ic, 1); if (event->data.mouse.button & wuss_BUTTON_ADJUST) return image_click(window, ic, -1); + if (event->data.mouse.button & wuss_BUTTON_MENU) + return image_open_menu(ic); + return result_OK; + + case wuss_EVENT_MENU_SELECT: + menu = event->data.menu_select.menu; + index = event->data.menu_select.index; + printf("image menu: picked \"%s\"\n", + menu->items[index].text ? menu->items[index].text : "(sep)"); return result_OK; case wuss_EVENT_QUIT: + wuss_menu_destroy(ic->menu); free(ic->bitmap.base); free(ic->ninepatch.base); free(ic); /* task_data was calloc'd per instance by the spawner */ diff --git a/libraries/wuss/test/tasks/image.h b/libraries/wuss/test/tasks/image.h index cfba7e3..ec292b0 100644 --- a/libraries/wuss/test/tasks/image.h +++ b/libraries/wuss/test/tasks/image.h @@ -6,6 +6,7 @@ #ifdef WUSS_APP #include "framebuf/bitmap.h" +#include "wuss/menu.h" #include "wuss/window.h" #define IMAGE_MAX_NAMES 64 @@ -17,6 +18,8 @@ typedef struct image_task { wuss_window_t *window; + wuss_t *wuss; /* for wuss_get_pointer when opening the menu */ + wuss_task_t *delegate; /* the wuss task backing this window */ bitmap_t bitmap; /* owned: base freed by the caller when done */ bitmap_t ninepatch; /* owned: 9-patch tiled behind the main image */ const char *resources; /* root passed to path_join_filename on click */ @@ -25,6 +28,9 @@ typedef struct image_task * leafnames (extension stripped) found under * resources/images at spawn time */ int nnames; + wuss_menu_t *menu; /* owned: MENU-button demo tree, built from a + * descriptor string; rebuilt on each open, + * freed at QUIT */ } image_task_t; From e2de08063b47c71c7a26f1bd1ca331775e0cdafb Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 6 Sep 2026 19:23:32 +0100 Subject: [PATCH 03/98] style: list result_t declaration first in every function Move the result_t rc/err/crc local to the top of its declaration block, ahead of the other locals, keeping column alignment. Affects 45 functions across wuss core, menu/icon/component/helpers, the wuss test tasks, bmfont and the cache test. Co-Authored-By: Claude Sonnet 5 --- apps/wuss/tasks.c | 2 +- libraries/datastruct/cache/test/cache-test.c | 2 +- libraries/framebuf/bmfont/bmfont.c | 2 +- libraries/framebuf/bmfont/test/bmfont-test.c | 2 +- libraries/wuss/component/fontmenu.c | 2 +- libraries/wuss/core/idle.c | 4 ++-- libraries/wuss/core/redraw.c | 4 ++-- libraries/wuss/core/set-backdrop.c | 2 +- libraries/wuss/core/set-palette.c | 4 ++-- libraries/wuss/core/window/create-placed.c | 2 +- libraries/wuss/core/window/set-hidden.c | 2 +- libraries/wuss/core/window/try-close.c | 2 +- libraries/wuss/helpers/create-from-desc.c | 4 ++-- libraries/wuss/icon/create.c | 2 +- libraries/wuss/icon/plot.c | 2 +- libraries/wuss/menu/menu.c | 4 ++-- libraries/wuss/test/tasks/ball.c | 2 +- libraries/wuss/test/tasks/blank.c | 4 ++-- libraries/wuss/test/tasks/chars.c | 6 +++--- libraries/wuss/test/tasks/checker.c | 2 +- libraries/wuss/test/tasks/clock.c | 2 +- libraries/wuss/test/tasks/curve.c | 2 +- libraries/wuss/test/tasks/gradient.c | 2 +- libraries/wuss/test/tasks/greeble.c | 2 +- libraries/wuss/test/tasks/image.c | 8 ++++---- libraries/wuss/test/tasks/lissajous.c | 2 +- libraries/wuss/test/tasks/minesweeper.c | 2 +- libraries/wuss/test/tasks/palette.c | 4 ++-- libraries/wuss/test/tasks/porter-duff.c | 6 +++--- libraries/wuss/test/tasks/sofa.c | 2 +- libraries/wuss/test/tasks/swatches.c | 2 +- libraries/wuss/test/tasks/text.c | 4 ++-- 32 files changed, 47 insertions(+), 47 deletions(-) diff --git a/apps/wuss/tasks.c b/apps/wuss/tasks.c index bcab8c3..2d97388 100644 --- a/apps/wuss/tasks.c +++ b/apps/wuss/tasks.c @@ -103,12 +103,12 @@ static result_t spawn_palette(void) static result_t spawn_image(void) { + result_t rc; image_task_t *t; const char *leafname; const char *filename; char buf[DPTLIB_MAXPATH]; char ninepatch[DPTLIB_MAXPATH]; - result_t rc; t = calloc(1, sizeof(*t)); if (t == NULL) return result_OOM; diff --git a/libraries/datastruct/cache/test/cache-test.c b/libraries/datastruct/cache/test/cache-test.c index e4fbaaa..3caad0b 100644 --- a/libraries/datastruct/cache/test/cache-test.c +++ b/libraries/datastruct/cache/test/cache-test.c @@ -63,9 +63,9 @@ static result_t cache_test_outer(const cacheconfig_t *config, size_t cachelength, int maxkey) { + result_t err; unsigned char *block; cache_t *cache; - result_t err; block = malloc(cachelength); if (block == NULL) diff --git a/libraries/framebuf/bmfont/bmfont.c b/libraries/framebuf/bmfont/bmfont.c index 8157235..c7e77b6 100644 --- a/libraries/framebuf/bmfont/bmfont.c +++ b/libraries/framebuf/bmfont/bmfont.c @@ -1360,9 +1360,9 @@ result_t bmfont_draw_relief(bmfont_t *bmfont, const point_t *offset, point_t *end_pos) { + result_t rc; colour_t transparent; point_t shadowpos; - result_t rc; transparent = colour_rgba(0, 0, 0, 0); shadowpos = POINT(pos->x + offset->x, pos->y + offset->y); diff --git a/libraries/framebuf/bmfont/test/bmfont-test.c b/libraries/framebuf/bmfont/test/bmfont-test.c index 7633d6b..bd6e2ad 100644 --- a/libraries/framebuf/bmfont/test/bmfont-test.c +++ b/libraries/framebuf/bmfont/test/bmfont-test.c @@ -767,9 +767,9 @@ static result_t bmfont_enum_cb(const char *name, static result_t bmfont_enumerate_test(const char *resources) { + result_t rc; const char *dir; bmfont_enum_check_t chk; - result_t rc; int i; dir = path_join_filename(resources, 2, "resources", "bmfonts"); diff --git a/libraries/wuss/component/fontmenu.c b/libraries/wuss/component/fontmenu.c index 8bf3f14..fac6cf2 100644 --- a/libraries/wuss/component/fontmenu.c +++ b/libraries/wuss/component/fontmenu.c @@ -200,9 +200,9 @@ result_t wuss_fontmenu_create(wuss_fontmenu_t **out, const wuss_t *wuss, const wuss_alloc_t *alloc) { + result_t rc; namelist_t nl; wuss_fontmenu_t *fm; - result_t rc; if (out == NULL || dir == NULL) return result_NULL_ARG; diff --git a/libraries/wuss/core/idle.c b/libraries/wuss/core/idle.c index a27b1ef..d7f12b0 100644 --- a/libraries/wuss/core/idle.c +++ b/libraries/wuss/core/idle.c @@ -4,8 +4,8 @@ result_t wuss_idle(wuss_t *wuss) { - wuss_event_t event; result_t rc; + wuss_event_t event; list_t *e; event.kind = wuss_EVENT_IDLE; @@ -13,9 +13,9 @@ result_t wuss_idle(wuss_t *wuss) rc = result_OK; for (e = wuss->tasks.next; e != NULL; ) { + result_t crc; wuss_task_t *task; list_t *next; - result_t crc; task = (wuss_task_t *) e; next = e->next; /* an autoclose task may free its own node in the handler */ diff --git a/libraries/wuss/core/redraw.c b/libraries/wuss/core/redraw.c index fe6d31a..ff02032 100644 --- a/libraries/wuss/core/redraw.c +++ b/libraries/wuss/core/redraw.c @@ -91,8 +91,8 @@ static void redraw_window(wuss_t *wuss, content.y0 - win->scroll.y); { - wuss_event_t event; result_t crc; + wuss_event_t event; event.kind = wuss_EVENT_REDRAW; event.data.redraw.scr = wuss->scr; @@ -147,8 +147,8 @@ static void redraw_from(wuss_t *wuss, result_t wuss_redraw(wuss_t *wuss) { - box_t full; result_t rc; + box_t full; full.x0 = 0; full.y0 = 0; diff --git a/libraries/wuss/core/set-backdrop.c b/libraries/wuss/core/set-backdrop.c index d098467..48a9f14 100644 --- a/libraries/wuss/core/set-backdrop.c +++ b/libraries/wuss/core/set-backdrop.c @@ -8,8 +8,8 @@ result_t wuss_set_backdrop(wuss_t *wuss, const wuss_backdrop_t *backdrop) { - box_t screen; result_t rc; + box_t screen; wuss_backdrop_t resolved; assert(wuss != NULL); diff --git a/libraries/wuss/core/set-palette.c b/libraries/wuss/core/set-palette.c index e459990..3bda667 100644 --- a/libraries/wuss/core/set-palette.c +++ b/libraries/wuss/core/set-palette.c @@ -11,9 +11,9 @@ result_t wuss_set_palette(wuss_t *wuss, const colour_t *palette, int npalette) { + result_t rc; wuss_event_t event; box_t screen; - result_t rc; list_t *e; assert(wuss != NULL); @@ -33,9 +33,9 @@ result_t wuss_set_palette(wuss_t *wuss, event.kind = wuss_EVENT_PALETTE; for (e = wuss->tasks.next; e != NULL; ) { + result_t crc; wuss_task_t *task; list_t *next; - result_t crc; task = (wuss_task_t *) e; next = e->next; /* an autoclose task may free its own node in the handler */ diff --git a/libraries/wuss/core/window/create-placed.c b/libraries/wuss/core/window/create-placed.c index f348417..7c1b739 100644 --- a/libraries/wuss/core/window/create-placed.c +++ b/libraries/wuss/core/window/create-placed.c @@ -83,8 +83,8 @@ result_t wuss_window_create_placed(wuss_task_t *task, size2d_t min_doc, wuss_window_t **window) { - wuss_t *wuss; result_t rc; + wuss_t *wuss; int left, top, right, bottom; int fw, fh; box_t screen, content, consumed; diff --git a/libraries/wuss/core/window/set-hidden.c b/libraries/wuss/core/window/set-hidden.c index 373c247..f930043 100644 --- a/libraries/wuss/core/window/set-hidden.c +++ b/libraries/wuss/core/window/set-hidden.c @@ -8,8 +8,8 @@ result_t wuss_window_set_hidden(wuss_window_t *window, int hidden) { - wuss_event_t event; result_t rc; + wuss_event_t event; int was_hidden; was_hidden = (window->flags & wuss_WINDOW_HIDDEN) != 0; diff --git a/libraries/wuss/core/window/try-close.c b/libraries/wuss/core/window/try-close.c index d8a14a4..882c978 100644 --- a/libraries/wuss/core/window/try-close.c +++ b/libraries/wuss/core/window/try-close.c @@ -8,8 +8,8 @@ result_t wuss_window_try_close(wuss_window_t *window) { - wuss_event_t event; result_t rc; + wuss_event_t event; if (window == NULL) return result_OK; diff --git a/libraries/wuss/helpers/create-from-desc.c b/libraries/wuss/helpers/create-from-desc.c index 85ac41d..baf3dc3 100644 --- a/libraries/wuss/helpers/create-from-desc.c +++ b/libraries/wuss/helpers/create-from-desc.c @@ -135,10 +135,10 @@ void wuss_menu_destroy(wuss_menu_t *menu) * freed with, the result of wuss_menu_create_from_desc. */ static result_t menu_deep_copy(const wuss_menu_t *src, wuss_menu_t **out) { + result_t rc; wuss_menu_t *m; wuss_menu_item_t *items; int i; - result_t rc; m = malloc(sizeof(*m)); if (m == NULL) @@ -302,13 +302,13 @@ static void build_discard(Building *b) result_t wuss_menu_create_from_desc(wuss_menu_t **out, const char *desc, ...) { + result_t rc; Parser parser; Building stack[WUSS_MENU_DESC_DEPTH]; int need_title[WUSS_MENU_DESC_DEPTH]; char *titles[WUSS_MENU_DESC_DEPTH]; int sp; va_list ap; - result_t rc; char text[WUSS_MENU_DESC_TEXT]; int i; diff --git a/libraries/wuss/icon/create.c b/libraries/wuss/icon/create.c index aeac0f5..be9ef71 100644 --- a/libraries/wuss/icon/create.c +++ b/libraries/wuss/icon/create.c @@ -14,6 +14,7 @@ result_t wuss_icon_create(wuss_window_t *window, const wuss_icon_spec_t *spec, wuss_icon_t **icon) { + result_t rc; wuss_t *w; wuss_icon_t *it; wuss_icon_t **grown; @@ -21,7 +22,6 @@ result_t wuss_icon_create(wuss_window_t *window, const char *src; size_t len; int newcap; - result_t rc; assert(window != NULL); assert(spec != NULL); diff --git a/libraries/wuss/icon/plot.c b/libraries/wuss/icon/plot.c index a305e96..caa1c4b 100644 --- a/libraries/wuss/icon/plot.c +++ b/libraries/wuss/icon/plot.c @@ -13,8 +13,8 @@ result_t wuss_icon_plot(wuss_window_t *window, const box_t *content, point_t scroll) { - wuss_icon_t scratch; result_t rc; + wuss_icon_t scratch; assert(window != NULL); assert(spec != NULL); diff --git a/libraries/wuss/menu/menu.c b/libraries/wuss/menu/menu.c index 2f7099f..ac02081 100644 --- a/libraries/wuss/menu/menu.c +++ b/libraries/wuss/menu/menu.c @@ -492,11 +492,11 @@ static result_t wuss__menu_spawn(wuss_t *wuss, struct wuss__menu *parent, struct wuss__menu **out) { + result_t rc; struct wuss__menu *node; wuss_task_t *menu_task; wuss_icon_spec_t *specs; wuss_icon_t **made; - result_t rc; int fh; int pitch; int sep_h; @@ -779,9 +779,9 @@ result_t wuss_menu_open(wuss_task_t *task, point_t at, wuss_menu_handle_t *out) { + result_t rc; struct wuss__menu *root; wuss_t *wuss; - result_t rc; assert(task != NULL); assert(menu != NULL); diff --git a/libraries/wuss/test/tasks/ball.c b/libraries/wuss/test/tasks/ball.c index ce73ddd..261ef83 100644 --- a/libraries/wuss/test/tasks/ball.c +++ b/libraries/wuss/test/tasks/ball.c @@ -53,9 +53,9 @@ static box_t ball_local_box(int vx0, int vy0, int vx1, int vy1, int radius) result_t ball_create(wuss_t *wuss, ball_task_t *task) { + result_t rc; wuss_task_t *delegate; wuss_task_desc_t delegate_desc; - result_t rc; task->bg = colour_rgb(0xFF, 0x00, 0x00); task->nballs = 1; diff --git a/libraries/wuss/test/tasks/blank.c b/libraries/wuss/test/tasks/blank.c index 9986416..f1ef9b6 100644 --- a/libraries/wuss/test/tasks/blank.c +++ b/libraries/wuss/test/tasks/blank.c @@ -19,9 +19,9 @@ result_t blank_create(wuss_t *wuss, blank_task_t *task) { + result_t rc; wuss_task_t *delegate; wuss_task_desc_t delegate_desc; - result_t rc; task->npalette = 16; // TODO: Read max palette index from wuss task->index = 0; @@ -55,8 +55,8 @@ result_t blank_create(wuss_t *wuss, blank_task_t *task) static result_t blank_idle(void *task_data) { - blank_task_t *bc; result_t rc; + blank_task_t *bc; bc = task_data; diff --git a/libraries/wuss/test/tasks/chars.c b/libraries/wuss/test/tasks/chars.c index bc8e2ca..80830c6 100644 --- a/libraries/wuss/test/tasks/chars.c +++ b/libraries/wuss/test/tasks/chars.c @@ -51,10 +51,10 @@ static bmfont_t *chars_load_font(chars_task_t *task, int idx, const char *name) { + result_t rc; const char *leaf; const char *filename; bmfont_t *font; - result_t rc; if (task->fonts[idx] != NULL) return task->fonts[idx]; @@ -107,9 +107,9 @@ result_t chars_create(wuss_t *wuss, const char *resources, chars_task_t *task) { + result_t rc; wuss_task_t *delegate; wuss_task_desc_t delegate_desc; - result_t rc; bmfont_t *font; const char *bmfonts_dir; const wuss_menu_t *menu; @@ -280,8 +280,8 @@ result_t chars_handle(wuss_window_t *window, case wuss_EVENT_MENU_SELECT: { - const char *name; result_t rc; + const char *name; name = wuss_fontmenu_selected(cc->fontmenu, event); if (name == NULL) diff --git a/libraries/wuss/test/tasks/checker.c b/libraries/wuss/test/tasks/checker.c index 2d284fd..32656ee 100644 --- a/libraries/wuss/test/tasks/checker.c +++ b/libraries/wuss/test/tasks/checker.c @@ -20,9 +20,9 @@ result_t checker_create(wuss_t*wuss, checker_task_t *task) { + result_t rc; wuss_task_t *delegate; wuss_task_desc_t delegate_desc; - result_t rc; task->black = colour_rgb(0x00, 0x00, 0x00); task->white = colour_rgb(0xFF, 0xFF, 0xFF); diff --git a/libraries/wuss/test/tasks/clock.c b/libraries/wuss/test/tasks/clock.c index bf8f475..2bdbfb7 100644 --- a/libraries/wuss/test/tasks/clock.c +++ b/libraries/wuss/test/tasks/clock.c @@ -102,9 +102,9 @@ static result_t clock_create_window(wuss_t *wuss, result_t clock_create(wuss_t *wuss, bmfont_t *font, clock_task_t *task) { + result_t rc; wuss_task_t *delegate; wuss_task_desc_t delegate_desc; - result_t rc; task->font = font; task->bg = colour_rgb(0x1D, 0x2B, 0x53); diff --git a/libraries/wuss/test/tasks/curve.c b/libraries/wuss/test/tasks/curve.c index 38e7b34..2c5def7 100644 --- a/libraries/wuss/test/tasks/curve.c +++ b/libraries/wuss/test/tasks/curve.c @@ -25,9 +25,9 @@ result_t curve_create(wuss_t *wuss, curve_task_t *task) { + result_t rc; wuss_task_t *delegate; wuss_task_desc_t delegate_desc; - result_t rc; task->bg = colour_rgb(0xFF, 0xFF, 0xFF); task->line = colour_rgb(0x00, 0x00, 0x00); diff --git a/libraries/wuss/test/tasks/gradient.c b/libraries/wuss/test/tasks/gradient.c index 4dd1e52..160be39 100644 --- a/libraries/wuss/test/tasks/gradient.c +++ b/libraries/wuss/test/tasks/gradient.c @@ -35,9 +35,9 @@ static int dither(int v, int x, int y) result_t gradient_create(wuss_t *wuss, gradient_task_t *task) { + result_t rc; wuss_task_t *delegate; wuss_task_desc_t delegate_desc; - result_t rc; /* gradient_redraw paints every pixel itself */ delegate_desc.handle = gradient_handle; diff --git a/libraries/wuss/test/tasks/greeble.c b/libraries/wuss/test/tasks/greeble.c index d5e9f30..58236e7 100644 --- a/libraries/wuss/test/tasks/greeble.c +++ b/libraries/wuss/test/tasks/greeble.c @@ -387,11 +387,11 @@ result_t greeble_handle(wuss_window_t *window, result_t greeble_create(wuss_t *wuss, greeble_task_t *task) { + result_t rc; wuss_task_t *delegate; wuss_task_desc_t delegate_desc; size2d_t grid_px; box_t content; - result_t rc; /* the full generator grid in pixels: content sized so cols/rows land * exactly on the GREEBLE_MAX_* caps in 8-pixel tiles */ diff --git a/libraries/wuss/test/tasks/image.c b/libraries/wuss/test/tasks/image.c index 193a8ab..aceffe5 100644 --- a/libraries/wuss/test/tasks/image.c +++ b/libraries/wuss/test/tasks/image.c @@ -54,11 +54,11 @@ result_t image_create(wuss_t *wuss, const char *background_path, image_task_t *task) { + result_t rc; wuss_task_t *delegate; wuss_task_desc_t delegate_desc; const char *images_dir; - result_t rc; - size2d_t sz; + size2d_t sz; task->wuss = wuss; task->delegate = NULL; @@ -156,12 +156,12 @@ static result_t image_click(wuss_window_t *window, image_task_t *ic, int step) { + result_t rc; const char *leafname; const char *filename; char buf[DPTLIB_MAXPATH]; bitmap_t next; size2d_t sz; - result_t rc; if (ic->nnames == 0) return result_OK; /* nothing to cycle to */ @@ -206,8 +206,8 @@ static const wuss_menu_t image_menu_export = static result_t image_open_menu(image_task_t *ic) { - wuss_menu_t *m; result_t rc; + wuss_menu_t *m; rc = wuss_menu_create_from_desc(&m, "Display, Open, !Show grid, !Wireframe, >Export, |Quit", diff --git a/libraries/wuss/test/tasks/lissajous.c b/libraries/wuss/test/tasks/lissajous.c index ee5ab11..9ada0dc 100644 --- a/libraries/wuss/test/tasks/lissajous.c +++ b/libraries/wuss/test/tasks/lissajous.c @@ -23,9 +23,9 @@ static const int lissajous_freqs[][2] = result_t lissajous_create(wuss_t *wuss, lissajous_task_t *task) { + result_t rc; wuss_task_t *delegate; wuss_task_desc_t delegate_desc; - result_t rc; task->bg = colour_rgb(0x00, 0x00, 0x00); task->fg = colour_rgb(0x00, 0xFF, 0x00); diff --git a/libraries/wuss/test/tasks/minesweeper.c b/libraries/wuss/test/tasks/minesweeper.c index 87f66b5..5182e3d 100644 --- a/libraries/wuss/test/tasks/minesweeper.c +++ b/libraries/wuss/test/tasks/minesweeper.c @@ -180,9 +180,9 @@ result_t minesweeper_create(wuss_t *wuss, bmfont_t *font, minesweeper_task_t *task) { + result_t rc; wuss_task_t *delegate; wuss_task_desc_t delegate_desc; - result_t rc; task->wuss = wuss; task->font = font; diff --git a/libraries/wuss/test/tasks/palette.c b/libraries/wuss/test/tasks/palette.c index e90b196..d975465 100644 --- a/libraries/wuss/test/tasks/palette.c +++ b/libraries/wuss/test/tasks/palette.c @@ -113,11 +113,11 @@ result_t palette_create(wuss_t *wuss, const char *startup_name, palette_task_t *task) { + result_t rc; wuss_task_desc_t delegate_desc; const char *dir; char dirbuf[DPTLIB_MAXPATH]; int i; - result_t rc; task->wuss = wuss; task->resources = resources; @@ -267,10 +267,10 @@ static result_t palette_click(palette_task_t *pc, const wuss_event_t *event) static result_t palette_menu_select(palette_task_t *pc, const wuss_event_t *event) { + result_t rc; int index; int old; colour_t loaded[PALETTE_NCOLOURS]; - result_t rc; int i; if (event->data.menu_select.menu != &pc->menu) diff --git a/libraries/wuss/test/tasks/porter-duff.c b/libraries/wuss/test/tasks/porter-duff.c index 6d97058..a91636c 100644 --- a/libraries/wuss/test/tasks/porter-duff.c +++ b/libraries/wuss/test/tasks/porter-duff.c @@ -133,9 +133,9 @@ static result_t load_demo_png(bitmap_t *bm, const char *resources, const char *leafname) { + result_t rc; const char *leafname_ext; const char *filename; - result_t rc; leafname_ext = path_join_leafname(leafname, "png"); filename = path_join_filename(resources, 3, @@ -163,9 +163,9 @@ result_t porter_duff_create(wuss_t *wuss, const char *resources, porter_duff_task_t *task) { + result_t rc; wuss_task_t *delegate; wuss_task_desc_t delegate_desc; - result_t rc; task->font = font; task->rule = composite_RULE_CLEAR; @@ -293,12 +293,12 @@ static void porter_duff_draw_checkerboard(const porter_duff_task_t *pd, static result_t porter_duff_redraw(const wuss_event_t *event, void *task_data) { + result_t rc; porter_duff_task_t *pd; screen_t *scr; const box_t *content, *bounds; const char *name; point_t pos; - result_t rc; pd = task_data; diff --git a/libraries/wuss/test/tasks/sofa.c b/libraries/wuss/test/tasks/sofa.c index b827ca2..c6bd0ba 100644 --- a/libraries/wuss/test/tasks/sofa.c +++ b/libraries/wuss/test/tasks/sofa.c @@ -356,9 +356,9 @@ static void draw_vertex_dots(screen_t *scr, result_t sofa_create(wuss_t*wuss, sofa_task_t*task) { + result_t rc; wuss_task_t *delegate; wuss_task_desc_t delegate_desc; - result_t rc; task->bg = colour_rgb(0x7E, 0x25, 0x53); task->line = colour_rgb(0xFF, 0xA3, 0x00); diff --git a/libraries/wuss/test/tasks/swatches.c b/libraries/wuss/test/tasks/swatches.c index cefd1b1..e18cb67 100644 --- a/libraries/wuss/test/tasks/swatches.c +++ b/libraries/wuss/test/tasks/swatches.c @@ -72,9 +72,9 @@ static result_t swatches_redraw(swatches_task_t *task, result_t swatches_create(wuss_t *wuss, swatches_task_t *task) { + result_t rc; wuss_task_t *delegate; wuss_task_desc_t delegate_desc; - result_t rc; task->wuss = wuss; task->window = NULL; diff --git a/libraries/wuss/test/tasks/text.c b/libraries/wuss/test/tasks/text.c index 695c2ba..52f3fe5 100644 --- a/libraries/wuss/test/tasks/text.c +++ b/libraries/wuss/test/tasks/text.c @@ -78,10 +78,10 @@ static bmfont_t *text_load_font(text_task_t *task, int idx, const char *name) { + result_t rc; const char *leaf; const char *filename; bmfont_t *font; - result_t rc; if (task->fonts[idx] != NULL) return task->fonts[idx]; @@ -142,12 +142,12 @@ result_t text_create(wuss_t *wuss, const char *resources, text_task_t *task) { + result_t rc; wuss_task_t *delegate; wuss_task_desc_t delegate_desc; const char *bmfonts_dir; const wuss_menu_t *menu; size2d_t sz; - result_t rc; task->wuss = wuss; task->font = wuss_get_font(wuss); From 8aa171f907368238a377c66f5eb9ff4dd9515ad2 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 6 Sep 2026 19:24:17 +0100 Subject: [PATCH 04/98] docs: note the result_t-declared-first convention in CLAUDE.md Co-Authored-By: Claude Sonnet 5 --- CLAUDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 7a781ae..5a7ef3e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -89,7 +89,7 @@ New source/header files must be added by hand to the relevant `set(..._SOURCES . ## Code style - C99, `-Wall -Wextra -pedantic`. Allman brace style, 2-space indentation, tabs converted to spaces (see `.astylerc`/`.editorconfig` — astyle is the formatter of record). -- Declare variables at the top of each scope (pre-C99 style is followed throughout, even though the standard is C99), ordered by first use. +- Declare variables at the top of each scope (pre-C99 style is followed throughout, even though the standard is C99), ordered by first use. Exception: a `result_t` status local (`rc`, `err`, `crc`, ...) is always declared first in its block, ahead of the use-ordered locals. - File header comment format: `/* filename.c -- one-line description */`. - Section breaks within files use `/* ----- ... ----- */` rule comments. - Public API docs use Doxygen (`\file`, `\param`, `\return`); a `Doxyfile` exists for generating them. From 2b4fd05dec0592644ba7c3bdd3033a6804bee39d Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 6 Sep 2026 22:51:52 +0100 Subject: [PATCH 05/98] fix(wuss): restore image demo window to full height The 2/3-height size was a temporary scroll-testing tweak; show the whole image. --- libraries/wuss/test/tasks/image.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/libraries/wuss/test/tasks/image.c b/libraries/wuss/test/tasks/image.c index aceffe5..1361d12 100644 --- a/libraries/wuss/test/tasks/image.c +++ b/libraries/wuss/test/tasks/image.c @@ -109,8 +109,7 @@ result_t image_create(wuss_t *wuss, sz.h = task->bitmap.size.h + NINEPATCHSZ * 2; rc = wuss_window_create_placed(delegate, - /* shorter than the bitmap so there's something to scroll through */ - SIZE2D(sz.w, sz.h * 2 / 3), + SIZE2D(sz.w, sz.h), "Image", wuss_WINDOW_NONE, wuss_BACKDROP_COLOUR(palette_PICO8_PINK), From cd3f1207724bbca83dda14957d2d569c334defaa Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 6 Sep 2026 22:52:14 +0100 Subject: [PATCH 06/98] feat(wuss): add prog_info standard dialogue component wuss_proginfo is a shared WUSS_COMPONENTS helper modelled on the RISC OS Toolbox ProgInfo / "Info" dialogue: a small fixed window of Name / Purpose / Author / Version rows a task fills in once from a wuss_proginfo_desc_t, then hangs off its Menu-button pop-up as a wuss_menu_item_t.window. The dialogue window is created hidden, wuss_WINDOW_NO_CLOSE, on a task the caller passes in; it lays out from the task's font metrics with one right-justified label and one value per non-NULL field. wuss_proginfo_destroy closes the window and frees the handle. The owning task must not be an autoclose task (the hidden window would keep its window list non-empty) and must outlive the handle; as with any borrowed menu window, close any menu chain referencing it first. The image test task grows an "Info" row wired to a wuss_proginfo, and drops autoclose accordingly (QUIT now arrives via wuss_destroy). Co-Authored-By: Claude Sonnet 5 --- CMakeLists.txt | 4 +- include/wuss/component/proginfo.h | 110 ++++++++++++++ libraries/wuss/component/proginfo.c | 220 ++++++++++++++++++++++++++++ libraries/wuss/test/tasks/image.c | 53 ++++++- libraries/wuss/test/tasks/image.h | 6 + 5 files changed, 387 insertions(+), 6 deletions(-) create mode 100644 include/wuss/component/proginfo.h create mode 100644 libraries/wuss/component/proginfo.c diff --git a/CMakeLists.txt b/CMakeLists.txt index be63626..d9dca41 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -133,6 +133,7 @@ endif() if(WUSS_COMPONENTS) list(APPEND PUBLIC_HEADERS include/wuss/component/fontmenu.h) list(APPEND PUBLIC_HEADERS include/wuss/component/colourmenu.h) + list(APPEND PUBLIC_HEADERS include/wuss/component/proginfo.h) endif() # The public headers must be set as properties of the library, not as @@ -435,7 +436,8 @@ set(WUSS_MENU_SOURCES set(WUSS_COMPONENT_SOURCES libraries/wuss/component/fontmenu.c - libraries/wuss/component/colourmenu.c) + libraries/wuss/component/colourmenu.c + libraries/wuss/component/proginfo.c) set(WUSS_SOURCES ${WUSS_CORE_SOURCES}) if(WUSS_FURNITURE) diff --git a/include/wuss/component/proginfo.h b/include/wuss/component/proginfo.h new file mode 100644 index 0000000..2ba36b4 --- /dev/null +++ b/include/wuss/component/proginfo.h @@ -0,0 +1,110 @@ +/* wuss/component/proginfo.h -- a "program information" standard dialogue */ + +/** + * \file proginfo.h + * + * A shared wuss component: the RISC OS "Info" / Toolbox ProgInfo dialogue in + * miniature -- a small fixed window of Name / Purpose / Author / Version + * rows a task fills in once, then hangs off its Menu-button pop-up. + * + * A wuss_proginfo owns one window (titlebar caption only -- no close, back, + * toggle, scrollbars or resize), created hidden on a task the caller passes + * in and laid out from a wuss_proginfo_desc_t: one row per non-NULL field, + * the field name right-justified in a left column and its value + * left-justified beside it. + * + * It is meant to be used as a wuss_menu_item_t::window -- point an "Info" + * menu row at wuss_proginfo_window() and wuss shows it where a submenu would + * open. A task can equally wuss_window_set_hidden() it directly. + * + * The window is created wuss_WINDOW_NO_CLOSE and stays on the caller's task + * until wuss_proginfo_destroy closes it. That task must therefore not be an + * autoclose task -- its window list would never empty -- and must outlive + * the handle. As for any borrowed wuss_menu_item_t::window, the dialogue + * must also outlive every menu chain that references it: close the chain + * (wuss_menu_close) before wuss_proginfo_destroy. + * + * Built only when WUSS_COMPONENTS is defined (which implies WUSS_MENUS). + */ + +#ifndef WUSS_COMPONENT_PROGINFO_H +#define WUSS_COMPONENT_PROGINFO_H + +#ifdef __cplusplus +extern "C" +{ +#endif + +#include "base/result.h" + +#include "wuss/task.h" +#include "wuss/window.h" + +/* ----------------------------------------------------------------------- */ + +/** + * The rows of a program-information dialogue. Every field is borrowed and + * copied; a NULL field is left out entirely, so a zero-initialised desc with + * only \c name set yields a one-row dialogue. + */ +typedef struct wuss_proginfo_desc +{ + const char *name; /**< Program name; row "Name". */ + const char *purpose; /**< One-line description; row "Purpose". */ + const char *author; /**< Author / copyright line; row "Author". */ + const char *version; /**< Version string, e.g. "1.23 (14 Feb 2026)"; row + "Version". */ +} +wuss_proginfo_desc_t; + +/** Opaque handle: owns the dialogue window and the label strings in it. */ +typedef struct wuss_proginfo wuss_proginfo_t; + +/* ----------------------------------------------------------------------- */ + +/** + * Build a program-information dialogue: a hidden window on \p task, laid out + * from \p desc with one label row per non-NULL field. + * + * The window's on-screen size is derived from \p task's font metrics and the + * text in \p desc; it is created with wuss_WINDOW_HIDDEN, so nothing shows + * until the caller reveals it (directly, or via a wuss_menu_item_t::window + * that names wuss_proginfo_window()). + * + * \param[out] out Filled with the new handle on success, untouched on + * failure. + * \param[in] task Task the dialogue window is created on. Must not be an + * autoclose task and must outlive the handle. + * \param[in] desc Field values; each borrowed and copied. At least \c name + * must be non-NULL. + * \return \ref result_OK, \ref result_OOM, \ref result_NULL_ARG if \p out, + * \p task, \p desc or \c desc->name is NULL, or a wuss_window_create + * / wuss_icon_create code. + */ +result_t wuss_proginfo_create(wuss_proginfo_t **out, + wuss_task_t *task, + const wuss_proginfo_desc_t *desc); + +/** + * Free a program-information dialogue: closes its window and frees every + * string in it. Safe to pass NULL. + * + * \param[in] doomed Handle to free, or NULL. + */ +void wuss_proginfo_destroy(wuss_proginfo_t *doomed); + +/** + * The dialogue's window, for use as a wuss_menu_item_t::window or to show + * and hide directly. Borrowed; valid until wuss_proginfo_destroy. NULL only + * if \p pi is NULL. + * + * \param[in] pi Handle. + * \return The window, or NULL. + */ +wuss_window_t *wuss_proginfo_window(const wuss_proginfo_t *pi); + +#ifdef __cplusplus +} +#endif + +#endif /* WUSS_COMPONENT_PROGINFO_H */ diff --git a/libraries/wuss/component/proginfo.c b/libraries/wuss/component/proginfo.c new file mode 100644 index 0000000..28373ea --- /dev/null +++ b/libraries/wuss/component/proginfo.c @@ -0,0 +1,220 @@ +/* wuss/component/proginfo.c -- a "program information" standard dialogue */ + +#include +#include +#include + +#ifdef FORTIFY +#include "fortify/fortify.h" +#endif + +#include "base/result.h" +#include "framebuf/bmfont.h" +#include "geom/box.h" +#include "geom/size.h" + +#include "wuss/icon.h" +#include "wuss/task.h" +#include "wuss/window.h" + +#include "wuss/component/proginfo.h" + +#include "../core/impl.h" + +/* ----------------------------------------------------------------------- */ + +/* ponytail: flat list of LABEL icons, no bevelled field borders, no "Web + * site" action button -- the four text rows are the whole dialogue. Add a + * button row through here if a task ever needs one. */ + +#define PROGINFO_MARGIN 12 /* px border around the row block */ +#define PROGINFO_GAP 12 /* px between the name column and the value column */ +#define PROGINFO_ROW_PAD 6 /* px added to the font height for the row pitch */ + +/* The rows, in display order. Only fields set in the desc get a row. */ +static const char *const proginfo_labels[] = +{ + "Name", "Purpose", "Author", "Version" +}; + +/* The dialogue is one window on a task the caller passes in. It carries + * wuss_WINDOW_NO_CLOSE so a menu chain that borrows it as a + * wuss_menu_item_t::window can't have it pulled from under it, and + * wuss_proginfo_destroy closes it explicitly. The task must therefore not be + * an autoclose one -- its window list would never empty -- and must outlive + * the handle. */ +struct wuss_proginfo +{ + wuss_alloc_t alloc; /* copied hooks; the wuss_t itself is not retained */ + wuss_window_t *window; /* owned; closed by wuss_proginfo_destroy */ +}; + +/* ----------------------------------------------------------------------- */ + +result_t wuss_proginfo_create(wuss_proginfo_t **out, + wuss_task_t *task, + const wuss_proginfo_desc_t *desc) +{ + result_t rc; + const wuss_alloc_t *a; + wuss_t *wuss; + bmfont_t *font; + wuss_proginfo_t *pi; + const char *values[4]; + int nrows; + int fonth; + int rowh; + int namew; + int valuew; + int w; + int h; + box_t content; + wuss_icon_spec_t specs[8]; + int nspecs; + int i; + + if (out == NULL || task == NULL || desc == NULL || desc->name == NULL) + return result_NULL_ARG; + + wuss = task->wuss; + a = &wuss->alloc; + font = wuss_get_font(wuss); + + values[0] = desc->name; + values[1] = desc->purpose; + values[2] = desc->author; + values[3] = desc->version; + + /* Column widths: the widest name label on the left, the widest value on the + * right. bmfont_measure with no target width returns the whole string's + * width in actual_width. A NULL font leaves both at 0; the icons still lay + * out, just with nothing measured to size them. */ + nrows = 0; + namew = 0; + valuew = 0; + for (i = 0; i < 4; i++) + { + bmfont_width_t lw; + bmfont_width_t vw; + + if (values[i] == NULL) + continue; + + lw = vw = 0; + if (font != NULL) + { + size_t vlen; + + vlen = strlen(values[i]); + bmfont_measure(font, proginfo_labels[i], (int) strlen(proginfo_labels[i]), + INT_MAX, NULL, &lw); + if (vlen > 0) + bmfont_measure(font, values[i], (int) vlen, INT_MAX, NULL, &vw); + } + if (lw > namew) + namew = lw; + if (vw > valuew) + valuew = vw; + nrows++; + } + + fonth = 0; + if (font != NULL) + bmfont_get_info(font, NULL, &fonth); + rowh = fonth + PROGINFO_ROW_PAD; + if (rowh < 12) + rowh = 12; + + if (namew < 1) + namew = 1; + if (valuew < 1) + valuew = 1; + + w = PROGINFO_MARGIN * 2 + namew + PROGINFO_GAP + valuew; + h = PROGINFO_MARGIN * 2 + rowh * nrows; + + pi = a->malloc(sizeof(*pi)); + if (pi == NULL) + return result_OOM; + pi->alloc = *a; + pi->window = NULL; + a = &pi->alloc; /* use the copy from here on -- outlives the wuss_t */ + + content = (box_t) BOX_POS_SIZE(0, 0, w, h); + rc = wuss_window_create(task, + &content, + "About this program", + wuss_WINDOW_NO_BACK | wuss_WINDOW_NO_CLOSE | + wuss_WINDOW_NO_TOGGLE_SIZE | wuss_WINDOW_NO_VSCROLL | + wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE | + wuss_WINDOW_HIDDEN, + wuss_BACKDROP_COLOUR(wuss_COLOUR_WHITE), + SIZE2D(w, h), + SIZE2D(w, h), + &pi->window); + if (rc != result_OK) + { + a->free(pi); + return rc; + } + + /* One right-justified name label and one left value label per row. Zero the + * whole array so the icon-spec fields this component does not set (pattern, + * bitmap, group, swatch) are their safe defaults. */ + memset(specs, 0, sizeof(specs)); + nspecs = 0; + for (i = 0; i < 4; i++) + { + int y; + + if (values[i] == NULL) + continue; + + y = PROGINFO_MARGIN + rowh * (nspecs / 2); + + specs[nspecs].bbox = (box_t) BOX_POS_SIZE(PROGINFO_MARGIN, y, namew, rowh); + specs[nspecs].type = wuss_ICON_TYPE_LABEL; + specs[nspecs].text = proginfo_labels[i]; + specs[nspecs].fg = wuss_COLOUR_BLACK; + specs[nspecs].bg = wuss_NO_BACKGROUND; + specs[nspecs].flags = wuss_ICON_FLAGS_JUSTIFY_RIGHT; + nspecs++; + + specs[nspecs].bbox = (box_t) BOX_POS_SIZE(PROGINFO_MARGIN + namew + + PROGINFO_GAP, y, valuew, rowh); + specs[nspecs].type = wuss_ICON_TYPE_LABEL; + specs[nspecs].text = values[i]; + specs[nspecs].fg = wuss_COLOUR_BLACK; + specs[nspecs].bg = wuss_NO_BACKGROUND; + specs[nspecs].flags = wuss_ICON_FLAGS_NONE; + nspecs++; + } + + rc = wuss_icon_create_array(pi->window, specs, nspecs, NULL); + if (rc != result_OK) + { + wuss_window_close(pi->window); /* frees the window and any icons on it */ + a->free(pi); + return rc; + } + + *out = pi; + return result_OK; +} + +void wuss_proginfo_destroy(wuss_proginfo_t *doomed) +{ + wuss_alloc_t alloc; + + if (doomed == NULL) + return; + + alloc = doomed->alloc; + wuss_window_close(doomed->window); /* frees the window and its icons */ + alloc.free(doomed); +} + +wuss_window_t *wuss_proginfo_window(const wuss_proginfo_t *pi) +{ + return pi ? pi->window : NULL; +} diff --git a/libraries/wuss/test/tasks/image.c b/libraries/wuss/test/tasks/image.c index 1361d12..5a3ddf8 100644 --- a/libraries/wuss/test/tasks/image.c +++ b/libraries/wuss/test/tasks/image.c @@ -65,7 +65,9 @@ result_t image_create(wuss_t *wuss, task->resources = resources; task->index = 0; task->nnames = 0; - task->menu = NULL; + task->menu = NULL; + task->proginfo = NULL; + task->menu_handle = NULL; images_dir = path_join_filename(resources, 2, "resources", "images"); rc = dirscan_walk(images_dir, image__scan_entry, task); @@ -103,7 +105,10 @@ result_t image_create(wuss_t *wuss, return rc; } task->delegate = delegate; - wuss_task_set_autoclose(delegate, 1); + /* No autoclose: the task also owns the hidden proginfo window below, so its + * window list never empties while the main window is up. QUIT is delivered + * at wuss_destroy instead. Closing the main window early leaks this block + * until then -- fine for a demo. */ sz.w = task->bitmap.size.w + NINEPATCHSZ * 2; sz.h = task->bitmap.size.h + NINEPATCHSZ * 2; @@ -117,9 +122,28 @@ result_t image_create(wuss_t *wuss, SIZE2D(32, 32), &task->window); if (rc != result_OK) + { wuss_task_destroy(delegate); /* QUIT frees the two bitmaps and the block */ + return rc; + } + + /* The "Info" menu row's standard dialogue. Hung off the descriptor menu as + * a wuss_menu_item_t.window in image_open_menu. A create failure is + * non-fatal -- the task just runs without an Info dialogue. */ + { + static const wuss_proginfo_desc_t desc = + { + "Image", + "Cycle the PNGs under resources/images", + "(c) DPTLib contributors", + "1.0 (" __DATE__ ")" + }; + + if (wuss_proginfo_create(&task->proginfo, delegate, &desc) != result_OK) + task->proginfo = NULL; + } - return rc; + return result_OK; } static result_t image_redraw(const wuss_event_t *event, void *task_data) @@ -207,18 +231,32 @@ static result_t image_open_menu(image_task_t *ic) { result_t rc; wuss_menu_t *m; + int i; rc = wuss_menu_create_from_desc(&m, - "Display, Open, !Show grid, !Wireframe, >Export, |Quit", + "Image, Info, New..., Open, !Show grid, !Wireframe, >Export, |Quit", &image_menu_export); if (rc != result_OK) return rc; + /* The descriptor syntax has no "open this window on hover" mark, so point + * the "Info" row at the proginfo dialogue by hand: wuss treats a + * wuss_menu_item_t.window exactly like a submenu, showing it where one would + * open. */ + if (ic->proginfo != NULL) + for (i = 0; i < m->nitems; i++) + if (m->items[i].text != NULL && strcmp(m->items[i].text, "Info") == 0) + { + ((wuss_menu_item_t *) m->items)[i].window = + wuss_proginfo_window(ic->proginfo); + break; + } + wuss_menu_destroy(ic->menu); ic->menu = m; return wuss_menu_open(ic->delegate, ic->menu, wuss_get_pointer(ic->wuss), - NULL); + &ic->menu_handle); } result_t image_handle(wuss_window_t *window, @@ -255,7 +293,12 @@ result_t image_handle(wuss_window_t *window, return result_OK; case wuss_EVENT_QUIT: + /* close any open chain first: it may hold the proginfo window as a + * borrowed wuss_menu_item_t.window, and destroying that below would leave + * the chain pointing at freed memory */ + wuss_menu_close(ic->menu_handle); wuss_menu_destroy(ic->menu); + wuss_proginfo_destroy(ic->proginfo); /* closes its dialogue window */ free(ic->bitmap.base); free(ic->ninepatch.base); free(ic); /* task_data was calloc'd per instance by the spawner */ diff --git a/libraries/wuss/test/tasks/image.h b/libraries/wuss/test/tasks/image.h index ec292b0..6c4f3ab 100644 --- a/libraries/wuss/test/tasks/image.h +++ b/libraries/wuss/test/tasks/image.h @@ -6,6 +6,7 @@ #ifdef WUSS_APP #include "framebuf/bitmap.h" +#include "wuss/component/proginfo.h" #include "wuss/menu.h" #include "wuss/window.h" @@ -31,6 +32,11 @@ typedef struct image_task wuss_menu_t *menu; /* owned: MENU-button demo tree, built from a * descriptor string; rebuilt on each open, * freed at QUIT */ + wuss_proginfo_t *proginfo; /* owned: the "Info" row's standard dialogue, + * hung off the menu as a wuss_menu_item_t.window; + * freed at QUIT */ + wuss_menu_handle_t menu_handle; /* the open chain, if any -- closed before the + * proginfo it borrows is destroyed at QUIT */ } image_task_t; From a5ab1d2e253c55c0e095a564f63c5a5f053bab11 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 6 Sep 2026 23:11:32 +0100 Subject: [PATCH 07/98] fix(wuss): don't paint the image into the proginfo dialogue The proginfo dialogue shares the image task, so image_handle receives REDRAW for it too and was blitting the bitmap behind its labels. Bail from the REDRAW case unless the event is for the task's own image window. --- libraries/wuss/test/tasks/image.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/libraries/wuss/test/tasks/image.c b/libraries/wuss/test/tasks/image.c index 5a3ddf8..53fcc13 100644 --- a/libraries/wuss/test/tasks/image.c +++ b/libraries/wuss/test/tasks/image.c @@ -272,6 +272,9 @@ result_t image_handle(wuss_window_t *window, switch (event->kind) { case wuss_EVENT_REDRAW: + /* the proginfo dialogue shares this task; only paint our own window */ + if (window != ic->window) + return result_OK; return image_redraw(event, task_data); case wuss_EVENT_MOUSE: From 9852e7f202f2d6f040a7cfc6181192dae73e0511 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 6 Sep 2026 23:14:44 +0100 Subject: [PATCH 08/98] feat(wuss): add wuss_WINDOW_NO_REDRAW flag A window with this flag gets no wuss_EVENT_REDRAW: wuss fills its backdrop and draws its icons, and for a window whose appearance is entirely backdrop and/or icons that is the lot. Saves such a task a no-op redraw handler. wuss_proginfo sets it -- the dialogue is labels on white -- which also removes the need for the image test task to filter REDRAW by window handle now that the dialogue shares its task. --- include/wuss/wuss.h | 11 ++++++++++- libraries/wuss/component/proginfo.c | 2 +- libraries/wuss/core/redraw.c | 3 +++ libraries/wuss/test/tasks/image.c | 3 --- 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/include/wuss/wuss.h b/include/wuss/wuss.h index 77bc9ef..ac9a1cb 100644 --- a/include/wuss/wuss.h +++ b/include/wuss/wuss.h @@ -239,7 +239,16 @@ typedef enum wuss_window_flags * at runtime, and it is honoured regardless of the WUSS_FURNITURE build * option. */ - wuss_WINDOW_HIDDEN = 1 << 9 + wuss_WINDOW_HIDDEN = 1 << 9, + + /** + * No wuss_EVENT_REDRAW is delivered for this window: wuss fills its + * backdrop and draws its icons unaided, which is the whole of its + * appearance. For a window whose content is entirely backdrop colour + * and/or icons (a label-only dialogue, say) this saves the task a no-op + * redraw handler and a window-handle check in it. + */ + wuss_WINDOW_NO_REDRAW = 1 << 10 } wuss_window_flags_t; diff --git a/libraries/wuss/component/proginfo.c b/libraries/wuss/component/proginfo.c index 28373ea..3fa6499 100644 --- a/libraries/wuss/component/proginfo.c +++ b/libraries/wuss/component/proginfo.c @@ -147,7 +147,7 @@ result_t wuss_proginfo_create(wuss_proginfo_t **out, wuss_WINDOW_NO_BACK | wuss_WINDOW_NO_CLOSE | wuss_WINDOW_NO_TOGGLE_SIZE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE | - wuss_WINDOW_HIDDEN, + wuss_WINDOW_NO_REDRAW | wuss_WINDOW_HIDDEN, wuss_BACKDROP_COLOUR(wuss_COLOUR_WHITE), SIZE2D(w, h), SIZE2D(w, h), diff --git a/libraries/wuss/core/redraw.c b/libraries/wuss/core/redraw.c index ff02032..0dc826a 100644 --- a/libraries/wuss/core/redraw.c +++ b/libraries/wuss/core/redraw.c @@ -90,6 +90,9 @@ static void redraw_window(wuss_t *wuss, content.x0 - win->scroll.x, content.y0 - win->scroll.y); + /* NO_REDRAW: the backdrop fill above plus the icons below are the + * window's whole appearance, so skip the client redraw entirely */ + if (!(win->flags & wuss_WINDOW_NO_REDRAW)) { result_t crc; wuss_event_t event; diff --git a/libraries/wuss/test/tasks/image.c b/libraries/wuss/test/tasks/image.c index 53fcc13..5a3ddf8 100644 --- a/libraries/wuss/test/tasks/image.c +++ b/libraries/wuss/test/tasks/image.c @@ -272,9 +272,6 @@ result_t image_handle(wuss_window_t *window, switch (event->kind) { case wuss_EVENT_REDRAW: - /* the proginfo dialogue shares this task; only paint our own window */ - if (window != ic->window) - return result_OK; return image_redraw(event, task_data); case wuss_EVENT_MOUSE: From 138b53b2477deec89f91e80f8ac7548bfa836730 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 6 Sep 2026 23:16:05 +0100 Subject: [PATCH 09/98] docs(wuss): document wuss_WINDOW_NO_REDRAW and the proginfo component --- docs/windowing/wuss.md | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/docs/windowing/wuss.md b/docs/windowing/wuss.md index 59071ff..af75383 100644 --- a/docs/windowing/wuss.md +++ b/docs/windowing/wuss.md @@ -80,8 +80,9 @@ wuss_task_t; - `wuss_WINDOW_NO_HSCROLL` — no horizontal scrollbar on the bottom edge. - `wuss_WINDOW_NO_RESIZE` — no resize button in the bottom-right corner. - `wuss_WINDOW_NO_RESIZE_BLIT` — a resize (drag or toggle-size) always fully redraws the window's content instead of blitting the preserved region; for a task whose rendering depends on window size in ways a partial redraw can't patch (e.g. a layout that spans the whole window). +- `wuss_WINDOW_NO_REDRAW` — no `wuss_EVENT_REDRAW` is delivered for this window: Wuss fills its backdrop and draws its icons, and that is its whole appearance. For a window that is nothing but backdrop and/or icons (a label-only dialogue, say) this saves the task a no-op redraw handler — and, when the window shares a task with a window that does paint, a window-handle check in that handler. -`wuss_WINDOW_NO_CLOSE`/`NO_BACK`/`NO_TOGGLE_SIZE` are ignored if `flags` includes `wuss_WINDOW_NO_TITLEBAR`; `NO_VSCROLL`/`NO_HSCROLL`/`NO_RESIZE`/`NO_RESIZE_BLIT` apply regardless. +`wuss_WINDOW_NO_CLOSE`/`NO_BACK`/`NO_TOGGLE_SIZE` are ignored if `flags` includes `wuss_WINDOW_NO_TITLEBAR`; `NO_VSCROLL`/`NO_HSCROLL`/`NO_RESIZE`/`NO_RESIZE_BLIT`/`NO_REDRAW` apply regardless. All furniture actions (back, toggle-size, resize-drag, scrollbar arrow/thumb) are handled entirely within Wuss via `wuss_mouse_click`/`wuss_mouse_move` — no new client events. @@ -187,7 +188,7 @@ In a redraw callback: start drawing at `bounds.x0 - scroll.x`, `bounds.y0 - scro ## Redrawing - `wuss_redraw` repaints every window, back-to-front, unconditionally, having first painted the configured backdrop colour (see Setup) behind them, if any. -- Within a window, Wuss paints in a fixed order: the window background colour, then its icons (see "Icons" below), then the task's `wuss_EVENT_REDRAW` handler — so a task always draws over the background and any icons, never under them. +- Within a window, Wuss paints in a fixed order: the window background colour, then its icons (see "Icons" below), then the task's `wuss_EVENT_REDRAW` handler — so a task always draws over the background and any icons, never under them. A `wuss_WINDOW_NO_REDRAW` window skips that last step: background and icons are all it gets. - `wuss_invalidate` / `wuss_window_invalidate` mark a screen-space or window-local region dirty; window management calls these automatically for its own changes, but a task must call one of them itself whenever its content changes on its own (e.g. an animation), passing the union of the old and new areas that need repainting. - `wuss_redraw_dirty` repaints only the accumulated dirty region, then clears it, painting the backdrop colour into each dirty region first if one was configured. Without a configured backdrop, Wuss only repaints windows, not the background between/behind them, so a caller whose invalidation can expose background (e.g. after a window move) should clear that region itself first. - `wuss_get_dirty_count`/`wuss_get_dirty(wuss, index, out)` fetch the currently accumulated dirty regions (coalesced as they accumulate, up to a fixed cap after which further regions are merged into the last one) without redrawing. @@ -217,6 +218,36 @@ Wuss draws icons in creation order (later icons paint on top); hit-testing scans The bevel's light (top/left) and dark (bottom/right) edge shades come from `config->bevel.light` / `config->bevel.dark` at `wuss_create` time, validated like the other furniture colours; both default to the titlebar fill colour when `config` is `NULL`. +## Components + +Built with the `WUSS_COMPONENTS` CMake option, `libraries/wuss/component/` holds small reusable task helpers layered on the core. Their headers are under `include/wuss/component/`. + +### Program-information dialogue + +`wuss_proginfo` is the RISC OS "Info" / Toolbox ProgInfo dialogue in miniature: a small fixed window of Name / Purpose / Author / Version rows a task fills in once, then typically hangs off its Menu-button pop-up. + +```C +typedef struct wuss_proginfo_desc +{ + const char *name; /* row "Name" -- required */ + const char *purpose; /* row "Purpose" */ + const char *author; /* row "Author" */ + const char *version; /* row "Version" */ +} +wuss_proginfo_desc_t; + +result_t wuss_proginfo_create(wuss_proginfo_t **out, wuss_task_t *task, + const wuss_proginfo_desc_t *desc); +void wuss_proginfo_destroy(wuss_proginfo_t *doomed); +wuss_window_t *wuss_proginfo_window(const wuss_proginfo_t *pi); +``` + +`wuss_proginfo_create` builds one hidden window on `task`, sized from that task's font metrics and the text in `desc`, one label row per non-NULL field (the name right-justified in a left column, the value left-justified beside it). Each `desc` field is borrowed and copied. + +The window is created `wuss_WINDOW_NO_CLOSE` and `wuss_WINDOW_NO_REDRAW` — it is labels on a flat backdrop, so it needs no redraw handler — and stays on `task` until `wuss_proginfo_destroy` closes it. So `task` must **not** be an autoclose task: the hidden window would keep its window list from ever emptying. It must also outlive the handle, and — as for any borrowed menu-item window — any menu chain referencing `wuss_proginfo_window()` must be closed before the dialogue is destroyed. + +Point a menu item's `window` field at `wuss_proginfo_window()` to have Wuss show the dialogue where a submenu would open. + ## Glossary Terms as this document and the API use them. Several are RISC OS conventions, which Wuss follows. From 42557d19fa985b2b79a874f18fc04a5e57a224b4 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 6 Sep 2026 23:21:36 +0100 Subject: [PATCH 10/98] feat(wuss): add a border field to label icons wuss_icon_spec_t gains `border` (wuss_icon_border_t: NONE / RIDGE / GROOVE), honoured by wuss_ICON_TYPE_LABEL and ignored by every other type. RIDGE is a 1px raised bevel, GROOVE a 1px sunken one -- a RISC OS-style read-only display field -- both in the window manager's bevel shades. Zero stays the current borderless look, so zero-initialised specs are unaffected. An out-of-range border on a label spec is result_WUSS_BAD_ICON. --- docs/windowing/wuss.md | 4 ++-- include/wuss/icon.h | 22 ++++++++++++++++++++++ libraries/wuss/icon.h | 1 + libraries/wuss/icon/draw.c | 17 +++++++++++++++++ libraries/wuss/icon/from-spec.c | 6 ++++++ 5 files changed, 48 insertions(+), 2 deletions(-) diff --git a/docs/windowing/wuss.md b/docs/windowing/wuss.md index af75383..4cd1920 100644 --- a/docs/windowing/wuss.md +++ b/docs/windowing/wuss.md @@ -198,14 +198,14 @@ In a redraw callback: start drawing at `bounds.x0 - scroll.x`, `bounds.y0 - scro Taking inspiration from RISC OS, a window can carry **icons**: rectangular UI elements Wuss draws and hit-tests inside the content area. v1 ships two types: -- `wuss_ICON_TYPE_LABEL` — static text. Clicks fall through to the task as `wuss_EVENT_MOUSE`. +- `wuss_ICON_TYPE_LABEL` — static text, optionally in a 1px raised or sunken border (`spec.border` — `wuss_ICON_BORDER_NONE` / `_RIDGE` / `_GROOVE`, the last a RISC OS-style read-only display field). Clicks fall through to the task as `wuss_EVENT_MOUSE`. - `wuss_ICON_TYPE_BUTTON` — a bevelled rectangle with a centred label and pressed-state feedback (the bevel inverts and the label shifts one pixel down-right while held). Clicks and hovers arrive as `wuss_EVENT_ICON`. The enum is left open for sprite and editable-text types later. Icons are dynamic and owned by their window: -- `wuss_icon_create(window, spec, &icon)` — returns an opaque `wuss_icon_t *`. The spec gives the bounding box, type, text (copied; `NULL` treated as `""`), foreground and background palette indices, and flags. A `wuss_ICON_TYPE_BUTTON` must pass a real `bg`; passing `wuss_NO_BACKGROUND` is rejected with `result_WUSS_BAD_ICON`. An unknown type is also `result_WUSS_BAD_ICON`; an out-of-range `fg`/`bg` is `result_WUSS_BAD_COLOUR`. +- `wuss_icon_create(window, spec, &icon)` — returns an opaque `wuss_icon_t *`. The spec gives the bounding box, type, text (copied; `NULL` treated as `""`), foreground and background palette indices, a `border` (label only), and flags. A `wuss_ICON_TYPE_BUTTON` must pass a real `bg`; passing `wuss_NO_BACKGROUND` is rejected with `result_WUSS_BAD_ICON`. An unknown type is also `result_WUSS_BAD_ICON`; an out-of-range `fg`/`bg` is `result_WUSS_BAD_COLOUR`. - `wuss_icon_delete(icon)` — NULL-safe. - `wuss_icon_set_text(icon, text)`, `wuss_icon_set_hidden(icon, hidden)`. - Getters: `wuss_icon_get_bbox`, `wuss_icon_get_type`, `wuss_icon_get_text` (never `NULL`), `wuss_icon_get_window`. diff --git a/include/wuss/icon.h b/include/wuss/icon.h index 94b36f2..2999d0a 100644 --- a/include/wuss/icon.h +++ b/include/wuss/icon.h @@ -105,6 +105,24 @@ typedef enum wuss_icon_type } wuss_icon_type_t; +/** + * The border drawn around a wuss_ICON_TYPE_LABEL, inside its bounding box. + * Ignored by every other icon type. Uses the window manager's bevel shades + * (wuss_config_t::bevel), the same as a button. + */ +typedef enum wuss_icon_border +{ + /** No border; the label fills its box as before. The default. */ + wuss_ICON_BORDER_NONE = 0, + /** A 1px raised bevel: light top/left, dark bottom/right, like a button at + * rest. */ + wuss_ICON_BORDER_RIDGE, + /** A 1px sunken bevel: dark top/left, light bottom/right -- a RISC OS-style + * read-only display field. */ + wuss_ICON_BORDER_GROOVE +} +wuss_icon_border_t; + /** Icon appearance and behaviour flags, combinable with bitwise OR. */ typedef enum wuss_icon_flags { @@ -195,6 +213,10 @@ typedef struct wuss_icon_spec * draw in the left gutter, as an index into the system palette. Ignored * unless that flag is set; ignored by all other icon types. */ wuss_colour_t swatch; + /** wuss_ICON_TYPE_LABEL: border drawn inside the bounding box. Zero + * (wuss_ICON_BORDER_NONE) is the default for zero-initialised specs. + * Ignored by all other icon types. */ + wuss_icon_border_t border; /** Appearance/behaviour flags. */ wuss_icon_flags_t flags; } diff --git a/libraries/wuss/icon.h b/libraries/wuss/icon.h index ff67acc..d4fe3c8 100644 --- a/libraries/wuss/icon.h +++ b/libraries/wuss/icon.h @@ -36,6 +36,7 @@ struct wuss_icon int group; /* radio: exclusive-selection group; 0 = none */ wuss_colour_t swatch; /* menu entry + FLAGS_SWATCH: left-gutter chip * colour; wuss_NO_BACKGROUND otherwise */ + wuss_icon_border_t border; /* label: inside-bbox border; NONE otherwise */ wuss_icon_flags_t flags; wuss_icon_state_t state; }; diff --git a/libraries/wuss/icon/draw.c b/libraries/wuss/icon/draw.c index b058a7e..5af105e 100644 --- a/libraries/wuss/icon/draw.c +++ b/libraries/wuss/icon/draw.c @@ -145,6 +145,23 @@ static void wuss__icon_draw_label(const icon_draw_ctx_t *c) bg = icon_blend_ground(c, c->fg); } + if (icon->border != wuss_ICON_BORDER_NONE) + { + colour_t light, dark; + + light = c->wuss->palette[c->wuss->bevel_light]; + dark = c->wuss->palette[c->wuss->bevel_dark]; + + screen_draw_line(c->scr, b->x0, b->y0, b->x1 - 1, b->y0, + icon->border == wuss_ICON_BORDER_RIDGE ? light : dark); + screen_draw_line(c->scr, b->x0, b->y0, b->x0, b->y1 - 1, + icon->border == wuss_ICON_BORDER_RIDGE ? light : dark); + screen_draw_line(c->scr, b->x0, b->y1 - 1, b->x1 - 1, b->y1 - 1, + icon->border == wuss_ICON_BORDER_RIDGE ? dark : light); + screen_draw_line(c->scr, b->x1 - 1, b->y0, b->x1 - 1, b->y1 - 1, + icon->border == wuss_ICON_BORDER_RIDGE ? dark : light); + } + if (!c->have_font) return; diff --git a/libraries/wuss/icon/from-spec.c b/libraries/wuss/icon/from-spec.c index fa0883c..1ef4fc5 100644 --- a/libraries/wuss/icon/from-spec.c +++ b/libraries/wuss/icon/from-spec.c @@ -56,6 +56,10 @@ result_t wuss__icon_from_spec(const wuss_t *w, spec->pattern >= screen_PATTERN__LIMIT) return result_WUSS_BAD_ICON; + if (spec->type == wuss_ICON_TYPE_LABEL && + spec->border > wuss_ICON_BORDER_GROOVE) + return result_WUSS_BAD_ICON; + if (fg >= w->npalette) return result_WUSS_BAD_COLOUR; @@ -76,6 +80,8 @@ result_t wuss__icon_from_spec(const wuss_t *w, out->bitmap = (spec->type == wuss_ICON_TYPE_BITMAP) ? spec->bitmap : NULL; out->group = (spec->type == wuss_ICON_TYPE_RADIO) ? spec->group : 0; out->swatch = swatch; + out->border = (spec->type == wuss_ICON_TYPE_LABEL) ? spec->border + : wuss_ICON_BORDER_NONE; out->flags = spec->flags; out->state = wuss_ICON_STATE_NONE; From 54247cd3e4c6e0e2c6f4f813c5b24aa358b32b1e Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 6 Sep 2026 23:24:18 +0100 Subject: [PATCH 11/98] feat(wuss): show proginfo values in sunken display fields Each value label now carries wuss_ICON_BORDER_GROOVE and is centred in its box, with a few pixels of padding added each side, matching the RISC OS ProgInfo look. --- include/wuss/component/proginfo.h | 4 ++-- libraries/wuss/component/proginfo.c | 21 ++++++++++++--------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/include/wuss/component/proginfo.h b/include/wuss/component/proginfo.h index 2ba36b4..d28acd7 100644 --- a/include/wuss/component/proginfo.h +++ b/include/wuss/component/proginfo.h @@ -10,8 +10,8 @@ * A wuss_proginfo owns one window (titlebar caption only -- no close, back, * toggle, scrollbars or resize), created hidden on a task the caller passes * in and laid out from a wuss_proginfo_desc_t: one row per non-NULL field, - * the field name right-justified in a left column and its value - * left-justified beside it. + * the field name right-justified in a left column and its value centred in a + * sunken display field beside it. * * It is meant to be used as a wuss_menu_item_t::window -- point an "Info" * menu row at wuss_proginfo_window() and wuss shows it where a submenu would diff --git a/libraries/wuss/component/proginfo.c b/libraries/wuss/component/proginfo.c index 3fa6499..c0eb863 100644 --- a/libraries/wuss/component/proginfo.c +++ b/libraries/wuss/component/proginfo.c @@ -27,9 +27,10 @@ * site" action button -- the four text rows are the whole dialogue. Add a * button row through here if a task ever needs one. */ -#define PROGINFO_MARGIN 12 /* px border around the row block */ -#define PROGINFO_GAP 12 /* px between the name column and the value column */ -#define PROGINFO_ROW_PAD 6 /* px added to the font height for the row pitch */ +#define PROGINFO_MARGIN 12 /* px border around the row block */ +#define PROGINFO_GAP 12 /* px between the name column and the value column */ +#define PROGINFO_ROW_PAD 6 /* px added to the font height for the row pitch */ +#define PROGINFO_FIELD_PAD 4 /* px each side of the text inside a value field */ /* The rows, in display order. Only fields set in the desc get a row. */ static const char *const proginfo_labels[] = @@ -129,6 +130,7 @@ result_t wuss_proginfo_create(wuss_proginfo_t **out, namew = 1; if (valuew < 1) valuew = 1; + valuew += PROGINFO_FIELD_PAD * 2; /* room for the groove border + inset */ w = PROGINFO_MARGIN * 2 + namew + PROGINFO_GAP + valuew; h = PROGINFO_MARGIN * 2 + rowh * nrows; @@ -180,13 +182,14 @@ result_t wuss_proginfo_create(wuss_proginfo_t **out, specs[nspecs].flags = wuss_ICON_FLAGS_JUSTIFY_RIGHT; nspecs++; - specs[nspecs].bbox = (box_t) BOX_POS_SIZE(PROGINFO_MARGIN + namew + + specs[nspecs].bbox = (box_t) BOX_POS_SIZE(PROGINFO_MARGIN + namew + PROGINFO_GAP, y, valuew, rowh); - specs[nspecs].type = wuss_ICON_TYPE_LABEL; - specs[nspecs].text = values[i]; - specs[nspecs].fg = wuss_COLOUR_BLACK; - specs[nspecs].bg = wuss_NO_BACKGROUND; - specs[nspecs].flags = wuss_ICON_FLAGS_NONE; + specs[nspecs].type = wuss_ICON_TYPE_LABEL; + specs[nspecs].text = values[i]; + specs[nspecs].fg = wuss_COLOUR_BLACK; + specs[nspecs].bg = wuss_NO_BACKGROUND; + specs[nspecs].border = wuss_ICON_BORDER_GROOVE; /* RISC OS display field */ + specs[nspecs].flags = wuss_ICON_FLAGS_JUSTIFY_CENTRE; nspecs++; } From e848b88c09f32441e71f34446404b4ca7a62fa62 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 6 Sep 2026 23:32:40 +0100 Subject: [PATCH 12/98] fix(wuss): deliver task QUIT before tearing down the menu chain wuss_destroy closed doomed->menu_chain first, freeing the chain node, then delivered wuss_EVENT_QUIT. A QUIT handler that closes its own menu chain (via a wuss_menu_handle_t it still holds) then dereferenced the freed node before wuss_menu_close's stale-handle guard could fire -- a heap use-after-free at shutdown. Deliver QUIT to every registered task first, so a handler tears down its own windows and menu chain against live objects; the manager's own leftover-chain close and the z_order sweep then just find less to do. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/core/destroy.c | 46 +++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/libraries/wuss/core/destroy.c b/libraries/wuss/core/destroy.c index b8582de..a85ab5d 100644 --- a/libraries/wuss/core/destroy.c +++ b/libraries/wuss/core/destroy.c @@ -15,9 +15,35 @@ void wuss_destroy(wuss_t *doomed) if (doomed == NULL) return; + /* QUIT then free any tasks the caller left registered, so a task's + * client-owned task_data (freed only from its QUIT handler, per the + * task_data-ownership contract) is not leaked. Done before the menu-chain + * and window teardown below so a QUIT handler can still close its own + * windows and its own menu chain (via a wuss_menu_handle_t it still holds) + * against live objects -- the leftover-chain close and the z_order sweep + * then just find less to do. + * + * ponytail: a task whose QUIT handler leaves an open chain standing, and + * whose flashing pick then gets delivered by the wuss_menu_close below to + * an already-freed earlier task, would fault. No task does that today; fix + * by tracking chain ownership per task if one ever needs to. */ + e = doomed->tasks.next; + while (e != NULL) + { + list_t *next; + wuss_event_t event; + + next = e->next; + event.kind = wuss_EVENT_QUIT; + (void) wuss__deliver(wuss__task_from_link(e), NULL, &event); + wuss__free(doomed, e); + e = next; + } + #ifdef WUSS_MENUS - /* Drop any open menu chain first: its windows are freed by the z_order - * sweep below, but the chain nodes and their icon-handle arrays are not. */ + /* Drop any menu chain a QUIT handler left open: its windows are freed by + * the z_order sweep below, but the chain nodes and their icon-handle + * arrays are not. NULL (the common case now) is a no-op. */ wuss_menu_close(doomed->menu_chain); #endif @@ -34,22 +60,6 @@ void wuss_destroy(wuss_t *doomed) e = next; } - /* Windows are gone; QUIT then free any tasks the caller left registered, - * so a task's client-owned task_data (freed only from its QUIT handler, - * per the task_data-ownership contract) is not leaked. */ - e = doomed->tasks.next; - while (e != NULL) - { - list_t *next; - wuss_event_t event; - - next = e->next; - event.kind = wuss_EVENT_QUIT; - (void) wuss__deliver(wuss__task_from_link(e), NULL, &event); - wuss__free(doomed, e); - e = next; - } - packer_destroy(doomed->layout); wuss__free(doomed, doomed->palette); wuss__free(doomed, doomed); /* reads doomed->alloc.free before freeing */ From a99956cbcb653713d24fc83893048ce093f083d1 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 6 Sep 2026 23:33:56 +0100 Subject: [PATCH 13/98] refactor(wuss): use MIN/MAX macros for proginfo layout clamps Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/component/proginfo.c | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/libraries/wuss/component/proginfo.c b/libraries/wuss/component/proginfo.c index c0eb863..44aa7e6 100644 --- a/libraries/wuss/component/proginfo.c +++ b/libraries/wuss/component/proginfo.c @@ -9,6 +9,7 @@ #endif #include "base/result.h" +#include "base/utils.h" #include "framebuf/bmfont.h" #include "geom/box.h" #include "geom/size.h" @@ -112,25 +113,18 @@ result_t wuss_proginfo_create(wuss_proginfo_t **out, if (vlen > 0) bmfont_measure(font, values[i], (int) vlen, INT_MAX, NULL, &vw); } - if (lw > namew) - namew = lw; - if (vw > valuew) - valuew = vw; + namew = MAX(namew, (int) lw); + valuew = MAX(valuew, (int) vw); nrows++; } fonth = 0; if (font != NULL) bmfont_get_info(font, NULL, &fonth); - rowh = fonth + PROGINFO_ROW_PAD; - if (rowh < 12) - rowh = 12; - - if (namew < 1) - namew = 1; - if (valuew < 1) - valuew = 1; - valuew += PROGINFO_FIELD_PAD * 2; /* room for the groove border + inset */ + rowh = MAX(fonth + PROGINFO_ROW_PAD, 12); + + namew = MAX(namew, 1); + valuew = MAX(valuew, 1) + PROGINFO_FIELD_PAD * 2; /* groove border + inset */ w = PROGINFO_MARGIN * 2 + namew + PROGINFO_GAP + valuew; h = PROGINFO_MARGIN * 2 + rowh * nrows; From cb3643a6663eb55681dca69696057d5eddac4b8f Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 6 Sep 2026 23:35:21 +0100 Subject: [PATCH 14/98] style(wuss): align the image_create init block Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/test/tasks/image.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/libraries/wuss/test/tasks/image.c b/libraries/wuss/test/tasks/image.c index 5a3ddf8..aaa7d47 100644 --- a/libraries/wuss/test/tasks/image.c +++ b/libraries/wuss/test/tasks/image.c @@ -60,11 +60,11 @@ result_t image_create(wuss_t *wuss, const char *images_dir; size2d_t sz; - task->wuss = wuss; - task->delegate = NULL; - task->resources = resources; - task->index = 0; - task->nnames = 0; + task->wuss = wuss; + task->delegate = NULL; + task->resources = resources; + task->index = 0; + task->nnames = 0; task->menu = NULL; task->proginfo = NULL; task->menu_handle = NULL; From fb61a91e60c02e89f4b7b8cee024897b7200be36 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 6 Sep 2026 23:38:14 +0100 Subject: [PATCH 15/98] feat(wuss): add wuss_COLOUR_WINDOW and wuss_COLOUR_MENU named colours Two more nearest-palette named colours for the conventional body fills: WINDOW is a light grey, MENU is white. Resolved the same way as the other wuss_COLOUR_* names, via the symbolic_rgb table in rebuild-palettecache. Co-Authored-By: Claude Sonnet 5 --- docs/windowing/wuss.md | 2 +- include/wuss/wuss.h | 3 +++ libraries/wuss/core/rebuild-palettecache.c | 4 +++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/windowing/wuss.md b/docs/windowing/wuss.md index 4cd1920..706f23e 100644 --- a/docs/windowing/wuss.md +++ b/docs/windowing/wuss.md @@ -28,7 +28,7 @@ A `wuss_backdrop_t` is `{ colour, pattern, pattern_bg }`; the `wuss_BACKDROP_COL A `wuss_colour_t` is a system-palette index. Values `0..127` are raw indices; `wuss_COLOUR_SYMBOLIC` (128) and above are *symbolic* — roles wuss resolves to a concrete index against the live palette (and, for the chrome roles, the live config): -- `wuss_COLOUR_BLACK`, `_WHITE`, `_RED`, `_GREEN`, `_BLUE`, `_YELLOW`, `_CYAN`, `_MAGENTA`, `_GREY` — nearest system-palette entry to the named RGB, the same lookup as `wuss_nearest_colour`. +- `wuss_COLOUR_BLACK`, `_WHITE`, `_RED`, `_GREEN`, `_BLUE`, `_YELLOW`, `_CYAN`, `_MAGENTA`, `_GREY`, `_WINDOW` (light grey), `_MENU` (white) — nearest system-palette entry to the named RGB, the same lookup as `wuss_nearest_colour`. `_WINDOW` and `_MENU` name the conventional window-body and menu-body fills. - `wuss_COLOUR_TITLE_BG`, `_TITLE_FG`, `_BUTTON_HILIGHT`, `_BUTTON_SHADOW`, `_ACCENT_BG`, `_ACCENT_FG`, `_BACKDROP` — echo the matching `wuss_config_t` field (`furniture.title.bg`, `bevel.light`, `bevel.dark`, `backdrop.colour`, …). Pass a symbolic value anywhere a `wuss_colour_t` is taken: config furniture/bevel/accent/backdrop, `wuss_window_create` / `wuss_window_set_background` backgrounds, icon specs. It is resolved once when stored, so drawing never pays for it and reads back a plain index. `wuss_set_palette` and `wuss_set_backdrop` re-resolve. `wuss_NO_BACKGROUND` is not symbolic and always passes through. diff --git a/include/wuss/wuss.h b/include/wuss/wuss.h index ac9a1cb..58307b0 100644 --- a/include/wuss/wuss.h +++ b/include/wuss/wuss.h @@ -134,6 +134,9 @@ typedef unsigned char wuss_colour_t; #define wuss_COLOUR_CYAN (wuss_COLOUR_SYMBOLIC + 6) #define wuss_COLOUR_MAGENTA (wuss_COLOUR_SYMBOLIC + 7) #define wuss_COLOUR_GREY (wuss_COLOUR_SYMBOLIC + 8) +/* Conventional body fills: window is a light grey, menu is white. */ +#define wuss_COLOUR_WINDOW (wuss_COLOUR_SYMBOLIC + 9) +#define wuss_COLOUR_MENU (wuss_COLOUR_SYMBOLIC + 10) /* Chrome roles: echo the matching wuss_config_t field, resolved to a * concrete index -- e.g. wuss_COLOUR_TITLE_BG is furniture.title.bg, diff --git a/libraries/wuss/core/rebuild-palettecache.c b/libraries/wuss/core/rebuild-palettecache.c index a2350fa..dc96b43 100644 --- a/libraries/wuss/core/rebuild-palettecache.c +++ b/libraries/wuss/core/rebuild-palettecache.c @@ -17,7 +17,9 @@ static const unsigned char wuss__named_rgb[][3] = { 0xFF, 0xFF, 0x00 }, /* wuss_COLOUR_YELLOW */ { 0x00, 0xFF, 0xFF }, /* wuss_COLOUR_CYAN */ { 0xFF, 0x00, 0xFF }, /* wuss_COLOUR_MAGENTA */ - { 0x80, 0x80, 0x80 } /* wuss_COLOUR_GREY */ + { 0x80, 0x80, 0x80 }, /* wuss_COLOUR_GREY */ + { 0xDD, 0xDD, 0xDD }, /* wuss_COLOUR_WINDOW -- light grey window body */ + { 0xFF, 0xFF, 0xFF } /* wuss_COLOUR_MENU -- white menu body */ }; void wuss__rebuild_palettecache(wuss_t *wuss) From f6c237c3737bc1f9c4a297b4fb71e82d940b1a0a Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 6 Sep 2026 23:43:17 +0100 Subject: [PATCH 16/98] refactor(wuss)!: make WINDOW and MENU colours config-backed roles wuss_COLOUR_WINDOW / wuss_COLOUR_MENU move from the named-colour block (fixed RGB, nearest-palette) into the chrome-roles block, echoing two new wuss_config_t::body fields: body.window and body.menu. When config is NULL they default to wuss_COLOUR_GREY (light grey) and wuss_COLOUR_WHITE. Resolved and range-checked at wuss_create like backdrop, and always honoured regardless of WUSS_FURNITURE / WUSS_ICONS / WUSS_MENUS. BREAKING CHANGE: the numeric values of wuss_COLOUR_WINDOW and wuss_COLOUR_MENU changed (SYMBOLIC+9/+10 to SYMBOLIC+23/+24), and they no longer resolve to a fixed light-grey / white -- a caller wanting those must set wuss_config_t::body. Co-Authored-By: Claude Sonnet 5 --- docs/windowing/wuss.md | 6 ++--- include/wuss/wuss.h | 29 ++++++++++++++++------ libraries/wuss/core/create.c | 11 ++++++++ libraries/wuss/core/impl.h | 2 ++ libraries/wuss/core/rebuild-palettecache.c | 7 +++--- 5 files changed, 42 insertions(+), 13 deletions(-) diff --git a/docs/windowing/wuss.md b/docs/windowing/wuss.md index 706f23e..3e467ca 100644 --- a/docs/windowing/wuss.md +++ b/docs/windowing/wuss.md @@ -28,10 +28,10 @@ A `wuss_backdrop_t` is `{ colour, pattern, pattern_bg }`; the `wuss_BACKDROP_COL A `wuss_colour_t` is a system-palette index. Values `0..127` are raw indices; `wuss_COLOUR_SYMBOLIC` (128) and above are *symbolic* — roles wuss resolves to a concrete index against the live palette (and, for the chrome roles, the live config): -- `wuss_COLOUR_BLACK`, `_WHITE`, `_RED`, `_GREEN`, `_BLUE`, `_YELLOW`, `_CYAN`, `_MAGENTA`, `_GREY`, `_WINDOW` (light grey), `_MENU` (white) — nearest system-palette entry to the named RGB, the same lookup as `wuss_nearest_colour`. `_WINDOW` and `_MENU` name the conventional window-body and menu-body fills. -- `wuss_COLOUR_TITLE_BG`, `_TITLE_FG`, `_BUTTON_HILIGHT`, `_BUTTON_SHADOW`, `_ACCENT_BG`, `_ACCENT_FG`, `_BACKDROP` — echo the matching `wuss_config_t` field (`furniture.title.bg`, `bevel.light`, `bevel.dark`, `backdrop.colour`, …). +- `wuss_COLOUR_BLACK`, `_WHITE`, `_RED`, `_GREEN`, `_BLUE`, `_YELLOW`, `_CYAN`, `_MAGENTA`, `_GREY` — nearest system-palette entry to the named RGB, the same lookup as `wuss_nearest_colour`. +- `wuss_COLOUR_TITLE_BG`, `_TITLE_FG`, `_BUTTON_HILIGHT`, `_BUTTON_SHADOW`, `_ACCENT_BG`, `_ACCENT_FG`, `_BACKDROP`, `_WINDOW`, `_MENU` — echo the matching `wuss_config_t` field (`furniture.title.bg`, `bevel.light`, `bevel.dark`, `backdrop.colour`, `body.window`, `body.menu`, …). `body.window` (work-area fill) defaults to `wuss_COLOUR_GREY`, `body.menu` (menu fill) to `wuss_COLOUR_WHITE`, when `config` is NULL. -Pass a symbolic value anywhere a `wuss_colour_t` is taken: config furniture/bevel/accent/backdrop, `wuss_window_create` / `wuss_window_set_background` backgrounds, icon specs. It is resolved once when stored, so drawing never pays for it and reads back a plain index. `wuss_set_palette` and `wuss_set_backdrop` re-resolve. `wuss_NO_BACKGROUND` is not symbolic and always passes through. +Pass a symbolic value anywhere a `wuss_colour_t` is taken: config furniture/bevel/accent/body/backdrop, `wuss_window_create` / `wuss_window_set_background` backgrounds, icon specs. It is resolved once when stored, so drawing never pays for it and reads back a plain index. `wuss_set_palette` and `wuss_set_backdrop` re-resolve. `wuss_NO_BACKGROUND` is not symbolic and always passes through. Destroy with `wuss_destroy`, which also destroys any windows still open on it. diff --git a/include/wuss/wuss.h b/include/wuss/wuss.h index 58307b0..b6e660d 100644 --- a/include/wuss/wuss.h +++ b/include/wuss/wuss.h @@ -119,7 +119,7 @@ typedef unsigned char wuss_colour_t; * the named colours pick the nearest system-palette entry to the RGB the * name implies, the chrome roles echo the matching wuss_config_t field. * Accepted anywhere a wuss_colour_t is: config furniture/bevel/accent/ - * backdrop, window backgrounds (see wuss_window_create), icon specs. + * body/backdrop, window backgrounds (see wuss_window_create), icon specs. * wuss_NO_BACKGROUND is not symbolic and always passes through unchanged. */ #define wuss_COLOUR_SYMBOLIC ((wuss_colour_t) 128) @@ -134,14 +134,12 @@ typedef unsigned char wuss_colour_t; #define wuss_COLOUR_CYAN (wuss_COLOUR_SYMBOLIC + 6) #define wuss_COLOUR_MAGENTA (wuss_COLOUR_SYMBOLIC + 7) #define wuss_COLOUR_GREY (wuss_COLOUR_SYMBOLIC + 8) -/* Conventional body fills: window is a light grey, menu is white. */ -#define wuss_COLOUR_WINDOW (wuss_COLOUR_SYMBOLIC + 9) -#define wuss_COLOUR_MENU (wuss_COLOUR_SYMBOLIC + 10) /* Chrome roles: echo the matching wuss_config_t field, resolved to a * concrete index -- e.g. wuss_COLOUR_TITLE_BG is furniture.title.bg, * wuss_COLOUR_BUTTON_HILIGHT is bevel.light, wuss_COLOUR_BUTTON_SHADOW is - * bevel.dark, wuss_COLOUR_BACKDROP is backdrop.colour. */ + * bevel.dark, wuss_COLOUR_BACKDROP is backdrop.colour, wuss_COLOUR_WINDOW is + * body.window, wuss_COLOUR_MENU is body.menu. */ #define wuss_COLOUR_TITLE_BG (wuss_COLOUR_SYMBOLIC + 16) #define wuss_COLOUR_TITLE_FG (wuss_COLOUR_SYMBOLIC + 17) #define wuss_COLOUR_BUTTON_HILIGHT (wuss_COLOUR_SYMBOLIC + 18) @@ -149,6 +147,8 @@ typedef unsigned char wuss_colour_t; #define wuss_COLOUR_ACCENT_BG (wuss_COLOUR_SYMBOLIC + 20) #define wuss_COLOUR_ACCENT_FG (wuss_COLOUR_SYMBOLIC + 21) #define wuss_COLOUR_BACKDROP (wuss_COLOUR_SYMBOLIC + 22) +#define wuss_COLOUR_WINDOW (wuss_COLOUR_SYMBOLIC + 23) +#define wuss_COLOUR_MENU (wuss_COLOUR_SYMBOLIC + 24) /** Furniture chrome colours, one entry per class of furniture. Title is * the only two-tone class (fill + text); the rest are drawn as a single @@ -307,8 +307,8 @@ wuss_backdrop_t; * * \note titlebar_height and palette are ignored when the library is built * with WUSS_FURNITURE off; bevel and accent are ignored when built - * with both WUSS_FURNITURE and WUSS_ICONS off. backdrop is always - * honoured. See the backdrop sub-struct for its own notes. + * with both WUSS_FURNITURE and WUSS_ICONS off. backdrop and body are + * always honoured. See the backdrop sub-struct for its own notes. */ typedef struct wuss_config { @@ -349,6 +349,21 @@ typedef struct wuss_config } accent; + /** + * Conventional body fills, as system-palette indices: window is the + * work-area behind a task's content, menu the pop-up menu background. Read + * back through wuss_COLOUR_WINDOW / wuss_COLOUR_MENU. When config is NULL, + * window defaults to wuss_COLOUR_GREY (a light grey) and menu to + * wuss_COLOUR_WHITE. Always honoured, regardless of the WUSS_FURNITURE / + * WUSS_ICONS / WUSS_MENUS options. + */ + struct + { + wuss_colour_t window; /**< Work-area body fill. */ + wuss_colour_t menu; /**< Menu body fill. */ + } + body; + /** Desktop background, painted behind windows on every redraw. */ wuss_backdrop_t backdrop; } diff --git a/libraries/wuss/core/create.c b/libraries/wuss/core/create.c index dc47067..077729e 100644 --- a/libraries/wuss/core/create.c +++ b/libraries/wuss/core/create.c @@ -118,12 +118,23 @@ result_t wuss_create(screen_t *scr, w->backdrop = config->backdrop; w->backdrop.colour = wuss__resolve_colour(w, w->backdrop.colour); w->backdrop.pattern_bg = wuss__resolve_colour(w, w->backdrop.pattern_bg); + w->window_bg = wuss__resolve_colour(w, config->body.window); + w->menu_bg = wuss__resolve_colour(w, config->body.menu); } else { w->backdrop.colour = wuss_NO_BACKGROUND; w->backdrop.pattern = screen_PATTERN_SOLID; w->backdrop.pattern_bg = wuss_NO_BACKGROUND; + w->window_bg = wuss__resolve_colour(w, wuss_COLOUR_GREY); + w->menu_bg = wuss__resolve_colour(w, wuss_COLOUR_WHITE); + } + + if (w->window_bg >= w->npalette || w->menu_bg >= w->npalette) + { + wuss__free(w, w->palette); + wuss__free(w, w); + return result_WUSS_BAD_COLOUR; } #ifdef WUSS_FURNITURE diff --git a/libraries/wuss/core/impl.h b/libraries/wuss/core/impl.h index 33ea210..d3737ad 100644 --- a/libraries/wuss/core/impl.h +++ b/libraries/wuss/core/impl.h @@ -128,6 +128,8 @@ struct wuss wuss_colour_t accent_bg; /* default action button fill */ wuss_colour_t accent_fg; /* default action button text */ #endif + wuss_colour_t window_bg; /* work-area body fill; wuss_COLOUR_WINDOW */ + wuss_colour_t menu_bg; /* menu body fill; wuss_COLOUR_MENU */ wuss_backdrop_t backdrop; /* colour==wuss_NO_BACKGROUND: none */ #ifdef WUSS_FURNITURE int titlebar_height; diff --git a/libraries/wuss/core/rebuild-palettecache.c b/libraries/wuss/core/rebuild-palettecache.c index dc96b43..219474d 100644 --- a/libraries/wuss/core/rebuild-palettecache.c +++ b/libraries/wuss/core/rebuild-palettecache.c @@ -17,9 +17,7 @@ static const unsigned char wuss__named_rgb[][3] = { 0xFF, 0xFF, 0x00 }, /* wuss_COLOUR_YELLOW */ { 0x00, 0xFF, 0xFF }, /* wuss_COLOUR_CYAN */ { 0xFF, 0x00, 0xFF }, /* wuss_COLOUR_MAGENTA */ - { 0x80, 0x80, 0x80 }, /* wuss_COLOUR_GREY */ - { 0xDD, 0xDD, 0xDD }, /* wuss_COLOUR_WINDOW -- light grey window body */ - { 0xFF, 0xFF, 0xFF } /* wuss_COLOUR_MENU -- white menu body */ + { 0x80, 0x80, 0x80 } /* wuss_COLOUR_GREY */ }; void wuss__rebuild_palettecache(wuss_t *wuss) @@ -57,4 +55,7 @@ void wuss__rebuild_palettecache(wuss_t *wuss) #endif if (wuss->backdrop.colour != wuss_NO_BACKGROUND) cache[wuss_COLOUR_BACKDROP - wuss_COLOUR_SYMBOLIC] = wuss->backdrop.colour; + + cache[wuss_COLOUR_WINDOW - wuss_COLOUR_SYMBOLIC] = wuss->window_bg; + cache[wuss_COLOUR_MENU - wuss_COLOUR_SYMBOLIC] = wuss->menu_bg; } From c5880fdc56204c1a5755d51ca48f81ec81850514 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 6 Sep 2026 23:47:07 +0100 Subject: [PATCH 17/98] feat(wuss): paint menu and window backdrops from the body role colours Menu windows now use wuss_COLOUR_MENU for their backdrop; the proginfo dialogue and the flat-fill demo windows (text, swatches) use wuss_COLOUR_WINDOW. Demos that draw their own backgrounds (wuss_NO_BACKGROUND) or pick a colour on purpose are left alone. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/component/proginfo.c | 2 +- libraries/wuss/menu/menu.c | 2 +- libraries/wuss/test/tasks/swatches.c | 2 +- libraries/wuss/test/tasks/text.c | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/libraries/wuss/component/proginfo.c b/libraries/wuss/component/proginfo.c index 44aa7e6..d820c12 100644 --- a/libraries/wuss/component/proginfo.c +++ b/libraries/wuss/component/proginfo.c @@ -144,7 +144,7 @@ result_t wuss_proginfo_create(wuss_proginfo_t **out, wuss_WINDOW_NO_TOGGLE_SIZE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE | wuss_WINDOW_NO_REDRAW | wuss_WINDOW_HIDDEN, - wuss_BACKDROP_COLOUR(wuss_COLOUR_WHITE), + wuss_BACKDROP_COLOUR(wuss_COLOUR_WINDOW), SIZE2D(w, h), SIZE2D(w, h), &pi->window); diff --git a/libraries/wuss/menu/menu.c b/libraries/wuss/menu/menu.c index ac02081..1f6b5fc 100644 --- a/libraries/wuss/menu/menu.c +++ b/libraries/wuss/menu/menu.c @@ -730,7 +730,7 @@ static result_t wuss__menu_spawn(wuss_t *wuss, rc = wuss_window_create(menu_task, &content, menu->title ? menu->title : "", menu_flags, - wuss_BACKDROP_COLOUR(wuss_COLOUR_WHITE), + wuss_BACKDROP_COLOUR(wuss_COLOUR_MENU), doc, min_doc, &node->window); if (rc != result_OK) { diff --git a/libraries/wuss/test/tasks/swatches.c b/libraries/wuss/test/tasks/swatches.c index e18cb67..af4869e 100644 --- a/libraries/wuss/test/tasks/swatches.c +++ b/libraries/wuss/test/tasks/swatches.c @@ -104,7 +104,7 @@ result_t swatches_create(wuss_t *wuss, swatches_task_t *task) SIZE2D(SWATCHES_DOC_W, 140), "Swatches", wuss_WINDOW_NO_RESIZE_BLIT, /* grid spans the whole window; a resize redraws all of it */ - wuss_BACKDROP_COLOUR(wuss_nearest_colour(wuss, 0xDD, 0xDD, 0xDD)), + wuss_BACKDROP_COLOUR(wuss_COLOUR_WINDOW), SIZE2D(SWATCHES_DOC_W, SWATCHES_DOC_H), SIZE2D(0, 0), &task->window); diff --git a/libraries/wuss/test/tasks/text.c b/libraries/wuss/test/tasks/text.c index 52f3fe5..869aaf3 100644 --- a/libraries/wuss/test/tasks/text.c +++ b/libraries/wuss/test/tasks/text.c @@ -220,7 +220,7 @@ result_t text_create(wuss_t *wuss, sz, "Sample Text", wuss_WINDOW_NO_RESIZE_BLIT, /* paragraph reflows across the whole window, so a resize must redraw all of it, not just the newly (un)covered edge */ - wuss_BACKDROP_COLOUR(wuss_nearest_colour(wuss, 0xFF, 0xFF, 0xFF)), + wuss_BACKDROP_COLOUR(wuss_COLOUR_WINDOW), sz, SIZE2D(0, 0), &task->window); From a2d768b290f1cb94442019668dd4e5deb4473070 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 6 Sep 2026 23:53:06 +0100 Subject: [PATCH 18/98] fix(wuss): set config.body in the wuss demo so menus aren't black fill_chrome_config left the new wuss_config_t::body sub-struct uninitialised on its stack config, so wuss_COLOUR_MENU / wuss_COLOUR_WINDOW resolved from garbage -- menu backdrops came out black. Extend g_chrome to carry body.window and body.menu per palette (PICO-8: light grey / white; WIMP16: 75% grey / white) and assign them. Cover both roles in the symbolic-colour test. Co-Authored-By: Claude Sonnet 5 --- apps/wuss/main.c | 14 +++++++++----- libraries/wuss/test/wuss-test.c | 6 +++++- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/apps/wuss/main.c b/apps/wuss/main.c index 940af95..2b38382 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -33,22 +33,24 @@ /* ----------------------------------------------------------------------- */ -/* Furniture/bevel/accent/backdrop colour indices, one row per palette. Same - * field order as the assignments in run_wuss. */ -static const wuss_colour_t g_chrome[2][15] = +/* Furniture/bevel/accent/backdrop/body colour indices, one row per palette. + * Same field order as the assignments in fill_chrome_config. */ +static const wuss_colour_t g_chrome[2][17] = { /* PICO-8 */ { palette_PICO8_DARK_BLUE, palette_PICO8_WHITE, palette_PICO8_GREEN, palette_PICO8_RED, palette_PICO8_ORANGE, palette_PICO8_LAVENDER, palette_PICO8_BLUE, palette_PICO8_DARK_BLUE, palette_PICO8_LIGHT_GREY, palette_PICO8_WHITE, palette_PICO8_DARK_GREY, palette_PICO8_DARK_BLUE, - palette_PICO8_WHITE, palette_PICO8_WHITE, palette_PICO8_LIGHT_GREY }, + palette_PICO8_WHITE, palette_PICO8_WHITE, palette_PICO8_LIGHT_GREY, + palette_PICO8_LIGHT_GREY, palette_PICO8_WHITE }, /* RISC OS 16-colour Wimp */ { palette_WIMP16_GREY_75, palette_WIMP16_BLACK, palette_WIMP16_GREEN, palette_WIMP16_RED, palette_WIMP16_ORANGE, palette_WIMP16_LIGHT_BLUE, palette_WIMP16_GREY_50, palette_WIMP16_GREY_62, palette_WIMP16_GREY_87, palette_WIMP16_WHITE, palette_WIMP16_GREY_50, palette_WIMP16_ORANGE, - palette_WIMP16_BLACK, palette_WIMP16_GREY_50, palette_WIMP16_GREY_37 } + palette_WIMP16_BLACK, palette_WIMP16_GREY_50, palette_WIMP16_GREY_37, + palette_WIMP16_GREY_75, palette_WIMP16_WHITE } }; static void fill_chrome_config(wuss_config_t *config, int palette_index) @@ -72,6 +74,8 @@ static void fill_chrome_config(wuss_config_t *config, int palette_index) config->backdrop.colour = c[13]; config->backdrop.pattern = screen_PATTERN_DOTS; config->backdrop.pattern_bg = c[14]; + config->body.window = c[15]; + config->body.menu = c[16]; } /* Redraw the whole screen one pixel at a time: each wuss_redraw_dirty call is diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 1129ace..61db459 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -487,6 +487,8 @@ result_t wuss_test(const char *resources) symcfg.furniture.title.bg = wuss_COLOUR_BLUE; /* -> index 2 */ symcfg.furniture.title.fg = wuss_COLOUR_WHITE; /* -> index 3 */ symcfg.backdrop = wuss_BACKDROP_COLOUR(wuss_COLOUR_GREEN); /* -> 1 */ + symcfg.body.window = wuss_COLOUR_RED; /* -> index 0 */ + symcfg.body.menu = wuss_COLOUR_WHITE; /* -> index 3 */ rc = wuss_create(&scr, NULL, 0, sympal, 5, &symcfg, NULL, &symw); if (rc != result_OK) @@ -504,7 +506,9 @@ result_t wuss_test(const char *resources) wuss__resolve_colour(symw, wuss_NO_BACKGROUND) != wuss_NO_BACKGROUND || wuss__resolve_colour(symw, wuss_COLOUR_TITLE_BG) != 2 || wuss__resolve_colour(symw, wuss_COLOUR_TITLE_FG) != 3 || - wuss__resolve_colour(symw, wuss_COLOUR_BACKDROP) != 1) + wuss__resolve_colour(symw, wuss_COLOUR_BACKDROP) != 1 || + wuss__resolve_colour(symw, wuss_COLOUR_WINDOW) != 0 || + wuss__resolve_colour(symw, wuss_COLOUR_MENU) != 3) { wuss_destroy(symw); goto Failure; From f79d331a0a3ae645391015092d320d7da74d7a0c Mon Sep 17 00:00:00 2001 From: David Thomas Date: Mon, 7 Sep 2026 00:36:28 +0100 Subject: [PATCH 19/98] fix(wuss): right-justified icon labels align on their true right edge wuss__icon_draw_label measured the label with target_width set to the box interior less 2px. A label whose text filled its box (the widest of a right-justified set) made bmfont_measure break early and return a short width, so pos.x = b->x1 - 1 - width placed it too far left; bmfont_draw then drew the full string and its right edge overhung the narrower labels. The proginfo dialogue's name column showed this as a ragged right edge. Measure the whole string (INT_MAX, no split) so every label's right edge lands at b->x1 - 1. Drop the now-unused interior_w/split_point locals. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/icon/draw.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libraries/wuss/icon/draw.c b/libraries/wuss/icon/draw.c index 5af105e..cc3f422 100644 --- a/libraries/wuss/icon/draw.c +++ b/libraries/wuss/icon/draw.c @@ -1,5 +1,6 @@ /* wuss/icon/draw.c -- draw a work-area icon */ +#include #include #include "base/utils.h" @@ -131,7 +132,6 @@ static void wuss__icon_draw_label(const icon_draw_ctx_t *c) const box_t *b = &c->b; colour_t bg; point_t pos; - int interior_w, split_point; bmfont_width_t width; if (icon->bg != wuss_NO_BACKGROUND) @@ -165,10 +165,10 @@ static void wuss__icon_draw_label(const icon_draw_ctx_t *c) if (!c->have_font) return; - interior_w = MAX((b->x1 - b->x0) - 2, 1); - + /* Measure the whole string, not the box interior: a clipped measurement + * left the widest label of a right-justified set overhanging the rest. */ bmfont_measure(c->font, icon->text, (int) strlen(icon->text), - interior_w, &split_point, &width); + INT_MAX, NULL, &width); if (icon->flags & wuss_ICON_FLAGS_JUSTIFY_CENTRE) pos.x = b->x0 + ((b->x1 - b->x0) - width) / 2; From dc10ddf85ab280a448612094354a71819d619445 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Mon, 7 Sep 2026 00:56:37 +0100 Subject: [PATCH 20/98] fix(wuss): tell the owning task when wuss closes its menu chain wuss__menu_click_outside and wuss_menu_open's replace-branch freed the whole menu chain without notifying the task that holds the wuss_menu_handle_t from wuss_menu_open. The task's stored handle then dangled, and its next wuss_menu_close (e.g. image.c on QUIT) was a heap-use-after-free. Add a wuss_EVENT_MENU_CLOSED task event, delivered to the chain's owner before the nodes are freed whenever wuss tears the chain down itself -- not for a pick (wuss_EVENT_MENU_SELECT covers that) nor for a wuss_menu_close the task made. Tasks drop their handle on it. A new wuss__menu_abandon helper is the single chokepoint for both internal close paths; it unlinks wuss->menu_chain before delivering so a re-entrant wuss_menu_close is a no-op. image.c also dropped its handle on a SELECT pick (same latent UAF). The wuss menu-move test now stores handles and asserts MENU_CLOSED fires on both the replace and the click-outside path, then calls wuss_menu_close on the now-NULL handle. Co-Authored-By: Claude Sonnet 5 --- include/wuss/menu.h | 6 +++++ include/wuss/task.h | 17 ++++++++++--- libraries/wuss/core/task/deliver.c | 1 + libraries/wuss/menu/menu.c | 38 ++++++++++++++++++++++++----- libraries/wuss/test/tasks/chars.c | 4 +++ libraries/wuss/test/tasks/greeble.c | 4 +++ libraries/wuss/test/tasks/image.c | 6 +++++ libraries/wuss/test/tasks/palette.c | 8 ++++++ libraries/wuss/test/wuss-test.c | 27 +++++++++++++++++--- 9 files changed, 98 insertions(+), 13 deletions(-) diff --git a/include/wuss/menu.h b/include/wuss/menu.h index 5dc7aa3..440fc81 100644 --- a/include/wuss/menu.h +++ b/include/wuss/menu.h @@ -108,6 +108,12 @@ typedef struct wuss__menu *wuss_menu_handle_t; * delivered to \p task's handle (with window == NULL); its data.menu_select * carries the (sub)menu, the item index and the release button. * + * If wuss instead closes the chain itself -- a click outside every menu + * window, or a later wuss_menu_open -- \p task's handle gets a + * wuss_EVENT_MENU_CLOSED (window == NULL, no data). A task that kept \p out + * must drop it there; the chain is freed by the time the event arrives. No + * such event follows a wuss_menu_close the task made. + * * \param[in] task Task opening the menu; receives wuss_EVENT_MENU_SELECT. * The menu windows are wuss-owned, not task's. * \param[in] menu Menu to show; borrowed, must outlive the open chain. diff --git a/include/wuss/task.h b/include/wuss/task.h index 7bed6dc..91d4c7e 100644 --- a/include/wuss/task.h +++ b/include/wuss/task.h @@ -72,7 +72,15 @@ typedef enum wuss_event_kind wuss_EVENT_PALETTE, /** A leaf menu item was picked; delivered to the task that opened the * menu. */ - wuss_EVENT_MENU_SELECT + wuss_EVENT_MENU_SELECT, + /** The menu chain the task opened has been closed by wuss itself -- a + * click outside every menu window, or another wuss_menu_open -- rather + * than by a pick or by the task's own wuss_menu_close. Delivered to the + * task that opened it (window == NULL); carries no data. A task that + * stored the wuss_menu_open handle must drop it here: the chain is + * already freed. Not fired for a SELECT pick (a wuss_EVENT_MENU_SELECT + * with a chain-closing button) or for a wuss_menu_close the task made. */ + wuss_EVENT_MENU_CLOSED } wuss_event_kind_t; @@ -92,8 +100,8 @@ typedef wuss_event_kind_t wuss_window_event_kind_t; * no window (window == NULL): the app-wide notifications. * * Members: wuss_EVENT_IDLE, wuss_EVENT_QUIT, wuss_EVENT_PALETTE, - * wuss_EVENT_MENU_SELECT, wuss_EVENT_ICON (reserved for a future shared/dock - * element; nothing emits it yet). + * wuss_EVENT_MENU_SELECT, wuss_EVENT_MENU_CLOSED, wuss_EVENT_ICON (reserved + * for a future shared/dock element; nothing emits it yet). */ typedef wuss_event_kind_t wuss_task_event_kind_t; @@ -187,7 +195,8 @@ typedef struct wuss_event /* wuss_EVENT_OPEN, wuss_EVENT_PRE_SHOW, wuss_EVENT_SHOW, * wuss_EVENT_PRE_CLOSE, wuss_EVENT_CLOSE, wuss_EVENT_IDLE, - * wuss_EVENT_QUIT and wuss_EVENT_PALETTE carry no data. */ + * wuss_EVENT_QUIT, wuss_EVENT_PALETTE and wuss_EVENT_MENU_CLOSED carry + * no data. */ } data; } diff --git a/libraries/wuss/core/task/deliver.c b/libraries/wuss/core/task/deliver.c index 6be99e2..02d8062 100644 --- a/libraries/wuss/core/task/deliver.c +++ b/libraries/wuss/core/task/deliver.c @@ -29,6 +29,7 @@ static int wuss__kind_ok_for(wuss_event_kind_t kind, int have_window) case wuss_EVENT_QUIT: case wuss_EVENT_PALETTE: case wuss_EVENT_MENU_SELECT: + case wuss_EVENT_MENU_CLOSED: return !have_window; case wuss_EVENT_ICON: diff --git a/libraries/wuss/menu/menu.c b/libraries/wuss/menu/menu.c index 1f6b5fc..d54898b 100644 --- a/libraries/wuss/menu/menu.c +++ b/libraries/wuss/menu/menu.c @@ -133,6 +133,36 @@ static void wuss__menu_close_from(struct wuss__menu *node) } } +/* Tear the whole open chain down because wuss decided to, not the client: a + * click outside every menu window, or another wuss_menu_open. The task that + * opened it still holds the handle wuss_menu_open handed back, so tell it the + * chain is gone (wuss_EVENT_MENU_CLOSED) before the nodes are freed -- a pick + * has wuss_EVENT_MENU_SELECT for that, but these paths have nothing. Unlinks + * wuss->menu_chain first so a re-entrant wuss_menu_close from the handler is a + * no-op. */ +static void wuss__menu_abandon(wuss_t *wuss) +{ + struct wuss__menu *root; + wuss_task_t *owner; + + root = wuss->menu_chain; + if (root == NULL) + return; + + owner = root->owner; + wuss->menu_chain = NULL; + + if (owner != NULL) + { + wuss_event_t ev; + + ev.kind = wuss_EVENT_MENU_CLOSED; + (void) wuss__deliver(owner, NULL, &ev); + } + + wuss__menu_close_from(root); +} + /* Compute where a submenu (or a borrowed window standing in for one) opens * off the row `icon` in parent level `self`: content top-left in screen * space, the child's own titlebar then sitting above it so the two rows line @@ -789,10 +819,7 @@ result_t wuss_menu_open(wuss_task_t *task, wuss = task->wuss; if (wuss->menu_chain != NULL) - { - wuss__menu_close_from(wuss->menu_chain); - wuss->menu_chain = NULL; - } + wuss__menu_abandon(wuss); /* RISC OS convention: the pointer opens the menu sitting a little inside its * first item, not on the top-left corner. Shift the content top-left up and @@ -968,7 +995,6 @@ int wuss__menu_click_outside(wuss_t *wuss, const wuss_window_t *hit) return 0; } - wuss__menu_close_from(wuss->menu_chain); - wuss->menu_chain = NULL; + wuss__menu_abandon(wuss); return 1; } diff --git a/libraries/wuss/test/tasks/chars.c b/libraries/wuss/test/tasks/chars.c index 80830c6..0198c6c 100644 --- a/libraries/wuss/test/tasks/chars.c +++ b/libraries/wuss/test/tasks/chars.c @@ -303,6 +303,10 @@ result_t chars_handle(wuss_window_t *window, return rc; } + case wuss_EVENT_MENU_CLOSED: + cc->menu_handle = NULL; /* wuss closed the chain under us */ + return result_OK; + case wuss_EVENT_REDRAW: return chars_redraw(event, task_data); diff --git a/libraries/wuss/test/tasks/greeble.c b/libraries/wuss/test/tasks/greeble.c index 58236e7..cd0427d 100644 --- a/libraries/wuss/test/tasks/greeble.c +++ b/libraries/wuss/test/tasks/greeble.c @@ -376,6 +376,10 @@ result_t greeble_handle(wuss_window_t *window, case wuss_EVENT_MENU_SELECT: return greeble_menu_select(task, event); + case wuss_EVENT_MENU_CLOSED: + task->menu_handle = NULL; /* wuss closed the chain under us */ + return result_OK; + case wuss_EVENT_QUIT: free(task); /* task_data was calloc'd per instance by the spawner */ return result_OK; diff --git a/libraries/wuss/test/tasks/image.c b/libraries/wuss/test/tasks/image.c index aaa7d47..73e0c12 100644 --- a/libraries/wuss/test/tasks/image.c +++ b/libraries/wuss/test/tasks/image.c @@ -290,6 +290,12 @@ result_t image_handle(wuss_window_t *window, index = event->data.menu_select.index; printf("image menu: picked \"%s\"\n", menu->items[index].text ? menu->items[index].text : "(sep)"); + if (!wuss_menu_should_keep_open(event)) + ic->menu_handle = NULL; /* SELECT pick already freed the chain */ + return result_OK; + + case wuss_EVENT_MENU_CLOSED: + ic->menu_handle = NULL; /* wuss closed the chain under us */ return result_OK; case wuss_EVENT_QUIT: diff --git a/libraries/wuss/test/tasks/palette.c b/libraries/wuss/test/tasks/palette.c index d975465..323a481 100644 --- a/libraries/wuss/test/tasks/palette.c +++ b/libraries/wuss/test/tasks/palette.c @@ -325,8 +325,12 @@ result_t palette_handle(wuss_window_t *window, const wuss_event_t *event, void *task_data) { + palette_task_t *pc; + NOT_USED(window); + pc = task_data; + switch (event->kind) { case wuss_EVENT_REDRAW: @@ -338,6 +342,10 @@ result_t palette_handle(wuss_window_t *window, case wuss_EVENT_MENU_SELECT: return palette_menu_select(task_data, event); + case wuss_EVENT_MENU_CLOSED: + pc->menu_handle = NULL; /* wuss closed the chain under us */ + return result_OK; + case wuss_EVENT_QUIT: free(task_data); /* calloc'd per instance by the spawner */ return result_OK; diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 61db459..14f6f30 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -333,9 +333,11 @@ static void menu_move_over_row(wuss_t *wuss, * so the test can see the press arrived and which menu it raised. */ typedef struct menu_task { - wuss_task_t *self; + wuss_task_t *self; const wuss_menu_t *menu; - int menu_press_count; + int menu_press_count; + wuss_menu_handle_t menu_handle; /* last chain this task opened */ + int menu_closed_count; /* wuss_EVENT_MENU_CLOSED deliveries */ } menu_task_t; @@ -355,7 +357,13 @@ static result_t menu_open_handle(wuss_window_t *window, { mt->menu_press_count++; return wuss_menu_open(mt->self, mt->menu, - event->data.mouse.point, NULL); + event->data.mouse.point, &mt->menu_handle); + } + + if (event->kind == wuss_EVENT_MENU_CLOSED) + { + mt->menu_closed_count++; + mt->menu_handle = NULL; /* chain freed under us; handle now stale */ } return result_OK; @@ -4517,11 +4525,24 @@ ColourMenuOK: ; if (mwuss->menu_chain == NULL || mwuss->menu_chain->menu != &menu_b) goto MoveCheckFail; + /* replacing menu A's chain told task A its handle was gone */ + if (mta.menu_closed_count != 1) goto MoveCheckFail; + if (mta.menu_handle != NULL) goto MoveCheckFail; + /* a plain SELECT press on bare backdrop still just dismisses */ wuss_mouse_click(mwuss, POINT(4, 196), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, NULL); if (mwuss->menu_chain != NULL) goto MoveCheckFail; + /* the click-outside dismissal told task B too. Regression: before the + * MENU_CLOSED fix wuss freed the chain here without telling task B, so + * mtb.menu_handle dangled and this wuss_menu_close was a use-after-free + * (ASan: heap-use-after-free in wuss_menu_close). */ + if (mtb.menu_closed_count != 1) goto MoveCheckFail; + if (mtb.menu_handle != NULL) goto MoveCheckFail; + wuss_menu_close(mtb.menu_handle); /* NULL now: safe no-op */ + if (mwuss->menu_chain != NULL) goto MoveCheckFail; + rc = result_OK; goto MoveDestroy; From 6f23a9a5226ab52685c38034bee3b5e469d532bd Mon Sep 17 00:00:00 2001 From: David Thomas Date: Mon, 7 Sep 2026 01:12:59 +0100 Subject: [PATCH 21/98] fix(wuss): keep the menu task alive through client task teardown Two teardown paths left a menu chain's owner holding a freed wuss_menu_handle_t, so a QUIT handler that called wuss_menu_close on it faulted: - wuss_task_destroy closed the owner's chain with a bare wuss_menu_close, then delivered QUIT. A close_chain_on_quit-style handler then double-freed the chain. It now calls wuss__menu_abandon, which delivers wuss_EVENT_MENU_CLOSED first so the handle is dropped before QUIT. - wuss_destroy freed the internal wuss__menu_task in the same task sweep that fires client QUITs. A client QUIT handler closing a chain it still held then walked the freed menu task's window list (ASan heap-use-after-free in list_remove via wuss_window_close). The sweep now skips wuss__menu_task and tears it down last, after every client QUIT and the leftover-chain close. Retires the matching ponytail TODO. wuss__menu_abandon is de-static'd and declared in core impl.h. New two-phase wuss-test regression covers both paths; each phase reproduces its UAF when its fix is reverted. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/core/destroy.c | 47 +++++++-- libraries/wuss/core/impl.h | 8 ++ libraries/wuss/core/task/destroy.c | 12 ++- libraries/wuss/menu/menu.c | 5 +- libraries/wuss/test/wuss-test.c | 147 +++++++++++++++++++++++++++++ 5 files changed, 203 insertions(+), 16 deletions(-) diff --git a/libraries/wuss/core/destroy.c b/libraries/wuss/core/destroy.c index a85ab5d..95dbf69 100644 --- a/libraries/wuss/core/destroy.c +++ b/libraries/wuss/core/destroy.c @@ -10,11 +10,18 @@ void wuss_destroy(wuss_t *doomed) { - list_t *e; + list_t *e; + wuss_task_t *menu_task; if (doomed == NULL) return; +#ifdef WUSS_MENUS + menu_task = doomed->menu_task; +#else + menu_task = NULL; +#endif + /* QUIT then free any tasks the caller left registered, so a task's * client-owned task_data (freed only from its QUIT handler, per the * task_data-ownership contract) is not leaked. Done before the menu-chain @@ -23,10 +30,11 @@ void wuss_destroy(wuss_t *doomed) * against live objects -- the leftover-chain close and the z_order sweep * then just find less to do. * - * ponytail: a task whose QUIT handler leaves an open chain standing, and - * whose flashing pick then gets delivered by the wuss_menu_close below to - * an already-freed earlier task, would fault. No task does that today; fix - * by tracking chain ownership per task if one ever needs to. */ + * The internal wuss__menu_task is skipped here and torn down last: a client + * task's QUIT handler may wuss_menu_close a chain it still holds, which + * closes menu windows off wuss__menu_task's window list -- so that task + * must outlive every client QUIT, regardless of registration order (it is + * created lazily on the first wuss_menu_open, so it can sit anywhere). */ e = doomed->tasks.next; while (e != NULL) { @@ -34,17 +42,38 @@ void wuss_destroy(wuss_t *doomed) wuss_event_t event; next = e->next; - event.kind = wuss_EVENT_QUIT; - (void) wuss__deliver(wuss__task_from_link(e), NULL, &event); - wuss__free(doomed, e); + if (wuss__task_from_link(e) != menu_task) + { + event.kind = wuss_EVENT_QUIT; + (void) wuss__deliver(wuss__task_from_link(e), NULL, &event); + wuss__free(doomed, e); + } e = next; } #ifdef WUSS_MENUS /* Drop any menu chain a QUIT handler left open: its windows are freed by * the z_order sweep below, but the chain nodes and their icon-handle - * arrays are not. NULL (the common case now) is a no-op. */ + * arrays are not. NULL (the common case now) is a no-op. Still runs + * against a live wuss__menu_task. + * + * ponytail: a bare wuss_menu_close, not wuss__menu_abandon -- every client + * task is already freed by the sweep above, so the chain's owner (and any + * flashing pick's owner) is dangling and must not be delivered to. No path + * leaves a mid-flash pick standing at wuss_destroy today; track the flash + * owner per task if one ever does. */ wuss_menu_close(doomed->menu_chain); + + /* Now the menu task has nothing left to own -- QUIT and free it. */ + if (menu_task != NULL) + { + wuss_event_t event; + + event.kind = wuss_EVENT_QUIT; + (void) wuss__deliver(menu_task, NULL, &event); + wuss__free(doomed, &menu_task->link); + doomed->menu_task = NULL; + } #endif e = doomed->z_order.next; diff --git a/libraries/wuss/core/impl.h b/libraries/wuss/core/impl.h index d3737ad..cb3f8af 100644 --- a/libraries/wuss/core/impl.h +++ b/libraries/wuss/core/impl.h @@ -460,6 +460,14 @@ result_t wuss__deliver(wuss_task_t *task, wuss_window_t *win_or_null, const wuss_event_t *ev); +#ifdef WUSS_MENUS +/* Tear down the whole open menu chain because wuss decided to (not the + * client): unlinks wuss->menu_chain, delivers wuss_EVENT_MENU_CLOSED to its + * owner so a stored handle is dropped, then frees the nodes. No-op if no + * chain is open. Defined in menu/menu.c. */ +void wuss__menu_abandon(wuss_t *wuss); +#endif + /* Notify a window's task that it has been moved or resized, via * wuss_EVENT_OPEN; the return value is discarded, matching how furniture * drawing and other in-line notifications are treated. */ diff --git a/libraries/wuss/core/task/destroy.c b/libraries/wuss/core/task/destroy.c index 74c7760..aefab74 100644 --- a/libraries/wuss/core/task/destroy.c +++ b/libraries/wuss/core/task/destroy.c @@ -26,12 +26,14 @@ void wuss_task_destroy(wuss_task_t *doomed) doomed->flags |= wuss_TASK__REAPING; #ifdef WUSS_MENUS - /* If this task opened the live menu chain, close it now: MENU_SELECT is - * delivered to the chain's owner, and leaving the chain open would leave - * its owner (and any pending pick flash's owner) pointing at `doomed` - * after it is freed below. */ + /* If this task opened the live menu chain, tear it down now: leaving it + * open would leave its owner (and any pending pick flash's owner) pointing + * at `doomed` after it is freed below. wuss__menu_abandon also delivers + * wuss_EVENT_MENU_CLOSED to `doomed`, so a wuss_menu_handle_t it still + * holds is nulled before the QUIT below -- otherwise a QUIT handler that + * calls wuss_menu_close on that stale handle double-frees the chain. */ if (wuss->menu_chain != NULL && wuss->menu_chain->owner == doomed) - wuss_menu_close(wuss->menu_chain); + wuss__menu_abandon(wuss); #endif /* One QUIT while every window is still alive. */ diff --git a/libraries/wuss/menu/menu.c b/libraries/wuss/menu/menu.c index d54898b..751b27f 100644 --- a/libraries/wuss/menu/menu.c +++ b/libraries/wuss/menu/menu.c @@ -139,8 +139,9 @@ static void wuss__menu_close_from(struct wuss__menu *node) * chain is gone (wuss_EVENT_MENU_CLOSED) before the nodes are freed -- a pick * has wuss_EVENT_MENU_SELECT for that, but these paths have nothing. Unlinks * wuss->menu_chain first so a re-entrant wuss_menu_close from the handler is a - * no-op. */ -static void wuss__menu_abandon(wuss_t *wuss) + * no-op. Also used by wuss_task_destroy when the chain's owner is the task + * going away, so declared in core impl.h. */ +void wuss__menu_abandon(wuss_t *wuss) { struct wuss__menu *root; wuss_task_t *owner; diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 14f6f30..a5b4f3e 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -273,6 +273,14 @@ static void reap_test_tasks(void) mk_task_count = 0; } +/* Drop the registry without destroying anything -- for a block that hands + * its tasks straight to wuss_destroy and wants that teardown path exercised + * rather than wuss_task_destroy's. */ +static void forget_test_tasks(void) +{ + mk_task_count = 0; +} + /* ----------------------------------------------------------------------- */ #if defined(WUSS_MENUS) && defined(WUSS_ICONS) @@ -338,6 +346,8 @@ typedef struct menu_task int menu_press_count; wuss_menu_handle_t menu_handle; /* last chain this task opened */ int menu_closed_count; /* wuss_EVENT_MENU_CLOSED deliveries */ + int close_chain_on_quit; /* mimic image.c: close a still-open + * chain from the QUIT handler */ } menu_task_t; @@ -366,6 +376,9 @@ static result_t menu_open_handle(wuss_window_t *window, mt->menu_handle = NULL; /* chain freed under us; handle now stale */ } + if (event->kind == wuss_EVENT_QUIT && mt->close_chain_on_quit) + wuss_menu_close(mt->menu_handle); /* against the menu task, still live */ + return result_OK; } #endif @@ -4563,6 +4576,140 @@ ColourMenuOK: ; if (rc != result_OK) return result_TEST_FAILED; } + + printf("test: a task teardown that leaves a menu chain open is safe -- " + "wuss_task_destroy abandons it before QUIT, and wuss_destroy frees " + "the internal menu task after every client QUIT\n"); + { + static const wuss_menu_item_t q_items[] = + { + { "Q-one", wuss_MENU_ITEM_NONE, NULL } + }; + static const wuss_menu_t q_menu = { "Q", q_items, NELEMS(q_items) }; + + const char *fontfile; + bmfont_t *font = NULL; + wuss_font_desc_t fdesc; + screen_t qscr; + bitmap_t qbm; + void *qpixels; + wuss_t *qwuss; + menu_task_t qmt; + wuss_task_t *task_q; + wuss_window_t *wq; + box_t bq; + + fontfile = path_join_filename(resources, 3, "resources", "bmfonts", + path_join_leafname("Tiny", "png")); + rc = bmfont_create(fontfile, &font); + if (rc != result_OK) + { + printf("wuss_test: menu-quit test could not load %s\n", fontfile); + goto Failure; + } + + qwuss = NULL; + qpixels = malloc((size_t) rowbytes * 200); + if (qpixels == NULL) { rc = result_OOM; goto QuitFail; } + rc = bitmap_init(&qbm, SIZE2D(200, 200), pixelfmt_bgrx8888, rowbytes, + NULL, qpixels); + if (rc != result_OK) goto QuitFreeOnly; + screen_for_bitmap(&qscr, &qbm); + + fdesc.font = font; + fdesc.font_class = wuss_FONT_CLASS_NONE; + fdesc.name = NULL; + rc = wuss_create(&qscr, &fdesc, 1, NULL, 0, NULL, NULL, &qwuss); + if (rc != result_OK) goto QuitFreeOnly; + + memset(&qmt, 0, sizeof(qmt)); + qmt.menu = &q_menu; + qmt.close_chain_on_quit = 1; + + /* the client task is registered first; the internal menu task is created + * only on the wuss_menu_open below, so it lands *after* the client in + * wuss::tasks. wuss_destroy's task sweep therefore reaches the client's + * QUIT while the menu task node is still, by list order, pending. */ + task_q = mk_task(qwuss, menu_open_handle, &qmt); + if (task_q == NULL) { rc = result_OOM; goto QuitDestroy; } + qmt.self = task_q; + + bq.x0 = 6; bq.y0 = 6; bq.x1 = 60; bq.y1 = 60; + rc = wuss_window_create(task_q, &bq, "Q", + wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | + wuss_WINDOW_NO_CLOSE | wuss_WINDOW_NO_BACK | + wuss_WINDOW_NO_TOGGLE_SIZE | wuss_WINDOW_NO_VSCROLL | + wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + box_size(&bq), SIZE2D(0, 0), &wq); + if (rc != result_OK) goto QuitDestroy; + NOT_USED(wq); + + wuss_mouse_click(qwuss, POINT(20, 20), wuss_BUTTON_MENU, + wuss_MOUSE_DOWN, NULL); + wuss_mouse_click(qwuss, POINT(20, 20), wuss_BUTTON_MENU, + wuss_MOUSE_UP, NULL); + if (qwuss->menu_chain == NULL) goto QuitCheckFail; + if (qmt.menu_handle == NULL) goto QuitCheckFail; + + /* Phase 1: wuss_task_destroy on the chain's owner. It abandons the chain + * (wuss_EVENT_MENU_CLOSED), which nulls qmt.menu_handle, *before* the + * QUIT it then delivers -- so the close_chain_on_quit handler's + * wuss_menu_close is a NULL no-op, not a double-free of the just-freed + * chain (ASan: heap-use-after-free in wuss_menu_close). */ + reap_test_tasks(); + if (qmt.menu_handle != NULL) goto QuitCheckFail; + if (qwuss->menu_chain != NULL) goto QuitCheckFail; + + /* Phase 2: same again but torn down by wuss_destroy's own task sweep, + * with task_q left registered. The internal menu task used to be freed by + * that sweep before task_q's QUIT, so the QUIT handler's wuss_menu_close + * walked a freed task's window list (ASan: heap-use-after-free in + * list_remove via wuss_window_close). */ + memset(&qmt, 0, sizeof(qmt)); + qmt.menu = &q_menu; + qmt.close_chain_on_quit = 1; + task_q = mk_task(qwuss, menu_open_handle, &qmt); + if (task_q == NULL) { rc = result_OOM; goto QuitDestroy; } + qmt.self = task_q; + rc = wuss_window_create(task_q, &bq, "Q2", + wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | + wuss_WINDOW_NO_CLOSE | wuss_WINDOW_NO_BACK | + wuss_WINDOW_NO_TOGGLE_SIZE | wuss_WINDOW_NO_VSCROLL | + wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + box_size(&bq), SIZE2D(0, 0), &wq); + if (rc != result_OK) goto QuitDestroy; + wuss_mouse_click(qwuss, POINT(20, 20), wuss_BUTTON_MENU, + wuss_MOUSE_DOWN, NULL); + wuss_mouse_click(qwuss, POINT(20, 20), wuss_BUTTON_MENU, + wuss_MOUSE_UP, NULL); + if (qwuss->menu_chain == NULL) goto QuitCheckFail; + + forget_test_tasks(); /* drop task_q from the registry; wuss_destroy frees it */ + wuss_destroy(qwuss); /* QUIT -> task_q closes the chain; must not fault */ + qwuss = NULL; + + rc = result_OK; + goto QuitFreeOnly; + +QuitCheckFail: + printf("wuss_test: menu-quit check failed (chain=%p handle=%p)\n", + (void *) qwuss->menu_chain, (void *) qmt.menu_handle); + rc = result_TEST_FAILED; + goto QuitDestroy; + +QuitDestroy: + reap_test_tasks(); + if (qwuss != NULL) + wuss_destroy(qwuss); +QuitFreeOnly: + free(qpixels); +QuitFail: + bmfont_destroy(font); + if (rc != result_OK) + return result_TEST_FAILED; + } #endif /* WUSS_ICONS */ #endif /* WUSS_MENUS */ From 308ba93f529b7031e079c18f6a9e37fa5052a52f Mon Sep 17 00:00:00 2001 From: David Thomas Date: Mon, 7 Sep 2026 09:07:55 +0100 Subject: [PATCH 22/98] feat(wuss): row leading in proginfo, batched icon border draw proginfo: tighten margins, add PROGINFO_ROW_LEADING so the label/value fields no longer abut -- rowh is the pitch, the field box is rowh minus the leading. icon draw: replace the four screen_draw_line calls for a LABEL icon's GROOVE/RIDGE border with two screen_draw_lines polylines (top+left, bottom+right). Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/component/proginfo.c | 19 +++++++++++-------- libraries/wuss/icon/draw.c | 21 +++++++++++++-------- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/libraries/wuss/component/proginfo.c b/libraries/wuss/component/proginfo.c index d820c12..90fbe3e 100644 --- a/libraries/wuss/component/proginfo.c +++ b/libraries/wuss/component/proginfo.c @@ -28,10 +28,11 @@ * site" action button -- the four text rows are the whole dialogue. Add a * button row through here if a task ever needs one. */ -#define PROGINFO_MARGIN 12 /* px border around the row block */ -#define PROGINFO_GAP 12 /* px between the name column and the value column */ -#define PROGINFO_ROW_PAD 6 /* px added to the font height for the row pitch */ -#define PROGINFO_FIELD_PAD 4 /* px each side of the text inside a value field */ +#define PROGINFO_MARGIN 4 /* px border around the row block */ +#define PROGINFO_GAP 4 /* px between the name column and the value column */ +#define PROGINFO_ROW_PAD 12 /* px added to the font height for the row pitch */ +#define PROGINFO_ROW_LEADING 4 /* px leading between rows */ +#define PROGINFO_FIELD_PAD 4 /* px each side of the text inside a value field */ /* The rows, in display order. Only fields set in the desc get a row. */ static const char *const proginfo_labels[] = @@ -121,7 +122,7 @@ result_t wuss_proginfo_create(wuss_proginfo_t **out, fonth = 0; if (font != NULL) bmfont_get_info(font, NULL, &fonth); - rowh = MAX(fonth + PROGINFO_ROW_PAD, 12); + rowh = MAX(fonth + PROGINFO_ROW_PAD, 12) + PROGINFO_ROW_LEADING; namew = MAX(namew, 1); valuew = MAX(valuew, 1) + PROGINFO_FIELD_PAD * 2; /* groove border + inset */ @@ -162,13 +163,15 @@ result_t wuss_proginfo_create(wuss_proginfo_t **out, for (i = 0; i < 4; i++) { int y; + int fieldh; if (values[i] == NULL) continue; - y = PROGINFO_MARGIN + rowh * (nspecs / 2); + y = PROGINFO_MARGIN + rowh * (nspecs / 2); + fieldh = rowh - PROGINFO_ROW_LEADING; /* rowh is the pitch; leave a gap */ - specs[nspecs].bbox = (box_t) BOX_POS_SIZE(PROGINFO_MARGIN, y, namew, rowh); + specs[nspecs].bbox = (box_t) BOX_POS_SIZE(PROGINFO_MARGIN, y, namew, fieldh); specs[nspecs].type = wuss_ICON_TYPE_LABEL; specs[nspecs].text = proginfo_labels[i]; specs[nspecs].fg = wuss_COLOUR_BLACK; @@ -177,7 +180,7 @@ result_t wuss_proginfo_create(wuss_proginfo_t **out, nspecs++; specs[nspecs].bbox = (box_t) BOX_POS_SIZE(PROGINFO_MARGIN + namew + - PROGINFO_GAP, y, valuew, rowh); + PROGINFO_GAP, y, valuew, fieldh); specs[nspecs].type = wuss_ICON_TYPE_LABEL; specs[nspecs].text = values[i]; specs[nspecs].fg = wuss_COLOUR_BLACK; diff --git a/libraries/wuss/icon/draw.c b/libraries/wuss/icon/draw.c index cc3f422..8ec3299 100644 --- a/libraries/wuss/icon/draw.c +++ b/libraries/wuss/icon/draw.c @@ -148,18 +148,23 @@ static void wuss__icon_draw_label(const icon_draw_ctx_t *c) if (icon->border != wuss_ICON_BORDER_NONE) { colour_t light, dark; + int ridge; + point_t tl[3]; /* top + left edges */ + point_t br[3]; /* bottom + right edges */ light = c->wuss->palette[c->wuss->bevel_light]; dark = c->wuss->palette[c->wuss->bevel_dark]; + ridge = (icon->border == wuss_ICON_BORDER_RIDGE); - screen_draw_line(c->scr, b->x0, b->y0, b->x1 - 1, b->y0, - icon->border == wuss_ICON_BORDER_RIDGE ? light : dark); - screen_draw_line(c->scr, b->x0, b->y0, b->x0, b->y1 - 1, - icon->border == wuss_ICON_BORDER_RIDGE ? light : dark); - screen_draw_line(c->scr, b->x0, b->y1 - 1, b->x1 - 1, b->y1 - 1, - icon->border == wuss_ICON_BORDER_RIDGE ? dark : light); - screen_draw_line(c->scr, b->x1 - 1, b->y0, b->x1 - 1, b->y1 - 1, - icon->border == wuss_ICON_BORDER_RIDGE ? dark : light); + tl[0] = POINT(b->x1 - 1, b->y0); + tl[1] = POINT(b->x0, b->y0); + tl[2] = POINT(b->x0, b->y1 - 1); + br[0] = POINT(b->x0, b->y1 - 1); + br[1] = POINT(b->x1 - 1, b->y1 - 1); + br[2] = POINT(b->x1 - 1, b->y0); + + screen_draw_lines(c->scr, tl, 3, ridge ? light : dark); + screen_draw_lines(c->scr, br, 3, ridge ? dark : light); } if (!c->have_font) From 6a90225c4db1270566b444c74297422e37b207d0 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Mon, 7 Sep 2026 09:38:55 +0100 Subject: [PATCH 23/98] feat(wuss): 2px icon bevel, drop proginfo trailing row leading icon draw: widen the LABEL GROOVE/RIDGE border to two pixels by drawing the L-shaped hi/lo polyline pair twice, the second inset by one. proginfo: subtract one PROGINFO_ROW_LEADING from the window height so the bottom margin matches the top -- rowh is the pitch and the final row needs no trailing leading. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/component/proginfo.c | 3 ++- libraries/wuss/icon/draw.c | 27 +++++++++++++++++---------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/libraries/wuss/component/proginfo.c b/libraries/wuss/component/proginfo.c index 90fbe3e..949356d 100644 --- a/libraries/wuss/component/proginfo.c +++ b/libraries/wuss/component/proginfo.c @@ -128,7 +128,8 @@ result_t wuss_proginfo_create(wuss_proginfo_t **out, valuew = MAX(valuew, 1) + PROGINFO_FIELD_PAD * 2; /* groove border + inset */ w = PROGINFO_MARGIN * 2 + namew + PROGINFO_GAP + valuew; - h = PROGINFO_MARGIN * 2 + rowh * nrows; + /* rowh is the pitch (field + leading); the last row has no trailing leading */ + h = PROGINFO_MARGIN * 2 + rowh * nrows - PROGINFO_ROW_LEADING; pi = a->malloc(sizeof(*pi)); if (pi == NULL) diff --git a/libraries/wuss/icon/draw.c b/libraries/wuss/icon/draw.c index 8ec3299..a261c1b 100644 --- a/libraries/wuss/icon/draw.c +++ b/libraries/wuss/icon/draw.c @@ -147,24 +147,31 @@ static void wuss__icon_draw_label(const icon_draw_ctx_t *c) if (icon->border != wuss_ICON_BORDER_NONE) { - colour_t light, dark; + colour_t light, dark, hi, lo; int ridge; + int d; point_t tl[3]; /* top + left edges */ point_t br[3]; /* bottom + right edges */ light = c->wuss->palette[c->wuss->bevel_light]; dark = c->wuss->palette[c->wuss->bevel_dark]; ridge = (icon->border == wuss_ICON_BORDER_RIDGE); + hi = ridge ? light : dark; + lo = ridge ? dark : light; - tl[0] = POINT(b->x1 - 1, b->y0); - tl[1] = POINT(b->x0, b->y0); - tl[2] = POINT(b->x0, b->y1 - 1); - br[0] = POINT(b->x0, b->y1 - 1); - br[1] = POINT(b->x1 - 1, b->y1 - 1); - br[2] = POINT(b->x1 - 1, b->y0); - - screen_draw_lines(c->scr, tl, 3, ridge ? light : dark); - screen_draw_lines(c->scr, br, 3, ridge ? dark : light); + /* two-pixel bevel: the outer L/R pair, then the same pair inset by one */ + for (d = 0; d < 2; d++) + { + tl[0] = POINT(b->x1 - 1 - d, b->y0 + d); + tl[1] = POINT(b->x0 + d, b->y0 + d); + tl[2] = POINT(b->x0 + d, b->y1 - 1 - d); + br[0] = POINT(b->x0 + d, b->y1 - 1 - d); + br[1] = POINT(b->x1 - 1 - d, b->y1 - 1 - d); + br[2] = POINT(b->x1 - 1 - d, b->y0 + d); + + screen_draw_lines(c->scr, tl, 3, hi); + screen_draw_lines(c->scr, br, 3, lo); + } } if (!c->have_font) From e50a384733ee9f31f2c3ea793470aeabfad75c06 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Mon, 7 Sep 2026 10:31:54 +0100 Subject: [PATCH 24/98] refactor(wuss): split proginfo onto a reusable info grid component New wuss_info component (component/info.h, component/info.c): builds a fixed label:value grid window from a wuss_info_row_t array -- measures the two columns, sizes the window from the task font, lays out two LABEL icons per row. This is the layout half that proginfo carried inline. proginfo is now a thin cap: wuss_proginfo_create maps the non-NULL desc fields to rows and calls wuss_info_create; wuss_proginfo_t is wuss_info_t and wuss_proginfo_destroy / _window are #define aliases. Public API and behaviour unchanged; the image test task still drives it as before. First consumer of a planned app-support component set; a second label:value dialogue will reuse wuss_info directly. Co-Authored-By: Claude Sonnet 5 --- CMakeLists.txt | 2 + include/wuss/component/info.h | 111 ++++++++++++++ include/wuss/component/proginfo.h | 47 +++--- libraries/wuss/component/info.c | 215 ++++++++++++++++++++++++++++ libraries/wuss/component/proginfo.c | 207 +++----------------------- 5 files changed, 372 insertions(+), 210 deletions(-) create mode 100644 include/wuss/component/info.h create mode 100644 libraries/wuss/component/info.c diff --git a/CMakeLists.txt b/CMakeLists.txt index d9dca41..e184622 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -133,6 +133,7 @@ endif() if(WUSS_COMPONENTS) list(APPEND PUBLIC_HEADERS include/wuss/component/fontmenu.h) list(APPEND PUBLIC_HEADERS include/wuss/component/colourmenu.h) + list(APPEND PUBLIC_HEADERS include/wuss/component/info.h) list(APPEND PUBLIC_HEADERS include/wuss/component/proginfo.h) endif() @@ -437,6 +438,7 @@ set(WUSS_MENU_SOURCES set(WUSS_COMPONENT_SOURCES libraries/wuss/component/fontmenu.c libraries/wuss/component/colourmenu.c + libraries/wuss/component/info.c libraries/wuss/component/proginfo.c) set(WUSS_SOURCES ${WUSS_CORE_SOURCES}) diff --git a/include/wuss/component/info.h b/include/wuss/component/info.h new file mode 100644 index 0000000..bdaa984 --- /dev/null +++ b/include/wuss/component/info.h @@ -0,0 +1,111 @@ +/* wuss/component/info.h -- a label:value grid dialogue */ + +/** + * \file info.h + * + * A shared wuss component: a small fixed window laid out as a grid of + * label:value rows -- the base of the RISC OS "Info" / Toolbox ProgInfo + * dialogue, and of any dialogue that is just a column of named readouts. + * + * A wuss_info owns one window (titlebar caption only -- no close, back, + * toggle, scrollbars or resize), created hidden on a task the caller passes + * in and laid out from an array of wuss_info_row_t: each row is a label + * right-justified in a left column and a value centred in a sunken display + * field beside it. The window is sized from the task's font metrics and the + * widest label and value. + * + * It is meant to be used as a wuss_menu_item_t::window -- point an "Info" + * menu row at wuss_info_window() and wuss shows it where a submenu would + * open. A task can equally wuss_window_set_hidden() it directly. + * + * The window is created wuss_WINDOW_NO_CLOSE and stays on the caller's task + * until wuss_info_destroy closes it. That task must therefore not be an + * autoclose task -- its window list would never empty -- and must outlive + * the handle. As for any borrowed wuss_menu_item_t::window, the dialogue + * must also outlive every menu chain that references it: close the chain + * (wuss_menu_close) before wuss_info_destroy. + * + * Built only when WUSS_COMPONENTS is defined (which implies WUSS_MENUS). + */ + +#ifndef WUSS_COMPONENT_INFO_H +#define WUSS_COMPONENT_INFO_H + +#ifdef __cplusplus +extern "C" +{ +#endif + +#include "base/result.h" + +#include "wuss/task.h" +#include "wuss/window.h" + +/* ----------------------------------------------------------------------- */ + +/** + * One row of an info dialogue: a label and its value. Both are borrowed and + * copied by wuss_info_create. + */ +typedef struct wuss_info_row +{ + const char *label; /**< Left-column caption, right-justified. */ + const char *value; /**< Right-column readout, shown in a sunken field. */ +} +wuss_info_row_t; + +/** Opaque handle: owns the dialogue window and the strings in it. */ +typedef struct wuss_info wuss_info_t; + +/* ----------------------------------------------------------------------- */ + +/** + * Build a label:value grid dialogue: a hidden window on \p task, one row per + * entry in \p rows. + * + * The window's on-screen size is derived from \p task's font metrics and the + * text in \p rows; it is created with wuss_WINDOW_HIDDEN, so nothing shows + * until the caller reveals it (directly, or via a wuss_menu_item_t::window + * that names wuss_info_window()). + * + * \param[out] out Filled with the new handle on success, untouched on + * failure. + * \param[in] task Task the dialogue window is created on. Must not be an + * autoclose task and must outlive the handle. + * \param[in] title Titlebar caption; borrowed and copied by + * wuss_window_create. + * \param[in] rows Row array; each label and value is borrowed and copied. + * \param[in] nrows Number of rows; must be >= 1. + * \return \ref result_OK, \ref result_OOM, \ref result_NULL_ARG if \p out, + * \p task, \p title or \p rows is NULL, \ref result_BAD_ARG if \p + * nrows is < 1, or a wuss_window_create / wuss_icon_create code. + */ +result_t wuss_info_create(wuss_info_t **out, + wuss_task_t *task, + const char *title, + const wuss_info_row_t *rows, + int nrows); + +/** + * Free an info dialogue: closes its window and frees every string in it. + * Safe to pass NULL. + * + * \param[in] doomed Handle to free, or NULL. + */ +void wuss_info_destroy(wuss_info_t *doomed); + +/** + * The dialogue's window, for use as a wuss_menu_item_t::window or to show + * and hide directly. Borrowed; valid until wuss_info_destroy. NULL only if + * \p info is NULL. + * + * \param[in] info Handle. + * \return The window, or NULL. + */ +wuss_window_t *wuss_info_window(const wuss_info_t *info); + +#ifdef __cplusplus +} +#endif + +#endif /* WUSS_COMPONENT_INFO_H */ diff --git a/include/wuss/component/proginfo.h b/include/wuss/component/proginfo.h index d28acd7..d909106 100644 --- a/include/wuss/component/proginfo.h +++ b/include/wuss/component/proginfo.h @@ -7,22 +7,14 @@ * miniature -- a small fixed window of Name / Purpose / Author / Version * rows a task fills in once, then hangs off its Menu-button pop-up. * - * A wuss_proginfo owns one window (titlebar caption only -- no close, back, - * toggle, scrollbars or resize), created hidden on a task the caller passes - * in and laid out from a wuss_proginfo_desc_t: one row per non-NULL field, - * the field name right-justified in a left column and its value centred in a - * sunken display field beside it. - * - * It is meant to be used as a wuss_menu_item_t::window -- point an "Info" - * menu row at wuss_proginfo_window() and wuss shows it where a submenu would - * open. A task can equally wuss_window_set_hidden() it directly. - * - * The window is created wuss_WINDOW_NO_CLOSE and stays on the caller's task - * until wuss_proginfo_destroy closes it. That task must therefore not be an - * autoclose task -- its window list would never empty -- and must outlive - * the handle. As for any borrowed wuss_menu_item_t::window, the dialogue - * must also outlive every menu chain that references it: close the chain - * (wuss_menu_close) before wuss_proginfo_destroy. + * This is a thin cap over \ref info.h: wuss_proginfo_create maps the + * non-NULL fields of a wuss_proginfo_desc_t to wuss_info_row_t rows and + * calls wuss_info_create. wuss_proginfo_t is wuss_info_t, and the destroy / + * window calls forward straight through, so all of info.h's lifetime rules + * apply verbatim: the window is wuss_WINDOW_NO_CLOSE, stays on the caller's + * task until wuss_proginfo_destroy, that task must not be an autoclose task + * and must outlive the handle, and the dialogue must outlive every menu + * chain that borrows it as a wuss_menu_item_t::window. * * Built only when WUSS_COMPONENTS is defined (which implies WUSS_MENUS). */ @@ -40,6 +32,8 @@ extern "C" #include "wuss/task.h" #include "wuss/window.h" +#include "wuss/component/info.h" + /* ----------------------------------------------------------------------- */ /** @@ -57,14 +51,17 @@ typedef struct wuss_proginfo_desc } wuss_proginfo_desc_t; -/** Opaque handle: owns the dialogue window and the label strings in it. */ -typedef struct wuss_proginfo wuss_proginfo_t; +/** + * Opaque handle: a wuss_info_t. Owns the dialogue window and its strings. + */ +typedef wuss_info_t wuss_proginfo_t; /* ----------------------------------------------------------------------- */ /** * Build a program-information dialogue: a hidden window on \p task, laid out - * from \p desc with one label row per non-NULL field. + * with one label row per non-NULL field of \p desc, in the order Name, + * Purpose, Author, Version. * * The window's on-screen size is derived from \p task's font metrics and the * text in \p desc; it is created with wuss_WINDOW_HIDDEN, so nothing shows @@ -78,8 +75,8 @@ typedef struct wuss_proginfo wuss_proginfo_t; * \param[in] desc Field values; each borrowed and copied. At least \c name * must be non-NULL. * \return \ref result_OK, \ref result_OOM, \ref result_NULL_ARG if \p out, - * \p task, \p desc or \c desc->name is NULL, or a wuss_window_create - * / wuss_icon_create code. + * \p task, \p desc or \c desc->name is NULL, or a wuss_info_create + * code. */ result_t wuss_proginfo_create(wuss_proginfo_t **out, wuss_task_t *task, @@ -87,21 +84,21 @@ result_t wuss_proginfo_create(wuss_proginfo_t **out, /** * Free a program-information dialogue: closes its window and frees every - * string in it. Safe to pass NULL. + * string in it. Safe to pass NULL. An alias for wuss_info_destroy. * * \param[in] doomed Handle to free, or NULL. */ -void wuss_proginfo_destroy(wuss_proginfo_t *doomed); +#define wuss_proginfo_destroy wuss_info_destroy /** * The dialogue's window, for use as a wuss_menu_item_t::window or to show * and hide directly. Borrowed; valid until wuss_proginfo_destroy. NULL only - * if \p pi is NULL. + * if \p pi is NULL. An alias for wuss_info_window. * * \param[in] pi Handle. * \return The window, or NULL. */ -wuss_window_t *wuss_proginfo_window(const wuss_proginfo_t *pi); +#define wuss_proginfo_window wuss_info_window #ifdef __cplusplus } diff --git a/libraries/wuss/component/info.c b/libraries/wuss/component/info.c new file mode 100644 index 0000000..bc048ef --- /dev/null +++ b/libraries/wuss/component/info.c @@ -0,0 +1,215 @@ +/* wuss/component/info.c -- a label:value grid dialogue */ + +#include +#include +#include + +#ifdef FORTIFY +#include "fortify/fortify.h" +#endif + +#include "base/result.h" +#include "base/utils.h" +#include "framebuf/bmfont.h" +#include "geom/box.h" +#include "geom/size.h" + +#include "wuss/icon.h" +#include "wuss/task.h" +#include "wuss/window.h" + +#include "wuss/component/info.h" + +#include "../core/impl.h" + +/* ----------------------------------------------------------------------- */ + +/* ponytail: a flat grid of LABEL icons, two per row -- no action buttons, no + * file-type sprite icon (the RISC OS Info dialogue's first field). Add either + * through here if a component ever needs one. */ + +#define INFO_MARGIN 4 /* px border around the row block */ +#define INFO_GAP 4 /* px between the label column and the value column */ +#define INFO_ROW_PAD 12 /* px added to the font height for the row pitch */ +#define INFO_ROW_LEADING 4 /* px leading between rows */ +#define INFO_FIELD_PAD 4 /* px each side of the text inside a value field */ + +/* The dialogue is one window on a task the caller passes in. It carries + * wuss_WINDOW_NO_CLOSE so a menu chain that borrows it as a + * wuss_menu_item_t::window can't have it pulled from under it, and + * wuss_info_destroy closes it explicitly. The task must therefore not be an + * autoclose one -- its window list would never empty -- and must outlive the + * handle. */ +struct wuss_info +{ + wuss_alloc_t alloc; /* copied hooks; the wuss_t itself is not retained */ + wuss_window_t *window; /* owned; closed by wuss_info_destroy */ +}; + +/* ----------------------------------------------------------------------- */ + +result_t wuss_info_create(wuss_info_t **out, + wuss_task_t *task, + const char *title, + const wuss_info_row_t *rows, + int nrows) +{ + result_t rc; + const wuss_alloc_t *a; + wuss_t *wuss; + bmfont_t *font; + wuss_info_t *info; + int fonth; + int rowh; + int fieldh; + int labelw; + int valuew; + int w; + int h; + box_t content; + wuss_icon_spec_t *specs; + int nspecs; + int i; + + if (out == NULL || task == NULL || title == NULL || rows == NULL) + return result_NULL_ARG; + if (nrows < 1) + return result_BAD_ARG; + + wuss = task->wuss; + a = &wuss->alloc; + font = wuss_get_font(wuss); + + /* Column widths: the widest label on the left, the widest value on the + * right. bmfont_measure with no target width returns the whole string's + * width in actual_width. A NULL font leaves both at 0; the icons still lay + * out, just with nothing measured to size them. */ + labelw = 0; + valuew = 0; + for (i = 0; i < nrows; i++) + { + bmfont_width_t lw; + bmfont_width_t vw; + + lw = vw = 0; + if (font != NULL) + { + size_t llen; + size_t vlen; + + llen = rows[i].label ? strlen(rows[i].label) : 0; + vlen = rows[i].value ? strlen(rows[i].value) : 0; + if (llen > 0) + bmfont_measure(font, rows[i].label, (int) llen, INT_MAX, NULL, &lw); + if (vlen > 0) + bmfont_measure(font, rows[i].value, (int) vlen, INT_MAX, NULL, &vw); + } + labelw = MAX(labelw, (int) lw); + valuew = MAX(valuew, (int) vw); + } + + fonth = 0; + if (font != NULL) + bmfont_get_info(font, NULL, &fonth); + rowh = MAX(fonth + INFO_ROW_PAD, 12) + INFO_ROW_LEADING; + fieldh = rowh - INFO_ROW_LEADING; /* rowh is the pitch; leave a gap */ + + labelw = MAX(labelw, 1); + valuew = MAX(valuew, 1) + INFO_FIELD_PAD * 2; /* groove border + inset */ + + w = INFO_MARGIN * 2 + labelw + INFO_GAP + valuew; + /* rowh is the pitch (field + leading); the last row has no trailing leading */ + h = INFO_MARGIN * 2 + rowh * nrows - INFO_ROW_LEADING; + + info = a->malloc(sizeof(*info)); + if (info == NULL) + return result_OOM; + info->alloc = *a; + info->window = NULL; + a = &info->alloc; /* use the copy from here on -- outlives the wuss_t */ + + content = (box_t) BOX_POS_SIZE(0, 0, w, h); + rc = wuss_window_create(task, + &content, + title, + wuss_WINDOW_NO_BACK | wuss_WINDOW_NO_CLOSE | + wuss_WINDOW_NO_TOGGLE_SIZE | wuss_WINDOW_NO_VSCROLL | + wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE | + wuss_WINDOW_NO_REDRAW | wuss_WINDOW_HIDDEN, + wuss_BACKDROP_COLOUR(wuss_COLOUR_WINDOW), + SIZE2D(w, h), + SIZE2D(w, h), + &info->window); + if (rc != result_OK) + { + a->free(info); + return rc; + } + + /* One right-justified label and one centred value per row. Zeroed so the + * icon-spec fields this component does not set (pattern, bitmap, group, + * swatch) are their safe defaults. */ + specs = a->malloc((size_t) nrows * 2 * sizeof(*specs)); + if (specs == NULL) + { + wuss_window_close(info->window); + a->free(info); + return result_OOM; + } + memset(specs, 0, (size_t) nrows * 2 * sizeof(*specs)); + + nspecs = 0; + for (i = 0; i < nrows; i++) + { + int y; + + y = INFO_MARGIN + rowh * i; + + specs[nspecs].bbox = (box_t) BOX_POS_SIZE(INFO_MARGIN, y, labelw, fieldh); + specs[nspecs].type = wuss_ICON_TYPE_LABEL; + specs[nspecs].text = rows[i].label; + specs[nspecs].fg = wuss_COLOUR_BLACK; + specs[nspecs].bg = wuss_NO_BACKGROUND; + specs[nspecs].flags = wuss_ICON_FLAGS_JUSTIFY_RIGHT; + nspecs++; + + specs[nspecs].bbox = (box_t) BOX_POS_SIZE(INFO_MARGIN + labelw + INFO_GAP, + y, valuew, fieldh); + specs[nspecs].type = wuss_ICON_TYPE_LABEL; + specs[nspecs].text = rows[i].value; + specs[nspecs].fg = wuss_COLOUR_BLACK; + specs[nspecs].bg = wuss_NO_BACKGROUND; + specs[nspecs].border = wuss_ICON_BORDER_GROOVE; /* RISC OS display field */ + specs[nspecs].flags = wuss_ICON_FLAGS_JUSTIFY_CENTRE; + nspecs++; + } + + rc = wuss_icon_create_array(info->window, specs, nspecs, NULL); + a->free(specs); + if (rc != result_OK) + { + wuss_window_close(info->window); /* frees the window and any icons on it */ + a->free(info); + return rc; + } + + *out = info; + return result_OK; +} + +void wuss_info_destroy(wuss_info_t *doomed) +{ + wuss_alloc_t alloc; + + if (doomed == NULL) + return; + + alloc = doomed->alloc; + wuss_window_close(doomed->window); /* frees the window and its icons */ + alloc.free(doomed); +} + +wuss_window_t *wuss_info_window(const wuss_info_t *info) +{ + return info ? info->window : NULL; +} diff --git a/libraries/wuss/component/proginfo.c b/libraries/wuss/component/proginfo.c index 949356d..991865d 100644 --- a/libraries/wuss/component/proginfo.c +++ b/libraries/wuss/component/proginfo.c @@ -1,221 +1,58 @@ /* wuss/component/proginfo.c -- a "program information" standard dialogue */ -#include #include -#include #ifdef FORTIFY #include "fortify/fortify.h" #endif #include "base/result.h" -#include "base/utils.h" -#include "framebuf/bmfont.h" -#include "geom/box.h" -#include "geom/size.h" -#include "wuss/icon.h" #include "wuss/task.h" -#include "wuss/window.h" +#include "wuss/component/info.h" #include "wuss/component/proginfo.h" -#include "../core/impl.h" - /* ----------------------------------------------------------------------- */ -/* ponytail: flat list of LABEL icons, no bevelled field borders, no "Web - * site" action button -- the four text rows are the whole dialogue. Add a - * button row through here if a task ever needs one. */ - -#define PROGINFO_MARGIN 4 /* px border around the row block */ -#define PROGINFO_GAP 4 /* px between the name column and the value column */ -#define PROGINFO_ROW_PAD 12 /* px added to the font height for the row pitch */ -#define PROGINFO_ROW_LEADING 4 /* px leading between rows */ -#define PROGINFO_FIELD_PAD 4 /* px each side of the text inside a value field */ - -/* The rows, in display order. Only fields set in the desc get a row. */ -static const char *const proginfo_labels[] = -{ - "Name", "Purpose", "Author", "Version" -}; - -/* The dialogue is one window on a task the caller passes in. It carries - * wuss_WINDOW_NO_CLOSE so a menu chain that borrows it as a - * wuss_menu_item_t::window can't have it pulled from under it, and - * wuss_proginfo_destroy closes it explicitly. The task must therefore not be - * an autoclose one -- its window list would never empty -- and must outlive - * the handle. */ -struct wuss_proginfo -{ - wuss_alloc_t alloc; /* copied hooks; the wuss_t itself is not retained */ - wuss_window_t *window; /* owned; closed by wuss_proginfo_destroy */ -}; - -/* ----------------------------------------------------------------------- */ +/* ponytail: this whole component is now the desc -> rows mapping below; + * wuss/component/info.c does the window, the measuring and the layout. */ result_t wuss_proginfo_create(wuss_proginfo_t **out, wuss_task_t *task, const wuss_proginfo_desc_t *desc) { - result_t rc; - const wuss_alloc_t *a; - wuss_t *wuss; - bmfont_t *font; - wuss_proginfo_t *pi; - const char *values[4]; - int nrows; - int fonth; - int rowh; - int namew; - int valuew; - int w; - int h; - box_t content; - wuss_icon_spec_t specs[8]; - int nspecs; - int i; + wuss_info_row_t rows[4]; + int nrows; if (out == NULL || task == NULL || desc == NULL || desc->name == NULL) return result_NULL_ARG; - wuss = task->wuss; - a = &wuss->alloc; - font = wuss_get_font(wuss); - - values[0] = desc->name; - values[1] = desc->purpose; - values[2] = desc->author; - values[3] = desc->version; - - /* Column widths: the widest name label on the left, the widest value on the - * right. bmfont_measure with no target width returns the whole string's - * width in actual_width. A NULL font leaves both at 0; the icons still lay - * out, just with nothing measured to size them. */ - nrows = 0; - namew = 0; - valuew = 0; - for (i = 0; i < 4; i++) + nrows = 0; + if (desc->name != NULL) { - bmfont_width_t lw; - bmfont_width_t vw; - - if (values[i] == NULL) - continue; - - lw = vw = 0; - if (font != NULL) - { - size_t vlen; - - vlen = strlen(values[i]); - bmfont_measure(font, proginfo_labels[i], (int) strlen(proginfo_labels[i]), - INT_MAX, NULL, &lw); - if (vlen > 0) - bmfont_measure(font, values[i], (int) vlen, INT_MAX, NULL, &vw); - } - namew = MAX(namew, (int) lw); - valuew = MAX(valuew, (int) vw); + rows[nrows].label = "Name"; + rows[nrows].value = desc->name; nrows++; } - - fonth = 0; - if (font != NULL) - bmfont_get_info(font, NULL, &fonth); - rowh = MAX(fonth + PROGINFO_ROW_PAD, 12) + PROGINFO_ROW_LEADING; - - namew = MAX(namew, 1); - valuew = MAX(valuew, 1) + PROGINFO_FIELD_PAD * 2; /* groove border + inset */ - - w = PROGINFO_MARGIN * 2 + namew + PROGINFO_GAP + valuew; - /* rowh is the pitch (field + leading); the last row has no trailing leading */ - h = PROGINFO_MARGIN * 2 + rowh * nrows - PROGINFO_ROW_LEADING; - - pi = a->malloc(sizeof(*pi)); - if (pi == NULL) - return result_OOM; - pi->alloc = *a; - pi->window = NULL; - a = &pi->alloc; /* use the copy from here on -- outlives the wuss_t */ - - content = (box_t) BOX_POS_SIZE(0, 0, w, h); - rc = wuss_window_create(task, - &content, - "About this program", - wuss_WINDOW_NO_BACK | wuss_WINDOW_NO_CLOSE | - wuss_WINDOW_NO_TOGGLE_SIZE | wuss_WINDOW_NO_VSCROLL | - wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE | - wuss_WINDOW_NO_REDRAW | wuss_WINDOW_HIDDEN, - wuss_BACKDROP_COLOUR(wuss_COLOUR_WINDOW), - SIZE2D(w, h), - SIZE2D(w, h), - &pi->window); - if (rc != result_OK) + if (desc->purpose != NULL) { - a->free(pi); - return rc; + rows[nrows].label = "Purpose"; + rows[nrows].value = desc->purpose; + nrows++; } - - /* One right-justified name label and one left value label per row. Zero the - * whole array so the icon-spec fields this component does not set (pattern, - * bitmap, group, swatch) are their safe defaults. */ - memset(specs, 0, sizeof(specs)); - nspecs = 0; - for (i = 0; i < 4; i++) + if (desc->author != NULL) { - int y; - int fieldh; - - if (values[i] == NULL) - continue; - - y = PROGINFO_MARGIN + rowh * (nspecs / 2); - fieldh = rowh - PROGINFO_ROW_LEADING; /* rowh is the pitch; leave a gap */ - - specs[nspecs].bbox = (box_t) BOX_POS_SIZE(PROGINFO_MARGIN, y, namew, fieldh); - specs[nspecs].type = wuss_ICON_TYPE_LABEL; - specs[nspecs].text = proginfo_labels[i]; - specs[nspecs].fg = wuss_COLOUR_BLACK; - specs[nspecs].bg = wuss_NO_BACKGROUND; - specs[nspecs].flags = wuss_ICON_FLAGS_JUSTIFY_RIGHT; - nspecs++; - - specs[nspecs].bbox = (box_t) BOX_POS_SIZE(PROGINFO_MARGIN + namew + - PROGINFO_GAP, y, valuew, fieldh); - specs[nspecs].type = wuss_ICON_TYPE_LABEL; - specs[nspecs].text = values[i]; - specs[nspecs].fg = wuss_COLOUR_BLACK; - specs[nspecs].bg = wuss_NO_BACKGROUND; - specs[nspecs].border = wuss_ICON_BORDER_GROOVE; /* RISC OS display field */ - specs[nspecs].flags = wuss_ICON_FLAGS_JUSTIFY_CENTRE; - nspecs++; + rows[nrows].label = "Author"; + rows[nrows].value = desc->author; + nrows++; } - - rc = wuss_icon_create_array(pi->window, specs, nspecs, NULL); - if (rc != result_OK) + if (desc->version != NULL) { - wuss_window_close(pi->window); /* frees the window and any icons on it */ - a->free(pi); - return rc; + rows[nrows].label = "Version"; + rows[nrows].value = desc->version; + nrows++; } - *out = pi; - return result_OK; -} - -void wuss_proginfo_destroy(wuss_proginfo_t *doomed) -{ - wuss_alloc_t alloc; - - if (doomed == NULL) - return; - - alloc = doomed->alloc; - wuss_window_close(doomed->window); /* frees the window and its icons */ - alloc.free(doomed); -} - -wuss_window_t *wuss_proginfo_window(const wuss_proginfo_t *pi) -{ - return pi ? pi->window : NULL; + return wuss_info_create(out, task, "About this program", rows, nrows); } From 0ff4ef177a2f4f7637017a5f993686b88bf18d36 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Mon, 7 Sep 2026 10:35:07 +0100 Subject: [PATCH 25/98] refactor(wuss): simplify wuss_info_create - info_widen helper folds the per-row strlen-guard + bmfont_measure pair into one call site each for label and value. - spec array moves to the stack (INFO_MAX_ROWS = 16 rows); drops the malloc, its OOM branch, the memset-with-cast and the matching free. nrows outside 1..INFO_MAX_ROWS is now result_BAD_ARG. - index specs[i*2] / [i*2+1] directly through named label/value pointers; nspecs, the alloc-copy alias and the llen/vlen locals go. Behaviour unchanged: wuss_icon_create deep-copies each spec and its text, and the font-absent path still yields rowh from MAX(0 + PAD, 12). Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/component/info.c | 156 +++++++++++++++----------------- 1 file changed, 72 insertions(+), 84 deletions(-) diff --git a/libraries/wuss/component/info.c b/libraries/wuss/component/info.c index bc048ef..c9c9257 100644 --- a/libraries/wuss/component/info.c +++ b/libraries/wuss/component/info.c @@ -26,13 +26,15 @@ /* ponytail: a flat grid of LABEL icons, two per row -- no action buttons, no * file-type sprite icon (the RISC OS Info dialogue's first field). Add either - * through here if a component ever needs one. */ + * through here if a component ever needs one. The spec array is on the stack, + * so the row count is capped; INFO_MAX_ROWS is well past any real dialogue. */ -#define INFO_MARGIN 4 /* px border around the row block */ -#define INFO_GAP 4 /* px between the label column and the value column */ -#define INFO_ROW_PAD 12 /* px added to the font height for the row pitch */ -#define INFO_ROW_LEADING 4 /* px leading between rows */ -#define INFO_FIELD_PAD 4 /* px each side of the text inside a value field */ +#define INFO_MARGIN 4 /* px border around the row block */ +#define INFO_GAP 4 /* px between the label column and the value column */ +#define INFO_ROW_PAD 12 /* px added to the font height for the row pitch */ +#define INFO_ROW_LEADING 4 /* px leading between rows */ +#define INFO_FIELD_PAD 4 /* px each side of the text inside a value field */ +#define INFO_MAX_ROWS 16 /* The dialogue is one window on a task the caller passes in. It carries * wuss_WINDOW_NO_CLOSE so a menu chain that borrows it as a @@ -48,64 +50,58 @@ struct wuss_info /* ----------------------------------------------------------------------- */ +/* Widest of "s" (whole string, no wrap) and "cur", in pixels; "cur" + * unchanged when the font or string is absent. bmfont_measure with no target + * width returns the whole string's width in its last out parameter. */ +static int info_widen(bmfont_t *font, const char *s, int cur) +{ + bmfont_width_t w; + + if (font == NULL || s == NULL || *s == '\0') + return cur; + + bmfont_measure(font, s, (int) strlen(s), INT_MAX, NULL, &w); + return MAX(cur, (int) w); +} + result_t wuss_info_create(wuss_info_t **out, wuss_task_t *task, const char *title, const wuss_info_row_t *rows, int nrows) { - result_t rc; - const wuss_alloc_t *a; - wuss_t *wuss; - bmfont_t *font; - wuss_info_t *info; - int fonth; - int rowh; - int fieldh; - int labelw; - int valuew; - int w; - int h; - box_t content; - wuss_icon_spec_t *specs; - int nspecs; - int i; + result_t rc; + wuss_t *wuss; + bmfont_t *font; + wuss_info_t *info; + int fonth; + int rowh; + int fieldh; + int labelw; + int valuew; + int w; + int h; + box_t content; + wuss_icon_spec_t specs[INFO_MAX_ROWS * 2]; + int i; if (out == NULL || task == NULL || title == NULL || rows == NULL) return result_NULL_ARG; - if (nrows < 1) + if (nrows < 1 || nrows > INFO_MAX_ROWS) return result_BAD_ARG; wuss = task->wuss; - a = &wuss->alloc; font = wuss_get_font(wuss); /* Column widths: the widest label on the left, the widest value on the - * right. bmfont_measure with no target width returns the whole string's - * width in actual_width. A NULL font leaves both at 0; the icons still lay - * out, just with nothing measured to size them. */ + * right. A NULL font leaves both at 0; the icons still lay out, just with + * nothing measured to size them. */ labelw = 0; valuew = 0; for (i = 0; i < nrows; i++) { - bmfont_width_t lw; - bmfont_width_t vw; - - lw = vw = 0; - if (font != NULL) - { - size_t llen; - size_t vlen; - - llen = rows[i].label ? strlen(rows[i].label) : 0; - vlen = rows[i].value ? strlen(rows[i].value) : 0; - if (llen > 0) - bmfont_measure(font, rows[i].label, (int) llen, INT_MAX, NULL, &lw); - if (vlen > 0) - bmfont_measure(font, rows[i].value, (int) vlen, INT_MAX, NULL, &vw); - } - labelw = MAX(labelw, (int) lw); - valuew = MAX(valuew, (int) vw); + labelw = info_widen(font, rows[i].label, labelw); + valuew = info_widen(font, rows[i].value, valuew); } fonth = 0; @@ -121,12 +117,11 @@ result_t wuss_info_create(wuss_info_t **out, /* rowh is the pitch (field + leading); the last row has no trailing leading */ h = INFO_MARGIN * 2 + rowh * nrows - INFO_ROW_LEADING; - info = a->malloc(sizeof(*info)); + info = wuss->alloc.malloc(sizeof(*info)); if (info == NULL) return result_OOM; - info->alloc = *a; + info->alloc = wuss->alloc; /* the copy outlives the wuss_t */ info->window = NULL; - a = &info->alloc; /* use the copy from here on -- outlives the wuss_t */ content = (box_t) BOX_POS_SIZE(0, 0, w, h); rc = wuss_window_create(task, @@ -142,54 +137,47 @@ result_t wuss_info_create(wuss_info_t **out, &info->window); if (rc != result_OK) { - a->free(info); + info->alloc.free(info); return rc; } /* One right-justified label and one centred value per row. Zeroed so the * icon-spec fields this component does not set (pattern, bitmap, group, - * swatch) are their safe defaults. */ - specs = a->malloc((size_t) nrows * 2 * sizeof(*specs)); - if (specs == NULL) - { - wuss_window_close(info->window); - a->free(info); - return result_OOM; - } - memset(specs, 0, (size_t) nrows * 2 * sizeof(*specs)); - - nspecs = 0; + * swatch) are their safe defaults. wuss_icon_create deep-copies each spec + * and its text, so this stack array can go out of scope after the call. */ + memset(specs, 0, sizeof(specs)); for (i = 0; i < nrows; i++) { - int y; - - y = INFO_MARGIN + rowh * i; - - specs[nspecs].bbox = (box_t) BOX_POS_SIZE(INFO_MARGIN, y, labelw, fieldh); - specs[nspecs].type = wuss_ICON_TYPE_LABEL; - specs[nspecs].text = rows[i].label; - specs[nspecs].fg = wuss_COLOUR_BLACK; - specs[nspecs].bg = wuss_NO_BACKGROUND; - specs[nspecs].flags = wuss_ICON_FLAGS_JUSTIFY_RIGHT; - nspecs++; - - specs[nspecs].bbox = (box_t) BOX_POS_SIZE(INFO_MARGIN + labelw + INFO_GAP, - y, valuew, fieldh); - specs[nspecs].type = wuss_ICON_TYPE_LABEL; - specs[nspecs].text = rows[i].value; - specs[nspecs].fg = wuss_COLOUR_BLACK; - specs[nspecs].bg = wuss_NO_BACKGROUND; - specs[nspecs].border = wuss_ICON_BORDER_GROOVE; /* RISC OS display field */ - specs[nspecs].flags = wuss_ICON_FLAGS_JUSTIFY_CENTRE; - nspecs++; + wuss_icon_spec_t *label; + wuss_icon_spec_t *value; + int y; + + label = &specs[i * 2]; + value = &specs[i * 2 + 1]; + y = INFO_MARGIN + rowh * i; + + label->bbox = (box_t) BOX_POS_SIZE(INFO_MARGIN, y, labelw, fieldh); + label->type = wuss_ICON_TYPE_LABEL; + label->text = rows[i].label; + label->fg = wuss_COLOUR_BLACK; + label->bg = wuss_NO_BACKGROUND; + label->flags = wuss_ICON_FLAGS_JUSTIFY_RIGHT; + + value->bbox = (box_t) BOX_POS_SIZE(INFO_MARGIN + labelw + INFO_GAP, + y, valuew, fieldh); + value->type = wuss_ICON_TYPE_LABEL; + value->text = rows[i].value; + value->fg = wuss_COLOUR_BLACK; + value->bg = wuss_NO_BACKGROUND; + value->border = wuss_ICON_BORDER_GROOVE; /* RISC OS display field */ + value->flags = wuss_ICON_FLAGS_JUSTIFY_CENTRE; } - rc = wuss_icon_create_array(info->window, specs, nspecs, NULL); - a->free(specs); + rc = wuss_icon_create_array(info->window, specs, nrows * 2, NULL); if (rc != result_OK) { wuss_window_close(info->window); /* frees the window and any icons on it */ - a->free(info); + info->alloc.free(info); return rc; } From 72ca1e9eeef534e71c83c43626b06ecb3f6f0ecf Mon Sep 17 00:00:00 2001 From: David Thomas Date: Mon, 7 Sep 2026 11:47:27 +0100 Subject: [PATCH 26/98] test(wuss): add GROOVE and RIDGE border example icons New "Borders" group in the icons task: a framed pair of LABEL icons, one wuss_ICON_BORDER_GROOVE (sunken display field) above one wuss_ICON_BORDER_RIDGE (raised). ICONS_N_BORDERS keeps the spec-count enum in step. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/test/tasks/icons.c | 44 ++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/libraries/wuss/test/tasks/icons.c b/libraries/wuss/test/tasks/icons.c index 149eb00..632cd11 100644 --- a/libraries/wuss/test/tasks/icons.c +++ b/libraries/wuss/test/tasks/icons.c @@ -38,9 +38,11 @@ enum ICONS_N_RADIOS = 8, /* frame + 3 radios + option + state label + 2 justified labels */ ICONS_N_BITMAPS = 3, /* frame + decorative + interactive bitmap */ ICONS_N_PATTERN = 2, /* frame + one PATTERN swatch */ + ICONS_N_BORDERS = 3, /* frame + a GROOVE label + a RIDGE label */ ICONS_N_MENU = 7, /* plain, ticked, swatch, submenu, disabled, rule, separator entry */ ICONS_NSPECS = ICONS_N_INTRO + ICONS_N_BUTTONS + ICONS_N_RADIOS + - ICONS_N_BITMAPS + ICONS_N_PATTERN + ICONS_N_MENU + ICONS_N_BITMAPS + ICONS_N_PATTERN + ICONS_N_BORDERS + + ICONS_N_MENU }; /* Running state threaded through the icons_add_* helpers: where to write the @@ -290,6 +292,45 @@ static void icons_add_pattern(icons_layout_t *lay) lay->y = top + 76; } +/* A grouping frame captioned "Borders", holding a GROOVE-bordered label (a + * sunken RISC OS display field) above a RIDGE-bordered one (raised). */ +static void icons_add_borders(icons_layout_t *lay) +{ + wuss_icon_spec_t *s; + int top; + + top = lay->y; + s = &lay->specs[lay->n]; + s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN, top, 200, 80); + s->type = wuss_ICON_TYPE_FRAME; + s->text = "Borders"; + s->fg = lay->black; + s->bg = wuss_NO_BACKGROUND; + lay->n++; + + s = &lay->specs[lay->n]; + s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN + 10, top + 20, 180, 22); + s->type = wuss_ICON_TYPE_LABEL; + s->text = "Groove"; + s->fg = lay->black; + s->bg = lay->grey6; + s->border = wuss_ICON_BORDER_GROOVE; + s->flags = wuss_ICON_FLAGS_JUSTIFY_CENTRE; + lay->n++; + + s = &lay->specs[lay->n]; + s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN + 10, top + 48, 180, 22); + s->type = wuss_ICON_TYPE_LABEL; + s->text = "Ridge"; + s->fg = lay->black; + s->bg = lay->grey6; + s->border = wuss_ICON_BORDER_RIDGE; + s->flags = wuss_ICON_FLAGS_JUSTIFY_CENTRE; + lay->n++; + + lay->y = top + 96; +} + /* A menu-entry strip: plain, ticked, a swatch entry, a submenu entry, a * disabled entry, then a dashed rule and a SEPARATOR-flagged entry below it. * Hover the pointer over any live entry to see the highlight track; the rule @@ -445,6 +486,7 @@ result_t icons_create(wuss_t *wuss, icons_add_radios(&lay, &i_opt, &i_state); icons_add_bitmaps(&lay, task->has_sprite ? &task->sprite : NULL, &i_hotspot); icons_add_pattern(&lay); + icons_add_borders(&lay); icons_add_menu(&lay, &i_ticked); rc = wuss_icon_create_array(task->window, specs, lay.n, made); From 152c83036aab4c9ff050691781555d23b63eff5c Mon Sep 17 00:00:00 2001 From: David Thomas Date: Mon, 7 Sep 2026 11:48:12 +0100 Subject: [PATCH 27/98] feat(wuss): paint the icons demo from the standard window colour The icons task hard-coded 0xBB/0xDD greys for its icon backgrounds and the crosshatch backdrop. Swap them for the wuss_COLOUR_WINDOW / wuss_COLOUR_GREY chrome roles so the demo tracks the configured colour scheme; keep the crosshatch texture by hatching GREY over WINDOW. task->paper stays a literal -- no public call resolves a symbolic wuss_colour_t to a concrete colour_t for the ruler glyph blend. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/test/tasks/icons.c | 39 +++++++++++++++++-------------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/libraries/wuss/test/tasks/icons.c b/libraries/wuss/test/tasks/icons.c index 632cd11..be07c42 100644 --- a/libraries/wuss/test/tasks/icons.c +++ b/libraries/wuss/test/tasks/icons.c @@ -53,9 +53,8 @@ typedef struct icons_layout int n; /* specs[0..n) are filled in */ int y; /* document y of the next group */ wuss_colour_t black; - wuss_colour_t grey5; - wuss_colour_t grey6; - wuss_colour_t red; /* menu swatch demo colour */ + wuss_colour_t window; /* wuss_COLOUR_WINDOW: the standard work-area fill */ + wuss_colour_t red; /* menu swatch demo colour */ } icons_layout_t; @@ -85,7 +84,7 @@ static void icons_add_intro(icons_layout_t *lay, int *button, int *counter) s->type = wuss_ICON_TYPE_BUTTON; s->text = "Press me"; s->fg = lay->black; - s->bg = lay->grey6; + s->bg = lay->window; s->flags = wuss_ICON_FLAGS_DEFAULT; lay->n++; @@ -104,7 +103,7 @@ static void icons_add_intro(icons_layout_t *lay, int *button, int *counter) s->type = wuss_ICON_TYPE_BUTTON; s->text = "Scrolled"; s->fg = lay->black; - s->bg = lay->grey6; + s->bg = lay->window; lay->n++; } @@ -129,7 +128,7 @@ static void icons_add_buttons(icons_layout_t *lay) s->type = wuss_ICON_TYPE_BUTTON; s->text = "Normal"; s->fg = lay->black; - s->bg = lay->grey6; + s->bg = lay->window; lay->n++; s = &lay->specs[lay->n]; @@ -137,7 +136,7 @@ static void icons_add_buttons(icons_layout_t *lay) s->type = wuss_ICON_TYPE_BUTTON; s->text = "Default"; s->fg = lay->black; - s->bg = lay->grey6; + s->bg = lay->window; s->flags = wuss_ICON_FLAGS_DEFAULT; lay->n++; @@ -146,7 +145,7 @@ static void icons_add_buttons(icons_layout_t *lay) s->type = wuss_ICON_TYPE_BUTTON; s->text = "Disabled"; s->fg = lay->black; - s->bg = lay->grey6; + s->bg = lay->window; s->flags = wuss_ICON_FLAGS_DISABLED; lay->n++; @@ -285,7 +284,7 @@ static void icons_add_pattern(icons_layout_t *lay) s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN + 10, top + 20, 180, 26); s->type = wuss_ICON_TYPE_PATTERN; s->fg = lay->black; - s->bg = lay->grey6; + s->bg = lay->window; s->pattern = screen_PATTERN_DIAGONAL; lay->n++; @@ -313,7 +312,7 @@ static void icons_add_borders(icons_layout_t *lay) s->type = wuss_ICON_TYPE_LABEL; s->text = "Groove"; s->fg = lay->black; - s->bg = lay->grey6; + s->bg = lay->window; s->border = wuss_ICON_BORDER_GROOVE; s->flags = wuss_ICON_FLAGS_JUSTIFY_CENTRE; lay->n++; @@ -323,7 +322,7 @@ static void icons_add_borders(icons_layout_t *lay) s->type = wuss_ICON_TYPE_LABEL; s->text = "Ridge"; s->fg = lay->black; - s->bg = lay->grey6; + s->bg = lay->window; s->border = wuss_ICON_BORDER_RIDGE; s->flags = wuss_ICON_FLAGS_JUSTIFY_CENTRE; lay->n++; @@ -423,7 +422,10 @@ result_t icons_create(wuss_t *wuss, task->font = font; task->label = colour_rgb(0x00, 0x00, 0x00); - task->paper = colour_rgb(0xDD, 0xDD, 0xDD); /* the window bg, below */ + /* only the ruler-text glyph blend; approximates the wuss_COLOUR_WINDOW / + * wuss_COLOUR_BACKDROP crosshatch the rulers sit on -- no public call + * resolves a symbolic wuss_colour_t to a concrete colour_t here */ + task->paper = colour_rgb(0xDD, 0xDD, 0xDD); task->window = NULL; task->button = NULL; task->counter = NULL; @@ -451,18 +453,19 @@ result_t icons_create(wuss_t *wuss, } memset(&lay, 0, sizeof(lay)); - lay.black = wuss_nearest_colour(wuss, 0x00, 0x00, 0x00); - lay.grey5 = wuss_nearest_colour(wuss, 0xBB, 0xBB, 0xBB); - lay.grey6 = wuss_nearest_colour(wuss, 0xDD, 0xDD, 0xDD); - lay.red = wuss_nearest_colour(wuss, 0xCC, 0x33, 0x33); + lay.black = wuss_nearest_colour(wuss, 0x00, 0x00, 0x00); + lay.window = wuss_COLOUR_WINDOW; /* the standard work-area fill */ + lay.red = wuss_nearest_colour(wuss, 0xCC, 0x33, 0x33); + /* crosshatch the standard window colour over the standard backdrop colour, + * so the texture tracks the chrome config rather than fixed greys */ rc = wuss_window_create_placed(delegate, SIZE2D(ICONS_DOC_W, 200), "Icons", wuss_WINDOW_NONE, - wuss_BACKDROP_PATTERN(lay.grey5, + wuss_BACKDROP_PATTERN(wuss_COLOUR_GREY, screen_PATTERN_CROSSHATCH, - lay.grey6), + wuss_COLOUR_WINDOW), SIZE2D(ICONS_DOC_W, ICONS_DOC_H), SIZE2D(0, 0), &task->window); From 3fbc27c52f8c8fbe29a736631038727a2b5d4dd7 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Mon, 7 Sep 2026 11:49:42 +0100 Subject: [PATCH 28/98] test(wuss): use wuss_COLOUR_BLACK instead of wuss_nearest_colour lay.black is exactly black, so the wuss_COLOUR_BLACK symbol serves. lay.red stays on wuss_nearest_colour -- 0xCC3333 is an off-primary demo tone with no matching wuss_COLOUR_* symbol. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/test/tasks/icons.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/wuss/test/tasks/icons.c b/libraries/wuss/test/tasks/icons.c index be07c42..10c31a2 100644 --- a/libraries/wuss/test/tasks/icons.c +++ b/libraries/wuss/test/tasks/icons.c @@ -453,9 +453,9 @@ result_t icons_create(wuss_t *wuss, } memset(&lay, 0, sizeof(lay)); - lay.black = wuss_nearest_colour(wuss, 0x00, 0x00, 0x00); + lay.black = wuss_COLOUR_BLACK; lay.window = wuss_COLOUR_WINDOW; /* the standard work-area fill */ - lay.red = wuss_nearest_colour(wuss, 0xCC, 0x33, 0x33); + lay.red = wuss_nearest_colour(wuss, 0xCC, 0x33, 0x33); /* off-primary; no symbol */ /* crosshatch the standard window colour over the standard backdrop colour, * so the texture tracks the chrome config rather than fixed greys */ From 97afa07cce5b25d0302b7a925b552fbe446fcffa Mon Sep 17 00:00:00 2001 From: David Thomas Date: Mon, 7 Sep 2026 13:30:02 +0100 Subject: [PATCH 29/98] feat(wuss): add wuss_ICON_BORDER_ACTION and a bevel-edge primitive New label border style: a 6px-per-edge "action" surround -- a 2px raised outset, a 2px moat in wuss_COLOUR_ACCENT_BG (the default-action-button fill), then a 2px raised inset -- for a RISC OS default-action look. wuss__icon_from_spec accepts it; the icons demo grows an "Action" example alongside Groove and Ridge. Factor the shared edge draw into screen_draw_bevel_edge(scr, box, a, b): one 2px bevel ring inside an inclusive-exclusive box, colour a top/left, b bottom/right, the bottom edge owning both corners so the ring closes cleanly. GROOVE/RIDGE draw one ring; ACTION nests three on boxes inset 0/2/4. Co-Authored-By: Claude Sonnet 5 --- include/framebuf/screen.h | 21 +++++++++++ include/wuss/icon.h | 6 ++- libraries/framebuf/screen/screen-draw.c | 32 ++++++++++++++++ libraries/wuss/icon/draw.c | 50 +++++++++++++------------ libraries/wuss/icon/from-spec.c | 2 +- libraries/wuss/test/tasks/icons.c | 22 ++++++++--- 6 files changed, 103 insertions(+), 30 deletions(-) diff --git a/include/framebuf/screen.h b/include/framebuf/screen.h index aa8087f..01c7d87 100644 --- a/include/framebuf/screen.h +++ b/include/framebuf/screen.h @@ -279,6 +279,27 @@ void screen_draw_rect(screen_t *scr, size2d_t size, colour_t colour); +/** + * Draws one 2px-wide bevel ring just inside `box`: colour `a` along the top + * and left edges, colour `b` along the bottom and right. The bottom edge + * owns both bottom corners so the ring closes with no gap or overlap. `box` + * is inclusive-exclusive, as elsewhere. Swap `a` and `b` for a sunken rather + * than a raised look; pass the same colour for both for a plain 2px frame. + * Nest calls on successively inset boxes to build a thicker surround. + * + * Assumes `box` is at least 4x4; smaller boxes lose their edge shape but + * draw harmlessly. Clipped to the screen's clip region. + * + * \param[in] scr Screen to draw upon. + * \param[in] box Box whose inner 2px border is drawn. + * \param[in] a Colour of the top and left edges. + * \param[in] b Colour of the bottom and right edges. + */ +void screen_draw_bevel_edge(screen_t *scr, + const box_t *box, + colour_t a, + colour_t b); + /** * Draws a one-pixel unfilled circle outline (integer midpoint algorithm, no * anti-aliasing). Clipped to the screen's clip region. A negative radius diff --git a/include/wuss/icon.h b/include/wuss/icon.h index 2999d0a..18f3a0f 100644 --- a/include/wuss/icon.h +++ b/include/wuss/icon.h @@ -119,7 +119,11 @@ typedef enum wuss_icon_border wuss_ICON_BORDER_RIDGE, /** A 1px sunken bevel: dark top/left, light bottom/right -- a RISC OS-style * read-only display field. */ - wuss_ICON_BORDER_GROOVE + wuss_ICON_BORDER_GROOVE, + /** A 6px-per-edge "action" surround: a 2px raised outset, a 2px moat filled + * with wuss_COLOUR_ACCENT_BG, then a 2px raised inset -- like a RISC OS + * default-action button. */ + wuss_ICON_BORDER_ACTION } wuss_icon_border_t; diff --git a/libraries/framebuf/screen/screen-draw.c b/libraries/framebuf/screen/screen-draw.c index af8548a..093b011 100644 --- a/libraries/framebuf/screen/screen-draw.c +++ b/libraries/framebuf/screen/screen-draw.c @@ -480,6 +480,38 @@ void screen_draw_rect(screen_t *scr, screen_draw_lines(scr, p, 5, colour); } +void screen_draw_bevel_edge(screen_t *scr, + const box_t *box, + colour_t a, + colour_t b) +{ + int x0, y0, x1, y1; + int w, h; + + x0 = box->x0; + y0 = box->y0; + x1 = box->x1; + y1 = box->y1; + w = x1 - x0; + h = y1 - y0; + + /* Colour A: a 2px top edge, then a 2px left edge below it. The top runs + * full width bar the top-right 2px, which colour B's right edge claims. */ + screen_fill_rect(scr, x0, y0, SIZE2D(w - 2, 2), a); + screen_fill_rect(scr, x0, y0 + 2, SIZE2D(2, h - 3), a); + + /* Colour B: a 2px right edge, then a 2px bottom edge. The right runs from + * the top down to the bottom edge; the bottom runs full width, so it owns + * both bottom corners (A's left edge stops just above it). */ + screen_fill_rect(scr, x1 - 2, y0 + 1, SIZE2D(2, h - 1), b); + screen_fill_rect(scr, x0 + 2, y1 - 2, SIZE2D(w - 2, 2), b); + + screen_set_pixel(scr, x0, y1 - 1, a); + screen_set_pixel(scr, x0 + 1, y1 - 1, b); + screen_set_pixel(scr, x1 - 2, y0, a); + screen_set_pixel(scr, x1 - 1, y0, b); +} + void screen_draw_dashed_line(screen_t *scr, int x0, int y0, diff --git a/libraries/wuss/icon/draw.c b/libraries/wuss/icon/draw.c index a261c1b..f8bc297 100644 --- a/libraries/wuss/icon/draw.c +++ b/libraries/wuss/icon/draw.c @@ -147,30 +147,34 @@ static void wuss__icon_draw_label(const icon_draw_ctx_t *c) if (icon->border != wuss_ICON_BORDER_NONE) { - colour_t light, dark, hi, lo; - int ridge; - int d; - point_t tl[3]; /* top + left edges */ - point_t br[3]; /* bottom + right edges */ - - light = c->wuss->palette[c->wuss->bevel_light]; - dark = c->wuss->palette[c->wuss->bevel_dark]; - ridge = (icon->border == wuss_ICON_BORDER_RIDGE); - hi = ridge ? light : dark; - lo = ridge ? dark : light; - - /* two-pixel bevel: the outer L/R pair, then the same pair inset by one */ - for (d = 0; d < 2; d++) + colour_t light, dark, accent, hi, lo; + int raised; + box_t ring; + + light = c->wuss->palette[c->wuss->bevel_light]; + dark = c->wuss->palette[c->wuss->bevel_dark]; + accent = c->wuss->palette[c->wuss->accent_bg]; /* the action-button fill */ + + /* RIDGE reads raised (light top/left); GROOVE reads sunken; ACTION uses + * both. */ + raised = (icon->border == wuss_ICON_BORDER_RIDGE); + hi = raised ? light : dark; + lo = raised ? dark : light; + + /* GROOVE/RIDGE: one 2px bevel ring. ACTION: 6px per edge as three nested + * 2px rings -- a raised outset, an accent moat, then a raised inset. */ + ring = *b; + screen_draw_bevel_edge(c->scr, &ring, hi, lo); + + if (icon->border == wuss_ICON_BORDER_ACTION) { - tl[0] = POINT(b->x1 - 1 - d, b->y0 + d); - tl[1] = POINT(b->x0 + d, b->y0 + d); - tl[2] = POINT(b->x0 + d, b->y1 - 1 - d); - br[0] = POINT(b->x0 + d, b->y1 - 1 - d); - br[1] = POINT(b->x1 - 1 - d, b->y1 - 1 - d); - br[2] = POINT(b->x1 - 1 - d, b->y0 + d); - - screen_draw_lines(c->scr, tl, 3, hi); - screen_draw_lines(c->scr, br, 3, lo); + ring = (box_t) BOX_POS_SIZE(b->x0 + 2, b->y0 + 2, + b->x1 - b->x0 - 4, b->y1 - b->y0 - 4); + screen_draw_bevel_edge(c->scr, &ring, accent, accent); + + ring = (box_t) BOX_POS_SIZE(b->x0 + 4, b->y0 + 4, + b->x1 - b->x0 - 8, b->y1 - b->y0 - 8); + screen_draw_bevel_edge(c->scr, &ring, lo, hi); /* swap for inner */ } } diff --git a/libraries/wuss/icon/from-spec.c b/libraries/wuss/icon/from-spec.c index 1ef4fc5..bf68f70 100644 --- a/libraries/wuss/icon/from-spec.c +++ b/libraries/wuss/icon/from-spec.c @@ -57,7 +57,7 @@ result_t wuss__icon_from_spec(const wuss_t *w, return result_WUSS_BAD_ICON; if (spec->type == wuss_ICON_TYPE_LABEL && - spec->border > wuss_ICON_BORDER_GROOVE) + spec->border > wuss_ICON_BORDER_ACTION) return result_WUSS_BAD_ICON; if (fg >= w->npalette) diff --git a/libraries/wuss/test/tasks/icons.c b/libraries/wuss/test/tasks/icons.c index 10c31a2..9a48836 100644 --- a/libraries/wuss/test/tasks/icons.c +++ b/libraries/wuss/test/tasks/icons.c @@ -38,7 +38,7 @@ enum ICONS_N_RADIOS = 8, /* frame + 3 radios + option + state label + 2 justified labels */ ICONS_N_BITMAPS = 3, /* frame + decorative + interactive bitmap */ ICONS_N_PATTERN = 2, /* frame + one PATTERN swatch */ - ICONS_N_BORDERS = 3, /* frame + a GROOVE label + a RIDGE label */ + ICONS_N_BORDERS = 4, /* frame + GROOVE + RIDGE + ACTION labels */ ICONS_N_MENU = 7, /* plain, ticked, swatch, submenu, disabled, rule, separator entry */ ICONS_NSPECS = ICONS_N_INTRO + ICONS_N_BUTTONS + ICONS_N_RADIOS + ICONS_N_BITMAPS + ICONS_N_PATTERN + ICONS_N_BORDERS + @@ -292,7 +292,9 @@ static void icons_add_pattern(icons_layout_t *lay) } /* A grouping frame captioned "Borders", holding a GROOVE-bordered label (a - * sunken RISC OS display field) above a RIDGE-bordered one (raised). */ + * sunken RISC OS display field), a RIDGE-bordered one (raised), then an + * ACTION-bordered one -- a 6px surround: raised outset, accent moat, raised + * inset, like a default-action button. */ static void icons_add_borders(icons_layout_t *lay) { wuss_icon_spec_t *s; @@ -300,7 +302,7 @@ static void icons_add_borders(icons_layout_t *lay) top = lay->y; s = &lay->specs[lay->n]; - s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN, top, 200, 80); + s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN, top, 200, 116); s->type = wuss_ICON_TYPE_FRAME; s->text = "Borders"; s->fg = lay->black; @@ -318,7 +320,7 @@ static void icons_add_borders(icons_layout_t *lay) lay->n++; s = &lay->specs[lay->n]; - s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN + 10, top + 48, 180, 22); + s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN + 10, top + 46, 180, 22); s->type = wuss_ICON_TYPE_LABEL; s->text = "Ridge"; s->fg = lay->black; @@ -327,7 +329,17 @@ static void icons_add_borders(icons_layout_t *lay) s->flags = wuss_ICON_FLAGS_JUSTIFY_CENTRE; lay->n++; - lay->y = top + 96; + s = &lay->specs[lay->n]; + s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN + 10, top + 72, 180, 34); + s->type = wuss_ICON_TYPE_LABEL; + s->text = "Action"; + s->fg = lay->black; + s->bg = lay->window; + s->border = wuss_ICON_BORDER_ACTION; + s->flags = wuss_ICON_FLAGS_JUSTIFY_CENTRE; + lay->n++; + + lay->y = top + 132; } /* A menu-entry strip: plain, ticked, a swatch entry, a submenu entry, a From 35e07d0c337dc6af65b4529c3747ad2d07fc6467 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Mon, 7 Sep 2026 13:30:34 +0100 Subject: [PATCH 30/98] style(wuss): one palette constant per line in g_chrome The two g_chrome rows were wrapped four-per-line, so matching a colour to its config field (fill_chrome_config assigns c[0]..c[16] in order) meant counting across. One constant per line, a blank between the palettes. Co-Authored-By: Claude Sonnet 5 --- apps/wuss/main.c | 51 ++++++++++++++++++++++++++++++++++++------------ 1 file changed, 39 insertions(+), 12 deletions(-) diff --git a/apps/wuss/main.c b/apps/wuss/main.c index 2b38382..b9a1d06 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -38,19 +38,46 @@ static const wuss_colour_t g_chrome[2][17] = { /* PICO-8 */ - { palette_PICO8_DARK_BLUE, palette_PICO8_WHITE, palette_PICO8_GREEN, - palette_PICO8_RED, palette_PICO8_ORANGE, palette_PICO8_LAVENDER, - palette_PICO8_BLUE, palette_PICO8_DARK_BLUE, palette_PICO8_LIGHT_GREY, - palette_PICO8_WHITE, palette_PICO8_DARK_GREY, palette_PICO8_DARK_BLUE, - palette_PICO8_WHITE, palette_PICO8_WHITE, palette_PICO8_LIGHT_GREY, - palette_PICO8_LIGHT_GREY, palette_PICO8_WHITE }, + { + palette_PICO8_DARK_BLUE, + palette_PICO8_WHITE, + palette_PICO8_GREEN, + palette_PICO8_RED, + palette_PICO8_ORANGE, + palette_PICO8_LAVENDER, + palette_PICO8_BLUE, + palette_PICO8_DARK_BLUE, + palette_PICO8_LIGHT_GREY, + palette_PICO8_WHITE, + palette_PICO8_DARK_GREY, + palette_PICO8_DARK_BLUE, + palette_PICO8_WHITE, + palette_PICO8_WHITE, + palette_PICO8_LIGHT_GREY, + palette_PICO8_LIGHT_GREY, + palette_PICO8_WHITE + }, + /* RISC OS 16-colour Wimp */ - { palette_WIMP16_GREY_75, palette_WIMP16_BLACK, palette_WIMP16_GREEN, - palette_WIMP16_RED, palette_WIMP16_ORANGE, palette_WIMP16_LIGHT_BLUE, - palette_WIMP16_GREY_50, palette_WIMP16_GREY_62, palette_WIMP16_GREY_87, - palette_WIMP16_WHITE, palette_WIMP16_GREY_50, palette_WIMP16_ORANGE, - palette_WIMP16_BLACK, palette_WIMP16_GREY_50, palette_WIMP16_GREY_37, - palette_WIMP16_GREY_75, palette_WIMP16_WHITE } + { + palette_WIMP16_GREY_75, + palette_WIMP16_BLACK, + palette_WIMP16_GREEN, + palette_WIMP16_RED, + palette_WIMP16_ORANGE, + palette_WIMP16_LIGHT_BLUE, + palette_WIMP16_GREY_50, + palette_WIMP16_GREY_62, + palette_WIMP16_GREY_87, + palette_WIMP16_WHITE, + palette_WIMP16_GREY_50, + palette_WIMP16_ORANGE, + palette_WIMP16_BLACK, + palette_WIMP16_GREY_50, + palette_WIMP16_GREY_37, + palette_WIMP16_GREY_87, + palette_WIMP16_WHITE + } }; static void fill_chrome_config(wuss_config_t *config, int palette_index) From e98fa6dd1713a4fedd7e9af9fba7381a1afc0573 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Mon, 7 Sep 2026 13:58:12 +0100 Subject: [PATCH 31/98] refactor(wuss): draw button bevels through screen_draw_bevel_edge icon_bevel dropped its four screen_draw_line calls for one screen_draw_bevel_edge, so work-area buttons and the default-action button now carry a 2px bevel to match the ACTION icon border. screen_draw_bevel_edge gains an ASCII sketch of its pixel layout and a note that it draws only the ring. Co-Authored-By: Claude Sonnet 5 --- include/framebuf/screen.h | 8 ++++--- libraries/framebuf/screen/screen-draw.c | 30 +++++++++++++++++-------- libraries/wuss/icon/draw.c | 8 ++----- 3 files changed, 28 insertions(+), 18 deletions(-) diff --git a/include/framebuf/screen.h b/include/framebuf/screen.h index 01c7d87..94d6c87 100644 --- a/include/framebuf/screen.h +++ b/include/framebuf/screen.h @@ -282,10 +282,12 @@ void screen_draw_rect(screen_t *scr, /** * Draws one 2px-wide bevel ring just inside `box`: colour `a` along the top * and left edges, colour `b` along the bottom and right. The bottom edge - * owns both bottom corners so the ring closes with no gap or overlap. `box` - * is inclusive-exclusive, as elsewhere. Swap `a` and `b` for a sunken rather + * owns both bottom corners so the ring closes with no gap or overlap. Only + * the ring is drawn -- fill the interior first if you want one. `box` is + * inclusive-exclusive, as elsewhere. Swap `a` and `b` for a sunken rather * than a raised look; pass the same colour for both for a plain 2px frame. - * Nest calls on successively inset boxes to build a thicker surround. + * Nest calls on successively inset boxes to build a thicker surround (as the + * wuss action-button border does). * * Assumes `box` is at least 4x4; smaller boxes lose their edge shape but * draw harmlessly. Clipped to the screen's clip region. diff --git a/libraries/framebuf/screen/screen-draw.c b/libraries/framebuf/screen/screen-draw.c index 093b011..da1f5d4 100644 --- a/libraries/framebuf/screen/screen-draw.c +++ b/libraries/framebuf/screen/screen-draw.c @@ -495,20 +495,32 @@ void screen_draw_bevel_edge(screen_t *scr, w = x1 - x0; h = y1 - y0; - /* Colour A: a 2px top edge, then a 2px left edge below it. The top runs - * full width bar the top-right 2px, which colour B's right edge claims. */ + /* For a 5x5 box draw like so using colours A and B: + * + * AAAAB + * AAABB + * AA BB + * AABBB + * ABBBB + * + * Using rects for the bulk of the filling, then single pixel fixups in the + * following order: + * + * 00037 + * 00044 + * 11 44 + * 11555 + * 26555 + */ + screen_fill_rect(scr, x0, y0, SIZE2D(w - 2, 2), a); screen_fill_rect(scr, x0, y0 + 2, SIZE2D(2, h - 3), a); - - /* Colour B: a 2px right edge, then a 2px bottom edge. The right runs from - * the top down to the bottom edge; the bottom runs full width, so it owns - * both bottom corners (A's left edge stops just above it). */ + screen_set_pixel(scr, x0, y1 - 1, a); + screen_set_pixel(scr, x1 - 2, y0, a); + screen_fill_rect(scr, x1 - 2, y0 + 1, SIZE2D(2, h - 1), b); screen_fill_rect(scr, x0 + 2, y1 - 2, SIZE2D(w - 2, 2), b); - - screen_set_pixel(scr, x0, y1 - 1, a); screen_set_pixel(scr, x0 + 1, y1 - 1, b); - screen_set_pixel(scr, x1 - 2, y0, a); screen_set_pixel(scr, x1 - 1, y0, b); } diff --git a/libraries/wuss/icon/draw.c b/libraries/wuss/icon/draw.c index f8bc297..9496fd1 100644 --- a/libraries/wuss/icon/draw.c +++ b/libraries/wuss/icon/draw.c @@ -42,11 +42,7 @@ static void icon_bevel(screen_t *scr, { screen_fill_rect(scr, b->x0, b->y0, SIZE2D(b->x1 - b->x0, b->y1 - b->y0), fill); - - screen_draw_line(scr, b->x0, b->y0, b->x1 - 1, b->y0, light); - screen_draw_line(scr, b->x0, b->y0, b->x0, b->y1 - 1, light); - screen_draw_line(scr, b->x0, b->y1 - 1, b->x1 - 1, b->y1 - 1, dark); - screen_draw_line(scr, b->x1 - 1, b->y0, b->x1 - 1, b->y1 - 1, dark); + screen_draw_bevel_edge(scr, b, light, dark); } /* Resolve the ground an icon's text/glyph blends against: an explicit icon bg, @@ -265,7 +261,7 @@ static void wuss__icon_draw_button(const icon_draw_ctx_t *c) if (is_default) { /* default action button: a flat accent-filled rectangle inside a - * one-pixel accent-text border, distinct from the bevelled ordinary + * two-pixel accent-text border, distinct from the bevelled ordinary * buttons around it */ base = c->wuss->palette[c->wuss->accent_bg]; light = c->wuss->palette[c->wuss->accent_fg]; From 87d4b4d8a1999cd59666a4ae15034662c82aff9a Mon Sep 17 00:00:00 2001 From: David Thomas Date: Mon, 7 Sep 2026 14:20:53 +0100 Subject: [PATCH 32/98] fix(wuss): nudge centred icon label down 1px The vertically centred label sat one pixel high because the leftover vertical space is halved with integer division, which rounds down. Add 1px to pos.y to compensate. bmfont_draw itself positions text by the top-left of the char cell, not a baseline. --- libraries/wuss/icon/draw.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/libraries/wuss/icon/draw.c b/libraries/wuss/icon/draw.c index 9496fd1..f8e5f35 100644 --- a/libraries/wuss/icon/draw.c +++ b/libraries/wuss/icon/draw.c @@ -188,7 +188,10 @@ static void wuss__icon_draw_label(const icon_draw_ctx_t *c) pos.x = b->x1 - 1 - width; else pos.x = b->x0 + 1; - pos.y = b->y0 + (b->y1 - b->y0 - c->font_height) / 2; + + /* +1 so the centred label sits visually level; integer division of the + * leftover vertical space rounds it one pixel high otherwise */ + pos.y = b->y0 + (b->y1 - b->y0 - c->font_height) / 2 + 1; bmfont_draw(c->font, c->scr, icon->text, (int) strlen(icon->text), c->fg, bg, &pos, NULL); From 6fbc78fb43353da21a9fbba3d5c445a6afa3c647 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Mon, 7 Sep 2026 14:39:03 +0100 Subject: [PATCH 33/98] fix(wuss): ignore mouse clicks outside the image window image_handle is shared with the proginfo dialogue window. A SELECT or ADJUST click on that dialogue cycled the image; a MENU click reopened the menu against the wrong window. Gate the MOUSE case on window == ic->window. --- libraries/wuss/test/tasks/image.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libraries/wuss/test/tasks/image.c b/libraries/wuss/test/tasks/image.c index 73e0c12..8d23986 100644 --- a/libraries/wuss/test/tasks/image.c +++ b/libraries/wuss/test/tasks/image.c @@ -275,6 +275,8 @@ result_t image_handle(wuss_window_t *window, return image_redraw(event, task_data); case wuss_EVENT_MOUSE: + if (window != ic->window) + return result_OK; /* not the image window (e.g. the proginfo dialogue) */ if (event->data.mouse.action != wuss_MOUSE_DOWN) return result_OK; if (event->data.mouse.button & wuss_BUTTON_SELECT) From cf9c01a3b5842a5afd3c873c5e8bbb9c3a0bd878 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Mon, 7 Sep 2026 14:59:52 +0100 Subject: [PATCH 34/98] feat(wuss): add a dedicated window outline colour The window outline was hardwired to the titlebar fill. Add wuss_furniture_palette_t.outline (wuss_NO_BACKGROUND = follow the titlebar fill, as before) so it can be set independently. The demo's RISC-OS Wimp chrome now outlines windows in black; the PICO-8 row keeps the old dark-blue. --- apps/wuss/main.c | 37 ++++++++++++++++++--------------- include/wuss/wuss.h | 2 ++ libraries/wuss/core/create.c | 5 +++++ libraries/wuss/furniture/draw.c | 2 +- libraries/wuss/test/wuss-test.c | 1 + 5 files changed, 29 insertions(+), 18 deletions(-) diff --git a/apps/wuss/main.c b/apps/wuss/main.c index b9a1d06..e4dfd37 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -35,12 +35,13 @@ /* Furniture/bevel/accent/backdrop/body colour indices, one row per palette. * Same field order as the assignments in fill_chrome_config. */ -static const wuss_colour_t g_chrome[2][17] = +static const wuss_colour_t g_chrome[2][18] = { /* PICO-8 */ { palette_PICO8_DARK_BLUE, palette_PICO8_WHITE, + palette_PICO8_DARK_BLUE, palette_PICO8_GREEN, palette_PICO8_RED, palette_PICO8_ORANGE, @@ -57,11 +58,12 @@ static const wuss_colour_t g_chrome[2][17] = palette_PICO8_LIGHT_GREY, palette_PICO8_WHITE }, - + /* RISC OS 16-colour Wimp */ { palette_WIMP16_GREY_75, palette_WIMP16_BLACK, + palette_WIMP16_BLACK, palette_WIMP16_GREEN, palette_WIMP16_RED, palette_WIMP16_ORANGE, @@ -87,22 +89,23 @@ static void fill_chrome_config(wuss_config_t *config, int palette_index) config->titlebar_height = 0; config->furniture.title.bg = c[0]; config->furniture.title.fg = c[1]; - config->furniture.back = c[2]; - config->furniture.close = c[3]; - config->furniture.toggle = c[4]; - config->furniture.resize = c[5]; - config->furniture.scroll.arrows = c[6]; - config->furniture.scroll.wells = c[7]; - config->furniture.scroll.sausages = c[8]; - config->bevel.light = c[9]; - config->bevel.dark = c[10]; - config->accent.bg = c[11]; - config->accent.fg = c[12]; - config->backdrop.colour = c[13]; + config->furniture.outline = c[2]; + config->furniture.back = c[3]; + config->furniture.close = c[4]; + config->furniture.toggle = c[5]; + config->furniture.resize = c[6]; + config->furniture.scroll.arrows = c[7]; + config->furniture.scroll.wells = c[8]; + config->furniture.scroll.sausages = c[9]; + config->bevel.light = c[10]; + config->bevel.dark = c[11]; + config->accent.bg = c[12]; + config->accent.fg = c[13]; + config->backdrop.colour = c[14]; config->backdrop.pattern = screen_PATTERN_DOTS; - config->backdrop.pattern_bg = c[14]; - config->body.window = c[15]; - config->body.menu = c[16]; + config->backdrop.pattern_bg = c[15]; + config->body.window = c[16]; + config->body.menu = c[17]; } /* Redraw the whole screen one pixel at a time: each wuss_redraw_dirty call is diff --git a/include/wuss/wuss.h b/include/wuss/wuss.h index b6e660d..0db8788 100644 --- a/include/wuss/wuss.h +++ b/include/wuss/wuss.h @@ -163,6 +163,8 @@ typedef struct wuss_furniture_palette wuss_colour_t fg; /**< Titlebar text. */ } title; + wuss_colour_t outline; /**< Window outline. wuss_NO_BACKGROUND means + follow the titlebar fill. */ wuss_colour_t back; /**< Send-to-back icon. */ wuss_colour_t close; /**< Close icon. */ wuss_colour_t toggle; /**< Toggle-size icon. */ diff --git a/libraries/wuss/core/create.c b/libraries/wuss/core/create.c index 077729e..a3565c0 100644 --- a/libraries/wuss/core/create.c +++ b/libraries/wuss/core/create.c @@ -143,6 +143,9 @@ result_t wuss_create(screen_t *scr, pal = config->furniture; pal.title.bg = wuss__resolve_colour(w, pal.title.bg); pal.title.fg = wuss__resolve_colour(w, pal.title.fg); + pal.outline = (pal.outline == wuss_NO_BACKGROUND) + ? pal.title.bg + : wuss__resolve_colour(w, pal.outline); pal.back = wuss__resolve_colour(w, pal.back); pal.close = wuss__resolve_colour(w, pal.close); pal.toggle = wuss__resolve_colour(w, pal.toggle); @@ -162,6 +165,7 @@ result_t wuss_create(screen_t *scr, pal.title.bg = bg; pal.title.fg = fg; + pal.outline = bg; pal.back = fg; pal.close = fg; pal.toggle = fg; @@ -178,6 +182,7 @@ result_t wuss_create(screen_t *scr, if (pal.title.bg >= w->npalette || pal.title.fg >= w->npalette || + pal.outline >= w->npalette || pal.back >= w->npalette || pal.close >= w->npalette || pal.toggle >= w->npalette || diff --git a/libraries/wuss/furniture/draw.c b/libraries/wuss/furniture/draw.c index 38d73fe..e9d8fc7 100644 --- a/libraries/wuss/furniture/draw.c +++ b/libraries/wuss/furniture/draw.c @@ -222,7 +222,7 @@ void wuss__furniture_draw(wuss_t *wuss, width = window->visible.x1 - window->visible.x0; height = window->visible.y1 - window->visible.y0; - border = wuss->palette[wuss->furniture_colours.title.bg]; /* no dedicated outline class; matches titlebar chrome */ + border = wuss->palette[wuss->furniture_colours.outline]; wuss->scr->clip = visible_clipped; screen_fill_rect(wuss->scr, window->visible.x0, window->visible.y0, SIZE2D(width, 1), border); diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index a5b4f3e..f5f8404 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -459,6 +459,7 @@ result_t wuss_test(const char *resources) bad_config.titlebar_height = 0; bad_config.furniture.title.bg = 100; /* in range as a byte, but well past any test palette and below wuss_COLOUR_SYMBOLIC */ bad_config.furniture.title.fg = 0; + bad_config.furniture.outline = 0; bad_config.furniture.back = 0; bad_config.furniture.close = 0; bad_config.furniture.toggle = 0; From a76578f721d1f8fd5aee9c1d52f5ffb5a3f01996 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Mon, 7 Sep 2026 15:46:43 +0100 Subject: [PATCH 35/98] feat(wuss): draw default buttons with the action-border surround wuss__icon_draw_button's DEFAULT branch now paints an accent fill inside the same 6px three-ring "action" surround as wuss_ICON_BORDER_ACTION, via a shared icon_draw_action_border helper, replacing the old flat 2px faux border. Only the innermost 2px ring tracks the pressed state (flips to sunken); the outer outset and accent moat stay put. The demo's two DEFAULT buttons in the icons task grow 4px per side to seat the wider surround. --- include/wuss/icon.h | 7 +- libraries/wuss/icon/draw.c | 107 +++++++++++++++++------------- libraries/wuss/test/tasks/icons.c | 8 ++- 3 files changed, 72 insertions(+), 50 deletions(-) diff --git a/include/wuss/icon.h b/include/wuss/icon.h index 18f3a0f..8442c15 100644 --- a/include/wuss/icon.h +++ b/include/wuss/icon.h @@ -142,9 +142,10 @@ typedef enum wuss_icon_flags /** wuss_ICON_TYPE_LABEL: centre the text in the bounding box. Takes * precedence over wuss_ICON_FLAGS_JUSTIFY_RIGHT. */ wuss_ICON_FLAGS_JUSTIFY_CENTRE = 1 << 3, - /** wuss_ICON_TYPE_BUTTON: draw as a default action button, in the window - * manager's accent colours (see wuss_config_t::accent) instead of the - * ordinary bevel. Ignored by other icon types. */ + /** wuss_ICON_TYPE_BUTTON: draw as a default action button -- an accent fill + * (see wuss_config_t::accent) inside the same 6px "action" surround as + * wuss_ICON_BORDER_ACTION, instead of the ordinary 1px bevel. Ignored by + * other icon types. */ wuss_ICON_FLAGS_DEFAULT = 1 << 4, /** wuss_ICON_TYPE_BITMAP: hit-test the icon and raise wuss_EVENT_ICON on a * click, like a button. Without it a bitmap icon is pure decoration and diff --git a/libraries/wuss/icon/draw.c b/libraries/wuss/icon/draw.c index f8e5f35..95fd890 100644 --- a/libraries/wuss/icon/draw.c +++ b/libraries/wuss/icon/draw.c @@ -34,17 +34,44 @@ icon_draw_ctx_t; /* ----------------------------------------------------------------------- */ -static void icon_bevel(screen_t *scr, - const box_t *b, - colour_t fill, - colour_t light, - colour_t dark) +static void icon_fill_bevel(screen_t *scr, + const box_t *b, + colour_t fill, + colour_t light, + colour_t dark) { screen_fill_rect(scr, b->x0, b->y0, SIZE2D(b->x1 - b->x0, b->y1 - b->y0), fill); screen_draw_bevel_edge(scr, b, light, dark); } +/* The 6px-per-edge "action" surround shared by wuss_ICON_BORDER_ACTION and a + * default button: a 2px sunken outset (dark top/left), a 2px accent moat, then + * a 2px raised inset. Only the inset tracks the pressed state -- it flips to + * sunken -- so a pressed action button reads as pushed in without the whole + * surround inverting. */ +static void icon_draw_action_border(screen_t *scr, + const box_t *b, + colour_t light, + colour_t dark, + colour_t accent, + int pressed) +{ + box_t ring; + + ring = *b; + screen_draw_bevel_edge(scr, &ring, dark, light); + + ring = (box_t) BOX_POS_SIZE(b->x0 + 2, b->y0 + 2, + b->x1 - b->x0 - 4, b->y1 - b->y0 - 4); + screen_draw_bevel_edge(scr, &ring, accent, accent); + + ring = (box_t) BOX_POS_SIZE(b->x0 + 4, b->y0 + 4, + b->x1 - b->x0 - 8, b->y1 - b->y0 - 8); + screen_draw_bevel_edge(scr, &ring, pressed ? dark : light, + pressed ? light : dark); +} + /* Resolve the ground an icon's text/glyph blends against: an explicit icon bg, * else the window's dominant backdrop colour (a pattern fill blends against its * clear-bit colour, not its foreground), else "fallback" so bmfont still has a @@ -143,34 +170,26 @@ static void wuss__icon_draw_label(const icon_draw_ctx_t *c) if (icon->border != wuss_ICON_BORDER_NONE) { - colour_t light, dark, accent, hi, lo; - int raised; - box_t ring; + colour_t light, dark, accent; light = c->wuss->palette[c->wuss->bevel_light]; dark = c->wuss->palette[c->wuss->bevel_dark]; accent = c->wuss->palette[c->wuss->accent_bg]; /* the action-button fill */ - /* RIDGE reads raised (light top/left); GROOVE reads sunken; ACTION uses - * both. */ - raised = (icon->border == wuss_ICON_BORDER_RIDGE); - hi = raised ? light : dark; - lo = raised ? dark : light; - - /* GROOVE/RIDGE: one 2px bevel ring. ACTION: 6px per edge as three nested - * 2px rings -- a raised outset, an accent moat, then a raised inset. */ - ring = *b; - screen_draw_bevel_edge(c->scr, &ring, hi, lo); - if (icon->border == wuss_ICON_BORDER_ACTION) { - ring = (box_t) BOX_POS_SIZE(b->x0 + 2, b->y0 + 2, - b->x1 - b->x0 - 4, b->y1 - b->y0 - 4); - screen_draw_bevel_edge(c->scr, &ring, accent, accent); - - ring = (box_t) BOX_POS_SIZE(b->x0 + 4, b->y0 + 4, - b->x1 - b->x0 - 8, b->y1 - b->y0 - 8); - screen_draw_bevel_edge(c->scr, &ring, lo, hi); /* swap for inner */ + icon_draw_action_border(c->scr, b, light, dark, accent, 0); + } + else + { + /* RIDGE reads raised (light top/left); GROOVE reads sunken. One 2px + * bevel ring. */ + box_t ring = *b; + + if (icon->border == wuss_ICON_BORDER_RIDGE) + screen_draw_bevel_edge(c->scr, &ring, light, dark); + else + screen_draw_bevel_edge(c->scr, &ring, dark, light); } } @@ -261,32 +280,30 @@ static void wuss__icon_draw_button(const icon_draw_ctx_t *c) pressed = wuss__icon_pressed(icon); is_default = (icon->flags & wuss_ICON_FLAGS_DEFAULT) != 0; + base = c->wuss->palette[icon->bg]; + light = c->wuss->palette[c->wuss->bevel_light]; + dark = c->wuss->palette[c->wuss->bevel_dark]; + label = c->fg; + + if (icon->flags & wuss_ICON_FLAGS_DISABLED) + label = c->wuss->palette[c->wuss->bevel_dark]; /* greyed: sink toward dark */ + if (is_default) { - /* default action button: a flat accent-filled rectangle inside a - * two-pixel accent-text border, distinct from the bevelled ordinary - * buttons around it */ - base = c->wuss->palette[c->wuss->accent_bg]; - light = c->wuss->palette[c->wuss->accent_fg]; - dark = light; - label = c->wuss->palette[c->wuss->accent_fg]; + /* default action button: an accent-filled rectangle inside the same 6px + * "action" surround as wuss_ICON_BORDER_ACTION, distinct from the plain + * bevelled buttons around it */ + screen_fill_rect(c->scr, b->x0, b->y0, + SIZE2D(b->x1 - b->x0, b->y1 - b->y0), base); + icon_draw_action_border(c->scr, b, light, dark, + c->wuss->palette[c->wuss->accent_bg], pressed); } else { - base = c->wuss->palette[icon->bg]; - light = c->wuss->palette[c->wuss->bevel_light]; - dark = c->wuss->palette[c->wuss->bevel_dark]; - label = c->fg; + icon_fill_bevel(c->scr, b, base, pressed ? dark : light, + pressed ? light : dark); } - if (icon->flags & wuss_ICON_FLAGS_DISABLED) - label = c->wuss->palette[c->wuss->bevel_dark]; /* greyed: sink toward dark */ - - if (pressed) - icon_bevel(c->scr, b, base, dark, light); - else - icon_bevel(c->scr, b, base, light, dark); - if (c->have_font) { point_t pos; diff --git a/libraries/wuss/test/tasks/icons.c b/libraries/wuss/test/tasks/icons.c index 9a48836..6659bf6 100644 --- a/libraries/wuss/test/tasks/icons.c +++ b/libraries/wuss/test/tasks/icons.c @@ -80,7 +80,9 @@ static void icons_add_intro(icons_layout_t *lay, int *button, int *counter) *button = lay->n; s = &lay->specs[lay->n]; - s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN, lay->y, 80, 22); + /* 4px larger on every side than a plain button to seat the DEFAULT icon's + * 6px action surround */ + s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN - 4, lay->y - 4, 88, 30); s->type = wuss_ICON_TYPE_BUTTON; s->text = "Press me"; s->fg = lay->black; @@ -132,7 +134,9 @@ static void icons_add_buttons(icons_layout_t *lay) lay->n++; s = &lay->specs[lay->n]; - s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN + 74, top + 22, 56, 22); + /* 4px larger on every side than the plain buttons beside it to seat the + * DEFAULT icon's 6px action surround */ + s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN + 70, top + 18, 64, 30); s->type = wuss_ICON_TYPE_BUTTON; s->text = "Default"; s->fg = lay->black; From 4f3ccfe3360dcd390ce117a315b295b65c04f3af Mon Sep 17 00:00:00 2001 From: David Thomas Date: Mon, 7 Sep 2026 15:59:54 +0100 Subject: [PATCH 36/98] feat(wuss): use orange accent chrome in the PICO-8 demo palette The default action button's fill (accent.bg) in the PICO-8 chrome row was dark blue, the same as the titlebar. Switch it to orange so default buttons stand out from the surrounding chrome. --- apps/wuss/main.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/wuss/main.c b/apps/wuss/main.c index e4dfd37..1a3703e 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -51,7 +51,7 @@ static const wuss_colour_t g_chrome[2][18] = palette_PICO8_LIGHT_GREY, palette_PICO8_WHITE, palette_PICO8_DARK_GREY, - palette_PICO8_DARK_BLUE, + palette_PICO8_ORANGE, palette_PICO8_WHITE, palette_PICO8_WHITE, palette_PICO8_LIGHT_GREY, From 783d31353e1daaeee99ccffa20ecb6ca5c4ace71 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Mon, 7 Sep 2026 16:20:36 +0100 Subject: [PATCH 37/98] feat(wuss): fill pressed buttons with a darker face colour Add a wuss_COLOUR_BUTTON_PRESSED chrome role, backed by a new config->bevel.pressed field and struct wuss::bevel_pressed. While a button is held its face fills with this colour in place of the icon's own background; this applies to both plain bevelled buttons and default action buttons. The field is resolved and range-validated at wuss_create time alongside the other bevel colours, in both the WUSS_FURNITURE and WUSS_ICONS-only paths, and echoed into the palette cache. Passing wuss_NO_BACKGROUND (or a NULL config) makes it follow bevel.dark, so existing callers are unchanged. The demo's fill_chrome_config uses the sentinel. --- apps/wuss/main.c | 17 +++--- docs/windowing/wuss.md | 4 +- include/wuss/wuss.h | 38 +++++++------ libraries/wuss/core/create.c | 66 +++++++++++++--------- libraries/wuss/core/impl.h | 9 +-- libraries/wuss/core/rebuild-palettecache.c | 1 + libraries/wuss/icon/draw.c | 4 ++ libraries/wuss/test/wuss-test.c | 5 ++ 8 files changed, 88 insertions(+), 56 deletions(-) diff --git a/apps/wuss/main.c b/apps/wuss/main.c index 1a3703e..f263295 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -35,7 +35,7 @@ /* Furniture/bevel/accent/backdrop/body colour indices, one row per palette. * Same field order as the assignments in fill_chrome_config. */ -static const wuss_colour_t g_chrome[2][18] = +static const wuss_colour_t g_chrome[2][19] = { /* PICO-8 */ { @@ -51,6 +51,7 @@ static const wuss_colour_t g_chrome[2][18] = palette_PICO8_LIGHT_GREY, palette_PICO8_WHITE, palette_PICO8_DARK_GREY, + palette_PICO8_LAVENDER, palette_PICO8_ORANGE, palette_PICO8_WHITE, palette_PICO8_WHITE, @@ -73,6 +74,7 @@ static const wuss_colour_t g_chrome[2][18] = palette_WIMP16_GREY_87, palette_WIMP16_WHITE, palette_WIMP16_GREY_50, + palette_WIMP16_GREY_62, palette_WIMP16_ORANGE, palette_WIMP16_BLACK, palette_WIMP16_GREY_50, @@ -99,13 +101,14 @@ static void fill_chrome_config(wuss_config_t *config, int palette_index) config->furniture.scroll.sausages = c[9]; config->bevel.light = c[10]; config->bevel.dark = c[11]; - config->accent.bg = c[12]; - config->accent.fg = c[13]; - config->backdrop.colour = c[14]; + config->bevel.pressed = c[12]; + config->accent.bg = c[13]; + config->accent.fg = c[14]; + config->backdrop.colour = c[15]; config->backdrop.pattern = screen_PATTERN_DOTS; - config->backdrop.pattern_bg = c[15]; - config->body.window = c[16]; - config->body.menu = c[17]; + config->backdrop.pattern_bg = c[16]; + config->body.window = c[17]; + config->body.menu = c[18]; } /* Redraw the whole screen one pixel at a time: each wuss_redraw_dirty call is diff --git a/docs/windowing/wuss.md b/docs/windowing/wuss.md index 3e467ca..2bcdb1c 100644 --- a/docs/windowing/wuss.md +++ b/docs/windowing/wuss.md @@ -29,7 +29,7 @@ A `wuss_backdrop_t` is `{ colour, pattern, pattern_bg }`; the `wuss_BACKDROP_COL A `wuss_colour_t` is a system-palette index. Values `0..127` are raw indices; `wuss_COLOUR_SYMBOLIC` (128) and above are *symbolic* — roles wuss resolves to a concrete index against the live palette (and, for the chrome roles, the live config): - `wuss_COLOUR_BLACK`, `_WHITE`, `_RED`, `_GREEN`, `_BLUE`, `_YELLOW`, `_CYAN`, `_MAGENTA`, `_GREY` — nearest system-palette entry to the named RGB, the same lookup as `wuss_nearest_colour`. -- `wuss_COLOUR_TITLE_BG`, `_TITLE_FG`, `_BUTTON_HILIGHT`, `_BUTTON_SHADOW`, `_ACCENT_BG`, `_ACCENT_FG`, `_BACKDROP`, `_WINDOW`, `_MENU` — echo the matching `wuss_config_t` field (`furniture.title.bg`, `bevel.light`, `bevel.dark`, `backdrop.colour`, `body.window`, `body.menu`, …). `body.window` (work-area fill) defaults to `wuss_COLOUR_GREY`, `body.menu` (menu fill) to `wuss_COLOUR_WHITE`, when `config` is NULL. +- `wuss_COLOUR_TITLE_BG`, `_TITLE_FG`, `_BUTTON_HILIGHT`, `_BUTTON_SHADOW`, `_BUTTON_PRESSED`, `_ACCENT_BG`, `_ACCENT_FG`, `_BACKDROP`, `_WINDOW`, `_MENU` — echo the matching `wuss_config_t` field (`furniture.title.bg`, `bevel.light`, `bevel.dark`, `bevel.pressed`, `backdrop.colour`, `body.window`, `body.menu`, …). `body.window` (work-area fill) defaults to `wuss_COLOUR_GREY`, `body.menu` (menu fill) to `wuss_COLOUR_WHITE`, when `config` is NULL. Pass a symbolic value anywhere a `wuss_colour_t` is taken: config furniture/bevel/accent/body/backdrop, `wuss_window_create` / `wuss_window_set_background` backgrounds, icon specs. It is resolved once when stored, so drawing never pays for it and reads back a plain index. `wuss_set_palette` and `wuss_set_backdrop` re-resolve. `wuss_NO_BACKGROUND` is not symbolic and always passes through. @@ -216,7 +216,7 @@ An icon's bounding box is in **virtual content space** — the same space as `wu Wuss draws icons in creation order (later icons paint on top); hit-testing scans in reverse, so the topmost icon at a point wins. When the pointer leaves a pressed button its pressed state clears; v1 does not re-press on drag-back-in and does not track which mouse button is held. -The bevel's light (top/left) and dark (bottom/right) edge shades come from `config->bevel.light` / `config->bevel.dark` at `wuss_create` time, validated like the other furniture colours; both default to the titlebar fill colour when `config` is `NULL`. +The bevel's light (top/left) and dark (bottom/right) edge shades come from `config->bevel.light` / `config->bevel.dark` at `wuss_create` time, validated like the other furniture colours; both default to the titlebar fill colour when `config` is `NULL`. A held button also fills its face with `config->bevel.pressed` in place of the icon's own background; pass `wuss_NO_BACKGROUND` (or `NULL` config) to have it follow `bevel.dark`. ## Components diff --git a/include/wuss/wuss.h b/include/wuss/wuss.h index 0db8788..1586760 100644 --- a/include/wuss/wuss.h +++ b/include/wuss/wuss.h @@ -138,17 +138,19 @@ typedef unsigned char wuss_colour_t; /* Chrome roles: echo the matching wuss_config_t field, resolved to a * concrete index -- e.g. wuss_COLOUR_TITLE_BG is furniture.title.bg, * wuss_COLOUR_BUTTON_HILIGHT is bevel.light, wuss_COLOUR_BUTTON_SHADOW is - * bevel.dark, wuss_COLOUR_BACKDROP is backdrop.colour, wuss_COLOUR_WINDOW is - * body.window, wuss_COLOUR_MENU is body.menu. */ -#define wuss_COLOUR_TITLE_BG (wuss_COLOUR_SYMBOLIC + 16) -#define wuss_COLOUR_TITLE_FG (wuss_COLOUR_SYMBOLIC + 17) -#define wuss_COLOUR_BUTTON_HILIGHT (wuss_COLOUR_SYMBOLIC + 18) -#define wuss_COLOUR_BUTTON_SHADOW (wuss_COLOUR_SYMBOLIC + 19) -#define wuss_COLOUR_ACCENT_BG (wuss_COLOUR_SYMBOLIC + 20) -#define wuss_COLOUR_ACCENT_FG (wuss_COLOUR_SYMBOLIC + 21) -#define wuss_COLOUR_BACKDROP (wuss_COLOUR_SYMBOLIC + 22) -#define wuss_COLOUR_WINDOW (wuss_COLOUR_SYMBOLIC + 23) -#define wuss_COLOUR_MENU (wuss_COLOUR_SYMBOLIC + 24) + * bevel.dark, wuss_COLOUR_BUTTON_PRESSED is bevel.pressed, + * wuss_COLOUR_BACKDROP is backdrop.colour, + * wuss_COLOUR_WINDOW is body.window, wuss_COLOUR_MENU is body.menu. */ +#define wuss_COLOUR_TITLE_BG (wuss_COLOUR_SYMBOLIC + 16) +#define wuss_COLOUR_TITLE_FG (wuss_COLOUR_SYMBOLIC + 17) +#define wuss_COLOUR_BUTTON_HILIGHT (wuss_COLOUR_SYMBOLIC + 18) +#define wuss_COLOUR_BUTTON_SHADOW (wuss_COLOUR_SYMBOLIC + 19) +#define wuss_COLOUR_ACCENT_BG (wuss_COLOUR_SYMBOLIC + 20) +#define wuss_COLOUR_ACCENT_FG (wuss_COLOUR_SYMBOLIC + 21) +#define wuss_COLOUR_BACKDROP (wuss_COLOUR_SYMBOLIC + 22) +#define wuss_COLOUR_WINDOW (wuss_COLOUR_SYMBOLIC + 23) +#define wuss_COLOUR_MENU (wuss_COLOUR_SYMBOLIC + 24) +#define wuss_COLOUR_BUTTON_PRESSED (wuss_COLOUR_SYMBOLIC + 25) /** Furniture chrome colours, one entry per class of furniture. Title is * the only two-tone class (fill + text); the rest are drawn as a single @@ -326,14 +328,18 @@ typedef struct wuss_config /** * Bevelled work-area button edge shades, as indices into the system * palette: light on the top/left edges, dark on the bottom/right (swapped - * when the button is pressed). Both default to the titlebar fill colour - * when config is NULL. Ignored when both WUSS_FURNITURE and WUSS_ICONS are - * off. + * when the button is pressed). pressed is the button face fill while it is + * held down, in place of the icon's own background. light and dark default + * to the titlebar fill colour when config is NULL; pressed defaults to + * dark. Read back through wuss_COLOUR_BUTTON_HILIGHT / + * wuss_COLOUR_BUTTON_SHADOW / wuss_COLOUR_BUTTON_PRESSED. Ignored when + * both WUSS_FURNITURE and WUSS_ICONS are off. */ struct { - wuss_colour_t light; /**< Top/left bevel edge. */ - wuss_colour_t dark; /**< Bottom/right bevel edge. */ + wuss_colour_t light; /**< Top/left bevel edge. */ + wuss_colour_t dark; /**< Bottom/right bevel edge. */ + wuss_colour_t pressed; /**< Button face fill while held down. */ } bevel; diff --git a/libraries/wuss/core/create.c b/libraries/wuss/core/create.c index a3565c0..cbfbf5d 100644 --- a/libraries/wuss/core/create.c +++ b/libraries/wuss/core/create.c @@ -34,13 +34,15 @@ static const colour_t wuss__default_palette[] = static result_t validate_bevel_backdrop(const wuss_t *w, wuss_colour_t blight, wuss_colour_t bdark, + wuss_colour_t bpressed, wuss_colour_t abg, wuss_colour_t afg) { - if (blight >= w->npalette || - bdark >= w->npalette || - abg >= w->npalette || - afg >= w->npalette || + if (blight >= w->npalette || + bdark >= w->npalette || + bpressed >= w->npalette || + abg >= w->npalette || + afg >= w->npalette || wuss__validate_backdrop(w, &w->backdrop) != result_OK) return result_WUSS_BAD_COLOUR; @@ -65,7 +67,7 @@ result_t wuss_create(screen_t *scr, wuss_colour_t bg, fg; #endif #if defined(WUSS_FURNITURE) || defined(WUSS_ICONS) - wuss_colour_t blight, bdark; + wuss_colour_t blight, bdark, bpressed; wuss_colour_t abg, afg; #endif #ifdef WUSS_FURNITURE @@ -153,10 +155,13 @@ result_t wuss_create(screen_t *scr, pal.scroll.arrows = wuss__resolve_colour(w, pal.scroll.arrows); pal.scroll.wells = wuss__resolve_colour(w, pal.scroll.wells); pal.scroll.sausages = wuss__resolve_colour(w, pal.scroll.sausages); - blight = wuss__resolve_colour(w, config->bevel.light); - bdark = wuss__resolve_colour(w, config->bevel.dark); - abg = wuss__resolve_colour(w, config->accent.bg); - afg = wuss__resolve_colour(w, config->accent.fg); + blight = wuss__resolve_colour(w, config->bevel.light); + bdark = wuss__resolve_colour(w, config->bevel.dark); + bpressed = (config->bevel.pressed == wuss_NO_BACKGROUND) + ? bdark + : wuss__resolve_colour(w, config->bevel.pressed); + abg = wuss__resolve_colour(w, config->accent.bg); + afg = wuss__resolve_colour(w, config->accent.fg); } else { @@ -174,10 +179,11 @@ result_t wuss_create(screen_t *scr, pal.scroll.wells = bg; pal.scroll.sausages = fg; - blight = 0; - bdark = 0; - abg = bg; /* default action button: the titlebar colours */ - afg = fg; + blight = 0; + bdark = 0; + bpressed = 0; + abg = bg; /* default action button: the titlebar colours */ + afg = fg; } if (pal.title.bg >= w->npalette || @@ -190,7 +196,7 @@ result_t wuss_create(screen_t *scr, pal.scroll.arrows >= w->npalette || pal.scroll.wells >= w->npalette || pal.scroll.sausages >= w->npalette || - validate_bevel_backdrop(w, blight, bdark, abg, afg) != result_OK) + validate_bevel_backdrop(w, blight, bdark, bpressed, abg, afg) != result_OK) { wuss__free(w, w->palette); wuss__free(w, w); @@ -200,6 +206,7 @@ result_t wuss_create(screen_t *scr, w->furniture_colours = pal; w->bevel_light = blight; w->bevel_dark = bdark; + w->bevel_pressed = bpressed; w->accent_bg = abg; w->accent_fg = afg; @@ -229,28 +236,33 @@ result_t wuss_create(screen_t *scr, #ifdef WUSS_ICONS if (config != NULL) { - blight = wuss__resolve_colour(w, config->bevel.light); - bdark = wuss__resolve_colour(w, config->bevel.dark); - abg = wuss__resolve_colour(w, config->accent.bg); - afg = wuss__resolve_colour(w, config->accent.fg); + blight = wuss__resolve_colour(w, config->bevel.light); + bdark = wuss__resolve_colour(w, config->bevel.dark); + bpressed = (config->bevel.pressed == wuss_NO_BACKGROUND) + ? bdark + : wuss__resolve_colour(w, config->bevel.pressed); + abg = wuss__resolve_colour(w, config->accent.bg); + afg = wuss__resolve_colour(w, config->accent.fg); } else { - blight = 0; - bdark = 0; - abg = 0; - afg = (w->npalette > 1) ? 1 : 0; + blight = 0; + bdark = 0; + bpressed = 0; + abg = 0; + afg = (w->npalette > 1) ? 1 : 0; } - if (validate_bevel_backdrop(w, blight, bdark, abg, afg) != result_OK) + if (validate_bevel_backdrop(w, blight, bdark, bpressed, abg, afg) != result_OK) { wuss__free(w, w->palette); wuss__free(w, w); return result_WUSS_BAD_COLOUR; } - w->bevel_light = blight; - w->bevel_dark = bdark; - w->accent_bg = abg; - w->accent_fg = afg; + w->bevel_light = blight; + w->bevel_dark = bdark; + w->bevel_pressed = bpressed; + w->accent_bg = abg; + w->accent_fg = afg; #else if (wuss__validate_backdrop(w, &w->backdrop) != result_OK) { diff --git a/libraries/wuss/core/impl.h b/libraries/wuss/core/impl.h index cb3f8af..fd8fb97 100644 --- a/libraries/wuss/core/impl.h +++ b/libraries/wuss/core/impl.h @@ -123,10 +123,11 @@ struct wuss wuss_furniture_palette_t furniture_colours; #endif #if defined(WUSS_FURNITURE) || defined(WUSS_ICONS) - wuss_colour_t bevel_light; /* work-area button top/left edge */ - wuss_colour_t bevel_dark; /* work-area button bottom/right edge */ - wuss_colour_t accent_bg; /* default action button fill */ - wuss_colour_t accent_fg; /* default action button text */ + wuss_colour_t bevel_light; /* work-area button top/left edge */ + wuss_colour_t bevel_dark; /* work-area button bottom/right edge */ + wuss_colour_t bevel_pressed; /* work-area button face while held */ + wuss_colour_t accent_bg; /* default action button fill */ + wuss_colour_t accent_fg; /* default action button text */ #endif wuss_colour_t window_bg; /* work-area body fill; wuss_COLOUR_WINDOW */ wuss_colour_t menu_bg; /* menu body fill; wuss_COLOUR_MENU */ diff --git a/libraries/wuss/core/rebuild-palettecache.c b/libraries/wuss/core/rebuild-palettecache.c index 219474d..a5280fd 100644 --- a/libraries/wuss/core/rebuild-palettecache.c +++ b/libraries/wuss/core/rebuild-palettecache.c @@ -50,6 +50,7 @@ void wuss__rebuild_palettecache(wuss_t *wuss) #if defined(WUSS_FURNITURE) || defined(WUSS_ICONS) cache[wuss_COLOUR_BUTTON_HILIGHT - wuss_COLOUR_SYMBOLIC] = wuss->bevel_light; cache[wuss_COLOUR_BUTTON_SHADOW - wuss_COLOUR_SYMBOLIC] = wuss->bevel_dark; + cache[wuss_COLOUR_BUTTON_PRESSED - wuss_COLOUR_SYMBOLIC] = wuss->bevel_pressed; cache[wuss_COLOUR_ACCENT_BG - wuss_COLOUR_SYMBOLIC] = wuss->accent_bg; cache[wuss_COLOUR_ACCENT_FG - wuss_COLOUR_SYMBOLIC] = wuss->accent_fg; #endif diff --git a/libraries/wuss/icon/draw.c b/libraries/wuss/icon/draw.c index 95fd890..6f8dd92 100644 --- a/libraries/wuss/icon/draw.c +++ b/libraries/wuss/icon/draw.c @@ -285,6 +285,10 @@ static void wuss__icon_draw_button(const icon_draw_ctx_t *c) dark = c->wuss->palette[c->wuss->bevel_dark]; label = c->fg; + /* a held button fills darker regardless of type */ + if (pressed) + base = c->wuss->palette[c->wuss->bevel_pressed]; + if (icon->flags & wuss_ICON_FLAGS_DISABLED) label = c->wuss->palette[c->wuss->bevel_dark]; /* greyed: sink toward dark */ diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index f5f8404..1c1dcf2 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -467,6 +467,11 @@ result_t wuss_test(const char *resources) bad_config.furniture.scroll.arrows = 0; bad_config.furniture.scroll.wells = 0; bad_config.furniture.scroll.sausages = 0; + bad_config.bevel.light = 0; + bad_config.bevel.dark = 0; + bad_config.bevel.pressed = 0; + bad_config.accent.bg = 0; + bad_config.accent.fg = 0; rc = wuss_create(&scr, NULL, 0, NULL, 0, &bad_config, NULL, &bad_wuss); if (rc != result_WUSS_BAD_COLOUR) goto Failure; From 549476790b628603f954c4c7421ce455eea2e304 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Mon, 7 Sep 2026 16:46:30 +0100 Subject: [PATCH 38/98] docs(wuss): reorganise TODO into dependency layers Drop the completed DONE/DEFER backlog and regroup the remaining work into eight dependency-ordered layers, from core plumbing (masks, ColourTrans, UTF-8) up through sprites, furniture, multi-task and apps. Prerequisite chains are flagged inline with 'blocks:' notes. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/TODO.txt | 103 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 libraries/wuss/TODO.txt diff --git a/libraries/wuss/TODO.txt b/libraries/wuss/TODO.txt new file mode 100644 index 0000000..e8d8809 --- /dev/null +++ b/libraries/wuss/TODO.txt @@ -0,0 +1,103 @@ +Wuss + +Ordered by dependency. Each layer builds on the ones above it. +Items marked (blocks: X) are prerequisites for X. + + +LAYER 0 - BUGS (fix before building on top) + +- Window outline clicks are treated as work area clicks + blocks: Snap to edges, Dragging against window edge shuffles window +- Various window size toggle ops need checking out +- Auto window placement packer still not working as expected + blocks: reliable multi-window tasks +- Image task not shrinking window when new image loads + + +LAYER 1 - CORE PLUMBING (no wuss deps; unblocks everything else) + +- Masks: how to get (alpha) masks into the system? Another channel, + fixed value, or a second bitmap? Decide the model first. + blocks: Draw bitmap through mask, Masked sprites/bitmaps, furniture tinting +- ColourTrans + blocks: Sprites, palette images, Support palette images +- UTF-8 everywhere + blocks: text abstraction, window/menu templates +- Is having font handling in wuss-core a bad thing? Decide arch now. + blocks: Wuss text abstraction + + +LAYER 2 - TEXT & FONTS (needs UTF-8, font-arch decision) + +- Wuss text abstraction (wraps bmfont, supplies the Wuss font(s)) +- How to get bold etc. into the system? How to select symbols? +- bmfont metrics: baselines and leading - button labels look too high +- More symbols + + +LAYER 3 - SPRITES (needs masks, ColourTrans) + +- Sprites (with names, with masks, with multiple sprites) +- Sprite scaling +- Icon sprites pool +- Masked sprites/bitmaps +- Draw bitmap through (bayer) mask +- Import Kare style icons + + +LAYER 4 - WINDOW FURNITURE & INTERACTION (needs bug layer, text layer) + +- Set wuss window config after the fact (goes with palette work) + blocks: Furniture icons toggling state, min window size +- Proper furniture: close, minimise, maximise, scrolling (half done) +- Minimum window size based on furniture requested + (later: squash-able furniture) +- Furniture icons toggling state when clicked +- Scroll: auto repeat on scroll buttons +- Snap to edges +- Dragging against window edge shuffles window in opposite direction +- Input focus, text entry, furniture tinting + + +LAYER 5 - DRAG & TEMPLATES (needs sprites, config-after-the-fact) + +- Dragbox concepts (mouse clamping, marching ants) + blocks: Drag & Drop +- Window and menu templates (text format?) +- Drag & Drop + + +LAYER 6 - MULTI-TASK (needs stable windows, templates) + +- Messaging between tasks + blocks: Desktop cohort +- Backdrop: register events, or a task that covers it like Pinboard +- Icon bar - a separate task? +- Test submenu dialogues +- Dialogue abstraction from app engine? + + +LAYER 7 - APPS & POLISH (needs most of the above) + +- Have a Paint app as a target +- Desktop cohort: Filer, Task Manager, Palette Util +- Colour Picker component +- Proper desktop font +- Support palette images +- Greebling symmetry +- 8x8 icon libraries - share the greeble code with other parts of wuss +- Tiled fills (tile_1) +- Rounded corner windows + + +NO STRONG DEPS (do any time) + +- Optimise +- Generalise APIs (make more Wimp-like) +- Make as much use of DPTLib functions as possible +- Fix coordinates to be exactly like Wimp/RO? +- Set overall UI scale (like EIG values) +- Window stacks (a-la nested wimp) +- Nested wimp type nesting +- Automatic pane handling? +- Stress test: moving windows in a circle a-la nested Wimp From b269cb7e1077bc78154fc9286ba7150a9d94c569 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Mon, 7 Sep 2026 21:08:15 +0100 Subject: [PATCH 39/98] fix(wuss): tile furniture hit boxes over the outline band and corners Every furniture part except the window outline was hit-tested against the exact rectangle it is drawn from, leaving three bands inside window->visible that fell through wuss__furniture_hit_test to wuss_FURNITURE_CONTENT: the 1px outline border, the WUSS_DIVIDER_PX seam between the content and the scroll strips, and the carved L-margin on NO_VSCROLL + NO_HSCROLL + resize-on windows. A click there reached the client as a bogus workarea click. Add a grown wuss___hit_box variant of each furniture geometry helper -- the drawn rectangle stretched outward from the content box toward the window edge -- and switch furniture/hit-test.c to them. The drawn wuss___box helpers and every draw/invalidate caller are unchanged, so the appearance is pixel-for-pixel identical. Grown boxes overlap, so the hit-test order now encodes corner ownership (icons before TITLE, RESIZE before the scroll strips, VSCROLL before HSCROLL); a nearest_edge_region fallback attributes the one residual outline band -- an edge with no scrollbar or resize icon -- to the closest furniture, only ever reporting CONTENT for a fully chromeless outlined window's bare 1px frame (documented exception). wuss__content_box, wuss__furniture_carve_for and the divider accounting are untouched: the seam is absorbed by moving hit-box edges, not by re-reserving space, so nothing in window create/resize moves. Extend the wuss test with a direct wuss__furniture_hit_test sweep: per-band assertions on a base window plus a full visible-box pixel sweep asserting the region is never NONE and is CONTENT iff the pixel lies in the drawn content box, repeated across seven furniture-flag combos. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/TODO.txt | 5 +- libraries/wuss/core/impl.h | 2 + libraries/wuss/furniture.h | 13 +- libraries/wuss/furniture/back-box.c | 16 ++ libraries/wuss/furniture/close-box.c | 16 ++ libraries/wuss/furniture/hit-test.c | 82 +++++++-- libraries/wuss/furniture/invalidate.c | 4 + libraries/wuss/furniture/resize-box.c | 28 +++ libraries/wuss/furniture/scroll-box.c | 95 ++++++++++ libraries/wuss/furniture/titlebar-box.c | 16 ++ libraries/wuss/furniture/toggle-box.c | 15 ++ libraries/wuss/test/wuss-test.c | 225 +++++++++++++++++++++++- 12 files changed, 500 insertions(+), 17 deletions(-) diff --git a/libraries/wuss/TODO.txt b/libraries/wuss/TODO.txt index e8d8809..2288ef0 100644 --- a/libraries/wuss/TODO.txt +++ b/libraries/wuss/TODO.txt @@ -6,8 +6,6 @@ Items marked (blocks: X) are prerequisites for X. LAYER 0 - BUGS (fix before building on top) -- Window outline clicks are treated as work area clicks - blocks: Snap to edges, Dragging against window edge shuffles window - Various window size toggle ops need checking out - Auto window placement packer still not working as expected blocks: reliable multi-window tasks @@ -54,8 +52,9 @@ LAYER 4 - WINDOW FURNITURE & INTERACTION (needs bug layer, text layer) (later: squash-able furniture) - Furniture icons toggling state when clicked - Scroll: auto repeat on scroll buttons -- Snap to edges +- Snap to edges (outline-click bug fixed; unblocked) - Dragging against window edge shuffles window in opposite direction + (outline-click bug fixed; unblocked) - Input focus, text entry, furniture tinting diff --git a/libraries/wuss/core/impl.h b/libraries/wuss/core/impl.h index fd8fb97..0f19ae6 100644 --- a/libraries/wuss/core/impl.h +++ b/libraries/wuss/core/impl.h @@ -374,7 +374,9 @@ void wuss__scroll_step(wuss_window_t *window, point_t delta); #ifdef WUSS_FURNITURE void wuss__titlebar_box(const wuss_window_t *window, box_t *out); +void wuss__title_hit_box(const wuss_window_t *window, box_t *out); void wuss__close_box(const wuss_window_t *window, box_t *out); +void wuss__close_hit_box(const wuss_window_t *window, box_t *out); void wuss__content_box(const wuss_window_t *window, box_t *out); #else /* No furniture: the content area is the whole visible footprint. */ diff --git a/libraries/wuss/furniture.h b/libraries/wuss/furniture.h index 0bf378b..bc03e1a 100644 --- a/libraries/wuss/furniture.h +++ b/libraries/wuss/furniture.h @@ -110,22 +110,33 @@ wuss__furniture_ops_t; extern const wuss__furniture_ops_t wuss__furniture_default_ops; -/* geometry: titlebar icons */ +/* geometry: titlebar icons. The _box helpers are the drawn rectangles; the + * _hit_box helpers are the same rectangles grown outward to tile the outline + * band and the window corners for hit testing (see furniture/hit-test.c). */ void wuss__back_box(const wuss_window_t *window, box_t *out); +void wuss__back_hit_box(const wuss_window_t *window, box_t *out); void wuss__toggle_box(const wuss_window_t *window, box_t *out); +void wuss__toggle_hit_box(const wuss_window_t *window, box_t *out); void wuss__resize_box(const wuss_window_t *window, box_t *out); +void wuss__resize_hit_box(const wuss_window_t *window, box_t *out); /* geometry: vertical scrollbar */ void wuss__vscroll_up_box(const wuss_window_t *window, box_t *out); +void wuss__vscroll_up_hit_box(const wuss_window_t *window, box_t *out); void wuss__vscroll_down_box(const wuss_window_t *window, box_t *out); +void wuss__vscroll_down_hit_box(const wuss_window_t *window, box_t *out); void wuss__vscroll_well_box(const wuss_window_t *window, box_t *out); +void wuss__vscroll_well_hit_box(const wuss_window_t *window, box_t *out); void wuss__vscroll_sausage_box(const wuss_window_t *window, box_t *out); int wuss__vscroll_well_px(const wuss_window_t *window); /* geometry: horizontal scrollbar */ void wuss__hscroll_left_box(const wuss_window_t *window, box_t *out); +void wuss__hscroll_left_hit_box(const wuss_window_t *window, box_t *out); void wuss__hscroll_right_box(const wuss_window_t *window, box_t *out); +void wuss__hscroll_right_hit_box(const wuss_window_t *window, box_t *out); void wuss__hscroll_well_box(const wuss_window_t *window, box_t *out); +void wuss__hscroll_well_hit_box(const wuss_window_t *window, box_t *out); void wuss__hscroll_sausage_box(const wuss_window_t *window, box_t *out); int wuss__hscroll_well_px(const wuss_window_t *window); diff --git a/libraries/wuss/furniture/back-box.c b/libraries/wuss/furniture/back-box.c index 3998a78..4a53450 100644 --- a/libraries/wuss/furniture/back-box.c +++ b/libraries/wuss/furniture/back-box.c @@ -17,3 +17,19 @@ void wuss__back_box(const wuss_window_t *window, box_t *out) out->x1 = out->x0 + size; out->y1 = out->y0 + size; } + +/* BACK is the leftmost titlebar icon, so its hit box owns the top-left corner: + * it grows left and up to the window edge, swallowing the outline pixel and the + * inset strip between the outline and the drawn icon. Its right and bottom + * edges (facing CLOSE and the titlebar) are unchanged. */ +void wuss__back_hit_box(const wuss_window_t *window, box_t *out) +{ + box_t drawn; + + wuss__back_box(window, &drawn); + + out->x0 = window->visible.x0; + out->y0 = window->visible.y0; + out->x1 = drawn.x1; + out->y1 = drawn.y1; +} diff --git a/libraries/wuss/furniture/close-box.c b/libraries/wuss/furniture/close-box.c index e7c19c8..3de8147 100644 --- a/libraries/wuss/furniture/close-box.c +++ b/libraries/wuss/furniture/close-box.c @@ -19,3 +19,19 @@ void wuss__close_box(const wuss_window_t *window, box_t *out) out->x1 = out->x0 + size; out->y1 = out->y0 + size; } + +/* CLOSE always grows up over the top outline. It only owns the top-left corner + * when there is no BACK icon to its left; with BACK present its left edge is + * unchanged and butts BACK's right edge, so the two hit boxes tile with no + * seam. */ +void wuss__close_hit_box(const wuss_window_t *window, box_t *out) +{ + box_t drawn; + + wuss__close_box(window, &drawn); + + out->x0 = (window->flags & wuss_WINDOW_NO_BACK) ? window->visible.x0 : drawn.x0; + out->y0 = window->visible.y0; + out->x1 = drawn.x1; + out->y1 = drawn.y1; +} diff --git a/libraries/wuss/furniture/hit-test.c b/libraries/wuss/furniture/hit-test.c index b243900..59702c3 100644 --- a/libraries/wuss/furniture/hit-test.c +++ b/libraries/wuss/furniture/hit-test.c @@ -2,6 +2,58 @@ #include "../core/impl.h" +/* Attribute a point that is inside window->visible but outside the content box + * and outside every furniture hit box above -- the 1px outline band on an edge + * that has no scrollbar or resize icon to absorb it -- to the nearest + * furniture. Never returns CONTENT except for a fully chromeless outlined + * window (no titlebar, no scrollbars, no resize), whose bare 1px frame has + * genuinely no furniture behind it (documented exception). */ +static wuss_furniture_region_t nearest_edge_region(const wuss_window_t *window, + point_t p) +{ + box_t content; + int has_v, has_h, has_resize, has_title; + + wuss__content_box(window, &content); + + has_v = !(window->flags & wuss_WINDOW_NO_VSCROLL); + has_h = !(window->flags & wuss_WINDOW_NO_HSCROLL); + has_resize = !(window->flags & wuss_WINDOW_NO_RESIZE); + has_title = !(window->flags & wuss_WINDOW_NO_TITLEBAR); + + if (p.y >= content.y1) + return has_h ? wuss_FURNITURE_HSCROLL_WELL + : has_resize ? wuss_FURNITURE_RESIZE + : has_v ? wuss_FURNITURE_VSCROLL_WELL + : has_title ? wuss_FURNITURE_TITLE + : wuss_FURNITURE_CONTENT; + + if (p.x >= content.x1) + return has_v ? wuss_FURNITURE_VSCROLL_WELL + : has_resize ? wuss_FURNITURE_RESIZE + : has_h ? wuss_FURNITURE_HSCROLL_WELL + : has_title ? wuss_FURNITURE_TITLE + : wuss_FURNITURE_CONTENT; + + if (p.y < content.y0) + return has_title ? wuss_FURNITURE_TITLE : wuss_FURNITURE_CONTENT; + + /* left outline column, level with the content */ + return has_h ? wuss_FURNITURE_HSCROLL_WELL + : has_title ? wuss_FURNITURE_TITLE + : wuss_FURNITURE_CONTENT; +} + +/* The furniture hit boxes are grown outward from the content box to tile every + * pixel of window->visible -- the drawn boxes are the un-suffixed wuss__*_box + * helpers, these _hit_box variants swallow the outline band, the four corners + * and the interior divider seam. Grown boxes OVERLAP where they meet, so the + * test order below is load-bearing: + * - icons before TITLE: the titlebar corners belong to BACK / CLOSE / TOGGLE; + * - RESIZE before the scroll strips: the bottom-right corner is the resize + * icon when there is one; + * - VSCROLL before HSCROLL: with no resize icon the bottom-right corner + * resolves to VSCROLL_DOWN. */ wuss_furniture_region_t wuss__furniture_hit_test(const wuss_window_t *window, point_t p) { @@ -11,66 +63,74 @@ wuss_furniture_region_t wuss__furniture_hit_test(const wuss_window_t *window, { if (!(window->flags & wuss_WINDOW_NO_BACK)) { - wuss__back_box(window, &box); + wuss__back_hit_box(window, &box); if (box_contains_point(&box, p.x, p.y)) return wuss_FURNITURE_BACK; } if (!(window->flags & wuss_WINDOW_NO_CLOSE)) { - wuss__close_box(window, &box); + wuss__close_hit_box(window, &box); if (box_contains_point(&box, p.x, p.y)) return wuss_FURNITURE_CLOSE; } if (!(window->flags & wuss_WINDOW_NO_TOGGLE_SIZE)) { - wuss__toggle_box(window, &box); + wuss__toggle_hit_box(window, &box); if (box_contains_point(&box, p.x, p.y)) return wuss_FURNITURE_TOGGLE_SIZE; } - wuss__titlebar_box(window, &box); + wuss__title_hit_box(window, &box); if (box_contains_point(&box, p.x, p.y)) return wuss_FURNITURE_TITLE; } if (!(window->flags & wuss_WINDOW_NO_RESIZE)) { - wuss__resize_box(window, &box); + wuss__resize_hit_box(window, &box); if (box_contains_point(&box, p.x, p.y)) return wuss_FURNITURE_RESIZE; } if (!(window->flags & wuss_WINDOW_NO_VSCROLL)) { - wuss__vscroll_up_box(window, &box); + wuss__vscroll_up_hit_box(window, &box); if (box_contains_point(&box, p.x, p.y)) return wuss_FURNITURE_VSCROLL_UP; - wuss__vscroll_down_box(window, &box); + wuss__vscroll_down_hit_box(window, &box); if (box_contains_point(&box, p.x, p.y)) return wuss_FURNITURE_VSCROLL_DOWN; - wuss__vscroll_well_box(window, &box); + wuss__vscroll_well_hit_box(window, &box); if (box_contains_point(&box, p.x, p.y)) return wuss_FURNITURE_VSCROLL_WELL; } if (!(window->flags & wuss_WINDOW_NO_HSCROLL)) { - wuss__hscroll_left_box(window, &box); + wuss__hscroll_left_hit_box(window, &box); if (box_contains_point(&box, p.x, p.y)) return wuss_FURNITURE_HSCROLL_LEFT; - wuss__hscroll_right_box(window, &box); + wuss__hscroll_right_hit_box(window, &box); if (box_contains_point(&box, p.x, p.y)) return wuss_FURNITURE_HSCROLL_RIGHT; - wuss__hscroll_well_box(window, &box); + wuss__hscroll_well_hit_box(window, &box); if (box_contains_point(&box, p.x, p.y)) return wuss_FURNITURE_HSCROLL_WELL; } + /* The grown boxes cover every chrome pixel except the outline band on an edge + * with no scrollbar and no resize icon. Attribute that to the nearest + * furniture rather than letting it fall through to the workarea. */ + wuss__content_box(window, &box); + if (box_contains_point(&window->visible, p.x, p.y) && + !box_contains_point(&box, p.x, p.y)) + return nearest_edge_region(window, p); + return wuss_FURNITURE_CONTENT; } diff --git a/libraries/wuss/furniture/invalidate.c b/libraries/wuss/furniture/invalidate.c index 51c03d5..9b1e817 100644 --- a/libraries/wuss/furniture/invalidate.c +++ b/libraries/wuss/furniture/invalidate.c @@ -7,6 +7,10 @@ * (toggle-size) also mark the *old* furniture strips dirty, since a blit * that reused the old pixels leaves stale titlebar/scrollbar/outline * pixels sitting wherever those strips used to be. */ +/* The dirty rects marked here -- the titlebar strip, the right carve column + * (full height), the bottom carve row (full width) and the four outline edges + * -- already union to every pixel of "visible", so they also cover the grown + * furniture hit boxes (furniture/hit-test.c): nothing extra to invalidate. */ void wuss__furniture_invalidate_for(wuss_window_t *window, const box_t *visible) { diff --git a/libraries/wuss/furniture/resize-box.c b/libraries/wuss/furniture/resize-box.c index a55aba8..13e4a97 100644 --- a/libraries/wuss/furniture/resize-box.c +++ b/libraries/wuss/furniture/resize-box.c @@ -14,3 +14,31 @@ void wuss__resize_box(const wuss_window_t *window, box_t *out) out->y1 = window->visible.y1 - outline_px; out->y0 = out->y1 - size; } + +/* The resize hit box owns the bottom-right corner: it grows right and down over + * the outline. On a side with no scroll strip it also grows inward to the + * content edge, swallowing the divider seam (scrollbars present) or the whole + * carved margin (NO_VSCROLL + NO_HSCROLL + resize on). On a side that does have + * a scroll strip its edge is unchanged and butts that strip. */ +void wuss__resize_hit_box(const wuss_window_t *window, box_t *out) +{ + point_t carve; + int outline_px, size; + + outline_px = wuss__outline_px(window); + size = wuss__button_size(window); + wuss__furniture_carve_for(window->flags, size, &carve); + + out->x1 = window->visible.x1; + out->y1 = window->visible.y1; + + if (!(window->flags & wuss_WINDOW_NO_VSCROLL)) + out->x0 = window->visible.x1 - outline_px - size; /* butt the vscroll strip */ + else + out->x0 = window->visible.x1 - outline_px - carve.x; /* = content right edge */ + + if (!(window->flags & wuss_WINDOW_NO_HSCROLL)) + out->y0 = window->visible.y1 - outline_px - size; /* butt the hscroll strip */ + else + out->y0 = window->visible.y1 - outline_px - carve.y; /* = content bottom edge */ +} diff --git a/libraries/wuss/furniture/scroll-box.c b/libraries/wuss/furniture/scroll-box.c index 42aaf39..69f6f71 100644 --- a/libraries/wuss/furniture/scroll-box.c +++ b/libraries/wuss/furniture/scroll-box.c @@ -39,6 +39,53 @@ static void scroll_strip(const wuss_window_t *window, } } +/* The scrollbar strip's HIT box: as scroll_strip, but grown so it tiles the + * outline band and the divider seam that the drawn strip leaves for the + * outline / interior rule. + * + * - breadth (cross axis) grows out to the visible edge it hugs; + * - the content-facing inner edge moves in by WUSS_DIVIDER_PX to the content + * edge, swallowing the seam; + * - the far (resize-corner) end: the VERTICAL strip owns the bottom-right + * corner whenever there is no resize icon; the HORIZONTAL strip stops short + * of the corner whenever either a resize icon OR a vertical scrollbar is + * present, so the drawn-over-nothing overlap at the corner is avoided and + * the tiling stays gap-free. */ +static void scroll_strip_hit(const wuss_window_t *window, + int horizontal, + box_t *out) +{ + point_t carve; + box_t titlebar; + int outline_px, size, has_resize, has_vscroll; + + outline_px = wuss__outline_px(window); + size = wuss__button_size(window); + has_resize = !(window->flags & wuss_WINDOW_NO_RESIZE); + has_vscroll = !(window->flags & wuss_WINDOW_NO_VSCROLL); + wuss__furniture_carve_for(window->flags, size, &carve); + + if (horizontal) + { + out->y1 = window->visible.y1; + out->y0 = window->visible.y1 - outline_px - carve.y; /* = content bottom edge */ + out->x0 = window->visible.x0; + out->x1 = (has_resize || has_vscroll) + ? window->visible.x1 - outline_px - size + : window->visible.x1; + } + else + { + out->x1 = window->visible.x1; + out->x0 = window->visible.x1 - outline_px - carve.x; /* = content right edge */ + wuss__titlebar_box(window, &titlebar); + out->y0 = titlebar.y1; + out->y1 = has_resize + ? window->visible.y1 - outline_px - size + : window->visible.y1; + } +} + /* One "size"-long slice off an end of "strip" along its long axis: the near * end (up / left arrow) when far_end is 0, the far end (down / right arrow) * when far_end is 1. The cross axis spans the whole strip. */ @@ -200,6 +247,30 @@ void wuss__vscroll_sausage_box(const wuss_window_t *window, box_t *out) scroll_sausage(window, 0, out); } +void wuss__vscroll_up_hit_box(const wuss_window_t *window, box_t *out) +{ + box_t strip; + + scroll_strip_hit(window, 0, &strip); + scroll_end_slice(&strip, 0, 0, wuss__button_size(window), out); +} + +void wuss__vscroll_down_hit_box(const wuss_window_t *window, box_t *out) +{ + box_t strip; + + scroll_strip_hit(window, 0, &strip); + scroll_end_slice(&strip, 0, 1, wuss__button_size(window), out); +} + +void wuss__vscroll_well_hit_box(const wuss_window_t *window, box_t *out) +{ + box_t strip; + + scroll_strip_hit(window, 0, &strip); + scroll_middle_slice(&strip, 0, wuss__button_size(window), out); +} + /* ----- horizontal scrollbar -------------------------------------------- */ void wuss__hscroll_left_box(const wuss_window_t *window, box_t *out) @@ -236,3 +307,27 @@ void wuss__hscroll_sausage_box(const wuss_window_t *window, box_t *out) { scroll_sausage(window, 1, out); } + +void wuss__hscroll_left_hit_box(const wuss_window_t *window, box_t *out) +{ + box_t strip; + + scroll_strip_hit(window, 1, &strip); + scroll_end_slice(&strip, 1, 0, wuss__button_size(window), out); +} + +void wuss__hscroll_right_hit_box(const wuss_window_t *window, box_t *out) +{ + box_t strip; + + scroll_strip_hit(window, 1, &strip); + scroll_end_slice(&strip, 1, 1, wuss__button_size(window), out); +} + +void wuss__hscroll_well_hit_box(const wuss_window_t *window, box_t *out) +{ + box_t strip; + + scroll_strip_hit(window, 1, &strip); + scroll_middle_slice(&strip, 1, wuss__button_size(window), out); +} diff --git a/libraries/wuss/furniture/titlebar-box.c b/libraries/wuss/furniture/titlebar-box.c index 6a620b6..b2153c9 100644 --- a/libraries/wuss/furniture/titlebar-box.c +++ b/libraries/wuss/furniture/titlebar-box.c @@ -13,3 +13,19 @@ void wuss__titlebar_box(const wuss_window_t *window, box_t *out) out->x1 = window->visible.x1 - outline_px; out->y1 = out->y0 + wuss__titlebar_height(window); } + +/* The titlebar's hit box grows over the top, left and right outline so a click + * on the outline band there routes to TITLE rather than falling through to the + * workarea. The bottom edge is unchanged -- it butts the content top, which + * does not move. */ +void wuss__title_hit_box(const wuss_window_t *window, box_t *out) +{ + int outline_px; + + outline_px = wuss__outline_px(window); + + out->x0 = window->visible.x0; + out->y0 = window->visible.y0; + out->x1 = window->visible.x1; + out->y1 = window->visible.y0 + outline_px + wuss__titlebar_height(window); +} diff --git a/libraries/wuss/furniture/toggle-box.c b/libraries/wuss/furniture/toggle-box.c index 326ecf6..ebae725 100644 --- a/libraries/wuss/furniture/toggle-box.c +++ b/libraries/wuss/furniture/toggle-box.c @@ -17,3 +17,18 @@ void wuss__toggle_box(const wuss_window_t *window, box_t *out) out->y0 = titlebar.y0 + inset; out->y1 = out->y0 + size; } + +/* TOGGLE_SIZE is the rightmost titlebar icon, so its hit box owns the top-right + * corner: it grows right and up to the window edge. Its left edge (facing the + * title text) is unchanged. */ +void wuss__toggle_hit_box(const wuss_window_t *window, box_t *out) +{ + box_t drawn; + + wuss__toggle_box(window, &drawn); + + out->x0 = drawn.x0; + out->y0 = window->visible.y0; + out->x1 = window->visible.x1; + out->y1 = drawn.y1; +} diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 1c1dcf2..1ed5da2 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -32,9 +32,13 @@ #endif /* white-box: the menu-flash test drives picks through the icon layer and - * reads back struct wuss__menu / struct wuss_icon state directly */ -#if defined(WUSS_MENUS) && defined(WUSS_ICONS) + * reads back struct wuss__menu / struct wuss_icon state directly; the + * furniture hit-test sweep calls wuss__furniture_hit_test and the box + * helpers from impl.h / furniture.h directly */ +#if defined(WUSS_FURNITURE) && defined(WUSS_ICONS) #include "../core/impl.h" +#endif +#if defined(WUSS_MENUS) && defined(WUSS_ICONS) #include "../icon.h" #endif @@ -419,6 +423,57 @@ static int dirty_union_area(wuss_t *wuss, const box_t *bounds) #if defined(WUSS_FURNITURE) && defined(WUSS_ICONS) +/* Sweep every pixel of a window's visible box through wuss__furniture_hit_test + * and check the tiling invariant: no pixel is wuss_FURNITURE_NONE, and a pixel + * reports wuss_FURNITURE_CONTENT if and only if it lies inside the drawn + * content box -- with one documented exception, the bare outline band of a + * fully chromeless window (no titlebar, no scrollbars, no resize), where + * content_may_leak is passed non-zero to allow CONTENT on 1px-frame pixels + * outside the content box too. Returns 1 on pass, 0 on the first breach + * (printing the offending pixel). */ +static int furniture_hit_sweep(const wuss_window_t *window, + int content_may_leak) +{ + box_t visible, content; + int x, y, inside; + + wuss_window_get_visible_bounds((wuss_window_t *) window, &visible); + wuss__content_box(window, &content); + + for (y = visible.y0; y < visible.y1; y++) + { + for (x = visible.x0; x < visible.x1; x++) + { + wuss_furniture_region_t region; + + region = wuss__furniture_hit_test(window, POINT(x, y)); + inside = box_contains_point(&content, x, y); + + if (region == wuss_FURNITURE_NONE) + { + printf("wuss_test: hit sweep: (%d,%d) is FURNITURE_NONE\n", x, y); + return 0; + } + + if (inside && region != wuss_FURNITURE_CONTENT) + { + printf("wuss_test: hit sweep: content pixel (%d,%d) reported %d\n", + x, y, (int) region); + return 0; + } + + if (!inside && region == wuss_FURNITURE_CONTENT && !content_may_leak) + { + printf("wuss_test: hit sweep: chrome pixel (%d,%d) reported CONTENT\n", + x, y); + return 0; + } + } + } + + return 1; +} + result_t wuss_test(const char *resources) { result_t rc; @@ -4754,6 +4809,172 @@ ColourMenuOK: ; goto Failure; } + printf("test: furniture hit boxes tile the visible box with no leaks\n"); + + { + static test_task_t tc_hs; + wuss_task_t *delegate_hs; + box_t box_hs, visible, content; + box_t back, close, toggle, resize; + box_t vup, vdown, vwell, hleft, hright, hwell; + wuss_window_t *win_hs; + int midx, midy; + + tc_hs.redraw_count = 0; + tc_hs.mouse_count = 0; + delegate_hs = mk_task(wuss, test_handle, &tc_hs); + if (delegate_hs == NULL) goto Failure; + + /* 120x80 content: wide enough that BACK, CLOSE and TOGGLE_SIZE do not + * overlap in the titlebar. */ + box_hs.x0 = 5; box_hs.y0 = 5; + box_hs.x1 = 125; box_hs.y1 = 85; + rc = wuss_window_create(delegate_hs, &box_hs, "HS", wuss_WINDOW_NONE, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + SIZE2D(400, 400), SIZE2D(0, 0), &win_hs); + if (rc != result_OK) + goto Failure; + + wuss_window_get_visible_bounds(win_hs, &visible); + wuss_window_get_content_bounds(win_hs, &content); + wuss__back_box(win_hs, &back); + wuss__close_box(win_hs, &close); + wuss__toggle_box(win_hs, &toggle); + wuss__resize_box(win_hs, &resize); + wuss__vscroll_up_box(win_hs, &vup); + wuss__vscroll_down_box(win_hs, &vdown); + wuss__vscroll_well_box(win_hs, &vwell); + wuss__hscroll_left_box(win_hs, &hleft); + wuss__hscroll_right_box(win_hs, &hright); + wuss__hscroll_well_box(win_hs, &hwell); + + midx = (content.x0 + content.x1) / 2; + midy = (content.y0 + content.y1) / 2; + + /* outline band, each edge */ + if (wuss__furniture_hit_test(win_hs, POINT(midx, visible.y0)) != wuss_FURNITURE_TITLE) + goto Failure; + if (wuss__furniture_hit_test(win_hs, POINT(visible.x0, midy)) != wuss_FURNITURE_HSCROLL_WELL) + goto Failure; /* left outline column, level with the content: nearest chrome is the hscroll strip */ + if (wuss__furniture_hit_test(win_hs, POINT(visible.x1 - 1, midy)) != wuss_FURNITURE_VSCROLL_WELL) + goto Failure; + if (wuss__furniture_hit_test(win_hs, POINT(midx, visible.y1 - 1)) != wuss_FURNITURE_HSCROLL_WELL) + goto Failure; + + /* the four window corners belong to the adjacent furniture */ + if (wuss__furniture_hit_test(win_hs, POINT(visible.x0, visible.y0)) != wuss_FURNITURE_BACK) + goto Failure; + if (wuss__furniture_hit_test(win_hs, POINT(visible.x1 - 1, visible.y0)) != wuss_FURNITURE_TOGGLE_SIZE) + goto Failure; + if (wuss__furniture_hit_test(win_hs, POINT(visible.x1 - 1, visible.y1 - 1)) != wuss_FURNITURE_RESIZE) + goto Failure; + if (wuss__furniture_hit_test(win_hs, POINT(visible.x0, visible.y1 - 1)) != wuss_FURNITURE_HSCROLL_LEFT) + goto Failure; + + /* icon centres */ + if (wuss__furniture_hit_test(win_hs, POINT((back.x0 + back.x1) / 2, (back.y0 + back.y1) / 2)) != wuss_FURNITURE_BACK) + goto Failure; + if (wuss__furniture_hit_test(win_hs, POINT((close.x0 + close.x1) / 2, (close.y0 + close.y1) / 2)) != wuss_FURNITURE_CLOSE) + goto Failure; + if (wuss__furniture_hit_test(win_hs, POINT((toggle.x0 + toggle.x1) / 2, (toggle.y0 + toggle.y1) / 2)) != wuss_FURNITURE_TOGGLE_SIZE) + goto Failure; + + /* scroll-part centres */ + if (wuss__furniture_hit_test(win_hs, POINT((vup.x0 + vup.x1) / 2, (vup.y0 + vup.y1) / 2)) != wuss_FURNITURE_VSCROLL_UP) + goto Failure; + if (wuss__furniture_hit_test(win_hs, POINT((vdown.x0 + vdown.x1) / 2, (vdown.y0 + vdown.y1) / 2)) != wuss_FURNITURE_VSCROLL_DOWN) + goto Failure; + if (wuss__furniture_hit_test(win_hs, POINT((vwell.x0 + vwell.x1) / 2, (vwell.y0 + vwell.y1) / 2)) != wuss_FURNITURE_VSCROLL_WELL) + goto Failure; + if (wuss__furniture_hit_test(win_hs, POINT((hleft.x0 + hleft.x1) / 2, (hleft.y0 + hleft.y1) / 2)) != wuss_FURNITURE_HSCROLL_LEFT) + goto Failure; + if (wuss__furniture_hit_test(win_hs, POINT((hright.x0 + hright.x1) / 2, (hright.y0 + hright.y1) / 2)) != wuss_FURNITURE_HSCROLL_RIGHT) + goto Failure; + if (wuss__furniture_hit_test(win_hs, POINT((hwell.x0 + hwell.x1) / 2, (hwell.y0 + hwell.y1) / 2)) != wuss_FURNITURE_HSCROLL_WELL) + goto Failure; + + /* divider seam between the content and the scroll strips */ + if (wuss__furniture_hit_test(win_hs, POINT(content.x1, midy)) != wuss_FURNITURE_VSCROLL_WELL) + goto Failure; + if (wuss__furniture_hit_test(win_hs, POINT(midx, content.y1)) != wuss_FURNITURE_HSCROLL_WELL) + goto Failure; + if (wuss__furniture_hit_test(win_hs, POINT(content.x1, content.y1 - 1)) == wuss_FURNITURE_CONTENT) + goto Failure; + if (wuss__furniture_hit_test(win_hs, POINT(content.x1 - 1, content.y1)) == wuss_FURNITURE_CONTENT) + goto Failure; + + /* genuine content */ + if (wuss__furniture_hit_test(win_hs, POINT(content.x0 + 5, content.y0 + 5)) != wuss_FURNITURE_CONTENT) + goto Failure; + if (wuss__furniture_hit_test(win_hs, POINT(content.x1 - 1, content.y1 - 1)) != wuss_FURNITURE_CONTENT) + goto Failure; + + if (!furniture_hit_sweep(win_hs, 0)) + goto Failure; + + wuss_window_close(win_hs); + } + + printf("test: furniture hit tiling holds for every furniture-flag combo\n"); + + { + static const wuss_window_flags_t combos[] = + { + wuss_WINDOW_NO_OUTLINE, + wuss_WINDOW_NO_RESIZE, + wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL, + wuss_WINDOW_NO_BACK, + wuss_WINDOW_NO_TOGGLE_SIZE, + wuss_WINDOW_NO_CLOSE, + wuss_WINDOW_NO_TITLEBAR + }; + static test_task_t tc_cb; + wuss_task_t *delegate_cb; + box_t box_cb, visible; + wuss_window_t *win_cb; + unsigned int i; + + for (i = 0; i < sizeof combos / sizeof combos[0]; i++) + { + int leak; + + tc_cb.redraw_count = 0; + tc_cb.mouse_count = 0; + delegate_cb = mk_task(wuss, test_handle, &tc_cb); + if (delegate_cb == NULL) goto Failure; + + box_cb.x0 = 5; box_cb.y0 = 5; + box_cb.x1 = 125; box_cb.y1 = 85; + rc = wuss_window_create(delegate_cb, &box_cb, "CB", combos[i], + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + SIZE2D(400, 400), SIZE2D(0, 0), &win_cb); + if (rc != result_OK) + goto Failure; + + /* NO_TITLEBAR still keeps a 1px top frame with nothing behind it: a + * click there is the documented CONTENT exception. */ + leak = (combos[i] & wuss_WINDOW_NO_TITLEBAR) && + !(combos[i] & wuss_WINDOW_NO_OUTLINE); + + if (!furniture_hit_sweep(win_cb, leak)) + { + printf("wuss_test: combo index %u (flags 0x%X) failed the hit sweep\n", + i, (unsigned int) combos[i]); + goto Failure; + } + + if ((combos[i] & wuss_WINDOW_NO_BACK) && + wuss__furniture_hit_test(win_cb, POINT(box_cb.x0, box_cb.y0)) != wuss_FURNITURE_CLOSE) + { + wuss_window_get_visible_bounds(win_cb, &visible); + if (wuss__furniture_hit_test(win_cb, POINT(visible.x0, visible.y0)) != wuss_FURNITURE_CLOSE) + goto Failure; + } + + wuss_window_close(win_cb); + } + } + wuss_destroy(wuss); free(pixels); From 6e7c26334ad224bcc308b84350f62cda82cdff6d Mon Sep 17 00:00:00 2001 From: David Thomas Date: Mon, 7 Sep 2026 21:37:45 +0100 Subject: [PATCH 40/98] fix(wuss): give the resize icon dividing seams either side The vertical and horizontal scroll strips butted straight onto the resize box, so the icon read as merged with them. Pull each drawn strip's resize-corner end in by WUSS_DIVIDER_PX and draw a 1px seam in the freed gap along the resize box's top and left edges, meeting at the corner, in the interior-rule colour. Hit boxes are unchanged; they already over-tile the outline band independently of the drawn strip. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/furniture/draw.c | 31 ++++++++++++++++++++++++++- libraries/wuss/furniture/scroll-box.c | 4 ++-- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/libraries/wuss/furniture/draw.c b/libraries/wuss/furniture/draw.c index e9d8fc7..8356a6a 100644 --- a/libraries/wuss/furniture/draw.c +++ b/libraries/wuss/furniture/draw.c @@ -136,11 +136,40 @@ void wuss__furniture_draw(wuss_t *wuss, if (!(window->flags & wuss_WINDOW_NO_RESIZE)) { - box_t resize; + box_t resize, rule; + int top_seam, left_seam; wuss__resize_box(window, &resize); + + /* Dividing seams in the 1px gap the scroll strips leave (scroll_strip + * pulls each end in by WUSS_DIVIDER_PX): a rule along the resize box's top + * edge (vscroll strip above) and its left edge (hscroll strip to the + * left), in the interior rules' colour. */ + top_seam = (window->flags & wuss_WINDOW_NO_VSCROLL) ? 0 : WUSS_DIVIDER_PX; + left_seam = (window->flags & wuss_WINDOW_NO_HSCROLL) ? 0 : WUSS_DIVIDER_PX; + fill_furniture_rect(wuss, &resize, full, wuss->palette[wuss->furniture_colours.resize]); + + if (top_seam > 0) + { + rule.x0 = resize.x0 - left_seam; + rule.x1 = resize.x1; + rule.y0 = resize.y0 - top_seam; + rule.y1 = resize.y0; + fill_furniture_rect(wuss, &rule, full, + wuss->palette[wuss->furniture_colours.title.bg]); + } + + if (left_seam > 0) + { + rule.x0 = resize.x0 - left_seam; + rule.x1 = resize.x0; + rule.y0 = resize.y0 - top_seam; + rule.y1 = resize.y1; + fill_furniture_rect(wuss, &rule, full, + wuss->palette[wuss->furniture_colours.title.bg]); + } } if (!(window->flags & wuss_WINDOW_NO_VSCROLL)) diff --git a/libraries/wuss/furniture/scroll-box.c b/libraries/wuss/furniture/scroll-box.c index 69f6f71..f5c0dc4 100644 --- a/libraries/wuss/furniture/scroll-box.c +++ b/libraries/wuss/furniture/scroll-box.c @@ -27,7 +27,7 @@ static void scroll_strip(const wuss_window_t *window, out->y1 = window->visible.y1 - outline_px; out->y0 = out->y1 - size; out->x0 = window->visible.x0 + outline_px; - out->x1 = window->visible.x1 - outline_px - size; /* clear of resize corner */ + out->x1 = window->visible.x1 - outline_px - size - WUSS_DIVIDER_PX; /* seam then resize corner */ } else { @@ -35,7 +35,7 @@ static void scroll_strip(const wuss_window_t *window, out->x0 = out->x1 - size; wuss__titlebar_box(window, &titlebar); out->y0 = titlebar.y1; - out->y1 = window->visible.y1 - outline_px - size; /* clear of resize corner */ + out->y1 = window->visible.y1 - outline_px - size - WUSS_DIVIDER_PX; /* seam then resize corner */ } } From a401c8a7c183e4eeb4f86f49335ed46c2cb2a5ae Mon Sep 17 00:00:00 2001 From: David Thomas Date: Mon, 7 Sep 2026 21:54:44 +0100 Subject: [PATCH 41/98] fix(wuss): stop pre-filling the desktop backdrop behind windows fill_backdrop_excluding_content only cut wuss_NO_BACKGROUND windows' content out of the desktop pre-fill, so every opaque window (and every window's furniture band) got painted with the desktop pattern and then immediately overdrawn -- wasted fill and, for a NO_BACKGROUND task that does not repaint every pixel, a flash of backdrop. Cut the whole visible box of every non-hidden window instead; each one opaquely repaints its whole visible box every redraw. With both scrollbars off and a resize icon on, the content box is still carved back button_size on its right and bottom and no scroll strip paints that margin. That margin is now no longer desktop-filled either, so wuss__furniture_draw fills the carved L-band with the chrome colour -- a single paint, no overdraw. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/TODO.txt | 2 ++ libraries/wuss/core/redraw.c | 16 ++++++------- libraries/wuss/furniture/draw.c | 40 +++++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 9 deletions(-) diff --git a/libraries/wuss/TODO.txt b/libraries/wuss/TODO.txt index 2288ef0..dfbd7fd 100644 --- a/libraries/wuss/TODO.txt +++ b/libraries/wuss/TODO.txt @@ -10,6 +10,8 @@ LAYER 0 - BUGS (fix before building on top) - Auto window placement packer still not working as expected blocks: reliable multi-window tasks - Image task not shrinking window when new image loads +- Backgrounds are being drawn behind window furniture. +- Optimise furniture layout (& cache it) LAYER 1 - CORE PLUMBING (no wuss deps; unblocks everything else) diff --git a/libraries/wuss/core/redraw.c b/libraries/wuss/core/redraw.c index 0dc826a..d06481a 100644 --- a/libraries/wuss/core/redraw.c +++ b/libraries/wuss/core/redraw.c @@ -3,10 +3,11 @@ #include "impl.h" /* Fill "area" with the desktop backdrop, except for whatever part of it is - * covered by a wuss_NO_BACKGROUND window's content -- that task owns those - * pixels outright, so a desktop-coloured pre-fill there would flash (or, - * for a task that doesn't repaint every pixel every time, permanently - * show) backdrop instead of whatever was already on screen. */ + * covered by a window's visible box -- every non-hidden window opaquely + * repaints its whole visible box each redraw (furniture over the chrome + * band, task/backdrop over the content), so a desktop pre-fill underneath + * is wasted work and, for a wuss_NO_BACKGROUND task that doesn't repaint + * every pixel, a flash of backdrop over what was there. */ static void fill_backdrop_excluding_content(wuss_t *wuss, const box_t *area) { box_t cuts[WUSS_MAX_INVALIDATE_PIECES]; @@ -20,16 +21,13 @@ static void fill_backdrop_excluding_content(wuss_t *wuss, const box_t *area) e = e->next) { wuss_window_t *win; - box_t content, clipped; + box_t clipped; win = wuss__window_from_link(e); if (win->flags & wuss_WINDOW_HIDDEN) continue; - if (win->bg.colour != wuss_NO_BACKGROUND) - continue; - wuss__content_box(win, &content); - if (box_intersection(&content, area, &clipped)) + if (box_intersection(&win->visible, area, &clipped)) continue; /* no overlap with the area being filled */ cuts[ncuts++] = clipped; diff --git a/libraries/wuss/furniture/draw.c b/libraries/wuss/furniture/draw.c index 8356a6a..cbf9b90 100644 --- a/libraries/wuss/furniture/draw.c +++ b/libraries/wuss/furniture/draw.c @@ -134,6 +134,46 @@ void wuss__furniture_draw(wuss_t *wuss, } } + /* With both scrollbars off but a resize icon on, the content box is still + * carved back button_size on its right and bottom (see + * wuss__furniture_carve_for) and no scroll strip paints that margin. It is + * chrome, so furniture owns it: fill it here. Nothing pre-fills it + * (fill_backdrop_excluding_content cuts the whole visible box), so this is + * the only paint of the region -- no redundant fill, no flicker. */ + if ((window->flags & wuss_WINDOW_NO_VSCROLL) && + (window->flags & wuss_WINDOW_NO_HSCROLL) && + !(window->flags & wuss_WINDOW_NO_RESIZE)) + { + box_t content, band; + point_t carve; + int outline_px; + + outline_px = wuss__outline_px(window); + wuss__content_box(window, &content); + wuss__furniture_carve_for(window->flags, wuss__button_size(window), &carve); + + if (carve.x > 0) + { + band.x0 = content.x1; + band.x1 = window->visible.x1 - outline_px; + band.y0 = window->visible.y0 + outline_px + + wuss__titlebar_height(window); + band.y1 = window->visible.y1 - outline_px; + fill_furniture_rect(wuss, &band, full, + wuss->palette[wuss->furniture_colours.title.bg]); + } + + if (carve.y > 0) + { + band.x0 = window->visible.x0 + outline_px; + band.x1 = content.x1; + band.y0 = content.y1; + band.y1 = window->visible.y1 - outline_px; + fill_furniture_rect(wuss, &band, full, + wuss->palette[wuss->furniture_colours.title.bg]); + } + } + if (!(window->flags & wuss_WINDOW_NO_RESIZE)) { box_t resize, rule; From 4fee28538b85f2f12745d5a6cd368ff48d9f3a5c Mon Sep 17 00:00:00 2001 From: David Thomas Date: Mon, 7 Sep 2026 22:12:58 +0100 Subject: [PATCH 42/98] feat(wuss): add Saturn task, a ringed-planet dot sketch Port of a cryptic BBC BASIC MODE 4 one-liner: three rejection-sampled point clouds - an outer ring, a sheared shadow band across it and a filled disc for the planet body - stippled into a 256x256 window. RND(n) is stood in by a small LCG so a Select click can re-seed for a fresh sketch; the RISC OS (255 - y) plot flip is kept verbatim so the image lands the same way up as on a BBC/RISC OS screen. wuss_EVENT_IDLE bumps the seed and invalidates, so it churns a new sketch every null event. Co-Authored-By: Claude Sonnet 5 --- CMakeLists.txt | 1 + apps/wuss/tasks.c | 15 +- libraries/wuss/test/tasks/saturn.c | 227 +++++++++++++++++++++++++++++ libraries/wuss/test/tasks/saturn.h | 34 +++++ 4 files changed, 276 insertions(+), 1 deletion(-) create mode 100644 libraries/wuss/test/tasks/saturn.c create mode 100644 libraries/wuss/test/tasks/saturn.h diff --git a/CMakeLists.txt b/CMakeLists.txt index e184622..6e0adb7 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -735,6 +735,7 @@ if(BUILD_APPS) libraries/wuss/test/tasks/minesweeper.c libraries/wuss/test/tasks/palette.c libraries/wuss/test/tasks/porter-duff.c + libraries/wuss/test/tasks/saturn.c libraries/wuss/test/tasks/sofa.c libraries/wuss/test/tasks/swatches.c libraries/wuss/test/tasks/text.c) diff --git a/apps/wuss/tasks.c b/apps/wuss/tasks.c index 2d97388..66b9a57 100644 --- a/apps/wuss/tasks.c +++ b/apps/wuss/tasks.c @@ -31,6 +31,7 @@ #include "tasks/minesweeper.h" #include "tasks/palette.h" #include "tasks/porter-duff.h" +#include "tasks/saturn.h" #include "tasks/sofa.h" #include "tasks/swatches.h" #include "tasks/text.h" @@ -187,6 +188,17 @@ static result_t spawn_minesweeper(void) return result_OK; } +static result_t spawn_saturn(void) +{ + saturn_task_t *t = calloc(1, sizeof(*t)); + result_t rc; + if (t == NULL) return result_OOM; + rc = saturn_create(g.wuss, t); + if (rc != result_OK) return rc; + if (t->window == NULL) { free(t); return rc; } + return result_OK; +} + static result_t spawn_sofa(void) { sofa_task_t *t = calloc(1, sizeof(*t)); @@ -281,6 +293,7 @@ static const wuss_menu_item_t g_launch_items[] = { "Minesweeper", wuss_MENU_ITEM_NONE, NULL }, { "Palette", wuss_MENU_ITEM_NONE, NULL }, { "Porter-Duff", wuss_MENU_ITEM_NONE, NULL }, + { "Saturn", wuss_MENU_ITEM_NONE, NULL }, { "Sofa", wuss_MENU_ITEM_NONE, NULL }, { "Swatches", wuss_MENU_ITEM_NONE, NULL }, { "Text", wuss_MENU_ITEM_NONE, NULL } @@ -291,7 +304,7 @@ static const task_spawn_fn_t g_launch_spawn[] = spawn_ball, spawn_blank, spawn_chars, spawn_checker, spawn_clock, spawn_curve, spawn_gradient, spawn_greeble, spawn_icons, spawn_image, spawn_lissajous, spawn_minesweeper, spawn_palette, - spawn_porter_duff, spawn_sofa, spawn_swatches, spawn_text + spawn_porter_duff, spawn_saturn, spawn_sofa, spawn_swatches, spawn_text }; static const wuss_menu_t g_launch_menu = diff --git a/libraries/wuss/test/tasks/saturn.c b/libraries/wuss/test/tasks/saturn.c new file mode 100644 index 0000000..098edca --- /dev/null +++ b/libraries/wuss/test/tasks/saturn.c @@ -0,0 +1,227 @@ +/* wuss/test/tasks/saturn.c -- Elite loading-screen planet, recreated */ + +#ifdef WUSS_APP + +#include +#include + +#ifdef FORTIFY +#include "fortify/fortify.h" +#endif + +#include "base/utils.h" +#include "framebuf/palettes.h" +#include "geom/box.h" + +#include "saturn.h" + +/* A recreation of the ringed planet from the loading screen of Elite (Ian + * Bell and David Braben, Acornsoft, 1984), the stippled Saturn-like world + * the BBC Micro drew while the game loaded from tape. + * + * The source is one line of BBC BASIC (MODE 4, VDU5) that seeds the RNG from + * TIME then runs three FOR loops, each rejection-sampling random points and + * PLOT 69 (plot single point) at the survivors. In MODE 4 the plot area is + * 1280x1024 OS units, so the original scales its -128..127 sample space by 4 + * and plots each point at y = (255 - (v + 128)). RISC OS screen coords run + * bottom-up and wuss's run top-down, so that same expression - kept verbatim + * here, only without the *4 - lands the sketch the same way up as it drew on + * a BBC/RISC OS screen. The shear in loop 2 is asymmetric in y, so this is + * the term that actually decides which way the ring's shadow band tilts. + * + * loop 1: X%,Y% in -127..127; keep if (X*X+Y*Y)/256 > 17 -> the ring + * loop 2: sheared X% = R6+R5/4, Y% = R6; keep if 32<=E<80 and + * (R5<0 or (X*X+Y*Y)/256 > 16) -> ring shadow band + * loop 3: R1,R2 random; keep if R1*R1+R2*R2 < 16384; + * x = sqrt(16384-P)/2 -> planet body + */ + +#define SATURN_SIZE 256 /* window is SATURN_SIZE x SATURN_SIZE */ + +/* BBC BASIC RND(n>0) returns an integer 1..n. A small LCG stands in for it so + * a Select click can re-seed for a fresh sketch. */ +static unsigned long saturn_rnd_state; + +static void saturn_rnd_seed(unsigned long seed) +{ + saturn_rnd_state = seed ? seed : 1; +} + +static int saturn_rnd(int n) +{ + saturn_rnd_state = saturn_rnd_state * 1664525UL + 1013904223UL; + return (int) ((saturn_rnd_state >> 16) % (unsigned long) n) + 1; +} + +result_t saturn_create(wuss_t *wuss, saturn_task_t *task) +{ + result_t rc; + wuss_task_t *delegate; + wuss_task_desc_t delegate_desc; + + task->bg = colour_rgb(0x00, 0x00, 0x00); + task->fg = colour_rgb(0xFF, 0xFF, 0xFF); + task->seed = 1; + + /* saturn_redraw paints its own background */ + delegate_desc.handle = saturn_handle; + delegate_desc.task_data = task; + delegate_desc.name = "saturn"; + rc = wuss_task_create(wuss, &delegate_desc, &delegate); + if (rc != result_OK) + { + free(task); /* nothing registered yet; the spawner will not free it */ + return rc; + } + wuss_task_set_autoclose(delegate, 1); + + rc = wuss_window_create_placed(delegate, + SIZE2D(SATURN_SIZE, SATURN_SIZE), + "Saturn", + wuss_WINDOW_NONE, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + SIZE2D(SATURN_SIZE, SATURN_SIZE), + SIZE2D(0, 0), + &task->window); + if (rc != result_OK) + wuss_task_destroy(delegate); /* unregister; its QUIT frees the task block */ + + return rc; +} + +/* plot one point in window content space, clipped to the window. x,y are + * already in the window's top-down pixel space (callers apply the RISC OS + * (255 - ...) flip). */ +static void saturn_plot(screen_t *scr, + int ox, + int oy, + int x, + int y, + colour_t c) +{ + if (x < 0 || x >= SATURN_SIZE || y < 0 || y >= SATURN_SIZE) + return; + + screen_set_pixel(scr, ox + x, oy + y, c); +} + +static result_t saturn_redraw(const wuss_event_t *event, saturn_task_t *task) +{ + screen_t *scr; + const box_t *content, *bounds; + int ox, oy; + int i; + int x, y, p; + + scr = event->data.redraw.scr; + content = event->data.redraw.content; + bounds = event->data.redraw.bounds; + + screen_fill_rect(scr, content->x0, content->y0, box_size(content), task->bg); + + /* no scroll: window is fixed at SATURN_SIZE and non-resizable */ + ox = bounds->x0; + oy = bounds->y0; + + saturn_rnd_seed(task->seed); + + /* loop 1 - the ring: keep points outside the inner disc */ + for (i = 0; i <= 477; i++) + { + x = saturn_rnd(255) - 128; + y = saturn_rnd(255) - 128; + p = (x * x + y * y) / 256; + if (p > 17) + saturn_plot(scr, ox, oy, x + 128, 255 - (y + 128), task->fg); + } + + /* loop 2 - ring shadow band: sheared sample with a banded energy gate */ + for (i = 0; i <= 1280; i++) + { + int r5, r6, r7, e; + + r5 = saturn_rnd(255) - 128; + r6 = saturn_rnd(255) - 128; + r7 = r5 / 4; + x = r6 + r7; + y = r6; + p = (x * x + y * y) / 256; + e = ((r6 + r7) * (r6 + r7) + r5 * r5 + r6 * r6) / 256; + if (e >= 32 && e < 80 && (r5 < 0 || p > 16)) + saturn_plot(scr, ox, oy, x + 128, y + 128, task->fg); + } + + /* loop 3 - planet body: filled half-disc offset right */ + for (i = 0; i <= 1280; i++) + { + int r1, r2; + + r1 = saturn_rnd(255) - 128; + r2 = saturn_rnd(255) - 128; + p = r1 * r1 + r2 * r2; + if (p < 16384) + { + x = (int) (sqrt((double) (16384 - p)) / 2.0) + 128; + y = 255 - (r2 / 2 + 128); + saturn_plot(scr, ox, oy, x, y, task->fg); + } + } + + return result_OK; +} + +static result_t saturn_idle(saturn_task_t *task) +{ + task->seed += 0x9E3779B9UL; /* churn: a fresh sketch every null event */ + wuss_window_invalidate_all(task->window); + + return result_OK; +} + +static result_t saturn_mouse(saturn_task_t *task, + wuss_mouse_action_t action, + wuss_button_t button, + wuss_window_t *window) +{ + if (action != wuss_MOUSE_DOWN) + return result_OK; + + if (button & wuss_BUTTON_SELECT) + { + task->seed += 0x9E3779B9UL; /* fresh sketch */ + wuss_window_invalidate_all(window); + } + + return result_OK; +} + +result_t saturn_handle(wuss_window_t *window, + const wuss_event_t *event, + void *task_data) +{ + saturn_task_t *task; + + task = task_data; + + switch (event->kind) + { + case wuss_EVENT_REDRAW: + return saturn_redraw(event, task); + + case wuss_EVENT_MOUSE: + return saturn_mouse(task, event->data.mouse.action, + event->data.mouse.button, window); + + case wuss_EVENT_IDLE: + return saturn_idle(task); + + case wuss_EVENT_QUIT: + free(task); /* task_data was calloc'd per instance by the spawner */ + return result_OK; + + default: + return result_OK; + } +} + +#endif /* WUSS_APP */ diff --git a/libraries/wuss/test/tasks/saturn.h b/libraries/wuss/test/tasks/saturn.h new file mode 100644 index 0000000..d9a6630 --- /dev/null +++ b/libraries/wuss/test/tasks/saturn.h @@ -0,0 +1,34 @@ +/* wuss/test/tasks/saturn.h -- Elite loading-screen planet, recreated */ + +#ifndef TASKS_SATURN_H +#define TASKS_SATURN_H + +#ifdef WUSS_APP + +#include "framebuf/colour.h" +#include "wuss/window.h" + +/* window task: recreates the ringed planet from the loading screen of + * Elite (Ian Bell and David Braben, Acornsoft, 1984). A cryptic BBC BASIC + * one-liner stipples it out of three rejection-sampled point clouds - a + * ring of dots, a sheared streak across it and a filled disc for the + * planet body. Select re-seeds the RNG for a fresh sketch; the idle + * handler re-seeds every null event so it churns. The plot is + * deterministic in the seed. */ +typedef struct saturn_task +{ + wuss_window_t *window; + colour_t bg, fg; + unsigned long seed; /* RNG state; a Select click bumps it */ +} +saturn_task_t; + +wuss_window_fn_t saturn_handle; + +/* create the planet window against the given wuss instance; "task" is a + * per-instance block owned by the window and freed when it closes */ +result_t saturn_create(wuss_t *wuss, saturn_task_t *task); + +#endif /* WUSS_APP */ + +#endif /* TASKS_SATURN_H */ From 0a1c8d5ee5c163d47dc441ee8ddd0052a63cfd1c Mon Sep 17 00:00:00 2001 From: David Thomas Date: Mon, 7 Sep 2026 22:42:06 +0100 Subject: [PATCH 43/98] refactor(wuss): hoist saturn's magic constants to named defines The three sample loops repeated 255/128/256, the loop counts and the 16384 planet radius inline. Name them, and wrap the repeated saturn_rnd(255)-128 sample expression as SATURN_SAMPLE(). --- libraries/wuss/test/tasks/saturn.c | 52 +++++++++++++++++++----------- 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/libraries/wuss/test/tasks/saturn.c b/libraries/wuss/test/tasks/saturn.c index 098edca..57fa4a9 100644 --- a/libraries/wuss/test/tasks/saturn.c +++ b/libraries/wuss/test/tasks/saturn.c @@ -36,7 +36,19 @@ * x = sqrt(16384-P)/2 -> planet body */ -#define SATURN_SIZE 256 /* window is SATURN_SIZE x SATURN_SIZE */ +#define SATURN_SIZE 256 /* window is SATURN_SIZE x SATURN_SIZE */ + +/* The original works in a -128..127 sample space (BBC BASIC signed byte). */ +#define SATURN_HALF 128 /* sample-space centre / bias */ +#define SATURN_RANGE 255 /* saturn_rnd() span: 1..255, then -HALF */ +#define SATURN_FLIP 255 /* RISC OS bottom-up -> wuss top-down: FLIP - v */ +#define SATURN_ENERGY_SHIFT 256 /* (x*x+y*y) energy divisor */ + +#define SATURN_RING_ITERS 477 /* loop 1: the ring */ +#define SATURN_BAND_ITERS 1280 /* loop 2: ring shadow band */ +#define SATURN_BODY_ITERS 1280 /* loop 3: planet body */ + +#define SATURN_BODY_R2 16384 /* planet body: keep if r1*r1+r2*r2 < this */ /* BBC BASIC RND(n>0) returns an integer 1..n. A small LCG stands in for it so * a Select click can re-seed for a fresh sketch. */ @@ -53,6 +65,9 @@ static int saturn_rnd(int n) return (int) ((saturn_rnd_state >> 16) % (unsigned long) n) + 1; } +/* one signed sample in the original -128..127 space */ +#define SATURN_SAMPLE() (saturn_rnd(SATURN_RANGE) - SATURN_HALF) + result_t saturn_create(wuss_t *wuss, saturn_task_t *task) { result_t rc; @@ -126,43 +141,44 @@ static result_t saturn_redraw(const wuss_event_t *event, saturn_task_t *task) saturn_rnd_seed(task->seed); /* loop 1 - the ring: keep points outside the inner disc */ - for (i = 0; i <= 477; i++) + for (i = 0; i <= SATURN_RING_ITERS; i++) { - x = saturn_rnd(255) - 128; - y = saturn_rnd(255) - 128; - p = (x * x + y * y) / 256; + x = SATURN_SAMPLE(); + y = SATURN_SAMPLE(); + p = (x * x + y * y) / SATURN_ENERGY_SHIFT; if (p > 17) - saturn_plot(scr, ox, oy, x + 128, 255 - (y + 128), task->fg); + saturn_plot(scr, ox, oy, x + SATURN_HALF, SATURN_FLIP - (y + SATURN_HALF), + task->fg); } /* loop 2 - ring shadow band: sheared sample with a banded energy gate */ - for (i = 0; i <= 1280; i++) + for (i = 0; i <= SATURN_BAND_ITERS; i++) { int r5, r6, r7, e; - r5 = saturn_rnd(255) - 128; - r6 = saturn_rnd(255) - 128; + r5 = SATURN_SAMPLE(); + r6 = SATURN_SAMPLE(); r7 = r5 / 4; x = r6 + r7; y = r6; - p = (x * x + y * y) / 256; - e = ((r6 + r7) * (r6 + r7) + r5 * r5 + r6 * r6) / 256; + p = (x * x + y * y) / SATURN_ENERGY_SHIFT; + e = ((r6 + r7) * (r6 + r7) + r5 * r5 + r6 * r6) / SATURN_ENERGY_SHIFT; if (e >= 32 && e < 80 && (r5 < 0 || p > 16)) - saturn_plot(scr, ox, oy, x + 128, y + 128, task->fg); + saturn_plot(scr, ox, oy, x + SATURN_HALF, y + SATURN_HALF, task->fg); } /* loop 3 - planet body: filled half-disc offset right */ - for (i = 0; i <= 1280; i++) + for (i = 0; i <= SATURN_BODY_ITERS; i++) { int r1, r2; - r1 = saturn_rnd(255) - 128; - r2 = saturn_rnd(255) - 128; + r1 = SATURN_SAMPLE(); + r2 = SATURN_SAMPLE(); p = r1 * r1 + r2 * r2; - if (p < 16384) + if (p < SATURN_BODY_R2) { - x = (int) (sqrt((double) (16384 - p)) / 2.0) + 128; - y = 255 - (r2 / 2 + 128); + x = (int) (sqrt((double) (SATURN_BODY_R2 - p)) / 2.0) + SATURN_HALF; + y = SATURN_FLIP - (r2 / 2 + SATURN_HALF); saturn_plot(scr, ox, oy, x, y, task->fg); } } From 8020cfcdb9c2c728c4f58863f6e0f65fdef32e24 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Mon, 7 Sep 2026 23:12:39 +0100 Subject: [PATCH 44/98] feat(framebuf): add screen_fill_rects multi-rect fill op Fills several box_t rectangles in one call, all in the same colour; equivalent to a loop of screen_fill_rect. Use it for the four one-pixel window outline edges in wuss__furniture_draw, replacing four calls with one. Co-Authored-By: Claude Sonnet 5 --- include/framebuf/screen.h | 17 +++++++++++++++++ libraries/framebuf/screen/screen-draw.c | 15 +++++++++++++++ libraries/wuss/furniture/draw.c | 22 ++++++++++++++-------- 3 files changed, 46 insertions(+), 8 deletions(-) diff --git a/include/framebuf/screen.h b/include/framebuf/screen.h index 94d6c87..6c9b668 100644 --- a/include/framebuf/screen.h +++ b/include/framebuf/screen.h @@ -81,6 +81,23 @@ void screen_fill_rect(screen_t *scr, size2d_t size, colour_t colour); +/** + * Fills several solid rectangles in one call, all in the same colour. + * + * Equivalent to calling `screen_fill_rect` once per box. A non-positive + * `nboxes` draws nothing. Each box is inclusive-exclusive and clipped + * independently to the screen's clip region. + * + * \param[in] scr Screen to draw upon. + * \param[in] boxes Array of boxes to fill. + * \param[in] nboxes Number of boxes in `boxes`. + * \param[in] colour Colour of the rectangles. + */ +void screen_fill_rects(screen_t *scr, + const box_t *boxes, + int nboxes, + colour_t colour); + /** * Special case of `screen_fill_rect`. * diff --git a/libraries/framebuf/screen/screen-draw.c b/libraries/framebuf/screen/screen-draw.c index da1f5d4..858a6b8 100644 --- a/libraries/framebuf/screen/screen-draw.c +++ b/libraries/framebuf/screen/screen-draw.c @@ -193,6 +193,21 @@ void screen_fill_rect(screen_t *scr, screen_fill_hline(scr, draw_box.x0, yy, draw_box.x1 - draw_box.x0, colour); } +void screen_fill_rects(screen_t *scr, + const box_t *boxes, + int nboxes, + colour_t colour) +{ + int i; + + for (i = 0; i < nboxes; i++) + screen_fill_rect(scr, + boxes[i].x0, + boxes[i].y0, + box_size(&boxes[i]), + colour); +} + void screen_fill_square(screen_t *scr, int x, int y, diff --git a/libraries/wuss/furniture/draw.c b/libraries/wuss/furniture/draw.c index cbf9b90..d1235e1 100644 --- a/libraries/wuss/furniture/draw.c +++ b/libraries/wuss/furniture/draw.c @@ -100,7 +100,8 @@ void wuss__furniture_draw(wuss_t *wuss, pos.y = titlebar.y0 + 2; wuss->scr->clip = text_clip; bmfont_draw(titlefont, wuss->scr, window->title, titlelen, - wuss->palette[wuss->furniture_colours.title.fg], wuss->palette[wuss->furniture_colours.title.bg], + wuss->palette[wuss->furniture_colours.title.fg], + wuss->palette[wuss->furniture_colours.title.bg], &pos, NULL); wuss->scr->clip = clipped; } @@ -286,17 +287,22 @@ void wuss__furniture_draw(wuss_t *wuss, if (!(window->flags & wuss_WINDOW_NO_OUTLINE)) { - int width, height; colour_t border; + box_t edges[4]; - width = window->visible.x1 - window->visible.x0; - height = window->visible.y1 - window->visible.y0; border = wuss->palette[wuss->furniture_colours.outline]; + /* top, bottom, left, right -- one pixel thick each */ + edges[0].x0 = window->visible.x0; edges[0].y0 = window->visible.y0; + edges[0].x1 = window->visible.x1; edges[0].y1 = window->visible.y0 + 1; + edges[1].x0 = window->visible.x0; edges[1].y0 = window->visible.y1 - 1; + edges[1].x1 = window->visible.x1; edges[1].y1 = window->visible.y1; + edges[2].x0 = window->visible.x0; edges[2].y0 = window->visible.y0; + edges[2].x1 = window->visible.x0 + 1; edges[2].y1 = window->visible.y1; + edges[3].x0 = window->visible.x1 - 1; edges[3].y0 = window->visible.y0; + edges[3].x1 = window->visible.x1; edges[3].y1 = window->visible.y1; + wuss->scr->clip = visible_clipped; - screen_fill_rect(wuss->scr, window->visible.x0, window->visible.y0, SIZE2D(width, 1), border); - screen_fill_rect(wuss->scr, window->visible.x0, window->visible.y1 - 1, SIZE2D(width, 1), border); - screen_fill_rect(wuss->scr, window->visible.x0, window->visible.y0, SIZE2D(1, height), border); - screen_fill_rect(wuss->scr, window->visible.x1 - 1, window->visible.y0, SIZE2D(1, height), border); + screen_fill_rects(wuss->scr, edges, 4, border); } } From 8bde3e106e967c1d0a82c3b4444b0c8085ecb1b7 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Mon, 7 Sep 2026 23:38:17 +0100 Subject: [PATCH 45/98] perf(wuss): cache furniture layout, split paint into two phases wuss__furniture_draw recomputed every furniture rectangle from window geometry on every redraw. Split it: phase 1 (wuss__furniture_layout_build, new furniture/layout.c) computes every filled rect once and caches it in struct wuss_window; phase 2 (draw.c) walks the cache clipping each rect to the redraw region and filling it. The cache is dropped by wuss__furniture_invalidate_for -- the existing choke point for resize, toggle-size and scrollbar-flag changes -- and by wuss_window_move, since the cached rects hold absolute screen coords. It stores a paint-class tag per rect rather than a resolved colour, so a palette change needs no rebuild. The two scrollbar sausages track window->scroll and are still computed live each paint; the title string is font-dependent and also drawn live. Co-Authored-By: Claude Sonnet 5 --- CMakeLists.txt | 1 + libraries/wuss/TODO.txt | 1 - libraries/wuss/core/impl.h | 14 + libraries/wuss/core/window/create.c | 1 + libraries/wuss/core/window/move.c | 4 + libraries/wuss/furniture.h | 51 ++++ libraries/wuss/furniture/draw.c | 351 ++++++++------------------ libraries/wuss/furniture/invalidate.c | 4 + libraries/wuss/furniture/layout.c | 226 +++++++++++++++++ 9 files changed, 413 insertions(+), 240 deletions(-) create mode 100644 libraries/wuss/furniture/layout.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 6e0adb7..6a0ec32 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -399,6 +399,7 @@ set(WUSS_FURNITURE_SOURCES libraries/wuss/furniture/draw.c libraries/wuss/furniture/hit-test.c libraries/wuss/furniture/invalidate.c + libraries/wuss/furniture/layout.c libraries/wuss/furniture/ops.c libraries/wuss/furniture/resize-box.c libraries/wuss/furniture/scroll-action.c diff --git a/libraries/wuss/TODO.txt b/libraries/wuss/TODO.txt index dfbd7fd..4139e5d 100644 --- a/libraries/wuss/TODO.txt +++ b/libraries/wuss/TODO.txt @@ -11,7 +11,6 @@ LAYER 0 - BUGS (fix before building on top) blocks: reliable multi-window tasks - Image task not shrinking window when new image loads - Backgrounds are being drawn behind window furniture. -- Optimise furniture layout (& cache it) LAYER 1 - CORE PLUMBING (no wuss deps; unblocks everything else) diff --git a/libraries/wuss/core/impl.h b/libraries/wuss/core/impl.h index 0f19ae6..feef029 100644 --- a/libraries/wuss/core/impl.h +++ b/libraries/wuss/core/impl.h @@ -207,6 +207,7 @@ struct wuss_window #ifdef WUSS_FURNITURE box_t pre_toggle; /* visible bounds to restore on the next toggle */ char title[WUSS_TITLE_MAX + 1]; + wuss__furniture_layout_t furniture_layout; /* lazily built; see furniture.h */ #endif #ifdef WUSS_ICONS wuss_icon_t **icons; /* owned; array of owned icon pointers */ @@ -240,6 +241,14 @@ static inline void wuss__chrome_repaint_for(wuss_window_t *window, { window->wuss->furniture_ops->invalidate_for(window, visible); } + +/* Drop just the cached furniture layout, without queuing any dirty region -- + * for a window move, where the caller already handles the repaint but the + * cache (absolute coords) must be rebuilt for the new position. */ +static inline void wuss__chrome_invalidate_layout(wuss_window_t *window) +{ + window->furniture_layout.valid = 0; +} #else static inline void wuss__chrome_draw(wuss_t *wuss, wuss_window_t *window, @@ -261,6 +270,11 @@ static inline void wuss__chrome_repaint_for(wuss_window_t *window, (void) window; (void) visible; } + +static inline void wuss__chrome_invalidate_layout(wuss_window_t *window) +{ + (void) window; +} #endif wuss_window_t *wuss__window_at(wuss_t *wuss, point_t p); diff --git a/libraries/wuss/core/window/create.c b/libraries/wuss/core/window/create.c index a48ed7b..941807f 100644 --- a/libraries/wuss/core/window/create.c +++ b/libraries/wuss/core/window/create.c @@ -97,6 +97,7 @@ result_t wuss_window_create(wuss_task_t *task, win->min_doc = min_doc; #ifdef WUSS_FURNITURE win->state = wuss_WINDOW_STATE_NONE; + win->furniture_layout.valid = 0; /* built lazily on first paint */ #endif #ifdef WUSS_ICONS win->icons = NULL; diff --git a/libraries/wuss/core/window/move.c b/libraries/wuss/core/window/move.c index 9c96a85..6d1357d 100644 --- a/libraries/wuss/core/window/move.c +++ b/libraries/wuss/core/window/move.c @@ -18,6 +18,10 @@ void wuss_window_move(wuss_window_t *window, point_t p) * slot back and stop tracking this window's position */ wuss__release_packed(window); + /* the cached furniture layout holds absolute screen coords, so a move + * stales it -- both exit paths below rewrite window->visible */ + wuss__chrome_invalidate_layout(window); + width = window->visible.x1 - window->visible.x0; height = window->visible.y1 - window->visible.y0; outline_px = wuss__outline_px(window); diff --git a/libraries/wuss/furniture.h b/libraries/wuss/furniture.h index bc03e1a..79ffe8d 100644 --- a/libraries/wuss/furniture.h +++ b/libraries/wuss/furniture.h @@ -12,6 +12,57 @@ #define WUSS_SCROLL_END_GAP 2 /* sausage along-axis margin from its well's ends, purely cosmetic */ #define WUSS_SCROLL_STEP 20 /* pixels stepped per scrollbar arrow click */ +/* Cached furniture layout ------------------------------------------------- */ + +/* Which chrome colour a cached furniture rect is painted in. The layout + * stores this class rather than a resolved colour so a palette change needs + * no layout rebuild: wuss__furniture_draw resolves it through wuss->palette + * at paint time. */ +typedef enum wuss__furniture_paint_class +{ + wuss__FURNITURE_PAINT_TITLE_BG, /* titlebar fill, bands, seams, interior rules */ + wuss__FURNITURE_PAINT_CLOSE, + wuss__FURNITURE_PAINT_BACK, + wuss__FURNITURE_PAINT_TOGGLE, + wuss__FURNITURE_PAINT_RESIZE, + wuss__FURNITURE_PAINT_SCROLL_ARROWS, + wuss__FURNITURE_PAINT_SCROLL_WELLS, + wuss__FURNITURE_PAINT_OUTLINE +} +wuss__furniture_paint_class_t; + +/* One filled rectangle in the cached layout. */ +typedef struct wuss__furniture_piece +{ + box_t rect; + wuss__furniture_paint_class_t paint; +} +wuss__furniture_piece_t; + +/* Upper bound on pieces a single window's furniture can contribute: + * titlebar(1) + close/back/toggle(3) + no-scroll resize bands(2) + + * resize icon + its two seams(3) + v/h scroll arrows+well(3+3) + + * two interior rules(2) + four outline edges(4) = 21. Round up. */ +#define WUSS__FURNITURE_MAX_PIECES 24 + +/* Per-window cache of the furniture layout: every filled rect except the two + * scrollbar sausages (which move with window->scroll and are recomputed each + * paint) and the title text (font-dependent, drawn live). Rebuilt lazily by + * wuss__furniture_draw when "valid" is 0; wuss__furniture_invalidate* clear + * it on any geometry change. */ +typedef struct wuss__furniture_layout +{ + int valid; + int npieces; + wuss__furniture_piece_t pieces[WUSS__FURNITURE_MAX_PIECES]; + box_t titlebar; /* for the live title-text pass; empty if no titlebar */ + int has_titlebar; +} +wuss__furniture_layout_t; + +/* Populate window->furniture_layout from the current geometry and flags. */ +void wuss__furniture_layout_build(wuss_window_t *window); + /* Which region of a window's border (or its content) a point falls in. */ typedef enum wuss_furniture_region { diff --git a/libraries/wuss/furniture/draw.c b/libraries/wuss/furniture/draw.c index d1235e1..5c4e593 100644 --- a/libraries/wuss/furniture/draw.c +++ b/libraries/wuss/furniture/draw.c @@ -1,5 +1,13 @@ /* wuss/furniture/draw.c -- wuss - minimal window manager */ +/* Two-phase furniture paint. Phase 1 (wuss__furniture_layout_build, cached) + * computes every filled rect from the window geometry. Phase 2 (here) walks + * the cache clipping each rect to the redraw region "full" and filling it. + * The two scrollbar sausages track window->scroll so they are recomputed + * each paint rather than cached; the title string is font-dependent and + * drawn live. */ + +#include #include #include "base/utils.h" @@ -25,210 +33,131 @@ static void fill_furniture_rect(wuss_t *wuss, screen_fill_rect(wuss->scr, b->x0, b->y0, box_size(b), colour); } -void wuss__furniture_draw(wuss_t *wuss, - wuss_window_t *window, - const box_t *full) +/* Resolve a cached piece's paint class to a concrete palette colour. */ +static colour_t paint_colour(const wuss_t *wuss, + wuss__furniture_paint_class_t paint) { - box_t visible_clipped; - box_t clipped; - box_t titlebar; - bmfont_t *titlefont; - - /* window titles are drawn in the bold weight (font slot 1) when one was - * supplied, falling back to the system font */ - titlefont = (wuss->nfonts > 1 && wuss->fonts[1] != NULL) - ? wuss->fonts[1] - : wuss->fonts[0]; + const wuss_furniture_palette_t *fc; - if (box_intersection(&window->visible, full, &visible_clipped)) - return; /* offscreen */ + fc = &wuss->furniture_colours; - wuss__titlebar_box(window, &titlebar); - if (!box_intersection(&titlebar, full, &clipped)) + switch (paint) { - wuss->scr->clip = clipped; - screen_fill_rect(wuss->scr, - titlebar.x0, titlebar.y0, box_size(&titlebar), - wuss->palette[wuss->furniture_colours.title.bg]); - - if (titlefont != NULL && window->title[0] != '\0') - { - point_t pos; - int text_x0, text_x1, titlelen, split_point; - bmfont_width_t width; - box_t text_box, text_clip; - - text_x0 = titlebar.x0 + 2; - if (!(window->flags & wuss_WINDOW_NO_CLOSE)) - { - box_t close; - - wuss__close_box(window, &close); - text_x0 = close.x1 + 2; - } - else if (!(window->flags & wuss_WINDOW_NO_BACK)) - { - box_t back; - - wuss__back_box(window, &back); - text_x0 = back.x1 + 2; - } - - text_x1 = titlebar.x1 - 2; - if (!(window->flags & wuss_WINDOW_NO_TOGGLE_SIZE)) - { - box_t toggle; - - wuss__toggle_box(window, &toggle); - text_x1 = toggle.x0 - 2; - } - - /* A title too wide for its slot mustn't bleed into a neighbouring - * icon: clip drawing to the slot itself, not just the whole titlebar, - * so a too-long title is cut off cleanly rather than overdrawing - * whatever furniture the current redraw didn't happen to touch. */ - text_box.x0 = text_x0; - text_box.y0 = titlebar.y0; - text_box.x1 = text_x1; - text_box.y1 = titlebar.y1; - if (text_x1 > text_x0 && !box_intersection(&text_box, &clipped, &text_clip)) - { - titlelen = (int) strlen(window->title); - bmfont_measure(titlefont, window->title, titlelen, text_x1 - text_x0, &split_point, &width); - - pos.x = (split_point < titlelen) ? text_x0 : text_x0 + MAX(0, ((text_x1 - text_x0) - width) / 2); - pos.y = titlebar.y0 + 2; - wuss->scr->clip = text_clip; - bmfont_draw(titlefont, wuss->scr, window->title, titlelen, - wuss->palette[wuss->furniture_colours.title.fg], - wuss->palette[wuss->furniture_colours.title.bg], - &pos, NULL); - wuss->scr->clip = clipped; - } - } + case wuss__FURNITURE_PAINT_TITLE_BG: return wuss->palette[fc->title.bg]; + case wuss__FURNITURE_PAINT_CLOSE: return wuss->palette[fc->close]; + case wuss__FURNITURE_PAINT_BACK: return wuss->palette[fc->back]; + case wuss__FURNITURE_PAINT_TOGGLE: return wuss->palette[fc->toggle]; + case wuss__FURNITURE_PAINT_RESIZE: return wuss->palette[fc->resize]; + case wuss__FURNITURE_PAINT_SCROLL_ARROWS: return wuss->palette[fc->scroll.arrows]; + case wuss__FURNITURE_PAINT_SCROLL_WELLS: return wuss->palette[fc->scroll.wells]; + case wuss__FURNITURE_PAINT_OUTLINE: return wuss->palette[fc->outline]; + } - if (!(window->flags & wuss_WINDOW_NO_CLOSE)) - { - box_t close; + assert(!"unhandled furniture paint class"); + return wuss->palette[fc->title.bg]; +} - wuss__close_box(window, &close); - fill_furniture_rect(wuss, &close, full, - wuss->palette[wuss->furniture_colours.close]); - } +/* The title string, drawn into its titlebar slot. Split out of the main + * routine only to keep the phase-2 loop readable; still runs every paint + * because measurement depends on the current font. */ +static void draw_title(wuss_t *wuss, + wuss_window_t *window, + const box_t *titlebar, + const box_t *titlebar_clip) +{ + bmfont_t *titlefont; + point_t pos; + int text_x0, text_x1, titlelen, split_point; + bmfont_width_t width; + box_t text_box, text_clip; - if (!(window->flags & wuss_WINDOW_NO_BACK)) - { - box_t back; + if (window->title[0] == '\0') + return; - wuss__back_box(window, &back); - fill_furniture_rect(wuss, &back, full, - wuss->palette[wuss->furniture_colours.back]); - } + /* window titles are drawn in the bold weight (font slot 1) when one was + * supplied, falling back to the system font */ + titlefont = (wuss->nfonts > 1 && wuss->fonts[1] != NULL) + ? wuss->fonts[1] + : wuss->fonts[0]; + if (titlefont == NULL) + return; - if (!(window->flags & wuss_WINDOW_NO_TOGGLE_SIZE)) - { - box_t toggle; + text_x0 = titlebar->x0 + 2; + if (!(window->flags & wuss_WINDOW_NO_CLOSE)) + { + box_t close; - wuss__toggle_box(window, &toggle); - fill_furniture_rect(wuss, &toggle, full, - wuss->palette[wuss->furniture_colours.toggle]); - } + wuss__close_box(window, &close); + text_x0 = close.x1 + 2; } - - /* With both scrollbars off but a resize icon on, the content box is still - * carved back button_size on its right and bottom (see - * wuss__furniture_carve_for) and no scroll strip paints that margin. It is - * chrome, so furniture owns it: fill it here. Nothing pre-fills it - * (fill_backdrop_excluding_content cuts the whole visible box), so this is - * the only paint of the region -- no redundant fill, no flicker. */ - if ((window->flags & wuss_WINDOW_NO_VSCROLL) && - (window->flags & wuss_WINDOW_NO_HSCROLL) && - !(window->flags & wuss_WINDOW_NO_RESIZE)) + else if (!(window->flags & wuss_WINDOW_NO_BACK)) { - box_t content, band; - point_t carve; - int outline_px; + box_t back; - outline_px = wuss__outline_px(window); - wuss__content_box(window, &content); - wuss__furniture_carve_for(window->flags, wuss__button_size(window), &carve); + wuss__back_box(window, &back); + text_x0 = back.x1 + 2; + } - if (carve.x > 0) - { - band.x0 = content.x1; - band.x1 = window->visible.x1 - outline_px; - band.y0 = window->visible.y0 + outline_px - + wuss__titlebar_height(window); - band.y1 = window->visible.y1 - outline_px; - fill_furniture_rect(wuss, &band, full, - wuss->palette[wuss->furniture_colours.title.bg]); - } + text_x1 = titlebar->x1 - 2; + if (!(window->flags & wuss_WINDOW_NO_TOGGLE_SIZE)) + { + box_t toggle; - if (carve.y > 0) - { - band.x0 = window->visible.x0 + outline_px; - band.x1 = content.x1; - band.y0 = content.y1; - band.y1 = window->visible.y1 - outline_px; - fill_furniture_rect(wuss, &band, full, - wuss->palette[wuss->furniture_colours.title.bg]); - } + wuss__toggle_box(window, &toggle); + text_x1 = toggle.x0 - 2; } - if (!(window->flags & wuss_WINDOW_NO_RESIZE)) - { - box_t resize, rule; - int top_seam, left_seam; + /* A title too wide for its slot mustn't bleed into a neighbouring icon: + * clip drawing to the slot itself, not just the whole titlebar, so a + * too-long title is cut off cleanly rather than overdrawing whatever + * furniture the current redraw didn't happen to touch. */ + text_box.x0 = text_x0; + text_box.y0 = titlebar->y0; + text_box.x1 = text_x1; + text_box.y1 = titlebar->y1; + if (text_x1 <= text_x0 || box_intersection(&text_box, titlebar_clip, &text_clip)) + return; + + titlelen = (int) strlen(window->title); + bmfont_measure(titlefont, window->title, titlelen, text_x1 - text_x0, &split_point, &width); + + pos.x = (split_point < titlelen) ? text_x0 : text_x0 + MAX(0, ((text_x1 - text_x0) - width) / 2); + pos.y = titlebar->y0 + 2; + wuss->scr->clip = text_clip; + bmfont_draw(titlefont, wuss->scr, window->title, titlelen, + wuss->palette[wuss->furniture_colours.title.fg], + wuss->palette[wuss->furniture_colours.title.bg], + &pos, NULL); +} - wuss__resize_box(window, &resize); +void wuss__furniture_draw(wuss_t *wuss, + wuss_window_t *window, + const box_t *full) +{ + box_t visible_clipped; + box_t sausage; + int i; - /* Dividing seams in the 1px gap the scroll strips leave (scroll_strip - * pulls each end in by WUSS_DIVIDER_PX): a rule along the resize box's top - * edge (vscroll strip above) and its left edge (hscroll strip to the - * left), in the interior rules' colour. */ - top_seam = (window->flags & wuss_WINDOW_NO_VSCROLL) ? 0 : WUSS_DIVIDER_PX; - left_seam = (window->flags & wuss_WINDOW_NO_HSCROLL) ? 0 : WUSS_DIVIDER_PX; + if (box_intersection(&window->visible, full, &visible_clipped)) + return; /* offscreen */ - fill_furniture_rect(wuss, &resize, full, - wuss->palette[wuss->furniture_colours.resize]); + if (!window->furniture_layout.valid) + wuss__furniture_layout_build(window); - if (top_seam > 0) - { - rule.x0 = resize.x0 - left_seam; - rule.x1 = resize.x1; - rule.y0 = resize.y0 - top_seam; - rule.y1 = resize.y0; - fill_furniture_rect(wuss, &rule, full, - wuss->palette[wuss->furniture_colours.title.bg]); - } + /* phase 2: clip-and-fill every cached rect */ + for (i = 0; i < window->furniture_layout.npieces; i++) + { + const wuss__furniture_piece_t *piece; - if (left_seam > 0) - { - rule.x0 = resize.x0 - left_seam; - rule.x1 = resize.x0; - rule.y0 = resize.y0 - top_seam; - rule.y1 = resize.y1; - fill_furniture_rect(wuss, &rule, full, - wuss->palette[wuss->furniture_colours.title.bg]); - } + piece = &window->furniture_layout.pieces[i]; + fill_furniture_rect(wuss, &piece->rect, full, + paint_colour(wuss, piece->paint)); } + /* the two scrollbar sausages: geometry depends on window->scroll, so they + * are computed live rather than cached */ if (!(window->flags & wuss_WINDOW_NO_VSCROLL)) { - box_t up, down, well, sausage; - - wuss__vscroll_up_box(window, &up); - fill_furniture_rect(wuss, &up, full, - wuss->palette[wuss->furniture_colours.scroll.arrows]); - - wuss__vscroll_down_box(window, &down); - fill_furniture_rect(wuss, &down, full, - wuss->palette[wuss->furniture_colours.scroll.arrows]); - - wuss__vscroll_well_box(window, &well); - fill_furniture_rect(wuss, &well, full, - wuss->palette[wuss->furniture_colours.scroll.wells]); - wuss__vscroll_sausage_box(window, &sausage); fill_furniture_rect(wuss, &sausage, full, wuss->palette[wuss->furniture_colours.scroll.sausages]); @@ -236,73 +165,17 @@ void wuss__furniture_draw(wuss_t *wuss, if (!(window->flags & wuss_WINDOW_NO_HSCROLL)) { - box_t left, right, well, sausage; - - wuss__hscroll_left_box(window, &left); - fill_furniture_rect(wuss, &left, full, - wuss->palette[wuss->furniture_colours.scroll.arrows]); - - wuss__hscroll_right_box(window, &right); - fill_furniture_rect(wuss, &right, full, - wuss->palette[wuss->furniture_colours.scroll.arrows]); - - wuss__hscroll_well_box(window, &well); - fill_furniture_rect(wuss, &well, full, - wuss->palette[wuss->furniture_colours.scroll.wells]); - wuss__hscroll_sausage_box(window, &sausage); fill_furniture_rect(wuss, &sausage, full, wuss->palette[wuss->furniture_colours.scroll.sausages]); } + /* the title string, drawn live over its (already-filled) titlebar slot */ + if (window->furniture_layout.has_titlebar) { - box_t content, rule; - point_t carve; - - /* Interior rules: where furniture is carved off the content area's right - * or bottom edge, the last pixel of the carve is a dividing line. */ - wuss__content_box(window, &content); - wuss__furniture_carve_for(window->flags, wuss__button_size(window), &carve); - - if (carve.x > 0) - { - rule.x0 = content.x1; - rule.x1 = content.x1 + WUSS_DIVIDER_PX; - rule.y0 = content.y0; - rule.y1 = content.y1; - fill_furniture_rect(wuss, &rule, full, - wuss->palette[wuss->furniture_colours.title.bg]); - } - - if (carve.y > 0) - { - rule.x0 = content.x0; - rule.x1 = content.x1 + ((carve.x > 0) ? WUSS_DIVIDER_PX : 0); /* meet the vertical rule at the corner */ - rule.y0 = content.y1; - rule.y1 = content.y1 + WUSS_DIVIDER_PX; - fill_furniture_rect(wuss, &rule, full, - wuss->palette[wuss->furniture_colours.title.bg]); - } - } - - if (!(window->flags & wuss_WINDOW_NO_OUTLINE)) - { - colour_t border; - box_t edges[4]; - - border = wuss->palette[wuss->furniture_colours.outline]; - - /* top, bottom, left, right -- one pixel thick each */ - edges[0].x0 = window->visible.x0; edges[0].y0 = window->visible.y0; - edges[0].x1 = window->visible.x1; edges[0].y1 = window->visible.y0 + 1; - edges[1].x0 = window->visible.x0; edges[1].y0 = window->visible.y1 - 1; - edges[1].x1 = window->visible.x1; edges[1].y1 = window->visible.y1; - edges[2].x0 = window->visible.x0; edges[2].y0 = window->visible.y0; - edges[2].x1 = window->visible.x0 + 1; edges[2].y1 = window->visible.y1; - edges[3].x0 = window->visible.x1 - 1; edges[3].y0 = window->visible.y0; - edges[3].x1 = window->visible.x1; edges[3].y1 = window->visible.y1; + box_t titlebar_clip; - wuss->scr->clip = visible_clipped; - screen_fill_rects(wuss->scr, edges, 4, border); + if (!box_intersection(&window->furniture_layout.titlebar, full, &titlebar_clip)) + draw_title(wuss, window, &window->furniture_layout.titlebar, &titlebar_clip); } } diff --git a/libraries/wuss/furniture/invalidate.c b/libraries/wuss/furniture/invalidate.c index 9b1e817..7b668b3 100644 --- a/libraries/wuss/furniture/invalidate.c +++ b/libraries/wuss/furniture/invalidate.c @@ -17,6 +17,10 @@ void wuss__furniture_invalidate_for(wuss_window_t *window, int outline_px; point_t carve; + /* Every geometry change routes through here, so it is also the one place + * the cached furniture layout is dropped. */ + window->furniture_layout.valid = 0; + outline_px = wuss__outline_px(window); /* Ask for the same carve the layout uses rather than reading the * scrollbar flags directly: a window with both scrollbars off but resize diff --git a/libraries/wuss/furniture/layout.c b/libraries/wuss/furniture/layout.c new file mode 100644 index 0000000..f59d970 --- /dev/null +++ b/libraries/wuss/furniture/layout.c @@ -0,0 +1,226 @@ +/* wuss/furniture/layout.c -- wuss - minimal window manager */ + +/* Precompute the fixed part of a window's furniture layout -- every filled + * rectangle bar the two scrollbar sausages (which track window->scroll and + * are cheap to recompute each paint) and the title string (font-dependent). + * wuss__furniture_draw calls this once when the cache is stale, then does a + * pure clip-and-fill loop over the result. The cache is invalidated on every + * geometry change through wuss__furniture_invalidate*. */ + +#include + +#include "../core/impl.h" + +/* Append one rect/paint-class pair, unless it would overflow the fixed store + * (WUSS__FURNITURE_MAX_PIECES is sized to the worst case, so this is a guard + * against a future furniture addition, not an expected path). */ +static void push_piece(wuss__furniture_layout_t *layout, + const box_t *rect, + wuss__furniture_paint_class_t paint) +{ + wuss__furniture_piece_t *piece; + + if (layout->npieces >= WUSS__FURNITURE_MAX_PIECES) + { + assert(!"furniture layout piece overflow"); + return; + } + + piece = &layout->pieces[layout->npieces++]; + piece->rect = *rect; + piece->paint = paint; +} + +void wuss__furniture_layout_build(wuss_window_t *window) +{ + wuss__furniture_layout_t *layout; + box_t titlebar; + box_t content; + point_t carve; + int outline_px; + + layout = &window->furniture_layout; + layout->npieces = 0; + layout->has_titlebar = 0; + + outline_px = wuss__outline_px(window); + wuss__content_box(window, &content); + wuss__furniture_carve_for(window->flags, wuss__button_size(window), &carve); + + /* titlebar fill + its icons ------------------------------------------- */ + wuss__titlebar_box(window, &titlebar); + if (!(window->flags & wuss_WINDOW_NO_TITLEBAR)) + { + layout->titlebar = titlebar; + layout->has_titlebar = 1; + + push_piece(layout, &titlebar, wuss__FURNITURE_PAINT_TITLE_BG); + + if (!(window->flags & wuss_WINDOW_NO_CLOSE)) + { + box_t close; + + wuss__close_box(window, &close); + push_piece(layout, &close, wuss__FURNITURE_PAINT_CLOSE); + } + + if (!(window->flags & wuss_WINDOW_NO_BACK)) + { + box_t back; + + wuss__back_box(window, &back); + push_piece(layout, &back, wuss__FURNITURE_PAINT_BACK); + } + + if (!(window->flags & wuss_WINDOW_NO_TOGGLE_SIZE)) + { + box_t toggle; + + wuss__toggle_box(window, &toggle); + push_piece(layout, &toggle, wuss__FURNITURE_PAINT_TOGGLE); + } + } + + /* resize-only carve bands ------------------------------------------------ + * Both scrollbars off but a resize icon on: the content box is still + * carved back on its right and bottom and no scroll strip paints that + * margin, so furniture fills it (see the note in the old draw.c). */ + if ((window->flags & wuss_WINDOW_NO_VSCROLL) && + (window->flags & wuss_WINDOW_NO_HSCROLL) && + !(window->flags & wuss_WINDOW_NO_RESIZE)) + { + box_t band; + + if (carve.x > 0) + { + band.x0 = content.x1; + band.x1 = window->visible.x1 - outline_px; + band.y0 = window->visible.y0 + outline_px + + wuss__titlebar_height(window); + band.y1 = window->visible.y1 - outline_px; + push_piece(layout, &band, wuss__FURNITURE_PAINT_TITLE_BG); + } + + if (carve.y > 0) + { + band.x0 = window->visible.x0 + outline_px; + band.x1 = content.x1; + band.y0 = content.y1; + band.y1 = window->visible.y1 - outline_px; + push_piece(layout, &band, wuss__FURNITURE_PAINT_TITLE_BG); + } + } + + /* resize icon + its dividing seams ---------------------------------- */ + if (!(window->flags & wuss_WINDOW_NO_RESIZE)) + { + box_t resize, rule; + int top_seam, left_seam; + + wuss__resize_box(window, &resize); + + top_seam = (window->flags & wuss_WINDOW_NO_VSCROLL) ? 0 : WUSS_DIVIDER_PX; + left_seam = (window->flags & wuss_WINDOW_NO_HSCROLL) ? 0 : WUSS_DIVIDER_PX; + + push_piece(layout, &resize, wuss__FURNITURE_PAINT_RESIZE); + + if (top_seam > 0) + { + rule.x0 = resize.x0 - left_seam; + rule.x1 = resize.x1; + rule.y0 = resize.y0 - top_seam; + rule.y1 = resize.y0; + push_piece(layout, &rule, wuss__FURNITURE_PAINT_TITLE_BG); + } + + if (left_seam > 0) + { + rule.x0 = resize.x0 - left_seam; + rule.x1 = resize.x0; + rule.y0 = resize.y0 - top_seam; + rule.y1 = resize.y1; + push_piece(layout, &rule, wuss__FURNITURE_PAINT_TITLE_BG); + } + } + + /* vertical scrollbar: arrows + well (sausage is drawn live) --------- */ + if (!(window->flags & wuss_WINDOW_NO_VSCROLL)) + { + box_t up, down, well; + + wuss__vscroll_up_box(window, &up); + push_piece(layout, &up, wuss__FURNITURE_PAINT_SCROLL_ARROWS); + + wuss__vscroll_down_box(window, &down); + push_piece(layout, &down, wuss__FURNITURE_PAINT_SCROLL_ARROWS); + + wuss__vscroll_well_box(window, &well); + push_piece(layout, &well, wuss__FURNITURE_PAINT_SCROLL_WELLS); + } + + /* horizontal scrollbar: arrows + well ----------------------------- */ + if (!(window->flags & wuss_WINDOW_NO_HSCROLL)) + { + box_t left, right, well; + + wuss__hscroll_left_box(window, &left); + push_piece(layout, &left, wuss__FURNITURE_PAINT_SCROLL_ARROWS); + + wuss__hscroll_right_box(window, &right); + push_piece(layout, &right, wuss__FURNITURE_PAINT_SCROLL_ARROWS); + + wuss__hscroll_well_box(window, &well); + push_piece(layout, &well, wuss__FURNITURE_PAINT_SCROLL_WELLS); + } + + /* interior rules where furniture is carved off the content edges --- */ + if (carve.x > 0) + { + box_t rule; + + rule.x0 = content.x1; + rule.x1 = content.x1 + WUSS_DIVIDER_PX; + rule.y0 = content.y0; + rule.y1 = content.y1; + push_piece(layout, &rule, wuss__FURNITURE_PAINT_TITLE_BG); + } + + if (carve.y > 0) + { + box_t rule; + + rule.x0 = content.x0; + rule.x1 = content.x1 + ((carve.x > 0) ? WUSS_DIVIDER_PX : 0); /* meet the vertical rule at the corner */ + rule.y0 = content.y1; + rule.y1 = content.y1 + WUSS_DIVIDER_PX; + push_piece(layout, &rule, wuss__FURNITURE_PAINT_TITLE_BG); + } + + /* window outline: four one-pixel edges ---------------------------- */ + if (!(window->flags & wuss_WINDOW_NO_OUTLINE)) + { + box_t edge; + + edge.x0 = window->visible.x0; + edge.y0 = window->visible.y0; + edge.x1 = window->visible.x1; + edge.y1 = window->visible.y0 + 1; + push_piece(layout, &edge, wuss__FURNITURE_PAINT_OUTLINE); /* top */ + + edge.y0 = window->visible.y1 - 1; + edge.y1 = window->visible.y1; + push_piece(layout, &edge, wuss__FURNITURE_PAINT_OUTLINE); /* bottom */ + + edge.x0 = window->visible.x0; + edge.y0 = window->visible.y0; + edge.x1 = window->visible.x0 + 1; + edge.y1 = window->visible.y1; + push_piece(layout, &edge, wuss__FURNITURE_PAINT_OUTLINE); /* left */ + + edge.x0 = window->visible.x1 - 1; + edge.x1 = window->visible.x1; + push_piece(layout, &edge, wuss__FURNITURE_PAINT_OUTLINE); /* right */ + } + + layout->valid = 1; +} From dcd4a981e73bae86a3316f7e8c1be201f3e5eddc Mon Sep 17 00:00:00 2001 From: David Thomas Date: Tue, 8 Sep 2026 00:57:31 +0100 Subject: [PATCH 46/98] feat(utils): add rng.h, a seedable deterministic PRNG Header-only. rng_seed plus two step functions: rng_xorshift32 (Marsaglia xorshift32, shifts 13/17/5) and rng_lcg32 (Numerical Recipes LCG), with rng_range for a bounded int draw off the high bits. Replaces the two hand-rolled copies in the wuss demo tasks: saturn.c's 'unsigned long' LCG (now rng_lcg32, which correctly wraps at 2^32) and greeble.c's GREEBLE_XORSHIFT macro (now rng_range, same 13/17/5 so the generated patterns are byte-identical). greeble_task_t.seed is now rng_t so rng_lcg32 can advance it in place. Co-Authored-By: Claude Sonnet 5 --- CMakeLists.txt | 1 + include/utils/rng.h | 113 ++++++++++++++++++++++++++++ libraries/wuss/test/tasks/greeble.c | 33 +++----- libraries/wuss/test/tasks/greeble.h | 3 +- libraries/wuss/test/tasks/saturn.c | 13 ++-- 5 files changed, 134 insertions(+), 29 deletions(-) create mode 100644 include/utils/rng.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 6a0ec32..9afd133 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -119,6 +119,7 @@ set(PUBLIC_HEADERS include/utils/maths.h include/utils/pack.h include/utils/primes.h + include/utils/rng.h include/wuss/task.h include/wuss/window.h include/wuss/wuss.h) diff --git a/include/utils/rng.h b/include/utils/rng.h new file mode 100644 index 0000000..ca701b5 --- /dev/null +++ b/include/utils/rng.h @@ -0,0 +1,113 @@ +/* utils/rng.h -- tiny deterministic pseudo-random number generators */ + +/** + * \file rng.h + * + * Small, seedable, header-only PRNGs for reproducible procedural content + * (dither, scatter, wireframe jitter, test fixtures). + * + * Every generator here is a pure function of its state word, so re-seeding + * with the same value always replays the same sequence -- unlike the C + * library's rand(), which shares one hidden global. State is a single + * uint32_t the caller owns. + * + * Two step functions are provided. rng_xorshift32 is Marsaglia's xorshift32: + * good bit avalanche, period 2^32 - 1, never returns 0 and must not be + * seeded 0. rng_lcg32 is the Numerical Recipes linear congruential + * generator: full period 2^32, but its low bits are weak, so take from the + * top (rng_range below already does). + * + * Prefer rng_xorshift32 unless you specifically need to match an existing + * LCG stream. + */ + +#ifndef UTILS_RNG_H +#define UTILS_RNG_H + +#ifdef __cplusplus +extern "C" +{ +#endif + +#include + +/* ----------------------------------------------------------------------- */ + +/** Generator state. A single 32-bit word the caller stores. */ +typedef uint32_t rng_t; + +/** + * Seed a generator. + * + * A zero seed is remapped to 1 so rng_xorshift32 (which cannot use a zero + * state) is always safe; this leaves rng_lcg32 unable to produce the single + * state value 0 from a fresh seed, which is harmless. + * + * \param[out] s Generator state. + * \param[in] seed Seed value. + */ +static __inline__ void rng_seed(rng_t *s, uint32_t seed) +{ + *s = seed ? seed : 1u; +} + +/** + * Advance the state one xorshift32 step and return it. + * + * \param[in,out] s Generator state (must be non-zero; rng_seed guarantees + * this). + * \return The new state word. + */ +static __inline__ uint32_t rng_xorshift32(rng_t *s) +{ + uint32_t x; + + x = *s; + x ^= x << 13; + x ^= x >> 17; + x ^= x << 5; + *s = x; + + return x; +} + +/** + * Advance the state one LCG step and return it. + * + * Constants are those from Numerical Recipes. The low-order bits cycle + * short; consumers wanting a bounded value should use rng_range, which + * samples the high bits. + * + * \param[in,out] s Generator state. + * \return The new state word. + */ +static __inline__ uint32_t rng_lcg32(rng_t *s) +{ + *s = *s * 1664525u + 1013904223u; + + return *s; +} + +/** + * Return a value in [0, n) using an xorshift32 step. + * + * Uses a plain modulo, so the distribution is very slightly biased for n + * that do not divide 2^32; negligible for the small n typical of grid and + * palette indexing. n must be positive. + * + * \param[in,out] s Generator state. + * \param[in] n Exclusive upper bound (> 0). + * \return A value in [0, n). + */ +static __inline__ int rng_range(rng_t *s, int n) +{ + return (int) (rng_xorshift32(s) % (uint32_t) n); +} + +/* ----------------------------------------------------------------------- */ + +#ifdef __cplusplus +} +#endif + +#endif /* UTILS_RNG_H */ diff --git a/libraries/wuss/test/tasks/greeble.c b/libraries/wuss/test/tasks/greeble.c index cd0427d..f425e71 100644 --- a/libraries/wuss/test/tasks/greeble.c +++ b/libraries/wuss/test/tasks/greeble.c @@ -12,6 +12,7 @@ #include "base/utils.h" #include "framebuf/colour.h" #include "geom/box.h" +#include "utils/rng.h" #include "wuss/menu.h" #include "greeble.h" @@ -40,11 +41,6 @@ static wuss_menu_t g_greeble_menu = * is free. greeble_redraw skips these. */ #define GREEBLE_EMPTY 0xFF -/* one xorshift32 step on lvalue s; kept local to greeble_generate so the same - * seed always yields the same grid */ -#define GREEBLE_XORSHIFT(s) \ - ((s) ^= (s) << 13, (s) ^= (s) >> 17, (s) ^= (s) << 5) - /* try to drop prefab p with its top-left at (row,col): succeeds only if every * non-hole cell of the prefab lands on an in-bounds, still-empty grid cell, so * blocks never overlap. pal is the greeble_palettes[] row to tag every cell @@ -100,13 +96,13 @@ static int greeble_try_prefab(greeble_task_t *task, static void greeble_generate(greeble_task_t *task) { - unsigned int s; + rng_t s; int i; unsigned char order[GREEBLE_NPREFAB]; /* prefab indices, largest area first */ int tries_each; int r, c; - s = task->seed; + rng_seed(&s, task->seed); /* insertion-sort a fresh index list by w*h descending; NPREFAB is small and * this runs once per regenerate */ @@ -151,17 +147,12 @@ static void greeble_generate(greeble_task_t *task) int row, col; unsigned char pal; - GREEBLE_XORSHIFT(s); - row = (int) (s % (unsigned int) task->rows); - GREEBLE_XORSHIFT(s); - col = (int) (s % (unsigned int) task->cols); + row = rng_range(&s, task->rows); + col = rng_range(&s, task->cols); pal = task->palette; if (task->random_prefab_palettes) - { - GREEBLE_XORSHIFT(s); - pal = (unsigned char) (s % (unsigned int) GREEBLE_NPALETTE); - } + pal = (unsigned char) rng_range(&s, GREEBLE_NPALETTE); greeble_try_prefab(task, p, row, col, pal); } @@ -170,11 +161,8 @@ static void greeble_generate(greeble_task_t *task) for (r = 0; r < task->rows; r++) for (c = 0; c < task->cols; c++) if (task->grid[r][c] == GREEBLE_EMPTY) - { - GREEBLE_XORSHIFT(s); task->grid[r][c] = - greeble_filler[s % (unsigned int) GREEBLE_NFILLER]; - } + greeble_filler[rng_range(&s, GREEBLE_NFILLER)]; } /* recompute the grid extent for the window's content box, then regenerate */ @@ -274,9 +262,10 @@ static result_t greeble_redraw(const wuss_event_t *event, /* Select: advance to a fresh pattern (new seed, regenerate the grid). */ static result_t greeble_select(greeble_task_t *task, wuss_window_t *window) { - /* LCG step; any nonzero state keeps the pattern xorshift off its fixed - * point */ - task->seed = task->seed * 1664525u + 1013904223u; + /* LCG-hop to the next start seed; rng_range's xorshift then walks that. + * Remap the one zero state so the xorshift never lands on its fixed + * point. */ + rng_lcg32(&task->seed); if (task->seed == 0) task->seed = 1; diff --git a/libraries/wuss/test/tasks/greeble.h b/libraries/wuss/test/tasks/greeble.h index e665e42..e44a0bd 100644 --- a/libraries/wuss/test/tasks/greeble.h +++ b/libraries/wuss/test/tasks/greeble.h @@ -5,6 +5,7 @@ #ifdef WUSS_APP +#include "utils/rng.h" #include "wuss/menu.h" #include "wuss/window.h" @@ -30,7 +31,7 @@ typedef struct greeble_task wuss_task_t *delegate; /* the task that owns the menu */ wuss_window_t *window; wuss_menu_handle_t menu_handle; /* live only between open and a SELECT pick */ - unsigned int seed; /* current pattern seed; advanced on click */ + rng_t seed; /* current pattern seed; advanced on click */ int cols, rows; /* live grid extent, <= the MAX_* caps */ unsigned char palette; /* greeble_palettes[] row for this pattern */ int random_prefab_palettes; /* nonzero: each scattered prefab diff --git a/libraries/wuss/test/tasks/saturn.c b/libraries/wuss/test/tasks/saturn.c index 57fa4a9..fff22a8 100644 --- a/libraries/wuss/test/tasks/saturn.c +++ b/libraries/wuss/test/tasks/saturn.c @@ -12,6 +12,7 @@ #include "base/utils.h" #include "framebuf/palettes.h" #include "geom/box.h" +#include "utils/rng.h" #include "saturn.h" @@ -50,19 +51,19 @@ #define SATURN_BODY_R2 16384 /* planet body: keep if r1*r1+r2*r2 < this */ -/* BBC BASIC RND(n>0) returns an integer 1..n. A small LCG stands in for it so - * a Select click can re-seed for a fresh sketch. */ -static unsigned long saturn_rnd_state; +/* BBC BASIC RND(n>0) returns an integer 1..n. utils/rng's LCG stands in for + * it (its high bits, which is where an LCG's usable randomness sits) so a + * Select click can re-seed for a fresh sketch. */ +static rng_t saturn_rnd_state; static void saturn_rnd_seed(unsigned long seed) { - saturn_rnd_state = seed ? seed : 1; + rng_seed(&saturn_rnd_state, (uint32_t) seed); } static int saturn_rnd(int n) { - saturn_rnd_state = saturn_rnd_state * 1664525UL + 1013904223UL; - return (int) ((saturn_rnd_state >> 16) % (unsigned long) n) + 1; + return (int) ((rng_lcg32(&saturn_rnd_state) >> 16) % (uint32_t) n) + 1; } /* one signed sample in the original -128..127 space */ From 97d540b02af1292904a951b852ade1384329ad97 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Tue, 8 Sep 2026 00:57:42 +0100 Subject: [PATCH 47/98] feat(io): add namelist_scan, dir leafname collection into a table Wraps dirscan_walk with an extension filter, extension stripping and an optional ascending sort, writing the results into a caller-owned char[cap][stride] table. Entries longer than stride and entries past cap are dropped silently. Replaces the near-identical scan callbacks in two wuss demo tasks: palette.c (add_name + name_cmp, sorted) and image.c (image__scan_entry, unsorted). Co-Authored-By: Claude Sonnet 5 --- CMakeLists.txt | 2 + include/io/namelist.h | 42 +++++++++++++ libraries/io/namelist/namelist.c | 97 +++++++++++++++++++++++++++++ libraries/wuss/test/tasks/image.c | 30 ++------- libraries/wuss/test/tasks/palette.c | 46 ++------------ 5 files changed, 149 insertions(+), 68 deletions(-) create mode 100644 include/io/namelist.h create mode 100644 libraries/io/namelist/namelist.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 9afd133..b087e7b 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -101,6 +101,7 @@ set(PUBLIC_HEADERS include/geom/point.h include/geom/size.h include/io/dirscan.h + include/io/namelist.h include/io/path.h include/io/stream-mem.h include/io/stream-mtfcomp.h @@ -298,6 +299,7 @@ set(GEOM_SOURCES set(IO_SOURCES libraries/io/dirscan/dirscan.c + libraries/io/namelist/namelist.c libraries/io/path/path.c libraries/io/stream/stream-mem.c libraries/io/stream/stream-mtfcomp.c diff --git a/include/io/namelist.h b/include/io/namelist.h new file mode 100644 index 0000000..e4b00ed --- /dev/null +++ b/include/io/namelist.h @@ -0,0 +1,42 @@ +/* io/namelist.h -- collect matching dir leafnames into a fixed table */ + +#ifndef DPTLIB_NAMELIST_H +#define DPTLIB_NAMELIST_H + +#include + +#include "base/result.h" + +/** + * Scan a directory (via dirscan_walk) and copy the leafnames that end in \p + * ext into a caller-owned fixed-size table, with the extension stripped. + * + * The table is `cap` slots of `stride` bytes each, laid out contiguously + * (i.e. a `char names[cap][stride]`). An entry is stored only if its name + * with \p ext removed, plus a NUL, fits in \p stride; longer names are + * skipped silently. Scanning stops once \p cap names are stored -- this is + * not an error, `*pcount` simply reports the cap. + * + * \param[in] dir Directory to scan. + * \param[in] ext Extension to match and strip, leading dot included + * (e.g. ".png"). NULL or "" matches every entry and + * strips nothing. + * \param[out] names Base of the `cap`-by-`stride` character table. + * \param[in] stride Bytes per table slot (must be >= 2). + * \param[in] cap Number of slots in the table (must be >= 0). + * \param[in] sorted Non-zero to sort the stored names ascending (strcmp). + * \param[out] pcount Number of names stored. Set on every non-error return. + * \return \ref result_OK (including when the cap was hit), \ref + * result_NULL_ARG if \p dir, \p names or \p pcount is NULL, \ref + * result_BAD_ARG if \p stride < 2 or \p cap < 0, \ref + * result_FILE_NOT_FOUND if \p dir cannot be opened. + */ +result_t namelist_scan(const char *dir, + const char *ext, + char *names, + size_t stride, + int cap, + int sorted, + int *pcount); + +#endif /* DPTLIB_NAMELIST_H */ diff --git a/libraries/io/namelist/namelist.c b/libraries/io/namelist/namelist.c new file mode 100644 index 0000000..43d3964 --- /dev/null +++ b/libraries/io/namelist/namelist.c @@ -0,0 +1,97 @@ +/* io/namelist.c -- collect matching dir leafnames into a fixed table */ + +#include +#include +#include + +#include "base/result.h" +#include "io/dirscan.h" + +#include "io/namelist.h" + +/* ----------------------------------------------------------------------- */ + +typedef struct namelist_ctx +{ + char *names; /* base of the cap-by-stride table */ + size_t stride; + int cap; + const char *ext; /* NULL or "" for "match everything" */ + size_t extlen; + int count; +} +namelist_ctx_t; + +static result_t namelist_entry(const char *leaf, void *opaque) +{ + namelist_ctx_t *ctx; + size_t leaflen; + size_t namelen; + + ctx = opaque; + leaflen = strlen(leaf); + + if (ctx->count >= ctx->cap) + return result_STOP_WALK; + + if (ctx->extlen > 0) + { + if (leaflen <= ctx->extlen || + strcmp(leaf + leaflen - ctx->extlen, ctx->ext) != 0) + return result_OK; + namelen = leaflen - ctx->extlen; + } + else + { + namelen = leaflen; + } + + if (namelen + 1 > ctx->stride) + return result_OK; /* would not fit its slot */ + + memcpy(ctx->names + (size_t) ctx->count * ctx->stride, leaf, namelen); + ctx->names[(size_t) ctx->count * ctx->stride + namelen] = '\0'; + ctx->count++; + + return result_OK; +} + +static int namelist_cmp(const void *a, const void *b) +{ + return strcmp(a, b); +} + +result_t namelist_scan(const char *dir, + const char *ext, + char *names, + size_t stride, + int cap, + int sorted, + int *pcount) +{ + result_t rc; + namelist_ctx_t ctx; + + if (dir == NULL || names == NULL || pcount == NULL) + return result_NULL_ARG; + if (stride < 2 || cap < 0) + return result_BAD_ARG; + + ctx.names = names; + ctx.stride = stride; + ctx.cap = cap; + ctx.ext = ext; + ctx.extlen = (ext != NULL) ? strlen(ext) : 0; + ctx.count = 0; + + rc = dirscan_walk(dir, namelist_entry, &ctx); + if (rc != result_OK) + return rc; + + if (sorted && ctx.count > 1) + qsort(names, (size_t) ctx.count, stride, namelist_cmp); + + *pcount = ctx.count; + + return result_OK; +} diff --git a/libraries/wuss/test/tasks/image.c b/libraries/wuss/test/tasks/image.c index 8d23986..89618e7 100644 --- a/libraries/wuss/test/tasks/image.c +++ b/libraries/wuss/test/tasks/image.c @@ -14,7 +14,7 @@ #include "base/utils.h" #include "framebuf/palettes.h" #include "geom/box.h" -#include "io/dirscan.h" +#include "io/namelist.h" #include "io/path.h" #include "wuss/menu.h" #include "wuss/menu-desc.h" @@ -23,30 +23,6 @@ #define NINEPATCHSZ 9 #define IMAGE_EXT ".png" -#define IMAGE_EXT_LEN 4 - -static result_t image__scan_entry(const char *leaf, void *opaque) -{ - image_task_t *ic; - size_t leaflen; - - ic = opaque; - leaflen = strlen(leaf); - - if (leaflen <= IMAGE_EXT_LEN || - leaflen - IMAGE_EXT_LEN >= IMAGE_MAX_NAME_LEN) - return result_OK; - if (strcmp(leaf + leaflen - IMAGE_EXT_LEN, IMAGE_EXT) != 0) - return result_OK; - if (ic->nnames >= IMAGE_MAX_NAMES) - return result_STOP_WALK; - - memcpy(ic->names[ic->nnames], leaf, leaflen - IMAGE_EXT_LEN); - ic->names[ic->nnames][leaflen - IMAGE_EXT_LEN] = '\0'; - ic->nnames++; - - return result_OK; -} result_t image_create(wuss_t *wuss, const char *resources, @@ -70,7 +46,9 @@ result_t image_create(wuss_t *wuss, task->menu_handle = NULL; images_dir = path_join_filename(resources, 2, "resources", "images"); - rc = dirscan_walk(images_dir, image__scan_entry, task); + rc = namelist_scan(images_dir, IMAGE_EXT, task->names[0], + sizeof(task->names[0]), IMAGE_MAX_NAMES, 0 /* unsorted */, + &task->nnames); if (rc != result_OK) { free(task); /* nothing registered yet; the spawner will not free it */ diff --git a/libraries/wuss/test/tasks/palette.c b/libraries/wuss/test/tasks/palette.c index 323a481..105dc64 100644 --- a/libraries/wuss/test/tasks/palette.c +++ b/libraries/wuss/test/tasks/palette.c @@ -13,14 +13,13 @@ #include "base/utils.h" #include "framebuf/palettes.h" #include "geom/box.h" -#include "io/dirscan.h" +#include "io/namelist.h" #include "io/path.h" #include "wuss/menu.h" #include "palette.h" #define PALETTE_HEX_EXT ".hex" -#define PALETTE_HEX_EXT_LEN 4 #define PALETTE_NCOLOURS 16 /* one colour_t[] row per *.hex file; the * system palette wuss_create was given is * fixed at this length */ @@ -29,42 +28,6 @@ * file and the dashed rule above it */ #define PALETTE_MENU_INVERT_INDEX(pc) ((pc)->nnames) -/* ----------------------------------------------------------------------- */ -/* Scan resources/palettes for *.hex files, collecting each leafname (ext - * stripped) into task->names, sorted, capped at PALETTE_MAX_FILES. */ - -static int name_cmp(const void *a, const void *b) -{ - return strcmp(a, b); -} - -static result_t add_name(const char *leaf, void *opaque) -{ - palette_task_t *pc; - size_t leaflen; - size_t namelen; - - pc = opaque; - leaflen = strlen(leaf); - - if (pc->nnames >= PALETTE_MAX_FILES) - return result_STOP_WALK; - if (leaflen <= PALETTE_HEX_EXT_LEN) - return result_OK; - if (strcmp(leaf + leaflen - PALETTE_HEX_EXT_LEN, PALETTE_HEX_EXT) != 0) - return result_OK; - - namelen = leaflen - PALETTE_HEX_EXT_LEN; - if (namelen >= sizeof(pc->names[0])) - return result_OK; - - memcpy(pc->names[pc->nnames], leaf, namelen); - pc->names[pc->nnames][namelen] = '\0'; - pc->nnames++; - - return result_OK; -} - /* ----------------------------------------------------------------------- */ /* Parse a *.hex file: one "rrggbb" line per colour, no leading '#'. Fails * (leaving *out untouched) unless exactly PALETTE_NCOLOURS well-formed lines @@ -127,10 +90,9 @@ result_t palette_create(wuss_t *wuss, dir = path_join_filename(resources, 2, "resources", "palettes"); strcpy(dirbuf, dir); /* path_join_filename's buffer is reused by the scan */ - dirscan_walk(dirbuf, add_name, task); - if (task->nnames > 1) - qsort(task->names, (size_t) task->nnames, sizeof(task->names[0]), - name_cmp); + namelist_scan(dirbuf, PALETTE_HEX_EXT, task->names[0], + sizeof(task->names[0]), PALETTE_MAX_FILES, 1 /* sorted */, + &task->nnames); if (startup_name != NULL) for (i = 0; i < task->nnames; i++) From 4f0806812ff29e82b78c215d31a7403300d53c1f Mon Sep 17 00:00:00 2001 From: David Thomas Date: Tue, 8 Sep 2026 10:13:34 +0100 Subject: [PATCH 48/98] fix(wuss): saturn redraw honours scroll offset; rename wuss_WINDOW_NONE saturn_redraw pinned its plot origin to bounds->x0/y0, so dragging the scrollbars or shrinking the window below its 256x256 document left the sketch stuck to the window instead of scrolling under it. Subtract the redraw event's scroll offset from the doc-to-screen origin, matching gradient/ball/clock. The stale "no scroll: window is fixed" comment was the bug's origin. Also rename wuss_WINDOW_NONE to wuss_WINDOW_DEFAULT across the enum and all call sites: the value means "all furniture present", not "no furniture", and the old name read as the latter. Co-Authored-By: Claude Sonnet 5 --- include/wuss/window.h | 2 +- include/wuss/wuss.h | 2 +- libraries/wuss/test/tasks/ball.c | 2 +- libraries/wuss/test/tasks/checker.c | 4 +- libraries/wuss/test/tasks/clock.c | 2 +- libraries/wuss/test/tasks/curve.c | 2 +- libraries/wuss/test/tasks/gradient.c | 2 +- libraries/wuss/test/tasks/greeble.c | 2 +- libraries/wuss/test/tasks/icons.c | 2 +- libraries/wuss/test/tasks/image.c | 2 +- libraries/wuss/test/tasks/lissajous.c | 2 +- libraries/wuss/test/tasks/minesweeper.c | 2 +- libraries/wuss/test/tasks/porter-duff.c | 2 +- libraries/wuss/test/tasks/saturn.c | 9 ++-- libraries/wuss/test/tasks/sofa.c | 2 +- libraries/wuss/test/wuss-test.c | 66 ++++++++++++------------- 16 files changed, 53 insertions(+), 52 deletions(-) diff --git a/include/wuss/window.h b/include/wuss/window.h index 0358448..ef47e97 100644 --- a/include/wuss/window.h +++ b/include/wuss/window.h @@ -49,7 +49,7 @@ extern "C" * wuss_WINDOW_NO_TITLEBAR. * \param[in] flags Appearance flags, e.g. wuss_WINDOW_NO_TITLEBAR / * wuss_WINDOW_NO_OUTLINE, OR'd together, or - * wuss_WINDOW_NONE for the default furniture. + * wuss_WINDOW_DEFAULT for the default furniture. * \param[in] bg Content background, filled by Wuss before each redraw: * a flat colour or an 8x8 fill pattern (see * wuss_backdrop_t). Set its colour to wuss_NO_BACKGROUND diff --git a/include/wuss/wuss.h b/include/wuss/wuss.h index 1586760..79f8102 100644 --- a/include/wuss/wuss.h +++ b/include/wuss/wuss.h @@ -191,7 +191,7 @@ wuss_furniture_palette_t; typedef enum wuss_window_flags { /** Default: every furniture region drawn. */ - wuss_WINDOW_NONE = 0, + wuss_WINDOW_DEFAULT = 0, /** * No titlebar; content fills the full visible area, and no drag handle diff --git a/libraries/wuss/test/tasks/ball.c b/libraries/wuss/test/tasks/ball.c index 261ef83..353d60e 100644 --- a/libraries/wuss/test/tasks/ball.c +++ b/libraries/wuss/test/tasks/ball.c @@ -82,7 +82,7 @@ result_t ball_create(wuss_t *wuss, ball_task_t *task) rc = wuss_window_create_placed(delegate, SIZE2D(200, 160), "Bouncing Ball", - wuss_WINDOW_NONE, + wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), SIZE2D(200, 160), SIZE2D(0, 0), diff --git a/libraries/wuss/test/tasks/checker.c b/libraries/wuss/test/tasks/checker.c index 32656ee..2f4761b 100644 --- a/libraries/wuss/test/tasks/checker.c +++ b/libraries/wuss/test/tasks/checker.c @@ -45,7 +45,7 @@ result_t checker_create(wuss_t*wuss, checker_task_t *task) rc = wuss_window_create_placed(delegate, SIZE2D(160, 160), "Checker 1", - wuss_WINDOW_NONE, + wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), SIZE2D(160, 160), SIZE2D(0, 0), @@ -59,7 +59,7 @@ result_t checker_create(wuss_t*wuss, checker_task_t *task) rc = wuss_window_create_placed(delegate, SIZE2D(160, 160), "Checker 2", - wuss_WINDOW_NONE, + wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), SIZE2D(160, 160), SIZE2D(0, 0), diff --git a/libraries/wuss/test/tasks/clock.c b/libraries/wuss/test/tasks/clock.c index 2bdbfb7..4a1ab5e 100644 --- a/libraries/wuss/test/tasks/clock.c +++ b/libraries/wuss/test/tasks/clock.c @@ -93,7 +93,7 @@ static result_t clock_create_window(wuss_t *wuss, return wuss_window_create_placed(delegate, SIZE2D(160, 160), "Clock", - wuss_WINDOW_NONE, + wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), SIZE2D(160, 160), SIZE2D(0, 0), diff --git a/libraries/wuss/test/tasks/curve.c b/libraries/wuss/test/tasks/curve.c index 2c5def7..ecb24fc 100644 --- a/libraries/wuss/test/tasks/curve.c +++ b/libraries/wuss/test/tasks/curve.c @@ -55,7 +55,7 @@ result_t curve_create(wuss_t *wuss, curve_task_t *task) rc = wuss_window_create_placed(delegate, SIZE2D(220, 160), "Curve", - wuss_WINDOW_NONE, + wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), SIZE2D(220, 160), SIZE2D(0, 0), diff --git a/libraries/wuss/test/tasks/gradient.c b/libraries/wuss/test/tasks/gradient.c index 160be39..b8132d5 100644 --- a/libraries/wuss/test/tasks/gradient.c +++ b/libraries/wuss/test/tasks/gradient.c @@ -54,7 +54,7 @@ result_t gradient_create(wuss_t *wuss, gradient_task_t *task) rc = wuss_window_create_placed(delegate, SIZE2D(GRADIENT_OPEN_WIDTH, GRADIENT_OPEN_HEIGHT), "Gradient", - wuss_WINDOW_NONE, + wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), SIZE2D(GRADIENT_DOC_WIDTH, GRADIENT_DOC_HEIGHT), SIZE2D(0, 0), diff --git a/libraries/wuss/test/tasks/greeble.c b/libraries/wuss/test/tasks/greeble.c index f425e71..5a7a9f3 100644 --- a/libraries/wuss/test/tasks/greeble.c +++ b/libraries/wuss/test/tasks/greeble.c @@ -413,7 +413,7 @@ result_t greeble_create(wuss_t *wuss, greeble_task_t *task) rc = wuss_window_create_placed(delegate, grid_px, "Greeble", - wuss_WINDOW_NONE, + wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), grid_px, SIZE2D(0, 0), diff --git a/libraries/wuss/test/tasks/icons.c b/libraries/wuss/test/tasks/icons.c index 6659bf6..8dfa916 100644 --- a/libraries/wuss/test/tasks/icons.c +++ b/libraries/wuss/test/tasks/icons.c @@ -478,7 +478,7 @@ result_t icons_create(wuss_t *wuss, rc = wuss_window_create_placed(delegate, SIZE2D(ICONS_DOC_W, 200), "Icons", - wuss_WINDOW_NONE, + wuss_WINDOW_DEFAULT, wuss_BACKDROP_PATTERN(wuss_COLOUR_GREY, screen_PATTERN_CROSSHATCH, wuss_COLOUR_WINDOW), diff --git a/libraries/wuss/test/tasks/image.c b/libraries/wuss/test/tasks/image.c index 89618e7..d5d6d26 100644 --- a/libraries/wuss/test/tasks/image.c +++ b/libraries/wuss/test/tasks/image.c @@ -94,7 +94,7 @@ result_t image_create(wuss_t *wuss, rc = wuss_window_create_placed(delegate, SIZE2D(sz.w, sz.h), "Image", - wuss_WINDOW_NONE, + wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(palette_PICO8_PINK), sz, SIZE2D(32, 32), diff --git a/libraries/wuss/test/tasks/lissajous.c b/libraries/wuss/test/tasks/lissajous.c index 9ada0dc..f6c7766 100644 --- a/libraries/wuss/test/tasks/lissajous.c +++ b/libraries/wuss/test/tasks/lissajous.c @@ -50,7 +50,7 @@ result_t lissajous_create(wuss_t *wuss, lissajous_task_t *task) rc = wuss_window_create_placed(delegate, SIZE2D(220, 220), "Lissajous", - wuss_WINDOW_NONE, + wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), SIZE2D(220, 220), SIZE2D(0, 0), diff --git a/libraries/wuss/test/tasks/minesweeper.c b/libraries/wuss/test/tasks/minesweeper.c index 5182e3d..45d2f58 100644 --- a/libraries/wuss/test/tasks/minesweeper.c +++ b/libraries/wuss/test/tasks/minesweeper.c @@ -203,7 +203,7 @@ result_t minesweeper_create(wuss_t *wuss, rc = wuss_window_create_placed(delegate, SIZE2D(MS_WIDTH, MS_HEIGHT), "Minesweeper", - wuss_WINDOW_NONE, + wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), SIZE2D(MS_WIDTH, MS_HEIGHT), SIZE2D(0, 0), diff --git a/libraries/wuss/test/tasks/porter-duff.c b/libraries/wuss/test/tasks/porter-duff.c index a91636c..6c09319 100644 --- a/libraries/wuss/test/tasks/porter-duff.c +++ b/libraries/wuss/test/tasks/porter-duff.c @@ -204,7 +204,7 @@ result_t porter_duff_create(wuss_t *wuss, rc = wuss_window_create_placed(delegate, SIZE2D(PD_SIZE, PD_SIZE + PD_LABEL_HEIGHT), "Porter-Duff", - wuss_WINDOW_NONE, + wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), SIZE2D(PD_SIZE, PD_SIZE + PD_LABEL_HEIGHT), SIZE2D(0, 0), diff --git a/libraries/wuss/test/tasks/saturn.c b/libraries/wuss/test/tasks/saturn.c index fff22a8..8e55f55 100644 --- a/libraries/wuss/test/tasks/saturn.c +++ b/libraries/wuss/test/tasks/saturn.c @@ -94,7 +94,7 @@ result_t saturn_create(wuss_t *wuss, saturn_task_t *task) rc = wuss_window_create_placed(delegate, SIZE2D(SATURN_SIZE, SATURN_SIZE), "Saturn", - wuss_WINDOW_NONE, + wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), SIZE2D(SATURN_SIZE, SATURN_SIZE), SIZE2D(0, 0), @@ -135,9 +135,10 @@ static result_t saturn_redraw(const wuss_event_t *event, saturn_task_t *task) screen_fill_rect(scr, content->x0, content->y0, box_size(content), task->bg); - /* no scroll: window is fixed at SATURN_SIZE and non-resizable */ - ox = bounds->x0; - oy = bounds->y0; + /* plot in doc space (saturn_plot clips to 0..SATURN_SIZE); origin carries + * the scroll so a scrolled/shrunk window shows the right slice */ + ox = bounds->x0 - event->data.redraw.scroll.x; + oy = bounds->y0 - event->data.redraw.scroll.y; saturn_rnd_seed(task->seed); diff --git a/libraries/wuss/test/tasks/sofa.c b/libraries/wuss/test/tasks/sofa.c index c6bd0ba..0b04fa5 100644 --- a/libraries/wuss/test/tasks/sofa.c +++ b/libraries/wuss/test/tasks/sofa.c @@ -384,7 +384,7 @@ result_t sofa_create(wuss_t*wuss, sofa_task_t*task) rc = wuss_window_create_placed(delegate, SIZE2D(180, 160), "Sofa", - wuss_WINDOW_NONE, + wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), SIZE2D(180, 160), SIZE2D(0, 0), diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 1ed5da2..2c365f2 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -601,7 +601,7 @@ result_t wuss_test(const char *resources) symdel = mk_task(symw, NULL, NULL); if (symdel == NULL) { wuss_destroy(symw); goto Failure; } symbox.x0 = 0; symbox.y0 = 0; symbox.x1 = 80; symbox.y1 = 80; - rc = wuss_window_create(symdel, &symbox, "sym", wuss_WINDOW_NONE, + rc = wuss_window_create(symdel, &symbox, "sym", wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_COLOUR_RED), SIZE2D(80, 80), SIZE2D(80, 80), &symwin); if (rc != result_OK) { wuss_destroy(symw); goto Failure; } @@ -624,7 +624,7 @@ result_t wuss_test(const char *resources) rc = wuss_window_create(mk_task(wuss, NULL, NULL), &box_a, "toosmall", - wuss_WINDOW_NONE, + wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), box_size(&box_a), SIZE2D(0, 0), @@ -1351,7 +1351,7 @@ result_t wuss_test(const char *resources) rc = wuss_window_create(delegate_h, &box_h, "H", - wuss_WINDOW_NONE, + wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), box_size(&box_h), SIZE2D(0, 0), @@ -1488,7 +1488,7 @@ result_t wuss_test(const char *resources) box_m.x0 = 10; box_m.y0 = 10; box_m.x1 = 210; box_m.y1 = 210; /* 200x200 content, floored at 80x60 */ - rc = wuss_window_create(delegate_m, &box_m, "M", wuss_WINDOW_NONE, + rc = wuss_window_create(delegate_m, &box_m, "M", wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), SIZE2D(200, 200), SIZE2D(80, 60), &win_m); @@ -1538,7 +1538,7 @@ result_t wuss_test(const char *resources) box_t_win.x0 = 10; box_t_win.y0 = 10; box_t_win.x1 = 50; box_t_win.y1 = 50; /* 40x40 content, room to grow to a 150x150 doc without the toggled box needing to be repositioned off (10,10) -- this test is about the in-place grow blit */ - rc = wuss_window_create(delegate_t, &box_t_win, "T", wuss_WINDOW_NONE, + rc = wuss_window_create(delegate_t, &box_t_win, "T", wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), SIZE2D(150, 150), SIZE2D(0, 0), &win_t); if (rc != result_OK) @@ -1705,7 +1705,7 @@ result_t wuss_test(const char *resources) box_r.x0 = 10; box_r.y0 = 10; box_r.x1 = 50; box_r.y1 = 50; /* 40x40 content; doc bigger than that, so it starts scrollable */ - rc = wuss_window_create(delegate_r, &box_r, "R", wuss_WINDOW_NONE, + rc = wuss_window_create(delegate_r, &box_r, "R", wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), SIZE2D(70, 70), SIZE2D(0, 0), &win_r); if (rc != result_OK) @@ -1810,7 +1810,7 @@ result_t wuss_test(const char *resources) box_d.x0 = 10; box_d.y0 = 10; box_d.x1 = 90; box_d.y1 = 90; /* 80x80 content; doc taller, so it starts scrollable */ - rc = wuss_window_create(delegate_d, &box_d, "D", wuss_WINDOW_NONE, + rc = wuss_window_create(delegate_d, &box_d, "D", wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), SIZE2D(80, 140), SIZE2D(0, 0), &win_d); if (rc != result_OK) @@ -1915,7 +1915,7 @@ result_t wuss_test(const char *resources) box_d.x0 = 10; box_d.y0 = 10; box_d.x1 = 110; box_d.y1 = 90; /* 100x80 content; doc taller, so scrollable */ - rc = wuss_window_create(delegate_d, &box_d, "D", wuss_WINDOW_NONE, + rc = wuss_window_create(delegate_d, &box_d, "D", wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), SIZE2D(100, 140), SIZE2D(0, 0), &win_d); if (rc != result_OK) @@ -1927,7 +1927,7 @@ result_t wuss_test(const char *resources) /* occluder covering the right half of D's content box and beyond */ box_o.x0 = split_x; box_o.y0 = content_before.y0 - 5; box_o.x1 = content_before.x1 + 40; box_o.y1 = content_before.y1 + 40; - rc = wuss_window_create(delegate_o, &box_o, "O", wuss_WINDOW_NONE, + rc = wuss_window_create(delegate_o, &box_o, "O", wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), SIZE2D(box_o.x1 - box_o.x0, box_o.y1 - box_o.y0), SIZE2D(0, 0), &win_o); @@ -2027,7 +2027,7 @@ result_t wuss_test(const char *resources) box_s.x0 = 10; box_s.y0 = 10; box_s.x1 = 110; box_s.y1 = 90; /* 100x80 content; doc taller, so scrollable */ - rc = wuss_window_create(delegate_s, &box_s, "S", wuss_WINDOW_NONE, + rc = wuss_window_create(delegate_s, &box_s, "S", wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), SIZE2D(100, 200), SIZE2D(0, 0), &win_s); if (rc != result_OK) @@ -2038,7 +2038,7 @@ result_t wuss_test(const char *resources) box_o.x0 = split_x; box_o.y0 = content.y0 - 5; box_o.x1 = content.x1 + 40; box_o.y1 = content.y1 + 40; - rc = wuss_window_create(delegate_o, &box_o, "O", wuss_WINDOW_NONE, + rc = wuss_window_create(delegate_o, &box_o, "O", wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), SIZE2D(box_o.x1 - box_o.x0, box_o.y1 - box_o.y0), SIZE2D(0, 0), &win_o); @@ -2130,7 +2130,7 @@ result_t wuss_test(const char *resources) box_m.x0 = 10; box_m.y0 = 10; box_m.x1 = 110; box_m.y1 = 110; /* 100x100 content; doc taller, so scrollable */ - rc = wuss_window_create(delegate_m, &box_m, "M", wuss_WINDOW_NONE, + rc = wuss_window_create(delegate_m, &box_m, "M", wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), SIZE2D(100, 300), SIZE2D(0, 0), &win_m); if (rc != result_OK) @@ -2145,7 +2145,7 @@ result_t wuss_test(const char *resources) * repaint region touches O, so without the fix O stays overpainted. */ box_o.x0 = content.x0 - 5; box_o.y0 = content.y0 + 40; box_o.x1 = content.x1 + 5; box_o.y1 = content.y0 + 70; - rc = wuss_window_create(delegate_o, &box_o, "O", wuss_WINDOW_NONE, + rc = wuss_window_create(delegate_o, &box_o, "O", wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), SIZE2D(box_o.x1 - box_o.x0, box_o.y1 - box_o.y0), SIZE2D(0, 0), &win_o); @@ -2211,7 +2211,7 @@ result_t wuss_test(const char *resources) box_m.x0 = 10; box_m.y0 = 10; box_m.x1 = 110; box_m.y1 = 110; /* 100x100 content; doc taller, scrollable */ - rc = wuss_window_create(delegate_m, &box_m, "M", wuss_WINDOW_NONE, + rc = wuss_window_create(delegate_m, &box_m, "M", wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), SIZE2D(100, 300), SIZE2D(0, 0), &win_m); if (rc != result_OK) @@ -2222,7 +2222,7 @@ result_t wuss_test(const char *resources) /* O strictly inside M's content, gap on every side */ box_o.x0 = content.x0 + 25; box_o.y0 = content.y0 + 25; box_o.x1 = content.x0 + 75; box_o.y1 = content.y0 + 65; - rc = wuss_window_create(delegate_o, &box_o, "O", wuss_WINDOW_NONE, + rc = wuss_window_create(delegate_o, &box_o, "O", wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), SIZE2D(box_o.x1 - box_o.x0, box_o.y1 - box_o.y0), SIZE2D(0, 0), &win_o); @@ -2362,7 +2362,7 @@ result_t wuss_test(const char *resources) box_u.x0 = 80; box_u.y0 = 80; box_u.x1 = 120; box_u.y1 = 120; /* 40x40 content */ - rc = wuss_window_create(mk_task(wuss, NULL, NULL), &box_u, "U", wuss_WINDOW_NONE, + rc = wuss_window_create(mk_task(wuss, NULL, NULL), &box_u, "U", wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), SIZE2D(70, 70), SIZE2D(0, 0), &win_u); /* doc size well within the 200x200 screen: growth is doc-limited, not screen-limited */ if (rc != result_OK) @@ -2472,7 +2472,7 @@ result_t wuss_test(const char *resources) * unrelated to toggle-size specifically). * Doc big enough that maximize is * screen-limited, not doc-limited. */ - rc = wuss_window_create(delegate_v, &box_v, "V", wuss_WINDOW_NONE, + rc = wuss_window_create(delegate_v, &box_v, "V", wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), SIZE2D(200, 200), SIZE2D(0, 0), &win_v); if (rc != result_OK) @@ -2587,7 +2587,7 @@ result_t wuss_test(const char *resources) * have a real interior the blit can keep; * doc big enough that maximize is * screen-limited so the window must move */ - rc = wuss_window_create(delegate_p, &box_p, "P", wuss_WINDOW_NONE, + rc = wuss_window_create(delegate_p, &box_p, "P", wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), SIZE2D(400, 400), SIZE2D(0, 0), &win_p); if (rc != result_OK) @@ -2724,7 +2724,7 @@ result_t wuss_test(const char *resources) box_z.x0 = 20; box_z.y0 = 20; box_z.x1 = 60; box_z.y1 = 60; /* 40x40 content */ - rc = wuss_window_create(delegate_z, &box_z, "Z", wuss_WINDOW_NONE, + rc = wuss_window_create(delegate_z, &box_z, "Z", wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), axis == 0 ? SIZE2D(0, 0) : SIZE2D(60, 0), SIZE2D(0, 0), &win_z); @@ -3567,7 +3567,7 @@ result_t wuss_test(const char *resources) box_s.x0 = 10; box_s.y0 = 10; box_s.x1 = 60; box_s.y1 = 60; /* 50x50 content onto a 200x200 doc: room to scroll */ - rc = wuss_window_create(delegate_s, &box_s, "S", wuss_WINDOW_NONE, + rc = wuss_window_create(delegate_s, &box_s, "S", wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), SIZE2D(200, 200), SIZE2D(0, 0), &win_s); if (rc != result_OK) @@ -3642,7 +3642,7 @@ result_t wuss_test(const char *resources) rc = wuss_window_create(delegate_r, &box_r, "rules", - wuss_WINDOW_NONE, /* all furniture present */ + wuss_WINDOW_DEFAULT, /* all furniture present */ wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), SIZE2D(400, 400), SIZE2D(0, 0), @@ -3686,7 +3686,7 @@ result_t wuss_test(const char *resources) rc = wuss_window_create_placed(delegate_p, SIZE2D(40, 30), "P", - wuss_WINDOW_NONE, + wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), SIZE2D(40, 30), SIZE2D(0, 0), @@ -3709,7 +3709,7 @@ result_t wuss_test(const char *resources) rc = wuss_window_create_placed(delegate_p, SIZE2D(40, 30), "P", - wuss_WINDOW_NONE, + wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), SIZE2D(40, 30), SIZE2D(0, 0), @@ -3792,7 +3792,7 @@ result_t wuss_test(const char *resources) box_ps.x0 = 0; box_ps.y0 = 0; box_ps.x1 = 60; box_ps.y1 = 60; - rc = wuss_window_create(delegate_ps, &box_ps, "PS", wuss_WINDOW_NONE, + rc = wuss_window_create(delegate_ps, &box_ps, "PS", wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), box_size(&box_ps), SIZE2D(0, 0), &win_ps); if (rc != result_OK) @@ -3856,12 +3856,12 @@ result_t wuss_test(const char *resources) box_q.x0 = 0; box_q.y0 = 0; box_q.x1 = 40; box_q.y1 = 40; - rc = wuss_window_create(delegate_q, &box_q, "Q1", wuss_WINDOW_NONE, + rc = wuss_window_create(delegate_q, &box_q, "Q1", wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), box_size(&box_q), SIZE2D(0, 0), &win_q1); if (rc != result_OK) goto Failure; - rc = wuss_window_create(delegate_q, &box_q, "Q2", wuss_WINDOW_NONE, + rc = wuss_window_create(delegate_q, &box_q, "Q2", wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), box_size(&box_q), SIZE2D(0, 0), &win_q2); if (rc != result_OK) @@ -3891,12 +3891,12 @@ result_t wuss_test(const char *resources) box_ac.x0 = 0; box_ac.y0 = 0; box_ac.x1 = 40; box_ac.y1 = 40; - rc = wuss_window_create(delegate_ac, &box_ac, "AC1", wuss_WINDOW_NONE, + rc = wuss_window_create(delegate_ac, &box_ac, "AC1", wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), box_size(&box_ac), SIZE2D(0, 0), &win_ac1); if (rc != result_OK) goto Failure; - rc = wuss_window_create(delegate_ac, &box_ac, "AC2", wuss_WINDOW_NONE, + rc = wuss_window_create(delegate_ac, &box_ac, "AC2", wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), box_size(&box_ac), SIZE2D(0, 0), &win_ac2); if (rc != result_OK) @@ -3947,7 +3947,7 @@ result_t wuss_test(const char *resources) delegate_w1 = mk_task(wuss, test_handle, &tc_w1); if (delegate_w1 == NULL) goto Failure; wuss_task_set_autoclose(delegate_w1, 1); - rc = wuss_window_create(delegate_w1, &box_w, "W1", wuss_WINDOW_NONE, + rc = wuss_window_create(delegate_w1, &box_w, "W1", wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), box_size(&box_w), SIZE2D(0, 0), &win_w1); if (rc != result_OK) @@ -3956,7 +3956,7 @@ result_t wuss_test(const char *resources) delegate_w2 = mk_task(wuss, close_on_idle_handle, &tc_w2); if (delegate_w2 == NULL) goto Failure; wuss_task_set_autoclose(delegate_w2, 1); - rc = wuss_window_create(delegate_w2, &box_w, "W2", wuss_WINDOW_NONE, + rc = wuss_window_create(delegate_w2, &box_w, "W2", wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), box_size(&box_w), SIZE2D(0, 0), &g_close_on_idle_win); @@ -3966,7 +3966,7 @@ result_t wuss_test(const char *resources) delegate_w3 = mk_task(wuss, test_handle, &tc_w3); if (delegate_w3 == NULL) goto Failure; wuss_task_set_autoclose(delegate_w3, 1); - rc = wuss_window_create(delegate_w3, &box_w, "W3", wuss_WINDOW_NONE, + rc = wuss_window_create(delegate_w3, &box_w, "W3", wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), box_size(&box_w), SIZE2D(0, 0), &win_w3); if (rc != result_OK) @@ -4829,7 +4829,7 @@ ColourMenuOK: ; * overlap in the titlebar. */ box_hs.x0 = 5; box_hs.y0 = 5; box_hs.x1 = 125; box_hs.y1 = 85; - rc = wuss_window_create(delegate_hs, &box_hs, "HS", wuss_WINDOW_NONE, + rc = wuss_window_create(delegate_hs, &box_hs, "HS", wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), SIZE2D(400, 400), SIZE2D(0, 0), &win_hs); if (rc != result_OK) @@ -5038,7 +5038,7 @@ result_t wuss_test(const char *resources) printf("test: window_create too small\n"); box_a.x0 = 0; box_a.y0 = 0; box_a.x1 = 100; box_a.y1 = 0; - rc = wuss_window_create(mk_task(wuss, NULL, NULL), &box_a, "toosmall", wuss_WINDOW_NONE, + rc = wuss_window_create(mk_task(wuss, NULL, NULL), &box_a, "toosmall", wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), box_size(&box_a), SIZE2D(0, 0), &win_a); From 56230061573e2187926402fc1027bab9201c202d Mon Sep 17 00:00:00 2001 From: David Thomas Date: Tue, 8 Sep 2026 11:01:10 +0100 Subject: [PATCH 49/98] feat(wuss): add wuss_ICON_BORDER_DIVIDER label border A 4px-per-edge divider surround: an outer sunken 2px bevel wrapping an inner raised one, drawn with the bevel highlight against the icon's own face colour rather than the full bevel shadow, so it reads lighter than RIDGE/GROOVE. Extends the spec validator upper bound and adds a Divider example to the icons test task's Borders frame. Co-Authored-By: Claude Sonnet 5 --- apps/wuss/main.c | 93 +++++++++++----------- include/wuss/icon.h | 8 +- include/wuss/wuss.h | 16 ++-- libraries/wuss/core/create.c | 30 +++++-- libraries/wuss/core/impl.h | 1 + libraries/wuss/core/rebuild-palettecache.c | 1 + libraries/wuss/icon/draw.c | 22 ++++- libraries/wuss/icon/from-spec.c | 2 +- libraries/wuss/test/tasks/icons.c | 25 ++++-- 9 files changed, 127 insertions(+), 71 deletions(-) diff --git a/apps/wuss/main.c b/apps/wuss/main.c index f263295..f0175b1 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -35,52 +35,54 @@ /* Furniture/bevel/accent/backdrop/body colour indices, one row per palette. * Same field order as the assignments in fill_chrome_config. */ -static const wuss_colour_t g_chrome[2][19] = +static const wuss_colour_t g_chrome[2][20] = { /* PICO-8 */ { - palette_PICO8_DARK_BLUE, - palette_PICO8_WHITE, - palette_PICO8_DARK_BLUE, - palette_PICO8_GREEN, - palette_PICO8_RED, - palette_PICO8_ORANGE, - palette_PICO8_LAVENDER, - palette_PICO8_BLUE, - palette_PICO8_DARK_BLUE, - palette_PICO8_LIGHT_GREY, - palette_PICO8_WHITE, - palette_PICO8_DARK_GREY, - palette_PICO8_LAVENDER, - palette_PICO8_ORANGE, - palette_PICO8_WHITE, - palette_PICO8_WHITE, - palette_PICO8_LIGHT_GREY, - palette_PICO8_LIGHT_GREY, - palette_PICO8_WHITE + palette_PICO8_DARK_BLUE, /* [0] furniture.title.bg */ + palette_PICO8_WHITE, /* [1] furniture.title.fg */ + palette_PICO8_DARK_BLUE, /* [2] furniture.outline */ + palette_PICO8_GREEN, /* [3] furniture.back */ + palette_PICO8_RED, /* [4] furniture.close */ + palette_PICO8_ORANGE, /* [5] furniture.toggle */ + palette_PICO8_LAVENDER, /* [6] furniture.resize */ + palette_PICO8_BLUE, /* [7] furniture.scroll.arrows */ + palette_PICO8_DARK_BLUE, /* [8] furniture.scroll.wells */ + palette_PICO8_LIGHT_GREY, /* [9] furniture.scroll.sausages */ + palette_PICO8_WHITE, /* [10] bevel.light */ + palette_PICO8_DARK_GREY, /* [11] bevel.dark */ + palette_PICO8_LIGHT_GREY, /* [12] bevel.pressed */ + palette_PICO8_LIGHT_GREY, /* [13] bevel.divider */ + palette_PICO8_LAVENDER, /* [14] accent.bg */ + palette_PICO8_ORANGE, /* [15] accent.fg */ + palette_PICO8_WHITE, /* [16] backdrop.colour */ + palette_PICO8_WHITE, /* [17] backdrop.pattern_bg */ + palette_PICO8_LIGHT_GREY, /* [18] body.window */ + palette_PICO8_LIGHT_GREY /* [19] body.menu */ }, /* RISC OS 16-colour Wimp */ { - palette_WIMP16_GREY_75, - palette_WIMP16_BLACK, - palette_WIMP16_BLACK, - palette_WIMP16_GREEN, - palette_WIMP16_RED, - palette_WIMP16_ORANGE, - palette_WIMP16_LIGHT_BLUE, - palette_WIMP16_GREY_50, - palette_WIMP16_GREY_62, - palette_WIMP16_GREY_87, - palette_WIMP16_WHITE, - palette_WIMP16_GREY_50, - palette_WIMP16_GREY_62, - palette_WIMP16_ORANGE, - palette_WIMP16_BLACK, - palette_WIMP16_GREY_50, - palette_WIMP16_GREY_37, - palette_WIMP16_GREY_87, - palette_WIMP16_WHITE + palette_WIMP16_GREY_75, /* [0] furniture.title.bg */ + palette_WIMP16_BLACK, /* [1] furniture.title.fg */ + palette_WIMP16_BLACK, /* [2] furniture.outline */ + palette_WIMP16_GREEN, /* [3] furniture.back */ + palette_WIMP16_RED, /* [4] furniture.close */ + palette_WIMP16_ORANGE, /* [5] furniture.toggle */ + palette_WIMP16_LIGHT_BLUE,/* [6] furniture.resize */ + palette_WIMP16_GREY_50, /* [7] furniture.scroll.arrows */ + palette_WIMP16_GREY_62, /* [8] furniture.scroll.wells */ + palette_WIMP16_GREY_87, /* [9] furniture.scroll.sausages */ + palette_WIMP16_WHITE, /* [10] bevel.light */ + palette_WIMP16_GREY_50, /* [11] bevel.dark */ + palette_WIMP16_GREY_87, /* [12] bevel.pressed */ + palette_WIMP16_GREY_87, /* [13] bevel.divider */ + palette_WIMP16_GREY_62, /* [14] accent.bg */ + palette_WIMP16_ORANGE, /* [15] accent.fg */ + palette_WIMP16_BLACK, /* [16] backdrop.colour */ + palette_WIMP16_GREY_50, /* [17] backdrop.pattern_bg */ + palette_WIMP16_GREY_37, /* [18] body.window */ + palette_WIMP16_GREY_87 /* [19] body.menu */ } }; @@ -102,13 +104,14 @@ static void fill_chrome_config(wuss_config_t *config, int palette_index) config->bevel.light = c[10]; config->bevel.dark = c[11]; config->bevel.pressed = c[12]; - config->accent.bg = c[13]; - config->accent.fg = c[14]; - config->backdrop.colour = c[15]; + config->bevel.divider = c[13]; + config->accent.bg = c[14]; + config->accent.fg = c[15]; + config->backdrop.colour = c[16]; config->backdrop.pattern = screen_PATTERN_DOTS; - config->backdrop.pattern_bg = c[16]; - config->body.window = c[17]; - config->body.menu = c[18]; + config->backdrop.pattern_bg = c[17]; + config->body.window = c[18]; + config->body.menu = c[19]; } /* Redraw the whole screen one pixel at a time: each wuss_redraw_dirty call is diff --git a/include/wuss/icon.h b/include/wuss/icon.h index 8442c15..407ffa0 100644 --- a/include/wuss/icon.h +++ b/include/wuss/icon.h @@ -123,7 +123,13 @@ typedef enum wuss_icon_border /** A 6px-per-edge "action" surround: a 2px raised outset, a 2px moat filled * with wuss_COLOUR_ACCENT_BG, then a 2px raised inset -- like a RISC OS * default-action button. */ - wuss_ICON_BORDER_ACTION + wuss_ICON_BORDER_ACTION, + /** A 4px-per-edge "divider": two 2px bevels, an outer sunken ring wrapping an + * inner raised one. Drawn in a lighter pair of shades than RIDGE/GROOVE -- + * the bevel highlight against the icon's own face colour rather than the + * full bevel shadow -- so it reads as a soft inset panel rather than a hard + * button edge. */ + wuss_ICON_BORDER_DIVIDER } wuss_icon_border_t; diff --git a/include/wuss/wuss.h b/include/wuss/wuss.h index 79f8102..58c342c 100644 --- a/include/wuss/wuss.h +++ b/include/wuss/wuss.h @@ -139,6 +139,7 @@ typedef unsigned char wuss_colour_t; * concrete index -- e.g. wuss_COLOUR_TITLE_BG is furniture.title.bg, * wuss_COLOUR_BUTTON_HILIGHT is bevel.light, wuss_COLOUR_BUTTON_SHADOW is * bevel.dark, wuss_COLOUR_BUTTON_PRESSED is bevel.pressed, + * wuss_COLOUR_BORDER_DIVIDER is bevel.divider, * wuss_COLOUR_BACKDROP is backdrop.colour, * wuss_COLOUR_WINDOW is body.window, wuss_COLOUR_MENU is body.menu. */ #define wuss_COLOUR_TITLE_BG (wuss_COLOUR_SYMBOLIC + 16) @@ -151,6 +152,7 @@ typedef unsigned char wuss_colour_t; #define wuss_COLOUR_WINDOW (wuss_COLOUR_SYMBOLIC + 23) #define wuss_COLOUR_MENU (wuss_COLOUR_SYMBOLIC + 24) #define wuss_COLOUR_BUTTON_PRESSED (wuss_COLOUR_SYMBOLIC + 25) +#define wuss_COLOUR_BORDER_DIVIDER (wuss_COLOUR_SYMBOLIC + 26) /** Furniture chrome colours, one entry per class of furniture. Title is * the only two-tone class (fill + text); the rest are drawn as a single @@ -329,17 +331,21 @@ typedef struct wuss_config * Bevelled work-area button edge shades, as indices into the system * palette: light on the top/left edges, dark on the bottom/right (swapped * when the button is pressed). pressed is the button face fill while it is - * held down, in place of the icon's own background. light and dark default - * to the titlebar fill colour when config is NULL; pressed defaults to - * dark. Read back through wuss_COLOUR_BUTTON_HILIGHT / - * wuss_COLOUR_BUTTON_SHADOW / wuss_COLOUR_BUTTON_PRESSED. Ignored when - * both WUSS_FURNITURE and WUSS_ICONS are off. + * held down, in place of the icon's own background. divider is the lighter + * edge shade for a wuss_ICON_BORDER_DIVIDER label surround, paired with + * light for its sunken and raised rings. light and dark default to the + * titlebar fill colour when config is NULL; pressed defaults to dark; + * divider defaults to light. Read back through wuss_COLOUR_BUTTON_HILIGHT + * / wuss_COLOUR_BUTTON_SHADOW / wuss_COLOUR_BUTTON_PRESSED / + * wuss_COLOUR_BORDER_DIVIDER. Ignored when both WUSS_FURNITURE and + * WUSS_ICONS are off. */ struct { wuss_colour_t light; /**< Top/left bevel edge. */ wuss_colour_t dark; /**< Bottom/right bevel edge. */ wuss_colour_t pressed; /**< Button face fill while held down. */ + wuss_colour_t divider; /**< Lighter edge shade for a DIVIDER border. */ } bevel; diff --git a/libraries/wuss/core/create.c b/libraries/wuss/core/create.c index cbfbf5d..fabe5c0 100644 --- a/libraries/wuss/core/create.c +++ b/libraries/wuss/core/create.c @@ -35,14 +35,16 @@ static result_t validate_bevel_backdrop(const wuss_t *w, wuss_colour_t blight, wuss_colour_t bdark, wuss_colour_t bpressed, + wuss_colour_t bdivider, wuss_colour_t abg, wuss_colour_t afg) { - if (blight >= w->npalette || - bdark >= w->npalette || - bpressed >= w->npalette || - abg >= w->npalette || - afg >= w->npalette || + if (blight >= w->npalette || + bdark >= w->npalette || + bpressed >= w->npalette || + bdivider >= w->npalette || + abg >= w->npalette || + afg >= w->npalette || wuss__validate_backdrop(w, &w->backdrop) != result_OK) return result_WUSS_BAD_COLOUR; @@ -67,7 +69,7 @@ result_t wuss_create(screen_t *scr, wuss_colour_t bg, fg; #endif #if defined(WUSS_FURNITURE) || defined(WUSS_ICONS) - wuss_colour_t blight, bdark, bpressed; + wuss_colour_t blight, bdark, bpressed, bdivider; wuss_colour_t abg, afg; #endif #ifdef WUSS_FURNITURE @@ -160,6 +162,9 @@ result_t wuss_create(screen_t *scr, bpressed = (config->bevel.pressed == wuss_NO_BACKGROUND) ? bdark : wuss__resolve_colour(w, config->bevel.pressed); + bdivider = (config->bevel.divider == wuss_NO_BACKGROUND) + ? blight + : wuss__resolve_colour(w, config->bevel.divider); abg = wuss__resolve_colour(w, config->accent.bg); afg = wuss__resolve_colour(w, config->accent.fg); } @@ -182,6 +187,7 @@ result_t wuss_create(screen_t *scr, blight = 0; bdark = 0; bpressed = 0; + bdivider = 0; abg = bg; /* default action button: the titlebar colours */ afg = fg; } @@ -196,7 +202,8 @@ result_t wuss_create(screen_t *scr, pal.scroll.arrows >= w->npalette || pal.scroll.wells >= w->npalette || pal.scroll.sausages >= w->npalette || - validate_bevel_backdrop(w, blight, bdark, bpressed, abg, afg) != result_OK) + validate_bevel_backdrop(w, blight, bdark, bpressed, bdivider, + abg, afg) != result_OK) { wuss__free(w, w->palette); wuss__free(w, w); @@ -207,6 +214,7 @@ result_t wuss_create(screen_t *scr, w->bevel_light = blight; w->bevel_dark = bdark; w->bevel_pressed = bpressed; + w->bevel_divider = bdivider; w->accent_bg = abg; w->accent_fg = afg; @@ -241,6 +249,9 @@ result_t wuss_create(screen_t *scr, bpressed = (config->bevel.pressed == wuss_NO_BACKGROUND) ? bdark : wuss__resolve_colour(w, config->bevel.pressed); + bdivider = (config->bevel.divider == wuss_NO_BACKGROUND) + ? blight + : wuss__resolve_colour(w, config->bevel.divider); abg = wuss__resolve_colour(w, config->accent.bg); afg = wuss__resolve_colour(w, config->accent.fg); } @@ -249,10 +260,12 @@ result_t wuss_create(screen_t *scr, blight = 0; bdark = 0; bpressed = 0; + bdivider = 0; abg = 0; afg = (w->npalette > 1) ? 1 : 0; } - if (validate_bevel_backdrop(w, blight, bdark, bpressed, abg, afg) != result_OK) + if (validate_bevel_backdrop(w, blight, bdark, bpressed, bdivider, + abg, afg) != result_OK) { wuss__free(w, w->palette); wuss__free(w, w); @@ -261,6 +274,7 @@ result_t wuss_create(screen_t *scr, w->bevel_light = blight; w->bevel_dark = bdark; w->bevel_pressed = bpressed; + w->bevel_divider = bdivider; w->accent_bg = abg; w->accent_fg = afg; #else diff --git a/libraries/wuss/core/impl.h b/libraries/wuss/core/impl.h index feef029..cb22292 100644 --- a/libraries/wuss/core/impl.h +++ b/libraries/wuss/core/impl.h @@ -126,6 +126,7 @@ struct wuss wuss_colour_t bevel_light; /* work-area button top/left edge */ wuss_colour_t bevel_dark; /* work-area button bottom/right edge */ wuss_colour_t bevel_pressed; /* work-area button face while held */ + wuss_colour_t bevel_divider; /* lighter edge for a DIVIDER border */ wuss_colour_t accent_bg; /* default action button fill */ wuss_colour_t accent_fg; /* default action button text */ #endif diff --git a/libraries/wuss/core/rebuild-palettecache.c b/libraries/wuss/core/rebuild-palettecache.c index a5280fd..fc3c114 100644 --- a/libraries/wuss/core/rebuild-palettecache.c +++ b/libraries/wuss/core/rebuild-palettecache.c @@ -51,6 +51,7 @@ void wuss__rebuild_palettecache(wuss_t *wuss) cache[wuss_COLOUR_BUTTON_HILIGHT - wuss_COLOUR_SYMBOLIC] = wuss->bevel_light; cache[wuss_COLOUR_BUTTON_SHADOW - wuss_COLOUR_SYMBOLIC] = wuss->bevel_dark; cache[wuss_COLOUR_BUTTON_PRESSED - wuss_COLOUR_SYMBOLIC] = wuss->bevel_pressed; + cache[wuss_COLOUR_BORDER_DIVIDER - wuss_COLOUR_SYMBOLIC] = wuss->bevel_divider; cache[wuss_COLOUR_ACCENT_BG - wuss_COLOUR_SYMBOLIC] = wuss->accent_bg; cache[wuss_COLOUR_ACCENT_FG - wuss_COLOUR_SYMBOLIC] = wuss->accent_fg; #endif diff --git a/libraries/wuss/icon/draw.c b/libraries/wuss/icon/draw.c index 6f8dd92..b1ef705 100644 --- a/libraries/wuss/icon/draw.c +++ b/libraries/wuss/icon/draw.c @@ -170,16 +170,30 @@ static void wuss__icon_draw_label(const icon_draw_ctx_t *c) if (icon->border != wuss_ICON_BORDER_NONE) { - colour_t light, dark, accent; + colour_t light, dark, accent, divider; - light = c->wuss->palette[c->wuss->bevel_light]; - dark = c->wuss->palette[c->wuss->bevel_dark]; - accent = c->wuss->palette[c->wuss->accent_bg]; /* the action-button fill */ + light = c->wuss->palette[c->wuss->bevel_light]; + dark = c->wuss->palette[c->wuss->bevel_dark]; + accent = c->wuss->palette[c->wuss->accent_bg]; /* the action-button fill */ + divider = c->wuss->palette[c->wuss->bevel_divider]; if (icon->border == wuss_ICON_BORDER_ACTION) { icon_draw_action_border(c->scr, b, light, dark, accent, 0); } + else if (icon->border == wuss_ICON_BORDER_DIVIDER) + { + /* Two 2px bevels in a lighter pair than RIDGE/GROOVE: bevel_divider + * against bevel_light rather than the full light/dark contrast. Outer + * ring sunken, inner ring raised. */ + box_t ring = *b; + + screen_draw_bevel_edge(c->scr, &ring, divider, light); + + ring = (box_t) BOX_POS_SIZE(b->x0 + 2, b->y0 + 2, + b->x1 - b->x0 - 4, b->y1 - b->y0 - 4); + screen_draw_bevel_edge(c->scr, &ring, light, divider); + } else { /* RIDGE reads raised (light top/left); GROOVE reads sunken. One 2px diff --git a/libraries/wuss/icon/from-spec.c b/libraries/wuss/icon/from-spec.c index bf68f70..561ba05 100644 --- a/libraries/wuss/icon/from-spec.c +++ b/libraries/wuss/icon/from-spec.c @@ -57,7 +57,7 @@ result_t wuss__icon_from_spec(const wuss_t *w, return result_WUSS_BAD_ICON; if (spec->type == wuss_ICON_TYPE_LABEL && - spec->border > wuss_ICON_BORDER_ACTION) + spec->border > wuss_ICON_BORDER_DIVIDER) return result_WUSS_BAD_ICON; if (fg >= w->npalette) diff --git a/libraries/wuss/test/tasks/icons.c b/libraries/wuss/test/tasks/icons.c index 8dfa916..680681d 100644 --- a/libraries/wuss/test/tasks/icons.c +++ b/libraries/wuss/test/tasks/icons.c @@ -23,7 +23,7 @@ #include "icons.h" #define ICONS_DOC_W 260 -#define ICONS_DOC_H 900 /* taller than the window, so scrolling is exercised */ +#define ICONS_DOC_H 1100 /* taller than the window, so scrolling is exercised */ #define ICONS_MARGIN 28 /* left edge of everything except frame captions */ #define ICONS_ROW 20 /* vertical pitch between stacked simple icons */ @@ -38,7 +38,7 @@ enum ICONS_N_RADIOS = 8, /* frame + 3 radios + option + state label + 2 justified labels */ ICONS_N_BITMAPS = 3, /* frame + decorative + interactive bitmap */ ICONS_N_PATTERN = 2, /* frame + one PATTERN swatch */ - ICONS_N_BORDERS = 4, /* frame + GROOVE + RIDGE + ACTION labels */ + ICONS_N_BORDERS = 6, /* frame + GROOVE + RIDGE + ACTION + DIVIDER labels */ ICONS_N_MENU = 7, /* plain, ticked, swatch, submenu, disabled, rule, separator entry */ ICONS_NSPECS = ICONS_N_INTRO + ICONS_N_BUTTONS + ICONS_N_RADIOS + ICONS_N_BITMAPS + ICONS_N_PATTERN + ICONS_N_BORDERS + @@ -101,7 +101,7 @@ static void icons_add_intro(icons_layout_t *lay, int *button, int *counter) lay->y += 46; s = &lay->specs[lay->n]; - s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN, 780, 90, 52); + s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN, 980, 90, 52); s->type = wuss_ICON_TYPE_BUTTON; s->text = "Scrolled"; s->fg = lay->black; @@ -296,9 +296,10 @@ static void icons_add_pattern(icons_layout_t *lay) } /* A grouping frame captioned "Borders", holding a GROOVE-bordered label (a - * sunken RISC OS display field), a RIDGE-bordered one (raised), then an + * sunken RISC OS display field), a RIDGE-bordered one (raised), an * ACTION-bordered one -- a 6px surround: raised outset, accent moat, raised - * inset, like a default-action button. */ + * inset, like a default-action button -- then a DIVIDER-bordered one: a 4px + * surround, outer sunken ring around inner raised, in lighter shades. */ static void icons_add_borders(icons_layout_t *lay) { wuss_icon_spec_t *s; @@ -306,7 +307,7 @@ static void icons_add_borders(icons_layout_t *lay) top = lay->y; s = &lay->specs[lay->n]; - s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN, top, 200, 116); + s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN, top, 200, 150); s->type = wuss_ICON_TYPE_FRAME; s->text = "Borders"; s->fg = lay->black; @@ -343,7 +344,17 @@ static void icons_add_borders(icons_layout_t *lay) s->flags = wuss_ICON_FLAGS_JUSTIFY_CENTRE; lay->n++; - lay->y = top + 132; + s = &lay->specs[lay->n]; + s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN + 10, top + 110, 180, 30); + s->type = wuss_ICON_TYPE_LABEL; + s->text = "Divider"; + s->fg = lay->black; + s->bg = lay->window; + s->border = wuss_ICON_BORDER_DIVIDER; + s->flags = wuss_ICON_FLAGS_JUSTIFY_CENTRE; + lay->n++; + + lay->y = top + 166; } /* A menu-entry strip: plain, ticked, a swatch entry, a submenu entry, a From 70a9a7e3107fe284040bf1935bd63b7ccf86293d Mon Sep 17 00:00:00 2001 From: David Thomas Date: Tue, 8 Sep 2026 12:57:59 +0100 Subject: [PATCH 50/98] fix(wuss): paint interior rules and resize seams in outline colour The 1px dividers between the content area and its right/bottom furniture, and the seams separating the resize corner from the scrollbar strips, were painted in wuss__FURNITURE_PAINT_TITLE_BG. With a titlebar fill colour like PICO8_YELLOW that made the interior edges yellow rather than the intended outline colour. Point them at wuss__FURNITURE_PAINT_OUTLINE so they match the window outline. TITLE_BG now covers only the titlebar fill and the resize-carve bands. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/furniture.h | 2 +- libraries/wuss/furniture/layout.c | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/libraries/wuss/furniture.h b/libraries/wuss/furniture.h index 79ffe8d..052e4fe 100644 --- a/libraries/wuss/furniture.h +++ b/libraries/wuss/furniture.h @@ -20,7 +20,7 @@ * at paint time. */ typedef enum wuss__furniture_paint_class { - wuss__FURNITURE_PAINT_TITLE_BG, /* titlebar fill, bands, seams, interior rules */ + wuss__FURNITURE_PAINT_TITLE_BG, /* titlebar fill, resize-carve bands */ wuss__FURNITURE_PAINT_CLOSE, wuss__FURNITURE_PAINT_BACK, wuss__FURNITURE_PAINT_TOGGLE, diff --git a/libraries/wuss/furniture/layout.c b/libraries/wuss/furniture/layout.c index f59d970..aa2d580 100644 --- a/libraries/wuss/furniture/layout.c +++ b/libraries/wuss/furniture/layout.c @@ -130,7 +130,7 @@ void wuss__furniture_layout_build(wuss_window_t *window) rule.x1 = resize.x1; rule.y0 = resize.y0 - top_seam; rule.y1 = resize.y0; - push_piece(layout, &rule, wuss__FURNITURE_PAINT_TITLE_BG); + push_piece(layout, &rule, wuss__FURNITURE_PAINT_OUTLINE); } if (left_seam > 0) @@ -139,7 +139,7 @@ void wuss__furniture_layout_build(wuss_window_t *window) rule.x1 = resize.x0; rule.y0 = resize.y0 - top_seam; rule.y1 = resize.y1; - push_piece(layout, &rule, wuss__FURNITURE_PAINT_TITLE_BG); + push_piece(layout, &rule, wuss__FURNITURE_PAINT_OUTLINE); } } @@ -182,7 +182,7 @@ void wuss__furniture_layout_build(wuss_window_t *window) rule.x1 = content.x1 + WUSS_DIVIDER_PX; rule.y0 = content.y0; rule.y1 = content.y1; - push_piece(layout, &rule, wuss__FURNITURE_PAINT_TITLE_BG); + push_piece(layout, &rule, wuss__FURNITURE_PAINT_OUTLINE); } if (carve.y > 0) @@ -193,7 +193,7 @@ void wuss__furniture_layout_build(wuss_window_t *window) rule.x1 = content.x1 + ((carve.x > 0) ? WUSS_DIVIDER_PX : 0); /* meet the vertical rule at the corner */ rule.y0 = content.y1; rule.y1 = content.y1 + WUSS_DIVIDER_PX; - push_piece(layout, &rule, wuss__FURNITURE_PAINT_TITLE_BG); + push_piece(layout, &rule, wuss__FURNITURE_PAINT_OUTLINE); } /* window outline: four one-pixel edges ---------------------------- */ From ca50f13d027f26246cd92e8afbd0fd87ad84de66 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Tue, 8 Sep 2026 14:18:40 +0100 Subject: [PATCH 51/98] feat(wuss): centralise text rendering; split button/accent chrome colours Text rendering: add wuss__text_draw / wuss__text_measure wrappers over bmfont in core/text.c and route every non-test text draw and measure through them (furniture titlebar, icon labels/frames/buttons, menu layout, info component). wuss__text_draw biases pos.y by WUSS_TEXT_BASELINE_ADJUST (default 1px down) for optical vertical centring; override with -DWUSS_TEXT_BASELINE_ADJUST=n at build time. Drop the now-redundant open-coded +1 nudge in wuss__icon_draw_label. The menu decoration glyph (tick/submenu arrow) keeps a raw bmfont_draw: it has a vector fallback that would not get the shift. Chrome colours: split the old bevel.pressed / accent.bg / accent.fg into a dedicated button sub-struct (bg / fg / pressed) plus a single-field accent (colour), with matching wuss_COLOUR_BUTTON_BG / _BUTTON_FG / _ACCENT read-back symbols. Rework the wuss demo's fill_chrome_config to set fields directly per palette rather than index a parallel table. Co-Authored-By: Claude Sonnet 5 --- CMakeLists.txt | 1 + apps/wuss/main.c | 128 +++++++++------------ include/wuss/icon.h | 8 +- include/wuss/wuss.h | 66 +++++++---- libraries/wuss/TODO.txt | 4 +- libraries/wuss/component/info.c | 2 +- libraries/wuss/core/create.c | 124 +++++++++++--------- libraries/wuss/core/impl.h | 32 +++++- libraries/wuss/core/rebuild-palettecache.c | 7 +- libraries/wuss/core/text.c | 43 +++++++ libraries/wuss/furniture/draw.c | 10 +- libraries/wuss/icon/draw.c | 65 ++++++----- libraries/wuss/icon/from-spec.c | 6 +- libraries/wuss/menu/menu.c | 10 +- libraries/wuss/test/wuss-test.c | 8 +- 15 files changed, 298 insertions(+), 216 deletions(-) create mode 100644 libraries/wuss/core/text.c diff --git a/CMakeLists.txt b/CMakeLists.txt index b087e7b..cc88e15 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -373,6 +373,7 @@ set(WUSS_CORE_SOURCES libraries/wuss/core/scroll-step.c libraries/wuss/core/set-backdrop.c libraries/wuss/core/set-palette.c + libraries/wuss/core/text.c libraries/wuss/core/task/create.c libraries/wuss/core/task/destroy.c libraries/wuss/core/task/set-autoclose.c diff --git a/apps/wuss/main.c b/apps/wuss/main.c index f0175b1..0e4016c 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -33,85 +33,61 @@ /* ----------------------------------------------------------------------- */ -/* Furniture/bevel/accent/backdrop/body colour indices, one row per palette. - * Same field order as the assignments in fill_chrome_config. */ -static const wuss_colour_t g_chrome[2][20] = +/* Fill config with the chrome colours for the PICO-8 (default) or RISC OS + * 16-colour Wimp palette. */ +static void fill_chrome_config(wuss_config_t *config, int use_wimp16) { - /* PICO-8 */ + config->titlebar_height = 0; + config->backdrop.pattern = screen_PATTERN_DOTS; + + if (use_wimp16) { - palette_PICO8_DARK_BLUE, /* [0] furniture.title.bg */ - palette_PICO8_WHITE, /* [1] furniture.title.fg */ - palette_PICO8_DARK_BLUE, /* [2] furniture.outline */ - palette_PICO8_GREEN, /* [3] furniture.back */ - palette_PICO8_RED, /* [4] furniture.close */ - palette_PICO8_ORANGE, /* [5] furniture.toggle */ - palette_PICO8_LAVENDER, /* [6] furniture.resize */ - palette_PICO8_BLUE, /* [7] furniture.scroll.arrows */ - palette_PICO8_DARK_BLUE, /* [8] furniture.scroll.wells */ - palette_PICO8_LIGHT_GREY, /* [9] furniture.scroll.sausages */ - palette_PICO8_WHITE, /* [10] bevel.light */ - palette_PICO8_DARK_GREY, /* [11] bevel.dark */ - palette_PICO8_LIGHT_GREY, /* [12] bevel.pressed */ - palette_PICO8_LIGHT_GREY, /* [13] bevel.divider */ - palette_PICO8_LAVENDER, /* [14] accent.bg */ - palette_PICO8_ORANGE, /* [15] accent.fg */ - palette_PICO8_WHITE, /* [16] backdrop.colour */ - palette_PICO8_WHITE, /* [17] backdrop.pattern_bg */ - palette_PICO8_LIGHT_GREY, /* [18] body.window */ - palette_PICO8_LIGHT_GREY /* [19] body.menu */ - }, - - /* RISC OS 16-colour Wimp */ + config->furniture.title.bg = palette_WIMP16_GREY_75; + config->furniture.title.fg = palette_WIMP16_BLACK; + config->furniture.outline = palette_WIMP16_BLACK; + config->furniture.back = palette_WIMP16_GREEN; + config->furniture.close = palette_WIMP16_RED; + config->furniture.toggle = palette_WIMP16_ORANGE; + config->furniture.resize = palette_WIMP16_LIGHT_BLUE; + config->furniture.scroll.arrows = palette_WIMP16_GREY_50; + config->furniture.scroll.wells = palette_WIMP16_GREY_62; + config->furniture.scroll.sausages = palette_WIMP16_GREY_87; + config->bevel.light = palette_WIMP16_WHITE; + config->bevel.dark = palette_WIMP16_GREY_50; + config->bevel.divider = palette_WIMP16_GREY_75; + config->button.bg = palette_WIMP16_GREY_87; + config->button.fg = palette_WIMP16_BLACK; + config->button.pressed = palette_WIMP16_GREY_62; + config->accent.colour = palette_WIMP16_ORANGE; + config->backdrop.colour = palette_WIMP16_GREY_50; + config->backdrop.pattern_bg = palette_WIMP16_GREY_37; + config->body.window = palette_WIMP16_GREY_87; + config->body.menu = palette_WIMP16_WHITE; + } + else { - palette_WIMP16_GREY_75, /* [0] furniture.title.bg */ - palette_WIMP16_BLACK, /* [1] furniture.title.fg */ - palette_WIMP16_BLACK, /* [2] furniture.outline */ - palette_WIMP16_GREEN, /* [3] furniture.back */ - palette_WIMP16_RED, /* [4] furniture.close */ - palette_WIMP16_ORANGE, /* [5] furniture.toggle */ - palette_WIMP16_LIGHT_BLUE,/* [6] furniture.resize */ - palette_WIMP16_GREY_50, /* [7] furniture.scroll.arrows */ - palette_WIMP16_GREY_62, /* [8] furniture.scroll.wells */ - palette_WIMP16_GREY_87, /* [9] furniture.scroll.sausages */ - palette_WIMP16_WHITE, /* [10] bevel.light */ - palette_WIMP16_GREY_50, /* [11] bevel.dark */ - palette_WIMP16_GREY_87, /* [12] bevel.pressed */ - palette_WIMP16_GREY_87, /* [13] bevel.divider */ - palette_WIMP16_GREY_62, /* [14] accent.bg */ - palette_WIMP16_ORANGE, /* [15] accent.fg */ - palette_WIMP16_BLACK, /* [16] backdrop.colour */ - palette_WIMP16_GREY_50, /* [17] backdrop.pattern_bg */ - palette_WIMP16_GREY_37, /* [18] body.window */ - palette_WIMP16_GREY_87 /* [19] body.menu */ + config->furniture.title.bg = palette_PICO8_DARK_BLUE; + config->furniture.title.fg = palette_PICO8_WHITE; + config->furniture.outline = palette_PICO8_BLACK; + config->furniture.back = palette_PICO8_GREEN; + config->furniture.close = palette_PICO8_RED; + config->furniture.toggle = palette_PICO8_ORANGE; + config->furniture.resize = palette_PICO8_LAVENDER; + config->furniture.scroll.arrows = palette_PICO8_BLUE; + config->furniture.scroll.wells = palette_PICO8_DARK_BLUE; + config->furniture.scroll.sausages = palette_PICO8_LIGHT_GREY; + config->bevel.light = palette_PICO8_WHITE; + config->bevel.dark = palette_PICO8_DARK_GREY; + config->bevel.divider = palette_PICO8_LAVENDER; + config->button.bg = palette_PICO8_LIGHT_GREY; + config->button.fg = palette_PICO8_BLACK; + config->button.pressed = palette_PICO8_LAVENDER; + config->accent.colour = palette_PICO8_ORANGE; + config->backdrop.colour = palette_PICO8_LAVENDER; + config->backdrop.pattern_bg = palette_PICO8_LIGHT_GREY; + config->body.window = palette_PICO8_LIGHT_GREY; + config->body.menu = palette_PICO8_WHITE; } -}; - -static void fill_chrome_config(wuss_config_t *config, int palette_index) -{ - const wuss_colour_t *c = g_chrome[palette_index]; - - config->titlebar_height = 0; - config->furniture.title.bg = c[0]; - config->furniture.title.fg = c[1]; - config->furniture.outline = c[2]; - config->furniture.back = c[3]; - config->furniture.close = c[4]; - config->furniture.toggle = c[5]; - config->furniture.resize = c[6]; - config->furniture.scroll.arrows = c[7]; - config->furniture.scroll.wells = c[8]; - config->furniture.scroll.sausages = c[9]; - config->bevel.light = c[10]; - config->bevel.dark = c[11]; - config->bevel.pressed = c[12]; - config->bevel.divider = c[13]; - config->accent.bg = c[14]; - config->accent.fg = c[15]; - config->backdrop.colour = c[16]; - config->backdrop.pattern = screen_PATTERN_DOTS; - config->backdrop.pattern_bg = c[17]; - config->body.window = c[18]; - config->body.menu = c[19]; } /* Redraw the whole screen one pixel at a time: each wuss_redraw_dirty call is @@ -348,7 +324,7 @@ static result_t run_wuss(const char *resources) * picker menus -- [2] Symbols is chrome-only, * never a text font choice */ - fill_chrome_config(&config, use_wimp16 ? 1 : 0); + fill_chrome_config(&config, use_wimp16); for (i = 0; i < nfonts; i++) { diff --git a/include/wuss/icon.h b/include/wuss/icon.h index 407ffa0..7694643 100644 --- a/include/wuss/icon.h +++ b/include/wuss/icon.h @@ -121,14 +121,14 @@ typedef enum wuss_icon_border * read-only display field. */ wuss_ICON_BORDER_GROOVE, /** A 6px-per-edge "action" surround: a 2px raised outset, a 2px moat filled - * with wuss_COLOUR_ACCENT_BG, then a 2px raised inset -- like a RISC OS + * with wuss_COLOUR_ACCENT, then a 2px raised inset -- like a RISC OS * default-action button. */ wuss_ICON_BORDER_ACTION, /** A 4px-per-edge "divider": two 2px bevels, an outer sunken ring wrapping an * inner raised one. Drawn in a lighter pair of shades than RIDGE/GROOVE -- - * the bevel highlight against the icon's own face colour rather than the - * full bevel shadow -- so it reads as a soft inset panel rather than a hard - * button edge. */ + * bevel.divider against bevel.light rather than the full light/dark + * contrast -- so it reads as a soft inset panel rather than a hard button + * edge. */ wuss_ICON_BORDER_DIVIDER } wuss_icon_border_t; diff --git a/include/wuss/wuss.h b/include/wuss/wuss.h index 58c342c..53f2acd 100644 --- a/include/wuss/wuss.h +++ b/include/wuss/wuss.h @@ -138,21 +138,24 @@ typedef unsigned char wuss_colour_t; /* Chrome roles: echo the matching wuss_config_t field, resolved to a * concrete index -- e.g. wuss_COLOUR_TITLE_BG is furniture.title.bg, * wuss_COLOUR_BUTTON_HILIGHT is bevel.light, wuss_COLOUR_BUTTON_SHADOW is - * bevel.dark, wuss_COLOUR_BUTTON_PRESSED is bevel.pressed, - * wuss_COLOUR_BORDER_DIVIDER is bevel.divider, + * bevel.dark, wuss_COLOUR_BORDER_DIVIDER is bevel.divider, + * wuss_COLOUR_BUTTON_BG is button.bg, wuss_COLOUR_BUTTON_FG is button.fg, + * wuss_COLOUR_BUTTON_PRESSED is button.pressed, + * wuss_COLOUR_ACCENT is accent.colour, * wuss_COLOUR_BACKDROP is backdrop.colour, * wuss_COLOUR_WINDOW is body.window, wuss_COLOUR_MENU is body.menu. */ #define wuss_COLOUR_TITLE_BG (wuss_COLOUR_SYMBOLIC + 16) #define wuss_COLOUR_TITLE_FG (wuss_COLOUR_SYMBOLIC + 17) #define wuss_COLOUR_BUTTON_HILIGHT (wuss_COLOUR_SYMBOLIC + 18) #define wuss_COLOUR_BUTTON_SHADOW (wuss_COLOUR_SYMBOLIC + 19) -#define wuss_COLOUR_ACCENT_BG (wuss_COLOUR_SYMBOLIC + 20) -#define wuss_COLOUR_ACCENT_FG (wuss_COLOUR_SYMBOLIC + 21) +#define wuss_COLOUR_ACCENT (wuss_COLOUR_SYMBOLIC + 20) +#define wuss_COLOUR_BUTTON_FG (wuss_COLOUR_SYMBOLIC + 21) #define wuss_COLOUR_BACKDROP (wuss_COLOUR_SYMBOLIC + 22) #define wuss_COLOUR_WINDOW (wuss_COLOUR_SYMBOLIC + 23) #define wuss_COLOUR_MENU (wuss_COLOUR_SYMBOLIC + 24) #define wuss_COLOUR_BUTTON_PRESSED (wuss_COLOUR_SYMBOLIC + 25) #define wuss_COLOUR_BORDER_DIVIDER (wuss_COLOUR_SYMBOLIC + 26) +#define wuss_COLOUR_BUTTON_BG (wuss_COLOUR_SYMBOLIC + 27) /** Furniture chrome colours, one entry per class of furniture. Title is * the only two-tone class (fill + text); the rest are drawn as a single @@ -312,9 +315,9 @@ wuss_backdrop_t; * Optional creation-time configuration. * * \note titlebar_height and palette are ignored when the library is built - * with WUSS_FURNITURE off; bevel and accent are ignored when built - * with both WUSS_FURNITURE and WUSS_ICONS off. backdrop and body are - * always honoured. See the backdrop sub-struct for its own notes. + * with WUSS_FURNITURE off; bevel, button and accent are ignored when + * built with both WUSS_FURNITURE and WUSS_ICONS off. backdrop and body + * are always honoured. See the backdrop sub-struct for its own notes. */ typedef struct wuss_config { @@ -328,38 +331,55 @@ typedef struct wuss_config wuss_furniture_palette_t furniture; /** - * Bevelled work-area button edge shades, as indices into the system - * palette: light on the top/left edges, dark on the bottom/right (swapped - * when the button is pressed). pressed is the button face fill while it is - * held down, in place of the icon's own background. divider is the lighter + * Bevel edge shades for work-area buttons and bordered labels, as indices + * into the system palette: light on the top/left edges, dark on the + * bottom/right (swapped when a button is pressed). divider is the lighter * edge shade for a wuss_ICON_BORDER_DIVIDER label surround, paired with * light for its sunken and raised rings. light and dark default to the - * titlebar fill colour when config is NULL; pressed defaults to dark; - * divider defaults to light. Read back through wuss_COLOUR_BUTTON_HILIGHT - * / wuss_COLOUR_BUTTON_SHADOW / wuss_COLOUR_BUTTON_PRESSED / - * wuss_COLOUR_BORDER_DIVIDER. Ignored when both WUSS_FURNITURE and + * titlebar fill colour when config is NULL; divider defaults to light. + * Read back through wuss_COLOUR_BUTTON_HILIGHT / wuss_COLOUR_BUTTON_SHADOW + * / wuss_COLOUR_BORDER_DIVIDER. Ignored when both WUSS_FURNITURE and * WUSS_ICONS are off. */ struct { wuss_colour_t light; /**< Top/left bevel edge. */ wuss_colour_t dark; /**< Bottom/right bevel edge. */ - wuss_colour_t pressed; /**< Button face fill while held down. */ wuss_colour_t divider; /**< Lighter edge shade for a DIVIDER border. */ } bevel; /** - * Fill and text colours for a default action button -- a work-area button - * icon created with wuss_ICON_FLAGS_DEFAULT, drawn to stand out from the - * ordinary bevelled buttons around it (RISC OS's "default action button"). - * Both default to the titlebar colours (bg / fg) when config is NULL. - * Ignored when both WUSS_FURNITURE and WUSS_ICONS are off. + * Work-area button colours, as indices into the system palette. bg is the + * button face, used when a wuss_ICON_TYPE_BUTTON spec passes + * wuss_NO_BACKGROUND rather than its own fill. fg is the button label. + * pressed replaces bg on the face while the button is held down. bg and + * pressed default to bevel.light and bevel.dark when config is NULL; fg + * defaults to the titlebar text colour. Read back through + * wuss_COLOUR_BUTTON_BG / wuss_COLOUR_BUTTON_FG / + * wuss_COLOUR_BUTTON_PRESSED. Ignored when both WUSS_FURNITURE and + * WUSS_ICONS are off. + */ + struct + { + wuss_colour_t bg; /**< Button face fill. */ + wuss_colour_t fg; /**< Button label. */ + wuss_colour_t pressed; /**< Button face fill while held down. */ + } + button; + + /** + * Accent colour for a default action button -- a wuss_ICON_TYPE_BUTTON + * created with wuss_ICON_FLAGS_DEFAULT, drawn to stand out from the + * ordinary bevelled buttons around it (RISC OS's "default action button"): + * its face fill at rest, and the moat of the wuss_ICON_BORDER_ACTION + * surround. Its label uses button.fg. Defaults to the titlebar fill colour + * when config is NULL. Read back through wuss_COLOUR_ACCENT. Ignored when + * both WUSS_FURNITURE and WUSS_ICONS are off. */ struct { - wuss_colour_t bg; /**< Default-button fill. */ - wuss_colour_t fg; /**< Default-button text. */ + wuss_colour_t colour; /**< Default-button accent fill. */ } accent; diff --git a/libraries/wuss/TODO.txt b/libraries/wuss/TODO.txt index 4139e5d..12e4670 100644 --- a/libraries/wuss/TODO.txt +++ b/libraries/wuss/TODO.txt @@ -10,7 +10,9 @@ LAYER 0 - BUGS (fix before building on top) - Auto window placement packer still not working as expected blocks: reliable multi-window tasks - Image task not shrinking window when new image loads -- Backgrounds are being drawn behind window furniture. +- Missing horizontal frame line under title bar +- Resize-only windows? - check +- Backdrop colours reversed? - check LAYER 1 - CORE PLUMBING (no wuss deps; unblocks everything else) diff --git a/libraries/wuss/component/info.c b/libraries/wuss/component/info.c index c9c9257..7bd7aee 100644 --- a/libraries/wuss/component/info.c +++ b/libraries/wuss/component/info.c @@ -60,7 +60,7 @@ static int info_widen(bmfont_t *font, const char *s, int cur) if (font == NULL || s == NULL || *s == '\0') return cur; - bmfont_measure(font, s, (int) strlen(s), INT_MAX, NULL, &w); + wuss__text_measure(font, s, (int) strlen(s), INT_MAX, NULL, &w); return MAX(cur, (int) w); } diff --git a/libraries/wuss/core/create.c b/libraries/wuss/core/create.c index fabe5c0..36b1c86 100644 --- a/libraries/wuss/core/create.c +++ b/libraries/wuss/core/create.c @@ -34,17 +34,19 @@ static const colour_t wuss__default_palette[] = static result_t validate_bevel_backdrop(const wuss_t *w, wuss_colour_t blight, wuss_colour_t bdark, - wuss_colour_t bpressed, wuss_colour_t bdivider, - wuss_colour_t abg, - wuss_colour_t afg) + wuss_colour_t btnbg, + wuss_colour_t btnfg, + wuss_colour_t btnpressed, + wuss_colour_t accent) { - if (blight >= w->npalette || - bdark >= w->npalette || - bpressed >= w->npalette || - bdivider >= w->npalette || - abg >= w->npalette || - afg >= w->npalette || + if (blight >= w->npalette || + bdark >= w->npalette || + bdivider >= w->npalette || + btnbg >= w->npalette || + btnfg >= w->npalette || + btnpressed >= w->npalette || + accent >= w->npalette || wuss__validate_backdrop(w, &w->backdrop) != result_OK) return result_WUSS_BAD_COLOUR; @@ -69,8 +71,8 @@ result_t wuss_create(screen_t *scr, wuss_colour_t bg, fg; #endif #if defined(WUSS_FURNITURE) || defined(WUSS_ICONS) - wuss_colour_t blight, bdark, bpressed, bdivider; - wuss_colour_t abg, afg; + wuss_colour_t blight, bdark, bdivider; + wuss_colour_t btnbg, btnfg, btnpressed, accent; #endif #ifdef WUSS_FURNITURE int font_height; @@ -157,16 +159,19 @@ result_t wuss_create(screen_t *scr, pal.scroll.arrows = wuss__resolve_colour(w, pal.scroll.arrows); pal.scroll.wells = wuss__resolve_colour(w, pal.scroll.wells); pal.scroll.sausages = wuss__resolve_colour(w, pal.scroll.sausages); - blight = wuss__resolve_colour(w, config->bevel.light); - bdark = wuss__resolve_colour(w, config->bevel.dark); - bpressed = (config->bevel.pressed == wuss_NO_BACKGROUND) - ? bdark - : wuss__resolve_colour(w, config->bevel.pressed); - bdivider = (config->bevel.divider == wuss_NO_BACKGROUND) - ? blight - : wuss__resolve_colour(w, config->bevel.divider); - abg = wuss__resolve_colour(w, config->accent.bg); - afg = wuss__resolve_colour(w, config->accent.fg); + blight = wuss__resolve_colour(w, config->bevel.light); + bdark = wuss__resolve_colour(w, config->bevel.dark); + bdivider = (config->bevel.divider == wuss_NO_BACKGROUND) + ? blight + : wuss__resolve_colour(w, config->bevel.divider); + btnbg = (config->button.bg == wuss_NO_BACKGROUND) + ? blight + : wuss__resolve_colour(w, config->button.bg); + btnfg = wuss__resolve_colour(w, config->button.fg); + btnpressed = (config->button.pressed == wuss_NO_BACKGROUND) + ? bdark + : wuss__resolve_colour(w, config->button.pressed); + accent = wuss__resolve_colour(w, config->accent.colour); } else { @@ -184,12 +189,13 @@ result_t wuss_create(screen_t *scr, pal.scroll.wells = bg; pal.scroll.sausages = fg; - blight = 0; - bdark = 0; - bpressed = 0; - bdivider = 0; - abg = bg; /* default action button: the titlebar colours */ - afg = fg; + blight = 0; + bdark = 0; + bdivider = 0; + btnbg = 0; + btnfg = fg; + btnpressed = 0; + accent = bg; /* default action button: the titlebar fill */ } if (pal.title.bg >= w->npalette || @@ -202,8 +208,8 @@ result_t wuss_create(screen_t *scr, pal.scroll.arrows >= w->npalette || pal.scroll.wells >= w->npalette || pal.scroll.sausages >= w->npalette || - validate_bevel_backdrop(w, blight, bdark, bpressed, bdivider, - abg, afg) != result_OK) + validate_bevel_backdrop(w, blight, bdark, bdivider, + btnbg, btnfg, btnpressed, accent) != result_OK) { wuss__free(w, w->palette); wuss__free(w, w); @@ -213,10 +219,11 @@ result_t wuss_create(screen_t *scr, w->furniture_colours = pal; w->bevel_light = blight; w->bevel_dark = bdark; - w->bevel_pressed = bpressed; w->bevel_divider = bdivider; - w->accent_bg = abg; - w->accent_fg = afg; + w->button_bg = btnbg; + w->button_fg = btnfg; + w->button_pressed = btnpressed; + w->accent = accent; if (config != NULL && config->titlebar_height > 0) { @@ -244,39 +251,44 @@ result_t wuss_create(screen_t *scr, #ifdef WUSS_ICONS if (config != NULL) { - blight = wuss__resolve_colour(w, config->bevel.light); - bdark = wuss__resolve_colour(w, config->bevel.dark); - bpressed = (config->bevel.pressed == wuss_NO_BACKGROUND) - ? bdark - : wuss__resolve_colour(w, config->bevel.pressed); - bdivider = (config->bevel.divider == wuss_NO_BACKGROUND) - ? blight - : wuss__resolve_colour(w, config->bevel.divider); - abg = wuss__resolve_colour(w, config->accent.bg); - afg = wuss__resolve_colour(w, config->accent.fg); + blight = wuss__resolve_colour(w, config->bevel.light); + bdark = wuss__resolve_colour(w, config->bevel.dark); + bdivider = (config->bevel.divider == wuss_NO_BACKGROUND) + ? blight + : wuss__resolve_colour(w, config->bevel.divider); + btnbg = (config->button.bg == wuss_NO_BACKGROUND) + ? blight + : wuss__resolve_colour(w, config->button.bg); + btnfg = wuss__resolve_colour(w, config->button.fg); + btnpressed = (config->button.pressed == wuss_NO_BACKGROUND) + ? bdark + : wuss__resolve_colour(w, config->button.pressed); + accent = wuss__resolve_colour(w, config->accent.colour); } else { - blight = 0; - bdark = 0; - bpressed = 0; - bdivider = 0; - abg = 0; - afg = (w->npalette > 1) ? 1 : 0; + blight = 0; + bdark = 0; + bdivider = 0; + btnbg = 0; + btnfg = (w->npalette > 1) ? 1 : 0; + btnpressed = 0; + accent = 0; } - if (validate_bevel_backdrop(w, blight, bdark, bpressed, bdivider, - abg, afg) != result_OK) + if (validate_bevel_backdrop(w, blight, bdark, bdivider, + btnbg, btnfg, btnpressed, accent) != result_OK) { wuss__free(w, w->palette); wuss__free(w, w); return result_WUSS_BAD_COLOUR; } - w->bevel_light = blight; - w->bevel_dark = bdark; - w->bevel_pressed = bpressed; - w->bevel_divider = bdivider; - w->accent_bg = abg; - w->accent_fg = afg; + w->bevel_light = blight; + w->bevel_dark = bdark; + w->bevel_divider = bdivider; + w->button_bg = btnbg; + w->button_fg = btnfg; + w->button_pressed = btnpressed; + w->accent = accent; #else if (wuss__validate_backdrop(w, &w->backdrop) != result_OK) { diff --git a/libraries/wuss/core/impl.h b/libraries/wuss/core/impl.h index cb22292..ac1d8a4 100644 --- a/libraries/wuss/core/impl.h +++ b/libraries/wuss/core/impl.h @@ -123,12 +123,13 @@ struct wuss wuss_furniture_palette_t furniture_colours; #endif #if defined(WUSS_FURNITURE) || defined(WUSS_ICONS) - wuss_colour_t bevel_light; /* work-area button top/left edge */ - wuss_colour_t bevel_dark; /* work-area button bottom/right edge */ - wuss_colour_t bevel_pressed; /* work-area button face while held */ - wuss_colour_t bevel_divider; /* lighter edge for a DIVIDER border */ - wuss_colour_t accent_bg; /* default action button fill */ - wuss_colour_t accent_fg; /* default action button text */ + wuss_colour_t bevel_light; /* work-area button top/left edge */ + wuss_colour_t bevel_dark; /* work-area button bottom/right edge */ + wuss_colour_t bevel_divider; /* lighter edge for a DIVIDER border */ + wuss_colour_t button_bg; /* button face when spec bg is NONE */ + wuss_colour_t button_fg; /* button label */ + wuss_colour_t button_pressed; /* button face while held */ + wuss_colour_t accent; /* default action button fill */ #endif wuss_colour_t window_bg; /* work-area body fill; wuss_COLOUR_WINDOW */ wuss_colour_t menu_bg; /* menu body fill; wuss_COLOUR_MENU */ @@ -280,6 +281,25 @@ static inline void wuss__chrome_invalidate_layout(wuss_window_t *window) wuss_window_t *wuss__window_at(wuss_t *wuss, point_t p); +/* Centralised text rendering. Every wuss text draw goes through + * wuss__text_draw so an optical vertical bias (WUSS_TEXT_BASELINE_ADJUST, + * default 1px down) is applied uniformly; wuss__text_measure is a plain + * pass-through kept alongside for a single point of policy. */ +result_t wuss__text_measure(bmfont_t *font, + const char *text, + int len, + bmfont_width_t target_width, + int *split_point, + bmfont_width_t *actual_width); +result_t wuss__text_draw(bmfont_t *font, + screen_t *scr, + const char *text, + int len, + colour_t fg, + colour_t bg, + const point_t *pos, + point_t *end_pos); + /* Rebuild wuss->palettecache (white, black and the symbolic[] table) from * the current palette and the stored chrome colours. Call after the palette * or any chrome colour changes; the chrome fields must already be concrete diff --git a/libraries/wuss/core/rebuild-palettecache.c b/libraries/wuss/core/rebuild-palettecache.c index fc3c114..7b904b4 100644 --- a/libraries/wuss/core/rebuild-palettecache.c +++ b/libraries/wuss/core/rebuild-palettecache.c @@ -50,10 +50,11 @@ void wuss__rebuild_palettecache(wuss_t *wuss) #if defined(WUSS_FURNITURE) || defined(WUSS_ICONS) cache[wuss_COLOUR_BUTTON_HILIGHT - wuss_COLOUR_SYMBOLIC] = wuss->bevel_light; cache[wuss_COLOUR_BUTTON_SHADOW - wuss_COLOUR_SYMBOLIC] = wuss->bevel_dark; - cache[wuss_COLOUR_BUTTON_PRESSED - wuss_COLOUR_SYMBOLIC] = wuss->bevel_pressed; cache[wuss_COLOUR_BORDER_DIVIDER - wuss_COLOUR_SYMBOLIC] = wuss->bevel_divider; - cache[wuss_COLOUR_ACCENT_BG - wuss_COLOUR_SYMBOLIC] = wuss->accent_bg; - cache[wuss_COLOUR_ACCENT_FG - wuss_COLOUR_SYMBOLIC] = wuss->accent_fg; + cache[wuss_COLOUR_BUTTON_BG - wuss_COLOUR_SYMBOLIC] = wuss->button_bg; + cache[wuss_COLOUR_BUTTON_FG - wuss_COLOUR_SYMBOLIC] = wuss->button_fg; + cache[wuss_COLOUR_BUTTON_PRESSED - wuss_COLOUR_SYMBOLIC] = wuss->button_pressed; + cache[wuss_COLOUR_ACCENT - wuss_COLOUR_SYMBOLIC] = wuss->accent; #endif if (wuss->backdrop.colour != wuss_NO_BACKGROUND) cache[wuss_COLOUR_BACKDROP - wuss_COLOUR_SYMBOLIC] = wuss->backdrop.colour; diff --git a/libraries/wuss/core/text.c b/libraries/wuss/core/text.c new file mode 100644 index 0000000..6d04abe --- /dev/null +++ b/libraries/wuss/core/text.c @@ -0,0 +1,43 @@ +/* text.c -- wuss centralised bitmap-font text rendering */ + +#include "geom/point.h" +#include "framebuf/bmfont.h" +#include "framebuf/screen.h" + +#include "impl.h" + +/* Optical vertical bias applied to every text draw so glyphs sit visually + * centred rather than mathematically centred. Positive shifts text down. */ +#ifndef WUSS_TEXT_BASELINE_ADJUST +#define WUSS_TEXT_BASELINE_ADJUST 1 +#endif + +/* ----------------------------------------------------------------------- */ + +result_t wuss__text_measure(bmfont_t *font, + const char *text, + int len, + bmfont_width_t target_width, + int *split_point, + bmfont_width_t *actual_width) +{ + return bmfont_measure(font, text, len, target_width, split_point, + actual_width); +} + +result_t wuss__text_draw(bmfont_t *font, + screen_t *scr, + const char *text, + int len, + colour_t fg, + colour_t bg, + const point_t *pos, + point_t *end_pos) +{ + point_t adjusted; + + adjusted.x = pos->x; + adjusted.y = pos->y + WUSS_TEXT_BASELINE_ADJUST; + + return bmfont_draw(font, scr, text, len, fg, bg, &adjusted, end_pos); +} diff --git a/libraries/wuss/furniture/draw.c b/libraries/wuss/furniture/draw.c index 5c4e593..a9b9405 100644 --- a/libraries/wuss/furniture/draw.c +++ b/libraries/wuss/furniture/draw.c @@ -119,15 +119,15 @@ static void draw_title(wuss_t *wuss, return; titlelen = (int) strlen(window->title); - bmfont_measure(titlefont, window->title, titlelen, text_x1 - text_x0, &split_point, &width); + wuss__text_measure(titlefont, window->title, titlelen, text_x1 - text_x0, &split_point, &width); pos.x = (split_point < titlelen) ? text_x0 : text_x0 + MAX(0, ((text_x1 - text_x0) - width) / 2); pos.y = titlebar->y0 + 2; wuss->scr->clip = text_clip; - bmfont_draw(titlefont, wuss->scr, window->title, titlelen, - wuss->palette[wuss->furniture_colours.title.fg], - wuss->palette[wuss->furniture_colours.title.bg], - &pos, NULL); + wuss__text_draw(titlefont, wuss->scr, window->title, titlelen, + wuss->palette[wuss->furniture_colours.title.fg], + wuss->palette[wuss->furniture_colours.title.bg], + &pos, NULL); } void wuss__furniture_draw(wuss_t *wuss, diff --git a/libraries/wuss/icon/draw.c b/libraries/wuss/icon/draw.c index b1ef705..4e4dfb9 100644 --- a/libraries/wuss/icon/draw.c +++ b/libraries/wuss/icon/draw.c @@ -174,7 +174,7 @@ static void wuss__icon_draw_label(const icon_draw_ctx_t *c) light = c->wuss->palette[c->wuss->bevel_light]; dark = c->wuss->palette[c->wuss->bevel_dark]; - accent = c->wuss->palette[c->wuss->accent_bg]; /* the action-button fill */ + accent = c->wuss->palette[c->wuss->accent]; /* the action-button fill */ divider = c->wuss->palette[c->wuss->bevel_divider]; if (icon->border == wuss_ICON_BORDER_ACTION) @@ -212,8 +212,8 @@ static void wuss__icon_draw_label(const icon_draw_ctx_t *c) /* Measure the whole string, not the box interior: a clipped measurement * left the widest label of a right-justified set overhanging the rest. */ - bmfont_measure(c->font, icon->text, (int) strlen(icon->text), - INT_MAX, NULL, &width); + wuss__text_measure(c->font, icon->text, (int) strlen(icon->text), + INT_MAX, NULL, &width); if (icon->flags & wuss_ICON_FLAGS_JUSTIFY_CENTRE) pos.x = b->x0 + ((b->x1 - b->x0) - width) / 2; @@ -222,12 +222,10 @@ static void wuss__icon_draw_label(const icon_draw_ctx_t *c) else pos.x = b->x0 + 1; - /* +1 so the centred label sits visually level; integer division of the - * leftover vertical space rounds it one pixel high otherwise */ - pos.y = b->y0 + (b->y1 - b->y0 - c->font_height) / 2 + 1; + pos.y = b->y0 + (b->y1 - b->y0 - c->font_height) / 2; - bmfont_draw(c->font, c->scr, icon->text, (int) strlen(icon->text), - c->fg, bg, &pos, NULL); + wuss__text_draw(c->font, c->scr, icon->text, (int) strlen(icon->text), + c->fg, bg, &pos, NULL); } /* ----------------------------------------------------------------------- */ @@ -247,9 +245,9 @@ static void wuss__icon_draw_frame(const icon_draw_ctx_t *c) int split_point; bmfont_width_t width; - bmfont_measure(c->font, icon->text, (int) strlen(icon->text), - (b->x1 - b->x0) - WUSS_FRAME_CAPTION_INSET * 2, - &split_point, &width); + wuss__text_measure(c->font, icon->text, (int) strlen(icon->text), + (b->x1 - b->x0) - WUSS_FRAME_CAPTION_INSET * 2, + &split_point, &width); cap_w = width; } @@ -277,8 +275,8 @@ static void wuss__icon_draw_frame(const icon_draw_ctx_t *c) pos.x = cap_x; pos.y = b->y0; - bmfont_draw(c->font, c->scr, icon->text, (int) strlen(icon->text), - c->fg, bg, &pos, NULL); + wuss__text_draw(c->font, c->scr, icon->text, (int) strlen(icon->text), + c->fg, bg, &pos, NULL); } } @@ -294,14 +292,20 @@ static void wuss__icon_draw_button(const icon_draw_ctx_t *c) pressed = wuss__icon_pressed(icon); is_default = (icon->flags & wuss_ICON_FLAGS_DEFAULT) != 0; - base = c->wuss->palette[icon->bg]; + /* a plain button whose spec left bg as wuss_NO_BACKGROUND takes the config + * button face */ + base = (icon->bg != wuss_NO_BACKGROUND) + ? c->wuss->palette[icon->bg] + : c->wuss->palette[c->wuss->button_bg]; light = c->wuss->palette[c->wuss->bevel_light]; dark = c->wuss->palette[c->wuss->bevel_dark]; - label = c->fg; + /* a default action button labels in button.fg to read against its accent + * fill; a plain button uses the icon's own fg */ + label = is_default ? c->wuss->palette[c->wuss->button_fg] : c->fg; - /* a held button fills darker regardless of type */ + /* a held button fills with the pressed shade regardless of type */ if (pressed) - base = c->wuss->palette[c->wuss->bevel_pressed]; + base = c->wuss->palette[c->wuss->button_pressed]; if (icon->flags & wuss_ICON_FLAGS_DISABLED) label = c->wuss->palette[c->wuss->bevel_dark]; /* greyed: sink toward dark */ @@ -310,11 +314,12 @@ static void wuss__icon_draw_button(const icon_draw_ctx_t *c) { /* default action button: an accent-filled rectangle inside the same 6px * "action" surround as wuss_ICON_BORDER_ACTION, distinct from the plain - * bevelled buttons around it */ + * bevelled buttons around it. */ + screen_fill_rect(c->scr, b->x0, b->y0, SIZE2D(b->x1 - b->x0, b->y1 - b->y0), base); icon_draw_action_border(c->scr, b, light, dark, - c->wuss->palette[c->wuss->accent_bg], pressed); + c->wuss->palette[c->wuss->accent], pressed); } else { @@ -330,8 +335,8 @@ static void wuss__icon_draw_button(const icon_draw_ctx_t *c) interior_w = MAX((b->x1 - b->x0) - 2, 1); - bmfont_measure(c->font, icon->text, (int) strlen(icon->text), - interior_w, &split_point, &width); + wuss__text_measure(c->font, icon->text, (int) strlen(icon->text), + interior_w, &split_point, &width); pos.x = b->x0 + ((b->x1 - b->x0) - width) / 2; pos.y = b->y0 + (b->y1 - b->y0 - c->font_height) / 2; @@ -341,8 +346,8 @@ static void wuss__icon_draw_button(const icon_draw_ctx_t *c) pos.y += 1; } - bmfont_draw(c->font, c->scr, icon->text, (int) strlen(icon->text), - label, base, &pos, NULL); + wuss__text_draw(c->font, c->scr, icon->text, (int) strlen(icon->text), + label, base, &pos, NULL); } } @@ -412,14 +417,14 @@ static void wuss__icon_draw_radio_option(const icon_draw_ctx_t *c) tx = g.x1 + 4; interior_w = MAX((b->x1 - tx) - 1, 1); - bmfont_measure(c->font, icon->text, (int) strlen(icon->text), - interior_w, &split_point, &width); + wuss__text_measure(c->font, icon->text, (int) strlen(icon->text), + interior_w, &split_point, &width); NOT_USED(width); pos.x = tx; pos.y = b->y0 + (b->y1 - b->y0 - c->font_height) / 2; - bmfont_draw(c->font, c->scr, icon->text, (int) strlen(icon->text), - glyph, bg, &pos, NULL); + wuss__text_draw(c->font, c->scr, icon->text, (int) strlen(icon->text), + glyph, bg, &pos, NULL); } } @@ -544,12 +549,12 @@ static void wuss__icon_draw_menu_entry(const icon_draw_ctx_t *c) * the same width is left spare at text_x1 too since the fill already * spans the full column */ space_w = 0; - bmfont_measure(c->font, " ", 1, INT_MAX, NULL, &space_w); + wuss__text_measure(c->font, " ", 1, INT_MAX, NULL, &space_w); pos.x = text_x0 + (int) space_w; pos.y = b->y0 + (b->y1 - b->y0 - c->font_height) / 2; - bmfont_draw(c->font, c->scr, icon->text, (int) strlen(icon->text), - text_ink, text_ground, &pos, NULL); + wuss__text_draw(c->font, c->scr, icon->text, (int) strlen(icon->text), + text_ink, text_ground, &pos, NULL); } } diff --git a/libraries/wuss/icon/from-spec.c b/libraries/wuss/icon/from-spec.c index 561ba05..f5aa915 100644 --- a/libraries/wuss/icon/from-spec.c +++ b/libraries/wuss/icon/from-spec.c @@ -44,9 +44,9 @@ result_t wuss__icon_from_spec(const wuss_t *w, return result_WUSS_BAD_ICON; } - if ((spec->type == wuss_ICON_TYPE_BUTTON || - spec->type == wuss_ICON_TYPE_PATTERN) && - bg == wuss_NO_BACKGROUND) + /* a BUTTON may leave bg unset -- it then draws on the config button face + * (wuss->button_bg); a PATTERN needs a concrete clear-bit colour */ + if (spec->type == wuss_ICON_TYPE_PATTERN && bg == wuss_NO_BACKGROUND) return result_WUSS_BAD_ICON; if (spec->type == wuss_ICON_TYPE_BITMAP && spec->bitmap == NULL) diff --git a/libraries/wuss/menu/menu.c b/libraries/wuss/menu/menu.c index 751b27f..6124b6c 100644 --- a/libraries/wuss/menu/menu.c +++ b/libraries/wuss/menu/menu.c @@ -589,8 +589,8 @@ static result_t wuss__menu_spawn(wuss_t *wuss, len = (int) strlen(text); if (len == 0) continue; /* empty label (bare rule row); nothing to measure */ - if (bmfont_measure(wuss->fonts[0], text, len, - INT_MAX, &split, &w) == result_OK && (int) w > widest) + if (wuss__text_measure(wuss->fonts[0], text, len, + INT_MAX, &split, &w) == result_OK && (int) w > widest) widest = (int) w; } @@ -600,7 +600,7 @@ static result_t wuss__menu_spawn(wuss_t *wuss, { bmfont_width_t space_w = 0; - bmfont_measure(wuss->fonts[0], " ", 1, INT_MAX, NULL, &space_w); + wuss__text_measure(wuss->fonts[0], " ", 1, INT_MAX, NULL, &space_w); widest += 2 * (int) space_w; } @@ -620,8 +620,8 @@ static result_t wuss__menu_spawn(wuss_t *wuss, titlefont = (wuss->nfonts > 1 && wuss->fonts[1] != NULL) ? wuss->fonts[1] : wuss->fonts[0]; titlelen = (int) strlen(menu->title); - if (bmfont_measure(titlefont, menu->title, titlelen, INT_MAX, &split, - &title_w) == result_OK && + if (wuss__text_measure(titlefont, menu->title, titlelen, INT_MAX, &split, + &title_w) == result_OK && (int) title_w + 2 * WUSS_MENU_TITLE_PAD > width) width = (int) title_w + 2 * WUSS_MENU_TITLE_PAD; } diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 2c365f2..7fb1857 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -524,9 +524,11 @@ result_t wuss_test(const char *resources) bad_config.furniture.scroll.sausages = 0; bad_config.bevel.light = 0; bad_config.bevel.dark = 0; - bad_config.bevel.pressed = 0; - bad_config.accent.bg = 0; - bad_config.accent.fg = 0; + bad_config.bevel.divider = 0; + bad_config.button.bg = 0; + bad_config.button.fg = 0; + bad_config.button.pressed = 0; + bad_config.accent.colour = 0; rc = wuss_create(&scr, NULL, 0, NULL, 0, &bad_config, NULL, &bad_wuss); if (rc != result_WUSS_BAD_COLOUR) goto Failure; From 9f5e696a60444129249636e58f2c3e2429b92b40 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Tue, 8 Sep 2026 15:13:01 +0100 Subject: [PATCH 52/98] feat(wuss): draw wuss_ICON_TYPE_FRAME surround as a DIVIDER border Extract icon_draw_divider_border from the inline wuss_ICON_BORDER_DIVIDER case in wuss__icon_draw_label and reuse it for the grouping frame, which now draws its surround as the two-ring divider bevel instead of a 1px fg line box. The caption still breaks the top edge: the caption slot is overpainted back to the frame ground before the text is drawn. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/icon/draw.c | 61 ++++++++++++++++++++++---------------- 1 file changed, 35 insertions(+), 26 deletions(-) diff --git a/libraries/wuss/icon/draw.c b/libraries/wuss/icon/draw.c index 4e4dfb9..4a351ab 100644 --- a/libraries/wuss/icon/draw.c +++ b/libraries/wuss/icon/draw.c @@ -72,6 +72,25 @@ static void icon_draw_action_border(screen_t *scr, pressed ? light : dark); } +/* The wuss_ICON_BORDER_DIVIDER surround: two 2px bevels in a lighter pair + * than RIDGE/GROOVE -- bevel_divider against bevel_light rather than the full + * light/dark contrast. Outer ring sunken, inner ring raised. Shared by the + * bordered label and the grouping frame. */ +static void icon_draw_divider_border(screen_t *scr, + const box_t *b, + colour_t light, + colour_t divider) +{ + box_t ring; + + ring = *b; + screen_draw_bevel_edge(scr, &ring, divider, light); + + ring = (box_t) BOX_POS_SIZE(b->x0 + 2, b->y0 + 2, + b->x1 - b->x0 - 4, b->y1 - b->y0 - 4); + screen_draw_bevel_edge(scr, &ring, light, divider); +} + /* Resolve the ground an icon's text/glyph blends against: an explicit icon bg, * else the window's dominant backdrop colour (a pattern fill blends against its * clear-bit colour, not its foreground), else "fallback" so bmfont still has a @@ -183,16 +202,7 @@ static void wuss__icon_draw_label(const icon_draw_ctx_t *c) } else if (icon->border == wuss_ICON_BORDER_DIVIDER) { - /* Two 2px bevels in a lighter pair than RIDGE/GROOVE: bevel_divider - * against bevel_light rather than the full light/dark contrast. Outer - * ring sunken, inner ring raised. */ - box_t ring = *b; - - screen_draw_bevel_edge(c->scr, &ring, divider, light); - - ring = (box_t) BOX_POS_SIZE(b->x0 + 2, b->y0 + 2, - b->x1 - b->x0 - 4, b->y1 - b->y0 - 4); - screen_draw_bevel_edge(c->scr, &ring, light, divider); + icon_draw_divider_border(c->scr, b, light, divider); } else { @@ -234,10 +244,12 @@ static void wuss__icon_draw_frame(const icon_draw_ctx_t *c) { const wuss_icon_t *icon = c->icon; const box_t *b = &c->b; - colour_t bg; - int cap_w, cap_x, gap_x0, gap_x1, mid_y; + colour_t bg, light, divider; + int cap_w, cap_x, gap_x0, gap_x1; - bg = icon_blend_ground(c, c->fg); + bg = icon_blend_ground(c, c->fg); + light = c->wuss->palette[c->wuss->bevel_light]; + divider = c->wuss->palette[c->wuss->bevel_divider]; cap_w = 0; if (c->have_font) @@ -251,23 +263,20 @@ static void wuss__icon_draw_frame(const icon_draw_ctx_t *c) cap_w = width; } - mid_y = b->y0 + c->font_height / 2; - - /* left, right and bottom edges are unbroken */ - screen_draw_line(c->scr, b->x0, mid_y, b->x0, b->y1 - 1, c->fg); - screen_draw_line(c->scr, b->x1 - 1, mid_y, b->x1 - 1, b->y1 - 1, c->fg); - screen_draw_line(c->scr, b->x0, b->y1 - 1, b->x1 - 1, b->y1 - 1, c->fg); + /* the whole surround is a wuss_ICON_BORDER_DIVIDER ring... */ + icon_draw_divider_border(c->scr, b, light, divider); - /* the top edge is broken around the caption. INSET (8) always exceeds PAD - * (2), so gap_x0 sits a few pixels right of b->x0 and the left stub is - * always drawn; only the right stub can vanish, when a wide caption pushes - * gap_x1 past the frame's right edge. */ + /* ...with the top edge broken around the caption: overpaint the caption + * slot (the ring is 2px, plus a PAD margin either side) back to the frame + * ground. INSET (8) always exceeds PAD (2), so gap_x0 sits a few pixels + * right of b->x0 and the left stub always survives; only the right stub + * can vanish, when a wide caption pushes gap_x1 past the frame edge. */ cap_x = b->x0 + WUSS_FRAME_CAPTION_INSET; gap_x0 = cap_x - WUSS_FRAME_CAPTION_PAD; gap_x1 = cap_x + cap_w + WUSS_FRAME_CAPTION_PAD; - screen_draw_line(c->scr, b->x0, mid_y, gap_x0, mid_y, c->fg); - if (gap_x1 < b->x1 - 1) - screen_draw_line(c->scr, gap_x1, mid_y, b->x1 - 1, mid_y, c->fg); + if (gap_x1 > gap_x0) + screen_fill_rect(c->scr, gap_x0, b->y0, + SIZE2D(MIN(gap_x1, b->x1) - gap_x0, 2), bg); if (c->have_font && cap_w > 0) { From b81d3f936380cfc375f09ddedeb2e820e0a0dd61 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Tue, 8 Sep 2026 15:37:08 +0100 Subject: [PATCH 53/98] feat(wuss): curve task cycles curve type on Adjust, with label and per-point colours The curve task now holds npoints (2..6) instead of a fixed cubic: an Adjust click steps it, cycling line / quadratic / cubic / quartic / quintic. curve_point dispatches on the count to the matching curve_bezier_point_on_* helper. A curve_kind_name label, drawn with the system font and pinned to the content's top-left, names the active type. Marker colours now distinguish the two end points (red, task->blob) from the interior control points (green / blue / orange / purple by position). The mouse wheel still adjusts nsegments and SELECT still drags markers. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/test/tasks/curve.c | 89 +++++++++++++++++++++++++++++-- libraries/wuss/test/tasks/curve.h | 14 +++-- 2 files changed, 94 insertions(+), 9 deletions(-) diff --git a/libraries/wuss/test/tasks/curve.c b/libraries/wuss/test/tasks/curve.c index ecb24fc..8fbbcf6 100644 --- a/libraries/wuss/test/tasks/curve.c +++ b/libraries/wuss/test/tasks/curve.c @@ -10,11 +10,15 @@ #include "fortify/fortify.h" #endif +#include + #include "base/utils.h" +#include "framebuf/bmfont.h" #include "framebuf/curve.h" #include "framebuf/palettes.h" #include "geom/box.h" #include "utils/fxp.h" +#include "wuss/wuss.h" #include "curve.h" @@ -32,13 +36,17 @@ result_t curve_create(wuss_t *wuss, curve_task_t *task) task->bg = colour_rgb(0xFF, 0xFF, 0xFF); task->line = colour_rgb(0x00, 0x00, 0x00); task->blob = colour_rgb(0xFF, 0x00, 0x00); + task->wuss = wuss; task->nsegments = CURVE_SEGMENTS_DEFAULT; + task->npoints = 4; /* cubic, matching the original task */ task->dragging = -1; task->points[0] = POINT(10, 10); task->points[1] = POINT(10, 140); task->points[2] = POINT(210, 10); task->points[3] = POINT(210, 140); + task->points[4] = POINT(110, 10); + task->points[5] = POINT(110, 140); /* curve_redraw paints its own background */ delegate_desc.handle = curve_handle; @@ -74,6 +82,56 @@ static int blob_hit(const point_t *p, int x, int y) y >= p->y - half && y < p->y + half; } +/* Marker colour for control point i of a curve with npoints points: the two + * end points (0 and npoints - 1) draw in task->blob (red); the interior + * control points cycle through a set of other bright hues so each is + * distinct. */ +static colour_t blob_colour(const curve_task_t *task, int i) +{ + colour_t control[4]; + + if (i == 0 || i == task->npoints - 1) + return task->blob; + + control[0] = colour_rgb(0x00, 0xC0, 0x00); /* green */ + control[1] = colour_rgb(0x00, 0x80, 0xFF); /* blue */ + control[2] = colour_rgb(0xFF, 0xA0, 0x00); /* orange */ + control[3] = colour_rgb(0xC0, 0x00, 0xFF); /* purple */ + + return control[(i - 1) % NELEMS(control)]; +} + +/* Curve-type name for each valid task->npoints, indexed by + * npoints - CURVE_MINCONTROLPTS. */ +static const char *curve_kind_name(int npoints) +{ + static const char *const names[] = + { + "Line", "Quadratic", "Cubic", "Quartic", "Quintic" + }; + + return names[npoints - CURVE_MINCONTROLPTS]; +} + +/* The point at time t on the curve through the first task->npoints points, + * dispatching on the count: 2 is a straight line, 3..6 the quadratic through + * quintic Beziers. */ +static point_t curve_point(const curve_task_t *task, fix16_t t) +{ + const point_t *p = task->points; + + switch (task->npoints) + { + case 2: return curve_point_on_line(p[0], p[1], t); + case 3: return curve_bezier_point_on_quad(p[0], p[1], p[2], t); + case 4: return curve_bezier_point_on_cubic(p[0], p[1], p[2], p[3], t); + case 5: return curve_bezier_point_on_quartic(p[0], p[1], p[2], p[3], + p[4], t); + default: return curve_bezier_point_on_quintic(p[0], p[1], p[2], p[3], + p[4], p[5], t); + } +} + static result_t curve_redraw(const wuss_event_t *event, curve_task_t *task) { screen_t *scr; @@ -97,8 +155,7 @@ static result_t curve_redraw(const wuss_event_t *event, curve_task_t *task) for (i = 1; i <= task->nsegments; i++) { t = i * FIX16_ONE / task->nsegments; - cur = curve_bezier_point_on_cubic(task->points[0], task->points[1], - task->points[2], task->points[3], t); + cur = curve_point(task, t); cur.x += bounds->x0 - sx; cur.y += bounds->y0 - sy; screen_draw_line(scr, prev.x, prev.y, cur.x, cur.y, task->line); @@ -107,11 +164,24 @@ static result_t curve_redraw(const wuss_event_t *event, curve_task_t *task) } half = CURVE_BLOBSZ / 2; - for (i = 0; i < CURVE_NCONTROLPTS; i++) + for (i = 0; i < task->npoints; i++) { cur = task->points[i]; cur.x += bounds->x0 - sx; cur.y += bounds->y0 - sy; - screen_fill_square(scr, cur.x - half, cur.y - half, CURVE_BLOBSZ, task->blob); + screen_fill_square(scr, cur.x - half, cur.y - half, CURVE_BLOBSZ, + blob_colour(task, i)); + } + + /* curve-type label, pinned to the content's top-left corner (not the + * scrolled document) so an Adjust click's effect is always readable */ + { + bmfont_t *font = wuss_get_font(task->wuss); + const char *name = curve_kind_name(task->npoints); + point_t pos = POINT(content->x0 + 2, content->y0 + 2); + + if (font != NULL) + bmfont_draw(font, scr, name, (int) strlen(name), + task->line, task->bg, &pos, NULL); } return result_OK; @@ -132,9 +202,18 @@ static result_t curve_mouse(curve_task_t *task, switch (action) { case wuss_MOUSE_DOWN: + if (button & wuss_BUTTON_ADJUST) + { + /* cycle line -> quad -> cubic -> quartic -> quintic -> line */ + task->npoints++; + if (task->npoints > CURVE_MAXCONTROLPTS) + task->npoints = CURVE_MINCONTROLPTS; + wuss_window_invalidate_all(window); + break; + } if (!(button & wuss_BUTTON_SELECT)) break; - for (i = 0; i < CURVE_NCONTROLPTS; i++) + for (i = 0; i < task->npoints; i++) { if (blob_hit(&task->points[i], x, y)) { diff --git a/libraries/wuss/test/tasks/curve.h b/libraries/wuss/test/tasks/curve.h index 92e842a..8c32415 100644 --- a/libraries/wuss/test/tasks/curve.h +++ b/libraries/wuss/test/tasks/curve.h @@ -7,17 +7,23 @@ #include "framebuf/colour.h" #include "geom/point.h" +#include "wuss/wuss.h" #include "wuss/window.h" -#define CURVE_NCONTROLPTS 4 /* cubic Bezier: start, 2 control points, end */ +#define CURVE_MINCONTROLPTS 2 /* a straight line */ +#define CURVE_MAXCONTROLPTS 6 /* a quintic Bezier */ -/* a single cubic Bezier curve with draggable control points, redrawn as - * nsegments straight line segments; the mouse wheel adjusts nsegments */ +/* a single Bezier curve with draggable control points, redrawn as nsegments + * straight line segments. The mouse wheel adjusts nsegments; an Adjust click + * cycles the curve type (line, quadratic, cubic, quartic, quintic) by + * stepping npoints, the count of points[] actually in play. */ typedef struct curve_task { + wuss_t *wuss; /* borrowed; for wuss_get_font in the redraw */ wuss_window_t *window; colour_t bg, line, blob; - point_t points[CURVE_NCONTROLPTS]; + point_t points[CURVE_MAXCONTROLPTS]; + int npoints; /* CURVE_MINCONTROLPTS..CURVE_MAXCONTROLPTS */ int nsegments; int dragging; /* index into points, or -1 if not dragging */ } From d71dce509da9eb9b7041df8c100910b4fff3ce2f Mon Sep 17 00:00:00 2001 From: David Thomas Date: Tue, 8 Sep 2026 18:40:10 +0100 Subject: [PATCH 54/98] feat(framebuf): add RLE-compressed read-only bitmaps bitmap_compress() squashes a loaded bitmap in place into a per-scanline run-length-encoded blob; screen_copy_bitmap() decodes it on the fly, skipping transparent runs by pointer arithmetic. bitmap_decompress() restores the raw buffer byte-for-byte. The compressed marker is bit 30 of bm->format (pixelfmt_RLE_FLAG); the low bits keep the real pixelfmt_t so size, palette and span stay valid. New macros pixelfmt_base() / pixelfmt_is_rle() and bitmap_is_compressed(). Row opcode stream uses leading-zero-count dispatch (literal / repeat / skip, short and long forms, EOL). skip runs encode wholly-zero pixels only, keeping the codec lossless; decode zero-fills them, the blit leaves them untouched. v1 limits: 32bpp screens only, source must decode to the screen's exact format, alpha-tested (not blended) blit, no scanline dedup or offset table. Other bitmap ops (clear, fill_pattern, convert, save_png, composite, ninepatch) guard with pixelfmt_is_rle() and decline. New test module 'rle' and docs/framebuf/rle-bitmap.md. A.png compresses to 0.30x and round-trips byte-exact. Co-Authored-By: Claude Sonnet 5 --- CMakeLists.txt | 5 + apps/test/main.c | 1 + docs/framebuf/rle-bitmap.md | 102 +++ include/framebuf/bitmap.h | 37 ++ include/framebuf/pixelfmt.h | 21 + include/test/all-tests.h | 3 +- libraries/framebuf/bitmap/bitmap.c | 9 + libraries/framebuf/bitmap/compress.c | 621 ++++++++++++++++++ libraries/framebuf/bitmap/fill-pattern.c | 3 + libraries/framebuf/bitmap/rle.h | 146 ++++ libraries/framebuf/bitmap/test/rle-test.c | 312 +++++++++ libraries/framebuf/composite/composite.c | 3 + .../framebuf/screen/screen-copy-bitmap-rle.c | 79 +++ .../framebuf/screen/screen-copy-bitmap-rle.h | 35 + .../framebuf/screen/screen-copy-ninepatch.c | 3 + libraries/framebuf/screen/screen-draw.c | 5 + 16 files changed, 1384 insertions(+), 1 deletion(-) create mode 100644 docs/framebuf/rle-bitmap.md create mode 100644 libraries/framebuf/bitmap/compress.c create mode 100644 libraries/framebuf/bitmap/rle.h create mode 100644 libraries/framebuf/bitmap/test/rle-test.c create mode 100644 libraries/framebuf/screen/screen-copy-bitmap-rle.c create mode 100644 libraries/framebuf/screen/screen-copy-bitmap-rle.h diff --git a/CMakeLists.txt b/CMakeLists.txt index cc88e15..1adc285 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -249,8 +249,10 @@ set(DATASTRUCT_SOURCES set(FRAMEBUF_SOURCES libraries/framebuf/bitmap/bitmap.c + libraries/framebuf/bitmap/compress.c libraries/framebuf/bitmap/fill-pattern.c libraries/framebuf/bitmap/load.c + libraries/framebuf/bitmap/rle.h libraries/framebuf/bitmap/save.c libraries/framebuf/bmfont/bmfont.c libraries/framebuf/bmfont/enumerate.c @@ -261,6 +263,8 @@ set(FRAMEBUF_SOURCES libraries/framebuf/pattern/pattern.c libraries/framebuf/pixelfmt/log2bpp.c libraries/framebuf/screen/screen.c + libraries/framebuf/screen/screen-copy-bitmap-rle.c + libraries/framebuf/screen/screen-copy-bitmap-rle.h libraries/framebuf/screen/screen-copy-ninepatch.c libraries/framebuf/screen/screen-copy-rect.c libraries/framebuf/screen/screen-draw.c @@ -659,6 +663,7 @@ if(BUILD_TESTS) libraries/datastruct/hash/test/hash-test.c libraries/datastruct/list/test/list-test.c libraries/datastruct/ntree/test/ntree-test.c + libraries/framebuf/bitmap/test/rle-test.c libraries/framebuf/bmfont/test/bmfont-test.c libraries/framebuf/composite/test/composite-test.c libraries/framebuf/curve/test/curve-test.c diff --git a/apps/test/main.c b/apps/test/main.c index aaf9f21..a23cb32 100644 --- a/apps/test/main.c +++ b/apps/test/main.c @@ -53,6 +53,7 @@ static const test_t tests[] = { "bmfont", bmfont_test }, { "composite", composite_test }, + { "rle", bitmap_rle_test }, { "curve", curve_test }, { "screen", screen_test }, diff --git a/docs/framebuf/rle-bitmap.md b/docs/framebuf/rle-bitmap.md new file mode 100644 index 0000000..bc9d985 --- /dev/null +++ b/docs/framebuf/rle-bitmap.md @@ -0,0 +1,102 @@ +# [DPTLib](https://github.com/dpt/DPTLib) > framebuf > RLE-compressed bitmaps + +An optional **read-only run-length-encoded** representation of a `bitmap_t`. +UI art, icons and nine-patches are typically sparse: long runs of one colour +and large fully-transparent regions. Storing them raw wastes RAM and, at blit +time, memory bandwidth. `bitmap_compress()` squashes a loaded bitmap in place +into a per-scanline ("laterally counted") opcode stream; `screen_copy_bitmap()` +decodes it on the fly, skipping transparent runs by pointer arithmetic. + +## Lifecycle + +```C +bitmap_t bm; +bitmap_load_png(&bm, "icon.png"); /* raw rgba8888 */ +bitmap_compress(&bm); /* now RLE, bm.base is a blob */ + +screen_copy_bitmap(&scr, x, y, &bm);/* blits straight from the blob */ + +/* bitmap_decompress(&bm); to get raw pixels back, or just: */ +free(bm.base); +``` + +`bitmap_compress()` is idempotent. `bitmap_decompress()` restores the raw +`rowbytes`-strided buffer byte-for-byte and is a no-op on an uncompressed +bitmap. + +## The compressed flag + +There is no new `bitmap_t` field. Bit 30 of `bm->format` (`pixelfmt_RLE_FLAG`, +clear of the sign bit) marks a compressed pixel store. The low bits still hold +the real `pixelfmt_t`, so `bm->size`, `bm->palette` and `bm->span` stay valid +while compressed; `bm->rowbytes` is 0. + +- `pixelfmt_is_rle(f)` / `bitmap_is_compressed(bm)` — test the flag. +- `pixelfmt_base(f)` — strip the flag, yielding the decoded pixel format. + +`pixelfmt_log2bpp()` and `spanregistry_get()` are **not** flag-tolerant: a +compressed format reaching them is a missing-guard bug and trips `assert(0)`. +Every other bitmap entry point (`bitmap_clear`, `bitmap_fill_pattern`, +`bitmap_convert`, `bitmap_save_png`, `composite`, `screen_copy_ninepatch`) +guards with `pixelfmt_is_rle()` and declines with `result_NOT_SUPPORTED`. + +## Blob layout + +`bm->base` points at an 8-byte header followed by `bm->size.h` opcode streams, +one per row, concatenated, each EOL-terminated. No magic, no format/size (all +still in the struct), no row-offset index. + +| bytes | field | meaning | +|-------|------------|-----------------------------------------------------| +| 0..3 | `rowbytes` | u32 LE — original stride, restored by decompress | +| 4..7 | `data_len` | u32 LE — encoded byte count that follows | + +To blit from a clipped top row the decoder walks the opcode stream forward +(no writes) counting EOL bytes — `bitmap__rle_skip_row()`. + +## Row opcodes + +Dispatch is by the count of leading zero bits of the first byte (`clz8`, +UTF-8-prefix style). `PIXEL` is 1 byte for an 8bpp base, 4 bytes for 32bpp. +Long forms store `length - shortmax - 1`; a zero length field means `2^bits`. + +``` +1LLLLLLL literal : 1..128 pixels follow inline +01LLLLLL PIXEL repeat : 1..64 copies of one inline pixel +001LLLLL skip : 1..32 zero pixels [alpha only] +0001LLLL LLLLLLLL literal long : 129..4224 pixels follow inline +00001LLL LLLLLLLL PIXEL repeat long : 65..2112 copies +000001LL LLLLLLLL skip long : 33..1056 zero pixels [alpha only] +00000001 EOL +00000000 reserved (decoder rejects) +``` + +A `skip` run drops the pixel bytes entirely, so it is only emitted for a +**wholly zero** pixel (`0x00000000`, the usual PNG transparent value). +Non-zero-but-alpha-0 pixels are encoded as literal/repeat and survive a round +trip. `bitmap_decompress()` writes zeros for skip runs; the blit leaves the +destination untouched. + +## Supported formats + +| base format | compress | skip runs | +|-----------------------------------------------|----------|-----------| +| `pixelfmt_y8` | yes | no | +| `pixelfmt_{bgrx,rgbx,xbgr,xrgb}8888` | yes | no | +| `pixelfmt_{bgra,rgba}8888` | yes | yes | +| `pixelfmt_{abgr,argb}8888` | yes | no | +| `p1`..`p8`, 12/15/16bpp | no (`result_NOT_SUPPORTED`) | — | + +## v1 limitations + +- **32bpp screens only.** A p4 screen target declines `result_NOT_SUPPORTED`. +- **Source must decode to the screen's exact format** — the blit is a byte + copy, no per-pixel conversion. `pixelfmt_base(src->format) == scr->format`. +- **Alpha-tested, not alpha-blended.** Transparent runs are skipped, opaque + runs copied. This matches `screen_copy_bitmap`'s p4 path but not its 32bpp + path (which blends per pixel). A blended RLE path would decode alpha runs + into `span->blendarray`. +- No scanline dedup, no row-offset table — pure sequential decode. +- No `composite()` or nine-patch support for compressed bitmaps. +- No colour-key transparency — wholly-zero pixels only. +- Runtime `bitmap_compress()` only — no `.rle` file format or offline tool. diff --git a/include/framebuf/bitmap.h b/include/framebuf/bitmap.h index 34d1509..a219b42 100644 --- a/include/framebuf/bitmap.h +++ b/include/framebuf/bitmap.h @@ -139,4 +139,41 @@ result_t bitmap_convert(const bitmap_t *bm, pixelfmt_t newfmt, bitmap_t **newbm); +/** Non-zero if `bm` holds an RLE-compressed (read-only) pixel store. */ +#define bitmap_is_compressed(bm) pixelfmt_is_rle((bm)->format) + +/** + * Compress a bitmap's pixels in place into a read-only run-length-encoded + * blob, replacing `bm->base`. `bm->size`, `bm->palette` and `bm->span` stay + * valid for the decoded format; `bm->format` gains \ref pixelfmt_RLE_FLAG + * and `bm->rowbytes` becomes 0. Idempotent (a second call returns \ref + * result_OK). + * + * Suitable for sparse UI art: long single-colour runs and, for the alpha + * formats, fully-transparent regions collapse to a few bytes, saving RAM and + * blit-time memory bandwidth. Only \ref screen_copy_bitmap can consume the + * result; other bitmap operations decline with \ref result_NOT_SUPPORTED. + * + * Supported source formats: \ref pixelfmt_y8, the 32bpp `*8888` formats + * (`bgrx`/`rgbx`/`xbgr`/`xrgb`) and the 32bpp alpha formats + * (`bgra`/`rgba`/`abgr`/`argb`). Transparent-run compression applies to + * `bgra8888`/`rgba8888` only. + * + * \param[in] bm Bitmap to compress in place. + * \return \ref result_OK on success, \ref result_NOT_SUPPORTED for an + * unsupported format, \ref result_OOM on allocation failure. + */ +result_t bitmap_compress(bitmap_t *bm); + +/** + * Reverse \ref bitmap_compress, restoring a raw `rowbytes`-strided pixel + * buffer byte-for-byte. `bm->format` loses \ref pixelfmt_RLE_FLAG and + * `bm->rowbytes` is restored. A no-op returning \ref result_OK if `bm` is + * not compressed. + * + * \param[in] bm Bitmap to decompress in place. + * \return \ref result_OK on success, \ref result_OOM on allocation failure. + */ +result_t bitmap_decompress(bitmap_t *bm); + #endif /* FRAMEBUF_BITMAP_H */ diff --git a/include/framebuf/pixelfmt.h b/include/framebuf/pixelfmt.h index c8b0f63..fe3849a 100644 --- a/include/framebuf/pixelfmt.h +++ b/include/framebuf/pixelfmt.h @@ -95,6 +95,27 @@ typedef unsigned int pixelfmt_argb8888_t; /* 32bpp argb8888 */ typedef unsigned int pixelfmt_xxxa8888_t; /* any 32bpp alpha */ +/* ----------------------------------------------------------------------- */ + +/** + * Bit 30 of a bitmap's `format` word flags an RLE-compressed pixel store: + * its `base` points at an encoded blob rather than a raw `rowbytes`-strided + * buffer. The low bits still hold the real \ref pixelfmt_t of a decoded + * pixel, so `size`, `palette` and `span` stay valid while compressed. Bit 30 + * (not the sign bit) keeps the word non-negative. + * + * \see pixelfmt_base, pixelfmt_is_rle + */ +#define pixelfmt_RLE_FLAG (1 << 30) + +/** Strip any flag bits, yielding the underlying pixel format. */ +#define pixelfmt_base(f) ((pixelfmt_t) ((f) & (pixelfmt_RLE_FLAG - 1))) + +/** Non-zero if `f` names an RLE-compressed pixel store. */ +#define pixelfmt_is_rle(f) (((f) & pixelfmt_RLE_FLAG) != 0) + +/* ----------------------------------------------------------------------- */ + typedef unsigned int pixelfmt_any_t; /* generic/unspecified pixel */ typedef unsigned char pixelfmt_any8_t; /* any 8bpp pixel */ typedef unsigned short pixelfmt_any16_t; /* any 16bpp pixel */ diff --git a/include/test/all-tests.h b/include/test/all-tests.h index aca2be7..15130ad 100644 --- a/include/test/all-tests.h +++ b/include/test/all-tests.h @@ -23,7 +23,8 @@ extern testfn_t pickle_test, tagdb_test; /* framebuf */ -extern testfn_t bmfont_test, +extern testfn_t bitmap_rle_test, + bmfont_test, composite_test, curve_test, screen_test; diff --git a/libraries/framebuf/bitmap/bitmap.c b/libraries/framebuf/bitmap/bitmap.c index 139d70a..bf31200 100644 --- a/libraries/framebuf/bitmap/bitmap.c +++ b/libraries/framebuf/bitmap/bitmap.c @@ -72,6 +72,12 @@ void bitmap_clear(bitmap_t *bm, colour_t colour) assert(bm); + if (pixelfmt_is_rle(bm->format)) + { + assert(!"bitmap_clear on compressed bitmap"); + return; + } + log2bpp = pixelfmt_log2bpp(bm->format); px = colour_to_pixel(bm->palette, bm->palette ? 1 << (1 << log2bpp) : 0, @@ -189,6 +195,9 @@ result_t bitmap_convert(const bitmap_t *src, { *dst = NULL; + if (pixelfmt_is_rle(src->format)) + return result_NOT_SUPPORTED; + switch (src->format) { case pixelfmt_p4: diff --git a/libraries/framebuf/bitmap/compress.c b/libraries/framebuf/bitmap/compress.c new file mode 100644 index 0000000..32f8655 --- /dev/null +++ b/libraries/framebuf/bitmap/compress.c @@ -0,0 +1,621 @@ +/* framebuf/bitmap/compress.c -- RLE compress/decompress a read-only bitmap */ + +#include +#include +#include +#include +#include + +#include "base/result.h" +#include "base/utils.h" +#include "utils/barith.h" + +#include "framebuf/pixelfmt.h" + +#include "framebuf/bitmap.h" + +#include "rle.h" + +/* ----------------------------------------------------------------------- */ + +void bitmap__rle_put32(uint8_t *p, uint32_t v) +{ + p[0] = (uint8_t) (v ); + p[1] = (uint8_t) (v >> 8); + p[2] = (uint8_t) (v >> 16); + p[3] = (uint8_t) (v >> 24); +} + +uint32_t bitmap__rle_get32(const uint8_t *p) +{ + return (uint32_t) p[0] + | (uint32_t) p[1] << 8 + | (uint32_t) p[2] << 16 + | (uint32_t) p[3] << 24; +} + +/* ----------------------------------------------------------------------- */ + +/* Read one pixel from `p` (1 or 4 bytes) as a host value. */ +static pixelfmt_any_t rle__load_pixel(const uint8_t *p, int log2bpp) +{ + if (log2bpp == 3) + return p[0]; + + return (pixelfmt_any_t) p[0] + | (pixelfmt_any_t) p[1] << 8 + | (pixelfmt_any_t) p[2] << 16 + | (pixelfmt_any_t) p[3] << 24; +} + +/* ----------------------------------------------------------------------- */ + +/* + * Encoder cursor. `p` walks the output, `end` is one past the buffer; + * `overflow` latches once a write would pass `end`. + */ +typedef struct rle__enc +{ + uint8_t *p; + uint8_t *end; + int overflow; +} +rle__enc_t; + +static void rle__put(rle__enc_t *e, uint8_t b) +{ + if (e->p >= e->end) + { + e->overflow = 1; + return; + } + *e->p++ = b; +} + +static void rle__put_pixel(rle__enc_t *e, pixelfmt_any_t px, int bpp) +{ + int i; + + for (i = 0; i < bpp; i++) + rle__put(e, (uint8_t) (px >> (8 * i))); +} + +/* ZEROIS2N: a length equal to 2^bits is stored as a zero field. */ +static unsigned int rle__lenfield(int length, int bits) +{ + if (length == (1 << bits)) + return 0; + + return (unsigned int) length; +} + +/* Emit a literal run: `length` pixels starting at `src` (already at the run). */ +static void rle__emit_literal(rle__enc_t *e, + const uint8_t *src, + int length, + int bpp) +{ + int chunk; + int i; + + while (length > 0) + { + if (length <= bitmap__RLE_LITERAL_LEN_MAX) + { + chunk = length; + rle__put(e, (uint8_t) (bitmap__RLE_VAL(bitmap__RLE_LITERAL_ID) + | rle__lenfield(chunk, bitmap__RLE_LITERAL_LEN_BITS))); + } + else + { + unsigned int store; + + chunk = MIN(length, bitmap__RLE_LITERAL_LONG_LEN_MAX); + store = (unsigned int) (chunk - bitmap__RLE_LITERAL_LONG_LEN_MIN); + rle__put(e, (uint8_t) (bitmap__RLE_VAL(bitmap__RLE_LITERAL_LONG_ID) + | (store >> 8))); + rle__put(e, (uint8_t) store); + } + + for (i = 0; i < chunk * bpp; i++) + rle__put(e, src[i]); + + src += (size_t) chunk * bpp; + length -= chunk; + } +} + +/* Emit a repeat run: `length` copies of pixel `px`. */ +static void rle__emit_repeat(rle__enc_t *e, + pixelfmt_any_t px, + int length, + int bpp) +{ + int chunk; + + while (length > 0) + { + if (length <= bitmap__RLE_REPEAT_LEN_MAX) + { + chunk = length; + rle__put(e, (uint8_t) (bitmap__RLE_VAL(bitmap__RLE_REPEAT_ID) + | rle__lenfield(chunk, bitmap__RLE_REPEAT_LEN_BITS))); + } + else + { + unsigned int store; + + chunk = MIN(length, bitmap__RLE_REPEAT_LONG_LEN_MAX); + store = (unsigned int) (chunk - bitmap__RLE_REPEAT_LONG_LEN_MIN); + rle__put(e, (uint8_t) (bitmap__RLE_VAL(bitmap__RLE_REPEAT_LONG_ID) + | (store >> 8))); + rle__put(e, (uint8_t) store); + } + + rle__put_pixel(e, px, bpp); + length -= chunk; + } +} + +/* Emit a skip run: `length` transparent pixels. */ +static void rle__emit_skip(rle__enc_t *e, int length) +{ + int chunk; + + while (length > 0) + { + if (length <= bitmap__RLE_SKIP_LEN_MAX) + { + chunk = length; + rle__put(e, (uint8_t) (bitmap__RLE_VAL(bitmap__RLE_SKIP_ID) + | rle__lenfield(chunk, bitmap__RLE_SKIP_LEN_BITS))); + } + else + { + unsigned int store; + + chunk = MIN(length, bitmap__RLE_SKIP_LONG_LEN_MAX); + store = (unsigned int) (chunk - bitmap__RLE_SKIP_LONG_LEN_MIN); + rle__put(e, (uint8_t) (bitmap__RLE_VAL(bitmap__RLE_SKIP_LONG_ID) + | (store >> 8))); + rle__put(e, (uint8_t) store); + } + + length -= chunk; + } +} + +/* ----------------------------------------------------------------------- */ + +result_t bitmap__rle_encode_row(const void *srcrow, + int npix, + int log2bpp, + int has_alpha, + uint8_t *dst, + size_t dstcap, + size_t *dstused) +{ + rle__enc_t e; + const uint8_t *src; + int bpp; + int i; + int runstart; + + assert(srcrow); + assert(dst); + assert(log2bpp == 3 || log2bpp == 5); + + e.p = dst; + e.end = dst + dstcap; + e.overflow = 0; + + src = srcrow; + bpp = 1 << (log2bpp - 3); + + i = 0; + while (i < npix) + { + pixelfmt_any_t px0; + int run; + int transparent; + + px0 = rle__load_pixel(src + (size_t) i * bpp, log2bpp); + + /* Maximal identical run from i. */ + run = 1; + while (i + run < npix + && rle__load_pixel(src + (size_t) (i + run) * bpp, log2bpp) == px0) + run++; + + /* A skip run drops the pixel bytes entirely, so it must be reversible: + * only wholly-zero pixels qualify (the common PNG transparent value). + * Non-zero-but-alpha-0 pixels go through as literal/repeat. */ + transparent = has_alpha && (px0 == 0); + + if (run >= 2 || transparent) + { + if (transparent) + rle__emit_skip(&e, run); + else + rle__emit_repeat(&e, px0, run, bpp); + i += run; + continue; + } + + /* Not worth a repeat: accumulate a literal run until the next repeatable + * pair (or a transparent pixel for alpha formats). */ + runstart = i; + i++; + while (i < npix) + { + pixelfmt_any_t a; + + a = rle__load_pixel(src + (size_t) i * bpp, log2bpp); + + if (has_alpha && a == 0) + break; + if (i + 1 < npix + && rle__load_pixel(src + (size_t) (i + 1) * bpp, log2bpp) == a) + break; + i++; + } + rle__emit_literal(&e, src + (size_t) runstart * bpp, i - runstart, bpp); + } + + rle__put(&e, bitmap__RLE_EOL_BYTE); + + if (e.overflow) + return result_BUFFER_OVERFLOW; + + *dstused = (size_t) (e.p - dst); + return result_OK; +} + +/* ----------------------------------------------------------------------- */ + +const uint8_t *bitmap__rle_decode_row(const uint8_t *p, + int log2bpp, + void *dstrow, + int skip, + int plot, + int zero_skip) +{ + uint8_t *dst; + int bpp; + + assert(p); + assert(log2bpp == 3 || log2bpp == 5); + + dst = dstrow; + bpp = 1 << (log2bpp - 3); + + for (;;) + { + uint8_t op; + int id; + int length; + int take; /* pixels of this run actually written */ + int drop; /* pixels of this run swallowed by `skip` */ + + op = *p++; + id = clz8(op); + + if (id == bitmap__RLE_EOL_ID) + break; + + /* Decode opcode + length; `srcpx` (literal) / `px` (repeat) set below. */ + if (id == bitmap__RLE_LITERAL_ID) + { + length = op & (bitmap__RLE_LITERAL_LEN_MAX - 1); + if (length == 0) + length = bitmap__RLE_LITERAL_LEN_MAX; + } + else if (id == bitmap__RLE_REPEAT_ID) + { + length = op & (bitmap__RLE_REPEAT_LEN_MAX - 1); + if (length == 0) + length = bitmap__RLE_REPEAT_LEN_MAX; + } + else if (id == bitmap__RLE_SKIP_ID) + { + length = op & (bitmap__RLE_SKIP_LEN_MAX - 1); + if (length == 0) + length = bitmap__RLE_SKIP_LEN_MAX; + } + else if (id == bitmap__RLE_LITERAL_LONG_ID) + { + length = ((op & (bitmap__RLE_VAL(bitmap__RLE_LITERAL_LONG_ID) - 1)) << 8) + | *p++; + length += bitmap__RLE_LITERAL_LONG_LEN_MIN; + } + else if (id == bitmap__RLE_REPEAT_LONG_ID) + { + length = ((op & (bitmap__RLE_VAL(bitmap__RLE_REPEAT_LONG_ID) - 1)) << 8) + | *p++; + length += bitmap__RLE_REPEAT_LONG_LEN_MIN; + } + else if (id == bitmap__RLE_SKIP_LONG_ID) + { + length = ((op & (bitmap__RLE_VAL(bitmap__RLE_SKIP_LONG_ID) - 1)) << 8) + | *p++; + length += bitmap__RLE_SKIP_LONG_LEN_MIN; + } + else + { + assert(!"bitmap__rle_decode_row: reserved opcode"); + break; + } + + /* Clip this run against the remaining left-skip and plot budget. The + * stream is still walked to EOL after `plot` hits 0 (take clamps to 0), + * so the returned cursor always lands on the next row. */ + drop = MIN(skip, length); + skip -= drop; + take = length - drop; + if (take > plot) + take = plot; + if (take < 0) + take = 0; + + switch (id) + { + case bitmap__RLE_LITERAL_ID: + case bitmap__RLE_LITERAL_LONG_ID: + if (take > 0) + memcpy(dst, p + (size_t) drop * bpp, (size_t) take * bpp); + p += (size_t) length * bpp; + dst += (size_t) take * bpp; + plot -= take; + break; + + case bitmap__RLE_REPEAT_ID: + case bitmap__RLE_REPEAT_LONG_ID: + if (bpp == 1) + { + if (take > 0) + memset(dst, p[0], (size_t) take); + } + else + { + int k; + + for (k = 0; k < take; k++) + memcpy(dst + (size_t) k * bpp, p, (size_t) bpp); + } + p += bpp; + dst += (size_t) take * bpp; + plot -= take; + break; + + default: /* skip */ + if (zero_skip && take > 0) + memset(dst, 0, (size_t) take * bpp); + dst += (size_t) take * bpp; + plot -= take; + break; + } + } + + return p; +} + +/* ----------------------------------------------------------------------- */ + +const uint8_t *bitmap__rle_skip_row(const uint8_t *p, + const uint8_t *end, + int log2bpp) +{ + int bpp; + + assert(log2bpp == 3 || log2bpp == 5); + bpp = 1 << (log2bpp - 3); + + while (p < end) + { + uint8_t op; + int id; + int length; + + op = *p++; + id = clz8(op); + + if (id == bitmap__RLE_EOL_ID) + break; + + switch (id) + { + case bitmap__RLE_LITERAL_ID: + length = op & (bitmap__RLE_LITERAL_LEN_MAX - 1); + if (length == 0) + length = bitmap__RLE_LITERAL_LEN_MAX; + p += (size_t) length * bpp; + break; + + case bitmap__RLE_REPEAT_ID: + p += bpp; + break; + + case bitmap__RLE_SKIP_ID: + break; + + case bitmap__RLE_LITERAL_LONG_ID: + length = ((op & (bitmap__RLE_VAL(bitmap__RLE_LITERAL_LONG_ID) - 1)) << 8) + | *p++; + length += bitmap__RLE_LITERAL_LONG_LEN_MIN; + p += (size_t) length * bpp; + break; + + case bitmap__RLE_REPEAT_LONG_ID: + p += 1; /* low length byte */ + p += bpp; /* inline pixel */ + break; + + case bitmap__RLE_SKIP_LONG_ID: + p += 1; /* low length byte */ + break; + + default: + assert(!"bitmap__rle_skip_row: reserved opcode"); + return end; + } + } + + return p; +} + +/* ----------------------------------------------------------------------- */ + +/* Is `fmt` a format the RLE codec accepts? Sets *has_alpha for the alpha + * formats whose alpha lives in the top byte (bgra/rgba), which are the ones + * that get `skip` runs. + * ponytail: abgr8888/argb8888 carry alpha in the low byte; they still + * compress but with no skip runs. Add a per-format alpha shift if sparse + * abgr/argb art shows up. */ +static int rle__format_ok(pixelfmt_t fmt, int *has_alpha) +{ + *has_alpha = 0; + + switch (fmt) + { + case pixelfmt_bgra8888: + case pixelfmt_rgba8888: + *has_alpha = 1; + return 1; + + case pixelfmt_y8: + case pixelfmt_bgrx8888: + case pixelfmt_rgbx8888: + case pixelfmt_xbgr8888: + case pixelfmt_xrgb8888: + case pixelfmt_abgr8888: + case pixelfmt_argb8888: + return 1; + + default: + return 0; + } +} + +/* ----------------------------------------------------------------------- */ + +result_t bitmap_compress(bitmap_t *bm) +{ + result_t rc; + pixelfmt_t base; + int has_alpha; + int log2bpp; + int bpp; + int w, h; + size_t worst; + size_t rowworst; + uint8_t *blob; + uint8_t *cursor; + const uint8_t *srcrow; + uint32_t data_len; + uint8_t *shrunk; + int y; + + assert(bm); + + if (pixelfmt_is_rle(bm->format)) + return result_OK; /* already compressed */ + + base = pixelfmt_base(bm->format); + if (!rle__format_ok(base, &has_alpha)) + return result_NOT_SUPPORTED; + + log2bpp = pixelfmt_log2bpp(base); + bpp = 1 << (log2bpp - 3); + w = bm->size.w; + h = bm->size.h; + + /* Worst case per row: every pixel literal, one control byte per 128 px, + * plus an EOL. */ + rowworst = (size_t) w * bpp + + ((size_t) w / bitmap__RLE_LITERAL_LEN_MAX + 1) * 2 + + 1; + worst = bitmap__RLE_HEADER_SIZE + rowworst * (size_t) h; + + blob = malloc(worst); + if (blob == NULL) + return result_OOM; + + cursor = blob + bitmap__RLE_HEADER_SIZE; + srcrow = bm->base; + for (y = 0; y < h; y++) + { + size_t used; + + rc = bitmap__rle_encode_row(srcrow, w, log2bpp, has_alpha, + cursor, worst - (size_t) (cursor - blob), + &used); + if (rc != result_OK) + { + free(blob); + return rc; + } + + cursor += used; + srcrow += bm->rowbytes; + } + + data_len = (uint32_t) (cursor - blob - bitmap__RLE_HEADER_SIZE); + bitmap__rle_put32(blob + 0, (uint32_t) bm->rowbytes); + bitmap__rle_put32(blob + 4, data_len); + + shrunk = realloc(blob, bitmap__RLE_HEADER_SIZE + data_len); + if (shrunk != NULL) + blob = shrunk; + + free(bm->base); + bm->base = blob; + bm->format = base | pixelfmt_RLE_FLAG; + bm->rowbytes = 0; + /* bm->span and bm->palette stay valid for the decoded (base) format. */ + + return result_OK; +} + +/* ----------------------------------------------------------------------- */ + +result_t bitmap_decompress(bitmap_t *bm) +{ + pixelfmt_t base; + int log2bpp; + uint32_t rowbytes; + const uint8_t *p; + uint8_t *raw; + uint8_t *dstrow; + int y; + + assert(bm); + + if (!pixelfmt_is_rle(bm->format)) + return result_OK; /* not compressed */ + + assert(bm->base != NULL); + + base = pixelfmt_base(bm->format); + log2bpp = pixelfmt_log2bpp(base); + + rowbytes = bitmap__rle_get32((const uint8_t *) bm->base + 0); + p = (const uint8_t *) bm->base + bitmap__RLE_HEADER_SIZE; + + raw = malloc((size_t) rowbytes * bm->size.h); + if (raw == NULL) + return result_OOM; + + dstrow = raw; + for (y = 0; y < bm->size.h; y++) + { + p = bitmap__rle_decode_row(p, log2bpp, dstrow, 0, bm->size.w, 1); + dstrow += rowbytes; + } + + free(bm->base); + bm->base = raw; + bm->format = base; + bm->rowbytes = (int) rowbytes; + + return result_OK; +} diff --git a/libraries/framebuf/bitmap/fill-pattern.c b/libraries/framebuf/bitmap/fill-pattern.c index f30ea2e..050ca43 100644 --- a/libraries/framebuf/bitmap/fill-pattern.c +++ b/libraries/framebuf/bitmap/fill-pattern.c @@ -26,6 +26,9 @@ result_t bitmap_fill_pattern(bitmap_t *bm, assert(bm); assert(pattern); + if (pixelfmt_is_rle(bm->format)) + return result_NOT_SUPPORTED; + log2bpp = pixelfmt_log2bpp(bm->format); if (log2bpp != 3 && log2bpp != 5) return result_NOT_SUPPORTED; diff --git a/libraries/framebuf/bitmap/rle.h b/libraries/framebuf/bitmap/rle.h new file mode 100644 index 0000000..32b1d58 --- /dev/null +++ b/libraries/framebuf/bitmap/rle.h @@ -0,0 +1,146 @@ +/* framebuf/bitmap/rle.h -- private RLE bitmap encode/decode shared internals */ + +#ifndef FRAMEBUF_BITMAP_RLE_H +#define FRAMEBUF_BITMAP_RLE_H + +#include +#include + +#include "base/result.h" + +/* ----------------------------------------------------------------------- */ + +/** + * Blob header prefixing a compressed bitmap's `base`. Two little-endian + * `uint32_t`s, written and read byte-wise (see bitmap__rle_put32 / + * bitmap__rle_get32) so the on-blob layout is host-endian independent. + */ +#define bitmap__RLE_HEADER_SIZE 8 + +/* ----------------------------------------------------------------------- */ + +/* + * Row opcode stream. The opcode of the first byte of each command is its + * count of leading zero bits (clz8), UTF-8-prefix style. Lengths are pixel + * counts. A zero length field means 2^(field width). Long forms store + * `length - shortmax - 1` so their range begins where the short form ends. + * + * 1LLLLLLL literal : 1..128 px, then len px inline + * 01LLLLLL repeat : 1..64 copies of one inline px + * 001LLLLL skip : 1..32 transparent px [alpha only] + * 0001LLLL LLLLLLLL literal long : 129..4224 px, then len px inline + * 00001LLL LLLLLLLL repeat long : 65..2112 copies + * 000001LL LLLLLLLL skip long : 33..1056 px [alpha only] + * 00000001 EOL + * 00000000 reserved (decoder rejects) + */ + +#define bitmap__RLE_LITERAL_ID 0 +#define bitmap__RLE_REPEAT_ID 1 +#define bitmap__RLE_SKIP_ID 2 +#define bitmap__RLE_LITERAL_LONG_ID 3 +#define bitmap__RLE_REPEAT_LONG_ID 4 +#define bitmap__RLE_SKIP_LONG_ID 5 +/* id 6 reserved */ +#define bitmap__RLE_EOL_ID 7 + +#define bitmap__RLE_LITERAL_LEN_BITS 7 +#define bitmap__RLE_REPEAT_LEN_BITS 6 +#define bitmap__RLE_SKIP_LEN_BITS 5 +#define bitmap__RLE_LITERAL_LONG_LEN_BITS 12 +#define bitmap__RLE_REPEAT_LONG_LEN_BITS 11 +#define bitmap__RLE_SKIP_LONG_LEN_BITS 10 + +#define bitmap__RLE_LITERAL_LEN_MAX (1 << bitmap__RLE_LITERAL_LEN_BITS) /* 128 */ +#define bitmap__RLE_REPEAT_LEN_MAX (1 << bitmap__RLE_REPEAT_LEN_BITS) /* 64 */ +#define bitmap__RLE_SKIP_LEN_MAX (1 << bitmap__RLE_SKIP_LEN_BITS) /* 32 */ + +#define bitmap__RLE_LITERAL_LONG_LEN_MIN (bitmap__RLE_LITERAL_LEN_MAX + 1) +#define bitmap__RLE_REPEAT_LONG_LEN_MIN (bitmap__RLE_REPEAT_LEN_MAX + 1) +#define bitmap__RLE_SKIP_LONG_LEN_MIN (bitmap__RLE_SKIP_LEN_MAX + 1) + +#define bitmap__RLE_LITERAL_LONG_LEN_MAX \ + ((1 << bitmap__RLE_LITERAL_LONG_LEN_BITS) - 1 + bitmap__RLE_LITERAL_LONG_LEN_MIN) +#define bitmap__RLE_REPEAT_LONG_LEN_MAX \ + ((1 << bitmap__RLE_REPEAT_LONG_LEN_BITS) - 1 + bitmap__RLE_REPEAT_LONG_LEN_MIN) +#define bitmap__RLE_SKIP_LONG_LEN_MAX \ + ((1 << bitmap__RLE_SKIP_LONG_LEN_BITS) - 1 + bitmap__RLE_SKIP_LONG_LEN_MIN) + +#define bitmap__RLE_EOL_BYTE 0x01u + +/* First byte of each short opcode: a single set bit at position (7 - id). */ +#define bitmap__RLE_VAL(id) ((0x80u >> (id)) & 0xFFu) + +/* ----------------------------------------------------------------------- */ + +/** Write `v` as four little-endian bytes to `p`. */ +void bitmap__rle_put32(uint8_t *p, uint32_t v); + +/** Read four little-endian bytes from `p`. */ +uint32_t bitmap__rle_get32(const uint8_t *p); + +/* ----------------------------------------------------------------------- */ + +/** + * Encode one source row into the opcode stream. + * + * \param[in] srcrow Row of `npix` pixels, `1 << (log2bpp - 3)` bytes + * each. + * \param[in] npix Pixels in the row. + * \param[in] log2bpp 3 (1-byte pixels) or 5 (4-byte pixels). + * \param[in] has_alpha Non-zero to emit `skip` for runs of alpha-0 pixels. + * \param[out] dst Output cursor. + * \param[in] dstcap Bytes available at `dst`. + * \param[out] dstused Bytes written (including the EOL). + * \return \ref result_OK, or \ref result_BUFFER_OVERFLOW if `dstcap` is + * short. + */ +result_t bitmap__rle_encode_row(const void *srcrow, + int npix, + int log2bpp, + int has_alpha, + uint8_t *dst, + size_t dstcap, + size_t *dstused); + +/** + * Decode one row of the opcode stream into `dstrow`, honouring a left-skip + * and a plot count for clipping. + * + * Literal runs are `memcpy`d, repeat runs splatted, `skip` runs advance the + * destination pointer only (whole-byte formats, so the copy is + * format-agnostic given `bpp`). `dstrow` points at the first pixel to write + * (already offset to the clip's left edge); `skip` pixels of the row are + * consumed before writing begins and at most `plot` pixels are written. + * + * \param[in] p Cursor at the row's first opcode. + * \param[in] log2bpp 3 or 5. + * \param[in] dstrow Destination, at the first pixel to be written. + * \param[in] skip Row pixels to consume before writing. + * \param[in] plot Maximum pixels to write. + * \param[in] zero_skip Non-zero to write zero pixels for `skip` runs + * (lossless decode); zero to leave them untouched + * (alpha-tested blit). + * \return Cursor just past this row's EOL. + */ +const uint8_t *bitmap__rle_decode_row(const uint8_t *p, + int log2bpp, + void *dstrow, + int skip, + int plot, + int zero_skip); + +/** + * Advance past one row's opcodes without decoding, for reaching a clipped + * top row. + * + * \param[in] p Cursor at the row's first opcode. + * \param[in] end One past the last valid stream byte. + * \param[in] log2bpp 3 or 5 (sizes inline literal/repeat pixels). + * \return Cursor just past this row's EOL (clamped to `end`). + */ +const uint8_t *bitmap__rle_skip_row(const uint8_t *p, + const uint8_t *end, + int log2bpp); + +#endif /* FRAMEBUF_BITMAP_RLE_H */ diff --git a/libraries/framebuf/bitmap/test/rle-test.c b/libraries/framebuf/bitmap/test/rle-test.c new file mode 100644 index 0000000..a72081c --- /dev/null +++ b/libraries/framebuf/bitmap/test/rle-test.c @@ -0,0 +1,312 @@ +/* framebuf/bitmap/test/rle-test.c -- RLE-compressed bitmap tests */ + +#include +#include +#include +#include +#include + +#include "base/result.h" +#include "io/path.h" + +#include "framebuf/pixelfmt.h" + +#include "framebuf/bitmap.h" +#include "framebuf/screen.h" + +#include "../rle.h" + +/* ----------------------------------------------------------------------- */ + +#define W 256 +#define H 200 + +/* ----------------------------------------------------------------------- */ + +/* Encode one row then decode it unclipped, compare byte-for-byte. */ +static int roundtrip_row(const void *row, int npix, int log2bpp, int has_alpha) +{ + uint8_t enc[4 * W + W / 64 + 16]; + uint8_t dec[4 * W]; + size_t used; + result_t rc; + int bpp; + + bpp = 1 << (log2bpp - 3); + + memset(dec, 0xAB, sizeof dec); + + rc = bitmap__rle_encode_row(row, npix, log2bpp, has_alpha, + enc, sizeof enc, &used); + if (rc != result_OK) + { + fprintf(stderr, "roundtrip_row: encode rc=&%x\n", rc); + return 0; + } + + (void) bitmap__rle_decode_row(enc, log2bpp, dec, 0, npix, 1); + + (void) used; + + if (memcmp(dec, row, (size_t) npix * bpp) != 0) + { + fprintf(stderr, "roundtrip_row: mismatch (npix=%d log2bpp=%d)\n", + npix, log2bpp); + return 0; + } + return 1; +} + +/* ----------------------------------------------------------------------- */ + +static int test_synthetic(void) +{ + uint32_t r32[W]; + uint8_t r8[W]; + int i; + int ok = 1; + + /* flat */ + for (i = 0; i < W; i++) { r32[i] = 0xFF204060u; r8[i] = 0x7F; } + ok &= roundtrip_row(r32, W, 5, 1); + ok &= roundtrip_row(r8, W, 3, 0); + + /* all transparent (fully zero: exercises skip runs) */ + for (i = 0; i < W; i++) r32[i] = 0x00000000u; + ok &= roundtrip_row(r32, W, 5, 1); + + /* non-zero but alpha-0: must survive as literal/repeat, not skip */ + for (i = 0; i < W; i++) r32[i] = 0x00123456u; + ok &= roundtrip_row(r32, W, 5, 1); + + /* zero and non-zero-transparent interleaved with opaque */ + for (i = 0; i < W; i++) + r32[i] = (i % 5 == 0) ? 0x00000000u + : (i % 5 == 1) ? 0x00AABBCCu + : 0xFF000000u | (uint32_t) i; + ok &= roundtrip_row(r32, W, 5, 1); + + /* high-noise */ + for (i = 0; i < W; i++) { r32[i] = 0xFF000000u | (uint32_t) (i * 2654435761u); + r8[i] = (uint8_t) (i * 37 + 1); } + ok &= roundtrip_row(r32, W, 5, 1); + ok &= roundtrip_row(r8, W, 3, 0); + + /* alternating pairs */ + for (i = 0; i < W; i++) { r32[i] = (i & 1) ? 0xFF111111u : 0xFFEEEEEEu; + r8[i] = (i & 1) ? 0x10 : 0xE0; } + ok &= roundtrip_row(r32, W, 5, 1); + ok &= roundtrip_row(r8, W, 3, 0); + + /* single pixel */ + r32[0] = 0xFF010203u; r8[0] = 0x42; + ok &= roundtrip_row(r32, 1, 5, 1); + ok &= roundtrip_row(r8, 1, 3, 0); + + /* run lengths straddling the short/long boundary */ + for (i = 0; i < W; i++) { r32[i] = 0xFF777777u; r8[i] = 0x55; } + ok &= roundtrip_row(r32, 128, 5, 1); /* exactly LITERAL_LEN_MAX repeats */ + ok &= roundtrip_row(r32, 129, 5, 1); + ok &= roundtrip_row(r8, 64, 3, 0); /* REPEAT_LEN_MAX */ + ok &= roundtrip_row(r8, 65, 3, 0); + + /* literal run straddling the boundary: no two adjacent pixels equal */ + for (i = 0; i < W; i++) r8[i] = (uint8_t) ((i * 131 + 7) | 1); + for (i = 1; i < W; i++) if (r8[i] == r8[i - 1]) r8[i] ^= 2; + ok &= roundtrip_row(r8, 128, 3, 0); + ok &= roundtrip_row(r8, 129, 3, 0); + + return ok; +} + +/* ----------------------------------------------------------------------- */ + +/* Reference blit matching the RLE decoder: copy every pixel except a wholly + * zero one (which the encoder turns into a skip run). */ +static void ref_blit(uint32_t *dst, + int dst_rowwords, + const uint32_t *src, + int src_rowwords, + int w, + int h) +{ + int x, y; + + for (y = 0; y < h; y++) + for (x = 0; x < w; x++) + { + uint32_t px = src[y * src_rowwords + x]; + if (px != 0) + dst[y * dst_rowwords + x] = px; + } +} + +static int test_blit(const char *resources) +{ + result_t rc; + bitmap_t bm; + bitmap_t cbm; + uint32_t *rawpix; + const char *filename; + int w, h; + int rowwords; + uint32_t *scr_ref; + uint32_t *scr_rle; + screen_t s; + box_t clip; + size_t npix; + int ok = 1; + + filename = path_join_filename(resources, 3, "resources", "composite", "A.png"); + rc = bitmap_load_png(&bm, filename); + if (rc) + { + fprintf(stderr, "test_blit: load rc=&%x\n", rc); + return 0; + } + /* bitmap_load_png yields rgba8888 (R,G,B,A byte order). */ + + w = bm.size.w; + h = bm.size.h; + npix = (size_t) w * h; + + /* Keep an untouched copy of the raw pixels. */ + rawpix = malloc(npix * 4); + memcpy(rawpix, bm.base, npix * 4); + + /* --- round trip --- */ + cbm = bm; + cbm.base = malloc(npix * 4); + memcpy(cbm.base, bm.base, npix * 4); + bitmap_init(&cbm, bm.size, bm.format, bm.rowbytes, NULL, cbm.base); + + rc = bitmap_compress(&cbm); + if (rc) { fprintf(stderr, "compress rc=&%x\n", rc); ok = 0; goto done; } + + if (!bitmap_is_compressed(&cbm) + || pixelfmt_base(cbm.format) != bm.format) + { + fprintf(stderr, "compress: flag/format wrong\n"); + ok = 0; goto done; + } + { + uint32_t data_len = bitmap__rle_get32((const uint8_t *) cbm.base + 4); + fprintf(stdout, "rle: A.png %zu raw -> %u encoded (%.2f)\n", + npix * 4, (unsigned) data_len, (double) data_len / (double) (npix * 4)); + if ((size_t) data_len >= npix * 4) + fprintf(stderr, "rle: warning: no size win on A.png\n"); + } + + rc = bitmap_decompress(&cbm); + if (rc) { fprintf(stderr, "decompress rc=&%x\n", rc); ok = 0; goto done; } + + if (cbm.rowbytes != bm.rowbytes || memcmp(cbm.base, rawpix, npix * 4) != 0) + { + fprintf(stderr, "round trip: pixels or rowbytes differ\n"); + ok = 0; goto done; + } + + /* --- blit equivalence, unclipped then clipped --- */ + rowwords = w; + scr_ref = malloc(npix * 4); + scr_rle = malloc(npix * 4); + + /* recompress for the blit */ + rc = bitmap_compress(&cbm); + if (rc) { fprintf(stderr, "compress2 rc=&%x\n", rc); ok = 0; goto done2; } + + /* unclipped */ + memset(scr_ref, 0, npix * 4); + memset(scr_rle, 0, npix * 4); + ref_blit(scr_ref, rowwords, rawpix, w, w, h); + + screen_init(&s, bm.size, pixelfmt_rgba8888, w * 4, NULL, scr_rle); + rc = screen_copy_bitmap(&s, 0, 0, &cbm); + if (rc) { fprintf(stderr, "blit rc=&%x\n", rc); ok = 0; goto done2; } + if (memcmp(scr_ref, scr_rle, npix * 4) != 0) + { + fprintf(stderr, "blit: unclipped mismatch\n"); + ok = 0; goto done2; + } + + /* clipped: crop top and left */ + memset(scr_ref, 0, npix * 4); + memset(scr_rle, 0, npix * 4); + { + int cx = w / 3, cy = h / 4; + int y; + + for (y = cy; y < h; y++) + ref_blit(scr_ref + y * rowwords + cx, rowwords, + rawpix + y * w + cx, w, w - cx, 1); + + clip.x0 = cx; clip.y0 = cy; clip.x1 = w; clip.y1 = h; + s.clip = clip; + rc = screen_copy_bitmap(&s, 0, 0, &cbm); + if (rc) { fprintf(stderr, "blit clipped rc=&%x\n", rc); ok = 0; goto done2; } + if (memcmp(scr_ref, scr_rle, npix * 4) != 0) + { + fprintf(stderr, "blit: clipped mismatch\n"); + ok = 0; goto done2; + } + } + +done2: + free(scr_ref); + free(scr_rle); +done: + free(cbm.base); + free(rawpix); + free(bm.base); + return ok; +} + +/* ----------------------------------------------------------------------- */ + +static int test_y8(void) +{ + bitmap_t bm; + uint8_t *pix; + uint8_t *copy; + int i; + result_t rc; + int ok = 1; + size_t n = (size_t) W * H; + + pix = malloc(n); + copy = malloc(n); + for (i = 0; i < (int) n; i++) + pix[i] = (uint8_t) ((i / 17) ^ (i * 13)); + memcpy(copy, pix, n); + + bitmap_init(&bm, SIZE2D(W, H), pixelfmt_y8, W, NULL, pix); + + rc = bitmap_compress(&bm); + if (rc) { fprintf(stderr, "y8 compress rc=&%x\n", rc); ok = 0; goto done; } + rc = bitmap_decompress(&bm); + if (rc) { fprintf(stderr, "y8 decompress rc=&%x\n", rc); ok = 0; goto done; } + + if (bm.rowbytes != W || memcmp(bm.base, copy, n) != 0) + { + fprintf(stderr, "y8: round trip differs\n"); + ok = 0; + } + +done: + free(bm.base); + free(copy); + return ok; +} + +/* ----------------------------------------------------------------------- */ + +result_t bitmap_rle_test(const char *resources) +{ + int ok = 1; + + ok &= test_synthetic(); + ok &= test_y8(); + ok &= test_blit(resources); + + return ok ? result_TEST_PASSED : result_TEST_FAILED; +} diff --git a/libraries/framebuf/composite/composite.c b/libraries/framebuf/composite/composite.c index b2859cd..b6b66c9 100644 --- a/libraries/framebuf/composite/composite.c +++ b/libraries/framebuf/composite/composite.c @@ -964,6 +964,9 @@ result_t composite(composite_rule_t rule, src->format != dst->format) return result_BAD_ARG; + if (pixelfmt_is_rle(src->format) || pixelfmt_is_rle(dst->format)) + return result_NOT_SUPPORTED; + switch (src->format) { case pixelfmt_rgba8888: diff --git a/libraries/framebuf/screen/screen-copy-bitmap-rle.c b/libraries/framebuf/screen/screen-copy-bitmap-rle.c new file mode 100644 index 0000000..b1bb0c4 --- /dev/null +++ b/libraries/framebuf/screen/screen-copy-bitmap-rle.c @@ -0,0 +1,79 @@ +/* framebuf/screen/screen-copy-bitmap-rle.c -- RLE bitmap blit */ + +#include +#include +#include + +#include "base/result.h" + +#include "geom/box.h" + +#include "framebuf/pixelfmt.h" + +#include "framebuf/bitmap.h" +#include "framebuf/screen.h" + +#include "../bitmap/rle.h" + +#include "screen-copy-bitmap-rle.h" + +result_t screen_copy_bitmap_rle(screen_t *scr, + int x, + int y, + const bitmap_t *src, + const box_t *draw_box) +{ + pixelfmt_t base; + int log2bpp; + const uint8_t *blob; + const uint8_t *p; + const uint8_t *end; + uint8_t *dstbase; + int firstrow; + int skip, plot; + int rows; + int bpp; + int i; + + assert(scr); + assert(src); + assert(pixelfmt_is_rle(src->format)); + + base = pixelfmt_base(src->format); + + /* v1: 32bpp screen, source decodes to the screen's exact byte layout. */ + if (pixelfmt_log2bpp(scr->format) != 5) + return result_NOT_SUPPORTED; + if (base != scr->format) + return result_NOT_SUPPORTED; + + log2bpp = pixelfmt_log2bpp(base); + bpp = 1 << (log2bpp - 3); + + blob = src->base; + p = blob + bitmap__RLE_HEADER_SIZE; + end = p + bitmap__rle_get32(blob + 4); + + /* Walk past rows clipped off the top. */ + firstrow = draw_box->y0 - y; + for (i = 0; i < firstrow; i++) + p = bitmap__rle_skip_row(p, end, log2bpp); + + skip = draw_box->x0 - x; + plot = draw_box->x1 - draw_box->x0; + rows = draw_box->y1 - draw_box->y0; + + dstbase = scr->base; + for (i = 0; i < rows; i++) + { + uint8_t *dstrow; + + dstrow = dstbase + + (size_t) (draw_box->y0 + i) * scr->rowbytes + + (size_t) draw_box->x0 * bpp; + + p = bitmap__rle_decode_row(p, log2bpp, dstrow, skip, plot, 0); + } + + return result_OK; +} diff --git a/libraries/framebuf/screen/screen-copy-bitmap-rle.h b/libraries/framebuf/screen/screen-copy-bitmap-rle.h new file mode 100644 index 0000000..b176d26 --- /dev/null +++ b/libraries/framebuf/screen/screen-copy-bitmap-rle.h @@ -0,0 +1,35 @@ +/* framebuf/screen/screen-copy-bitmap-rle.h -- RLE bitmap blit (private) */ + +#ifndef FRAMEBUF_SCREEN_COPY_BITMAP_RLE_H +#define FRAMEBUF_SCREEN_COPY_BITMAP_RLE_H + +#include "base/result.h" + +#include "geom/box.h" + +#include "framebuf/bitmap.h" +#include "framebuf/screen.h" + +/** + * Blit an RLE-compressed bitmap onto a screen, top-left at (x, y), clipped + * to `draw_box` (already intersected with the screen clip). + * + * v1 constraints, else \ref result_NOT_SUPPORTED: - screen must be 32bpp + * (`pixelfmt_log2bpp(scr->format) == 5`); - `pixelfmt_base(src->format)` + * must equal `scr->format` (byte copy, no per-pixel conversion); - blit is + * alpha-tested (transparent runs skipped, opaque runs copied), not + * alpha-blended. + * + * \param[in] scr Destination screen. + * \param[in] x,y Where the bitmap's top-left lands, screen pixels. + * \param[in] src Compressed source bitmap. + * \param[in] draw_box Visible rectangle in screen pixels. + * \return \ref result_OK, or \ref result_NOT_SUPPORTED. + */ +result_t screen_copy_bitmap_rle(screen_t *scr, + int x, + int y, + const bitmap_t *src, + const box_t *draw_box); + +#endif /* FRAMEBUF_SCREEN_COPY_BITMAP_RLE_H */ diff --git a/libraries/framebuf/screen/screen-copy-ninepatch.c b/libraries/framebuf/screen/screen-copy-ninepatch.c index be1c6b7..668e432 100644 --- a/libraries/framebuf/screen/screen-copy-ninepatch.c +++ b/libraries/framebuf/screen/screen-copy-ninepatch.c @@ -91,6 +91,9 @@ result_t screen_copy_ninepatch(screen_t *scr, assert(src->size.w > 0 && src->size.w % 3 == 0); assert(src->size.h > 0 && src->size.h % 3 == 0); + if (pixelfmt_is_rle(src->format)) + return result_NOT_SUPPORTED; + if (box_is_empty(dst)) return result_OK; diff --git a/libraries/framebuf/screen/screen-draw.c b/libraries/framebuf/screen/screen-draw.c index 858a6b8..608ee6f 100644 --- a/libraries/framebuf/screen/screen-draw.c +++ b/libraries/framebuf/screen/screen-draw.c @@ -15,6 +15,8 @@ #include "framebuf/screen.h" +#include "screen-copy-bitmap-rle.h" + /* Number of pixels converted per span call in screen_copy_bitmap(). Bounds * the size of its stack scratch buffers so arbitrarily wide bitmaps don't * blow the stack (relevant on RISC OS). */ @@ -351,6 +353,9 @@ result_t screen_copy_bitmap(screen_t *scr, int x, int y, const bitmap_t *src) if (box_intersection(&clip_box, &src_box, &draw_box)) return result_OK; /* nothing visible */ + if (pixelfmt_is_rle(src->format)) + return screen_copy_bitmap_rle(scr, x, y, src, &draw_box); + /* Source pixels loaded from PNG are always laid out R,G,B,A/X byte order * (see bitmap_load_png()), the same layout colour_t::primary uses, so * source pixels can be read directly into a colour_t with no conversion. */ From 5237e91c819659cac9b335426cef906a6e6a7609 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Tue, 8 Sep 2026 22:14:24 +0100 Subject: [PATCH 55/98] feat(wuss): load a directory of PNGs into an RLE-compressed icon set wuss_icons_load(wuss, dir) scans a directory for *.png, loads and RLE-compresses each in place, and interns its leafname (sans .png) in an atom_set. wuss_icons_lookup(name) returns a 0-based index (atom values are not array indices, so a parallel array tracks entry position); wuss_icons_bitmap(index) returns the compressed bitmap. The set lives on wuss_t, is replaced on reload and freed by wuss_destroy. A wuss_ICON_TYPE_BITMAP spec with bitmap == NULL and icon_set = wuss_ICON_SET(idx) draws the loaded entry at idx; the macro offsets by one so a zero-initialised spec reads as "no entry". An out-of-range index is result_WUSS_BAD_INDEX. The RLE blit decodes into the screen's channel order with no per-pixel convert, so wuss_icons_load swaps R/B into screen order before compressing, and screen_copy_bitmap_rle now folds alpha out of its format check so a bgra8888 source blits onto a bgrx8888 screen (skip runs still carry the mask). The demo's icons task loads resources/wuss/icons and shows the four fixtures in an "Icon set" frame; the SDL frontend switches to the 32bpp screen so the compressed blit path is exercised. Co-Authored-By: Claude Sonnet 5 --- CMakeLists.txt | 1 + docs/windowing/wuss.md | 10 + include/wuss/icon.h | 69 +++- include/wuss/wuss.h | 2 + libraries/framebuf/bitmap/compress.c | 38 ++- .../framebuf/screen/screen-copy-bitmap-rle.c | 23 +- libraries/wuss/core/create.c | 4 + libraries/wuss/core/destroy.c | 4 + libraries/wuss/core/impl.h | 14 + libraries/wuss/icon.h | 5 + libraries/wuss/icon/from-spec.c | 20 +- libraries/wuss/icon/registry.c | 306 ++++++++++++++++++ libraries/wuss/test/tasks/icons.c | 68 +++- libraries/wuss/test/wuss-test.c | 81 +++++ resources/wuss/icons/optoff.png | Bin 0 -> 191 bytes resources/wuss/icons/opton.png | Bin 0 -> 446 bytes resources/wuss/icons/radoff.png | Bin 0 -> 697 bytes resources/wuss/icons/radon.png | Bin 0 -> 816 bytes .../wuss/{ => source}/ninepatch.aseprite | Bin 19 files changed, 620 insertions(+), 25 deletions(-) create mode 100644 libraries/wuss/icon/registry.c create mode 100755 resources/wuss/icons/optoff.png create mode 100755 resources/wuss/icons/opton.png create mode 100755 resources/wuss/icons/radoff.png create mode 100755 resources/wuss/icons/radon.png rename resources/wuss/{ => source}/ninepatch.aseprite (100%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 1adc285..68e87bb 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -432,6 +432,7 @@ set(WUSS_ICON_SOURCES libraries/wuss/icon/hit-test.c libraries/wuss/icon/invalidate.c libraries/wuss/icon/plot.c + libraries/wuss/icon/registry.c libraries/wuss/icon/screen-box.c libraries/wuss/icon/set-hidden.c libraries/wuss/icon/set-hover.c diff --git a/docs/windowing/wuss.md b/docs/windowing/wuss.md index 2bcdb1c..6261f54 100644 --- a/docs/windowing/wuss.md +++ b/docs/windowing/wuss.md @@ -218,6 +218,16 @@ Wuss draws icons in creation order (later icons paint on top); hit-testing scans The bevel's light (top/left) and dark (bottom/right) edge shades come from `config->bevel.light` / `config->bevel.dark` at `wuss_create` time, validated like the other furniture colours; both default to the titlebar fill colour when `config` is `NULL`. A held button also fills its face with `config->bevel.pressed` in place of the icon's own background; pass `wuss_NO_BACKGROUND` (or `NULL` config) to have it follow `bevel.dark`. +### Loaded icon set + +`wuss_icons_load(wuss, dir)` scans a directory for `*.png`, loads each one and RLE-compresses it in place (`bitmap_compress`), and records its leafname sans `.png`. Entries are indexed in the order the platform's directory scan yields — unspecified, so address them by name: + +- `wuss_icons_lookup(wuss, name)` → 0-based index, or `-1`. +- `wuss_icons_bitmap(wuss, index)` → the compressed `bitmap_t *` (window-manager-owned), or `NULL` out of range. +- `wuss_icons_count(wuss)`. + +A `wuss_ICON_TYPE_BITMAP` spec with `bitmap == NULL` and `icon_set = wuss_ICON_SET(idx)` draws the loaded entry at `idx` (the `wuss_ICON_SET` macro offsets by one so a zero-initialised spec means "no entry"). An out-of-range index is `result_WUSS_BAD_INDEX`. Calling `wuss_icons_load` again replaces the set; `wuss_destroy` frees it. A missing directory is not an error — it yields a zero-length set. `dir` is copied internally, so a `path_join_filename` result is safe to pass. + ## Components Built with the `WUSS_COMPONENTS` CMake option, `libraries/wuss/component/` holds small reusable task helpers layered on the core. Their headers are under `include/wuss/component/`. diff --git a/include/wuss/icon.h b/include/wuss/icon.h index 7694643..57ade2b 100644 --- a/include/wuss/icon.h +++ b/include/wuss/icon.h @@ -186,6 +186,13 @@ wuss_icon_flags_t; /** Decode the font slot (0..3) from an icon's flags. */ #define wuss_ICON_FONT_OF(f) (((f) >> 9) & 3) +/** + * Encode a 0-based icon-set index (from \ref wuss_icons_lookup) for + * wuss_icon_spec::icon_set. The stored value is offset by one so a + * zero-initialised spec reads as "no icon-set entry". + */ +#define wuss_ICON_SET(i) ((i) + 1) + /** * Description of an icon at creation. Copied by value into the icon; the * caller keeps ownership of \c text, which is copied. @@ -213,8 +220,15 @@ typedef struct wuss_icon_spec screen_pattern_t pattern; /** wuss_ICON_TYPE_BITMAP: the image to draw. Borrowed, not copied; must * outlive the icon. Ignored by other types; NULL (the default) is only valid - * when type is not wuss_ICON_TYPE_BITMAP. */ + * when type is not wuss_ICON_TYPE_BITMAP, or when \c icon_set selects a + * bitmap from the window manager's loaded icon set instead. */ const bitmap_t *bitmap; + /** wuss_ICON_TYPE_BITMAP: when \c bitmap is NULL, draw an entry from the + * window manager's loaded icon set (see \ref wuss_icons_load). Zero (the + * default for a zero-initialised spec) means "no icon-set entry"; encode a + * 0-based index from \ref wuss_icons_lookup with \ref wuss_ICON_SET. + * Ignored by other types and when \c bitmap is set. */ + int icon_set; /** wuss_ICON_TYPE_RADIO: exclusive-selection group. Selecting a radio clears * every other selected radio on the same window with the same group. Zero * (the default) means "no group": such a radio still toggles but never @@ -383,6 +397,59 @@ int wuss_icon_get_selected(const wuss_icon_t *icon); */ void wuss_icon_set_selected(wuss_icon_t *icon, int selected); +/* ----------------------------------------------------------------------- */ + +/** + * Scan a directory for PNG images and load them as the window manager's icon + * set. Each ".png" file becomes one indexed entry: its pixels are loaded and + * then RLE-compressed in place (see \ref bitmap_compress), and its leafname + * sans ".png" is recorded so \ref wuss_icons_lookup can resolve it back to + * the index. Non-".png" entries are ignored. Entry indices are assigned in + * the order the platform's directory scan yields, which is unspecified -- + * address entries by name, not by a hardcoded index. + * + * Calling this again replaces the previous set (the old bitmaps and names + * are freed). \ref wuss_destroy frees the set. + * + * \param[in] wuss Window manager. + * \param[in] dir Directory to scan for "*.png". + * \return \ref result_OK on success (including an empty or missing + * directory, which yields a zero-length set), \ref result_OOM on + * allocation failure, or a code propagated from the PNG loader or + * the compressor (the set is left empty on failure). + */ +result_t wuss_icons_load(wuss_t *wuss, const char *dir); + +/** + * Number of entries in the loaded icon set (see \ref wuss_icons_load). Zero + * if none has been loaded. + * + * \param[in] wuss Window manager. + * \return The entry count. + */ +int wuss_icons_count(const wuss_t *wuss); + +/** + * Resolve an icon-set entry's name to its index. + * + * \param[in] wuss Window manager. + * \param[in] name Entry name: a loaded file's leafname without ".png". + * \return The 0-based index, or -1 if no entry has that name. Pass the index + * through \ref wuss_ICON_SET to put it in a spec's \c icon_set. + */ +int wuss_icons_lookup(const wuss_t *wuss, const char *name); + +/** + * Fetch an icon-set entry's (compressed) bitmap by index. + * + * \param[in] wuss Window manager. + * \param[in] index 0-based index, 0..\ref wuss_icons_count -1. + * \return The bitmap, owned by the window manager and valid until the next + * \ref wuss_icons_load or \ref wuss_destroy, or NULL if \p index is + * out of range. + */ +const bitmap_t *wuss_icons_bitmap(const wuss_t *wuss, int index); + #endif /* WUSS_ICONS */ #ifdef __cplusplus diff --git a/include/wuss/wuss.h b/include/wuss/wuss.h index 53f2acd..836a0f8 100644 --- a/include/wuss/wuss.h +++ b/include/wuss/wuss.h @@ -36,6 +36,8 @@ extern "C" * colour). */ #define result_WUSS_BAD_ICON (result_BASE_WUSS + 2) +/** An icon-set index (see wuss_icons_load) was out of range. */ +#define result_WUSS_BAD_INDEX (result_BASE_WUSS + 3) /* ----------------------------------------------------------------------- */ diff --git a/libraries/framebuf/bitmap/compress.c b/libraries/framebuf/bitmap/compress.c index 32f8655..29c750a 100644 --- a/libraries/framebuf/bitmap/compress.c +++ b/libraries/framebuf/bitmap/compress.c @@ -6,6 +6,7 @@ #include #include +#include "base/debug.h" #include "base/result.h" #include "base/utils.h" #include "utils/barith.h" @@ -294,8 +295,8 @@ const uint8_t *bitmap__rle_decode_row(const uint8_t *p, uint8_t op; int id; int length; - int take; /* pixels of this run actually written */ - int drop; /* pixels of this run swallowed by `skip` */ + int take; /* pixels of this run actually written */ + int drop; /* pixels of this run swallowed by `skip` */ op = *p++; id = clz8(op); @@ -500,20 +501,20 @@ static int rle__format_ok(pixelfmt_t fmt, int *has_alpha) result_t bitmap_compress(bitmap_t *bm) { - result_t rc; - pixelfmt_t base; - int has_alpha; - int log2bpp; - int bpp; - int w, h; - size_t worst; - size_t rowworst; - uint8_t *blob; - uint8_t *cursor; + result_t rc; + pixelfmt_t base; + int has_alpha; + int log2bpp; + int bpp; + int w, h; + size_t worst; + size_t rowworst; + uint8_t *blob; + uint8_t *cursor; const uint8_t *srcrow; - uint32_t data_len; - uint8_t *shrunk; - int y; + uint32_t data_len; + uint8_t *shrunk; + int y; assert(bm); @@ -560,6 +561,13 @@ result_t bitmap_compress(bitmap_t *bm) } data_len = (uint32_t) (cursor - blob - bitmap__RLE_HEADER_SIZE); + + if ((size_t) bitmap__RLE_HEADER_SIZE + data_len >= (size_t) bm->rowbytes * h) + logf_warning("bitmap_compress: %ux%u RLE grew to %u bytes from %zu", + (unsigned) w, (unsigned) h, + (unsigned) (bitmap__RLE_HEADER_SIZE + data_len), + (size_t) bm->rowbytes * h); + bitmap__rle_put32(blob + 0, (uint32_t) bm->rowbytes); bitmap__rle_put32(blob + 4, data_len); diff --git a/libraries/framebuf/screen/screen-copy-bitmap-rle.c b/libraries/framebuf/screen/screen-copy-bitmap-rle.c index b1bb0c4..36efba6 100644 --- a/libraries/framebuf/screen/screen-copy-bitmap-rle.c +++ b/libraries/framebuf/screen/screen-copy-bitmap-rle.c @@ -17,6 +17,23 @@ #include "screen-copy-bitmap-rle.h" +/* Map a 32bpp 8888 format to its no-alpha ("x") sibling: bgra->bgrx, + * rgba->rgbx, etc. The RLE blit writes whole 4-byte pixels and honours skip + * runs for transparency, so an alpha source and its matching x screen (or the + * reverse) decode identically -- only the channel order has to agree. Passes + * non-8888 formats through unchanged. */ +static pixelfmt_t rle__dealpha(pixelfmt_t f) +{ + switch (f) + { + case pixelfmt_bgra8888: return pixelfmt_bgrx8888; + case pixelfmt_rgba8888: return pixelfmt_rgbx8888; + case pixelfmt_abgr8888: return pixelfmt_xbgr8888; + case pixelfmt_argb8888: return pixelfmt_xrgb8888; + default: return f; + } +} + result_t screen_copy_bitmap_rle(screen_t *scr, int x, int y, @@ -41,10 +58,12 @@ result_t screen_copy_bitmap_rle(screen_t *scr, base = pixelfmt_base(src->format); - /* v1: 32bpp screen, source decodes to the screen's exact byte layout. */ + /* v1: 32bpp screen, source decodes to the screen's channel order. An alpha + * source onto its matching x screen (or vice versa) is fine -- the decode + * is byte-identical -- so compare with alpha folded out. */ if (pixelfmt_log2bpp(scr->format) != 5) return result_NOT_SUPPORTED; - if (base != scr->format) + if (rle__dealpha(base) != rle__dealpha(scr->format)) return result_NOT_SUPPORTED; log2bpp = pixelfmt_log2bpp(base); diff --git a/libraries/wuss/core/create.c b/libraries/wuss/core/create.c index 36b1c86..b75fd87 100644 --- a/libraries/wuss/core/create.c +++ b/libraries/wuss/core/create.c @@ -330,6 +330,10 @@ result_t wuss_create(screen_t *scr, #ifdef WUSS_ICONS w->pressed_icon = NULL; w->hover_icon = NULL; + w->icon.names = NULL; + w->icon.atoms = NULL; + w->icon.bitmaps = NULL; + w->icon.nbitmaps = 0; #endif #ifdef WUSS_MENUS w->menu_chain = NULL; diff --git a/libraries/wuss/core/destroy.c b/libraries/wuss/core/destroy.c index 95dbf69..c555df1 100644 --- a/libraries/wuss/core/destroy.c +++ b/libraries/wuss/core/destroy.c @@ -89,6 +89,10 @@ void wuss_destroy(wuss_t *doomed) e = next; } +#ifdef WUSS_ICONS + wuss__icons_registry_free(doomed); +#endif + packer_destroy(doomed->layout); wuss__free(doomed, doomed->palette); wuss__free(doomed, doomed); /* reads doomed->alloc.free before freeing */ diff --git a/libraries/wuss/core/impl.h b/libraries/wuss/core/impl.h index ac1d8a4..30ebc4e 100644 --- a/libraries/wuss/core/impl.h +++ b/libraries/wuss/core/impl.h @@ -164,6 +164,20 @@ struct wuss * over, NULL when none; drives * hover-highlight repaint of * menu-entry icons */ + /* Icon set loaded by wuss_icons_load. index i is the i-th ".png" the + * directory scan yielded (order unspecified -- address by name). + * names interns the leafnames-sans-".png"; atoms[i] is entry i's atom + * in it (atom values are not array indices, so the position is tracked + * explicitly); bitmaps[i] is the matching RLE-compressed bitmap. All + * owned; freed by wuss__icons_registry_free. NULL / 0 until first load. */ + struct + { + struct atom_set *names; + int *atoms; + bitmap_t *bitmaps; + int nbitmaps; + } + icon; #endif #ifdef WUSS_MENUS struct wuss__menu *menu_chain; /* head (root) of the open pop-up diff --git a/libraries/wuss/icon.h b/libraries/wuss/icon.h index d4fe3c8..b553eb3 100644 --- a/libraries/wuss/icon.h +++ b/libraries/wuss/icon.h @@ -114,4 +114,9 @@ wuss_icon_t *wuss__icon_hit_test(wuss_window_t *window, point_t doc_point); * not invalidate or swap-remove. */ void wuss__icons_free(wuss_window_t *window); +/* Free the wuss-wide icon set loaded by wuss_icons_load (the atom_set and the + * compressed bitmaps), leaving the fields NULL / 0. Safe on an unloaded set. + * Called by wuss_icons_load before a reload and by wuss_destroy. */ +void wuss__icons_registry_free(wuss_t *wuss); + #endif /* WUSS_ICON_IMPL_H */ diff --git a/libraries/wuss/icon/from-spec.c b/libraries/wuss/icon/from-spec.c index f5aa915..e1e0852 100644 --- a/libraries/wuss/icon/from-spec.c +++ b/libraries/wuss/icon/from-spec.c @@ -12,8 +12,9 @@ result_t wuss__icon_from_spec(const wuss_t *w, const wuss_icon_spec_t *spec, wuss_icon_t *out) { - wuss_colour_t fg, bg, swatch; - int has_swatch; + wuss_colour_t fg, bg, swatch; + int has_swatch; + const bitmap_t *bitmap; assert(w != NULL); assert(spec != NULL); @@ -49,7 +50,18 @@ result_t wuss__icon_from_spec(const wuss_t *w, if (spec->type == wuss_ICON_TYPE_PATTERN && bg == wuss_NO_BACKGROUND) return result_WUSS_BAD_ICON; - if (spec->type == wuss_ICON_TYPE_BITMAP && spec->bitmap == NULL) + /* A BITMAP icon draws spec->bitmap, or -- when that is NULL -- the loaded + * icon-set entry spec->icon_set names (wuss_ICON_SET encodes the 0-based + * index +1, so 0 means "no entry"). */ + bitmap = spec->bitmap; + if (spec->type == wuss_ICON_TYPE_BITMAP && bitmap == NULL && spec->icon_set > 0) + { + bitmap = wuss_icons_bitmap(w, spec->icon_set - 1); + if (bitmap == NULL) + return result_WUSS_BAD_INDEX; + } + + if (spec->type == wuss_ICON_TYPE_BITMAP && bitmap == NULL) return result_WUSS_BAD_ICON; if (spec->type == wuss_ICON_TYPE_PATTERN && @@ -77,7 +89,7 @@ result_t wuss__icon_from_spec(const wuss_t *w, out->bg = bg; out->pattern = (spec->type == wuss_ICON_TYPE_PATTERN) ? spec->pattern : screen_PATTERN_SOLID; - out->bitmap = (spec->type == wuss_ICON_TYPE_BITMAP) ? spec->bitmap : NULL; + out->bitmap = (spec->type == wuss_ICON_TYPE_BITMAP) ? bitmap : NULL; out->group = (spec->type == wuss_ICON_TYPE_RADIO) ? spec->group : 0; out->swatch = swatch; out->border = (spec->type == wuss_ICON_TYPE_LABEL) ? spec->border diff --git a/libraries/wuss/icon/registry.c b/libraries/wuss/icon/registry.c new file mode 100644 index 0000000..341d9f8 --- /dev/null +++ b/libraries/wuss/icon/registry.c @@ -0,0 +1,306 @@ +/* wuss/icon/registry.c -- wuss-wide icon set loaded from a directory of PNGs */ + +#include +#include +#include +#include + +#ifdef FORTIFY +#include "fortify/fortify.h" +#endif + +#include "base/result.h" +#include "datastruct/atom.h" +#include "framebuf/bitmap.h" +#include "framebuf/pixelfmt.h" +#include "io/dirscan.h" +#include "io/path.h" + +/* path_join_filename returns a single static buffer, so the per-entry join in + * the callback would clobber a caller's dir pointer if we held it directly -- + * keep our own copy. Bounded by that buffer's own DPTLIB_MAXPATH. */ +#define ICONS_DIR_MAX 256 + +#include "../core/impl.h" + +#ifdef WUSS_ICONS + +/* State threaded through the dirscan callback while a set is being built. The + * three arrays grow in lockstep via wuss__array_grow. */ +typedef struct icons_load +{ + wuss_t *wuss; + char dir[ICONS_DIR_MAX]; /* own copy; see ICONS_DIR_MAX */ + int *atoms; /* owned; atoms[i] is entry i's atom in wuss->icon.names */ + bitmap_t *bitmaps; /* owned */ + int n; + int cap; /* shared allocation length of atoms[] and bitmaps[] */ + result_t rc; /* first failure; stops the walk */ +} +icons_load_t; + +/* True when "leaf" ends in ".png" (case-sensitive, matching the fixtures). + * Writes the leaf sans that suffix into "name" (capacity "cap"). */ +static int png_leaf(const char *leaf, char *name, size_t cap) +{ + size_t len; + + len = strlen(leaf); + if (len < 4 || len - 4 >= cap) + return 0; + if (strcmp(leaf + len - 4, ".png") != 0) + return 0; + + memcpy(name, leaf, len - 4); + name[len - 4] = '\0'; + return 1; +} + +/* The RLE blit (screen_copy_bitmap_rle) decodes straight into the screen's + * byte layout -- no per-pixel format convert -- so a compressed icon must + * already be in screen order. bitmap_load_png yields rgb(x|a)8888; the screen + * is usually bgr(x|a)8888. Swap R and B in place (byte 0 <-> byte 2; the + * alpha/pad byte is untouched, so this maps rgbx<->bgrx and rgba<->bgra) and + * relabel the format. Bitmap must be uncompressed and 32bpp. + * ponytail: lives here until a second caller needs rgb<->bgr; then fold into + * bitmap_convert. */ +static void icons_swap_rb(bitmap_t *bm) +{ + unsigned char *row; + int x, y; + + assert(!pixelfmt_is_rle(bm->format)); + assert(pixelfmt_log2bpp(bm->format) == 5); + + for (y = 0; y < bm->size.h; y++) + { + row = (unsigned char *) bm->base + (size_t) y * bm->rowbytes; + for (x = 0; x < bm->size.w; x++) + { + unsigned char t; + + t = row[x * 4 + 0]; + row[x * 4 + 0] = row[x * 4 + 2]; + row[x * 4 + 2] = t; + } + } + + switch (bm->format) + { + case pixelfmt_rgbx8888: bm->format = pixelfmt_bgrx8888; break; + case pixelfmt_rgba8888: bm->format = pixelfmt_bgra8888; break; + case pixelfmt_bgrx8888: bm->format = pixelfmt_rgbx8888; break; + case pixelfmt_bgra8888: bm->format = pixelfmt_rgba8888; break; + default: break; + } +} + +/* True when "a" and "b" are 32bpp RGB(A/X) formats of opposite R/B order. */ +static int icons_rb_swapped(pixelfmt_t a, pixelfmt_t b) +{ + return (a == pixelfmt_rgbx8888 && b == pixelfmt_bgrx8888) || + (a == pixelfmt_bgrx8888 && b == pixelfmt_rgbx8888) || + (a == pixelfmt_rgba8888 && b == pixelfmt_bgra8888) || + (a == pixelfmt_bgra8888 && b == pixelfmt_rgba8888) || + (a == pixelfmt_rgbx8888 && b == pixelfmt_bgra8888) || + (a == pixelfmt_rgba8888 && b == pixelfmt_bgrx8888); +} + +static result_t icons_load_entry(const char *leaf, void *opaque) +{ + result_t rc; + icons_load_t *st = opaque; + char name[128]; + const char *path; + bitmap_t bm; + atom_t atom; + int oldcap; + + if (!png_leaf(leaf, name, sizeof(name))) + return result_OK; /* not a ".png", or name too long -- skip */ + + path = path_join_filename(st->dir, 1, leaf); + + rc = bitmap_load_png(&bm, path); + if (rc != result_OK) + { + st->rc = rc; + return result_STOP_WALK; + } + + /* bring the pixels into screen byte order before compressing -- see + * icons_swap_rb. Only the R/B-swapped 32bpp case is handled; a matching + * format needs nothing, anything else is left for the blit to reject. */ + if (pixelfmt_log2bpp(bm.format) == 5 && + icons_rb_swapped(bm.format, st->wuss->scr->format)) + icons_swap_rb(&bm); + + rc = bitmap_compress(&bm); + if (rc != result_OK) + { + free(bm.base); + st->rc = rc; + return result_STOP_WALK; + } + + /* Intern the name. atom_new returns result_ATOM_NAME_EXISTS (with the + * existing atom) for a repeat -- a directory scan can't repeat a leaf, so + * treat only a hard error as failure. */ + rc = atom_new(st->wuss->icon.names, + (const unsigned char *) name, strlen(name) + 1, &atom); + if (rc != result_OK && rc != result_ATOM_NAME_EXISTS) + { + free(bm.base); + st->rc = rc; + return result_STOP_WALK; + } + + oldcap = st->cap; + if (wuss__array_grow(&st->wuss->alloc, (void **) &st->bitmaps, + sizeof(*st->bitmaps), st->n, &st->cap, 1, 8) != 0) + { + free(bm.base); + st->rc = result_OOM; + return result_STOP_WALK; + } + if (st->cap != oldcap) /* bitmaps[] grew; bring atoms[] to the same length */ + { + int *grown; + + grown = wuss__realloc(st->wuss, st->atoms, + (size_t) st->cap * sizeof(*st->atoms)); + if (grown == NULL) + { + free(bm.base); + st->rc = result_OOM; + return result_STOP_WALK; + } + st->atoms = grown; + } + + st->atoms[st->n] = (int) atom; + st->bitmaps[st->n++] = bm; + return result_OK; +} + +void wuss__icons_registry_free(wuss_t *wuss) +{ + int i; + + assert(wuss != NULL); + + for (i = 0; i < wuss->icon.nbitmaps; i++) + free(wuss->icon.bitmaps[i].base); + + wuss__free(wuss, wuss->icon.bitmaps); + wuss__free(wuss, wuss->icon.atoms); + wuss->icon.bitmaps = NULL; + wuss->icon.atoms = NULL; + wuss->icon.nbitmaps = 0; + + if (wuss->icon.names != NULL) + { + atom_destroy(wuss->icon.names); + wuss->icon.names = NULL; + } +} + +result_t wuss_icons_load(wuss_t *wuss, const char *dir) +{ + result_t rc; + icons_load_t st; + int i; + + assert(wuss != NULL); + + if (dir == NULL) + return result_NULL_ARG; + + wuss__icons_registry_free(wuss); + + wuss->icon.names = atom_create(); + if (wuss->icon.names == NULL) + return result_OOM; + + if (strlen(dir) >= sizeof(st.dir)) + { + atom_destroy(wuss->icon.names); + wuss->icon.names = NULL; + return result_BAD_ARG; + } + + st.wuss = wuss; + strcpy(st.dir, dir); + st.atoms = NULL; + st.bitmaps = NULL; + st.n = 0; + st.cap = 0; + st.rc = result_OK; + + rc = dirscan_walk(dir, icons_load_entry, &st); + + /* A missing directory just yields an empty set. */ + if (rc == result_FILE_NOT_FOUND) + rc = result_OK; + + if (rc == result_OK && st.rc != result_OK) + rc = st.rc; + + if (rc != result_OK) + { + for (i = 0; i < st.n; i++) + free(st.bitmaps[i].base); + wuss__free(wuss, st.bitmaps); + wuss__free(wuss, st.atoms); + atom_destroy(wuss->icon.names); + wuss->icon.names = NULL; + return rc; + } + + wuss->icon.atoms = st.atoms; + wuss->icon.bitmaps = st.bitmaps; + wuss->icon.nbitmaps = st.n; + return result_OK; +} + +int wuss_icons_count(const wuss_t *wuss) +{ + assert(wuss != NULL); + return wuss->icon.nbitmaps; +} + +int wuss_icons_lookup(const wuss_t *wuss, const char *name) +{ + atom_t atom; + int i; + + assert(wuss != NULL); + + if (name == NULL || wuss->icon.names == NULL) + return -1; + + atom = atom_for_block((atom_set_t *) wuss->icon.names, + (const unsigned char *) name, strlen(name) + 1); + if (atom == atom_NOT_FOUND) + return -1; + + /* atom values are not array indices -- find which entry holds it. A handful + * of icons, so a linear scan is cheaper than a second index structure. */ + for (i = 0; i < wuss->icon.nbitmaps; i++) + if (wuss->icon.atoms[i] == (int) atom) + return i; + + return -1; +} + +const bitmap_t *wuss_icons_bitmap(const wuss_t *wuss, int index) +{ + assert(wuss != NULL); + + if (index < 0 || index >= wuss->icon.nbitmaps) + return NULL; + + return &wuss->icon.bitmaps[index]; +} + +#endif /* WUSS_ICONS */ diff --git a/libraries/wuss/test/tasks/icons.c b/libraries/wuss/test/tasks/icons.c index 680681d..cb67fd2 100644 --- a/libraries/wuss/test/tasks/icons.c +++ b/libraries/wuss/test/tasks/icons.c @@ -23,7 +23,7 @@ #include "icons.h" #define ICONS_DOC_W 260 -#define ICONS_DOC_H 1100 /* taller than the window, so scrolling is exercised */ +#define ICONS_DOC_H 1160 /* taller than the window, so scrolling is exercised */ #define ICONS_MARGIN 28 /* left edge of everything except frame captions */ #define ICONS_ROW 20 /* vertical pitch between stacked simple icons */ @@ -37,12 +37,13 @@ enum ICONS_N_BUTTONS = 4, /* frame + normal + default + disabled button */ ICONS_N_RADIOS = 8, /* frame + 3 radios + option + state label + 2 justified labels */ ICONS_N_BITMAPS = 3, /* frame + decorative + interactive bitmap */ + ICONS_N_ICONSET = 5, /* frame + opton + optoff + radon + radoff from the loaded set */ ICONS_N_PATTERN = 2, /* frame + one PATTERN swatch */ ICONS_N_BORDERS = 6, /* frame + GROOVE + RIDGE + ACTION + DIVIDER labels */ ICONS_N_MENU = 7, /* plain, ticked, swatch, submenu, disabled, rule, separator entry */ ICONS_NSPECS = ICONS_N_INTRO + ICONS_N_BUTTONS + ICONS_N_RADIOS + - ICONS_N_BITMAPS + ICONS_N_PATTERN + ICONS_N_BORDERS + - ICONS_N_MENU + ICONS_N_BITMAPS + ICONS_N_ICONSET + ICONS_N_PATTERN + + ICONS_N_BORDERS + ICONS_N_MENU }; /* Running state threaded through the icons_add_* helpers: where to write the @@ -269,6 +270,54 @@ static void icons_add_bitmaps(icons_layout_t *lay, lay->y = top + sprite->size.h + 50; } +/* A grouping frame captioned "Icon set", holding the four fixtures loaded by + * wuss_icons_load -- opton/optoff/radon/radoff -- each drawn as a BITMAP icon + * that references the loaded set by index via wuss_ICON_SET. Laid out only + * when the set loaded (all four looked up); skipped otherwise. */ +static void icons_add_iconset(icons_layout_t *lay, const wuss_t *wuss) +{ + static const char *const names[4] = { "opton", "optoff", "radon", "radoff" }; + wuss_icon_spec_t *s; + int top; + int idx[4]; + int i; + + for (i = 0; i < 4; i++) + { + idx[i] = wuss_icons_lookup(wuss, names[i]); + if (idx[i] < 0) + return; + } + + top = lay->y; + s = &lay->specs[lay->n]; + s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN, top, 200, 44); + s->type = wuss_ICON_TYPE_FRAME; + s->text = "Icon set"; + s->fg = lay->black; + s->bg = wuss_NO_BACKGROUND; + lay->n++; + + for (i = 0; i < 4; i++) + { + const bitmap_t *bm; + int w, h; + + bm = wuss_icons_bitmap(wuss, idx[i]); + w = bm ? bm->size.w : 16; + h = bm ? bm->size.h : 16; + + s = &lay->specs[lay->n]; + s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN + 10 + i * (w + 6), + top + 20, w, h); + s->type = wuss_ICON_TYPE_BITMAP; + s->icon_set = wuss_ICON_SET(idx[i]); + lay->n++; + } + + lay->y = top + 60; +} + /* A grouping frame captioned "Pattern", holding one PATTERN-filled swatch. */ static void icons_add_pattern(icons_layout_t *lay) { @@ -467,6 +516,18 @@ result_t icons_create(wuss_t *wuss, if (bitmap_load_png(&task->sprite, sprite_path) == result_OK) task->has_sprite = 1; + /* load the wuss-wide icon set; path_join_filename hands back one shared + * static buffer and wuss_icons_load re-joins per file, so copy it first */ + { + char icons_dir[256]; + + strncpy(icons_dir, + path_join_filename(resources, 3, "resources", "wuss", "icons"), + sizeof(icons_dir) - 1); + icons_dir[sizeof(icons_dir) - 1] = '\0'; + (void) wuss_icons_load(wuss, icons_dir); /* absent set just skips the group */ + } + delegate_desc.handle = icons_handle; delegate_desc.task_data = task; delegate_desc.name = "icons"; @@ -515,6 +576,7 @@ result_t icons_create(wuss_t *wuss, icons_add_buttons(&lay); icons_add_radios(&lay, &i_opt, &i_state); icons_add_bitmaps(&lay, task->has_sprite ? &task->sprite : NULL, &i_hotspot); + icons_add_iconset(&lay, wuss); icons_add_pattern(&lay); icons_add_borders(&lay); icons_add_menu(&lay, &i_ticked); diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 7fb1857..b68fee4 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -4977,6 +4977,87 @@ ColourMenuOK: ; } } + printf("test: wuss_icons_load scans resources/wuss/icons and compresses\n"); + + { + char icons_dir[256]; + const bitmap_t *icon_bm; + int idx, opton; + + /* copy: path_join_filename hands back one shared static buffer, and + * wuss_icons_load's own per-file joins would clobber it mid-call */ + strncpy(icons_dir, + path_join_filename(resources, 3, "resources", "wuss", "icons"), + sizeof(icons_dir) - 1); + icons_dir[sizeof(icons_dir) - 1] = '\0'; + + rc = wuss_icons_load(wuss, icons_dir); + if (rc != result_OK) + goto Failure; + + /* the four fixtures: optoff/opton/radoff/radon */ + if (wuss_icons_count(wuss) != 4) + goto Failure; + + opton = wuss_icons_lookup(wuss, "opton"); + if (opton < 0 || wuss_icons_lookup(wuss, "radoff") < 0) + goto Failure; + if (wuss_icons_lookup(wuss, "nonesuch") != -1) + goto Failure; + + icon_bm = wuss_icons_bitmap(wuss, opton); + if (icon_bm == NULL || !bitmap_is_compressed(icon_bm)) + goto Failure; + if (wuss_icons_bitmap(wuss, 4) != NULL) + goto Failure; + + /* an icon spec picks it up by index via wuss_ICON_SET */ + { + static test_task_t tc_ic; + wuss_task_t *delegate_ic; + wuss_window_t *win_ic; + wuss_icon_t *icon; + wuss_icon_spec_t spec; + box_t box_ic; + + memset(&tc_ic, 0, sizeof(tc_ic)); + delegate_ic = mk_task(wuss, test_handle, &tc_ic); + if (delegate_ic == NULL) goto Failure; + + box_ic.x0 = 5; box_ic.y0 = 5; box_ic.x1 = 125; box_ic.y1 = 105; + rc = wuss_window_create(delegate_ic, &box_ic, "IC", wuss_WINDOW_DEFAULT, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + SIZE2D(400, 400), SIZE2D(0, 0), &win_ic); + if (rc != result_OK) + goto Failure; + + memset(&spec, 0, sizeof(spec)); + spec.bbox = (box_t) BOX_POS_SIZE(0, 0, 16, 16); + spec.type = wuss_ICON_TYPE_BITMAP; + spec.icon_set = wuss_ICON_SET(opton); + rc = wuss_icon_create(win_ic, &spec, &icon); + if (rc != result_OK) + goto Failure; + + /* a bogus index is rejected */ + spec.icon_set = wuss_ICON_SET(99); + if (wuss_icon_create(win_ic, &spec, &icon) != result_WUSS_BAD_INDEX) + goto Failure; + rc = result_OK; + + wuss_window_close(win_ic); + } + + /* a reload replaces the set cleanly (no leak, ASan would catch it) */ + rc = wuss_icons_load(wuss, icons_dir); + if (rc != result_OK || wuss_icons_count(wuss) != 4) + goto Failure; + + idx = wuss_icons_lookup(wuss, "optoff"); + if (idx < 0) + goto Failure; + } + wuss_destroy(wuss); free(pixels); diff --git a/resources/wuss/icons/optoff.png b/resources/wuss/icons/optoff.png new file mode 100755 index 0000000000000000000000000000000000000000..e1768e5250faac8ffd2ee9582ae1e6a8375869d8 GIT binary patch literal 191 zcmeAS@N?(olHy`uVBq!ia0vp^Vj#@H1SGw${mubWjKx9jP7LeL$-D$|Dm`5sLo9ml zPBG*=puoc{eB}TBNxW>IB;y|OOh?#*#zPU(_)19Sv~r>mdKI;Vst0MU*{D*ylh literal 0 HcmV?d00001 diff --git a/resources/wuss/icons/opton.png b/resources/wuss/icons/opton.png new file mode 100755 index 0000000000000000000000000000000000000000..373b9e66c8f47734e9fde50ecf8c409e35ea6c44 GIT binary patch literal 446 zcmV;v0YUzWP)Px$cu7P-R5*>LRI#eUKoFeMgA|IOSOz48m1Sxn7Lj!RK+*(3I7lNX_9FNT66`Fs z*Fr?_8?=ap_yK~AB01l?hHG-cSoAdCLx4#jt~eT;X?qN^H<8UED9i> z&%e_mtHog)z2Tt%4FU z`J8jxwgEI6jf4`j*$hCx-wzv)HH*XHFr7{-l}fo>4rhON;ro8C*8|XQw<8OFOH3vc z0IgOllgT8J*zI4V*uIt|K zcL3w@_;$N#nx-gPx%b4f%&R5*>5lrc{fK^TVL+1oq91q>G;1_~+*pn!r}yGyLZMy+g2kV+eV0UbYq zl7iS=uAoy2gR!uH#9%?NkibPZ%7)q59d>7KzMUBhJpu$2o@9#2-GA6yWO6@ zP?7h3!h8RXF%~MNaB^}2YwcCLhI0-{l0ZrcDW!;_XmW9J@mKJ7@hyN*DTU+X<7*`V z5Cj2+hlc?GL^Q-0TLXX!5iKyrURY}p$1%M3*GkS)uh(&ScnGcay}7x$&p}z1kFB+k zQvS1&%hYN$jE;^X&vOt_Gjz^PF~(qwxv`|jz`y{clu$~|hTi+}vMfPFw-z-T4G1A{ zdU`q?5|PKB&fQw%oP!VoN+};`t&fZ`V2s@$dXY}2gEURSIo}QPJl{3O-0Glv?CtF# zj$;TRc7mcPHjFXQTEjVaV@VvxXti2M(-c{jZ88AZ-roKYhT#L}+}UOOA4^M1*xA_u z5v@m2^gILrA;h!({{F8-bVo{wv9U4q_4Qp_$;!$KwzjsQl=@z+R$t-#>uPx%>PbXFR5*==lut`*M;OL`Gjrzr_2gudm`WDGf)GJ4n|h!~DR||!n|7madKYas ziFBJR-1!B}rrl`#1T9_kc2&VIU|bo*bfbiFTN_05LKYfpP8Yofv1xtaT?{<$kN16M z7{M6L%*?!qqUdENllj0HQ%+WrCZvOnVM?5}6QDlN3_|6#f${2$& zhD;_y6h)(&n3zB*wKp6NzpPfPy>Xxq!!Z0YlgShcg#v49Ys}8hQY;pcQgU^5MZe$Y z;^G3&^RR9EdN3G#wX(AEdIXB1$P9zX=#a#jSX_S+_h*;&%*bduB4)1=*Q ze<6b)c$rG2SY2IZa&nS`g993k#@MxzB%#r0&}y}iQsTNUj^p6_{^v5E&oAWjc}k@c z0L^A|EaWll!K0MIwrzahFIzzn%!gq}u~-BcW%7S|@VpBgt@X5(&1Qw?c}OXr1w54y z00a(@9$5g)9D|KF(28sjcwbbKrPF{vaE+mJ5tI|g%H1u@*6k)@$vC1&+|U> uJg@Bg{ Date: Tue, 8 Sep 2026 22:48:42 +0100 Subject: [PATCH 56/98] refactor(wuss): lay out icons Buttons demo as a 2x2 grid Show Default, Normal, Default+Disabled and Normal+Disabled in a two-by-two grid rather than three buttons in a row. Column 0 is the DEFAULT accent button (inset 4px to seat its action surround), column 1 the plain one; the bottom row adds DISABLED. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/test/tasks/icons.c | 74 ++++++++++++++++++------------- 1 file changed, 42 insertions(+), 32 deletions(-) diff --git a/libraries/wuss/test/tasks/icons.c b/libraries/wuss/test/tasks/icons.c index cb67fd2..1cc2f04 100644 --- a/libraries/wuss/test/tasks/icons.c +++ b/libraries/wuss/test/tasks/icons.c @@ -34,7 +34,7 @@ enum { ICONS_N_INTRO = 4, /* heading, counter button, counter label, scrolled-away button */ - ICONS_N_BUTTONS = 4, /* frame + normal + default + disabled button */ + ICONS_N_BUTTONS = 5, /* frame + 2x2 grid: default/normal x plain/disabled */ ICONS_N_RADIOS = 8, /* frame + 3 radios + option + state label + 2 justified labels */ ICONS_N_BITMAPS = 3, /* frame + decorative + interactive bitmap */ ICONS_N_ICONSET = 5, /* frame + opton + optoff + radon + radoff from the loaded set */ @@ -110,51 +110,61 @@ static void icons_add_intro(icons_layout_t *lay, int *button, int *counter) lay->n++; } -/* A grouping frame captioned "Buttons", with a normal, a default (accent) and - * a disabled button side by side inside it. */ +/* A grouping frame captioned "Buttons", holding a 2x2 grid: column 0 is the + * DEFAULT (accent) button, column 1 the plain one; the bottom row adds + * DISABLED. The DEFAULT column is inset 4px on every side to seat the accent + * icon's 6px action surround. */ static void icons_add_buttons(icons_layout_t *lay) { wuss_icon_spec_t *s; int top; + int col; + int row; + int x; + int y; top = lay->y; s = &lay->specs[lay->n]; - s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN, top, 200, 56); + s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN, top, 200, 90); s->type = wuss_ICON_TYPE_FRAME; s->text = "Buttons"; s->fg = lay->black; s->bg = wuss_NO_BACKGROUND; lay->n++; - s = &lay->specs[lay->n]; - s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN + 10, top + 22, 56, 22); - s->type = wuss_ICON_TYPE_BUTTON; - s->text = "Normal"; - s->fg = lay->black; - s->bg = lay->window; - lay->n++; - - s = &lay->specs[lay->n]; - /* 4px larger on every side than the plain buttons beside it to seat the - * DEFAULT icon's 6px action surround */ - s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN + 70, top + 18, 64, 30); - s->type = wuss_ICON_TYPE_BUTTON; - s->text = "Default"; - s->fg = lay->black; - s->bg = lay->window; - s->flags = wuss_ICON_FLAGS_DEFAULT; - lay->n++; - - s = &lay->specs[lay->n]; - s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN + 138, top + 22, 56, 22); - s->type = wuss_ICON_TYPE_BUTTON; - s->text = "Disabled"; - s->fg = lay->black; - s->bg = lay->window; - s->flags = wuss_ICON_FLAGS_DISABLED; - lay->n++; + for (row = 0; row < 2; row++) + { + for (col = 0; col < 2; col++) + { + x = ICONS_MARGIN + 10 + col * 100; + y = top + 22 + row * 34; + + s = &lay->specs[lay->n]; + s->fg = lay->black; + s->bg = lay->window; + s->type = wuss_ICON_TYPE_BUTTON; + + if (col == 0) + { + s->bbox = (box_t) BOX_POS_SIZE(x - 4, y - 4, 64, 30); + s->text = "Default"; + s->flags = wuss_ICON_FLAGS_DEFAULT; + } + else + { + s->bbox = (box_t) BOX_POS_SIZE(x, y, 56, 22); + s->text = "Normal"; + s->flags = 0; + } + + if (row == 1) + s->flags |= wuss_ICON_FLAGS_DISABLED; + + lay->n++; + } + } - lay->y = top + 70; + lay->y = top + 104; } /* A grouping frame captioned "Radios & options", with two justified labels, From 9a9a5d1e2c2acfefe0e8a46ce85c6482ebd39f07 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Tue, 8 Sep 2026 22:54:23 +0100 Subject: [PATCH 57/98] feat(framebuf): render 32bpp RLE bitmaps onto 4bpp paletted screens screen_copy_bitmap_rle() now dispatches to a p4 path when the screen is 4bpp. The row codec only emits whole 4-byte pixels, so each visible row is decoded into a heap 32bpp scratch (skip runs zero-filled) then converted pixel-by-pixel to the nearest palette entry, matching screen_copy_bitmap_p4's alpha-tested transfer: zero-alpha pixels (skip runs and genuinely transparent source) leave the background alone. Still 32bpp source only; a non-32bpp RLE source onto a 4bpp screen returns result_NOT_SUPPORTED. An opaque 0x00000000 source pixel (only reachable for the no-alpha *x8888 bases) drops out, same trade-off as the raw p4 blit. The rle blit test now also blits raw-vs-compressed onto a 4bpp screen and asserts the results are byte-identical. Co-Authored-By: Claude Sonnet 5 --- libraries/framebuf/bitmap/test/rle-test.c | 46 +++++++++ .../framebuf/screen/screen-copy-bitmap-rle.c | 94 +++++++++++++++++++ .../framebuf/screen/screen-copy-bitmap-rle.h | 11 ++- 3 files changed, 146 insertions(+), 5 deletions(-) diff --git a/libraries/framebuf/bitmap/test/rle-test.c b/libraries/framebuf/bitmap/test/rle-test.c index a72081c..dce195b 100644 --- a/libraries/framebuf/bitmap/test/rle-test.c +++ b/libraries/framebuf/bitmap/test/rle-test.c @@ -251,6 +251,52 @@ static int test_blit(const char *resources) } } + /* --- 4bpp screen: RLE path must match the raw p4 blit --- */ + { + colour_t pal[16]; + uint8_t *p4_ref; + uint8_t *p4_rle; + int p4_rowbytes; + int i; + + /* A spread of greys is enough for colour_to_pixel's nearest match to + * exercise more than one entry. */ + for (i = 0; i < 16; i++) + { + unsigned int g = (unsigned int) (i * 17); + pal[i].primary = 0xFF000000u | (g << 16) | (g << 8) | g; + } + + p4_rowbytes = (w + 1) / 2; + p4_ref = malloc((size_t) p4_rowbytes * h); + p4_rle = malloc((size_t) p4_rowbytes * h); + memset(p4_ref, 0, (size_t) p4_rowbytes * h); + memset(p4_rle, 0, (size_t) p4_rowbytes * h); + + rc = bitmap_decompress(&cbm); + if (rc) { fprintf(stderr, "p4 decompress rc=&%x\n", rc); ok = 0; } + + screen_init(&s, bm.size, pixelfmt_p4, p4_rowbytes, pal, p4_ref); + rc = screen_copy_bitmap(&s, 0, 0, &cbm); + if (rc) { fprintf(stderr, "p4 raw blit rc=&%x\n", rc); ok = 0; } + + rc = bitmap_compress(&cbm); + if (rc) { fprintf(stderr, "p4 compress rc=&%x\n", rc); ok = 0; } + + screen_init(&s, bm.size, pixelfmt_p4, p4_rowbytes, pal, p4_rle); + rc = screen_copy_bitmap(&s, 0, 0, &cbm); + if (rc) { fprintf(stderr, "p4 rle blit rc=&%x\n", rc); ok = 0; } + + if (ok && memcmp(p4_ref, p4_rle, (size_t) p4_rowbytes * h) != 0) + { + fprintf(stderr, "blit: 4bpp RLE vs raw mismatch\n"); + ok = 0; + } + + free(p4_ref); + free(p4_rle); + } + done2: free(scr_ref); free(scr_rle); diff --git a/libraries/framebuf/screen/screen-copy-bitmap-rle.c b/libraries/framebuf/screen/screen-copy-bitmap-rle.c index 36efba6..13f95cf 100644 --- a/libraries/framebuf/screen/screen-copy-bitmap-rle.c +++ b/libraries/framebuf/screen/screen-copy-bitmap-rle.c @@ -3,11 +3,13 @@ #include #include #include +#include #include "base/result.h" #include "geom/box.h" +#include "framebuf/colour.h" #include "framebuf/pixelfmt.h" #include "framebuf/bitmap.h" @@ -34,6 +36,95 @@ static pixelfmt_t rle__dealpha(pixelfmt_t f) } } +/* Blit a 32bpp RLE source onto a 4bpp paletted screen. The row codec only + * emits whole 4-byte pixels, so each visible row is decoded (skip runs + * zero-filled) into a scratch buffer, then converted pixel-by-pixel to the + * nearest palette index -- matching screen_copy_bitmap_p4's alpha-tested + * transfer. Zero-alpha pixels (skip runs, and genuinely transparent source + * pixels) leave the background alone. + * + * ponytail: an opaque source with a 0x00000000 pixel (only possible for the + * no-alpha *x8888 bases) loses that pixel, same trade-off as the raw p4 + * blit. Carry an explicit mask if opaque-black art shows up. */ +static result_t screen_copy_bitmap_rle_p4(screen_t *scr, + int x, + int y, + const bitmap_t *src, + const box_t *draw_box) +{ + pixelfmt_t base; + int log2bpp; + const uint8_t *blob; + const uint8_t *p; + const uint8_t *end; + uint8_t *dstbase; + pixelfmt_rgba8888_t *scratch; + int firstrow; + int skip, plot; + int rows; + int i; + + base = pixelfmt_base(src->format); + log2bpp = pixelfmt_log2bpp(base); + + if (log2bpp != 5) + return result_NOT_SUPPORTED; /* v1: 32bpp source only */ + + blob = src->base; + p = blob + bitmap__RLE_HEADER_SIZE; + end = p + bitmap__rle_get32(blob + 4); + + firstrow = draw_box->y0 - y; + for (i = 0; i < firstrow; i++) + p = bitmap__rle_skip_row(p, end, log2bpp); + + skip = draw_box->x0 - x; + plot = draw_box->x1 - draw_box->x0; + rows = draw_box->y1 - draw_box->y0; + + scratch = malloc((size_t) plot * sizeof(*scratch)); + if (scratch == NULL) + return result_OOM; + + dstbase = scr->base; + for (i = 0; i < rows; i++) + { + uint8_t *rowp; + int xx; + + for (xx = 0; xx < plot; xx++) + scratch[xx] = 0; /* zero_skip below only fills covered skip runs */ + + p = bitmap__rle_decode_row(p, log2bpp, scratch, skip, plot, 1); + + rowp = dstbase + (size_t) (draw_box->y0 + i) * scr->rowbytes; + + for (xx = 0; xx < plot; xx++) + { + colour_t c; + int dstx; + uint8_t *scrp; + int shift; + pixelfmt_any_t pxl; + + c.primary = scratch[xx]; + if (colour_get_alpha(&c) == 0) + continue; + + dstx = draw_box->x0 + xx; + scrp = rowp + (dstx >> 1); + shift = (dstx & 1) * 4; + pxl = colour_to_pixel(scr->palette, 16, c, scr->format); + + *scrp = (uint8_t) ((*scrp & ~(0xF << shift)) | ((pxl & 0xF) << shift)); + } + } + + free(scratch); + + return result_OK; +} + result_t screen_copy_bitmap_rle(screen_t *scr, int x, int y, @@ -56,6 +147,9 @@ result_t screen_copy_bitmap_rle(screen_t *scr, assert(src); assert(pixelfmt_is_rle(src->format)); + if (pixelfmt_log2bpp(scr->format) == 2) + return screen_copy_bitmap_rle_p4(scr, x, y, src, draw_box); + base = pixelfmt_base(src->format); /* v1: 32bpp screen, source decodes to the screen's channel order. An alpha diff --git a/libraries/framebuf/screen/screen-copy-bitmap-rle.h b/libraries/framebuf/screen/screen-copy-bitmap-rle.h index b176d26..c97d264 100644 --- a/libraries/framebuf/screen/screen-copy-bitmap-rle.h +++ b/libraries/framebuf/screen/screen-copy-bitmap-rle.h @@ -14,11 +14,12 @@ * Blit an RLE-compressed bitmap onto a screen, top-left at (x, y), clipped * to `draw_box` (already intersected with the screen clip). * - * v1 constraints, else \ref result_NOT_SUPPORTED: - screen must be 32bpp - * (`pixelfmt_log2bpp(scr->format) == 5`); - `pixelfmt_base(src->format)` - * must equal `scr->format` (byte copy, no per-pixel conversion); - blit is - * alpha-tested (transparent runs skipped, opaque runs copied), not - * alpha-blended. + * Constraints, else \ref result_NOT_SUPPORTED: - screen must be 32bpp or + * 4bpp paletted; - source must decode to 32bpp; - on a 32bpp screen + * `pixelfmt_base(src->format)` must match the screen's channel order (byte + * copy, no per-pixel conversion), on a 4bpp screen each pixel is converted + * to the nearest palette entry. The blit is alpha-tested (transparent runs + * skipped, opaque runs written), not alpha-blended. * * \param[in] scr Destination screen. * \param[in] x,y Where the bitmap's top-left lands, screen pixels. From ff8f9a43d0f79b7554074a3d2476ad5392880331 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Tue, 8 Sep 2026 23:10:25 +0100 Subject: [PATCH 58/98] fix(wuss): cover the drawn resize icon in its hit box with one scrollbar wuss__resize_hit_box only grew inward to the drawn icon on an axis whose carve was non-zero. A NO_HSCROLL window that still has a vscrollbar has carve.y == 0, so its resize hit box collapsed to the 1px outline band: clicks on the visible resize icon never registered as wuss_FURNITURE_RESIZE and a resize-drag did nothing (seen in the chars task). The drawn icon (wuss__resize_box) is always the button-sized square at the inner bottom-right corner whatever the scrollbar config, so the hit box now matches that square directly and spills over the outline for the corner, with no per-axis carve term. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/furniture/resize-box.c | 30 +++++++++++---------------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/libraries/wuss/furniture/resize-box.c b/libraries/wuss/furniture/resize-box.c index 13e4a97..c13b27d 100644 --- a/libraries/wuss/furniture/resize-box.c +++ b/libraries/wuss/furniture/resize-box.c @@ -15,30 +15,24 @@ void wuss__resize_box(const wuss_window_t *window, box_t *out) out->y0 = out->y1 - size; } -/* The resize hit box owns the bottom-right corner: it grows right and down over - * the outline. On a side with no scroll strip it also grows inward to the - * content edge, swallowing the divider seam (scrollbars present) or the whole - * carved margin (NO_VSCROLL + NO_HSCROLL + resize on). On a side that does have - * a scroll strip its edge is unchanged and butts that strip. */ +/* The resize hit box owns the bottom-right corner: the button-sized drawn + * icon square, grown out over the outline band so the extreme corner still + * hits. Independent of which scrollbars are present -- a strip butts the + * icon at the same edge the icon would sit at with no strip there. */ void wuss__resize_hit_box(const wuss_window_t *window, box_t *out) { - point_t carve; - int outline_px, size; + int outline_px, size; outline_px = wuss__outline_px(window); size = wuss__button_size(window); - wuss__furniture_carve_for(window->flags, size, &carve); + /* The drawn icon (wuss__resize_box) is always the button-sized square at + * the bottom-right, inside the outline. Match it exactly, then grow out + * over the outline band so the very corner still counts as a hit. Whether + * a scroll strip butts the icon or (no strip) nothing paints that margin, + * the icon's near edges are the same, so no per-axis branch is needed. */ + out->x0 = window->visible.x1 - outline_px - size; + out->y0 = window->visible.y1 - outline_px - size; out->x1 = window->visible.x1; out->y1 = window->visible.y1; - - if (!(window->flags & wuss_WINDOW_NO_VSCROLL)) - out->x0 = window->visible.x1 - outline_px - size; /* butt the vscroll strip */ - else - out->x0 = window->visible.x1 - outline_px - carve.x; /* = content right edge */ - - if (!(window->flags & wuss_WINDOW_NO_HSCROLL)) - out->y0 = window->visible.y1 - outline_px - size; /* butt the hscroll strip */ - else - out->y0 = window->visible.y1 - outline_px - carve.y; /* = content bottom edge */ } From d960b996722266e69e50763a60a36bcdf74c8958 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Tue, 8 Sep 2026 23:11:26 +0100 Subject: [PATCH 59/98] test(wuss): assert the resize icon centre hits RESIZE for every scrollbar combo The flag-combo hit sweep only proved the visible box stays leak-free, so a resize hit box that collapsed to the outline band (NO_HSCROLL or NO_VSCROLL, one scrollbar still present) passed -- those pixels just fell to a neighbouring region. Add NO_VSCROLL and NO_HSCROLL to the combo table and check wuss__resize_box's own centre resolves to RESIZE wherever the window keeps its resize icon. Fails against the pre-fix resize-box.c. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/test/wuss-test.c | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index b68fee4..eb4422c 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -4925,7 +4925,9 @@ ColourMenuOK: ; wuss_WINDOW_NO_OUTLINE, wuss_WINDOW_NO_RESIZE, wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL, - wuss_WINDOW_NO_BACK, + wuss_WINDOW_NO_VSCROLL, /* one scrollbar + resize: carve is 0 on the */ + wuss_WINDOW_NO_HSCROLL, /* stripless axis, so the icon's near edge is */ + wuss_WINDOW_NO_BACK, /* the only thing sizing its hit box */ wuss_WINDOW_NO_TOGGLE_SIZE, wuss_WINDOW_NO_CLOSE, wuss_WINDOW_NO_TITLEBAR @@ -4965,6 +4967,24 @@ ColourMenuOK: ; goto Failure; } + /* Where the window keeps its resize icon, the icon's own drawn centre + * must hit RESIZE. The sweep alone misses a collapsed hit box: those + * pixels just fall to a neighbouring region, still leak-free. */ + if (!(combos[i] & wuss_WINDOW_NO_RESIZE)) + { + box_t resize; + + wuss__resize_box(win_cb, &resize); + if (wuss__furniture_hit_test(win_cb, + POINT((resize.x0 + resize.x1) / 2, + (resize.y0 + resize.y1) / 2)) != wuss_FURNITURE_RESIZE) + { + printf("wuss_test: combo index %u (flags 0x%X): resize icon centre " + "does not hit RESIZE\n", i, (unsigned int) combos[i]); + goto Failure; + } + } + if ((combos[i] & wuss_WINDOW_NO_BACK) && wuss__furniture_hit_test(win_cb, POINT(box_cb.x0, box_cb.y0)) != wuss_FURNITURE_CLOSE) { From 6598bb57431906f37b2e6f62182b2c299f367936 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Tue, 8 Sep 2026 23:50:42 +0100 Subject: [PATCH 60/98] feat(wuss): draw a full-width divider rule under the titlebar The titlebar's own bottom pixel row is painted as an OUTLINE rule spanning the full width between the side outlines, giving a clean separation from the content area. No geometry moves -- the row was already inside the titlebar box, now over-painted after its fill. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/TODO.txt | 5 +--- libraries/wuss/furniture/layout.c | 13 +++++++++ libraries/wuss/test/wuss-test.c | 44 +++++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 4 deletions(-) diff --git a/libraries/wuss/TODO.txt b/libraries/wuss/TODO.txt index 12e4670..1d7321a 100644 --- a/libraries/wuss/TODO.txt +++ b/libraries/wuss/TODO.txt @@ -10,8 +10,6 @@ LAYER 0 - BUGS (fix before building on top) - Auto window placement packer still not working as expected blocks: reliable multi-window tasks - Image task not shrinking window when new image loads -- Missing horizontal frame line under title bar -- Resize-only windows? - check - Backdrop colours reversed? - check @@ -39,9 +37,8 @@ LAYER 2 - TEXT & FONTS (needs UTF-8, font-arch decision) LAYER 3 - SPRITES (needs masks, ColourTrans) - Sprites (with names, with masks, with multiple sprites) +- Masked sprites/bitmaps. Covered by RLE work? - Sprite scaling -- Icon sprites pool -- Masked sprites/bitmaps - Draw bitmap through (bayer) mask - Import Kare style icons diff --git a/libraries/wuss/furniture/layout.c b/libraries/wuss/furniture/layout.c index aa2d580..9888b82 100644 --- a/libraries/wuss/furniture/layout.c +++ b/libraries/wuss/furniture/layout.c @@ -56,6 +56,19 @@ void wuss__furniture_layout_build(wuss_window_t *window) push_piece(layout, &titlebar, wuss__FURNITURE_PAINT_TITLE_BG); + /* dividing rule between the titlebar and the content, full width between + * the side outlines. Drawn as the titlebar's own bottom pixel row (no + * geometry moves), over the fill just pushed. */ + { + box_t rule; + + rule.x0 = titlebar.x0; + rule.x1 = titlebar.x1; + rule.y0 = titlebar.y1 - WUSS_DIVIDER_PX; + rule.y1 = titlebar.y1; + push_piece(layout, &rule, wuss__FURNITURE_PAINT_OUTLINE); + } + if (!(window->flags & wuss_WINDOW_NO_CLOSE)) { box_t close; diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index eb4422c..6a4278f 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -4997,6 +4997,50 @@ ColourMenuOK: ; } } + printf("test: a titlebar carries a full-width divider rule at its foot\n"); + + { + static test_task_t tc_dv; + wuss_task_t *delegate_dv; + box_t box_dv; + wuss_window_t *win_dv; + int i, found; + + tc_dv.redraw_count = 0; + tc_dv.mouse_count = 0; + delegate_dv = mk_task(wuss, test_handle, &tc_dv); + if (delegate_dv == NULL) goto Failure; + + box_dv.x0 = 5; box_dv.y0 = 5; + box_dv.x1 = 125; box_dv.y1 = 85; + rc = wuss_window_create(delegate_dv, &box_dv, "DV", wuss_WINDOW_DEFAULT, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + SIZE2D(400, 400), SIZE2D(0, 0), &win_dv); + if (rc != result_OK) + goto Failure; + + wuss__furniture_layout_build(win_dv); + + /* an OUTLINE piece exactly one pixel tall, sitting on the titlebar's + * bottom edge and spanning its full width */ + found = 0; + for (i = 0; i < win_dv->furniture_layout.npieces; i++) + { + const wuss__furniture_piece_t *p = &win_dv->furniture_layout.pieces[i]; + + if (p->paint == wuss__FURNITURE_PAINT_OUTLINE && + p->rect.y1 == win_dv->furniture_layout.titlebar.y1 && + p->rect.y1 - p->rect.y0 == WUSS_DIVIDER_PX && + p->rect.x0 == win_dv->furniture_layout.titlebar.x0 && + p->rect.x1 == win_dv->furniture_layout.titlebar.x1) + found = 1; + } + if (!found) + goto Failure; + + wuss_window_close(win_dv); + } + printf("test: wuss_icons_load scans resources/wuss/icons and compresses\n"); { From a5a4f986d114fa04c340646d63fe8c66b99f6e19 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Wed, 9 Sep 2026 00:03:27 +0100 Subject: [PATCH 61/98] fix(wuss): swap desktop backdrop dot and field colours for the Wimp palette The DOTS backdrop pattern paints backdrop.colour on set bits (the sparse dots) and pattern_bg on clear bits (the field). The Wimp-palette config had the two the wrong way round, so the darker GREY_37 tiled as the field and GREY_50 as the speckle. Swap them. Co-Authored-By: Claude Sonnet 5 --- apps/wuss/main.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/wuss/main.c b/apps/wuss/main.c index 0e4016c..8b039ea 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -59,8 +59,8 @@ static void fill_chrome_config(wuss_config_t *config, int use_wimp16) config->button.fg = palette_WIMP16_BLACK; config->button.pressed = palette_WIMP16_GREY_62; config->accent.colour = palette_WIMP16_ORANGE; - config->backdrop.colour = palette_WIMP16_GREY_50; - config->backdrop.pattern_bg = palette_WIMP16_GREY_37; + config->backdrop.colour = palette_WIMP16_GREY_37; + config->backdrop.pattern_bg = palette_WIMP16_GREY_50; config->body.window = palette_WIMP16_GREY_87; config->body.menu = palette_WIMP16_WHITE; } From 362470b5bc95e5811de47bcd1da67d56de072c88 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Wed, 9 Sep 2026 11:02:12 +0100 Subject: [PATCH 62/98] feat(framebuf): add pixelmap cached format-conversion tables Blit inner loops that call colour_to_pixel per pixel run a weighted linear palette scan every pixel. pixelmap builds that mapping once into a small lookup table, caches it in a 4-entry module-global LRU keyed on (srcfmt, destfmt, FNV-1a hash of the palette bytes), and turns the inner loop into a channel-extract, compose and array read. Handles both directions: - Deep (32bpp *8888) to paletted (p1/p2/p4/p8): the source R/G/B channels are quantised to a policy-chosen split (R4G4B4 for p1/p2/p4, R5G6B5 for p8), composed into an index, and used to read entries packed to the destination bit width. Deep colours lose a little accuracy for a table small enough to stay resident (2KB for 32bpp-to-p4). - Paletted to deep: entries is nentries deep pixels indexed by the raw palette index; no quantisation, bit-exact. Converted call sites: - screen_copy_bitmap_p4 (now returns result_t) and screen_copy_bitmap_rle_p4 route the opaque path through the table. Their output shifts slightly from the old exact-colour match to the quantised match; imperceptible for a <=16-entry palette. A format pair pixelmap cannot map now returns result_NOT_SUPPORTED rather than limping along one colour_to_pixel call per pixel. - bmconv_p4_to_bgrx8888 replaces its hand-written local 16-entry map[] with pixelmap_get; bit-exact. Co-Authored-By: Claude Sonnet 5 --- CLAUDE.md | 2 +- CMakeLists.txt | 5 + apps/test/main.c | 1 + docs/framebuf/drawing.md | 6 + docs/framebuf/pixelmap.md | 166 ++++++++++++ docs/framebuf/rle-bitmap.md | 14 +- include/framebuf/pixelmap.h | 87 +++++++ include/test/all-tests.h | 1 + libraries/framebuf/bitmap/bitmap.c | 23 +- libraries/framebuf/pixelmap/build.c | 109 ++++++++ libraries/framebuf/pixelmap/get.c | 241 ++++++++++++++++++ libraries/framebuf/pixelmap/impl.h | 45 ++++ .../framebuf/pixelmap/test/pixelmap-test.c | 168 ++++++++++++ .../framebuf/screen/screen-copy-bitmap-rle.c | 18 +- libraries/framebuf/screen/screen-draw.c | 42 ++- 15 files changed, 903 insertions(+), 25 deletions(-) create mode 100644 docs/framebuf/pixelmap.md create mode 100644 include/framebuf/pixelmap.h create mode 100644 libraries/framebuf/pixelmap/build.c create mode 100644 libraries/framebuf/pixelmap/get.c create mode 100644 libraries/framebuf/pixelmap/impl.h create mode 100644 libraries/framebuf/pixelmap/test/pixelmap-test.c diff --git a/CLAUDE.md b/CLAUDE.md index 5a7ef3e..feeb7c9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -82,7 +82,7 @@ New source/header files must be added by hand to the relevant `set(..._SOURCES . **Test structure.** There is a single test binary (`DPTLibTest`, from `apps/test/main.c`) that dispatches to per-module `_test(resources)` functions declared centrally in `include/test/all-tests.h`. Tests return `result_TEST_PASSED`/`result_TEST_FAILED` (`result_BASE_TEST`). -**Notable sub-libraries with their own docs** (`docs/*.md`): `io/stream` (chainable byte-stream sources/transforms, PackBits and move-to-front compression), `framebuf/bmfont` (proportional bitmap fonts loaded from specially-formatted PNGs), `framebuf/composite` (Porter-Duff compositing on RGBA/BGRA bitmaps), `wuss` (minimal window manager: creation, z-ordering, mouse routing, dirty-region redraw, client-delegated content). +**Notable sub-libraries with their own docs** (`docs/*.md`): `io/stream` (chainable byte-stream sources/transforms, PackBits and move-to-front compression), `framebuf/bmfont` (proportional bitmap fonts loaded from specially-formatted PNGs), `framebuf/composite` (Porter-Duff compositing on RGBA/BGRA bitmaps), `framebuf/pixelmap` (cached deep-to-paletted colour conversion tables for blit inner loops), `wuss` (minimal window manager: creation, z-ordering, mouse routing, dirty-region redraw, client-delegated content). **RISC OS.** `TARGET_RISCOS` (set by an external toolchain file) switches CMake to fetch/build zlib+libpng from source, link OSLib from `$GCCSDK_INSTALL_ENV`, and applies `riscos_set_flags()`. `pngusr-ro.dfa`/`pngusr-rw.dfa` select libpng's build config depending on `DPTLIB_IMAGES_READ_ONLY`. diff --git a/CMakeLists.txt b/CMakeLists.txt index 68e87bb..b7ca801 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -86,6 +86,7 @@ set(PUBLIC_HEADERS include/framebuf/palettes.h include/framebuf/pattern.h include/framebuf/pixelfmt.h + include/framebuf/pixelmap.h include/framebuf/screen.h include/framebuf/span-bgrx8888.h include/framebuf/span-p4.h @@ -262,6 +263,9 @@ set(FRAMEBUF_SOURCES libraries/framebuf/palettes/palettes.c libraries/framebuf/pattern/pattern.c libraries/framebuf/pixelfmt/log2bpp.c + libraries/framebuf/pixelmap/build.c + libraries/framebuf/pixelmap/get.c + libraries/framebuf/pixelmap/impl.h libraries/framebuf/screen/screen.c libraries/framebuf/screen/screen-copy-bitmap-rle.c libraries/framebuf/screen/screen-copy-bitmap-rle.h @@ -668,6 +672,7 @@ if(BUILD_TESTS) libraries/framebuf/bmfont/test/bmfont-test.c libraries/framebuf/composite/test/composite-test.c libraries/framebuf/curve/test/curve-test.c + libraries/framebuf/pixelmap/test/pixelmap-test.c libraries/framebuf/screen/test/screen-test.c libraries/geom/box/test/box-test.c libraries/geom/layout/test/layout-test.c diff --git a/apps/test/main.c b/apps/test/main.c index a23cb32..8438fc2 100644 --- a/apps/test/main.c +++ b/apps/test/main.c @@ -55,6 +55,7 @@ static const test_t tests[] = { "composite", composite_test }, { "rle", bitmap_rle_test }, { "curve", curve_test }, + { "pixelmap", pixelmap_test }, { "screen", screen_test }, { "box", box_test }, diff --git a/docs/framebuf/drawing.md b/docs/framebuf/drawing.md index a30f935..f69a536 100644 --- a/docs/framebuf/drawing.md +++ b/docs/framebuf/drawing.md @@ -53,6 +53,10 @@ the P4 handler address an odd nibble so callers never pre-pack. `screen_fill_hli (and thus `screen_fill_rect` and the circle scanline fill) is a thin wrapper over it. +`copy` ops that blit a deep bitmap onto a paletted screen turn the per-pixel +nearest-palette match into a cached mask-and-lookup via +[pixelmap](pixelmap.md). + ### Axis 2 — ACTION (what kind of mark) | verb | mark | object it takes | @@ -211,3 +215,5 @@ sites updated in the same change: `screen_draw_bitmap` → `screen_copy_bitmap`, - [composite.md](composite.md) — Porter-Duff bitmap compositing - [bmfont.md](bmfont.md) — proportional bitmap fonts +- [pixelmap.md](pixelmap.md) — cached deep-to-paletted conversion tables +- [rle-bitmap.md](rle-bitmap.md) — RLE-compressed bitmaps diff --git a/docs/framebuf/pixelmap.md b/docs/framebuf/pixelmap.md new file mode 100644 index 0000000..ed96a1f --- /dev/null +++ b/docs/framebuf/pixelmap.md @@ -0,0 +1,166 @@ +# [DPTLib](https://github.com/dpt/DPTLib) > framebuf > pixelmap + +`pixelmap` caches lookup tables that convert between a "deep" (32bpp) pixel +format and a paletted (`p1`/`p2`/`p4`/`p8`) one, in either direction. It +replaces a per-pixel `colour_to_pixel` call in a blit inner loop with a table +read. + +## Rationale + +**Deep to paletted.** A bitmap-to-paletted blit inner loop otherwise calls +`colour_to_pixel` per pixel, and for a paletted target that runs a weighted +linear scan of the whole palette (`closest_palette_entry`) every time — +O(pixels x palette-entries) per blit. `pixelmap` does that scan once. The +source pixel's R, G and B channels are quantised to a few bits each, composed +into an index, and used to read a small table. Deep source colours are +deliberately quantised — a 16-colour palette cannot resolve finer than about +4:4:4 anyway — so the table stays resident: 2KB for a 32bpp-to-p4 map. + +**Paletted to deep.** No palette scan is needed — the table is just +`nentries` deep pixels, indexed by the raw palette index. The win here is the +shared, build-once cache across repeated conversions of the same palette, and +one code path for both directions. `bmconv_p4_to_bgrx8888` was the +hand-written version of this (a local 16-entry `map[]`); it now calls +`pixelmap_get`. + +## API + +```C +const pixelmap_t *pixelmap_get(pixelfmt_t srcfmt, + pixelfmt_t destfmt, + const colour_t *palette, + int nentries); +``` + +Exactly one of `srcfmt` / `destfmt` must be paletted and the other a 32bpp +`*8888` format; `palette` is that paletted side's palette. Returns a cached +table, or `NULL` if the pair is unsupported, `palette` is `NULL`, or +allocation failed. The pointer is owned by the module and stays valid until a +later `pixelmap_get` call with a different key evicts it. + +```C +typedef struct pixelmap +{ + pixelfmt_t srcfmt; + pixelfmt_t destfmt; + int dest_log2bpp; /* 0..3, deep->paletted only */ + unsigned char entry_bytes; /* 0 = packed paletted, else deep */ + unsigned char rbits, gbits, bbits; + unsigned char rshift, gshift, bshift; /* into the source pixel */ + unsigned int nentries; + const unsigned char *entries; +} +pixelmap_t; +``` + +**Deep to paletted** (`entry_bytes == 0`). `entries` is packed to the +destination bit width, LSB-first within each byte: `p4` holds two 4-bit +entries per byte, `p2` four, `p1` eight, `p8` one byte per entry. + +**Paletted to deep** (`entry_bytes == 4`). `entries` is `nentries` deep +pixels; the index is the raw palette index. `rbits`/`gbits`/`bbits` and the +shifts are 0. + +## Using it — deep to paletted + +A caller that blits in a loop fetches the table once, before the loop, and +bails if it is unsupported; then does a mask-and-lookup per pixel: + +```C +const pixelmap_t *pm; + +pm = pixelmap_get(pixelfmt_rgba8888, scr->format, scr->palette, 16); +if (pm == NULL) + return result_NOT_SUPPORTED; +... +r = (px >> pm->rshift) & 0xFF; +g = (px >> pm->gshift) & 0xFF; +b = (px >> pm->bshift) & 0xFF; +idx = ((r >> (8 - pm->rbits)) << (pm->gbits + pm->bbits)) + | ((g >> (8 - pm->gbits)) << pm->bbits) + | ( b >> (8 - pm->bbits)); +pxl = (pm->entries[idx >> 1] >> ((idx & 1) << 2)) & 0xF; /* p4 */ +``` + +Alpha is not part of the index. Callers keep their own alpha test and route +only the opaque path through the table. There is no `colour_to_pixel` +fallback: a `NULL` return means the blit cannot proceed and the call sites +propagate `result_NOT_SUPPORTED`. + +Call sites: `screen_copy_bitmap_p4` (`screen-draw.c`) and +`screen_copy_bitmap_rle_p4` (`screen-copy-bitmap-rle.c`). Both treat the source +as RGBA8888 byte order. + +## Using it — paletted to deep + +```C +const pixelmap_t *pm; +const pixelfmt_bgrx8888_t *map; + +pm = pixelmap_get(pixelfmt_p4, pixelfmt_bgrx8888, src->palette, 16); +map = (const pixelfmt_bgrx8888_t *) pm->entries; +... +*outpx++ = map[nibble]; +``` + +Call site: `bmconv_p4_to_bgrx8888` (`bitmap.c`). + +## Policy + +For a deep source, the channel split is chosen by a built-in table keyed on +`destfmt`. There is no caller override in v1. + +| source | dest | bits/channel | entries | table bytes | +|------------------|-------------|--------------|---------|-------------| +| any 32bpp `*8888`| `p1` | R4 G4 B4 | 4096 | 512 | +| any 32bpp `*8888`| `p2` | R4 G4 B4 | 4096 | 1024 | +| any 32bpp `*8888`| `p4` | R4 G4 B4 | 4096 | 2048 | +| any 32bpp `*8888`| `p8` | R5 G6 B5 | 65536 | 65536 | +| `p1`..`p8` | 32bpp `*8888` | *(raw index)* | palette size | 4 x entries | + +`p8` keeps a wider index so 256-colour output stays faithful. A paletted +source has no quantisation — one deep pixel per palette entry. + +## Cache + +A module-global LRU of 4 entries, keyed on `(srcfmt, destfmt, FNV-1a hash of +the palette bytes)`. Hashing the palette contents (rather than its address) +keeps the key correct when a palette is mutated in place or a freed palette's +address is reused. The design mirrors `spanregistry_get`. Single-threaded, like +the rest of `framebuf`. + +## Behaviour change on adoption + +Converting a **deep-to-paletted** call site from `colour_to_pixel` to +`pixelmap` changes its output slightly: the old path matched on the exact +24-bit source colour, the new path quantises to the policy split first. For a +<=16-entry palette this is imperceptible; a palette with two entries inside +one quantisation cell could move a pixel by one index. The converted sites +(`screen_copy_bitmap_p4`, `screen_copy_bitmap_rle_p4`) now return +`result_NOT_SUPPORTED` for a format pair `pixelmap` cannot map, where before +they would have limped along one `colour_to_pixel` call per pixel. + +A **paletted-to-deep** call site is bit-exact — the entries are the same +`colour_to_pixel` results the local `map[]` held. + +## Build cost + +For a deep source, building a table is one `colour_to_pixel` call per index +value, at `get` time only: ~65K calls for a 32bpp-to-p4 map (negligible), +~16M for a `p8` map (a few milliseconds). For a paletted source it is one +call per palette entry (16 or 256). Amortised across every subsequent blit +that hits the cache. + +## Not done + +- No 16bpp (`565`) support on either side — add when a call site needs it. +- Paletted-to-deep only fills 4-byte (`*8888`) entries; 12/15/16bpp deep + destinations are not built. +- No `pixelmap_get_ex` with a caller-supplied channel split. +- `span_p4_blendconst` still re-quantises per pixel; its input is a blend + result, not a source pixel, so it is a separate change. + +## Related + +- [drawing.md](drawing.md) — the `framebuf` drawing taxonomy +- [rle-bitmap.md](rle-bitmap.md) — RLE bitmaps; its p4 blit path uses pixelmap diff --git a/docs/framebuf/rle-bitmap.md b/docs/framebuf/rle-bitmap.md index bc9d985..f5518e4 100644 --- a/docs/framebuf/rle-bitmap.md +++ b/docs/framebuf/rle-bitmap.md @@ -87,11 +87,19 @@ destination untouched. | `pixelfmt_{abgr,argb}8888` | yes | no | | `p1`..`p8`, 12/15/16bpp | no (`result_NOT_SUPPORTED`) | — | +## Screen targets + +- **32bpp screen** — the source must decode to the screen's exact channel + order; the blit is then a byte copy, no per-pixel conversion + (`pixelfmt_base(src->format) == scr->format`, alpha folded out). +- **p4 screen** — each decoded pixel is mapped to the nearest palette index. A + cached [pixelmap](pixelmap.md) table (`pixelmap_get(pixelfmt_rgba8888, + scr->format, scr->palette, 16)`) turns the inner loop into a mask-and-lookup; + if that pair is unsupported the blit declines `result_NOT_SUPPORTED`. +- **Other depths** decline `result_NOT_SUPPORTED`. + ## v1 limitations -- **32bpp screens only.** A p4 screen target declines `result_NOT_SUPPORTED`. -- **Source must decode to the screen's exact format** — the blit is a byte - copy, no per-pixel conversion. `pixelfmt_base(src->format) == scr->format`. - **Alpha-tested, not alpha-blended.** Transparent runs are skipped, opaque runs copied. This matches `screen_copy_bitmap`'s p4 path but not its 32bpp path (which blends per pixel). A blended RLE path would decode alpha runs diff --git a/include/framebuf/pixelmap.h b/include/framebuf/pixelmap.h new file mode 100644 index 0000000..1ba4ceb --- /dev/null +++ b/include/framebuf/pixelmap.h @@ -0,0 +1,87 @@ +/* framebuf/pixelmap.h -- cached bitmap-format conversion tables */ + +#ifndef FRAMEBUF_PIXELMAP_H +#define FRAMEBUF_PIXELMAP_H + +#ifdef __cplusplus +extern "C" +{ +#endif + +#include "framebuf/colour.h" +#include "framebuf/pixelfmt.h" + +/* ----------------------------------------------------------------------- */ + +/** + * A cached lookup table converting between a "deep" (32bpp) pixel format and + * a paletted (p1/p2/p4/p8) one, in either direction. It replaces a per-pixel + * \ref colour_to_pixel call in a blit inner loop with a table read. + * + * **Deep source to paletted destination.** \ref colour_to_pixel would run a + * weighted linear scan of the whole palette per pixel. The pixelmap does + * that once: the source pixel's red, green and blue channels are extracted + * with \ref rshift / \ref gshift / \ref bshift, quantised to \ref rbits / + * \ref gbits / \ref bbits, composed into the index `(r << (gbits + bbits)) | + * (g << bbits) | b`, and used to read \ref entries. The quantisation trades + * a little colour accuracy for a table small enough to stay resident (2KB + * for a 32bpp-to-p4 map). \ref entries is packed to the destination bit + * width, LSB-first within each byte: p4 holds two 4-bit entries per byte, p2 + * four, p1 eight, p8 one byte per entry. \ref entry_bytes is 0. + * + * **Paletted source to deep destination.** The index is the raw palette + * index (0..\ref nentries - 1); there is no quantisation and + * rbits/gbits/bbits and the shifts are 0. \ref entries is an array of \ref + * nentries pixels, each \ref entry_bytes wide (4 for a 32bpp destination), + * holding the destination pixel for that palette index: `dst = ((const + * pixelfmt_any32_t *) pm->entries)[palette_index]`. + * + * The table is owned by the pixelmap module and lives in a small cache. A + * caller that blits in a loop calls \ref pixelmap_get once, before the loop. + */ +typedef struct pixelmap +{ + pixelfmt_t srcfmt; + pixelfmt_t destfmt; + int dest_log2bpp; /* 0..3, deep->paletted only */ + unsigned char entry_bytes; /* 0 = packed paletted, else deep */ + unsigned char rbits, gbits, bbits; + unsigned char rshift, gshift, bshift; /* into the source pixel */ + unsigned int nentries; + const unsigned char *entries; +} +pixelmap_t; + +/* ----------------------------------------------------------------------- */ + +/** + * Return a cached table converting a `srcfmt` pixel to `destfmt` under + * `palette`. + * + * Exactly one of `srcfmt` / `destfmt` must be paletted (p1/p2/p4/p8) and the + * other a 32bpp `*8888` format. `palette` is always the paletted side's + * palette. + * + * The returned pointer is owned by the pixelmap module and stays valid until + * a later \ref pixelmap_get call with a different key evicts it. Callers + * that blit in a loop fetch it once, before the loop. + * + * \param[in] srcfmt Source pixel format. + * \param[in] destfmt Destination pixel format. + * \param[in] palette Palette of the paletted side. + * \param[in] nentries Number of entries in `palette`. + * \return A cached table, or NULL if the format pair is unsupported, + * `palette` is NULL, or allocation failed. + */ +const pixelmap_t *pixelmap_get(pixelfmt_t srcfmt, + pixelfmt_t destfmt, + const colour_t *palette, + int nentries); + +/* ----------------------------------------------------------------------- */ + +#ifdef __cplusplus +} +#endif + +#endif /* FRAMEBUF_PIXELMAP_H */ diff --git a/include/test/all-tests.h b/include/test/all-tests.h index 15130ad..91612cb 100644 --- a/include/test/all-tests.h +++ b/include/test/all-tests.h @@ -27,6 +27,7 @@ extern testfn_t bitmap_rle_test, bmfont_test, composite_test, curve_test, + pixelmap_test, screen_test; /* geom */ diff --git a/libraries/framebuf/bitmap/bitmap.c b/libraries/framebuf/bitmap/bitmap.c index bf31200..9b54420 100644 --- a/libraries/framebuf/bitmap/bitmap.c +++ b/libraries/framebuf/bitmap/bitmap.c @@ -6,6 +6,7 @@ #include #include "framebuf/bitmap.h" +#include "framebuf/pixelmap.h" #include "framebuf/span-registry.h" result_t bitmap_init(bitmap_t *bm, @@ -132,19 +133,23 @@ void bitmap_clear(bitmap_t *bm, colour_t colour) static result_t bmconv_p4_to_bgrx8888(const bitmap_t *src, bitmap_t **pdst) { - result_t rc; - bitmap_t *dst; - pixelfmt_bgrx8888_t *outpixels; - pixelfmt_bgrx8888_t map[16]; - int i; - pixelfmt_p4_t *inpixels; - int x,y; + result_t rc; + const pixelmap_t *pm; + const pixelfmt_bgrx8888_t *map; + bitmap_t *dst; + pixelfmt_bgrx8888_t *outpixels; + pixelfmt_p4_t *inpixels; + int x,y; assert(src); assert(src->palette); - for (i = 0; i < 16; i++) - map[i] = colour_to_pixel(src->palette, 16, src->palette[i], pixelfmt_bgrx8888); + /* cached p4-palette -> bgrx8888 expansion table (built once, shared across + * conversions of the same palette) */ + pm = pixelmap_get(pixelfmt_p4, pixelfmt_bgrx8888, src->palette, 16); + if (pm == NULL) + return result_NOT_SUPPORTED; + map = (const pixelfmt_bgrx8888_t *) pm->entries; outpixels = malloc(src->size.w * sizeof(pixelfmt_bgrx8888_t) * src->size.h); // rowbytes rounding needed? if (outpixels == NULL) diff --git a/libraries/framebuf/pixelmap/build.c b/libraries/framebuf/pixelmap/build.c new file mode 100644 index 0000000..373ab75 --- /dev/null +++ b/libraries/framebuf/pixelmap/build.c @@ -0,0 +1,109 @@ +/* framebuf/pixelmap/build.c -- fill a pixelmap's entry table */ + +#include +#include +#include + +#include "framebuf/colour.h" +#include "framebuf/pixelfmt.h" + +#include "impl.h" + +/* paletted -> deep: one destination pixel per palette entry, index = the raw + * palette index. */ +static int pixelmap__build_paletted_to_deep(pixelmap_t *pm, + const colour_t *palette, + int nentries) +{ + uint32_t *entries; + int i; + + entries = calloc(pm->nentries, sizeof(*entries)); + if (entries == NULL) + return -1; + + for (i = 0; i < nentries; i++) + entries[i] = (uint32_t) colour_to_pixel(palette, nentries, + palette[i], pm->destfmt); + + pm->entries = (const unsigned char *) entries; + + return 0; +} + +/* Replicate an n-bit value up to 8 bits (e.g. 4-bit 0xA -> 0xAA), so a + * quantised channel spans the full 0..255 range before the nearest-colour + * match. */ +static unsigned int pixelmap__expand(unsigned int v, unsigned int nbits) +{ + unsigned int out; + unsigned int filled; + + out = 0; + filled = 0; + + while (filled < 8) + { + out = (out << nbits) | v; + filled += nbits; + } + + return (out >> (filled - 8)) & 0xFF; +} + +int pixelmap__build_table(pixelmap_t *pm, + const colour_t *palette, + int nentries) +{ + unsigned char *entries; + size_t nbytes; + unsigned int per_byte; + unsigned int rbits, gbits, bbits; + unsigned int r, g, b; + unsigned int idx; + unsigned int pal; + colour_t c; + + if (pm->entry_bytes != 0) + return pixelmap__build_paletted_to_deep(pm, palette, nentries); + + rbits = pm->rbits; + gbits = pm->gbits; + bbits = pm->bbits; + + per_byte = 8u >> pm->dest_log2bpp; /* p1:8 p2:4 p4:2 p8:1 */ + nbytes = (pm->nentries + per_byte - 1) / per_byte; + + entries = calloc(nbytes, 1); + if (entries == NULL) + return -1; + + for (r = 0; r < (1u << rbits); r++) + for (g = 0; g < (1u << gbits); g++) + for (b = 0; b < (1u << bbits); b++) + { + idx = (r << (gbits + bbits)) | (g << bbits) | b; + + c = colour_rgb((int) pixelmap__expand(r, rbits), + (int) pixelmap__expand(g, gbits), + (int) pixelmap__expand(b, bbits)); + + pal = colour_to_pixel(palette, nentries, c, pm->destfmt) + & ((1u << (1u << pm->dest_log2bpp)) - 1); + + /* pack LSB-first: p8 is a plain byte store */ + if (pm->dest_log2bpp == 3) + entries[idx] = (unsigned char) pal; + else + { + unsigned int bit; + + bit = (idx % per_byte) << pm->dest_log2bpp; + entries[idx / per_byte] |= (unsigned char) (pal << bit); + } + } + + pm->entries = entries; + + return 0; +} diff --git a/libraries/framebuf/pixelmap/get.c b/libraries/framebuf/pixelmap/get.c new file mode 100644 index 0000000..720a986 --- /dev/null +++ b/libraries/framebuf/pixelmap/get.c @@ -0,0 +1,241 @@ +/* framebuf/pixelmap/get.c -- pixelmap lookup and cache */ + +#include +#include + +#include "framebuf/colour.h" +#include "framebuf/pixelfmt.h" + +#include "framebuf/pixelmap.h" + +#include "impl.h" + +/* ----------------------------------------------------------------------- */ + +static int pixelmap__is_deep(pixelfmt_t fmt) +{ + switch (fmt) + { + case pixelfmt_bgrx8888: + case pixelfmt_rgbx8888: + case pixelfmt_xbgr8888: + case pixelfmt_xrgb8888: + case pixelfmt_bgra8888: + case pixelfmt_rgba8888: + case pixelfmt_abgr8888: + case pixelfmt_argb8888: + return 1; + default: + return 0; + } +} + +static int pixelmap__paletted_log2bpp(pixelfmt_t fmt) +{ + switch (fmt) + { + case pixelfmt_p1: return 0; + case pixelfmt_p2: return 1; + case pixelfmt_p4: return 2; + case pixelfmt_p8: return 3; + default: return -1; + } +} + +/* deep -> paletted: fill the channel quantisation layout. p1/p2/p4 quantise to + * 4:4:4 (2KB max); p8 to 5:6:5 to keep 256-colour output faithful. */ +static int pixelmap__layout_deep_to_paletted(pixelfmt_t srcfmt, + int dest_log2bpp, + pixelmap_t *out) +{ + out->entry_bytes = 0; + + if (dest_log2bpp == 3) + { + out->rbits = 5; out->gbits = 6; out->bbits = 5; + } + else + { + out->rbits = 4; out->gbits = 4; out->bbits = 4; + } + + /* 8-bit channel extract from the source pixel */ + switch (srcfmt) + { + case pixelfmt_bgrx8888: + case pixelfmt_bgra8888: + out->rshift = 16; out->gshift = 8; out->bshift = 0; + break; + case pixelfmt_rgbx8888: + case pixelfmt_rgba8888: + out->rshift = 0; out->gshift = 8; out->bshift = 16; + break; + case pixelfmt_xbgr8888: + case pixelfmt_abgr8888: + out->rshift = 24; out->gshift = 16; out->bshift = 8; + break; + default: /* xrgb8888, argb8888 */ + out->rshift = 8; out->gshift = 16; out->bshift = 24; + break; + } + + out->nentries = 1u << (out->rbits + out->gbits + out->bbits); + + return 0; +} + +/* paletted -> deep: the index is the raw palette index, one deep pixel per + * entry. nentries comes from the palette, filled in by the caller. */ +static int pixelmap__layout_paletted_to_deep(int nentries, pixelmap_t *out) +{ + out->entry_bytes = 4; + out->rbits = out->gbits = out->bbits = 0; + out->rshift = out->gshift = out->bshift = 0; + out->nentries = (unsigned int) nentries; + + return 0; +} + +int pixelmap__layout_for(pixelfmt_t srcfmt, + pixelfmt_t destfmt, + pixelmap_t *out) +{ + int src_deep; + int dst_deep; + int dest_log2bpp; + + src_deep = pixelmap__is_deep(srcfmt); + dst_deep = pixelmap__is_deep(destfmt); + + out->srcfmt = srcfmt; + out->destfmt = destfmt; + out->dest_log2bpp = -1; + out->entries = NULL; + + if (src_deep && !dst_deep) + { + dest_log2bpp = pixelmap__paletted_log2bpp(destfmt); + if (dest_log2bpp < 0) + return -1; + out->dest_log2bpp = dest_log2bpp; + return pixelmap__layout_deep_to_paletted(srcfmt, dest_log2bpp, out); + } + + if (!src_deep && dst_deep) + { + if (pixelmap__paletted_log2bpp(srcfmt) < 0) + return -1; + /* nentries is set from the palette count by the caller-side layout call */ + return pixelmap__layout_paletted_to_deep(0, out); + } + + return -1; /* deep<->deep and paletted<->paletted are not pixelmap's job */ +} + +/* ----------------------------------------------------------------------- */ + +/* FNV-1a over the palette bytes. */ +static unsigned int pixelmap__hash(const colour_t *palette, int nentries) +{ + const unsigned char *p; + size_t n; + unsigned int h; + size_t i; + + p = (const unsigned char *) palette; + n = (size_t) nentries * sizeof(*palette); + h = 2166136261u; + + for (i = 0; i < n; i++) + { + h ^= p[i]; + h *= 16777619u; + } + + return h; +} + +/* ----------------------------------------------------------------------- */ + +#define PIXELMAP_CACHE_SIZE 4 + +struct pixelmap__cacheent +{ + int used; + pixelfmt_t srcfmt; + pixelfmt_t destfmt; + unsigned int palhash; + unsigned int lru; /* higher = more recently used */ + pixelmap_t map; +}; + +const pixelmap_t *pixelmap_get(pixelfmt_t srcfmt, + pixelfmt_t destfmt, + const colour_t *palette, + int nentries) +{ + static struct pixelmap__cacheent cache[PIXELMAP_CACHE_SIZE]; + static unsigned int clock; + + unsigned int palhash; + int victim; + unsigned int oldest; + int i; + + if (palette == NULL) + return NULL; + + palhash = pixelmap__hash(palette, nentries); + + for (i = 0; i < PIXELMAP_CACHE_SIZE; i++) + if (cache[i].used && + cache[i].srcfmt == srcfmt && + cache[i].destfmt == destfmt && + cache[i].palhash == palhash) + { + cache[i].lru = ++clock; + return &cache[i].map; + } + + /* miss: pick a victim (unused, else least-recently-used) */ + victim = 0; + oldest = cache[0].used ? cache[0].lru : 0; + for (i = 0; i < PIXELMAP_CACHE_SIZE; i++) + { + if (!cache[i].used) + { + victim = i; + break; + } + if (cache[i].lru < oldest) + { + oldest = cache[i].lru; + victim = i; + } + } + + if (cache[victim].used) + { + free((void *) cache[victim].map.entries); + cache[victim].used = 0; + } + + if (pixelmap__layout_for(srcfmt, destfmt, &cache[victim].map) != 0) + return NULL; + + /* paletted -> deep: the entry count is the palette size, not known to + * pixelmap__layout_for */ + if (cache[victim].map.entry_bytes != 0) + cache[victim].map.nentries = (unsigned int) nentries; + + if (pixelmap__build_table(&cache[victim].map, palette, nentries) != 0) + return NULL; + + cache[victim].used = 1; + cache[victim].srcfmt = srcfmt; + cache[victim].destfmt = destfmt; + cache[victim].palhash = palhash; + cache[victim].lru = ++clock; + + return &cache[victim].map; +} diff --git a/libraries/framebuf/pixelmap/impl.h b/libraries/framebuf/pixelmap/impl.h new file mode 100644 index 0000000..4ed94fb --- /dev/null +++ b/libraries/framebuf/pixelmap/impl.h @@ -0,0 +1,45 @@ +/* framebuf/pixelmap/impl.h -- pixelmap internals */ + +#ifndef PIXELMAP_IMPL_H +#define PIXELMAP_IMPL_H + +#include "framebuf/colour.h" +#include "framebuf/pixelfmt.h" + +#include "framebuf/pixelmap.h" + +/* ----------------------------------------------------------------------- */ + +/** + * Fill in the channel layout of `out` (rbits/gbits/bbits, rshift/gshift/bshift, + * destfmt, dest_log2bpp, nentries) for the given format pair, from the built-in + * policy table. + * + * The source format must be a 32bpp `*8888` format; its channels are extracted + * with an 8-bit right shift (\ref pixelmap_t rshift/gshift/bshift). + * + * \param[in] srcfmt Source pixel format. + * \param[in] destfmt Destination format. + * \param[out] out Partly-filled pixelmap (entries not touched). + * \return 0 on success, -1 if the pair is unsupported. + */ +int pixelmap__layout_for(pixelfmt_t srcfmt, + pixelfmt_t destfmt, + pixelmap_t *out); + +/** + * Allocate and fill `pm->entries` from `palette`, using the channel layout + * already set in `pm` by \ref pixelmap__layout_for. + * + * \param[in,out] pm Pixelmap with its layout filled in. + * \param[in] palette Destination palette. + * \param[in] nentries Number of entries in `palette`. + * \return 0 on success, -1 on allocation failure. + */ +int pixelmap__build_table(pixelmap_t *pm, + const colour_t *palette, + int nentries); + +/* ----------------------------------------------------------------------- */ + +#endif /* PIXELMAP_IMPL_H */ diff --git a/libraries/framebuf/pixelmap/test/pixelmap-test.c b/libraries/framebuf/pixelmap/test/pixelmap-test.c new file mode 100644 index 0000000..4abfd8d --- /dev/null +++ b/libraries/framebuf/pixelmap/test/pixelmap-test.c @@ -0,0 +1,168 @@ +/* framebuf/pixelmap/test/pixelmap-test.c */ + +#include +#include + +#include "base/result.h" + +#include "framebuf/colour.h" +#include "framebuf/palettes.h" +#include "framebuf/pixelfmt.h" +#include "framebuf/pixelmap.h" + +#include "test/all-tests.h" + +/* compose the packed index for an rgba8888 source pixel */ +static unsigned int pm_index(const pixelmap_t *pm, pixelfmt_rgba8888_t px) +{ + unsigned int r, g, b; + + r = (px >> pm->rshift) & 0xFF; + g = (px >> pm->gshift) & 0xFF; + b = (px >> pm->bshift) & 0xFF; + + return ((r >> (8 - pm->rbits)) << (pm->gbits + pm->bbits)) + | ((g >> (8 - pm->gbits)) << pm->bbits) + | ( b >> (8 - pm->bbits)); +} + +static unsigned int pm_lookup(const pixelmap_t *pm, unsigned int idx) +{ + unsigned int per_byte; + unsigned int bit; + + if (pm->dest_log2bpp == 3) + return pm->entries[idx]; + + per_byte = 8u >> pm->dest_log2bpp; + bit = (idx % per_byte) << pm->dest_log2bpp; + + return (pm->entries[idx / per_byte] >> bit) & ((1u << (1u << pm->dest_log2bpp)) - 1); +} + +static int chan_dist(pixelfmt_rgba8888_t a, pixelfmt_rgba8888_t b) +{ + int dr, dg, db; + + dr = (int) PIXELFMT_Rxxx8888(a) - (int) PIXELFMT_Rxxx8888(b); + dg = (int) PIXELFMT_xGxx8888(a) - (int) PIXELFMT_xGxx8888(b); + db = (int) PIXELFMT_xxBx8888(a) - (int) PIXELFMT_xxBx8888(b); + + return dr * dr + dg * dg + db * db; +} + +result_t pixelmap_test(const char *resources) +{ + colour_t pal[palette_PICO8__LENGTH]; + const pixelmap_t *pm, *pm2, *pm8; + unsigned int idx; + int i; + + (void) resources; + + define_pico8_palette(pal); + + /* 1. rgba8888 -> p4: known colours land on the expected index */ + pm = pixelmap_get(pixelfmt_rgba8888, pixelfmt_p4, pal, palette_PICO8__LENGTH); + if (pm == NULL) + { + printf("pixelmap: get rgba8888->p4 returned NULL\n"); + return result_TEST_FAILED; + } + + for (i = 0; i < palette_PICO8__LENGTH; i++) + { + unsigned int got; + + idx = pm_index(pm, pal[i].primary); + got = pm_lookup(pm, idx); + + /* the exact palette colour, quantised to 4:4:4, should match itself or a + * near neighbour -- assert the resolved colour is close, not that the + * index is identical */ + if (chan_dist(pal[i].primary, pal[got].primary) > chan_dist(pal[i].primary, pal[i].primary) + 3 * 16 * 16) + { + printf("pixelmap: entry %d resolves too far (idx %u -> pal %u)\n", i, idx, got); + return result_TEST_FAILED; + } + } + + /* 2. same args -> same cached pointer */ + pm2 = pixelmap_get(pixelfmt_rgba8888, pixelfmt_p4, pal, palette_PICO8__LENGTH); + if (pm2 != pm) + { + printf("pixelmap: cache miss on identical request\n"); + return result_TEST_FAILED; + } + + /* 3. unsupported pair -> NULL */ + if (pixelmap_get(pixelfmt_rgba8888, pixelfmt_rgba8888, pal, palette_PICO8__LENGTH) != NULL) + { + printf("pixelmap: unsupported destfmt did not return NULL\n"); + return result_TEST_FAILED; + } + + /* 4. rgba8888 -> p8 (64KB table): every palette entry resolves close */ + pm8 = pixelmap_get(pixelfmt_rgba8888, pixelfmt_p8, pal, palette_PICO8__LENGTH); + if (pm8 == NULL) + { + printf("pixelmap: get rgba8888->p8 returned NULL\n"); + return result_TEST_FAILED; + } + if (pm8->nentries != 65536) + { + printf("pixelmap: p8 table has %u entries, expected 65536\n", pm8->nentries); + return result_TEST_FAILED; + } + + for (i = 0; i < palette_PICO8__LENGTH; i++) + { + unsigned int got; + + idx = pm_index(pm8, pal[i].primary); + got = pm_lookup(pm8, idx); + + /* 5:6:5 quantisation: within ~8 levels per channel */ + if (chan_dist(pal[i].primary, pal[got].primary) > 3 * 8 * 8) + { + printf("pixelmap: p8 entry %d resolves too far (pal %u)\n", i, got); + return result_TEST_FAILED; + } + } + + /* 5. paletted -> deep: p4 -> bgrx8888, one deep pixel per palette index */ + { + const pixelmap_t *pmd; + const pixelfmt_bgrx8888_t *map; + + pmd = pixelmap_get(pixelfmt_p4, pixelfmt_bgrx8888, pal, palette_PICO8__LENGTH); + if (pmd == NULL) + { + printf("pixelmap: get p4->bgrx8888 returned NULL\n"); + return result_TEST_FAILED; + } + if (pmd->entry_bytes != 4 || pmd->nentries != palette_PICO8__LENGTH) + { + printf("pixelmap: p4->bgrx8888 layout wrong (entry_bytes %u, nentries %u)\n", + pmd->entry_bytes, pmd->nentries); + return result_TEST_FAILED; + } + + map = (const pixelfmt_bgrx8888_t *) pmd->entries; + for (i = 0; i < palette_PICO8__LENGTH; i++) + { + pixelfmt_bgrx8888_t want; + + want = (pixelfmt_bgrx8888_t) colour_to_pixel(pal, palette_PICO8__LENGTH, + pal[i], pixelfmt_bgrx8888); + if (map[i] != want) + { + printf("pixelmap: p4->bgrx8888 entry %d = 0x%08X, want 0x%08X\n", + i, map[i], want); + return result_TEST_FAILED; + } + } + } + + return result_TEST_PASSED; +} diff --git a/libraries/framebuf/screen/screen-copy-bitmap-rle.c b/libraries/framebuf/screen/screen-copy-bitmap-rle.c index 13f95cf..40d9576 100644 --- a/libraries/framebuf/screen/screen-copy-bitmap-rle.c +++ b/libraries/framebuf/screen/screen-copy-bitmap-rle.c @@ -11,6 +11,7 @@ #include "framebuf/colour.h" #include "framebuf/pixelfmt.h" +#include "framebuf/pixelmap.h" #include "framebuf/bitmap.h" #include "framebuf/screen.h" @@ -59,6 +60,7 @@ static result_t screen_copy_bitmap_rle_p4(screen_t *scr, const uint8_t *end; uint8_t *dstbase; pixelfmt_rgba8888_t *scratch; + const pixelmap_t *pm; int firstrow; int skip, plot; int rows; @@ -82,6 +84,12 @@ static result_t screen_copy_bitmap_rle_p4(screen_t *scr, plot = draw_box->x1 - draw_box->x0; rows = draw_box->y1 - draw_box->y0; + /* The decode buffer is RGBA8888 byte order. The cached RGB->index table + * turns the per-pixel nearest-palette match into a mask-and-lookup. */ + pm = pixelmap_get(pixelfmt_rgba8888, scr->format, scr->palette, 16); + if (pm == NULL) + return result_NOT_SUPPORTED; + scratch = malloc((size_t) plot * sizeof(*scratch)); if (scratch == NULL) return result_OOM; @@ -102,6 +110,7 @@ static result_t screen_copy_bitmap_rle_p4(screen_t *scr, for (xx = 0; xx < plot; xx++) { colour_t c; + unsigned int r, g, b, idx; int dstx; uint8_t *scrp; int shift; @@ -114,7 +123,14 @@ static result_t screen_copy_bitmap_rle_p4(screen_t *scr, dstx = draw_box->x0 + xx; scrp = rowp + (dstx >> 1); shift = (dstx & 1) * 4; - pxl = colour_to_pixel(scr->palette, 16, c, scr->format); + + r = (c.primary >> pm->rshift) & 0xFF; + g = (c.primary >> pm->gshift) & 0xFF; + b = (c.primary >> pm->bshift) & 0xFF; + idx = ((r >> (8 - pm->rbits)) << (pm->gbits + pm->bbits)) + | ((g >> (8 - pm->gbits)) << pm->bbits) + | ( b >> (8 - pm->bbits)); + pxl = (pm->entries[idx >> 1] >> ((idx & 1) << 2)) & 0xF; *scrp = (uint8_t) ((*scrp & ~(0xF << shift)) | ((pxl & 0xF) << shift)); } diff --git a/libraries/framebuf/screen/screen-draw.c b/libraries/framebuf/screen/screen-draw.c index 608ee6f..f10ad47 100644 --- a/libraries/framebuf/screen/screen-draw.c +++ b/libraries/framebuf/screen/screen-draw.c @@ -9,6 +9,7 @@ #include "base/utils.h" #include "framebuf/colour.h" +#include "framebuf/pixelmap.h" #include "framebuf/span-registry.h" #include "geom/line.h" #include "utils/fxp.h" @@ -222,18 +223,21 @@ void screen_fill_square(screen_t *scr, /* ----------------------------------------------------------------------- */ /* Blit "src" onto the paletted screen, its top-left at (x, y), clipped to - * "draw_box". No linear channel bits to blend, so this falls back to - * alpha-tested transfer (skip fully transparent, else nearest palette match) - * rather than true blending, matching screen_set_pixel's case 2. */ -static void screen_copy_bitmap_p4(screen_t *scr, - int x, - int y, - const bitmap_t *src, - const box_t *draw_box, - int has_alpha) + * "draw_box". No linear channel bits to blend, so this does an alpha-tested + * transfer (skip fully transparent, else nearest palette match) rather than + * true blending, matching screen_set_pixel's case 2. Returns + * result_NOT_SUPPORTED if there is no deep->paletted conversion table for the + * screen's format. */ +static result_t screen_copy_bitmap_p4(screen_t *scr, + int x, + int y, + const bitmap_t *src, + const box_t *draw_box, + int has_alpha) { const unsigned char *srcrow; unsigned char *dstbase; + const pixelmap_t *pm; int clipped_width, clipped_height; int yy; @@ -243,6 +247,12 @@ static void screen_copy_bitmap_p4(screen_t *scr, srcrow = (const unsigned char *) src->base + (draw_box->y0 - y) * src->rowbytes; dstbase = scr->base; + /* Source pixels here are RGBA8888 byte order (see screen_copy_bitmap). The + * cached RGB->index table turns the inner loop into a mask-and-lookup. */ + pm = pixelmap_get(pixelfmt_rgba8888, scr->format, scr->palette, 16); + if (pm == NULL) + return result_NOT_SUPPORTED; + for (yy = 0; yy < clipped_height; yy++) { const pixelfmt_rgba8888_t *srcpx; @@ -255,6 +265,7 @@ static void screen_copy_bitmap_p4(screen_t *scr, for (xx = 0; xx < clipped_width; xx++) { colour_t c; + unsigned int r, g, b, idx; int dstx; unsigned char *scrp; int shift; @@ -267,13 +278,22 @@ static void screen_copy_bitmap_p4(screen_t *scr, dstx = draw_box->x0 + xx; scrp = rowp + (dstx >> 1); shift = (dstx & 1) * 4; - pxl = colour_to_pixel(scr->palette, 16, c, scr->format); + + r = (c.primary >> pm->rshift) & 0xFF; + g = (c.primary >> pm->gshift) & 0xFF; + b = (c.primary >> pm->bshift) & 0xFF; + idx = ((r >> (8 - pm->rbits)) << (pm->gbits + pm->bbits)) + | ((g >> (8 - pm->gbits)) << pm->bbits) + | ( b >> (8 - pm->bbits)); + pxl = (pm->entries[idx >> 1] >> ((idx & 1) << 2)) & 0xF; *scrp = (unsigned char) ((*scrp & ~(0xF << shift)) | ((pxl & 0xF) << shift)); } srcrow += src->rowbytes; } + + return result_OK; } /* Blit "src" onto the 32bpp screen, its top-left at (x, y), clipped to @@ -363,7 +383,7 @@ result_t screen_copy_bitmap(screen_t *scr, int x, int y, const bitmap_t *src) switch (pixelfmt_log2bpp(scr->format)) { - case 2: screen_copy_bitmap_p4(scr, x, y, src, &draw_box, has_alpha); break; + case 2: return screen_copy_bitmap_p4(scr, x, y, src, &draw_box, has_alpha); case 5: screen_copy_bitmap_32(scr, x, y, src, &draw_box, has_alpha); break; default: From d4ba9d32b00f65abb10b6f98519eb7f3b2fb2a12 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Wed, 9 Sep 2026 11:34:12 +0100 Subject: [PATCH 63/98] test(wuss): wrap the icons menu strip in a captioned frame Every other group in the icons task sits inside a grouping FRAME; the menu strip was the only one floating loose. Add a "Menu" frame around it, inset the entries by 10px to match the others, and bump the group's spec count and y advance for the surround. --- libraries/wuss/test/tasks/icons.c | 37 ++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/libraries/wuss/test/tasks/icons.c b/libraries/wuss/test/tasks/icons.c index 1cc2f04..44cb800 100644 --- a/libraries/wuss/test/tasks/icons.c +++ b/libraries/wuss/test/tasks/icons.c @@ -40,7 +40,7 @@ enum ICONS_N_ICONSET = 5, /* frame + opton + optoff + radon + radoff from the loaded set */ ICONS_N_PATTERN = 2, /* frame + one PATTERN swatch */ ICONS_N_BORDERS = 6, /* frame + GROOVE + RIDGE + ACTION + DIVIDER labels */ - ICONS_N_MENU = 7, /* plain, ticked, swatch, submenu, disabled, rule, separator entry */ + ICONS_N_MENU = 8, /* frame + plain, ticked, swatch, submenu, disabled, rule, separator entry */ ICONS_NSPECS = ICONS_N_INTRO + ICONS_N_BUTTONS + ICONS_N_RADIOS + ICONS_N_BITMAPS + ICONS_N_ICONSET + ICONS_N_PATTERN + ICONS_N_BORDERS + ICONS_N_MENU @@ -416,18 +416,29 @@ static void icons_add_borders(icons_layout_t *lay) lay->y = top + 166; } -/* A menu-entry strip: plain, ticked, a swatch entry, a submenu entry, a - * disabled entry, then a dashed rule and a SEPARATOR-flagged entry below it. - * Hover the pointer over any live entry to see the highlight track; the rule - * stays inert. Returns the "Show grid" index (started ticked) via *ticked. */ +/* A grouping frame captioned "Menu", holding a menu-entry strip: plain, + * ticked, a swatch entry, a submenu entry, a disabled entry, then a dashed + * rule and a SEPARATOR-flagged entry below it. Hover the pointer over any live + * entry to see the highlight track; the rule stays inert. Returns the "Show + * grid" index (started ticked) via *ticked. */ static void icons_add_menu(icons_layout_t *lay, int *ticked) { wuss_icon_spec_t *s; int top; + int row; top = lay->y; s = &lay->specs[lay->n]; - s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN, top, 180, 16); + s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN, top, 200, ICONS_ROW * 7 + 30); + s->type = wuss_ICON_TYPE_FRAME; + s->text = "Menu"; + s->fg = lay->black; + s->bg = wuss_NO_BACKGROUND; + lay->n++; + + row = top + 20; + s = &lay->specs[lay->n]; + s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN + 10, row, 180, 16); s->type = wuss_ICON_TYPE_MENU_ENTRY; s->text = "Open"; s->fg = lay->black; @@ -435,7 +446,7 @@ static void icons_add_menu(icons_layout_t *lay, int *ticked) lay->n++; s = &lay->specs[lay->n]; - s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN, top + ICONS_ROW, 180, 16); + s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN + 10, row + ICONS_ROW, 180, 16); s->type = wuss_ICON_TYPE_MENU_ENTRY; s->text = "Show grid"; s->fg = lay->black; @@ -444,7 +455,7 @@ static void icons_add_menu(icons_layout_t *lay, int *ticked) lay->n++; s = &lay->specs[lay->n]; - s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN, top + ICONS_ROW * 2, 180, 16); + s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN + 10, row + ICONS_ROW * 2, 180, 16); s->type = wuss_ICON_TYPE_MENU_ENTRY; s->text = "Layer colour"; s->fg = lay->black; @@ -454,7 +465,7 @@ static void icons_add_menu(icons_layout_t *lay, int *ticked) lay->n++; s = &lay->specs[lay->n]; - s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN, top + ICONS_ROW * 3, 180, 16); + s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN + 10, row + ICONS_ROW * 3, 180, 16); s->type = wuss_ICON_TYPE_MENU_ENTRY; s->text = "Export"; s->fg = lay->black; @@ -463,7 +474,7 @@ static void icons_add_menu(icons_layout_t *lay, int *ticked) lay->n++; s = &lay->specs[lay->n]; - s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN, top + ICONS_ROW * 4, 180, 16); + s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN + 10, row + ICONS_ROW * 4, 180, 16); s->type = wuss_ICON_TYPE_MENU_ENTRY; s->text = "Disabled"; s->fg = lay->black; @@ -472,14 +483,14 @@ static void icons_add_menu(icons_layout_t *lay, int *ticked) lay->n++; s = &lay->specs[lay->n]; - s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN, top + ICONS_ROW * 5, 180, 10); + s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN + 10, row + ICONS_ROW * 5, 180, 10); s->type = wuss_ICON_TYPE_RULE; s->fg = lay->black; s->bg = wuss_NO_BACKGROUND; lay->n++; s = &lay->specs[lay->n]; - s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN, top + ICONS_ROW * 6, 180, 16); + s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN + 10, row + ICONS_ROW * 6, 180, 16); s->type = wuss_ICON_TYPE_MENU_ENTRY; s->text = "Quit"; s->fg = lay->black; @@ -487,7 +498,7 @@ static void icons_add_menu(icons_layout_t *lay, int *ticked) s->flags = wuss_ICON_FLAGS_SEPARATOR; lay->n++; - lay->y = top + ICONS_ROW * 7 + 10; + lay->y = top + ICONS_ROW * 7 + 46; } /* ----------------------------------------------------------------------- */ From ed320c94f25dc4d27f7f92a09d8d99783ca2936a Mon Sep 17 00:00:00 2001 From: David Thomas Date: Wed, 9 Sep 2026 12:11:01 +0100 Subject: [PATCH 64/98] test: add smred test image Co-Authored-By: Claude Sonnet 5 --- resources/images/smred.png | Bin 0 -> 343671 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 resources/images/smred.png diff --git a/resources/images/smred.png b/resources/images/smred.png new file mode 100644 index 0000000000000000000000000000000000000000..a7681c09b3e8f714f9a22e6e8273803782737c61 GIT binary patch literal 343671 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�`vD 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%0A9aZvQ~&O;J80$i>td|HQy8^AqzH0{n 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 JPDHLkV1gLm495Tf literal 0 HcmV?d00001 From 41db2cf5d3601dc23ebf8c8a716d232600c175a8 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Wed, 9 Sep 2026 13:01:02 +0100 Subject: [PATCH 65/98] fix(wuss): recalc furniture layout on toggle-size no-blit path wuss__furniture_invalidate_for is the only place the cached furniture layout is dropped. Toggle-size's no-blit fallback (and the scroll-reclamp path that funnels into it) repainted the old+new footprint but never called it, so the furniture layout stayed cached at the pre-toggle geometry -- titlebar, outline and scrollbars drew at the old size. The blit fast path already invalidated furniture; this path now does too. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/furniture/toggle-action.c | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/libraries/wuss/furniture/toggle-action.c b/libraries/wuss/furniture/toggle-action.c index 69b5c6b..18c0246 100644 --- a/libraries/wuss/furniture/toggle-action.c +++ b/libraries/wuss/furniture/toggle-action.c @@ -148,12 +148,16 @@ void wuss__furniture_toggle_size(wuss_window_t *window) else { /* No blit: the window is not topmost, its content self-lays-out - * (NO_RESIZE_BLIT), the old and new content boxes do not overlap, or - * screen_copy_rect declined. Repaint the whole union of old and new - * footprint. */ + * (NO_RESIZE_BLIT), the old and new content boxes do not overlap, + * screen_copy_rect declined, or the toggle re-clamped scroll. Repaint + * the whole union of old and new footprint, and drop the cached + * furniture layout so it rebuilds at the new geometry -- the blit path + * above gets this via wuss__furniture_invalidate, this path otherwise + * would not. */ logf_info("%s", "wuss furniture toggle: no blit fast path, repainting the " "whole old+new footprint"); box_union(&before, &window->visible, &dirty); wuss__invalidate_clipped(window, &dirty); + wuss__furniture_invalidate(window); } } From 7556f2d5007ee633c83efd6fe5103e19ff2c6e9d Mon Sep 17 00:00:00 2001 From: David Thomas Date: Wed, 9 Sep 2026 13:07:21 +0100 Subject: [PATCH 66/98] feat(wuss): make screen depth and startup palette command-line options The demo's framebuffer format was a compile-time WUSS_SDL_32BPP define in the SDL backend, and the startup palette came from a WUSS_PALETTE env var. Both are now command-line flags on the wuss app: -depth <4|32> (default 4, paletted p4 as before; 32 selects bgrx8888) and -palette NAME (default PICO-8). wuss_frontend_open gains an int depth parameter; the SDL backend stashes it, rejects anything but 4 or 32, and branches at run time in open() and present(). The RISC OS backend (locked to 16-colour mode) ignores it. The depth is an int rather than a bool so other pixel depths can slot in later. Co-Authored-By: Claude Sonnet 5 --- apps/wuss/frontend-riscos.c | 5 +++ apps/wuss/frontend-sdl.c | 62 ++++++++++++++++++++----------------- apps/wuss/frontend.h | 7 +++++ apps/wuss/main.c | 28 ++++++++++------- 4 files changed, 62 insertions(+), 40 deletions(-) diff --git a/apps/wuss/frontend-riscos.c b/apps/wuss/frontend-riscos.c index 7b225bd..6988966 100644 --- a/apps/wuss/frontend-riscos.c +++ b/apps/wuss/frontend-riscos.c @@ -88,6 +88,7 @@ result_t wuss_frontend_open(int width, int height, const colour_t *palette, int npalette, + int depth, void **pixels, int *rowbytes, pixelfmt_t *fmt, @@ -99,6 +100,10 @@ result_t wuss_frontend_open(int width, _kernel_oserror *err; char msg[160]; /* deferred so it prints after the mode restore */ + /* this backend is locked to the 16-colour Wimp mode; -depth only makes + * sense for the SDL backend */ + NOT_USED(depth); + msg[0] = '\0'; fe = calloc(1, sizeof(*fe)); diff --git a/apps/wuss/frontend-sdl.c b/apps/wuss/frontend-sdl.c index c6ea89b..cdeae70 100644 --- a/apps/wuss/frontend-sdl.c +++ b/apps/wuss/frontend-sdl.c @@ -22,10 +22,11 @@ #include "frontend.h" -/* Screen pixel format for the demo: 1 = 32bpp pixelfmt_bgrx8888 (feeds SDL - * directly, no per-frame conversion); 0 = pixelfmt_p4 paletted (exercises - * screen_copy_rect's nibble-packed blit path instead). */ -#define WUSS_SDL_32BPP 0 +/* Screen pixel format for the demo, chosen at run time via -depth: 32 = + * pixelfmt_bgrx8888 (feeds SDL directly, no per-frame conversion); 4 (the + * default) = pixelfmt_p4 paletted (exercises screen_copy_rect's nibble-packed + * blit path instead). No other depth is handled yet. Stashed in struct + * wuss_frontend so present() can branch on it. */ /* Integer window zoom: the fixed Wuss screen is drawn at this many device * pixels per screen pixel. F2 steps it up, Shift-F2 down, clamped to @@ -44,6 +45,7 @@ struct wuss_frontend int scr_width; int scr_height; int scale; /* device pixels per screen pixel; see WUSS_SDL_*_SCALE */ + int depth; /* framebuffer bits per pixel: 32 (bgrx8888) or 4 (p4) */ void *pixels; /* the private framebuffer handed to the caller */ }; @@ -83,6 +85,7 @@ result_t wuss_frontend_open(int width, int height, const colour_t *palette, int npalette, + int depth, void **pixels, int *rowbytes, pixelfmt_t *fmt, @@ -94,11 +97,14 @@ result_t wuss_frontend_open(int width, NOT_USED(palette); NOT_USED(npalette); -#if WUSS_SDL_32BPP - stride = width * 4; /* pixelfmt_bgrx8888: 4 bytes/pixel */ -#else - stride = width / 2; /* pixelfmt_p4: 2 pixels/byte */ -#endif + if (depth != 4 && depth != 32) + { + fprintf(stderr, "Error: unsupported -depth %d (want 4 or 32)\n", depth); + return result_BAD_ARG; + } + + stride = (depth == 32) ? width * 4 /* pixelfmt_bgrx8888: 4 bytes/pixel */ + : width / 2; /* pixelfmt_p4: 2 pixels/byte */ fe = calloc(1, sizeof(*fe)); if (fe == NULL) @@ -107,6 +113,7 @@ result_t wuss_frontend_open(int width, fe->scr_width = width; fe->scr_height = height; fe->scale = WUSS_SDL_DEFAULT_SCALE; + fe->depth = depth; fe->pixels = malloc((size_t) stride * height); if (fe->pixels == NULL) @@ -148,11 +155,7 @@ result_t wuss_frontend_open(int width, /* keep pixels crisp when F2 scales the window up */ SDL_SetTextureScaleMode(fe->texture, SDL_SCALEMODE_NEAREST); -#if WUSS_SDL_32BPP - *fmt = pixelfmt_bgrx8888; -#else - *fmt = pixelfmt_p4; -#endif + *fmt = (depth == 32) ? pixelfmt_bgrx8888 : pixelfmt_p4; *pixels = fe->pixels; *rowbytes = stride; @@ -274,22 +277,25 @@ bool wuss_frontend_poll(wuss_frontend_t *fe, wuss_input_t *event) void wuss_frontend_present(wuss_frontend_t *fe, const bitmap_t *bm) { -#if WUSS_SDL_32BPP - SDL_UpdateTexture(fe->texture, NULL, bm->base, bm->rowbytes); -#else - bitmap_t *disp; - - /* wuss draws into a paletted bitmap; SDL wants bgrx. bitmap_convert reads - * the palette straight off `bm`, which the caller updates when the palette - * task's picker menu changes it, so a live palette change just shows up in - * the next converted frame. */ - if (bitmap_convert(bm, pixelfmt_bgrx8888, &disp) == result_OK) + if (fe->depth == 32) { - SDL_UpdateTexture(fe->texture, NULL, disp->base, disp->rowbytes); - free(disp->base); - free(disp); + SDL_UpdateTexture(fe->texture, NULL, bm->base, bm->rowbytes); + } + else + { + bitmap_t *disp; + + /* wuss draws into a paletted bitmap; SDL wants bgrx. bitmap_convert reads + * the palette straight off `bm`, which the caller updates when the palette + * task's picker menu changes it, so a live palette change just shows up in + * the next converted frame. */ + if (bitmap_convert(bm, pixelfmt_bgrx8888, &disp) == result_OK) + { + SDL_UpdateTexture(fe->texture, NULL, disp->base, disp->rowbytes); + free(disp->base); + free(disp); + } } -#endif SDL_RenderTexture(fe->renderer, fe->texture, NULL, NULL); SDL_RenderPresent(fe->renderer); diff --git a/apps/wuss/frontend.h b/apps/wuss/frontend.h index 8729da1..592cdf9 100644 --- a/apps/wuss/frontend.h +++ b/apps/wuss/frontend.h @@ -52,6 +52,12 @@ wuss_input_t; * `palette` / `npalette` are the initial system palette: a backend that owns * the physical palette (RISC OS 16-colour mode) programmes it here. * + * `depth` is the framebuffer's bits per pixel: 32 for a direct-colour + * surface (no per-frame conversion), 4 for a paletted one (exercises the + * nibble-packed blit path). Those are the only values the SDL backend + * accepts today; a backend with a fixed format (RISC OS 16-colour mode) + * ignores it. + * * On return `*pixels` points at storage for width*height pixels at * `*rowbytes` stride, and `*fmt` is the pixel format that storage expects. * The caller wraps this in a bitmap_t and hands it to wuss. The backend may @@ -63,6 +69,7 @@ result_t wuss_frontend_open(int width, int height, const colour_t *palette, int npalette, + int depth, void **pixels, int *rowbytes, pixelfmt_t *fmt, diff --git a/apps/wuss/main.c b/apps/wuss/main.c index 8b039ea..c82d357 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -233,7 +233,9 @@ static void wuss_frame(void *arg) * picker menu swaps the system palette live (wuss_set_palette, picked up by * menu_handle's wuss_EVENT_PALETTE case); the quit input or closing the * window exits */ -static result_t run_wuss(const char *resources) +static result_t run_wuss(const char *resources, + const char *palette_name, + int depth) { const int scr_width = 640; const int scr_height = 480; @@ -258,15 +260,11 @@ static result_t run_wuss(const char *resources) int palette_index; { - /* WUSS_PALETTE names a *.hex file under resources/palettes (extension - * stripped, e.g. "RISC-OS"); default is PICO-8. Chrome was never derived - * from *.hex content (see fill_chrome_config), so it stays keyed by - * whether the startup file is "RISC-OS" specifically, not by whatever - * the picker menu later loads. */ - const char *palette_name = getenv("WUSS_PALETTE"); - - if (palette_name == NULL) - palette_name = "PICO-8"; + /* palette_name (from -palette, default "PICO-8") names a *.hex file under + * resources/palettes, extension stripped, e.g. "RISC-OS". Chrome was + * never derived from *.hex content (see fill_chrome_config), so it stays + * keyed by whether the startup file is "RISC-OS" specifically, not by + * whatever the picker menu later loads. */ use_wimp16 = (strcmp(palette_name, "RISC-OS") == 0); palette_index = use_wimp16 ? 1 : 0; @@ -303,7 +301,7 @@ static result_t run_wuss(const char *resources) } rc = wuss_frontend_open(scr_width, scr_height, palette, NELEMS(palette), - &pixels, &rowbytes, &fmt, &frontend); + depth, &pixels, &rowbytes, &fmt, &frontend); logf_info("wuss: wuss_frontend_open -> rc=0x%X (%s)", rc, result_string(rc)); if (rc != result_OK) goto Failure; @@ -425,14 +423,20 @@ int main(int argc, char *argv[]) #else const char *resources = "."; #endif + const char *palette_name = "PICO-8"; + int depth = 4; int i; result_t rc; for (i = 1; i < argc; i++) if (strcmp(argv[i], "-resources") == 0 && i + 1 < argc) resources = argv[++i]; + else if (strcmp(argv[i], "-palette") == 0 && i + 1 < argc) + palette_name = argv[++i]; + else if (strcmp(argv[i], "-depth") == 0 && i + 1 < argc) + depth = atoi(argv[++i]); - rc = run_wuss(resources); + rc = run_wuss(resources, palette_name, depth); return rc == result_TEST_PASSED ? EXIT_SUCCESS : EXIT_FAILURE; } From f816f52e5db673547a7d94da2da4e068b4c47bce Mon Sep 17 00:00:00 2001 From: David Thomas Date: Wed, 9 Sep 2026 13:27:34 +0100 Subject: [PATCH 67/98] docs(wuss): update TODO Drop resolved ColourTrans and backdrop-colour items. Note image-expand corruption bug and bodged bmfont metrics. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/TODO.txt | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/libraries/wuss/TODO.txt b/libraries/wuss/TODO.txt index 1d7321a..549a6a1 100644 --- a/libraries/wuss/TODO.txt +++ b/libraries/wuss/TODO.txt @@ -10,16 +10,13 @@ LAYER 0 - BUGS (fix before building on top) - Auto window placement packer still not working as expected blocks: reliable multi-window tasks - Image task not shrinking window when new image loads -- Backdrop colours reversed? - check - +- Saw corruption when loading large image then expanding window size LAYER 1 - CORE PLUMBING (no wuss deps; unblocks everything else) - Masks: how to get (alpha) masks into the system? Another channel, fixed value, or a second bitmap? Decide the model first. blocks: Draw bitmap through mask, Masked sprites/bitmaps, furniture tinting -- ColourTrans - blocks: Sprites, palette images, Support palette images - UTF-8 everywhere blocks: text abstraction, window/menu templates - Is having font handling in wuss-core a bad thing? Decide arch now. @@ -30,11 +27,11 @@ LAYER 2 - TEXT & FONTS (needs UTF-8, font-arch decision) - Wuss text abstraction (wraps bmfont, supplies the Wuss font(s)) - How to get bold etc. into the system? How to select symbols? -- bmfont metrics: baselines and leading - button labels look too high +- bmfont metrics: baselines and leading - button labels look too high (presently bodged) - More symbols -LAYER 3 - SPRITES (needs masks, ColourTrans) +LAYER 3 - SPRITES (needs masks) - Sprites (with names, with masks, with multiple sprites) - Masked sprites/bitmaps. Covered by RLE work? From d8fc5f2cff336027e53bcba280d5008fde2a282e Mon Sep 17 00:00:00 2001 From: David Thomas Date: Wed, 9 Sep 2026 14:26:00 +0100 Subject: [PATCH 68/98] feat(wuss): add --scale option and switch the demo to getopt_long The wuss demo's command line grew a hand-rolled strcmp scan for -resources/-palette/-depth. Add a -s/--scale option to set the initial SDL window zoom (0 = backend default; F2/Shift-F2 still adjust it live), threaded through run_wuss and wuss_frontend_open alongside depth. The RISC OS backend ignores it (fixed 1:1 Wimp-mode window). Desktop builds now parse with getopt_long: short -r/-p/-d/-s plus --resources/--palette/--depth/--scale, with a usage string on error. GCCSDK newlib lacks getopt_long, so main.c keeps the old strcmp fallback under #ifdef __riscos (single-dash long spellings only). BREAKING CHANGE: desktop no longer accepts the single-dash long forms (-resources); use --resources or -r. Co-Authored-By: Claude Sonnet 5 --- apps/wuss/frontend-riscos.c | 6 +- apps/wuss/frontend-sdl.c | 10 +++- apps/wuss/frontend.h | 5 ++ apps/wuss/main.c | 114 +++++++++++++++++++++++++++++++----- 4 files changed, 115 insertions(+), 20 deletions(-) diff --git a/apps/wuss/frontend-riscos.c b/apps/wuss/frontend-riscos.c index 6988966..f783daa 100644 --- a/apps/wuss/frontend-riscos.c +++ b/apps/wuss/frontend-riscos.c @@ -89,6 +89,7 @@ result_t wuss_frontend_open(int width, const colour_t *palette, int npalette, int depth, + int scale, void **pixels, int *rowbytes, pixelfmt_t *fmt, @@ -100,9 +101,10 @@ result_t wuss_frontend_open(int width, _kernel_oserror *err; char msg[160]; /* deferred so it prints after the mode restore */ - /* this backend is locked to the 16-colour Wimp mode; -depth only makes - * sense for the SDL backend */ + /* this backend is locked to the 16-colour Wimp mode with a fixed 1:1 + * window; -depth and -scale only make sense for the SDL backend */ NOT_USED(depth); + NOT_USED(scale); msg[0] = '\0'; diff --git a/apps/wuss/frontend-sdl.c b/apps/wuss/frontend-sdl.c index cdeae70..c40755d 100644 --- a/apps/wuss/frontend-sdl.c +++ b/apps/wuss/frontend-sdl.c @@ -29,7 +29,8 @@ * wuss_frontend so present() can branch on it. */ /* Integer window zoom: the fixed Wuss screen is drawn at this many device - * pixels per screen pixel. F2 steps it up, Shift-F2 down, clamped to + * pixels per screen pixel. The initial value comes from -scale (0 => use the + * default here); F2 steps it up, Shift-F2 down, clamped to * [WUSS_SDL_MIN_SCALE, WUSS_SDL_MAX_SCALE]. */ #define WUSS_SDL_DEFAULT_SCALE 2 #define WUSS_SDL_MIN_SCALE 1 @@ -86,6 +87,7 @@ result_t wuss_frontend_open(int width, const colour_t *palette, int npalette, int depth, + int scale, void **pixels, int *rowbytes, pixelfmt_t *fmt, @@ -103,6 +105,10 @@ result_t wuss_frontend_open(int width, return result_BAD_ARG; } + if (scale <= 0) + scale = WUSS_SDL_DEFAULT_SCALE; + scale = CLAMP(scale, WUSS_SDL_MIN_SCALE, WUSS_SDL_MAX_SCALE); + stride = (depth == 32) ? width * 4 /* pixelfmt_bgrx8888: 4 bytes/pixel */ : width / 2; /* pixelfmt_p4: 2 pixels/byte */ @@ -112,7 +118,7 @@ result_t wuss_frontend_open(int width, fe->scr_width = width; fe->scr_height = height; - fe->scale = WUSS_SDL_DEFAULT_SCALE; + fe->scale = scale; fe->depth = depth; fe->pixels = malloc((size_t) stride * height); diff --git a/apps/wuss/frontend.h b/apps/wuss/frontend.h index 592cdf9..4421d2e 100644 --- a/apps/wuss/frontend.h +++ b/apps/wuss/frontend.h @@ -58,6 +58,10 @@ wuss_input_t; * accepts today; a backend with a fixed format (RISC OS 16-colour mode) * ignores it. * + * `scale` is the initial integer window zoom (device pixels per screen + * pixel) for backends with a resizable window; <= 0 means "backend default". + * The RISC OS backend ignores it. + * * On return `*pixels` points at storage for width*height pixels at * `*rowbytes` stride, and `*fmt` is the pixel format that storage expects. * The caller wraps this in a bitmap_t and hands it to wuss. The backend may @@ -70,6 +74,7 @@ result_t wuss_frontend_open(int width, const colour_t *palette, int npalette, int depth, + int scale, void **pixels, int *rowbytes, pixelfmt_t *fmt, diff --git a/apps/wuss/main.c b/apps/wuss/main.c index c82d357..93c5f00 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -4,6 +4,12 @@ #include #include +/* RISC OS's GCCSDK newlib has getopt() but not the GNU getopt_long() + * extension, so parse_args() keeps a hand-rolled fallback there. */ +#ifndef __riscos +#include +#endif + #ifdef __EMSCRIPTEN__ #include #endif @@ -235,7 +241,8 @@ static void wuss_frame(void *arg) * window exits */ static result_t run_wuss(const char *resources, const char *palette_name, - int depth) + int depth, + int scale) { const int scr_width = 640; const int scr_height = 480; @@ -301,7 +308,7 @@ static result_t run_wuss(const char *resources, } rc = wuss_frontend_open(scr_width, scr_height, palette, NELEMS(palette), - depth, &pixels, &rowbytes, &fmt, &frontend); + depth, scale, &pixels, &rowbytes, &fmt, &frontend); logf_info("wuss: wuss_frontend_open -> rc=0x%X (%s)", rc, result_string(rc)); if (rc != result_OK) goto Failure; @@ -413,30 +420,105 @@ static result_t run_wuss(const char *resources, /* ----------------------------------------------------------------------- */ +/* Parsed command-line options. Members are use-ordered to match run_wuss's + * parameter list. */ +typedef struct wuss_options +{ + const char *resources; /* -r/--resources: fixture root */ + const char *palette_name; /* -p/--palette: startup *.hex leafname */ + int depth; /* -d/--depth: framebuffer bpp (4 or 32) */ + int scale; /* -s/--scale: initial window zoom, 0 = default */ +} +wuss_options_t; + +static const char wuss_usage[] = + "usage: wuss [-r|--resources DIR] [-p|--palette NAME] " + "[-d|--depth 4|32] [-s|--scale N]\n"; + +#ifndef __riscos + +/* Desktop: getopt_long. Accepts the short forms and the "--" long forms; the + * historical single-dash long spellings (-resources) are no longer accepted. + * Returns false and prints usage on an unknown option or missing argument. */ +static bool parse_args(int argc, char *argv[], wuss_options_t *opts) +{ + static const struct option longopts[] = + { + { "resources", required_argument, NULL, 'r' }, + { "palette", required_argument, NULL, 'p' }, + { "depth", required_argument, NULL, 'd' }, + { "scale", required_argument, NULL, 's' }, + { NULL, 0, NULL, 0 } + }; + + int c; + + for (;;) + { + c = getopt_long(argc, argv, "r:p:d:s:", longopts, NULL); + if (c == -1) + break; + + switch (c) + { + case 'r': opts->resources = optarg; break; + case 'p': opts->palette_name = optarg; break; + case 'd': opts->depth = atoi(optarg); break; + case 's': opts->scale = atoi(optarg); break; + default: + fputs(wuss_usage, stderr); + return false; + } + } + + return true; +} + +#else /* __riscos */ + +/* RISC OS: no getopt_long. Hand-rolled scan of the same options, single-dash + * long spellings only (matches the pre-getopt behaviour). */ +static bool parse_args(int argc, char *argv[], wuss_options_t *opts) +{ + int i; + + for (i = 1; i < argc; i++) + if (strcmp(argv[i], "-resources") == 0 && i + 1 < argc) + opts->resources = argv[++i]; + else if (strcmp(argv[i], "-palette") == 0 && i + 1 < argc) + opts->palette_name = argv[++i]; + else if (strcmp(argv[i], "-depth") == 0 && i + 1 < argc) + opts->depth = atoi(argv[++i]); + else if (strcmp(argv[i], "-scale") == 0 && i + 1 < argc) + opts->scale = atoi(argv[++i]); + + return true; +} + +#endif /* __riscos */ + int main(int argc, char *argv[]) { /* path_join_filename splices the root and each branch with the platform * separator, so the "here" root differs: "." on Unix, but on RISC OS the * currently-selected directory is "@" ("." there would give "..resources"). */ #ifdef __riscos - const char *resources = "@"; + const char *default_resources = "@"; #else - const char *resources = "."; + const char *default_resources = "."; #endif - const char *palette_name = "PICO-8"; - int depth = 4; - int i; - result_t rc; + wuss_options_t opts; + result_t rc; - for (i = 1; i < argc; i++) - if (strcmp(argv[i], "-resources") == 0 && i + 1 < argc) - resources = argv[++i]; - else if (strcmp(argv[i], "-palette") == 0 && i + 1 < argc) - palette_name = argv[++i]; - else if (strcmp(argv[i], "-depth") == 0 && i + 1 < argc) - depth = atoi(argv[++i]); + opts.resources = default_resources; + opts.palette_name = "PICO-8"; + opts.depth = 4; + opts.scale = 0; /* 0 = let the frontend pick its default */ + + if (!parse_args(argc, argv, &opts)) + return EXIT_FAILURE; - rc = run_wuss(resources, palette_name, depth); + rc = run_wuss(opts.resources, opts.palette_name, opts.depth, opts.scale); return rc == result_TEST_PASSED ? EXIT_SUCCESS : EXIT_FAILURE; } From 3a6023c7a40d994adec07b010d61157f161cbef4 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Wed, 9 Sep 2026 17:35:18 +0100 Subject: [PATCH 69/98] feat(wuss): blit icon-set bitmaps for radio and option icons When the loaded icon set carries radon/radoff (RADIO) or opton/optoff (OPTION), blit that bitmap for the control's current state, centred in the glyph square and clipped to it. The hand-drawn square ring / box-and-tick stays as the fallback when the set is absent or lacks the entry. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/icon/draw.c | 49 ++++++++++++++++++++++++++++++++++---- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/libraries/wuss/icon/draw.c b/libraries/wuss/icon/draw.c index 4a351ab..954e77a 100644 --- a/libraries/wuss/icon/draw.c +++ b/libraries/wuss/icon/draw.c @@ -362,10 +362,47 @@ static void wuss__icon_draw_button(const icon_draw_ctx_t *c) /* ----------------------------------------------------------------------- */ +/* Blit the icon-set bitmap for a radio/option's current state, centred in the + * glyph square "g", clipped to it. Names are radon/radoff for RADIO and + * opton/optoff for OPTION. Returns 1 when a bitmap was drawn, 0 when the set is + * absent or lacks that entry -- the caller then draws the vector glyph. */ +static int wuss__icon_blit_radio_option(const icon_draw_ctx_t *c, + const box_t *g) +{ + const char *name; + const bitmap_t *bm; + screen_t clipped; + int idx, bx, by; + + if (c->icon->type == wuss_ICON_TYPE_RADIO) + name = wuss__icon_selected(c->icon) ? "radon" : "radoff"; + else + name = wuss__icon_selected(c->icon) ? "opton" : "optoff"; + + idx = wuss_icons_lookup(c->wuss, name); + if (idx < 0) + return 0; + + bm = wuss_icons_bitmap(c->wuss, idx); + if (bm == NULL) + return 0; + + bx = g->x0 + ((g->x1 - g->x0) - bm->size.w) / 2; + by = g->y0 + ((g->y1 - g->y0) - bm->size.h) / 2; + + clipped = *c->scr; + if (box_intersection(&c->scr->clip, g, &clipped.clip)) + return 1; /* fully clipped away, but still "handled" -- no vector fallback */ + + screen_copy_bitmap(&clipped, bx, by, bm); + return 1; +} + /* wuss_ICON_TYPE_RADIO and wuss_ICON_TYPE_OPTION: a font-height square glyph at - * the left, vertically centred, with the label to its right. RADIO draws a - * square ring with a solid centre when selected; OPTION draws a box with a tick - * when selected. */ + * the left, vertically centred, with the label to its right. If the icon set + * carries radon/radoff (RADIO) or opton/optoff (OPTION) those bitmaps are + * blitted for the state; otherwise RADIO draws a square ring with a solid + * centre when selected and OPTION draws a box with a tick when selected. */ static void wuss__icon_draw_radio_option(const icon_draw_ctx_t *c) { const wuss_icon_t *icon = c->icon; @@ -392,7 +429,11 @@ static void wuss__icon_draw_radio_option(const icon_draw_ctx_t *c) screen_fill_rect(c->scr, b->x0, b->y0, SIZE2D(b->x1 - b->x0, b->y1 - b->y0), bg); - if (icon->type == wuss_ICON_TYPE_RADIO) + if (wuss__icon_blit_radio_option(c, &g)) + { + /* bitmap drawn -- fall through to the label */ + } + else if (icon->type == wuss_ICON_TYPE_RADIO) { /* a square ring (no circle primitive); a solid centre when selected */ screen_draw_line(c->scr, g.x0 + 2, g.y0, g.x1 - 3, g.y0, glyph); From 5f250b0bbb01e816c6d29b84a2a33039412f5f7b Mon Sep 17 00:00:00 2001 From: David Thomas Date: Wed, 9 Sep 2026 17:40:17 +0100 Subject: [PATCH 70/98] assets(wuss): update Digits bmfont PNGs --- resources/bmfonts/Digits-Bold.png | Bin 859 -> 930 bytes resources/bmfonts/Digits-Regular.png | Bin 903 -> 967 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/resources/bmfonts/Digits-Bold.png b/resources/bmfonts/Digits-Bold.png index 6b798bb5c8cac9592c6b0a723f5fd2cac7f29a43..a884102a83733abb447a87f1bd30577cf49f2498 100644 GIT binary patch delta 811 zcmV+`1JwN62BHU$gnt7FNkly z4=}*!65VH6sQl^Y*qCI1)*u*#BK0Iud=iXT7~f$WFXQ+=j=2FaUPQ=pl?G`LJ*xkP zKz7!^E5HO0Nq(}-j@zVz7)js6p~%fc^x`iK`ip@Zs!68apMNa;gANb{!^3e8pPq+S zjE^^%dw>uCfi=nOtx=@7O)>&m41mMX9UL5e2SBT70cP=x8K8Jhz5^gP%x(rS$u1!+ zlSXuI$^hhteg)u0W6_A7d(qpm^|fNG6oyAJ9FhN7J0 zAk^gPMp1YLpnolz_b?wIAWRQAC;+;GC`$=>;<~aaZ&6u+0Ze5D9zGbLu7FO_oM#Yn z-Z)+w7#W8h*@vPBk$6gYPB?Jm&xth&1@hmsI3%{K zKiQWjcU8g@K3mR^{_&^Wuu=E#Eh~(hZj0VaW*3RI5`V|6F<9Mm4xQB?rw;wXGd4+P z6)f#2yM*~@fdA2#dp;_593dM+Sg-=-x)ZV9Tc|9c^$WZGcLCfAz}yt`Q33Y*X6OO{ zUjszH0uX%#uv4S}=lLiI}f2C(x@u9_{vP-&|L(5W!NWdP<> z>17Of0DtiCVh+G4XnFw9n=0tNQ~)BTVMr(yPntMD`GAd5`rIHRe`2TJ!9tWFSmTK- zccBmzs!(>Jzb|mW_vAlN#bsIdzpwohK^qIISUxX$s;F;^tC62kpxhX9xw7;16rLl_ zdaX*=CnwnP!CC;sKKNCDSg7KSj%a@5D22sPpMO1=YjQz$)vbBIv}9KR4jy*lO(wG7 zfEu<<6<{^BMRxq8aB$eS0Pq?>bSnU}uPdOD5AI{iKog+$i@tZflI zyMkNj0LmrN0&tR(l)h3$6f%gJ4D^YhjY1VDZe-y`PZd!}<$kvJ2ak;BjS>%=2Qf6g pTO?6mQ$;*&mi~~M2h$T#e*uQLouy%AEj9oE002ovPDHLkV1fb2cC-Kh delta 739 zcmV<90v!FK2ipdagnt6TNklMs!w!8o8acyU2tPQT2`w`gUXqIkvh##q16#77kp+Ov1r2-jNU`USqQY=gqcw1V;0wj!6~O!q z^GgM=)L;R~8Gl4gCHTa1W}{DT$-n^0qQBPwyo+X^0eameUKI*809po14LAx?N-^2* zqf4nZJ+5jZZO>|n=R{qiR+K+yX<0Z>x}Es^N@7py%=Ych-Iefw&mvfNA${`?oFVq) ztz?Da&}}oS6Caiga%~B%rC{e?YS`Erl!6mRtUO~;WPfRbm%Uz9?Ucvew~B2?aAOD? zRvoowQpz#Voy4L`5rrSh%;<;rG$b2BiPIOV&C_lCHXlo&v zA*7`HVRi%v3*{wqviAm_Nz{|-WYRzEr(+^ zz00yuw)_=9tb@M-5PtY0CsSAq>$8W@GpNaKbt~R4CD~PXL%^$ZP`paQ4XVhuCTyu< zssT<-2>_o1h|Lbbj5h#0M`|hn!hL|*>~@m#6S-q=x3^k zN(Rx%pv`ZKHWP+X1*scbM5(DFDyfveNZr8>3x^vq6r)E`_o*VD508FZGT8%Be*yXq VosCnrh%W#D002ovPDHLkV1n9{P5b}= diff --git a/resources/bmfonts/Digits-Regular.png b/resources/bmfonts/Digits-Regular.png index 6219f9ff12177e35000c30e382e2ba9e80b550bf..de4cf73af9e66f29a0412f338f37a4f5d09a0b94 100755 GIT binary patch delta 848 zcmV-W1F!st2ge7Hgnt7qNkl>_At>%pGKqkHRSephpGGJN_V((Fb?GC zZa;N5j*Y`~f8+LW9e9L|@?`99O6puhziCDDr^!aYu%Q!8Z-1m{rYG#Wrx{IBs@P>F z86EY~%uQl>M8nn3$=F)z)B;qITBi$u43_;^Ps|Cl(Mw8I@dmIf0auh&@H{+2A!qHyzN?{TWa}+@w5zrwu z0%D>ZEk>mJv~#j=Rl1BI+!}{1iPRxSoCs?MAc-jC2&>5Z#^c)+f4$G!9n2bk@@+jk z+rn4ADw-{RH$_vlIs6rdu`XvX18UzzL>CP6u`3t|wtpWX|4kpca#;1Q6GRyd=f>U*H#WK;%aLy)M(bWg-} zkO|M}W}>AugJ@a30I@_>cl}Yc{6dS7jYS(lGnJ$Zum`P5)^kB!sO%8qd(l4NT~BU% zkp2S5j6u@-Oz0hpQAEp5bm~kZv5RFVP{oEmfzFObpGFk!sFR55`kF`t$@X0SED`7; zM@dhxkVuWnheXgWfPuRmI=BAy<_=OcOXf*u383Vc4Eh%g^Hz_%(Ng74RpYA&WJ{` zzSciY4}uW3dw+)^#~>@jdwd!KXcR$)U&fgPdea-#gIbMR5LX0rltw^Il*w&Hs+XO` z{*ge~H7;9()KMcA!kGa`DvBCm6@}0QdH#lwOK`wXo?YOImT1{^}F=sYmjhhl(Y`wWFIdVm~uz3&WB#lZXVP=6T%-)1UaUdD>BYdYihcZ43X z2+%CZKw7BomT^etI0Vw!TFLg=iR>(Opw#uT*=4h}fYjqw^}8NzD3b-Wu%glC^C0sP zknTYGDhS^=0-;6EK(T8@8h0JEPN13v)hD3!$)~vpvI!&_voi-3L4NwQMhN(*iJ;}` zG6?ORH-9LVlTxuGs}cC2yawFFwNMqaxC zGGh>Gya3v^cUA(~Q_##NP|bn9`f2pvig0#SYy5Y!Ub($DQC{kpnIVSOnKa zw20$50fwjt7Rj4kYRCHABAEo)>8_izT{kx@vb)g!d3WUv9+WnE{sGL!!m6B&uz&yn O00{s|MNUMnLSTYbG<~Q5 From 0ba5456b8fe19206005ee261e1dd3a567207f393 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Wed, 9 Sep 2026 18:01:11 +0100 Subject: [PATCH 71/98] refactor(wuss)!: rename wuss_window_invalidate_all to _visible "all" misread as the whole document when the macro only ever covered the visible content rectangle -- wuss_window_invalidate_extent is the full-document one. Pure rename, same behaviour; all call sites and the CHANGELOG updated. While here, greeble's Select/Adjust/Menu-toggle paths regenerate the whole generator grid (doc is larger than the initial window) but were only invalidating the visible rectangle, leaving scrolled-away tiles stale after a click; switch those three to wuss_window_invalidate_extent. Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 6 +++++- include/wuss/window.h | 17 ++++++++++------- libraries/wuss/test/tasks/chars.c | 2 +- libraries/wuss/test/tasks/checker.c | 4 ++-- libraries/wuss/test/tasks/clock.c | 4 ++-- libraries/wuss/test/tasks/curve.c | 6 +++--- libraries/wuss/test/tasks/greeble.c | 6 +++--- libraries/wuss/test/tasks/lissajous.c | 4 ++-- libraries/wuss/test/tasks/minesweeper.c | 6 +++--- libraries/wuss/test/tasks/porter-duff.c | 6 +++--- libraries/wuss/test/tasks/saturn.c | 4 ++-- libraries/wuss/test/tasks/sofa.c | 6 +++--- libraries/wuss/test/tasks/swatches.c | 2 +- libraries/wuss/test/tasks/text.c | 4 ++-- 14 files changed, 42 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7609133..3ec94f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -100,7 +100,7 @@ _Unreleased_ until one is cut. a live icon per element. - `wuss_window_invalidate_extent()` — marks a window's whole virtual document extent dirty (`window->doc`), not just the visible content - rectangle `wuss_window_invalidate_all()` covers, so a change touching the + rectangle `wuss_window_invalidate_visible()` covers, so a change touching the whole document repaints correctly at any scroll position. - Clicking in a scrollbar well (not on the sausage) now pages the content one visible extent towards the click, keeping one `WUSS_SCROLL_STEP` of @@ -235,6 +235,10 @@ _Unreleased_ until one is cut. ### Changed +- **Breaking:** `wuss_window_invalidate_all()` is renamed + `wuss_window_invalidate_visible()` -- "all" misread as the whole document + when it only ever covered the visible content rectangle. Same behaviour; + use `wuss_window_invalidate_extent()` for the full document. - The `wuss` demo task launcher is split out of `apps/wuss/main.c` into `apps/wuss/tasks.c` behind `tasks.h`: the 19 `spawn_*` callbacks, every menu table and `task_handle_event` move over, and the shared file-scope diff --git a/include/wuss/window.h b/include/wuss/window.h index ef47e97..a491483 100644 --- a/include/wuss/window.h +++ b/include/wuss/window.h @@ -245,18 +245,21 @@ void wuss_window_get_content_bounds(const wuss_window_t *window, */ void wuss_window_invalidate(wuss_window_t *window, const box_t *local_box); -/** Mark a window's whole content area as dirty. Shorthand for +/** Mark a window's whole visible content rectangle as dirty -- not the + * parts of the document scrolled out of view; use + * wuss_window_invalidate_extent for that. Shorthand for * wuss_window_invalidate(window, NULL). */ -#define wuss_window_invalidate_all(window) \ +#define wuss_window_invalidate_visible(window) \ wuss_window_invalidate((window), NULL) /** * Mark a window's whole virtual document extent as dirty, not just the part - * currently on screen. wuss_window_invalidate_all only covers the visible - * content rectangle, which is enough when the document is no bigger than the - * window; use this when a change touches the whole document (a colour or - * theme swap, say) so every scroll position repaints correctly. The extent - * is clipped to what is actually visible before anything is queued. + * currently on screen. wuss_window_invalidate_visible only covers the + * visible content rectangle, which is enough when the document is no bigger + * than the window; use this when a change touches the whole document (a + * colour or theme swap, say) so every scroll position repaints correctly. + * The extent is clipped to what is actually visible before anything is + * queued. * * \param[in] window Window whose whole document changed. */ diff --git a/libraries/wuss/test/tasks/chars.c b/libraries/wuss/test/tasks/chars.c index 0198c6c..4156fcd 100644 --- a/libraries/wuss/test/tasks/chars.c +++ b/libraries/wuss/test/tasks/chars.c @@ -87,7 +87,7 @@ static result_t chars_set_font(chars_task_t *task, int idx, const char *name) task->current = idx; wuss_window_resize(task->window, chars_window_size(task, font)); - wuss_window_invalidate_all(task->window); + wuss_window_invalidate_visible(task->window); return result_OK; } diff --git a/libraries/wuss/test/tasks/checker.c b/libraries/wuss/test/tasks/checker.c index 2f4761b..a9c55eb 100644 --- a/libraries/wuss/test/tasks/checker.c +++ b/libraries/wuss/test/tasks/checker.c @@ -130,7 +130,7 @@ static result_t checker_mouse(wuss_window_t *window, void *task_data) pattern = (window == cc->window2) ? &cc->pattern2 : &cc->pattern; *pattern = (*pattern + 1) % checker_PATTERN__COUNT; - wuss_window_invalidate_all(window); + wuss_window_invalidate_visible(window); return result_OK; } @@ -148,7 +148,7 @@ static result_t checker_scroll(wuss_window_t *window, *band += delta; *band = CLAMP(*band, CHECKER_BAND_MIN, CHECKER_BAND_MAX); - wuss_window_invalidate_all(window); + wuss_window_invalidate_visible(window); return result_OK; } diff --git a/libraries/wuss/test/tasks/clock.c b/libraries/wuss/test/tasks/clock.c index 4a1ab5e..5c2e99c 100644 --- a/libraries/wuss/test/tasks/clock.c +++ b/libraries/wuss/test/tasks/clock.c @@ -214,7 +214,7 @@ static result_t clock_mouse(clock_task_t *cc, wuss_button_t button) if (button & wuss_BUTTON_SELECT) { cc->show_second = !cc->show_second; - wuss_window_invalidate_all(cc->window); + wuss_window_invalidate_visible(cc->window); } return result_OK; @@ -241,7 +241,7 @@ result_t clock_handle(wuss_window_t *window, return clock_mouse(cc, event->data.mouse.button); case wuss_EVENT_IDLE: - wuss_window_invalidate_all(cc->window); + wuss_window_invalidate_visible(cc->window); return result_OK; case wuss_EVENT_QUIT: diff --git a/libraries/wuss/test/tasks/curve.c b/libraries/wuss/test/tasks/curve.c index 8fbbcf6..13d83ef 100644 --- a/libraries/wuss/test/tasks/curve.c +++ b/libraries/wuss/test/tasks/curve.c @@ -208,7 +208,7 @@ static result_t curve_mouse(curve_task_t *task, task->npoints++; if (task->npoints > CURVE_MAXCONTROLPTS) task->npoints = CURVE_MINCONTROLPTS; - wuss_window_invalidate_all(window); + wuss_window_invalidate_visible(window); break; } if (!(button & wuss_BUTTON_SELECT)) @@ -228,7 +228,7 @@ static result_t curve_mouse(curve_task_t *task, break; task->points[task->dragging].x = x; task->points[task->dragging].y = y; - wuss_window_invalidate_all(window); + wuss_window_invalidate_visible(window); break; case wuss_MOUSE_UP: @@ -246,7 +246,7 @@ static result_t curve_scroll(curve_task_t *task, task->nsegments += delta; task->nsegments = CLAMP(task->nsegments, CURVE_SEGMENTS_MIN, CURVE_SEGMENTS_MAX); - wuss_window_invalidate_all(window); + wuss_window_invalidate_visible(window); return result_OK; } diff --git a/libraries/wuss/test/tasks/greeble.c b/libraries/wuss/test/tasks/greeble.c index 5a7a9f3..c2f9755 100644 --- a/libraries/wuss/test/tasks/greeble.c +++ b/libraries/wuss/test/tasks/greeble.c @@ -270,7 +270,7 @@ static result_t greeble_select(greeble_task_t *task, wuss_window_t *window) task->seed = 1; greeble_generate(task); - wuss_window_invalidate_all(window); + wuss_window_invalidate_extent(window); return result_OK; } @@ -285,7 +285,7 @@ static result_t greeble_toggle_randpal(greeble_task_t *task, { task->random_prefab_palettes = !task->random_prefab_palettes; greeble_generate(task); - wuss_window_invalidate_all(window); + wuss_window_invalidate_extent(window); return result_OK; } @@ -297,7 +297,7 @@ static result_t greeble_adjust(greeble_task_t *task, wuss_window_t *window) { task->palette = (unsigned char) ((task->palette + 1) % GREEBLE_NPALETTE); greeble_generate(task); - wuss_window_invalidate_all(window); + wuss_window_invalidate_extent(window); return result_OK; } diff --git a/libraries/wuss/test/tasks/lissajous.c b/libraries/wuss/test/tasks/lissajous.c index f6c7766..69a6c04 100644 --- a/libraries/wuss/test/tasks/lissajous.c +++ b/libraries/wuss/test/tasks/lissajous.c @@ -121,7 +121,7 @@ static result_t lissajous_mouse(wuss_window_t *window, lc->freq_index = (lc->freq_index + 1) % (int) NELEMS(lissajous_freqs); lc->a = lissajous_freqs[lc->freq_index][0]; lc->b = lissajous_freqs[lc->freq_index][1]; - wuss_window_invalidate_all(lc->window); /* whole figure changes */ + wuss_window_invalidate_visible(lc->window); /* whole figure changes */ } else if (button & wuss_BUTTON_ADJUST) { @@ -143,7 +143,7 @@ static result_t lissajous_idle(void *task_data) else if (lc->phase < 0.0) lc->phase += 2.0 * M_PI; - wuss_window_invalidate_all(lc->window); /* ponytail: repaint whole content; figure fills it and is cheap */ + wuss_window_invalidate_visible(lc->window); /* ponytail: repaint whole content; figure fills it and is cheap */ return result_OK; } diff --git a/libraries/wuss/test/tasks/minesweeper.c b/libraries/wuss/test/tasks/minesweeper.c index 45d2f58..ceeeea2 100644 --- a/libraries/wuss/test/tasks/minesweeper.c +++ b/libraries/wuss/test/tasks/minesweeper.c @@ -397,7 +397,7 @@ static result_t minesweeper_mouse(minesweeper_task_t *ms, if (button & (wuss_BUTTON_SELECT | wuss_BUTTON_ADJUST)) { minesweeper_reset(ms); - wuss_window_invalidate_all(ms->window); + wuss_window_invalidate_visible(ms->window); } return result_OK; } @@ -442,7 +442,7 @@ static result_t minesweeper_mouse(minesweeper_task_t *ms, return result_OK; } - wuss_window_invalidate_all(ms->window); + wuss_window_invalidate_visible(ms->window); return result_OK; } @@ -487,7 +487,7 @@ result_t minesweeper_handle(wuss_window_t *window, case wuss_EVENT_MENU_SELECT: /* only item is "New Game" */ minesweeper_reset(ms); - wuss_window_invalidate_all(ms->window); + wuss_window_invalidate_visible(ms->window); return result_OK; case wuss_EVENT_QUIT: diff --git a/libraries/wuss/test/tasks/porter-duff.c b/libraries/wuss/test/tasks/porter-duff.c index 6c09319..24fadba 100644 --- a/libraries/wuss/test/tasks/porter-duff.c +++ b/libraries/wuss/test/tasks/porter-duff.c @@ -345,7 +345,7 @@ static result_t porter_duff_idle(void *task_data) } /* the ramp changes every frame, so the whole pane is stale every frame */ - wuss_window_invalidate_all(pd->window); + wuss_window_invalidate_visible(pd->window); return result_OK; } @@ -359,7 +359,7 @@ static result_t porter_duff_mouse(wuss_window_t *window, void *task_data) pd->rule = (pd->rule + 1) % composite_RULE__LIMIT; pd->frame = 0; - wuss_window_invalidate_all(window); + wuss_window_invalidate_visible(window); return result_OK; } @@ -377,7 +377,7 @@ static result_t porter_duff_scroll(wuss_window_t *window, PD_FRAMES_MIN, PD_FRAMES_MAX); pd->frame = MIN(pd->frame, pd->frames_per_rule); - wuss_window_invalidate_all(window); + wuss_window_invalidate_visible(window); return result_OK; } diff --git a/libraries/wuss/test/tasks/saturn.c b/libraries/wuss/test/tasks/saturn.c index 8e55f55..c28c4ea 100644 --- a/libraries/wuss/test/tasks/saturn.c +++ b/libraries/wuss/test/tasks/saturn.c @@ -191,7 +191,7 @@ static result_t saturn_redraw(const wuss_event_t *event, saturn_task_t *task) static result_t saturn_idle(saturn_task_t *task) { task->seed += 0x9E3779B9UL; /* churn: a fresh sketch every null event */ - wuss_window_invalidate_all(task->window); + wuss_window_invalidate_visible(task->window); return result_OK; } @@ -207,7 +207,7 @@ static result_t saturn_mouse(saturn_task_t *task, if (button & wuss_BUTTON_SELECT) { task->seed += 0x9E3779B9UL; /* fresh sketch */ - wuss_window_invalidate_all(window); + wuss_window_invalidate_visible(window); } return result_OK; diff --git a/libraries/wuss/test/tasks/sofa.c b/libraries/wuss/test/tasks/sofa.c index 0b04fa5..fce7065 100644 --- a/libraries/wuss/test/tasks/sofa.c +++ b/libraries/wuss/test/tasks/sofa.c @@ -521,7 +521,7 @@ static result_t sofa_mouse(wuss_window_t *window, { sc->shape = (sc->shape + 1) % sofa_SHAPE__LIMIT; sc->turns = 0; - wuss_window_invalidate_all(window); + wuss_window_invalidate_visible(window); } else if (button & wuss_BUTTON_SELECT) { @@ -542,7 +542,7 @@ static result_t sofa_scroll(wuss_window_t *window, sc->zoom += delta * SOFA_ZOOM_PER_NOTCH; sc->zoom = CLAMP(sc->zoom, SOFA_ZOOM_MIN, SOFA_ZOOM_MAX); - wuss_window_invalidate_all(window); + wuss_window_invalidate_visible(window); return result_OK; } @@ -567,7 +567,7 @@ static result_t sofa_idle(void *task_data) } } - wuss_window_invalidate_all(task->window); + wuss_window_invalidate_visible(task->window); return result_OK; } diff --git a/libraries/wuss/test/tasks/swatches.c b/libraries/wuss/test/tasks/swatches.c index af4869e..deb23d9 100644 --- a/libraries/wuss/test/tasks/swatches.c +++ b/libraries/wuss/test/tasks/swatches.c @@ -174,7 +174,7 @@ static result_t swatches_menu_select(swatches_task_t *task, task->paper = picked; /* every cell shares the new paper: dirty the whole document, not just the - * visible rectangle wuss_window_invalidate_all covers */ + * visible rectangle wuss_window_invalidate_visible covers */ wuss_window_invalidate_extent(task->window); return result_OK; } diff --git a/libraries/wuss/test/tasks/text.c b/libraries/wuss/test/tasks/text.c index 869aaf3..f2c70d9 100644 --- a/libraries/wuss/test/tasks/text.c +++ b/libraries/wuss/test/tasks/text.c @@ -112,7 +112,7 @@ static result_t text_set_font(text_task_t *task, int idx, const char *name) task->font = font; task->current = idx; - wuss_window_invalidate_all(task->window); + wuss_window_invalidate_visible(task->window); return result_OK; } @@ -125,7 +125,7 @@ static result_t text_set_sample(text_task_t *task, int idx) task->sample = idx; task->text = text_samples[idx].text; - wuss_window_invalidate_all(task->window); + wuss_window_invalidate_visible(task->window); return result_OK; } From 216b8d3c15daaf9f1aa348b24c2daf07c14badde Mon Sep 17 00:00:00 2001 From: David Thomas Date: Wed, 9 Sep 2026 18:21:17 +0100 Subject: [PATCH 72/98] chore(wuss): regenerate greeble prefab tiles Rebuild greeble_prefab_cells / greeble_prefab from the current sheet; GREEBLE_NPREFAB 103 -> 137. --- libraries/wuss/test/tasks/greeble-tiles.h | 499 ++++++++++++++-------- 1 file changed, 332 insertions(+), 167 deletions(-) diff --git a/libraries/wuss/test/tasks/greeble-tiles.h b/libraries/wuss/test/tasks/greeble-tiles.h index 914a77a..db86382 100644 --- a/libraries/wuss/test/tasks/greeble-tiles.h +++ b/libraries/wuss/test/tasks/greeble-tiles.h @@ -223,11 +223,25 @@ static const unsigned short greeble_tiles[GREEBLE_NTILES][GREEBLE_TILE_PX] = * grid of stamp indices in greeble_prefab_cells[], 0xFF marking a * cell the block's bounding box does not fill. 1x1 clusters are * dropped. GREEBLE_NPREFAB is 0 when the sheet was not supplied. */ -#define GREEBLE_NPREFAB 103 +#define GREEBLE_NPREFAB 137 #define GREEBLE_PREFAB_HOLE 0xFF static const unsigned char greeble_prefab_cells[] = { + 0x79, 0x7E, + 0x89, 0x8A, + 0x79, 0x7A, 0x7E, + 0x7C, 0xFF, 0x7C, + 0x89, 0x7A, 0x8A, + 0x79, 0x7A, 0x7A, 0x7E, + 0x7C, 0xFF, 0xFF, 0x7C, + 0x7C, 0xFF, 0xFF, 0x7C, + 0x89, 0x7A, 0x7A, 0x8A, + 0x79, 0x7A, 0x7A, 0x7A, 0x7E, + 0x7C, 0xFF, 0xFF, 0xFF, 0x7C, + 0x7C, 0xFF, 0xFF, 0xFF, 0x7C, + 0x7C, 0xFF, 0xFF, 0xFF, 0x7C, + 0x89, 0x7A, 0x7A, 0x7A, 0x8A, 0x86, 0x80, 0x86, @@ -253,21 +267,29 @@ static const unsigned char greeble_prefab_cells[] = 0x7D, 0x7A, 0x7E, 0xFF, 0xFF, 0x7C, 0xFF, 0xFF, 0x80, - 0x79, 0x7E, - 0x89, 0x8A, - 0x79, 0x7A, 0x7E, - 0x7C, 0xFF, 0x7C, - 0x89, 0x7A, 0x8A, - 0x79, 0x7A, 0x7A, 0x7E, - 0x7C, 0xFF, 0xFF, 0x7C, - 0x7C, 0xFF, 0xFF, 0x7C, - 0x89, 0x7A, 0x7A, 0x8A, - 0x79, 0x7A, 0x7A, 0x7A, 0x7E, - 0x7C, 0xFF, 0xFF, 0xFF, 0x7C, - 0x7C, 0xFF, 0xFF, 0xFF, 0x7C, - 0x7C, 0xFF, 0xFF, 0xFF, 0x7C, - 0x89, 0x7A, 0x7A, 0x7A, 0x8A, + 0x79, 0x7A, 0x7A, 0x7B, + 0x7C, 0xFF, 0xFF, 0xFF, + 0x7C, 0xFF, 0xFF, 0xFF, + 0x80, 0xFF, 0xFF, 0xFF, + 0x7D, 0x7A, 0x7A, 0x7E, + 0xFF, 0xFF, 0xFF, 0x7C, + 0xFF, 0xFF, 0xFF, 0x7C, + 0xFF, 0xFF, 0xFF, 0x80, 0x7D, 0x7A, 0x84, + 0x4C, 0x0E, + 0x10, 0x57, + 0x4C, 0x4D, 0x0E, + 0x50, 0x0F, 0x51, + 0x10, 0x56, 0x57, + 0x4C, 0x4D, 0x4D, 0x0E, + 0x50, 0x0F, 0x0F, 0x51, + 0x50, 0x0F, 0x0F, 0x51, + 0x10, 0x56, 0x56, 0x57, + 0x4C, 0x4D, 0x4D, 0x4D, 0x0E, + 0x50, 0x0F, 0x0F, 0x0F, 0x51, + 0x50, 0x0F, 0xFF, 0x0F, 0x51, + 0x50, 0x0F, 0x0F, 0x0F, 0x51, + 0x10, 0x56, 0x56, 0x56, 0x57, 0x86, 0xFF, 0x89, 0x84, 0xFF, 0x86, @@ -279,7 +301,29 @@ static const unsigned char greeble_prefab_cells[] = 0xFF, 0xFF, 0x86, 0xFF, 0xFF, 0x7C, 0x7D, 0x7A, 0x8A, + 0x86, 0xFF, 0xFF, 0xFF, + 0x7C, 0xFF, 0xFF, 0xFF, + 0x7C, 0xFF, 0xFF, 0xFF, + 0x89, 0x7A, 0x7A, 0x84, + 0xFF, 0xFF, 0xFF, 0x86, + 0xFF, 0xFF, 0xFF, 0x7C, + 0xFF, 0xFF, 0xFF, 0x7C, + 0x7D, 0x7A, 0x7A, 0x8A, 0x7D, 0x7A, 0x7A, 0x7A, 0x84, + 0x1C, 0x1D, + 0x27, 0x28, + 0x1C, 0x4E, 0x1D, + 0x52, 0x0F, 0x53, + 0x27, 0x58, 0x28, + 0x1C, 0x4E, 0x4E, 0x1D, + 0x52, 0x0F, 0x0F, 0x53, + 0x52, 0x0F, 0x0F, 0x53, + 0x27, 0x58, 0x58, 0x28, + 0x1C, 0x4E, 0x4E, 0x4E, 0x1D, + 0x52, 0x0F, 0x0F, 0x0F, 0x53, + 0x52, 0x0F, 0xFF, 0x0F, 0x53, + 0x52, 0x0F, 0x0F, 0x0F, 0x53, + 0x27, 0x58, 0x58, 0x58, 0x28, 0x17, 0x22, 0x17, @@ -308,6 +352,10 @@ static const unsigned char greeble_prefab_cells[] = 0xFF, 0x17, 0x17, 0xFF, 0xFF, 0x05, 0x05, 0xFF, 0x2E, 0xAF, 0xB0, 0x2F, + 0x98, 0x99, 0x9A, + 0x9B, 0x9C, 0x9D, + 0x9E, 0x9F, 0xA0, + 0xA2, 0xA3, 0xA4, 0x2E, 0x06, 0x2F, 0x2E, 0xA6, 0xFF, 0x22, @@ -320,7 +368,31 @@ static const unsigned char greeble_prefab_cells[] = 0x64, 0x06, 0x65, 0x05, 0xFF, 0xFF, 0x22, 0xFF, 0xFF, + 0x1E, 0x1F, + 0x29, 0x2A, + 0x1E, 0x4F, 0x1F, + 0x54, 0x0F, 0x55, + 0x29, 0x59, 0x2A, + 0x1E, 0x4F, 0x4F, 0x1F, + 0x54, 0x0F, 0x0F, 0x55, + 0x54, 0x0F, 0x0F, 0x55, + 0x29, 0x59, 0x59, 0x2A, + 0x1E, 0x4F, 0x4F, 0x4F, 0x1F, + 0x54, 0x0F, 0x0F, 0x0F, 0x55, + 0x54, 0x0F, 0xFF, 0x0F, 0x55, + 0x54, 0x0F, 0x0F, 0x0F, 0x55, + 0x29, 0x59, 0x59, 0x59, 0x2A, + 0x98, 0x99, 0x9A, 0x9B, 0x9C, 0x9D, 0x9E, 0x9F, 0xA0, 0x2E, 0x06, 0x06, 0x06, 0x2F, + 0x98, + 0x99, + 0x9A, + 0x9B, + 0x9C, + 0x9D, + 0x9E, + 0x9F, + 0xA0, 0x97, 0xA1, 0x97, @@ -350,6 +422,20 @@ static const unsigned char greeble_prefab_cells[] = 0xFF, 0x05, 0x05, 0xFF, 0x95, 0xAF, 0xB0, 0x96, 0x95, 0x06, 0x96, + 0x1E, 0x1F, + 0x29, 0x2A, + 0x1E, 0x4F, 0x1F, + 0x54, 0x44, 0x55, + 0x29, 0x59, 0x2A, + 0x1E, 0x4F, 0x4F, 0x1F, + 0x54, 0x44, 0x44, 0x55, + 0x54, 0x44, 0x44, 0x55, + 0x29, 0x59, 0x59, 0x2A, + 0x1E, 0x4F, 0x4F, 0x4F, 0x1F, + 0x54, 0x44, 0x44, 0x44, 0x55, + 0x54, 0x44, 0x44, 0x44, 0x55, + 0x54, 0x44, 0x44, 0x44, 0x55, + 0x29, 0x59, 0x59, 0x59, 0x2A, 0x95, 0xA6, 0xFF, 0xA1, 0x64, 0x96, @@ -406,26 +492,46 @@ static const unsigned char greeble_prefab_cells[] = 0x3B, 0x3B, 0x43, - 0xAC, - 0xAE, + 0xA8, 0x1F, + 0x29, 0xAA, + 0xA8, 0x4F, 0x1F, + 0x54, 0xA9, 0x55, + 0x29, 0x59, 0xAA, + 0xA8, 0x4F, 0x4F, 0x1F, + 0x54, 0xA9, 0xA9, 0x55, + 0x54, 0xA9, 0xA9, 0x55, + 0x29, 0x59, 0x59, 0xAA, + 0xA8, 0x4F, 0x4F, 0x4F, 0x1F, + 0x54, 0xA9, 0xA9, 0xA9, 0x55, + 0x54, 0xA9, 0xA9, 0xA9, 0x55, + 0x54, 0xA9, 0xA9, 0xA9, 0x55, + 0x29, 0x59, 0x59, 0x59, 0xAA, 0x0D, 0x0A, 0x0B, 0x2C, 0xA5, 0x2D, 0x0D, 0x0A, 0x0A, 0x0B, 0x2C, 0xA5, 0xA5, 0x2D, 0x0D, 0x0A, 0x0A, 0x0A, 0x0B, 0x2C, 0xA5, 0xA5, 0xA5, 0x2D, + 0x1A, 0x1B, + 0x25, 0x26, + 0x1A, 0x30, 0x1B, + 0x35, 0x36, 0x37, + 0x25, 0x3C, 0x26, + 0x1A, 0x30, 0x30, 0x1B, + 0x35, 0x36, 0x36, 0x37, + 0x35, 0x36, 0x36, 0x37, + 0x25, 0x3C, 0x3C, 0x26, + 0x1A, 0x30, 0x30, 0x30, 0x1B, + 0x35, 0x36, 0x36, 0x36, 0x37, + 0x35, 0x36, 0x36, 0x36, 0x37, + 0x35, 0x36, 0x36, 0x36, 0x37, + 0x25, 0x3C, 0x3C, 0x3C, 0x26, 0x18, 0x19, 0x23, 0x24, 0x1A, 0x1B, 0x25, 0x26, 0x3D, 0x3E, 0x45, 0x46, - 0x1C, 0x1D, - 0x27, 0x28, - 0x1E, 0x1F, - 0x29, 0x2A, - 0x20, 0x1F, - 0x29, 0x2B, 0x3F, 0x40, 0x47, 0x48, 0x41, 0x42, @@ -436,41 +542,8 @@ static const unsigned char greeble_prefab_cells[] = 0x38, 0x39, 0xBE, 0xBF, 0xC0, 0xC9, - 0x98, 0x99, 0x9A, 0x9B, 0x9C, 0x9D, 0x9E, 0x9F, 0xA0, - 0x98, 0x99, 0x9A, - 0x9B, 0x9C, 0x9D, - 0x9E, 0x9F, 0xA0, - 0xA2, 0xA3, 0xA4, - 0x98, - 0x99, - 0x9A, - 0x9B, - 0x9C, - 0x9D, - 0x9E, - 0x9F, - 0xA0, - 0x4C, 0x4D, 0x0E, - 0x50, 0x0F, 0x51, - 0x10, 0x56, 0x57, - 0x1C, 0x4E, 0x1D, - 0x52, 0x0F, 0x53, - 0x27, 0x58, 0x28, - 0x1E, 0x4F, 0x1F, - 0x54, 0x0F, 0x55, - 0x29, 0x59, 0x2A, - 0x1E, 0x4F, 0x1F, - 0x54, 0x44, 0x55, - 0x29, 0x59, 0x2A, - 0xA8, 0x4F, 0x1F, - 0x54, 0xA9, 0x55, - 0x29, 0x59, 0xAA, - 0x1A, 0x30, 0x1B, - 0x35, 0x36, 0x37, - 0x25, 0x3C, 0x26, - 0xC2, 0xC3, 0xC4, - 0xC5, 0x0F, 0xC6, - 0xC7, 0xC8, 0xC1, + 0xAC, + 0xAE, 0x8B, 0x0A, 0x0B, 0x8C, 0x0D, 0x0A, 0x0E, 0x0C, 0x01, 0x09, 0x8D, 0x09, 0x01, 0x0C, 0x03, 0x08, 0xFF, 0xFF, 0xFF, 0x08, 0x03, @@ -478,132 +551,224 @@ static const unsigned char greeble_prefab_cells[] = 0x02, 0x08, 0xFF, 0xFF, 0xFF, 0x08, 0x02, 0x0C, 0x01, 0x09, 0x92, 0x09, 0x01, 0x0C, 0x10, 0x0A, 0x0B, 0x93, 0x0D, 0x0A, 0x94, + 0xC2, 0xC3, 0xC4, + 0xC5, 0x0F, 0xC6, + 0xC7, 0xC8, 0xC1, + 0x11, 0x12, + 0x13, 0x14, + 0x11, 0x8D, 0x12, + 0x8F, 0x07, 0x90, + 0x13, 0x92, 0x14, + 0x11, 0x8D, 0x8D, 0x12, + 0x8F, 0x07, 0x07, 0x90, + 0x8F, 0x07, 0x07, 0x90, + 0x13, 0x92, 0x92, 0x14, + 0x11, 0x8D, 0x8D, 0x8D, 0x12, + 0x8F, 0x07, 0x07, 0x07, 0x90, + 0x8F, 0x07, 0x07, 0x07, 0x90, + 0x8F, 0x07, 0x07, 0x07, 0x90, + 0x13, 0x92, 0x92, 0x92, 0x14, + 0x5A, 0x5C, + 0x6E, 0x70, 0x5A, 0x5B, 0x5C, 0x66, 0x63, 0x67, 0x6E, 0x6F, 0x70, + 0x5A, 0x5B, 0x5B, 0x5C, + 0x66, 0x63, 0x63, 0x67, + 0x66, 0x63, 0x63, 0x67, + 0x6E, 0x6F, 0x6F, 0x70, + 0x5A, 0x5B, 0x5B, 0x5B, 0x5C, + 0x66, 0x63, 0x63, 0x63, 0x67, + 0x66, 0x63, 0x63, 0x63, 0x67, + 0x66, 0x63, 0x63, 0x63, 0x67, + 0x6E, 0x6F, 0x6F, 0x6F, 0x70, + 0x81, 0x83, + 0x87, 0x88, + 0x81, 0x82, 0x83, + 0x85, 0x16, 0x85, + 0x87, 0x82, 0x88, + 0x81, 0x82, 0x82, 0x83, + 0x85, 0x16, 0x16, 0x85, + 0x85, 0x16, 0x16, 0x85, + 0x87, 0x82, 0x82, 0x88, + 0x81, 0x82, 0x82, 0x82, 0x83, + 0x85, 0x16, 0x16, 0x16, 0x85, + 0x85, 0x16, 0x16, 0x16, 0x85, + 0x85, 0x16, 0x16, 0x16, 0x85, + 0x87, 0x82, 0x82, 0x82, 0x88, + 0x5D, 0x5F, + 0x71, 0x73, 0x5D, 0x5E, 0x5F, 0x68, 0x63, 0x69, 0x71, 0x72, 0x73, + 0x5D, 0x5E, 0x5E, 0x5F, + 0x68, 0x63, 0x63, 0x69, + 0x68, 0x63, 0x63, 0x69, + 0x71, 0x72, 0x72, 0x73, + 0x5D, 0x5E, 0x5E, 0x5E, 0x5F, + 0x68, 0x63, 0x63, 0x63, 0x69, + 0x68, 0x63, 0x63, 0x63, 0x69, + 0x68, 0x63, 0x63, 0x63, 0x69, + 0x71, 0x72, 0x72, 0x72, 0x73, 0x60, 0x61, 0x62, 0x6A, 0x6B, 0x0F, 0x74, 0x75, 0x76, + 0x60, 0x62, + 0x74, 0x76, 0x60, 0x63, 0x62, 0x63, 0x6C, 0x0F, 0x74, 0x75, 0x76, - 0x81, 0x82, 0x83, - 0x85, 0x16, 0x85, - 0x87, 0x82, 0x88, - 0x11, 0x8D, 0x12, - 0x8F, 0x07, 0x90, - 0x13, 0x92, 0x14, + 0x60, 0x63, 0x63, 0x62, + 0x63, 0x6C, 0x6C, 0x0F, + 0x63, 0x6C, 0x6C, 0x0F, + 0x74, 0x75, 0x75, 0x76, + 0x60, 0x63, 0x63, 0x63, 0x62, + 0x63, 0x6C, 0x6C, 0x6C, 0x0F, + 0x63, 0x6C, 0x6C, 0x6C, 0x0F, + 0x63, 0x6C, 0x6C, 0x6C, 0x0F, + 0x74, 0x75, 0x75, 0x75, 0x76, }; static const struct { unsigned char w, h; unsigned short off; } greeble_prefab[GREEBLE_NPREFAB] = { - { 1, 2, 0 }, /* 0 */ - { 1, 3, 2 }, /* 1 */ - { 1, 4, 5 }, /* 2 */ - { 1, 5, 9 }, /* 3 */ - { 2, 1, 14 }, /* 4 */ - { 2, 2, 16 }, /* 5 */ - { 2, 2, 20 }, /* 6 */ - { 3, 3, 24 }, /* 7 */ - { 3, 3, 33 }, /* 8 */ - { 2, 2, 42 }, /* 9 */ - { 3, 3, 46 }, /* 10 */ - { 4, 4, 55 }, /* 11 */ - { 5, 5, 71 }, /* 12 */ - { 3, 1, 96 }, /* 13 */ - { 2, 2, 99 }, /* 14 */ - { 2, 2, 103 }, /* 15 */ - { 4, 1, 107 }, /* 16 */ - { 3, 3, 111 }, /* 17 */ - { 3, 3, 120 }, /* 18 */ - { 5, 1, 129 }, /* 19 */ - { 1, 2, 134 }, /* 20 */ - { 1, 3, 136 }, /* 21 */ - { 1, 4, 139 }, /* 22 */ - { 1, 5, 143 }, /* 23 */ - { 2, 1, 148 }, /* 24 */ - { 2, 2, 150 }, /* 25 */ - { 2, 2, 154 }, /* 26 */ - { 3, 3, 158 }, /* 27 */ - { 3, 3, 167 }, /* 28 */ - { 4, 3, 176 }, /* 29 */ - { 3, 1, 188 }, /* 30 */ - { 2, 2, 191 }, /* 31 */ - { 2, 2, 195 }, /* 32 */ - { 4, 1, 199 }, /* 33 */ - { 3, 3, 203 }, /* 34 */ - { 3, 3, 212 }, /* 35 */ - { 5, 1, 221 }, /* 36 */ - { 1, 2, 226 }, /* 37 */ - { 1, 3, 228 }, /* 38 */ - { 1, 4, 231 }, /* 39 */ - { 1, 5, 235 }, /* 40 */ - { 2, 1, 240 }, /* 41 */ - { 2, 2, 242 }, /* 42 */ - { 2, 2, 246 }, /* 43 */ - { 3, 3, 250 }, /* 44 */ - { 3, 3, 259 }, /* 45 */ - { 4, 3, 268 }, /* 46 */ - { 3, 1, 280 }, /* 47 */ - { 2, 2, 283 }, /* 48 */ - { 2, 2, 287 }, /* 49 */ - { 4, 1, 291 }, /* 50 */ - { 3, 3, 295 }, /* 51 */ - { 3, 3, 304 }, /* 52 */ - { 5, 1, 313 }, /* 53 */ - { 1, 2, 318 }, /* 54 */ - { 1, 3, 320 }, /* 55 */ - { 1, 4, 323 }, /* 56 */ - { 1, 5, 327 }, /* 57 */ - { 2, 1, 332 }, /* 58 */ - { 1, 2, 334 }, /* 59 */ - { 1, 3, 336 }, /* 60 */ - { 1, 4, 339 }, /* 61 */ - { 1, 5, 343 }, /* 62 */ - { 2, 1, 348 }, /* 63 */ - { 1, 2, 350 }, /* 64 */ - { 1, 3, 352 }, /* 65 */ - { 1, 4, 355 }, /* 66 */ - { 1, 5, 359 }, /* 67 */ - { 1, 2, 364 }, /* 68 */ - { 3, 1, 366 }, /* 69 */ - { 3, 1, 369 }, /* 70 */ - { 4, 1, 372 }, /* 71 */ - { 4, 1, 376 }, /* 72 */ - { 5, 1, 380 }, /* 73 */ - { 5, 1, 385 }, /* 74 */ - { 2, 2, 390 }, /* 75 */ - { 2, 2, 394 }, /* 76 */ - { 2, 2, 398 }, /* 77 */ - { 2, 2, 402 }, /* 78 */ - { 2, 2, 406 }, /* 79 */ - { 2, 2, 410 }, /* 80 */ - { 2, 2, 414 }, /* 81 */ - { 2, 2, 418 }, /* 82 */ - { 2, 2, 422 }, /* 83 */ - { 2, 2, 426 }, /* 84 */ - { 2, 2, 430 }, /* 85 */ - { 9, 1, 434 }, /* 86 */ - { 3, 4, 443 }, /* 87 */ - { 1, 9, 455 }, /* 88 */ - { 3, 3, 464 }, /* 89 */ - { 3, 3, 473 }, /* 90 */ - { 3, 3, 482 }, /* 91 */ - { 3, 3, 491 }, /* 92 */ - { 3, 3, 500 }, /* 93 */ - { 3, 3, 509 }, /* 94 */ - { 3, 3, 518 }, /* 95 */ - { 7, 7, 527 }, /* 96 */ - { 3, 3, 576 }, /* 97 */ - { 3, 3, 585 }, /* 98 */ - { 3, 3, 594 }, /* 99 */ - { 3, 3, 603 }, /* 100 */ - { 3, 3, 612 }, /* 101 */ - { 3, 3, 621 }, /* 102 */ + { 2, 2, 0 }, /* 0 */ + { 3, 3, 4 }, /* 1 */ + { 4, 4, 13 }, /* 2 */ + { 5, 5, 29 }, /* 3 */ + { 1, 2, 54 }, /* 4 */ + { 1, 3, 56 }, /* 5 */ + { 1, 4, 59 }, /* 6 */ + { 1, 5, 63 }, /* 7 */ + { 2, 1, 68 }, /* 8 */ + { 2, 2, 70 }, /* 9 */ + { 2, 2, 74 }, /* 10 */ + { 3, 3, 78 }, /* 11 */ + { 3, 3, 87 }, /* 12 */ + { 4, 4, 96 }, /* 13 */ + { 4, 4, 112 }, /* 14 */ + { 3, 1, 128 }, /* 15 */ + { 2, 2, 131 }, /* 16 */ + { 3, 3, 135 }, /* 17 */ + { 4, 4, 144 }, /* 18 */ + { 5, 5, 160 }, /* 19 */ + { 2, 2, 185 }, /* 20 */ + { 2, 2, 189 }, /* 21 */ + { 4, 1, 193 }, /* 22 */ + { 3, 3, 197 }, /* 23 */ + { 3, 3, 206 }, /* 24 */ + { 4, 4, 215 }, /* 25 */ + { 4, 4, 231 }, /* 26 */ + { 5, 1, 247 }, /* 27 */ + { 2, 2, 252 }, /* 28 */ + { 3, 3, 256 }, /* 29 */ + { 4, 4, 265 }, /* 30 */ + { 5, 5, 281 }, /* 31 */ + { 1, 2, 306 }, /* 32 */ + { 1, 3, 308 }, /* 33 */ + { 1, 4, 311 }, /* 34 */ + { 1, 5, 315 }, /* 35 */ + { 2, 1, 320 }, /* 36 */ + { 2, 2, 322 }, /* 37 */ + { 2, 2, 326 }, /* 38 */ + { 3, 3, 330 }, /* 39 */ + { 3, 3, 339 }, /* 40 */ + { 4, 3, 348 }, /* 41 */ + { 3, 4, 360 }, /* 42 */ + { 3, 1, 372 }, /* 43 */ + { 2, 2, 375 }, /* 44 */ + { 2, 2, 379 }, /* 45 */ + { 4, 1, 383 }, /* 46 */ + { 3, 3, 387 }, /* 47 */ + { 3, 3, 396 }, /* 48 */ + { 2, 2, 405 }, /* 49 */ + { 3, 3, 409 }, /* 50 */ + { 4, 4, 418 }, /* 51 */ + { 5, 5, 434 }, /* 52 */ + { 9, 1, 459 }, /* 53 */ + { 5, 1, 468 }, /* 54 */ + { 1, 9, 473 }, /* 55 */ + { 1, 2, 482 }, /* 56 */ + { 1, 3, 484 }, /* 57 */ + { 1, 4, 487 }, /* 58 */ + { 1, 5, 491 }, /* 59 */ + { 2, 1, 496 }, /* 60 */ + { 2, 2, 498 }, /* 61 */ + { 2, 2, 502 }, /* 62 */ + { 3, 3, 506 }, /* 63 */ + { 3, 3, 515 }, /* 64 */ + { 4, 3, 524 }, /* 65 */ + { 3, 1, 536 }, /* 66 */ + { 2, 2, 539 }, /* 67 */ + { 3, 3, 543 }, /* 68 */ + { 4, 4, 552 }, /* 69 */ + { 5, 5, 568 }, /* 70 */ + { 2, 2, 593 }, /* 71 */ + { 2, 2, 597 }, /* 72 */ + { 4, 1, 601 }, /* 73 */ + { 3, 3, 605 }, /* 74 */ + { 3, 3, 614 }, /* 75 */ + { 5, 1, 623 }, /* 76 */ + { 1, 2, 628 }, /* 77 */ + { 1, 3, 630 }, /* 78 */ + { 1, 4, 633 }, /* 79 */ + { 1, 5, 637 }, /* 80 */ + { 2, 1, 642 }, /* 81 */ + { 1, 2, 644 }, /* 82 */ + { 1, 3, 646 }, /* 83 */ + { 1, 4, 649 }, /* 84 */ + { 1, 5, 653 }, /* 85 */ + { 2, 1, 658 }, /* 86 */ + { 1, 2, 660 }, /* 87 */ + { 1, 3, 662 }, /* 88 */ + { 1, 4, 665 }, /* 89 */ + { 1, 5, 669 }, /* 90 */ + { 2, 2, 674 }, /* 91 */ + { 3, 3, 678 }, /* 92 */ + { 4, 4, 687 }, /* 93 */ + { 5, 5, 703 }, /* 94 */ + { 3, 1, 728 }, /* 95 */ + { 3, 1, 731 }, /* 96 */ + { 4, 1, 734 }, /* 97 */ + { 4, 1, 738 }, /* 98 */ + { 5, 1, 742 }, /* 99 */ + { 5, 1, 747 }, /* 100 */ + { 2, 2, 752 }, /* 101 */ + { 3, 3, 756 }, /* 102 */ + { 4, 4, 765 }, /* 103 */ + { 5, 5, 781 }, /* 104 */ + { 2, 2, 806 }, /* 105 */ + { 2, 2, 810 }, /* 106 */ + { 2, 2, 814 }, /* 107 */ + { 2, 2, 818 }, /* 108 */ + { 2, 2, 822 }, /* 109 */ + { 2, 2, 826 }, /* 110 */ + { 2, 2, 830 }, /* 111 */ + { 2, 2, 834 }, /* 112 */ + { 1, 2, 838 }, /* 113 */ + { 7, 7, 840 }, /* 114 */ + { 3, 3, 889 }, /* 115 */ + { 2, 2, 898 }, /* 116 */ + { 3, 3, 902 }, /* 117 */ + { 4, 4, 911 }, /* 118 */ + { 5, 5, 927 }, /* 119 */ + { 2, 2, 952 }, /* 120 */ + { 3, 3, 956 }, /* 121 */ + { 4, 4, 965 }, /* 122 */ + { 5, 5, 981 }, /* 123 */ + { 2, 2, 1006 }, /* 124 */ + { 3, 3, 1010 }, /* 125 */ + { 4, 4, 1019 }, /* 126 */ + { 5, 5, 1035 }, /* 127 */ + { 2, 2, 1060 }, /* 128 */ + { 3, 3, 1064 }, /* 129 */ + { 4, 4, 1073 }, /* 130 */ + { 5, 5, 1089 }, /* 131 */ + { 3, 3, 1114 }, /* 132 */ + { 2, 2, 1123 }, /* 133 */ + { 3, 3, 1127 }, /* 134 */ + { 4, 4, 1136 }, /* 135 */ + { 5, 5, 1152 }, /* 136 */ }; /* stamps the generator scatters into cells no prefab claimed: the From 7ca797ee29eff9e6604a5d19f162d086065d0b7a Mon Sep 17 00:00:00 2001 From: David Thomas Date: Wed, 9 Sep 2026 18:30:36 +0100 Subject: [PATCH 73/98] fix(wuss): resize image window on load, add 8px border image_click now calls wuss_window_resize before wuss_window_set_doc so the visible frame tracks the new bitmap; set_doc alone only changed the scroll extent. Add an 8px solid PICO-8 pink band inset inside the ninepatch frame (IMAGE_BORDERSZ / IMAGE_MARGINSZ). --- libraries/wuss/TODO.txt | 3 +- libraries/wuss/test/tasks/image.c | 48 ++++++++++++++++++++++++------- 2 files changed, 39 insertions(+), 12 deletions(-) diff --git a/libraries/wuss/TODO.txt b/libraries/wuss/TODO.txt index 549a6a1..1a1ad9e 100644 --- a/libraries/wuss/TODO.txt +++ b/libraries/wuss/TODO.txt @@ -10,7 +10,7 @@ LAYER 0 - BUGS (fix before building on top) - Auto window placement packer still not working as expected blocks: reliable multi-window tasks - Image task not shrinking window when new image loads -- Saw corruption when loading large image then expanding window size + LAYER 1 - CORE PLUMBING (no wuss deps; unblocks everything else) @@ -53,6 +53,7 @@ LAYER 4 - WINDOW FURNITURE & INTERACTION (needs bug layer, text layer) - Dragging against window edge shuffles window in opposite direction (outline-click bug fixed; unblocked) - Input focus, text entry, furniture tinting +- Icon layout LAYER 5 - DRAG & TEMPLATES (needs sprites, config-after-the-fact) diff --git a/libraries/wuss/test/tasks/image.c b/libraries/wuss/test/tasks/image.c index d5d6d26..f4f56c3 100644 --- a/libraries/wuss/test/tasks/image.c +++ b/libraries/wuss/test/tasks/image.c @@ -21,7 +21,9 @@ #include "image.h" -#define NINEPATCHSZ 9 +#define NINEPATCHSZ 9 +#define IMAGE_BORDERSZ 8 /* solid inset band drawn inside the ninepatch */ +#define IMAGE_MARGINSZ (NINEPATCHSZ + IMAGE_BORDERSZ) #define IMAGE_EXT ".png" result_t image_create(wuss_t *wuss, @@ -88,8 +90,8 @@ result_t image_create(wuss_t *wuss, * at wuss_destroy instead. Closing the main window early leaks this block * until then -- fine for a demo. */ - sz.w = task->bitmap.size.w + NINEPATCHSZ * 2; - sz.h = task->bitmap.size.h + NINEPATCHSZ * 2; + sz.w = task->bitmap.size.w + IMAGE_MARGINSZ * 2; + sz.h = task->bitmap.size.h + IMAGE_MARGINSZ * 2; rc = wuss_window_create_placed(delegate, SIZE2D(sz.w, sz.h), @@ -132,6 +134,7 @@ static result_t image_redraw(const wuss_event_t *event, void *task_data) int sx, sy; int bx, by; box_t behind; + box_t band[4]; ic = task_data; @@ -139,15 +142,34 @@ static result_t image_redraw(const wuss_event_t *event, void *task_data) bounds = event->data.redraw.bounds; sx = event->data.redraw.scroll.x; sy = event->data.redraw.scroll.y; - bx = bounds->x0 - sx + NINEPATCHSZ; - by = bounds->y0 - sy + NINEPATCHSZ; + bx = bounds->x0 - sx + IMAGE_MARGINSZ; + by = bounds->y0 - sy + IMAGE_MARGINSZ; - behind.x0 = bx - NINEPATCHSZ; - behind.y0 = by - NINEPATCHSZ; - behind.x1 = behind.x0 + ic->bitmap.size.w + NINEPATCHSZ * 2; - behind.y1 = behind.y0 + ic->bitmap.size.h + NINEPATCHSZ * 2; + behind.x0 = bx - IMAGE_MARGINSZ; + behind.y0 = by - IMAGE_MARGINSZ; + behind.x1 = behind.x0 + ic->bitmap.size.w + IMAGE_MARGINSZ * 2; + behind.y1 = behind.y0 + ic->bitmap.size.h + IMAGE_MARGINSZ * 2; screen_copy_ninepatch(scr, &behind, &ic->ninepatch, 0); + /* solid 8px band between the ninepatch frame and the image */ + band[0].x0 = bx - IMAGE_BORDERSZ; + band[0].y0 = by - IMAGE_BORDERSZ; + band[0].x1 = bx + ic->bitmap.size.w + IMAGE_BORDERSZ; + band[0].y1 = by; /* top */ + band[1].x0 = bx - IMAGE_BORDERSZ; + band[1].y0 = by + ic->bitmap.size.h; + band[1].x1 = bx + ic->bitmap.size.w + IMAGE_BORDERSZ; + band[1].y1 = by + ic->bitmap.size.h + IMAGE_BORDERSZ; /* bottom */ + band[2].x0 = bx - IMAGE_BORDERSZ; + band[2].y0 = by; + band[2].x1 = bx; + band[2].y1 = by + ic->bitmap.size.h; /* left */ + band[3].x0 = bx + ic->bitmap.size.w; + band[3].y0 = by; + band[3].x1 = bx + ic->bitmap.size.w + IMAGE_BORDERSZ; + band[3].y1 = by + ic->bitmap.size.h; /* right */ + screen_fill_rects(scr, band, 4, colour_rgb(0xFF, 0x77, 0xA8)); /* PICO-8 pink */ + screen_copy_bitmap(scr, bx, by, &ic->bitmap); return result_OK; @@ -184,8 +206,12 @@ static result_t image_click(wuss_window_t *window, free(ic->bitmap.base); ic->bitmap = next; - sz.w = ic->bitmap.size.w + NINEPATCHSZ * 2; - sz.h = ic->bitmap.size.h + NINEPATCHSZ * 2; + sz.w = ic->bitmap.size.w + IMAGE_MARGINSZ * 2; + sz.h = ic->bitmap.size.h + IMAGE_MARGINSZ * 2; + + rc = wuss_window_resize(window, sz); + if (rc != result_OK) + return rc; return wuss_window_set_doc(window, sz); } From 42f358b384c762afa086f33e5f6e0e1140f9c21b Mon Sep 17 00:00:00 2001 From: David Thomas Date: Wed, 9 Sep 2026 18:46:58 +0100 Subject: [PATCH 74/98] feat(framebuf): support 1bpp paletted (p1) bitmaps Add span_p1 (fill + blendconst, MSB-first bit packing) and register it, so bitmap_init hands out a valid span for pixelfmt_p1. Add a p1 case to screen_copy_bitmap: blit a 32bpp RGBA source onto a 1bpp screen, quantising to the 2-entry screen palette via the existing deep->paletted pixelmap fast path (pixelmap already handled p1). Alpha-tested transfer, both indices opaque. Cover both with tests: rgba8888->p1 in pixelmap-test, screen_copy_bitmap to a p1 screen (packing, spill, odd-x) in screen-test. --- CMakeLists.txt | 2 + include/framebuf/span-p1.h | 10 ++ .../framebuf/pixelmap/test/pixelmap-test.c | 34 ++++++ libraries/framebuf/screen/screen-draw.c | 73 ++++++++++++ libraries/framebuf/screen/test/screen-test.c | 74 +++++++++++- libraries/framebuf/span-registry/regdata.h | 2 + libraries/framebuf/span/p1.c | 105 ++++++++++++++++++ 7 files changed, 299 insertions(+), 1 deletion(-) create mode 100644 include/framebuf/span-p1.h create mode 100644 libraries/framebuf/span/p1.c diff --git a/CMakeLists.txt b/CMakeLists.txt index b7ca801..e5027ae 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -89,6 +89,7 @@ set(PUBLIC_HEADERS include/framebuf/pixelmap.h include/framebuf/screen.h include/framebuf/span-bgrx8888.h + include/framebuf/span-p1.h include/framebuf/span-p4.h include/framebuf/span-registry.h include/framebuf/span-rgba8888.h @@ -280,6 +281,7 @@ set(FRAMEBUF_SOURCES libraries/framebuf/span/all8888.c libraries/framebuf/span/all8888.h libraries/framebuf/span/bgrx8888.c + libraries/framebuf/span/p1.c libraries/framebuf/span/p4.c libraries/framebuf/span/rgbx8888.c libraries/framebuf/span/xbgr8888.c) diff --git a/include/framebuf/span-p1.h b/include/framebuf/span-p1.h new file mode 100644 index 0000000..1bf5c68 --- /dev/null +++ b/include/framebuf/span-p1.h @@ -0,0 +1,10 @@ +/* framebuf/span-p1.h -- P1 (1bpp paletted) format plot methods */ + +#ifndef SPAN_P1_H +#define SPAN_P1_H + +#include "framebuf/span.h" + +extern const span_t span_p1; + +#endif /* SPAN_P1_H */ diff --git a/libraries/framebuf/pixelmap/test/pixelmap-test.c b/libraries/framebuf/pixelmap/test/pixelmap-test.c index 4abfd8d..599d30a 100644 --- a/libraries/framebuf/pixelmap/test/pixelmap-test.c +++ b/libraries/framebuf/pixelmap/test/pixelmap-test.c @@ -130,6 +130,40 @@ result_t pixelmap_test(const char *resources) } } + /* 4b. rgba8888 -> p1 (2KB 4:4:4 table): a 2-entry black/white palette + * resolves dark colours to index 0 and light ones to index 1 */ + { + const pixelmap_t *pm1; + colour_t bw[2]; + + bw[0] = colour_rgb(0x00, 0x00, 0x00); + bw[1] = colour_rgb(0xFF, 0xFF, 0xFF); + + pm1 = pixelmap_get(pixelfmt_rgba8888, pixelfmt_p1, bw, 2); + if (pm1 == NULL) + { + printf("pixelmap: get rgba8888->p1 returned NULL\n"); + return result_TEST_FAILED; + } + if (pm1->dest_log2bpp != 0 || pm1->nentries != 4096) + { + printf("pixelmap: p1 layout wrong (dest_log2bpp %d, nentries %u)\n", + pm1->dest_log2bpp, pm1->nentries); + return result_TEST_FAILED; + } + + if (pm_lookup(pm1, pm_index(pm1, colour_rgb(0x10, 0x10, 0x10).primary)) != 0) + { + printf("pixelmap: p1 dark colour did not resolve to index 0\n"); + return result_TEST_FAILED; + } + if (pm_lookup(pm1, pm_index(pm1, colour_rgb(0xF0, 0xF0, 0xF0).primary)) != 1) + { + printf("pixelmap: p1 light colour did not resolve to index 1\n"); + return result_TEST_FAILED; + } + } + /* 5. paletted -> deep: p4 -> bgrx8888, one deep pixel per palette index */ { const pixelmap_t *pmd; diff --git a/libraries/framebuf/screen/screen-draw.c b/libraries/framebuf/screen/screen-draw.c index f10ad47..1b7eb31 100644 --- a/libraries/framebuf/screen/screen-draw.c +++ b/libraries/framebuf/screen/screen-draw.c @@ -296,6 +296,78 @@ static result_t screen_copy_bitmap_p4(screen_t *scr, return result_OK; } +/* Blit "src" onto the 1bpp screen, its top-left at (x, y), clipped to + * "draw_box". Like screen_copy_bitmap_p4 this is an alpha-tested transfer + * (skip fully transparent, else nearest of the two palette entries) rather + * than a blend -- a 1bpp screen has no channel bits to blend. The cached + * RGB->index table turns the inner loop into a mask-and-lookup; bit 7 of a + * byte is the leftmost pixel. Returns result_NOT_SUPPORTED if there is no + * deep->paletted conversion table for the screen's format. */ +static result_t screen_copy_bitmap_p1(screen_t *scr, + int x, + int y, + const bitmap_t *src, + const box_t *draw_box, + int has_alpha) +{ + const unsigned char *srcrow; + unsigned char *dstbase; + const pixelmap_t *pm; + int clipped_width, clipped_height; + int yy; + + clipped_width = draw_box->x1 - draw_box->x0; + clipped_height = draw_box->y1 - draw_box->y0; + + srcrow = (const unsigned char *) src->base + (draw_box->y0 - y) * src->rowbytes; + dstbase = scr->base; + + pm = pixelmap_get(pixelfmt_rgba8888, scr->format, scr->palette, 2); + if (pm == NULL) + return result_NOT_SUPPORTED; + + for (yy = 0; yy < clipped_height; yy++) + { + const pixelfmt_rgba8888_t *srcpx; + unsigned char *rowp; + int xx; + + srcpx = (const pixelfmt_rgba8888_t *) srcrow + (draw_box->x0 - x); + rowp = dstbase + (draw_box->y0 + yy) * scr->rowbytes; + + for (xx = 0; xx < clipped_width; xx++) + { + colour_t c; + unsigned int r, g, b, idx; + int dstx, shift; + unsigned char *scrp; + pixelfmt_any_t pxl; + + c.primary = srcpx[xx]; + if (has_alpha && colour_get_alpha(&c) == 0) + continue; /* fully transparent: leave background alone */ + + dstx = draw_box->x0 + xx; + scrp = rowp + (dstx >> 3); + shift = 7 - (dstx & 7); + + r = (c.primary >> pm->rshift) & 0xFF; + g = (c.primary >> pm->gshift) & 0xFF; + b = (c.primary >> pm->bshift) & 0xFF; + idx = ((r >> (8 - pm->rbits)) << (pm->gbits + pm->bbits)) + | ((g >> (8 - pm->gbits)) << pm->bbits) + | ( b >> (8 - pm->bbits)); + pxl = (pm->entries[idx >> 3] >> (idx & 7)) & 1; + + *scrp = (unsigned char) ((*scrp & ~(1 << shift)) | ((pxl & 1) << shift)); + } + + srcrow += src->rowbytes; + } + + return result_OK; +} + /* Blit "src" onto the 32bpp screen, its top-left at (x, y), clipped to * "draw_box", alpha-blending row spans through the span registry. */ static void screen_copy_bitmap_32(screen_t *scr, @@ -383,6 +455,7 @@ result_t screen_copy_bitmap(screen_t *scr, int x, int y, const bitmap_t *src) switch (pixelfmt_log2bpp(scr->format)) { + case 0: return screen_copy_bitmap_p1(scr, x, y, src, &draw_box, has_alpha); case 2: return screen_copy_bitmap_p4(scr, x, y, src, &draw_box, has_alpha); case 5: screen_copy_bitmap_32(scr, x, y, src, &draw_box, has_alpha); break; diff --git a/libraries/framebuf/screen/test/screen-test.c b/libraries/framebuf/screen/test/screen-test.c index 7fec322..b35ee7f 100644 --- a/libraries/framebuf/screen/test/screen-test.c +++ b/libraries/framebuf/screen/test/screen-test.c @@ -740,6 +740,77 @@ static result_t test_fill_circle(void) /* ----------------------------------------------------------------------- */ +/* Blit an rgba8888 source onto a 1bpp screen: each pixel must quantise to + * the nearer of the two palette entries and pack MSB-first, bit 7 leftmost. */ +static result_t test_copy_bitmap_p1(void) +{ +#define P1_ROWBYTES (WIDTH / 8) + static unsigned char p1pixels[P1_ROWBYTES * HEIGHT]; + static pixelfmt_rgba8888_t srcbuf[16 * 4]; + + screen_t scr; + bitmap_t src; + colour_t pal[2]; + int x, y; + + pal[0] = colour_rgb(0x00, 0x00, 0x00); + pal[1] = colour_rgb(0xFF, 0xFF, 0xFF); + + /* 16x4 source: left half dark (-> index 0), right half light (-> index 1) */ + for (y = 0; y < 4; y++) + for (x = 0; x < 16; x++) + srcbuf[y * 16 + x] = (x < 8 ? colour_rgb(0x10, 0x10, 0x10) + : colour_rgb(0xF0, 0xF0, 0xF0)).primary; + + bitmap_init(&src, SIZE2D(16, 4), pixelfmt_rgba8888, + 16 * (int) sizeof(srcbuf[0]), NULL, srcbuf); + + memset(p1pixels, 0, sizeof(p1pixels)); + screen_init(&scr, SIZE2D(WIDTH, HEIGHT), pixelfmt_p1, P1_ROWBYTES, pal, + p1pixels); + + if (screen_copy_bitmap(&scr, 0, 0, &src) != result_OK) + { + printf("screen: copy_bitmap to p1 screen failed\n"); + return result_TEST_FAILED; + } + + /* row 0: byte 0 = eight dark pixels = 0x00; byte 1 = eight light = 0xFF */ + if (p1pixels[0] != 0x00 || p1pixels[1] != 0xFF) + { + printf("screen: p1 blit packed wrong (0x%02X 0x%02X, want 0x00 0xFF)\n", + p1pixels[0], p1pixels[1]); + return result_TEST_FAILED; + } + + /* an untouched row past the source stays clear */ + if (p1pixels[4 * P1_ROWBYTES] != 0x00) + { + printf("screen: p1 blit spilled past the source\n"); + return result_TEST_FAILED; + } + + /* odd x offset: the first light pixel must land in bit 7-(3&7) of byte 0 */ + memset(p1pixels, 0, sizeof(p1pixels)); + { + pixelfmt_rgba8888_t one = colour_rgb(0xF0, 0xF0, 0xF0).primary; + bitmap_t dot; + + bitmap_init(&dot, SIZE2D(1, 1), pixelfmt_rgba8888, + (int) sizeof(one), NULL, &one); + screen_copy_bitmap(&scr, 3, 0, &dot); + if (p1pixels[0] != (1 << (7 - 3))) + { + printf("screen: p1 blit odd-x packed wrong (0x%02X)\n", p1pixels[0]); + return result_TEST_FAILED; + } + } + + return result_TEST_PASSED; +} + +/* ----------------------------------------------------------------------- */ + result_t screen_test(const char *resources) { typedef result_t (*screentestfn)(void); @@ -755,7 +826,8 @@ result_t screen_test(const char *resources) test_draw_lines, test_draw_rect, test_draw_circle, - test_fill_circle + test_fill_circle, + test_copy_bitmap_p1 }; result_t rc; diff --git a/libraries/framebuf/span-registry/regdata.h b/libraries/framebuf/span-registry/regdata.h index 37b3fe1..52acecc 100644 --- a/libraries/framebuf/span-registry/regdata.h +++ b/libraries/framebuf/span-registry/regdata.h @@ -6,6 +6,7 @@ #include "base/utils.h" #include "framebuf/span-bgrx8888.h" +#include "framebuf/span-p1.h" #include "framebuf/span-p4.h" #include "framebuf/span-rgbx8888.h" #include "framebuf/span-xbgr8888.h" @@ -15,6 +16,7 @@ static const span_t *spans[] = { &span_bgrx8888, + &span_p1, &span_p4, &span_rgbx8888, &span_xbgr8888, diff --git a/libraries/framebuf/span/p1.c b/libraries/framebuf/span/p1.c new file mode 100644 index 0000000..b1e32d5 --- /dev/null +++ b/libraries/framebuf/span/p1.c @@ -0,0 +1,105 @@ +/* framebuf/span/p1.c -- P1 (1bpp paletted) format plot methods */ + +#include + +#include "framebuf/colour.h" +#include "framebuf/pixelfmt.h" + +#include "framebuf/span.h" + +#include "framebuf/span-p1.h" + +#define SPAN_P1_NENTRIES 2 + +/* pixel values here are unpacked palette indices, one per byte (not the + * eight-bits-per-byte layout used in screen memory); packing/unpacking + * into the actual screen bytes is the caller's job, as with + * screen_set_pixel's other pixel formats. src2 is an array of colour_t, + * not pre-quantised pixels, so the blend happens in full RGB precision + * before re-quantising to the nearest palette entry. */ +static void span_p1_blendconst(void *vdst, + const void *vsrc1, + const void *vsrc2, + int length, + int alpha, + const void *context) +{ + unsigned char *pdst; + const unsigned char *psrc1; + const colour_t *psrc2; + const colour_t *palette; + + pdst = vdst; + psrc1 = vsrc1; + psrc2 = vsrc2; + palette = context; + + while (length--) + { + colour_t old_colour, new_colour, blended; + pixelfmt_rgba8888_t oldpx, newpx; + int old_r, old_g, old_b; + int new_r, new_g, new_b; + int blend_r, blend_g, blend_b; + + old_colour = palette[*psrc1 & 1]; + oldpx = old_colour.primary; + old_r = PIXELFMT_Rxxx8888(oldpx); + old_g = PIXELFMT_xGxx8888(oldpx); + old_b = PIXELFMT_xxBx8888(oldpx); + + new_colour = *psrc2; + newpx = new_colour.primary; + new_r = PIXELFMT_Rxxx8888(newpx); + new_g = PIXELFMT_xGxx8888(newpx); + new_b = PIXELFMT_xxBx8888(newpx); + + blend_r = (new_r * alpha + old_r * (255 - alpha)) / 255; + blend_g = (new_g * alpha + old_g * (255 - alpha)) / 255; + blend_b = (new_b * alpha + old_b * (255 - alpha)) / 255; + + blended = colour_rgb(blend_r, blend_g, blend_b); + *pdst = (unsigned char) colour_to_pixel(palette, SPAN_P1_NENTRIES, blended, pixelfmt_p1); + + pdst++; + psrc1++; + psrc2++; + } +} + +/* unlike span_p1_blendconst above, this works directly on packed screen + * bytes: "dst" is the row base, "first" the bit (pixel) index into it, so + * odd start columns and odd lengths are handled without the caller packing. + * "pixel" is a palette index in its low bit. Bit 7 of a byte is the + * leftmost pixel, matching PNG's MSB-first 1bpp packing. */ +static void span_p1_fill(void *vdst, + int first, + pixelfmt_any_t pixel, + int length) +{ + unsigned char *base; + unsigned char bit; + int x; + + base = vdst; + bit = (unsigned char) (pixel & 1); + + for (x = first; x < first + length; x++) + { + unsigned char *p; + int shift; + + p = base + (x >> 3); + shift = 7 - (x & 7); + *p = (unsigned char) ((*p & ~(1 << shift)) | (bit << shift)); + } +} + +const span_t span_p1 = +{ + pixelfmt_p1, + NULL, /* copy: unneeded so far (bit packing makes a generic array copy awkward) */ + span_p1_fill, + span_p1_blendconst, + NULL, /* blendarray: unneeded so far */ +}; From 4e5c97f8a0373a28e06d579fc5159b1a986389d8 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Wed, 9 Sep 2026 19:11:05 +0100 Subject: [PATCH 75/98] feat(wuss): support a 1bpp (pixelfmt_p1) desktop Add p1 plot paths to the screen primitives so the wuss demo can run on a monochrome framebuffer: - screen_set_pixel / screen_blend_pixel / screen_fill_pattern gain a p1 case, packed MSB-first to match PNG and the existing pattern bit order. - New pixelfmt_paletted_nentries() replaces the (scr->format == pixelfmt_p4) ? 16 : 0 hack at every colour_to_pixel call site, which passed nentries=0 (broken) for any non-p4 paletted screen. - bitmap_convert grows a p1 -> bgrx8888 path, used by the SDL frontend's per-frame display conversion. - apps/wuss: --depth 1 selects pixelfmt_p1 (stride (width + 7) / 8). - screen_copy_rect returns result_NOT_SUPPORTED for p1; callers fall back to redraw. Fix a latent bug in spanregistry_get(): its one-slot cache started with lastformat zero-initialised, which equals pixelfmt_p1 (the first enum member), so the first p1 lookup short-circuited to a NULL span and tripped the assert in screen_fill_hline. Seed lastformat with pixelfmt_unknown and guard the fast path on a non-NULL span. Co-Authored-By: Claude Sonnet 5 --- CMakeLists.txt | 1 + apps/wuss/frontend-sdl.c | 20 +++--- apps/wuss/main.c | 4 +- include/framebuf/pixelfmt.h | 11 ++++ libraries/framebuf/bitmap/bitmap.c | 64 +++++++++++++++++++ .../framebuf/pixelfmt/paletted-nentries.c | 21 ++++++ libraries/framebuf/screen/screen-draw.c | 37 ++++++++++- libraries/framebuf/screen/screen-fill-hline.c | 2 +- .../framebuf/screen/screen-fill-pattern.c | 47 +++++++++++++- libraries/framebuf/screen/test/screen-test.c | 39 +++++++++++ libraries/framebuf/span-registry/get.c | 7 +- 11 files changed, 237 insertions(+), 16 deletions(-) create mode 100644 libraries/framebuf/pixelfmt/paletted-nentries.c diff --git a/CMakeLists.txt b/CMakeLists.txt index e5027ae..1cfbca7 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -264,6 +264,7 @@ set(FRAMEBUF_SOURCES libraries/framebuf/palettes/palettes.c libraries/framebuf/pattern/pattern.c libraries/framebuf/pixelfmt/log2bpp.c + libraries/framebuf/pixelfmt/paletted-nentries.c libraries/framebuf/pixelmap/build.c libraries/framebuf/pixelmap/get.c libraries/framebuf/pixelmap/impl.h diff --git a/apps/wuss/frontend-sdl.c b/apps/wuss/frontend-sdl.c index c40755d..434522a 100644 --- a/apps/wuss/frontend-sdl.c +++ b/apps/wuss/frontend-sdl.c @@ -22,11 +22,12 @@ #include "frontend.h" -/* Screen pixel format for the demo, chosen at run time via -depth: 32 = +/* Screen pixel format for the demo, chosen at run time via --depth: 32 = * pixelfmt_bgrx8888 (feeds SDL directly, no per-frame conversion); 4 (the * default) = pixelfmt_p4 paletted (exercises screen_copy_rect's nibble-packed - * blit path instead). No other depth is handled yet. Stashed in struct - * wuss_frontend so present() can branch on it. */ + * blit path instead); 1 = pixelfmt_p1 monochrome. Paletted depths are converted + * to bgrx8888 per frame via bitmap_convert. Stashed in struct wuss_frontend so + * present() can branch on it. */ /* Integer window zoom: the fixed Wuss screen is drawn at this many device * pixels per screen pixel. The initial value comes from -scale (0 => use the @@ -46,7 +47,7 @@ struct wuss_frontend int scr_width; int scr_height; int scale; /* device pixels per screen pixel; see WUSS_SDL_*_SCALE */ - int depth; /* framebuffer bits per pixel: 32 (bgrx8888) or 4 (p4) */ + int depth; /* framebuffer bits per pixel: 32 (bgrx8888), 4 (p4) or 1 (p1) */ void *pixels; /* the private framebuffer handed to the caller */ }; @@ -99,9 +100,9 @@ result_t wuss_frontend_open(int width, NOT_USED(palette); NOT_USED(npalette); - if (depth != 4 && depth != 32) + if (depth != 1 && depth != 4 && depth != 32) { - fprintf(stderr, "Error: unsupported -depth %d (want 4 or 32)\n", depth); + fprintf(stderr, "Error: unsupported depth %d (want 1, 4 or 32)\n", depth); return result_BAD_ARG; } @@ -110,7 +111,8 @@ result_t wuss_frontend_open(int width, scale = CLAMP(scale, WUSS_SDL_MIN_SCALE, WUSS_SDL_MAX_SCALE); stride = (depth == 32) ? width * 4 /* pixelfmt_bgrx8888: 4 bytes/pixel */ - : width / 2; /* pixelfmt_p4: 2 pixels/byte */ + : (depth == 4) ? width / 2 /* pixelfmt_p4: 2 pixels/byte */ + : (width + 7) / 8; /* pixelfmt_p1: 8 pixels/byte */ fe = calloc(1, sizeof(*fe)); if (fe == NULL) @@ -161,7 +163,9 @@ result_t wuss_frontend_open(int width, /* keep pixels crisp when F2 scales the window up */ SDL_SetTextureScaleMode(fe->texture, SDL_SCALEMODE_NEAREST); - *fmt = (depth == 32) ? pixelfmt_bgrx8888 : pixelfmt_p4; + *fmt = (depth == 32) ? pixelfmt_bgrx8888 + : (depth == 4) ? pixelfmt_p4 + : pixelfmt_p1; *pixels = fe->pixels; *rowbytes = stride; diff --git a/apps/wuss/main.c b/apps/wuss/main.c index 93c5f00..ab57d9f 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -426,14 +426,14 @@ typedef struct wuss_options { const char *resources; /* -r/--resources: fixture root */ const char *palette_name; /* -p/--palette: startup *.hex leafname */ - int depth; /* -d/--depth: framebuffer bpp (4 or 32) */ + int depth; /* -d/--depth: framebuffer bpp (1, 4 or 32) */ int scale; /* -s/--scale: initial window zoom, 0 = default */ } wuss_options_t; static const char wuss_usage[] = "usage: wuss [-r|--resources DIR] [-p|--palette NAME] " - "[-d|--depth 4|32] [-s|--scale N]\n"; + "[-d|--depth 1|4|32] [-s|--scale N]\n"; #ifndef __riscos diff --git a/include/framebuf/pixelfmt.h b/include/framebuf/pixelfmt.h index fe3849a..ff6d2d9 100644 --- a/include/framebuf/pixelfmt.h +++ b/include/framebuf/pixelfmt.h @@ -200,6 +200,17 @@ typedef unsigned int pixelfmt_any32_t; /* any 32bpp pixel */ */ int pixelfmt_log2bpp(pixelfmt_t fmt); +/** + * Number of palette entries a packed paletted format addresses: 2 for p1, 4 + * for p2, 16 for p4, 256 for p8. 0 for any non-paletted format (its + * "palette" is not indexed). Useful as the `nentries` argument to \ref + * colour_to_pixel when the destination is a fixed-size paletted screen. + * + * \param fmt Pixel format. + * \return Entry count, or 0 if `fmt` is not a packed paletted format. + */ +int pixelfmt_paletted_nentries(pixelfmt_t fmt); + /* ----------------------------------------------------------------------- */ #ifdef __cplusplus diff --git a/libraries/framebuf/bitmap/bitmap.c b/libraries/framebuf/bitmap/bitmap.c index 9b54420..d285e51 100644 --- a/libraries/framebuf/bitmap/bitmap.c +++ b/libraries/framebuf/bitmap/bitmap.c @@ -194,6 +194,59 @@ static result_t bmconv_p4_to_bgrx8888(const bitmap_t *src, bitmap_t **pdst) return rc; } +/* As bmconv_p4_to_bgrx8888 but for 1bpp paletted, packed MSB-first (bit 7 is + * the leftmost pixel) to match the screen p1 plot helpers. */ +static result_t bmconv_p1_to_bgrx8888(const bitmap_t *src, bitmap_t **pdst) +{ + result_t rc; + const pixelmap_t *pm; + const pixelfmt_bgrx8888_t *map; + bitmap_t *dst; + pixelfmt_bgrx8888_t *outpixels; + const unsigned char *inrow; + int x, y; + + assert(src); + assert(src->palette); + + pm = pixelmap_get(pixelfmt_p1, pixelfmt_bgrx8888, src->palette, 2); + if (pm == NULL) + return result_NOT_SUPPORTED; + map = (const pixelfmt_bgrx8888_t *) pm->entries; + + outpixels = malloc(src->size.w * sizeof(pixelfmt_bgrx8888_t) * src->size.h); + if (outpixels == NULL) + return result_OOM; + + dst = malloc(sizeof(*dst)); + if (dst == NULL) + { + free(outpixels); + return result_OOM; + } + + rc = bitmap_init(dst, + src->size, + pixelfmt_bgrx8888, + src->size.w * sizeof(pixelfmt_bgrx8888_t), + NULL, + outpixels); + if (rc) + return rc; + + inrow = src->base; + for (y = 0; y < src->size.h; y++) + { + for (x = 0; x < src->size.w; x++) + *outpixels++ = map[(inrow[x >> 3] >> (7 - (x & 7))) & 1]; + inrow += src->rowbytes; + } + + *pdst = dst; + + return rc; +} + result_t bitmap_convert(const bitmap_t *src, pixelfmt_t newfmt, bitmap_t **dst) @@ -205,6 +258,17 @@ result_t bitmap_convert(const bitmap_t *src, switch (src->format) { + case pixelfmt_p1: + switch (newfmt) + { + case pixelfmt_bgrx8888: + return bmconv_p1_to_bgrx8888(src, dst); + + default: + return result_NOT_SUPPORTED; + } + break; + case pixelfmt_p4: switch (newfmt) { diff --git a/libraries/framebuf/pixelfmt/paletted-nentries.c b/libraries/framebuf/pixelfmt/paletted-nentries.c new file mode 100644 index 0000000..d7f4682 --- /dev/null +++ b/libraries/framebuf/pixelfmt/paletted-nentries.c @@ -0,0 +1,21 @@ +/* framebuf/pixelfmt/paletted-nentries.c -- palette entry count for a packed paletted format */ + +#include "framebuf/pixelfmt.h" + +int pixelfmt_paletted_nentries(pixelfmt_t fmt) +{ + switch (fmt) + { + case pixelfmt_p1: + return 2; + case pixelfmt_p2: + return 4; + case pixelfmt_p4: + return 16; + case pixelfmt_p8: + return 256; + + default: + return 0; + } +} diff --git a/libraries/framebuf/screen/screen-draw.c b/libraries/framebuf/screen/screen-draw.c index 1b7eb31..2caf91a 100644 --- a/libraries/framebuf/screen/screen-draw.c +++ b/libraries/framebuf/screen/screen-draw.c @@ -26,6 +26,20 @@ /* Each helper writes the single pixel (x, y), already known to be inside the * clip, with the colour previously resolved to "pxl". */ +static void screen_set_pixel_p1(screen_t *scr, + int x, + int y, + pixelfmt_any_t pxl) +{ + unsigned char *scrp; + int shift; + + scrp = (unsigned char *) scr->base + y * scr->rowbytes + (x >> 3); + shift = 7 - (x & 7); /* bit 7 is the leftmost pixel */ + + *scrp = (unsigned char) ((*scrp & ~(1 << shift)) | ((pxl & 1) << shift)); +} + static void screen_set_pixel_p4(screen_t *scr, int x, int y, @@ -88,10 +102,11 @@ void screen_set_pixel(screen_t *scr, int x, int y, colour_t colour) return; pxl = colour_to_pixel(scr->palette, - (scr->format == pixelfmt_p4) ? 16 : 0, + pixelfmt_paletted_nentries(scr->format), colour, scr->format); switch (pixelfmt_log2bpp(scr->format)) { + case 0: screen_set_pixel_p1(scr, x, y, pxl); break; case 2: screen_set_pixel_p4(scr, x, y, pxl); break; case 3: screen_set_pixel_8(scr, x, y, pxl); break; case 4: screen_set_pixel_16(scr, x, y, pxl); break; @@ -106,6 +121,25 @@ void screen_set_pixel(screen_t *scr, int x, int y, colour_t colour) /* Each helper alpha-blends "colour" at "alpha" into the single pixel (x, y), * already known to be inside the clip. */ +static void screen_blend_pixel_p1(screen_t *scr, + int x, + int y, + colour_t colour, + int alpha) +{ + unsigned char *scrp; + int shift; + unsigned char idx, out; + + scrp = (unsigned char *) scr->base + y * scr->rowbytes + (x >> 3); + shift = 7 - (x & 7); + idx = (*scrp >> shift) & 1; + + scr->span->blendconst(&out, &idx, &colour, 1, alpha, scr->palette); + + *scrp = (unsigned char) ((*scrp & ~(1 << shift)) | ((out & 1) << shift)); +} + static void screen_blend_pixel_p4(screen_t *scr, int x, int y, @@ -158,6 +192,7 @@ static void screen_blend_pixel(screen_t *scr, switch (pixelfmt_log2bpp(scr->format)) { + case 0: screen_blend_pixel_p1(scr, x, y, colour, alpha); break; case 2: screen_blend_pixel_p4(scr, x, y, colour, alpha); break; case 5: screen_blend_pixel_32(scr, x, y, colour, alpha); break; diff --git a/libraries/framebuf/screen/screen-fill-hline.c b/libraries/framebuf/screen/screen-fill-hline.c index 45403da..5069d98 100644 --- a/libraries/framebuf/screen/screen-fill-hline.c +++ b/libraries/framebuf/screen/screen-fill-hline.c @@ -34,7 +34,7 @@ void screen_fill_hline(screen_t *scr, int x, int y, int w, colour_t colour) clipped_width = draw_box.x1 - draw_box.x0; fmt = colour_to_pixel(scr->palette, - (scr->format == pixelfmt_p4) ? 16 : 0, + pixelfmt_paletted_nentries(scr->format), colour, scr->format); /* the per-format run fill lives in the span table; "first" lets it address diff --git a/libraries/framebuf/screen/screen-fill-pattern.c b/libraries/framebuf/screen/screen-fill-pattern.c index 9751db2..f6dcdc1 100644 --- a/libraries/framebuf/screen/screen-fill-pattern.c +++ b/libraries/framebuf/screen/screen-fill-pattern.c @@ -17,6 +17,45 @@ * bit per pixel rather than using the run. */ typedef pixelfmt_any_t pattern_runs_t[8][8]; +static void screen_fill_pattern_p1(screen_t *scr, + const pattern_t *pattern, + const box_t *draw_box, + int stencil, + pattern_runs_t runs) +{ + unsigned char *rowp; + int row, col, x, y; + + rowp = (unsigned char *) scr->base + draw_box->y0 * scr->rowbytes; + for (y = draw_box->y0; y < draw_box->y1; y++) + { + const pixelfmt_any_t *run; + uint8_t bits; + + row = (y - pattern->origin.y) & 7; + run = runs[row]; + bits = pattern->bits[row]; + col = 0; + for (x = draw_box->x0; x < draw_box->x1; x++) + { + if (!stencil || + (bits & (0x80u >> ((x - pattern->origin.x) & 7)))) + { + unsigned char *scrp; + int shift; + + scrp = rowp + (x >> 3); + shift = 7 - (x & 7); + + *scrp = (unsigned char) ((*scrp & ~(1 << shift)) | + ((run[col] & 1) << shift)); + } + col = (col + 1) & 7; + } + rowp += scr->rowbytes; + } +} + static void screen_fill_pattern_p4(screen_t *scr, const pattern_t *pattern, const box_t *draw_box, @@ -131,10 +170,10 @@ void screen_fill_pattern(screen_t *scr, stencil = (pattern->flags & pattern_FLAG_STENCIL) != 0; fg_fmt = colour_to_pixel(scr->palette, - (scr->format == pixelfmt_p4) ? 16 : 0, + pixelfmt_paletted_nentries(scr->format), pattern->fg, scr->format); bg_fmt = colour_to_pixel(scr->palette, - (scr->format == pixelfmt_p4) ? 16 : 0, + pixelfmt_paletted_nentries(scr->format), pattern->bg, scr->format); xphase = ((draw_box.x0 - pattern->origin.x) & 7); @@ -150,6 +189,10 @@ void screen_fill_pattern(screen_t *scr, switch (pixelfmt_log2bpp(scr->format)) { + case 0: + screen_fill_pattern_p1(scr, pattern, &draw_box, stencil, runs); + break; + case 2: screen_fill_pattern_p4(scr, pattern, &draw_box, stencil, runs); break; diff --git a/libraries/framebuf/screen/test/screen-test.c b/libraries/framebuf/screen/test/screen-test.c index b35ee7f..08d8496 100644 --- a/libraries/framebuf/screen/test/screen-test.c +++ b/libraries/framebuf/screen/test/screen-test.c @@ -806,6 +806,45 @@ static result_t test_copy_bitmap_p1(void) } } + /* screen_set_pixel: white pixel at x=2 sets bit 7-2 of byte 0 */ + memset(p1pixels, 0, sizeof(p1pixels)); + screen_set_pixel(&scr, 2, 0, pal[1]); + if (p1pixels[0] != (1 << (7 - 2))) + { + printf("screen: p1 set_pixel packed wrong (0x%02X)\n", p1pixels[0]); + return result_TEST_FAILED; + } + + /* screen_fill_hline: 8 white pixels from x=0 fill byte 0 */ + memset(p1pixels, 0, sizeof(p1pixels)); + screen_fill_hline(&scr, 0, 1, 8, pal[1]); + if (p1pixels[P1_ROWBYTES] != 0xFF) + { + printf("screen: p1 fill_hline packed wrong (0x%02X)\n", + p1pixels[P1_ROWBYTES]); + return result_TEST_FAILED; + } + + /* screen_fill_pattern: solid all-bits pattern in fg colour = 0xFF row */ + memset(p1pixels, 0, sizeof(p1pixels)); + { + pattern_t pat; + box_t b; + + memset(&pat, 0, sizeof(pat)); + memset(pat.bits, 0xFF, sizeof(pat.bits)); + pat.fg = pal[1]; + pat.bg = pal[0]; + b.x0 = 0; b.y0 = 2; b.x1 = 8; b.y1 = 3; + screen_fill_pattern(&scr, &b, &pat); + if (p1pixels[2 * P1_ROWBYTES] != 0xFF) + { + printf("screen: p1 fill_pattern packed wrong (0x%02X)\n", + p1pixels[2 * P1_ROWBYTES]); + return result_TEST_FAILED; + } + } + return result_TEST_PASSED; } diff --git a/libraries/framebuf/span-registry/get.c b/libraries/framebuf/span-registry/get.c index 52f99df..eea5cef 100644 --- a/libraries/framebuf/span-registry/get.c +++ b/libraries/framebuf/span-registry/get.c @@ -12,12 +12,15 @@ const span_t *spanregistry_get(pixelfmt_t format) { - static pixelfmt_t lastformat; + /* seed the cache with a value no real pixelfmt_t takes, so the first call + * for pixelfmt_p1 (== 0) still does the lookup rather than returning the + * zero-initialised lastspan (NULL) */ + static pixelfmt_t lastformat = pixelfmt_unknown; static const span_t *lastspan; int i; /* avoid full lookup where possible */ - if (lastformat == format) + if (lastformat == format && lastspan != NULL) return lastspan; for (i = 0; i < nspans; i++) From ca028b2c5c522b83b2ed3604ef7e774c721549c0 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Wed, 9 Sep 2026 19:27:12 +0100 Subject: [PATCH 76/98] feat(bmfont): render to a 1bpp (pixelfmt_p1) screen Add p1 drawchar variants and a pixelfmt_p1 case to bmfont_draw's dispatch. A single bmfont_p1_plot_row helper does the per-pixel MSB-first bit write for both 1- and 2-byte glyph rows and both opaque and transparent backgrounds; the LUT-driven width unrolling the p4 path uses buys nothing for a plain bit write. The rest of bmfont_draw (log2bpp, shift, screen pointer math, native fg/bg via 1 << (1 << log2bpp)) already handles log2bpp == 0. Add pixelfmt_p1 to bmfont_test's format table. Co-Authored-By: Claude Sonnet 5 --- libraries/framebuf/bmfont/bmfont.c | 171 +++++++++++++++++++ libraries/framebuf/bmfont/test/bmfont-test.c | 1 + 2 files changed, 172 insertions(+) diff --git a/libraries/framebuf/bmfont/bmfont.c b/libraries/framebuf/bmfont/bmfont.c index c7e77b6..886c6c6 100644 --- a/libraries/framebuf/bmfont/bmfont.c +++ b/libraries/framebuf/bmfont/bmfont.c @@ -986,6 +986,168 @@ static void bmfont_drawchar_p4_2w_t(void *vscreen, /* -------------------------------------------------------------------------- */ +/* Plot one glyph row onto a p1 (1bpp) screen row. "row" is the glyph bits + * already shifted down by right_skip, so bit (cw - 1) is the leftmost visible + * pixel. "scr" points at the byte holding pixel column 0; "shift" (0..7) is + * that pixel's bit offset from the byte's MSB, matching the MSB-first 1bpp + * packing used elsewhere. When "opaque" is zero, clear glyph bits are left + * untouched. */ +static void bmfont_p1_plot_row(unsigned char *scr, + unsigned int row, + int cw, + int shift, + pixelfmt_any_t fg, + pixelfmt_any_t bg, + int opaque) +{ + int i; + + for (i = 0; i < cw; i++) + { + int set; + int bitpos; + unsigned char *p; + int b; + + set = (row >> (cw - 1 - i)) & 1; + if (!set && !opaque) + continue; + + bitpos = shift + i; + p = scr + (bitpos >> 3); + b = 7 - (bitpos & 7); + + *p = (unsigned char) ((*p & ~(1 << b)) + | (((set ? fg : bg) & 1) << b)); + } +} + +/* Draw a character to a p1 screen. One helper covers 1- and 2-byte glyph rows + * and both opaque and transparent backgrounds -- the p1 inner loop is a plain + * per-pixel bit write, so the LUT-driven width unrolling the p4 path needs + * buys nothing here. */ +static void bmfont_drawchar_p1_1w_o(void *vscreen, + const void *vglyph, + int top_skip, + int right_skip, + int shift, + int rowbytes, + int charwidth, + int charheight, + pixelfmt_any_t fg, + pixelfmt_any_t bg) +{ + unsigned char *scr = vscreen; + const unsigned char *gly = vglyph; + int stride; + + assert(charwidth > 0); + assert(charheight > 0); + + gly += top_skip; + stride = rowbytes; + + while (charheight--) + { + unsigned int row = (unsigned int) *gly++ >> right_skip; + + bmfont_p1_plot_row(scr, row, charwidth, shift, fg, bg, 1); + scr += stride; + } +} + +static void bmfont_drawchar_p1_1w_t(void *vscreen, + const void *vglyph, + int top_skip, + int right_skip, + int shift, + int rowbytes, + int charwidth, + int charheight, + pixelfmt_any_t fg, + pixelfmt_any_t bg) +{ + unsigned char *scr = vscreen; + const unsigned char *gly = vglyph; + int stride; + + assert(charwidth > 0); + assert(charheight > 0); + + gly += top_skip; + stride = rowbytes; + + while (charheight--) + { + unsigned int row = (unsigned int) *gly++ >> right_skip; + + bmfont_p1_plot_row(scr, row, charwidth, shift, fg, bg, 0); + scr += stride; + } +} + +static void bmfont_drawchar_p1_2w_o(void *vscreen, + const void *vglyph, + int top_skip, + int right_skip, + int shift, + int rowbytes, + int charwidth, + int charheight, + pixelfmt_any_t fg, + pixelfmt_any_t bg) +{ + unsigned char *scr = vscreen; + const unsigned short *gly = vglyph; + int stride; + + assert(charwidth > 0); + assert(charheight > 0); + + gly += top_skip; + stride = rowbytes; + + while (charheight--) + { + unsigned int row = (unsigned int) *gly++ >> right_skip; + + bmfont_p1_plot_row(scr, row, charwidth, shift, fg, bg, 1); + scr += stride; + } +} + +static void bmfont_drawchar_p1_2w_t(void *vscreen, + const void *vglyph, + int top_skip, + int right_skip, + int shift, + int rowbytes, + int charwidth, + int charheight, + pixelfmt_any_t fg, + pixelfmt_any_t bg) +{ + unsigned char *scr = vscreen; + const unsigned short *gly = vglyph; + int stride; + + assert(charwidth > 0); + assert(charheight > 0); + + gly += top_skip; + stride = rowbytes; + + while (charheight--) + { + unsigned int row = (unsigned int) *gly++ >> right_skip; + + bmfont_p1_plot_row(scr, row, charwidth, shift, fg, bg, 0); + scr += stride; + } +} + +/* -------------------------------------------------------------------------- */ + /* Draw a character, a maximum of one byte wide, to any 8888 screen using an * opaque background. */ static void bmfont_drawchar_any8888_1w_o(void *vscreen, @@ -1198,6 +1360,15 @@ result_t bmfont_draw(bmfont_t *bmfont, switch (scr->format) { + case pixelfmt_p1: + switch (bmfont->glyphrowbytes) + { + case 1: drawfn = (bgalpha < 255) ? bmfont_drawchar_p1_1w_t : bmfont_drawchar_p1_1w_o; break; + case 2: drawfn = (bgalpha < 255) ? bmfont_drawchar_p1_2w_t : bmfont_drawchar_p1_2w_o; break; + default: assert(0); return result_NOT_SUPPORTED; + } + break; + case pixelfmt_p4: switch (bmfont->glyphrowbytes) { diff --git a/libraries/framebuf/bmfont/test/bmfont-test.c b/libraries/framebuf/bmfont/test/bmfont-test.c index bd6e2ad..7ad7f82 100644 --- a/libraries/framebuf/bmfont/test/bmfont-test.c +++ b/libraries/framebuf/bmfont/test/bmfont-test.c @@ -1027,6 +1027,7 @@ result_t bmfont_test(const char *resources) } tab[] = { + { 800, 600, pixelfmt_p1 }, { 800, 600, pixelfmt_p4 }, { 800, 600, pixelfmt_bgrx8888 } }; From 918736f8d5337a078f8395681896083a805fccef Mon Sep 17 00:00:00 2001 From: David Thomas Date: Wed, 9 Sep 2026 19:35:25 +0100 Subject: [PATCH 77/98] feat(wuss): draw the control polygon's convex hull in the curve task Andrew's monotone chain over the active control points, drawn as a closed light-grey polyline before the curve and blobs. Also fixes the curve-type label origin: it used the per-redraw dirty piece (event->data.redraw.content) instead of the full content box (bounds), so a partial redraw drew the label at the wrong place. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/test/tasks/curve.c | 74 +++++++++++++++++++++++++++++-- 1 file changed, 71 insertions(+), 3 deletions(-) diff --git a/libraries/wuss/test/tasks/curve.c b/libraries/wuss/test/tasks/curve.c index 13d83ef..9e39148 100644 --- a/libraries/wuss/test/tasks/curve.c +++ b/libraries/wuss/test/tasks/curve.c @@ -27,6 +27,58 @@ #define CURVE_SEGMENTS_MIN 4 #define CURVE_SEGMENTS_MAX 128 +/* 2D cross product of (b - a) and (c - a); > 0 for a left (counter-clockwise) + * turn, < 0 for a right turn, 0 if collinear. long to keep the products of + * two point coordinates from overflowing an int. */ +static long curve_cross(point_t a, point_t b, point_t c) +{ + return (long) (b.x - a.x) * (c.y - a.y) - (long) (b.y - a.y) * (c.x - a.x); +} + +static int curve_point_cmp(const void *va, const void *vb) +{ + const point_t *a = va; + const point_t *b = vb; + + if (a->x != b->x) + return a->x - b->x; + return a->y - b->y; +} + +/* Andrew's monotone chain: fill "hull" (capacity 2 * n + 1) with the convex + * hull of the first n of "src" as a closed polyline (first point repeated at + * the end) and return its point count. n < 3, or all points collinear, + * degenerates to the sorted span, still returned closed. */ +static int curve_convex_hull(const point_t *src, int n, point_t *hull) +{ + point_t pts[CURVE_MAXCONTROLPTS]; + int k, i; + + memcpy(pts, src, (size_t) n * sizeof(*pts)); + qsort(pts, (size_t) n, sizeof(*pts), curve_point_cmp); + + k = 0; + for (i = 0; i < n; i++) /* lower hull */ + { + while (k >= 2 && curve_cross(hull[k - 2], hull[k - 1], pts[i]) <= 0) + k--; + hull[k++] = pts[i]; + } + { + int lower, j; + + lower = k + 1; + for (j = n - 2; j >= 0; j--) /* upper hull */ + { + while (k >= lower && curve_cross(hull[k - 2], hull[k - 1], pts[j]) <= 0) + k--; + hull[k++] = pts[j]; + } + } + + return k; /* hull[0] == hull[k - 1], a closed loop */ +} + result_t curve_create(wuss_t *wuss, curve_task_t *task) { result_t rc; @@ -149,6 +201,21 @@ static result_t curve_redraw(const wuss_event_t *event, curve_task_t *task) screen_fill_rect(scr, content->x0, content->y0, box_size(content), task->bg); + /* control polygon's convex hull, drawn first so the curve and the blobs + * sit on top of it */ + { + point_t hull[2 * CURVE_MAXCONTROLPTS + 1]; + int nhull, k; + + nhull = curve_convex_hull(task->points, task->npoints, hull); + for (k = 0; k < nhull; k++) + { + hull[k].x += bounds->x0 - sx; + hull[k].y += bounds->y0 - sy; + } + screen_draw_lines(scr, hull, nhull, colour_rgb(0xC0, 0xC0, 0xC0)); + } + prev = task->points[0]; prev.x += bounds->x0 - sx; prev.y += bounds->y0 - sy; @@ -172,12 +239,13 @@ static result_t curve_redraw(const wuss_event_t *event, curve_task_t *task) blob_colour(task, i)); } - /* curve-type label, pinned to the content's top-left corner (not the - * scrolled document) so an Adjust click's effect is always readable */ + /* curve-type label, pinned to the content area's top-left corner (bounds, + * not the per-redraw dirty piece "content", and not the scrolled document) + * so it stays put and readable on a partial redraw */ { bmfont_t *font = wuss_get_font(task->wuss); const char *name = curve_kind_name(task->npoints); - point_t pos = POINT(content->x0 + 2, content->y0 + 2); + point_t pos = POINT(bounds->x0 + 2, bounds->y0 + 2); if (font != NULL) bmfont_draw(font, scr, name, (int) strlen(name), From bea9304a08e0b55254af1fe1faf25b9f8460fd4b Mon Sep 17 00:00:00 2001 From: David Thomas Date: Wed, 9 Sep 2026 19:42:18 +0100 Subject: [PATCH 78/98] feat(framebuf): support 2bpp paletted (p2) bitmaps Mirror the recent p1 work for pixelfmt_p2 (four palette entries, packed MSB-first: bits 7..6 of a byte are the leftmost pixel). - span/p2.c + span-p2.h: span_p2_fill and span_p2_blendconst, registered in the span registry. - screen-draw.c: screen_set_pixel_p2, screen_blend_pixel_p2 and screen_copy_bitmap_p2, wired into the three pixelfmt_log2bpp switches. - screen-fill-pattern.c: screen_fill_pattern_p2. - bitmap.c: bmconv_p2_to_bgrx8888 for bitmap_convert. - pixelmap and pixelfmt_paletted_nentries already handle p2 generically. - Tests: p2 cases in pixelmap-test.c and screen-test.c. Co-Authored-By: Claude Sonnet 5 --- CMakeLists.txt | 2 + include/framebuf/span-p2.h | 10 ++ libraries/framebuf/bitmap/bitmap.c | 64 ++++++++++ .../framebuf/pixelmap/test/pixelmap-test.c | 36 ++++++ libraries/framebuf/screen/screen-draw.c | 107 +++++++++++++++++ .../framebuf/screen/screen-fill-pattern.c | 43 +++++++ libraries/framebuf/screen/test/screen-test.c | 113 +++++++++++++++++- libraries/framebuf/span-registry/regdata.h | 2 + libraries/framebuf/span/p2.c | 108 +++++++++++++++++ 9 files changed, 484 insertions(+), 1 deletion(-) create mode 100644 include/framebuf/span-p2.h create mode 100644 libraries/framebuf/span/p2.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 1cfbca7..2c54a5b 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -90,6 +90,7 @@ set(PUBLIC_HEADERS include/framebuf/screen.h include/framebuf/span-bgrx8888.h include/framebuf/span-p1.h + include/framebuf/span-p2.h include/framebuf/span-p4.h include/framebuf/span-registry.h include/framebuf/span-rgba8888.h @@ -283,6 +284,7 @@ set(FRAMEBUF_SOURCES libraries/framebuf/span/all8888.h libraries/framebuf/span/bgrx8888.c libraries/framebuf/span/p1.c + libraries/framebuf/span/p2.c libraries/framebuf/span/p4.c libraries/framebuf/span/rgbx8888.c libraries/framebuf/span/xbgr8888.c) diff --git a/include/framebuf/span-p2.h b/include/framebuf/span-p2.h new file mode 100644 index 0000000..dec43ea --- /dev/null +++ b/include/framebuf/span-p2.h @@ -0,0 +1,10 @@ +/* framebuf/span-p2.h -- P2 (2bpp paletted) format plot methods */ + +#ifndef SPAN_P2_H +#define SPAN_P2_H + +#include "framebuf/span.h" + +extern const span_t span_p2; + +#endif /* SPAN_P2_H */ diff --git a/libraries/framebuf/bitmap/bitmap.c b/libraries/framebuf/bitmap/bitmap.c index d285e51..a0bae7e 100644 --- a/libraries/framebuf/bitmap/bitmap.c +++ b/libraries/framebuf/bitmap/bitmap.c @@ -247,6 +247,59 @@ static result_t bmconv_p1_to_bgrx8888(const bitmap_t *src, bitmap_t **pdst) return rc; } +/* As bmconv_p1_to_bgrx8888 but for 2bpp paletted, packed MSB-first (bits 7..6 + * are the leftmost pixel) to match the screen p2 plot helpers. */ +static result_t bmconv_p2_to_bgrx8888(const bitmap_t *src, bitmap_t **pdst) +{ + result_t rc; + const pixelmap_t *pm; + const pixelfmt_bgrx8888_t *map; + bitmap_t *dst; + pixelfmt_bgrx8888_t *outpixels; + const unsigned char *inrow; + int x, y; + + assert(src); + assert(src->palette); + + pm = pixelmap_get(pixelfmt_p2, pixelfmt_bgrx8888, src->palette, 4); + if (pm == NULL) + return result_NOT_SUPPORTED; + map = (const pixelfmt_bgrx8888_t *) pm->entries; + + outpixels = malloc(src->size.w * sizeof(pixelfmt_bgrx8888_t) * src->size.h); + if (outpixels == NULL) + return result_OOM; + + dst = malloc(sizeof(*dst)); + if (dst == NULL) + { + free(outpixels); + return result_OOM; + } + + rc = bitmap_init(dst, + src->size, + pixelfmt_bgrx8888, + src->size.w * sizeof(pixelfmt_bgrx8888_t), + NULL, + outpixels); + if (rc) + return rc; + + inrow = src->base; + for (y = 0; y < src->size.h; y++) + { + for (x = 0; x < src->size.w; x++) + *outpixels++ = map[(inrow[x >> 2] >> (6 - ((x & 3) << 1))) & 3]; + inrow += src->rowbytes; + } + + *pdst = dst; + + return rc; +} + result_t bitmap_convert(const bitmap_t *src, pixelfmt_t newfmt, bitmap_t **dst) @@ -269,6 +322,17 @@ result_t bitmap_convert(const bitmap_t *src, } break; + case pixelfmt_p2: + switch (newfmt) + { + case pixelfmt_bgrx8888: + return bmconv_p2_to_bgrx8888(src, dst); + + default: + return result_NOT_SUPPORTED; + } + break; + case pixelfmt_p4: switch (newfmt) { diff --git a/libraries/framebuf/pixelmap/test/pixelmap-test.c b/libraries/framebuf/pixelmap/test/pixelmap-test.c index 599d30a..5ce2813 100644 --- a/libraries/framebuf/pixelmap/test/pixelmap-test.c +++ b/libraries/framebuf/pixelmap/test/pixelmap-test.c @@ -164,6 +164,42 @@ result_t pixelmap_test(const char *resources) } } + /* 4c. rgba8888 -> p2 (4:4:4 table): a 4-entry greyscale palette resolves + * each grey band to its nearest index */ + { + const pixelmap_t *pm2; + colour_t grey[4]; + + grey[0] = colour_rgb(0x00, 0x00, 0x00); + grey[1] = colour_rgb(0x55, 0x55, 0x55); + grey[2] = colour_rgb(0xAA, 0xAA, 0xAA); + grey[3] = colour_rgb(0xFF, 0xFF, 0xFF); + + pm2 = pixelmap_get(pixelfmt_rgba8888, pixelfmt_p2, grey, 4); + if (pm2 == NULL) + { + printf("pixelmap: get rgba8888->p2 returned NULL\n"); + return result_TEST_FAILED; + } + if (pm2->dest_log2bpp != 1 || pm2->nentries != 4096) + { + printf("pixelmap: p2 layout wrong (dest_log2bpp %d, nentries %u)\n", + pm2->dest_log2bpp, pm2->nentries); + return result_TEST_FAILED; + } + + if (pm_lookup(pm2, pm_index(pm2, colour_rgb(0x08, 0x08, 0x08).primary)) != 0) + { + printf("pixelmap: p2 black did not resolve to index 0\n"); + return result_TEST_FAILED; + } + if (pm_lookup(pm2, pm_index(pm2, colour_rgb(0xF8, 0xF8, 0xF8).primary)) != 3) + { + printf("pixelmap: p2 white did not resolve to index 3\n"); + return result_TEST_FAILED; + } + } + /* 5. paletted -> deep: p4 -> bgrx8888, one deep pixel per palette index */ { const pixelmap_t *pmd; diff --git a/libraries/framebuf/screen/screen-draw.c b/libraries/framebuf/screen/screen-draw.c index 2caf91a..36f13b5 100644 --- a/libraries/framebuf/screen/screen-draw.c +++ b/libraries/framebuf/screen/screen-draw.c @@ -40,6 +40,20 @@ static void screen_set_pixel_p1(screen_t *scr, *scrp = (unsigned char) ((*scrp & ~(1 << shift)) | ((pxl & 1) << shift)); } +static void screen_set_pixel_p2(screen_t *scr, + int x, + int y, + pixelfmt_any_t pxl) +{ + unsigned char *scrp; + int shift; + + scrp = (unsigned char *) scr->base + y * scr->rowbytes + (x >> 2); + shift = 6 - ((x & 3) << 1); /* bits 7..6 are the leftmost pixel */ + + *scrp = (unsigned char) ((*scrp & ~(3 << shift)) | ((pxl & 3) << shift)); +} + static void screen_set_pixel_p4(screen_t *scr, int x, int y, @@ -107,6 +121,7 @@ void screen_set_pixel(screen_t *scr, int x, int y, colour_t colour) switch (pixelfmt_log2bpp(scr->format)) { case 0: screen_set_pixel_p1(scr, x, y, pxl); break; + case 1: screen_set_pixel_p2(scr, x, y, pxl); break; case 2: screen_set_pixel_p4(scr, x, y, pxl); break; case 3: screen_set_pixel_8(scr, x, y, pxl); break; case 4: screen_set_pixel_16(scr, x, y, pxl); break; @@ -140,6 +155,25 @@ static void screen_blend_pixel_p1(screen_t *scr, *scrp = (unsigned char) ((*scrp & ~(1 << shift)) | ((out & 1) << shift)); } +static void screen_blend_pixel_p2(screen_t *scr, + int x, + int y, + colour_t colour, + int alpha) +{ + unsigned char *scrp; + int shift; + unsigned char idx, out; + + scrp = (unsigned char *) scr->base + y * scr->rowbytes + (x >> 2); + shift = 6 - ((x & 3) << 1); + idx = (*scrp >> shift) & 3; + + scr->span->blendconst(&out, &idx, &colour, 1, alpha, scr->palette); + + *scrp = (unsigned char) ((*scrp & ~(3 << shift)) | ((out & 3) << shift)); +} + static void screen_blend_pixel_p4(screen_t *scr, int x, int y, @@ -193,6 +227,7 @@ static void screen_blend_pixel(screen_t *scr, switch (pixelfmt_log2bpp(scr->format)) { case 0: screen_blend_pixel_p1(scr, x, y, colour, alpha); break; + case 1: screen_blend_pixel_p2(scr, x, y, colour, alpha); break; case 2: screen_blend_pixel_p4(scr, x, y, colour, alpha); break; case 5: screen_blend_pixel_32(scr, x, y, colour, alpha); break; @@ -403,6 +438,77 @@ static result_t screen_copy_bitmap_p1(screen_t *scr, return result_OK; } +/* Blit "src" onto the 2bpp screen, its top-left at (x, y), clipped to + * "draw_box". Alpha-tested transfer to the nearest of the four palette + * entries, exactly as screen_copy_bitmap_p1 but two bits per pixel: bits + * 7..6 of a byte are the leftmost pixel and the cached RGB->index table is + * packed four entries to the byte. Returns result_NOT_SUPPORTED if there is + * no deep->paletted conversion table for the screen's format. */ +static result_t screen_copy_bitmap_p2(screen_t *scr, + int x, + int y, + const bitmap_t *src, + const box_t *draw_box, + int has_alpha) +{ + const unsigned char *srcrow; + unsigned char *dstbase; + const pixelmap_t *pm; + int clipped_width, clipped_height; + int yy; + + clipped_width = draw_box->x1 - draw_box->x0; + clipped_height = draw_box->y1 - draw_box->y0; + + srcrow = (const unsigned char *) src->base + (draw_box->y0 - y) * src->rowbytes; + dstbase = scr->base; + + pm = pixelmap_get(pixelfmt_rgba8888, scr->format, scr->palette, 4); + if (pm == NULL) + return result_NOT_SUPPORTED; + + for (yy = 0; yy < clipped_height; yy++) + { + const pixelfmt_rgba8888_t *srcpx; + unsigned char *rowp; + int xx; + + srcpx = (const pixelfmt_rgba8888_t *) srcrow + (draw_box->x0 - x); + rowp = dstbase + (draw_box->y0 + yy) * scr->rowbytes; + + for (xx = 0; xx < clipped_width; xx++) + { + colour_t c; + unsigned int r, g, b, idx; + int dstx, shift; + unsigned char *scrp; + pixelfmt_any_t pxl; + + c.primary = srcpx[xx]; + if (has_alpha && colour_get_alpha(&c) == 0) + continue; /* fully transparent: leave background alone */ + + dstx = draw_box->x0 + xx; + scrp = rowp + (dstx >> 2); + shift = 6 - ((dstx & 3) << 1); + + r = (c.primary >> pm->rshift) & 0xFF; + g = (c.primary >> pm->gshift) & 0xFF; + b = (c.primary >> pm->bshift) & 0xFF; + idx = ((r >> (8 - pm->rbits)) << (pm->gbits + pm->bbits)) + | ((g >> (8 - pm->gbits)) << pm->bbits) + | ( b >> (8 - pm->bbits)); + pxl = (pm->entries[idx >> 2] >> ((idx & 3) << 1)) & 3; + + *scrp = (unsigned char) ((*scrp & ~(3 << shift)) | ((pxl & 3) << shift)); + } + + srcrow += src->rowbytes; + } + + return result_OK; +} + /* Blit "src" onto the 32bpp screen, its top-left at (x, y), clipped to * "draw_box", alpha-blending row spans through the span registry. */ static void screen_copy_bitmap_32(screen_t *scr, @@ -491,6 +597,7 @@ result_t screen_copy_bitmap(screen_t *scr, int x, int y, const bitmap_t *src) switch (pixelfmt_log2bpp(scr->format)) { case 0: return screen_copy_bitmap_p1(scr, x, y, src, &draw_box, has_alpha); + case 1: return screen_copy_bitmap_p2(scr, x, y, src, &draw_box, has_alpha); case 2: return screen_copy_bitmap_p4(scr, x, y, src, &draw_box, has_alpha); case 5: screen_copy_bitmap_32(scr, x, y, src, &draw_box, has_alpha); break; diff --git a/libraries/framebuf/screen/screen-fill-pattern.c b/libraries/framebuf/screen/screen-fill-pattern.c index f6dcdc1..16a4e69 100644 --- a/libraries/framebuf/screen/screen-fill-pattern.c +++ b/libraries/framebuf/screen/screen-fill-pattern.c @@ -56,6 +56,45 @@ static void screen_fill_pattern_p1(screen_t *scr, } } +static void screen_fill_pattern_p2(screen_t *scr, + const pattern_t *pattern, + const box_t *draw_box, + int stencil, + pattern_runs_t runs) +{ + unsigned char *rowp; + int row, col, x, y; + + rowp = (unsigned char *) scr->base + draw_box->y0 * scr->rowbytes; + for (y = draw_box->y0; y < draw_box->y1; y++) + { + const pixelfmt_any_t *run; + uint8_t bits; + + row = (y - pattern->origin.y) & 7; + run = runs[row]; + bits = pattern->bits[row]; + col = 0; + for (x = draw_box->x0; x < draw_box->x1; x++) + { + if (!stencil || + (bits & (0x80u >> ((x - pattern->origin.x) & 7)))) + { + unsigned char *scrp; + int shift; + + scrp = rowp + (x >> 2); + shift = 6 - ((x & 3) << 1); + + *scrp = (unsigned char) ((*scrp & ~(3 << shift)) | + ((run[col] & 3) << shift)); + } + col = (col + 1) & 7; + } + rowp += scr->rowbytes; + } +} + static void screen_fill_pattern_p4(screen_t *scr, const pattern_t *pattern, const box_t *draw_box, @@ -193,6 +232,10 @@ void screen_fill_pattern(screen_t *scr, screen_fill_pattern_p1(scr, pattern, &draw_box, stencil, runs); break; + case 1: + screen_fill_pattern_p2(scr, pattern, &draw_box, stencil, runs); + break; + case 2: screen_fill_pattern_p4(scr, pattern, &draw_box, stencil, runs); break; diff --git a/libraries/framebuf/screen/test/screen-test.c b/libraries/framebuf/screen/test/screen-test.c index 08d8496..8bacb42 100644 --- a/libraries/framebuf/screen/test/screen-test.c +++ b/libraries/framebuf/screen/test/screen-test.c @@ -848,6 +848,116 @@ static result_t test_copy_bitmap_p1(void) return result_TEST_PASSED; } +static result_t test_copy_bitmap_p2(void) +{ +#define P2_ROWBYTES (WIDTH / 4) + static unsigned char p2pixels[P2_ROWBYTES * HEIGHT]; + static pixelfmt_rgba8888_t srcbuf[16 * 4]; + + screen_t scr; + bitmap_t src; + colour_t pal[4]; + int x, y; + + pal[0] = colour_rgb(0x00, 0x00, 0x00); + pal[1] = colour_rgb(0x55, 0x55, 0x55); + pal[2] = colour_rgb(0xAA, 0xAA, 0xAA); + pal[3] = colour_rgb(0xFF, 0xFF, 0xFF); + + /* 16x4 source: left half black (-> index 0), right half white (-> index 3) */ + for (y = 0; y < 4; y++) + for (x = 0; x < 16; x++) + srcbuf[y * 16 + x] = (x < 8 ? colour_rgb(0x08, 0x08, 0x08) + : colour_rgb(0xF8, 0xF8, 0xF8)).primary; + + bitmap_init(&src, SIZE2D(16, 4), pixelfmt_rgba8888, + 16 * (int) sizeof(srcbuf[0]), NULL, srcbuf); + + memset(p2pixels, 0, sizeof(p2pixels)); + screen_init(&scr, SIZE2D(WIDTH, HEIGHT), pixelfmt_p2, P2_ROWBYTES, pal, + p2pixels); + + if (screen_copy_bitmap(&scr, 0, 0, &src) != result_OK) + { + printf("screen: copy_bitmap to p2 screen failed\n"); + return result_TEST_FAILED; + } + + /* row 0: bytes 0..1 = four black pixels each = 0x00; bytes 2..3 = four + * white (index 3 = 0b11) each = 0xFF */ + if (p2pixels[0] != 0x00 || p2pixels[1] != 0x00 || + p2pixels[2] != 0xFF || p2pixels[3] != 0xFF) + { + printf("screen: p2 blit packed wrong (0x%02X 0x%02X 0x%02X 0x%02X)\n", + p2pixels[0], p2pixels[1], p2pixels[2], p2pixels[3]); + return result_TEST_FAILED; + } + + /* an untouched row past the source stays clear */ + if (p2pixels[4 * P2_ROWBYTES] != 0x00) + { + printf("screen: p2 blit spilled past the source\n"); + return result_TEST_FAILED; + } + + /* odd x offset: first white pixel lands in bits 6-((3&3)<<1) = 0 of byte 0 */ + memset(p2pixels, 0, sizeof(p2pixels)); + { + pixelfmt_rgba8888_t one = colour_rgb(0xF8, 0xF8, 0xF8).primary; + bitmap_t dot; + + bitmap_init(&dot, SIZE2D(1, 1), pixelfmt_rgba8888, + (int) sizeof(one), NULL, &one); + screen_copy_bitmap(&scr, 3, 0, &dot); + if (p2pixels[0] != (3 << 0)) + { + printf("screen: p2 blit odd-x packed wrong (0x%02X)\n", p2pixels[0]); + return result_TEST_FAILED; + } + } + + /* screen_set_pixel: white pixel at x=1 sets bits 6-((1&3)<<1) = 4 of byte 0 */ + memset(p2pixels, 0, sizeof(p2pixels)); + screen_set_pixel(&scr, 1, 0, pal[3]); + if (p2pixels[0] != (3 << 4)) + { + printf("screen: p2 set_pixel packed wrong (0x%02X)\n", p2pixels[0]); + return result_TEST_FAILED; + } + + /* screen_fill_hline: 4 white pixels from x=0 fill byte 0 */ + memset(p2pixels, 0, sizeof(p2pixels)); + screen_fill_hline(&scr, 0, 1, 4, pal[3]); + if (p2pixels[P2_ROWBYTES] != 0xFF) + { + printf("screen: p2 fill_hline packed wrong (0x%02X)\n", + p2pixels[P2_ROWBYTES]); + return result_TEST_FAILED; + } + + /* screen_fill_pattern: solid all-bits pattern in fg colour = 0xFF row */ + memset(p2pixels, 0, sizeof(p2pixels)); + { + pattern_t pat; + box_t b; + + memset(&pat, 0, sizeof(pat)); + memset(pat.bits, 0xFF, sizeof(pat.bits)); + pat.fg = pal[3]; + pat.bg = pal[0]; + b.x0 = 0; b.y0 = 2; b.x1 = 4; b.y1 = 3; + screen_fill_pattern(&scr, &b, &pat); + if (p2pixels[2 * P2_ROWBYTES] != 0xFF) + { + printf("screen: p2 fill_pattern packed wrong (0x%02X)\n", + p2pixels[2 * P2_ROWBYTES]); + return result_TEST_FAILED; + } + } + + return result_TEST_PASSED; +} + /* ----------------------------------------------------------------------- */ result_t screen_test(const char *resources) @@ -866,7 +976,8 @@ result_t screen_test(const char *resources) test_draw_rect, test_draw_circle, test_fill_circle, - test_copy_bitmap_p1 + test_copy_bitmap_p1, + test_copy_bitmap_p2 }; result_t rc; diff --git a/libraries/framebuf/span-registry/regdata.h b/libraries/framebuf/span-registry/regdata.h index 52acecc..546e6ae 100644 --- a/libraries/framebuf/span-registry/regdata.h +++ b/libraries/framebuf/span-registry/regdata.h @@ -7,6 +7,7 @@ #include "framebuf/span-bgrx8888.h" #include "framebuf/span-p1.h" +#include "framebuf/span-p2.h" #include "framebuf/span-p4.h" #include "framebuf/span-rgbx8888.h" #include "framebuf/span-xbgr8888.h" @@ -17,6 +18,7 @@ static const span_t *spans[] = { &span_bgrx8888, &span_p1, + &span_p2, &span_p4, &span_rgbx8888, &span_xbgr8888, diff --git a/libraries/framebuf/span/p2.c b/libraries/framebuf/span/p2.c new file mode 100644 index 0000000..61dbef8 --- /dev/null +++ b/libraries/framebuf/span/p2.c @@ -0,0 +1,108 @@ +/* framebuf/span/p2.c -- P2 (2bpp paletted) format plot methods */ + +#include + +#include "framebuf/colour.h" +#include "framebuf/pixelfmt.h" + +#include "framebuf/span.h" + +#include "framebuf/span-p2.h" + +#define SPAN_P2_NENTRIES 4 +#define SPAN_P2_MASK 3 + +/* pixel values here are unpacked palette indices, one per byte (not the + * four-pixels-per-byte layout used in screen memory); packing/unpacking + * into the actual screen bytes is the caller's job, as with + * screen_set_pixel's other pixel formats. src2 is an array of colour_t, + * not pre-quantised pixels, so the blend happens in full RGB precision + * before re-quantising to the nearest palette entry. */ +static void span_p2_blendconst(void *vdst, + const void *vsrc1, + const void *vsrc2, + int length, + int alpha, + const void *context) +{ + unsigned char *pdst; + const unsigned char *psrc1; + const colour_t *psrc2; + const colour_t *palette; + + pdst = vdst; + psrc1 = vsrc1; + psrc2 = vsrc2; + palette = context; + + while (length--) + { + colour_t old_colour, new_colour, blended; + pixelfmt_rgba8888_t oldpx, newpx; + int old_r, old_g, old_b; + int new_r, new_g, new_b; + int blend_r, blend_g, blend_b; + + old_colour = palette[*psrc1 & SPAN_P2_MASK]; + oldpx = old_colour.primary; + old_r = PIXELFMT_Rxxx8888(oldpx); + old_g = PIXELFMT_xGxx8888(oldpx); + old_b = PIXELFMT_xxBx8888(oldpx); + + new_colour = *psrc2; + newpx = new_colour.primary; + new_r = PIXELFMT_Rxxx8888(newpx); + new_g = PIXELFMT_xGxx8888(newpx); + new_b = PIXELFMT_xxBx8888(newpx); + + blend_r = (new_r * alpha + old_r * (255 - alpha)) / 255; + blend_g = (new_g * alpha + old_g * (255 - alpha)) / 255; + blend_b = (new_b * alpha + old_b * (255 - alpha)) / 255; + + blended = colour_rgb(blend_r, blend_g, blend_b); + *pdst = (unsigned char) colour_to_pixel(palette, SPAN_P2_NENTRIES, + blended, pixelfmt_p2); + + pdst++; + psrc1++; + psrc2++; + } +} + +/* unlike span_p2_blendconst above, this works directly on packed screen + * bytes: "dst" is the row base, "first" the pixel index into it, so odd + * start columns and odd lengths are handled without the caller packing. + * "pixel" is a palette index in its low two bits. Bits 7..6 of a byte are + * the leftmost pixel, matching the MSB-first sub-byte packing used + * elsewhere in framebuf. */ +static void span_p2_fill(void *vdst, + int first, + pixelfmt_any_t pixel, + int length) +{ + unsigned char *base; + unsigned char idx; + int x; + + base = vdst; + idx = (unsigned char) (pixel & SPAN_P2_MASK); + + for (x = first; x < first + length; x++) + { + unsigned char *p; + int shift; + + p = base + (x >> 2); + shift = 6 - ((x & 3) << 1); + *p = (unsigned char) ((*p & ~(SPAN_P2_MASK << shift)) | (idx << shift)); + } +} + +const span_t span_p2 = +{ + pixelfmt_p2, + NULL, /* copy: unneeded so far (bit packing makes a generic array copy awkward) */ + span_p2_fill, + span_p2_blendconst, + NULL, /* blendarray: unneeded so far */ +}; From 549b6d1a5b99c116297009890e9dfac965d3243f Mon Sep 17 00:00:00 2001 From: David Thomas Date: Wed, 9 Sep 2026 19:43:04 +0100 Subject: [PATCH 79/98] feat(wuss): support a 2bpp (pixelfmt_p2) desktop The SDL demo now accepts -d/--depth 2, selecting a pixelfmt_p2 framebuffer (stride (width + 3) / 4) alongside the existing 1, 4 and 32. Co-Authored-By: Claude Sonnet 5 --- apps/wuss/frontend-sdl.c | 14 ++++++++------ apps/wuss/main.c | 4 ++-- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/apps/wuss/frontend-sdl.c b/apps/wuss/frontend-sdl.c index 434522a..77f1d19 100644 --- a/apps/wuss/frontend-sdl.c +++ b/apps/wuss/frontend-sdl.c @@ -25,9 +25,9 @@ /* Screen pixel format for the demo, chosen at run time via --depth: 32 = * pixelfmt_bgrx8888 (feeds SDL directly, no per-frame conversion); 4 (the * default) = pixelfmt_p4 paletted (exercises screen_copy_rect's nibble-packed - * blit path instead); 1 = pixelfmt_p1 monochrome. Paletted depths are converted - * to bgrx8888 per frame via bitmap_convert. Stashed in struct wuss_frontend so - * present() can branch on it. */ + * blit path instead); 2 = pixelfmt_p2; 1 = pixelfmt_p1 monochrome. Paletted + * depths are converted to bgrx8888 per frame via bitmap_convert. Stashed in + * struct wuss_frontend so present() can branch on it. */ /* Integer window zoom: the fixed Wuss screen is drawn at this many device * pixels per screen pixel. The initial value comes from -scale (0 => use the @@ -47,7 +47,7 @@ struct wuss_frontend int scr_width; int scr_height; int scale; /* device pixels per screen pixel; see WUSS_SDL_*_SCALE */ - int depth; /* framebuffer bits per pixel: 32 (bgrx8888), 4 (p4) or 1 (p1) */ + int depth; /* framebuffer bits per pixel: 32 (bgrx8888), 4 (p4), 2 (p2) or 1 (p1) */ void *pixels; /* the private framebuffer handed to the caller */ }; @@ -100,9 +100,9 @@ result_t wuss_frontend_open(int width, NOT_USED(palette); NOT_USED(npalette); - if (depth != 1 && depth != 4 && depth != 32) + if (depth != 1 && depth != 2 && depth != 4 && depth != 32) { - fprintf(stderr, "Error: unsupported depth %d (want 1, 4 or 32)\n", depth); + fprintf(stderr, "Error: unsupported depth %d (want 1, 2, 4 or 32)\n", depth); return result_BAD_ARG; } @@ -112,6 +112,7 @@ result_t wuss_frontend_open(int width, stride = (depth == 32) ? width * 4 /* pixelfmt_bgrx8888: 4 bytes/pixel */ : (depth == 4) ? width / 2 /* pixelfmt_p4: 2 pixels/byte */ + : (depth == 2) ? (width + 3) / 4 /* pixelfmt_p2: 4 pixels/byte */ : (width + 7) / 8; /* pixelfmt_p1: 8 pixels/byte */ fe = calloc(1, sizeof(*fe)); @@ -165,6 +166,7 @@ result_t wuss_frontend_open(int width, *fmt = (depth == 32) ? pixelfmt_bgrx8888 : (depth == 4) ? pixelfmt_p4 + : (depth == 2) ? pixelfmt_p2 : pixelfmt_p1; *pixels = fe->pixels; diff --git a/apps/wuss/main.c b/apps/wuss/main.c index ab57d9f..5a82d8c 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -426,14 +426,14 @@ typedef struct wuss_options { const char *resources; /* -r/--resources: fixture root */ const char *palette_name; /* -p/--palette: startup *.hex leafname */ - int depth; /* -d/--depth: framebuffer bpp (1, 4 or 32) */ + int depth; /* -d/--depth: framebuffer bpp (1, 2, 4 or 32) */ int scale; /* -s/--scale: initial window zoom, 0 = default */ } wuss_options_t; static const char wuss_usage[] = "usage: wuss [-r|--resources DIR] [-p|--palette NAME] " - "[-d|--depth 1|4|32] [-s|--scale N]\n"; + "[-d|--depth 1|2|4|32] [-s|--scale N]\n"; #ifndef __riscos From 800aced19c3a5110c029eab8a12ae9fa74db4784 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Wed, 9 Sep 2026 19:44:07 +0100 Subject: [PATCH 80/98] feat(bmfont): render to a 2bpp (pixelfmt_p2) screen Add bmfont_p2_plot_row and the four bmfont_drawchar_p2_{1w,2w}_{o,t} wrappers, dispatched from bmfont_draw's pixelfmt_p2 case. Two bits per pixel, MSB-first, matching the screen p2 plot helpers. bmfont-test.c exercises an 800x600 p2 screen. Co-Authored-By: Claude Sonnet 5 --- libraries/framebuf/bmfont/bmfont.c | 164 +++++++++++++++++++ libraries/framebuf/bmfont/test/bmfont-test.c | 1 + 2 files changed, 165 insertions(+) diff --git a/libraries/framebuf/bmfont/bmfont.c b/libraries/framebuf/bmfont/bmfont.c index 886c6c6..22d05bc 100644 --- a/libraries/framebuf/bmfont/bmfont.c +++ b/libraries/framebuf/bmfont/bmfont.c @@ -1022,6 +1022,39 @@ static void bmfont_p1_plot_row(unsigned char *scr, } } +/* As bmfont_p1_plot_row but for a p2 (2bpp) screen row: two bits per pixel, + * "shift" is a bit offset (0, 2, 4 or 6) and pixel column i lands at bit-pair + * (shift + 2 * i), MSB-first within the byte. */ +static void bmfont_p2_plot_row(unsigned char *scr, + unsigned int row, + int cw, + int shift, + pixelfmt_any_t fg, + pixelfmt_any_t bg, + int opaque) +{ + int i; + + for (i = 0; i < cw; i++) + { + int set; + int bitpos; + unsigned char *p; + int b; + + set = (row >> (cw - 1 - i)) & 1; + if (!set && !opaque) + continue; + + bitpos = shift + (i << 1); + p = scr + (bitpos >> 3); + b = 6 - (bitpos & 7); + + *p = (unsigned char) ((*p & ~(3 << b)) + | (((set ? fg : bg) & 3) << b)); + } +} + /* Draw a character to a p1 screen. One helper covers 1- and 2-byte glyph rows * and both opaque and transparent backgrounds -- the p1 inner loop is a plain * per-pixel bit write, so the LUT-driven width unrolling the p4 path needs @@ -1148,6 +1181,128 @@ static void bmfont_drawchar_p1_2w_t(void *vscreen, /* -------------------------------------------------------------------------- */ +static void bmfont_drawchar_p2_1w_o(void *vscreen, + const void *vglyph, + int top_skip, + int right_skip, + int shift, + int rowbytes, + int charwidth, + int charheight, + pixelfmt_any_t fg, + pixelfmt_any_t bg) +{ + unsigned char *scr = vscreen; + const unsigned char *gly = vglyph; + int stride; + + assert(charwidth > 0); + assert(charheight > 0); + + gly += top_skip; + stride = rowbytes; + + while (charheight--) + { + unsigned int row = (unsigned int) *gly++ >> right_skip; + + bmfont_p2_plot_row(scr, row, charwidth, shift, fg, bg, 1); + scr += stride; + } +} + +static void bmfont_drawchar_p2_1w_t(void *vscreen, + const void *vglyph, + int top_skip, + int right_skip, + int shift, + int rowbytes, + int charwidth, + int charheight, + pixelfmt_any_t fg, + pixelfmt_any_t bg) +{ + unsigned char *scr = vscreen; + const unsigned char *gly = vglyph; + int stride; + + assert(charwidth > 0); + assert(charheight > 0); + + gly += top_skip; + stride = rowbytes; + + while (charheight--) + { + unsigned int row = (unsigned int) *gly++ >> right_skip; + + bmfont_p2_plot_row(scr, row, charwidth, shift, fg, bg, 0); + scr += stride; + } +} + +static void bmfont_drawchar_p2_2w_o(void *vscreen, + const void *vglyph, + int top_skip, + int right_skip, + int shift, + int rowbytes, + int charwidth, + int charheight, + pixelfmt_any_t fg, + pixelfmt_any_t bg) +{ + unsigned char *scr = vscreen; + const unsigned short *gly = vglyph; + int stride; + + assert(charwidth > 0); + assert(charheight > 0); + + gly += top_skip; + stride = rowbytes; + + while (charheight--) + { + unsigned int row = (unsigned int) *gly++ >> right_skip; + + bmfont_p2_plot_row(scr, row, charwidth, shift, fg, bg, 1); + scr += stride; + } +} + +static void bmfont_drawchar_p2_2w_t(void *vscreen, + const void *vglyph, + int top_skip, + int right_skip, + int shift, + int rowbytes, + int charwidth, + int charheight, + pixelfmt_any_t fg, + pixelfmt_any_t bg) +{ + unsigned char *scr = vscreen; + const unsigned short *gly = vglyph; + int stride; + + assert(charwidth > 0); + assert(charheight > 0); + + gly += top_skip; + stride = rowbytes; + + while (charheight--) + { + unsigned int row = (unsigned int) *gly++ >> right_skip; + + bmfont_p2_plot_row(scr, row, charwidth, shift, fg, bg, 0); + scr += stride; + } +} + +/* -------------------------------------------------------------------------- */ + /* Draw a character, a maximum of one byte wide, to any 8888 screen using an * opaque background. */ static void bmfont_drawchar_any8888_1w_o(void *vscreen, @@ -1369,6 +1524,15 @@ result_t bmfont_draw(bmfont_t *bmfont, } break; + case pixelfmt_p2: + switch (bmfont->glyphrowbytes) + { + case 1: drawfn = (bgalpha < 255) ? bmfont_drawchar_p2_1w_t : bmfont_drawchar_p2_1w_o; break; + case 2: drawfn = (bgalpha < 255) ? bmfont_drawchar_p2_2w_t : bmfont_drawchar_p2_2w_o; break; + default: assert(0); return result_NOT_SUPPORTED; + } + break; + case pixelfmt_p4: switch (bmfont->glyphrowbytes) { diff --git a/libraries/framebuf/bmfont/test/bmfont-test.c b/libraries/framebuf/bmfont/test/bmfont-test.c index 7ad7f82..52acd9c 100644 --- a/libraries/framebuf/bmfont/test/bmfont-test.c +++ b/libraries/framebuf/bmfont/test/bmfont-test.c @@ -1028,6 +1028,7 @@ result_t bmfont_test(const char *resources) tab[] = { { 800, 600, pixelfmt_p1 }, + { 800, 600, pixelfmt_p2 }, { 800, 600, pixelfmt_p4 }, { 800, 600, pixelfmt_bgrx8888 } }; From a03316427d6af4d060cd2c7b625ec327c862e7f7 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Wed, 9 Sep 2026 19:44:21 +0100 Subject: [PATCH 81/98] refactor(framebuf): rename screen_set_pixel_8 to _p8 Match the _p1/_p2/_p4 naming of the other paletted set-pixel helpers. Co-Authored-By: Claude Sonnet 5 --- libraries/framebuf/screen/screen-draw.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/libraries/framebuf/screen/screen-draw.c b/libraries/framebuf/screen/screen-draw.c index 36f13b5..877d010 100644 --- a/libraries/framebuf/screen/screen-draw.c +++ b/libraries/framebuf/screen/screen-draw.c @@ -68,10 +68,10 @@ static void screen_set_pixel_p4(screen_t *scr, *scrp = (unsigned char) ((*scrp & ~(0xF << shift)) | ((pxl & 0xF) << shift)); } -static void screen_set_pixel_8(screen_t *scr, - int x, - int y, - pixelfmt_any_t pxl) +static void screen_set_pixel_p8(screen_t *scr, + int x, + int y, + pixelfmt_any_t pxl) { pixelfmt_any8_t *scrp; @@ -123,7 +123,7 @@ void screen_set_pixel(screen_t *scr, int x, int y, colour_t colour) case 0: screen_set_pixel_p1(scr, x, y, pxl); break; case 1: screen_set_pixel_p2(scr, x, y, pxl); break; case 2: screen_set_pixel_p4(scr, x, y, pxl); break; - case 3: screen_set_pixel_8(scr, x, y, pxl); break; + case 3: screen_set_pixel_p8(scr, x, y, pxl); break; case 4: screen_set_pixel_16(scr, x, y, pxl); break; case 5: screen_set_pixel_32(scr, x, y, pxl); break; From dc6262415a77cd803e71ef2e2c9f7fca5e1ea039 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Wed, 9 Sep 2026 20:01:28 +0100 Subject: [PATCH 82/98] feat(wuss): cycle 2x2/4x4/8x8 Bayer dithers in the gradient task SELECT and ADJUST clicks step the ordered-dither matrix forward and backward through 2x2, 4x4 and 8x8. Each matrix cell is mapped to a fixed -8..+8 offset regardless of matrix size, so a larger matrix gives a finer pattern rather than a noisier one (the 4x4 output is unchanged from before). The current size is drawn as a label in the content area's top-left corner. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/test/tasks/gradient.c | 128 +++++++++++++++++++++++---- libraries/wuss/test/tasks/gradient.h | 5 +- 2 files changed, 117 insertions(+), 16 deletions(-) diff --git a/libraries/wuss/test/tasks/gradient.c b/libraries/wuss/test/tasks/gradient.c index b8132d5..1a21c87 100644 --- a/libraries/wuss/test/tasks/gradient.c +++ b/libraries/wuss/test/tasks/gradient.c @@ -8,7 +8,11 @@ #include "fortify/fortify.h" #endif +#include +#include + #include "base/utils.h" +#include "framebuf/bmfont.h" #include "framebuf/colour.h" #include "geom/box.h" @@ -19,18 +23,59 @@ #define GRADIENT_OPEN_WIDTH 100 #define GRADIENT_OPEN_HEIGHT 100 -/* 4x4 ordered (Bayer) dither matrix, values 0..15 */ -static const int bayer4x4[4][4] = +/* ordered (Bayer) dither matrices, each holding values 0 .. dim*dim-1. + * SELECT/ADJUST clicks cycle task->dither_index forward/backward through + * this table. */ +static const struct +{ + int dim; + int cell[64]; /* row-major, first dim*dim entries used */ +} +gradient_dithers[] = { - { 0, 8, 2, 10 }, - { 12, 4, 14, 6 }, - { 3, 11, 1, 9 }, - { 15, 7, 13, 5 }, + { + 2, + { + 0, 2, + 3, 1, + } + }, + { + 4, + { + 0, 8, 2, 10, + 12, 4, 14, 6, + 3, 11, 1, 9, + 15, 7, 13, 5, + } + }, + { + 8, + { + 0, 32, 8, 40, 2, 34, 10, 42, + 48, 16, 56, 24, 50, 18, 58, 26, + 12, 44, 4, 36, 14, 46, 6, 38, + 60, 28, 52, 20, 62, 30, 54, 22, + 3, 35, 11, 43, 1, 33, 9, 41, + 51, 19, 59, 27, 49, 17, 57, 25, + 15, 47, 7, 39, 13, 45, 5, 37, + 63, 31, 55, 23, 61, 29, 53, 21, + } + }, }; -static int dither(int v, int x, int y) +/* offset "v" by the dither cell for (x, y), mapped to a fixed -8..+8 swing + * (matching the original 4x4 code) whatever the matrix size, so a larger + * matrix just gives a finer pattern rather than a louder one */ +static int dither(int index, int v, int x, int y) { - return CLAMP(v + bayer4x4[y & 3][x & 3] - 8, 0, 255); + int dim, n, m; + + dim = gradient_dithers[index].dim; + n = dim * dim; + m = gradient_dithers[index].cell[(y % dim) * dim + (x % dim)]; + + return CLAMP(v + (m * 16 / (n - 1)) - 8, 0, 255); } result_t gradient_create(wuss_t *wuss, gradient_task_t *task) @@ -39,6 +84,9 @@ result_t gradient_create(wuss_t *wuss, gradient_task_t *task) wuss_task_t *delegate; wuss_task_desc_t delegate_desc; + task->wuss = wuss; + task->dither_index = 1; /* 4x4, matching the original */ + /* gradient_redraw paints every pixel itself */ delegate_desc.handle = gradient_handle; delegate_desc.task_data = task; @@ -67,11 +115,13 @@ result_t gradient_create(wuss_t *wuss, gradient_task_t *task) static result_t gradient_redraw(const wuss_event_t *event, void *task_data) { - screen_t *scr; - const box_t *content, *bounds; - int sx, sy, x, y, lx, ly; + gradient_task_t *gc; + screen_t *scr; + const box_t *content, *bounds; + int di, sx, sy, x, y, lx, ly; - NOT_USED(task_data); + gc = task_data; + di = gc->dither_index; scr = event->data.redraw.scr; content = event->data.redraw.content; @@ -87,12 +137,55 @@ static result_t gradient_redraw(const wuss_event_t *event, void *task_data) ly = y - bounds->y0 + sy; screen_set_pixel(scr, x, y, - colour_rgb(dither(lx * 255 / GRADIENT_DOC_WIDTH, lx, ly), - dither(ly * 255 / GRADIENT_DOC_HEIGHT, lx, ly), - dither(255 - (lx + ly) * 255 / (GRADIENT_DOC_WIDTH + GRADIENT_DOC_HEIGHT), lx, ly))); + colour_rgb(dither(di, lx * 255 / GRADIENT_DOC_WIDTH, lx, ly), + dither(di, ly * 255 / GRADIENT_DOC_HEIGHT, lx, ly), + dither(di, 255 - (lx + ly) * 255 / (GRADIENT_DOC_WIDTH + GRADIENT_DOC_HEIGHT), lx, ly))); } } + /* matrix-size label, pinned to the content area's top-left corner (bounds, + * not the per-redraw dirty piece, and not the scrolled document) so it + * stays put on a partial redraw */ + { + bmfont_t *font = wuss_get_font(gc->wuss); + int dim = gradient_dithers[di].dim; + char label[8]; + point_t pos = POINT(bounds->x0 + 2, bounds->y0 + 2); + colour_t ink = colour_rgb(0xFF, 0xFF, 0xFF); + colour_t bg = colour_rgba(0, 0, 0, 0); /* transparent */ + + sprintf(label, "%dx%d", dim, dim); + + if (font != NULL) + bmfont_draw(font, scr, label, (int) strlen(label), ink, bg, &pos, NULL); + } + + return result_OK; +} + +static result_t gradient_mouse(const wuss_event_t *event, void *task_data) +{ + gradient_task_t *gc; + wuss_button_t button; + int n; + + gc = task_data; + + if (event->data.mouse.action != wuss_MOUSE_DOWN) + return result_OK; + + button = event->data.mouse.button; + n = (int) NELEMS(gradient_dithers); + + if (button & wuss_BUTTON_SELECT) + gc->dither_index = (gc->dither_index + 1) % n; + else if (button & wuss_BUTTON_ADJUST) + gc->dither_index = (gc->dither_index + n - 1) % n; + else + return result_OK; + + wuss_window_invalidate_visible(gc->window); /* whole fill changes */ + return result_OK; } @@ -102,6 +195,8 @@ result_t gradient_handle(wuss_window_t *window, { gradient_task_t *gc; + NOT_USED(window); + gc = task_data; switch (event->kind) @@ -109,6 +204,9 @@ result_t gradient_handle(wuss_window_t *window, case wuss_EVENT_REDRAW: return gradient_redraw(event, task_data); + case wuss_EVENT_MOUSE: + return gradient_mouse(event, task_data); + case wuss_EVENT_QUIT: free(gc); /* task_data was calloc'd per instance by the spawner */ return result_OK; diff --git a/libraries/wuss/test/tasks/gradient.h b/libraries/wuss/test/tasks/gradient.h index 0ad2c65..e6ce817 100644 --- a/libraries/wuss/test/tasks/gradient.h +++ b/libraries/wuss/test/tasks/gradient.h @@ -9,10 +9,13 @@ /* fills its whole content area with a two-axis colour gradient; opens * small (100x100) against a large (400x400) document, so scrollbars - * appear and the fill can be scrolled around */ + * appear and the fill can be scrolled around. SELECT/ADJUST clicks cycle + * the ordered-dither matrix forward/backward through 2x2, 4x4 and 8x8. */ typedef struct gradient_task { + wuss_t *wuss; /* borrowed; for wuss_get_font in the redraw */ wuss_window_t *window; + int dither_index; } gradient_task_t; From 3193eb94f39c5c8daa5096840d11796de25055ce Mon Sep 17 00:00:00 2001 From: David Thomas Date: Wed, 9 Sep 2026 22:01:13 +0100 Subject: [PATCH 83/98] feat(bmfont): add pixel-font PNGs and a TTF-to-PNG generator Adds three small paletted bitmap faces under resources/bmfonts -- 04b_03, 04b_25 and Nokia -- plus tools/ttf2bmfont.py, which renders a TTF into the bmfont PNG format the loader expects. Registers the three new leafnames in bmfont_enumerate_test (bmfonts_enum_extra[], MAXFONTS_ENUM bumped to 12) so the fixture-directory walk still matches. Co-Authored-By: Claude Sonnet 5 --- libraries/framebuf/bmfont/test/bmfont-test.c | 15 +- resources/bmfonts/04b_03.png | Bin 0 -> 579 bytes resources/bmfonts/04b_25.png | Bin 0 -> 762 bytes resources/bmfonts/Nokia.png | Bin 0 -> 1006 bytes tools/ttf2bmfont.py | 285 +++++++++++++++++++ 5 files changed, 295 insertions(+), 5 deletions(-) create mode 100644 resources/bmfonts/04b_03.png create mode 100644 resources/bmfonts/04b_25.png create mode 100644 resources/bmfonts/Nokia.png create mode 100755 tools/ttf2bmfont.py diff --git a/libraries/framebuf/bmfont/test/bmfont-test.c b/libraries/framebuf/bmfont/test/bmfont-test.c index 52acd9c..11c7f25 100644 --- a/libraries/framebuf/bmfont/test/bmfont-test.c +++ b/libraries/framebuf/bmfont/test/bmfont-test.c @@ -197,14 +197,19 @@ static bmtestfont_t bmfonts[MAXFONTS] = { "Digits-Bold", NULL } }; -/* Symbols.png isn't Latin text, so it's excluded from bmfonts[] above (used - * to draw lorem_ipsum in the clipping/layout tests) but must still show up - * in the enumerate test, which just walks the fixture directory. */ -#define MAXFONTS_ENUM 9 +/* Fixture PNGs not in bmfonts[] above (which is the Latin-text set the + * clipping/layout tests draw lorem_ipsum with) but which the enumerate test + * must still see, since it just walks the fixture directory: Symbols.png is + * not Latin text; 04b_03, 04b_25 and Nokia are tiny pixel faces added for + * the wuss tasks. */ +#define MAXFONTS_ENUM 12 static const char *bmfonts_enum_extra[MAXFONTS_ENUM - MAXFONTS] = { - "Symbols" + "Symbols", + "04b_03", + "04b_25", + "Nokia" }; /* ----------------------------------------------------------------------- */ diff --git a/resources/bmfonts/04b_03.png b/resources/bmfonts/04b_03.png new file mode 100644 index 0000000000000000000000000000000000000000..491fd8a97a8bac9b7ebaf9b36e74677772b0afa6 GIT binary patch literal 579 zcmV-J0=)f+P)b&=eUOg00053dG|(PX(Rph4S0qIy;xt}F!x zHaPcQUX?-Dw;<=@z^MbR#X|&bk0c>PFf^*%I}!imm)_I(qTd96b}A8Mr9+AZS5d*` zBLK+c(xCxZ07x!D2M{F)bmi#}nqHs| z%T=4p1qTq$=cZcPgiFaqFfL7p$u;5H87hF*WU9sNH{li&PJ+(e9k8A0qu&7%vg*L9 z#$pu=@bpXVEhxZ#2@^a+V+IC~KzmTYC*pVVPakx6_)mcM)ARIv0K7kj>L2rO01HH* RhKm3I002ovPDHLkV1l`W0zm)( literal 0 HcmV?d00001 diff --git a/resources/bmfonts/04b_25.png b/resources/bmfonts/04b_25.png new file mode 100644 index 0000000000000000000000000000000000000000..d808fa27117357e364406e7621b89fed2e03c1bc GIT binary patch literal 762 zcmVb&=eUOg00081Nklh0fbVT&^fmyJ{3d{m7spH5WmgQE&DZov~QiPg0|NqIZ0shjDDvH|bJ)kP!Z75@FSK1Hq7+)*@wYMXkDWL?o9waRB#im@0UT{K$e$UR78 zJgjkiN1$fn`WFZi6JMHI)K5TKK)P*`E5=-D1VW7C&5UAUlJ`?k^Et>ofVhwD4uZo% z*(M9JK_a3)Hv_o=g!=$dLGCLXqK9qq~`g5miQ>O zG}#w>2Q~U7$kM#nNo!}9K~n6|T@~A(b7TkKNn>KI5iu59pC1(1m#l|k<*>Cf-iTBF?(t>Nustf6y)Al|6H_u zi=xFQf)=P>M4LcodRzUEqUBcrlQBHm&$aeKwDNWf$V@J_FN?P35ubs+AzIejv(&z& swN&g8Xrwj&sn&{_)>8W;=znSLFGd=4z!smozyJUM07*qoM6N<$f^B7P+5i9m literal 0 HcmV?d00001 diff --git a/resources/bmfonts/Nokia.png b/resources/bmfonts/Nokia.png new file mode 100644 index 0000000000000000000000000000000000000000..90bdfff7713fc0cbcd6f1a645158cc777ee5d188 GIT binary patch literal 1006 zcmVPx#1ZP1_K>z@;j|==^1poj5AY({UO#lFTCIA3{ga82g0001h=l}q9FaQARU;qF* zm;eA5aGbhPJOBUy3{Xr|Mf>;j000000RONqJAL5tvKdX%k)fTPks%l>B&*O4IqFfN2|;K@x7ZCami_aS5M$Xq3caBs!=qG% z3?-KSAZKdf>4b1JM|ak&Iw7t9nKG&0su1?b>E7O=YAma%6Jp6>Y<8-sws&N*%ZidH zr1iTfq&3E)bro@cqY!$t^YkdBceK~@D8BwGV4gsH^!G-rjyN3BU zyocan6!>dG8$yk*=AnHEt*?DsAOMd?$V*hxi_qUF;rTm5pI;2yd!D~Ak~0s5Z~9PG zp2ON1B(QElZS}i)f}zk~RbBE^Rl*QY8QWlsWIWbf&~SMEjPY2xFhUbf?k&?S85<4F z*~s%d#;cpeGIN@mJYgI^qp2Q^S%J2?-cV;iXo=@b4VK+ocv8*T(V5UhJXTndeu|s0 zt`}-yxG0nhY?)bpBDCOIILZ}Bu%be%K}E~HA{HTM+))Vlh|n}o9PiKuQ`vP7q4;W! zv(JU3qI7982swg~v{)1xIg2*1#%iziLQG#&baqEDax?3M&{+IwkLZVXLeN`p)Vz;OD?-MC}lauxm0B1tRxu`GFkNcVJIWRGxEjJSjb+8 z(EeP=oU;E*r~^_4G+f$id2BkXouw5mBaqr+(rYsqdB+Ot%SbHqoEAf!O9 zmTC0FHtL{TsL+H5Li1iI`yj*vA&SzB&Oon;VFyAV$i~qwE7GG8$c{oFTZX@(4?;nx zEy6>neIsNyN_OkpM<9GyKOPXm@J0w^1Nj2ueC^i&T*)q!Y~g6gaju3LKGp@<@{`>? c|M!yp5BA`tKVH!_;s5{u07*qoM6N<$f`^pJQ~&?~ literal 0 HcmV?d00001 diff --git a/tools/ttf2bmfont.py b/tools/ttf2bmfont.py new file mode 100755 index 0000000..e9e7bb0 --- /dev/null +++ b/tools/ttf2bmfont.py @@ -0,0 +1,285 @@ +#!/usr/bin/env python3 +"""ttf2bmfont.py -- render a pixel-design TTF into a DPTLib bmfont PNG. + +DPTLib's framebuf/bmfont loader (libraries/framebuf/bmfont/bmfont.c) reads +proportional bitmap fonts from a very specific 2bpp paletted PNG: + + * 4-entry palette, order is the contract: + 0 = background, 1 = glyph ink, 2 = advance-width marker, 3 = grid + (index 3 is loaded but never interpreted -- it is a visual aid only). + * exactly 32 glyph cells per row, packed edge to edge; cellW = pngW / 32, + and cellW must be <= 16. + * the top row of every cell is the "advance strip": it holds `advance` + index-2 pixels (left-justified here) and no index-1 pixels. The loader + counts index-2 pixels in the cell to get the advance width. + * the remaining cellH-1 rows are the glyph bitmap; index-1 pixels are ink. + * cell 0 is U+0020 (space); codepoints run contiguously upward. There is no + character map and no stored first-codepoint. + * there is no baseline in the format; the grey grid lines this script draws + (left sidebearing, baseline, cell bottom) are purely cosmetic. + +Requires freetype-py ( pip install freetype-py ). PNG is written by hand so +Pillow is not needed. + +Usage: + python3 tools/ttf2bmfont.py FONT.ttf OUT.png --size PX [options] +""" + +import argparse +import struct +import sys +import zlib + +import freetype + +# Palette matches resources/bmfonts/Nokia.png and Digits-Bold.png. +PALETTE = [(255, 255, 255), (0, 0, 0), (0, 0, 255), (192, 192, 192)] + +IDX_BG = 0 +IDX_INK = 1 +IDX_ADW = 2 +IDX_GRID = 3 + +CHARS_PER_ROW = 32 +MAX_CELL_W = 16 + + +def load_glyphs(path, size, first, last): + """Return (list of glyph dicts, face) for codepoints first..last inclusive. + + Each glyph dict has: cp, bits (list of (x, y) set pixels, origin at the + pen position, y down), left, top, width, rows, advance. + """ + face = freetype.Face(path) + face.set_pixel_sizes(0, size) + + glyphs = [] + for cp in range(first, last + 1): + face.load_char(chr(cp), + freetype.FT_LOAD_RENDER | freetype.FT_LOAD_TARGET_MONO) + slot = face.glyph + bm = slot.bitmap + bits = [] + for by in range(bm.rows): + rowbase = by * bm.pitch + for bx in range(bm.width): + byte = bm.buffer[rowbase + (bx >> 3)] + if byte & (0x80 >> (bx & 7)): + bits.append((bx, by)) + glyphs.append(dict(cp=cp, + bits=bits, + left=slot.bitmap_left, + top=slot.bitmap_top, + width=bm.width, + rows=bm.rows, + advance=slot.advance.x >> 6)) + return glyphs, face + + +def measure_cell(glyphs, pad, advance_source): + """Work out the common cell geometry from the rendered glyphs. + + Returns (cellW, cellH, charH, ascent, minLeft). + """ + ascent = 0 + descent = 0 + ink_right = 0 + min_left = 0 + for g in glyphs: + if g["bits"]: + ascent = max(ascent, g["top"]) + descent = max(descent, g["rows"] - g["top"]) + ink_right = max(ink_right, g["left"] + g["width"]) + min_left = min(min_left, g["left"]) + + max_advance = 0 + for g in glyphs: + adv = g["advance"] if advance_source == "ttf" else _ink_advance(g) + max_advance = max(max_advance, adv) + + cell_w = max(ink_right - min_left, max_advance) + pad + char_h = ascent + descent + pad + cell_h = char_h + 1 # + strip row + + if cell_w < 1: + cell_w = 1 + if cell_w > MAX_CELL_W: + sys.exit("ttf2bmfont: cell width %d exceeds the loader limit of %d; " + "use a smaller --size" % (cell_w, MAX_CELL_W)) + + return cell_w, cell_h, char_h, ascent, min_left + + +def _ink_advance(g): + if not g["bits"]: + return 0 + return g["left"] + g["width"] + 1 + + +def advance_for(g, cell_w, advance_source): + """Clamp the chosen advance into [1, cell_w]. + + At least 1 so every strip row carries an index-2 pixel (the loader's + height detector requires it). + """ + adv = g["advance"] if advance_source == "ttf" else _ink_advance(g) + if adv < 1: + adv = 1 + if adv > cell_w: + adv = cell_w + return adv + + +def build_indices(glyphs, cell_w, cell_h, char_h, ascent, min_left, + advance_source, draw_grid): + """Render every glyph into an H*W buffer of palette indices.""" + nrows = (len(glyphs) + CHARS_PER_ROW - 1) // CHARS_PER_ROW + w = CHARS_PER_ROW * cell_w + h = nrows * cell_h + buf = bytearray(w * h) # zero == IDX_BG + + def put(x, y, idx): + if 0 <= x < w and 0 <= y < h: + buf[y * w + x] = idx + + baseline = ascent # body-row index of the baseline + + if draw_grid: + for r in range(nrows): + body0 = r * cell_h + 1 + for c in range(CHARS_PER_ROW): + x0 = c * cell_w + for dy in range(char_h): # left sidebearing line + put(x0, body0 + dy, IDX_GRID) + for dx in range(cell_w): # baseline and cell-bottom lines + put(x0 + dx, body0 + baseline, IDX_GRID) + put(x0 + dx, body0 + char_h - 1, IDX_GRID) + + warned = [False] + advances = [] + for i, g in enumerate(glyphs): + r, c = divmod(i, CHARS_PER_ROW) + x0 = c * cell_w + y_strip = r * cell_h + body0 = y_strip + 1 + + adv = advance_for(g, cell_w, advance_source) + advances.append(adv) + for dx in range(adv): # advance strip + put(x0 + dx, y_strip, IDX_ADW) + + for bx, by in g["bits"]: # glyph ink (wins over any grid pixel) + gx = x0 + (g["left"] - min_left) + bx + gy = body0 + (ascent - g["top"]) + by + if not (x0 <= gx < x0 + cell_w and body0 <= gy < body0 + char_h): + if not warned[0]: + sys.stderr.write("ttf2bmfont: warning: glyph U+%04X " + "overflows its cell; clipping\n" % g["cp"]) + warned[0] = True + continue + put(gx, gy, IDX_INK) + + return buf, w, h, advances + + +def write_png(path, buf, w, h): + """Write a 2bpp, colour-type-3, non-interlaced PNG.""" + def chunk(tag, data): + return (struct.pack(">I", len(data)) + tag + data + + struct.pack(">I", zlib.crc32(tag + data) & 0xFFFFFFFF)) + + sig = b"\x89PNG\r\n\x1a\n" + ihdr = struct.pack(">IIBBBBB", w, h, 2, 3, 0, 0, 0) + + plte = b"".join(struct.pack("BBB", *rgb) for rgb in PALETTE) + + raw = bytearray() + rowbytes = (w * 2 + 7) // 8 + for y in range(h): + raw.append(0) # filter type 0 (none) + row = bytearray(rowbytes) + base = y * w + for x in range(w): + row[x >> 2] |= (buf[base + x] & 3) << (6 - 2 * (x & 3)) + raw.extend(row) + + with open(path, "wb") as f: + f.write(sig) + f.write(chunk(b"IHDR", ihdr)) + f.write(chunk(b"PLTE", plte)) + f.write(chunk(b"IDAT", zlib.compress(bytes(raw), 9))) + f.write(chunk(b"IEND", b"")) + + +def selfcheck(buf, w, h, cell_w, cell_h, advances): + """Re-parse the buffer with the loader's own rules and assert agreement.""" + assert w % CHARS_PER_ROW == 0, "width not a multiple of 32" + assert w // CHARS_PER_ROW == cell_w <= MAX_CELL_W, "bad cell width" + assert h % cell_h == 0, "height not a multiple of the cell height" + + def has(idx, y): + base = y * w + return any(buf[base + x] == idx for x in range(w)) + + for y in range(h): + if y % cell_h == 0: + assert has(IDX_ADW, y), "strip row %d has no advance pixel" % y + assert not has(IDX_INK, y), "strip row %d has ink" % y + else: + assert not has(IDX_ADW, y), "body row %d has an advance pixel" % y + + # round-trip the advance counts + for i, want in enumerate(advances): + r, c = divmod(i, CHARS_PER_ROW) + y = r * cell_h + x0 = c * cell_w + got = sum(1 for dx in range(cell_w) if buf[y * w + x0 + dx] == IDX_ADW) + assert got == want, "glyph %d advance %d != %d" % (i, got, want) + + print("selfcheck OK: %dx%d, cell %dx%d, %d glyphs" % + (w, h, cell_w, cell_h, len(advances))) + + +def main(argv): + p = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + p.add_argument("font") + p.add_argument("out") + p.add_argument("--size", type=int, required=True, + help="pixel em size passed to FreeType") + p.add_argument("--pad", type=int, default=1, + help="extra pixels added to the glyph bbox for grid lines") + p.add_argument("--advance-source", choices=("ttf", "ink"), default="ttf", + help="'ttf' uses the font's advance, 'ink' uses ink width+1") + p.add_argument("--no-grid", action="store_true", + help="omit the grey guide lines") + p.add_argument("--first", type=lambda s: int(s, 0), default=0x20) + p.add_argument("--last", type=lambda s: int(s, 0), default=0x7E) + p.add_argument("--selfcheck", action="store_true", + help="re-parse the result and assert it is well formed") + args = p.parse_args(argv) + + glyphs, _ = load_glyphs(args.font, args.size, args.first, args.last) + + # pad the cell count up to a multiple of 32 with blank glyphs + while len(glyphs) % CHARS_PER_ROW: + glyphs.append(dict(cp=0, bits=[], left=0, top=0, width=0, rows=0, + advance=1)) + + cell_w, cell_h, char_h, ascent, min_left = measure_cell( + glyphs, args.pad, args.advance_source) + + buf, w, h, advances = build_indices(glyphs, cell_w, cell_h, char_h, + ascent, min_left, args.advance_source, + not args.no_grid) + + write_png(args.out, buf, w, h) + print("wrote %s (%dx%d, cell %dx%d, charheight %d, %d glyphs)" % + (args.out, w, h, cell_w, cell_h, cell_h - 1, len(glyphs))) + + if args.selfcheck: + selfcheck(buf, w, h, cell_w, cell_h, advances) + + +if __name__ == "__main__": + main(sys.argv[1:]) From 7054b1505fbeab1907471b3684800249521b0072 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Wed, 9 Sep 2026 22:46:21 +0100 Subject: [PATCH 84/98] chore(tools): mark Python scripts executable --- tools/gen_greeble_tiles.py | 0 tools/test_wrap_doxygen.py | 0 tools/test_wrap_protos.py | 0 tools/wrap_doxygen.py | 0 tools/wrap_protos.py | 0 5 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 tools/gen_greeble_tiles.py mode change 100644 => 100755 tools/test_wrap_doxygen.py mode change 100644 => 100755 tools/test_wrap_protos.py mode change 100644 => 100755 tools/wrap_doxygen.py mode change 100644 => 100755 tools/wrap_protos.py diff --git a/tools/gen_greeble_tiles.py b/tools/gen_greeble_tiles.py old mode 100644 new mode 100755 diff --git a/tools/test_wrap_doxygen.py b/tools/test_wrap_doxygen.py old mode 100644 new mode 100755 diff --git a/tools/test_wrap_protos.py b/tools/test_wrap_protos.py old mode 100644 new mode 100755 diff --git a/tools/wrap_doxygen.py b/tools/wrap_doxygen.py old mode 100644 new mode 100755 diff --git a/tools/wrap_protos.py b/tools/wrap_protos.py old mode 100644 new mode 100755 From 43d046868061c7b165bf149e083c0fb2b0b56094 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Wed, 9 Sep 2026 23:19:27 +0100 Subject: [PATCH 85/98] fix(wuss): drop furniture layout cache on NO_RESIZE_BLIT resize The NO_RESIZE_BLIT branch of wuss_window_resize invalidated the dirty region but never dropped the cached furniture layout, so wuss__furniture_draw kept painting titlebar/carve/outline rects at the previous frame's width. The blit branch gets this via wuss__chrome_repaint; add the equivalent wuss__chrome_invalidate_layout call here. Surfaced by the sample-text task (libraries/wuss/test/tasks/text.c), which resizes such a window every idle frame and drew its furniture inconsistently. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/core/window/resize.c | 6 +++ libraries/wuss/test/wuss-test.c | 63 +++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/libraries/wuss/core/window/resize.c b/libraries/wuss/core/window/resize.c index a3d26e1..c876422 100644 --- a/libraries/wuss/core/window/resize.c +++ b/libraries/wuss/core/window/resize.c @@ -135,6 +135,12 @@ result_t wuss_window_resize(wuss_window_t *window, size2d_t size) box_union(&before, &window->visible, &dirty); wuss__invalidate_clipped(window, &dirty); + + /* the dirty box above covers every furniture strip, but nothing here + * has dropped the cached furniture layout -- it still holds the old + * width's rects. Force a rebuild so the redraw paints furniture at the + * new geometry (the else branch gets this via wuss__chrome_repaint). */ + wuss__chrome_invalidate_layout(window); } else { diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 6a4278f..4667779 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -3030,6 +3030,69 @@ result_t wuss_test(const char *resources) wuss_window_close(win_nb2); } + printf("test: resizing a wuss_WINDOW_NO_RESIZE_BLIT window drops its cached " + "furniture layout so the chrome redraws at the new width\n"); + + { + static test_task_t tc_fl; + wuss_task_t *delegate_fl; + box_t box_fl, after_fl; + wuss_window_t *win_fl; + int i, old_titlebar_x1, found; + + tc_fl.redraw_count = 0; + tc_fl.mouse_count = 0; + delegate_fl = mk_task(wuss, test_handle, &tc_fl); + if (delegate_fl == NULL) goto Failure; + + box_fl.x0 = 0; box_fl.y0 = 0; + box_fl.x1 = 60; box_fl.y1 = 60; + rc = wuss_window_create(delegate_fl, &box_fl, "FL", wuss_WINDOW_NO_RESIZE_BLIT, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + box_size(&box_fl), + SIZE2D(0, 0), + &win_fl); + if (rc != result_OK) + goto Failure; + + /* build and cache the furniture layout at the creation width */ + wuss__furniture_layout_build(win_fl); + if (!win_fl->furniture_layout.valid) + goto Failure; + old_titlebar_x1 = win_fl->furniture_layout.titlebar.x1; + + rc = wuss_window_resize(win_fl, SIZE2D(140, 60)); /* grow the width */ + if (rc != result_OK) + goto Failure; + + /* the resize must have invalidated the cache: nothing else here rebuilds + * it, so a stale valid==1 means furniture would paint at the old width */ + if (win_fl->furniture_layout.valid) + goto Failure; + + /* rebuilding now must track the new, wider window */ + wuss__furniture_layout_build(win_fl); + wuss_window_get_visible_bounds(win_fl, &after_fl); + if (win_fl->furniture_layout.titlebar.x1 <= old_titlebar_x1) + goto Failure; /* titlebar still at the old width */ + + /* the full-width divider rule must span to the new right edge too */ + found = 0; + for (i = 0; i < win_fl->furniture_layout.npieces; i++) + { + const wuss__furniture_piece_t *p = &win_fl->furniture_layout.pieces[i]; + + if (p->paint == wuss__FURNITURE_PAINT_OUTLINE && + p->rect.y1 == win_fl->furniture_layout.titlebar.y1 && + p->rect.x1 == win_fl->furniture_layout.titlebar.x1) + found = 1; + } + if (!found) + goto Failure; + + wuss_window_close(win_fl); + } + printf("test: dragging a clear window onto an occluder leaves the occluder untouched\n"); { From 996ee39acabe1fde5a16cac233aa865e83225c7b Mon Sep 17 00:00:00 2001 From: David Thomas Date: Wed, 9 Sep 2026 23:55:46 +0100 Subject: [PATCH 86/98] fix(bitmap): stop bitmap_fill_pattern inverting the clip test box_intersection returns non-zero when the intersection is empty, but the guard negated it, so any non-NULL area that overlapped the bitmap returned early and painted nothing. Only area==NULL worked. Drop the negation to match screen_fill_pattern. Co-Authored-By: Claude Sonnet 5 --- libraries/framebuf/bitmap/fill-pattern.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/framebuf/bitmap/fill-pattern.c b/libraries/framebuf/bitmap/fill-pattern.c index 050ca43..57d4f0d 100644 --- a/libraries/framebuf/bitmap/fill-pattern.c +++ b/libraries/framebuf/bitmap/fill-pattern.c @@ -42,7 +42,7 @@ result_t bitmap_fill_pattern(bitmap_t *bm, { clip = full; } - else if (!box_intersection(area, &full, &clip)) + else if (box_intersection(area, &full, &clip)) { return result_OK; /* nothing to fill */ } From eb024f5a3dbad2c895a5a2f55dd2a5c56ff25a96 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Wed, 9 Sep 2026 23:57:11 +0100 Subject: [PATCH 87/98] fix(bitmap): bound bitmap__rle_decode_row against an end pointer The decoder ran a bare for(;;) that only stopped on an EOL opcode, so a truncated or corrupt RLE blob (short data_len, missing EOL, bit-rot) read opcodes past the allocation. Every other row walker already threads an `end` limit; the decoder did not. Add the `end` parameter, stop at it, and have a reserved opcode bail to `end` (release builds previously did a bare `break`, desynchronising the caller's row cursor) so a bad blob decodes to nothing rather than garbage. bitmap_decompress now reads data_len from the blob header to form `end`; the two screen RLE blitters and the row round-trip test already had it. Co-Authored-By: Claude Sonnet 5 --- libraries/framebuf/bitmap/compress.c | 21 +++++++++++++------ libraries/framebuf/bitmap/rle.h | 8 ++++++- libraries/framebuf/bitmap/test/rle-test.c | 9 ++++---- .../framebuf/screen/screen-copy-bitmap-rle.c | 4 ++-- 4 files changed, 29 insertions(+), 13 deletions(-) diff --git a/libraries/framebuf/bitmap/compress.c b/libraries/framebuf/bitmap/compress.c index 29c750a..6056208 100644 --- a/libraries/framebuf/bitmap/compress.c +++ b/libraries/framebuf/bitmap/compress.c @@ -275,6 +275,7 @@ result_t bitmap__rle_encode_row(const void *srcrow, /* ----------------------------------------------------------------------- */ const uint8_t *bitmap__rle_decode_row(const uint8_t *p, + const uint8_t *end, int log2bpp, void *dstrow, int skip, @@ -285,12 +286,13 @@ const uint8_t *bitmap__rle_decode_row(const uint8_t *p, int bpp; assert(p); + assert(end); assert(log2bpp == 3 || log2bpp == 5); dst = dstrow; bpp = 1 << (log2bpp - 3); - for (;;) + while (p < end) { uint8_t op; int id; @@ -302,7 +304,7 @@ const uint8_t *bitmap__rle_decode_row(const uint8_t *p, id = clz8(op); if (id == bitmap__RLE_EOL_ID) - break; + return p; /* Decode opcode + length; `srcpx` (literal) / `px` (repeat) set below. */ if (id == bitmap__RLE_LITERAL_ID) @@ -343,8 +345,11 @@ const uint8_t *bitmap__rle_decode_row(const uint8_t *p, } else { + /* Reserved / forward-incompatible opcode, or a corrupt stream. Bail to + * `end` so this row and every later row decode to nothing rather than + * garbage from a desynchronised cursor. */ assert(!"bitmap__rle_decode_row: reserved opcode"); - break; + return end; } /* Clip this run against the remaining left-skip and plot budget. The @@ -591,7 +596,9 @@ result_t bitmap_decompress(bitmap_t *bm) pixelfmt_t base; int log2bpp; uint32_t rowbytes; + const uint8_t *blob; const uint8_t *p; + const uint8_t *end; uint8_t *raw; uint8_t *dstrow; int y; @@ -606,8 +613,10 @@ result_t bitmap_decompress(bitmap_t *bm) base = pixelfmt_base(bm->format); log2bpp = pixelfmt_log2bpp(base); - rowbytes = bitmap__rle_get32((const uint8_t *) bm->base + 0); - p = (const uint8_t *) bm->base + bitmap__RLE_HEADER_SIZE; + blob = bm->base; + rowbytes = bitmap__rle_get32(blob + 0); + p = blob + bitmap__RLE_HEADER_SIZE; + end = p + bitmap__rle_get32(blob + 4); raw = malloc((size_t) rowbytes * bm->size.h); if (raw == NULL) @@ -616,7 +625,7 @@ result_t bitmap_decompress(bitmap_t *bm) dstrow = raw; for (y = 0; y < bm->size.h; y++) { - p = bitmap__rle_decode_row(p, log2bpp, dstrow, 0, bm->size.w, 1); + p = bitmap__rle_decode_row(p, end, log2bpp, dstrow, 0, bm->size.w, 1); dstrow += rowbytes; } diff --git a/libraries/framebuf/bitmap/rle.h b/libraries/framebuf/bitmap/rle.h index 32b1d58..4fee9f0 100644 --- a/libraries/framebuf/bitmap/rle.h +++ b/libraries/framebuf/bitmap/rle.h @@ -114,6 +114,11 @@ result_t bitmap__rle_encode_row(const void *srcrow, * consumed before writing begins and at most `plot` pixels are written. * * \param[in] p Cursor at the row's first opcode. + * \param[in] end One past the last valid stream byte. Decoding stops + * here even if no EOL is seen; a reserved opcode also + * bails to `end`, so a truncated or corrupt blob + * decodes to nothing rather than reading past the + * buffer. * \param[in] log2bpp 3 or 5. * \param[in] dstrow Destination, at the first pixel to be written. * \param[in] skip Row pixels to consume before writing. @@ -121,9 +126,10 @@ result_t bitmap__rle_encode_row(const void *srcrow, * \param[in] zero_skip Non-zero to write zero pixels for `skip` runs * (lossless decode); zero to leave them untouched * (alpha-tested blit). - * \return Cursor just past this row's EOL. + * \return Cursor just past this row's EOL (clamped to `end`). */ const uint8_t *bitmap__rle_decode_row(const uint8_t *p, + const uint8_t *end, int log2bpp, void *dstrow, int skip, diff --git a/libraries/framebuf/bitmap/test/rle-test.c b/libraries/framebuf/bitmap/test/rle-test.c index dce195b..e335596 100644 --- a/libraries/framebuf/bitmap/test/rle-test.c +++ b/libraries/framebuf/bitmap/test/rle-test.c @@ -24,7 +24,10 @@ /* ----------------------------------------------------------------------- */ /* Encode one row then decode it unclipped, compare byte-for-byte. */ -static int roundtrip_row(const void *row, int npix, int log2bpp, int has_alpha) +static int roundtrip_row(const void *row, + int npix, + int log2bpp, + int has_alpha) { uint8_t enc[4 * W + W / 64 + 16]; uint8_t dec[4 * W]; @@ -44,9 +47,7 @@ static int roundtrip_row(const void *row, int npix, int log2bpp, int has_alpha) return 0; } - (void) bitmap__rle_decode_row(enc, log2bpp, dec, 0, npix, 1); - - (void) used; + (void) bitmap__rle_decode_row(enc, enc + used, log2bpp, dec, 0, npix, 1); if (memcmp(dec, row, (size_t) npix * bpp) != 0) { diff --git a/libraries/framebuf/screen/screen-copy-bitmap-rle.c b/libraries/framebuf/screen/screen-copy-bitmap-rle.c index 40d9576..3f91735 100644 --- a/libraries/framebuf/screen/screen-copy-bitmap-rle.c +++ b/libraries/framebuf/screen/screen-copy-bitmap-rle.c @@ -103,7 +103,7 @@ static result_t screen_copy_bitmap_rle_p4(screen_t *scr, for (xx = 0; xx < plot; xx++) scratch[xx] = 0; /* zero_skip below only fills covered skip runs */ - p = bitmap__rle_decode_row(p, log2bpp, scratch, skip, plot, 1); + p = bitmap__rle_decode_row(p, end, log2bpp, scratch, skip, plot, 1); rowp = dstbase + (size_t) (draw_box->y0 + i) * scr->rowbytes; @@ -201,7 +201,7 @@ result_t screen_copy_bitmap_rle(screen_t *scr, + (size_t) (draw_box->y0 + i) * scr->rowbytes + (size_t) draw_box->x0 * bpp; - p = bitmap__rle_decode_row(p, log2bpp, dstrow, skip, plot, 0); + p = bitmap__rle_decode_row(p, end, log2bpp, dstrow, skip, plot, 0); } return result_OK; From bd0d429ac2bee4a7e2a16a32c03b561bb13286e5 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Wed, 9 Sep 2026 23:57:57 +0100 Subject: [PATCH 88/98] fix(bmfont): range-check the glyph id in bmfont_draw bmfont_draw formed gid = c - ' ' from a raw signed char and used it to index adw[] and glyphs + gid*glyphbytes with no bounds check, so a tab, a DEL or any byte >= 0x80 (UTF-8/Latin-1) produced a negative or oversized index and an out-of-bounds read/write. bmfont_measure already filters c < ' ' and gid >= totalchars; apply the same guard in both of bmfont_draw's loops. Also give the fully-vertically-clipped case (clippedcharheight <= 0) a release-build bail: it was caught only by assert(), so an NDEBUG build would enter drawfn with a negative height and loop ~INT_MAX times. Co-Authored-By: Claude Sonnet 5 --- libraries/framebuf/bmfont/bmfont.c | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/libraries/framebuf/bmfont/bmfont.c b/libraries/framebuf/bmfont/bmfont.c index 22d05bc..aae597d 100644 --- a/libraries/framebuf/bmfont/bmfont.c +++ b/libraries/framebuf/bmfont/bmfont.c @@ -1596,7 +1596,16 @@ result_t bmfont_draw(bmfont_t *bmfont, int x = pos->x; + /* box_intersects said something is visible, but a glyph can still be fully + * clipped vertically at a boundary. drawfn's `while (charheight--)` would + * then run ~INT_MAX times, so bail rather than trust the assert alone. */ assert(clippedcharheight > 0); + if (clippedcharheight <= 0) + { + if (end_pos) + *end_pos = POINT(pos->x, pos->y); + return result_OK; + } while (len--) { @@ -1606,7 +1615,15 @@ result_t bmfont_draw(bmfont_t *bmfont, const void *glyph; c = *text++; - gid = c - ' '; + + /* control characters and anything outside the glyph table draw nothing + * and advance nothing -- matching bmfont_measure. */ + if ((unsigned char) c < ' ') + continue; + gid = (unsigned char) c - ' '; + if (gid >= bmfont->totalchars) + continue; + advance = bmfont_advance_for(bmfont, gid) + tracking; x += advance; @@ -1669,8 +1686,13 @@ result_t bmfont_draw(bmfont_t *bmfont, int gid; int advance; - c = *text++; - gid = c - ' '; + c = *text++; + if ((unsigned char) c < ' ') + continue; + gid = (unsigned char) c - ' '; + if (gid >= bmfont->totalchars) + continue; + advance = bmfont_advance_for(bmfont, gid) + tracking; x += advance; From 27da007a9de36a92c082f0116a7402aa2becb33c Mon Sep 17 00:00:00 2001 From: David Thomas Date: Wed, 9 Sep 2026 23:58:31 +0100 Subject: [PATCH 89/98] fix(wuss): guard against a double free when a QUIT handler reaps its task wuss_destroy's task sweep delivered wuss_EVENT_QUIT and then wuss__free'd each task node without first setting wuss_TASK__REAPING. A QUIT handler that closed its last autoclose window hit wuss_window_close's self-destruct path (AUTOCLOSE set, REAPING clear, windows empty), which delivered a second QUIT and freed the node -- which the sweep then freed again. A handler calling wuss_task_destroy(self) hit the same double free via that function's own unlink-and-free. Set wuss_TASK__REAPING before QUIT in the sweep, matching wuss_task_destroy, and make wuss_task_destroy a no-op when the task is already being reaped. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/core/destroy.c | 11 ++++++++++- libraries/wuss/core/task/destroy.c | 6 ++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/libraries/wuss/core/destroy.c b/libraries/wuss/core/destroy.c index c555df1..6734e7b 100644 --- a/libraries/wuss/core/destroy.c +++ b/libraries/wuss/core/destroy.c @@ -44,8 +44,17 @@ void wuss_destroy(wuss_t *doomed) next = e->next; if (wuss__task_from_link(e) != menu_task) { + wuss_task_t *task = wuss__task_from_link(e); + + /* Mark the teardown before QUIT so a handler that closes its last + * autoclose window -- or calls wuss_window_close / wuss_task_destroy + * on itself -- doesn't also fire QUIT and free this node from under + * the sweep, which would then wuss__free it a second time. Same guard + * wuss_task_destroy sets for its own internal QUIT. */ + task->flags |= wuss_TASK__REAPING; + event.kind = wuss_EVENT_QUIT; - (void) wuss__deliver(wuss__task_from_link(e), NULL, &event); + (void) wuss__deliver(task, NULL, &event); wuss__free(doomed, e); } e = next; diff --git a/libraries/wuss/core/task/destroy.c b/libraries/wuss/core/task/destroy.c index aefab74..2268c05 100644 --- a/libraries/wuss/core/task/destroy.c +++ b/libraries/wuss/core/task/destroy.c @@ -19,6 +19,12 @@ void wuss_task_destroy(wuss_task_t *doomed) if (doomed == NULL) return; + /* Already being reaped -- by wuss_destroy's sweep, or a re-entrant call + * from this task's own QUIT handler. The outer teardown owns the unlink + * and free; doing it here too would double-free the node. */ + if (doomed->flags & wuss_TASK__REAPING) + return; + wuss = doomed->wuss; /* Mark the teardown so wuss_window_close's autoclose path doesn't also From 2f1cc043b07e7f79845e866d7555c2ac0f84ad38 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Wed, 9 Sep 2026 23:59:50 +0100 Subject: [PATCH 90/98] fix(bitmap): size the RLE compress buffer for one control byte per pixel The worst-case row estimate assumed one control byte per 128 pixels, but the encoder emits a 1-byte literal control for every isolated non-repeatable pixel. A legal y8 image of the form singleton, pair, singleton, pair, ... needs ~1.33*w bytes/row (literal-of-1 + repeat-of-2 per three pixels), overflowing the allocation and returning result_BUFFER_OVERFLOW on valid input. bpp >= 4 formats were hidden by the dominant w*bpp literal-data term. Budget w*(bpp+1) plus the literal-long split control and an EOL. Adds a y8 round-trip test on the pathological pattern. Co-Authored-By: Claude Sonnet 5 --- libraries/framebuf/bitmap/compress.c | 13 +++++++++---- libraries/framebuf/bitmap/test/rle-test.c | 23 +++++++++++++++++++++++ 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/libraries/framebuf/bitmap/compress.c b/libraries/framebuf/bitmap/compress.c index 6056208..abd25a4 100644 --- a/libraries/framebuf/bitmap/compress.c +++ b/libraries/framebuf/bitmap/compress.c @@ -535,10 +535,15 @@ result_t bitmap_compress(bitmap_t *bm) w = bm->size.w; h = bm->size.h; - /* Worst case per row: every pixel literal, one control byte per 128 px, - * plus an EOL. */ - rowworst = (size_t) w * bpp - + ((size_t) w / bitmap__RLE_LITERAL_LEN_MAX + 1) * 2 + /* Worst case per row: the encoder can be driven to a one-byte control per + * pixel (e.g. singleton, pair, singleton, pair ... forces a literal-of-1 + * then a repeat-of-2 for every three source pixels), so budget one control + * byte plus the pixel bytes for every pixel, one extra control per 128 px + * for the literal-long split, plus an EOL. For bpp >= 4 the w*bpp term + * dominates and this is barely above the old estimate; for y8 it is the + * difference between fitting and a spurious result_BUFFER_OVERFLOW. */ + rowworst = (size_t) w * (bpp + 1) + + ((size_t) w / bitmap__RLE_LITERAL_LEN_MAX + 1) + 1; worst = bitmap__RLE_HEADER_SIZE + rowworst * (size_t) h; diff --git a/libraries/framebuf/bitmap/test/rle-test.c b/libraries/framebuf/bitmap/test/rle-test.c index e335596..a70e95a 100644 --- a/libraries/framebuf/bitmap/test/rle-test.c +++ b/libraries/framebuf/bitmap/test/rle-test.c @@ -339,6 +339,29 @@ static int test_y8(void) ok = 0; } + /* Pathological expansion: singleton, pair, singleton, pair, ... forces a + * literal-of-1 then a repeat-of-2 per three pixels, ~1.33*w bytes/row. The + * worst-case allocation must cover it rather than returning + * result_BUFFER_OVERFLOW on this legal input. (bm.base was freed and + * replaced by the round trip above, so allocate afresh.) */ + { + uint8_t *p2 = malloc(n); + + for (i = 0; i < (int) n; i++) + p2[i] = (uint8_t) (i % 3 == 0 ? (i / 3 + 1) : 0x80); + memcpy(copy, p2, n); + bitmap_init(&bm, SIZE2D(W, H), pixelfmt_y8, W, NULL, p2); + rc = bitmap_compress(&bm); + if (rc) { fprintf(stderr, "y8 pathological compress rc=&%x\n", rc); ok = 0; goto done; } + rc = bitmap_decompress(&bm); + if (rc) { fprintf(stderr, "y8 pathological decompress rc=&%x\n", rc); ok = 0; goto done; } + if (memcmp(bm.base, copy, n) != 0) + { + fprintf(stderr, "y8 pathological: round trip differs\n"); + ok = 0; + } + } + done: free(bm.base); free(copy); From ac4e5eacb423d1c621d253ad1d96da56c49223ff Mon Sep 17 00:00:00 2001 From: David Thomas Date: Thu, 10 Sep 2026 00:00:38 +0100 Subject: [PATCH 91/98] docs(wuss): spell out that wuss_EVENT_CLOSE is not a veto point The close furniture now routes through wuss_window_try_close, which fires PRE_CLOSE (vetoable) then CLOSE then frees the window. A task carried over from the pre-PRE_CLOSE contract may still treat CLOSE as "decide whether to keep the window" and hold its wuss_window_t past the handler -- now a use-after-free. Document the contract at the enum: drop the handle in CLOSE, put keep-open logic in PRE_CLOSE. Co-Authored-By: Claude Sonnet 5 --- include/wuss/task.h | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/include/wuss/task.h b/include/wuss/task.h index 91d4c7e..e3ea2af 100644 --- a/include/wuss/task.h +++ b/include/wuss/task.h @@ -60,7 +60,12 @@ typedef enum wuss_event_kind * vetoes it. Never fired by wuss_window_close. */ wuss_EVENT_PRE_CLOSE, /** A window has closed after a successful wuss_window_try_close. Never - * fired by wuss_window_close. */ + * fired by wuss_window_close. This is not a veto point: PRE_CLOSE has + * already been accepted and wuss frees the window the instant this + * handler returns, so the task must drop any wuss_window_t it holds for + * this window here -- keeping or using it afterwards is a use-after-free. + * Put "keep the window open" logic (e.g. an unsaved-changes prompt) in + * PRE_CLOSE and veto from there. */ wuss_EVENT_CLOSE, /** Wuss has finished its pending work; once per task per wuss_idle. */ wuss_EVENT_IDLE, From 156b3ec11d7ba73a32b65692b2d9a358b1c11ef0 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Thu, 10 Sep 2026 00:01:10 +0100 Subject: [PATCH 92/98] fix(bitmap): convert every p4 pixel in bmconv_p4_to_bgrx8888 The loop ran x < src->size.w / 8 and expanded 8 pixels per iteration, so the final w % 8 pixels of each row were never written -- the output had uninitialised garbage columns for any width not a multiple of 8. It also walked src->base continuously, ignoring row padding in src->rowbytes. Switch to a per-pixel loop honouring both width and rowbytes, matching the p1 and p2 converters. Co-Authored-By: Claude Sonnet 5 --- libraries/framebuf/bitmap/bitmap.c | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/libraries/framebuf/bitmap/bitmap.c b/libraries/framebuf/bitmap/bitmap.c index a0bae7e..04c07a3 100644 --- a/libraries/framebuf/bitmap/bitmap.c +++ b/libraries/framebuf/bitmap/bitmap.c @@ -138,8 +138,8 @@ static result_t bmconv_p4_to_bgrx8888(const bitmap_t *src, bitmap_t **pdst) const pixelfmt_bgrx8888_t *map; bitmap_t *dst; pixelfmt_bgrx8888_t *outpixels; - pixelfmt_p4_t *inpixels; - int x,y; + const unsigned char *inrow; + int x, y; assert(src); assert(src->palette); @@ -171,22 +171,14 @@ static result_t bmconv_p4_to_bgrx8888(const bitmap_t *src, bitmap_t **pdst) if (rc) return rc; - inpixels = src->base; + inrow = src->base; for (y = 0; y < src->size.h; y++) { - for (x = 0; x < src->size.w / 8; x++) - { - pixelfmt_p4_t in = *inpixels++; // fetches 8 pixels - // 0xABCDEFGH is 8 4bpp pixels shown H,G,F,E,D,C,B,A - *outpixels++ = map[(in >> 0) & 0xF]; - *outpixels++ = map[(in >> 4) & 0xF]; - *outpixels++ = map[(in >> 8) & 0xF]; - *outpixels++ = map[(in >> 12) & 0xF]; - *outpixels++ = map[(in >> 16) & 0xF]; - *outpixels++ = map[(in >> 20) & 0xF]; - *outpixels++ = map[(in >> 24) & 0xF]; - *outpixels++ = map[(in >> 28) & 0xF]; - } + /* per-pixel, so widths that aren't a multiple of 8 keep their last + * w % 8 pixels, and row padding in src->rowbytes is respected */ + for (x = 0; x < src->size.w; x++) + *outpixels++ = map[(inrow[x >> 1] >> ((x & 1) << 2)) & 0xF]; + inrow += src->rowbytes; } *pdst = dst; From 7e72caadef14e790504ce506a5e7251349975fec Mon Sep 17 00:00:00 2001 From: David Thomas Date: Thu, 10 Sep 2026 00:01:43 +0100 Subject: [PATCH 93/98] fix(wuss): clear stale pointer state when a window is hidden wuss_window_set_hidden marked a window hidden but left wuss->furniture.dragging, wuss->pressed_icon and wuss->hover_icon pointing into it. Hiding a window mid-drag (from an event handler) meant the next wuss_mouse_move still took the dragging branch and drove wuss_window_move on an off-screen window every pointer report. Clear the same pointer state wuss_window_close does. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/core/window/set-hidden.c | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/libraries/wuss/core/window/set-hidden.c b/libraries/wuss/core/window/set-hidden.c index f930043..f1b9467 100644 --- a/libraries/wuss/core/window/set-hidden.c +++ b/libraries/wuss/core/window/set-hidden.c @@ -18,8 +18,27 @@ result_t wuss_window_set_hidden(wuss_window_t *window, int hidden) if (hidden) { + wuss_t *wuss = window->wuss; + + /* Drop any pointer state that targets this window: an off-screen window + * must not keep being driven by the mouse. Same set wuss_window_close + * clears. */ +#ifdef WUSS_FURNITURE + if (wuss->furniture.dragging == window) + { + wuss->furniture.dragging = NULL; + wuss->furniture.drag_kind = wuss_FURNITURE_DRAG_NONE; + } +#endif +#ifdef WUSS_ICONS + if (wuss->pressed_icon != NULL && wuss->pressed_icon->window == window) + wuss->pressed_icon = NULL; + if (wuss->hover_icon != NULL && wuss->hover_icon->window == window) + wuss->hover_icon = NULL; +#endif + /* still on screen: repaint its footprint now, then mark it gone */ - wuss_invalidate(window->wuss, &window->visible); + wuss_invalidate(wuss, &window->visible); window->flags |= wuss_WINDOW_HIDDEN; return result_OK; } From fbcdcd082706df56172bba20882071d8e8e0a455 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Thu, 10 Sep 2026 00:02:35 +0100 Subject: [PATCH 94/98] fix(wuss): match the drawn scroll strip to its hit region at the corner scroll_strip always reserved size + WUSS_DIVIDER_PX at the far end for the resize corner, but scroll_strip_hit only reserves it when something owns that corner (a resize icon, or -- for the horizontal strip -- a vertical scrollbar). With a horizontal scrollbar and NO_VSCROLL and NO_RESIZE the drawn strip stopped short while the hit box ran to the visible edge: the rightmost size + 1 px band showed stale pixels yet hit-tested as HSCROLL_WELL and started a scrollbar drag over empty chrome. Give scroll_strip the same far-end rule. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/furniture/scroll-box.c | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/libraries/wuss/furniture/scroll-box.c b/libraries/wuss/furniture/scroll-box.c index f5c0dc4..c56d69d 100644 --- a/libraries/wuss/furniture/scroll-box.c +++ b/libraries/wuss/furniture/scroll-box.c @@ -11,23 +11,34 @@ /* The scrollbar strip: full breadth, running between the titlebar (v) or left * outline (h) and the resize corner. "long" axis is y when vertical, x when - * horizontal. */ + * horizontal. + * + * The far end stops short of the bottom-right corner (a seam then a + * size x size square) only when something actually owns that corner: a + * resize icon, or -- for the horizontal strip -- the vertical scrollbar it + * would otherwise draw over. With neither present the strip runs to the + * outline. This must match scroll_strip_hit's far-end rule so the drawn + * strip and its hit region agree. */ static void scroll_strip(const wuss_window_t *window, int horizontal, box_t *out) { box_t titlebar; - int outline_px, size; + int outline_px, size, has_resize, has_vscroll; - outline_px = wuss__outline_px(window); - size = wuss__button_size(window); + outline_px = wuss__outline_px(window); + size = wuss__button_size(window); + has_resize = !(window->flags & wuss_WINDOW_NO_RESIZE); + has_vscroll = !(window->flags & wuss_WINDOW_NO_VSCROLL); if (horizontal) { out->y1 = window->visible.y1 - outline_px; out->y0 = out->y1 - size; out->x0 = window->visible.x0 + outline_px; - out->x1 = window->visible.x1 - outline_px - size - WUSS_DIVIDER_PX; /* seam then resize corner */ + out->x1 = (has_resize || has_vscroll) + ? window->visible.x1 - outline_px - size - WUSS_DIVIDER_PX /* seam then resize corner */ + : window->visible.x1 - outline_px; } else { @@ -35,7 +46,9 @@ static void scroll_strip(const wuss_window_t *window, out->x0 = out->x1 - size; wuss__titlebar_box(window, &titlebar); out->y0 = titlebar.y1; - out->y1 = window->visible.y1 - outline_px - size - WUSS_DIVIDER_PX; /* seam then resize corner */ + out->y1 = has_resize + ? window->visible.y1 - outline_px - size - WUSS_DIVIDER_PX /* seam then resize corner */ + : window->visible.y1 - outline_px; } } From 9ca2a86e62c8ed88e29da4db6df57a85bce10df1 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Thu, 10 Sep 2026 00:03:27 +0100 Subject: [PATCH 95/98] fix(wuss): stop nearest_edge_region turning frame chrome into a resize The fallback claimed wuss_FURNITURE_RESIZE for a whole bottom or right carve band whenever a resize icon was present, and a scroll well for an edge with no scrollbar strip. A click on the bare carve band left of the corner icon (NO_TITLEBAR, NO_VSCROLL, NO_HSCROLL, !NO_RESIZE) then started a resize far from the visible handle. Only hand off to a scroll well when that scrollbar's strip runs the full edge; the resize corner already has its own hit box, so every other edge pixel is frame -> TITLE or CONTENT. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/furniture/hit-test.c | 38 ++++++++++++++--------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/libraries/wuss/furniture/hit-test.c b/libraries/wuss/furniture/hit-test.c index 59702c3..04bc36c 100644 --- a/libraries/wuss/furniture/hit-test.c +++ b/libraries/wuss/furniture/hit-test.c @@ -3,42 +3,42 @@ #include "../core/impl.h" /* Attribute a point that is inside window->visible but outside the content box - * and outside every furniture hit box above -- the 1px outline band on an edge - * that has no scrollbar or resize icon to absorb it -- to the nearest - * furniture. Never returns CONTENT except for a fully chromeless outlined - * window (no titlebar, no scrollbars, no resize), whose bare 1px frame has - * genuinely no furniture behind it (documented exception). */ + * and outside every furniture hit box above to the nearest furniture. Such a + * point is a bare outline/carve pixel: an edge that has a scrollbar strip + * running its full length hands off to that strip's well; every other edge + * pixel (including the carve band left over when only a lone resize corner is + * present -- the corner itself already has its own hit box) is frame, not a + * widget, so it resolves to TITLE or CONTENT rather than starting a resize or + * scrollbar drag from somewhere the user cannot see the handle. */ static wuss_furniture_region_t nearest_edge_region(const wuss_window_t *window, point_t p) { box_t content; - int has_v, has_h, has_resize, has_title; + int has_v, has_h, has_title; wuss__content_box(window, &content); - has_v = !(window->flags & wuss_WINDOW_NO_VSCROLL); - has_h = !(window->flags & wuss_WINDOW_NO_HSCROLL); - has_resize = !(window->flags & wuss_WINDOW_NO_RESIZE); - has_title = !(window->flags & wuss_WINDOW_NO_TITLEBAR); + has_v = !(window->flags & wuss_WINDOW_NO_VSCROLL); + has_h = !(window->flags & wuss_WINDOW_NO_HSCROLL); + has_title = !(window->flags & wuss_WINDOW_NO_TITLEBAR); + /* bottom edge: the hscroll strip runs its full width */ if (p.y >= content.y1) - return has_h ? wuss_FURNITURE_HSCROLL_WELL - : has_resize ? wuss_FURNITURE_RESIZE - : has_v ? wuss_FURNITURE_VSCROLL_WELL - : has_title ? wuss_FURNITURE_TITLE + return has_h ? wuss_FURNITURE_HSCROLL_WELL + : has_title ? wuss_FURNITURE_TITLE : wuss_FURNITURE_CONTENT; + /* right edge: the vscroll strip runs its full height */ if (p.x >= content.x1) - return has_v ? wuss_FURNITURE_VSCROLL_WELL - : has_resize ? wuss_FURNITURE_RESIZE - : has_h ? wuss_FURNITURE_HSCROLL_WELL - : has_title ? wuss_FURNITURE_TITLE + return has_v ? wuss_FURNITURE_VSCROLL_WELL + : has_title ? wuss_FURNITURE_TITLE : wuss_FURNITURE_CONTENT; if (p.y < content.y0) return has_title ? wuss_FURNITURE_TITLE : wuss_FURNITURE_CONTENT; - /* left outline column, level with the content */ + /* left outline column, level with the content: only the hscroll strip's + * grown hit box reaches here, and only if there is one */ return has_h ? wuss_FURNITURE_HSCROLL_WELL : has_title ? wuss_FURNITURE_TITLE : wuss_FURNITURE_CONTENT; From 490fc8870627595d32aad2e48a337b65b02b6fd6 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Thu, 10 Sep 2026 00:04:07 +0100 Subject: [PATCH 96/98] fix(screen): guard the span fill pointer, not just the span, in release The comment promises a release build no-ops rather than dereferencing NULL, but the guard tested only scr->span and then called scr->span->fill(...). A span registered without a fill entry would pass the check and call through a NULL pointer. Test scr->span->fill too, matching the assert. Co-Authored-By: Claude Sonnet 5 --- libraries/framebuf/screen/screen-fill-hline.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/framebuf/screen/screen-fill-hline.c b/libraries/framebuf/screen/screen-fill-hline.c index 5069d98..6880aef 100644 --- a/libraries/framebuf/screen/screen-fill-hline.c +++ b/libraries/framebuf/screen/screen-fill-hline.c @@ -42,7 +42,7 @@ void screen_fill_hline(screen_t *scr, int x, int y, int w, colour_t colour) * with no span-registry entry -- the assert catches that in debug builds, * but a release build must still no-op rather than dereference NULL. */ assert(scr->span && scr->span->fill); - if (scr->span == NULL) + if (scr->span == NULL || scr->span->fill == NULL) return; rowp = (unsigned char *) scr->base + draw_box.y0 * scr->rowbytes; From 970e47d05c7d017b46ce7157fa4caaa2fea71086 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Thu, 10 Sep 2026 00:06:57 +0100 Subject: [PATCH 97/98] docs(wuss): update TODO --- libraries/wuss/TODO.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/wuss/TODO.txt b/libraries/wuss/TODO.txt index 1a1ad9e..d39e91c 100644 --- a/libraries/wuss/TODO.txt +++ b/libraries/wuss/TODO.txt @@ -54,6 +54,7 @@ LAYER 4 - WINDOW FURNITURE & INTERACTION (needs bug layer, text layer) (outline-click bug fixed; unblocked) - Input focus, text entry, furniture tinting - Icon layout +- Furniture colour selection needs improvements (e.g. losing menu readability when palette changed) LAYER 5 - DRAG & TEMPLATES (needs sprites, config-after-the-fact) From b414c2e407aae61a12ee0abcce1c4ae420dce6aa Mon Sep 17 00:00:00 2001 From: David Thomas Date: Thu, 10 Sep 2026 00:17:47 +0100 Subject: [PATCH 98/98] docs(changelog): record p1/p2 support and the wuss-picklepuss fixes Covers every commit since the last CHANGELOG touch (0ba5456): the 1bpp/2bpp paletted format work across bitmap/span/screen/bmfont and the wuss demo, the curve-task convex hull, the ttf2bmfont generator, the gradient-task dither cycling, the screen_set_pixel_8 -> _p8 rename, and the run of correctness fixes from the wuss-picklepuss review (RLE bounds, bmfont glyph range check, task double-free, fill-pattern clip test, p4 conversion, hidden-window pointer state, scroll strip / hit-region mismatch). Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 111 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 108 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ec94f4..c47943a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -232,6 +232,35 @@ _Unreleased_ until one is cut. pool. Released areas are not coalesced. - `POINT(x, y)` and `SIZE2D(w, h)` compound-literal macros in `geom/point.h` and `geom/size.h`. +- 1bpp and 2bpp paletted pixel formats (`pixelfmt_p1`, `pixelfmt_p2`), + packed MSB-first (bit 7 / bits 7..6 are the leftmost pixel) to match PNG + and the existing pattern bit order. `span_p1` / `span_p2` (`fill` + + `blendconst`) are registered so `bitmap_init()` hands out a valid span; + `screen_set_pixel` / `screen_blend_pixel` / `screen_fill_pattern` / + `screen_copy_bitmap` gain p1 and p2 cases; `bitmap_convert()` grows + `p1 -> bgrx8888` and `p2 -> bgrx8888` paths for the SDL frontend's + per-frame display conversion. `pixelmap` already handled both generically. +- `bmfont_draw()` renders to a `pixelfmt_p1` or `pixelfmt_p2` screen. A + single `bmfont_p1_plot_row` / `bmfont_p2_plot_row` does the per-pixel + MSB-first bit write for both 1- and 2-byte glyph rows and both opaque + and transparent backgrounds. +- `pixelfmt_paletted_nentries()` — palette entry count for any paletted + format, replacing the `(scr->format == pixelfmt_p4) ? 16 : 0` hack at + every `colour_to_pixel()` call site (which passed a broken `nentries=0` + for any non-p4 paletted screen). +- The `wuss` SDL demo accepts `-d`/`--depth 1` and `--depth 2`, selecting a + `pixelfmt_p1` (stride `(width + 7) / 8`) or `pixelfmt_p2` (stride + `(width + 3) / 4`) framebuffer alongside the existing 4 and 32. +- The `wuss` curve task draws the active control points' convex hull + (Andrew's monotone chain) as a closed light-grey polyline before the + curve and blobs. +- The `wuss` gradient task cycles its ordered-dither matrix through 2x2, + 4x4 and 8x8 on SELECT / ADJUST clicks; each matrix cell maps to a fixed + -8..+8 offset so a larger matrix gives a finer pattern, not a noisier + one (4x4 output unchanged). The current size is drawn top-left. +- `tools/ttf2bmfont.py` — renders a TTF into the bmfont PNG format the + loader expects. Three small paletted bitmap faces (`04b_03`, `04b_25`, + `Nokia`) added under `resources/bmfonts/`. ### Changed @@ -372,6 +401,14 @@ _Unreleased_ until one is cut. leaf now delivers `wuss_EVENT_MENU_SELECT` (with `window == NULL`) to that task — `data.menu_select` carries `{ const struct wuss_menu *menu; int index; wuss_button_t button; }`. `wuss_menu_select_fn_t` is removed. +- `screen_set_pixel_8()` is renamed `screen_set_pixel_p8()`, matching the + `_p1` / `_p2` / `_p4` naming of the other paletted set-pixel helpers. +- The greeble prefab tables (`greeble_prefab_cells` / `greeble_prefab`) are + regenerated from the current sheet; `GREEBLE_NPREFAB` 103 -> 137. +- The `wuss` image task resizes the window frame on load (via + `wuss_window_resize()` before `wuss_window_set_doc()`, which alone only + moved the scroll extent) and insets an 8px solid border inside the + ninepatch frame. ### Fixed @@ -449,9 +486,9 @@ _Unreleased_ until one is cut. `NDEBUG`). - `screen_fill_hline()` (and so `screen_fill_rect()`, which calls it per row) is a no-op in release builds for a `screen_t` whose pixel format has - no span-registry entry, instead of dereferencing the NULL `scr->span` — - the guarding `assert` was compiled out under `NDEBUG` so this previously - crashed. + no span-registry entry — or a span with no `fill` — instead of + dereferencing the NULL `scr->span` / `scr->span->fill`; the guarding + `assert` was compiled out under `NDEBUG` so this previously crashed. - `wuss_create()`'s font-slot arrays and loop bound in the interactive demo are now sized from one named, compile-time-checked constant (`WUSS_MAIN_NFONTS`, checked against `wuss_MAX_FONTS`) instead of three @@ -488,3 +525,71 @@ _Unreleased_ until one is cut. endpoints. - `screen_draw_ninepatch()` clamps its corner cells so they no longer overlap and double-draw when the destination box is smaller than the source corners. +- `spanregistry_get()`'s one-slot cache seeded `lastformat` to zero, which + equals `pixelfmt_p1` (the first enum member), so the first p1 lookup + short-circuited to a NULL span and tripped the `screen_fill_hline()` + assert. Seeded with `pixelfmt_unknown` and the fast path guarded on a + non-NULL span. +- The `NO_RESIZE_BLIT` branch of `wuss_window_resize()` invalidated the + dirty region but never dropped the cached furniture layout, so + `wuss__furniture_draw` kept painting titlebar/carve/outline rects at the + previous frame's width. It now calls `wuss__chrome_invalidate_layout`, + matching the blit branch's `wuss__chrome_repaint`. +- `bitmap_fill_pattern()` negated the `box_intersection()` result (which is + non-zero when the intersection is *empty*), so any non-NULL `area` that + overlapped the bitmap returned early and painted nothing — only + `area == NULL` worked. Drop the negation to match `screen_fill_pattern()`. +- `bitmap__rle_decode_row()` ran a bare `for (;;)` that only stopped on an + EOL opcode, so a truncated or corrupt RLE blob read opcodes past the + allocation. It now takes an `end` limit (like every other row walker), + stops at it, and has a reserved opcode bail to `end` (release builds + previously did a bare `break`, desynchronising the caller's row cursor). + `bitmap_decompress()` reads `data_len` from the blob header to form + `end`. +- `bmfont_draw()` formed `gid = c - ' '` from a raw signed `char` and + indexed `adw[]` and the glyph bitmaps with no bounds check, so a tab, a + DEL or any byte `>= 0x80` produced a negative or oversized index and an + out-of-bounds read/write. Both draw loops now filter `c < ' '` and + `gid >= totalchars`, matching `bmfont_measure()`. The + fully-vertically-clipped case (`clippedcharheight <= 0`) also gets a + release-build bail rather than entering `drawfn` with a negative height + and looping ~`INT_MAX` times. +- `wuss_destroy()`'s task sweep delivered `wuss_EVENT_QUIT` and freed each + task node without first setting `wuss_TASK__REAPING`. A QUIT handler that + closed its last autoclose window hit `wuss_window_close`'s self-destruct + path, which delivered a second QUIT and freed the node — which the sweep + then freed again. `wuss_TASK__REAPING` is now set before QUIT in the + sweep, and `wuss_task_destroy()` is a no-op when the task is already + being reaped. +- `bitmap_compress()`'s worst-case row estimate assumed one control byte + per 128 pixels, but the encoder emits a 1-byte literal control for every + isolated non-repeatable pixel. A legal y8 image of the form singleton, + pair, singleton, pair, ... needs ~1.33*w bytes/row, overflowing the + allocation and returning `result_BUFFER_OVERFLOW` on valid input. The + budget is now `w * (bpp + 1)` plus the literal-long split control and an + EOL. +- `bmconv_p4_to_bgrx8888()` ran `x < src->size.w / 8` and expanded 8 pixels + per iteration, so the final `w % 8` pixels of each row were never written + — the output had uninitialised garbage columns for any width not a + multiple of 8 — and it ignored row padding in `src->rowbytes`. Switched + to a per-pixel loop honouring both, matching the p1 and p2 converters. +- `wuss_window_set_hidden()` marked a window hidden but left + `wuss->furniture.dragging`, `wuss->pressed_icon` and `wuss->hover_icon` + pointing into it, so hiding a window mid-drag from an event handler meant + the next `wuss_mouse_move()` still drove `wuss_window_move()` on an + off-screen window every pointer report. It now clears the same pointer + state `wuss_window_close()` does. +- `scroll_strip()` always reserved `size + WUSS_DIVIDER_PX` at the far end + for the resize corner, but `scroll_strip_hit()` only reserves it when + something owns that corner. With a horizontal scrollbar and `NO_VSCROLL` + and `NO_RESIZE` the drawn strip stopped short while the hit box ran to + the visible edge: the rightmost band showed stale pixels yet hit-tested + as `HSCROLL_WELL` and started a scrollbar drag over empty chrome. + `scroll_strip()` gets the same far-end rule. +- `wuss__furniture_hit_test()`'s `nearest_edge_region()` fallback claimed + `wuss_FURNITURE_RESIZE` for a whole bottom or right carve band whenever a + resize icon was present, and a scroll well for an edge with no scrollbar + strip. A click on the bare carve band left of the corner icon then + started a resize far from the visible handle. It now hands off to a + scroll well only when that scrollbar's strip runs the full edge; + every other edge pixel resolves to `TITLE` or `CONTENT`.