From 076c25a6419b2f8a70aa9f0f7a8132835178681d Mon Sep 17 00:00:00 2001 From: David Thomas Date: Thu, 27 Aug 2026 00:48:48 +0100 Subject: [PATCH 001/287] Start 0.6.0 --- CMakeLists.txt | 4 ++-- README.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 50283d94..03aa4842 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,7 +14,7 @@ endif() list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake/Modules/) -project(DPTLib VERSION 0.5.0 DESCRIPTION "DPT's C Library" LANGUAGES C) +project(DPTLib VERSION 0.6.0 DESCRIPTION "DPT's C Library" LANGUAGES C) # The values set in the toolchain file aren't available until this point. if(TARGET_RISCOS) @@ -40,7 +40,7 @@ add_subdirectory(libraries/fortify) add_library(DPTLib) set_target_properties(DPTLib PROPERTIES - VERSION 0.5.0 + VERSION 0.6.0 DESCRIPTION "DPT's Portable C Library" C_STANDARD 99 PREFIX "" # remove 'lib' prefix diff --git a/README.md b/README.md index e42b101b..187fac83 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # DPTLib -version 0.5.0 +version 0.6.0 [![Build status](https://github.com/dpt/DPTLib/actions/workflows/ci.yml/badge.svg)](https://github.com/dpt/DPTLib/actions) From e889d0ee581c54e661a3f2dc341afda654d36351 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Thu, 27 Aug 2026 09:56:07 +0100 Subject: [PATCH 002/287] feat(wuss): add wuss_get_font accessor Lets tasks read the system font handle given to wuss_create, so they can draw their own content in the same face as window titlebars. --- CMakeLists.txt | 1 + include/wuss/wuss.h | 9 +++++++++ libraries/wuss/get-font.c | 12 ++++++++++++ 3 files changed, 22 insertions(+) create mode 100644 libraries/wuss/get-font.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 03aa4842..a5f350b7 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -331,6 +331,7 @@ set(WUSS_SOURCES libraries/wuss/furniture/toggle-box.c libraries/wuss/furniture/vscroll-box.c libraries/wuss/furniture.h + libraries/wuss/get-font.c libraries/wuss/idle.c libraries/wuss/impl.h libraries/wuss/invalidate.c diff --git a/include/wuss/wuss.h b/include/wuss/wuss.h index c8add3aa..00299250 100644 --- a/include/wuss/wuss.h +++ b/include/wuss/wuss.h @@ -143,6 +143,15 @@ result_t wuss_create(screen_t *scr, */ void wuss_destroy(wuss_t *doomed); +/** + * Fetch the system font (see wuss_create), for tasks to draw their own + * content in the same face as window titlebars. + * + * \param[in] wuss Window manager. + * \return System font, or NULL if none was given to wuss_create. + */ +bmfont_t *wuss_get_font(const wuss_t *wuss); + /** * Redraw every window, back-to-front. * diff --git a/libraries/wuss/get-font.c b/libraries/wuss/get-font.c new file mode 100644 index 00000000..206cd958 --- /dev/null +++ b/libraries/wuss/get-font.c @@ -0,0 +1,12 @@ +/* get-font.c -- wuss - minimal window manager */ + +#include + +#include "impl.h" + +bmfont_t *wuss_get_font(const wuss_t *wuss) +{ + assert(wuss != NULL); + + return wuss->font; +} From 671ad5a447b5735061776a1738fb71e50dfd4039 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Thu, 27 Aug 2026 11:00:17 +0100 Subject: [PATCH 003/287] test(wuss): add Chars task, a 32x8 grid of the system font's glyphs Displays every glyph 0-255 from wuss's system font, one per cell, so the whole font can be eyeballed at a glance. Skips itself if wuss was created without a font. --- CMakeLists.txt | 1 + libraries/wuss/test/tasks/chars.c | 124 ++++++++++++++++++++++++++++++ libraries/wuss/test/tasks/chars.h | 35 +++++++++ libraries/wuss/test/wuss-test.c | 5 ++ 4 files changed, 165 insertions(+) create mode 100644 libraries/wuss/test/tasks/chars.c create mode 100644 libraries/wuss/test/tasks/chars.h diff --git a/CMakeLists.txt b/CMakeLists.txt index a5f350b7..da9885d9 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -535,6 +535,7 @@ if(BUILD_TESTS) libraries/datastruct/vector/test/vector-test.c libraries/wuss/test/tasks/ball.c libraries/wuss/test/tasks/blank.c + libraries/wuss/test/tasks/chars.c libraries/wuss/test/tasks/checker.c libraries/wuss/test/tasks/curve.c libraries/wuss/test/tasks/gradient.c diff --git a/libraries/wuss/test/tasks/chars.c b/libraries/wuss/test/tasks/chars.c new file mode 100644 index 00000000..0f005525 --- /dev/null +++ b/libraries/wuss/test/tasks/chars.c @@ -0,0 +1,124 @@ +/* chars.c -- wuss test - system font glyph grid task */ + +#ifdef USE_SDL + +#ifdef FORTIFY +#include "fortify/fortify.h" +#endif + +#include "base/utils.h" +#include "framebuf/palettes.h" +#include "geom/box.h" +#include "geom/point.h" + +#include "chars.h" + +#define CHARS_COLS 32 +#define CHARS_ROWS 8 +#define CHARS_PAD 2 + +result_t chars_create(wuss_t *wuss, + const colour_t *palette, + chars_task_t *task) +{ + wuss_task_t delegate; + box_t box; + bmfont_t *font; + int font_width, font_height, cell_w, cell_h; + + font = wuss_get_font(wuss); + if (font == NULL) + { + task->window = NULL; + return result_OK; + } + + bmfont_get_info(font, &font_width, &font_height); + cell_w = font_width + CHARS_PAD * 2; + cell_h = font_height + CHARS_PAD * 2; + + task->font = font; + task->fg = palette[palette_PICO8_BLACK]; + task->bg = palette[palette_PICO8_WHITE]; + + delegate = wuss_task_start(chars_handle, task, wuss_NO_BACKGROUND); /* chars_redraw paints every cell itself */ + box = (box_t) BOX_POS_SIZE(500, 100, cell_w * CHARS_COLS, cell_h * CHARS_ROWS); + + return wuss_window_create(wuss, + &box, + "Chars", + wuss_WINDOW_NO_RESIZE | + wuss_WINDOW_NO_TOGGLE_SIZE | + wuss_WINDOW_NO_VSCROLL | + wuss_WINDOW_NO_HSCROLL, + &delegate, + box.x1 - box.x0, + box.y1 - box.y0, + &task->window); +} + +void chars_destroy(chars_task_t *task) +{ + if (task->window != NULL) + wuss_window_close(task->window); +} + +static result_t chars_redraw(const wuss_event_t *event, void *task_data) +{ + chars_task_t *cc; + screen_t *scr; + const box_t *bounds; + int font_width, font_height, cell_w, cell_h; + int i, sx, sy; + + cc = task_data; + + scr = event->data.redraw.scr; + bounds = event->data.redraw.bounds; + sx = event->data.redraw.scroll.x; + sy = event->data.redraw.scroll.y; + + bmfont_get_info(cc->font, &font_width, &font_height); + cell_w = font_width + CHARS_PAD * 2; + cell_h = font_height + CHARS_PAD * 2; + + for (i = 0; i < CHARS_COLS * CHARS_ROWS; i++) + { + int col, row, x, y; + char ch; + point_t pos; + + col = i % CHARS_COLS; + row = i / CHARS_COLS; + x = bounds->x0 - sx + col * cell_w; + y = bounds->y0 - sy + row * cell_h; + + screen_draw_rect(scr, x, y, cell_w, cell_h, cc->bg); + + ch = (char) i; + pos.x = x + CHARS_PAD; + pos.y = y + CHARS_PAD; + bmfont_draw(cc->font, scr, &ch, 1, cc->fg, cc->bg, &pos, NULL); + } + + return result_OK; +} + +result_t chars_handle(wuss_window_t *window, + const wuss_event_t *event, + void *task_data) +{ + if (event->kind == wuss_EVENT_CLOSE) + { + wuss_window_close(window); + ((chars_task_t *) task_data)->window = NULL; + return result_OK; + } + + if (event->kind != wuss_EVENT_REDRAW) + return result_OK; + + return chars_redraw(event, task_data); +} + +#endif /* USE_SDL */ diff --git a/libraries/wuss/test/tasks/chars.h b/libraries/wuss/test/tasks/chars.h new file mode 100644 index 00000000..80495809 --- /dev/null +++ b/libraries/wuss/test/tasks/chars.h @@ -0,0 +1,35 @@ +/* chars.h -- wuss test - system font glyph grid task */ + +#ifndef TASKS_CHARS_H +#define TASKS_CHARS_H + +#ifdef USE_SDL + +#include "framebuf/bmfont.h" +#include "framebuf/colour.h" +#include "wuss/window.h" + +/* displays every glyph (0-255) of the wuss system font as a 32x8 grid, + * so the whole font can be eyeballed at a glance */ +typedef struct chars_task +{ + wuss_window_t *window; + bmfont_t *font; + colour_t fg, bg; +} +chars_task_t; + +wuss_event_fn_t chars_handle; + +/* create the glyph-grid window against the given wuss instance; does + * nothing and returns result_OK if wuss has no system font */ +result_t chars_create(wuss_t *wuss, + const colour_t *palette, + chars_task_t *task); + +/* destroy the glyph-grid window created by chars_create, if any */ +void chars_destroy(chars_task_t *task); + +#endif /* USE_SDL */ + +#endif /* TASKS_CHARS_H */ diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 97a7ac2b..585c5459 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -31,6 +31,7 @@ #include "tasks/ball.h" #include "tasks/blank.h" +#include "tasks/chars.h" #include "tasks/checker.h" #include "tasks/curve.h" #include "tasks/gradient.h" @@ -57,6 +58,7 @@ static bmfont_t *g_daydream_font; static ball_task_t g_ball_task; static text_task_t g_text_task; static blank_task_t g_blank_task; +static chars_task_t g_chars_task; static palette_task_t g_palette_task; static image_task_t g_image_task; static checker_task_t g_checker_task; @@ -67,6 +69,7 @@ static gradient_task_t g_gradient_task; static result_t spawn_ball(void) { return ball_create(g_wuss, g_palette, &g_ball_task); } static result_t spawn_text(void) { return text_create(g_wuss, g_palette, g_daydream_font, &g_text_task); } static result_t spawn_blank(void) { return blank_create(g_wuss, g_npalette, &g_blank_task); } +static result_t spawn_chars(void) { return chars_create(g_wuss, g_palette, &g_chars_task); } static result_t spawn_palette(void) { return palette_create(g_wuss, g_palette, g_npalette, &g_palette_task); } static result_t spawn_image(void) { return image_create(g_wuss, g_palette, g_resources, &g_image_task); } static result_t spawn_checker(void) { return checker_create(g_wuss, g_palette, &g_checker_task); } @@ -77,6 +80,7 @@ static result_t spawn_gradient(void) { return gradient_create(g_wuss, &g_gradien static void destroy_ball(void) { ball_destroy(&g_ball_task); } static void destroy_text(void) { text_destroy(&g_text_task); } static void destroy_blank(void) { blank_destroy(&g_blank_task); } +static void destroy_chars(void) { chars_destroy(&g_chars_task); } static void destroy_palette(void) { palette_destroy(&g_palette_task); } static void destroy_image(void) { image_destroy(&g_image_task); } static void destroy_checker(void) { checker_destroy(&g_checker_task); } @@ -89,6 +93,7 @@ static launcher_entry_t g_launcher_entries[] = { "Ball", spawn_ball, destroy_ball, false }, { "Text", spawn_text, destroy_text, false }, { "Blank", spawn_blank, destroy_blank, false }, + { "Chars", spawn_chars, destroy_chars, false }, { "Palette", spawn_palette, destroy_palette, false }, { "Image", spawn_image, destroy_image, false }, { "Checker", spawn_checker, destroy_checker, false }, From 04c42cfc87fabecd0645e11e29490b4b93a4e396 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Thu, 27 Aug 2026 11:05:54 +0100 Subject: [PATCH 004/287] fix(pack): avoid signed left-shift overflow in 32-bit word unpacking bp[w3] is an unsigned char promoted to int; shifting a byte >= 0x80 left by 24 overflowed signed int range, undefined behaviour caught by UBSan. Cast each byte to uint32_t before shifting. --- libraries/utils/pack/unpack.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/libraries/utils/pack/unpack.c b/libraries/utils/pack/unpack.c index b0251590..01f2911e 100644 --- a/libraries/utils/pack/unpack.c +++ b/libraries/utils/pack/unpack.c @@ -244,7 +244,7 @@ static size_t name(const unsigned char *buf, const char *fmt, va_list args) \ a = va_arg(args, uint32_t *); \ while (n--) \ { \ - *a++ = (uint32_t) (bp[w0] | (bp[w1] << 8) | (bp[w2] << 16) | (bp[w3] << 24)); \ + *a++ = (uint32_t) ((uint32_t) bp[w0] | ((uint32_t) bp[w1] << 8) | ((uint32_t) bp[w2] << 16) | ((uint32_t) bp[w3] << 24)); \ bp += 4; \ } \ } \ @@ -257,7 +257,7 @@ static size_t name(const unsigned char *buf, const char *fmt, va_list args) \ a = va_arg(args, uint64_t *); \ while (n--) \ { \ - *a++ = (uint32_t) (bp[w0] | (bp[w1] << 8) | (bp[w2] << 16) | (bp[w3] << 24)); \ + *a++ = (uint32_t) ((uint32_t) bp[w0] | ((uint32_t) bp[w1] << 8) | ((uint32_t) bp[w2] << 16) | ((uint32_t) bp[w3] << 24)); \ bp += 4; \ } \ } \ @@ -270,7 +270,7 @@ static size_t name(const unsigned char *buf, const char *fmt, va_list args) \ a = va_arg(args, int64_t *); \ while (n--) \ { \ - *a++ = (int32_t) (bp[w0] | (bp[w1] << 8) | (bp[w2] << 16) | (bp[w3] << 24)); \ + *a++ = (int32_t) ((uint32_t) bp[w0] | ((uint32_t) bp[w1] << 8) | ((uint32_t) bp[w2] << 16) | ((uint32_t) bp[w3] << 24)); \ bp += 4; \ } \ } \ @@ -473,7 +473,7 @@ static size_t name(const unsigned char *buf, const char *fmt, va_list args) \ while (n--) \ { \ pI = va_arg(args, uint32_t *); \ - *pI = (uint32_t) (bp[w0] | (bp[w1] << 8) | (bp[w2] << 16) | (bp[w3] << 24)); \ + *pI = (uint32_t) ((uint32_t) bp[w0] | ((uint32_t) bp[w1] << 8) | ((uint32_t) bp[w2] << 16) | ((uint32_t) bp[w3] << 24)); \ bp += 4; \ } \ break; \ @@ -482,7 +482,7 @@ static size_t name(const unsigned char *buf, const char *fmt, va_list args) \ while (n--) \ { \ pQ = va_arg(args, uint64_t *); \ - *pQ = (uint32_t) (bp[w0] | (bp[w1] << 8) | (bp[w2] << 16) | (bp[w3] << 24)); \ + *pQ = (uint32_t) ((uint32_t) bp[w0] | ((uint32_t) bp[w1] << 8) | ((uint32_t) bp[w2] << 16) | ((uint32_t) bp[w3] << 24)); \ bp += 4; \ } \ break; \ @@ -491,7 +491,7 @@ static size_t name(const unsigned char *buf, const char *fmt, va_list args) \ while (n--) \ { \ pq = va_arg(args, int64_t *); \ - *pq = (int32_t) (bp[w0] | (bp[w1] << 8) | (bp[w2] << 16) | (bp[w3] << 24)); \ + *pq = (int32_t) ((uint32_t) bp[w0] | ((uint32_t) bp[w1] << 8) | ((uint32_t) bp[w2] << 16) | ((uint32_t) bp[w3] << 24)); \ bp += 4; \ } \ break; \ From d440e3b639fbbf5f33819559ffe315659d3bf98e Mon Sep 17 00:00:00 2001 From: David Thomas Date: Thu, 27 Aug 2026 11:11:26 +0100 Subject: [PATCH 005/287] build: add USE_ASAN option for AddressSanitizer/UBSan builds Wraps the flags previously passed by hand into the ad-hoc build-asan/ directory, so any build tree can opt in with -DUSE_ASAN=YES. --- CMakeLists.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index da9885d9..815f0484 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,6 +28,7 @@ if(CCACHE_FOUND) endif(CCACHE_FOUND) option(USE_FORTIFY "Use Fortify" OFF) +option(USE_ASAN "Build with AddressSanitizer and UndefinedBehaviorSanitizer" OFF) option(DPTLIB_IMAGES_READ_ONLY "Remove libpng write support" OFF) # Referencing CMAKE_TOOLCHAIN_FILE avoids a warning on rebuilds. @@ -395,6 +396,11 @@ if(USE_FORTIFY) target_link_libraries(DPTLib PUBLIC Fortify) endif() +if(USE_ASAN) + target_compile_options(DPTLib PUBLIC -fsanitize=address,undefined -g -O0) + target_link_options(DPTLib PUBLIC -fsanitize=address,undefined) +endif() + if(DPTLIB_IMAGES_READ_ONLY) target_compile_definitions(DPTLib PRIVATE DPTLIB_IMAGES_READ_ONLY) endif() From d0042a2c942023068c4885eb882d6d7fb04620e6 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Thu, 27 Aug 2026 11:17:50 +0100 Subject: [PATCH 006/287] fix(colour): avoid signed integer overflow in closest_palette_entry Squared per-channel deltas multiplied by libjpeg's fixed-point weights overflowed signed int at the top of the byte range (e.g. 255 apart on green), undefined behaviour caught by UBSan. Cast each squared delta to unsigned int before the multiply. --- libraries/framebuf/colour/colour.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libraries/framebuf/colour/colour.c b/libraries/framebuf/colour/colour.c index 216eab11..1b77838f 100644 --- a/libraries/framebuf/colour/colour.c +++ b/libraries/framebuf/colour/colour.c @@ -63,9 +63,9 @@ static unsigned int closest_palette_entry(const colour_t *palette, dr = ent_r - req_r; dg = ent_g - req_g; db = ent_b - req_b; - curdist = ((dr * dr) * red_weight + - (dg * dg) * green_weight + - (db * db) * blue_weight) >> 16; + curdist = ((unsigned int) (dr * dr) * red_weight + + (unsigned int) (dg * dg) * green_weight + + (unsigned int) (db * db) * blue_weight) >> 16; if (curdist < dist) { dist = curdist; From e9fc00718bc8a1731a7820f1142334758b9aa817 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Thu, 27 Aug 2026 11:17:55 +0100 Subject: [PATCH 007/287] fix(wuss): stop Chars task drawing glyphs the font doesn't have bmfont_draw indexes glyphs as c - ' ' with no bound check, and Chars fed it every byte value 0-255; bytes past the font's actual glyph count walked off the glyph table (AddressSanitizer heap-buffer-overflow). Add bmfont_get_count() and have chars_redraw leave a cell blank instead of drawing when its byte value falls outside the font's range. --- include/framebuf/bmfont.h | 10 ++++++++++ libraries/framebuf/bmfont/bmfont.c | 5 +++++ libraries/wuss/test/tasks/chars.c | 7 +++++++ 3 files changed, 22 insertions(+) diff --git a/include/framebuf/bmfont.h b/include/framebuf/bmfont.h index cf41a2b5..9f290fad 100644 --- a/include/framebuf/bmfont.h +++ b/include/framebuf/bmfont.h @@ -38,6 +38,16 @@ void bmfont_destroy(bmfont_t *bmfont); */ void bmfont_get_info(bmfont_t *bmfont, int *width, int *height); +/** + * Read the number of glyphs in the specified bitmap font. Glyphs are laid + * out contiguously starting at ' ' (space, 0x20), so a char c has a glyph + * iff c >= ' ' and c < ' ' + bmfont_get_count(bmfont). + * + * \param[in] bmfont Bitmap font to query. + * \return Number of glyphs in the font. + */ +int bmfont_get_count(bmfont_t *bmfont); + /** * Measure the width of a string drawn with the specified font. * diff --git a/libraries/framebuf/bmfont/bmfont.c b/libraries/framebuf/bmfont/bmfont.c index 16ff74da..5dd01fa4 100644 --- a/libraries/framebuf/bmfont/bmfont.c +++ b/libraries/framebuf/bmfont/bmfont.c @@ -519,6 +519,11 @@ void bmfont_get_info(bmfont_t *bmfont, int *width, int *height) *height = bmfont->charheight; } +int bmfont_get_count(bmfont_t *bmfont) +{ + return bmfont->totalchars; +} + result_t bmfont_measure(bmfont_t *bmfont, const char *text, int textlen, diff --git a/libraries/wuss/test/tasks/chars.c b/libraries/wuss/test/tasks/chars.c index 0f005525..f3b01130 100644 --- a/libraries/wuss/test/tasks/chars.c +++ b/libraries/wuss/test/tasks/chars.c @@ -69,6 +69,7 @@ static result_t chars_redraw(const wuss_event_t *event, void *task_data) screen_t *scr; const box_t *bounds; int font_width, font_height, cell_w, cell_h; + int first, count; int i, sx, sy; cc = task_data; @@ -82,6 +83,9 @@ static result_t chars_redraw(const wuss_event_t *event, void *task_data) cell_w = font_width + CHARS_PAD * 2; cell_h = font_height + CHARS_PAD * 2; + first = ' '; /* bmfont glyphs are laid out contiguously starting here */ + count = bmfont_get_count(cc->font); + for (i = 0; i < CHARS_COLS * CHARS_ROWS; i++) { int col, row, x, y; @@ -95,6 +99,9 @@ static result_t chars_redraw(const wuss_event_t *event, void *task_data) screen_draw_rect(scr, x, y, cell_w, cell_h, cc->bg); + if (i < first || i >= first + count) + continue; /* no glyph for this byte value: leave the cell blank */ + ch = (char) i; pos.x = x + CHARS_PAD; pos.y = y + CHARS_PAD; From 2792111d6c33f59220ca907ac860515e58d6b435 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Thu, 27 Aug 2026 12:56:43 +0100 Subject: [PATCH 008/287] feat(wuss): add Platonic solid models to Sofa task Adjust click now cycles through sofa, ship, tetrahedron, cube, octahedron, icosahedron and dodecahedron wireframes. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/test/tasks/sofa.c | 162 ++++++++++++++++++++++++++++++- libraries/wuss/test/tasks/sofa.h | 10 +- 2 files changed, 165 insertions(+), 7 deletions(-) diff --git a/libraries/wuss/test/tasks/sofa.c b/libraries/wuss/test/tasks/sofa.c index 7db04833..ca1642f5 100644 --- a/libraries/wuss/test/tasks/sofa.c +++ b/libraries/wuss/test/tasks/sofa.c @@ -44,7 +44,7 @@ static const box3_t sofa_parts[] = /* cube corner edges, indexing box3_corners' bit-numbered corners (bit0=x, * bit1=y, bit2=z; each pair differs in exactly one bit) */ -static const int cube_edges[12][2] = +static const int box_cube_edges[12][2] = { { 0, 1 }, { 2, 3 }, { 4, 5 }, { 6, 7 }, { 0, 2 }, { 1, 3 }, { 4, 6 }, { 5, 7 }, @@ -76,6 +76,138 @@ static const int ship_edges[18][2] = { 5, 8 }, { 8, 9 }, { 9, 5 }, /* right wing */ }; +/* the five Platonic solids, vertices normalised to unit circumradius */ + +static const vec3_t tetra_vertices[4] = +{ + { 0.577350269189626, 0.577350269189626, 0.577350269189626 }, + { 0.577350269189626, -0.577350269189626, -0.577350269189626 }, + { -0.577350269189626, 0.577350269189626, -0.577350269189626 }, + { -0.577350269189626, -0.577350269189626, 0.577350269189626 }, +}; +static const int tetra_edges[6][2] = +{ + { 0, 1 }, { 0, 2 }, { 0, 3 }, { 1, 2 }, + { 1, 3 }, { 2, 3 }, +}; + +static const vec3_t cube_vertices[8] = +{ + { 0.577350269189626, 0.577350269189626, 0.577350269189626 }, + { 0.577350269189626, 0.577350269189626, -0.577350269189626 }, + { 0.577350269189626, -0.577350269189626, 0.577350269189626 }, + { 0.577350269189626, -0.577350269189626, -0.577350269189626 }, + { -0.577350269189626, 0.577350269189626, 0.577350269189626 }, + { -0.577350269189626, 0.577350269189626, -0.577350269189626 }, + { -0.577350269189626, -0.577350269189626, 0.577350269189626 }, + { -0.577350269189626, -0.577350269189626, -0.577350269189626 }, +}; +static const int cube_edges[12][2] = +{ + { 0, 1 }, { 0, 2 }, { 0, 4 }, { 1, 3 }, + { 1, 5 }, { 2, 3 }, { 2, 6 }, { 3, 7 }, + { 4, 5 }, { 4, 6 }, { 5, 7 }, { 6, 7 }, +}; + +static const vec3_t octa_vertices[6] = +{ + { 1, 0, 0 }, + { -1, 0, 0 }, + { 0, 1, 0 }, + { 0, -1, 0 }, + { 0, 0, 1 }, + { 0, 0, -1 }, +}; +static const int octa_edges[12][2] = +{ + { 0, 2 }, { 0, 3 }, { 0, 4 }, { 0, 5 }, + { 1, 2 }, { 1, 3 }, { 1, 4 }, { 1, 5 }, + { 2, 4 }, { 2, 5 }, { 3, 4 }, { 3, 5 }, +}; + +static const vec3_t icosa_vertices[12] = +{ + { 0, 0.525731112119134, 0.85065080835204 }, + { 0, 0.525731112119134, -0.85065080835204 }, + { 0, -0.525731112119134, 0.85065080835204 }, + { 0, -0.525731112119134, -0.85065080835204 }, + { 0.525731112119134, 0.85065080835204, 0 }, + { 0.525731112119134, -0.85065080835204, 0 }, + { -0.525731112119134, 0.85065080835204, 0 }, + { -0.525731112119134, -0.85065080835204, 0 }, + { 0.85065080835204, 0, 0.525731112119134 }, + { 0.85065080835204, 0, -0.525731112119134 }, + { -0.85065080835204, 0, 0.525731112119134 }, + { -0.85065080835204, 0, -0.525731112119134 }, +}; +static const int icosa_edges[30][2] = +{ + { 0, 2 }, { 0, 4 }, { 0, 6 }, { 0, 8 }, + { 0, 10 }, { 1, 3 }, { 1, 4 }, { 1, 6 }, + { 1, 9 }, { 1, 11 }, { 2, 5 }, { 2, 7 }, + { 2, 8 }, { 2, 10 }, { 3, 5 }, { 3, 7 }, + { 3, 9 }, { 3, 11 }, { 4, 6 }, { 4, 8 }, + { 4, 9 }, { 5, 7 }, { 5, 8 }, { 5, 9 }, + { 6, 10 }, { 6, 11 }, { 7, 10 }, { 7, 11 }, + { 8, 9 }, { 10, 11 }, +}; + +static const vec3_t dodeca_vertices[20] = +{ + { 0.577350269189626, 0.577350269189626, 0.577350269189626 }, + { 0.577350269189626, 0.577350269189626, -0.577350269189626 }, + { 0.577350269189626, -0.577350269189626, 0.577350269189626 }, + { 0.577350269189626, -0.577350269189626, -0.577350269189626 }, + { -0.577350269189626, 0.577350269189626, 0.577350269189626 }, + { -0.577350269189626, 0.577350269189626, -0.577350269189626 }, + { -0.577350269189626, -0.577350269189626, 0.577350269189626 }, + { -0.577350269189626, -0.577350269189626, -0.577350269189626 }, + { 0, 0.35682208977309, 0.934172358962716 }, + { 0, 0.35682208977309, -0.934172358962716 }, + { 0, -0.35682208977309, 0.934172358962716 }, + { 0, -0.35682208977309, -0.934172358962716 }, + { 0.35682208977309, 0.934172358962716, 0 }, + { 0.35682208977309, -0.934172358962716, 0 }, + { -0.35682208977309, 0.934172358962716, 0 }, + { -0.35682208977309, -0.934172358962716, 0 }, + { 0.934172358962716, 0, 0.35682208977309 }, + { 0.934172358962716, 0, -0.35682208977309 }, + { -0.934172358962716, 0, 0.35682208977309 }, + { -0.934172358962716, 0, -0.35682208977309 }, +}; +static const int dodeca_edges[30][2] = +{ + { 0, 8 }, { 0, 12 }, { 0, 16 }, { 1, 9 }, + { 1, 12 }, { 1, 17 }, { 2, 10 }, { 2, 13 }, + { 2, 16 }, { 3, 11 }, { 3, 13 }, { 3, 17 }, + { 4, 8 }, { 4, 14 }, { 4, 18 }, { 5, 9 }, + { 5, 14 }, { 5, 19 }, { 6, 10 }, { 6, 15 }, + { 6, 18 }, { 7, 11 }, { 7, 15 }, { 7, 19 }, + { 8, 10 }, { 9, 11 }, { 12, 14 }, { 13, 15 }, + { 16, 17 }, { 18, 19 }, +}; + +/* a wireframe model: a vertex array plus an edge index array */ +typedef struct wireframe +{ + const vec3_t *vertices; + int nvertices; + const int (*edges)[2]; + int nedges; +} +wireframe_t; + +#define WIREFRAME(v, e) { v, NELEMS(v), e, NELEMS(e) } + +static const wireframe_t polyhedra[] = +{ + WIREFRAME(tetra_vertices, tetra_edges), + WIREFRAME(cube_vertices, cube_edges), + WIREFRAME(octa_vertices, octa_edges), + WIREFRAME(icosa_vertices, icosa_edges), + WIREFRAME(dodeca_vertices, dodeca_edges), +}; + static void box3_corners(const box3_t *box, vec3_t out[8]) { int i; @@ -196,13 +328,13 @@ static result_t sofa_redraw(const wuss_event_t *event, void *task_data) { const fix8_point_t *a, *b; - a = &screen[cube_edges[i][0]]; - b = &screen[cube_edges[i][1]]; + a = &screen[box_cube_edges[i][0]]; + b = &screen[box_cube_edges[i][1]]; screen_draw_line_wu_fix8(scr, a->x, a->y, b->x, b->y, sc->line); } } } - else + else if (sc->shape == sofa_SHAPE_SHIP) { fix8_point_t screen[NELEMS(ship_vertices)]; int i; @@ -219,6 +351,26 @@ static result_t sofa_redraw(const wuss_event_t *event, void *task_data) screen_draw_line_wu_fix8(scr, a->x, a->y, b->x, b->y, sc->line); } } + else + { + const wireframe_t *wf; + fix8_point_t screen[NELEMS(dodeca_vertices)]; /* largest solid */ + int i; + + wf = &polyhedra[sc->shape - sofa_SHAPE_TETRAHEDRON]; + + for (i = 0; i < wf->nvertices; i++) + screen[i] = project(rotate_xy(wf->vertices[i], SOFA_TILT, sc->angle), cx, cy, unit); + + for (i = 0; i < wf->nedges; i++) + { + const fix8_point_t *a, *b; + + a = &screen[wf->edges[i][0]]; + b = &screen[wf->edges[i][1]]; + screen_draw_line_wu_fix8(scr, a->x, a->y, b->x, b->y, sc->line); + } + } return result_OK; } @@ -231,7 +383,7 @@ static result_t sofa_mouse(wuss_window_t *window, wuss_button_t button, void *ta if (button == wuss_BUTTON_ADJUST) { - sc->shape = (sc->shape == sofa_SHAPE_SOFA) ? sofa_SHAPE_SHIP : sofa_SHAPE_SOFA; + sc->shape = (sc->shape + 1) % sofa_SHAPE__LIMIT; wuss_window_invalidate_all(window); } else diff --git a/libraries/wuss/test/tasks/sofa.h b/libraries/wuss/test/tasks/sofa.h index 20ace4aa..7a19ba1b 100644 --- a/libraries/wuss/test/tasks/sofa.h +++ b/libraries/wuss/test/tasks/sofa.h @@ -14,11 +14,17 @@ typedef enum sofa_shape { sofa_SHAPE_SOFA, - sofa_SHAPE_SHIP + sofa_SHAPE_SHIP, + sofa_SHAPE_TETRAHEDRON, + sofa_SHAPE_CUBE, + sofa_SHAPE_OCTAHEDRON, + sofa_SHAPE_ICOSAHEDRON, + sofa_SHAPE_DODECAHEDRON, + sofa_SHAPE__LIMIT } sofa_shape_t; -/* a wireframe sofa (seat, backrest, two arms) or a spaceship, +/* a wireframe sofa (seat, backrest, two arms), spaceship or Platonic solid, * spinning about its vertical axis; a Select click pauses/resumes the spin, * an Adjust click cycles the model */ typedef struct sofa_task From 8b809aadcbf0474d05c1837f437e02de2dd6f9a4 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Thu, 27 Aug 2026 13:43:42 +0100 Subject: [PATCH 009/287] fix(geom): avoid signed integer overflow in line clipping Replace line_clip's w*dy/h maths with a muldiv() helper that widens the multiply using 32-bit half-word arithmetic and does a bit-serial division, so no 64-bit or floating-point intermediate is needed while still avoiding UBSan's signed-overflow trap. --- libraries/geom/line/line.c | 60 +++++++++++++++++++++++++++++++++++--- 1 file changed, 56 insertions(+), 4 deletions(-) diff --git a/libraries/geom/line/line.c b/libraries/geom/line/line.c index c35898a7..7519fe7c 100644 --- a/libraries/geom/line/line.c +++ b/libraries/geom/line/line.c @@ -13,6 +13,58 @@ typedef unsigned int outcode_t; #define outcode_BOTTOM (1u << 2) #define outcode_TOP (1u << 3) +/* Compute (a * b) / c, truncating toward zero, without overflow and + * without using a 64-bit or floating-point intermediate. Assumes the + * true result fits in an int (guaranteed here as it's a coordinate). */ +static int muldiv(int a, int b, int c) +{ + unsigned int a_lo, a_hi, b_lo, b_hi; + unsigned int lo_lo, hi_lo, lo_hi, hi_hi; + unsigned int cross, cross_carry, lo_carry; + unsigned int hi, lo; + unsigned int ua, ub, uc; + unsigned int rem, quot, bit; + int neg; + int i; + + neg = 0; + if (a < 0) { neg = !neg; ua = 0u - (unsigned int) a; } else ua = (unsigned int) a; + if (b < 0) { neg = !neg; ub = 0u - (unsigned int) b; } else ub = (unsigned int) b; + if (c < 0) { neg = !neg; uc = 0u - (unsigned int) c; } else uc = (unsigned int) c; + + /* widen ua * ub into a 64-bit result held as two 32-bit halves */ + a_lo = ua & 0xFFFFu; a_hi = ua >> 16; + b_lo = ub & 0xFFFFu; b_hi = ub >> 16; + + lo_lo = a_lo * b_lo; + hi_lo = a_hi * b_lo; + lo_hi = a_lo * b_hi; + hi_hi = a_hi * b_hi; + + cross = hi_lo + lo_hi; + cross_carry = (cross < hi_lo) ? (1u << 16) : 0u; + lo = lo_lo + (cross << 16); + lo_carry = (lo < lo_lo) ? 1u : 0u; + hi = hi_hi + (cross >> 16) + cross_carry + lo_carry; + + /* long-divide the 64-bit (hi:lo) dividend by uc, one bit at a time */ + rem = 0; + quot = 0; + for (i = 63; i >= 0; i--) + { + bit = (i >= 32) ? ((hi >> (i - 32)) & 1u) : ((lo >> i) & 1u); + rem = (rem << 1) | bit; + if (rem >= uc) + { + rem -= uc; + if (i < 32) + quot |= (1u << i); + } + } + + return neg ? -(int) quot : (int) quot; +} + static INLINE outcode_t compute_outcode(const box_t *clip, int x, int y) { outcode_t code; @@ -80,23 +132,23 @@ int line_clip(const box_t *clip, if (oc & outcode_TOP) { - x = x0 + w * (clip->y1 - 1 - y0) / h; + x = x0 + muldiv(w, clip->y1 - 1 - y0, h); y = clip->y1 - 1; } else if (oc & outcode_BOTTOM) { - x = x0 + w * (clip->y0 - y0) / h; + x = x0 + muldiv(w, clip->y0 - y0, h); y = clip->y0; } else if (oc & outcode_RIGHT) { x = clip->x1 - 1; - y = y0 + h * (clip->x1 - 1 - x0) / w; + y = y0 + muldiv(h, clip->x1 - 1 - x0, w); } else if (oc & outcode_LEFT) { x = clip->x0; - y = y0 + h * (clip->x0 - x0) / w; + y = y0 + muldiv(h, clip->x0 - x0, w); } if (oc == oc0) From 00ae6cd9b28a6f07fde2853d69c1f898c64cc324 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Thu, 27 Aug 2026 14:01:55 +0100 Subject: [PATCH 010/287] fix(wuss): allow partial-redraw fast path for occluded-but-clear windows wuss_window_move()'s blit-and-slide optimisation only fired for the topmost window, so dragging a window sent to the back always fell back to a full-footprint redraw even when nothing above it actually overlapped its old position. Replace the topmost check with an explicit occlusion test (wuss__occluded_above) that walks the z-order list above the window and only declines the fast path when some window's visible box genuinely overlaps "before". --- libraries/wuss/window/move.c | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/libraries/wuss/window/move.c b/libraries/wuss/window/move.c index 3b594240..e2e7ecb1 100644 --- a/libraries/wuss/window/move.c +++ b/libraries/wuss/window/move.c @@ -2,6 +2,22 @@ #include "../impl.h" +/* true if any window above "window" in z-order overlaps "before" -- if + * so, "before" isn't purely this window's own rendering and the blit + * fast path in wuss_window_move() must not be used */ +static int wuss__occluded_above(wuss_window_t *window, const box_t *before) +{ + list_t *e; + + for (e = window->wuss->z_order.next; e != &window->link; e = e->next) + { + if (box_intersects(&((wuss_window_t *) e)->visible, before)) + return 1; + } + + return 0; +} + /* p is the window's content top-left; the furniture offset (outline plus * any titlebar) is constant for a given window, so the footprint just * follows it */ @@ -23,16 +39,17 @@ void wuss_window_move(wuss_window_t *window, point_t p) wuss__notify_open(window); - if (window->wuss->z_order.next == &window->link && + if (!wuss__occluded_above(window, &before) && screen_copy_rect(window->wuss->scr, &before, (point_t) { window->visible.x0, window->visible.y0 }, &copied)) { - /* Topmost, and the screen format supports the blit: every pixel of - * "before" is genuinely this window's own rendering (nothing above it - * to have punched holes in it), so sliding those pixels to the new - * position is exactly as correct as asking the task to redraw there, - * but far cheaper -- only the vacated sliver behind the old position - * still needs an actual repaint. */ + /* Nothing above this window overlaps its old footprint, and the screen + * format supports the blit: every pixel of "before" is genuinely this + * window's own rendering (nothing above it to have punched holes in + * it), so sliding those pixels to the new position is exactly as + * correct as asking the task to redraw there, but far cheaper -- only + * the vacated sliver behind the old position still needs an actual + * repaint. */ wuss__invalidate_minus(window->wuss, &before, &window->visible); /* "copied" can be smaller than the new footprint if either end of the @@ -43,8 +60,9 @@ void wuss_window_move(wuss_window_t *window, point_t p) } else { - /* Not topmost, or the blit was declined (e.g. paletted screen): fall - * back to a normal clipped redraw of the whole moved footprint. */ + /* Something above overlaps the old footprint, or the blit was declined + * (e.g. paletted screen): fall back to a normal clipped redraw of the + * whole moved footprint. */ box_union(&before, &window->visible, &dirty); wuss__invalidate_clipped(window, &dirty); } From b550405806e167dcfb8edc8d762c0d2e31edafa8 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Thu, 27 Aug 2026 14:10:41 +0100 Subject: [PATCH 011/287] test(wuss): cover move fast path for a back-most, unoccluded window Regression test for the fix in wuss_window_move(): a window sent to the back with nothing above it overlapping its old footprint must still take the blit-and-slide fast path, not fall back to a full redraw just because it isn't topmost. --- libraries/wuss/test/wuss-test.c | 89 +++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 585c5459..44cdd94c 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -1948,6 +1948,95 @@ result_t wuss_test(const char *resources) wuss_window_close(win_v); } + printf("test: dragging a back-most window with nothing above it still blits\n"); + + { + test_task_t tc_k, tc_l; + wuss_task_t delegate_k, delegate_l; + box_t box_k, box_l; + wuss_window_t *win_k, *win_l; + int before_k, before_l; + + tc_k.redraw_count = 0; + tc_k.mouse_count = 0; + delegate_k.handle = test_handle; + delegate_k.task_data = &tc_k; + delegate_k.bg = wuss_NO_BACKGROUND; + + box_k.x0 = 0; box_k.y0 = 140; /* clear of the still-open A/B windows above */ + box_k.x1 = 50; box_k.y1 = 175; + rc = wuss_window_create(wuss, + &box_k, + "K", + wuss_WINDOW_NO_BACK | wuss_WINDOW_NO_TOGGLE_SIZE | + wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | + wuss_WINDOW_NO_RESIZE, + &delegate_k, + box_k.x1 - box_k.x0, + box_k.y1 - box_k.y0, + &win_k); + if (rc != result_OK) + goto Failure; + + tc_l.redraw_count = 0; + tc_l.mouse_count = 0; + delegate_l.handle = test_handle; + delegate_l.task_data = &tc_l; + delegate_l.bg = wuss_NO_BACKGROUND; + + box_l.x0 = 120; box_l.y0 = 140; /* well clear of K, so never overlaps it */ + box_l.x1 = 170; box_l.y1 = 175; + rc = wuss_window_create(wuss, + &box_l, + "L", + wuss_WINDOW_NO_BACK | wuss_WINDOW_NO_TOGGLE_SIZE | + wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | + wuss_WINDOW_NO_RESIZE, + &delegate_l, + box_l.x1 - box_l.x0, + box_l.y1 - box_l.y0, + &win_l); + if (rc != result_OK) + goto Failure; + + wuss_window_restack(win_k, wuss_ZORDER_BACK); /* K is no longer topmost, but L never overlaps it */ + + rc = wuss_redraw_dirty(wuss); /* flush the restack's own dirty region first */ + if (rc != result_OK) + goto Failure; + + wuss_window_get_visible_bounds(win_k, &visible); + + rc = wuss_mouse_click(wuss, (point_t) { visible.x0 + 31, visible.y0 + 11 }, wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* K's titlebar, clear of the close icon */ + if (rc != result_OK) + goto Failure; + if (hit != win_k) + goto Failure; + + before_k = tc_k.redraw_count; + before_l = tc_l.redraw_count; + rc = wuss_mouse_move(wuss, (point_t) { visible.x0 + 45, visible.y0 + 21 }, &hit); + if (rc != result_OK) + goto Failure; + if (hit != win_k) + goto Failure; + + rc = wuss_redraw_dirty(wuss); + if (rc != result_OK) + goto Failure; + if (tc_k.redraw_count != before_k || tc_l.redraw_count != before_l) + goto Failure; /* blitted, not redrawn: nothing above K overlapped its old + * footprint, so the move fast path must still apply even + * though K isn't topmost */ + + rc = wuss_mouse_click(wuss, (point_t) { visible.x0 + 45, visible.y0 + 21 }, wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); + if (rc != result_OK) + goto Failure; + + wuss_window_close(win_k); + wuss_window_close(win_l); + } + printf("test: destroy mid-drag then move doesn't crash\n"); tc_c.redraw_count = 0; From 7a319b9933eb5f6162b05bd435afb1094e915d40 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Thu, 27 Aug 2026 14:23:37 +0100 Subject: [PATCH 012/287] fix(wuss): only invalidate the grown/shrunk sliver on resize wuss_window_resize() used to invalidate the union of the old and new footprint on every resize, forcing a full repaint even though a resize's top-left corner is fixed and everything within both footprints is unchanged. Add invalidate_grown_or_shrunk() to decompose the delta into the shrunk-away and grown-into slivers, each still clipped against occluders via wuss__invalidate_clipped (unlike wuss__invalidate_minus, which has no occlusion awareness and would have broken the fully-occluded-resize test). Add a regression test asserting the untouched interior corner is never marked dirty and the dirtied area stays below a full repaint. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/test/wuss-test.c | 97 +++++++++++++++++++++++++++++++++ libraries/wuss/window/resize.c | 38 ++++++++++++- 2 files changed, 132 insertions(+), 3 deletions(-) diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 44cdd94c..982d93c6 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -2037,6 +2037,103 @@ result_t wuss_test(const char *resources) wuss_window_close(win_l); } + printf("test: resizing a window only invalidates the grown/shrunk sliver\n"); + + { + test_task_t tc_m2; + wuss_task_t delegate_m2; + box_t box_m2, before2, after2, region; + wuss_window_t *win_m2; + int i, dirty_area, full_area, interior_x, interior_y, interior_dirty; + + tc_m2.redraw_count = 0; + tc_m2.mouse_count = 0; + delegate_m2.handle = test_handle; + delegate_m2.task_data = &tc_m2; + delegate_m2.bg = wuss_NO_BACKGROUND; + + box_m2.x0 = 0; box_m2.y0 = 0; + box_m2.x1 = 40; box_m2.y1 = 40; + rc = wuss_window_create(wuss, + &box_m2, + "M2", + wuss_WINDOW_NO_BACK | wuss_WINDOW_NO_TOGGLE_SIZE | + wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL, + &delegate_m2, + box_m2.x1 - box_m2.x0, + box_m2.y1 - box_m2.y0, + &win_m2); + if (rc != result_OK) + goto Failure; + + rc = wuss_redraw_dirty(wuss); /* flush the creation invalidation first */ + if (rc != result_OK) + goto Failure; + + wuss_window_get_visible_bounds(win_m2, &before2); + interior_x = before2.x0 + 2; /* well inside the untouched top-left corner */ + interior_y = before2.y0 + 2; + + rc = wuss_window_resize(win_m2, 80, 80); /* grow */ + if (rc != result_OK) + goto Failure; + + if (wuss_get_dirty_count(wuss) == 0) + goto Failure; + + interior_dirty = 0; + dirty_area = 0; + for (i = 0; i < wuss_get_dirty_count(wuss); i++) + { + wuss_get_dirty(wuss, i, ®ion); + if (box_contains_point(®ion, interior_x, interior_y)) + interior_dirty = 1; + dirty_area += (region.x1 - region.x0) * (region.y1 - region.y0); + } + if (interior_dirty) + goto Failure; /* untouched top-left corner, unchanged by growing bottom-right */ + + wuss_window_get_visible_bounds(win_m2, &after2); + full_area = (after2.x1 - after2.x0) * (after2.y1 - after2.y0); + if (dirty_area >= full_area) + goto Failure; /* must be less than a full redraw of the grown footprint */ + + rc = wuss_redraw_dirty(wuss); + if (rc != result_OK) + goto Failure; + + before2 = after2; + + rc = wuss_window_resize(win_m2, 40, 40); /* shrink back */ + if (rc != result_OK) + goto Failure; + + if (wuss_get_dirty_count(wuss) == 0) + goto Failure; + + interior_dirty = 0; + dirty_area = 0; + for (i = 0; i < wuss_get_dirty_count(wuss); i++) + { + wuss_get_dirty(wuss, i, ®ion); + if (box_contains_point(®ion, interior_x, interior_y)) + interior_dirty = 1; + dirty_area += (region.x1 - region.x0) * (region.y1 - region.y0); + } + if (interior_dirty) + goto Failure; /* still untouched: the corner that remains after shrinking */ + + full_area = (before2.x1 - before2.x0) * (before2.y1 - before2.y0); + if (dirty_area >= full_area) + goto Failure; + + rc = wuss_redraw_dirty(wuss); + if (rc != result_OK) + goto Failure; + + wuss_window_close(win_m2); + } + printf("test: destroy mid-drag then move doesn't crash\n"); tc_c.redraw_count = 0; diff --git a/libraries/wuss/window/resize.c b/libraries/wuss/window/resize.c index 9a7f48c1..aba0a521 100644 --- a/libraries/wuss/window/resize.c +++ b/libraries/wuss/window/resize.c @@ -1,11 +1,39 @@ /* resize.c -- wuss - minimal window manager */ +#include "base/utils.h" + #include "../impl.h" +/* Invalidate the part of "a" not covered by "b", given both share the same + * top-left corner (true of a window's visible box before/after a resize, as + * only the bottom-right corner moves): the difference splits into exactly + * two non-overlapping rectangles, each clipped against occluders before + * queueing. */ +static void invalidate_grown_or_shrunk(wuss_window_t *window, + const box_t *a, + const box_t *b) +{ + box_t piece; + + if (a->x1 > b->x1) + { + piece.x0 = b->x1; piece.y0 = a->y0; + piece.x1 = a->x1; piece.y1 = a->y1; + wuss__invalidate_clipped(window, &piece); + } + + if (a->y1 > b->y1) + { + piece.x0 = a->x0; piece.y0 = b->y1; + piece.x1 = MIN(a->x1, b->x1); piece.y1 = a->y1; + wuss__invalidate_clipped(window, &piece); + } +} + result_t wuss_window_resize(wuss_window_t *window, int width, int height) { int outline_px, titlebar_height; - box_t before, dirty; + box_t before; point_t carve; if (!wuss__size_ok(width, height)) @@ -21,8 +49,12 @@ result_t wuss_window_resize(wuss_window_t *window, int width, int height) wuss__notify_open(window); - box_union(&before, &window->visible, &dirty); - wuss__invalidate_clipped(window, &dirty); + /* The top-left corner is fixed, so any pixels within both the old and new + * footprint are unchanged and don't need repainting -- only the shrunk-away + * sliver (revealing whatever is now behind it) and the grown-into sliver + * (this window's own previously-undrawn content) do. */ + invalidate_grown_or_shrunk(window, &before, &window->visible); + invalidate_grown_or_shrunk(window, &window->visible, &before); return result_OK; } From dd26e5e4464568157b7e10141292dcf72f2bc31c Mon Sep 17 00:00:00 2001 From: David Thomas Date: Thu, 27 Aug 2026 14:27:33 +0100 Subject: [PATCH 013/287] fix(wuss): force old furniture dirty when growing on resize MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The grown/shrunk sliver invalidation added in 7a319b9 treats the old ∩ new footprint as fully unchanged, but window furniture (outline, scrollbars) is drawn along the trailing edge of the old footprint. When a window grows, that edge lands inside the new interior and was never marked dirty, leaving stale outline/scrollbar pixels baked into the enlarged window. Mirror the fix already used by toggle-size (toggle-action.c): force the old footprint's furniture dirty when growing, then invalidate furniture at the new position too. Also move the resize regression test's interior probe point below the titlebar, since the titlebar itself is legitimately redrawn now. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/test/wuss-test.c | 4 ++-- libraries/wuss/window/resize.c | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 982d93c6..d22ddcf7 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -2071,8 +2071,8 @@ result_t wuss_test(const char *resources) goto Failure; wuss_window_get_visible_bounds(win_m2, &before2); - interior_x = before2.x0 + 2; /* well inside the untouched top-left corner */ - interior_y = before2.y0 + 2; + interior_x = before2.x0 + 2; /* inside the untouched left edge */ + interior_y = before2.y0 + 25; /* below the titlebar, in plain content */ rc = wuss_window_resize(win_m2, 80, 80); /* grow */ if (rc != result_OK) diff --git a/libraries/wuss/window/resize.c b/libraries/wuss/window/resize.c index aba0a521..807f1d6c 100644 --- a/libraries/wuss/window/resize.c +++ b/libraries/wuss/window/resize.c @@ -56,5 +56,15 @@ result_t wuss_window_resize(wuss_window_t *window, int width, int height) invalidate_grown_or_shrunk(window, &before, &window->visible); invalidate_grown_or_shrunk(window, &window->visible, &before); + /* Growing strands old furniture (e.g. the old outline/scrollbar edge) + * inside what's now interior content -- a region both calls above treat + * as already-valid and so never repaint. Force its old position dirty + * too. Shrinking needs no such help: old furniture positions only ever + * land outside the new, smaller box, already covered above. */ + if (window->visible.x1 - window->visible.x0 > before.x1 - before.x0 || + window->visible.y1 - window->visible.y0 > before.y1 - before.y0) + wuss__furniture_invalidate_for(window, &before); + wuss__furniture_invalidate(window); + return result_OK; } From d2a0a0589a5fc2981596343f3f8bc77c6d38da67 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Thu, 27 Aug 2026 14:32:21 +0100 Subject: [PATCH 014/287] fix(wuss): honour wuss_WINDOW_NO_TOGGLE_BLIT in resize The grown/shrunk-sliver optimization assumes a task's content is just anchored positions plus furniture, so the old/new overlap needs no repaint. That's false for a task like the palette swatch grid, which lays itself out across the whole window -- exactly the case wuss_WINDOW_NO_TOGGLE_BLIT already exists to flag for toggle-size, but resize.c never checked it, so such tasks got stale interior content on a plain resize. Fall back to full old-union-new invalidation when the flag is set. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/test/wuss-test.c | 49 +++++++++++++++++++++++++++++++++ libraries/wuss/window/resize.c | 45 ++++++++++++++++++++---------- 2 files changed, 79 insertions(+), 15 deletions(-) diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index d22ddcf7..5e11f948 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -2134,6 +2134,55 @@ result_t wuss_test(const char *resources) wuss_window_close(win_m2); } + printf("test: resizing a wuss_WINDOW_NO_TOGGLE_BLIT window redraws it fully\n"); + + { + test_task_t tc_nb2; + wuss_task_t delegate_nb2; + box_t box_nb2, before3, after3, region; + wuss_window_t *win_nb2; + int i, dirty_area, full_area; + + tc_nb2.redraw_count = 0; + tc_nb2.mouse_count = 0; + delegate_nb2.handle = test_handle; + delegate_nb2.task_data = &tc_nb2; + delegate_nb2.bg = wuss_NO_BACKGROUND; + + box_nb2.x0 = 0; box_nb2.y0 = 0; + box_nb2.x1 = 40; box_nb2.y1 = 40; + rc = wuss_window_create(wuss, &box_nb2, "NB2", wuss_WINDOW_NO_TOGGLE_BLIT, + &delegate_nb2, + box_nb2.x1 - box_nb2.x0, + box_nb2.y1 - box_nb2.y0, + &win_nb2); + if (rc != result_OK) + goto Failure; + + rc = wuss_redraw_dirty(wuss); /* flush the creation invalidation first */ + if (rc != result_OK) + goto Failure; + + wuss_window_get_visible_bounds(win_nb2, &before3); + + rc = wuss_window_resize(win_nb2, 80, 80); /* grow */ + if (rc != result_OK) + goto Failure; + + wuss_window_get_visible_bounds(win_nb2, &after3); + full_area = (after3.x1 - after3.x0) * (after3.y1 - after3.y0); + dirty_area = 0; + for (i = 0; i < wuss_get_dirty_count(wuss); i++) + { + wuss_get_dirty(wuss, i, ®ion); + dirty_area += (region.x1 - region.x0) * (region.y1 - region.y0); + } + if (dirty_area < full_area) + goto Failure; /* NO_TOGGLE_BLIT must fully redraw, not just the sliver */ + + wuss_window_close(win_nb2); + } + printf("test: destroy mid-drag then move doesn't crash\n"); tc_c.redraw_count = 0; diff --git a/libraries/wuss/window/resize.c b/libraries/wuss/window/resize.c index 807f1d6c..60fdfddb 100644 --- a/libraries/wuss/window/resize.c +++ b/libraries/wuss/window/resize.c @@ -49,22 +49,37 @@ result_t wuss_window_resize(wuss_window_t *window, int width, int height) wuss__notify_open(window); - /* The top-left corner is fixed, so any pixels within both the old and new - * footprint are unchanged and don't need repainting -- only the shrunk-away - * sliver (revealing whatever is now behind it) and the grown-into sliver - * (this window's own previously-undrawn content) do. */ - invalidate_grown_or_shrunk(window, &before, &window->visible); - invalidate_grown_or_shrunk(window, &window->visible, &before); + if (window->flags & wuss_WINDOW_NO_TOGGLE_BLIT) + { + /* This task's content isn't just anchored positions plus furniture -- + * it lays itself out across the whole window (e.g. a palette swatch + * grid), so the "unchanged interior" assumption below doesn't hold: + * every pixel of the new footprint needs redrawing, not just the + * grown/shrunk sliver. */ + box_t dirty; - /* Growing strands old furniture (e.g. the old outline/scrollbar edge) - * inside what's now interior content -- a region both calls above treat - * as already-valid and so never repaint. Force its old position dirty - * too. Shrinking needs no such help: old furniture positions only ever - * land outside the new, smaller box, already covered above. */ - if (window->visible.x1 - window->visible.x0 > before.x1 - before.x0 || - window->visible.y1 - window->visible.y0 > before.y1 - before.y0) - wuss__furniture_invalidate_for(window, &before); - wuss__furniture_invalidate(window); + box_union(&before, &window->visible, &dirty); + wuss__invalidate_clipped(window, &dirty); + } + else + { + /* The top-left corner is fixed, so any pixels within both the old and + * new footprint are unchanged and don't need repainting -- only the + * shrunk-away sliver (revealing whatever is now behind it) and the + * grown-into sliver (this window's own previously-undrawn content) do. */ + invalidate_grown_or_shrunk(window, &before, &window->visible); + invalidate_grown_or_shrunk(window, &window->visible, &before); + + /* Growing strands old furniture (e.g. the old outline/scrollbar edge) + * inside what's now interior content -- a region both calls above treat + * as already-valid and so never repaint. Force its old position dirty + * too. Shrinking needs no such help: old furniture positions only ever + * land outside the new, smaller box, already covered above. */ + if (window->visible.x1 - window->visible.x0 > before.x1 - before.x0 || + window->visible.y1 - window->visible.y0 > before.y1 - before.y0) + wuss__furniture_invalidate_for(window, &before); + wuss__furniture_invalidate(window); + } return result_OK; } From dd0dca6a95147cceb329d0cd26e0bc2fb97b13bd Mon Sep 17 00:00:00 2001 From: David Thomas Date: Thu, 27 Aug 2026 14:45:25 +0100 Subject: [PATCH 015/287] fix(wuss): also check the destination footprint for occlusion on move wuss__occluded_above() only checked the window's old footprint before allowing the move blit fast path. screen_copy_rect() is a raw pixel copy with no awareness of window ownership or z-order, so when a window's NEW footprint landed under an occluder (its old footprint having been clear), the blit pasted stale pixels straight over that occluder with no invalidation to fix it up afterwards -- corrupting it until something unrelated redrew it. Check both the old and new footprint against occluders above the window in z-order before allowing the blit. Add a regression test dragging a clear window onto a topmost occluder and asserting the exposed sliver of the destination gets queued dirty by the fallback clipped redraw. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/test/wuss-test.c | 88 +++++++++++++++++++++++++++++++++ libraries/wuss/window/move.c | 28 ++++++----- 2 files changed, 104 insertions(+), 12 deletions(-) diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 5e11f948..c9c6150c 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -2183,6 +2183,94 @@ result_t wuss_test(const char *resources) wuss_window_close(win_nb2); } + printf("test: dragging a clear window onto an occluder redraws the occluder\n"); + + { + test_task_t tc_n, tc_o; + wuss_task_t delegate_n, delegate_o; + box_t box_n, box_o, exposed, region; + wuss_window_t *win_n, *win_o; + int i, exposed_dirty; + + tc_n.redraw_count = 0; + tc_n.mouse_count = 0; + delegate_n.handle = test_handle; + delegate_n.task_data = &tc_n; + delegate_n.bg = wuss_NO_BACKGROUND; + + box_n.x0 = 0; box_n.y0 = 140; /* clear of any occluder to start */ + box_n.x1 = 60; box_n.y1 = 170; + rc = wuss_window_create(wuss, &box_n, "N", + wuss_WINDOW_NO_BACK | wuss_WINDOW_NO_TOGGLE_SIZE | + wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | + wuss_WINDOW_NO_RESIZE, + &delegate_n, + box_n.x1 - box_n.x0, + box_n.y1 - box_n.y0, + &win_n); + if (rc != result_OK) + goto Failure; + + tc_o.redraw_count = 0; + tc_o.mouse_count = 0; + delegate_o.handle = test_handle; + delegate_o.task_data = &tc_o; + delegate_o.bg = wuss_NO_BACKGROUND; + + box_o.x0 = 90; box_o.y0 = 140; /* N will be dragged partly on top of O */ + box_o.x1 = 130; box_o.y1 = 180; + rc = wuss_window_create(wuss, &box_o, "O", + wuss_WINDOW_NO_BACK | wuss_WINDOW_NO_TOGGLE_SIZE | + wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | + wuss_WINDOW_NO_RESIZE, + &delegate_o, + box_o.x1 - box_o.x0, + box_o.y1 - box_o.y0, + &win_o); + if (rc != result_OK) + goto Failure; + + /* O is created after N, so O is topmost -- N's destination footprint + * will overlap an occluder above it in z-order. */ + + rc = wuss_redraw_dirty(wuss); /* flush both creations first */ + if (rc != result_OK) + goto Failure; + + wuss_window_get_visible_bounds(win_n, &visible); + + /* Move N far enough right that its new footprint lands partly under O + * (which stays wholly untouched), while its old footprint started + * entirely clear of O. */ + wuss_window_move(win_n, (point_t) { visible.x0 + 80, visible.y0 + 10 }); + + wuss_window_get_visible_bounds(win_n, &visible); + exposed.x0 = visible.x0; exposed.y0 = visible.y0; + exposed.x1 = box_o.x0; exposed.y1 = visible.y1; /* N's part left of O */ + + /* The part of N's new footprint not covered by O must be queued dirty + * right away, from the fallback clipped redraw -- if the blit fast path + * had wrongly fired instead (because it only checked the old footprint + * for occlusion), N's stale pixels would have been pasted straight over + * O with no invalidation of this exposed sliver at all. */ + exposed_dirty = 0; + for (i = 0; i < wuss_get_dirty_count(wuss); i++) + { + wuss_get_dirty(wuss, i, ®ion); + if (box_intersects(®ion, &exposed)) + exposed_dirty = 1; + } + if (!exposed_dirty) + goto Failure; + + rc = wuss_redraw_dirty(wuss); + if (rc != result_OK) + goto Failure; + + wuss_window_close(win_n); + wuss_window_close(win_o); + } + printf("test: destroy mid-drag then move doesn't crash\n"); tc_c.redraw_count = 0; diff --git a/libraries/wuss/window/move.c b/libraries/wuss/window/move.c index e2e7ecb1..2c5741aa 100644 --- a/libraries/wuss/window/move.c +++ b/libraries/wuss/window/move.c @@ -2,16 +2,18 @@ #include "../impl.h" -/* true if any window above "window" in z-order overlaps "before" -- if - * so, "before" isn't purely this window's own rendering and the blit - * fast path in wuss_window_move() must not be used */ -static int wuss__occluded_above(wuss_window_t *window, const box_t *before) +/* true if any window above "window" in z-order overlaps "box" -- if so, + * "box" isn't purely this window's own rendering (if it's the old + * footprint) or would have this window's stale pixels blitted over an + * occluder (if it's the new footprint), so the blit fast path in + * wuss_window_move() must not be used for either case */ +static int wuss__occluded_above(wuss_window_t *window, const box_t *box) { list_t *e; for (e = window->wuss->z_order.next; e != &window->link; e = e->next) { - if (box_intersects(&((wuss_window_t *) e)->visible, before)) + if (box_intersects(&((wuss_window_t *) e)->visible, box)) return 1; } @@ -40,16 +42,18 @@ void wuss_window_move(wuss_window_t *window, point_t p) wuss__notify_open(window); if (!wuss__occluded_above(window, &before) && + !wuss__occluded_above(window, &window->visible) && screen_copy_rect(window->wuss->scr, &before, (point_t) { window->visible.x0, window->visible.y0 }, &copied)) { - /* Nothing above this window overlaps its old footprint, and the screen - * format supports the blit: every pixel of "before" is genuinely this - * window's own rendering (nothing above it to have punched holes in - * it), so sliding those pixels to the new position is exactly as - * correct as asking the task to redraw there, but far cheaper -- only - * the vacated sliver behind the old position still needs an actual - * repaint. */ + /* Nothing above this window overlaps its old OR new footprint, and the + * screen format supports the blit: every pixel of "before" is genuinely + * this window's own rendering (nothing above it to have punched holes in + * it), and nothing above it at the destination would have its rendering + * clobbered by the raw pixel copy, so sliding those pixels to the new + * position is exactly as correct as asking the task to redraw there, but + * far cheaper -- only the vacated sliver behind the old position still + * needs an actual repaint. */ wuss__invalidate_minus(window->wuss, &before, &window->visible); /* "copied" can be smaller than the new footprint if either end of the From feb4c71c3eda5ea4cd1202ec9a4752d3aa1b0c3b Mon Sep 17 00:00:00 2001 From: David Thomas Date: Thu, 27 Aug 2026 14:53:49 +0100 Subject: [PATCH 016/287] fix(wuss): keep the move blit even when the destination is occluded dd0dca6 fixed the destination-occlusion corruption bug by declining the blit fast path entirely whenever either the old or new footprint overlapped an occluder above the window in z-order. That's correct but overly broad: it falls back to a full clipped redraw of the whole moved footprint even when only a small part of the new position is covered, discarding the blit's exposed-sliver savings for no reason -- the exposed part is still purely this window's own content. Only the old footprint still needs to be occlusion-free (its pixels must genuinely be this window's own rendering to be worth sliding). The new footprint can still land under an occluder: after the blit, call wuss__invalidate_uncovered() to force exactly the overlapped part back to its rightful owner (the occluder now painted over), leaving the window's own exposed content untouched since the blit already placed it correctly. Rework the regression test from dd0dca6 to assert both halves: the occluded overlap is forced dirty (fixing the corruption), and the exposed sliver is NOT (proving the blit is still used, not a full fallback redraw). Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/test/wuss-test.c | 38 +++++++++++++++++++++++---------- libraries/wuss/window/move.c | 31 ++++++++++++++++----------- 2 files changed, 45 insertions(+), 24 deletions(-) diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index c9c6150c..bce0469f 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -2188,9 +2188,9 @@ result_t wuss_test(const char *resources) { test_task_t tc_n, tc_o; wuss_task_t delegate_n, delegate_o; - box_t box_n, box_o, exposed, region; + box_t box_n, box_o, visible_o, exposed, occluded, region; wuss_window_t *win_n, *win_o; - int i, exposed_dirty; + int i, exposed_dirty, occluded_dirty; tc_n.redraw_count = 0; tc_n.mouse_count = 0; @@ -2245,14 +2245,30 @@ result_t wuss_test(const char *resources) wuss_window_move(win_n, (point_t) { visible.x0 + 80, visible.y0 + 10 }); wuss_window_get_visible_bounds(win_n, &visible); - exposed.x0 = visible.x0; exposed.y0 = visible.y0; - exposed.x1 = box_o.x0; exposed.y1 = visible.y1; /* N's part left of O */ - - /* The part of N's new footprint not covered by O must be queued dirty - * right away, from the fallback clipped redraw -- if the blit fast path - * had wrongly fired instead (because it only checked the old footprint - * for occlusion), N's stale pixels would have been pasted straight over - * O with no invalidation of this exposed sliver at all. */ + wuss_window_get_visible_bounds(win_o, &visible_o); + exposed.x0 = visible.x0; exposed.y0 = visible.y0; + exposed.x1 = visible_o.x0; exposed.y1 = visible.y1; /* N's part left of O */ + box_intersection(&visible, &visible_o, &occluded); /* N's part under O */ + + /* The part of N's new footprint that lands under O must be forced dirty + * (unclipped, so the redraw picks up O as the topmost owner there) -- + * the blit is a raw pixel copy that just pasted N's stale pixels + * straight over O's rendering, and nothing else would ever ask O to + * repair that. */ + occluded_dirty = 0; + for (i = 0; i < wuss_get_dirty_count(wuss); i++) + { + wuss_get_dirty(wuss, i, ®ion); + if (box_intersects(®ion, &occluded)) + occluded_dirty = 1; + } + if (!occluded_dirty) + goto Failure; + + /* The exposed part of N's new footprint, not under any occluder, must + * NOT be queued dirty -- it was already moved there correctly by the + * blit, so redrawing it too would be exactly the "repaint what could + * have been left in place" waste this fast path exists to avoid. */ exposed_dirty = 0; for (i = 0; i < wuss_get_dirty_count(wuss); i++) { @@ -2260,7 +2276,7 @@ result_t wuss_test(const char *resources) if (box_intersects(®ion, &exposed)) exposed_dirty = 1; } - if (!exposed_dirty) + if (exposed_dirty) goto Failure; rc = wuss_redraw_dirty(wuss); diff --git a/libraries/wuss/window/move.c b/libraries/wuss/window/move.c index 2c5741aa..886d3b4f 100644 --- a/libraries/wuss/window/move.c +++ b/libraries/wuss/window/move.c @@ -3,10 +3,9 @@ #include "../impl.h" /* true if any window above "window" in z-order overlaps "box" -- if so, - * "box" isn't purely this window's own rendering (if it's the old - * footprint) or would have this window's stale pixels blitted over an - * occluder (if it's the new footprint), so the blit fast path in - * wuss_window_move() must not be used for either case */ + * "box" isn't purely this window's own rendering, so blitting it as the + * source of a move would slide stale/wrong pixels rather than this + * window's own content */ static int wuss__occluded_above(wuss_window_t *window, const box_t *box) { list_t *e; @@ -42,18 +41,16 @@ void wuss_window_move(wuss_window_t *window, point_t p) wuss__notify_open(window); if (!wuss__occluded_above(window, &before) && - !wuss__occluded_above(window, &window->visible) && screen_copy_rect(window->wuss->scr, &before, (point_t) { window->visible.x0, window->visible.y0 }, &copied)) { - /* Nothing above this window overlaps its old OR new footprint, and the - * screen format supports the blit: every pixel of "before" is genuinely - * this window's own rendering (nothing above it to have punched holes in - * it), and nothing above it at the destination would have its rendering - * clobbered by the raw pixel copy, so sliding those pixels to the new - * position is exactly as correct as asking the task to redraw there, but - * far cheaper -- only the vacated sliver behind the old position still - * needs an actual repaint. */ + /* Nothing above this window overlaps its old footprint, and the screen + * format supports the blit: every pixel of "before" is genuinely this + * window's own rendering (nothing above it to have punched holes in + * it), so sliding those pixels to the new position is exactly as + * correct as asking the task to redraw there, but far cheaper -- only + * the vacated sliver behind the old position still needs an actual + * repaint. */ wuss__invalidate_minus(window->wuss, &before, &window->visible); /* "copied" can be smaller than the new footprint if either end of the @@ -61,6 +58,14 @@ void wuss_window_move(wuss_window_t *window, point_t p) * off-screen): the leftover part has no valid source pixels behind it, * so it needs a real repaint too, not just the vacated sliver above. */ wuss__invalidate_minus(window->wuss, &window->visible, &copied); + + /* The blit is a raw pixel copy: if the new footprint lands under a + * window above this one in z-order, it just pasted this window's stale + * pixels straight over that occluder's rendering. Force exactly that + * overlap dirty (unclipped, so the redraw picks up the occluder as the + * topmost owner there) to repair it -- this window's own newly-covered + * area needs no such fix, since it's rightfully hidden anyway. */ + wuss__invalidate_uncovered(window); } else { From c38a17aa5be2c849dd756b63d5d0ea9bf3804c79 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Thu, 27 Aug 2026 16:03:24 +0100 Subject: [PATCH 017/287] feat(wuss): blit the clean part of a partially-occluded move source wuss_window_move() used to require a window's whole old footprint be occlusion-free before blitting; any overlap at all fell back to a full clipped redraw of the moved footprint. Split "before" into its clean (unoccluded) and hidden pieces via wuss__clip_to_visible(), blit each clean piece individually to its translated destination, and only genuinely repaint the translated hidden pieces plus any destination occlusion. Exposes subtract_boxes as wuss__subtract_boxes for reuse here. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/impl.h | 8 ++ libraries/wuss/test/wuss-test.c | 103 ++++++++++++++++++++++++++ libraries/wuss/window/invalidate.c | 10 +-- libraries/wuss/window/move.c | 113 ++++++++++++++++++----------- 4 files changed, 186 insertions(+), 48 deletions(-) diff --git a/libraries/wuss/impl.h b/libraries/wuss/impl.h index 531766f6..d9e6479e 100644 --- a/libraries/wuss/impl.h +++ b/libraries/wuss/impl.h @@ -74,6 +74,14 @@ int wuss__clip_to_visible(wuss_window_t *window, const box_t *box, box_t *out); +/* Subtract each of "cuts" (an array of "ncuts" boxes) from "whole", writing + * the surviving pieces to "out" (capacity WUSS_MAX_INVALIDATE_PIECES) and + * returning their count. */ +int wuss__subtract_boxes(const box_t *whole, + const box_t *cuts, + int ncuts, + box_t *out); + /* 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/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index bce0469f..df4ebc5a 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -2287,6 +2287,109 @@ result_t wuss_test(const char *resources) wuss_window_close(win_o); } + printf("test: moving a partly-occluded window blits its clean part and only repaints the occluded part\n"); + + { + test_task_t tc_a, tc_b; + wuss_task_t delegate_a, delegate_b; + box_t box_a, box_b, visible_a, visible_b_before; + box_t clean_new, hidden_new, region; + wuss_window_t *win_a, *win_b; + int i, dx, clean_dirty, hidden_dirty; + + tc_b.redraw_count = 0; + tc_b.mouse_count = 0; + delegate_b.handle = test_handle; + delegate_b.task_data = &tc_b; + delegate_b.bg = wuss_NO_BACKGROUND; + + box_b.x0 = 20; box_b.y0 = 10; /* left half will sit under A */ + box_b.x1 = 80; box_b.y1 = 50; + rc = wuss_window_create(wuss, &box_b, "B", + wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | + wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | + wuss_WINDOW_NO_RESIZE, + &delegate_b, + box_b.x1 - box_b.x0, + box_b.y1 - box_b.y0, + &win_b); + if (rc != result_OK) + goto Failure; + + tc_a.redraw_count = 0; + tc_a.mouse_count = 0; + delegate_a.handle = test_handle; + delegate_a.task_data = &tc_a; + delegate_a.bg = wuss_NO_BACKGROUND; + + box_a.x0 = 0; box_a.y0 = 0; /* created after B, so A is topmost */ + box_a.x1 = 40; box_a.y1 = 100; + rc = wuss_window_create(wuss, &box_a, "A", + wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | + wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | + wuss_WINDOW_NO_RESIZE, + &delegate_a, + box_a.x1 - box_a.x0, + box_a.y1 - box_a.y0, + &win_a); + if (rc != result_OK) + goto Failure; + + /* B's old footprint (x:20-80,y:10-50) is split by A (x:0-40) into a + * hidden strip (x:20-40, under A) and a clean strip (x:40-80, exposed). */ + + rc = wuss_redraw_dirty(wuss); /* flush both creations first */ + if (rc != result_OK) + goto Failure; + + wuss_window_get_visible_bounds(win_b, &visible_b_before); + wuss_window_get_visible_bounds(win_a, &visible_a); + + /* Move B far enough right that its whole new footprint clears A. */ + dx = 60; + wuss_window_move(win_b, (point_t) { visible_b_before.x0 + dx, + visible_b_before.y0 }); + + /* The clean strip (previously exposed, genuinely B's own pixels) lands + * at its translated destination and must have been blitted there, not + * repainted. */ + clean_new.x0 = 40 + dx; clean_new.y0 = 10; + clean_new.x1 = 80 + dx; clean_new.y1 = 50; + + /* The hidden strip (previously under A, never B's valid rendering) has + * no valid source pixels, so its translated destination must be a real + * repaint. */ + hidden_new.x0 = 20 + dx; hidden_new.y0 = 10; + hidden_new.x1 = 40 + dx; hidden_new.y1 = 50; + + hidden_dirty = 0; + for (i = 0; i < wuss_get_dirty_count(wuss); i++) + { + wuss_get_dirty(wuss, i, ®ion); + if (box_intersects(®ion, &hidden_new)) + hidden_dirty = 1; + } + if (!hidden_dirty) + goto Failure; + + clean_dirty = 0; + for (i = 0; i < wuss_get_dirty_count(wuss); i++) + { + wuss_get_dirty(wuss, i, ®ion); + if (box_intersects(®ion, &clean_new)) + clean_dirty = 1; + } + if (clean_dirty) + goto Failure; + + rc = wuss_redraw_dirty(wuss); + if (rc != result_OK) + goto Failure; + + wuss_window_close(win_b); + wuss_window_close(win_a); + } + printf("test: destroy mid-drag then move doesn't crash\n"); tc_c.redraw_count = 0; diff --git a/libraries/wuss/window/invalidate.c b/libraries/wuss/window/invalidate.c index 9ed97811..accaa784 100644 --- a/libraries/wuss/window/invalidate.c +++ b/libraries/wuss/window/invalidate.c @@ -98,10 +98,10 @@ int wuss__clip_to_visible(wuss_window_t *window, const box_t *box, box_t *out) /* Subtract each of "cuts" (an array of "ncuts" boxes) from "whole", writing * the surviving pieces to "out" (capacity WUSS_MAX_INVALIDATE_PIECES) and * returning their count. */ -static int subtract_boxes(const box_t *whole, - const box_t *cuts, - int ncuts, - box_t *out) +int wuss__subtract_boxes(const box_t *whole, + const box_t *cuts, + int ncuts, + box_t *out) { box_t scratch[WUSS_MAX_INVALIDATE_PIECES]; box_t *cur, *nxt, *tmp; @@ -162,7 +162,7 @@ void wuss__invalidate_uncovered(wuss_window_t *window) int nvisible, nhidden, i; nvisible = wuss__clip_to_visible(window, &window->visible, visible); - nhidden = subtract_boxes(&window->visible, visible, nvisible, hidden); + nhidden = wuss__subtract_boxes(&window->visible, visible, nvisible, hidden); for (i = 0; i < nhidden; i++) wuss_invalidate(window->wuss, &hidden[i]); diff --git a/libraries/wuss/window/move.c b/libraries/wuss/window/move.c index 886d3b4f..fc12e890 100644 --- a/libraries/wuss/window/move.c +++ b/libraries/wuss/window/move.c @@ -2,21 +2,13 @@ #include "../impl.h" -/* true if any window above "window" in z-order overlaps "box" -- if so, - * "box" isn't purely this window's own rendering, so blitting it as the - * source of a move would slide stale/wrong pixels rather than this - * window's own content */ -static int wuss__occluded_above(wuss_window_t *window, const box_t *box) +/* Translate "box" by (dx, dy) into "out". */ +static void translate_box(const box_t *box, int dx, int dy, box_t *out) { - list_t *e; - - for (e = window->wuss->z_order.next; e != &window->link; e = e->next) - { - if (box_intersects(&((wuss_window_t *) e)->visible, box)) - return 1; - } - - return 0; + out->x0 = box->x0 + dx; + out->y0 = box->y0 + dy; + out->x1 = box->x1 + dx; + out->y1 = box->y1 + dy; } /* p is the window's content top-left; the furniture offset (outline plus @@ -24,8 +16,12 @@ static int wuss__occluded_above(wuss_window_t *window, const box_t *box) * follows it */ void wuss_window_move(wuss_window_t *window, point_t p) { - int width, height, outline_px, titlebar_height; - box_t before, dirty, copied; + box_t clean[WUSS_MAX_INVALIDATE_PIECES]; + box_t dest[WUSS_MAX_INVALIDATE_PIECES]; + int width, height, outline_px, titlebar_height; + int dx, dy, nclean, i; + box_t before, dirty, copied; + int blit_failed; width = window->visible.x1 - window->visible.x0; height = window->visible.y1 - window->visible.y0; @@ -33,6 +29,12 @@ void wuss_window_move(wuss_window_t *window, point_t p) titlebar_height = wuss__titlebar_height(window); before = window->visible; + /* The clean (non-occluded) pieces of "before" are genuinely this + * window's own rendering; whatever isn't clean is hidden behind some + * other window and has no valid pixels of this window's content to + * slide. Computed against the current z-order, before the move. */ + nclean = wuss__clip_to_visible(window, &before, clean); + window->visible.x0 = p.x - outline_px; window->visible.y0 = p.y - outline_px - titlebar_height; window->visible.x1 = window->visible.x0 + width; @@ -40,38 +42,63 @@ void wuss_window_move(wuss_window_t *window, point_t p) wuss__notify_open(window); - if (!wuss__occluded_above(window, &before) && - screen_copy_rect(window->wuss->scr, &before, - (point_t) { window->visible.x0, window->visible.y0 }, &copied)) + dx = window->visible.x0 - before.x0; + dy = window->visible.y0 - before.y0; + + blit_failed = 0; + for (i = 0; i < nclean; i++) { - /* Nothing above this window overlaps its old footprint, and the screen - * format supports the blit: every pixel of "before" is genuinely this - * window's own rendering (nothing above it to have punched holes in - * it), so sliding those pixels to the new position is exactly as - * correct as asking the task to redraw there, but far cheaper -- only - * the vacated sliver behind the old position still needs an actual - * repaint. */ - wuss__invalidate_minus(window->wuss, &before, &window->visible); - - /* "copied" can be smaller than the new footprint if either end of the - * move was partly off-screen (e.g. dragging back on-screen from - * off-screen): the leftover part has no valid source pixels behind it, - * so it needs a real repaint too, not just the vacated sliver above. */ - wuss__invalidate_minus(window->wuss, &window->visible, &copied); - - /* The blit is a raw pixel copy: if the new footprint lands under a - * window above this one in z-order, it just pasted this window's stale - * pixels straight over that occluder's rendering. Force exactly that - * overlap dirty (unclipped, so the redraw picks up the occluder as the - * topmost owner there) to repair it -- this window's own newly-covered - * area needs no such fix, since it's rightfully hidden anyway. */ + translate_box(&clean[i], dx, dy, &dest[i]); + + if (!screen_copy_rect(window->wuss->scr, &clean[i], + (point_t) { dest[i].x0, dest[i].y0 }, &copied)) + { + /* The screen format doesn't support the blit at all (e.g. paletted): + * this fails identically for every piece, so bail out before any + * blits have happened rather than leaving a half-blitted window. */ + blit_failed = 1; + break; + } + + /* Each clean piece is, by construction, clear of any occluder at its + * old position, so the vacated sliver left behind by sliding it to + * "dest[i]" is safe to invalidate raw, without re-checking occlusion. */ + wuss__invalidate_minus(window->wuss, &clean[i], &dest[i]); + + /* "copied" can be smaller than "dest[i]" if the move was partly + * off-screen: the leftover part has no valid source pixels behind it, + * so it needs a real repaint too. */ + wuss__invalidate_minus(window->wuss, &dest[i], &copied); + } + + if (nclean > 0 && !blit_failed) + { + box_t hidden[WUSS_MAX_INVALIDATE_PIECES]; + int nhidden; + + /* Whatever of "before" wasn't clean has no valid source pixels: its + * translated destination needs a genuine repaint, clipped against + * whatever's above this window there now. */ + nhidden = wuss__subtract_boxes(&before, clean, nclean, hidden); + for (i = 0; i < nhidden; i++) + { + box_t hidden_dest; + + translate_box(&hidden[i], dx, dy, &hidden_dest); + wuss__invalidate_clipped(window, &hidden_dest); + } + + /* Each blit is a raw pixel copy: if a clean piece's destination lands + * under a window above this one in z-order, it just pasted this + * window's stale pixels straight over that occluder's rendering. Force + * exactly that overlap dirty (unclipped, so the redraw picks up the + * occluder as the topmost owner there) to repair it. */ wuss__invalidate_uncovered(window); } else { - /* Something above overlaps the old footprint, or the blit was declined - * (e.g. paletted screen): fall back to a normal clipped redraw of the - * whole moved footprint. */ + /* Nothing of "before" was clean, or the blit was declined: fall back + * to a normal clipped redraw of the whole moved footprint. */ box_union(&before, &window->visible, &dirty); wuss__invalidate_clipped(window, &dirty); } From 34b7d6b903aad7c8e3be376efa667fa15c7c44a5 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Thu, 27 Aug 2026 16:38:33 +0100 Subject: [PATCH 018/287] fix(wuss): scope move blit repair to actually-corrupted pieces, guard against inter-piece clobber wuss_window_move() forced a window's whole current occluded footprint dirty after any blit, even parts a blit never touched (never-blitted occluded source pieces need no such repair) -- unnecessarily redrawing an untouched occluder above it. Sequential per-piece screen_copy_rect() calls could also clobber each other: one piece's destination landing on another still-unread piece's source (e.g. dragging past a thin mid-window occluding band), producing corrupted "bands" in the moved window's own content. Detect this before blitting and fall back to a full clipped redraw rather than risk it. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/test/wuss-test.c | 178 ++++++++++++++++++++++++++++++++ libraries/wuss/window/move.c | 58 +++++++++-- 2 files changed, 225 insertions(+), 11 deletions(-) diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index df4ebc5a..a1701e07 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -2390,6 +2390,184 @@ result_t wuss_test(const char *resources) wuss_window_close(win_a); } + printf("test: moving a window whose occluded piece was never blitted doesn't redraw the occluder\n"); + + { + test_task_t tc_a, tc_b; + wuss_task_t delegate_a, delegate_b; + box_t box_a, box_b, visible_a, visible_b_before; + box_t region; + wuss_window_t *win_a, *win_b; + int i, occluder_dirty; + + tc_b.redraw_count = 0; + tc_b.mouse_count = 0; + delegate_b.handle = test_handle; + delegate_b.task_data = &tc_b; + delegate_b.bg = wuss_NO_BACKGROUND; + + box_b.x0 = 0; box_b.y0 = 0; /* right part sits under A throughout */ + box_b.x1 = 60; box_b.y1 = 40; + rc = wuss_window_create(wuss, &box_b, "B", + wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | + wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | + wuss_WINDOW_NO_RESIZE, + &delegate_b, + box_b.x1 - box_b.x0, + box_b.y1 - box_b.y0, + &win_b); + if (rc != result_OK) + goto Failure; + + tc_a.redraw_count = 0; + tc_a.mouse_count = 0; + delegate_a.handle = test_handle; + delegate_a.task_data = &tc_a; + delegate_a.bg = wuss_NO_BACKGROUND; + + box_a.x0 = 40; box_a.y0 = 0; /* created after B, so A is topmost */ + box_a.x1 = 100; box_a.y1 = 40; + rc = wuss_window_create(wuss, &box_a, "A", + wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | + wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | + wuss_WINDOW_NO_RESIZE, + &delegate_a, + box_a.x1 - box_a.x0, + box_a.y1 - box_a.y0, + &win_a); + if (rc != result_OK) + goto Failure; + + rc = wuss_redraw_dirty(wuss); /* flush both creations first */ + if (rc != result_OK) + goto Failure; + + wuss_window_get_visible_bounds(win_b, &visible_b_before); + wuss_window_get_visible_bounds(win_a, &visible_a); + + /* Move B straight down: its clean piece (x:0-40) and hidden piece + * (x:40-60, under A) both stay clear of / under A exactly as before -- + * nothing about A's own pixels is ever touched by the blit, so A must + * not be forced to redraw. */ + wuss_window_move(win_b, (point_t) { visible_b_before.x0, + visible_b_before.y0 + 5 }); + + occluder_dirty = 0; + for (i = 0; i < wuss_get_dirty_count(wuss); i++) + { + wuss_get_dirty(wuss, i, ®ion); + if (box_intersects(®ion, &visible_a)) + occluder_dirty = 1; + } + if (occluder_dirty) + goto Failure; + + rc = wuss_redraw_dirty(wuss); + if (rc != result_OK) + goto Failure; + + wuss_window_close(win_b); + wuss_window_close(win_a); + } + + printf("test: moving a window split by a mid-band occluder past the gap between bands falls back safely instead of corrupting either band\n"); + + { + test_task_t tc_a, tc_b; + wuss_task_t delegate_a, delegate_b; + box_t box_a, box_b, visible_b_before; + box_t top_clear, bottom_clear, region; + wuss_window_t *win_a, *win_b; + int i, top_dirty, bottom_dirty; + + tc_b.redraw_count = 0; + tc_b.mouse_count = 0; + delegate_b.handle = test_handle; + delegate_b.task_data = &tc_b; + delegate_b.bg = wuss_NO_BACKGROUND; + + box_b.x0 = 0; box_b.y0 = 0; /* middle band sits under A */ + box_b.x1 = 60; box_b.y1 = 60; + rc = wuss_window_create(wuss, &box_b, "B", + wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | + wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | + wuss_WINDOW_NO_RESIZE, + &delegate_b, + box_b.x1 - box_b.x0, + box_b.y1 - box_b.y0, + &win_b); + if (rc != result_OK) + goto Failure; + + tc_a.redraw_count = 0; + tc_a.mouse_count = 0; + delegate_a.handle = test_handle; + delegate_a.task_data = &tc_a; + delegate_a.bg = wuss_NO_BACKGROUND; + + box_a.x0 = 0; box_a.y0 = 20; /* created after B, so A is topmost */ + box_a.x1 = 60; box_a.y1 = 40; + rc = wuss_window_create(wuss, &box_a, "A", + wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | + wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | + wuss_WINDOW_NO_RESIZE, + &delegate_a, + box_a.x1 - box_a.x0, + box_a.y1 - box_a.y0, + &win_a); + if (rc != result_OK) + goto Failure; + + /* B's old footprint (y:0-60) is split by A (y:20-40) into a top clean + * band (y:0-20), a hidden middle band (y:20-40) and a bottom clean band + * (y:40-60), each spanning the full width. */ + + rc = wuss_redraw_dirty(wuss); /* flush both creations first */ + if (rc != result_OK) + goto Failure; + + wuss_window_get_visible_bounds(win_b, &visible_b_before); + + /* Move B down by 25px, past the 20px gap between the two clean bands: + * the top band's destination (y:25-45) would land on the bottom band's + * still-unread old source (y:40-60), so no ordering of independent + * single-rect blits is safe. This must fall back to a full clipped + * redraw of the moved footprint rather than risk corrupting either + * band with stale, wrongly-shifted rows -- the reported "horizontal + * bands redrawn incorrectly" bug. */ + wuss_window_move(win_b, (point_t) { visible_b_before.x0, + visible_b_before.y0 + 25 }); + + /* New footprint is (0,25)-(60,85). Check the parts of each translated + * band that are clear of A (still at y:20-40): the top band's clear + * remainder (y:40-45) and the whole bottom band (y:65-85, entirely + * clear of A). A full fallback redraw must cover both in full -- a + * corrupting partial blit would instead leave one of them stale. */ + top_clear.x0 = 0; top_clear.y0 = 40; + top_clear.x1 = 60; top_clear.y1 = 45; + bottom_clear.x0 = 0; bottom_clear.y0 = 65; + bottom_clear.x1 = 60; bottom_clear.y1 = 85; + + top_dirty = bottom_dirty = 0; + for (i = 0; i < wuss_get_dirty_count(wuss); i++) + { + wuss_get_dirty(wuss, i, ®ion); + if (box_contains_box(&top_clear, ®ion)) + top_dirty = 1; + if (box_contains_box(&bottom_clear, ®ion)) + bottom_dirty = 1; + } + if (!top_dirty || !bottom_dirty) + goto Failure; + + rc = wuss_redraw_dirty(wuss); + if (rc != result_OK) + goto Failure; + + wuss_window_close(win_b); + wuss_window_close(win_a); + } + printf("test: destroy mid-drag then move doesn't crash\n"); tc_c.redraw_count = 0; diff --git a/libraries/wuss/window/move.c b/libraries/wuss/window/move.c index fc12e890..100dcfc3 100644 --- a/libraries/wuss/window/move.c +++ b/libraries/wuss/window/move.c @@ -14,6 +14,25 @@ static void translate_box(const box_t *box, int dx, int dy, box_t *out) /* p is the window's content top-left; the furniture offset (outline plus * any titlebar) is constant for a given window, so the footprint just * follows it */ +/* true if sliding any clean piece to its destination would overwrite a + * still-unread clean piece's old pixels: sequential single-rect blits (each + * a self-consistent memmove) can still corrupt each other when one piece's + * destination lands on another piece's still-unread source, which happens + * whenever the move delta is large enough relative to the gap between + * pieces (e.g. dragging fast past a thin occluding band) */ +static int wuss__pieces_would_clobber(const box_t *clean, const box_t *dest, + int n) +{ + int i, j; + + for (i = 0; i < n; i++) + for (j = 0; j < n; j++) + if (i != j && box_intersects(&dest[i], &clean[j])) + return 1; + + return 0; +} + void wuss_window_move(wuss_window_t *window, point_t p) { box_t clean[WUSS_MAX_INVALIDATE_PIECES]; @@ -45,11 +64,17 @@ void wuss_window_move(wuss_window_t *window, point_t p) dx = window->visible.x0 - before.x0; dy = window->visible.y0 - before.y0; - blit_failed = 0; for (i = 0; i < nclean; i++) - { translate_box(&clean[i], dx, dy, &dest[i]); + /* If sliding the pieces independently would let one clobber another's + * still-unread source, there is no ordering of these single-rect blits + * that's safe: fall back rather than risk corrupting this window's own + * pixels. */ + blit_failed = nclean == 0 || wuss__pieces_would_clobber(clean, dest, nclean); + + for (i = 0; i < nclean && !blit_failed; i++) + { if (!screen_copy_rect(window->wuss->scr, &clean[i], (point_t) { dest[i].x0, dest[i].y0 }, &copied)) { @@ -69,6 +94,23 @@ void wuss_window_move(wuss_window_t *window, point_t p) * off-screen: the leftover part has no valid source pixels behind it, * so it needs a real repaint too. */ wuss__invalidate_minus(window->wuss, &dest[i], &copied); + + /* The blit is a raw pixel copy: if this piece's destination lands under + * a window above this one in z-order, it just pasted this window's + * stale pixels straight over that occluder's rendering there -- and + * only there, so only that overlap (not the rest of this window's + * footprint) needs forcing dirty to repair it. */ + { + box_t visible_at_dest[WUSS_MAX_INVALIDATE_PIECES]; + box_t corrupted[WUSS_MAX_INVALIDATE_PIECES]; + int nvisible, ncorrupted, k; + + nvisible = wuss__clip_to_visible(window, &dest[i], visible_at_dest); + ncorrupted = wuss__subtract_boxes(&dest[i], visible_at_dest, nvisible, + corrupted); + for (k = 0; k < ncorrupted; k++) + wuss_invalidate(window->wuss, &corrupted[k]); + } } if (nclean > 0 && !blit_failed) @@ -87,18 +129,12 @@ void wuss_window_move(wuss_window_t *window, point_t p) translate_box(&hidden[i], dx, dy, &hidden_dest); wuss__invalidate_clipped(window, &hidden_dest); } - - /* Each blit is a raw pixel copy: if a clean piece's destination lands - * under a window above this one in z-order, it just pasted this - * window's stale pixels straight over that occluder's rendering. Force - * exactly that overlap dirty (unclipped, so the redraw picks up the - * occluder as the topmost owner there) to repair it. */ - wuss__invalidate_uncovered(window); } else { - /* Nothing of "before" was clean, or the blit was declined: fall back - * to a normal clipped redraw of the whole moved footprint. */ + /* Nothing of "before" was clean, the pieces would have clobbered each + * other, or the blit was declined: fall back to a normal clipped + * redraw of the whole moved footprint. */ box_union(&before, &window->visible, &dirty); wuss__invalidate_clipped(window, &dirty); } From 77dfb176704d2c4fd51a950d59a7bcd4f1de012c Mon Sep 17 00:00:00 2001 From: David Thomas Date: Thu, 27 Aug 2026 17:04:15 +0100 Subject: [PATCH 019/287] test(wuss): cover overlap-growth drag that falls back to a full clipped move Confirms wuss_window_move never marks an occluder dirty when the piecewise blit is declined (wuss__pieces_would_clobber): with no blit, nothing ever touches the occluder's pixels, so it must stay untouched even as the overlap between the dragged window and the occluder grows during the move. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/test/wuss-test.c | 85 +++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index a1701e07..aa64ac23 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -2568,6 +2568,91 @@ result_t wuss_test(const char *resources) wuss_window_close(win_a); } + printf("test: dragging a window deeper under an occluder never re-marks the occluder when the blit is declined\n"); + + { + test_task_t tc_a, tc_b; + wuss_task_t delegate_a, delegate_b; + box_t box_a, box_b, visible_b_before, visible_a, region; + wuss_window_t *win_a, *win_b; + int i, a_dirty; + + tc_b.redraw_count = 0; + tc_b.mouse_count = 0; + delegate_b.handle = test_handle; + delegate_b.task_data = &tc_b; + delegate_b.bg = wuss_NO_BACKGROUND; + + box_b.x0 = 80; box_b.y0 = 80; /* corner already under A */ + box_b.x1 = 140; box_b.y1 = 140; + rc = wuss_window_create(wuss, &box_b, "B", + wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | + wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | + wuss_WINDOW_NO_RESIZE, + &delegate_b, + box_b.x1 - box_b.x0, + box_b.y1 - box_b.y0, + &win_b); + if (rc != result_OK) + goto Failure; + + tc_a.redraw_count = 0; + tc_a.mouse_count = 0; + delegate_a.handle = test_handle; + delegate_a.task_data = &tc_a; + delegate_a.bg = wuss_NO_BACKGROUND; + + box_a.x0 = 0; box_a.y0 = 0; /* created after B, so A is topmost */ + box_a.x1 = 100; box_a.y1 = 100; + rc = wuss_window_create(wuss, &box_a, "A", + wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | + wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | + wuss_WINDOW_NO_RESIZE, + &delegate_a, + box_a.x1 - box_a.x0, + box_a.y1 - box_a.y0, + &win_a); + if (rc != result_OK) + goto Failure; + + /* B's old footprint (80,80)-(140,140) overlaps A (0,0)-(100,100) in its + * corner (80,80)-(100,100); the rest of B is split into an L-shaped + * clean region of two pieces. Dragging B up-left by (-15,-15) grows the + * overlap without ever fully hiding or fully clearing it -- the + * "further under, overlap changes" scenario -- and also makes one clean + * piece's destination land on the other's still-unread source, so + * wuss__pieces_would_clobber declines the blit entirely. With no blit, + * nothing ever touches A's pixels, so A must not be marked dirty at + * all, anywhere, including across the newly-grown part of the overlap. */ + + rc = wuss_redraw_dirty(wuss); /* flush both creations first */ + if (rc != result_OK) + goto Failure; + + wuss_window_get_visible_bounds(win_b, &visible_b_before); + wuss_window_get_visible_bounds(win_a, &visible_a); + + wuss_window_move(win_b, (point_t) { visible_b_before.x0 - 15, + visible_b_before.y0 - 15 }); + + a_dirty = 0; + for (i = 0; i < wuss_get_dirty_count(wuss); i++) + { + wuss_get_dirty(wuss, i, ®ion); + if (box_intersects(®ion, &visible_a)) + a_dirty = 1; + } + if (a_dirty) + goto Failure; /* A was never touched, so it must never be marked dirty */ + + rc = wuss_redraw_dirty(wuss); + if (rc != result_OK) + goto Failure; + + wuss_window_close(win_b); + wuss_window_close(win_a); + } + printf("test: destroy mid-drag then move doesn't crash\n"); tc_c.redraw_count = 0; From feffbe6859a3409945371c2790e1625f35e8ca3d Mon Sep 17 00:00:00 2001 From: David Thomas Date: Thu, 27 Aug 2026 18:23:22 +0100 Subject: [PATCH 020/287] fix(wuss): allow safe blit ordering instead of always falling back on clobber risk wuss__pieces_would_clobber declared any pairwise dest/clean overlap unsafe, forcing a full-footprint redraw even when a different blit order would avoid the conflict entirely -- routinely true for the common case of a two-piece L-shaped corner occlusion. Adjust-dragging a window behind a corner occluder was regressing into a full redraw. Replace it with wuss__order_pieces, which builds the "must happen before" dependency graph and topologically sorts it, only falling back when a genuine cycle leaves no safe order. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/test/wuss-test.c | 93 +++++++++++++++++++++------------ libraries/wuss/window/move.c | 87 +++++++++++++++++++++--------- 2 files changed, 122 insertions(+), 58 deletions(-) diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index aa64ac23..102f3096 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -2470,7 +2470,7 @@ result_t wuss_test(const char *resources) wuss_window_close(win_a); } - printf("test: moving a window split by a mid-band occluder past the gap between bands falls back safely instead of corrupting either band\n"); + printf("test: moving a window split by a mid-band occluder past the gap between bands blits both bands in a safe order\n"); { test_task_t tc_a, tc_b; @@ -2529,24 +2529,22 @@ result_t wuss_test(const char *resources) wuss_window_get_visible_bounds(win_b, &visible_b_before); /* Move B down by 25px, past the 20px gap between the two clean bands: - * the top band's destination (y:25-45) would land on the bottom band's - * still-unread old source (y:40-60), so no ordering of independent - * single-rect blits is safe. This must fall back to a full clipped - * redraw of the moved footprint rather than risk corrupting either - * band with stale, wrongly-shifted rows -- the reported "horizontal - * bands redrawn incorrectly" bug. */ + * the top band's destination (y:25-45) lands on the bottom band's + * still-unread old source (y:40-60). Blitted in the other order -- + * bottom band first (its destination y:65-85 doesn't touch the top + * band's source), then the top band -- both blits are safe, so this + * is not a genuine clobber cycle (translating disjoint pieces by the + * same offset never produces one: any conflict is consistently + * oriented by the direction of the move). Only the top band's + * destination overlap with A (y:25-40) needs forcing dirty, plus the + * translated hidden band (y:45-65, never had valid pixels). */ wuss_window_move(win_b, (point_t) { visible_b_before.x0, visible_b_before.y0 + 25 }); - /* New footprint is (0,25)-(60,85). Check the parts of each translated - * band that are clear of A (still at y:20-40): the top band's clear - * remainder (y:40-45) and the whole bottom band (y:65-85, entirely - * clear of A). A full fallback redraw must cover both in full -- a - * corrupting partial blit would instead leave one of them stale. */ - top_clear.x0 = 0; top_clear.y0 = 40; - top_clear.x1 = 60; top_clear.y1 = 45; - bottom_clear.x0 = 0; bottom_clear.y0 = 65; - bottom_clear.x1 = 60; bottom_clear.y1 = 85; + top_clear.x0 = 0; top_clear.y0 = 25; + top_clear.x1 = 60; top_clear.y1 = 40; + bottom_clear.x0 = 0; bottom_clear.y0 = 45; + bottom_clear.x1 = 60; bottom_clear.y1 = 65; top_dirty = bottom_dirty = 0; for (i = 0; i < wuss_get_dirty_count(wuss); i++) @@ -2560,6 +2558,22 @@ result_t wuss_test(const char *resources) if (!top_dirty || !bottom_dirty) goto Failure; + /* The blit must have actually happened, not fallen back: the part of + * B's new footprint that's clear of A and not the hidden band (e.g. + * the bottom band's new position, y:65-85) must not be dirtied. */ + { + box_t clean_after, dirty_check; + + clean_after.x0 = 0; clean_after.y0 = 65; + clean_after.x1 = 60; clean_after.y1 = 85; + for (i = 0; i < wuss_get_dirty_count(wuss); i++) + { + wuss_get_dirty(wuss, i, ®ion); + if (!box_intersection(®ion, &clean_after, &dirty_check)) + goto Failure; + } + } + rc = wuss_redraw_dirty(wuss); if (rc != result_OK) goto Failure; @@ -2568,14 +2582,14 @@ result_t wuss_test(const char *resources) wuss_window_close(win_a); } - printf("test: dragging a window deeper under an occluder never re-marks the occluder when the blit is declined\n"); + printf("test: dragging a window deeper under a corner occluder blits both L-shaped pieces in a safe order\n"); { test_task_t tc_a, tc_b; wuss_task_t delegate_a, delegate_b; box_t box_a, box_b, visible_b_before, visible_a, region; wuss_window_t *win_a, *win_b; - int i, a_dirty; + int i; tc_b.redraw_count = 0; tc_b.mouse_count = 0; @@ -2617,13 +2631,15 @@ result_t wuss_test(const char *resources) /* B's old footprint (80,80)-(140,140) overlaps A (0,0)-(100,100) in its * corner (80,80)-(100,100); the rest of B is split into an L-shaped - * clean region of two pieces. Dragging B up-left by (-15,-15) grows the - * overlap without ever fully hiding or fully clearing it -- the - * "further under, overlap changes" scenario -- and also makes one clean - * piece's destination land on the other's still-unread source, so - * wuss__pieces_would_clobber declines the blit entirely. With no blit, - * nothing ever touches A's pixels, so A must not be marked dirty at - * all, anywhere, including across the newly-grown part of the overlap. */ + * clean region of two pieces, one of whose destination lands on the + * other's still-unread source -- but blitting the other piece first + * avoids that entirely, so this must NOT fall back to a full clipped + * redraw (that was the "Adjust drag behind a corner fully redraws the + * window" regression). Dragging B up-left by (-15,-15) also grows the + * overlap with A without ever fully hiding or fully clearing it -- the + * blit legitimately re-marks part of the old overlap dirty too, since + * the raw copy pastes B's stale pixels over A's correct rendering + * there before it gets repainted. */ rc = wuss_redraw_dirty(wuss); /* flush both creations first */ if (rc != result_OK) @@ -2635,15 +2651,28 @@ result_t wuss_test(const char *resources) wuss_window_move(win_b, (point_t) { visible_b_before.x0 - 15, visible_b_before.y0 - 15 }); - a_dirty = 0; - for (i = 0; i < wuss_get_dirty_count(wuss); i++) + /* The blit must have actually happened, not fallen back: B's own + * footprint (outside A) must not be dirtied wholesale. */ { - wuss_get_dirty(wuss, i, ®ion); - if (box_intersects(®ion, &visible_a)) - a_dirty = 1; + box_t visible_b_after, whole_footprint, dirty_area_box; + int dirty_area, footprint_area; + + wuss_window_get_visible_bounds(win_b, &visible_b_after); + box_union(&visible_b_before, &visible_b_after, &whole_footprint); + + dirty_area = 0; + for (i = 0; i < wuss_get_dirty_count(wuss); i++) + { + wuss_get_dirty(wuss, i, ®ion); + if (!box_intersection(®ion, &whole_footprint, &dirty_area_box)) + dirty_area += (dirty_area_box.x1 - dirty_area_box.x0) * + (dirty_area_box.y1 - dirty_area_box.y0); + } + footprint_area = (whole_footprint.x1 - whole_footprint.x0) * + (whole_footprint.y1 - whole_footprint.y0); + if (dirty_area >= footprint_area) + goto Failure; /* fell back to a full redraw instead of blitting */ } - if (a_dirty) - goto Failure; /* A was never touched, so it must never be marked dirty */ rc = wuss_redraw_dirty(wuss); if (rc != result_OK) diff --git a/libraries/wuss/window/move.c b/libraries/wuss/window/move.c index 100dcfc3..6f63250c 100644 --- a/libraries/wuss/window/move.c +++ b/libraries/wuss/window/move.c @@ -14,31 +14,65 @@ static void translate_box(const box_t *box, int dx, int dy, box_t *out) /* p is the window's content top-left; the furniture offset (outline plus * any titlebar) is constant for a given window, so the footprint just * follows it */ -/* true if sliding any clean piece to its destination would overwrite a - * still-unread clean piece's old pixels: sequential single-rect blits (each - * a self-consistent memmove) can still corrupt each other when one piece's - * destination lands on another piece's still-unread source, which happens - * whenever the move delta is large enough relative to the gap between - * pieces (e.g. dragging fast past a thin occluding band) */ -static int wuss__pieces_would_clobber(const box_t *clean, const box_t *dest, - int n) +/* Sequential single-rect blits (each a self-consistent memmove) can still + * corrupt each other when one piece's destination lands on another piece's + * still-unread source -- but that only actually matters if no blit order + * avoids it. Build the "must happen before" graph (piece j before piece i + * whenever dest[i] would overwrite clean[j]'s still-unread source) and + * topologically sort it: any window with more than one occluder-carved + * piece near a shared edge -- e.g. two bands split by a corner occluder -- + * routinely has one such pairwise overlap without there being a genuine + * cycle, and rejecting those outright regressed plain corner-occlusion + * drags into full fallback redraws. Only an actual cycle (i must precede j + * and j must precede i) has no safe order and needs the fallback. */ +static int wuss__order_pieces(const box_t *clean, const box_t *dest, int n, + int *order) { - int i, j; + int adj[WUSS_MAX_INVALIDATE_PIECES][WUSS_MAX_INVALIDATE_PIECES]; + int indeg[WUSS_MAX_INVALIDATE_PIECES]; + int queue[WUSS_MAX_INVALIDATE_PIECES]; + int i, j, head, tail, nout, u; + + for (i = 0; i < n; i++) + indeg[i] = 0; + for (j = 0; j < n; j++) + for (i = 0; i < n; i++) + adj[j][i] = 0; for (i = 0; i < n; i++) for (j = 0; j < n; j++) - if (i != j && box_intersects(&dest[i], &clean[j])) - return 1; + if (i != j && !adj[j][i] && box_intersects(&dest[i], &clean[j])) + { + adj[j][i] = 1; /* j must be blitted before i */ + indeg[i]++; + } - return 0; + tail = 0; + for (i = 0; i < n; i++) + if (indeg[i] == 0) + queue[tail++] = i; + + head = nout = 0; + while (head < tail) + { + u = queue[head++]; + order[nout++] = u; + + for (i = 0; i < n; i++) + if (adj[u][i] && --indeg[i] == 0) + queue[tail++] = i; + } + + return nout == n; } void wuss_window_move(wuss_window_t *window, point_t p) { box_t clean[WUSS_MAX_INVALIDATE_PIECES]; box_t dest[WUSS_MAX_INVALIDATE_PIECES]; + int order[WUSS_MAX_INVALIDATE_PIECES]; int width, height, outline_px, titlebar_height; - int dx, dy, nclean, i; + int dx, dy, nclean, i, idx; box_t before, dirty, copied; int blit_failed; @@ -67,16 +101,17 @@ void wuss_window_move(wuss_window_t *window, point_t p) for (i = 0; i < nclean; i++) translate_box(&clean[i], dx, dy, &dest[i]); - /* If sliding the pieces independently would let one clobber another's - * still-unread source, there is no ordering of these single-rect blits - * that's safe: fall back rather than risk corrupting this window's own - * pixels. */ - blit_failed = nclean == 0 || wuss__pieces_would_clobber(clean, dest, nclean); + /* If no ordering of these single-rect blits avoids one clobbering + * another's still-unread source, fall back rather than risk corrupting + * this window's own pixels. */ + blit_failed = nclean == 0 || !wuss__order_pieces(clean, dest, nclean, order); for (i = 0; i < nclean && !blit_failed; i++) { - if (!screen_copy_rect(window->wuss->scr, &clean[i], - (point_t) { dest[i].x0, dest[i].y0 }, &copied)) + idx = order[i]; + + if (!screen_copy_rect(window->wuss->scr, &clean[idx], + (point_t) { dest[idx].x0, dest[idx].y0 }, &copied)) { /* The screen format doesn't support the blit at all (e.g. paletted): * this fails identically for every piece, so bail out before any @@ -87,13 +122,13 @@ void wuss_window_move(wuss_window_t *window, point_t p) /* Each clean piece is, by construction, clear of any occluder at its * old position, so the vacated sliver left behind by sliding it to - * "dest[i]" is safe to invalidate raw, without re-checking occlusion. */ - wuss__invalidate_minus(window->wuss, &clean[i], &dest[i]); + * "dest[idx]" is safe to invalidate raw, without re-checking occlusion. */ + wuss__invalidate_minus(window->wuss, &clean[idx], &dest[idx]); - /* "copied" can be smaller than "dest[i]" if the move was partly + /* "copied" can be smaller than "dest[idx]" if the move was partly * off-screen: the leftover part has no valid source pixels behind it, * so it needs a real repaint too. */ - wuss__invalidate_minus(window->wuss, &dest[i], &copied); + wuss__invalidate_minus(window->wuss, &dest[idx], &copied); /* The blit is a raw pixel copy: if this piece's destination lands under * a window above this one in z-order, it just pasted this window's @@ -105,8 +140,8 @@ void wuss_window_move(wuss_window_t *window, point_t p) box_t corrupted[WUSS_MAX_INVALIDATE_PIECES]; int nvisible, ncorrupted, k; - nvisible = wuss__clip_to_visible(window, &dest[i], visible_at_dest); - ncorrupted = wuss__subtract_boxes(&dest[i], visible_at_dest, nvisible, + nvisible = wuss__clip_to_visible(window, &dest[idx], visible_at_dest); + ncorrupted = wuss__subtract_boxes(&dest[idx], visible_at_dest, nvisible, corrupted); for (k = 0; k < ncorrupted; k++) wuss_invalidate(window->wuss, &corrupted[k]); From 13ffa60de336d823340626836a716dd402861cd4 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Thu, 27 Aug 2026 19:10:14 +0100 Subject: [PATCH 021/287] fix(wuss): skip blitting a moved window's clean pixels under a stationary occluder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Blitting the full destination then force-invalidating the occluded overlap to "repair" it forced a spurious repaint of the occluder whenever a window was dragged underneath it. The occluder hasn't moved, so its pixels there are already correct — clip each clean piece's destination against current occlusion before blitting, so the occluded remainder is never touched and needs no repair. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/test/wuss-test.c | 50 ++++++++--------- libraries/wuss/window/move.c | 95 +++++++++++++++++++++------------ 2 files changed, 85 insertions(+), 60 deletions(-) diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 102f3096..1bc451ea 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -2183,7 +2183,7 @@ result_t wuss_test(const char *resources) wuss_window_close(win_nb2); } - printf("test: dragging a clear window onto an occluder redraws the occluder\n"); + printf("test: dragging a clear window onto an occluder leaves the occluder untouched\n"); { test_task_t tc_n, tc_o; @@ -2250,11 +2250,10 @@ result_t wuss_test(const char *resources) exposed.x1 = visible_o.x0; exposed.y1 = visible.y1; /* N's part left of O */ box_intersection(&visible, &visible_o, &occluded); /* N's part under O */ - /* The part of N's new footprint that lands under O must be forced dirty - * (unclipped, so the redraw picks up O as the topmost owner there) -- - * the blit is a raw pixel copy that just pasted N's stale pixels - * straight over O's rendering, and nothing else would ever ask O to - * repair that. */ + /* The part of N's new footprint that lands under O must NOT be queued + * dirty -- O hasn't moved, so its pixels are already correct there, and + * the move blit must have skipped blitting into that area rather than + * pasting N's stale pixels over it and forcing a repair. */ occluded_dirty = 0; for (i = 0; i < wuss_get_dirty_count(wuss); i++) { @@ -2262,7 +2261,7 @@ result_t wuss_test(const char *resources) if (box_intersects(®ion, &occluded)) occluded_dirty = 1; } - if (!occluded_dirty) + if (occluded_dirty) goto Failure; /* The exposed part of N's new footprint, not under any occluder, must @@ -2476,9 +2475,9 @@ result_t wuss_test(const char *resources) test_task_t tc_a, tc_b; wuss_task_t delegate_a, delegate_b; box_t box_a, box_b, visible_b_before; - box_t top_clear, bottom_clear, region; + box_t occluded_overlap, hidden_new, region; wuss_window_t *win_a, *win_b; - int i, top_dirty, bottom_dirty; + int i, occluded_dirty, hidden_dirty; tc_b.redraw_count = 0; tc_b.mouse_count = 0; @@ -2535,27 +2534,29 @@ result_t wuss_test(const char *resources) * band's source), then the top band -- both blits are safe, so this * is not a genuine clobber cycle (translating disjoint pieces by the * same offset never produces one: any conflict is consistently - * oriented by the direction of the move). Only the top band's - * destination overlap with A (y:25-40) needs forcing dirty, plus the - * translated hidden band (y:45-65, never had valid pixels). */ + * oriented by the direction of the move). The top band's destination + * overlap with A (y:25-40) is skipped by the blit entirely (A hasn't + * moved, its pixels there are already correct), so it must stay clean; + * the translated hidden band (y:45-65, never had valid pixels) still + * needs forcing dirty for a real repaint. */ wuss_window_move(win_b, (point_t) { visible_b_before.x0, visible_b_before.y0 + 25 }); - top_clear.x0 = 0; top_clear.y0 = 25; - top_clear.x1 = 60; top_clear.y1 = 40; - bottom_clear.x0 = 0; bottom_clear.y0 = 45; - bottom_clear.x1 = 60; bottom_clear.y1 = 65; + occluded_overlap.x0 = 0; occluded_overlap.y0 = 25; + occluded_overlap.x1 = 60; occluded_overlap.y1 = 40; + hidden_new.x0 = 0; hidden_new.y0 = 45; + hidden_new.x1 = 60; hidden_new.y1 = 65; - top_dirty = bottom_dirty = 0; + occluded_dirty = hidden_dirty = 0; for (i = 0; i < wuss_get_dirty_count(wuss); i++) { wuss_get_dirty(wuss, i, ®ion); - if (box_contains_box(&top_clear, ®ion)) - top_dirty = 1; - if (box_contains_box(&bottom_clear, ®ion)) - bottom_dirty = 1; + if (box_intersects(®ion, &occluded_overlap)) + occluded_dirty = 1; + if (box_contains_box(&hidden_new, ®ion)) + hidden_dirty = 1; } - if (!top_dirty || !bottom_dirty) + if (occluded_dirty || !hidden_dirty) goto Failure; /* The blit must have actually happened, not fallen back: the part of @@ -2637,9 +2638,8 @@ result_t wuss_test(const char *resources) * redraw (that was the "Adjust drag behind a corner fully redraws the * window" regression). Dragging B up-left by (-15,-15) also grows the * overlap with A without ever fully hiding or fully clearing it -- the - * blit legitimately re-marks part of the old overlap dirty too, since - * the raw copy pastes B's stale pixels over A's correct rendering - * there before it gets repainted. */ + * blit skips the part of each piece's destination that now lands under + * A, so A's own rendering there is never touched and needs no repair. */ rc = wuss_redraw_dirty(wuss); /* flush both creations first */ if (rc != result_OK) diff --git a/libraries/wuss/window/move.c b/libraries/wuss/window/move.c index 6f63250c..978d4f9b 100644 --- a/libraries/wuss/window/move.c +++ b/libraries/wuss/window/move.c @@ -69,10 +69,12 @@ static int wuss__order_pieces(const box_t *clean, const box_t *dest, int n, void wuss_window_move(wuss_window_t *window, point_t p) { box_t clean[WUSS_MAX_INVALIDATE_PIECES]; - box_t dest[WUSS_MAX_INVALIDATE_PIECES]; + box_t full_dest[WUSS_MAX_INVALIDATE_PIECES]; + box_t blit_src[WUSS_MAX_INVALIDATE_PIECES]; + box_t blit_dest[WUSS_MAX_INVALIDATE_PIECES]; int order[WUSS_MAX_INVALIDATE_PIECES]; int width, height, outline_px, titlebar_height; - int dx, dy, nclean, i, idx; + int dx, dy, nclean, nblit, overflow, i, idx; box_t before, dirty, copied; int blit_failed; @@ -99,19 +101,52 @@ void wuss_window_move(wuss_window_t *window, point_t p) dy = window->visible.y0 - before.y0; for (i = 0; i < nclean; i++) - translate_box(&clean[i], dx, dy, &dest[i]); + translate_box(&clean[i], dx, dy, &full_dest[i]); + + /* Split each clean piece's full (untrimmed) destination down to the parts + * not already sitting under an occluder above this window there: that + * occluder hasn't moved, so its pixels are already correct, and blitting + * this window's stale pixels over them would just have to be repainted + * straight back -- cheaper to never touch them at all. Only the surviving, + * genuinely-blittable sub-pieces go on to the clobber-ordering/blit below; + * the occluded remainder needs no repair because nothing was ever pasted + * over it. */ + nblit = 0; + overflow = 0; + for (i = 0; i < nclean && !overflow; i++) + { + box_t visible_dest[WUSS_MAX_INVALIDATE_PIECES]; + int nvisible, v; + + nvisible = wuss__clip_to_visible(window, &full_dest[i], visible_dest); + + for (v = 0; v < nvisible; v++) + { + if (nblit == WUSS_MAX_INVALIDATE_PIECES) + { + overflow = 1; + break; + } + + blit_dest[nblit] = visible_dest[v]; + translate_box(&visible_dest[v], -dx, -dy, &blit_src[nblit]); + nblit++; + } + } /* If no ordering of these single-rect blits avoids one clobbering * another's still-unread source, fall back rather than risk corrupting * this window's own pixels. */ - blit_failed = nclean == 0 || !wuss__order_pieces(clean, dest, nclean, order); + blit_failed = nclean == 0 || overflow || + !wuss__order_pieces(blit_src, blit_dest, nblit, order); - for (i = 0; i < nclean && !blit_failed; i++) + for (i = 0; i < nblit && !blit_failed; i++) { idx = order[i]; - if (!screen_copy_rect(window->wuss->scr, &clean[idx], - (point_t) { dest[idx].x0, dest[idx].y0 }, &copied)) + if (!screen_copy_rect(window->wuss->scr, &blit_src[idx], + (point_t) { blit_dest[idx].x0, blit_dest[idx].y0 }, + &copied)) { /* The screen format doesn't support the blit at all (e.g. paletted): * this fails identically for every piece, so bail out before any @@ -120,32 +155,12 @@ void wuss_window_move(wuss_window_t *window, point_t p) break; } - /* Each clean piece is, by construction, clear of any occluder at its - * old position, so the vacated sliver left behind by sliding it to - * "dest[idx]" is safe to invalidate raw, without re-checking occlusion. */ - wuss__invalidate_minus(window->wuss, &clean[idx], &dest[idx]); - - /* "copied" can be smaller than "dest[idx]" if the move was partly + /* "copied" can be smaller than "blit_dest[idx]" if the move was partly * off-screen: the leftover part has no valid source pixels behind it, - * so it needs a real repaint too. */ - wuss__invalidate_minus(window->wuss, &dest[idx], &copied); - - /* The blit is a raw pixel copy: if this piece's destination lands under - * a window above this one in z-order, it just pasted this window's - * stale pixels straight over that occluder's rendering there -- and - * only there, so only that overlap (not the rest of this window's - * footprint) needs forcing dirty to repair it. */ - { - box_t visible_at_dest[WUSS_MAX_INVALIDATE_PIECES]; - box_t corrupted[WUSS_MAX_INVALIDATE_PIECES]; - int nvisible, ncorrupted, k; - - nvisible = wuss__clip_to_visible(window, &dest[idx], visible_at_dest); - ncorrupted = wuss__subtract_boxes(&dest[idx], visible_at_dest, nvisible, - corrupted); - for (k = 0; k < ncorrupted; k++) - wuss_invalidate(window->wuss, &corrupted[k]); - } + * so it needs a real repaint too. Safe to invalidate raw, without + * re-checking occlusion, since "blit_dest[idx]" (and so its "copied" + * subset) was already clipped clear of every occluder above. */ + wuss__invalidate_minus(window->wuss, &blit_dest[idx], &copied); } if (nclean > 0 && !blit_failed) @@ -153,6 +168,16 @@ void wuss_window_move(wuss_window_t *window, point_t p) box_t hidden[WUSS_MAX_INVALIDATE_PIECES]; int nhidden; + /* Each clean piece is, by construction, clear of any occluder at its + * old position, so the vacated sliver left behind by sliding it to its + * full (untrimmed) new position is safe to invalidate raw, without + * re-checking occlusion -- regardless of whether every pixel of that + * new position actually got a blit above: the part that landed under an + * occluder was skipped there, but the old position is vacated either + * way. */ + for (i = 0; i < nclean; i++) + wuss__invalidate_minus(window->wuss, &clean[i], &full_dest[i]); + /* Whatever of "before" wasn't clean has no valid source pixels: its * translated destination needs a genuine repaint, clipped against * whatever's above this window there now. */ @@ -167,9 +192,9 @@ void wuss_window_move(wuss_window_t *window, point_t p) } else { - /* Nothing of "before" was clean, the pieces would have clobbered each - * other, or the blit was declined: fall back to a normal clipped - * redraw of the whole moved footprint. */ + /* Nothing of "before" was clean, splitting overflowed the piece budget, + * the pieces would have clobbered each other, or the blit was declined: + * fall back to a normal clipped redraw of the whole moved footprint. */ box_union(&before, &window->visible, &dirty); wuss__invalidate_clipped(window, &dirty); } From fb777df1d3012bae1d2ed327edc5a8a0a9080c11 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Thu, 27 Aug 2026 22:51:30 +0100 Subject: [PATCH 022/287] docs(wuss): sync wuss.md with recent move/resize and API changes Covers wuss_get_font, wuss_WINDOW_NO_TOGGLE_BLIT, the wuss_get_dirty_count/ wuss_get_dirty(index) pair, and drops the now-false "no finer than a window's bounding box" damage-tracking limitation. --- docs/wuss.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/wuss.md b/docs/wuss.md index 19ba7571..8106f7fb 100644 --- a/docs/wuss.md +++ b/docs/wuss.md @@ -20,7 +20,7 @@ result_t wuss_create(screen_t *scr, wuss_t **wuss); ``` -`font` and `palette` may both be NULL, for unlabelled titlebars and a built-in default palette respectively. `config` may be NULL for default titlebar height/colours. +`font` and `palette` may both be NULL, for unlabelled titlebars and a built-in default palette respectively. `config` may be NULL for default titlebar height/colours. `wuss_get_font` reads back the font passed in (or NULL), for a task that wants to draw its own content in the same face as titlebars. Destroy with `wuss_destroy`, which also destroys any windows still open on it. @@ -64,8 +64,9 @@ wuss_task_t; - `wuss_WINDOW_NO_VSCROLL` — no vertical scrollbar on the right edge. - `wuss_WINDOW_NO_HSCROLL` — no horizontal scrollbar on the bottom edge. - `wuss_WINDOW_NO_RESIZE` — no resize icon in the bottom-right corner. +- `wuss_WINDOW_NO_TOGGLE_BLIT` — 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_CLOSE`/`NO_BACK`/`NO_TOGGLE_SIZE` are ignored if `flags` includes `wuss_WINDOW_NO_TITLEBAR`; `NO_VSCROLL`/`NO_HSCROLL`/`NO_RESIZE` 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_TOGGLE_BLIT` 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. @@ -121,9 +122,9 @@ Each window carries a scroll offset, `(0, 0)` by default: the point in the task' - `wuss_redraw` repaints every window, back-to-front, unconditionally. - `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. 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` fetches the current accumulated dirty region without redrawing. +- `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. +- A window move or resize is clipped, piece by piece, against whatever's above it in the z-order, and any pixels a move can preserve are blitted directly rather than queued dirty; only the genuinely-changed pieces end up in the dirty region. This is an internal optimisation with no effect on a task's own redraw handling. ## Limitations - No menus: `wuss_BUTTON_MENU` is defined and routed like any other button, but Wuss has no built-in menu widget. -- No overlapping-window damage tracking finer than each window's own bounding box. From 50f69e232f44519560871e823be6f0f5d2999b7d Mon Sep 17 00:00:00 2001 From: David Thomas Date: Thu, 27 Aug 2026 22:55:07 +0100 Subject: [PATCH 023/287] refactor(bmfont): drop -font suffix from font PNG filenames Matches ms-sans-serif.png, which never had the suffix. --- docs/framebuf/bmfont.md | 6 +++--- libraries/framebuf/bmfont/test/bmfont-test.c | 12 ++++++------ libraries/wuss/test/wuss-test.c | 4 ++-- .../bmfonts/{daydream-font.png => daydream.png} | Bin resources/bmfonts/{digits-font.png => digits.png} | Bin .../{gliderrider-font.png => gliderrider.png} | Bin resources/bmfonts/{henry-font.png => henry.png} | Bin resources/bmfonts/{tall-font.png => tall.png} | Bin resources/bmfonts/{tiny-font.png => tiny.png} | Bin 9 files changed, 11 insertions(+), 11 deletions(-) rename resources/bmfonts/{daydream-font.png => daydream.png} (100%) rename resources/bmfonts/{digits-font.png => digits.png} (100%) rename resources/bmfonts/{gliderrider-font.png => gliderrider.png} (100%) rename resources/bmfonts/{henry-font.png => henry.png} (100%) rename resources/bmfonts/{tall-font.png => tall.png} (100%) rename resources/bmfonts/{tiny-font.png => tiny.png} (100%) diff --git a/docs/framebuf/bmfont.md b/docs/framebuf/bmfont.md index 19b5c2cd..2a9b94b3 100644 --- a/docs/framebuf/bmfont.md +++ b/docs/framebuf/bmfont.md @@ -2,15 +2,15 @@ "bmfont" is a sub-library of DPTLib for drawing proportionally spaced bitmap fonts. It reads font definitions from PNG files like this: -![Henry Font](../resources/bmfonts/henry-font.png) +![Henry Font](../resources/bmfonts/henry.png) or this: -![Digits Font](../resources/bmfonts/digits-font.png) +![Digits Font](../resources/bmfonts/digits.png) or even this: -![Tiny Font](../resources/bmfonts/tiny-font.png) +![Tiny Font](../resources/bmfonts/tiny.png) ...which have the glyphs laid out in a grid, with extra lines inserted, that define the advance widths. diff --git a/libraries/framebuf/bmfont/test/bmfont-test.c b/libraries/framebuf/bmfont/test/bmfont-test.c index f1f2f6a9..bed3b343 100644 --- a/libraries/framebuf/bmfont/test/bmfont-test.c +++ b/libraries/framebuf/bmfont/test/bmfont-test.c @@ -187,13 +187,13 @@ bmtestline_t; static bmtestfont_t bmfonts[MAXFONTS] = { - { "daydream-font", NULL }, - { "gliderrider-font", NULL }, - { "tiny-font", NULL }, - { "henry-font", NULL }, - { "tall-font", NULL }, + { "daydream", NULL }, + { "gliderrider", NULL }, + { "tiny", NULL }, + { "henry", NULL }, + { "tall", NULL }, { "ms-sans-serif", NULL }, - { "digits-font", NULL } + { "digits", NULL } }; /* ----------------------------------------------------------------------- */ diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 1bc451ea..9806417a 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -169,13 +169,13 @@ static result_t wuss_interactive_test(const char *resources) define_pico8_palette(palette); - leafname = path_join_leafname("digits-font", "png"); + leafname = path_join_leafname("digits", "png"); filename = path_join_filename(resources, 3, "resources", "bmfonts", leafname); rc = bmfont_create(filename, &font); if (rc != result_OK) goto Failure; - leafname = path_join_leafname("daydream-font", "png"); + leafname = path_join_leafname("daydream", "png"); filename = path_join_filename(resources, 3, "resources", "bmfonts", leafname); rc = bmfont_create(filename, &daydream_font); if (rc != result_OK) diff --git a/resources/bmfonts/daydream-font.png b/resources/bmfonts/daydream.png similarity index 100% rename from resources/bmfonts/daydream-font.png rename to resources/bmfonts/daydream.png diff --git a/resources/bmfonts/digits-font.png b/resources/bmfonts/digits.png similarity index 100% rename from resources/bmfonts/digits-font.png rename to resources/bmfonts/digits.png diff --git a/resources/bmfonts/gliderrider-font.png b/resources/bmfonts/gliderrider.png similarity index 100% rename from resources/bmfonts/gliderrider-font.png rename to resources/bmfonts/gliderrider.png diff --git a/resources/bmfonts/henry-font.png b/resources/bmfonts/henry.png similarity index 100% rename from resources/bmfonts/henry-font.png rename to resources/bmfonts/henry.png diff --git a/resources/bmfonts/tall-font.png b/resources/bmfonts/tall.png similarity index 100% rename from resources/bmfonts/tall-font.png rename to resources/bmfonts/tall.png diff --git a/resources/bmfonts/tiny-font.png b/resources/bmfonts/tiny.png similarity index 100% rename from resources/bmfonts/tiny-font.png rename to resources/bmfonts/tiny.png From 3324eb3fb7921a7364065e9ba1d60a4c7f7a64c8 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Thu, 27 Aug 2026 22:58:31 +0100 Subject: [PATCH 024/287] fix(bmfont): Fix digits.png --- resources/bmfonts/digits.png | Bin 688 -> 766 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/resources/bmfonts/digits.png b/resources/bmfonts/digits.png index 4111a4281f85f0c3f15ad0b4cca74177f7f52ac4..0a51d48b59e67408a726940814127d2dd0a72096 100644 GIT binary patch delta 718 zcmV;<0x|ut1^xw)Bt`*qQb$4nuFf3k00004XF*Lt006O%3;baP0000WV@Og>004R> z004l5008;`004mK004C`008P>0026e000+ooVrmwks%v@WJyFpR7l6Il{=0bF${(o zAO}d3`)Gkp&w_-?Im+x&wls8#nL>R6nAkg{ce$95)We$bdf_0Eps}8xBOEFkS&nd%#Ky z5whmh0SITEWsnv9)_xoM_yo33wuPt=T7%la0p!=np#$>sJsYSIgKg`UuGAfSHl01JF)TY_I@qn{1XYv;Sx-`Y&sLnXEnW%u%NINi<((t&gjCj*M39^+j#1$~3FP83o&zR3nzet{-ZE z-Vfw;0oNYvmVnv6rIpME=f47k5s*IsKn1`RFs#?BjR3&|fU)eNxt##_fJ_ZwF3n*F z=tuQ`{n-KBPhfC$A_ttYBH$q#^j>yDnu>*^0|HT4atUXOpsyXx?ON2$iB#~_xe_u&NK~#9!t(C!!+#n2wU8xVyOW%i;ws#+FapY05kFv)R zPeBgcTWPF)hu(7>v9f`LWG2;Atx=M}As=9VdmzU69^)Itn4q^9wO2j${oXDRAzt)H zyg&j0`JN!V`mq4Py!H|Zie5M)lh`G=O(h5k*fA#%sxAV7U*9$We-Q%_QAS!#{R$)? zGJv{%xDAjUQv#+65CdG1fFNN9=osk$buexKrafS$M1+itvjGsUAO@Myc5S<%$1Biz zvVMq(h-N1!1|Zu;b}Jyip2a{woK97>!=o7C1;8{MC=R}}2;qy@1Wn|`PRRzHya7BKC>?i0}XcbiFLaQ!QQ z8v*$N02%-`0F3|~0dfQ|)>+tbUjgm`LLHzf&A0>bZVPMxe_mIhaK>zaD>j6CDh8_; zHEb2YCTE5LP>S~)ByeEwiv;g^^6K+y5_}P_BUHr8^bx#YCLLLtzYhL^&fzd+A-E-YHwO`@5$5w zFE67GS^=l~cwEq;24gaH!0VSW7f9S0K->t(cn_c|;tt?_ER zOclVbIt)Q9MNG=#C5Xw^Yi0H-XO%Js@xE}7Rm$SocNs0=crbK4GHSl#m=h^WI7-#` ctT`F;39UcVMAVia7ytkO07*qoM6N<$g5{DQ*8l(j From 76f4331bb25c5cddbe76af757f4beec3b1791ce4 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Thu, 27 Aug 2026 23:35:38 +0100 Subject: [PATCH 025/287] refactor(wuss): rename wuss_WINDOW_NO_TOGGLE_BLIT to NO_RESIZE_BLIT The flag guards wuss_window_resize itself, not just the toggle-size icon action, so drag-resizing a flagged window also gets a full redraw instead of blit-preserve. The old name only described one of its two triggers. --- docs/wuss.md | 4 ++-- include/wuss/wuss.h | 2 +- libraries/wuss/furniture/toggle-action.c | 2 +- libraries/wuss/test/tasks/palette.c | 2 +- libraries/wuss/test/wuss-test.c | 12 ++++++------ libraries/wuss/window/resize.c | 2 +- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/wuss.md b/docs/wuss.md index 8106f7fb..b646fc09 100644 --- a/docs/wuss.md +++ b/docs/wuss.md @@ -64,9 +64,9 @@ wuss_task_t; - `wuss_WINDOW_NO_VSCROLL` — no vertical scrollbar on the right edge. - `wuss_WINDOW_NO_HSCROLL` — no horizontal scrollbar on the bottom edge. - `wuss_WINDOW_NO_RESIZE` — no resize icon in the bottom-right corner. -- `wuss_WINDOW_NO_TOGGLE_BLIT` — 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_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_CLOSE`/`NO_BACK`/`NO_TOGGLE_SIZE` are ignored if `flags` includes `wuss_WINDOW_NO_TITLEBAR`; `NO_VSCROLL`/`NO_HSCROLL`/`NO_RESIZE`/`NO_TOGGLE_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` 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. diff --git a/include/wuss/wuss.h b/include/wuss/wuss.h index 00299250..73579eef 100644 --- a/include/wuss/wuss.h +++ b/include/wuss/wuss.h @@ -95,7 +95,7 @@ typedef enum wuss_window_flags wuss_WINDOW_NO_VSCROLL = 1 << 5, /**< No vertical scrollbar on the right edge. */ wuss_WINDOW_NO_HSCROLL = 1 << 6, /**< No horizontal scrollbar on the bottom edge. */ wuss_WINDOW_NO_RESIZE = 1 << 7, /**< No resize icon in the bottom-right corner. */ - wuss_WINDOW_NO_TOGGLE_BLIT = 1 << 8 /**< Toggle-size always fully redraws the window's content instead of blitting the preserved region -- for a task whose rendering depends on the window's size in ways redraw can't patch incrementally (e.g. a palette that lays itself out across the whole window). */ + wuss_WINDOW_NO_RESIZE_BLIT = 1 << 8 /**< 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 the window's size in ways redraw can't patch incrementally (e.g. a palette that lays itself out across the whole window). */ } wuss_window_flags_t; diff --git a/libraries/wuss/furniture/toggle-action.c b/libraries/wuss/furniture/toggle-action.c index 376d59f6..e2d50886 100644 --- a/libraries/wuss/furniture/toggle-action.c +++ b/libraries/wuss/furniture/toggle-action.c @@ -80,7 +80,7 @@ void wuss__furniture_toggle_size(wuss_window_t *window) } } - if (!(window->flags & wuss_WINDOW_NO_TOGGLE_BLIT) && + if (!(window->flags & wuss_WINDOW_NO_RESIZE_BLIT) && window->wuss->z_order.next == &window->link && screen_copy_rect(window->wuss->scr, &before, (point_t) { before.x0, before.y0 }, &copied)) diff --git a/libraries/wuss/test/tasks/palette.c b/libraries/wuss/test/tasks/palette.c index cd1b1794..7d661773 100644 --- a/libraries/wuss/test/tasks/palette.c +++ b/libraries/wuss/test/tasks/palette.c @@ -29,7 +29,7 @@ result_t palette_create(wuss_t *wuss, return wuss_window_create(wuss, &box, "Palette", - wuss_WINDOW_NO_TOGGLE_BLIT, /* swatch grid is laid out across the whole window, so a resize must redraw all of it, not just the newly (un)covered edge */ + wuss_WINDOW_NO_RESIZE_BLIT, /* swatch grid is laid out across the whole window, so a resize must redraw all of it, not just the newly (un)covered edge */ &delegate, box.x1 - box.x0, box.y1 - box.y0, diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 9806417a..7e748676 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -1654,7 +1654,7 @@ result_t wuss_test(const char *resources) wuss_window_close(win_r); } - printf("test: wuss_WINDOW_NO_TOGGLE_BLIT redraws the whole window instead of blitting\n"); + printf("test: wuss_WINDOW_NO_RESIZE_BLIT redraws the whole window instead of blitting\n"); { test_task_t tc_nb; @@ -1672,7 +1672,7 @@ result_t wuss_test(const char *resources) box_nb.x0 = 10; box_nb.y0 = 10; box_nb.x1 = 50; box_nb.y1 = 50; /* 40x40 content, room to grow to a 200x200 doc */ - rc = wuss_window_create(wuss, &box_nb, "NB", wuss_WINDOW_NO_TOGGLE_BLIT, + rc = wuss_window_create(wuss, &box_nb, "NB", wuss_WINDOW_NO_RESIZE_BLIT, &delegate_nb, 200, 200, &win_nb); if (rc != result_OK) goto Failure; @@ -1722,7 +1722,7 @@ result_t wuss_test(const char *resources) interior_dirty = 1; } if (!interior_dirty) - goto Failure; /* wuss_WINDOW_NO_TOGGLE_BLIT must skip the blit path + goto Failure; /* wuss_WINDOW_NO_RESIZE_BLIT must skip the blit path * entirely, so even an interior pixel the blit would * otherwise have preserved comes out dirty */ @@ -2134,7 +2134,7 @@ result_t wuss_test(const char *resources) wuss_window_close(win_m2); } - printf("test: resizing a wuss_WINDOW_NO_TOGGLE_BLIT window redraws it fully\n"); + printf("test: resizing a wuss_WINDOW_NO_RESIZE_BLIT window redraws it fully\n"); { test_task_t tc_nb2; @@ -2151,7 +2151,7 @@ result_t wuss_test(const char *resources) box_nb2.x0 = 0; box_nb2.y0 = 0; box_nb2.x1 = 40; box_nb2.y1 = 40; - rc = wuss_window_create(wuss, &box_nb2, "NB2", wuss_WINDOW_NO_TOGGLE_BLIT, + rc = wuss_window_create(wuss, &box_nb2, "NB2", wuss_WINDOW_NO_RESIZE_BLIT, &delegate_nb2, box_nb2.x1 - box_nb2.x0, box_nb2.y1 - box_nb2.y0, @@ -2178,7 +2178,7 @@ result_t wuss_test(const char *resources) dirty_area += (region.x1 - region.x0) * (region.y1 - region.y0); } if (dirty_area < full_area) - goto Failure; /* NO_TOGGLE_BLIT must fully redraw, not just the sliver */ + goto Failure; /* NO_RESIZE_BLIT must fully redraw, not just the sliver */ wuss_window_close(win_nb2); } diff --git a/libraries/wuss/window/resize.c b/libraries/wuss/window/resize.c index 60fdfddb..f7465af4 100644 --- a/libraries/wuss/window/resize.c +++ b/libraries/wuss/window/resize.c @@ -49,7 +49,7 @@ result_t wuss_window_resize(wuss_window_t *window, int width, int height) wuss__notify_open(window); - if (window->flags & wuss_WINDOW_NO_TOGGLE_BLIT) + if (window->flags & wuss_WINDOW_NO_RESIZE_BLIT) { /* This task's content isn't just anchored positions plus furniture -- * it lays itself out across the whole window (e.g. a palette swatch From 6dbb33eba019c547b6e9d1a9681d0d53ef60c5c2 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Thu, 27 Aug 2026 23:35:43 +0100 Subject: [PATCH 026/287] fix(wuss/test): fully redraw the Lorem Ipsum task on resize Its paragraph reflows across the whole window width, so the default resize optimisation (preserve the unchanged interior, repaint only the grown/shrunk sliver) left stale wrapped lines behind whenever the task's own idle animation resized the window. Same fix already applied to the palette swatch task. --- libraries/wuss/test/tasks/text.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/wuss/test/tasks/text.c b/libraries/wuss/test/tasks/text.c index 0202eabf..8b8c4c9e 100644 --- a/libraries/wuss/test/tasks/text.c +++ b/libraries/wuss/test/tasks/text.c @@ -50,7 +50,7 @@ result_t text_create(wuss_t *wuss, rc = wuss_window_create(wuss, &box, "Lorem Ipsum", - wuss_WINDOW_NONE, + 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 */ &delegate, box.x1 - box.x0, box.y1 - box.y0, From 855700df9191959a2882048896e43b01f07a8a36 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Thu, 27 Aug 2026 23:35:48 +0100 Subject: [PATCH 027/287] fix(wuss): clip titlebar text to its own slot The text's clip rect was the whole titlebar, not the text_x0..text_x1 slot computed to dodge the close/back/toggle icons, so a title too long to fit was drawn past that boundary. It usually stayed hidden because the icon(s) painted after it happened to cover the overrun, but that only holds when the redraw region includes those icons too: a redraw covering just the title (e.g. after a title-only invalidate) left the overrun visible. Now the draw itself is clipped to the slot. --- libraries/wuss/furniture/draw.c | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/libraries/wuss/furniture/draw.c b/libraries/wuss/furniture/draw.c index bf0c2102..e3a6438b 100644 --- a/libraries/wuss/furniture/draw.c +++ b/libraries/wuss/furniture/draw.c @@ -32,6 +32,7 @@ void wuss__furniture_draw(wuss_t *wuss, 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)) @@ -58,16 +59,26 @@ void wuss__furniture_draw(wuss_t *wuss, text_x1 = toggle.x0 - 2; } - if (text_x1 > text_x0) + /* 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(wuss->font, 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(wuss->font, 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; } } From 5f51654ac987f16a16189226ea3f2841b67a33c1 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Fri, 28 Aug 2026 00:16:33 +0100 Subject: [PATCH 028/287] refactor(wuss)!: move bg from wuss_task_t to wuss_window_create wuss_task_t::bg was mutated at runtime via wuss_window_set_background, despite the struct being documented as an immutable content delegate copied in at creation. Move bg to a dedicated wuss_window_create parameter (and a wuss_window_t field) so window-owned mutable state lives on the window, not the delegate. BREAKING CHANGE: wuss_task_t drops its bg field (now {handle, task_data}); wuss_task_start drops its bg parameter; wuss_window_create gains a wuss_colour_t bg parameter after flags. Co-Authored-By: Claude Sonnet 5 --- docs/wuss.md | 12 +++-- include/wuss/window.h | 13 +++-- include/wuss/wuss.h | 2 +- libraries/wuss/impl.h | 1 + libraries/wuss/redraw.c | 4 +- libraries/wuss/task-start.c | 4 +- libraries/wuss/test/tasks/ball.c | 3 +- libraries/wuss/test/tasks/blank.c | 3 +- libraries/wuss/test/tasks/chars.c | 3 +- libraries/wuss/test/tasks/checker.c | 4 +- libraries/wuss/test/tasks/curve.c | 3 +- libraries/wuss/test/tasks/gradient.c | 3 +- libraries/wuss/test/tasks/image.c | 3 +- libraries/wuss/test/tasks/launcher.c | 3 +- libraries/wuss/test/tasks/palette.c | 3 +- libraries/wuss/test/tasks/sofa.c | 3 +- libraries/wuss/test/tasks/text.c | 3 +- libraries/wuss/test/wuss-test.c | 66 +++++++++++++------------- libraries/wuss/window/create.c | 7 ++- libraries/wuss/window/set-background.c | 2 +- 20 files changed, 78 insertions(+), 67 deletions(-) diff --git a/docs/wuss.md b/docs/wuss.md index b646fc09..5a981500 100644 --- a/docs/wuss.md +++ b/docs/wuss.md @@ -26,32 +26,34 @@ Destroy with `wuss_destroy`, which also destroys any windows still open on it. ## Windows -Create a window with a content bounding box, optional title, appearance flags and a task delegate: +Create a window with a content bounding box, optional title, appearance flags, a content background and a task delegate: ```C result_t wuss_window_create(wuss_t *wuss, const box_t *content, const char *title, - wuss_window_flags_t flags, const wuss_task_t *task, + wuss_window_flags_t flags, wuss_colour_t bg, + const wuss_task_t *task, int doc_width, int doc_height, wuss_window_t **window); ``` +`bg` is filled in by wuss before each redraw event, or `wuss_NO_BACKGROUND` for the task to draw its own background (avoids a redundant fill behind an opaque task); changeable later via `wuss_window_set_background`. + `doc_width`/`doc_height` are the virtual document extent behind the horizontal/vertical scrollbars' sausage proportion; pass `content`'s own width/height for a window with nothing to scroll. Set once at creation, immutable thereafter. Furniture is additional to `content`, not carved out of it: the window's content area always ends up exactly the box requested, and its on-screen footprint (`wuss_window_get_visible_bounds`) is `content` expanded outward by whatever furniture flags request — a titlebar above, and/or a 1px outline around all four sides. -`wuss_task_t` holds the task's event callback and its content background: +`wuss_task_t` holds the task's event callback: ```C typedef struct wuss_task { wuss_event_fn_t *handle; /* NULL => task receives no events */ void *task_data; - wuss_colour_t bg; /* filled by wuss before a redraw event, or wuss_NO_BACKGROUND */ } wuss_task_t; ``` -`wuss_task_make` builds one from `handle`/`task_data`/`bg`. +`wuss_task_start` builds one from `handle`/`task_data`. `flags` combines, by bitwise OR: diff --git a/include/wuss/window.h b/include/wuss/window.h index 255564d9..e9869b6d 100644 --- a/include/wuss/window.h +++ b/include/wuss/window.h @@ -102,9 +102,8 @@ typedef result_t (wuss_event_fn_t)(wuss_window_t *window, /** A window's content delegate. Copied by value into the window at creation. */ typedef struct wuss_task { - wuss_event_fn_t *handle; /**< NULL => task receives no events; Wuss still fills the content background per bg. */ + wuss_event_fn_t *handle; /**< NULL => task receives no events; Wuss still fills the content background per wuss_window_create's bg. */ void *task_data; - wuss_colour_t bg; /**< Content background, filled by Wuss before redraw is called, or wuss_NO_BACKGROUND for the task to draw its own background (avoids a redundant fill behind an opaque task). */ } wuss_task_t; @@ -113,12 +112,10 @@ wuss_task_t; * * \param[in] handle Event callback, or NULL for a task that receives no events. * \param[in] task_data Opaque pointer passed back to the callback. - * \param[in] bg Content background, or wuss_NO_BACKGROUND. * \return The populated task. */ wuss_task_t wuss_task_start(wuss_event_fn_t *handle, - void *task_data, - wuss_colour_t bg); + void *task_data); /** * Notify a window's task that it is shutting down, via wuss_EVENT_QUIT. @@ -160,19 +157,21 @@ result_t wuss_idle(wuss_t *wuss); * \param[in] content Requested content-area bounds, screen space. Copied in. * \param[in] title Titlebar label, or NULL for none. Copied in, truncated if too long. Ignored if flags includes 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. + * \param[in] bg Content background, filled by Wuss before each redraw, or wuss_NO_BACKGROUND for the task to draw its own background (avoids a redundant fill behind an opaque task). Changeable later via wuss_window_set_background. * \param[in] task Content delegate. Copied in. May be NULL for a window with no content handling. * \param[in] doc_width Virtual document width, for the horizontal scrollbar's sausage proportion; pass content's own width for a window with nothing to scroll. * \param[in] doc_height Virtual document height, for the vertical scrollbar's sausage proportion; pass content's own height for a window with nothing to scroll. * \param[out] window Newly created window. Becomes the topmost window. * \return \ref result_OK on success, \ref result_WUSS_TOO_SMALL if content's * width or height is not positive, \ref result_WUSS_BAD_COLOUR if - * task->bg is out of range for the palette, or another - * appropriate result code. + * bg is out of range for the palette, or another appropriate + * result code. */ result_t wuss_window_create(wuss_t *wuss, const box_t *content, const char *title, wuss_window_flags_t flags, + wuss_colour_t bg, const wuss_task_t *task, int doc_width, int doc_height, diff --git a/include/wuss/wuss.h b/include/wuss/wuss.h index 73579eef..ce1f8935 100644 --- a/include/wuss/wuss.h +++ b/include/wuss/wuss.h @@ -58,7 +58,7 @@ wuss_mouse_action_t; /** An index into a wuss_t's system palette (see wuss_create). Not a colour_t. */ typedef int wuss_colour_t; -/** Sentinel for wuss_task_t::bg meaning "no automatic background fill". */ +/** Sentinel for wuss_window_create's bg meaning "no automatic background fill". */ #define wuss_NO_BACKGROUND ((wuss_colour_t) -1) /** Furniture chrome colours, one entry per class of furniture. Title is diff --git a/libraries/wuss/impl.h b/libraries/wuss/impl.h index d9e6479e..cfcdf829 100644 --- a/libraries/wuss/impl.h +++ b/libraries/wuss/impl.h @@ -47,6 +47,7 @@ struct wuss_window box_t visible; /* full on-screen footprint: content expanded * outward by any titlebar/outline furniture */ wuss_task_t task; + wuss_colour_t bg; wuss_window_flags_t flags; point_t scroll; /* offset into virtual content space of the * content box's top-left; see wuss_window_set_scroll */ diff --git a/libraries/wuss/redraw.c b/libraries/wuss/redraw.c index 574aec77..bac48e89 100644 --- a/libraries/wuss/redraw.c +++ b/libraries/wuss/redraw.c @@ -32,11 +32,11 @@ static void redraw_window(wuss_t *wuss, { wuss->scr->clip = pieces[i]; - if (win->task.bg != wuss_NO_BACKGROUND) + if (win->bg != wuss_NO_BACKGROUND) screen_draw_rect(wuss->scr, content.x0, content.y0, content.x1 - content.x0, content.y1 - content.y0, - wuss->palette[win->task.bg]); + wuss->palette[win->bg]); if (win->task.handle != NULL) { diff --git a/libraries/wuss/task-start.c b/libraries/wuss/task-start.c index c7000704..192d2b73 100644 --- a/libraries/wuss/task-start.c +++ b/libraries/wuss/task-start.c @@ -5,14 +5,12 @@ #include "wuss/window.h" wuss_task_t wuss_task_start(wuss_event_fn_t *handle, - void *task_data, - wuss_colour_t bg) + void *task_data) { wuss_task_t task; task.handle = handle; task.task_data = task_data; - task.bg = bg; return task; } diff --git a/libraries/wuss/test/tasks/ball.c b/libraries/wuss/test/tasks/ball.c index 581f4940..1dcd0512 100644 --- a/libraries/wuss/test/tasks/ball.c +++ b/libraries/wuss/test/tasks/ball.c @@ -27,13 +27,14 @@ result_t ball_create(wuss_t *wuss, const colour_t *palette, ball_task_t *task) task->balls[0].dy = 2; task->balls[0].radius = 8; - delegate = wuss_task_start(ball_handle, task, wuss_NO_BACKGROUND); /* ball_redraw paints its own background every frame */ + delegate = wuss_task_start(ball_handle, task); /* ball_redraw paints its own background every frame */ box = (box_t) BOX_POS_SIZE(20, 20, 200, 160); return wuss_window_create(wuss, &box, "Bouncing Ball", wuss_WINDOW_NONE, + wuss_NO_BACKGROUND, &delegate, box.x1 - box.x0, box.y1 - box.y0, diff --git a/libraries/wuss/test/tasks/blank.c b/libraries/wuss/test/tasks/blank.c index a976a1ad..12d9c008 100644 --- a/libraries/wuss/test/tasks/blank.c +++ b/libraries/wuss/test/tasks/blank.c @@ -24,13 +24,14 @@ result_t blank_create(wuss_t *wuss, int npalette, blank_task_t *task) task->index = palette_PICO8_GREEN; task->frame_count = 0; - delegate = wuss_task_start(blank_handle, task, palette_PICO8_GREEN); /* wuss fills the content area itself */ + delegate = wuss_task_start(blank_handle, task); /* wuss fills the content area itself */ box = (box_t) BOX_POS_SIZE(260, 60, 200, 160); return wuss_window_create(wuss, &box, NULL, wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE, + palette_PICO8_GREEN, &delegate, box.x1 - box.x0, box.y1 - box.y0, diff --git a/libraries/wuss/test/tasks/chars.c b/libraries/wuss/test/tasks/chars.c index f3b01130..8d139527 100644 --- a/libraries/wuss/test/tasks/chars.c +++ b/libraries/wuss/test/tasks/chars.c @@ -41,7 +41,7 @@ result_t chars_create(wuss_t *wuss, task->fg = palette[palette_PICO8_BLACK]; task->bg = palette[palette_PICO8_WHITE]; - delegate = wuss_task_start(chars_handle, task, wuss_NO_BACKGROUND); /* chars_redraw paints every cell itself */ + delegate = wuss_task_start(chars_handle, task); /* chars_redraw paints every cell itself */ box = (box_t) BOX_POS_SIZE(500, 100, cell_w * CHARS_COLS, cell_h * CHARS_ROWS); return wuss_window_create(wuss, @@ -51,6 +51,7 @@ result_t chars_create(wuss_t *wuss, wuss_WINDOW_NO_TOGGLE_SIZE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL, + wuss_NO_BACKGROUND, &delegate, box.x1 - box.x0, box.y1 - box.y0, diff --git a/libraries/wuss/test/tasks/checker.c b/libraries/wuss/test/tasks/checker.c index 0578ce53..04e22ab3 100644 --- a/libraries/wuss/test/tasks/checker.c +++ b/libraries/wuss/test/tasks/checker.c @@ -31,13 +31,14 @@ result_t checker_create(wuss_t *wuss, task->band = CHECKER_BAND_DEFAULT; task->band2 = CHECKER_BAND_DEFAULT; - delegate = wuss_task_start(checker_handle, task, wuss_NO_BACKGROUND); /* checker_redraw paints every pixel itself */ + delegate = wuss_task_start(checker_handle, task); /* checker_redraw paints every pixel itself */ box = (box_t) BOX_POS_SIZE(440, 300, 160, 160); rc = wuss_window_create(wuss, &box, "Checker 1", wuss_WINDOW_NONE, + wuss_NO_BACKGROUND, &delegate, box.x1 - box.x0, box.y1 - box.y0, @@ -51,6 +52,7 @@ result_t checker_create(wuss_t *wuss, &box, "Checker 2", wuss_WINDOW_NONE, + wuss_NO_BACKGROUND, &delegate, box.x1 - box.x0, box.y1 - box.y0, diff --git a/libraries/wuss/test/tasks/curve.c b/libraries/wuss/test/tasks/curve.c index 2f25b1c4..0854b6db 100644 --- a/libraries/wuss/test/tasks/curve.c +++ b/libraries/wuss/test/tasks/curve.c @@ -39,13 +39,14 @@ result_t curve_create(wuss_t *wuss, task->points[2] = (point_t) { 210, 10 }; task->points[3] = (point_t) { 210, 140 }; - delegate = wuss_task_start(curve_handle, task, wuss_NO_BACKGROUND); /* curve_redraw paints its own background */ + delegate = wuss_task_start(curve_handle, task); /* curve_redraw paints its own background */ box = (box_t) BOX_POS_SIZE(20, 260, 220, 160); return wuss_window_create(wuss, &box, "Curve", wuss_WINDOW_NONE, + wuss_NO_BACKGROUND, &delegate, box.x1 - box.x0, box.y1 - box.y0, diff --git a/libraries/wuss/test/tasks/gradient.c b/libraries/wuss/test/tasks/gradient.c index 949558f8..ec683f6b 100644 --- a/libraries/wuss/test/tasks/gradient.c +++ b/libraries/wuss/test/tasks/gradient.c @@ -36,13 +36,14 @@ result_t gradient_create(wuss_t *wuss, gradient_task_t *task) wuss_task_t delegate; box_t box; - delegate = wuss_task_start(gradient_handle, task, wuss_NO_BACKGROUND); /* gradient_redraw paints every pixel itself */ + delegate = wuss_task_start(gradient_handle, task); /* gradient_redraw paints every pixel itself */ box = (box_t) BOX_POS_SIZE(620, 300, GRADIENT_OPEN_WIDTH, GRADIENT_OPEN_HEIGHT); return wuss_window_create(wuss, &box, "Gradient", wuss_WINDOW_NONE, + wuss_NO_BACKGROUND, &delegate, GRADIENT_DOC_WIDTH, GRADIENT_DOC_HEIGHT, diff --git a/libraries/wuss/test/tasks/image.c b/libraries/wuss/test/tasks/image.c index 63a5f333..2314819e 100644 --- a/libraries/wuss/test/tasks/image.c +++ b/libraries/wuss/test/tasks/image.c @@ -32,7 +32,7 @@ result_t image_create(wuss_t *wuss, if (rc != result_OK) return rc; - delegate = wuss_task_start(image_handle, task, palette_PICO8_BLACK); /* shows through the image's transparent pixels */ + delegate = wuss_task_start(image_handle, task); /* shows through the image's transparent pixels */ /* shorter than the bitmap so there's something to scroll through */ box = (box_t) BOX_POS_SIZE(370, 10, task->bitmap.width, task->bitmap.height * 2 / 3); @@ -40,6 +40,7 @@ result_t image_create(wuss_t *wuss, &box, "Image", wuss_WINDOW_NONE, + palette_PICO8_BLACK, &delegate, task->bitmap.width, task->bitmap.height, diff --git a/libraries/wuss/test/tasks/launcher.c b/libraries/wuss/test/tasks/launcher.c index 16fcf4a9..61dd17a1 100644 --- a/libraries/wuss/test/tasks/launcher.c +++ b/libraries/wuss/test/tasks/launcher.c @@ -36,13 +36,14 @@ result_t launcher_create(wuss_t *wuss, task->bg = palette[palette_PICO8_WHITE]; task->running_fg = palette[palette_PICO8_LIGHT_GREY]; - delegate = wuss_task_start(launcher_handle, task, wuss_NO_BACKGROUND); /* launcher_redraw paints its own background */ + delegate = wuss_task_start(launcher_handle, task); /* launcher_redraw paints its own background */ box = (box_t) BOX_POS_SIZE(10, 10, LAUNCHER_WIDTH, LAUNCHER_PAD * 2 + nentries * LAUNCHER_ROW_HEIGHT); return wuss_window_create(wuss, &box, "Launcher", wuss_WINDOW_NO_CLOSE, + wuss_NO_BACKGROUND, &delegate, box.x1 - box.x0, box.y1 - box.y0, diff --git a/libraries/wuss/test/tasks/palette.c b/libraries/wuss/test/tasks/palette.c index 7d661773..60c87f85 100644 --- a/libraries/wuss/test/tasks/palette.c +++ b/libraries/wuss/test/tasks/palette.c @@ -23,13 +23,14 @@ result_t palette_create(wuss_t *wuss, task->palette = palette; task->npalette = npalette; - delegate = wuss_task_start(palette_handle, task, palette_PICO8_BLACK); /* backdrop for any rounding gap around the grid */ + delegate = wuss_task_start(palette_handle, task); /* backdrop for any rounding gap around the grid */ box = (box_t) BOX_POS_SIZE(380, 260, 100, 100); return wuss_window_create(wuss, &box, "Palette", wuss_WINDOW_NO_RESIZE_BLIT, /* swatch grid is laid out across the whole window, so a resize must redraw all of it, not just the newly (un)covered edge */ + palette_PICO8_BLACK, &delegate, box.x1 - box.x0, box.y1 - box.y0, diff --git a/libraries/wuss/test/tasks/sofa.c b/libraries/wuss/test/tasks/sofa.c index ca1642f5..992798dc 100644 --- a/libraries/wuss/test/tasks/sofa.c +++ b/libraries/wuss/test/tasks/sofa.c @@ -266,13 +266,14 @@ result_t sofa_create(wuss_t *wuss, const colour_t *palette, sofa_task_t *task) task->spinning = true; task->shape = sofa_SHAPE_SOFA; - delegate = wuss_task_start(sofa_handle, task, wuss_NO_BACKGROUND); /* sofa_redraw paints its own background every frame */ + delegate = wuss_task_start(sofa_handle, task); /* sofa_redraw paints its own background every frame */ box = (box_t) BOX_POS_SIZE(250, 260, 180, 160); return wuss_window_create(wuss, &box, "Sofa", wuss_WINDOW_NONE, + wuss_NO_BACKGROUND, &delegate, box.x1 - box.x0, box.y1 - box.y0, diff --git a/libraries/wuss/test/tasks/text.c b/libraries/wuss/test/tasks/text.c index 8b8c4c9e..1d05faa9 100644 --- a/libraries/wuss/test/tasks/text.c +++ b/libraries/wuss/test/tasks/text.c @@ -41,7 +41,7 @@ result_t text_create(wuss_t *wuss, task->frame_count = 0; task->resizing = true; - delegate = wuss_task_start(text_handle, task, palette_PICO8_BLUE); + delegate = wuss_task_start(text_handle, task); box = (box_t) BOX_POS_SIZE(120, 100, 220, 180); task->base_width = box.x1 - box.x0; @@ -51,6 +51,7 @@ result_t text_create(wuss_t *wuss, &box, "Lorem Ipsum", 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 */ + palette_PICO8_BLUE, &delegate, box.x1 - box.x0, box.y1 - box.y0, diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 7e748676..3b2b63e8 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -588,6 +588,7 @@ result_t wuss_test(const char *resources) &box_a, "toosmall", wuss_WINDOW_NONE, + wuss_NO_BACKGROUND, NULL, box_a.x1 - box_a.x0, box_a.y1 - box_a.y0, @@ -602,7 +603,6 @@ result_t wuss_test(const char *resources) tc_a.open_count = 0; delegate_a.handle = test_handle; delegate_a.task_data = &tc_a; - delegate_a.bg = wuss_NO_BACKGROUND; box_a.x0 = 0; box_a.y0 = 0; @@ -614,6 +614,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_BACK | wuss_WINDOW_NO_TOGGLE_SIZE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, + wuss_NO_BACKGROUND, &delegate_a, box_a.x1 - box_a.x0, box_a.y1 - box_a.y0, @@ -625,7 +626,6 @@ result_t wuss_test(const char *resources) tc_b.mouse_count = 0; delegate_b.handle = test_handle; delegate_b.task_data = &tc_b; - delegate_b.bg = wuss_NO_BACKGROUND; box_b.x0 = 50; box_b.y0 = 50; @@ -637,6 +637,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_BACK | wuss_WINDOW_NO_TOGGLE_SIZE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, + wuss_NO_BACKGROUND, &delegate_b, box_b.x1 - box_b.x0, box_b.y1 - box_b.y0, @@ -913,7 +914,6 @@ result_t wuss_test(const char *resources) tc_d.mouse_count = 0; delegate_d.handle = test_handle; delegate_d.task_data = &tc_d; - delegate_d.bg = wuss_NO_BACKGROUND; box_d.x0 = 0; box_d.y0 = 160; box_d.x1 = 30; box_d.y1 = 175; /* shorter than the 20px titlebar_height, still valid: no titlebar to fit */ @@ -923,6 +923,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, + wuss_NO_BACKGROUND, &delegate_d, box_d.x1 - box_d.x0, box_d.y1 - box_d.y0, @@ -961,7 +962,6 @@ result_t wuss_test(const char *resources) tc_e.mouse_count = 0; delegate_e.handle = test_handle; delegate_e.task_data = &tc_e; - delegate_e.bg = wuss_NO_BACKGROUND; box_e.x0 = 100; box_e.y0 = 0; box_e.x1 = 150; box_e.y1 = 50; @@ -971,6 +971,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, + wuss_NO_BACKGROUND, &delegate_e, box_e.x1 - box_e.x0, box_e.y1 - box_e.y0, @@ -982,7 +983,6 @@ result_t wuss_test(const char *resources) tc_f.mouse_count = 0; delegate_f.handle = test_handle; delegate_f.task_data = &tc_f; - delegate_f.bg = wuss_NO_BACKGROUND; box_f.x0 = 130; box_f.y0 = 20; box_f.x1 = 180; box_f.y1 = 70; @@ -992,6 +992,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, + wuss_NO_BACKGROUND, &delegate_f, box_f.x1 - box_f.x0, box_f.y1 - box_f.y0, @@ -1050,7 +1051,6 @@ result_t wuss_test(const char *resources) tc_h.mouse_count = 0; delegate_h.handle = test_handle; delegate_h.task_data = &tc_h; - delegate_h.bg = wuss_NO_BACKGROUND; box_h.x0 = 10; box_h.y0 = 10; box_h.x1 = 30; box_h.y1 = 30; @@ -1060,6 +1060,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, + wuss_NO_BACKGROUND, &delegate_h, box_h.x1 - box_h.x0, box_h.y1 - box_h.y0, @@ -1071,7 +1072,6 @@ result_t wuss_test(const char *resources) tc_g.mouse_count = 0; delegate_g.handle = test_handle; delegate_g.task_data = &tc_g; - delegate_g.bg = wuss_NO_BACKGROUND; box_g.x0 = 0; box_g.y0 = 0; box_g.x1 = 150; box_g.y1 = 150; /* G is created after H, so G is topmost and fully covers H */ @@ -1081,6 +1081,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, + wuss_NO_BACKGROUND, &delegate_g, box_g.x1 - box_g.x0, box_g.y1 - box_g.y0, @@ -1133,7 +1134,6 @@ result_t wuss_test(const char *resources) tc_i.mouse_count = 0; delegate_i.handle = test_handle; delegate_i.task_data = &tc_i; - delegate_i.bg = wuss_NO_BACKGROUND; box_i.x0 = 0; box_i.y0 = 0; box_i.x1 = 100; box_i.y1 = 100; @@ -1143,6 +1143,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, + wuss_NO_BACKGROUND, &delegate_i, box_i.x1 - box_i.x0, box_i.y1 - box_i.y0, @@ -1154,7 +1155,6 @@ result_t wuss_test(const char *resources) tc_j.mouse_count = 0; delegate_j.handle = test_handle; delegate_j.task_data = &tc_j; - delegate_j.bg = wuss_NO_BACKGROUND; box_j.x0 = 50; box_j.y0 = 0; box_j.x1 = 150; box_j.y1 = 100; /* J created after I, so J is topmost, covering I's right half */ @@ -1164,6 +1164,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, + wuss_NO_BACKGROUND, &delegate_j, box_j.x1 - box_j.x0, box_j.y1 - box_j.y0, @@ -1205,7 +1206,6 @@ result_t wuss_test(const char *resources) tc_m.mouse_count = 0; delegate_m.handle = test_handle; delegate_m.task_data = &tc_m; - delegate_m.bg = wuss_NO_BACKGROUND; box_m.x0 = 10; box_m.y0 = 10; box_m.x1 = 60; box_m.y1 = 60; /* 50x50, fully on-screen, topmost (created last) */ @@ -1215,6 +1215,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, + wuss_NO_BACKGROUND, &delegate_m, box_m.x1 - box_m.x0, box_m.y1 - box_m.y0, @@ -1260,7 +1261,6 @@ result_t wuss_test(const char *resources) tc_h.mouse_count = 0; delegate_h.handle = test_handle; delegate_h.task_data = &tc_h; - delegate_h.bg = wuss_NO_BACKGROUND; box_h.x0 = 130; box_h.y0 = 50; box_h.x1 = 190; box_h.y1 = 100; @@ -1268,6 +1268,7 @@ result_t wuss_test(const char *resources) &box_h, "H", wuss_WINDOW_NONE, + wuss_NO_BACKGROUND, &delegate_h, box_h.x1 - box_h.x0, box_h.y1 - box_h.y0, @@ -1279,7 +1280,6 @@ result_t wuss_test(const char *resources) tc_g.mouse_count = 0; delegate_g.handle = test_handle; delegate_g.task_data = &tc_g; - delegate_g.bg = wuss_NO_BACKGROUND; box_g.x0 = 110; box_g.y0 = 30; box_g.x1 = 160; box_g.y1 = 80; @@ -1287,6 +1287,7 @@ result_t wuss_test(const char *resources) &box_g, "G", wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL, + wuss_NO_BACKGROUND, &delegate_g, box_g.x1 - box_g.x0, box_g.y1 - box_g.y0, @@ -1406,11 +1407,11 @@ result_t wuss_test(const char *resources) tc_t.mouse_count = 0; delegate_t.handle = test_handle; delegate_t.task_data = &tc_t; - delegate_t.bg = wuss_NO_BACKGROUND; 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 200x200 doc */ rc = wuss_window_create(wuss, &box_t_win, "T", wuss_WINDOW_NONE, + wuss_NO_BACKGROUND, &delegate_t, 200, 200, &win_t); if (rc != result_OK) goto Failure; @@ -1571,11 +1572,11 @@ result_t wuss_test(const char *resources) tc_r.mouse_count = 0; delegate_r.handle = test_handle; delegate_r.task_data = &tc_r; - delegate_r.bg = wuss_NO_BACKGROUND; 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(wuss, &box_r, "R", wuss_WINDOW_NONE, + wuss_NO_BACKGROUND, &delegate_r, 70, 70, &win_r); if (rc != result_OK) goto Failure; @@ -1668,11 +1669,11 @@ result_t wuss_test(const char *resources) tc_nb.mouse_count = 0; delegate_nb.handle = test_handle; delegate_nb.task_data = &tc_nb; - delegate_nb.bg = wuss_NO_BACKGROUND; box_nb.x0 = 10; box_nb.y0 = 10; box_nb.x1 = 50; box_nb.y1 = 50; /* 40x40 content, room to grow to a 200x200 doc */ rc = wuss_window_create(wuss, &box_nb, "NB", wuss_WINDOW_NO_RESIZE_BLIT, + wuss_NO_BACKGROUND, &delegate_nb, 200, 200, &win_nb); if (rc != result_OK) goto Failure; @@ -1747,11 +1748,10 @@ result_t wuss_test(const char *resources) tc_u.mouse_count = 0; delegate_u.handle = test_handle; delegate_u.task_data = &tc_u; - delegate_u.bg = wuss_NO_BACKGROUND; box_u.x0 = 80; box_u.y0 = 80; box_u.x1 = 120; box_u.y1 = 120; /* 40x40 content */ - rc = wuss_window_create(wuss, &box_u, "U", wuss_WINDOW_NONE, /* scrollbars on: carve.x/y = icon size */ + rc = wuss_window_create(wuss, &box_u, "U", wuss_WINDOW_NONE, wuss_NO_BACKGROUND, /* scrollbars on: carve.x/y = icon size */ &delegate_u, 70, 70, &win_u); /* doc size well within the 200x200 screen: growth is doc-limited, not screen-limited */ if (rc != result_OK) goto Failure; @@ -1845,7 +1845,6 @@ result_t wuss_test(const char *resources) tc_v.mouse_count = 0; delegate_v.handle = test_handle; delegate_v.task_data = &tc_v; - delegate_v.bg = wuss_NO_BACKGROUND; box_v.x0 = 10; box_v.y0 = 10; box_v.x1 = 70; box_v.y1 = 70; /* 60x60 content -- wide enough titlebar that @@ -1859,6 +1858,7 @@ result_t wuss_test(const char *resources) * Doc big enough that maximize is * screen-limited, not doc-limited. */ rc = wuss_window_create(wuss, &box_v, "V", wuss_WINDOW_NONE, + wuss_NO_BACKGROUND, &delegate_v, 200, 200, &win_v); if (rc != result_OK) goto Failure; @@ -1961,7 +1961,6 @@ result_t wuss_test(const char *resources) tc_k.mouse_count = 0; delegate_k.handle = test_handle; delegate_k.task_data = &tc_k; - delegate_k.bg = wuss_NO_BACKGROUND; box_k.x0 = 0; box_k.y0 = 140; /* clear of the still-open A/B windows above */ box_k.x1 = 50; box_k.y1 = 175; @@ -1971,6 +1970,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_BACK | wuss_WINDOW_NO_TOGGLE_SIZE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, + wuss_NO_BACKGROUND, &delegate_k, box_k.x1 - box_k.x0, box_k.y1 - box_k.y0, @@ -1982,7 +1982,6 @@ result_t wuss_test(const char *resources) tc_l.mouse_count = 0; delegate_l.handle = test_handle; delegate_l.task_data = &tc_l; - delegate_l.bg = wuss_NO_BACKGROUND; box_l.x0 = 120; box_l.y0 = 140; /* well clear of K, so never overlaps it */ box_l.x1 = 170; box_l.y1 = 175; @@ -1992,6 +1991,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_BACK | wuss_WINDOW_NO_TOGGLE_SIZE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, + wuss_NO_BACKGROUND, &delegate_l, box_l.x1 - box_l.x0, box_l.y1 - box_l.y0, @@ -2050,7 +2050,6 @@ result_t wuss_test(const char *resources) tc_m2.mouse_count = 0; delegate_m2.handle = test_handle; delegate_m2.task_data = &tc_m2; - delegate_m2.bg = wuss_NO_BACKGROUND; box_m2.x0 = 0; box_m2.y0 = 0; box_m2.x1 = 40; box_m2.y1 = 40; @@ -2059,6 +2058,7 @@ result_t wuss_test(const char *resources) "M2", wuss_WINDOW_NO_BACK | wuss_WINDOW_NO_TOGGLE_SIZE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL, + wuss_NO_BACKGROUND, &delegate_m2, box_m2.x1 - box_m2.x0, box_m2.y1 - box_m2.y0, @@ -2147,11 +2147,11 @@ result_t wuss_test(const char *resources) tc_nb2.mouse_count = 0; delegate_nb2.handle = test_handle; delegate_nb2.task_data = &tc_nb2; - delegate_nb2.bg = wuss_NO_BACKGROUND; box_nb2.x0 = 0; box_nb2.y0 = 0; box_nb2.x1 = 40; box_nb2.y1 = 40; rc = wuss_window_create(wuss, &box_nb2, "NB2", wuss_WINDOW_NO_RESIZE_BLIT, + wuss_NO_BACKGROUND, &delegate_nb2, box_nb2.x1 - box_nb2.x0, box_nb2.y1 - box_nb2.y0, @@ -2196,7 +2196,6 @@ result_t wuss_test(const char *resources) tc_n.mouse_count = 0; delegate_n.handle = test_handle; delegate_n.task_data = &tc_n; - delegate_n.bg = wuss_NO_BACKGROUND; box_n.x0 = 0; box_n.y0 = 140; /* clear of any occluder to start */ box_n.x1 = 60; box_n.y1 = 170; @@ -2204,6 +2203,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_BACK | wuss_WINDOW_NO_TOGGLE_SIZE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, + wuss_NO_BACKGROUND, &delegate_n, box_n.x1 - box_n.x0, box_n.y1 - box_n.y0, @@ -2215,7 +2215,6 @@ result_t wuss_test(const char *resources) tc_o.mouse_count = 0; delegate_o.handle = test_handle; delegate_o.task_data = &tc_o; - delegate_o.bg = wuss_NO_BACKGROUND; box_o.x0 = 90; box_o.y0 = 140; /* N will be dragged partly on top of O */ box_o.x1 = 130; box_o.y1 = 180; @@ -2223,6 +2222,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_BACK | wuss_WINDOW_NO_TOGGLE_SIZE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, + wuss_NO_BACKGROUND, &delegate_o, box_o.x1 - box_o.x0, box_o.y1 - box_o.y0, @@ -2300,7 +2300,6 @@ result_t wuss_test(const char *resources) tc_b.mouse_count = 0; delegate_b.handle = test_handle; delegate_b.task_data = &tc_b; - delegate_b.bg = wuss_NO_BACKGROUND; box_b.x0 = 20; box_b.y0 = 10; /* left half will sit under A */ box_b.x1 = 80; box_b.y1 = 50; @@ -2308,6 +2307,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, + wuss_NO_BACKGROUND, &delegate_b, box_b.x1 - box_b.x0, box_b.y1 - box_b.y0, @@ -2319,7 +2319,6 @@ result_t wuss_test(const char *resources) tc_a.mouse_count = 0; delegate_a.handle = test_handle; delegate_a.task_data = &tc_a; - delegate_a.bg = wuss_NO_BACKGROUND; box_a.x0 = 0; box_a.y0 = 0; /* created after B, so A is topmost */ box_a.x1 = 40; box_a.y1 = 100; @@ -2327,6 +2326,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, + wuss_NO_BACKGROUND, &delegate_a, box_a.x1 - box_a.x0, box_a.y1 - box_a.y0, @@ -2403,7 +2403,6 @@ result_t wuss_test(const char *resources) tc_b.mouse_count = 0; delegate_b.handle = test_handle; delegate_b.task_data = &tc_b; - delegate_b.bg = wuss_NO_BACKGROUND; box_b.x0 = 0; box_b.y0 = 0; /* right part sits under A throughout */ box_b.x1 = 60; box_b.y1 = 40; @@ -2411,6 +2410,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, + wuss_NO_BACKGROUND, &delegate_b, box_b.x1 - box_b.x0, box_b.y1 - box_b.y0, @@ -2422,7 +2422,6 @@ result_t wuss_test(const char *resources) tc_a.mouse_count = 0; delegate_a.handle = test_handle; delegate_a.task_data = &tc_a; - delegate_a.bg = wuss_NO_BACKGROUND; box_a.x0 = 40; box_a.y0 = 0; /* created after B, so A is topmost */ box_a.x1 = 100; box_a.y1 = 40; @@ -2430,6 +2429,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, + wuss_NO_BACKGROUND, &delegate_a, box_a.x1 - box_a.x0, box_a.y1 - box_a.y0, @@ -2483,7 +2483,6 @@ result_t wuss_test(const char *resources) tc_b.mouse_count = 0; delegate_b.handle = test_handle; delegate_b.task_data = &tc_b; - delegate_b.bg = wuss_NO_BACKGROUND; box_b.x0 = 0; box_b.y0 = 0; /* middle band sits under A */ box_b.x1 = 60; box_b.y1 = 60; @@ -2491,6 +2490,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, + wuss_NO_BACKGROUND, &delegate_b, box_b.x1 - box_b.x0, box_b.y1 - box_b.y0, @@ -2502,7 +2502,6 @@ result_t wuss_test(const char *resources) tc_a.mouse_count = 0; delegate_a.handle = test_handle; delegate_a.task_data = &tc_a; - delegate_a.bg = wuss_NO_BACKGROUND; box_a.x0 = 0; box_a.y0 = 20; /* created after B, so A is topmost */ box_a.x1 = 60; box_a.y1 = 40; @@ -2510,6 +2509,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, + wuss_NO_BACKGROUND, &delegate_a, box_a.x1 - box_a.x0, box_a.y1 - box_a.y0, @@ -2596,7 +2596,6 @@ result_t wuss_test(const char *resources) tc_b.mouse_count = 0; delegate_b.handle = test_handle; delegate_b.task_data = &tc_b; - delegate_b.bg = wuss_NO_BACKGROUND; box_b.x0 = 80; box_b.y0 = 80; /* corner already under A */ box_b.x1 = 140; box_b.y1 = 140; @@ -2604,6 +2603,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, + wuss_NO_BACKGROUND, &delegate_b, box_b.x1 - box_b.x0, box_b.y1 - box_b.y0, @@ -2615,7 +2615,6 @@ result_t wuss_test(const char *resources) tc_a.mouse_count = 0; delegate_a.handle = test_handle; delegate_a.task_data = &tc_a; - delegate_a.bg = wuss_NO_BACKGROUND; box_a.x0 = 0; box_a.y0 = 0; /* created after B, so A is topmost */ box_a.x1 = 100; box_a.y1 = 100; @@ -2623,6 +2622,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, + wuss_NO_BACKGROUND, &delegate_a, box_a.x1 - box_a.x0, box_a.y1 - box_a.y0, @@ -2688,7 +2688,6 @@ result_t wuss_test(const char *resources) tc_c.mouse_count = 0; delegate_c.handle = test_handle; delegate_c.task_data = &tc_c; - delegate_c.bg = wuss_NO_BACKGROUND; box_c.x0 = 0; box_c.y0 = 0; @@ -2700,6 +2699,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_BACK | wuss_WINDOW_NO_TOGGLE_SIZE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, + wuss_NO_BACKGROUND, &delegate_c, box_c.x1 - box_c.x0, box_c.y1 - box_c.y0, diff --git a/libraries/wuss/window/create.c b/libraries/wuss/window/create.c index fa3cdc03..45d0a830 100644 --- a/libraries/wuss/window/create.c +++ b/libraries/wuss/window/create.c @@ -14,6 +14,7 @@ result_t wuss_window_create(wuss_t *wuss, const box_t *content, const char *title, wuss_window_flags_t flags, + wuss_colour_t bg, const wuss_task_t *task, int doc_width, int doc_height, @@ -84,14 +85,12 @@ result_t wuss_window_create(wuss_t *wuss, else memset(&win->task, 0, sizeof(win->task)); - if (task == NULL) - win->task.bg = wuss_NO_BACKGROUND; - else if (task->bg != wuss_NO_BACKGROUND && - (task->bg < 0 || task->bg >= wuss->npalette)) + if (bg != wuss_NO_BACKGROUND && (bg < 0 || bg >= wuss->npalette)) { free(win); return result_WUSS_BAD_COLOUR; } + win->bg = bg; if (title != NULL) { diff --git a/libraries/wuss/window/set-background.c b/libraries/wuss/window/set-background.c index 92ee232f..8cfe7cb0 100644 --- a/libraries/wuss/window/set-background.c +++ b/libraries/wuss/window/set-background.c @@ -9,7 +9,7 @@ result_t wuss_window_set_background(wuss_window_t *window, wuss_colour_t bg) if (bg != wuss_NO_BACKGROUND && (bg < 0 || bg >= window->wuss->npalette)) return result_WUSS_BAD_COLOUR; - window->task.bg = bg; + window->bg = bg; wuss__content_box(window, &content); wuss__invalidate_clipped(window, &content); From 665a3d1ffb96a443372c32fae29fc580f20df24f Mon Sep 17 00:00:00 2001 From: David Thomas Date: Fri, 28 Aug 2026 00:33:16 +0100 Subject: [PATCH 029/287] docs(wuss): reformat wuss.h and window.h to 80 columns Doxygen comments and enum/struct member docs wrapped with nice greedy-fill wrapping, matching the description-column-aligned param style used elsewhere (cache.h, screen.h). --- include/wuss/window.h | 61 +++++++++++++++++----- include/wuss/wuss.h | 115 ++++++++++++++++++++++++++++++++---------- 2 files changed, 136 insertions(+), 40 deletions(-) diff --git a/include/wuss/window.h b/include/wuss/window.h index e9869b6d..a8e1a9e3 100644 --- a/include/wuss/window.h +++ b/include/wuss/window.h @@ -54,9 +54,23 @@ typedef struct wuss_event struct { screen_t *scr; - const box_t *content; /**< The region that actually needs repainting, screen space; a subset of bounds. Tasks should only touch pixels within this box. */ - const box_t *bounds; /**< The window's full (unclipped) content-area box, screen space, as per wuss_window_get_content_bounds; for converting screen position to document position. */ - point_t scroll; /**< Current scroll offset, as per wuss_window_get_scroll. */ + + /** + * The region that actually needs repainting, screen space; a + * subset of bounds. Tasks should only touch pixels within this + * box. + */ + const box_t *content; + + /** + * The window's full (unclipped) content-area box, screen space, + * as per wuss_window_get_content_bounds; for converting screen + * position to document position. + */ + const box_t *bounds; + + /** Current scroll offset, as per wuss_window_get_scroll. */ + point_t scroll; } redraw; @@ -102,7 +116,12 @@ typedef result_t (wuss_event_fn_t)(wuss_window_t *window, /** A window's content delegate. Copied by value into the window at creation. */ typedef struct wuss_task { - wuss_event_fn_t *handle; /**< NULL => task receives no events; Wuss still fills the content background per wuss_window_create's bg. */ + /** + * NULL => task receives no events; Wuss still fills the content + * background per wuss_window_create's bg. + */ + wuss_event_fn_t *handle; + void *task_data; } wuss_task_t; @@ -110,7 +129,8 @@ wuss_task_t; /** * Build a wuss_task_t from its fields. * - * \param[in] handle Event callback, or NULL for a task that receives no events. + * \param[in] handle Event callback, or NULL for a task that receives + * no events. * \param[in] task_data Opaque pointer passed back to the callback. * \return The populated task. */ @@ -154,13 +174,27 @@ result_t wuss_idle(wuss_t *wuss); * instead. The bottom/right edges are not clamped. * * \param[in] wuss Window manager to create the window on. - * \param[in] content Requested content-area bounds, screen space. Copied in. - * \param[in] title Titlebar label, or NULL for none. Copied in, truncated if too long. Ignored if flags includes 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. - * \param[in] bg Content background, filled by Wuss before each redraw, or wuss_NO_BACKGROUND for the task to draw its own background (avoids a redundant fill behind an opaque task). Changeable later via wuss_window_set_background. - * \param[in] task Content delegate. Copied in. May be NULL for a window with no content handling. - * \param[in] doc_width Virtual document width, for the horizontal scrollbar's sausage proportion; pass content's own width for a window with nothing to scroll. - * \param[in] doc_height Virtual document height, for the vertical scrollbar's sausage proportion; pass content's own height for a window with nothing to scroll. + * \param[in] content Requested content-area bounds, screen space. Copied + * in. + * \param[in] title Titlebar label, or NULL for none. Copied in, + * truncated if too long. Ignored if flags includes + * 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. + * \param[in] bg Content background, filled by Wuss before each + * redraw, or wuss_NO_BACKGROUND for the task to draw + * its own background (avoids a redundant fill behind + * an opaque task). Changeable later via + * wuss_window_set_background. + * \param[in] task Content delegate. Copied in. May be NULL for a + * window with no content handling. + * \param[in] doc_width Virtual document width, for the horizontal + * scrollbar's sausage proportion; pass content's own + * width for a window with nothing to scroll. + * \param[in] doc_height Virtual document height, for the vertical + * scrollbar's sausage proportion; pass content's own + * height for a window with nothing to scroll. * \param[out] window Newly created window. Becomes the topmost window. * \return \ref result_OK on success, \ref result_WUSS_TOO_SMALL if content's * width or height is not positive, \ref result_WUSS_BAD_COLOUR if @@ -248,7 +282,8 @@ void wuss_window_invalidate(wuss_window_t *window, const box_t *local_box); /** Mark a window's whole content area as dirty. Shorthand for * wuss_window_invalidate(window, NULL). */ -#define wuss_window_invalidate_all(window) wuss_window_invalidate((window), NULL) +#define wuss_window_invalidate_all(window) \ + wuss_window_invalidate((window), NULL) /** * Set a window's scroll offset: the point in virtual content space that diff --git a/include/wuss/wuss.h b/include/wuss/wuss.h index ce1f8935..f201ba25 100644 --- a/include/wuss/wuss.h +++ b/include/wuss/wuss.h @@ -23,8 +23,10 @@ extern "C" /* ----------------------------------------------------------------------- */ -#define result_WUSS_TOO_SMALL (result_BASE_WUSS + 0) /* Window/resize dimensions too small */ -#define result_WUSS_BAD_COLOUR (result_BASE_WUSS + 1) /* A palette index was out of range */ +/** Window/resize dimensions too small. */ +#define result_WUSS_TOO_SMALL (result_BASE_WUSS + 0) +/** A palette index was out of range. */ +#define result_WUSS_BAD_COLOUR (result_BASE_WUSS + 1) /* ----------------------------------------------------------------------- */ @@ -55,10 +57,15 @@ typedef enum wuss_mouse_action } wuss_mouse_action_t; -/** An index into a wuss_t's system palette (see wuss_create). Not a colour_t. */ +/** + * An index into a wuss_t's system palette (see wuss_create). Not a colour_t. + */ typedef int wuss_colour_t; -/** Sentinel for wuss_window_create's bg meaning "no automatic background fill". */ +/** + * Sentinel for wuss_window_create's bg meaning "no automatic background + * fill". + */ #define wuss_NO_BACKGROUND ((wuss_colour_t) -1) /** Furniture chrome colours, one entry per class of furniture. Title is @@ -86,16 +93,53 @@ wuss_palette_t; /** Per-window appearance flags, combinable with bitwise OR. */ typedef enum wuss_window_flags { - wuss_WINDOW_NONE = 0, /**< Default: every furniture region drawn. */ - wuss_WINDOW_NO_TITLEBAR = 1 << 0, /**< No titlebar; content fills the full visible area, and no drag handle exists. */ - wuss_WINDOW_NO_OUTLINE = 1 << 1, /**< No 1px border drawn around the visible area. */ - wuss_WINDOW_NO_CLOSE = 1 << 2, /**< No close icon in the titlebar. Ignored if flags includes wuss_WINDOW_NO_TITLEBAR. */ - wuss_WINDOW_NO_BACK = 1 << 3, /**< No send-to-back icon in the titlebar. Ignored if flags includes wuss_WINDOW_NO_TITLEBAR. */ - wuss_WINDOW_NO_TOGGLE_SIZE = 1 << 4, /**< No toggle-size icon in the titlebar. Ignored if flags includes wuss_WINDOW_NO_TITLEBAR. */ - wuss_WINDOW_NO_VSCROLL = 1 << 5, /**< No vertical scrollbar on the right edge. */ - wuss_WINDOW_NO_HSCROLL = 1 << 6, /**< No horizontal scrollbar on the bottom edge. */ - wuss_WINDOW_NO_RESIZE = 1 << 7, /**< No resize icon in the bottom-right corner. */ - wuss_WINDOW_NO_RESIZE_BLIT = 1 << 8 /**< 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 the window's size in ways redraw can't patch incrementally (e.g. a palette that lays itself out across the whole window). */ + /** Default: every furniture region drawn. */ + wuss_WINDOW_NONE = 0, + + /** + * No titlebar; content fills the full visible area, and no drag handle + * exists. + */ + wuss_WINDOW_NO_TITLEBAR = 1 << 0, + + /** No 1px border drawn around the visible area. */ + wuss_WINDOW_NO_OUTLINE = 1 << 1, + + /** + * No close icon in the titlebar. Ignored if flags includes + * wuss_WINDOW_NO_TITLEBAR. + */ + wuss_WINDOW_NO_CLOSE = 1 << 2, + + /** + * No send-to-back icon in the titlebar. Ignored if flags includes + * wuss_WINDOW_NO_TITLEBAR. + */ + wuss_WINDOW_NO_BACK = 1 << 3, + + /** + * No toggle-size icon in the titlebar. Ignored if flags includes + * wuss_WINDOW_NO_TITLEBAR. + */ + wuss_WINDOW_NO_TOGGLE_SIZE = 1 << 4, + + /** No vertical scrollbar on the right edge. */ + wuss_WINDOW_NO_VSCROLL = 1 << 5, + + /** No horizontal scrollbar on the bottom edge. */ + wuss_WINDOW_NO_HSCROLL = 1 << 6, + + /** No resize icon in the bottom-right corner. */ + wuss_WINDOW_NO_RESIZE = 1 << 7, + + /** + * 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 the window's size in ways redraw can't patch + * incrementally (e.g. a palette that lays itself out across the whole + * window). + */ + wuss_WINDOW_NO_RESIZE_BLIT = 1 << 8 } wuss_window_flags_t; @@ -111,18 +155,28 @@ wuss_zorder_t; /** Optional creation-time configuration. */ typedef struct wuss_config { - int titlebar_height; /**< Titlebar height in pixels, or 0 to derive from font metrics (or a built-in fallback if no font). */ - wuss_palette_t palette; /**< Furniture chrome colours. */ + /** + * Titlebar height in pixels, or 0 to derive from font metrics (or a + * built-in fallback if no font). + */ + int titlebar_height; + + /** Furniture chrome colours. */ + wuss_palette_t palette; } wuss_config_t; /** * Create a window manager. * - * \param[in] scr Screen to draw windows onto. Not owned; must outlive the wuss_t. - * \param[in] font Font used to draw titlebar labels, or NULL to draw titlebars unlabelled. Not owned. - * \param[in] palette System palette, copied in, or NULL to use a built-in default palette. - * \param[in] npalette Number of entries in palette. Ignored if palette is NULL. + * \param[in] scr Screen to draw windows onto. Not owned; must + * outlive the wuss_t. + * \param[in] font Font used to draw titlebar labels, or NULL to draw + * titlebars unlabelled. Not owned. + * \param[in] palette System palette, copied in, or NULL to use a + * built-in default palette. + * \param[in] npalette Number of entries in palette. Ignored if palette + * is NULL. * \param[in] config Creation-time configuration, or NULL for defaults. * \param[out] wuss Newly created window manager. * \return \ref result_OK on success, \ref result_WUSS_BAD_COLOUR if any of @@ -207,7 +261,8 @@ int wuss_get_dirty_count(const wuss_t *wuss); * before calling wuss_redraw_dirty. * * \param[in] wuss Window manager. - * \param[in] index Index of the region to fetch, 0 to wuss_get_dirty_count() - 1. + * \param[in] index Index of the region to fetch, 0 to + * wuss_get_dirty_count() - 1. * \param[out] out Filled in with the dirty region. */ void wuss_get_dirty(const wuss_t *wuss, int index, box_t *out); @@ -226,8 +281,10 @@ void wuss_get_dirty(const wuss_t *wuss, int index, box_t *out); * \param[in] p Screen coordinate. * \param[in] button Button pressed or released. * \param[in] action wuss_MOUSE_DOWN or wuss_MOUSE_UP. - * \param[out] hit Window under the pointer (or being dragged), or NULL if none. May be NULL if not needed. - * \return \ref result_OK, or a result code returned by the task's mouse callback. + * \param[out] hit Window under the pointer (or being dragged), or NULL if + * none. May be NULL if not needed. + * \return \ref result_OK, or a result code returned by the task's mouse + * callback. */ result_t wuss_mouse_click(wuss_t *wuss, point_t p, @@ -243,8 +300,10 @@ result_t wuss_mouse_click(wuss_t *wuss, * * \param[in] wuss Window manager. * \param[in] p Screen coordinate. - * \param[out] hit Window under the pointer (or being dragged), or NULL if none. May be NULL if not needed. - * \return \ref result_OK, or a result code returned by the task's mouse callback. + * \param[out] hit Window under the pointer (or being dragged), or NULL if + * none. May be NULL if not needed. + * \return \ref result_OK, or a result code returned by the task's mouse + * callback. */ result_t wuss_mouse_move(wuss_t *wuss, point_t p, wuss_window_t **hit); @@ -257,8 +316,10 @@ result_t wuss_mouse_move(wuss_t *wuss, point_t p, wuss_window_t **hit); * \param[in] wuss Window manager. * \param[in] p Screen coordinate. * \param[in] delta Scroll amount; sign and units are caller-defined. - * \param[out] hit Window under the pointer, or NULL if none. May be NULL if not needed. - * \return \ref result_OK, or a result code returned by the task's scroll callback. + * \param[out] hit Window under the pointer, or NULL if none. May be NULL + * if not needed. + * \return \ref result_OK, or a result code returned by the task's scroll + * callback. */ result_t wuss_scroll(wuss_t *wuss, point_t p, From 6940e01f9f4ff9aefdd229eb6009661344b5c23d Mon Sep 17 00:00:00 2001 From: David Thomas Date: Fri, 28 Aug 2026 00:44:35 +0100 Subject: [PATCH 030/287] build(tools): add wrap_doxygen.py to rewrap Doxygen comments to 80 cols Reflows /** ... */ blocks (\param/\return/\brief/prose paragraphs) greedy-fill, continuation lines aligned under the description column, matching the style used by hand in wuss.h/window.h. Leaves /**< trailing member comments and overlong code lines alone (those need structural judgement) and reports them for manual follow-up. --- tools/test_wrap_doxygen.py | 38 +++++++++ tools/wrap_doxygen.py | 166 +++++++++++++++++++++++++++++++++++++ 2 files changed, 204 insertions(+) create mode 100644 tools/test_wrap_doxygen.py create mode 100644 tools/wrap_doxygen.py diff --git a/tools/test_wrap_doxygen.py b/tools/test_wrap_doxygen.py new file mode 100644 index 00000000..b0fe1a5e --- /dev/null +++ b/tools/test_wrap_doxygen.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +"""Smallest possible smoke test for wrap_doxygen.py. Run directly: +python3 tools/test_wrap_doxygen.py +""" +from wrap_doxygen import process + +src = '''/** + * A short line. + * + * \\param[in] thing This description is deliberately long enough that it has to wrap across more than one line to fit. + * \\return Also deliberately long enough that this return description needs to wrap across two lines at least. + */ +int f(int thing); + +/** This single-line comment is deliberately long enough to need promoting to a multi-line block comment. */ +int g(void); +'''.splitlines() + +out, overflow = process(src, width=80) + +assert overflow == [], overflow +assert all(len(l) <= 80 for l in out), [l for l in out if len(l) > 80] +assert out[0] == '/**' +assert out[1] == ' * A short line.' +# continuation lines must keep the "*" one column right of "/**", with a +# space after it -- regression check for the "* text" (no leading space) +# alignment bug this script originally had. +cont = [l for l in out if 'wrap across more than' in l] +assert cont and cont[0][:3] == ' * ', repr(cont) +assert 'int g(void);' in out +assert out.count('/**') == 2 # original block + the promoted single-liner + +# idempotent: reformatting already-reformatted text changes nothing +out2, overflow2 = process(out, width=80) +assert out2 == out +assert overflow2 == [] + +print('ok') diff --git a/tools/wrap_doxygen.py b/tools/wrap_doxygen.py new file mode 100644 index 00000000..f64906ba --- /dev/null +++ b/tools/wrap_doxygen.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python3 +"""wrap_doxygen.py -- rewrap Doxygen /** ... */ block comments to a column +limit, greedy-fill, continuation lines aligned under the description text +(matching the \\param/\\return style already used across DPTLib headers). + +Scope (ponytail: lazy on purpose): + - Rewraps multi-line /** ... */ blocks: \\param/\\return/\\brief/\\file/etc + tag lines, and plain prose lines, each as one reflow-able paragraph. + - Promotes an overlong single-line /** ... */ comment into a multi-line + block. + - Leaves /**< ... */ trailing member comments and non-comment code lines + alone even if too long -- fixing those means moving the comment above + the declaration and touching struct/enum layout, a structural call this + script doesn't make. Still reported so a human can look. +""" +import argparse +import re +import sys +import textwrap + +OPEN_RE = re.compile(r'^(\s*)/\*\*\s*$') +CLOSE_RE = re.compile(r'^(\s*)\*/\s*$') +BLANK_RE = re.compile(r'^\s*\*\s*$') +SINGLE_LINE_RE = re.compile(r'^(\s*)/\*\*\s+(.*\S)\s+\*/\s*$') +PARAM_RE = re.compile(r'^(\s*\*\s*\\param(?:\[[^\]]*\])?\s+\S+\s+)(.*)$') +TAG_RE = re.compile( + r'^(\s*\*\s*\\(?:return|brief|file|note|warning|pre|post|throws?)\b\s*)(.*)$') +PROSE_RE = re.compile(r'^(\s*\*\s+)(\S.*)$') + + +def cont_prefix(prefix1, indent): + return indent + '*' + ' ' * (len(prefix1) - len(indent) - 1) + + +def fill(prefix1, words, width, indent): + text = ' '.join(w for w in words if w) + cont = cont_prefix(prefix1, indent) + lines = textwrap.wrap(text, width=width, initial_indent=prefix1, + subsequent_indent=cont, break_long_words=False, + break_on_hyphens=False) + return lines or [prefix1.rstrip()] + + +def wrap_single_line_comment(indent, text, width): + body_width = width - len(indent) - 3 # " * " prefix + lines = textwrap.wrap(text, width=body_width, break_long_words=False, + break_on_hyphens=False) + out = [indent + '/**'] + out += [indent + ' * ' + l for l in lines] + out.append(indent + ' */') + return out + + +def process(lines, width): + out = [] + overflow = [] + i = 0 + n = len(lines) + while i < n: + line = lines[i].rstrip('\n') + + m = SINGLE_LINE_RE.match(line) + if m and '/**<' not in line and len(line) > width: + indent, text = m.groups() + out.extend(wrap_single_line_comment(indent, text, width)) + i += 1 + continue + + m = OPEN_RE.match(line) + if m: + out.append(line) + indent = m.group(1) + ' ' # body lines' "*" sits one col right of "/**" + i += 1 + pending = None # (prefix1, [words...]) + + def flush(): + if pending is not None: + out.extend(fill(pending[0], pending[1], width, indent)) + + while i < n: + body = lines[i].rstrip('\n') + + mclose = CLOSE_RE.match(body) + if mclose: + flush() + pending = None + out.append(body) + i += 1 + break + + if BLANK_RE.match(body): + flush() + pending = None + out.append(body) + i += 1 + continue + + mtag = PARAM_RE.match(body) or TAG_RE.match(body) + if mtag: + flush() + pending = (mtag.group(1), [mtag.group(2)]) + i += 1 + continue + + mprose = PROSE_RE.match(body) + if mprose: + if pending is None: + pending = (mprose.group(1), [mprose.group(2)]) + else: + pending[1].append(mprose.group(2)) + i += 1 + continue + + # Doesn't look like a normal comment-body line -- pass through. + flush() + pending = None + out.append(body) + i += 1 + else: + flush() + continue + + if len(line) > width: + overflow.append((i + 1, line)) + out.append(line) + i += 1 + + return out, overflow + + +def main(): + ap = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + ap.add_argument('files', nargs='+') + ap.add_argument('--width', type=int, default=80) + ap.add_argument('--check', action='store_true', + help="don't write; exit 1 if any file would change") + args = ap.parse_args() + + changed = False + for path in args.files: + with open(path, encoding='utf-8') as f: + original = f.readlines() + new_lines, overflow = process(original, args.width) + new_text = '\n'.join(new_lines) + '\n' + old_text = ''.join(original) + + if new_text != old_text: + changed = True + if args.check: + print(f'{path}: would reformat') + else: + with open(path, 'w', encoding='utf-8') as f: + f.write(new_text) + print(f'{path}: reformatted') + for lineno, text in overflow: + print(f'{path}:{lineno}: still over {args.width} cols ' + f'(needs manual restructuring): {text.strip()}', + file=sys.stderr) + + if args.check and changed: + return 1 + return 0 + + +if __name__ == '__main__': + sys.exit(main()) From 9ee1dced67742b9d4cc339f4632764937791299b Mon Sep 17 00:00:00 2001 From: David Thomas Date: Fri, 28 Aug 2026 00:48:51 +0100 Subject: [PATCH 031/287] docs: rewrap Doxygen comments to 80 columns across include/ Run tools/wrap_doxygen.py over every DPTLib-owned .h file (fortify/ excluded, vendored third-party code). /**< trailing member comments left untouched by design; flagged on stderr for manual follow-up. Co-Authored-By: Claude Sonnet 5 --- include/databases/digest-db.h | 6 +- include/databases/filename-db.h | 4 +- include/databases/pickle.h | 20 +++-- include/datastruct/atom.h | 39 +++++----- include/datastruct/bitarr.h | 26 +++---- include/datastruct/cache.h | 13 ++-- include/datastruct/hash.h | 17 ++-- include/datastruct/vector.h | 17 ++-- include/framebuf/bitmap.h | 3 +- include/framebuf/bmfont.h | 6 +- include/framebuf/curve.h | 15 ++-- include/framebuf/screen.h | 56 ++++++------- include/framebuf/span-registry.h | 3 +- include/framebuf/span.h | 15 ++-- include/geom/box.h | 16 ++-- include/geom/layout.h | 3 +- include/geom/line.h | 3 +- include/geom/packer.h | 4 +- include/io/path.h | 9 ++- include/io/stream-mem.h | 3 +- include/io/stream-mtfcomp.h | 6 +- include/io/stream-packbits.h | 6 +- include/io/stream-stdio.h | 3 +- include/io/stream.h | 3 +- include/text/txtfmt.h | 19 +++-- include/utils/array.h | 8 +- include/utils/pack.h | 40 +++++----- include/wuss/window.h | 130 +++++++++++++++---------------- include/wuss/wuss.h | 109 +++++++++++++------------- 29 files changed, 304 insertions(+), 298 deletions(-) diff --git a/include/databases/digest-db.h b/include/databases/digest-db.h index b771ea41..e91eaca2 100644 --- a/include/databases/digest-db.h +++ b/include/databases/digest-db.h @@ -5,9 +5,9 @@ * * Digest database. * - * digestdb is a wrapper around an atom set specifically for holding - * (128-bit) digests. It is used by tagdb and filenamedb to share the cost of - * storing digest values by replacing 128-bit digests with smaller indices. + * digestdb is a wrapper around an atom set specifically for holding (128-bit) + * digests. It is used by tagdb and filenamedb to share the cost of storing + * digest values by replacing 128-bit digests with smaller indices. */ #ifndef DATABASES_DIGEST_DB_H diff --git a/include/databases/filename-db.h b/include/databases/filename-db.h index 86d78ea1..1a68715a 100644 --- a/include/databases/filename-db.h +++ b/include/databases/filename-db.h @@ -5,8 +5,8 @@ * * Filename database. * - * The filenamedb is an associative array which maps keys (such as digests) - * to filenames. The data is stored on disc. + * The filenamedb is an associative array which maps keys (such as digests) to + * filenames. The data is stored on disc. * * PrivateEye uses this to find out where an image lives given its digest. */ diff --git a/include/databases/pickle.h b/include/databases/pickle.h index 2dde0bd9..eaa48b19 100644 --- a/include/databases/pickle.h +++ b/include/databases/pickle.h @@ -6,12 +6,10 @@ * Storage of associative arrays. * * Its name borrowed from Python, this module provides pickle_pickle() and - * pickle_unpickle() which respectively serialise or deserialise an - * associative array to a file of the form: + * pickle_unpickle() which respectively serialise or deserialise an associative + * array to a file of the form: * - * # - * - * (zero or more) + * # (zero or more) * * When serialising, keys and values are read from through an abstract * pickle_reader_methods interface. They are then transformed into savable @@ -242,9 +240,9 @@ pickle_unformat_methods_t; /* ----------------------------------------------------------------------- */ /** - * Serialise associative array 'assocarr' to the file 'filename'. Interpret - * the contents of the associative array using the methods in 'reader'. - * Format the keys and values for storage using the methods in 'format'. + * Serialise associative array 'assocarr' to the file 'filename'. Interpret the + * contents of the associative array using the methods in 'reader'. Format the + * keys and values for storage using the methods in 'format'. * * \param[in] filename Filename to save to. * \param[in] assocarr Associative array to pickle. @@ -261,9 +259,9 @@ result_t pickle_pickle(const char *filename, void *opaque); /** - * Populate associative array 'assocarr' from the file 'filename'. Insert - * into the associative array using the methods in 'writer'. Parse - * the keys and values from storage using the methods in 'unformat'. + * Populate associative array 'assocarr' from the file 'filename'. Insert into + * the associative array using the methods in 'writer'. Parse the keys and + * values from storage using the methods in 'unformat'. * * \param[in] filename Filename to read from. * \param[in] assocarr Associative array to pickle. diff --git a/include/datastruct/atom.h b/include/datastruct/atom.h index d17c98b7..3d5b9e9f 100644 --- a/include/datastruct/atom.h +++ b/include/datastruct/atom.h @@ -6,16 +6,15 @@ * Indexed data block store. * * Atoms are indices assigned to blocks of stored data. Identical data blocks - * are assigned the same atom. Atoms belonging to the same set can be - * directly compared avoiding the need to memcmp, strcmp, or otherwise - * linearly compare the contents of the respective data blocks. + * are assigned the same atom. Atoms belonging to the same set can be directly + * compared avoiding the need to memcmp, strcmp, or otherwise linearly compare + * the contents of the respective data blocks. * - * Data blocks can be retrieved by quoting an atom to atom_get. This returns - * a pointer to the data block along with its length. + * Data blocks can be retrieved by quoting an atom to atom_get. This returns a + * pointer to the data block along with its length. * - * To avoid heap overhead, atoms are stored in a series of fixed-size pools - * of memory. The size of the pools may be specified when the set is first - * created. + * To avoid heap overhead, atoms are stored in a series of fixed-size pools of + * memory. The size of the pools may be specified when the set is first created. */ #ifndef DATASTRUCT_ATOM_H @@ -57,15 +56,13 @@ atom_set_t *atom_create(void); /** * Create a new atom set using the specified data sizes. * - * \param locpoolsz Size of a location pool, or zero for the default. - * Set this to the number of entries you typically expect to - * store. - * \param blkpoolsz Size of a block pool, or zero for the default. - * No inserted data block may be larger than this. - * Increasing this value will use fewer individual block - * pools, reducing heap overhead, at the expense of - * potentially greater wasted space should the block pool - * remain not fully allocated. + * \param locpoolsz Size of a location pool, or zero for the default. Set this + * to the number of entries you typically expect to store. + * \param blkpoolsz Size of a block pool, or zero for the default. No inserted + * data block may be larger than this. Increasing this value + * will use fewer individual block pools, reducing heap + * overhead, at the expense of potentially greater wasted space + * should the block pool remain not fully allocated. * * \return New atom set, or NULL if out of memory. */ @@ -117,8 +114,8 @@ void atom_delete(atom_set_t *set, atom_t atom); * * \param set Atom set. * \param atom Atom to retrieve. - * \param[out] length Length of data block, in bytes. - * NULL if length is not required. + * \param[out] length Length of data block, in bytes. NULL if length is not + * required. * * \return Data block. */ @@ -159,8 +156,8 @@ atom_t atom_for_block(atom_set_t *set, /** * Delete an existing atom specified by data block. * - * This is a convenience function equivalent to: - * atom_delete(set, atom_for_block(set, block, length)). + * This is a convenience function equivalent to: atom_delete(set, + * atom_for_block(set, block, length)). * * \param set Atom set. * \param block Data block to retrieve. diff --git a/include/datastruct/bitarr.h b/include/datastruct/bitarr.h index 33a81b55..56b082ff 100644 --- a/include/datastruct/bitarr.h +++ b/include/datastruct/bitarr.h @@ -5,9 +5,9 @@ * * Arrays of bits. * - * Bit arrays are an array of bits. They are of a fixed length and allocated - * by the client. The bit array library provides functions to manipulate bit - * arrays but not allocate them. + * Bit arrays are an array of bits. They are of a fixed length and allocated by + * the client. The bit array library provides functions to manipulate bit arrays + * but not allocate them. * * \see Bit Vector for manipulating a dynamically allocated bit array. * @@ -54,8 +54,7 @@ typedef unsigned int bitarr_elem_t; #define BITARR_MASK (BITARR_BITS - 1) /** - * Return the number of elements required to store the specified number of - * bits. + * Return the number of elements required to store the specified number of bits. */ #define BITARR_ELEMS(nbits) ((nbits + BITARR_BITS - 1) >> BITARR_SHIFT) @@ -63,15 +62,15 @@ typedef unsigned int bitarr_elem_t; * Declare a bit array with the specified number of bits. * * This uses the OSLib trick of declaring a macro to define a type and also - * declaring an 'equivalent' struct. In practice they're not actually - * equivalent as the former, bitarr_ARRAY, is an anonymous struct of a given - * length and the bitarr_t is a struct containing an array of 'UNKNOWN' - * length, which is actually defined as 1. + * declaring an 'equivalent' struct. In practice they're not actually equivalent + * as the former, bitarr_ARRAY, is an anonymous struct of a given length and the + * bitarr_t is a struct containing an array of 'UNKNOWN' length, which is + * actually defined as 1. * * This works out all right, letting us declare bit arrays by specifying the - * number of bits we require, but we end up needing to cast out declared - * entries to (bitarr_t *) when calling a 'real' function, e.g. bitarr_count - * which looks awkward. + * number of bits we require, but we end up needing to cast out declared entries + * to (bitarr_t *) when calling a 'real' function, e.g. bitarr_count which looks + * awkward. */ #define bitarr_ARRAY(nbits) \ struct { \ @@ -114,8 +113,7 @@ struct bitarr do { BITARR_OP(arr, bit, |=); } while (0) /** - * Clear a single bit. - * */ + * Clear a single bit. */ #define bitarr_clear(arr, bit) \ do { BITARR_OP(arr, bit, &= ~); } while (0) diff --git a/include/datastruct/cache.h b/include/datastruct/cache.h index 1041d9fa..a7983c82 100644 --- a/include/datastruct/cache.h +++ b/include/datastruct/cache.h @@ -46,8 +46,8 @@ typedef unsigned int cachekey_t; /** * Create a cache. * - * \param[in] config Pointer to cache parameters, or NULL for default - * cache parameters. + * \param[in] config Pointer to cache parameters, or NULL for default cache + * parameters. * \param[in] length Byte length of the cache to allocate. * \param[out] cache Returned pointer to the created cache. * @@ -67,8 +67,8 @@ void cache_destroy(cache_t *doomed); /** * Create a cache in a supplied block of memory. * - * \param[in] config Pointer to cache parameters, or NULL for default - * cache parameters. + * \param[in] config Pointer to cache parameters, or NULL for default cache + * parameters. * \param[in] block Pointer to memory to use. * \param[in] length Byte length of block. * \param[out] cache Returned pointer to the created cache. @@ -104,9 +104,8 @@ void *cache_get(cache_t *cache, cachekey_t key); * \param[in] key Key. * \param[in] data Pointer to data to store. * \param[in] length Length of data. - * \param[out] inserted Returned pointer to the inserted data, or NULL if - * not wanted. This valid until the next cache_put - * operation. + * \param[out] inserted Returned pointer to the inserted data, or NULL if not + * wanted. This valid until the next cache_put operation. * * \return Error indication. */ diff --git a/include/datastruct/hash.h b/include/datastruct/hash.h index e7ace3be..4d5b881f 100644 --- a/include/datastruct/hash.h +++ b/include/datastruct/hash.h @@ -5,8 +5,8 @@ * * Hash is an associative array. * - * The interface presently forces you to malloc all keys, and values passed - * in, yourself. + * The interface presently forces you to malloc all keys, and values passed in, + * yourself. */ #ifndef DATASTRUCT_HASH_H @@ -106,8 +106,8 @@ const void *hash_lookup(T *hash, const void *key); * Insert the specified key:value pair into the hash. * * The hash takes ownership of the key and value pointers. It will call the - * destroy functions passed to hash_create when the keys and values are to - * be destroyed. + * destroy functions passed to hash_create when the keys and values are to be + * destroyed. * * \param hash Hash. * \param key Key to insert. @@ -152,8 +152,8 @@ typedef int (hash_walk_callback_t)(const void *key, * \param cb Callback routine. * \param opaque Opaque pointer to pass to callback routine. * - * \return Error indication. - * \retval result_OK If the walk completed successfully. + * \return Error indication. \retval result_OK If the walk completed + * successfully. */ result_t hash_walk(const T *hash, hash_walk_callback_t *cb, void *opaque); @@ -168,9 +168,8 @@ result_t hash_walk(const T *hash, hash_walk_callback_t *cb, void *opaque); * \param[out] key Pointer to receive key. * \param[out] value Pointer to receive value. * - * \return Error indication. - * \retval result_OK If an element was found. - * \retval result_HASH_END If no elements remain. + * \return Error indication. \retval result_OK If an element was found. + * \retval result_HASH_END If no elements remain. */ result_t hash_walk_continuation(T *hash, int continuation, diff --git a/include/datastruct/vector.h b/include/datastruct/vector.h index 82755fa3..2ed1752a 100644 --- a/include/datastruct/vector.h +++ b/include/datastruct/vector.h @@ -3,14 +3,13 @@ /** * \file vector.h * - * Vector is an abstracted array which can be resized by both length and - * element width. + * Vector is an abstracted array which can be resized by both length and element + * width. * - * Elements are of a fixed size, stored contiguously and are addressed by - * index. + * Elements are of a fixed size, stored contiguously and are addressed by index. * * \warning If the vector is altered then pointers into the vector may be - * invalidated (should the block move when reallocated). + * invalidated (should the block move when reallocated). */ #ifndef DATASTRUCT_VECTOR_H @@ -83,8 +82,8 @@ result_t vector_set_length(vector_t *vector, unsigned int length); /* ----------------------------------------------------------------------- */ /** - * Reserve space for at least the specified number of elements in the - * specified vector. + * Reserve space for at least the specified number of elements in the specified + * vector. * * \param[in] vector Vector to change. * \param[in] need Required length. @@ -107,8 +106,8 @@ size_t vector_width(const vector_t *vector); /** * Change the byte width of element stored in the specified vector. * - * If the element width is reduced then any extra bytes are lost. If - * increased, then zeroes are inserted. + * If the element width is reduced then any extra bytes are lost. If increased, + * then zeroes are inserted. * * \param[in] vector Vector to change. * \param[in] width New element width. diff --git a/include/framebuf/bitmap.h b/include/framebuf/bitmap.h index 0863eee2..32fe965a 100644 --- a/include/framebuf/bitmap.h +++ b/include/framebuf/bitmap.h @@ -88,7 +88,8 @@ result_t bitmap_load_png(bitmap_t *bm, const char *filename); result_t bitmap_save_png(const bitmap_t *bm, const char *filename); /** - * Convert the given bitmap into a different pixel format, allocating a new bitmap structure for the result. + * Convert the given bitmap into a different pixel format, allocating a new + * bitmap structure for the result. * * \param[in] bm Bitmap to convert. * \param[in] newfmt New pixel format. diff --git a/include/framebuf/bmfont.h b/include/framebuf/bmfont.h index 9f290fad..e46cd9f4 100644 --- a/include/framebuf/bmfont.h +++ b/include/framebuf/bmfont.h @@ -39,9 +39,9 @@ void bmfont_destroy(bmfont_t *bmfont); void bmfont_get_info(bmfont_t *bmfont, int *width, int *height); /** - * Read the number of glyphs in the specified bitmap font. Glyphs are laid - * out contiguously starting at ' ' (space, 0x20), so a char c has a glyph - * iff c >= ' ' and c < ' ' + bmfont_get_count(bmfont). + * Read the number of glyphs in the specified bitmap font. Glyphs are laid out + * contiguously starting at ' ' (space, 0x20), so a char c has a glyph iff c >= + * ' ' and c < ' ' + bmfont_get_count(bmfont). * * \param[in] bmfont Bitmap font to query. * \return Number of glyphs in the font. diff --git a/include/framebuf/curve.h b/include/framebuf/curve.h index 1ac2ef6a..ecc5dc63 100644 --- a/include/framebuf/curve.h +++ b/include/framebuf/curve.h @@ -11,7 +11,8 @@ /** * Return the point on the line defined by points `p0` and `p1` at time `t`. * - * This is a straight linear interpolation between the two points: there is no curvature. + * This is a straight linear interpolation between the two points: there is no + * curvature. * * \param[in] p0 Start point. * \param[in] p1 End point. @@ -115,7 +116,8 @@ point_t curve_bezier_point_on_cubic_r(point_t p0, fix16_t t); /** - * As for \ref curve_bezier_point_on_quartic but is written in terms of cubics (and in turn of quads). + * As for \ref curve_bezier_point_on_quartic but is written in terms of cubics + * (and in turn of quads). * * \param[in] p0 Start point. * \param[in] p1 Control point 1. @@ -133,7 +135,8 @@ point_t curve_bezier_point_on_quartic_r(point_t p0, fix16_t t); /** - * As for \ref curve_bezier_point_on_quintic but is written in terms of quartics (and in turn of cubics, etc.). + * As for \ref curve_bezier_point_on_quintic but is written in terms of quartics + * (and in turn of cubics, etc.). * * \param[in] p0 Start point. * \param[in] p1 Control point 1. @@ -170,9 +173,11 @@ void curve_bezier_cubic_f(point_t p0, point_t *points); /** - * As for \ref curve_bezier_cubic but uses forward differencing (fixed-point version). + * As for \ref curve_bezier_cubic but uses forward differencing (fixed-point + * version). * - * \warning This suffers from drift (the end point is not guaranteed to be reached) if `nsteps` isn't a power of 2. + * \warning This suffers from drift (the end point is not guaranteed to be + * reached) if `nsteps` isn't a power of 2. * * \param[in] p0 Start point. * \param[in] p1 Control point 1. diff --git a/include/framebuf/screen.h b/include/framebuf/screen.h index 4c1942a3..d069fbbc 100644 --- a/include/framebuf/screen.h +++ b/include/framebuf/screen.h @@ -40,7 +40,8 @@ void screen_init(screen_t *scr, void *base); /** - * Initialize a previously allocated screen structure, for drawing to an existing bitmap. + * Initialize a previously allocated screen structure, for drawing to an + * existing bitmap. * * \param[in] scr Screen to initialize. * \param[in] bm Bitmap to draw to. @@ -96,13 +97,12 @@ void screen_draw_square(screen_t *scr, colour_t colour); /** - * Draws a bitmap, alpha-blending it against the screen where the bitmap - * has an alpha channel. On paletted screens, which have no linear channel - * bits to blend, this falls back to alpha-tested transparency instead - * (drawn at full strength, or not at all). + * Draws a bitmap, alpha-blending it against the screen where the bitmap has an + * alpha channel. On paletted screens, which have no linear channel bits to + * blend, this falls back to alpha-tested transparency instead (drawn at full + * strength, or not at all). * - * The bitmap is clipped to the screen's clip region. No scaling is - * performed. + * The bitmap is clipped to the screen's clip region. No scaling is performed. * * \param[in] scr Screen to draw upon. * \param[in] x X coordinate of leftmost point to draw bitmap at. @@ -112,34 +112,34 @@ void screen_draw_square(screen_t *scr, void screen_draw_bitmap(screen_t *scr, int x, int y, const bitmap_t *src); /** - * Copies a rectangular region of the screen to another position on the - * same screen (e.g. sliding an already-rendered window's pixels to a new - * position without asking its owner to redraw). Source and destination may - * overlap; copying is done in the correct row order to handle that safely. + * Copies a rectangular region of the screen to another position on the same + * screen (e.g. sliding an already-rendered window's pixels to a new position + * without asking its owner to redraw). Source and destination may overlap; + * copying is done in the correct row order to handle that safely. * * Both the source and destination are clipped to the screen's clip region, - * shrinking together so the copied area always maps source pixel to - * destination pixel 1:1. + * shrinking together so the copied area always maps source pixel to destination + * pixel 1:1. * * Callers must check the return value and fall back to a normal - * invalidate/redraw when it's false (e.g. out of memory, or an unknown - * pixel format), since a declined copy leaves the destination untouched. + * invalidate/redraw when it's false (e.g. out of memory, or an unknown pixel + * format), since a declined copy leaves the destination untouched. * * If "src" or the intended destination falls partly off-screen, the actual - * copied area shrinks to what both ends have in common on-screen: callers - * must invalidate whatever part of their intended (unclipped) destination - * falls outside "copied_dst", since it has no valid source pixels to have - * been copied from and so is left untouched, not merely stale. + * copied area shrinks to what both ends have in common on-screen: callers must + * invalidate whatever part of their intended (unclipped) destination falls + * outside "copied_dst", since it has no valid source pixels to have been copied + * from and so is left untouched, not merely stale. * * \param[in] scr Screen to copy within. * \param[in] src Screen-space region to copy from. * \param[in] dst Top-left of the destination. - * \param[out] copied_dst Set to the on-screen box actually copied to (may - * be smaller than intended if either end was - * partly off-screen). Pass NULL if not needed. - * Left unset if the copy was declined. - * \return True if the copy was performed, false if declined (unsupported - * pixel format). + * \param[out] copied_dst Set to the on-screen box actually copied to (may be + * smaller than intended if either end was partly + * off-screen). Pass NULL if not needed. Left unset if + * the copy was declined. + * \return True if the copy was performed, false if declined (unsupported pixel + * format). */ int screen_copy_rect(screen_t *scr, const box_t *src, @@ -165,7 +165,8 @@ void screen_draw_line(screen_t *scr, /** * Draws a line (fixed-point Wu version with anti-aliasing). * - * Coordinates are fixed point values of type `fix8_t`. Coordinates are inclusive. + * Coordinates are fixed point values of type `fix8_t`. Coordinates are + * inclusive. * * \param[in] scr Screen to draw upon. * \param[in] x0 X coordinate of first point of line. @@ -181,7 +182,8 @@ void screen_draw_line_wu_fix8(screen_t *scr, /** * Draws a line (floating point Wu version with anti-aliasing). * - * Coordinates are floating point values of type `float`. Coordinates are inclusive. + * Coordinates are floating point values of type `float`. Coordinates are + * inclusive. * * \param[in] scr Screen to draw upon. * \param[in] x0 X coordinate of first point of line. diff --git a/include/framebuf/span-registry.h b/include/framebuf/span-registry.h index c110ccc9..b1eb34ff 100644 --- a/include/framebuf/span-registry.h +++ b/include/framebuf/span-registry.h @@ -10,7 +10,8 @@ * Find an appropriate span for the specified pixel format. * * \param[in] fmt Required pixel format. - * \return A span, or NULL if no span is available for the specified pixel format. + * \return A span, or NULL if no span is available for the specified pixel + * format. */ const span_t *spanregistry_get(pixelfmt_t fmt); diff --git a/include/framebuf/span.h b/include/framebuf/span.h index 04e93158..724ff622 100644 --- a/include/framebuf/span.h +++ b/include/framebuf/span.h @@ -17,15 +17,17 @@ typedef void (span_copy_t)(void *dst, const void *src, int length); /** * Type of a "blend constant pixels" function. * - * This will blend the respective source pixels by the specified constant alpha value, writing the results to the destination buffer (like Porter-Duff Source Over Destination). + * This will blend the respective source pixels by the specified constant alpha + * value, writing the results to the destination buffer (like Porter-Duff Source + * Over Destination). * * \param[out] dst Destination pixels. * \param[in] src1 Source pixels 1. * \param[in] src2 Source pixels 2. * \param[in] length Length of pixels to blend. * \param[in] alpha Constant alpha value (0..255). - * \param[in] context Format-specific extra data (e.g. a palette for an - * indexed format); ignored where not needed, pass NULL. + * \param[in] context Format-specific extra data (e.g. a palette for an indexed + * format); ignored where not needed, pass NULL. */ typedef void (span_blendconst_t)(void *dst, const void *src1, @@ -37,7 +39,9 @@ typedef void (span_blendconst_t)(void *dst, /** * Type of a "blend array of pixels" function. * - * This will blend the respective source pixels by the specified alpha values, writing the results to the destination buffer (like Porter-Duff Source Over Destination). + * This will blend the respective source pixels by the specified alpha values, + * writing the results to the destination buffer (like Porter-Duff Source Over + * Destination). * * \param[out] dst Destination pixels. * \param[in] src1 Source pixels 1. @@ -54,7 +58,8 @@ typedef void (span_blendarray_t)(void *dst, /** * Defines a span. * - * A span is a group of functions that combine runs of pixels. They are keyed by pixel format. + * A span is a group of functions that combine runs of pixels. They are keyed by + * pixel format. */ typedef struct span { diff --git a/include/geom/box.h b/include/geom/box.h index f0530f29..2f5dd9cb 100644 --- a/include/geom/box.h +++ b/include/geom/box.h @@ -27,7 +27,10 @@ typedef os_box box_t; #endif -/** Initialises a box to an invalid state that will still produce a valid result when intersected with. */ +/** + * Initialises a box to an invalid state that will still produce a valid result + * when intersected with. + */ #define BOX_INIT { INT_MAX, INT_MAX, INT_MIN, INT_MIN } /** Initialises a box from a position (x,y) and a size (w,h). */ @@ -36,8 +39,8 @@ typedef os_box box_t; /** * Reset the box to an invalid state. * - * This sets x0,y0 to INT_MAX and the x1,y1 to INT_MIN. This is an invalid - * box but will still produce a valid result when intersected with. + * This sets x0,y0 to INT_MAX and the x1,y1 to INT_MIN. This is an invalid box + * but will still produce a valid result when intersected with. * * \param[in] b The box to reset. */ @@ -82,11 +85,14 @@ int box_intersects(const box_t *a, const box_t *b); int box_intersection(const box_t *a, const box_t *b, box_t *c); /** - * Populates the box "clipped" with the sizes of the edges discarded when clipping box "b" against "a". + * Populates the box "clipped" with the sizes of the edges discarded when + * clipping box "b" against "a". * * \param[in] a The first box. * \param[in] b The second box. - * \param[out] clipped Not really a box, but one scalar per edge. Values are positive where "b" extends outside of "a", zero otherwise. + * \param[out] clipped Not really a box, but one scalar per edge. Values are + * positive where "b" extends outside of "a", zero + * otherwise. */ void box_clipped(const box_t *a, const box_t *b, box_t *clipped); diff --git a/include/geom/layout.h b/include/geom/layout.h index 73971a83..131c8768 100644 --- a/include/geom/layout.h +++ b/include/geom/layout.h @@ -61,7 +61,8 @@ layout_spec_t; * \param[in] nelements Number of layout elements given. * \param[out] boxes An array of boxes to be populated. * \param[in] nboxes Number of boxes available. - * \return \ref result_OK on success, result_LAYOUT_BUFFER_FULL if too few boxes were supplied, or appropriate result code otherwise. + * \return \ref result_OK on success, result_LAYOUT_BUFFER_FULL if too few boxes + * were supplied, or appropriate result code otherwise. */ result_t layout_place(const layout_spec_t *spec, const layout_element_t *elements, diff --git a/include/geom/line.h b/include/geom/line.h index 82b37ec5..1d9224e7 100644 --- a/include/geom/line.h +++ b/include/geom/line.h @@ -11,7 +11,8 @@ extern "C" #include "geom/box.h" /** - * Clips the line (x0,y0)-(x1,y1) by box `clip` and returns the clipped points in `x0` and co. + * Clips the line (x0,y0)-(x1,y1) by box `clip` and returns the clipped points + * in `x0` and co. * * \param[in] clip Rectangular clip region. * \param[in,out] x0 X coordinate of first point of line (modified). diff --git a/include/geom/packer.h b/include/geom/packer.h index c47525b8..ea966075 100644 --- a/include/geom/packer.h +++ b/include/geom/packer.h @@ -78,8 +78,8 @@ result_t packer_place_at(T *packer, const box_t *area); /** - * Places a box of dimensions (w,h) in the next free area determined by - * location 'loc'. + * Places a box of dimensions (w,h) in the next free area determined by location + * 'loc'. * * \param[in] packer Packer to place box. * \param[in] loc Direction to search from for the next available area. diff --git a/include/io/path.h b/include/io/path.h index 4d92e330..8a12df5c 100644 --- a/include/io/path.h +++ b/include/io/path.h @@ -8,14 +8,17 @@ /** * Join 'leaf' with extension 'ext' according to the host convention. * - * Note: Returns a pointer to an internal static buffer of length `DPTLIB_MAXPATH`. + * Note: Returns a pointer to an internal static buffer of length + * `DPTLIB_MAXPATH`. */ const char *path_join_leafname(const char *leaf, const char *ext); /** - * Join 'root' with `nbranches` directory names according to the host convention. + * Join 'root' with `nbranches` directory names according to the host + * convention. * - * Note: Returns a pointer to an internal static buffer of length `DPTLIB_MAXPATH`. + * Note: Returns a pointer to an internal static buffer of length + * `DPTLIB_MAXPATH`. */ const char *path_join_filename(const char *root, int nbranches, ...); diff --git a/include/io/stream-mem.h b/include/io/stream-mem.h index b30c44d1..599f973e 100644 --- a/include/io/stream-mem.h +++ b/include/io/stream-mem.h @@ -19,7 +19,8 @@ extern "C" * * \param[in] block Block of memory to create the stream from. * \param[in] length Length of the block of memory in bytes. - * \param[out] s Pointer to a `stream_t` pointer to store the created stream. + * \param[out] s Pointer to a `stream_t` pointer to store the created + * stream. * \return \ref result_OK on success, or appropriate result code otherwise. */ result_t stream_mem_create(const unsigned char *block, diff --git a/include/io/stream-mtfcomp.h b/include/io/stream-mtfcomp.h index 0f0ec2f5..ffe7f6de 100644 --- a/include/io/stream-mtfcomp.h +++ b/include/io/stream-mtfcomp.h @@ -16,7 +16,8 @@ extern "C" * * \param[in] input The input stream. * \param[in] bufsz The buffer size. - * \param[out] s Pointer to a `stream_t` pointer to store the created stream. + * \param[out] s Pointer to a `stream_t` pointer to store the created + * stream. * \return \ref result_OK on success, or appropriate result code otherwise. */ result_t stream_mtfcomp_create(stream_t *input, int bufsz, stream_t **s); @@ -26,7 +27,8 @@ result_t stream_mtfcomp_create(stream_t *input, int bufsz, stream_t **s); * * \param[in] input The input stream. * \param[in] bufsz The buffer size. - * \param[out] s Pointer to a `stream_t` pointer to store the created stream. + * \param[out] s Pointer to a `stream_t` pointer to store the created + * stream. * \return \ref result_OK on success, or appropriate result code otherwise. */ result_t stream_mtfdecomp_create(stream_t *input, int bufsz, stream_t **s); diff --git a/include/io/stream-packbits.h b/include/io/stream-packbits.h index 77b9af18..836066bb 100644 --- a/include/io/stream-packbits.h +++ b/include/io/stream-packbits.h @@ -16,7 +16,8 @@ extern "C" * * \param[in] input Input stream. * \param[in] bufsz Buffer size in bytes (0 for a sensible default). - * \param[out] s Pointer to a `stream_t` pointer to store the created stream. + * \param[out] s Pointer to a `stream_t` pointer to store the created + * stream. * \return \ref result_OK on success, or appropriate result code otherwise. */ result_t stream_packbitscomp_create(stream_t *input, int bufsz, stream_t **s); @@ -26,7 +27,8 @@ result_t stream_packbitscomp_create(stream_t *input, int bufsz, stream_t **s); * * \param[in] input Input stream. * \param[in] bufsz Buffer size in bytes (0 for a sensible default). - * \param[out] s Pointer to a `stream_t` pointer to store the created stream. + * \param[out] s Pointer to a `stream_t` pointer to store the created + * stream. * \return \ref result_OK on success, or appropriate result code otherwise. */ result_t stream_packbitsdecomp_create(stream_t *input, int bufsz, stream_t **s); diff --git a/include/io/stream-stdio.h b/include/io/stream-stdio.h index 80e245a9..71bcd4f9 100644 --- a/include/io/stream-stdio.h +++ b/include/io/stream-stdio.h @@ -19,7 +19,8 @@ extern "C" * * \param[in] f File to create the stream from. * \param[in] bufsz Buffer size in bytes (0 for a sensible default). - * \param[out] s Pointer to a `stream_t` pointer to store the created stream. + * \param[out] s Pointer to a `stream_t` pointer to store the created + * stream. * \return \ref result_OK on success, or appropriate result code otherwise. */ result_t stream_stdio_create(FILE *f, int bufsz, stream_t **s); diff --git a/include/io/stream.h b/include/io/stream.h index dbcb8035..7e676fd4 100644 --- a/include/io/stream.h +++ b/include/io/stream.h @@ -3,8 +3,7 @@ /** * \file Stream (interface). * - * A stream is a generic interface which can be used to wrap sources of - * bytes. + * A stream is a generic interface which can be used to wrap sources of bytes. * * Single byte and block operations are supported. Byte access is efficient: * implemented as a macro. diff --git a/include/text/txtfmt.h b/include/text/txtfmt.h index efd19407..ab520886 100644 --- a/include/text/txtfmt.h +++ b/include/text/txtfmt.h @@ -3,11 +3,10 @@ /** * \file txtfmt.h * - * Word-wraps a string to a given character width. Wraps at character - * widths, not measured widths, so works best for monospaced text. + * Word-wraps a string to a given character width. Wraps at character widths, + * not measured widths, so works best for monospaced text. * - * - Breaks at spaces. - * - Forces a newline at \\n or \\r. + * - Breaks at spaces. - Forces a newline at \\n or \\r. */ #ifndef DATASTRUCT_TXTFMT_H @@ -81,8 +80,8 @@ int txtfmt_get_nlines(const txtfmt_t *tx); /** * Returns the wrapped width of a txtfmt. * - * e.g. If you wrap some text containing a word 10 characters long it'll - * never get any thinner than 10. + * e.g. If you wrap some text containing a word 10 characters long it'll never + * get any thinner than 10. * * \param[in] tx Txtfmt to query. * @@ -93,8 +92,8 @@ int txtfmt_get_wrapped_width(const txtfmt_t *tx); /** * Retrieve a line produced by the last wrap. * - * The returned pointer refers into the txtfmt's own copy of the string and - * is valid until the next call to txtfmt_wrap or txtfmt_destroy. It is not + * The returned pointer refers into the txtfmt's own copy of the string and is + * valid until the next call to txtfmt_wrap or txtfmt_destroy. It is not * NUL-terminated: use the returned length. * * \param[in] tx Txtfmt to query. @@ -112,8 +111,8 @@ result_t txtfmt_get_line(const txtfmt_t *tx, /* ----------------------------------------------------------------------- */ /** - * Print the wrapped text via printf, including line numbers (for testing - * and debugging). + * Print the wrapped text via printf, including line numbers (for testing and + * debugging). * * \param[in] tx Txtfmt to print. * diff --git a/include/utils/array.h b/include/utils/array.h index 917dcebb..eb5072b7 100644 --- a/include/utils/array.h +++ b/include/utils/array.h @@ -19,8 +19,8 @@ extern "C" /* ----------------------------------------------------------------------- */ /** - * Signifies an array of unknown length, e.g. when used as the size of an - * array which is the final member of a struct. + * Signifies an array of unknown length, e.g. when used as the size of an array + * which is the final member of a struct. */ #define UNKNOWN 1 @@ -80,8 +80,8 @@ void array_squeeze2(unsigned char *base, * * Presently the growth strategy is doubling. * - * 'block' can be NULL to perform an initial alloc. - * Start with used == allocated == 0. + * 'block' can be NULL to perform an initial alloc. Start with used == allocated + * == 0. * * \param block Pointer to pointer to block. Updated on success. * \param elemsize Element size in bytes. diff --git a/include/utils/pack.h b/include/utils/pack.h index c7f56a7a..43bf89a2 100644 --- a/include/utils/pack.h +++ b/include/utils/pack.h @@ -5,9 +5,9 @@ * * Structure packing and unpacking routines. * - * Inspired by printf, scanf and the Python 'struct' module these allow a - * string composed of formatting character to specify how data should be - * marshalled into memory. + * Inspired by printf, scanf and the Python 'struct' module these allow a string + * composed of formatting character to specify how data should be marshalled + * into memory. */ #ifndef UTILS_PACK_H @@ -29,22 +29,21 @@ extern "C" * * The format string argument accepts the following format characters: * - * - 'c' to pack into 8 bits (notional char) - * - 's' to pack into 16 bits (notional short) - * - 'i' to pack into 32 bits (notional int) - * - 'q' to pack into 64 bits (notional long long 'quad') + * - 'c' to pack into 8 bits (notional char) - 's' to pack into 16 bits + * (notional short) - 'i' to pack into 32 bits (notional int) - 'q' to pack + * into 64 bits (notional long long 'quad') * * Each format character may be preceded by a count. * * * Examples: * - * n = pack(outbuf, "ccc", 1, 2, 3); ("ccc" can also be written "3c") - * n = pack(outbuf, "2si", 0x2000, 12345, 1 << 31); + * n = pack(outbuf, "ccc", 1, 2, 3); ("ccc" can also be written "3c") n = + * pack(outbuf, "2si", 0x2000, 12345, 1 << 31); * * - * Using '*' instead of a count invokes array mode: the next argument is - * used as an array length and the next after that as an array base pointer. + * Using '*' instead of a count invokes array mode: the next argument is used as + * an array length and the next after that as an array base pointer. * * Example: * @@ -63,8 +62,8 @@ size_t pack(unsigned char *outbuf, const char *fmt, ...); /** * Structure unpacking. * - * The arguments are unpacked from 'buf' according to the format string - * 'fmt' using little-endian byte order. + * The arguments are unpacked from 'buf' according to the format string 'fmt' + * using little-endian byte order. * * \see pack for a description of the format string. * @@ -75,8 +74,8 @@ size_t pack(unsigned char *outbuf, const char *fmt, ...); * Retrieves three characters and an int from 'inbuf'. * * - * Using '*' instead of a count invokes array mode: the next argument is - * used as an array length and the next after that as an array base pointer. + * Using '*' instead of a count invokes array mode: the next argument is used as + * an array length and the next after that as an array base pointer. * * Example: * @@ -88,12 +87,10 @@ size_t pack(unsigned char *outbuf, const char *fmt, ...); * Additionally, unpack can specify different source and destination sizes by * prefixing a formatting character with a source size qualifier: * - * - 'b' - byte - * - 'h' - half-word - * - 'w' - word - * - 'd' - double word + * - 'b' - byte - 'h' - half-word - 'w' - word - 'd' - double word * - * (Note that these specifiers are all different than the formatting characters). + * (Note that these specifiers are all different than the formatting + * characters). * * With these qualifers, sign becomes important. You can write CSIQ for unsigned * arguments, or csiq for signed arguments. @@ -109,8 +106,7 @@ size_t pack(unsigned char *outbuf, const char *fmt, ...); * * unpack copes with different endian formats. Prefix the string with: * - * - '<' to unpack little endian data - * - '>' to unpack big endian data + * - '<' to unpack little endian data - '>' to unpack big endian data * * The default is [ought to be] platform dependent. * diff --git a/include/wuss/window.h b/include/wuss/window.h index a8e1a9e3..1867679e 100644 --- a/include/wuss/window.h +++ b/include/wuss/window.h @@ -3,8 +3,8 @@ /** * \file window.h * - * A Wuss window: creation, destruction, positioning, sizing and task - * delegation of content drawing and mouse handling. + * A Wuss window: creation, destruction, positioning, sizing and task delegation + * of content drawing and mouse handling. */ #ifndef WUSS_WINDOW_H @@ -56,16 +56,15 @@ typedef struct wuss_event screen_t *scr; /** - * The region that actually needs repainting, screen space; a - * subset of bounds. Tasks should only touch pixels within this - * box. + * The region that actually needs repainting, screen space; a subset of + * bounds. Tasks should only touch pixels within this box. */ const box_t *content; /** - * The window's full (unclipped) content-area box, screen space, - * as per wuss_window_get_content_bounds; for converting screen - * position to document position. + * The window's full (unclipped) content-area box, screen space, as per + * wuss_window_get_content_bounds; for converting screen position to + * document position. */ const box_t *bounds; @@ -117,8 +116,8 @@ typedef result_t (wuss_event_fn_t)(wuss_window_t *window, typedef struct wuss_task { /** - * NULL => task receives no events; Wuss still fills the content - * background per wuss_window_create's bg. + * NULL => task receives no events; Wuss still fills the content background + * per wuss_window_create's bg. */ wuss_event_fn_t *handle; @@ -129,8 +128,8 @@ wuss_task_t; /** * Build a wuss_task_t from its fields. * - * \param[in] handle Event callback, or NULL for a task that receives - * no events. + * \param[in] handle Event callback, or NULL for a task that receives no + * events. * \param[in] task_data Opaque pointer passed back to the callback. * \return The populated task. */ @@ -138,68 +137,67 @@ wuss_task_t wuss_task_start(wuss_event_fn_t *handle, void *task_data); /** - * Notify a window's task that it is shutting down, via wuss_EVENT_QUIT. - * Not called automatically by wuss_window_close; call it first if the - * task needs notice before its window is torn down. + * Notify a window's task that it is shutting down, via wuss_EVENT_QUIT. Not + * called automatically by wuss_window_close; call it first if the task needs + * notice before its window is torn down. * * \param[in] window Window whose task should be stopped. - * \return \ref result_OK on success, else the result returned by the - * task's handle callback. + * \return \ref result_OK on success, else the result returned by the task's + * handle callback. */ result_t wuss_task_stop(wuss_window_t *window); /** * Broadcast a wuss_EVENT_IDLE event to every window's task, in z-order. - * Intended to be called once per main-loop iteration, after other pending - * input has been handled, so tasks can drive their own animation/timers - * without the caller stepping each one individually. + * Intended to be called once per main-loop iteration, after other pending input + * has been handled, so tasks can drive their own animation/timers without the + * caller stepping each one individually. * * \param[in] wuss Window manager whose windows' tasks should go idle. - * \return \ref result_OK on success, else the first non-OK result returned - * by a task's handle callback. + * \return \ref result_OK on success, else the first non-OK result returned by a + * task's handle callback. */ result_t wuss_idle(wuss_t *wuss); /** * Create a window. * - * Furniture (titlebar/outline) is added outside \p content, not carved out - * of it: before clamping, the window's content area is exactly \p content, - * and its on-screen footprint (see wuss_window_get_visible_bounds) is - * \p content expanded outward by whatever furniture flags request. If that - * footprint would then fall off the top or left edge of the screen, the - * window (content included) is nudged right/down just enough to bring it - * flush with the edge, so the titlebar/close icon stay reachable; a window - * wider or taller than the screen keeps its top-left corner on-screen - * instead. The bottom/right edges are not clamped. + * Furniture (titlebar/outline) is added outside \p content, not carved out of + * it: before clamping, the window's content area is exactly \p content, and its + * on-screen footprint (see wuss_window_get_visible_bounds) is \p content + * expanded outward by whatever furniture flags request. If that footprint would + * then fall off the top or left edge of the screen, the window (content + * included) is nudged right/down just enough to bring it flush with the edge, + * so the titlebar/close icon stay reachable; a window wider or taller than the + * screen keeps its top-left corner on-screen instead. The bottom/right edges + * are not clamped. * * \param[in] wuss Window manager to create the window on. * \param[in] content Requested content-area bounds, screen space. Copied * in. - * \param[in] title Titlebar label, or NULL for none. Copied in, - * truncated if too long. Ignored if flags includes + * \param[in] title Titlebar label, or NULL for none. Copied in, truncated + * if too long. Ignored if flags includes * 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. - * \param[in] bg Content background, filled by Wuss before each - * redraw, or wuss_NO_BACKGROUND for the task to draw - * its own background (avoids a redundant fill behind - * an opaque task). Changeable later via + * \param[in] bg Content background, filled by Wuss before each redraw, + * or wuss_NO_BACKGROUND for the task to draw its own + * background (avoids a redundant fill behind an opaque + * task). Changeable later via * wuss_window_set_background. - * \param[in] task Content delegate. Copied in. May be NULL for a - * window with no content handling. - * \param[in] doc_width Virtual document width, for the horizontal - * scrollbar's sausage proportion; pass content's own - * width for a window with nothing to scroll. - * \param[in] doc_height Virtual document height, for the vertical - * scrollbar's sausage proportion; pass content's own - * height for a window with nothing to scroll. + * \param[in] task Content delegate. Copied in. May be NULL for a window + * with no content handling. + * \param[in] doc_width Virtual document width, for the horizontal scrollbar's + * sausage proportion; pass content's own width for a + * window with nothing to scroll. + * \param[in] doc_height Virtual document height, for the vertical scrollbar's + * sausage proportion; pass content's own height for a + * window with nothing to scroll. * \param[out] window Newly created window. Becomes the topmost window. * \return \ref result_OK on success, \ref result_WUSS_TOO_SMALL if content's - * width or height is not positive, \ref result_WUSS_BAD_COLOUR if - * bg is out of range for the palette, or another appropriate - * result code. + * width or height is not positive, \ref result_WUSS_BAD_COLOUR if bg is + * out of range for the palette, or another appropriate result code. */ result_t wuss_window_create(wuss_t *wuss, const box_t *content, @@ -256,8 +254,8 @@ void wuss_window_get_visible_bounds(const wuss_window_t *window, box_t *visible); /** - * Fetch a window's current content-area bounds, screen space (as requested - * at creation, or adjusted by a subsequent move/resize; excludes any + * Fetch a window's current content-area bounds, screen space (as requested at + * creation, or adjusted by a subsequent move/resize; excludes any * titlebar/outline furniture). Useful for computing invalidation regions * outside of a redraw callback. * @@ -268,15 +266,14 @@ void wuss_window_get_content_bounds(const wuss_window_t *window, box_t *content); /** - * Mark a region of a window's content as dirty, for the next - * wuss_redraw_dirty call. Content changes are opaque to Wuss, so tasks - * must call this themselves (e.g. the union of an animated element's old - * and new positions). + * Mark a region of a window's content as dirty, for the next wuss_redraw_dirty + * call. Content changes are opaque to Wuss, so tasks must call this themselves + * (e.g. the union of an animated element's old and new positions). * * \param[in] window Window whose content changed. - * \param[in] local_box Region, in window-local content coordinates (as - * passed to the task's mouse callback), or NULL to - * mark the whole content area dirty. + * \param[in] local_box Region, in window-local content coordinates (as passed + * to the task's mouse callback), or NULL to mark the + * whole content area dirty. */ void wuss_window_invalidate(wuss_window_t *window, const box_t *local_box); @@ -286,12 +283,11 @@ void wuss_window_invalidate(wuss_window_t *window, const box_t *local_box); wuss_window_invalidate((window), NULL) /** - * Set a window's scroll offset: the point in virtual content space that - * appears at the content area's top-left. Larger offsets bring later - * content into view. Invalidates the content area so the next redraw picks - * up the new offset; the task's redraw callback is responsible for using - * the offset (via wuss_window_get_scroll) to draw the right portion of its - * content. + * Set a window's scroll offset: the point in virtual content space that appears + * at the content area's top-left. Larger offsets bring later content into view. + * Invalidates the content area so the next redraw picks up the new offset; the + * task's redraw callback is responsible for using the offset (via + * wuss_window_get_scroll) to draw the right portion of its content. * * \param[in] window Window to scroll. * \param[in] p New scroll offset. @@ -312,10 +308,10 @@ void wuss_window_get_scroll(const wuss_window_t *window, point_t *p); * * \param[in] window Window to change. * \param[in] bg New content background, as an index into the system - * palette, or wuss_NO_BACKGROUND to hand background - * painting back to the task. - * \return \ref result_OK on success, \ref result_WUSS_BAD_COLOUR if bg is - * out of range for the palette. + * palette, or wuss_NO_BACKGROUND to hand background painting + * back to the task. + * \return \ref result_OK on success, \ref result_WUSS_BAD_COLOUR if bg is out + * of range for the palette. */ result_t wuss_window_set_background(wuss_window_t *window, wuss_colour_t bg); diff --git a/include/wuss/wuss.h b/include/wuss/wuss.h index f201ba25..77703078 100644 --- a/include/wuss/wuss.h +++ b/include/wuss/wuss.h @@ -63,8 +63,7 @@ wuss_mouse_action_t; typedef int wuss_colour_t; /** - * Sentinel for wuss_window_create's bg meaning "no automatic background - * fill". + * Sentinel for wuss_window_create's bg meaning "no automatic background fill". */ #define wuss_NO_BACKGROUND ((wuss_colour_t) -1) @@ -133,11 +132,10 @@ typedef enum wuss_window_flags wuss_WINDOW_NO_RESIZE = 1 << 7, /** - * 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 the window's size in ways redraw can't patch - * incrementally (e.g. a palette that lays itself out across the whole - * window). + * 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 the window's size in ways redraw can't patch incrementally (e.g. + * a palette that lays itself out across the whole window). */ wuss_WINDOW_NO_RESIZE_BLIT = 1 << 8 } @@ -156,8 +154,8 @@ wuss_zorder_t; typedef struct wuss_config { /** - * Titlebar height in pixels, or 0 to derive from font metrics (or a - * built-in fallback if no font). + * Titlebar height in pixels, or 0 to derive from font metrics (or a built-in + * fallback if no font). */ int titlebar_height; @@ -169,19 +167,19 @@ wuss_config_t; /** * Create a window manager. * - * \param[in] scr Screen to draw windows onto. Not owned; must - * outlive the wuss_t. + * \param[in] scr Screen to draw windows onto. Not owned; must outlive the + * wuss_t. * \param[in] font Font used to draw titlebar labels, or NULL to draw * titlebars unlabelled. Not owned. - * \param[in] palette System palette, copied in, or NULL to use a - * built-in default palette. - * \param[in] npalette Number of entries in palette. Ignored if palette - * is NULL. + * \param[in] palette System palette, copied in, or NULL to use a built-in + * default palette. + * \param[in] npalette Number of entries in palette. Ignored if palette is + * NULL. * \param[in] config Creation-time configuration, or NULL for defaults. * \param[out] wuss Newly created window manager. * \return \ref result_OK on success, \ref result_WUSS_BAD_COLOUR if any of - * config's palette entries are out of range for the palette, or - * another appropriate result code. + * config's palette entries are out of range for the palette, or another + * appropriate result code. */ result_t wuss_create(screen_t *scr, bmfont_t *font, @@ -198,8 +196,8 @@ result_t wuss_create(screen_t *scr, void wuss_destroy(wuss_t *doomed); /** - * Fetch the system font (see wuss_create), for tasks to draw their own - * content in the same face as window titlebars. + * Fetch the system font (see wuss_create), for tasks to draw their own content + * in the same face as window titlebars. * * \param[in] wuss Window manager. * \return System font, or NULL if none was given to wuss_create. @@ -210,18 +208,17 @@ bmfont_t *wuss_get_font(const wuss_t *wuss); * Redraw every window, back-to-front. * * \param[in] wuss Window manager. - * \return \ref result_OK on success, or the last non-OK result returned by - * a task's redraw callback (drawing continues past a failing - * window rather than stopping). + * \return \ref result_OK on success, or the last non-OK result returned by a + * task's redraw callback (drawing continues past a failing window + * rather than stopping). */ result_t wuss_redraw(wuss_t *wuss); /** - * Mark a screen-space region dirty. Window creation, destruction, move, - * resize and bring-to-front invalidate their own affected regions - * automatically; tasks must call this themselves when their content - * changes (e.g. an animation), passing the union of the old and new - * screen-space areas that need repainting. + * Mark a screen-space region dirty. Window creation, destruction, move, resize + * and bring-to-front invalidate their own affected regions automatically; tasks + * must call this themselves when their content changes (e.g. an animation), + * passing the union of the old and new screen-space areas that need repainting. * * \param[in] wuss Window manager. * \param[in] box Screen-space region to mark dirty. @@ -232,21 +229,20 @@ result_t wuss_invalidate(wuss_t *wuss, const box_t *box); /** * Redraw only the region accumulated by wuss_invalidate calls (and any * automatic invalidation from window management) since the last redraw, - * back-to-front, then clear the dirty region. Does nothing if nothing is - * dirty. + * back-to-front, then clear the dirty region. Does nothing if nothing is dirty. * * \param[in] wuss Window manager. - * \return \ref result_OK on success, or the last non-OK result returned by - * a task's redraw callback. + * \return \ref result_OK on success, or the last non-OK result returned by a + * task's redraw callback. */ result_t wuss_redraw_dirty(wuss_t *wuss); /** * Fetch the number of dirty regions currently accumulated (see * wuss_invalidate). Regions are self-coalescing: an invalidation already - * covered by an existing region is discarded, and one sharing a complete - * edge with an existing region extends it in place, so this stays small - * under most usage. + * covered by an existing region is discarded, and one sharing a complete edge + * with an existing region extends it in place, so this stays small under most + * usage. * * \param[in] wuss Window manager. * \return Number of dirty regions, 0 if nothing is dirty. @@ -254,28 +250,27 @@ result_t wuss_redraw_dirty(wuss_t *wuss); int wuss_get_dirty_count(const wuss_t *wuss); /** - * Fetch one of the current accumulated dirty regions (see - * wuss_invalidate). Wuss only repaints windows, not background - * between/behind them, so a caller whose invalidations can expose - * background (e.g. after a window move) should clear these regions itself - * before calling wuss_redraw_dirty. + * Fetch one of the current accumulated dirty regions (see wuss_invalidate). + * Wuss only repaints windows, not background between/behind them, so a caller + * whose invalidations can expose background (e.g. after a window move) should + * clear these regions itself before calling wuss_redraw_dirty. * * \param[in] wuss Window manager. - * \param[in] index Index of the region to fetch, 0 to - * wuss_get_dirty_count() - 1. + * \param[in] index Index of the region to fetch, 0 to wuss_get_dirty_count() - + * 1. * \param[out] out Filled in with the dirty region. */ void wuss_get_dirty(const wuss_t *wuss, int index, box_t *out); /** * Deliver a mouse-down or mouse-up event (action must be wuss_MOUSE_DOWN or - * wuss_MOUSE_UP). Hit-tests the topmost window at (x,y). On a down, a - * titlebar click brings the window to front if button is Select (Adjust - * and Menu leave the z-order unchanged) and starts a drag; on an up, an - * in-progress drag is ended instead of hit-testing (an Adjust click with - * no move in between sends the window to the back rather than dragging - * it). A click on the window's content never changes the z-order and is - * delivered to the task in window-local content coordinates. + * wuss_MOUSE_UP). Hit-tests the topmost window at (x,y). On a down, a titlebar + * click brings the window to front if button is Select (Adjust and Menu leave + * the z-order unchanged) and starts a drag; on an up, an in-progress drag is + * ended instead of hit-testing (an Adjust click with no move in between sends + * the window to the back rather than dragging it). A click on the window's + * content never changes the z-order and is delivered to the task in + * window-local content coordinates. * * \param[in] wuss Window manager. * \param[in] p Screen coordinate. @@ -293,10 +288,10 @@ result_t wuss_mouse_click(wuss_t *wuss, wuss_window_t **hit); /** - * Deliver a mouse-move event. Updates the dragged window's position if a - * drag is active (invalidating the affected region; call wuss_redraw_dirty - * to actually repaint it), otherwise hit-tests and delivers to the - * window's task as per wuss_mouse_click. + * Deliver a mouse-move event. Updates the dragged window's position if a drag + * is active (invalidating the affected region; call wuss_redraw_dirty to + * actually repaint it), otherwise hit-tests and delivers to the window's task + * as per wuss_mouse_click. * * \param[in] wuss Window manager. * \param[in] p Screen coordinate. @@ -309,15 +304,15 @@ result_t wuss_mouse_move(wuss_t *wuss, point_t p, wuss_window_t **hit); /** * Deliver a scroll event. Hit-tests the topmost window at p as per - * wuss_mouse_click, and delivers to the window's task in window-local - * content coordinates; dropped if the hit window has no scroll callback, - * or the pointer is over its titlebar. + * wuss_mouse_click, and delivers to the window's task in window-local content + * coordinates; dropped if the hit window has no scroll callback, or the pointer + * is over its titlebar. * * \param[in] wuss Window manager. * \param[in] p Screen coordinate. * \param[in] delta Scroll amount; sign and units are caller-defined. - * \param[out] hit Window under the pointer, or NULL if none. May be NULL - * if not needed. + * \param[out] hit Window under the pointer, or NULL if none. May be NULL if + * not needed. * \return \ref result_OK, or a result code returned by the task's scroll * callback. */ From f3bee362100e26e2d45d75ac6a8c397b9e83c7fd Mon Sep 17 00:00:00 2001 From: David Thomas Date: Fri, 28 Aug 2026 00:54:08 +0100 Subject: [PATCH 032/287] fix(test): replace deprecated sprintf with snprintf in cache/pickle tests --- libraries/databases/pickle/test/pickle-test.c | 2 +- libraries/datastruct/cache/test/cache-test.c | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/libraries/databases/pickle/test/pickle-test.c b/libraries/databases/pickle/test/pickle-test.c index 5e15e8a8..a72a268b 100644 --- a/libraries/databases/pickle/test/pickle-test.c +++ b/libraries/databases/pickle/test/pickle-test.c @@ -338,7 +338,7 @@ static result_t cheese_format_value(const void *vvalue, char *buf, size_t len, v NOT_USED(opaque); - sprintf(buf, + snprintf(buf, len, "%s %s %s %s %s %d", cheese_country_to_string(value->country), cheese_region_to_string(value->region), diff --git a/libraries/datastruct/cache/test/cache-test.c b/libraries/datastruct/cache/test/cache-test.c index 4125faa8..c1704c90 100644 --- a/libraries/datastruct/cache/test/cache-test.c +++ b/libraries/datastruct/cache/test/cache-test.c @@ -183,7 +183,7 @@ static result_t cache_test_put(cache_t *cache, int maxkey) for (i = 0; i < maxkey; i++) { - sprintf(data, "(%d)", i); + snprintf(data, sizeof(data), "(%d)", i); cache_put(cache, i, data, strlen(data) + 1, NULL); } @@ -197,7 +197,7 @@ static result_t cache_test_put(cache_t *cache, int maxkey) { char *cached; - int len = sprintf(data, "(%d)", i); + int len = snprintf(data, sizeof(data), "(%d)", i); cached = (char *) cache_get(cache, i); if (cached) { @@ -215,7 +215,7 @@ static result_t cache_test_put(cache_t *cache, int maxkey) for (i = 0; i < maxkey; i++) { - sprintf(data, "(%d)", i); + snprintf(data, sizeof(data), "(%d)", i); cache_put(cache, i, data, strlen(data) + 1, NULL); } From 135b03b9d4eea0ed4820d5be0d8aac607c91a127 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Fri, 28 Aug 2026 01:08:18 +0100 Subject: [PATCH 033/287] feat(wuss)!: own desktop backdrop painting, add Shift-F1 full redraw wuss_config_t gains a backdrop colour (wuss_NO_BACKGROUND to opt out, matching window bg), painted by wuss_redraw/wuss_redraw_dirty before windows -- previously every caller had to repaint the background itself around each dirty rect. BREAKING: existing wuss_config_t initialisers that build the struct field-by-field must now also set backdrop. wuss-test.c's per-frame background flash is replaced by config.backdrop, and Shift-F1 now forces a full wuss_redraw (backdrop included) to exercise it interactively. --- docs/wuss.md | 6 +++--- include/wuss/wuss.h | 21 ++++++++++++++++----- libraries/wuss/create.c | 7 ++++++- libraries/wuss/impl.h | 1 + libraries/wuss/redraw.c | 20 ++++++++++++++++++++ libraries/wuss/test/wuss-test.c | 22 +++------------------- 6 files changed, 49 insertions(+), 28 deletions(-) diff --git a/docs/wuss.md b/docs/wuss.md index 5a981500..dcd1c393 100644 --- a/docs/wuss.md +++ b/docs/wuss.md @@ -20,7 +20,7 @@ result_t wuss_create(screen_t *scr, wuss_t **wuss); ``` -`font` and `palette` may both be NULL, for unlabelled titlebars and a built-in default palette respectively. `config` may be NULL for default titlebar height/colours. `wuss_get_font` reads back the font passed in (or NULL), for a task that wants to draw its own content in the same face as titlebars. +`font` and `palette` may both be NULL, for unlabelled titlebars and a built-in default palette respectively. `config` may be NULL for default titlebar height/colours. `config->backdrop` sets a desktop background colour painted behind windows on every redraw, or `wuss_NO_BACKGROUND` (the default when `config` is NULL) to leave the background untouched and require the caller to repaint it itself. `wuss_get_font` reads back the font passed in (or NULL), for a task that wants to draw its own content in the same face as titlebars. Destroy with `wuss_destroy`, which also destroys any windows still open on it. @@ -121,9 +121,9 @@ Each window carries a scroll offset, `(0, 0)` by default: the point in the task' ## Redrawing -- `wuss_redraw` repaints every window, back-to-front, unconditionally. +- `wuss_redraw` repaints every window, back-to-front, unconditionally, having first painted the configured backdrop colour (see Setup) behind them, if any. - `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. 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_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. - A window move or resize is clipped, piece by piece, against whatever's above it in the z-order, and any pixels a move can preserve are blitted directly rather than queued dirty; only the genuinely-changed pieces end up in the dirty region. This is an internal optimisation with no effect on a task's own redraw handling. diff --git a/include/wuss/wuss.h b/include/wuss/wuss.h index 77703078..cbfc6214 100644 --- a/include/wuss/wuss.h +++ b/include/wuss/wuss.h @@ -161,6 +161,13 @@ typedef struct wuss_config /** Furniture chrome colours. */ wuss_palette_t palette; + + /** + * Desktop background colour, painted behind windows on every redraw, or + * wuss_NO_BACKGROUND to leave the background untouched (the caller must then + * repaint it itself before wuss_redraw / wuss_redraw_dirty). + */ + wuss_colour_t backdrop; } wuss_config_t; @@ -205,7 +212,8 @@ void wuss_destroy(wuss_t *doomed); bmfont_t *wuss_get_font(const wuss_t *wuss); /** - * Redraw every window, back-to-front. + * Redraw every window, back-to-front, having first painted the configured + * backdrop colour (see wuss_config_t::backdrop) behind them, if any. * * \param[in] wuss Window manager. * \return \ref result_OK on success, or the last non-OK result returned by a @@ -230,6 +238,8 @@ result_t wuss_invalidate(wuss_t *wuss, const box_t *box); * Redraw only the region accumulated by wuss_invalidate calls (and any * automatic invalidation from window management) since the last redraw, * back-to-front, then clear the dirty region. Does nothing if nothing is dirty. + * Each dirty region has the configured backdrop colour (see + * wuss_config_t::backdrop) painted into it first, if any. * * \param[in] wuss Window manager. * \return \ref result_OK on success, or the last non-OK result returned by a @@ -250,10 +260,11 @@ result_t wuss_redraw_dirty(wuss_t *wuss); int wuss_get_dirty_count(const wuss_t *wuss); /** - * Fetch one of the current accumulated dirty regions (see wuss_invalidate). - * Wuss only repaints windows, not background between/behind them, so a caller - * whose invalidations can expose background (e.g. after a window move) should - * clear these regions itself before calling wuss_redraw_dirty. + * Fetch one of the current accumulated dirty regions (see wuss_invalidate). If + * no backdrop colour was configured (see wuss_config_t::backdrop), wuss only + * repaints windows, not background between/behind them, so a caller whose + * invalidations can expose background (e.g. after a window move) should clear + * these regions itself before calling wuss_redraw_dirty. * * \param[in] wuss Window manager. * \param[in] index Index of the region to fetch, 0 to wuss_get_dirty_count() - diff --git a/libraries/wuss/create.c b/libraries/wuss/create.c index 424d6e5a..de22ba07 100644 --- a/libraries/wuss/create.c +++ b/libraries/wuss/create.c @@ -65,9 +65,12 @@ result_t wuss_create(screen_t *scr, if (config != NULL) { pal = config->palette; + w->backdrop = config->backdrop; } else { + w->backdrop = wuss_NO_BACKGROUND; + if (palette == NULL) { bg = palette_PICO8_DARK_BLUE; @@ -98,7 +101,9 @@ result_t wuss_create(screen_t *scr, pal.resize < 0 || pal.resize >= w->npalette || pal.arrows < 0 || pal.arrows >= w->npalette || pal.wells < 0 || pal.wells >= w->npalette || - pal.sausages < 0 || pal.sausages >= w->npalette) + pal.sausages < 0 || pal.sausages >= w->npalette || + (w->backdrop != wuss_NO_BACKGROUND && + (w->backdrop < 0 || w->backdrop >= w->npalette))) { free(w->palette); free(w); diff --git a/libraries/wuss/impl.h b/libraries/wuss/impl.h index cfcdf829..f815d181 100644 --- a/libraries/wuss/impl.h +++ b/libraries/wuss/impl.h @@ -33,6 +33,7 @@ struct wuss colour_t *palette; /* owned */ int npalette; wuss_palette_t furniture_colours; + wuss_colour_t backdrop; /* wuss_NO_BACKGROUND for none */ int titlebar_height; list_t z_order; /* anchor; head = topmost window */ struct wuss__furniture furniture; diff --git a/libraries/wuss/redraw.c b/libraries/wuss/redraw.c index bac48e89..b56a86f6 100644 --- a/libraries/wuss/redraw.c +++ b/libraries/wuss/redraw.c @@ -93,6 +93,14 @@ result_t wuss_redraw(wuss_t *wuss) full.x1 = wuss->scr->width; full.y1 = wuss->scr->height; + if (wuss->backdrop != wuss_NO_BACKGROUND) + { + wuss->scr->clip = full; + screen_draw_rect(wuss->scr, full.x0, full.y0, + full.x1 - full.x0, full.y1 - full.y0, + wuss->palette[wuss->backdrop]); + } + rc = result_OK; redraw_from(wuss, wuss->z_order.next, &full, &rc); @@ -117,7 +125,19 @@ result_t wuss_redraw_dirty(wuss_t *wuss) rc = result_OK; for (i = 0; i < wuss->ndirty; i++) + { + if (wuss->backdrop != wuss_NO_BACKGROUND) + { + wuss->scr->clip = wuss->dirty[i]; + screen_draw_rect(wuss->scr, + wuss->dirty[i].x0, wuss->dirty[i].y0, + wuss->dirty[i].x1 - wuss->dirty[i].x0, + wuss->dirty[i].y1 - wuss->dirty[i].y0, + wuss->palette[wuss->backdrop]); + } + redraw_from(wuss, wuss->z_order.next, &wuss->dirty[i], &rc); + } box_reset(&wuss->scr->clip); /* see wuss_redraw's comment on the same call */ diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 3b2b63e8..333594a8 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -242,6 +242,7 @@ static result_t wuss_interactive_test(const char *resources) config.palette.arrows = palette_PICO8_BLUE; config.palette.wells = palette_PICO8_DARK_BLUE; config.palette.sausages = palette_PICO8_LIGHT_GREY; + config.backdrop = palette_PICO8_LIGHT_GREY; rc = wuss_create(&scr, font, palette, NELEMS(palette), &config, &wuss); if (rc != result_OK) @@ -279,6 +280,8 @@ static result_t wuss_interactive_test(const char *resources) case SDL_EVENT_KEY_UP: if (event.key.key == SDLK_Q) quit = true; + else if (event.key.key == SDLK_F1 && (event.key.mod & SDL_KMOD_SHIFT)) + wuss_redraw(wuss); else if (event.key.key == SDLK_F1) garbage_pending = true; else if (event.key.key == SDLK_F3) @@ -384,25 +387,6 @@ static result_t wuss_interactive_test(const char *resources) } else { - int ndirty, i; - - ndirty = wuss_get_dirty_count(wuss); - for (i = 0; i < ndirty; i++) - { - box_t dirty; - - wuss_get_dirty(wuss, i, &dirty); - scr.clip = dirty; - screen_draw_rect(&scr, dirty.x0, dirty.y0, - dirty.x1 - dirty.x0, dirty.y1 - dirty.y0, - palette[palette_PICO8_WHITE]); - } - - /* narrowed above per dirty rect for the flash; screen_copy_rect (used - * for window-drag blitting) reads this clip too, so it must not leak - * into the next frame narrower than the whole screen */ - box_reset(&scr.clip); - wuss_redraw_dirty(wuss); } From 7e322983e1dbb4ed18b3eb5637cbb00ade1e539d Mon Sep 17 00:00:00 2001 From: David Thomas Date: Fri, 28 Aug 2026 09:47:54 +0100 Subject: [PATCH 034/287] fix(wuss): icon size falls back to wuss's titlebar height, not a hardcoded one wuss__icon_size_for used WUSS_DEFAULT_TITLEBAR_HEIGHT as its fallback for a NO_TITLEBAR window that still wants scrollbars/resize, so such a window's furniture didn't match its titled siblings whenever the actual (derived or configured) titlebar height differed from the hardcoded default. --- libraries/wuss/impl.h | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/libraries/wuss/impl.h b/libraries/wuss/impl.h index f815d181..5c98c894 100644 --- a/libraries/wuss/impl.h +++ b/libraries/wuss/impl.h @@ -124,14 +124,19 @@ static inline int wuss__outline_px(const wuss_window_t *window) return wuss__outline_px_for(window->flags); } -/* ponytail: falls back to the default titlebar height when the window has - * none, so NO_TITLEBAR windows that still opt into scrollbars/resize get a - * sane breadth rather than a negative one */ +/* ponytail: falls back to wuss's own titlebar height when the window has + * none, so NO_TITLEBAR windows that still opt into scrollbars/resize match + * their titled siblings instead of a hardcoded size; the hardcoded default + * is only a last-resort floor if even that isn't positive */ static inline int wuss__icon_size_for(const wuss_t *wuss, wuss_window_flags_t flags) { int size; size = wuss__titlebar_height_for(wuss, flags) - 2 * WUSS_ICON_INSET; + if (size > 0) + return size; + + size = wuss->titlebar_height - 2 * WUSS_ICON_INSET; return (size > 0) ? size : WUSS_DEFAULT_TITLEBAR_HEIGHT - 2 * WUSS_ICON_INSET; } From 5249ab4663fb304c5d164bf1c0eab9b4e7cbf229 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Fri, 28 Aug 2026 09:50:49 +0100 Subject: [PATCH 035/287] refactor(wuss): move window toggled bool into an internal state flags word Replaces struct wuss_window's standalone toggled int with a wuss_window_state_t bitfield (wuss__window_toggled/wuss__window_set_toggled helpers), separate from the public wuss_window_flags_t appearance flags, so future internal per-window state can add bits instead of int fields. --- libraries/wuss/furniture/toggle-action.c | 4 ++-- libraries/wuss/impl.h | 25 +++++++++++++++++++++++- libraries/wuss/window/create.c | 2 +- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/libraries/wuss/furniture/toggle-action.c b/libraries/wuss/furniture/toggle-action.c index e2d50886..1a4e62e1 100644 --- a/libraries/wuss/furniture/toggle-action.c +++ b/libraries/wuss/furniture/toggle-action.c @@ -10,7 +10,7 @@ void wuss__furniture_toggle_size(wuss_window_t *window) before = window->visible; - if (window->toggled) + if (wuss__window_toggled(window)) { new_visible = window->pre_toggle; } @@ -54,7 +54,7 @@ void wuss__furniture_toggle_size(wuss_window_t *window) } window->visible = new_visible; - window->toggled = !window->toggled; + wuss__window_set_toggled(window, !wuss__window_toggled(window)); { point_t new_scroll; diff --git a/libraries/wuss/impl.h b/libraries/wuss/impl.h index 5c98c894..f0858edf 100644 --- a/libraries/wuss/impl.h +++ b/libraries/wuss/impl.h @@ -26,6 +26,16 @@ #define WUSS_ICON_INSET 3 /* shared by close/back/toggle/resize icons and scrollbar breadth */ #define WUSS_MIN_CONTENT 20 /* resize-drag floor: content can never be squeezed smaller than this */ +/* Internal per-window state, distinct from the public wuss_window_flags_t + * appearance flags a caller sets at creation -- room to grow without + * widening struct wuss_window by an int per flag. */ +typedef enum wuss_window_state +{ + wuss_WINDOW_STATE_NONE = 0, + wuss_WINDOW_STATE_TOGGLED = 1 << 0 /* currently at TOGGLE_SIZE's "full" size */ +} +wuss_window_state_t; + struct wuss { screen_t *scr; @@ -53,7 +63,7 @@ struct wuss_window point_t scroll; /* offset into virtual content space of the * content box's top-left; see wuss_window_set_scroll */ int doc_width, doc_height; /* virtual document extent, set at creation */ - int toggled; /* currently at TOGGLE_SIZE's "full" size? */ + wuss_window_state_t state; /* see wuss_window_state_t */ box_t pre_toggle; /* visible bounds to restore on the next toggle */ char title[WUSS_TITLE_MAX + 1]; }; @@ -114,6 +124,19 @@ static inline int wuss__titlebar_height(const wuss_window_t *window) return wuss__titlebar_height_for(window->wuss, window->flags); } +static inline int wuss__window_toggled(const wuss_window_t *window) +{ + return (window->state & wuss_WINDOW_STATE_TOGGLED) != 0; +} + +static inline void wuss__window_set_toggled(wuss_window_t *window, int toggled) +{ + if (toggled) + window->state |= wuss_WINDOW_STATE_TOGGLED; + else + window->state &= (wuss_window_state_t) ~wuss_WINDOW_STATE_TOGGLED; +} + static inline int wuss__outline_px_for(wuss_window_flags_t flags) { return (flags & wuss_WINDOW_NO_OUTLINE) ? 0 : 1; diff --git a/libraries/wuss/window/create.c b/libraries/wuss/window/create.c index 45d0a830..76f6fb67 100644 --- a/libraries/wuss/window/create.c +++ b/libraries/wuss/window/create.c @@ -78,7 +78,7 @@ result_t wuss_window_create(wuss_t *wuss, win->scroll.y = 0; win->doc_width = doc_width; win->doc_height = doc_height; - win->toggled = 0; + win->state = wuss_WINDOW_STATE_NONE; if (task != NULL) win->task = *task; From 54f97b45dcac7bfc0d85f49492a36afe92738ed1 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Fri, 28 Aug 2026 10:05:00 +0100 Subject: [PATCH 036/287] refactor(wuss)!: nest scrollbar palette colours under wuss_palette_t::scroll Groups arrows/wells/sausages into a scroll sub-struct so the flat colour list reads as one furniture class instead of nine unrelated fields. BREAKING: existing wuss_config_t initialisers must update palette.arrows/wells/sausages to palette.scroll.arrows/wells/sausages. --- include/wuss/wuss.h | 10 +++++--- libraries/wuss/create.c | 36 ++++++++++++++-------------- libraries/wuss/furniture/draw.c | 16 ++++++------- libraries/wuss/test/wuss-test.c | 42 ++++++++++++++++----------------- 4 files changed, 54 insertions(+), 50 deletions(-) diff --git a/include/wuss/wuss.h b/include/wuss/wuss.h index cbfc6214..382133f5 100644 --- a/include/wuss/wuss.h +++ b/include/wuss/wuss.h @@ -83,9 +83,13 @@ typedef struct wuss_palette wuss_colour_t close; /**< Close icon. */ wuss_colour_t toggle; /**< Toggle-size icon. */ wuss_colour_t resize; /**< Resize icon. */ - wuss_colour_t arrows; /**< Scrollbar arrows. */ - wuss_colour_t wells; /**< Scrollbar wells. */ - wuss_colour_t sausages; /**< Scrollbar sausages. */ + struct + { + wuss_colour_t arrows; /**< Scrollbar arrows. */ + wuss_colour_t wells; /**< Scrollbar wells. */ + wuss_colour_t sausages; /**< Scrollbar sausages. */ + } + scroll; } wuss_palette_t; diff --git a/libraries/wuss/create.c b/libraries/wuss/create.c index de22ba07..82eaca5c 100644 --- a/libraries/wuss/create.c +++ b/libraries/wuss/create.c @@ -82,26 +82,26 @@ result_t wuss_create(screen_t *scr, fg = (w->npalette > 1) ? 1 : 0; } - pal.title.bg = bg; - pal.title.fg = fg; - pal.back = fg; - pal.close = fg; - pal.toggle = fg; - pal.resize = bg; - pal.arrows = bg; - pal.wells = bg; - pal.sausages = fg; + pal.title.bg = bg; + pal.title.fg = fg; + pal.back = fg; + pal.close = fg; + pal.toggle = fg; + pal.resize = bg; + pal.scroll.arrows = bg; + pal.scroll.wells = bg; + pal.scroll.sausages = fg; } - if (pal.title.bg < 0 || pal.title.bg >= w->npalette || - pal.title.fg < 0 || pal.title.fg >= w->npalette || - pal.back < 0 || pal.back >= w->npalette || - pal.close < 0 || pal.close >= w->npalette || - pal.toggle < 0 || pal.toggle >= w->npalette || - pal.resize < 0 || pal.resize >= w->npalette || - pal.arrows < 0 || pal.arrows >= w->npalette || - pal.wells < 0 || pal.wells >= w->npalette || - pal.sausages < 0 || pal.sausages >= w->npalette || + if (pal.title.bg < 0 || pal.title.bg >= w->npalette || + pal.title.fg < 0 || pal.title.fg >= w->npalette || + pal.back < 0 || pal.back >= w->npalette || + pal.close < 0 || pal.close >= w->npalette || + pal.toggle < 0 || pal.toggle >= w->npalette || + pal.resize < 0 || pal.resize >= w->npalette || + pal.scroll.arrows < 0 || pal.scroll.arrows >= w->npalette || + pal.scroll.wells < 0 || pal.scroll.wells >= w->npalette || + pal.scroll.sausages < 0 || pal.scroll.sausages >= w->npalette || (w->backdrop != wuss_NO_BACKGROUND && (w->backdrop < 0 || w->backdrop >= w->npalette))) { diff --git a/libraries/wuss/furniture/draw.c b/libraries/wuss/furniture/draw.c index e3a6438b..1d49e896 100644 --- a/libraries/wuss/furniture/draw.c +++ b/libraries/wuss/furniture/draw.c @@ -140,7 +140,7 @@ void wuss__furniture_draw(wuss_t *wuss, { wuss->scr->clip = clipped; screen_draw_rect(wuss->scr, up.x0, up.y0, up.x1 - up.x0, up.y1 - up.y0, - wuss->palette[wuss->furniture_colours.arrows]); + wuss->palette[wuss->furniture_colours.scroll.arrows]); } wuss__vscroll_down_box(window, &down); @@ -148,7 +148,7 @@ void wuss__furniture_draw(wuss_t *wuss, { wuss->scr->clip = clipped; screen_draw_rect(wuss->scr, down.x0, down.y0, down.x1 - down.x0, down.y1 - down.y0, - wuss->palette[wuss->furniture_colours.arrows]); + wuss->palette[wuss->furniture_colours.scroll.arrows]); } wuss__vscroll_well_box(window, &well); @@ -156,7 +156,7 @@ void wuss__furniture_draw(wuss_t *wuss, { wuss->scr->clip = clipped; screen_draw_rect(wuss->scr, well.x0, well.y0, well.x1 - well.x0, well.y1 - well.y0, - wuss->palette[wuss->furniture_colours.wells]); + wuss->palette[wuss->furniture_colours.scroll.wells]); } wuss__vscroll_sausage_box(window, &sausage); @@ -164,7 +164,7 @@ void wuss__furniture_draw(wuss_t *wuss, { wuss->scr->clip = clipped; screen_draw_rect(wuss->scr, sausage.x0, sausage.y0, sausage.x1 - sausage.x0, sausage.y1 - sausage.y0, - wuss->palette[wuss->furniture_colours.sausages]); + wuss->palette[wuss->furniture_colours.scroll.sausages]); } } @@ -177,7 +177,7 @@ void wuss__furniture_draw(wuss_t *wuss, { wuss->scr->clip = clipped; screen_draw_rect(wuss->scr, left.x0, left.y0, left.x1 - left.x0, left.y1 - left.y0, - wuss->palette[wuss->furniture_colours.arrows]); + wuss->palette[wuss->furniture_colours.scroll.arrows]); } wuss__hscroll_right_box(window, &right); @@ -185,7 +185,7 @@ void wuss__furniture_draw(wuss_t *wuss, { wuss->scr->clip = clipped; screen_draw_rect(wuss->scr, right.x0, right.y0, right.x1 - right.x0, right.y1 - right.y0, - wuss->palette[wuss->furniture_colours.arrows]); + wuss->palette[wuss->furniture_colours.scroll.arrows]); } wuss__hscroll_well_box(window, &well); @@ -193,7 +193,7 @@ void wuss__furniture_draw(wuss_t *wuss, { wuss->scr->clip = clipped; screen_draw_rect(wuss->scr, well.x0, well.y0, well.x1 - well.x0, well.y1 - well.y0, - wuss->palette[wuss->furniture_colours.wells]); + wuss->palette[wuss->furniture_colours.scroll.wells]); } wuss__hscroll_sausage_box(window, &sausage); @@ -201,7 +201,7 @@ void wuss__furniture_draw(wuss_t *wuss, { wuss->scr->clip = clipped; screen_draw_rect(wuss->scr, sausage.x0, sausage.y0, sausage.x1 - sausage.x0, sausage.y1 - sausage.y0, - wuss->palette[wuss->furniture_colours.sausages]); + wuss->palette[wuss->furniture_colours.scroll.sausages]); } } diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 333594a8..8c363a0d 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -232,17 +232,17 @@ static result_t wuss_interactive_test(const char *resources) { wuss_config_t config; - config.titlebar_height = 0; - config.palette.title.bg = palette_PICO8_DARK_BLUE; - config.palette.title.fg = palette_PICO8_WHITE; - config.palette.back = palette_PICO8_GREEN; - config.palette.close = palette_PICO8_RED; - config.palette.toggle = palette_PICO8_ORANGE; - config.palette.resize = palette_PICO8_LAVENDER; - config.palette.arrows = palette_PICO8_BLUE; - config.palette.wells = palette_PICO8_DARK_BLUE; - config.palette.sausages = palette_PICO8_LIGHT_GREY; - config.backdrop = palette_PICO8_LIGHT_GREY; + config.titlebar_height = 0; + config.palette.title.bg = palette_PICO8_DARK_BLUE; + config.palette.title.fg = palette_PICO8_WHITE; + config.palette.back = palette_PICO8_GREEN; + config.palette.close = palette_PICO8_RED; + config.palette.toggle = palette_PICO8_ORANGE; + config.palette.resize = palette_PICO8_LAVENDER; + config.palette.scroll.arrows = palette_PICO8_BLUE; + config.palette.scroll.wells = palette_PICO8_DARK_BLUE; + config.palette.scroll.sausages = palette_PICO8_LIGHT_GREY; + config.backdrop = palette_PICO8_LIGHT_GREY; rc = wuss_create(&scr, font, palette, NELEMS(palette), &config, &wuss); if (rc != result_OK) @@ -531,16 +531,16 @@ result_t wuss_test(const char *resources) printf("test: wuss_create with bad titlebar colour index\n"); - bad_config.titlebar_height = 0; - bad_config.palette.title.bg = 999; - bad_config.palette.title.fg = 0; - bad_config.palette.back = 0; - bad_config.palette.close = 0; - bad_config.palette.toggle = 0; - bad_config.palette.resize = 0; - bad_config.palette.arrows = 0; - bad_config.palette.wells = 0; - bad_config.palette.sausages = 0; + bad_config.titlebar_height = 0; + bad_config.palette.title.bg = 999; + bad_config.palette.title.fg = 0; + bad_config.palette.back = 0; + bad_config.palette.close = 0; + bad_config.palette.toggle = 0; + bad_config.palette.resize = 0; + bad_config.palette.scroll.arrows = 0; + bad_config.palette.scroll.wells = 0; + bad_config.palette.scroll.sausages = 0; rc = wuss_create(&scr, NULL, NULL, 0, &bad_config, &bad_wuss); if (rc != result_WUSS_BAD_COLOUR) goto Failure; From ddc103c456880d5289a54cc2c0b407c8c71fd1ac Mon Sep 17 00:00:00 2001 From: David Thomas Date: Fri, 28 Aug 2026 10:11:05 +0100 Subject: [PATCH 037/287] feat(wuss): auto-cycle sofa demo model after two rotations Adds a turns counter alongside angle, incremented each time the spin wraps; hitting SOFA_ROTATIONS_PER_MODEL advances to the next shape, same as an Adjust click (which also resets the counter). Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/test/tasks/sofa.c | 10 ++++++++++ libraries/wuss/test/tasks/sofa.h | 1 + 2 files changed, 11 insertions(+) diff --git a/libraries/wuss/test/tasks/sofa.c b/libraries/wuss/test/tasks/sofa.c index 992798dc..98b0cd8d 100644 --- a/libraries/wuss/test/tasks/sofa.c +++ b/libraries/wuss/test/tasks/sofa.c @@ -21,6 +21,7 @@ #define SOFA_TILT -0.5 /* static camera tilt, radians, so the seat is visible from above */ #define SOFA_SPIN_PER_FRAME 0.02 /* radians/frame at 60fps, one turn every ~5s */ +#define SOFA_ROTATIONS_PER_MODEL 2 /* auto-cycle to the next model after this many full turns */ #define SOFA_CAMERA_DIST 4.0 /* perspective divisor: bigger = flatter */ #define SOFA_UNIT_FRACTION 0.35 /* fraction of min(width,height) per model unit, at zoom 1.0 */ #define SOFA_ZOOM_MIN 0.2 @@ -265,6 +266,7 @@ result_t sofa_create(wuss_t *wuss, const colour_t *palette, sofa_task_t *task) task->zoom = 1.0; task->spinning = true; task->shape = sofa_SHAPE_SOFA; + task->turns = 0; delegate = wuss_task_start(sofa_handle, task); /* sofa_redraw paints its own background every frame */ box = (box_t) BOX_POS_SIZE(250, 260, 180, 160); @@ -385,6 +387,7 @@ static result_t sofa_mouse(wuss_window_t *window, wuss_button_t button, void *ta if (button == wuss_BUTTON_ADJUST) { sc->shape = (sc->shape + 1) % sofa_SHAPE__LIMIT; + sc->turns = 0; wuss_window_invalidate_all(window); } else @@ -420,7 +423,14 @@ static result_t sofa_idle(void *task_data) task->angle += SOFA_SPIN_PER_FRAME; if (task->angle > 2.0 * M_PI) + { task->angle -= 2.0 * M_PI; + if (++task->turns >= SOFA_ROTATIONS_PER_MODEL) + { + task->turns = 0; + task->shape = (task->shape + 1) % sofa_SHAPE__LIMIT; + } + } wuss_window_invalidate_all(task->window); diff --git a/libraries/wuss/test/tasks/sofa.h b/libraries/wuss/test/tasks/sofa.h index 7a19ba1b..a092ecaf 100644 --- a/libraries/wuss/test/tasks/sofa.h +++ b/libraries/wuss/test/tasks/sofa.h @@ -35,6 +35,7 @@ typedef struct sofa_task double zoom; /* scroll-adjustable */ bool spinning; sofa_shape_t shape; + int turns; /* completed rotations of the current model */ } sofa_task_t; From ecac4ff98463911ccc55f101b95cd648701bb286 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Fri, 28 Aug 2026 10:11:07 +0100 Subject: [PATCH 038/287] docs: note wuss internal naming convention in CLAUDE.md Records the wuss__ prefix and internal-bitflags-with-accessors pattern established this session, so future work follows it instead of reinventing per-window state as loose ints. Co-Authored-By: Claude Sonnet 5 --- CLAUDE.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 5f18cf77..ad68b665 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -51,6 +51,8 @@ Success prints `++ Tests completed in Ns: N of N tests passed.` New source/header files must be added by hand to the relevant `set(..._SOURCES ...)` list and, for public headers, to `PUBLIC_HEADERS`, in `CMakeLists.txt` — there is no globbing. +**Internal naming (see `wuss`).** Functions declared in a private `impl.h` and not part of the public header use a double-underscore prefix, e.g. `wuss__titlebar_height_for`; internal-only enum constants still use the module's normal single-underscore style (e.g. `wuss_WINDOW_STATE_TOGGLED`), matching public enums. Per-instance internal state that isn't part of the public appearance API (e.g. `struct wuss_window`'s toggled/maximised state) is kept as a bitflags enum with `wuss__window_*` accessor helpers rather than loose `int`/`bool` fields, leaving room to add flags without growing the struct. + **Error handling.** No exceptions; functions return `result_t` (`include/base/result.h`). Each module reserves a `result_BASE_` offset block and defines its own `result__*` codes starting from that base. Common generic codes (`result_OK`, `result_OOM`, `result_BAD_ARG`, etc.) live at `result_BASE_GENERIC`. Callers typically check `rc != result_OK` and propagate. **Debug/logging.** `include/base/debug.h` provides `logf_info/warning/error/fatal/abort`, plus `check(err)` (log-and-`goto failure`) and `sentinel` (unreachable-code marker), both of which are compiled out entirely when `NDEBUG` is set — don't rely on their side effects in release builds. From fa5d089bec583208e97851f1672084763226d9eb Mon Sep 17 00:00:00 2001 From: David Thomas Date: Fri, 28 Aug 2026 10:26:52 +0100 Subject: [PATCH 039/287] style(wuss): inset scrollbar sausage from its well's edges Adds WUSS_SCROLL_INSET so the drag handle sits with a small cosmetic margin on its cross-axis instead of filling the well's full breadth. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/furniture/hscroll-box.c | 4 ++-- libraries/wuss/furniture/vscroll-box.c | 4 ++-- libraries/wuss/impl.h | 1 + 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/libraries/wuss/furniture/hscroll-box.c b/libraries/wuss/furniture/hscroll-box.c index d25155ea..5ea4a61a 100644 --- a/libraries/wuss/furniture/hscroll-box.c +++ b/libraries/wuss/furniture/hscroll-box.c @@ -92,8 +92,8 @@ void wuss__hscroll_sausage_box(const wuss_window_t *window, box_t *out) else sausage_x0 = well.x0; - out->y0 = well.y0; - out->y1 = well.y1; + out->y0 = well.y0 + WUSS_SCROLL_INSET; + out->y1 = well.y1 - WUSS_SCROLL_INSET; out->x0 = sausage_x0; out->x1 = sausage_x0 + sausage_px; } diff --git a/libraries/wuss/furniture/vscroll-box.c b/libraries/wuss/furniture/vscroll-box.c index ae5f9d0b..20d4532c 100644 --- a/libraries/wuss/furniture/vscroll-box.c +++ b/libraries/wuss/furniture/vscroll-box.c @@ -94,8 +94,8 @@ void wuss__vscroll_sausage_box(const wuss_window_t *window, box_t *out) else sausage_y0 = well.y0; - out->x0 = well.x0; - out->x1 = well.x1; + out->x0 = well.x0 + WUSS_SCROLL_INSET; + out->x1 = well.x1 - WUSS_SCROLL_INSET; out->y0 = sausage_y0; out->y1 = sausage_y0 + sausage_px; } diff --git a/libraries/wuss/impl.h b/libraries/wuss/impl.h index f0858edf..28e965d7 100644 --- a/libraries/wuss/impl.h +++ b/libraries/wuss/impl.h @@ -25,6 +25,7 @@ #define WUSS_ICON_INSET 3 /* shared by close/back/toggle/resize icons and scrollbar breadth */ #define WUSS_MIN_CONTENT 20 /* resize-drag floor: content can never be squeezed smaller than this */ +#define WUSS_SCROLL_INSET 2 /* sausage cross-axis margin from its well's edges, purely cosmetic */ /* Internal per-window state, distinct from the public wuss_window_flags_t * appearance flags a caller sets at creation -- room to grow without From 96b937c5a8b863fdbe1446db50862d525a31c9bb Mon Sep 17 00:00:00 2001 From: David Thomas Date: Fri, 28 Aug 2026 10:34:47 +0100 Subject: [PATCH 040/287] fix(wuss): apply horizontal scroll offset in launcher redraw launcher_redraw read scroll.y but never scroll.x, so rows stayed put when scrolled horizontally (reachable once the window is resized narrower than LAUNCHER_WIDTH, which enables the hscroll bar). wuss_window_set_scroll's topmost live-blit-and-shift optimisation assumes redraw output is a deterministic function of the scroll offset; violating that shifted real pixels by the scroll delta while the exposed edge strip was redrawn at the stale unscrolled position, corrupting the window's content. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/test/tasks/launcher.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/libraries/wuss/test/tasks/launcher.c b/libraries/wuss/test/tasks/launcher.c index 61dd17a1..05f11934 100644 --- a/libraries/wuss/test/tasks/launcher.c +++ b/libraries/wuss/test/tasks/launcher.c @@ -60,7 +60,7 @@ static result_t launcher_redraw(const wuss_event_t *event, void *task_data) launcher_task_t *lc; screen_t *scr; const box_t *content, *bounds; - int i, font_width, font_height, sy; + int i, font_width, font_height, sx, sy; point_t pos; const launcher_entry_t *entry; @@ -69,6 +69,7 @@ static result_t launcher_redraw(const wuss_event_t *event, void *task_data) scr = event->data.redraw.scr; content = event->data.redraw.content; bounds = event->data.redraw.bounds; + sx = event->data.redraw.scroll.x; sy = event->data.redraw.scroll.y; screen_draw_rect(scr, content->x0, content->y0, @@ -82,7 +83,7 @@ static result_t launcher_redraw(const wuss_event_t *event, void *task_data) { entry = &lc->entries[i]; - pos.x = bounds->x0 + LAUNCHER_PAD; + pos.x = bounds->x0 - sx + LAUNCHER_PAD; pos.y = bounds->y0 - sy + LAUNCHER_PAD + i * LAUNCHER_ROW_HEIGHT + (LAUNCHER_ROW_HEIGHT - font_height) / 2; bmfont_draw(lc->font, scr, entry->name, (int) strlen(entry->name), From 57bb22723def186ef77eaa37f39bc86178d4f738 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Fri, 28 Aug 2026 11:16:39 +0100 Subject: [PATCH 041/287] fix(screen): make line drawing independent of the clip box The three line rasterizers seeded their Bresenham/Wu error terms from the endpoints line_clip() had recomputed on the clip boundary. As wuss splits a dirty rectangle into up to 32 pieces and redraws the same logical content under a different clip box for each, the same line took a different pixel path per piece, leaving visible seams at the piece boundaries. Use line_clip() against the clip box for rejection only, discarding its recomputed endpoints, and step from the true endpoints instead, leaving the per-pixel clip in screen_draw_pixel()/screen_blend_pixel() to do the real clipping. Bound the work with the screen bounds, which -- unlike the clip box -- never vary between calls: screen_draw_line() and screen_draw_line_wu_fix8() clip their endpoints against them, and screen_draw_line_wu_float() clamps its mid-point loop to them, fast- forwarding the gradient in closed form. Adds a screen test asserting a line drawn in one go and drawn once per piece of a partition of the canvas produce identical pixels, plus a check that nothing is drawn outside the clip box. Co-Authored-By: Claude Opus 5 --- CMakeLists.txt | 1 + apps/test/main.c | 1 + include/geom/line.h | 6 + include/test/all-tests.h | 3 +- libraries/framebuf/screen/screen-draw.c | 62 ++++- libraries/framebuf/screen/test/screen-test.c | 231 +++++++++++++++++++ 6 files changed, 300 insertions(+), 4 deletions(-) create mode 100644 libraries/framebuf/screen/test/screen-test.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 815f0484..f741e49e 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -530,6 +530,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/screen/test/screen-test.c libraries/geom/box/test/box-test.c libraries/geom/layout/test/layout-test.c libraries/geom/packer/test/packer-test.c diff --git a/apps/test/main.c b/apps/test/main.c index 9080cd2f..435b3b82 100644 --- a/apps/test/main.c +++ b/apps/test/main.c @@ -54,6 +54,7 @@ static const test_t tests[] = { "bmfont", bmfont_test }, { "composite", composite_test }, { "curve", curve_test }, + { "screen", screen_test }, { "box", box_test }, { "layout", layout_test }, diff --git a/include/geom/line.h b/include/geom/line.h index 1d9224e7..c57849d8 100644 --- a/include/geom/line.h +++ b/include/geom/line.h @@ -14,6 +14,12 @@ extern "C" * Clips the line (x0,y0)-(x1,y1) by box `clip` and returns the clipped points * in `x0` and co. * + * The returned points are rounded to the nearest integer position on the clip + * boundary, so they vary with the clip box given. Callers which step along the + * line incrementally (Bresenham, Wu, etc.) must not seed their error terms from + * them if the pixels drawn are to be independent of the clip box passed in; use + * the return value to reject and step from the original endpoints. + * * \param[in] clip Rectangular clip region. * \param[in,out] x0 X coordinate of first point of line (modified). * \param[in,out] y0 Y coordinate of first point of line (modified). diff --git a/include/test/all-tests.h b/include/test/all-tests.h index c3319030..9986c3a7 100644 --- a/include/test/all-tests.h +++ b/include/test/all-tests.h @@ -25,7 +25,8 @@ extern testfn_t pickle_test, /* framebuf */ extern testfn_t bmfont_test, composite_test, - curve_test; + curve_test, + screen_test; /* geom */ extern testfn_t box_test, diff --git a/libraries/framebuf/screen/screen-draw.c b/libraries/framebuf/screen/screen-draw.c index 686ee99f..94d11d54 100644 --- a/libraries/framebuf/screen/screen-draw.c +++ b/libraries/framebuf/screen/screen-draw.c @@ -351,11 +351,25 @@ void screen_draw_bitmap(screen_t *scr, int x, int y, const bitmap_t *src) /* ----------------------------------------------------------------------- */ +/* Build the box covering the whole screen, ignoring the current clip + * rectangle. Unlike the clip rectangle this is invariant across redraws, so + * clipping a line's endpoints against it yields the same result every time. + */ +static void screen_get_bounds(const screen_t *scr, box_t *bounds) +{ + bounds->x0 = 0; + bounds->y0 = 0; + bounds->x1 = scr->width; + bounds->y1 = scr->height; +} + void screen_draw_line(screen_t *scr, int x0, int y0, int x1, int y1, colour_t colour) { box_t clip_box; + box_t bounds; + int rx0, ry0, rx1, ry1; int dx, dy; int adx, ady; int sx, sy; @@ -364,9 +378,22 @@ void screen_draw_line(screen_t *scr, if (screen_get_clip(scr, &clip_box)) return; /* invalid clipped screen */ - if (line_clip(&clip_box, &x0, &y0, &x1, &y1) == 0) + /* Reject only: the clipped-back endpoints are discarded, since feeding + * them into the stepping maths below would make the pixels chosen depend + * on which clip rectangle we happened to be called with. */ + rx0 = x0; + ry0 = y0; + rx1 = x1; + ry1 = y1; + if (line_clip(&clip_box, &rx0, &ry0, &rx1, &ry1) == 0) return; + /* Bound the number of steps taken. Safe to feed into the stepping maths + * as the screen bounds never vary between calls. Cannot reject: the clip + * box is always a subset of the screen bounds and it just accepted. */ + screen_get_bounds(scr, &bounds); + (void) line_clip(&bounds, &x0, &y0, &x1, &y1); + dx = x1 - x0; adx = abs(dx); sx = SGN(dx); @@ -405,6 +432,8 @@ void screen_draw_line_wu_fix8(screen_t *scr, colour_t colour) { box_t clip_box_f8; + box_t bounds_f8; + fix8_t rx0_f8, ry0_f8, rx1_f8, ry1_f8; fix8_t dx_f8, dy_f8; int steep_b; /* a bool */ fix16_t grad_f16; @@ -423,9 +452,20 @@ void screen_draw_line_wu_fix8(screen_t *scr, /* scale up screen clip box to match the coordinate type */ box_scalelog2(&clip_box_f8, FIX8_SHIFT); - if (line_clip(&clip_box_f8, &x0_f8, &y0_f8, &x1_f8, &y1_f8) == 0) + /* Reject only: see screen_draw_line() for why the clipped-back endpoints + * are discarded rather than used. */ + rx0_f8 = x0_f8; + ry0_f8 = y0_f8; + rx1_f8 = x1_f8; + ry1_f8 = y1_f8; + if (line_clip(&clip_box_f8, &rx0_f8, &ry0_f8, &rx1_f8, &ry1_f8) == 0) return; + /* Bound the number of steps taken, using the invariant screen bounds. */ + screen_get_bounds(scr, &bounds_f8); + box_scalelog2(&bounds_f8, FIX8_SHIFT); + (void) line_clip(&bounds_f8, &x0_f8, &y0_f8, &x1_f8, &y1_f8); + dx_f8 = x1_f8 - x0_f8; dy_f8 = y1_f8 - y0_f8; @@ -521,6 +561,7 @@ void screen_draw_line_wu_float(screen_t *scr, colour_t colour) { box_t clip_box; + box_t bounds; int x0, y0, x1, y1; float dx, dy; int steep; /* bool */ @@ -532,6 +573,8 @@ void screen_draw_line_wu_float(screen_t *scr, int alpha1, alpha2; float yf; int ix1, iy1; + int xlo, xhi; + int xstart, xstop; int x, y; if (screen_get_clip(scr, &clip_box)) @@ -546,6 +589,8 @@ void screen_draw_line_wu_float(screen_t *scr, if (line_clip(&clip_box, &x0, &y0, &x1, &y1) == 0) return; + screen_get_bounds(scr, &bounds); + dx = fx1 - fx0; dy = fy1 - fy0; @@ -611,7 +656,18 @@ void screen_draw_line_wu_float(screen_t *scr, /* mid points */ - for (x = ix0 + 1; x < ix1; x++) + /* Bound the loop to the screen. Skipped steps are fast-forwarded through + * the gradient in closed form, so the pixels drawn stay a function of the + * true endpoints alone: the screen bounds, unlike the clip box, are the + * same on every call. */ + xlo = steep ? bounds.y0 : bounds.x0; + xhi = steep ? bounds.y1 : bounds.x1; + xstart = MAX(ix0 + 1, xlo - 1); + xstop = MIN(ix1, xhi + 1); + + yf += grad * (float) (xstart - (ix0 + 1)); + + for (x = xstart; x < xstop; x++) { y = floorf(yf); alpha1 = 255.0f * (y + 1.0f - yf); diff --git a/libraries/framebuf/screen/test/screen-test.c b/libraries/framebuf/screen/test/screen-test.c new file mode 100644 index 00000000..06dbb987 --- /dev/null +++ b/libraries/framebuf/screen/test/screen-test.c @@ -0,0 +1,231 @@ +/* screen-test.c -- test screen drawing */ + +#include +#include + +#include "base/result.h" +#include "base/utils.h" +#include "framebuf/colour.h" +#include "framebuf/pixelfmt.h" +#include "framebuf/screen.h" +#include "geom/box.h" +#include "utils/fxp.h" + +#include "test/all-tests.h" + +/* ----------------------------------------------------------------------- */ + +#define WIDTH 64 +#define HEIGHT 64 + +#define BACKGROUND 0xFF000000 + +typedef struct testscreen +{ + screen_t scr; + pixelfmt_bgrx8888_t pixels[WIDTH * HEIGHT]; +} +testscreen_t; + +typedef enum linekind +{ + linekind_INT, + linekind_WU_FIX8, + linekind_WU_FLOAT +} +linekind_t; + +typedef struct linetest +{ + int x0, y0, x1, y1; +} +linetest_t; + +/* Rectangles which, taken together, cover the whole canvas without overlap. + * Three separate partitions: vertical strips, horizontal strips, and an + * irregular split, mirroring the pieces wuss__clip_to_visible() generates. */ +typedef struct partition +{ + int nboxes; + box_t boxes[6]; +} +partition_t; + +static const partition_t partitions[] = +{ + { 3, { { 0, 0, 20, 64 }, { 20, 0, 41, 64 }, { 41, 0, 64, 64 } } }, + { 3, { { 0, 0, 64, 13 }, { 0, 13, 64, 47 }, { 0, 47, 64, 64 } } }, + { 5, { { 0, 0, 64, 17 }, { 0, 17, 9, 64 }, { 9, 17, 33, 40 }, + { 33, 17, 64, 40 }, { 9, 40, 64, 64 } } } +}; + +static const linetest_t lines[] = +{ + { 4, 32, 60, 32 }, /* horizontal */ + { 32, 4, 32, 60 }, /* vertical */ + { 4, 4, 60, 60 }, /* 45 degrees */ + { 60, 60, 4, 4 }, /* 45 degrees, reversed */ + { 2, 10, 62, 30 }, /* shallow */ + { 62, 30, 2, 10 }, /* shallow, reversed */ + { 10, 2, 30, 62 }, /* steep */ + { 30, 62, 10, 2 }, /* steep, reversed */ + { 4, 13, 60, 13 }, /* lands on a partition boundary */ + { 20, 0, 20, 64 }, /* lands on a partition boundary */ + { -30, 20, 90, 44 }, /* partially outside */ + { 20, -40, 44, 100 }, /* partially outside */ + { -20, -20, -5, -5 } /* wholly outside */ +}; + +/* ----------------------------------------------------------------------- */ + +static void testscreen_init(testscreen_t *ts) +{ + int i; + + for (i = 0; i < WIDTH * HEIGHT; i++) + ts->pixels[i] = BACKGROUND; + + screen_init(&ts->scr, + WIDTH, HEIGHT, + pixelfmt_bgrx8888, + WIDTH * (int) sizeof(ts->pixels[0]), + NULL, + ts->pixels); +} + +static void draw(screen_t *scr, linekind_t kind, const linetest_t *line, + colour_t colour) +{ + switch (kind) + { + case linekind_INT: + screen_draw_line(scr, line->x0, line->y0, line->x1, line->y1, colour); + break; + + case linekind_WU_FIX8: + screen_draw_line_wu_fix8(scr, + INT_TO_FIX8(line->x0), INT_TO_FIX8(line->y0), + INT_TO_FIX8(line->x1), INT_TO_FIX8(line->y1), + colour); + break; + + case linekind_WU_FLOAT: + screen_draw_line_wu_float(scr, + (float) line->x0, (float) line->y0, + (float) line->x1, (float) line->y1, + colour); + break; + } +} + +/* ----------------------------------------------------------------------- */ + +/* The same logical line drawn in one go, and drawn once per piece of a + * partition of the canvas, must produce identical pixels. */ +static result_t test_clip_invariance(void) +{ + static testscreen_t reference; + static testscreen_t pieced; + + colour_t colour; + size_t l, p, k; + int b; + + colour = colour_rgb(255, 255, 255); + + for (k = 0; k < 3; k++) + for (l = 0; l < NELEMS(lines); l++) + { + testscreen_init(&reference); + draw(&reference.scr, (linekind_t) k, &lines[l], colour); + + for (p = 0; p < NELEMS(partitions); p++) + { + testscreen_init(&pieced); + + for (b = 0; b < partitions[p].nboxes; b++) + { + pieced.scr.clip = partitions[p].boxes[b]; + draw(&pieced.scr, (linekind_t) k, &lines[l], colour); + } + + if (memcmp(reference.pixels, pieced.pixels, sizeof(reference.pixels))) + { + printf("screen: clip invariance failed for line %zu, " + "rasterizer %zu, partition %zu\n", l, k, p); + return result_TEST_FAILED; + } + } + } + + return result_TEST_PASSED; +} + +/* Guard against the test above passing vacuously because clipping stopped + * happening at all: pixels outside the clip box must stay untouched. */ +static result_t test_clipping_still_happens(void) +{ + static const box_t clip = { 0, 0, 64, 20 }; + + static testscreen_t ts; + + colour_t colour; + size_t k; + int x, y; + + colour = colour_rgb(255, 255, 255); + + for (k = 0; k < 3; k++) + { + const linetest_t line = { 4, 4, 60, 60 }; /* crosses the clip boundary */ + + testscreen_init(&ts); + ts.scr.clip = clip; + draw(&ts.scr, (linekind_t) k, &line, colour); + + for (y = 0; y < HEIGHT; y++) + for (x = 0; x < WIDTH; x++) + { + if (box_contains_point(&clip, x, y)) + continue; + + if (ts.pixels[y * WIDTH + x] != BACKGROUND) + { + printf("screen: pixel (%d,%d) drawn outside the clip box " + "by rasterizer %zu\n", x, y, k); + return result_TEST_FAILED; + } + } + } + + return result_TEST_PASSED; +} + +/* ----------------------------------------------------------------------- */ + +result_t screen_test(const char *resources) +{ + typedef result_t (*screentestfn)(void); + + static const screentestfn tests[] = + { + test_clip_invariance, + test_clipping_still_happens + }; + + result_t rc; + size_t i; + int nfailures; + + NOT_USED(resources); + + nfailures = 0; + for (i = 0; i < NELEMS(tests); i++) + { + rc = tests[i](); + if (rc != result_TEST_PASSED) + nfailures++; + } + + return (nfailures == 0) ? result_TEST_PASSED : result_TEST_FAILED; +} From 0e37b42e560e3de9318a75aba0e982bd7d4a5a2b Mon Sep 17 00:00:00 2001 From: David Thomas Date: Fri, 28 Aug 2026 11:25:57 +0100 Subject: [PATCH 042/287] fix(wuss): stop test tasks double-counting the scroll offset wuss_mouse_click, wuss_mouse_move and wuss_scroll all add the window's scroll offset before dispatching, so the point a task receives is already in virtual content space. The curve, ball and launcher tasks each added the offset a second time via wuss_window_get_scroll, putting hit-tests, drags, ball spawns and launcher row indices at twice the scroll offset once a window was scrolled. wuss_scroll also computed its event point after stepping the scrollbar, so the point described the content the wheel had just brought under the pointer rather than the content the pointer was over when it turned. Compute the point first. The wuss_EVENT_MOUSE documentation described the point as window-local content coordinates, which is what invited the extra addition; say that the scroll offset is already applied. Adds a wuss test covering all three dispatch paths. Co-Authored-By: Claude Opus 5 --- include/wuss/window.h | 8 +-- libraries/wuss/scroll.c | 10 +++- libraries/wuss/test/tasks/ball.c | 7 ++- libraries/wuss/test/tasks/curve.c | 8 ++- libraries/wuss/test/tasks/launcher.c | 8 ++- libraries/wuss/test/wuss-test.c | 73 ++++++++++++++++++++++++++++ 6 files changed, 97 insertions(+), 17 deletions(-) diff --git a/include/wuss/window.h b/include/wuss/window.h index 1867679e..6b2493a8 100644 --- a/include/wuss/window.h +++ b/include/wuss/window.h @@ -73,9 +73,11 @@ typedef struct wuss_event } redraw; - /** wuss_EVENT_MOUSE: point is window-local content coordinates (the - * content area's top-left is (0,0)). button is meaningful for - * DOWN/UP. */ + /** wuss_EVENT_MOUSE: point is in virtual content space -- the window's + * scroll offset has already been added, so the task must not add it + * again. With a scroll offset of (0,0) this is the same as window-local + * content coordinates, where the content area's top-left is (0,0). + * button is meaningful for DOWN/UP. */ struct { wuss_mouse_action_t action; diff --git a/libraries/wuss/scroll.c b/libraries/wuss/scroll.c index 3900d7b8..f4d0979a 100644 --- a/libraries/wuss/scroll.c +++ b/libraries/wuss/scroll.c @@ -22,20 +22,26 @@ result_t wuss_scroll(wuss_t *wuss, point_t p, int delta, wuss_window_t **hit) if (box_contains_point(&titlebar, x, y)) return result_OK; - wuss__furniture_scroll_step(win, (point_t) { 0, delta }); - if (win->task.handle != NULL) { box_t content; wuss_event_t event; + /* Note the pointer position before scrolling, so the event describes the + * content the pointer was over when the wheel turned, not the content the + * step below brings under it. */ wuss__content_box(win, &content); event.kind = wuss_EVENT_SCROLL; event.data.scroll.point.x = x - content.x0 + win->scroll.x; event.data.scroll.point.y = y - content.y0 + win->scroll.y; event.data.scroll.delta = delta; + + wuss__furniture_scroll_step(win, (point_t) { 0, delta }); + return win->task.handle(win, &event, win->task.task_data); } + wuss__furniture_scroll_step(win, (point_t) { 0, delta }); + return result_OK; } diff --git a/libraries/wuss/test/tasks/ball.c b/libraries/wuss/test/tasks/ball.c index 1dcd0512..ff4c320c 100644 --- a/libraries/wuss/test/tasks/ball.c +++ b/libraries/wuss/test/tasks/ball.c @@ -104,9 +104,12 @@ static result_t ball_mouse(wuss_window_t *window, if (bc->nballs >= BALL_MAX) return result_OK; + /* x,y already arrive in virtual content space, as ball positions are + * held; only the invalidation boxes below need the scroll offset taking + * back off to reach window-local coordinates. */ b = &bc->balls[bc->nballs++]; - b->x = x + scroll.x; - b->y = y + scroll.y; + b->x = x; + b->y = y; b->dx = (bc->nballs & 1) ? 3 : -3; b->dy = (bc->nballs & 2) ? 2 : -2; b->radius = 8; diff --git a/libraries/wuss/test/tasks/curve.c b/libraries/wuss/test/tasks/curve.c index 0854b6db..20bb6533 100644 --- a/libraries/wuss/test/tasks/curve.c +++ b/libraries/wuss/test/tasks/curve.c @@ -116,12 +116,10 @@ static result_t curve_mouse(curve_task_t *task, int y, wuss_window_t *window) { - int i; - point_t scroll; + int i; - wuss_window_get_scroll(window, &scroll); - x += scroll.x; - y += scroll.y; + /* x,y already arrive in virtual content space: wuss_mouse_click/move add the + * window's scroll offset before delivering the event. */ switch (action) { diff --git a/libraries/wuss/test/tasks/launcher.c b/libraries/wuss/test/tasks/launcher.c index 05f11934..c398c96b 100644 --- a/libraries/wuss/test/tasks/launcher.c +++ b/libraries/wuss/test/tasks/launcher.c @@ -97,16 +97,14 @@ static result_t launcher_mouse(wuss_window_t *window, int y, void *task_data) { launcher_task_t *lc; int i; - point_t scroll; launcher_entry_t *entry; result_t rc; lc = task_data; - wuss_window_get_scroll(window, &scroll); - NOT_USED(scroll.x); - - i = (y + scroll.y - LAUNCHER_PAD) / LAUNCHER_ROW_HEIGHT; + /* y already arrives in virtual content space, which is how the rows are + * laid out, so the scroll offset must not be added again here. */ + i = (y - LAUNCHER_PAD) / LAUNCHER_ROW_HEIGHT; if (i < 0 || i >= lc->nentries) return result_OK; diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 8c363a0d..24ea094d 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -445,6 +445,7 @@ typedef struct test_task wuss_mouse_action_t last_action; int last_x, last_y; wuss_button_t last_button; + int last_scroll_x, last_scroll_y; int close_count; int stop_count; int open_count; @@ -475,6 +476,11 @@ static result_t test_handle(wuss_window_t *window, tc->last_button = event->data.mouse.button; break; + case wuss_EVENT_SCROLL: + tc->last_scroll_x = event->data.scroll.point.x; + tc->last_scroll_y = event->data.scroll.point.y; + break; + case wuss_EVENT_CLOSE: tc->close_count++; break; @@ -2703,6 +2709,73 @@ result_t wuss_test(const char *resources) if (rc != result_OK) goto Failure; + printf("test: mouse and scroll events arrive in virtual content space, with the scroll offset applied exactly once\n"); + + { + test_task_t tc_s; + wuss_task_t delegate_s; + box_t box_s, content_s; + wuss_window_t *win_s; + point_t scroll; + + tc_s.mouse_count = 0; + delegate_s.handle = test_handle; + delegate_s.task_data = &tc_s; + + 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(wuss, &box_s, "S", wuss_WINDOW_NONE, + wuss_NO_BACKGROUND, + &delegate_s, 200, 200, &win_s); + if (rc != result_OK) + goto Failure; + + scroll.x = 30; + scroll.y = 40; + wuss_window_set_scroll(win_s, scroll); + wuss_window_get_scroll(win_s, &scroll); /* read back in case it clamped */ + + wuss_window_get_content_bounds(win_s, &content_s); + + rc = wuss_mouse_click(wuss, + (point_t) { content_s.x0 + 5, content_s.y0 + 7 }, + wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); + if (rc != result_OK) + goto Failure; + if (hit != win_s) + goto Failure; + if (tc_s.last_x != 5 + scroll.x || tc_s.last_y != 7 + scroll.y) + goto Failure; /* a task adding the scroll offset itself would double-count it */ /* a task adding the scroll offset itself would double-count it */ + + rc = wuss_mouse_click(wuss, + (point_t) { content_s.x0 + 5, content_s.y0 + 7 }, + wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); + if (rc != result_OK) + goto Failure; + + rc = wuss_mouse_move(wuss, + (point_t) { content_s.x0 + 11, content_s.y0 + 13 }, + &hit); + if (rc != result_OK) + goto Failure; + if (tc_s.last_x != 11 + scroll.x || tc_s.last_y != 13 + scroll.y) + goto Failure; + + tc_s.last_scroll_x = -1; + tc_s.last_scroll_y = -1; + rc = wuss_scroll(wuss, + (point_t) { content_s.x0 + 3, content_s.y0 + 4 }, + 1, &hit); + if (rc != result_OK) + goto Failure; + if (hit != win_s) + goto Failure; + if (tc_s.last_scroll_x != 3 + scroll.x || tc_s.last_scroll_y != 4 + scroll.y) + goto Failure; + + wuss_window_close(win_s); + } + printf("test: wuss_task_stop sends wuss_EVENT_QUIT to each window's task\n"); tc_a.stop_count = 0; From 060d92929990352c0c0f6ea9bfd14b5d77e16e2a Mon Sep 17 00:00:00 2001 From: David Thomas Date: Fri, 28 Aug 2026 12:22:04 +0100 Subject: [PATCH 043/287] refactor(wuss): move wuss_idle declaration to wuss.h wuss_idle operates on the window manager, not a single window, so it belongs alongside the other wuss_t entry points. Co-Authored-By: Claude Opus 5 --- include/wuss/window.h | 12 ------------ include/wuss/wuss.h | 12 ++++++++++++ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/include/wuss/window.h b/include/wuss/window.h index 6b2493a8..8c8fd8b7 100644 --- a/include/wuss/window.h +++ b/include/wuss/window.h @@ -149,18 +149,6 @@ wuss_task_t wuss_task_start(wuss_event_fn_t *handle, */ result_t wuss_task_stop(wuss_window_t *window); -/** - * Broadcast a wuss_EVENT_IDLE event to every window's task, in z-order. - * Intended to be called once per main-loop iteration, after other pending input - * has been handled, so tasks can drive their own animation/timers without the - * caller stepping each one individually. - * - * \param[in] wuss Window manager whose windows' tasks should go idle. - * \return \ref result_OK on success, else the first non-OK result returned by a - * task's handle callback. - */ -result_t wuss_idle(wuss_t *wuss); - /** * Create a window. * diff --git a/include/wuss/wuss.h b/include/wuss/wuss.h index 382133f5..9dcc4d43 100644 --- a/include/wuss/wuss.h +++ b/include/wuss/wuss.h @@ -336,6 +336,18 @@ result_t wuss_scroll(wuss_t *wuss, int delta, wuss_window_t **hit); +/** + * Broadcast a wuss_EVENT_IDLE event to every window's task, in z-order. + * Intended to be called once per main-loop iteration, after other pending input + * has been handled, so tasks can drive their own animation/timers without the + * caller stepping each one individually. + * + * \param[in] wuss Window manager whose windows' tasks should go idle. + * \return \ref result_OK on success, else the first non-OK result returned by a + * task's handle callback. + */ +result_t wuss_idle(wuss_t *wuss); + #ifdef __cplusplus } #endif From 3d3dc342d919ad7f667e7b0c58298bda59891f3e Mon Sep 17 00:00:00 2001 From: David Thomas Date: Fri, 28 Aug 2026 12:24:06 +0100 Subject: [PATCH 044/287] refactor(wuss): split the task API out into wuss/task.h Move the event and task types, wuss_task_start and wuss_task_stop out of window.h into a new public wuss/task.h, and relocate their sources to libraries/wuss/task/. Co-Authored-By: Claude Opus 5 --- CMakeLists.txt | 5 +- include/wuss/task.h | 156 ++++++++++++++++++ include/wuss/window.h | 126 +------------- libraries/wuss/{task-start.c => task/start.c} | 4 +- libraries/wuss/{task-stop.c => task/stop.c} | 4 +- 5 files changed, 164 insertions(+), 131 deletions(-) create mode 100644 include/wuss/task.h rename libraries/wuss/{task-start.c => task/start.c} (74%) rename libraries/wuss/{task-stop.c => task/stop.c} (77%) diff --git a/CMakeLists.txt b/CMakeLists.txt index f741e49e..02b958bf 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -111,6 +111,7 @@ set(PUBLIC_HEADERS include/utils/maths.h include/utils/pack.h include/utils/primes.h + include/wuss/task.h include/wuss/window.h include/wuss/wuss.h) @@ -340,8 +341,8 @@ set(WUSS_SOURCES libraries/wuss/mouse-move.c libraries/wuss/redraw.c libraries/wuss/scroll.c - libraries/wuss/task-start.c - libraries/wuss/task-stop.c + libraries/wuss/task/start.c + libraries/wuss/task/stop.c libraries/wuss/window/at.c libraries/wuss/window/create.c libraries/wuss/window/close.c diff --git a/include/wuss/task.h b/include/wuss/task.h new file mode 100644 index 00000000..b3cd3942 --- /dev/null +++ b/include/wuss/task.h @@ -0,0 +1,156 @@ +/* task.h -- wuss task API */ + +/** + * \file task.h + * + * A Wuss task: the content delegate a window hands its drawing and input + * events to, and the events themselves. + */ + +#ifndef WUSS_TASK_H +#define WUSS_TASK_H + +#ifdef __cplusplus +extern "C" +{ +#endif + +#include "base/result.h" +#include "geom/box.h" +#include "geom/point.h" +#include "framebuf/screen.h" + +#include "wuss/wuss.h" + +/* ----------------------------------------------------------------------- */ + +/** Which kind of event a wuss_event_t carries; more will be added over time. */ +typedef enum wuss_event_kind +{ + wuss_EVENT_IDLE, /**< Wuss has finished its pending tasks. */ + wuss_EVENT_REDRAW, /**< Part of the window's content needs repainting. */ + wuss_EVENT_OPEN, /**< Window moved or resized. */ + wuss_EVENT_CLOSE, /**< Close icon clicked; Wuss takes no action itself. */ + wuss_EVENT_MOUSE, /**< Button down/up over the window's content. */ + wuss_EVENT_SCROLL, /**< Mouse wheel used over the window's content. */ + wuss_EVENT_QUIT /**< Task shutting down, via wuss_task_stop. */ +} +wuss_event_kind_t; + +/** + * An event delivered to a task's handle callback. Only the union member + * matching \c kind is valid. + */ +typedef struct wuss_event +{ + wuss_event_kind_t kind; + union + { + /** wuss_EVENT_REDRAW: called with scr->clip already set to the + * on-screen, clipped content area. bounds and scroll are exactly what + * wuss_window_get_content_bounds/wuss_window_get_scroll would return, + * passed through so tasks don't need to call back into Wuss on every + * redraw. */ + struct + { + screen_t *scr; + + /** + * The region that actually needs repainting, screen space; a subset of + * bounds. Tasks should only touch pixels within this box. + */ + const box_t *content; + + /** + * The window's full (unclipped) content-area box, screen space, as per + * wuss_window_get_content_bounds; for converting screen position to + * document position. + */ + const box_t *bounds; + + /** Current scroll offset, as per wuss_window_get_scroll. */ + point_t scroll; + } + redraw; + + /** wuss_EVENT_MOUSE: point is in virtual content space -- the window's + * scroll offset has already been added, so the task must not add it + * again. With a scroll offset of (0,0) this is the same as window-local + * content coordinates, where the content area's top-left is (0,0). + * button is meaningful for DOWN/UP. */ + struct + { + wuss_mouse_action_t action; + point_t point; + wuss_button_t button; + } + mouse; + + /** wuss_EVENT_SCROLL: point is window-local content coordinates, as + * per mouse. delta's sign and units are as passed to wuss_scroll. */ + struct + { + point_t point; + int delta; + } + scroll; + + /* wuss_EVENT_IDLE, wuss_EVENT_CLOSE, wuss_EVENT_QUIT and + * wuss_EVENT_OPEN carry no data. */ + } + data; +} +wuss_event_t; + +/** + * Task event callback. + * + * \param[in] window The window receiving the event. + * \param[in] event The event; see wuss_event_t. + * \param[in] task_data As passed to wuss_window_create. + * \return \ref result_OK on success, else an appropriate result code. + */ +typedef result_t (wuss_event_fn_t)(wuss_window_t *window, + const wuss_event_t *event, + void *task_data); + +/** A window's content delegate. Copied by value into the window at creation. */ +typedef struct wuss_task +{ + /** + * NULL => task receives no events; Wuss still fills the content background + * per wuss_window_create's bg. + */ + wuss_event_fn_t *handle; + + void *task_data; +} +wuss_task_t; + +/** + * Build a wuss_task_t from its fields. + * + * \param[in] handle Event callback, or NULL for a task that receives no + * events. + * \param[in] task_data Opaque pointer passed back to the callback. + * \return The populated task. + */ +wuss_task_t wuss_task_start(wuss_event_fn_t *handle, + void *task_data); + +/** + * Notify a window's task that it is shutting down, via wuss_EVENT_QUIT. Not + * called automatically by wuss_window_close; call it first if the task needs + * notice before its window is torn down. + * + * \param[in] window Window whose task should be stopped. + * \return \ref result_OK on success, else the result returned by the task's + * handle callback. + */ +result_t wuss_task_stop(wuss_window_t *window); + +#ifdef __cplusplus +} +#endif + +#endif /* WUSS_TASK_H */ diff --git a/include/wuss/window.h b/include/wuss/window.h index 8c8fd8b7..1b0bd14f 100644 --- a/include/wuss/window.h +++ b/include/wuss/window.h @@ -20,135 +20,11 @@ extern "C" #include "geom/point.h" #include "framebuf/screen.h" +#include "wuss/task.h" #include "wuss/wuss.h" /* ----------------------------------------------------------------------- */ -/** Which kind of event a wuss_event_t carries; more will be added over time. */ -typedef enum wuss_event_kind -{ - wuss_EVENT_IDLE, /**< Wuss has finished its pending tasks. */ - wuss_EVENT_REDRAW, /**< Part of the window's content needs repainting. */ - wuss_EVENT_OPEN, /**< Window moved or resized. */ - wuss_EVENT_CLOSE, /**< Close icon clicked; Wuss takes no action itself. */ - wuss_EVENT_MOUSE, /**< Button down/up over the window's content. */ - wuss_EVENT_SCROLL, /**< Mouse wheel used over the window's content. */ - wuss_EVENT_QUIT /**< Task shutting down, via wuss_task_stop. */ -} -wuss_event_kind_t; - -/** - * An event delivered to a task's handle callback. Only the union member - * matching \c kind is valid. - */ -typedef struct wuss_event -{ - wuss_event_kind_t kind; - union - { - /** wuss_EVENT_REDRAW: called with scr->clip already set to the - * on-screen, clipped content area. bounds and scroll are exactly what - * wuss_window_get_content_bounds/wuss_window_get_scroll would return, - * passed through so tasks don't need to call back into Wuss on every - * redraw. */ - struct - { - screen_t *scr; - - /** - * The region that actually needs repainting, screen space; a subset of - * bounds. Tasks should only touch pixels within this box. - */ - const box_t *content; - - /** - * The window's full (unclipped) content-area box, screen space, as per - * wuss_window_get_content_bounds; for converting screen position to - * document position. - */ - const box_t *bounds; - - /** Current scroll offset, as per wuss_window_get_scroll. */ - point_t scroll; - } - redraw; - - /** wuss_EVENT_MOUSE: point is in virtual content space -- the window's - * scroll offset has already been added, so the task must not add it - * again. With a scroll offset of (0,0) this is the same as window-local - * content coordinates, where the content area's top-left is (0,0). - * button is meaningful for DOWN/UP. */ - struct - { - wuss_mouse_action_t action; - point_t point; - wuss_button_t button; - } - mouse; - - /** wuss_EVENT_SCROLL: point is window-local content coordinates, as - * per mouse. delta's sign and units are as passed to wuss_scroll. */ - struct - { - point_t point; - int delta; - } - scroll; - - /* wuss_EVENT_IDLE, wuss_EVENT_CLOSE, wuss_EVENT_QUIT and - * wuss_EVENT_OPEN carry no data. */ - } - data; -} -wuss_event_t; - -/** - * Task event callback. - * - * \param[in] window The window receiving the event. - * \param[in] event The event; see wuss_event_t. - * \param[in] task_data As passed to wuss_window_create. - * \return \ref result_OK on success, else an appropriate result code. - */ -typedef result_t (wuss_event_fn_t)(wuss_window_t *window, - const wuss_event_t *event, - void *task_data); - -/** A window's content delegate. Copied by value into the window at creation. */ -typedef struct wuss_task -{ - /** - * NULL => task receives no events; Wuss still fills the content background - * per wuss_window_create's bg. - */ - wuss_event_fn_t *handle; - - void *task_data; -} -wuss_task_t; - -/** - * Build a wuss_task_t from its fields. - * - * \param[in] handle Event callback, or NULL for a task that receives no - * events. - * \param[in] task_data Opaque pointer passed back to the callback. - * \return The populated task. - */ -wuss_task_t wuss_task_start(wuss_event_fn_t *handle, - void *task_data); - -/** - * Notify a window's task that it is shutting down, via wuss_EVENT_QUIT. Not - * called automatically by wuss_window_close; call it first if the task needs - * notice before its window is torn down. - * - * \param[in] window Window whose task should be stopped. - * \return \ref result_OK on success, else the result returned by the task's - * handle callback. - */ -result_t wuss_task_stop(wuss_window_t *window); - /** * Create a window. * diff --git a/libraries/wuss/task-start.c b/libraries/wuss/task/start.c similarity index 74% rename from libraries/wuss/task-start.c rename to libraries/wuss/task/start.c index 192d2b73..65eb812b 100644 --- a/libraries/wuss/task-start.c +++ b/libraries/wuss/task/start.c @@ -1,8 +1,8 @@ -/* task-start.c -- wuss - minimal window manager */ +/* start.c -- wuss - minimal window manager */ #include -#include "wuss/window.h" +#include "wuss/task.h" wuss_task_t wuss_task_start(wuss_event_fn_t *handle, void *task_data) diff --git a/libraries/wuss/task-stop.c b/libraries/wuss/task/stop.c similarity index 77% rename from libraries/wuss/task-stop.c rename to libraries/wuss/task/stop.c index 2945819f..b8cbd80b 100644 --- a/libraries/wuss/task-stop.c +++ b/libraries/wuss/task/stop.c @@ -1,6 +1,6 @@ -/* task-stop.c -- wuss - minimal window manager */ +/* stop.c -- wuss - minimal window manager */ -#include "impl.h" +#include "../impl.h" result_t wuss_task_stop(wuss_window_t *window) { From 793d66e2cd23321fcc576ec77072c326e16598cd Mon Sep 17 00:00:00 2001 From: David Thomas Date: Fri, 28 Aug 2026 12:39:01 +0100 Subject: [PATCH 045/287] refactor(geom): add size2d_t and use it for wuss window sizes Add a size2d_t (width, height) type alongside point_t, plus box_size to extract one from a box, and use them for wuss_window_create's document extent, wuss_window_resize's new size and struct wuss_window's stored document extent. Co-Authored-By: Claude Opus 5 --- CMakeLists.txt | 2 + docs/wuss.md | 6 +- include/geom/box.h | 10 ++ include/geom/size.h | 13 +++ include/wuss/window.h | 20 ++-- include/wuss/wuss.h | 1 + libraries/geom/box/size.c | 14 +++ libraries/wuss/furniture/drag-resize.c | 6 +- libraries/wuss/furniture/hscroll-box.c | 2 +- libraries/wuss/furniture/scroll-action.c | 8 +- libraries/wuss/furniture/toggle-action.c | 4 +- libraries/wuss/furniture/vscroll-box.c | 2 +- libraries/wuss/impl.h | 3 +- libraries/wuss/test/tasks/ball.c | 3 +- libraries/wuss/test/tasks/blank.c | 3 +- libraries/wuss/test/tasks/chars.c | 3 +- libraries/wuss/test/tasks/checker.c | 6 +- libraries/wuss/test/tasks/curve.c | 3 +- libraries/wuss/test/tasks/gradient.c | 3 +- libraries/wuss/test/tasks/image.c | 3 +- libraries/wuss/test/tasks/launcher.c | 3 +- libraries/wuss/test/tasks/palette.c | 3 +- libraries/wuss/test/tasks/sofa.c | 3 +- libraries/wuss/test/tasks/text.c | 5 +- libraries/wuss/test/wuss-test.c | 114 +++++++++-------------- libraries/wuss/window/create.c | 6 +- libraries/wuss/window/resize.c | 8 +- 27 files changed, 129 insertions(+), 128 deletions(-) create mode 100644 include/geom/size.h create mode 100644 libraries/geom/box/size.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 02b958bf..f9b0db89 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -94,6 +94,7 @@ set(PUBLIC_HEADERS include/geom/line.h include/geom/packer.h include/geom/point.h + include/geom/size.h include/io/path.h include/io/stream-mem.h include/io/stream-mtfcomp.h @@ -255,6 +256,7 @@ set(GEOM_SOURCES libraries/geom/box/reset.c libraries/geom/box/round.c libraries/geom/box/round4.c + libraries/geom/box/size.c libraries/geom/box/translated.c libraries/geom/box/union.c libraries/geom/layout/layout.c diff --git a/docs/wuss.md b/docs/wuss.md index dcd1c393..1d91fc58 100644 --- a/docs/wuss.md +++ b/docs/wuss.md @@ -32,13 +32,13 @@ Create a window with a content bounding box, optional title, appearance flags, a result_t wuss_window_create(wuss_t *wuss, const box_t *content, const char *title, wuss_window_flags_t flags, wuss_colour_t bg, const wuss_task_t *task, - int doc_width, int doc_height, + size2d_t doc, wuss_window_t **window); ``` `bg` is filled in by wuss before each redraw event, or `wuss_NO_BACKGROUND` for the task to draw its own background (avoids a redundant fill behind an opaque task); changeable later via `wuss_window_set_background`. -`doc_width`/`doc_height` are the virtual document extent behind the horizontal/vertical scrollbars' sausage proportion; pass `content`'s own width/height for a window with nothing to scroll. Set once at creation, immutable thereafter. +`doc` is the virtual document extent behind the horizontal/vertical scrollbars' sausage proportion; pass `content`'s own width/height for a window with nothing to scroll. Set once at creation, immutable thereafter. Furniture is additional to `content`, not carved out of it: the window's content area always ends up exactly the box requested, and its on-screen footprint (`wuss_window_get_visible_bounds`) is `content` expanded outward by whatever furniture flags request — a titlebar above, and/or a 1px outline around all four sides. @@ -114,7 +114,7 @@ Feed mouse events in with `wuss_mouse_click` (action `wuss_MOUSE_DOWN` or `wuss_ ## Scrolling -Each window carries a scroll offset, `(0, 0)` by default: the point in the task's virtual content space that appears at the content area's top-left. `wuss_window_set_scroll(window, x, y)` moves it (invalidating the content area so the next redraw picks it up); `wuss_window_get_scroll` reads it back. `wuss_scroll` applies this offset itself as Wuss's default scroll action, clamped to `doc_width`/`doc_height` (set at window creation), before also delivering `wuss_EVENT_SCROLL` to the task if it has a handle: +Each window carries a scroll offset, `(0, 0)` by default: the point in the task's virtual content space that appears at the content area's top-left. `wuss_window_set_scroll(window, x, y)` moves it (invalidating the content area so the next redraw picks it up); `wuss_window_get_scroll` reads it back. `wuss_scroll` applies this offset itself as Wuss's default scroll action, clamped to `doc` (set at window creation), before also delivering `wuss_EVENT_SCROLL` to the task if it has a handle: - window-local `x`/`y` delivered in mouse/scroll events (and expected in `wuss_window_invalidate`'s `local_box`) are in virtual content space, i.e. already shifted by the scroll offset. - a redraw event's `content` is still the on-screen (unscrolled) content box; a task reads the offset itself via `wuss_window_get_scroll` to work out which part of its content to paint there. diff --git a/include/geom/box.h b/include/geom/box.h index 2f5dd9cb..a2fd0736 100644 --- a/include/geom/box.h +++ b/include/geom/box.h @@ -5,6 +5,8 @@ #include +#include "geom/size.h" + #ifdef __cplusplus extern "C" { @@ -36,6 +38,14 @@ typedef os_box box_t; /** Initialises a box from a position (x,y) and a size (w,h). */ #define BOX_POS_SIZE(x, y, w, h) { (x), (y), (x) + (w), (y) + (h) } +/** + * Returns the size of the box "b". + * + * \param[in] b The box to measure. + * \return The box's width and height. + */ +size2d_t box_size(const box_t *b); + /** * Reset the box to an invalid state. * diff --git a/include/geom/size.h b/include/geom/size.h new file mode 100644 index 00000000..202f474b --- /dev/null +++ b/include/geom/size.h @@ -0,0 +1,13 @@ +/* size.h -- size type */ + +#ifndef GEOM_SIZE_H +#define GEOM_SIZE_H + +/** 2D size with integer dimensions. */ +typedef struct size2d +{ + int w, h; +} +size2d_t; + +#endif /* GEOM_SIZE_H */ diff --git a/include/wuss/window.h b/include/wuss/window.h index 1b0bd14f..60762c05 100644 --- a/include/wuss/window.h +++ b/include/wuss/window.h @@ -18,6 +18,7 @@ extern "C" #include "base/result.h" #include "geom/box.h" #include "geom/point.h" +#include "geom/size.h" #include "framebuf/screen.h" #include "wuss/task.h" @@ -54,11 +55,8 @@ extern "C" * wuss_window_set_background. * \param[in] task Content delegate. Copied in. May be NULL for a window * with no content handling. - * \param[in] doc_width Virtual document width, for the horizontal scrollbar's - * sausage proportion; pass content's own width for a - * window with nothing to scroll. - * \param[in] doc_height Virtual document height, for the vertical scrollbar's - * sausage proportion; pass content's own height for a + * \param[in] doc Virtual document extent, for the scrollbars' sausage + * proportions; pass content's own width and height for a * window with nothing to scroll. * \param[out] window Newly created window. Becomes the topmost window. * \return \ref result_OK on success, \ref result_WUSS_TOO_SMALL if content's @@ -71,8 +69,7 @@ result_t wuss_window_create(wuss_t *wuss, wuss_window_flags_t flags, wuss_colour_t bg, const wuss_task_t *task, - int doc_width, - int doc_height, + size2d_t doc, wuss_window_t **window); /** @@ -94,12 +91,11 @@ void wuss_window_move(wuss_window_t *window, point_t p); * Resize a window's content area, preserving its top-left position. * * \param[in] window Window to resize. - * \param[in] width New content width. - * \param[in] height New content height. - * \return \ref result_OK on success, \ref result_WUSS_TOO_SMALL if width or - * height is not positive. + * \param[in] size New content size. + * \return \ref result_OK on success, \ref result_WUSS_TOO_SMALL if size's width + * or height is not positive. */ -result_t wuss_window_resize(wuss_window_t *window, int width, int height); +result_t wuss_window_resize(wuss_window_t *window, size2d_t size); /** * Move a window to one end of the z-order. diff --git a/include/wuss/wuss.h b/include/wuss/wuss.h index 9dcc4d43..9c0f1f7c 100644 --- a/include/wuss/wuss.h +++ b/include/wuss/wuss.h @@ -20,6 +20,7 @@ extern "C" #include "framebuf/bmfont.h" #include "geom/box.h" #include "geom/point.h" +#include "geom/size.h" /* ----------------------------------------------------------------------- */ diff --git a/libraries/geom/box/size.c b/libraries/geom/box/size.c new file mode 100644 index 00000000..d5b0fd28 --- /dev/null +++ b/libraries/geom/box/size.c @@ -0,0 +1,14 @@ +/* size.c -- return the size of the specified box */ + +#include "geom/box.h" +#include "geom/size.h" + +size2d_t box_size(const box_t *box) +{ + size2d_t size; + + size.w = box->x1 - box->x0; + size.h = box->y1 - box->y0; + + return size; +} diff --git a/libraries/wuss/furniture/drag-resize.c b/libraries/wuss/furniture/drag-resize.c index 965ff557..19995ec4 100644 --- a/libraries/wuss/furniture/drag-resize.c +++ b/libraries/wuss/furniture/drag-resize.c @@ -13,8 +13,8 @@ void wuss__furniture_drag_resize(wuss_window_t *window, point_t p) width = p.x - content.x0; height = p.y - content.y0; - width = CLAMP(width, WUSS_MIN_CONTENT, window->doc_width); - height = CLAMP(height, WUSS_MIN_CONTENT, window->doc_height); + width = CLAMP(width, WUSS_MIN_CONTENT, window->doc.w); + height = CLAMP(height, WUSS_MIN_CONTENT, window->doc.h); - wuss_window_resize(window, width, height); + wuss_window_resize(window, (size2d_t) { width, height }); } diff --git a/libraries/wuss/furniture/hscroll-box.c b/libraries/wuss/furniture/hscroll-box.c index 5ea4a61a..76c870f8 100644 --- a/libraries/wuss/furniture/hscroll-box.c +++ b/libraries/wuss/furniture/hscroll-box.c @@ -77,7 +77,7 @@ void wuss__hscroll_sausage_box(const wuss_window_t *window, box_t *out) well_px = well.x1 - well.x0; content_size = content.x1 - content.x0; - doc_size = window->doc_width; + doc_size = window->doc.w; if (doc_size < content_size) doc_size = content_size; diff --git a/libraries/wuss/furniture/scroll-action.c b/libraries/wuss/furniture/scroll-action.c index 9dbeaf7a..9b9edf54 100644 --- a/libraries/wuss/furniture/scroll-action.c +++ b/libraries/wuss/furniture/scroll-action.c @@ -9,8 +9,8 @@ point_t wuss__scroll_clamp(const wuss_window_t *window, point_t desired) wuss__content_box(window, &content); - max_x = window->doc_width - (content.x1 - content.x0); - max_y = window->doc_height - (content.y1 - content.y0); + max_x = window->doc.w - (content.x1 - content.x0); + max_y = window->doc.h - (content.y1 - content.y0); if (max_x < 0) max_x = 0; if (max_y < 0) @@ -54,13 +54,13 @@ void wuss__furniture_drag_sausage(wuss_window_t *window, { well_px = wuss__hscroll_well_px(window); content_size = content.x1 - content.x0; - doc_size = window->doc_width; + doc_size = window->doc.w; } else { well_px = wuss__vscroll_well_px(window); content_size = content.y1 - content.y0; - doc_size = window->doc_height; + doc_size = window->doc.h; } max_scroll = doc_size - content_size; diff --git a/libraries/wuss/furniture/toggle-action.c b/libraries/wuss/furniture/toggle-action.c index 1a4e62e1..854bbcc8 100644 --- a/libraries/wuss/furniture/toggle-action.c +++ b/libraries/wuss/furniture/toggle-action.c @@ -42,8 +42,8 @@ void wuss__furniture_toggle_size(wuss_window_t *window) available_width = MAX(available_width, WUSS_MIN_CONTENT); available_height = MAX(available_height, WUSS_MIN_CONTENT); - width = MIN(window->doc_width, available_width); - height = MIN(window->doc_height, available_height); + width = MIN(window->doc.w, available_width); + height = MIN(window->doc.h, available_height); window->pre_toggle = window->visible; diff --git a/libraries/wuss/furniture/vscroll-box.c b/libraries/wuss/furniture/vscroll-box.c index 20d4532c..ee2631da 100644 --- a/libraries/wuss/furniture/vscroll-box.c +++ b/libraries/wuss/furniture/vscroll-box.c @@ -79,7 +79,7 @@ void wuss__vscroll_sausage_box(const wuss_window_t *window, box_t *out) well_px = well.y1 - well.y0; content_size = content.y1 - content.y0; - doc_size = window->doc_height; + doc_size = window->doc.h; if (doc_size < content_size) doc_size = content_size; diff --git a/libraries/wuss/impl.h b/libraries/wuss/impl.h index 28e965d7..be40269b 100644 --- a/libraries/wuss/impl.h +++ b/libraries/wuss/impl.h @@ -5,6 +5,7 @@ #include "datastruct/list.h" #include "geom/box.h" +#include "geom/size.h" #include "framebuf/screen.h" #include "framebuf/bmfont.h" @@ -63,7 +64,7 @@ struct wuss_window wuss_window_flags_t flags; point_t scroll; /* offset into virtual content space of the * content box's top-left; see wuss_window_set_scroll */ - int doc_width, doc_height; /* virtual document extent, set at creation */ + size2d_t doc; /* virtual document extent, set at creation */ wuss_window_state_t state; /* see wuss_window_state_t */ box_t pre_toggle; /* visible bounds to restore on the next toggle */ char title[WUSS_TITLE_MAX + 1]; diff --git a/libraries/wuss/test/tasks/ball.c b/libraries/wuss/test/tasks/ball.c index ff4c320c..c672be54 100644 --- a/libraries/wuss/test/tasks/ball.c +++ b/libraries/wuss/test/tasks/ball.c @@ -36,8 +36,7 @@ result_t ball_create(wuss_t *wuss, const colour_t *palette, ball_task_t *task) wuss_WINDOW_NONE, wuss_NO_BACKGROUND, &delegate, - box.x1 - box.x0, - box.y1 - box.y0, + box_size(&box), &task->window); } diff --git a/libraries/wuss/test/tasks/blank.c b/libraries/wuss/test/tasks/blank.c index 12d9c008..39f3b709 100644 --- a/libraries/wuss/test/tasks/blank.c +++ b/libraries/wuss/test/tasks/blank.c @@ -33,8 +33,7 @@ result_t blank_create(wuss_t *wuss, int npalette, blank_task_t *task) wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE, palette_PICO8_GREEN, &delegate, - box.x1 - box.x0, - box.y1 - box.y0, + box_size(&box), &task->window); } diff --git a/libraries/wuss/test/tasks/chars.c b/libraries/wuss/test/tasks/chars.c index 8d139527..9ba725a4 100644 --- a/libraries/wuss/test/tasks/chars.c +++ b/libraries/wuss/test/tasks/chars.c @@ -53,8 +53,7 @@ result_t chars_create(wuss_t *wuss, wuss_WINDOW_NO_HSCROLL, wuss_NO_BACKGROUND, &delegate, - box.x1 - box.x0, - box.y1 - box.y0, + box_size(&box), &task->window); } diff --git a/libraries/wuss/test/tasks/checker.c b/libraries/wuss/test/tasks/checker.c index 04e22ab3..7639a535 100644 --- a/libraries/wuss/test/tasks/checker.c +++ b/libraries/wuss/test/tasks/checker.c @@ -40,8 +40,7 @@ result_t checker_create(wuss_t *wuss, wuss_WINDOW_NONE, wuss_NO_BACKGROUND, &delegate, - box.x1 - box.x0, - box.y1 - box.y0, + box_size(&box), &task->window); if (rc != result_OK) return rc; @@ -54,8 +53,7 @@ result_t checker_create(wuss_t *wuss, wuss_WINDOW_NONE, wuss_NO_BACKGROUND, &delegate, - box.x1 - box.x0, - box.y1 - box.y0, + box_size(&box), &task->window2); if (rc != result_OK) { diff --git a/libraries/wuss/test/tasks/curve.c b/libraries/wuss/test/tasks/curve.c index 20bb6533..d31041eb 100644 --- a/libraries/wuss/test/tasks/curve.c +++ b/libraries/wuss/test/tasks/curve.c @@ -48,8 +48,7 @@ result_t curve_create(wuss_t *wuss, wuss_WINDOW_NONE, wuss_NO_BACKGROUND, &delegate, - box.x1 - box.x0, - box.y1 - box.y0, + box_size(&box), &task->window); } diff --git a/libraries/wuss/test/tasks/gradient.c b/libraries/wuss/test/tasks/gradient.c index ec683f6b..78f0c479 100644 --- a/libraries/wuss/test/tasks/gradient.c +++ b/libraries/wuss/test/tasks/gradient.c @@ -45,8 +45,7 @@ result_t gradient_create(wuss_t *wuss, gradient_task_t *task) wuss_WINDOW_NONE, wuss_NO_BACKGROUND, &delegate, - GRADIENT_DOC_WIDTH, - GRADIENT_DOC_HEIGHT, + (size2d_t) { GRADIENT_DOC_WIDTH, GRADIENT_DOC_HEIGHT }, &task->window); } diff --git a/libraries/wuss/test/tasks/image.c b/libraries/wuss/test/tasks/image.c index 2314819e..e840c7c5 100644 --- a/libraries/wuss/test/tasks/image.c +++ b/libraries/wuss/test/tasks/image.c @@ -42,8 +42,7 @@ result_t image_create(wuss_t *wuss, wuss_WINDOW_NONE, palette_PICO8_BLACK, &delegate, - task->bitmap.width, - task->bitmap.height, + (size2d_t) { task->bitmap.width, task->bitmap.height }, &task->window); } diff --git a/libraries/wuss/test/tasks/launcher.c b/libraries/wuss/test/tasks/launcher.c index c398c96b..0e8d75e6 100644 --- a/libraries/wuss/test/tasks/launcher.c +++ b/libraries/wuss/test/tasks/launcher.c @@ -45,8 +45,7 @@ result_t launcher_create(wuss_t *wuss, wuss_WINDOW_NO_CLOSE, wuss_NO_BACKGROUND, &delegate, - box.x1 - box.x0, - box.y1 - box.y0, + box_size(&box), &task->window); } diff --git a/libraries/wuss/test/tasks/palette.c b/libraries/wuss/test/tasks/palette.c index 60c87f85..83d968ce 100644 --- a/libraries/wuss/test/tasks/palette.c +++ b/libraries/wuss/test/tasks/palette.c @@ -32,8 +32,7 @@ result_t palette_create(wuss_t *wuss, wuss_WINDOW_NO_RESIZE_BLIT, /* swatch grid is laid out across the whole window, so a resize must redraw all of it, not just the newly (un)covered edge */ palette_PICO8_BLACK, &delegate, - box.x1 - box.x0, - box.y1 - box.y0, + box_size(&box), &task->window); } diff --git a/libraries/wuss/test/tasks/sofa.c b/libraries/wuss/test/tasks/sofa.c index 98b0cd8d..457aeb8d 100644 --- a/libraries/wuss/test/tasks/sofa.c +++ b/libraries/wuss/test/tasks/sofa.c @@ -277,8 +277,7 @@ result_t sofa_create(wuss_t *wuss, const colour_t *palette, sofa_task_t *task) wuss_WINDOW_NONE, wuss_NO_BACKGROUND, &delegate, - box.x1 - box.x0, - box.y1 - box.y0, + box_size(&box), &task->window); } diff --git a/libraries/wuss/test/tasks/text.c b/libraries/wuss/test/tasks/text.c index 1d05faa9..629bea22 100644 --- a/libraries/wuss/test/tasks/text.c +++ b/libraries/wuss/test/tasks/text.c @@ -53,8 +53,7 @@ result_t text_create(wuss_t *wuss, 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 */ palette_PICO8_BLUE, &delegate, - box.x1 - box.x0, - box.y1 - box.y0, + box_size(&box), &task->window); return rc; @@ -156,7 +155,7 @@ static result_t text_idle(void *task_data) angle = tcx->frame_count * (2.0 * M_PI / TEXT_RESIZE_PERIOD_FRAMES); width = tcx->base_width + (int) (TEXT_RESIZE_AMPLITUDE * sin(angle)); - rc = wuss_window_resize(tcx->window, width, height); + rc = wuss_window_resize(tcx->window, (size2d_t) { width, height }); if (rc != result_OK) logf_warning("text_idle: wuss_window_resize(%d, %d) failed", width, height); diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 24ea094d..f1193661 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -580,8 +580,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NONE, wuss_NO_BACKGROUND, NULL, - box_a.x1 - box_a.x0, - box_a.y1 - box_a.y0, + box_size(&box_a), &win_a); if (rc != result_WUSS_TOO_SMALL) goto Failure; @@ -606,8 +605,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_RESIZE, wuss_NO_BACKGROUND, &delegate_a, - box_a.x1 - box_a.x0, - box_a.y1 - box_a.y0, + box_size(&box_a), &win_a); if (rc != result_OK) goto Failure; @@ -629,8 +627,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_RESIZE, wuss_NO_BACKGROUND, &delegate_b, - box_b.x1 - box_b.x0, - box_b.y1 - box_b.y0, + box_size(&box_b), &win_b); if (rc != result_OK) goto Failure; @@ -872,13 +869,13 @@ result_t wuss_test(const char *resources) printf("test: window_resize valid and too-small cases\n"); - rc = wuss_window_resize(win_a, 50, 0); /* zero-height content is invalid */ + rc = wuss_window_resize(win_a, (size2d_t) { 50, 0 }); /* zero-height content is invalid */ if (rc != result_WUSS_TOO_SMALL) goto Failure; if (tc_a.open_count != 1) goto Failure; /* rejected resize: no wuss_EVENT_OPEN */ - rc = wuss_window_resize(win_a, 50, 50); + rc = wuss_window_resize(win_a, (size2d_t) { 50, 50 }); if (rc != result_OK) goto Failure; if (tc_a.open_count != 2) @@ -915,8 +912,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_RESIZE, wuss_NO_BACKGROUND, &delegate_d, - box_d.x1 - box_d.x0, - box_d.y1 - box_d.y0, + box_size(&box_d), &win_d); if (rc != result_OK) goto Failure; @@ -963,8 +959,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_RESIZE, wuss_NO_BACKGROUND, &delegate_e, - box_e.x1 - box_e.x0, - box_e.y1 - box_e.y0, + box_size(&box_e), &win_e); if (rc != result_OK) goto Failure; @@ -984,8 +979,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_RESIZE, wuss_NO_BACKGROUND, &delegate_f, - box_f.x1 - box_f.x0, - box_f.y1 - box_f.y0, + box_size(&box_f), &win_f); if (rc != result_OK) goto Failure; @@ -1052,8 +1046,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_RESIZE, wuss_NO_BACKGROUND, &delegate_h, - box_h.x1 - box_h.x0, - box_h.y1 - box_h.y0, + box_size(&box_h), &win_h); if (rc != result_OK) goto Failure; @@ -1073,8 +1066,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_RESIZE, wuss_NO_BACKGROUND, &delegate_g, - box_g.x1 - box_g.x0, - box_g.y1 - box_g.y0, + box_size(&box_g), &win_g); if (rc != result_OK) goto Failure; @@ -1096,7 +1088,7 @@ result_t wuss_test(const char *resources) if (tc_h.redraw_count != before_h || tc_g.redraw_count != before_g) goto Failure; /* nothing visible changed: no redraw of either window */ - rc = wuss_window_resize(win_h, 25, 25); /* still entirely within G's footprint */ + rc = wuss_window_resize(win_h, (size2d_t) { 25, 25 }); /* still entirely within G's footprint */ if (rc != result_OK) goto Failure; if (wuss_get_dirty_count(wuss) != 0) @@ -1135,8 +1127,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_RESIZE, wuss_NO_BACKGROUND, &delegate_i, - box_i.x1 - box_i.x0, - box_i.y1 - box_i.y0, + box_size(&box_i), &win_i); if (rc != result_OK) goto Failure; @@ -1156,8 +1147,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_RESIZE, wuss_NO_BACKGROUND, &delegate_j, - box_j.x1 - box_j.x0, - box_j.y1 - box_j.y0, + box_size(&box_j), &win_j); if (rc != result_OK) goto Failure; @@ -1207,8 +1197,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_RESIZE, wuss_NO_BACKGROUND, &delegate_m, - box_m.x1 - box_m.x0, - box_m.y1 - box_m.y0, + box_size(&box_m), &win_m); if (rc != result_OK) goto Failure; @@ -1260,8 +1249,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NONE, wuss_NO_BACKGROUND, &delegate_h, - box_h.x1 - box_h.x0, - box_h.y1 - box_h.y0, + box_size(&box_h), &win_h); if (rc != result_OK) goto Failure; @@ -1279,8 +1267,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL, wuss_NO_BACKGROUND, &delegate_g, - box_g.x1 - box_g.x0, - box_g.y1 - box_g.y0, + box_size(&box_g), &win_g); if (rc != result_OK) goto Failure; @@ -1402,7 +1389,8 @@ result_t wuss_test(const char *resources) box_t_win.x1 = 50; box_t_win.y1 = 50; /* 40x40 content, room to grow to a 200x200 doc */ rc = wuss_window_create(wuss, &box_t_win, "T", wuss_WINDOW_NONE, wuss_NO_BACKGROUND, - &delegate_t, 200, 200, &win_t); + &delegate_t, + (size2d_t) { 200, 200 }, &win_t); if (rc != result_OK) goto Failure; @@ -1567,7 +1555,8 @@ result_t wuss_test(const char *resources) box_r.x1 = 50; box_r.y1 = 50; /* 40x40 content; doc bigger than that, so it starts scrollable */ rc = wuss_window_create(wuss, &box_r, "R", wuss_WINDOW_NONE, wuss_NO_BACKGROUND, - &delegate_r, 70, 70, &win_r); + &delegate_r, + (size2d_t) { 70, 70 }, &win_r); if (rc != result_OK) goto Failure; @@ -1664,7 +1653,8 @@ result_t wuss_test(const char *resources) box_nb.x1 = 50; box_nb.y1 = 50; /* 40x40 content, room to grow to a 200x200 doc */ rc = wuss_window_create(wuss, &box_nb, "NB", wuss_WINDOW_NO_RESIZE_BLIT, wuss_NO_BACKGROUND, - &delegate_nb, 200, 200, &win_nb); + &delegate_nb, + (size2d_t) { 200, 200 }, &win_nb); if (rc != result_OK) goto Failure; @@ -1742,7 +1732,8 @@ 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(wuss, &box_u, "U", wuss_WINDOW_NONE, wuss_NO_BACKGROUND, /* scrollbars on: carve.x/y = icon size */ - &delegate_u, 70, 70, &win_u); /* doc size well within the 200x200 screen: growth is doc-limited, not screen-limited */ + &delegate_u, + (size2d_t) { 70, 70 }, &win_u); /* doc size well within the 200x200 screen: growth is doc-limited, not screen-limited */ if (rc != result_OK) goto Failure; @@ -1849,7 +1840,8 @@ result_t wuss_test(const char *resources) * screen-limited, not doc-limited. */ rc = wuss_window_create(wuss, &box_v, "V", wuss_WINDOW_NONE, wuss_NO_BACKGROUND, - &delegate_v, 200, 200, &win_v); + &delegate_v, + (size2d_t) { 200, 200 }, &win_v); if (rc != result_OK) goto Failure; @@ -1962,8 +1954,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_RESIZE, wuss_NO_BACKGROUND, &delegate_k, - box_k.x1 - box_k.x0, - box_k.y1 - box_k.y0, + box_size(&box_k), &win_k); if (rc != result_OK) goto Failure; @@ -1983,8 +1974,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_RESIZE, wuss_NO_BACKGROUND, &delegate_l, - box_l.x1 - box_l.x0, - box_l.y1 - box_l.y0, + box_size(&box_l), &win_l); if (rc != result_OK) goto Failure; @@ -2050,8 +2040,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL, wuss_NO_BACKGROUND, &delegate_m2, - box_m2.x1 - box_m2.x0, - box_m2.y1 - box_m2.y0, + box_size(&box_m2), &win_m2); if (rc != result_OK) goto Failure; @@ -2064,7 +2053,7 @@ result_t wuss_test(const char *resources) interior_x = before2.x0 + 2; /* inside the untouched left edge */ interior_y = before2.y0 + 25; /* below the titlebar, in plain content */ - rc = wuss_window_resize(win_m2, 80, 80); /* grow */ + rc = wuss_window_resize(win_m2, (size2d_t) { 80, 80 }); /* grow */ if (rc != result_OK) goto Failure; @@ -2094,7 +2083,7 @@ result_t wuss_test(const char *resources) before2 = after2; - rc = wuss_window_resize(win_m2, 40, 40); /* shrink back */ + rc = wuss_window_resize(win_m2, (size2d_t) { 40, 40 }); /* shrink back */ if (rc != result_OK) goto Failure; @@ -2143,8 +2132,7 @@ result_t wuss_test(const char *resources) rc = wuss_window_create(wuss, &box_nb2, "NB2", wuss_WINDOW_NO_RESIZE_BLIT, wuss_NO_BACKGROUND, &delegate_nb2, - box_nb2.x1 - box_nb2.x0, - box_nb2.y1 - box_nb2.y0, + box_size(&box_nb2), &win_nb2); if (rc != result_OK) goto Failure; @@ -2155,7 +2143,7 @@ result_t wuss_test(const char *resources) wuss_window_get_visible_bounds(win_nb2, &before3); - rc = wuss_window_resize(win_nb2, 80, 80); /* grow */ + rc = wuss_window_resize(win_nb2, (size2d_t) { 80, 80 }); /* grow */ if (rc != result_OK) goto Failure; @@ -2195,8 +2183,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_RESIZE, wuss_NO_BACKGROUND, &delegate_n, - box_n.x1 - box_n.x0, - box_n.y1 - box_n.y0, + box_size(&box_n), &win_n); if (rc != result_OK) goto Failure; @@ -2214,8 +2201,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_RESIZE, wuss_NO_BACKGROUND, &delegate_o, - box_o.x1 - box_o.x0, - box_o.y1 - box_o.y0, + box_size(&box_o), &win_o); if (rc != result_OK) goto Failure; @@ -2299,8 +2285,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_RESIZE, wuss_NO_BACKGROUND, &delegate_b, - box_b.x1 - box_b.x0, - box_b.y1 - box_b.y0, + box_size(&box_b), &win_b); if (rc != result_OK) goto Failure; @@ -2318,8 +2303,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_RESIZE, wuss_NO_BACKGROUND, &delegate_a, - box_a.x1 - box_a.x0, - box_a.y1 - box_a.y0, + box_size(&box_a), &win_a); if (rc != result_OK) goto Failure; @@ -2402,8 +2386,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_RESIZE, wuss_NO_BACKGROUND, &delegate_b, - box_b.x1 - box_b.x0, - box_b.y1 - box_b.y0, + box_size(&box_b), &win_b); if (rc != result_OK) goto Failure; @@ -2421,8 +2404,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_RESIZE, wuss_NO_BACKGROUND, &delegate_a, - box_a.x1 - box_a.x0, - box_a.y1 - box_a.y0, + box_size(&box_a), &win_a); if (rc != result_OK) goto Failure; @@ -2482,8 +2464,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_RESIZE, wuss_NO_BACKGROUND, &delegate_b, - box_b.x1 - box_b.x0, - box_b.y1 - box_b.y0, + box_size(&box_b), &win_b); if (rc != result_OK) goto Failure; @@ -2501,8 +2482,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_RESIZE, wuss_NO_BACKGROUND, &delegate_a, - box_a.x1 - box_a.x0, - box_a.y1 - box_a.y0, + box_size(&box_a), &win_a); if (rc != result_OK) goto Failure; @@ -2595,8 +2575,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_RESIZE, wuss_NO_BACKGROUND, &delegate_b, - box_b.x1 - box_b.x0, - box_b.y1 - box_b.y0, + box_size(&box_b), &win_b); if (rc != result_OK) goto Failure; @@ -2614,8 +2593,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_RESIZE, wuss_NO_BACKGROUND, &delegate_a, - box_a.x1 - box_a.x0, - box_a.y1 - box_a.y0, + box_size(&box_a), &win_a); if (rc != result_OK) goto Failure; @@ -2691,8 +2669,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_RESIZE, wuss_NO_BACKGROUND, &delegate_c, - box_c.x1 - box_c.x0, - box_c.y1 - box_c.y0, + box_size(&box_c), &win_c); if (rc != result_OK) goto Failure; @@ -2726,7 +2703,8 @@ result_t wuss_test(const char *resources) box_s.x1 = 60; box_s.y1 = 60; /* 50x50 content onto a 200x200 doc: room to scroll */ rc = wuss_window_create(wuss, &box_s, "S", wuss_WINDOW_NONE, wuss_NO_BACKGROUND, - &delegate_s, 200, 200, &win_s); + &delegate_s, + (size2d_t) { 200, 200 }, &win_s); if (rc != result_OK) goto Failure; diff --git a/libraries/wuss/window/create.c b/libraries/wuss/window/create.c index 76f6fb67..dc10adcb 100644 --- a/libraries/wuss/window/create.c +++ b/libraries/wuss/window/create.c @@ -16,8 +16,7 @@ result_t wuss_window_create(wuss_t *wuss, wuss_window_flags_t flags, wuss_colour_t bg, const wuss_task_t *task, - int doc_width, - int doc_height, + size2d_t doc, wuss_window_t **window) { wuss_window_t *win; @@ -76,8 +75,7 @@ result_t wuss_window_create(wuss_t *wuss, win->flags = flags; win->scroll.x = 0; win->scroll.y = 0; - win->doc_width = doc_width; - win->doc_height = doc_height; + win->doc = doc; win->state = wuss_WINDOW_STATE_NONE; if (task != NULL) diff --git a/libraries/wuss/window/resize.c b/libraries/wuss/window/resize.c index f7465af4..46e6e7ff 100644 --- a/libraries/wuss/window/resize.c +++ b/libraries/wuss/window/resize.c @@ -30,13 +30,13 @@ static void invalidate_grown_or_shrunk(wuss_window_t *window, } } -result_t wuss_window_resize(wuss_window_t *window, int width, int height) +result_t wuss_window_resize(wuss_window_t *window, size2d_t size) { int outline_px, titlebar_height; box_t before; point_t carve; - if (!wuss__size_ok(width, height)) + if (!wuss__size_ok(size.w, size.h)) return result_WUSS_TOO_SMALL; outline_px = wuss__outline_px(window); @@ -44,8 +44,8 @@ result_t wuss_window_resize(wuss_window_t *window, int width, int height) before = window->visible; wuss__furniture_carve_for(window->flags, wuss__icon_size(window), &carve); - window->visible.x1 = window->visible.x0 + width + 2 * outline_px + carve.x; - window->visible.y1 = window->visible.y0 + height + titlebar_height + 2 * outline_px + carve.y; + window->visible.x1 = window->visible.x0 + size.w + 2 * outline_px + carve.x; + window->visible.y1 = window->visible.y0 + size.h + titlebar_height + 2 * outline_px + carve.y; wuss__notify_open(window); From 93e7f1f8db2e607a044daa722a2ee5018abe3c26 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Fri, 28 Aug 2026 12:42:06 +0100 Subject: [PATCH 046/287] refactor(framebuf): use size2d_t for bitmap, screen and rect sizes Replace bitmap_t/screen_t's width and height members with a single size2d_t, and take a size2d_t in bitmap_init, screen_init and screen_draw_rect. Most screen_draw_rect call sites now pass box_size() directly instead of subtracting the box's edges by hand. Co-Authored-By: Claude Opus 5 --- include/framebuf/bitmap.h | 9 ++--- include/framebuf/screen.h | 11 ++---- libraries/framebuf/bitmap/bitmap.c | 26 ++++++------- libraries/framebuf/bitmap/load.c | 3 +- libraries/framebuf/bitmap/save.c | 10 ++--- libraries/framebuf/bmfont/test/bmfont-test.c | 4 +- libraries/framebuf/composite/composite.c | 8 ++-- .../framebuf/composite/test/composite-test.c | 22 +++++------ libraries/framebuf/curve/test/curve-test.c | 4 +- libraries/framebuf/screen/screen-draw.c | 16 ++++---- libraries/framebuf/screen/screen.c | 10 ++--- libraries/framebuf/screen/test/screen-test.c | 3 +- libraries/wuss/furniture/draw.c | 39 ++++++++----------- libraries/wuss/furniture/toggle-action.c | 4 +- libraries/wuss/redraw.c | 14 +++---- libraries/wuss/test/tasks/ball.c | 7 +--- libraries/wuss/test/tasks/chars.c | 2 +- libraries/wuss/test/tasks/curve.c | 3 +- libraries/wuss/test/tasks/image.c | 4 +- libraries/wuss/test/tasks/launcher.c | 3 +- libraries/wuss/test/tasks/palette.c | 2 +- libraries/wuss/test/tasks/sofa.c | 4 +- libraries/wuss/test/wuss-test.c | 6 +-- libraries/wuss/window/create.c | 4 +- 24 files changed, 94 insertions(+), 124 deletions(-) diff --git a/include/framebuf/bitmap.h b/include/framebuf/bitmap.h index 32fe965a..59305c41 100644 --- a/include/framebuf/bitmap.h +++ b/include/framebuf/bitmap.h @@ -7,10 +7,11 @@ #include "framebuf/colour.h" #include "framebuf/pixelfmt.h" #include "framebuf/span.h" +#include "geom/size.h" /** Common bitmap structure members (used for screens too). */ #define bitmap_COMMON_MEMBERS \ - int width, height; /**< Width and height of the bitmap in pixels. */ \ + size2d_t size; /**< Width and height of the bitmap in pixels. */ \ pixelfmt_t format; /**< Pixel format of the bitmap. */ \ int rowbytes; /**< Number of bytes per row of the bitmap. */ \ colour_t *palette; /**< Palette of the bitmap, or NULL. */ \ @@ -32,8 +33,7 @@ bitmap_t; * Initialise a previously allocated bitmap structure. * * \param[in] bm Bitmap to initialise. - * \param[in] width Width of the bitmap in pixels. - * \param[in] height Height of the bitmap in pixels. + * \param[in] size Width and height of the bitmap in pixels. * \param[in] fmt Pixel format of the bitmap. * \param[in] rowbytes Number of bytes per row of the bitmap. * \param[in] palette Palette of the bitmap, or NULL. @@ -41,8 +41,7 @@ bitmap_t; * \return \ref result_OK on success, or appropriate result code otherwise. */ result_t bitmap_init(bitmap_t *bm, - int width, - int height, + size2d_t size, pixelfmt_t fmt, int rowbytes, const colour_t *palette, diff --git a/include/framebuf/screen.h b/include/framebuf/screen.h index d069fbbc..41988ede 100644 --- a/include/framebuf/screen.h +++ b/include/framebuf/screen.h @@ -24,16 +24,14 @@ screen_t; * Initialize a previously allocated screen structure. * * \param[in] scr Screen to initialize. - * \param[in] width Width of screen in pixels. - * \param[in] height Height of screen in pixels. + * \param[in] size Width and height of the screen in pixels. * \param[in] fmt Pixel format of the screen. * \param[in] rowbytes Number of bytes per row of the screen. * \param[in] palette Palette of the screen, or NULL. * \param[in] base Base address of the screen. */ void screen_init(screen_t *scr, - int width, - int height, + size2d_t size, pixelfmt_t fmt, int rowbytes, colour_t *palette, @@ -73,13 +71,12 @@ void screen_draw_pixel(screen_t *scr, int x, int y, colour_t colour); * \param[in] scr Screen to draw upon. * \param[in] x X coordinate of leftmost point of rectangle. * \param[in] y Y coordinate of topmost point of rectangle. - * \param[in] width Width of rectangle. - * \param[in] height Height of rectangle. + * \param[in] size Width and height of rectangle. * \param[in] colour Colour of rectangle. */ void screen_draw_rect(screen_t *scr, int x, int y, - int width, int height, + size2d_t size, colour_t colour); /** diff --git a/libraries/framebuf/bitmap/bitmap.c b/libraries/framebuf/bitmap/bitmap.c index 1d6354a9..af839a25 100644 --- a/libraries/framebuf/bitmap/bitmap.c +++ b/libraries/framebuf/bitmap/bitmap.c @@ -9,8 +9,7 @@ #include "framebuf/span-registry.h" result_t bitmap_init(bitmap_t *bm, - int width, - int height, + size2d_t size, pixelfmt_t fmt, int rowbytes, const colour_t *palette, @@ -20,8 +19,7 @@ result_t bitmap_init(bitmap_t *bm, assert(bm); - bm->width = width; - bm->height = height; + bm->size = size; bm->format = fmt; bm->rowbytes = rowbytes; bm->palette = NULL; @@ -77,7 +75,7 @@ void bitmap_clear(bitmap_t *bm, colour_t colour) case 2: px *= 0x11; break; case 3: px *= 0x01; break; } - memset(bm->base, px, bm->rowbytes * bm->height); + memset(bm->base, px, bm->rowbytes * bm->size.h); break; case 5: /* 32bpp - pixels are ints */ @@ -87,18 +85,18 @@ void bitmap_clear(bitmap_t *bm, colour_t colour) pixelfmt_any32_t tmp2 = tmp1 ^ (tmp1 >> 8); if (tmp2 == 0) { - memset(bm->base, px, bm->rowbytes * bm->height); + memset(bm->base, px, bm->rowbytes * bm->size.h); } else { pixelfmt_any32_t *pixels; pixels = bm->base; - for (y = 0; y < bm->height; y++) + for (y = 0; y < bm->size.h; y++) { - for (x = 0; x < bm->width; x++) + for (x = 0; x < bm->size.w; x++) *pixels++ = px; - pixels += bm->rowbytes / sizeof(*pixels) - bm->width; + pixels += bm->rowbytes / sizeof(*pixels) - bm->size.w; } } } @@ -126,7 +124,7 @@ static result_t bmconv_p4_to_bgrx8888(const bitmap_t *src, bitmap_t **pdst) for (i = 0; i < 16; i++) map[i] = colour_to_pixel(src->palette, 16, src->palette[i], pixelfmt_bgrx8888); - outpixels = malloc(src->width * sizeof(pixelfmt_bgrx8888_t) * src->height); // rowbytes rounding needed? + outpixels = malloc(src->size.w * sizeof(pixelfmt_bgrx8888_t) * src->size.h); // rowbytes rounding needed? if (outpixels == NULL) return result_OOM; @@ -138,18 +136,18 @@ static result_t bmconv_p4_to_bgrx8888(const bitmap_t *src, bitmap_t **pdst) } rc = bitmap_init(dst, - src->width, src->height, + src->size, pixelfmt_bgrx8888, - src->width * sizeof(pixelfmt_bgrx8888_t), + src->size.w * sizeof(pixelfmt_bgrx8888_t), NULL, outpixels); if (rc) return rc; inpixels = src->base; - for (y = 0; y < src->height; y++) + for (y = 0; y < src->size.h; y++) { - for (x = 0; x < src->width / 8; x++) + 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 diff --git a/libraries/framebuf/bitmap/load.c b/libraries/framebuf/bitmap/load.c index 8177e37f..609a349e 100644 --- a/libraries/framebuf/bitmap/load.c +++ b/libraries/framebuf/bitmap/load.c @@ -124,8 +124,7 @@ result_t bitmap_load_png(bitmap_t *bm, const char *filename) png_read_image(png_ptr, row_pointers); - bitmap_init(bm, - pngwidth, pngheight, + bitmap_init(bm, (size2d_t) { pngwidth, pngheight }, bm_fmt, bm_rowbytes, NULL, /* no palette */ diff --git a/libraries/framebuf/bitmap/save.c b/libraries/framebuf/bitmap/save.c index 9b627c14..7be936e5 100755 --- a/libraries/framebuf/bitmap/save.c +++ b/libraries/framebuf/bitmap/save.c @@ -67,7 +67,7 @@ result_t bitmap_save_png(const bitmap_t *bm, const char *filename) png_init_io(png_ptr, fp); png_set_IHDR(png_ptr, info_ptr, - bm->width, bm->height, + bm->size.w, bm->size.h, 8, fmt, PNG_INTERLACE_NONE, @@ -81,7 +81,7 @@ result_t bitmap_save_png(const bitmap_t *bm, const char *filename) // png_set_filler(png_ptr, 0, PNG_FILLER_AFTER); // png_set_packing for <8bpp images - outrow = malloc(bytespp * bm->width * sizeof(png_byte)); + outrow = malloc(bytespp * bm->size.w * sizeof(png_byte)); if (outrow == NULL) { rc = result_OOM; @@ -90,14 +90,14 @@ result_t bitmap_save_png(const bitmap_t *bm, const char *filename) inrow = bm->base; - for (y = 0; y < bm->height; y++) + for (y = 0; y < bm->size.h; y++) { png_bytep pout = &outrow[0]; switch (bm->format) { case pixelfmt_bgrx8888: - for (x = 0; x < bm->width; x++) + for (x = 0; x < bm->size.w; x++) { pixelfmt_xxxa8888_t in = *inrow++; *pout++ = PIXELFMT_xxRx8888(in); @@ -107,7 +107,7 @@ result_t bitmap_save_png(const bitmap_t *bm, const char *filename) break; case pixelfmt_bgra8888: - for (x = 0; x < bm->width; x++) + for (x = 0; x < bm->size.w; x++) { pixelfmt_xxxa8888_t in = *inrow++; *pout++ = PIXELFMT_xxRx8888(in); diff --git a/libraries/framebuf/bmfont/test/bmfont-test.c b/libraries/framebuf/bmfont/test/bmfont-test.c index bed3b343..f46302dd 100644 --- a/libraries/framebuf/bmfont/test/bmfont-test.c +++ b/libraries/framebuf/bmfont/test/bmfont-test.c @@ -753,9 +753,7 @@ result_t bmfont_test_one_format(const char *resources, goto Failure; } - bitmap_init(&state.bm, - state.scr_width, - state.scr_height, + bitmap_init(&state.bm, (size2d_t) { state.scr_width, state.scr_height }, scr_fmt, scr_rowbytes, state.palette, diff --git a/libraries/framebuf/composite/composite.c b/libraries/framebuf/composite/composite.c index cd5f4fbe..3aa29acb 100644 --- a/libraries/framebuf/composite/composite.c +++ b/libraries/framebuf/composite/composite.c @@ -936,8 +936,8 @@ static void composite_xxxa8888(composite_rule_t rule, srcscan = src->base; dstscan = dst->base; - width = src->width; - height = src->height; + width = src->size.w; + height = src->size.h; rowbytes = src->rowbytes / sizeof(pixelfmt_xxxa8888_t); while (height--) @@ -959,8 +959,8 @@ result_t composite(composite_rule_t rule, if (src == NULL || dst == NULL) return result_NULL_ARG; - if (src->width != dst->width || - src->height != dst->height || + if (src->size.w != dst->size.w || + src->size.h != dst->size.h || src->format != dst->format) return result_BAD_ARG; diff --git a/libraries/framebuf/composite/test/composite-test.c b/libraries/framebuf/composite/test/composite-test.c index 4577a4b9..58d9bb45 100644 --- a/libraries/framebuf/composite/test/composite-test.c +++ b/libraries/framebuf/composite/test/composite-test.c @@ -34,7 +34,7 @@ static result_t bitmap_clone_by_size(bitmap_t *cloned, const bitmap_t *src) assert(cloned); assert(src); - pixelbytes = src->height * src->rowbytes; + pixelbytes = src->size.h * src->rowbytes; pixels = malloc(pixelbytes); if (pixels == NULL) return result_OOM; @@ -52,13 +52,13 @@ static result_t bitmap_clone_pixels(bitmap_t *dst, const bitmap_t *src) assert(dst); assert(src); - if (dst->width != src->width || - dst->height != src->height || + if (dst->size.w != src->size.w || + dst->size.h != src->size.h || dst->format != src->format || dst->rowbytes != src->rowbytes) return result_INCOMPATIBLE; - memcpy(dst->base, src->base, src->height * src->rowbytes); + memcpy(dst->base, src->base, src->size.h * src->rowbytes); return result_OK; } @@ -73,7 +73,7 @@ static result_t bitmap_plot(const bitmap_t *src, bitmap_t *dst, int x, int y) return result_INCOMPATIBLE; sp += x + y * dst->rowbytes / 4; - for (h = 0; h < src->height; h++) + for (h = 0; h < src->size.h; h++) { memcpy(sp, dp, src->rowbytes); sp += dst->rowbytes / 4; @@ -99,9 +99,9 @@ static result_t bitmap_convert_inplace(bitmap_t *bm, pixelfmt_t new_fmt) pixelfmt_rgbx8888_t *p = bm->base; int x,y; - for (y = 0; y < bm->height; y++) + for (y = 0; y < bm->size.h; y++) { - for (x = 0; x < bm->width; x++) + for (x = 0; x < bm->size.w; x++) { pixelfmt_rgbx8888_t px = *p; *p++ = PIXELFMT_MAKE_BGRA8888(PIXELFMT_Bxxx8888(px), @@ -131,9 +131,9 @@ static result_t bitmap_convert_inplace(bitmap_t *bm, pixelfmt_t new_fmt) pixelfmt_rgba8888_t *p = bm->base; int x,y; - for (y = 0; y < bm->height; y++) + for (y = 0; y < bm->size.h; y++) { - for (x = 0; x < bm->width; x++) + for (x = 0; x < bm->size.w; x++) { pixelfmt_rgba8888_t px = *p; *p++ = PIXELFMT_MAKE_BGRA8888(PIXELFMT_Bxxx8888(px), @@ -177,7 +177,7 @@ static result_t load_test_png(bitmap_t *bm, if (rc) return rc; - if (bm->width != SMALLWIDTH || bm->height != SMALLHEIGHT || bm->format != FORMAT) + if (bm->size.w != SMALLWIDTH || bm->size.h != SMALLHEIGHT || bm->format != FORMAT) { fprintf(stderr, "load_test_png: wrong width, height or format\n"); free(bm->base); @@ -210,7 +210,7 @@ result_t composite_test(const char *resources) goto Failure; } - bitmap_init(&bigbitmap, WIDTH, HEIGHT, FORMAT, scr_rowbytes, NULL, bigpixels); + bitmap_init(&bigbitmap, (size2d_t) { WIDTH, HEIGHT }, FORMAT, scr_rowbytes, NULL, bigpixels); rc = load_test_png(&bm[0], resources, "A"); /* source */ if (rc) diff --git a/libraries/framebuf/curve/test/curve-test.c b/libraries/framebuf/curve/test/curve-test.c index 6349a728..31e83507 100644 --- a/libraries/framebuf/curve/test/curve-test.c +++ b/libraries/framebuf/curve/test/curve-test.c @@ -804,9 +804,7 @@ result_t curve_test_one_format(const char *resources, goto Failure; } - bitmap_init(&state.bm, - state.scr_width, - state.scr_height, + bitmap_init(&state.bm, (size2d_t) { state.scr_width, state.scr_height }, scr_fmt, scr_rowbytes, state.palette, diff --git a/libraries/framebuf/screen/screen-draw.c b/libraries/framebuf/screen/screen-draw.c index 94d11d54..afff1d28 100644 --- a/libraries/framebuf/screen/screen-draw.c +++ b/libraries/framebuf/screen/screen-draw.c @@ -138,7 +138,7 @@ static void screen_blend_pixel(screen_t *scr, void screen_draw_rect(screen_t *scr, int x, int y, - int width, int height, + size2d_t size, colour_t colour) { box_t clip_box; @@ -152,8 +152,8 @@ void screen_draw_rect(screen_t *scr, rect_box.x0 = x; rect_box.y0 = y; - rect_box.x1 = x + width; - rect_box.y1 = y + height; + rect_box.x1 = x + size.w; + rect_box.y1 = y + size.h; if (box_intersection(&clip_box, &rect_box, &draw_box)) return; @@ -214,7 +214,7 @@ void screen_draw_rect(screen_t *scr, void screen_draw_square(screen_t *scr, int x, int y, int size, colour_t colour) { - screen_draw_rect(scr, x, y, size, size, colour); + screen_draw_rect(scr, x, y, (size2d_t) { size, size }, colour); } /* ----------------------------------------------------------------------- */ @@ -232,8 +232,8 @@ void screen_draw_bitmap(screen_t *scr, int x, int y, const bitmap_t *src) src_box.x0 = x; src_box.y0 = y; - src_box.x1 = x + src->width; - src_box.y1 = y + src->height; + src_box.x1 = x + src->size.w; + src_box.y1 = y + src->size.h; if (box_intersection(&clip_box, &src_box, &draw_box)) return; /* nothing visible */ @@ -359,8 +359,8 @@ static void screen_get_bounds(const screen_t *scr, box_t *bounds) { bounds->x0 = 0; bounds->y0 = 0; - bounds->x1 = scr->width; - bounds->y1 = scr->height; + bounds->x1 = scr->size.w; + bounds->y1 = scr->size.h; } void screen_draw_line(screen_t *scr, diff --git a/libraries/framebuf/screen/screen.c b/libraries/framebuf/screen/screen.c index 578ba073..e15fafdc 100644 --- a/libraries/framebuf/screen/screen.c +++ b/libraries/framebuf/screen/screen.c @@ -15,8 +15,7 @@ #include "framebuf/screen.h" void screen_init(screen_t *scr, - int width, - int height, + size2d_t size, pixelfmt_t fmt, int rowbytes, colour_t *palette, @@ -24,8 +23,7 @@ void screen_init(screen_t *scr, { assert(scr); - scr->width = width; - scr->height = height; + scr->size = size; scr->format = fmt; scr->rowbytes = rowbytes; scr->palette = palette; // FIXME: This doesn't clone the palette, whereas bitmap_init()'s equivalent does. @@ -48,8 +46,8 @@ int screen_get_clip(const screen_t *scr, box_t *clip) { clip->x0 = 0; clip->y0 = 0; - clip->x1 = scr->width; - clip->y1 = scr->height; + clip->x1 = scr->size.w; + clip->y1 = scr->size.h; if (box_is_empty(&scr->clip)) return 0; /* not empty */ diff --git a/libraries/framebuf/screen/test/screen-test.c b/libraries/framebuf/screen/test/screen-test.c index 06dbb987..5b59876f 100644 --- a/libraries/framebuf/screen/test/screen-test.c +++ b/libraries/framebuf/screen/test/screen-test.c @@ -85,8 +85,7 @@ static void testscreen_init(testscreen_t *ts) for (i = 0; i < WIDTH * HEIGHT; i++) ts->pixels[i] = BACKGROUND; - screen_init(&ts->scr, - WIDTH, HEIGHT, + screen_init(&ts->scr, (size2d_t) { WIDTH, HEIGHT }, pixelfmt_bgrx8888, WIDTH * (int) sizeof(ts->pixels[0]), NULL, diff --git a/libraries/wuss/furniture/draw.c b/libraries/wuss/furniture/draw.c index 1d49e896..426d9c38 100644 --- a/libraries/wuss/furniture/draw.c +++ b/libraries/wuss/furniture/draw.c @@ -23,8 +23,7 @@ void wuss__furniture_draw(wuss_t *wuss, { wuss->scr->clip = clipped; screen_draw_rect(wuss->scr, - titlebar.x0, titlebar.y0, - titlebar.x1 - titlebar.x0, titlebar.y1 - titlebar.y0, + titlebar.x0, titlebar.y0, box_size(&titlebar), wuss->palette[wuss->furniture_colours.title.bg]); if (wuss->font != NULL && window->title[0] != '\0') @@ -88,8 +87,7 @@ void wuss__furniture_draw(wuss_t *wuss, wuss__close_box(window, &close); screen_draw_rect(wuss->scr, - close.x0, close.y0, - close.x1 - close.x0, close.y1 - close.y0, + close.x0, close.y0, box_size(&close), wuss->palette[wuss->furniture_colours.close]); } @@ -99,8 +97,7 @@ void wuss__furniture_draw(wuss_t *wuss, wuss__back_box(window, &back); screen_draw_rect(wuss->scr, - back.x0, back.y0, - back.x1 - back.x0, back.y1 - back.y0, + back.x0, back.y0, box_size(&back), wuss->palette[wuss->furniture_colours.back]); } @@ -110,8 +107,7 @@ void wuss__furniture_draw(wuss_t *wuss, wuss__toggle_box(window, &toggle); screen_draw_rect(wuss->scr, - toggle.x0, toggle.y0, - toggle.x1 - toggle.x0, toggle.y1 - toggle.y0, + toggle.x0, toggle.y0, box_size(&toggle), wuss->palette[wuss->furniture_colours.toggle]); } } @@ -125,8 +121,7 @@ void wuss__furniture_draw(wuss_t *wuss, { wuss->scr->clip = clipped; screen_draw_rect(wuss->scr, - resize.x0, resize.y0, - resize.x1 - resize.x0, resize.y1 - resize.y0, + resize.x0, resize.y0, box_size(&resize), wuss->palette[wuss->furniture_colours.resize]); } } @@ -139,7 +134,7 @@ void wuss__furniture_draw(wuss_t *wuss, if (!box_intersection(&up, full, &clipped)) { wuss->scr->clip = clipped; - screen_draw_rect(wuss->scr, up.x0, up.y0, up.x1 - up.x0, up.y1 - up.y0, + screen_draw_rect(wuss->scr, up.x0, up.y0, box_size(&up), wuss->palette[wuss->furniture_colours.scroll.arrows]); } @@ -147,7 +142,7 @@ void wuss__furniture_draw(wuss_t *wuss, if (!box_intersection(&down, full, &clipped)) { wuss->scr->clip = clipped; - screen_draw_rect(wuss->scr, down.x0, down.y0, down.x1 - down.x0, down.y1 - down.y0, + screen_draw_rect(wuss->scr, down.x0, down.y0, box_size(&down), wuss->palette[wuss->furniture_colours.scroll.arrows]); } @@ -155,7 +150,7 @@ void wuss__furniture_draw(wuss_t *wuss, if (!box_intersection(&well, full, &clipped)) { wuss->scr->clip = clipped; - screen_draw_rect(wuss->scr, well.x0, well.y0, well.x1 - well.x0, well.y1 - well.y0, + screen_draw_rect(wuss->scr, well.x0, well.y0, box_size(&well), wuss->palette[wuss->furniture_colours.scroll.wells]); } @@ -163,7 +158,7 @@ void wuss__furniture_draw(wuss_t *wuss, if (!box_intersection(&sausage, full, &clipped)) { wuss->scr->clip = clipped; - screen_draw_rect(wuss->scr, sausage.x0, sausage.y0, sausage.x1 - sausage.x0, sausage.y1 - sausage.y0, + screen_draw_rect(wuss->scr, sausage.x0, sausage.y0, box_size(&sausage), wuss->palette[wuss->furniture_colours.scroll.sausages]); } } @@ -176,7 +171,7 @@ void wuss__furniture_draw(wuss_t *wuss, if (!box_intersection(&left, full, &clipped)) { wuss->scr->clip = clipped; - screen_draw_rect(wuss->scr, left.x0, left.y0, left.x1 - left.x0, left.y1 - left.y0, + screen_draw_rect(wuss->scr, left.x0, left.y0, box_size(&left), wuss->palette[wuss->furniture_colours.scroll.arrows]); } @@ -184,7 +179,7 @@ void wuss__furniture_draw(wuss_t *wuss, if (!box_intersection(&right, full, &clipped)) { wuss->scr->clip = clipped; - screen_draw_rect(wuss->scr, right.x0, right.y0, right.x1 - right.x0, right.y1 - right.y0, + screen_draw_rect(wuss->scr, right.x0, right.y0, box_size(&right), wuss->palette[wuss->furniture_colours.scroll.arrows]); } @@ -192,7 +187,7 @@ void wuss__furniture_draw(wuss_t *wuss, if (!box_intersection(&well, full, &clipped)) { wuss->scr->clip = clipped; - screen_draw_rect(wuss->scr, well.x0, well.y0, well.x1 - well.x0, well.y1 - well.y0, + screen_draw_rect(wuss->scr, well.x0, well.y0, box_size(&well), wuss->palette[wuss->furniture_colours.scroll.wells]); } @@ -200,7 +195,7 @@ void wuss__furniture_draw(wuss_t *wuss, if (!box_intersection(&sausage, full, &clipped)) { wuss->scr->clip = clipped; - screen_draw_rect(wuss->scr, sausage.x0, sausage.y0, sausage.x1 - sausage.x0, sausage.y1 - sausage.y0, + screen_draw_rect(wuss->scr, sausage.x0, sausage.y0, box_size(&sausage), wuss->palette[wuss->furniture_colours.scroll.sausages]); } } @@ -215,9 +210,9 @@ void wuss__furniture_draw(wuss_t *wuss, border = wuss->palette[wuss->furniture_colours.title.bg]; /* no dedicated outline class; matches titlebar chrome */ wuss->scr->clip = visible_clipped; - screen_draw_rect(wuss->scr, window->visible.x0, window->visible.y0, width, 1, border); - screen_draw_rect(wuss->scr, window->visible.x0, window->visible.y1 - 1, width, 1, border); - screen_draw_rect(wuss->scr, window->visible.x0, window->visible.y0, 1, height, border); - screen_draw_rect(wuss->scr, window->visible.x1 - 1, window->visible.y0, 1, height, border); + screen_draw_rect(wuss->scr, window->visible.x0, window->visible.y0, (size2d_t) { width, 1 }, border); + screen_draw_rect(wuss->scr, window->visible.x0, window->visible.y1 - 1, (size2d_t) { width, 1 }, border); + screen_draw_rect(wuss->scr, window->visible.x0, window->visible.y0, (size2d_t) { 1, height }, border); + screen_draw_rect(wuss->scr, window->visible.x1 - 1, window->visible.y0, (size2d_t) { 1, height }, border); } } diff --git a/libraries/wuss/furniture/toggle-action.c b/libraries/wuss/furniture/toggle-action.c index 854bbcc8..4203f778 100644 --- a/libraries/wuss/furniture/toggle-action.c +++ b/libraries/wuss/furniture/toggle-action.c @@ -29,8 +29,8 @@ void wuss__furniture_toggle_size(wuss_window_t *window) * account for scrollbar/resize-icon furniture (carve), which sits * outside the content area same as create.c/resize.c do, or the window * ends up carve.x/carve.y short of the doc size it's meant to reach. */ - available_width = window->wuss->scr->width - window->visible.x0 - 2 * outline_px - carve.x; - available_height = window->wuss->scr->height - window->visible.y0 - 2 * outline_px - titlebar_height - carve.y; + available_width = window->wuss->scr->size.w - window->visible.x0 - 2 * outline_px - carve.x; + available_height = window->wuss->scr->size.h - window->visible.y0 - 2 * outline_px - titlebar_height - carve.y; /* a window dragged far enough off-screen leaves no room at all to the * screen edge, so the above can go negative -- floor it like diff --git a/libraries/wuss/redraw.c b/libraries/wuss/redraw.c index b56a86f6..e3f403a3 100644 --- a/libraries/wuss/redraw.c +++ b/libraries/wuss/redraw.c @@ -34,8 +34,7 @@ static void redraw_window(wuss_t *wuss, if (win->bg != wuss_NO_BACKGROUND) screen_draw_rect(wuss->scr, - content.x0, content.y0, - content.x1 - content.x0, content.y1 - content.y0, + content.x0, content.y0, box_size(&content), wuss->palette[win->bg]); if (win->task.handle != NULL) @@ -90,14 +89,13 @@ result_t wuss_redraw(wuss_t *wuss) full.x0 = 0; full.y0 = 0; - full.x1 = wuss->scr->width; - full.y1 = wuss->scr->height; + full.x1 = wuss->scr->size.w; + full.y1 = wuss->scr->size.h; if (wuss->backdrop != wuss_NO_BACKGROUND) { wuss->scr->clip = full; - screen_draw_rect(wuss->scr, full.x0, full.y0, - full.x1 - full.x0, full.y1 - full.y0, + screen_draw_rect(wuss->scr, full.x0, full.y0, box_size(&full), wuss->palette[wuss->backdrop]); } @@ -130,9 +128,7 @@ result_t wuss_redraw_dirty(wuss_t *wuss) { wuss->scr->clip = wuss->dirty[i]; screen_draw_rect(wuss->scr, - wuss->dirty[i].x0, wuss->dirty[i].y0, - wuss->dirty[i].x1 - wuss->dirty[i].x0, - wuss->dirty[i].y1 - wuss->dirty[i].y0, + wuss->dirty[i].x0, wuss->dirty[i].y0, (size2d_t) { wuss->dirty[i].x1 - wuss->dirty[i].x0, wuss->dirty[i].y1 - wuss->dirty[i].y0 }, wuss->palette[wuss->backdrop]); } diff --git a/libraries/wuss/test/tasks/ball.c b/libraries/wuss/test/tasks/ball.c index c672be54..d0af7601 100644 --- a/libraries/wuss/test/tasks/ball.c +++ b/libraries/wuss/test/tasks/ball.c @@ -60,9 +60,7 @@ static result_t ball_redraw(const wuss_event_t *event, void *task_data) sx = event->data.redraw.scroll.x; sy = event->data.redraw.scroll.y; - screen_draw_rect(scr, content->x0, content->y0, - content->x1 - content->x0, - content->y1 - content->y0, + screen_draw_rect(scr, content->x0, content->y0, box_size(content), bc->bg); for (i = 0; i < bc->nballs; i++) @@ -71,8 +69,7 @@ static result_t ball_redraw(const wuss_event_t *event, void *task_data) b = &bc->balls[i]; - screen_draw_rect(scr, bounds->x0 - sx + b->x - b->radius, bounds->y0 - sy + b->y - b->radius, - b->radius * 2, b->radius * 2, bc->ball); + screen_draw_rect(scr, bounds->x0 - sx + b->x - b->radius, bounds->y0 - sy + b->y - b->radius, (size2d_t) { b->radius * 2, b->radius * 2 }, bc->ball); } return result_OK; diff --git a/libraries/wuss/test/tasks/chars.c b/libraries/wuss/test/tasks/chars.c index 9ba725a4..bd68a54e 100644 --- a/libraries/wuss/test/tasks/chars.c +++ b/libraries/wuss/test/tasks/chars.c @@ -97,7 +97,7 @@ static result_t chars_redraw(const wuss_event_t *event, void *task_data) x = bounds->x0 - sx + col * cell_w; y = bounds->y0 - sy + row * cell_h; - screen_draw_rect(scr, x, y, cell_w, cell_h, cc->bg); + screen_draw_rect(scr, x, y, (size2d_t) { cell_w, cell_h }, cc->bg); if (i < first || i >= first + count) continue; /* no glyph for this byte value: leave the cell blank */ diff --git a/libraries/wuss/test/tasks/curve.c b/libraries/wuss/test/tasks/curve.c index d31041eb..b19b09e3 100644 --- a/libraries/wuss/test/tasks/curve.c +++ b/libraries/wuss/test/tasks/curve.c @@ -79,8 +79,7 @@ static result_t curve_redraw(const wuss_event_t *event, curve_task_t *task) sx = event->data.redraw.scroll.x; sy = event->data.redraw.scroll.y; - screen_draw_rect(scr, content->x0, content->y0, - content->x1 - content->x0, content->y1 - content->y0, + screen_draw_rect(scr, content->x0, content->y0, box_size(content), task->bg); prev = task->points[0]; diff --git a/libraries/wuss/test/tasks/image.c b/libraries/wuss/test/tasks/image.c index e840c7c5..d9e55207 100644 --- a/libraries/wuss/test/tasks/image.c +++ b/libraries/wuss/test/tasks/image.c @@ -34,7 +34,7 @@ result_t image_create(wuss_t *wuss, delegate = wuss_task_start(image_handle, task); /* shows through the image's transparent pixels */ /* shorter than the bitmap so there's something to scroll through */ - box = (box_t) BOX_POS_SIZE(370, 10, task->bitmap.width, task->bitmap.height * 2 / 3); + box = (box_t) BOX_POS_SIZE(370, 10, task->bitmap.size.w, task->bitmap.size.h * 2 / 3); return wuss_window_create(wuss, &box, @@ -42,7 +42,7 @@ result_t image_create(wuss_t *wuss, wuss_WINDOW_NONE, palette_PICO8_BLACK, &delegate, - (size2d_t) { task->bitmap.width, task->bitmap.height }, + (size2d_t) { task->bitmap.size.w, task->bitmap.size.h }, &task->window); } diff --git a/libraries/wuss/test/tasks/launcher.c b/libraries/wuss/test/tasks/launcher.c index 0e8d75e6..32188120 100644 --- a/libraries/wuss/test/tasks/launcher.c +++ b/libraries/wuss/test/tasks/launcher.c @@ -71,8 +71,7 @@ static result_t launcher_redraw(const wuss_event_t *event, void *task_data) sx = event->data.redraw.scroll.x; sy = event->data.redraw.scroll.y; - screen_draw_rect(scr, content->x0, content->y0, - content->x1 - content->x0, content->y1 - content->y0, + screen_draw_rect(scr, content->x0, content->y0, box_size(content), lc->bg); bmfont_get_info(lc->font, &font_width, &font_height); diff --git a/libraries/wuss/test/tasks/palette.c b/libraries/wuss/test/tasks/palette.c index 83d968ce..4e44965a 100644 --- a/libraries/wuss/test/tasks/palette.c +++ b/libraries/wuss/test/tasks/palette.c @@ -77,7 +77,7 @@ static result_t palette_redraw(const wuss_event_t *event, void *task_data) x = bounds->x0 - sx + col * cell_w; y = bounds->y0 - sy + row * cell_h; - screen_draw_rect(scr, x, y, cell_w, cell_h, pc->palette[i]); + screen_draw_rect(scr, x, y, (size2d_t) { cell_w, cell_h }, pc->palette[i]); } return result_OK; diff --git a/libraries/wuss/test/tasks/sofa.c b/libraries/wuss/test/tasks/sofa.c index 457aeb8d..300c89da 100644 --- a/libraries/wuss/test/tasks/sofa.c +++ b/libraries/wuss/test/tasks/sofa.c @@ -305,9 +305,7 @@ static result_t sofa_redraw(const wuss_event_t *event, void *task_data) screen_draw_rect(scr, content->x0, - content->y0, - content->x1 - content->x0, - content->y1 - content->y0, + content->y0, box_size(content), sc->bg); cx = bounds->x0 - sx + (bounds->x1 - bounds->x0) / 2; diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index f1193661..125e537b 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -186,9 +186,9 @@ static result_t wuss_interactive_test(const char *resources) goto Failure; #if WUSS_TEST_32BPP - rc = bitmap_init(&bm, scr_width, scr_height, pixelfmt_bgrx8888, rowbytes, palette, pixels); + rc = bitmap_init(&bm, (size2d_t) { scr_width, scr_height }, pixelfmt_bgrx8888, rowbytes, palette, pixels); #else - rc = bitmap_init(&bm, scr_width, scr_height, pixelfmt_p4, rowbytes, palette, pixels); + rc = bitmap_init(&bm, (size2d_t) { scr_width, scr_height }, pixelfmt_p4, rowbytes, palette, pixels); #endif if (rc != result_OK) goto Failure; @@ -529,7 +529,7 @@ result_t wuss_test(const char *resources) if (pixels == NULL) goto Failure; - rc = bitmap_init(&bm, 200, 200, pixelfmt_bgrx8888, rowbytes, NULL, pixels); + rc = bitmap_init(&bm, (size2d_t) { 200, 200 }, pixelfmt_bgrx8888, rowbytes, NULL, pixels); if (rc != result_OK) goto Failure; diff --git a/libraries/wuss/window/create.c b/libraries/wuss/window/create.c index dc10adcb..591da41e 100644 --- a/libraries/wuss/window/create.c +++ b/libraries/wuss/window/create.c @@ -50,8 +50,8 @@ result_t wuss_window_create(wuss_t *wuss, /* nudge back on-screen so the titlebar/close icon stay reachable; a * window bigger than the screen keeps its top-left (titlebar) edge * on-screen rather than being centred or left alone */ - scr_width = wuss->scr->width; - scr_height = wuss->scr->height; + scr_width = wuss->scr->size.w; + scr_height = wuss->scr->size.h; dx = 0; if (win->visible.x0 < 0) From 78d6336d4372f30913016d351e564d74a305cbce Mon Sep 17 00:00:00 2001 From: David Thomas Date: Fri, 28 Aug 2026 13:16:08 +0100 Subject: [PATCH 047/287] feat(wuss): draw interior rules between content and furniture The content area's right and bottom edges abutted the scrollbars (or the resize corner) with no dividing line, leaving the frame's interior looking unfinished. Reserve a further pixel on each carved axis in wuss__furniture_carve_for -- so window creation, resize and toggle-size all account for it -- and draw the rule there, in the same colour as the outline. The requested content box is unaffected: like the rest of the furniture, the rule is added outside it, not carved out of it. Co-Authored-By: Claude Opus 5 --- libraries/wuss/furniture/draw.c | 38 ++++++++++++++++++++++ libraries/wuss/furniture/invalidate.c | 4 +-- libraries/wuss/impl.h | 7 ++++ libraries/wuss/test/wuss-test.c | 47 +++++++++++++++++++++++++++ 4 files changed, 94 insertions(+), 2 deletions(-) diff --git a/libraries/wuss/furniture/draw.c b/libraries/wuss/furniture/draw.c index 426d9c38..bc4dc31b 100644 --- a/libraries/wuss/furniture/draw.c +++ b/libraries/wuss/furniture/draw.c @@ -200,6 +200,44 @@ void wuss__furniture_draw(wuss_t *wuss, } } + { + 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__icon_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; + if (!box_intersection(&rule, full, &clipped)) + { + wuss->scr->clip = clipped; + screen_draw_rect(wuss->scr, rule.x0, rule.y0, box_size(&rule), + 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; + if (!box_intersection(&rule, full, &clipped)) + { + wuss->scr->clip = clipped; + screen_draw_rect(wuss->scr, rule.x0, rule.y0, box_size(&rule), + wuss->palette[wuss->furniture_colours.title.bg]); + } + } + } + if (!(window->flags & wuss_WINDOW_NO_OUTLINE)) { int width, height; diff --git a/libraries/wuss/furniture/invalidate.c b/libraries/wuss/furniture/invalidate.c index e7385d9e..2ddd5812 100644 --- a/libraries/wuss/furniture/invalidate.c +++ b/libraries/wuss/furniture/invalidate.c @@ -29,7 +29,7 @@ void wuss__furniture_invalidate_for(wuss_window_t *window, const box_t *visible) box_t column; column.x1 = visible->x1 - outline_px; - column.x0 = column.x1 - wuss__icon_size(window); + column.x0 = column.x1 - wuss__icon_size(window) - WUSS_DIVIDER_PX; /* include the interior rule */ column.y0 = visible->y0; column.y1 = visible->y1; wuss__invalidate_clipped(window, &column); @@ -40,7 +40,7 @@ void wuss__furniture_invalidate_for(wuss_window_t *window, const box_t *visible) box_t row; row.y1 = visible->y1 - outline_px; - row.y0 = row.y1 - wuss__icon_size(window); + row.y0 = row.y1 - wuss__icon_size(window) - WUSS_DIVIDER_PX; /* include the interior rule */ row.x0 = visible->x0; row.x1 = visible->x1; wuss__invalidate_clipped(window, &row); diff --git a/libraries/wuss/impl.h b/libraries/wuss/impl.h index be40269b..18425751 100644 --- a/libraries/wuss/impl.h +++ b/libraries/wuss/impl.h @@ -27,6 +27,7 @@ #define WUSS_ICON_INSET 3 /* shared by close/back/toggle/resize icons and scrollbar breadth */ #define WUSS_MIN_CONTENT 20 /* resize-drag floor: content can never be squeezed smaller than this */ #define WUSS_SCROLL_INSET 2 /* sausage cross-axis margin from its well's edges, purely cosmetic */ +#define WUSS_DIVIDER_PX 1 /* interior rule between the content area and the furniture on its right/bottom */ /* Internal per-window state, distinct from the public wuss_window_flags_t * appearance flags a caller sets at creation -- room to grow without @@ -190,6 +191,12 @@ static inline void wuss__furniture_carve_for(wuss_window_flags_t flags, carve->x = icon_size; carve->y = icon_size; } + + /* where furniture abuts the content area, a rule divides the two */ + if (carve->x > 0) + carve->x += WUSS_DIVIDER_PX; + if (carve->y > 0) + carve->y += WUSS_DIVIDER_PX; } #endif /* IMPL_H */ diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 125e537b..6e38a34e 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -2754,6 +2754,53 @@ result_t wuss_test(const char *resources) wuss_window_close(win_s); } + printf("test: content bounds survive furniture, including the interior rules\n"); + + { + /* Furniture -- outline, titlebar, scrollbars and the rules dividing the + * content from them -- is added outside the requested content box, never + * carved out of it, so what the caller asks for is what it gets, both at + * creation and after a resize. */ + test_task_t tc_r; + wuss_task_t delegate_r; + box_t box_r, content_r; + wuss_window_t *win_r; + + memset(&tc_r, 0, sizeof(tc_r)); + delegate_r.handle = test_handle; + delegate_r.task_data = &tc_r; + + box_r.x0 = 20; + box_r.y0 = 30; + box_r.x1 = 120; + box_r.y1 = 110; + rc = wuss_window_create(wuss, + &box_r, + "rules", + wuss_WINDOW_NONE, /* all furniture present */ + wuss_NO_BACKGROUND, + &delegate_r, + (size2d_t) { 400, 400 }, + &win_r); + if (rc != result_OK) + goto Failure; + + wuss_window_get_content_bounds(win_r, &content_r); + if (content_r.x1 - content_r.x0 != box_r.x1 - box_r.x0 || + content_r.y1 - content_r.y0 != box_r.y1 - box_r.y0) + goto Failure; + + rc = wuss_window_resize(win_r, (size2d_t) { 61, 47 }); + if (rc != result_OK) + goto Failure; + + wuss_window_get_content_bounds(win_r, &content_r); + if (content_r.x1 - content_r.x0 != 61 || content_r.y1 - content_r.y0 != 47) + goto Failure; + + wuss_window_close(win_r); + } + printf("test: wuss_task_stop sends wuss_EVENT_QUIT to each window's task\n"); tc_a.stop_count = 0; From 74983d498275a498103d0a778c0c1573e4ede5b3 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Fri, 28 Aug 2026 13:39:58 +0100 Subject: [PATCH 048/287] fix(wuss): invalidate the reserved furniture strip on resize wuss__furniture_invalidate_for decided the scrollbar strip widths from the NO_VSCROLL/NO_HSCROLL flags, but wuss__furniture_carve_for still reserves a strip on both axes for a window with both scrollbars off and resize on, to hold the resize icon and the interior rules. Growing such a window left the old icon and rules painted inside the new content area. Drive the strips off the same carve the layout uses. Also: - Floor the scroll sausage's cross-axis inset at 0 when the well is no wider than two insets, which a small titlebar font can produce; the box used to invert and the sausage vanish. - Zero-initialise test_task_t in the scroll-offset test, which only set mouse_count while test_handle increments the other counters. - Correct the screen_copy_rect failure comment in window/move.c and document muldiv's domain in geom/line. - Clamp the chars task's glyph count so no byte above CHAR_MAX reaches bmfont_draw, which indexes its glyph table off a plain char. The resize-sliver test summed per-region dirty areas, double-counting overlaps; it passed by 257 pixels and the extra strips tipped it over. Measure the true union instead. Co-Authored-By: Claude Opus 5 --- libraries/geom/line/line.c | 6 ++- libraries/wuss/furniture/hscroll-box.c | 10 +++-- libraries/wuss/furniture/invalidate.c | 15 +++++--- libraries/wuss/furniture/vscroll-box.c | 10 +++-- libraries/wuss/test/tasks/chars.c | 7 ++++ libraries/wuss/test/wuss-test.c | 51 ++++++++++++++++++++------ libraries/wuss/window/move.c | 5 ++- 7 files changed, 78 insertions(+), 26 deletions(-) diff --git a/libraries/geom/line/line.c b/libraries/geom/line/line.c index 7519fe7c..107b84a9 100644 --- a/libraries/geom/line/line.c +++ b/libraries/geom/line/line.c @@ -14,8 +14,10 @@ typedef unsigned int outcode_t; #define outcode_TOP (1u << 3) /* Compute (a * b) / c, truncating toward zero, without overflow and - * without using a 64-bit or floating-point intermediate. Assumes the - * true result fits in an int (guaranteed here as it's a coordinate). */ + * without using a 64-bit or floating-point intermediate. The caller must + * guarantee the true result fits in an int -- quotient bits at or above + * bit 31 are dropped silently. Holds here: every argument is a screen + * coordinate or a difference of two. */ static int muldiv(int a, int b, int c) { unsigned int a_lo, a_hi, b_lo, b_hi; diff --git a/libraries/wuss/furniture/hscroll-box.c b/libraries/wuss/furniture/hscroll-box.c index 76c870f8..763071e7 100644 --- a/libraries/wuss/furniture/hscroll-box.c +++ b/libraries/wuss/furniture/hscroll-box.c @@ -70,7 +70,7 @@ int wuss__hscroll_well_px(const wuss_window_t *window) void wuss__hscroll_sausage_box(const wuss_window_t *window, box_t *out) { box_t well, content; - int well_px, content_size, doc_size, sausage_px, sausage_x0; + int well_px, content_size, doc_size, sausage_px, sausage_x0, inset; wuss__hscroll_well_box(window, &well); wuss__content_box(window, &content); @@ -92,8 +92,12 @@ void wuss__hscroll_sausage_box(const wuss_window_t *window, box_t *out) else sausage_x0 = well.x0; - out->y0 = well.y0 + WUSS_SCROLL_INSET; - out->y1 = well.y1 - WUSS_SCROLL_INSET; + /* Inset the sausage from the well's long edges, but only while that + * leaves something to draw: a tiny titlebar font can make the well + * narrower than two insets, which would invert the box. */ + inset = (well.y1 - well.y0 > 2 * WUSS_SCROLL_INSET) ? WUSS_SCROLL_INSET : 0; + out->y0 = well.y0 + inset; + out->y1 = well.y1 - inset; out->x0 = sausage_x0; out->x1 = sausage_x0 + sausage_px; } diff --git a/libraries/wuss/furniture/invalidate.c b/libraries/wuss/furniture/invalidate.c index 2ddd5812..bd044e3f 100644 --- a/libraries/wuss/furniture/invalidate.c +++ b/libraries/wuss/furniture/invalidate.c @@ -9,9 +9,14 @@ * pixels sitting wherever those strips used to be. */ void wuss__furniture_invalidate_for(wuss_window_t *window, const box_t *visible) { - int outline_px; + int outline_px; + point_t carve; 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 + * on still reserves both strips, for the resize icon and the rules. */ + wuss__furniture_carve_for(window->flags, wuss__icon_size(window), &carve); if (!(window->flags & wuss_WINDOW_NO_TITLEBAR)) { @@ -24,23 +29,23 @@ void wuss__furniture_invalidate_for(wuss_window_t *window, const box_t *visible) wuss__invalidate_clipped(window, &titlebar); } - if (!(window->flags & wuss_WINDOW_NO_VSCROLL)) + if (carve.x > 0) { box_t column; column.x1 = visible->x1 - outline_px; - column.x0 = column.x1 - wuss__icon_size(window) - WUSS_DIVIDER_PX; /* include the interior rule */ + column.x0 = column.x1 - carve.x; /* includes the interior rule */ column.y0 = visible->y0; column.y1 = visible->y1; wuss__invalidate_clipped(window, &column); } - if (!(window->flags & wuss_WINDOW_NO_HSCROLL)) + if (carve.y > 0) { box_t row; row.y1 = visible->y1 - outline_px; - row.y0 = row.y1 - wuss__icon_size(window) - WUSS_DIVIDER_PX; /* include the interior rule */ + row.y0 = row.y1 - carve.y; /* includes the interior rule */ row.x0 = visible->x0; row.x1 = visible->x1; wuss__invalidate_clipped(window, &row); diff --git a/libraries/wuss/furniture/vscroll-box.c b/libraries/wuss/furniture/vscroll-box.c index ee2631da..58e84f12 100644 --- a/libraries/wuss/furniture/vscroll-box.c +++ b/libraries/wuss/furniture/vscroll-box.c @@ -72,7 +72,7 @@ int wuss__vscroll_well_px(const wuss_window_t *window) void wuss__vscroll_sausage_box(const wuss_window_t *window, box_t *out) { box_t well, content; - int well_px, content_size, doc_size, sausage_px, sausage_y0; + int well_px, content_size, doc_size, sausage_px, sausage_y0, inset; wuss__vscroll_well_box(window, &well); wuss__content_box(window, &content); @@ -94,8 +94,12 @@ void wuss__vscroll_sausage_box(const wuss_window_t *window, box_t *out) else sausage_y0 = well.y0; - out->x0 = well.x0 + WUSS_SCROLL_INSET; - out->x1 = well.x1 - WUSS_SCROLL_INSET; + /* Inset the sausage from the well's long edges, but only while that + * leaves something to draw: a tiny titlebar font can make the well + * narrower than two insets, which would invert the box. */ + inset = (well.x1 - well.x0 > 2 * WUSS_SCROLL_INSET) ? WUSS_SCROLL_INSET : 0; + out->x0 = well.x0 + inset; + out->x1 = well.x1 - inset; out->y0 = sausage_y0; out->y1 = sausage_y0 + sausage_px; } diff --git a/libraries/wuss/test/tasks/chars.c b/libraries/wuss/test/tasks/chars.c index bd68a54e..16c25168 100644 --- a/libraries/wuss/test/tasks/chars.c +++ b/libraries/wuss/test/tasks/chars.c @@ -2,6 +2,8 @@ #ifdef USE_SDL +#include + #ifdef FORTIFY #include "fortify/fortify.h" #endif @@ -85,6 +87,11 @@ static result_t chars_redraw(const wuss_event_t *event, void *task_data) first = ' '; /* bmfont glyphs are laid out contiguously starting here */ count = bmfont_get_count(cc->font); + /* bmfont indexes its glyph table off a plain char, so a byte value above + * CHAR_MAX would index negatively on a signed-char platform. Never draw + * one, however many glyphs the font claims. */ + if (first + count > CHAR_MAX + 1) + count = CHAR_MAX + 1 - first; for (i = 0; i < CHARS_COLS * CHARS_ROWS; i++) { diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 6e38a34e..0a744361 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -502,6 +502,38 @@ static result_t test_handle(wuss_window_t *window, /* ----------------------------------------------------------------------- */ +/* Total area covered by the dirty list, counting overlapped pixels once. + * Summing each region's area instead would double-count wherever two + * invalidations overlap, which they legitimately do. */ +static int dirty_union_area(wuss_t *wuss, const box_t *bounds) +{ + static unsigned char covered[512 * 512]; + + box_t region; + int w, h, i, x, y, area; + + w = bounds->x1 - bounds->x0; + h = bounds->y1 - bounds->y0; + if (w <= 0 || h <= 0 || w > 512 || h > 512) + return -1; + + memset(covered, 0, (size_t) w * h); + + for (i = 0; i < wuss_get_dirty_count(wuss); i++) + { + wuss_get_dirty(wuss, i, ®ion); + for (y = MAX(region.y0, bounds->y0); y < MIN(region.y1, bounds->y1); y++) + for (x = MAX(region.x0, bounds->x0); x < MIN(region.x1, bounds->x1); x++) + covered[(y - bounds->y0) * w + (x - bounds->x0)] = 1; + } + + area = 0; + for (i = 0; i < w * h; i++) + area += covered[i]; + + return area; +} + result_t wuss_test(const char *resources) { result_t rc; @@ -2061,20 +2093,19 @@ result_t wuss_test(const char *resources) goto Failure; interior_dirty = 0; - dirty_area = 0; for (i = 0; i < wuss_get_dirty_count(wuss); i++) { wuss_get_dirty(wuss, i, ®ion); if (box_contains_point(®ion, interior_x, interior_y)) interior_dirty = 1; - dirty_area += (region.x1 - region.x0) * (region.y1 - region.y0); } if (interior_dirty) goto Failure; /* untouched top-left corner, unchanged by growing bottom-right */ wuss_window_get_visible_bounds(win_m2, &after2); - full_area = (after2.x1 - after2.x0) * (after2.y1 - after2.y0); - if (dirty_area >= full_area) + full_area = (after2.x1 - after2.x0) * (after2.y1 - after2.y0); + dirty_area = dirty_union_area(wuss, &after2); + if (dirty_area < 0 || dirty_area >= full_area) goto Failure; /* must be less than a full redraw of the grown footprint */ rc = wuss_redraw_dirty(wuss); @@ -2091,19 +2122,18 @@ result_t wuss_test(const char *resources) goto Failure; interior_dirty = 0; - dirty_area = 0; for (i = 0; i < wuss_get_dirty_count(wuss); i++) { wuss_get_dirty(wuss, i, ®ion); if (box_contains_point(®ion, interior_x, interior_y)) interior_dirty = 1; - dirty_area += (region.x1 - region.x0) * (region.y1 - region.y0); } if (interior_dirty) goto Failure; /* still untouched: the corner that remains after shrinking */ - full_area = (before2.x1 - before2.x0) * (before2.y1 - before2.y0); - if (dirty_area >= full_area) + full_area = (before2.x1 - before2.x0) * (before2.y1 - before2.y0); + dirty_area = dirty_union_area(wuss, &before2); + if (dirty_area < 0 || dirty_area >= full_area) goto Failure; rc = wuss_redraw_dirty(wuss); @@ -2689,13 +2719,12 @@ result_t wuss_test(const char *resources) printf("test: mouse and scroll events arrive in virtual content space, with the scroll offset applied exactly once\n"); { - test_task_t tc_s; + test_task_t tc_s = { 0 }; wuss_task_t delegate_s; box_t box_s, content_s; wuss_window_t *win_s; point_t scroll; - tc_s.mouse_count = 0; delegate_s.handle = test_handle; delegate_s.task_data = &tc_s; @@ -2723,7 +2752,7 @@ result_t wuss_test(const char *resources) if (hit != win_s) goto Failure; if (tc_s.last_x != 5 + scroll.x || tc_s.last_y != 7 + scroll.y) - goto Failure; /* a task adding the scroll offset itself would double-count it */ /* a task adding the scroll offset itself would double-count it */ + goto Failure; /* a task adding the scroll offset itself would double-count it */ rc = wuss_mouse_click(wuss, (point_t) { content_s.x0 + 5, content_s.y0 + 7 }, diff --git a/libraries/wuss/window/move.c b/libraries/wuss/window/move.c index 978d4f9b..ef0ee7d0 100644 --- a/libraries/wuss/window/move.c +++ b/libraries/wuss/window/move.c @@ -149,8 +149,9 @@ void wuss_window_move(wuss_window_t *window, point_t p) &copied)) { /* The screen format doesn't support the blit at all (e.g. paletted): - * this fails identically for every piece, so bail out before any - * blits have happened rather than leaving a half-blitted window. */ + * this fails identically for every piece, so it fails on the first + * one, before any blit has happened. Bail out; the caller's fallback + * full invalidate repairs the window either way. */ blit_failed = 1; break; } From 229da8de924e721f8b06770c0ba78a93c7d7b69e Mon Sep 17 00:00:00 2001 From: David Thomas Date: Fri, 28 Aug 2026 16:22:47 +0100 Subject: [PATCH 049/287] feat(wuss): add an animated Porter-Duff demo task to the wuss test Adds an eleventh launcher task to the interactive wuss test: the two bundled composite demo images (resources/composite/A.png and B.png) composited under a Porter-Duff rule that cycles through all twelve operators, over an alpha checkerboard so transparency reads as transparent rather than white. composite() takes no offset or alpha parameter, so the animation is a per-frame edit on scratch copies: the destination is restored from a pristine B and the source's alpha channel is scaled by a triangle ramp (0 to 255 and back across each rule's turn). Alpha is non-premultiplied, so only the alpha byte is touched. Pacing is a per-idle frame counter, as in ball.c; clicking advances the rule and the wheel adjusts the cycle speed. The bitmap clone/convert helpers are copied from composite-test.c, where they are statics rather than library functions. Widens the launcher window to fit the longer entry name. Co-Authored-By: Claude Opus 5 --- CMakeLists.txt | 1 + libraries/wuss/test/tasks/launcher.c | 2 +- libraries/wuss/test/tasks/porter-duff.c | 415 ++++++++++++++++++++++++ libraries/wuss/test/tasks/porter-duff.h | 50 +++ libraries/wuss/test/wuss-test.c | 7 +- 5 files changed, 473 insertions(+), 2 deletions(-) create mode 100644 libraries/wuss/test/tasks/porter-duff.c create mode 100644 libraries/wuss/test/tasks/porter-duff.h diff --git a/CMakeLists.txt b/CMakeLists.txt index f9b0db89..e50ac931 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -552,6 +552,7 @@ if(BUILD_TESTS) libraries/wuss/test/tasks/image.c libraries/wuss/test/tasks/launcher.c libraries/wuss/test/tasks/palette.c + libraries/wuss/test/tasks/porter-duff.c libraries/wuss/test/tasks/sofa.c libraries/wuss/test/tasks/text.c libraries/wuss/test/wuss-test.c) diff --git a/libraries/wuss/test/tasks/launcher.c b/libraries/wuss/test/tasks/launcher.c index 32188120..1039b726 100644 --- a/libraries/wuss/test/tasks/launcher.c +++ b/libraries/wuss/test/tasks/launcher.c @@ -17,7 +17,7 @@ #define LAUNCHER_ROW_HEIGHT 20 #define LAUNCHER_PAD 4 -#define LAUNCHER_WIDTH 140 +#define LAUNCHER_WIDTH 160 result_t launcher_create(wuss_t *wuss, launcher_entry_t *entries, diff --git a/libraries/wuss/test/tasks/porter-duff.c b/libraries/wuss/test/tasks/porter-duff.c new file mode 100644 index 00000000..1a482abc --- /dev/null +++ b/libraries/wuss/test/tasks/porter-duff.c @@ -0,0 +1,415 @@ +/* porter-duff.c -- wuss test - animated Porter-Duff compositing task */ + +#ifdef USE_SDL + +#include +#include + +#ifdef FORTIFY +#include "fortify/fortify.h" +#endif + +#include "base/utils.h" +#include "framebuf/palettes.h" +#include "framebuf/pixelfmt.h" +#include "geom/box.h" +#include "geom/point.h" +#include "io/path.h" + +#include "porter-duff.h" + +#define PD_SIZE (256) /* the demo images are 256x256 */ +#define PD_LABEL_HEIGHT (20) /* strip below the pane, for the rule name */ +#define PD_CHECKER_BAND (8) /* checkerboard square size, in pixels */ +#define PD_FORMAT (pixelfmt_bgra8888) + +#define PD_FRAMES_DEFAULT (120) /* ~2s per rule at 60 main-loop passes/sec */ +#define PD_FRAMES_MIN (10) +#define PD_FRAMES_MAX (600) + +/* ----------------------------------------------------------------------- */ + +/* These three are lifted from libraries/framebuf/composite/test/composite-test.c, + * where they're static helpers rather than library functions: their memory + * management is raw malloc and the original carries a FIXME saying as much, so + * they're copied rather than promoted to framebuf. */ + +static result_t bitmap_clone_by_size(bitmap_t *cloned, const bitmap_t *src) +{ + size_t pixelbytes; + void *pixels; + + pixelbytes = src->size.h * src->rowbytes; + pixels = malloc(pixelbytes); + if (pixels == NULL) + return result_OOM; + + *cloned = *src; + cloned->base = pixels; + + return result_OK; +} + +static result_t bitmap_clone_pixels(bitmap_t *dst, const bitmap_t *src) +{ + if (dst->size.w != src->size.w || + dst->size.h != src->size.h || + dst->format != src->format || + dst->rowbytes != src->rowbytes) + return result_INCOMPATIBLE; + + memcpy(dst->base, src->base, src->size.h * src->rowbytes); + + return result_OK; +} + +/* Only the arms reachable from bitmap_load_png's output are implemented. */ +static result_t bitmap_convert_inplace(bitmap_t *bm, pixelfmt_t new_fmt) +{ + pixelfmt_any_t *p; + int x, y; + + if (new_fmt != pixelfmt_bgra8888) + return result_NOT_IMPLEMENTED; + + switch (bm->format) + { + case pixelfmt_bgra8888: + return result_OK; + + case pixelfmt_rgbx8888: + p = bm->base; + for (y = 0; y < bm->size.h; y++) + for (x = 0; x < bm->size.w; x++) + { + pixelfmt_rgbx8888_t px = *p; + /* the x byte isn't a real alpha channel, so force it opaque */ + *p++ = PIXELFMT_MAKE_BGRA8888(PIXELFMT_Bxxx8888(px), + PIXELFMT_xGxx8888(px), + PIXELFMT_xxRx8888(px), + 0xFF); + } + bm->format = pixelfmt_bgra8888; + return result_OK; + + case pixelfmt_rgba8888: + p = bm->base; + for (y = 0; y < bm->size.h; y++) + for (x = 0; x < bm->size.w; x++) + { + pixelfmt_rgba8888_t px = *p; + *p++ = PIXELFMT_MAKE_BGRA8888(PIXELFMT_Bxxx8888(px), + PIXELFMT_xGxx8888(px), + PIXELFMT_xxRx8888(px), + PIXELFMT_xxxA8888(px)); + } + bm->format = pixelfmt_bgra8888; + return result_OK; + + default: + return result_NOT_IMPLEMENTED; + } +} + +/* ----------------------------------------------------------------------- */ + +static const char *const rule_names[composite_RULE__LIMIT] = +{ + "CLEAR", + "SRC", + "DST", + "SRC OVER", + "DST OVER", + "SRC IN", + "DST IN", + "SRC OUT", + "DST OUT", + "SRC ATOP", + "DST ATOP", + "XOR" +}; + +static result_t load_demo_png(bitmap_t *bm, const char *resources, + const char *leafname) +{ + const char *leafname_ext; + const char *filename; + result_t rc; + + leafname_ext = path_join_leafname(leafname, "png"); + filename = path_join_filename(resources, 3, + "resources", "composite", leafname_ext); + + rc = bitmap_load_png(bm, filename); + if (rc != result_OK) + return rc; + + rc = bitmap_convert_inplace(bm, PD_FORMAT); + if (rc != result_OK) + { + free(bm->base); + return rc; + } + + return result_OK; +} + +/* ----------------------------------------------------------------------- */ + +result_t porter_duff_create(wuss_t *wuss, + const colour_t *palette, + bmfont_t *font, + const char *resources, + porter_duff_task_t *task) +{ + wuss_task_t delegate; + box_t box; + result_t rc; + + task->font = font; + task->rule = composite_RULE_CLEAR; + task->frame = 0; + task->frames_per_rule = PD_FRAMES_DEFAULT; + task->light = palette[palette_PICO8_LIGHT_GREY]; + task->dark = palette[palette_PICO8_DARK_GREY]; + task->fg = palette[palette_PICO8_WHITE]; + task->bg = palette[palette_PICO8_BLACK]; + + rc = load_demo_png(&task->a, resources, "A"); + if (rc != result_OK) + return rc; + + rc = load_demo_png(&task->b, resources, "B"); + if (rc != result_OK) + goto free_a; + + rc = bitmap_clone_by_size(&task->src, &task->a); + if (rc != result_OK) + goto free_b; + + rc = bitmap_clone_by_size(&task->dst, &task->b); + if (rc != result_OK) + goto free_src; + + delegate = wuss_task_start(porter_duff_handle, task); /* porter_duff_redraw paints every pixel itself */ + box = (box_t) BOX_POS_SIZE(60, 180, PD_SIZE, PD_SIZE + PD_LABEL_HEIGHT); + + rc = wuss_window_create(wuss, + &box, + "Porter-Duff", + wuss_WINDOW_NONE, + wuss_NO_BACKGROUND, + &delegate, + box_size(&box), + &task->window); + if (rc != result_OK) + goto free_dst; + + return result_OK; + +free_dst: + free(task->dst.base); +free_src: + free(task->src.base); +free_b: + free(task->b.base); +free_a: + free(task->a.base); + + return rc; +} + +void porter_duff_destroy(porter_duff_task_t *task) +{ + wuss_window_close(task->window); + free(task->dst.base); + free(task->src.base); + free(task->b.base); + free(task->a.base); +} + +/* ----------------------------------------------------------------------- */ + +/* Triangle ramp: 0 at the start of the rule's turn, 255 at its midpoint, back + * to 0 at its end. */ +static int porter_duff_ramp(const porter_duff_task_t *pd) +{ + int half; + + half = pd->frames_per_rule / 2; + if (half <= 0) + return 255; + + if (pd->frame < half) + return pd->frame * 255 / half; + else + return MAX(0, (pd->frames_per_rule - pd->frame) * 255 / half); +} + +/* Copy "a" into "src", scaling its alpha channel by "ramp" (0..255). The + * library's compositing works on non-premultiplied values, so the colour + * components are left alone. */ +static void porter_duff_ramp_src(porter_duff_task_t *pd, int ramp) +{ + pixelfmt_any_t *sp; + pixelfmt_any_t *dp; + int x, y; + + sp = pd->a.base; + dp = pd->src.base; + + for (y = 0; y < pd->a.size.h; y++) + for (x = 0; x < pd->a.size.w; x++) + { + pixelfmt_any_t px = *sp++; + unsigned int alpha; + + alpha = PIXELFMT_xxxA8888(px) * ramp / 255; + *dp++ = (px & ~PIXELFMT_xxxA8888_MASK) | + ((pixelfmt_any_t) alpha << PIXELFMT_xxxA8888_SHIFT); + } +} + +static void porter_duff_draw_checkerboard(const porter_duff_task_t *pd, + screen_t *scr, + const box_t *content, + const box_t *bounds) +{ + int x, y, lx, ly, band; + + for (y = content->y0; y < content->y1; y++) + for (x = content->x0; x < content->x1; x++) + { + lx = x - bounds->x0; + ly = y - bounds->y0; + band = lx / PD_CHECKER_BAND + ly / PD_CHECKER_BAND; + + screen_draw_pixel(scr, x, y, (band & 1) ? pd->dark : pd->light); + } +} + +static result_t porter_duff_redraw(const wuss_event_t *event, void *task_data) +{ + 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; + + scr = event->data.redraw.scr; + content = event->data.redraw.content; + bounds = event->data.redraw.bounds; + + porter_duff_draw_checkerboard(pd, scr, content, bounds); + + /* ponytail: the whole 256x256 pane is recomposited on every redraw -- two + * full-image memcpys plus two full-image passes. Fine for one window in a + * test harness; if the main loop ever gets tight, cache the composited + * bitmap and rebuild it only when the ramp value or rule actually changes. */ + rc = bitmap_clone_pixels(&pd->dst, &pd->b); + if (rc != result_OK) + return rc; + + porter_duff_ramp_src(pd, porter_duff_ramp(pd)); + + rc = composite(pd->rule, &pd->src, &pd->dst); + if (rc != result_OK) + return rc; + + screen_draw_bitmap(scr, bounds->x0, bounds->y0, &pd->dst); + + name = rule_names[pd->rule]; + pos.x = bounds->x0 + 2; + pos.y = bounds->y0 + PD_SIZE + 2; + + return bmfont_draw(pd->font, scr, name, (int) strlen(name), + pd->fg, pd->bg, &pos, NULL); +} + +static result_t porter_duff_idle(void *task_data) +{ + porter_duff_task_t *pd; + + pd = task_data; + + if (++pd->frame >= pd->frames_per_rule) + { + pd->frame = 0; + pd->rule = (pd->rule + 1) % composite_RULE__LIMIT; + } + + /* the ramp changes every frame, so the whole pane is stale every frame */ + wuss_window_invalidate_all(pd->window); + + return result_OK; +} + +static result_t porter_duff_mouse(wuss_window_t *window, void *task_data) +{ + porter_duff_task_t *pd; + + pd = task_data; + + pd->rule = (pd->rule + 1) % composite_RULE__LIMIT; + pd->frame = 0; + + wuss_window_invalidate_all(window); + + return result_OK; +} + +static result_t porter_duff_scroll(wuss_window_t *window, int delta, + void *task_data) +{ + porter_duff_task_t *pd; + + pd = task_data; + + pd->frames_per_rule += delta * 10; + pd->frames_per_rule = CLAMP(pd->frames_per_rule, + PD_FRAMES_MIN, PD_FRAMES_MAX); + pd->frame = MIN(pd->frame, pd->frames_per_rule); + + wuss_window_invalidate_all(window); + + return result_OK; +} + +result_t porter_duff_handle(wuss_window_t *window, + const wuss_event_t *event, + void *task_data) +{ + porter_duff_task_t *pd; + + pd = task_data; + + switch (event->kind) + { + case wuss_EVENT_REDRAW: + return porter_duff_redraw(event, task_data); + + case wuss_EVENT_MOUSE: + if (event->data.mouse.action != wuss_MOUSE_DOWN) + return result_OK; + return porter_duff_mouse(window, task_data); + + case wuss_EVENT_SCROLL: + return porter_duff_scroll(window, event->data.scroll.delta, task_data); + + case wuss_EVENT_IDLE: + return porter_duff_idle(task_data); + + case wuss_EVENT_CLOSE: + wuss_window_close(window); + pd->window = NULL; + return result_OK; + + default: + return result_OK; + } +} + +#endif /* USE_SDL */ diff --git a/libraries/wuss/test/tasks/porter-duff.h b/libraries/wuss/test/tasks/porter-duff.h new file mode 100644 index 00000000..d8dbd366 --- /dev/null +++ b/libraries/wuss/test/tasks/porter-duff.h @@ -0,0 +1,50 @@ +/* porter-duff.h -- wuss test - animated Porter-Duff compositing task */ + +#ifndef TASKS_PORTER_DUFF_H +#define TASKS_PORTER_DUFF_H + +#ifdef USE_SDL + +#include "framebuf/bitmap.h" +#include "framebuf/bmfont.h" +#include "framebuf/composite.h" +#include "wuss/window.h" + +/* window's task: the two composite demo images blended under a cycling + * Porter-Duff rule, over an alpha checkerboard. The source image's alpha is + * ramped up and back down across each rule's turn, so every operator is seen + * across its full range */ +typedef struct porter_duff_task +{ + wuss_window_t *window; + bmfont_t *font; + bitmap_t a; /* owned: pristine source, BGRA */ + bitmap_t b; /* owned: pristine destination, BGRA */ + bitmap_t src; /* owned: scratch, rebuilt each frame */ + bitmap_t dst; /* owned: scratch, rebuilt each frame */ + composite_rule_t rule; + int frame; /* frames elapsed in the current rule */ + int frames_per_rule; + colour_t light; /* checkerboard */ + colour_t dark; /* checkerboard */ + colour_t fg; /* rule name label */ + colour_t bg; /* rule name label */ +} +porter_duff_task_t; + +wuss_event_fn_t porter_duff_handle; + +/* load the two demo images and create the window against the given wuss + * instance; resources is the DPTLib repo root, for locating the bundled PNGs */ +result_t porter_duff_create(wuss_t *wuss, + const colour_t *palette, + bmfont_t *font, + const char *resources, + porter_duff_task_t *task); + +/* destroy the window and free the bitmaps allocated by porter_duff_create */ +void porter_duff_destroy(porter_duff_task_t *task); + +#endif /* USE_SDL */ + +#endif /* TASKS_PORTER_DUFF_H */ diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 0a744361..27f54847 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -38,6 +38,7 @@ #include "tasks/image.h" #include "tasks/launcher.h" #include "tasks/palette.h" +#include "tasks/porter-duff.h" #include "tasks/sofa.h" #include "tasks/text.h" @@ -65,6 +66,7 @@ static checker_task_t g_checker_task; static curve_task_t g_curve_task; static sofa_task_t g_sofa_task; static gradient_task_t g_gradient_task; +static porter_duff_task_t g_porter_duff_task; static result_t spawn_ball(void) { return ball_create(g_wuss, g_palette, &g_ball_task); } static result_t spawn_text(void) { return text_create(g_wuss, g_palette, g_daydream_font, &g_text_task); } @@ -76,6 +78,7 @@ static result_t spawn_checker(void) { return checker_create(g_wuss, g_palette, static result_t spawn_curve(void) { return curve_create(g_wuss, g_palette, &g_curve_task); } static result_t spawn_sofa(void) { return sofa_create(g_wuss, g_palette, &g_sofa_task); } static result_t spawn_gradient(void) { return gradient_create(g_wuss, &g_gradient_task); } +static result_t spawn_porter_duff(void) { return porter_duff_create(g_wuss, g_palette, g_daydream_font, g_resources, &g_porter_duff_task); } static void destroy_ball(void) { ball_destroy(&g_ball_task); } static void destroy_text(void) { text_destroy(&g_text_task); } @@ -87,6 +90,7 @@ static void destroy_checker(void) { checker_destroy(&g_checker_task); } static void destroy_curve(void) { curve_destroy(&g_curve_task); } static void destroy_sofa(void) { sofa_destroy(&g_sofa_task); } static void destroy_gradient(void) { gradient_destroy(&g_gradient_task); } +static void destroy_porter_duff(void) { porter_duff_destroy(&g_porter_duff_task); } static launcher_entry_t g_launcher_entries[] = { @@ -99,7 +103,8 @@ static launcher_entry_t g_launcher_entries[] = { "Checker", spawn_checker, destroy_checker, false }, { "Curve", spawn_curve, destroy_curve, false }, { "Sofa", spawn_sofa, destroy_sofa, false }, - { "Gradient", spawn_gradient, destroy_gradient, false } + { "Gradient", spawn_gradient, destroy_gradient, false }, + { "Porter-Duff", spawn_porter_duff, destroy_porter_duff, false } }; static wuss_button_t sdl_button_to_wuss(Uint8 button) From 2e67c4b1c8a666c1f1d432de50a7d8f28ea3efa0 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Fri, 28 Aug 2026 16:52:00 +0100 Subject: [PATCH 050/287] feat(wuss): scroll the opposite way on an Adjust-clicked scroll arrow Adjust-clicking a scroll arrow now steps against the direction the arrow points, so a single arrow can be worked both ways without moving the pointer. Toggle-size remains Select-only. Co-Authored-By: Claude Opus 5 --- libraries/wuss/mouse-click.c | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/libraries/wuss/mouse-click.c b/libraries/wuss/mouse-click.c index 88ff9c58..138640a3 100644 --- a/libraries/wuss/mouse-click.c +++ b/libraries/wuss/mouse-click.c @@ -62,24 +62,34 @@ result_t wuss_mouse_click(wuss_t *wuss, region == wuss_FURNITURE_HSCROLL_LEFT || region == wuss_FURNITURE_HSCROLL_RIGHT) { - if (action == wuss_MOUSE_DOWN && button == wuss_BUTTON_SELECT) + if (action == wuss_MOUSE_DOWN && + (button == wuss_BUTTON_SELECT || button == wuss_BUTTON_ADJUST)) { + /* Adjust-clicking a scroll arrow steps the opposite way to the arrow it + * points, so one arrow can be worked in both directions without moving + * the pointer. Toggle-size stays Select-only. */ + int step; + + step = (button == wuss_BUTTON_ADJUST) ? -WUSS_SCROLL_STEP + : WUSS_SCROLL_STEP; + switch (region) { case wuss_FURNITURE_TOGGLE_SIZE: - wuss__furniture_toggle_size(win); + if (button == wuss_BUTTON_SELECT) + wuss__furniture_toggle_size(win); break; case wuss_FURNITURE_VSCROLL_UP: - wuss__furniture_scroll_step(win, (point_t) { 0, -WUSS_SCROLL_STEP }); + wuss__furniture_scroll_step(win, (point_t) { 0, -step }); break; case wuss_FURNITURE_VSCROLL_DOWN: - wuss__furniture_scroll_step(win, (point_t) { 0, WUSS_SCROLL_STEP }); + wuss__furniture_scroll_step(win, (point_t) { 0, step }); break; case wuss_FURNITURE_HSCROLL_LEFT: - wuss__furniture_scroll_step(win, (point_t) { -WUSS_SCROLL_STEP, 0 }); + wuss__furniture_scroll_step(win, (point_t) { -step, 0 }); break; case wuss_FURNITURE_HSCROLL_RIGHT: - wuss__furniture_scroll_step(win, (point_t) { WUSS_SCROLL_STEP, 0 }); + wuss__furniture_scroll_step(win, (point_t) { step, 0 }); break; default: break; From fa65ea39d19308652785e681f9322d74a1a15f11 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Fri, 28 Aug 2026 17:15:04 +0100 Subject: [PATCH 051/287] docs(wuss): rewrap the window and wuss headers, retitle the README section Reflows the Doxygen parameter blocks in window.h and wuss.h, and renames the README's "Wuss" section to "Windowing". --- README.md | 2 +- include/wuss/window.h | 46 +++++++++++++++++++++---------------------- include/wuss/wuss.h | 22 ++++++++++----------- 3 files changed, 34 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 187fac83..2f17af0d 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ DPTLib is my platform independent C library. It contains a wide variety of funct - [`utils/pack.h`](https://github.com/dpt/DPTLib/blob/master/include/utils/pack.h) — structure packing and unpacking helpers - [`utils/primes.h`](https://github.com/dpt/DPTLib/blob/master/include/utils/primes.h) — cache of prime numbers -### Wuss +### Windowing - [`wuss/wuss.h`](https://github.com/dpt/DPTLib/blob/master/include/wuss/wuss.h) — minimal window manager {[docs](https://github.com/dpt/DPTLib/blob/master/docs/wuss.md)} - [`wuss/window.h`](https://github.com/dpt/DPTLib/blob/master/include/wuss/window.h) — window creation, positioning, sizing and client delegation diff --git a/include/wuss/window.h b/include/wuss/window.h index 60762c05..4444636b 100644 --- a/include/wuss/window.h +++ b/include/wuss/window.h @@ -39,26 +39,24 @@ extern "C" * screen keeps its top-left corner on-screen instead. The bottom/right edges * are not clamped. * - * \param[in] wuss Window manager to create the window on. - * \param[in] content Requested content-area bounds, screen space. Copied - * in. - * \param[in] title Titlebar label, or NULL for none. Copied in, truncated - * if too long. Ignored if flags includes - * 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. - * \param[in] bg Content background, filled by Wuss before each redraw, - * or wuss_NO_BACKGROUND for the task to draw its own - * background (avoids a redundant fill behind an opaque - * task). Changeable later via - * wuss_window_set_background. - * \param[in] task Content delegate. Copied in. May be NULL for a window - * with no content handling. - * \param[in] doc Virtual document extent, for the scrollbars' sausage - * proportions; pass content's own width and height for a - * window with nothing to scroll. - * \param[out] window Newly created window. Becomes the topmost window. + * \param[in] wuss Window manager to create the window on. + * \param[in] content Requested content-area bounds, screen space. Copied in. + * \param[in] title Titlebar label, or NULL for none. Copied in, truncated if + * too long. Ignored if flags includes + * 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. + * \param[in] bg Content background, filled by Wuss before each redraw, or + * wuss_NO_BACKGROUND for the task to draw its own + * background (avoids a redundant fill behind an opaque + * task). Changeable later via wuss_window_set_background. + * \param[in] task Content delegate. Copied in. May be NULL for a window + * with no content handling. + * \param[in] doc Virtual document extent, for the scrollbars' sausage + * proportions; pass content's own width and height for a + * window with nothing to scroll. + * \param[out] window Newly created window. Becomes the topmost window. * \return \ref result_OK on success, \ref result_WUSS_TOO_SMALL if content's * width or height is not positive, \ref result_WUSS_BAD_COLOUR if bg is * out of range for the palette, or another appropriate result code. @@ -132,10 +130,10 @@ void wuss_window_get_content_bounds(const wuss_window_t *window, * call. Content changes are opaque to Wuss, so tasks must call this themselves * (e.g. the union of an animated element's old and new positions). * - * \param[in] window Window whose content changed. - * \param[in] local_box Region, in window-local content coordinates (as passed - * to the task's mouse callback), or NULL to mark the - * whole content area dirty. + * \param[in] window Window whose content changed. + * \param[in] local_box Region, in window-local content coordinates (as passed + * to the task's mouse callback), or NULL to mark the whole + * content area dirty. */ void wuss_window_invalidate(wuss_window_t *window, const box_t *local_box); diff --git a/include/wuss/wuss.h b/include/wuss/wuss.h index 9c0f1f7c..60e04023 100644 --- a/include/wuss/wuss.h +++ b/include/wuss/wuss.h @@ -76,14 +76,14 @@ typedef struct wuss_palette { struct { - wuss_colour_t bg; /**< Titlebar fill. */ - wuss_colour_t fg; /**< Titlebar text. */ + wuss_colour_t bg; /**< Titlebar fill. */ + wuss_colour_t fg; /**< Titlebar text. */ } title; - wuss_colour_t back; /**< Send-to-back icon. */ - wuss_colour_t close; /**< Close icon. */ - wuss_colour_t toggle; /**< Toggle-size icon. */ - wuss_colour_t resize; /**< Resize icon. */ + wuss_colour_t back; /**< Send-to-back icon. */ + wuss_colour_t close; /**< Close icon. */ + wuss_colour_t toggle; /**< Toggle-size icon. */ + wuss_colour_t resize; /**< Resize icon. */ struct { wuss_colour_t arrows; /**< Scrollbar arrows. */ @@ -297,11 +297,11 @@ void wuss_get_dirty(const wuss_t *wuss, int index, box_t *out); * \return \ref result_OK, or a result code returned by the task's mouse * callback. */ -result_t wuss_mouse_click(wuss_t *wuss, - point_t p, - wuss_button_t button, - wuss_mouse_action_t action, - wuss_window_t **hit); +result_t wuss_mouse_click(wuss_t *wuss, + point_t p, + wuss_button_t button, + wuss_mouse_action_t action, + wuss_window_t **hit); /** * Deliver a mouse-move event. Updates the dragged window's position if a drag From bdb82889a48b5607c05071fb4ffd384a3a57ec46 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Fri, 28 Aug 2026 17:15:04 +0100 Subject: [PATCH 052/287] fix(wuss)!: make mouse buttons flags so chords can be reported Select/Menu/Adjust were consecutive enumerated values, so a chord such as Select+Adjust could not be expressed at all. They become flags -- Select 4, Menu 2, Adjust 1, matching the RISC OS button order -- with a wuss_BUTTON_NONE of zero, and every internal comparison switches from equality to a bit test. Where a chord is ambiguous Select wins: a Select+Adjust click on a scroll arrow scrolls the way the arrow points rather than backwards, and on the back icon sends the window to the back. BREAKING CHANGE: wuss_button_t's values have changed, and client code comparing the reported button for equality must now test with '&' or it will fail to match a chord. Co-Authored-By: Claude Opus 5 --- include/wuss/task.h | 7 ++++--- include/wuss/wuss.h | 12 +++++++++--- libraries/wuss/mouse-click.c | 24 +++++++++++++----------- libraries/wuss/test/tasks/ball.c | 4 ++-- libraries/wuss/test/tasks/sofa.c | 2 +- 5 files changed, 29 insertions(+), 20 deletions(-) diff --git a/include/wuss/task.h b/include/wuss/task.h index b3cd3942..78a11cb8 100644 --- a/include/wuss/task.h +++ b/include/wuss/task.h @@ -3,8 +3,8 @@ /** * \file task.h * - * A Wuss task: the content delegate a window hands its drawing and input - * events to, and the events themselves. + * A Wuss task: the content delegate a window hands its drawing and input events + * to, and the events themselves. */ #ifndef WUSS_TASK_H @@ -77,7 +77,8 @@ typedef struct wuss_event * scroll offset has already been added, so the task must not add it * again. With a scroll offset of (0,0) this is the same as window-local * content coordinates, where the content area's top-left is (0,0). - * button is meaningful for DOWN/UP. */ + * button is meaningful for DOWN/UP, and is a set of wuss_button_t + * flags, so test it with '&' rather than comparing for equality. */ struct { wuss_mouse_action_t action; diff --git a/include/wuss/wuss.h b/include/wuss/wuss.h index 60e04023..99d6e1cc 100644 --- a/include/wuss/wuss.h +++ b/include/wuss/wuss.h @@ -40,12 +40,18 @@ typedef struct wuss_window wuss_window_t; /** * Mouse buttons, RISC OS-style: Select is the primary action, Adjust the * secondary action, Menu pops up a menu. + * + * These are flags, OR'd together, so that chords (e.g. Select and Adjust + * pressed together) can be reported. The bit values match the RISC OS button + * order. Test them with '&' rather than comparing for equality, or a chord will + * match no button at all. */ typedef enum wuss_button { - wuss_BUTTON_SELECT, - wuss_BUTTON_MENU, - wuss_BUTTON_ADJUST + wuss_BUTTON_NONE = 0, + wuss_BUTTON_ADJUST = 1 << 0, + wuss_BUTTON_MENU = 1 << 1, + wuss_BUTTON_SELECT = 1 << 2 } wuss_button_t; diff --git a/libraries/wuss/mouse-click.c b/libraries/wuss/mouse-click.c index 138640a3..b7bee4be 100644 --- a/libraries/wuss/mouse-click.c +++ b/libraries/wuss/mouse-click.c @@ -36,9 +36,9 @@ result_t wuss_mouse_click(wuss_t *wuss, region = wuss__furniture_hit_test(win, (point_t) { x, y }); - if (region == wuss_FURNITURE_CLOSE && - action == wuss_MOUSE_DOWN && - button == wuss_BUTTON_SELECT) + if (region == wuss_FURNITURE_CLOSE && + action == wuss_MOUSE_DOWN && + (button & wuss_BUTTON_SELECT)) { if (win->task.handle == NULL) return result_OK; @@ -49,9 +49,9 @@ result_t wuss_mouse_click(wuss_t *wuss, if (region == wuss_FURNITURE_BACK && action == wuss_MOUSE_DOWN) { - if (button == wuss_BUTTON_SELECT) + if (button & wuss_BUTTON_SELECT) wuss_window_restack(win, wuss_ZORDER_BACK); - else if (button == wuss_BUTTON_ADJUST) + else if (button & wuss_BUTTON_ADJUST) wuss_window_restack(win, wuss_ZORDER_FRONT); return result_OK; } @@ -63,20 +63,22 @@ result_t wuss_mouse_click(wuss_t *wuss, region == wuss_FURNITURE_HSCROLL_RIGHT) { if (action == wuss_MOUSE_DOWN && - (button == wuss_BUTTON_SELECT || button == wuss_BUTTON_ADJUST)) + (button & (wuss_BUTTON_SELECT | wuss_BUTTON_ADJUST))) { /* Adjust-clicking a scroll arrow steps the opposite way to the arrow it * points, so one arrow can be worked in both directions without moving * the pointer. Toggle-size stays Select-only. */ int step; - step = (button == wuss_BUTTON_ADJUST) ? -WUSS_SCROLL_STEP - : WUSS_SCROLL_STEP; + /* Select wins a Select+Adjust chord, so a chord never scrolls backwards + * unexpectedly. */ + step = (button & wuss_BUTTON_SELECT) ? WUSS_SCROLL_STEP + : -WUSS_SCROLL_STEP; switch (region) { case wuss_FURNITURE_TOGGLE_SIZE: - if (button == wuss_BUTTON_SELECT) + if (button & wuss_BUTTON_SELECT) wuss__furniture_toggle_size(win); break; case wuss_FURNITURE_VSCROLL_UP: @@ -104,7 +106,7 @@ result_t wuss_mouse_click(wuss_t *wuss, { box_t content; - if (button == wuss_BUTTON_SELECT) + if (button & wuss_BUTTON_SELECT) wuss_window_restack(win, wuss_ZORDER_FRONT); wuss__content_box(win, &content); @@ -124,7 +126,7 @@ result_t wuss_mouse_click(wuss_t *wuss, { point_t scroll; - if (button == wuss_BUTTON_SELECT) + if (button & wuss_BUTTON_SELECT) wuss_window_restack(win, wuss_ZORDER_FRONT); wuss_window_get_scroll(win, &scroll); diff --git a/libraries/wuss/test/tasks/ball.c b/libraries/wuss/test/tasks/ball.c index d0af7601..c188b4cb 100644 --- a/libraries/wuss/test/tasks/ball.c +++ b/libraries/wuss/test/tasks/ball.c @@ -93,7 +93,7 @@ static result_t ball_mouse(wuss_window_t *window, wuss_window_get_scroll(window, &scroll); - if (button == wuss_BUTTON_SELECT) + if (button & wuss_BUTTON_SELECT) { ball_t *b; @@ -116,7 +116,7 @@ static result_t ball_mouse(wuss_window_t *window, local.y1 = b->y + b->radius - scroll.y; wuss_window_invalidate(bc->window, &local); } - else if (button == wuss_BUTTON_ADJUST) + else if (button & wuss_BUTTON_ADJUST) { ball_t *b; diff --git a/libraries/wuss/test/tasks/sofa.c b/libraries/wuss/test/tasks/sofa.c index 300c89da..92b00302 100644 --- a/libraries/wuss/test/tasks/sofa.c +++ b/libraries/wuss/test/tasks/sofa.c @@ -381,7 +381,7 @@ static result_t sofa_mouse(wuss_window_t *window, wuss_button_t button, void *ta sc = task_data; - if (button == wuss_BUTTON_ADJUST) + if (button & wuss_BUTTON_ADJUST) { sc->shape = (sc->shape + 1) % sofa_SHAPE__LIMIT; sc->turns = 0; From 6e2c57f035bf2693f7faa6935edceb55a5aab9f2 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Fri, 28 Aug 2026 17:27:38 +0100 Subject: [PATCH 053/287] docs(wuss): move wuss.md into a windowing directory Matches the per-area layout of the other docs (databases, datastruct, framebuf, geom, io). --- docs/{ => windowing}/wuss.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/{ => windowing}/wuss.md (100%) diff --git a/docs/wuss.md b/docs/windowing/wuss.md similarity index 100% rename from docs/wuss.md rename to docs/windowing/wuss.md From 8c0509dad75d54f1f7b9eabe2e82fd1e123b5997 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Fri, 28 Aug 2026 17:27:56 +0100 Subject: [PATCH 054/287] docs(wuss): add a glossary Names the terms the document already leans on: the three coordinate spaces (screen, window-local, virtual content), content area versus visible bounds, furniture and its parts, and the RISC OS button conventions. Co-Authored-By: Claude Opus 5 --- docs/windowing/wuss.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/docs/windowing/wuss.md b/docs/windowing/wuss.md index 1d91fc58..c5368c5e 100644 --- a/docs/windowing/wuss.md +++ b/docs/windowing/wuss.md @@ -127,6 +127,35 @@ Each window carries a scroll offset, `(0, 0)` by default: the point in the task' - `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. - A window move or resize is clipped, piece by piece, against whatever's above it in the z-order, and any pixels a move can preserve are blitted directly rather than queued dirty; only the genuinely-changed pieces end up in the dirty region. This is an internal optimisation with no effect on a task's own redraw handling. +## Glossary + +Terms as this document and the API use them. Several are RISC OS conventions, which Wuss follows. + +- **Adjust** — the secondary mouse button, `wuss_BUTTON_ADJUST`. Conventionally the variant of an action: Adjust on the back icon brings a window to front rather than sending it back, and Adjust on a scroll arrow steps against the direction the arrow points. +- **Backdrop** — the desktop background colour painted behind all windows, set by `config->backdrop` at `wuss_create` time. `wuss_NO_BACKGROUND` leaves the area behind windows untouched, making it the caller's to repaint. +- **Button flags** — `wuss_button_t` values are flags (Select 4, Menu 2, Adjust 1), OR'd together so a chord can be reported. Test a reported button with `&`, never for equality. +- **Chord** — two or more mouse buttons held together, e.g. Select+Adjust. Wuss's own furniture handling resolves an ambiguous chord in Select's favour. +- **Content area** — the part of a window belonging to its task. Its bounds are exactly what was passed to `wuss_window_create`, furniture being added outside it; read back with `wuss_window_get_content_bounds`. +- **Dirty region** — the accumulated set of screen-space boxes needing repaint, coalesced as they accumulate. `wuss_redraw_dirty` repaints and clears it. +- **Document extent** — `doc`, the size of a task's virtual content space, fixed at window creation. Sets how far a window can scroll and the scrollbar sausages' proportions. +- **Furniture** — everything Wuss draws around a window's content: outline, titlebar and its icons, scrollbars, resize icon. Drawn outside the content area, never carved out of it. Furniture clicks are handled entirely within Wuss and never reach the task. +- **Handle callback** — a task's single `wuss_event_fn_t`, receiving every event kind and dispatching on `event->kind`. A window whose task has no handle receives no events at all. +- **Icon** — a clickable furniture region in the titlebar or window corner: close, back, toggle-size, resize. +- **Invalidate** — mark a region dirty for the next `wuss_redraw_dirty`. Window management does this for its own changes; a task must do it for its own content changes. +- **Menu** — the middle mouse button, `wuss_BUTTON_MENU`. Routed like any other button; Wuss provides no menu widget of its own. +- **Outline** — the 1px border drawn around a window, suppressed by `wuss_WINDOW_NO_OUTLINE`. +- **Sausage** — the draggable thumb within a scrollbar well, sized in proportion to how much of the document extent the content area shows. +- **Screen space** — coordinates in the underlying `screen_t`, origin at its top-left. Visible and content bounds are in screen space. +- **Select** — the primary mouse button, `wuss_BUTTON_SELECT`. Performs the plain action, and raises a window when used on its titlebar. +- **Task** — the client of a window: an event callback plus an opaque `task_data` pointer, held in a `wuss_task_t`. Wuss owns the window; the task owns what's drawn inside it. +- **Titlebar** — the strip above the content area carrying the window's label and its icons, and the drag handle for moving the window. Suppressed by `wuss_WINDOW_NO_TITLEBAR`. +- **Toggle size** — the titlebar icon that switches a window between its normal size and a maximised size, and back. +- **Virtual content space** — the task's own full coordinate space, of size `doc`. Mouse and scroll events arrive in it, i.e. with the scroll offset already added. +- **Visible bounds** — a window's whole on-screen footprint, content plus furniture; `wuss_window_get_visible_bounds`. +- **Well** — the track a scrollbar's sausage slides along, between the two arrow icons. +- **Window-local coordinates** — coordinates relative to the content area's top-left, before the scroll offset is added. `wuss_window_invalidate` takes its box in these. +- **Z-order** — the back-to-front stacking order of windows. Changed with `wuss_window_restack`, or by a Select click on a titlebar; content clicks never change it. + ## Limitations - No menus: `wuss_BUTTON_MENU` is defined and routed like any other button, but Wuss has no built-in menu widget. From 5fa615d04e5cb8c0a3988b549b74e9b35adae857 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Fri, 28 Aug 2026 17:28:04 +0100 Subject: [PATCH 055/287] feat(wuss)!: let a window set a minimum content extent doc already caps how large a resize-drag or toggle-size can make a window; the floor was the fixed WUSS_MIN_CONTENT, so a task whose content stops making sense below some size had no way to say so. wuss_window_create gains a min_doc alongside doc, set once at creation. wuss__min_content clamps it up to WUSS_MIN_CONTENT, so a window can never be squeezed too small to grab, and down to doc, so a minimum can never demand a window larger than the document it shows. Pass (0,0) for the built-in floor. BREAKING CHANGE: wuss_window_create takes a min_doc argument between doc and window. Co-Authored-By: Claude Opus 5 --- docs/windowing/wuss.md | 9 ++- include/wuss/window.h | 8 ++- libraries/wuss/furniture/drag-resize.c | 10 +-- libraries/wuss/furniture/toggle-action.c | 12 ++-- libraries/wuss/impl.h | 15 ++++ libraries/wuss/test/tasks/ball.c | 1 + libraries/wuss/test/tasks/blank.c | 1 + libraries/wuss/test/tasks/chars.c | 1 + libraries/wuss/test/tasks/checker.c | 2 + libraries/wuss/test/tasks/curve.c | 1 + libraries/wuss/test/tasks/gradient.c | 1 + libraries/wuss/test/tasks/image.c | 1 + libraries/wuss/test/tasks/launcher.c | 1 + libraries/wuss/test/tasks/palette.c | 1 + libraries/wuss/test/tasks/porter-duff.c | 1 + libraries/wuss/test/tasks/sofa.c | 1 + libraries/wuss/test/tasks/text.c | 1 + libraries/wuss/test/wuss-test.c | 88 ++++++++++++++++++++++-- libraries/wuss/window/create.c | 2 + 19 files changed, 138 insertions(+), 19 deletions(-) diff --git a/docs/windowing/wuss.md b/docs/windowing/wuss.md index c5368c5e..a648f97a 100644 --- a/docs/windowing/wuss.md +++ b/docs/windowing/wuss.md @@ -32,13 +32,15 @@ Create a window with a content bounding box, optional title, appearance flags, a result_t wuss_window_create(wuss_t *wuss, const box_t *content, const char *title, wuss_window_flags_t flags, wuss_colour_t bg, const wuss_task_t *task, - size2d_t doc, + size2d_t doc, size2d_t min_doc, wuss_window_t **window); ``` `bg` is filled in by wuss before each redraw event, or `wuss_NO_BACKGROUND` for the task to draw its own background (avoids a redundant fill behind an opaque task); changeable later via `wuss_window_set_background`. -`doc` is the virtual document extent behind the horizontal/vertical scrollbars' sausage proportion; pass `content`'s own width/height for a window with nothing to scroll. Set once at creation, immutable thereafter. +`doc` is the virtual document extent behind the horizontal/vertical scrollbars' sausage proportion; pass `content`'s own width/height for a window with nothing to scroll. It is also the ceiling a resize-drag or toggle-size grows the content area to. Set once at creation, immutable thereafter. + +`min_doc` is the opposite end: the smallest content size a resize-drag or toggle-size will shrink to. Pass `(0, 0)` for the built-in floor. It is clamped both to that built-in floor, so a window can never be squeezed too small to grab, and to `doc`, so it can never demand a window larger than the document it shows. Also set once at creation. Furniture is additional to `content`, not carved out of it: the window's content area always ends up exactly the box requested, and its on-screen footprint (`wuss_window_get_visible_bounds`) is `content` expanded outward by whatever furniture flags request — a titlebar above, and/or a 1px outline around all four sides. @@ -137,7 +139,8 @@ Terms as this document and the API use them. Several are RISC OS conventions, wh - **Chord** — two or more mouse buttons held together, e.g. Select+Adjust. Wuss's own furniture handling resolves an ambiguous chord in Select's favour. - **Content area** — the part of a window belonging to its task. Its bounds are exactly what was passed to `wuss_window_create`, furniture being added outside it; read back with `wuss_window_get_content_bounds`. - **Dirty region** — the accumulated set of screen-space boxes needing repaint, coalesced as they accumulate. `wuss_redraw_dirty` repaints and clears it. -- **Document extent** — `doc`, the size of a task's virtual content space, fixed at window creation. Sets how far a window can scroll and the scrollbar sausages' proportions. +- **Document extent** — `doc`, the size of a task's virtual content space, fixed at window creation. Sets how far a window can scroll, the scrollbar sausages' proportions, and the size a resize-drag or toggle-size can grow the content area to. +- **Minimum extent** — `min_doc`, the smallest content size a resize-drag or toggle-size will leave a window at, fixed at window creation. `(0, 0)` means the built-in floor. - **Furniture** — everything Wuss draws around a window's content: outline, titlebar and its icons, scrollbars, resize icon. Drawn outside the content area, never carved out of it. Furniture clicks are handled entirely within Wuss and never reach the task. - **Handle callback** — a task's single `wuss_event_fn_t`, receiving every event kind and dispatching on `event->kind`. A window whose task has no handle receives no events at all. - **Icon** — a clickable furniture region in the titlebar or window corner: close, back, toggle-size, resize. diff --git a/include/wuss/window.h b/include/wuss/window.h index 4444636b..fa53be11 100644 --- a/include/wuss/window.h +++ b/include/wuss/window.h @@ -55,7 +55,12 @@ extern "C" * with no content handling. * \param[in] doc Virtual document extent, for the scrollbars' sausage * proportions; pass content's own width and height for a - * window with nothing to scroll. + * window with nothing to scroll. Also the ceiling a + * resize-drag or toggle-size will grow the content area to. + * \param[in] min_doc Minimum content size a resize-drag or toggle-size will + * shrink to. Pass (0,0) for the built-in floor. Clamped to + * doc, and to the built-in floor, so it can never make a + * window unusably small or larger than its document. * \param[out] window Newly created window. Becomes the topmost window. * \return \ref result_OK on success, \ref result_WUSS_TOO_SMALL if content's * width or height is not positive, \ref result_WUSS_BAD_COLOUR if bg is @@ -68,6 +73,7 @@ result_t wuss_window_create(wuss_t *wuss, wuss_colour_t bg, const wuss_task_t *task, size2d_t doc, + size2d_t min_doc, wuss_window_t **window); /** diff --git a/libraries/wuss/furniture/drag-resize.c b/libraries/wuss/furniture/drag-resize.c index 19995ec4..61c2f93d 100644 --- a/libraries/wuss/furniture/drag-resize.c +++ b/libraries/wuss/furniture/drag-resize.c @@ -6,15 +6,17 @@ void wuss__furniture_drag_resize(wuss_window_t *window, point_t p) { - box_t content; - int width, height; + box_t content; + size2d_t min; + int width, height; wuss__content_box(window, &content); + wuss__min_content(window, &min); width = p.x - content.x0; height = p.y - content.y0; - width = CLAMP(width, WUSS_MIN_CONTENT, window->doc.w); - height = CLAMP(height, WUSS_MIN_CONTENT, window->doc.h); + width = CLAMP(width, min.w, MAX(window->doc.w, min.w)); + height = CLAMP(height, min.h, MAX(window->doc.h, min.h)); wuss_window_resize(window, (size2d_t) { width, height }); } diff --git a/libraries/wuss/furniture/toggle-action.c b/libraries/wuss/furniture/toggle-action.c index 4203f778..4d6a6fcd 100644 --- a/libraries/wuss/furniture/toggle-action.c +++ b/libraries/wuss/furniture/toggle-action.c @@ -16,8 +16,9 @@ void wuss__furniture_toggle_size(wuss_window_t *window) } else { - int outline_px, titlebar_height, available_width, available_height, width, height; - point_t carve; + int outline_px, titlebar_height, available_width, available_height, width, height; + point_t carve; + size2d_t min; outline_px = wuss__outline_px(window); titlebar_height = wuss__titlebar_height(window); @@ -38,9 +39,10 @@ void wuss__furniture_toggle_size(wuss_window_t *window) * x0/y0 (an inverted box), which is never hit-testable again * (box_contains_point can't match x1doc.w, available_width); height = MIN(window->doc.h, available_height); diff --git a/libraries/wuss/impl.h b/libraries/wuss/impl.h index 18425751..7ab0ed0f 100644 --- a/libraries/wuss/impl.h +++ b/libraries/wuss/impl.h @@ -3,6 +3,7 @@ #ifndef IMPL_H #define IMPL_H +#include "base/utils.h" #include "datastruct/list.h" #include "geom/box.h" #include "geom/size.h" @@ -66,6 +67,8 @@ struct wuss_window point_t scroll; /* offset into virtual content space of the * content box's top-left; see wuss_window_set_scroll */ size2d_t doc; /* virtual document extent, set at creation */ + size2d_t min_doc; /* resize floor, set at creation; see + * wuss__min_content */ wuss_window_state_t state; /* see wuss_window_state_t */ box_t pre_toggle; /* visible bounds to restore on the next toggle */ char title[WUSS_TITLE_MAX + 1]; @@ -116,6 +119,18 @@ static inline int wuss__size_ok(int width, int height) return width > 0 && height > 0; } +/* The floor a resize-drag or toggle-size will shrink a window's content to: + * the client's min_doc where it set one, but never below WUSS_MIN_CONTENT (a + * window must stay big enough to grab) nor above the window's own doc extent + * (a window can't be forced larger than the document it shows). */ +static inline void wuss__min_content(const wuss_window_t *window, size2d_t *min) +{ + min->w = CLAMP(window->min_doc.w, WUSS_MIN_CONTENT, MAX(window->doc.w, + WUSS_MIN_CONTENT)); + min->h = CLAMP(window->min_doc.h, WUSS_MIN_CONTENT, MAX(window->doc.h, + WUSS_MIN_CONTENT)); +} + static inline int wuss__titlebar_height_for(const wuss_t *wuss, wuss_window_flags_t flags) { diff --git a/libraries/wuss/test/tasks/ball.c b/libraries/wuss/test/tasks/ball.c index c188b4cb..9e41f4ac 100644 --- a/libraries/wuss/test/tasks/ball.c +++ b/libraries/wuss/test/tasks/ball.c @@ -37,6 +37,7 @@ result_t ball_create(wuss_t *wuss, const colour_t *palette, ball_task_t *task) wuss_NO_BACKGROUND, &delegate, box_size(&box), + (size2d_t) { 0, 0 }, &task->window); } diff --git a/libraries/wuss/test/tasks/blank.c b/libraries/wuss/test/tasks/blank.c index 39f3b709..470a8817 100644 --- a/libraries/wuss/test/tasks/blank.c +++ b/libraries/wuss/test/tasks/blank.c @@ -34,6 +34,7 @@ result_t blank_create(wuss_t *wuss, int npalette, blank_task_t *task) palette_PICO8_GREEN, &delegate, box_size(&box), + (size2d_t) { 0, 0 }, &task->window); } diff --git a/libraries/wuss/test/tasks/chars.c b/libraries/wuss/test/tasks/chars.c index 16c25168..3cc75d2a 100644 --- a/libraries/wuss/test/tasks/chars.c +++ b/libraries/wuss/test/tasks/chars.c @@ -56,6 +56,7 @@ result_t chars_create(wuss_t *wuss, wuss_NO_BACKGROUND, &delegate, box_size(&box), + (size2d_t) { 0, 0 }, &task->window); } diff --git a/libraries/wuss/test/tasks/checker.c b/libraries/wuss/test/tasks/checker.c index 7639a535..71a22ae6 100644 --- a/libraries/wuss/test/tasks/checker.c +++ b/libraries/wuss/test/tasks/checker.c @@ -41,6 +41,7 @@ result_t checker_create(wuss_t *wuss, wuss_NO_BACKGROUND, &delegate, box_size(&box), + (size2d_t) { 0, 0 }, &task->window); if (rc != result_OK) return rc; @@ -54,6 +55,7 @@ result_t checker_create(wuss_t *wuss, wuss_NO_BACKGROUND, &delegate, box_size(&box), + (size2d_t) { 0, 0 }, &task->window2); if (rc != result_OK) { diff --git a/libraries/wuss/test/tasks/curve.c b/libraries/wuss/test/tasks/curve.c index b19b09e3..9fc73ef6 100644 --- a/libraries/wuss/test/tasks/curve.c +++ b/libraries/wuss/test/tasks/curve.c @@ -49,6 +49,7 @@ result_t curve_create(wuss_t *wuss, wuss_NO_BACKGROUND, &delegate, box_size(&box), + (size2d_t) { 0, 0 }, &task->window); } diff --git a/libraries/wuss/test/tasks/gradient.c b/libraries/wuss/test/tasks/gradient.c index 78f0c479..6e171fa6 100644 --- a/libraries/wuss/test/tasks/gradient.c +++ b/libraries/wuss/test/tasks/gradient.c @@ -46,6 +46,7 @@ result_t gradient_create(wuss_t *wuss, gradient_task_t *task) wuss_NO_BACKGROUND, &delegate, (size2d_t) { GRADIENT_DOC_WIDTH, GRADIENT_DOC_HEIGHT }, + (size2d_t) { 0, 0 }, &task->window); } diff --git a/libraries/wuss/test/tasks/image.c b/libraries/wuss/test/tasks/image.c index d9e55207..a804b49b 100644 --- a/libraries/wuss/test/tasks/image.c +++ b/libraries/wuss/test/tasks/image.c @@ -43,6 +43,7 @@ result_t image_create(wuss_t *wuss, palette_PICO8_BLACK, &delegate, (size2d_t) { task->bitmap.size.w, task->bitmap.size.h }, + (size2d_t) { 0, 0 }, &task->window); } diff --git a/libraries/wuss/test/tasks/launcher.c b/libraries/wuss/test/tasks/launcher.c index 1039b726..07c6829f 100644 --- a/libraries/wuss/test/tasks/launcher.c +++ b/libraries/wuss/test/tasks/launcher.c @@ -46,6 +46,7 @@ result_t launcher_create(wuss_t *wuss, wuss_NO_BACKGROUND, &delegate, box_size(&box), + (size2d_t) { 0, 0 }, &task->window); } diff --git a/libraries/wuss/test/tasks/palette.c b/libraries/wuss/test/tasks/palette.c index 4e44965a..0c781b41 100644 --- a/libraries/wuss/test/tasks/palette.c +++ b/libraries/wuss/test/tasks/palette.c @@ -33,6 +33,7 @@ result_t palette_create(wuss_t *wuss, palette_PICO8_BLACK, &delegate, box_size(&box), + (size2d_t) { 0, 0 }, &task->window); } diff --git a/libraries/wuss/test/tasks/porter-duff.c b/libraries/wuss/test/tasks/porter-duff.c index 1a482abc..82cc8e95 100644 --- a/libraries/wuss/test/tasks/porter-duff.c +++ b/libraries/wuss/test/tasks/porter-duff.c @@ -201,6 +201,7 @@ result_t porter_duff_create(wuss_t *wuss, wuss_NO_BACKGROUND, &delegate, box_size(&box), + (size2d_t) { 0, 0 }, &task->window); if (rc != result_OK) goto free_dst; diff --git a/libraries/wuss/test/tasks/sofa.c b/libraries/wuss/test/tasks/sofa.c index 92b00302..8d25a6da 100644 --- a/libraries/wuss/test/tasks/sofa.c +++ b/libraries/wuss/test/tasks/sofa.c @@ -278,6 +278,7 @@ result_t sofa_create(wuss_t *wuss, const colour_t *palette, sofa_task_t *task) wuss_NO_BACKGROUND, &delegate, box_size(&box), + (size2d_t) { 0, 0 }, &task->window); } diff --git a/libraries/wuss/test/tasks/text.c b/libraries/wuss/test/tasks/text.c index 629bea22..41fc2e4f 100644 --- a/libraries/wuss/test/tasks/text.c +++ b/libraries/wuss/test/tasks/text.c @@ -54,6 +54,7 @@ result_t text_create(wuss_t *wuss, palette_PICO8_BLUE, &delegate, box_size(&box), + (size2d_t) { 0, 0 }, &task->window); return rc; diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 27f54847..861f71a7 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -618,6 +618,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, NULL, box_size(&box_a), + (size2d_t) { 0, 0 }, &win_a); if (rc != result_WUSS_TOO_SMALL) goto Failure; @@ -643,6 +644,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_a, box_size(&box_a), + (size2d_t) { 0, 0 }, &win_a); if (rc != result_OK) goto Failure; @@ -665,6 +667,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_b, box_size(&box_b), + (size2d_t) { 0, 0 }, &win_b); if (rc != result_OK) goto Failure; @@ -950,6 +953,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_d, box_size(&box_d), + (size2d_t) { 0, 0 }, &win_d); if (rc != result_OK) goto Failure; @@ -997,6 +1001,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_e, box_size(&box_e), + (size2d_t) { 0, 0 }, &win_e); if (rc != result_OK) goto Failure; @@ -1017,6 +1022,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_f, box_size(&box_f), + (size2d_t) { 0, 0 }, &win_f); if (rc != result_OK) goto Failure; @@ -1084,6 +1090,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_h, box_size(&box_h), + (size2d_t) { 0, 0 }, &win_h); if (rc != result_OK) goto Failure; @@ -1104,6 +1111,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_g, box_size(&box_g), + (size2d_t) { 0, 0 }, &win_g); if (rc != result_OK) goto Failure; @@ -1165,6 +1173,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_i, box_size(&box_i), + (size2d_t) { 0, 0 }, &win_i); if (rc != result_OK) goto Failure; @@ -1185,6 +1194,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_j, box_size(&box_j), + (size2d_t) { 0, 0 }, &win_j); if (rc != result_OK) goto Failure; @@ -1235,6 +1245,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_m, box_size(&box_m), + (size2d_t) { 0, 0 }, &win_m); if (rc != result_OK) goto Failure; @@ -1287,6 +1298,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_h, box_size(&box_h), + (size2d_t) { 0, 0 }, &win_h); if (rc != result_OK) goto Failure; @@ -1305,6 +1317,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_g, box_size(&box_g), + (size2d_t) { 0, 0 }, &win_g); if (rc != result_OK) goto Failure; @@ -1405,6 +1418,53 @@ result_t wuss_test(const char *resources) wuss_window_close(win_h); } + printf("test: drag-resize stops at min_doc, not just WUSS_MIN_CONTENT\n"); + + { + test_task_t tc_m; + wuss_task_t delegate_m; + box_t box_m, content, visible; + wuss_window_t *win_m; + + tc_m.redraw_count = 0; + tc_m.mouse_count = 0; + delegate_m.handle = test_handle; + delegate_m.task_data = &tc_m; + + 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(wuss, &box_m, "M", wuss_WINDOW_NONE, + wuss_NO_BACKGROUND, + &delegate_m, + (size2d_t) { 200, 200 }, (size2d_t) { 80, 60 }, + &win_m); + if (rc != result_OK) + goto Failure; + + wuss_window_get_visible_bounds(win_m, &visible); + wuss_window_get_content_bounds(win_m, &content); + + rc = wuss_mouse_click(wuss, (point_t) { visible.x1 - 3, visible.y1 - 3 }, wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* M's resize icon */ + if (rc != result_OK) + goto Failure; + if (hit != win_m) + goto Failure; + + rc = wuss_mouse_move(wuss, (point_t) { content.x0 + 5, content.y0 + 5 }, &hit); /* drag far inside min_doc */ + if (rc != result_OK) + goto Failure; + + rc = wuss_mouse_click(wuss, (point_t) { content.x0 + 5, content.y0 + 5 }, wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); + if (rc != result_OK) + goto Failure; + + wuss_window_get_content_bounds(win_m, &content); + if (content.x1 - content.x0 != 80 || content.y1 - content.y0 != 60) + goto Failure; /* floored at min_doc, not squeezed down to WUSS_MIN_CONTENT */ + + wuss_window_close(win_m); + } + printf("test: toggle-size blits rather than redrawing the whole window\n"); { @@ -1427,7 +1487,7 @@ result_t wuss_test(const char *resources) rc = wuss_window_create(wuss, &box_t_win, "T", wuss_WINDOW_NONE, wuss_NO_BACKGROUND, &delegate_t, - (size2d_t) { 200, 200 }, &win_t); + (size2d_t) { 200, 200 }, (size2d_t) { 0, 0 }, &win_t); if (rc != result_OK) goto Failure; @@ -1593,7 +1653,7 @@ result_t wuss_test(const char *resources) rc = wuss_window_create(wuss, &box_r, "R", wuss_WINDOW_NONE, wuss_NO_BACKGROUND, &delegate_r, - (size2d_t) { 70, 70 }, &win_r); + (size2d_t) { 70, 70 }, (size2d_t) { 0, 0 }, &win_r); if (rc != result_OK) goto Failure; @@ -1691,7 +1751,7 @@ result_t wuss_test(const char *resources) rc = wuss_window_create(wuss, &box_nb, "NB", wuss_WINDOW_NO_RESIZE_BLIT, wuss_NO_BACKGROUND, &delegate_nb, - (size2d_t) { 200, 200 }, &win_nb); + (size2d_t) { 200, 200 }, (size2d_t) { 0, 0 }, &win_nb); if (rc != result_OK) goto Failure; @@ -1770,7 +1830,7 @@ result_t wuss_test(const char *resources) box_u.x1 = 120; box_u.y1 = 120; /* 40x40 content */ rc = wuss_window_create(wuss, &box_u, "U", wuss_WINDOW_NONE, wuss_NO_BACKGROUND, /* scrollbars on: carve.x/y = icon size */ &delegate_u, - (size2d_t) { 70, 70 }, &win_u); /* doc size well within the 200x200 screen: growth is doc-limited, not screen-limited */ + (size2d_t) { 70, 70 }, (size2d_t) { 0, 0 }, &win_u); /* doc size well within the 200x200 screen: growth is doc-limited, not screen-limited */ if (rc != result_OK) goto Failure; @@ -1878,7 +1938,7 @@ result_t wuss_test(const char *resources) rc = wuss_window_create(wuss, &box_v, "V", wuss_WINDOW_NONE, wuss_NO_BACKGROUND, &delegate_v, - (size2d_t) { 200, 200 }, &win_v); + (size2d_t) { 200, 200 }, (size2d_t) { 0, 0 }, &win_v); if (rc != result_OK) goto Failure; @@ -1992,6 +2052,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_k, box_size(&box_k), + (size2d_t) { 0, 0 }, &win_k); if (rc != result_OK) goto Failure; @@ -2012,6 +2073,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_l, box_size(&box_l), + (size2d_t) { 0, 0 }, &win_l); if (rc != result_OK) goto Failure; @@ -2078,6 +2140,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_m2, box_size(&box_m2), + (size2d_t) { 0, 0 }, &win_m2); if (rc != result_OK) goto Failure; @@ -2168,6 +2231,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_nb2, box_size(&box_nb2), + (size2d_t) { 0, 0 }, &win_nb2); if (rc != result_OK) goto Failure; @@ -2219,6 +2283,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_n, box_size(&box_n), + (size2d_t) { 0, 0 }, &win_n); if (rc != result_OK) goto Failure; @@ -2237,6 +2302,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_o, box_size(&box_o), + (size2d_t) { 0, 0 }, &win_o); if (rc != result_OK) goto Failure; @@ -2321,6 +2387,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_b, box_size(&box_b), + (size2d_t) { 0, 0 }, &win_b); if (rc != result_OK) goto Failure; @@ -2339,6 +2406,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_a, box_size(&box_a), + (size2d_t) { 0, 0 }, &win_a); if (rc != result_OK) goto Failure; @@ -2422,6 +2490,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_b, box_size(&box_b), + (size2d_t) { 0, 0 }, &win_b); if (rc != result_OK) goto Failure; @@ -2440,6 +2509,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_a, box_size(&box_a), + (size2d_t) { 0, 0 }, &win_a); if (rc != result_OK) goto Failure; @@ -2500,6 +2570,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_b, box_size(&box_b), + (size2d_t) { 0, 0 }, &win_b); if (rc != result_OK) goto Failure; @@ -2518,6 +2589,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_a, box_size(&box_a), + (size2d_t) { 0, 0 }, &win_a); if (rc != result_OK) goto Failure; @@ -2611,6 +2683,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_b, box_size(&box_b), + (size2d_t) { 0, 0 }, &win_b); if (rc != result_OK) goto Failure; @@ -2629,6 +2702,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_a, box_size(&box_a), + (size2d_t) { 0, 0 }, &win_a); if (rc != result_OK) goto Failure; @@ -2705,6 +2779,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_c, box_size(&box_c), + (size2d_t) { 0, 0 }, &win_c); if (rc != result_OK) goto Failure; @@ -2738,7 +2813,7 @@ result_t wuss_test(const char *resources) rc = wuss_window_create(wuss, &box_s, "S", wuss_WINDOW_NONE, wuss_NO_BACKGROUND, &delegate_s, - (size2d_t) { 200, 200 }, &win_s); + (size2d_t) { 200, 200 }, (size2d_t) { 0, 0 }, &win_s); if (rc != result_OK) goto Failure; @@ -2815,6 +2890,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_r, (size2d_t) { 400, 400 }, + (size2d_t) { 0, 0 }, &win_r); if (rc != result_OK) goto Failure; diff --git a/libraries/wuss/window/create.c b/libraries/wuss/window/create.c index 591da41e..421ab08e 100644 --- a/libraries/wuss/window/create.c +++ b/libraries/wuss/window/create.c @@ -17,6 +17,7 @@ result_t wuss_window_create(wuss_t *wuss, wuss_colour_t bg, const wuss_task_t *task, size2d_t doc, + size2d_t min_doc, wuss_window_t **window) { wuss_window_t *win; @@ -76,6 +77,7 @@ result_t wuss_window_create(wuss_t *wuss, win->scroll.x = 0; win->scroll.y = 0; win->doc = doc; + win->min_doc = min_doc; win->state = wuss_WINDOW_STATE_NONE; if (task != NULL) From 61e1f7a578d9a9ba963a9718970d08c6b95af4c8 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Fri, 28 Aug 2026 18:48:27 +0100 Subject: [PATCH 056/287] fix(wuss): track launcher running state in a separate array The launcher entry table is now const, so the per-row "running" flag can no longer live in it. Move it into launcher_task as a fixed-size bool array indexed by entry, capped at LAUNCHER_MAX_ENTRIES. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/test/tasks/launcher.c | 50 +++++++------- libraries/wuss/test/tasks/launcher.h | 37 ++++++----- libraries/wuss/test/wuss-test.c | 98 ++++++++++++++-------------- 3 files changed, 95 insertions(+), 90 deletions(-) diff --git a/libraries/wuss/test/tasks/launcher.c b/libraries/wuss/test/tasks/launcher.c index 07c6829f..ec779f4b 100644 --- a/libraries/wuss/test/tasks/launcher.c +++ b/libraries/wuss/test/tasks/launcher.c @@ -2,6 +2,7 @@ #ifdef USE_SDL +#include #include #ifdef FORTIFY @@ -19,18 +20,21 @@ #define LAUNCHER_PAD 4 #define LAUNCHER_WIDTH 160 -result_t launcher_create(wuss_t *wuss, - launcher_entry_t *entries, - int nentries, - bmfont_t *font, - const colour_t *palette, - launcher_task_t *task) +result_t launcher_create(wuss_t *wuss, + const launcher_entry_t *entries, + int nentries, + bmfont_t *font, + const colour_t *palette, + launcher_task_t *task) { wuss_task_t delegate; box_t box; + assert(nentries <= LAUNCHER_MAX_ENTRIES); + task->entries = entries; task->nentries = nentries; + memset(task->running, 0, sizeof(task->running)); task->font = font; task->fg = palette[palette_PICO8_BLACK]; task->bg = palette[palette_PICO8_WHITE]; @@ -58,11 +62,11 @@ void launcher_destroy(launcher_task_t *task) static result_t launcher_redraw(const wuss_event_t *event, void *task_data) { launcher_task_t *lc; - screen_t *scr; - const box_t *content, *bounds; - int i, font_width, font_height, sx, sy; - point_t pos; - const launcher_entry_t *entry; + screen_t *scr; + const box_t *content, *bounds; + int i, font_width, font_height, sx, sy; + point_t pos; + const launcher_entry_t *entry; lc = task_data; @@ -72,11 +76,9 @@ static result_t launcher_redraw(const wuss_event_t *event, void *task_data) sx = event->data.redraw.scroll.x; sy = event->data.redraw.scroll.y; - screen_draw_rect(scr, content->x0, content->y0, box_size(content), - lc->bg); + screen_draw_rect(scr, content->x0, content->y0, box_size(content), lc->bg); - bmfont_get_info(lc->font, &font_width, &font_height); - NOT_USED(font_width); + bmfont_get_info(lc->font, NULL, &font_height); for (i = 0; i < lc->nentries; i++) { @@ -86,7 +88,7 @@ static result_t launcher_redraw(const wuss_event_t *event, void *task_data) pos.y = bounds->y0 - sy + LAUNCHER_PAD + i * LAUNCHER_ROW_HEIGHT + (LAUNCHER_ROW_HEIGHT - font_height) / 2; bmfont_draw(lc->font, scr, entry->name, (int) strlen(entry->name), - entry->running ? lc->running_fg : lc->fg, lc->bg, &pos, NULL); + lc->running[i] ? lc->running_fg : lc->fg, lc->bg, &pos, NULL); } return result_OK; @@ -94,10 +96,10 @@ static result_t launcher_redraw(const wuss_event_t *event, void *task_data) static result_t launcher_mouse(wuss_window_t *window, int y, void *task_data) { - launcher_task_t *lc; - int i; - launcher_entry_t *entry; - result_t rc; + launcher_task_t *lc; + int i; + const launcher_entry_t *entry; + result_t rc; lc = task_data; @@ -107,14 +109,14 @@ static result_t launcher_mouse(wuss_window_t *window, int y, void *task_data) if (i < 0 || i >= lc->nentries) return result_OK; - entry = &lc->entries[i]; - if (entry->running) + if (lc->running[i]) return result_OK; - rc = entry->spawn(); + entry = &lc->entries[i]; + rc = entry->spawn(); if (rc == result_OK) { - entry->running = true; + lc->running[i] = true; wuss_window_invalidate_all(window); } diff --git a/libraries/wuss/test/tasks/launcher.h b/libraries/wuss/test/tasks/launcher.h index 05ebd89c..4147975e 100644 --- a/libraries/wuss/test/tasks/launcher.h +++ b/libraries/wuss/test/tasks/launcher.h @@ -14,25 +14,28 @@ typedef result_t (*launcher_spawn_fn_t)(void); typedef void (*launcher_destroy_fn_t)(void); -/* one clickable row; "running" is set once spawned and never cleared, so a - * second click is a no-op -- relaunching a task after its window closes - * needs the test restarted */ +/* one clickable row */ typedef struct launcher_entry { const char *name; - launcher_spawn_fn_t spawn; - launcher_destroy_fn_t destroy; - bool running; + launcher_spawn_fn_t spawn; + launcher_destroy_fn_t destroy; } launcher_entry_t; +/* a row's task is spawned at most once: launcher_task's "running" array is + * set on the first click and never cleared, so a second click is a no-op -- + * relaunching a task after its window closes needs the test restarted */ +#define LAUNCHER_MAX_ENTRIES 32 + typedef struct launcher_task { - launcher_entry_t *entries; /* owned by the caller, must outlive the window */ - int nentries; - bmfont_t *font; - colour_t fg, bg, running_fg; - wuss_window_t *window; + const launcher_entry_t *entries; /* owned by the caller, must outlive the window */ + int nentries; + bool running[LAUNCHER_MAX_ENTRIES]; + bmfont_t *font; + colour_t fg, bg, running_fg; + wuss_window_t *window; } launcher_task_t; @@ -40,12 +43,12 @@ wuss_event_fn_t launcher_handle; /* create a window listing "entries"; clicking a row calls its spawn * function once */ -result_t launcher_create(wuss_t *wuss, - launcher_entry_t *entries, - int nentries, - bmfont_t *font, - const colour_t *palette, - launcher_task_t *task); +result_t launcher_create(wuss_t *wuss, + const launcher_entry_t *entries, + int nentries, + bmfont_t *font, + const colour_t *palette, + launcher_task_t *task); void launcher_destroy(launcher_task_t *task); diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 861f71a7..c88a7e0c 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -50,61 +50,61 @@ /* the launcher's spawn callbacks take no arguments, so the pieces they need * are stashed here instead; wuss_interactive_test runs at most once per * process, so file-scope statics are as good as a context struct */ -static wuss_t *g_wuss; -static const colour_t *g_palette; -static int g_npalette; -static const char *g_resources; -static bmfont_t *g_daydream_font; - -static ball_task_t g_ball_task; -static text_task_t g_text_task; -static blank_task_t g_blank_task; -static chars_task_t g_chars_task; -static palette_task_t g_palette_task; -static image_task_t g_image_task; -static checker_task_t g_checker_task; -static curve_task_t g_curve_task; -static sofa_task_t g_sofa_task; -static gradient_task_t g_gradient_task; +static wuss_t *g_wuss; +static const colour_t *g_palette; +static int g_npalette; +static const char *g_resources; +static bmfont_t *g_daydream_font; + +static ball_task_t g_ball_task; +static text_task_t g_text_task; +static blank_task_t g_blank_task; +static chars_task_t g_chars_task; +static palette_task_t g_palette_task; +static image_task_t g_image_task; +static checker_task_t g_checker_task; +static curve_task_t g_curve_task; +static sofa_task_t g_sofa_task; +static gradient_task_t g_gradient_task; static porter_duff_task_t g_porter_duff_task; -static result_t spawn_ball(void) { return ball_create(g_wuss, g_palette, &g_ball_task); } -static result_t spawn_text(void) { return text_create(g_wuss, g_palette, g_daydream_font, &g_text_task); } -static result_t spawn_blank(void) { return blank_create(g_wuss, g_npalette, &g_blank_task); } -static result_t spawn_chars(void) { return chars_create(g_wuss, g_palette, &g_chars_task); } -static result_t spawn_palette(void) { return palette_create(g_wuss, g_palette, g_npalette, &g_palette_task); } -static result_t spawn_image(void) { return image_create(g_wuss, g_palette, g_resources, &g_image_task); } -static result_t spawn_checker(void) { return checker_create(g_wuss, g_palette, &g_checker_task); } -static result_t spawn_curve(void) { return curve_create(g_wuss, g_palette, &g_curve_task); } -static result_t spawn_sofa(void) { return sofa_create(g_wuss, g_palette, &g_sofa_task); } -static result_t spawn_gradient(void) { return gradient_create(g_wuss, &g_gradient_task); } +static result_t spawn_ball(void) { return ball_create(g_wuss, g_palette, &g_ball_task); } +static result_t spawn_text(void) { return text_create(g_wuss, g_palette, g_daydream_font, &g_text_task); } +static result_t spawn_blank(void) { return blank_create(g_wuss, g_npalette, &g_blank_task); } +static result_t spawn_chars(void) { return chars_create(g_wuss, g_palette, &g_chars_task); } +static result_t spawn_palette(void) { return palette_create(g_wuss, g_palette, g_npalette, &g_palette_task); } +static result_t spawn_image(void) { return image_create(g_wuss, g_palette, g_resources, &g_image_task); } +static result_t spawn_checker(void) { return checker_create(g_wuss, g_palette, &g_checker_task); } +static result_t spawn_curve(void) { return curve_create(g_wuss, g_palette, &g_curve_task); } +static result_t spawn_sofa(void) { return sofa_create(g_wuss, g_palette, &g_sofa_task); } +static result_t spawn_gradient(void) { return gradient_create(g_wuss, &g_gradient_task); } static result_t spawn_porter_duff(void) { return porter_duff_create(g_wuss, g_palette, g_daydream_font, g_resources, &g_porter_duff_task); } -static void destroy_ball(void) { ball_destroy(&g_ball_task); } -static void destroy_text(void) { text_destroy(&g_text_task); } -static void destroy_blank(void) { blank_destroy(&g_blank_task); } -static void destroy_chars(void) { chars_destroy(&g_chars_task); } -static void destroy_palette(void) { palette_destroy(&g_palette_task); } -static void destroy_image(void) { image_destroy(&g_image_task); } -static void destroy_checker(void) { checker_destroy(&g_checker_task); } -static void destroy_curve(void) { curve_destroy(&g_curve_task); } -static void destroy_sofa(void) { sofa_destroy(&g_sofa_task); } -static void destroy_gradient(void) { gradient_destroy(&g_gradient_task); } +static void destroy_ball(void) { ball_destroy(&g_ball_task); } +static void destroy_text(void) { text_destroy(&g_text_task); } +static void destroy_blank(void) { blank_destroy(&g_blank_task); } +static void destroy_chars(void) { chars_destroy(&g_chars_task); } +static void destroy_palette(void) { palette_destroy(&g_palette_task); } +static void destroy_image(void) { image_destroy(&g_image_task); } +static void destroy_checker(void) { checker_destroy(&g_checker_task); } +static void destroy_curve(void) { curve_destroy(&g_curve_task); } +static void destroy_sofa(void) { sofa_destroy(&g_sofa_task); } +static void destroy_gradient(void) { gradient_destroy(&g_gradient_task); } static void destroy_porter_duff(void) { porter_duff_destroy(&g_porter_duff_task); } -static launcher_entry_t g_launcher_entries[] = +static const launcher_entry_t g_launcher_entries[] = { - { "Ball", spawn_ball, destroy_ball, false }, - { "Text", spawn_text, destroy_text, false }, - { "Blank", spawn_blank, destroy_blank, false }, - { "Chars", spawn_chars, destroy_chars, false }, - { "Palette", spawn_palette, destroy_palette, false }, - { "Image", spawn_image, destroy_image, false }, - { "Checker", spawn_checker, destroy_checker, false }, - { "Curve", spawn_curve, destroy_curve, false }, - { "Sofa", spawn_sofa, destroy_sofa, false }, - { "Gradient", spawn_gradient, destroy_gradient, false }, - { "Porter-Duff", spawn_porter_duff, destroy_porter_duff, false } + { "Ball", spawn_ball, destroy_ball }, + { "Text", spawn_text, destroy_text }, + { "Blank", spawn_blank, destroy_blank }, + { "Chars", spawn_chars, destroy_chars }, + { "Palette", spawn_palette, destroy_palette }, + { "Image", spawn_image, destroy_image }, + { "Checker", spawn_checker, destroy_checker }, + { "Curve", spawn_curve, destroy_curve }, + { "Sofa", spawn_sofa, destroy_sofa }, + { "Gradient", spawn_gradient, destroy_gradient }, + { "Porter-Duff", spawn_porter_duff, destroy_porter_duff } }; static wuss_button_t sdl_button_to_wuss(Uint8 button) @@ -414,7 +414,7 @@ static result_t wuss_interactive_test(const char *resources) } for (i = 0; i < NELEMS(g_launcher_entries); i++) - if (g_launcher_entries[i].running) + if (launcher_task.running[i]) g_launcher_entries[i].destroy(); launcher_destroy(&launcher_task); From 9c468a4d7722d1992a509c5440d661dffdff40fb Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sat, 29 Aug 2026 00:28:45 +0100 Subject: [PATCH 057/287] refactor(geom): add POINT and SIZE2D compound-literal macros Replace inline (point_t) { x, y } and (size2d_t) { w, h } compound literals across the tree with POINT(x, y) / SIZE2D(w, h) helpers defined alongside their types. Also folds in a pre-existing null-pointer guard in bmfont_get_info and minor alignment tidy-ups that were already in the working tree. Co-Authored-By: Claude Sonnet 5 --- include/geom/point.h | 3 + include/geom/size.h | 3 + libraries/framebuf/bitmap/load.c | 2 +- libraries/framebuf/bmfont/bmfont.c | 8 +- libraries/framebuf/bmfont/test/bmfont-test.c | 2 +- .../framebuf/composite/test/composite-test.c | 2 +- libraries/framebuf/curve/test/curve-test.c | 2 +- libraries/framebuf/screen/screen-draw.c | 2 +- libraries/framebuf/screen/test/screen-test.c | 2 +- libraries/wuss/furniture/drag-resize.c | 2 +- libraries/wuss/furniture/draw.c | 8 +- libraries/wuss/furniture/scroll-action.c | 4 +- libraries/wuss/furniture/toggle-action.c | 2 +- libraries/wuss/mouse-click.c | 10 +- libraries/wuss/mouse-move.c | 6 +- libraries/wuss/redraw.c | 2 +- libraries/wuss/scroll.c | 4 +- libraries/wuss/test/tasks/ball.c | 4 +- libraries/wuss/test/tasks/blank.c | 2 +- libraries/wuss/test/tasks/chars.c | 8 +- libraries/wuss/test/tasks/checker.c | 4 +- libraries/wuss/test/tasks/curve.c | 10 +- libraries/wuss/test/tasks/gradient.c | 4 +- libraries/wuss/test/tasks/image.c | 4 +- libraries/wuss/test/tasks/launcher.c | 2 +- libraries/wuss/test/tasks/palette.c | 4 +- libraries/wuss/test/tasks/porter-duff.c | 2 +- libraries/wuss/test/tasks/sofa.c | 2 +- libraries/wuss/test/tasks/text.c | 4 +- libraries/wuss/test/wuss-test.c | 308 +++++++++--------- libraries/wuss/window/move.c | 2 +- libraries/wuss/window/set-scroll.c | 2 +- 32 files changed, 217 insertions(+), 209 deletions(-) diff --git a/include/geom/point.h b/include/geom/point.h index be19615c..a1360842 100644 --- a/include/geom/point.h +++ b/include/geom/point.h @@ -10,4 +10,7 @@ typedef struct point } point_t; +/** Construct a point_t compound literal. */ +#define POINT(x, y) ((point_t) { (x), (y) }) + #endif /* GEOM_POINT_H */ diff --git a/include/geom/size.h b/include/geom/size.h index 202f474b..5d66d4c0 100644 --- a/include/geom/size.h +++ b/include/geom/size.h @@ -10,4 +10,7 @@ typedef struct size2d } size2d_t; +/** Construct a size2d_t compound literal. */ +#define SIZE2D(w, h) ((size2d_t) { (w), (h) }) + #endif /* GEOM_SIZE_H */ diff --git a/libraries/framebuf/bitmap/load.c b/libraries/framebuf/bitmap/load.c index 609a349e..b4b3fbaa 100644 --- a/libraries/framebuf/bitmap/load.c +++ b/libraries/framebuf/bitmap/load.c @@ -124,7 +124,7 @@ result_t bitmap_load_png(bitmap_t *bm, const char *filename) png_read_image(png_ptr, row_pointers); - bitmap_init(bm, (size2d_t) { pngwidth, pngheight }, + bitmap_init(bm, SIZE2D(pngwidth, pngheight), bm_fmt, bm_rowbytes, NULL, /* no palette */ diff --git a/libraries/framebuf/bmfont/bmfont.c b/libraries/framebuf/bmfont/bmfont.c index 5dd01fa4..b01d6508 100644 --- a/libraries/framebuf/bmfont/bmfont.c +++ b/libraries/framebuf/bmfont/bmfont.c @@ -515,8 +515,10 @@ void bmfont_destroy(bmfont_t *bmfont) void bmfont_get_info(bmfont_t *bmfont, int *width, int *height) { - *width = bmfont->charwidth; - *height = bmfont->charheight; + if (width) + *width = bmfont->charwidth; + if (height) + *height = bmfont->charheight; } int bmfont_get_count(bmfont_t *bmfont) @@ -1246,7 +1248,7 @@ result_t bmfont_draw(bmfont_t *bmfont, } } - *end_pos = (point_t) { x, pos->y }; + *end_pos = POINT(x, pos->y); } return result_OK; diff --git a/libraries/framebuf/bmfont/test/bmfont-test.c b/libraries/framebuf/bmfont/test/bmfont-test.c index f46302dd..c9caed05 100644 --- a/libraries/framebuf/bmfont/test/bmfont-test.c +++ b/libraries/framebuf/bmfont/test/bmfont-test.c @@ -753,7 +753,7 @@ result_t bmfont_test_one_format(const char *resources, goto Failure; } - bitmap_init(&state.bm, (size2d_t) { state.scr_width, state.scr_height }, + bitmap_init(&state.bm, SIZE2D(state.scr_width, state.scr_height), scr_fmt, scr_rowbytes, state.palette, diff --git a/libraries/framebuf/composite/test/composite-test.c b/libraries/framebuf/composite/test/composite-test.c index 58d9bb45..fdb0fe95 100644 --- a/libraries/framebuf/composite/test/composite-test.c +++ b/libraries/framebuf/composite/test/composite-test.c @@ -210,7 +210,7 @@ result_t composite_test(const char *resources) goto Failure; } - bitmap_init(&bigbitmap, (size2d_t) { WIDTH, HEIGHT }, FORMAT, scr_rowbytes, NULL, bigpixels); + bitmap_init(&bigbitmap, SIZE2D(WIDTH, HEIGHT), FORMAT, scr_rowbytes, NULL, bigpixels); rc = load_test_png(&bm[0], resources, "A"); /* source */ if (rc) diff --git a/libraries/framebuf/curve/test/curve-test.c b/libraries/framebuf/curve/test/curve-test.c index 31e83507..5cca0837 100644 --- a/libraries/framebuf/curve/test/curve-test.c +++ b/libraries/framebuf/curve/test/curve-test.c @@ -804,7 +804,7 @@ result_t curve_test_one_format(const char *resources, goto Failure; } - bitmap_init(&state.bm, (size2d_t) { state.scr_width, state.scr_height }, + bitmap_init(&state.bm, SIZE2D(state.scr_width, state.scr_height), scr_fmt, scr_rowbytes, state.palette, diff --git a/libraries/framebuf/screen/screen-draw.c b/libraries/framebuf/screen/screen-draw.c index afff1d28..50c9b453 100644 --- a/libraries/framebuf/screen/screen-draw.c +++ b/libraries/framebuf/screen/screen-draw.c @@ -214,7 +214,7 @@ void screen_draw_rect(screen_t *scr, void screen_draw_square(screen_t *scr, int x, int y, int size, colour_t colour) { - screen_draw_rect(scr, x, y, (size2d_t) { size, size }, colour); + screen_draw_rect(scr, x, y, SIZE2D(size, size), colour); } /* ----------------------------------------------------------------------- */ diff --git a/libraries/framebuf/screen/test/screen-test.c b/libraries/framebuf/screen/test/screen-test.c index 5b59876f..fb8cd919 100644 --- a/libraries/framebuf/screen/test/screen-test.c +++ b/libraries/framebuf/screen/test/screen-test.c @@ -85,7 +85,7 @@ static void testscreen_init(testscreen_t *ts) for (i = 0; i < WIDTH * HEIGHT; i++) ts->pixels[i] = BACKGROUND; - screen_init(&ts->scr, (size2d_t) { WIDTH, HEIGHT }, + screen_init(&ts->scr, SIZE2D(WIDTH, HEIGHT), pixelfmt_bgrx8888, WIDTH * (int) sizeof(ts->pixels[0]), NULL, diff --git a/libraries/wuss/furniture/drag-resize.c b/libraries/wuss/furniture/drag-resize.c index 61c2f93d..21773b94 100644 --- a/libraries/wuss/furniture/drag-resize.c +++ b/libraries/wuss/furniture/drag-resize.c @@ -18,5 +18,5 @@ void wuss__furniture_drag_resize(wuss_window_t *window, point_t p) width = CLAMP(width, min.w, MAX(window->doc.w, min.w)); height = CLAMP(height, min.h, MAX(window->doc.h, min.h)); - wuss_window_resize(window, (size2d_t) { width, height }); + wuss_window_resize(window, SIZE2D(width, height)); } diff --git a/libraries/wuss/furniture/draw.c b/libraries/wuss/furniture/draw.c index bc4dc31b..abd8968d 100644 --- a/libraries/wuss/furniture/draw.c +++ b/libraries/wuss/furniture/draw.c @@ -248,9 +248,9 @@ void wuss__furniture_draw(wuss_t *wuss, border = wuss->palette[wuss->furniture_colours.title.bg]; /* no dedicated outline class; matches titlebar chrome */ wuss->scr->clip = visible_clipped; - screen_draw_rect(wuss->scr, window->visible.x0, window->visible.y0, (size2d_t) { width, 1 }, border); - screen_draw_rect(wuss->scr, window->visible.x0, window->visible.y1 - 1, (size2d_t) { width, 1 }, border); - screen_draw_rect(wuss->scr, window->visible.x0, window->visible.y0, (size2d_t) { 1, height }, border); - screen_draw_rect(wuss->scr, window->visible.x1 - 1, window->visible.y0, (size2d_t) { 1, height }, border); + screen_draw_rect(wuss->scr, window->visible.x0, window->visible.y0, SIZE2D(width, 1), border); + screen_draw_rect(wuss->scr, window->visible.x0, window->visible.y1 - 1, SIZE2D(width, 1), border); + screen_draw_rect(wuss->scr, window->visible.x0, window->visible.y0, SIZE2D(1, height), border); + screen_draw_rect(wuss->scr, window->visible.x1 - 1, window->visible.y0, SIZE2D(1, height), border); } } diff --git a/libraries/wuss/furniture/scroll-action.c b/libraries/wuss/furniture/scroll-action.c index 9b9edf54..903b7648 100644 --- a/libraries/wuss/furniture/scroll-action.c +++ b/libraries/wuss/furniture/scroll-action.c @@ -78,7 +78,7 @@ void wuss__furniture_drag_sausage(wuss_window_t *window, new_scroll = max_scroll; if (horizontal) - wuss_window_set_scroll(window, (point_t) { new_scroll, window->scroll.y }); + wuss_window_set_scroll(window, POINT(new_scroll, window->scroll.y)); else - wuss_window_set_scroll(window, (point_t) { window->scroll.x, new_scroll }); + wuss_window_set_scroll(window, POINT(window->scroll.x, new_scroll)); } diff --git a/libraries/wuss/furniture/toggle-action.c b/libraries/wuss/furniture/toggle-action.c index 4d6a6fcd..0fda6c67 100644 --- a/libraries/wuss/furniture/toggle-action.c +++ b/libraries/wuss/furniture/toggle-action.c @@ -85,7 +85,7 @@ void wuss__furniture_toggle_size(wuss_window_t *window) if (!(window->flags & wuss_WINDOW_NO_RESIZE_BLIT) && window->wuss->z_order.next == &window->link && screen_copy_rect(window->wuss->scr, &before, - (point_t) { before.x0, before.y0 }, &copied)) + POINT(before.x0, before.y0), &copied)) { /* Topmost, and the screen format supports the blit: the window's * top-left never moves for a toggle, so re-blitting "before" onto diff --git a/libraries/wuss/mouse-click.c b/libraries/wuss/mouse-click.c index b7bee4be..a68c9bb0 100644 --- a/libraries/wuss/mouse-click.c +++ b/libraries/wuss/mouse-click.c @@ -34,7 +34,7 @@ result_t wuss_mouse_click(wuss_t *wuss, if (win == NULL) return result_OK; - region = wuss__furniture_hit_test(win, (point_t) { x, y }); + region = wuss__furniture_hit_test(win, POINT(x, y)); if (region == wuss_FURNITURE_CLOSE && action == wuss_MOUSE_DOWN && @@ -82,16 +82,16 @@ result_t wuss_mouse_click(wuss_t *wuss, wuss__furniture_toggle_size(win); break; case wuss_FURNITURE_VSCROLL_UP: - wuss__furniture_scroll_step(win, (point_t) { 0, -step }); + wuss__furniture_scroll_step(win, POINT(0, -step)); break; case wuss_FURNITURE_VSCROLL_DOWN: - wuss__furniture_scroll_step(win, (point_t) { 0, step }); + wuss__furniture_scroll_step(win, POINT(0, step)); break; case wuss_FURNITURE_HSCROLL_LEFT: - wuss__furniture_scroll_step(win, (point_t) { -step, 0 }); + wuss__furniture_scroll_step(win, POINT(-step, 0)); break; case wuss_FURNITURE_HSCROLL_RIGHT: - wuss__furniture_scroll_step(win, (point_t) { step, 0 }); + wuss__furniture_scroll_step(win, POINT(step, 0)); break; default: break; diff --git a/libraries/wuss/mouse-move.c b/libraries/wuss/mouse-move.c index 859bd5c5..9b73076d 100644 --- a/libraries/wuss/mouse-move.c +++ b/libraries/wuss/mouse-move.c @@ -19,7 +19,7 @@ result_t wuss_mouse_move(wuss_t *wuss, point_t p, wuss_window_t **hit) switch (wuss->furniture.drag_kind) { case wuss_FURNITURE_DRAG_RESIZE: - wuss__furniture_drag_resize(win, (point_t) { x, y }); + wuss__furniture_drag_resize(win, POINT(x, y)); break; case wuss_FURNITURE_DRAG_VSCROLL_SAUSAGE: @@ -32,7 +32,7 @@ result_t wuss_mouse_move(wuss_t *wuss, point_t p, wuss_window_t **hit) case wuss_FURNITURE_DRAG_MOVE: default: - wuss_window_move(win, (point_t) { x - wuss->furniture.drag.x, y - wuss->furniture.drag.y }); + wuss_window_move(win, POINT(x - wuss->furniture.drag.x, y - wuss->furniture.drag.y)); break; } @@ -46,7 +46,7 @@ result_t wuss_mouse_move(wuss_t *wuss, point_t p, wuss_window_t **hit) if (win == NULL) return result_OK; - if (wuss__furniture_hit_test(win, (point_t) { x, y }) != wuss_FURNITURE_CONTENT) + if (wuss__furniture_hit_test(win, POINT(x, y)) != wuss_FURNITURE_CONTENT) return result_OK; if (win->task.handle != NULL) diff --git a/libraries/wuss/redraw.c b/libraries/wuss/redraw.c index e3f403a3..769257a9 100644 --- a/libraries/wuss/redraw.c +++ b/libraries/wuss/redraw.c @@ -128,7 +128,7 @@ result_t wuss_redraw_dirty(wuss_t *wuss) { wuss->scr->clip = wuss->dirty[i]; screen_draw_rect(wuss->scr, - wuss->dirty[i].x0, wuss->dirty[i].y0, (size2d_t) { wuss->dirty[i].x1 - wuss->dirty[i].x0, wuss->dirty[i].y1 - wuss->dirty[i].y0 }, + wuss->dirty[i].x0, wuss->dirty[i].y0, SIZE2D(wuss->dirty[i].x1 - wuss->dirty[i].x0, wuss->dirty[i].y1 - wuss->dirty[i].y0), wuss->palette[wuss->backdrop]); } diff --git a/libraries/wuss/scroll.c b/libraries/wuss/scroll.c index f4d0979a..7be356e7 100644 --- a/libraries/wuss/scroll.c +++ b/libraries/wuss/scroll.c @@ -36,12 +36,12 @@ result_t wuss_scroll(wuss_t *wuss, point_t p, int delta, wuss_window_t **hit) event.data.scroll.point.y = y - content.y0 + win->scroll.y; event.data.scroll.delta = delta; - wuss__furniture_scroll_step(win, (point_t) { 0, delta }); + wuss__furniture_scroll_step(win, POINT(0, delta)); return win->task.handle(win, &event, win->task.task_data); } - wuss__furniture_scroll_step(win, (point_t) { 0, delta }); + wuss__furniture_scroll_step(win, POINT(0, delta)); return result_OK; } diff --git a/libraries/wuss/test/tasks/ball.c b/libraries/wuss/test/tasks/ball.c index 9e41f4ac..8d5fa59b 100644 --- a/libraries/wuss/test/tasks/ball.c +++ b/libraries/wuss/test/tasks/ball.c @@ -37,7 +37,7 @@ result_t ball_create(wuss_t *wuss, const colour_t *palette, ball_task_t *task) wuss_NO_BACKGROUND, &delegate, box_size(&box), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &task->window); } @@ -70,7 +70,7 @@ static result_t ball_redraw(const wuss_event_t *event, void *task_data) b = &bc->balls[i]; - screen_draw_rect(scr, bounds->x0 - sx + b->x - b->radius, bounds->y0 - sy + b->y - b->radius, (size2d_t) { b->radius * 2, b->radius * 2 }, bc->ball); + screen_draw_rect(scr, bounds->x0 - sx + b->x - b->radius, bounds->y0 - sy + b->y - b->radius, SIZE2D(b->radius * 2, b->radius * 2), bc->ball); } return result_OK; diff --git a/libraries/wuss/test/tasks/blank.c b/libraries/wuss/test/tasks/blank.c index 470a8817..344a59d3 100644 --- a/libraries/wuss/test/tasks/blank.c +++ b/libraries/wuss/test/tasks/blank.c @@ -34,7 +34,7 @@ result_t blank_create(wuss_t *wuss, int npalette, blank_task_t *task) palette_PICO8_GREEN, &delegate, box_size(&box), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &task->window); } diff --git a/libraries/wuss/test/tasks/chars.c b/libraries/wuss/test/tasks/chars.c index 3cc75d2a..6f2d6603 100644 --- a/libraries/wuss/test/tasks/chars.c +++ b/libraries/wuss/test/tasks/chars.c @@ -49,14 +49,14 @@ result_t chars_create(wuss_t *wuss, return wuss_window_create(wuss, &box, "Chars", - wuss_WINDOW_NO_RESIZE | + wuss_WINDOW_NO_RESIZE | wuss_WINDOW_NO_TOGGLE_SIZE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL, wuss_NO_BACKGROUND, &delegate, box_size(&box), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &task->window); } @@ -105,7 +105,7 @@ static result_t chars_redraw(const wuss_event_t *event, void *task_data) x = bounds->x0 - sx + col * cell_w; y = bounds->y0 - sy + row * cell_h; - screen_draw_rect(scr, x, y, (size2d_t) { cell_w, cell_h }, cc->bg); + screen_draw_rect(scr, x, y, SIZE2D(cell_w, cell_h), cc->bg); if (i < first || i >= first + count) continue; /* no glyph for this byte value: leave the cell blank */ @@ -121,7 +121,7 @@ static result_t chars_redraw(const wuss_event_t *event, void *task_data) result_t chars_handle(wuss_window_t *window, const wuss_event_t *event, - void *task_data) + void *task_data) { if (event->kind == wuss_EVENT_CLOSE) { diff --git a/libraries/wuss/test/tasks/checker.c b/libraries/wuss/test/tasks/checker.c index 71a22ae6..d61baa56 100644 --- a/libraries/wuss/test/tasks/checker.c +++ b/libraries/wuss/test/tasks/checker.c @@ -41,7 +41,7 @@ result_t checker_create(wuss_t *wuss, wuss_NO_BACKGROUND, &delegate, box_size(&box), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &task->window); if (rc != result_OK) return rc; @@ -55,7 +55,7 @@ result_t checker_create(wuss_t *wuss, wuss_NO_BACKGROUND, &delegate, box_size(&box), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &task->window2); if (rc != result_OK) { diff --git a/libraries/wuss/test/tasks/curve.c b/libraries/wuss/test/tasks/curve.c index 9fc73ef6..24572802 100644 --- a/libraries/wuss/test/tasks/curve.c +++ b/libraries/wuss/test/tasks/curve.c @@ -34,10 +34,10 @@ result_t curve_create(wuss_t *wuss, task->nsegments = CURVE_SEGMENTS_DEFAULT; task->dragging = -1; - task->points[0] = (point_t) { 10, 10 }; - task->points[1] = (point_t) { 10, 140 }; - task->points[2] = (point_t) { 210, 10 }; - task->points[3] = (point_t) { 210, 140 }; + task->points[0] = POINT(10, 10); + task->points[1] = POINT(10, 140); + task->points[2] = POINT(210, 10); + task->points[3] = POINT(210, 140); delegate = wuss_task_start(curve_handle, task); /* curve_redraw paints its own background */ box = (box_t) BOX_POS_SIZE(20, 260, 220, 160); @@ -49,7 +49,7 @@ result_t curve_create(wuss_t *wuss, wuss_NO_BACKGROUND, &delegate, box_size(&box), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &task->window); } diff --git a/libraries/wuss/test/tasks/gradient.c b/libraries/wuss/test/tasks/gradient.c index 6e171fa6..491e8460 100644 --- a/libraries/wuss/test/tasks/gradient.c +++ b/libraries/wuss/test/tasks/gradient.c @@ -45,8 +45,8 @@ result_t gradient_create(wuss_t *wuss, gradient_task_t *task) wuss_WINDOW_NONE, wuss_NO_BACKGROUND, &delegate, - (size2d_t) { GRADIENT_DOC_WIDTH, GRADIENT_DOC_HEIGHT }, - (size2d_t) { 0, 0 }, + SIZE2D(GRADIENT_DOC_WIDTH, GRADIENT_DOC_HEIGHT), + SIZE2D(0, 0), &task->window); } diff --git a/libraries/wuss/test/tasks/image.c b/libraries/wuss/test/tasks/image.c index a804b49b..22d5d801 100644 --- a/libraries/wuss/test/tasks/image.c +++ b/libraries/wuss/test/tasks/image.c @@ -42,8 +42,8 @@ result_t image_create(wuss_t *wuss, wuss_WINDOW_NONE, palette_PICO8_BLACK, &delegate, - (size2d_t) { task->bitmap.size.w, task->bitmap.size.h }, - (size2d_t) { 0, 0 }, + SIZE2D(task->bitmap.size.w, task->bitmap.size.h), + SIZE2D(0, 0), &task->window); } diff --git a/libraries/wuss/test/tasks/launcher.c b/libraries/wuss/test/tasks/launcher.c index ec779f4b..3e3e93aa 100644 --- a/libraries/wuss/test/tasks/launcher.c +++ b/libraries/wuss/test/tasks/launcher.c @@ -50,7 +50,7 @@ result_t launcher_create(wuss_t *wuss, wuss_NO_BACKGROUND, &delegate, box_size(&box), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &task->window); } diff --git a/libraries/wuss/test/tasks/palette.c b/libraries/wuss/test/tasks/palette.c index 0c781b41..0ba14831 100644 --- a/libraries/wuss/test/tasks/palette.c +++ b/libraries/wuss/test/tasks/palette.c @@ -33,7 +33,7 @@ result_t palette_create(wuss_t *wuss, palette_PICO8_BLACK, &delegate, box_size(&box), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &task->window); } @@ -78,7 +78,7 @@ static result_t palette_redraw(const wuss_event_t *event, void *task_data) x = bounds->x0 - sx + col * cell_w; y = bounds->y0 - sy + row * cell_h; - screen_draw_rect(scr, x, y, (size2d_t) { cell_w, cell_h }, pc->palette[i]); + screen_draw_rect(scr, x, y, SIZE2D(cell_w, cell_h), pc->palette[i]); } return result_OK; diff --git a/libraries/wuss/test/tasks/porter-duff.c b/libraries/wuss/test/tasks/porter-duff.c index 82cc8e95..8ff10b92 100644 --- a/libraries/wuss/test/tasks/porter-duff.c +++ b/libraries/wuss/test/tasks/porter-duff.c @@ -201,7 +201,7 @@ result_t porter_duff_create(wuss_t *wuss, wuss_NO_BACKGROUND, &delegate, box_size(&box), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &task->window); if (rc != result_OK) goto free_dst; diff --git a/libraries/wuss/test/tasks/sofa.c b/libraries/wuss/test/tasks/sofa.c index 8d25a6da..c006691e 100644 --- a/libraries/wuss/test/tasks/sofa.c +++ b/libraries/wuss/test/tasks/sofa.c @@ -278,7 +278,7 @@ result_t sofa_create(wuss_t *wuss, const colour_t *palette, sofa_task_t *task) wuss_NO_BACKGROUND, &delegate, box_size(&box), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &task->window); } diff --git a/libraries/wuss/test/tasks/text.c b/libraries/wuss/test/tasks/text.c index 41fc2e4f..c96d9274 100644 --- a/libraries/wuss/test/tasks/text.c +++ b/libraries/wuss/test/tasks/text.c @@ -54,7 +54,7 @@ result_t text_create(wuss_t *wuss, palette_PICO8_BLUE, &delegate, box_size(&box), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &task->window); return rc; @@ -156,7 +156,7 @@ static result_t text_idle(void *task_data) angle = tcx->frame_count * (2.0 * M_PI / TEXT_RESIZE_PERIOD_FRAMES); width = tcx->base_width + (int) (TEXT_RESIZE_AMPLITUDE * sin(angle)); - rc = wuss_window_resize(tcx->window, (size2d_t) { width, height }); + rc = wuss_window_resize(tcx->window, SIZE2D(width, height)); if (rc != result_OK) logf_warning("text_idle: wuss_window_resize(%d, %d) failed", width, height); diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index c88a7e0c..686cd217 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -191,9 +191,9 @@ static result_t wuss_interactive_test(const char *resources) goto Failure; #if WUSS_TEST_32BPP - rc = bitmap_init(&bm, (size2d_t) { scr_width, scr_height }, pixelfmt_bgrx8888, rowbytes, palette, pixels); + rc = bitmap_init(&bm, SIZE2D(scr_width, scr_height), pixelfmt_bgrx8888, rowbytes, palette, pixels); #else - rc = bitmap_init(&bm, (size2d_t) { scr_width, scr_height }, pixelfmt_p4, rowbytes, palette, pixels); + rc = bitmap_init(&bm, SIZE2D(scr_width, scr_height), pixelfmt_p4, rowbytes, palette, pixels); #endif if (rc != result_OK) goto Failure; @@ -308,7 +308,7 @@ static result_t wuss_interactive_test(const char *resources) int x, y; sdl_pos_to_scr(window, scr_width, scr_height, event.button.x, event.button.y, &x, &y); - wuss_mouse_click(wuss, (point_t) { x, y }, sdl_button_to_wuss(event.button.button), wuss_MOUSE_DOWN, NULL); + wuss_mouse_click(wuss, POINT(x, y), sdl_button_to_wuss(event.button.button), wuss_MOUSE_DOWN, NULL); } break; @@ -317,7 +317,7 @@ static result_t wuss_interactive_test(const char *resources) int x, y; sdl_pos_to_scr(window, scr_width, scr_height, event.button.x, event.button.y, &x, &y); - wuss_mouse_click(wuss, (point_t) { x, y }, sdl_button_to_wuss(event.button.button), wuss_MOUSE_UP, NULL); + wuss_mouse_click(wuss, POINT(x, y), sdl_button_to_wuss(event.button.button), wuss_MOUSE_UP, NULL); } break; @@ -326,7 +326,7 @@ static result_t wuss_interactive_test(const char *resources) int x, y; sdl_pos_to_scr(window, scr_width, scr_height, event.motion.x, event.motion.y, &x, &y); - wuss_mouse_move(wuss, (point_t) { x, y }, NULL); + wuss_mouse_move(wuss, POINT(x, y), NULL); } break; @@ -335,7 +335,7 @@ static result_t wuss_interactive_test(const char *resources) int x, y; sdl_pos_to_scr(window, scr_width, scr_height, event.wheel.mouse_x, event.wheel.mouse_y, &x, &y); - wuss_scroll(wuss, (point_t) { x, y }, (int) event.wheel.y, NULL); + wuss_scroll(wuss, POINT(x, y), (int) event.wheel.y, NULL); } break; @@ -549,8 +549,8 @@ result_t wuss_test(const char *resources) wuss_t *wuss; wuss_config_t bad_config; wuss_t *bad_wuss; - test_task_t tc_a, tc_b, tc_c, tc_d; - wuss_task_t delegate_a, delegate_b, delegate_c, delegate_d; + test_task_t tc_a, tc_b, tc_c, tc_d; + wuss_task_t delegate_a, delegate_b, delegate_c, delegate_d; box_t box_a, box_b, box_c, box_d; wuss_window_t *win_a, *win_b, *win_c, *win_d; wuss_window_t *hit; @@ -566,7 +566,7 @@ result_t wuss_test(const char *resources) if (pixels == NULL) goto Failure; - rc = bitmap_init(&bm, (size2d_t) { 200, 200 }, pixelfmt_bgrx8888, rowbytes, NULL, pixels); + rc = bitmap_init(&bm, SIZE2D(200, 200), pixelfmt_bgrx8888, rowbytes, NULL, pixels); if (rc != result_OK) goto Failure; @@ -618,7 +618,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, NULL, box_size(&box_a), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &win_a); if (rc != result_WUSS_TOO_SMALL) goto Failure; @@ -628,7 +628,7 @@ result_t wuss_test(const char *resources) tc_a.redraw_count = 0; tc_a.mouse_count = 0; tc_a.open_count = 0; - delegate_a.handle = test_handle; + delegate_a.handle = test_handle; delegate_a.task_data = &tc_a; box_a.x0 = 0; @@ -644,14 +644,14 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_a, box_size(&box_a), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &win_a); if (rc != result_OK) goto Failure; tc_b.redraw_count = 0; tc_b.mouse_count = 0; - delegate_b.handle = test_handle; + delegate_b.handle = test_handle; delegate_b.task_data = &tc_b; box_b.x0 = 50; @@ -667,7 +667,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_b, box_size(&box_b), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &win_b); if (rc != result_OK) goto Failure; @@ -736,7 +736,7 @@ result_t wuss_test(const char *resources) printf("test: z-order hit test and local coordinate translation (B on top)\n"); tc_b.mouse_count = 0; - rc = wuss_mouse_click(wuss, (point_t) { 75, 75 }, wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); + rc = wuss_mouse_click(wuss, POINT(75, 75), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); if (rc != result_OK) goto Failure; if (hit != win_b) @@ -746,14 +746,14 @@ result_t wuss_test(const char *resources) if (tc_b.last_action != wuss_MOUSE_DOWN || tc_b.last_x != 25 || tc_b.last_y != 25) goto Failure; - rc = wuss_mouse_click(wuss, (point_t) { 75, 75 }, wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); + rc = wuss_mouse_click(wuss, POINT(75, 75), wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); if (rc != result_OK) goto Failure; printf("test: clicking a window's close icon sends wuss_EVENT_CLOSE, not a drag\n"); tc_a.close_count = 0; - rc = wuss_mouse_click(wuss, (point_t) { 6, 11 }, wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* A's close icon */ + rc = wuss_mouse_click(wuss, POINT(6, 11), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* A's close icon */ if (rc != result_OK) goto Failure; if (hit != win_a) @@ -761,7 +761,7 @@ result_t wuss_test(const char *resources) if (tc_a.close_count != 1) goto Failure; - rc = wuss_mouse_click(wuss, (point_t) { 31, 36 }, wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); /* if the close click had started a drag, this would move A */ + rc = wuss_mouse_click(wuss, POINT(31, 36), wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); /* if the close click had started a drag, this would move A */ if (rc != result_OK) goto Failure; @@ -772,20 +772,20 @@ result_t wuss_test(const char *resources) printf("test: click-to-front changes subsequent overlap hits\n"); tc_a.mouse_count = 0; - rc = wuss_mouse_click(wuss, (point_t) { 31, 11 }, wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* A's titlebar, above its content, clear of the close icon */ + rc = wuss_mouse_click(wuss, POINT(31, 11), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* A's titlebar, above its content, clear of the close icon */ if (rc != result_OK) goto Failure; if (hit != win_a) goto Failure; - rc = wuss_mouse_click(wuss, (point_t) { 31, 11 }, wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); + rc = wuss_mouse_click(wuss, POINT(31, 11), wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); if (rc != result_OK) goto Failure; printf("test: content click does not change z-order\n"); tc_b.mouse_count = 0; - rc = wuss_mouse_click(wuss, (point_t) { 120, 120 }, wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* B's content, only within B */ + rc = wuss_mouse_click(wuss, POINT(120, 120), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* B's content, only within B */ if (rc != result_OK) goto Failure; if (hit != win_b) @@ -793,12 +793,12 @@ result_t wuss_test(const char *resources) if (tc_b.mouse_count != 1) goto Failure; - rc = wuss_mouse_click(wuss, (point_t) { 120, 120 }, wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); + rc = wuss_mouse_click(wuss, POINT(120, 120), wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); if (rc != result_OK) goto Failure; tc_a.mouse_count = 0; - rc = wuss_mouse_click(wuss, (point_t) { 75, 75 }, wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* A still topmost: B's content click above didn't bring it to front */ + rc = wuss_mouse_click(wuss, POINT(75, 75), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* A still topmost: B's content click above didn't bring it to front */ if (rc != result_OK) goto Failure; if (hit != win_a) @@ -806,14 +806,14 @@ result_t wuss_test(const char *resources) if (tc_a.mouse_count != 1 || tc_a.last_x != 74 || tc_a.last_y != 54) goto Failure; - rc = wuss_mouse_click(wuss, (point_t) { 75, 75 }, wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); + rc = wuss_mouse_click(wuss, POINT(75, 75), wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); if (rc != result_OK) goto Failure; printf("test: titlebar click starts a drag, not delivered as content\n"); tc_a.mouse_count = 0; - rc = wuss_mouse_click(wuss, (point_t) { 31, 11 }, wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* A's titlebar, A already topmost, clear of the close icon */ + rc = wuss_mouse_click(wuss, POINT(31, 11), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* A's titlebar, A already topmost, clear of the close icon */ if (rc != result_OK) goto Failure; if (hit != win_a) @@ -829,7 +829,7 @@ result_t wuss_test(const char *resources) before_a = tc_a.redraw_count; before_b = tc_b.redraw_count; - rc = wuss_mouse_move(wuss, (point_t) { 31, 36 }, &hit); + rc = wuss_mouse_move(wuss, POINT(31, 36), &hit); if (rc != result_OK) goto Failure; if (hit != win_a) @@ -854,7 +854,7 @@ result_t wuss_test(const char *resources) printf("test: mouse-up ends the drag\n"); - rc = wuss_mouse_click(wuss, (point_t) { 31, 36 }, wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); + rc = wuss_mouse_click(wuss, POINT(31, 36), wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); if (rc != result_OK) goto Failure; if (hit != win_a) @@ -862,13 +862,13 @@ result_t wuss_test(const char *resources) printf("test: Adjust-drag moves a window without bringing it to front\n"); - rc = wuss_mouse_click(wuss, (point_t) { 140, 35 }, wuss_BUTTON_ADJUST, wuss_MOUSE_DOWN, &hit); /* B's titlebar, clear of A */ + rc = wuss_mouse_click(wuss, POINT(140, 35), wuss_BUTTON_ADJUST, wuss_MOUSE_DOWN, &hit); /* B's titlebar, clear of A */ if (rc != result_OK) goto Failure; if (hit != win_b) goto Failure; - rc = wuss_mouse_move(wuss, (point_t) { 145, 60 }, &hit); + rc = wuss_mouse_move(wuss, POINT(145, 60), &hit); if (rc != result_OK) goto Failure; if (hit != win_b) @@ -878,23 +878,23 @@ result_t wuss_test(const char *resources) if (visible.x0 != 54 || visible.y0 != 54) goto Failure; - rc = wuss_mouse_click(wuss, (point_t) { 145, 60 }, wuss_BUTTON_ADJUST, wuss_MOUSE_UP, &hit); + rc = wuss_mouse_click(wuss, POINT(145, 60), wuss_BUTTON_ADJUST, wuss_MOUSE_UP, &hit); if (rc != result_OK) goto Failure; if (hit != win_b) goto Failure; - rc = wuss_mouse_click(wuss, (point_t) { 75, 75 }, wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* within both A and B; A still topmost */ + rc = wuss_mouse_click(wuss, POINT(75, 75), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* within both A and B; A still topmost */ if (rc != result_OK) goto Failure; if (hit != win_a) goto Failure; - rc = wuss_mouse_click(wuss, (point_t) { 75, 75 }, wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); + rc = wuss_mouse_click(wuss, POINT(75, 75), wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); if (rc != result_OK) goto Failure; - rc = wuss_mouse_move(wuss, (point_t) { 200, 200 }, &hit); /* off all windows, drag must have ended */ + rc = wuss_mouse_move(wuss, POINT(200, 200), &hit); /* off all windows, drag must have ended */ if (rc != result_OK) goto Failure; if (hit != NULL) @@ -909,13 +909,13 @@ result_t wuss_test(const char *resources) printf("test: window_resize valid and too-small cases\n"); - rc = wuss_window_resize(win_a, (size2d_t) { 50, 0 }); /* zero-height content is invalid */ + rc = wuss_window_resize(win_a, SIZE2D(50, 0)); /* zero-height content is invalid */ if (rc != result_WUSS_TOO_SMALL) goto Failure; if (tc_a.open_count != 1) goto Failure; /* rejected resize: no wuss_EVENT_OPEN */ - rc = wuss_window_resize(win_a, (size2d_t) { 50, 50 }); + rc = wuss_window_resize(win_a, SIZE2D(50, 50)); if (rc != result_OK) goto Failure; if (tc_a.open_count != 2) @@ -953,7 +953,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_d, box_size(&box_d), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &win_d); if (rc != result_OK) goto Failure; @@ -965,7 +965,7 @@ result_t wuss_test(const char *resources) printf("test: click within a title-less window's top edge is delivered as content, not a drag\n"); - rc = wuss_mouse_click(wuss, (point_t) { 5, 165 }, wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); + rc = wuss_mouse_click(wuss, POINT(5, 165), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); if (rc != result_OK) goto Failure; if (hit != win_d) @@ -973,21 +973,21 @@ result_t wuss_test(const char *resources) if (tc_d.mouse_count != 1 || tc_d.last_action != wuss_MOUSE_DOWN || tc_d.last_x != 5 || tc_d.last_y != 5) goto Failure; - rc = wuss_mouse_click(wuss, (point_t) { 5, 165 }, wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); + rc = wuss_mouse_click(wuss, POINT(5, 165), wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); if (rc != result_OK) goto Failure; printf("test: content click on a title-less window does not change z-order\n"); { - test_task_t tc_e, tc_f; - wuss_task_t delegate_e, delegate_f; + test_task_t tc_e, tc_f; + wuss_task_t delegate_e, delegate_f; box_t box_e, box_f; wuss_window_t *win_e, *win_f; tc_e.redraw_count = 0; tc_e.mouse_count = 0; - delegate_e.handle = test_handle; + delegate_e.handle = test_handle; delegate_e.task_data = &tc_e; box_e.x0 = 100; box_e.y0 = 0; @@ -1001,14 +1001,14 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_e, box_size(&box_e), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &win_e); if (rc != result_OK) goto Failure; tc_f.redraw_count = 0; tc_f.mouse_count = 0; - delegate_f.handle = test_handle; + delegate_f.handle = test_handle; delegate_f.task_data = &tc_f; box_f.x0 = 130; box_f.y0 = 20; @@ -1022,30 +1022,30 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_f, box_size(&box_f), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &win_f); if (rc != result_OK) goto Failure; /* F was created after E, so F is topmost; clicking E's exposed content * (outside the overlap) is delivered to E but must not raise it */ - rc = wuss_mouse_click(wuss, (point_t) { 110, 10 }, wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* within E only */ + rc = wuss_mouse_click(wuss, POINT(110, 10), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* within E only */ if (rc != result_OK) goto Failure; if (hit != win_e) goto Failure; - rc = wuss_mouse_click(wuss, (point_t) { 110, 10 }, wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); + rc = wuss_mouse_click(wuss, POINT(110, 10), wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); if (rc != result_OK) goto Failure; - rc = wuss_mouse_click(wuss, (point_t) { 135, 25 }, wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* overlap: F still on top */ + rc = wuss_mouse_click(wuss, POINT(135, 25), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* overlap: F still on top */ if (rc != result_OK) goto Failure; if (hit != win_f) goto Failure; - rc = wuss_mouse_click(wuss, (point_t) { 135, 25 }, wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); + rc = wuss_mouse_click(wuss, POINT(135, 25), wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); if (rc != result_OK) goto Failure; @@ -1068,15 +1068,15 @@ result_t wuss_test(const char *resources) printf("test: moving/resizing a window entirely behind an occluder has no visible effect\n"); { - test_task_t tc_h, tc_g; - wuss_task_t delegate_h, delegate_g; + test_task_t tc_h, tc_g; + wuss_task_t delegate_h, delegate_g; box_t box_h, box_g; wuss_window_t *win_h, *win_g; - int before_h, before_g; + int before_h, before_g; tc_h.redraw_count = 0; tc_h.mouse_count = 0; - delegate_h.handle = test_handle; + delegate_h.handle = test_handle; delegate_h.task_data = &tc_h; box_h.x0 = 10; box_h.y0 = 10; @@ -1090,14 +1090,14 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_h, box_size(&box_h), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &win_h); if (rc != result_OK) goto Failure; tc_g.redraw_count = 0; tc_g.mouse_count = 0; - delegate_g.handle = test_handle; + delegate_g.handle = test_handle; delegate_g.task_data = &tc_g; box_g.x0 = 0; box_g.y0 = 0; @@ -1111,7 +1111,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_g, box_size(&box_g), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &win_g); if (rc != result_OK) goto Failure; @@ -1123,7 +1123,7 @@ result_t wuss_test(const char *resources) before_h = tc_h.redraw_count; before_g = tc_g.redraw_count; - wuss_window_move(win_h, (point_t) { 60, 60 }); /* still entirely within G's footprint */ + wuss_window_move(win_h, POINT(60, 60)); /* still entirely within G's footprint */ if (wuss_get_dirty_count(wuss) != 0) goto Failure; @@ -1133,7 +1133,7 @@ result_t wuss_test(const char *resources) if (tc_h.redraw_count != before_h || tc_g.redraw_count != before_g) goto Failure; /* nothing visible changed: no redraw of either window */ - rc = wuss_window_resize(win_h, (size2d_t) { 25, 25 }); /* still entirely within G's footprint */ + rc = wuss_window_resize(win_h, SIZE2D(25, 25)); /* still entirely within G's footprint */ if (rc != result_OK) goto Failure; if (wuss_get_dirty_count(wuss) != 0) @@ -1152,14 +1152,14 @@ result_t wuss_test(const char *resources) printf("test: bring-to-front only invalidates the newly-uncovered part\n"); { - test_task_t tc_i, tc_j; - wuss_task_t delegate_i, delegate_j; + test_task_t tc_i, tc_j; + wuss_task_t delegate_i, delegate_j; box_t box_i, box_j, dirty; wuss_window_t *win_i, *win_j; tc_i.redraw_count = 0; tc_i.mouse_count = 0; - delegate_i.handle = test_handle; + delegate_i.handle = test_handle; delegate_i.task_data = &tc_i; box_i.x0 = 0; box_i.y0 = 0; @@ -1173,14 +1173,14 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_i, box_size(&box_i), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &win_i); if (rc != result_OK) goto Failure; tc_j.redraw_count = 0; tc_j.mouse_count = 0; - delegate_j.handle = test_handle; + delegate_j.handle = test_handle; delegate_j.task_data = &tc_j; box_j.x0 = 50; box_j.y0 = 0; @@ -1194,7 +1194,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_j, box_size(&box_j), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &win_j); if (rc != result_OK) goto Failure; @@ -1223,15 +1223,15 @@ result_t wuss_test(const char *resources) printf("test: dragging off-screen and back on repaints the reappearing edge\n"); { - test_task_t tc_m; - wuss_task_t delegate_m; + test_task_t tc_m; + wuss_task_t delegate_m; box_t box_m; wuss_window_t *win_m; int before_m; tc_m.redraw_count = 0; tc_m.mouse_count = 0; - delegate_m.handle = test_handle; + delegate_m.handle = test_handle; delegate_m.task_data = &tc_m; box_m.x0 = 10; box_m.y0 = 10; @@ -1245,7 +1245,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_m, box_size(&box_m), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &win_m); if (rc != result_OK) goto Failure; @@ -1254,14 +1254,14 @@ result_t wuss_test(const char *resources) if (rc != result_OK) goto Failure; - wuss_window_move(win_m, (point_t) { -40, 10 }); /* slide left until half of M is off the left edge */ + wuss_window_move(win_m, POINT(-40, 10)); /* slide left until half of M is off the left edge */ rc = wuss_redraw_dirty(wuss); /* flush the vacated-sliver repaint from this move */ if (rc != result_OK) goto Failure; before_m = tc_m.redraw_count; - wuss_window_move(win_m, (point_t) { 10, 10 }); /* slide back: the part that re-enters the screen was + wuss_window_move(win_m, POINT(10, 10)); /* slide back: the part that re-enters the screen was * never blitted (its source pixels were off-screen), * so it must be a real task redraw, not a blit */ if (wuss_get_dirty_count(wuss) == 0) @@ -1298,7 +1298,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_h, box_size(&box_h), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &win_h); if (rc != result_OK) goto Failure; @@ -1317,7 +1317,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_g, box_size(&box_g), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &win_g); if (rc != result_OK) goto Failure; @@ -1326,53 +1326,53 @@ result_t wuss_test(const char *resources) * corner) never falls under H, so it stays clickable either way */ wuss_window_get_visible_bounds(win_g, &visible); - rc = wuss_mouse_click(wuss, (point_t) { 145, 65 }, wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* overlap of G and H */ + rc = wuss_mouse_click(wuss, POINT(145, 65), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* overlap of G and H */ if (rc != result_OK) goto Failure; if (hit != win_g) goto Failure; - rc = wuss_mouse_click(wuss, (point_t) { 145, 65 }, wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); + rc = wuss_mouse_click(wuss, POINT(145, 65), wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); if (rc != result_OK) goto Failure; - rc = wuss_mouse_click(wuss, (point_t) { visible.x0 + 5, visible.y0 + 5 }, wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* G's back icon */ + rc = wuss_mouse_click(wuss, POINT(visible.x0 + 5, visible.y0 + 5), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* G's back icon */ if (rc != result_OK) goto Failure; if (hit != win_g) goto Failure; - rc = wuss_mouse_click(wuss, (point_t) { visible.x0 + 5, visible.y0 + 5 }, wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); + rc = wuss_mouse_click(wuss, POINT(visible.x0 + 5, visible.y0 + 5), wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); if (rc != result_OK) goto Failure; - rc = wuss_mouse_click(wuss, (point_t) { 145, 65 }, wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* overlap: H now on top */ + rc = wuss_mouse_click(wuss, POINT(145, 65), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* overlap: H now on top */ if (rc != result_OK) goto Failure; if (hit != win_h) /* G was sent to back */ goto Failure; - rc = wuss_mouse_click(wuss, (point_t) { 145, 65 }, wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); + rc = wuss_mouse_click(wuss, POINT(145, 65), wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); if (rc != result_OK) goto Failure; - rc = wuss_mouse_click(wuss, (point_t) { visible.x0 + 5, visible.y0 + 5 }, wuss_BUTTON_ADJUST, wuss_MOUSE_DOWN, &hit); /* Adjust-click G's back icon */ + rc = wuss_mouse_click(wuss, POINT(visible.x0 + 5, visible.y0 + 5), wuss_BUTTON_ADJUST, wuss_MOUSE_DOWN, &hit); /* Adjust-click G's back icon */ if (rc != result_OK) goto Failure; if (hit != win_g) goto Failure; - rc = wuss_mouse_click(wuss, (point_t) { visible.x0 + 5, visible.y0 + 5 }, wuss_BUTTON_ADJUST, wuss_MOUSE_UP, &hit); + rc = wuss_mouse_click(wuss, POINT(visible.x0 + 5, visible.y0 + 5), wuss_BUTTON_ADJUST, wuss_MOUSE_UP, &hit); if (rc != result_OK) goto Failure; - rc = wuss_mouse_click(wuss, (point_t) { 145, 65 }, wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* overlap: G back on top */ + rc = wuss_mouse_click(wuss, POINT(145, 65), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* overlap: G back on top */ if (rc != result_OK) goto Failure; if (hit != win_g) /* Adjust-click on the back icon brought G back to front */ goto Failure; - rc = wuss_mouse_click(wuss, (point_t) { 145, 65 }, wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); + rc = wuss_mouse_click(wuss, POINT(145, 65), wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); if (rc != result_OK) goto Failure; @@ -1380,17 +1380,17 @@ result_t wuss_test(const char *resources) wuss_window_get_content_bounds(win_g, &content); /* G's doc_width/doc_height are 50x50, same as its initial content size */ - rc = wuss_mouse_click(wuss, (point_t) { visible.x1 - 3, visible.y1 - 3 }, wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* G's resize icon */ + rc = wuss_mouse_click(wuss, POINT(visible.x1 - 3, visible.y1 - 3), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* G's resize icon */ if (rc != result_OK) goto Failure; if (hit != win_g) goto Failure; - rc = wuss_mouse_move(wuss, (point_t) { content.x0 + 50, content.y0 + 50 }, &hit); /* drag to exactly the doc extent */ + rc = wuss_mouse_move(wuss, POINT(content.x0 + 50, content.y0 + 50), &hit); /* drag to exactly the doc extent */ if (rc != result_OK) goto Failure; - rc = wuss_mouse_click(wuss, (point_t) { content.x0 + 50, content.y0 + 50 }, wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); + rc = wuss_mouse_click(wuss, POINT(content.x0 + 50, content.y0 + 50), wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); if (rc != result_OK) goto Failure; @@ -1398,15 +1398,15 @@ result_t wuss_test(const char *resources) width = content.x1 - content.x0; height = content.y1 - content.y0; - rc = wuss_mouse_click(wuss, (point_t) { visible.x1 - 3, visible.y1 - 3 }, wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* G's resize icon again */ + rc = wuss_mouse_click(wuss, POINT(visible.x1 - 3, visible.y1 - 3), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* G's resize icon again */ if (rc != result_OK) goto Failure; - rc = wuss_mouse_move(wuss, (point_t) { content.x0 + 500, content.y0 + 500 }, &hit); /* drag far past the doc extent */ + rc = wuss_mouse_move(wuss, POINT(content.x0 + 500, content.y0 + 500), &hit); /* drag far past the doc extent */ if (rc != result_OK) goto Failure; - rc = wuss_mouse_click(wuss, (point_t) { content.x0 + 500, content.y0 + 500 }, wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); + rc = wuss_mouse_click(wuss, POINT(content.x0 + 500, content.y0 + 500), wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); if (rc != result_OK) goto Failure; @@ -1436,7 +1436,7 @@ result_t wuss_test(const char *resources) rc = wuss_window_create(wuss, &box_m, "M", wuss_WINDOW_NONE, wuss_NO_BACKGROUND, &delegate_m, - (size2d_t) { 200, 200 }, (size2d_t) { 80, 60 }, + SIZE2D(200, 200), SIZE2D(80, 60), &win_m); if (rc != result_OK) goto Failure; @@ -1444,17 +1444,17 @@ result_t wuss_test(const char *resources) wuss_window_get_visible_bounds(win_m, &visible); wuss_window_get_content_bounds(win_m, &content); - rc = wuss_mouse_click(wuss, (point_t) { visible.x1 - 3, visible.y1 - 3 }, wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* M's resize icon */ + rc = wuss_mouse_click(wuss, POINT(visible.x1 - 3, visible.y1 - 3), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* M's resize icon */ if (rc != result_OK) goto Failure; if (hit != win_m) goto Failure; - rc = wuss_mouse_move(wuss, (point_t) { content.x0 + 5, content.y0 + 5 }, &hit); /* drag far inside min_doc */ + rc = wuss_mouse_move(wuss, POINT(content.x0 + 5, content.y0 + 5), &hit); /* drag far inside min_doc */ if (rc != result_OK) goto Failure; - rc = wuss_mouse_click(wuss, (point_t) { content.x0 + 5, content.y0 + 5 }, wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); + rc = wuss_mouse_click(wuss, POINT(content.x0 + 5, content.y0 + 5), wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); if (rc != result_OK) goto Failure; @@ -1487,7 +1487,7 @@ result_t wuss_test(const char *resources) rc = wuss_window_create(wuss, &box_t_win, "T", wuss_WINDOW_NONE, wuss_NO_BACKGROUND, &delegate_t, - (size2d_t) { 200, 200 }, (size2d_t) { 0, 0 }, &win_t); + SIZE2D(200, 200), SIZE2D(0, 0), &win_t); if (rc != result_OK) goto Failure; @@ -1534,12 +1534,12 @@ result_t wuss_test(const char *resources) old_vscroll_x = before.x1 - outline_px - icon / 2; old_vscroll_y = interior_y; - rc = wuss_mouse_click(wuss, (point_t) { cx, cy }, wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* T's toggle-size icon: grow */ + rc = wuss_mouse_click(wuss, POINT(cx, cy), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* T's toggle-size icon: grow */ if (rc != result_OK) goto Failure; if (hit != win_t) goto Failure; - rc = wuss_mouse_click(wuss, (point_t) { cx, cy }, wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); + rc = wuss_mouse_click(wuss, POINT(cx, cy), wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); if (rc != result_OK) goto Failure; @@ -1601,12 +1601,12 @@ result_t wuss_test(const char *resources) cx = (toggle.x0 + toggle.x1) / 2; cy = (toggle.y0 + toggle.y1) / 2; - rc = wuss_mouse_click(wuss, (point_t) { cx, cy }, wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* T's toggle-size icon: shrink */ + rc = wuss_mouse_click(wuss, POINT(cx, cy), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* T's toggle-size icon: shrink */ if (rc != result_OK) goto Failure; if (hit != win_t) goto Failure; - rc = wuss_mouse_click(wuss, (point_t) { cx, cy }, wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); + rc = wuss_mouse_click(wuss, POINT(cx, cy), wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); if (rc != result_OK) goto Failure; @@ -1653,7 +1653,7 @@ result_t wuss_test(const char *resources) rc = wuss_window_create(wuss, &box_r, "R", wuss_WINDOW_NONE, wuss_NO_BACKGROUND, &delegate_r, - (size2d_t) { 70, 70 }, (size2d_t) { 0, 0 }, &win_r); + SIZE2D(70, 70), SIZE2D(0, 0), &win_r); if (rc != result_OK) goto Failure; @@ -1661,7 +1661,7 @@ result_t wuss_test(const char *resources) if (rc != result_OK) goto Failure; - wuss_window_set_scroll(win_r, (point_t) { 0, 15 }); /* within range: max_y = 70 - 40 = 30 */ + wuss_window_set_scroll(win_r, POINT(0, 15)); /* within range: max_y = 70 - 40 = 30 */ rc = wuss_redraw_dirty(wuss); /* flush the scroll's own invalidate */ if (rc != result_OK) @@ -1693,12 +1693,12 @@ result_t wuss_test(const char *resources) interior_x = (before.x0 + outline_px + before.x1 - outline_px - icon) / 2; interior_y = (before.y0 + outline_px + titlebar_height + before.y1 - outline_px - icon) / 2; - rc = wuss_mouse_click(wuss, (point_t) { cx, cy }, wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* R's toggle-size icon: grow past doc_height, forcing a scroll re-clamp */ + rc = wuss_mouse_click(wuss, POINT(cx, cy), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* R's toggle-size icon: grow past doc_height, forcing a scroll re-clamp */ if (rc != result_OK) goto Failure; if (hit != win_r) goto Failure; - rc = wuss_mouse_click(wuss, (point_t) { cx, cy }, wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); + rc = wuss_mouse_click(wuss, POINT(cx, cy), wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); if (rc != result_OK) goto Failure; @@ -1751,7 +1751,7 @@ result_t wuss_test(const char *resources) rc = wuss_window_create(wuss, &box_nb, "NB", wuss_WINDOW_NO_RESIZE_BLIT, wuss_NO_BACKGROUND, &delegate_nb, - (size2d_t) { 200, 200 }, (size2d_t) { 0, 0 }, &win_nb); + SIZE2D(200, 200), SIZE2D(0, 0), &win_nb); if (rc != result_OK) goto Failure; @@ -1781,12 +1781,12 @@ result_t wuss_test(const char *resources) interior_x = (before.x0 + outline_px + before.x1 - outline_px - icon) / 2; interior_y = (before.y0 + outline_px + titlebar_height + before.y1 - outline_px - icon) / 2; - rc = wuss_mouse_click(wuss, (point_t) { cx, cy }, wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* NB's toggle-size icon: grow */ + rc = wuss_mouse_click(wuss, POINT(cx, cy), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* NB's toggle-size icon: grow */ if (rc != result_OK) goto Failure; if (hit != win_nb) goto Failure; - rc = wuss_mouse_click(wuss, (point_t) { cx, cy }, wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); + rc = wuss_mouse_click(wuss, POINT(cx, cy), wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); if (rc != result_OK) goto Failure; @@ -1830,7 +1830,7 @@ result_t wuss_test(const char *resources) box_u.x1 = 120; box_u.y1 = 120; /* 40x40 content */ rc = wuss_window_create(wuss, &box_u, "U", wuss_WINDOW_NONE, wuss_NO_BACKGROUND, /* scrollbars on: carve.x/y = icon size */ &delegate_u, - (size2d_t) { 70, 70 }, (size2d_t) { 0, 0 }, &win_u); /* doc size well within the 200x200 screen: growth is doc-limited, not screen-limited */ + 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) goto Failure; @@ -1854,12 +1854,12 @@ result_t wuss_test(const char *resources) cx = (toggle.x0 + toggle.x1) / 2; cy = (toggle.y0 + toggle.y1) / 2; - rc = wuss_mouse_click(wuss, (point_t) { cx, cy }, wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* U's toggle-size icon: grow to doc size */ + rc = wuss_mouse_click(wuss, POINT(cx, cy), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* U's toggle-size icon: grow to doc size */ if (rc != result_OK) goto Failure; if (hit != win_u) goto Failure; - rc = wuss_mouse_click(wuss, (point_t) { cx, cy }, wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); + rc = wuss_mouse_click(wuss, POINT(cx, cy), wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); if (rc != result_OK) goto Failure; @@ -1887,12 +1887,12 @@ result_t wuss_test(const char *resources) cx = (toggle.x0 + toggle.x1) / 2; cy = (toggle.y0 + toggle.y1) / 2; - rc = wuss_mouse_click(wuss, (point_t) { cx, cy }, wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* U's toggle-size icon: shrink back */ + rc = wuss_mouse_click(wuss, POINT(cx, cy), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* U's toggle-size icon: shrink back */ if (rc != result_OK) goto Failure; if (hit != win_u) goto Failure; - rc = wuss_mouse_click(wuss, (point_t) { cx, cy }, wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); + rc = wuss_mouse_click(wuss, POINT(cx, cy), wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); if (rc != result_OK) goto Failure; @@ -1938,7 +1938,7 @@ result_t wuss_test(const char *resources) rc = wuss_window_create(wuss, &box_v, "V", wuss_WINDOW_NONE, wuss_NO_BACKGROUND, &delegate_v, - (size2d_t) { 200, 200 }, (size2d_t) { 0, 0 }, &win_v); + SIZE2D(200, 200), SIZE2D(0, 0), &win_v); if (rc != result_OK) goto Failure; @@ -1951,7 +1951,7 @@ result_t wuss_test(const char *resources) * above): titlebar drag calls wuss_window_move with no clamping. From * here, "available space to the screen edge" (scr_width - visible.x0 - * furniture) goes negative, further than furniture alone can absorb. */ - wuss_window_move(win_v, (point_t) { 310, 310 }); + wuss_window_move(win_v, POINT(310, 310)); outline_px = 1; titlebar_height = 20; @@ -1969,12 +1969,12 @@ result_t wuss_test(const char *resources) cx = (toggle.x0 + toggle.x1) / 2; cy = (toggle.y0 + toggle.y1) / 2; - rc = wuss_mouse_click(wuss, (point_t) { cx, cy }, wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* V's toggle-size icon: maximize while off-screen */ + rc = wuss_mouse_click(wuss, POINT(cx, cy), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* V's toggle-size icon: maximize while off-screen */ if (rc != result_OK) goto Failure; if (hit != win_v) goto Failure; - rc = wuss_mouse_click(wuss, (point_t) { cx, cy }, wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); + rc = wuss_mouse_click(wuss, POINT(cx, cy), wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); if (rc != result_OK) goto Failure; @@ -2007,12 +2007,12 @@ result_t wuss_test(const char *resources) cx = (toggle.x0 + toggle.x1) / 2; cy = (toggle.y0 + toggle.y1) / 2; - rc = wuss_mouse_click(wuss, (point_t) { cx, cy }, wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* V's toggle-size icon: shrink back */ + rc = wuss_mouse_click(wuss, POINT(cx, cy), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* V's toggle-size icon: shrink back */ if (rc != result_OK) goto Failure; if (hit != win_v) goto Failure; - rc = wuss_mouse_click(wuss, (point_t) { cx, cy }, wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); + rc = wuss_mouse_click(wuss, POINT(cx, cy), wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); if (rc != result_OK) goto Failure; @@ -2052,7 +2052,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_k, box_size(&box_k), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &win_k); if (rc != result_OK) goto Failure; @@ -2073,7 +2073,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_l, box_size(&box_l), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &win_l); if (rc != result_OK) goto Failure; @@ -2086,7 +2086,7 @@ result_t wuss_test(const char *resources) wuss_window_get_visible_bounds(win_k, &visible); - rc = wuss_mouse_click(wuss, (point_t) { visible.x0 + 31, visible.y0 + 11 }, wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* K's titlebar, clear of the close icon */ + rc = wuss_mouse_click(wuss, POINT(visible.x0 + 31, visible.y0 + 11), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* K's titlebar, clear of the close icon */ if (rc != result_OK) goto Failure; if (hit != win_k) @@ -2094,7 +2094,7 @@ result_t wuss_test(const char *resources) before_k = tc_k.redraw_count; before_l = tc_l.redraw_count; - rc = wuss_mouse_move(wuss, (point_t) { visible.x0 + 45, visible.y0 + 21 }, &hit); + rc = wuss_mouse_move(wuss, POINT(visible.x0 + 45, visible.y0 + 21), &hit); if (rc != result_OK) goto Failure; if (hit != win_k) @@ -2108,7 +2108,7 @@ result_t wuss_test(const char *resources) * footprint, so the move fast path must still apply even * though K isn't topmost */ - rc = wuss_mouse_click(wuss, (point_t) { visible.x0 + 45, visible.y0 + 21 }, wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); + rc = wuss_mouse_click(wuss, POINT(visible.x0 + 45, visible.y0 + 21), wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); if (rc != result_OK) goto Failure; @@ -2140,7 +2140,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_m2, box_size(&box_m2), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &win_m2); if (rc != result_OK) goto Failure; @@ -2153,7 +2153,7 @@ result_t wuss_test(const char *resources) interior_x = before2.x0 + 2; /* inside the untouched left edge */ interior_y = before2.y0 + 25; /* below the titlebar, in plain content */ - rc = wuss_window_resize(win_m2, (size2d_t) { 80, 80 }); /* grow */ + rc = wuss_window_resize(win_m2, SIZE2D(80, 80)); /* grow */ if (rc != result_OK) goto Failure; @@ -2182,7 +2182,7 @@ result_t wuss_test(const char *resources) before2 = after2; - rc = wuss_window_resize(win_m2, (size2d_t) { 40, 40 }); /* shrink back */ + rc = wuss_window_resize(win_m2, SIZE2D(40, 40)); /* shrink back */ if (rc != result_OK) goto Failure; @@ -2231,7 +2231,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_nb2, box_size(&box_nb2), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &win_nb2); if (rc != result_OK) goto Failure; @@ -2242,7 +2242,7 @@ result_t wuss_test(const char *resources) wuss_window_get_visible_bounds(win_nb2, &before3); - rc = wuss_window_resize(win_nb2, (size2d_t) { 80, 80 }); /* grow */ + rc = wuss_window_resize(win_nb2, SIZE2D(80, 80)); /* grow */ if (rc != result_OK) goto Failure; @@ -2283,7 +2283,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_n, box_size(&box_n), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &win_n); if (rc != result_OK) goto Failure; @@ -2302,7 +2302,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_o, box_size(&box_o), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &win_o); if (rc != result_OK) goto Failure; @@ -2319,7 +2319,7 @@ result_t wuss_test(const char *resources) /* Move N far enough right that its new footprint lands partly under O * (which stays wholly untouched), while its old footprint started * entirely clear of O. */ - wuss_window_move(win_n, (point_t) { visible.x0 + 80, visible.y0 + 10 }); + wuss_window_move(win_n, POINT(visible.x0 + 80, visible.y0 + 10)); wuss_window_get_visible_bounds(win_n, &visible); wuss_window_get_visible_bounds(win_o, &visible_o); @@ -2387,7 +2387,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_b, box_size(&box_b), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &win_b); if (rc != result_OK) goto Failure; @@ -2406,7 +2406,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_a, box_size(&box_a), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &win_a); if (rc != result_OK) goto Failure; @@ -2423,8 +2423,8 @@ result_t wuss_test(const char *resources) /* Move B far enough right that its whole new footprint clears A. */ dx = 60; - wuss_window_move(win_b, (point_t) { visible_b_before.x0 + dx, - visible_b_before.y0 }); + wuss_window_move(win_b, POINT(visible_b_before.x0 + dx, + visible_b_before.y0)); /* The clean strip (previously exposed, genuinely B's own pixels) lands * at its translated destination and must have been blitted there, not @@ -2490,7 +2490,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_b, box_size(&box_b), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &win_b); if (rc != result_OK) goto Failure; @@ -2509,7 +2509,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_a, box_size(&box_a), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &win_a); if (rc != result_OK) goto Failure; @@ -2525,8 +2525,8 @@ result_t wuss_test(const char *resources) * (x:40-60, under A) both stay clear of / under A exactly as before -- * nothing about A's own pixels is ever touched by the blit, so A must * not be forced to redraw. */ - wuss_window_move(win_b, (point_t) { visible_b_before.x0, - visible_b_before.y0 + 5 }); + wuss_window_move(win_b, POINT(visible_b_before.x0, + visible_b_before.y0 + 5)); occluder_dirty = 0; for (i = 0; i < wuss_get_dirty_count(wuss); i++) @@ -2570,7 +2570,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_b, box_size(&box_b), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &win_b); if (rc != result_OK) goto Failure; @@ -2589,7 +2589,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_a, box_size(&box_a), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &win_a); if (rc != result_OK) goto Failure; @@ -2616,8 +2616,8 @@ result_t wuss_test(const char *resources) * moved, its pixels there are already correct), so it must stay clean; * the translated hidden band (y:45-65, never had valid pixels) still * needs forcing dirty for a real repaint. */ - wuss_window_move(win_b, (point_t) { visible_b_before.x0, - visible_b_before.y0 + 25 }); + wuss_window_move(win_b, POINT(visible_b_before.x0, + visible_b_before.y0 + 25)); occluded_overlap.x0 = 0; occluded_overlap.y0 = 25; occluded_overlap.x1 = 60; occluded_overlap.y1 = 40; @@ -2683,7 +2683,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_b, box_size(&box_b), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &win_b); if (rc != result_OK) goto Failure; @@ -2702,7 +2702,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_a, box_size(&box_a), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &win_a); if (rc != result_OK) goto Failure; @@ -2725,8 +2725,8 @@ result_t wuss_test(const char *resources) wuss_window_get_visible_bounds(win_b, &visible_b_before); wuss_window_get_visible_bounds(win_a, &visible_a); - wuss_window_move(win_b, (point_t) { visible_b_before.x0 - 15, - visible_b_before.y0 - 15 }); + wuss_window_move(win_b, POINT(visible_b_before.x0 - 15, + visible_b_before.y0 - 15)); /* The blit must have actually happened, not fallen back: B's own * footprint (outside A) must not be dirtied wholesale. */ @@ -2779,12 +2779,12 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_c, box_size(&box_c), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &win_c); if (rc != result_OK) goto Failure; - rc = wuss_mouse_click(wuss, (point_t) { 31, 11 }, wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* C's titlebar, above its content, clear of the close icon */ + rc = wuss_mouse_click(wuss, POINT(31, 11), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* C's titlebar, above its content, clear of the close icon */ if (rc != result_OK) goto Failure; if (hit != win_c) @@ -2792,7 +2792,7 @@ result_t wuss_test(const char *resources) wuss_window_close(win_c); - rc = wuss_mouse_move(wuss, (point_t) { 20, 20 }, &hit); + rc = wuss_mouse_move(wuss, POINT(20, 20), &hit); if (rc != result_OK) goto Failure; @@ -2813,7 +2813,7 @@ result_t wuss_test(const char *resources) rc = wuss_window_create(wuss, &box_s, "S", wuss_WINDOW_NONE, wuss_NO_BACKGROUND, &delegate_s, - (size2d_t) { 200, 200 }, (size2d_t) { 0, 0 }, &win_s); + SIZE2D(200, 200), SIZE2D(0, 0), &win_s); if (rc != result_OK) goto Failure; @@ -2825,7 +2825,7 @@ result_t wuss_test(const char *resources) wuss_window_get_content_bounds(win_s, &content_s); rc = wuss_mouse_click(wuss, - (point_t) { content_s.x0 + 5, content_s.y0 + 7 }, + POINT(content_s.x0 + 5, content_s.y0 + 7), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); if (rc != result_OK) goto Failure; @@ -2835,13 +2835,13 @@ result_t wuss_test(const char *resources) goto Failure; /* a task adding the scroll offset itself would double-count it */ rc = wuss_mouse_click(wuss, - (point_t) { content_s.x0 + 5, content_s.y0 + 7 }, + POINT(content_s.x0 + 5, content_s.y0 + 7), wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); if (rc != result_OK) goto Failure; rc = wuss_mouse_move(wuss, - (point_t) { content_s.x0 + 11, content_s.y0 + 13 }, + POINT(content_s.x0 + 11, content_s.y0 + 13), &hit); if (rc != result_OK) goto Failure; @@ -2851,7 +2851,7 @@ result_t wuss_test(const char *resources) tc_s.last_scroll_x = -1; tc_s.last_scroll_y = -1; rc = wuss_scroll(wuss, - (point_t) { content_s.x0 + 3, content_s.y0 + 4 }, + POINT(content_s.x0 + 3, content_s.y0 + 4), 1, &hit); if (rc != result_OK) goto Failure; @@ -2889,8 +2889,8 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NONE, /* all furniture present */ wuss_NO_BACKGROUND, &delegate_r, - (size2d_t) { 400, 400 }, - (size2d_t) { 0, 0 }, + SIZE2D(400, 400), + SIZE2D(0, 0), &win_r); if (rc != result_OK) goto Failure; @@ -2900,7 +2900,7 @@ result_t wuss_test(const char *resources) content_r.y1 - content_r.y0 != box_r.y1 - box_r.y0) goto Failure; - rc = wuss_window_resize(win_r, (size2d_t) { 61, 47 }); + rc = wuss_window_resize(win_r, SIZE2D(61, 47)); if (rc != result_OK) goto Failure; diff --git a/libraries/wuss/window/move.c b/libraries/wuss/window/move.c index ef0ee7d0..19c05a54 100644 --- a/libraries/wuss/window/move.c +++ b/libraries/wuss/window/move.c @@ -145,7 +145,7 @@ void wuss_window_move(wuss_window_t *window, point_t p) idx = order[i]; if (!screen_copy_rect(window->wuss->scr, &blit_src[idx], - (point_t) { blit_dest[idx].x0, blit_dest[idx].y0 }, + POINT(blit_dest[idx].x0, blit_dest[idx].y0), &copied)) { /* The screen format doesn't support the blit at all (e.g. paletted): diff --git a/libraries/wuss/window/set-scroll.c b/libraries/wuss/window/set-scroll.c index 1543289e..531665f4 100644 --- a/libraries/wuss/window/set-scroll.c +++ b/libraries/wuss/window/set-scroll.c @@ -28,7 +28,7 @@ void wuss_window_set_scroll(wuss_window_t *window, point_t p) * newly-exposed edge strip(s) need an actual repaint. */ window->wuss->scr->clip = content; if (screen_copy_rect(window->wuss->scr, &content, - (point_t) { content.x0 - dx, content.y0 - dy }, &copied)) + POINT(content.x0 - dx, content.y0 - dy), &copied)) { wuss__invalidate_minus(window->wuss, &content, &copied); return; From fa735d7a5cec3922e7c2ea603be5531696d4c206 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sat, 29 Aug 2026 13:48:22 +0100 Subject: [PATCH 058/287] fix(wuss): preserve pointer offset in resize-corner drag Resize-drag recomputed width/height as pointer minus content origin on every move, discarding where within the resize icon the mouse-down click landed. The window's corner snapped to the raw pointer position on the first move, jumping if the click wasn't at the icon's exact corner pixel. Store the pointer's offset from the content box's bottom-right corner at mouse-down (mirroring how a titlebar drag already stores its content-relative offset), and subtract it on each move, so the grabbed point stays under the pointer through the drag. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01TWwV3bFEi9z5MZc5wqiJzZ --- libraries/wuss/furniture.h | 11 ++++++++--- libraries/wuss/furniture/drag-resize.c | 7 +++++-- libraries/wuss/mouse-click.c | 12 ++++++++++++ 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/libraries/wuss/furniture.h b/libraries/wuss/furniture.h index ddc707b3..1bac5c97 100644 --- a/libraries/wuss/furniture.h +++ b/libraries/wuss/furniture.h @@ -46,9 +46,14 @@ struct wuss__furniture { wuss_window_t *dragging; /* NULL when idle */ wuss_furniture_drag_kind_t drag_kind; - point_t drag; /* MOVE: pointer offset within content; - * RESIZE: unused, recomputed each move; - * *_SAUSAGE: pointer position at drag start */ + point_t drag; /* *_SAUSAGE: pointer position at drag start; + * MOVE: pointer offset within content */ + point_t drag_offset; /* RESIZE: pointer offset from the + * content box's bottom-right corner + * at drag start, so the grab point + * stays under the pointer instead + * of the window's edge snapping to + * it on the first move */ int drag_scroll_start; /* *_SAUSAGE: scroll.x/scroll.y at drag start */ }; diff --git a/libraries/wuss/furniture/drag-resize.c b/libraries/wuss/furniture/drag-resize.c index 21773b94..9a92a3a3 100644 --- a/libraries/wuss/furniture/drag-resize.c +++ b/libraries/wuss/furniture/drag-resize.c @@ -13,8 +13,11 @@ void wuss__furniture_drag_resize(wuss_window_t *window, point_t p) wuss__content_box(window, &content); wuss__min_content(window, &min); - width = p.x - content.x0; - height = p.y - content.y0; + /* Subtract the offset recorded at drag start so the point originally + * grabbed on the resize icon stays under the pointer, rather than the + * window's edge snapping to meet the pointer on the first move. */ + width = p.x - window->wuss->furniture.drag_offset.x - content.x0; + height = p.y - window->wuss->furniture.drag_offset.y - content.y0; width = CLAMP(width, min.w, MAX(window->doc.w, min.w)); height = CLAMP(height, min.h, MAX(window->doc.h, min.h)); diff --git a/libraries/wuss/mouse-click.c b/libraries/wuss/mouse-click.c index a68c9bb0..745f5e7b 100644 --- a/libraries/wuss/mouse-click.c +++ b/libraries/wuss/mouse-click.c @@ -136,6 +136,18 @@ result_t wuss_mouse_click(wuss_t *wuss, wuss->furniture.drag.x = x; wuss->furniture.drag.y = y; wuss->furniture.drag_scroll_start = (region == wuss_FURNITURE_VSCROLL_WELL) ? scroll.y : scroll.x; + + /* Resize needs the pointer's offset from the content box's current + * bottom-right corner, so the point grabbed on the resize icon stays + * under the pointer as it moves, rather than that corner jumping to + * meet the pointer on the very first move. */ + if (region == wuss_FURNITURE_RESIZE) + { + box_t content; + wuss__content_box(win, &content); + wuss->furniture.drag_offset.x = x - content.x1; + wuss->furniture.drag_offset.y = y - content.y1; + } } return result_OK; } From d99aa4ed66d62d969ed2cb3735adf17d22f980c6 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sat, 29 Aug 2026 17:54:14 +0100 Subject: [PATCH 059/287] docs: trim derivable build and module-layout notes from CLAUDE.md Co-Authored-By: Claude Sonnet 5 --- CLAUDE.md | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index ad68b665..340b704f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,12 +8,6 @@ DPTLib is a platform-independent C99 library (base, databases, datastruct, frame ## Build -``` -mkdir build && cd build -cmake -DBUILD_TESTS=YES .. -make -j4 -``` - Useful CMake options: - `BUILD_TESTS=YES` — build the `DPTLibTest` self-test executable. - `BUILD_SDL_TESTS=YES` — additionally build tests needing SDL2/SDL2_image. @@ -45,9 +39,7 @@ Success prints `++ Tests completed in Ns: N of N tests passed.` ## Architecture -**Module layout.** Each module lives in two places that must be kept in sync: -- `include//.h` — the public API, always wrapped in `extern "C"`, documented with Doxygen `\file`/`\param`/`\return` comments. -- `libraries///` — implementation `.c` files (often one function per file, e.g. `libraries/datastruct/vector/{create,destroy,insert,...}.c`), plus a private `impl.h` defining the opaque struct behind the public typedef and any internal-only declarations. +**Module layout.** Each module is split between `include//.h` (public API, `extern "C"`, Doxygen-documented) and `libraries///` (implementation `.c` files, often one function per file, plus a private `impl.h` for the opaque struct and internal-only declarations). New source/header files must be added by hand to the relevant `set(..._SOURCES ...)` list and, for public headers, to `PUBLIC_HEADERS`, in `CMakeLists.txt` — there is no globbing. From 131d0e5273ba8513e26b8a15f8e43873c0bf850e Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sat, 29 Aug 2026 19:37:33 +0100 Subject: [PATCH 060/287] feat(wuss): add work-area icons Add a work-area icon subsystem: static labels and clickable bevelled buttons that Wuss draws inside a window's content area and hit-tests before the content task sees a click. Icon boxes are in virtual document space so they scroll with the content; button clicks and hovers reach the task as wuss_EVENT_ICON, while labels and hidden or disabled icons fall through as wuss_EVENT_MOUSE. Includes the public wuss/icon.h API, per-window icon storage, drawing, mouse routing integration, and an icons test task. Co-Authored-By: Claude Sonnet 5 --- CMakeLists.txt | 16 ++ docs/windowing/wuss.md | 56 +++++-- include/wuss/icon.h | 177 +++++++++++++++++++++ include/wuss/task.h | 18 ++- include/wuss/wuss.h | 17 ++ libraries/wuss/create.c | 10 ++ libraries/wuss/destroy.c | 1 + libraries/wuss/furniture/back-box.c | 4 +- libraries/wuss/furniture/close-box.c | 4 +- libraries/wuss/furniture/content-box.c | 2 +- libraries/wuss/furniture/draw.c | 2 +- libraries/wuss/furniture/hscroll-box.c | 8 +- libraries/wuss/furniture/invalidate.c | 2 +- libraries/wuss/furniture/resize-box.c | 2 +- libraries/wuss/furniture/toggle-action.c | 2 +- libraries/wuss/furniture/toggle-box.c | 4 +- libraries/wuss/furniture/vscroll-box.c | 8 +- libraries/wuss/icon.h | 53 +++++++ libraries/wuss/icon/create.c | 82 ++++++++++ libraries/wuss/icon/delete.c | 34 ++++ libraries/wuss/icon/draw.c | 144 +++++++++++++++++ libraries/wuss/icon/free.c | 25 +++ libraries/wuss/icon/get-bbox.c | 8 + libraries/wuss/icon/get-text.c | 8 + libraries/wuss/icon/get-type.c | 8 + libraries/wuss/icon/get-window.c | 8 + libraries/wuss/icon/hit-test.c | 28 ++++ libraries/wuss/icon/invalidate.c | 10 ++ libraries/wuss/icon/screen-box.c | 17 ++ libraries/wuss/icon/set-hidden.c | 13 ++ libraries/wuss/icon/set-text.c | 32 ++++ libraries/wuss/impl.h | 30 ++-- libraries/wuss/mouse-click.c | 38 ++++- libraries/wuss/mouse-move.c | 41 ++++- libraries/wuss/redraw.c | 9 ++ libraries/wuss/test/tasks/icons.c | 189 +++++++++++++++++++++++ libraries/wuss/test/tasks/icons.h | 40 +++++ libraries/wuss/test/wuss-test.c | 9 +- libraries/wuss/window/close.c | 2 + libraries/wuss/window/create.c | 5 +- libraries/wuss/window/resize.c | 2 +- 41 files changed, 1109 insertions(+), 59 deletions(-) create mode 100644 include/wuss/icon.h create mode 100644 libraries/wuss/icon.h create mode 100644 libraries/wuss/icon/create.c create mode 100644 libraries/wuss/icon/delete.c create mode 100644 libraries/wuss/icon/draw.c create mode 100644 libraries/wuss/icon/free.c create mode 100644 libraries/wuss/icon/get-bbox.c create mode 100644 libraries/wuss/icon/get-text.c create mode 100644 libraries/wuss/icon/get-type.c create mode 100644 libraries/wuss/icon/get-window.c create mode 100644 libraries/wuss/icon/hit-test.c create mode 100644 libraries/wuss/icon/invalidate.c create mode 100644 libraries/wuss/icon/screen-box.c create mode 100644 libraries/wuss/icon/set-hidden.c create mode 100644 libraries/wuss/icon/set-text.c create mode 100644 libraries/wuss/test/tasks/icons.c create mode 100644 libraries/wuss/test/tasks/icons.h diff --git a/CMakeLists.txt b/CMakeLists.txt index e50ac931..8861523b 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -112,6 +112,7 @@ set(PUBLIC_HEADERS include/utils/maths.h include/utils/pack.h include/utils/primes.h + include/wuss/icon.h include/wuss/task.h include/wuss/window.h include/wuss/wuss.h) @@ -336,6 +337,20 @@ set(WUSS_SOURCES libraries/wuss/furniture/vscroll-box.c libraries/wuss/furniture.h libraries/wuss/get-font.c + libraries/wuss/icon/create.c + libraries/wuss/icon/delete.c + libraries/wuss/icon/draw.c + libraries/wuss/icon/free.c + libraries/wuss/icon/get-bbox.c + libraries/wuss/icon/get-text.c + libraries/wuss/icon/get-type.c + libraries/wuss/icon/get-window.c + libraries/wuss/icon/hit-test.c + libraries/wuss/icon/invalidate.c + libraries/wuss/icon/screen-box.c + libraries/wuss/icon/set-hidden.c + libraries/wuss/icon/set-text.c + libraries/wuss/icon.h libraries/wuss/idle.c libraries/wuss/impl.h libraries/wuss/invalidate.c @@ -549,6 +564,7 @@ if(BUILD_TESTS) libraries/wuss/test/tasks/checker.c libraries/wuss/test/tasks/curve.c libraries/wuss/test/tasks/gradient.c + libraries/wuss/test/tasks/icons.c libraries/wuss/test/tasks/image.c libraries/wuss/test/tasks/launcher.c libraries/wuss/test/tasks/palette.c diff --git a/docs/windowing/wuss.md b/docs/windowing/wuss.md index a648f97a..0871dd5c 100644 --- a/docs/windowing/wuss.md +++ b/docs/windowing/wuss.md @@ -62,12 +62,12 @@ wuss_task_t; - `wuss_WINDOW_NONE` — default: every furniture region drawn. - `wuss_WINDOW_NO_TITLEBAR` — no titlebar, and no drag handle. - `wuss_WINDOW_NO_OUTLINE` — no 1px border around the window. -- `wuss_WINDOW_NO_CLOSE` — no close icon in the titlebar. -- `wuss_WINDOW_NO_BACK` — no back icon in the titlebar (`wuss_BUTTON_SELECT` sends the window to back, `wuss_BUTTON_ADJUST` brings it to front). -- `wuss_WINDOW_NO_TOGGLE_SIZE` — no toggle-size icon in the titlebar. +- `wuss_WINDOW_NO_CLOSE` — no close button in the titlebar. +- `wuss_WINDOW_NO_BACK` — no back button in the titlebar (`wuss_BUTTON_SELECT` sends the window to back, `wuss_BUTTON_ADJUST` brings it to front). +- `wuss_WINDOW_NO_TOGGLE_SIZE` — no toggle-size button in the titlebar. - `wuss_WINDOW_NO_VSCROLL` — no vertical scrollbar on the right edge. - `wuss_WINDOW_NO_HSCROLL` — no horizontal scrollbar on the bottom edge. -- `wuss_WINDOW_NO_RESIZE` — no resize icon in the bottom-right corner. +- `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_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. @@ -83,6 +83,7 @@ typedef enum wuss_event_kind { wuss_EVENT_REDRAW, wuss_EVENT_MOUSE, + wuss_EVENT_ICON, wuss_EVENT_SCROLL } wuss_event_kind_t; @@ -93,8 +94,9 @@ typedef struct wuss_event union { struct { screen_t *scr; const box_t *content; } redraw; - struct { wuss_mouse_action_t action; int x, y; wuss_button_t button; } mouse; - struct { int x, y, delta; } scroll; + struct { wuss_mouse_action_t action; point_t point; wuss_button_t button; } mouse; + struct { wuss_icon_t *icon; wuss_mouse_action_t action; wuss_button_t button; } icon; + struct { point_t point; int delta; } scroll; } data; } @@ -110,6 +112,8 @@ For `wuss_EVENT_REDRAW`, `event->data.redraw.scr` is called with `scr->clip` alr For `wuss_EVENT_MOUSE` and `wuss_EVENT_SCROLL`, `event->data.mouse.point` and `event->data.scroll.point` are window-local content coordinates: the content area's top-left is `(0,0)` plus the window's current scroll offset (see "Scrolling" below). `event->data.mouse.action` is `wuss_MOUSE_DOWN`/`wuss_MOUSE_UP`/`wuss_MOUSE_MOVE`. A titlebar click never reaches a task's handle callback: it starts a drag (and, for `wuss_BUTTON_SELECT`, brings the window to front) instead. A content click, even on a `wuss_WINDOW_NO_TITLEBAR` window with no drag handle, never changes z-order — only a titlebar click raises a window — so tasks are free to use content clicks for their own purposes without Wuss reordering windows underneath them. +`wuss_EVENT_ICON` is delivered instead of `wuss_EVENT_MOUSE` whenever the pointer is inside a `wuss_ICON_TYPE_BUTTON` icon's bounding box (see "Icons" below): `event->data.icon.icon` names the icon, `action` and `button` carry the same values a `wuss_EVENT_MOUSE` would. `wuss_ICON_TYPE_LABEL` icons, and hidden or disabled icons, never raise it — clicks over them fall through as `wuss_EVENT_MOUSE`. + ## Mouse and scroll routing Feed mouse events in with `wuss_mouse_click` (action `wuss_MOUSE_DOWN` or `wuss_MOUSE_UP`) and `wuss_mouse_move`, and scroll events with `wuss_scroll`, each hit-testing the topmost window at `(x, y)` and delivering to its task in window-local coordinates. All three take an optional `wuss_window_t **hit` out-parameter naming the window under the pointer (or being dragged). Events are dropped if the hit window has no handle callback, or (for scroll) the pointer is over its titlebar. @@ -124,16 +128,41 @@ Each window carries a scroll offset, `(0, 0)` by default: the point in the task' ## 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. - `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. - A window move or resize is clipped, piece by piece, against whatever's above it in the z-order, and any pixels a move can preserve are blitted directly rather than queued dirty; only the genuinely-changed pieces end up in the dirty region. This is an internal optimisation with no effect on a task's own redraw handling. +## Icons + +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_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_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`. + +Flags: `wuss_ICON_FLAGS_HIDDEN` (not drawn, not hit-tested) and `wuss_ICON_FLAGS_DISABLED` (drawn greyed; clicks fall through as `wuss_EVENT_MOUSE`). + +An icon's bounding box is in **virtual content space** — the same space as `wuss_EVENT_MOUSE`'s `point` and `wuss_window_invalidate`'s box — so icons scroll with the content. The on-screen box is `content-top-left - scroll + bbox`. + +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`. + ## Glossary Terms as this document and the API use them. Several are RISC OS conventions, which Wuss follows. -- **Adjust** — the secondary mouse button, `wuss_BUTTON_ADJUST`. Conventionally the variant of an action: Adjust on the back icon brings a window to front rather than sending it back, and Adjust on a scroll arrow steps against the direction the arrow points. +- **Adjust** — the secondary mouse button, `wuss_BUTTON_ADJUST`. Conventionally the variant of an action: Adjust on the back button brings a window to front rather than sending it back, and Adjust on a scroll arrow steps against the direction the arrow points. - **Backdrop** — the desktop background colour painted behind all windows, set by `config->backdrop` at `wuss_create` time. `wuss_NO_BACKGROUND` leaves the area behind windows untouched, making it the caller's to repaint. - **Button flags** — `wuss_button_t` values are flags (Select 4, Menu 2, Adjust 1), OR'd together so a chord can be reported. Test a reported button with `&`, never for equality. - **Chord** — two or more mouse buttons held together, e.g. Select+Adjust. Wuss's own furniture handling resolves an ambiguous chord in Select's favour. @@ -141,9 +170,10 @@ Terms as this document and the API use them. Several are RISC OS conventions, wh - **Dirty region** — the accumulated set of screen-space boxes needing repaint, coalesced as they accumulate. `wuss_redraw_dirty` repaints and clears it. - **Document extent** — `doc`, the size of a task's virtual content space, fixed at window creation. Sets how far a window can scroll, the scrollbar sausages' proportions, and the size a resize-drag or toggle-size can grow the content area to. - **Minimum extent** — `min_doc`, the smallest content size a resize-drag or toggle-size will leave a window at, fixed at window creation. `(0, 0)` means the built-in floor. -- **Furniture** — everything Wuss draws around a window's content: outline, titlebar and its icons, scrollbars, resize icon. Drawn outside the content area, never carved out of it. Furniture clicks are handled entirely within Wuss and never reach the task. +- **Furniture** — everything Wuss draws around a window's content: outline, titlebar and its buttons, scrollbars, resize button. Drawn outside the content area, never carved out of it. Furniture clicks are handled entirely within Wuss and never reach the task. +- **Furniture button** — a clickable furniture region in the titlebar or window corner: close, back, toggle-size, resize. (Called an "icon" in earlier revisions; that name now means the work-area element below.) - **Handle callback** — a task's single `wuss_event_fn_t`, receiving every event kind and dispatching on `event->kind`. A window whose task has no handle receives no events at all. -- **Icon** — a clickable furniture region in the titlebar or window corner: close, back, toggle-size, resize. +- **Icon** — a rectangular UI element Wuss draws and hit-tests inside a window's content area: a static `wuss_ICON_TYPE_LABEL`, or a clickable bevelled `wuss_ICON_TYPE_BUTTON`. Created with `wuss_icon_create` and owned by its window. Its bounding box is in virtual content space, so it scrolls with the content; its screen position is `content-top-left - scroll + bbox`. See "Icons". - **Invalidate** — mark a region dirty for the next `wuss_redraw_dirty`. Window management does this for its own changes; a task must do it for its own content changes. - **Menu** — the middle mouse button, `wuss_BUTTON_MENU`. Routed like any other button; Wuss provides no menu widget of its own. - **Outline** — the 1px border drawn around a window, suppressed by `wuss_WINDOW_NO_OUTLINE`. @@ -151,12 +181,12 @@ Terms as this document and the API use them. Several are RISC OS conventions, wh - **Screen space** — coordinates in the underlying `screen_t`, origin at its top-left. Visible and content bounds are in screen space. - **Select** — the primary mouse button, `wuss_BUTTON_SELECT`. Performs the plain action, and raises a window when used on its titlebar. - **Task** — the client of a window: an event callback plus an opaque `task_data` pointer, held in a `wuss_task_t`. Wuss owns the window; the task owns what's drawn inside it. -- **Titlebar** — the strip above the content area carrying the window's label and its icons, and the drag handle for moving the window. Suppressed by `wuss_WINDOW_NO_TITLEBAR`. -- **Toggle size** — the titlebar icon that switches a window between its normal size and a maximised size, and back. +- **Titlebar** — the strip above the content area carrying the window's label and its buttons, and the drag handle for moving the window. Suppressed by `wuss_WINDOW_NO_TITLEBAR`. +- **Toggle size** — the titlebar button that switches a window between its normal size and a maximised size, and back. - **Virtual content space** — the task's own full coordinate space, of size `doc`. Mouse and scroll events arrive in it, i.e. with the scroll offset already added. - **Visible bounds** — a window's whole on-screen footprint, content plus furniture; `wuss_window_get_visible_bounds`. -- **Well** — the track a scrollbar's sausage slides along, between the two arrow icons. -- **Window-local coordinates** — coordinates relative to the content area's top-left, before the scroll offset is added. `wuss_window_invalidate` takes its box in these. +- **Well** — the track a scrollbar's sausage slides along, between the two arrow buttons. +- **Window-local coordinates** — coordinates relative to the content area's top-left. `wuss_window_invalidate` takes its box in virtual content space, i.e. with the scroll offset already added. - **Z-order** — the back-to-front stacking order of windows. Changed with `wuss_window_restack`, or by a Select click on a titlebar; content clicks never change it. ## Limitations diff --git a/include/wuss/icon.h b/include/wuss/icon.h new file mode 100644 index 00000000..ce8c3ea7 --- /dev/null +++ b/include/wuss/icon.h @@ -0,0 +1,177 @@ +/* icon.h -- wuss work-area icons */ + +/** + * \file icon.h + * + * Work-area icons: static labels and clickable bevelled buttons that Wuss draws + * inside a window's content area and hit-tests before the content task sees a + * click. + * + * An icon's bounding box is given in virtual document space -- the same + * coordinate space as wuss_EVENT_MOUSE's point and wuss_window_invalidate's + * local_box -- so an icon scrolls with the content it sits on. Its on-screen + * position is (content.x0 - scroll.x + bbox), using the window's current + * content bounds and scroll offset. + * + * Wuss fills a window's background, draws its icons, then delivers + * wuss_EVENT_REDRAW, so a task is free to paint over or around icon pixels. A + * click on a wuss_ICON_TYPE_BUTTON reaches the task as wuss_EVENT_ICON; clicks + * on a label, or on a hidden or disabled icon, fall through as + * wuss_EVENT_MOUSE. + */ + +#ifndef WUSS_ICON_H +#define WUSS_ICON_H + +#ifdef __cplusplus +extern "C" +{ +#endif + +#include "base/result.h" +#include "geom/box.h" + +#include "wuss/wuss.h" + +/* ----------------------------------------------------------------------- */ + +/** An opaque work-area icon handle, owned by the window it is created on. */ +typedef struct wuss_icon wuss_icon_t; + +/** + * What an icon looks like and how it behaves. The enum is left open so sprite + * and editable-text icons can be added later without breaking existing specs. + */ +typedef enum wuss_icon_type +{ + wuss_ICON_TYPE_LABEL = 0, /**< Static text drawn with the window manager's + * font. Not interactive: clicks fall through to + * the task as wuss_EVENT_MOUSE. */ + wuss_ICON_TYPE_BUTTON /**< Bevelled rectangle with a centred text label + * and pressed-state visual feedback; clicks and + * hovers are delivered to the task as + * wuss_EVENT_ICON. */ +} +wuss_icon_type_t; + +/** Icon appearance and behaviour flags, combinable with bitwise OR. */ +typedef enum wuss_icon_flags +{ + wuss_ICON_FLAGS_NONE = 0, + wuss_ICON_FLAGS_HIDDEN = 1 << 0, /**< Not drawn, not hit-tested. */ + wuss_ICON_FLAGS_DISABLED = 1 << 1 /**< Drawn greyed; clicks fall through to + * the task as wuss_EVENT_MOUSE rather + * than raising wuss_EVENT_ICON. */ +} +wuss_icon_flags_t; + +/** + * Description of an icon at creation. Copied by value into the icon; the caller + * keeps ownership of \c text, which is copied. + * + * A RISC OS-style validation string is deliberately omitted for now; a later \c + * validation field would stay source-compatible for callers that + * zero-initialise the spec. + */ +typedef struct wuss_icon_spec +{ + box_t bbox; /**< Bounding box, virtual document space, + * inclusive-exclusive. */ + wuss_icon_type_t type; /**< Icon type. */ + const char *text; /**< NUL-terminated label; copied. NULL means "". */ + wuss_colour_t fg; /**< Text colour, as an index into the system + * palette. */ + wuss_colour_t bg; /**< Fill/bevel base colour, as an index into the + * system palette. A label may pass + * wuss_NO_BACKGROUND for text with no fill; a + * button must pass a real index. */ + wuss_icon_flags_t flags; /**< Appearance/behaviour flags. */ +} +wuss_icon_spec_t; + +/* ----------------------------------------------------------------------- */ + +/** + * Create an icon on a window. The icon is owned by the window and freed when + * the window is closed (or the window manager destroyed). Its bounding box is + * invalidated so the next redraw paints it. + * + * \param[in] window Window to attach the icon to. + * \param[in] spec Icon description; copied. + * \param[out] icon Filled in with the new icon handle, or NULL if the caller + * does not need it. + * \return \ref result_OK on success, \ref result_OOM on allocation failure, + * \ref result_WUSS_BAD_COLOUR if fg or bg is out of range for the + * palette, or \ref result_WUSS_BAD_ICON if type is unknown or a button + * spec has no fill colour. + */ +result_t wuss_icon_create(wuss_window_t *window, + const wuss_icon_spec_t *spec, + wuss_icon_t **icon); + +/** + * Destroy an icon, unlinking it from its window and invalidating its bounding + * box so the next redraw clears it. Safe to pass NULL. + * + * \param[in] icon Icon to destroy, or NULL. + */ +void wuss_icon_delete(wuss_icon_t *icon); + +/** + * Replace an icon's label text. The new text is copied. Invalidates the icon's + * bounding box. + * + * \param[in] icon Icon to change. + * \param[in] text New NUL-terminated label; copied. NULL means "". + * \return \ref result_OK on success, \ref result_OOM on allocation failure (the + * icon keeps its old text). + */ +result_t wuss_icon_set_text(wuss_icon_t *icon, const char *text); + +/** + * Show or hide an icon, toggling wuss_ICON_FLAGS_HIDDEN. Invalidates the icon's + * bounding box. + * + * \param[in] icon Icon to change. + * \param[in] hidden Non-zero to hide the icon, zero to show it. + */ +void wuss_icon_set_hidden(wuss_icon_t *icon, int hidden); + +/** + * Fetch an icon's bounding box, in virtual document space. + * + * \param[in] icon Icon to query. + * \param[out] bbox Filled in with the bounding box. + */ +void wuss_icon_get_bbox(const wuss_icon_t *icon, box_t *bbox); + +/** + * Fetch an icon's type. + * + * \param[in] icon Icon to query. + * \return The icon's type. + */ +wuss_icon_type_t wuss_icon_get_type(const wuss_icon_t *icon); + +/** + * Fetch an icon's current label text. + * + * \param[in] icon Icon to query. + * \return The label, never NULL (may be ""). Owned by the icon; valid until the + * next wuss_icon_set_text or wuss_icon_delete on it. + */ +const char *wuss_icon_get_text(const wuss_icon_t *icon); + +/** + * Fetch the window an icon belongs to. + * + * \param[in] icon Icon to query. + * \return The owning window. + */ +wuss_window_t *wuss_icon_get_window(const wuss_icon_t *icon); + +#ifdef __cplusplus +} +#endif + +#endif /* WUSS_ICON_H */ diff --git a/include/wuss/task.h b/include/wuss/task.h index 78a11cb8..02b65a4a 100644 --- a/include/wuss/task.h +++ b/include/wuss/task.h @@ -21,6 +21,7 @@ extern "C" #include "framebuf/screen.h" #include "wuss/wuss.h" +#include "wuss/icon.h" /* ----------------------------------------------------------------------- */ @@ -30,8 +31,9 @@ typedef enum wuss_event_kind wuss_EVENT_IDLE, /**< Wuss has finished its pending tasks. */ wuss_EVENT_REDRAW, /**< Part of the window's content needs repainting. */ wuss_EVENT_OPEN, /**< Window moved or resized. */ - wuss_EVENT_CLOSE, /**< Close icon clicked; Wuss takes no action itself. */ + wuss_EVENT_CLOSE, /**< Close button clicked; Wuss takes no action itself. */ wuss_EVENT_MOUSE, /**< Button down/up over the window's content. */ + wuss_EVENT_ICON, /**< A work-area button icon was clicked or hovered. */ wuss_EVENT_SCROLL, /**< Mouse wheel used over the window's content. */ wuss_EVENT_QUIT /**< Task shutting down, via wuss_task_stop. */ } @@ -87,6 +89,20 @@ typedef struct wuss_event } mouse; + /** wuss_EVENT_ICON: delivered instead of wuss_EVENT_MOUSE while the + * pointer is inside a wuss_ICON_TYPE_BUTTON icon's bounding box. Label, + * hidden and disabled icons never raise this -- those clicks fall through + * as wuss_EVENT_MOUSE. action is DOWN/UP/MOVE; button is a set of + * wuss_button_t flags, so test it with '&' rather than comparing for + * equality. */ + struct + { + wuss_icon_t *icon; + wuss_mouse_action_t action; + wuss_button_t button; + } + icon; + /** wuss_EVENT_SCROLL: point is window-local content coordinates, as * per mouse. delta's sign and units are as passed to wuss_scroll. */ struct diff --git a/include/wuss/wuss.h b/include/wuss/wuss.h index 99d6e1cc..9752036e 100644 --- a/include/wuss/wuss.h +++ b/include/wuss/wuss.h @@ -28,6 +28,10 @@ extern "C" #define result_WUSS_TOO_SMALL (result_BASE_WUSS + 0) /** A palette index was out of range. */ #define result_WUSS_BAD_COLOUR (result_BASE_WUSS + 1) +/** + * An icon spec was malformed (unknown type, or BUTTON without a fill colour). + */ +#define result_WUSS_BAD_ICON (result_BASE_WUSS + 2) /* ----------------------------------------------------------------------- */ @@ -173,6 +177,19 @@ typedef struct wuss_config /** Furniture chrome colours. */ wuss_palette_t palette; + /** + * 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. + */ + struct + { + wuss_colour_t light; /**< Top/left bevel edge. */ + wuss_colour_t dark; /**< Bottom/right bevel edge. */ + } + bevel; + /** * Desktop background colour, painted behind windows on every redraw, or * wuss_NO_BACKGROUND to leave the background untouched (the caller must then diff --git a/libraries/wuss/create.c b/libraries/wuss/create.c index 82eaca5c..7e00e5fe 100644 --- a/libraries/wuss/create.c +++ b/libraries/wuss/create.c @@ -23,6 +23,7 @@ result_t wuss_create(screen_t *scr, wuss_t *w; wuss_palette_t pal; wuss_colour_t bg, fg; + wuss_colour_t blight, bdark; int font_height; int font_width; @@ -65,6 +66,8 @@ result_t wuss_create(screen_t *scr, if (config != NULL) { pal = config->palette; + blight = config->bevel.light; + bdark = config->bevel.dark; w->backdrop = config->backdrop; } else @@ -91,6 +94,9 @@ result_t wuss_create(screen_t *scr, pal.scroll.arrows = bg; pal.scroll.wells = bg; pal.scroll.sausages = fg; + + blight = pal.title.bg; + bdark = pal.title.bg; } if (pal.title.bg < 0 || pal.title.bg >= w->npalette || @@ -102,6 +108,8 @@ result_t wuss_create(screen_t *scr, pal.scroll.arrows < 0 || pal.scroll.arrows >= w->npalette || pal.scroll.wells < 0 || pal.scroll.wells >= w->npalette || pal.scroll.sausages < 0 || pal.scroll.sausages >= w->npalette || + blight < 0 || blight >= w->npalette || + bdark < 0 || bdark >= w->npalette || (w->backdrop != wuss_NO_BACKGROUND && (w->backdrop < 0 || w->backdrop >= w->npalette))) { @@ -111,6 +119,8 @@ result_t wuss_create(screen_t *scr, } w->furniture_colours = pal; + w->bevel_light = blight; + w->bevel_dark = bdark; if (config != NULL && config->titlebar_height > 0) { diff --git a/libraries/wuss/destroy.c b/libraries/wuss/destroy.c index c546af82..256b8791 100644 --- a/libraries/wuss/destroy.c +++ b/libraries/wuss/destroy.c @@ -21,6 +21,7 @@ void wuss_destroy(wuss_t *doomed) list_t *next; next = e->next; + wuss__icons_free((wuss_window_t *) e); free(e); e = next; } diff --git a/libraries/wuss/furniture/back-box.c b/libraries/wuss/furniture/back-box.c index f1ada608..deab1238 100644 --- a/libraries/wuss/furniture/back-box.c +++ b/libraries/wuss/furniture/back-box.c @@ -9,8 +9,8 @@ void wuss__back_box(const wuss_window_t *window, box_t *out) wuss__titlebar_box(window, &titlebar); - inset = WUSS_ICON_INSET; - size = wuss__icon_size(window); + inset = WUSS_BUTTON_INSET; + size = wuss__button_size(window); out->x0 = titlebar.x0 + inset; out->y0 = titlebar.y0 + inset; diff --git a/libraries/wuss/furniture/close-box.c b/libraries/wuss/furniture/close-box.c index c927f8c5..424af7c4 100644 --- a/libraries/wuss/furniture/close-box.c +++ b/libraries/wuss/furniture/close-box.c @@ -9,8 +9,8 @@ void wuss__close_box(const wuss_window_t *window, box_t *out) wuss__titlebar_box(window, &titlebar); - inset = WUSS_ICON_INSET; - size = wuss__icon_size(window); + inset = WUSS_BUTTON_INSET; + size = wuss__button_size(window); out->x0 = titlebar.x0 + inset; if (!(window->flags & wuss_WINDOW_NO_BACK)) diff --git a/libraries/wuss/furniture/content-box.c b/libraries/wuss/furniture/content-box.c index 0f8081e6..72f379eb 100644 --- a/libraries/wuss/furniture/content-box.c +++ b/libraries/wuss/furniture/content-box.c @@ -14,7 +14,7 @@ void wuss__content_box(const wuss_window_t *window, box_t *out) out->x1 = window->visible.x1 - outline_px; out->y1 = window->visible.y1 - outline_px; - wuss__furniture_carve_for(window->flags, wuss__icon_size(window), &carve); + wuss__furniture_carve_for(window->flags, wuss__button_size(window), &carve); out->x1 -= carve.x; out->y1 -= carve.y; } diff --git a/libraries/wuss/furniture/draw.c b/libraries/wuss/furniture/draw.c index abd8968d..a30cb44d 100644 --- a/libraries/wuss/furniture/draw.c +++ b/libraries/wuss/furniture/draw.c @@ -207,7 +207,7 @@ void wuss__furniture_draw(wuss_t *wuss, /* 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__icon_size(window), &carve); + wuss__furniture_carve_for(window->flags, wuss__button_size(window), &carve); if (carve.x > 0) { diff --git a/libraries/wuss/furniture/hscroll-box.c b/libraries/wuss/furniture/hscroll-box.c index 763071e7..31bbdc9b 100644 --- a/libraries/wuss/furniture/hscroll-box.c +++ b/libraries/wuss/furniture/hscroll-box.c @@ -7,7 +7,7 @@ static void hscroll_row(const wuss_window_t *window, box_t *out) int outline_px, size; outline_px = wuss__outline_px(window); - size = wuss__icon_size(window); + size = wuss__button_size(window); out->y1 = window->visible.y1 - outline_px; out->y0 = out->y1 - size; @@ -22,7 +22,7 @@ void wuss__hscroll_left_box(const wuss_window_t *window, box_t *out) int size; hscroll_row(window, &row); - size = wuss__icon_size(window); + size = wuss__button_size(window); out->y0 = row.y0; out->y1 = row.y1; @@ -36,7 +36,7 @@ void wuss__hscroll_right_box(const wuss_window_t *window, box_t *out) int size; hscroll_row(window, &row); - size = wuss__icon_size(window); + size = wuss__button_size(window); out->y0 = row.y0; out->y1 = row.y1; @@ -50,7 +50,7 @@ void wuss__hscroll_well_box(const wuss_window_t *window, box_t *out) int size; hscroll_row(window, &row); - size = wuss__icon_size(window); + size = wuss__button_size(window); out->y0 = row.y0; out->y1 = row.y1; diff --git a/libraries/wuss/furniture/invalidate.c b/libraries/wuss/furniture/invalidate.c index bd044e3f..dc2e3ba7 100644 --- a/libraries/wuss/furniture/invalidate.c +++ b/libraries/wuss/furniture/invalidate.c @@ -16,7 +16,7 @@ void wuss__furniture_invalidate_for(wuss_window_t *window, const box_t *visible) /* Ask for the same carve the layout uses rather than reading the * scrollbar flags directly: a window with both scrollbars off but resize * on still reserves both strips, for the resize icon and the rules. */ - wuss__furniture_carve_for(window->flags, wuss__icon_size(window), &carve); + wuss__furniture_carve_for(window->flags, wuss__button_size(window), &carve); if (!(window->flags & wuss_WINDOW_NO_TITLEBAR)) { diff --git a/libraries/wuss/furniture/resize-box.c b/libraries/wuss/furniture/resize-box.c index 0a9707b9..143ab98c 100644 --- a/libraries/wuss/furniture/resize-box.c +++ b/libraries/wuss/furniture/resize-box.c @@ -7,7 +7,7 @@ void wuss__resize_box(const wuss_window_t *window, box_t *out) int outline_px, size; outline_px = wuss__outline_px(window); - size = wuss__icon_size(window); + size = wuss__button_size(window); out->x1 = window->visible.x1 - outline_px; out->x0 = out->x1 - size; diff --git a/libraries/wuss/furniture/toggle-action.c b/libraries/wuss/furniture/toggle-action.c index 0fda6c67..6a330d6f 100644 --- a/libraries/wuss/furniture/toggle-action.c +++ b/libraries/wuss/furniture/toggle-action.c @@ -22,7 +22,7 @@ void wuss__furniture_toggle_size(wuss_window_t *window) outline_px = wuss__outline_px(window); titlebar_height = wuss__titlebar_height(window); - wuss__furniture_carve_for(window->flags, wuss__icon_size(window), &carve); + wuss__furniture_carve_for(window->flags, wuss__button_size(window), &carve); /* bounded by what's actually left of the screen from the window's * current top-left, not the screen's full width/height -- otherwise a diff --git a/libraries/wuss/furniture/toggle-box.c b/libraries/wuss/furniture/toggle-box.c index 8f7ee7d3..4d9cf3f4 100644 --- a/libraries/wuss/furniture/toggle-box.c +++ b/libraries/wuss/furniture/toggle-box.c @@ -9,8 +9,8 @@ void wuss__toggle_box(const wuss_window_t *window, box_t *out) wuss__titlebar_box(window, &titlebar); - inset = WUSS_ICON_INSET; - size = wuss__icon_size(window); + inset = WUSS_BUTTON_INSET; + size = wuss__button_size(window); out->x1 = titlebar.x1 - inset; out->x0 = out->x1 - size; diff --git a/libraries/wuss/furniture/vscroll-box.c b/libraries/wuss/furniture/vscroll-box.c index 58e84f12..ba873db6 100644 --- a/libraries/wuss/furniture/vscroll-box.c +++ b/libraries/wuss/furniture/vscroll-box.c @@ -8,7 +8,7 @@ static void vscroll_column(const wuss_window_t *window, box_t *out) int outline_px, size; outline_px = wuss__outline_px(window); - size = wuss__icon_size(window); + size = wuss__button_size(window); out->x1 = window->visible.x1 - outline_px; out->x0 = out->x1 - size; @@ -24,7 +24,7 @@ void wuss__vscroll_up_box(const wuss_window_t *window, box_t *out) int size; vscroll_column(window, &column); - size = wuss__icon_size(window); + size = wuss__button_size(window); out->x0 = column.x0; out->x1 = column.x1; @@ -38,7 +38,7 @@ void wuss__vscroll_down_box(const wuss_window_t *window, box_t *out) int size; vscroll_column(window, &column); - size = wuss__icon_size(window); + size = wuss__button_size(window); out->x0 = column.x0; out->x1 = column.x1; @@ -52,7 +52,7 @@ void wuss__vscroll_well_box(const wuss_window_t *window, box_t *out) int size; vscroll_column(window, &column); - size = wuss__icon_size(window); + size = wuss__button_size(window); out->x0 = column.x0; out->x1 = column.x1; diff --git a/libraries/wuss/icon.h b/libraries/wuss/icon.h new file mode 100644 index 00000000..a2c415c9 --- /dev/null +++ b/libraries/wuss/icon.h @@ -0,0 +1,53 @@ +/* icon.h -- wuss - work-area icons, internal */ + +#ifndef WUSS_ICON_IMPL_H +#define WUSS_ICON_IMPL_H + +#include "geom/box.h" +#include "geom/point.h" + +#include "framebuf/screen.h" + +#include "wuss/wuss.h" +#include "wuss/icon.h" + +struct wuss_icon +{ + wuss_window_t *window; /* owner; back-pointer for invalidate/get_window */ + box_t bbox; /* virtual document space */ + wuss_icon_type_t type; + char *text; /* owned; never NULL ("" instead) */ + wuss_colour_t fg; + wuss_colour_t bg; + wuss_icon_flags_t flags; + int pressed; /* button: 1 while held with the pointer inside */ +}; + +/* Convert an icon's bbox (virtual document space) to a screen-space box, using + * the owning window's current content box and scroll offset: + * screen = content.x0 - scroll.x + bbox. Mirrors wuss_window_invalidate. */ +void wuss__icon_screen_box(const wuss_icon_t *icon, box_t *out); + +/* Invalidate exactly this icon's bbox, via wuss_window_invalidate, so a + * set_text / pressed-state / hide change repaints just the icon. */ +void wuss__icon_invalidate(const wuss_icon_t *icon); + +/* Draw one icon. Called from redraw_window with wuss->scr->clip already set to + * the surviving content piece and the background already filled. "content" is + * the window's full (unclipped) content box, screen space; "scroll" is + * window->scroll. */ +void wuss__icon_draw(wuss_t *wuss, + const wuss_icon_t *icon, + const box_t *content, + point_t scroll); + +/* Hit-test every visible, enabled button icon of "window" against a point given + * in virtual document space. Returns the topmost (last-created wins) match, or + * NULL. Label, hidden and disabled icons are skipped. */ +wuss_icon_t *wuss__icon_hit_test(wuss_window_t *window, point_t doc_point); + +/* Free a window's whole icon store (text + nodes + array). Teardown only: does + * not invalidate or swap-remove. */ +void wuss__icons_free(wuss_window_t *window); + +#endif /* WUSS_ICON_IMPL_H */ diff --git a/libraries/wuss/icon/create.c b/libraries/wuss/icon/create.c new file mode 100644 index 00000000..823f1860 --- /dev/null +++ b/libraries/wuss/icon/create.c @@ -0,0 +1,82 @@ +/* create.c -- wuss - create a work-area icon */ + +#include +#include +#include + +#ifdef FORTIFY +#include "fortify/fortify.h" +#endif + +#include "../impl.h" + +result_t wuss_icon_create(wuss_window_t *window, + const wuss_icon_spec_t *spec, + wuss_icon_t **icon) +{ + wuss_t *w; + wuss_icon_t *it; + wuss_icon_t **grown; + const char *src; + size_t len; + int newcap; + + assert(window != NULL); + assert(spec != NULL); + + w = window->wuss; + + if (spec->type != wuss_ICON_TYPE_LABEL && spec->type != wuss_ICON_TYPE_BUTTON) + return result_WUSS_BAD_ICON; + + if (spec->type == wuss_ICON_TYPE_BUTTON && spec->bg == wuss_NO_BACKGROUND) + return result_WUSS_BAD_ICON; + + if (spec->fg < 0 || spec->fg >= w->npalette) + return result_WUSS_BAD_COLOUR; + + if (spec->bg != wuss_NO_BACKGROUND && + (spec->bg < 0 || spec->bg >= w->npalette)) + return result_WUSS_BAD_COLOUR; + + if (window->nicons == window->cap_icons) + { + newcap = (window->cap_icons == 0) ? 4 : window->cap_icons * 2; + grown = realloc(window->icons, newcap * sizeof(*window->icons)); + if (grown == NULL) + return result_OOM; + window->icons = grown; + window->cap_icons = newcap; + } + + it = malloc(sizeof(*it)); + if (it == NULL) + return result_OOM; + + src = (spec->text != NULL) ? spec->text : ""; + len = strlen(src); + it->text = malloc(len + 1); + if (it->text == NULL) + { + free(it); + return result_OOM; + } + memcpy(it->text, src, len + 1); + + it->window = window; + it->bbox = spec->bbox; + it->type = spec->type; + it->fg = spec->fg; + it->bg = spec->bg; + it->flags = spec->flags; + it->pressed = 0; + + window->icons[window->nicons++] = it; + + wuss__icon_invalidate(it); + + if (icon != NULL) + *icon = it; + + return result_OK; +} diff --git a/libraries/wuss/icon/delete.c b/libraries/wuss/icon/delete.c new file mode 100644 index 00000000..d8cc7ee5 --- /dev/null +++ b/libraries/wuss/icon/delete.c @@ -0,0 +1,34 @@ +/* delete.c -- wuss - destroy a work-area icon */ + +#include + +#ifdef FORTIFY +#include "fortify/fortify.h" +#endif + +#include "../impl.h" + +void wuss_icon_delete(wuss_icon_t *icon) +{ + wuss_window_t *window; + int i; + + if (icon == NULL) + return; + + window = icon->window; + + wuss__icon_invalidate(icon); + + for (i = 0; i < window->nicons; i++) + { + if (window->icons[i] == icon) + { + window->icons[i] = window->icons[--window->nicons]; + break; + } + } + + free(icon->text); + free(icon); +} diff --git a/libraries/wuss/icon/draw.c b/libraries/wuss/icon/draw.c new file mode 100644 index 00000000..98606923 --- /dev/null +++ b/libraries/wuss/icon/draw.c @@ -0,0 +1,144 @@ +/* draw.c -- wuss - draw a work-area icon */ + +#include + +#include "base/utils.h" +#include "geom/box.h" +#include "geom/point.h" +#include "geom/size.h" +#include "framebuf/bmfont.h" +#include "framebuf/screen.h" + +#include "../impl.h" + +/* ----------------------------------------------------------------------- */ + +static void icon_bevel(screen_t *scr, + const box_t *b, + colour_t fill, + colour_t light, + colour_t dark) +{ + screen_draw_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); +} + +/* ----------------------------------------------------------------------- */ + +void wuss__icon_draw(wuss_t *wuss, + const wuss_icon_t *icon, + const box_t *content, + point_t scroll) +{ + screen_t *scr; + box_t b; + colour_t fg; + int font_width, font_height; + int have_font; + + if (icon->flags & wuss_ICON_FLAGS_HIDDEN) + return; + + scr = wuss->scr; + + b.x0 = content->x0 - scroll.x + icon->bbox.x0; + b.y0 = content->y0 - scroll.y + icon->bbox.y0; + b.x1 = content->x0 - scroll.x + icon->bbox.x1; + b.y1 = content->y0 - scroll.y + icon->bbox.y1; + + if (b.x1 <= b.x0 || b.y1 <= b.y0) + return; + + fg = wuss->palette[icon->fg]; + + have_font = (wuss->font != NULL && icon->text[0] != '\0'); + if (have_font) + bmfont_get_info(wuss->font, &font_width, &font_height); + else + font_width = font_height = 0; + NOT_USED(font_width); + + switch (icon->type) + { + case wuss_ICON_TYPE_LABEL: + { + colour_t bg; + + if (icon->bg != wuss_NO_BACKGROUND) + { + bg = wuss->palette[icon->bg]; + screen_draw_rect(scr, b.x0, b.y0, + SIZE2D(b.x1 - b.x0, b.y1 - b.y0), bg); + } + else if (icon->window->bg != wuss_NO_BACKGROUND) + { + bg = wuss->palette[icon->window->bg]; + } + else + { + bg = fg; /* bmfont needs a blend colour; nothing better to offer */ + } + + if (have_font) + { + point_t pos; + + pos.x = b.x0 + 1; + pos.y = b.y0 + (b.y1 - b.y0 - font_height) / 2; + bmfont_draw(wuss->font, scr, icon->text, (int) strlen(icon->text), + fg, bg, &pos, NULL); + } + } + break; + + case wuss_ICON_TYPE_BUTTON: + { + colour_t light, dark, base; + int pressed; + + base = wuss->palette[icon->bg]; + light = wuss->palette[wuss->bevel_light]; + dark = wuss->palette[wuss->bevel_dark]; + pressed = icon->pressed; + + if (icon->flags & wuss_ICON_FLAGS_DISABLED) + fg = dark; /* greyed: label sinks toward the dark bevel shade */ + + if (pressed) + icon_bevel(scr, &b, base, dark, light); + else + icon_bevel(scr, &b, base, light, dark); + + if (have_font) + { + point_t pos; + int interior_w, split_point; + bmfont_width_t width; + + interior_w = (b.x1 - b.x0) - 2; + if (interior_w < 1) + interior_w = 1; + + bmfont_measure(wuss->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 - font_height) / 2; + if (pressed) + { + pos.x += 1; + pos.y += 1; + } + + bmfont_draw(wuss->font, scr, icon->text, (int) strlen(icon->text), + fg, base, &pos, NULL); + } + } + break; + } +} diff --git a/libraries/wuss/icon/free.c b/libraries/wuss/icon/free.c new file mode 100644 index 00000000..d7cbac7e --- /dev/null +++ b/libraries/wuss/icon/free.c @@ -0,0 +1,25 @@ +/* free.c -- wuss - free a window's whole icon store */ + +#include + +#ifdef FORTIFY +#include "fortify/fortify.h" +#endif + +#include "../impl.h" + +void wuss__icons_free(wuss_window_t *window) +{ + int i; + + for (i = 0; i < window->nicons; i++) + { + free(window->icons[i]->text); + free(window->icons[i]); + } + + free(window->icons); + window->icons = NULL; + window->nicons = 0; + window->cap_icons = 0; +} diff --git a/libraries/wuss/icon/get-bbox.c b/libraries/wuss/icon/get-bbox.c new file mode 100644 index 00000000..55bdbd8d --- /dev/null +++ b/libraries/wuss/icon/get-bbox.c @@ -0,0 +1,8 @@ +/* get-bbox.c -- wuss - read a work-area icon's bounding box */ + +#include "../impl.h" + +void wuss_icon_get_bbox(const wuss_icon_t *icon, box_t *bbox) +{ + *bbox = icon->bbox; +} diff --git a/libraries/wuss/icon/get-text.c b/libraries/wuss/icon/get-text.c new file mode 100644 index 00000000..2978cdb7 --- /dev/null +++ b/libraries/wuss/icon/get-text.c @@ -0,0 +1,8 @@ +/* get-text.c -- wuss - read a work-area icon's label */ + +#include "../impl.h" + +const char *wuss_icon_get_text(const wuss_icon_t *icon) +{ + return icon->text; +} diff --git a/libraries/wuss/icon/get-type.c b/libraries/wuss/icon/get-type.c new file mode 100644 index 00000000..84b34091 --- /dev/null +++ b/libraries/wuss/icon/get-type.c @@ -0,0 +1,8 @@ +/* get-type.c -- wuss - read a work-area icon's type */ + +#include "../impl.h" + +wuss_icon_type_t wuss_icon_get_type(const wuss_icon_t *icon) +{ + return icon->type; +} diff --git a/libraries/wuss/icon/get-window.c b/libraries/wuss/icon/get-window.c new file mode 100644 index 00000000..9921af6d --- /dev/null +++ b/libraries/wuss/icon/get-window.c @@ -0,0 +1,8 @@ +/* get-window.c -- wuss - read a work-area icon's owning window */ + +#include "../impl.h" + +wuss_window_t *wuss_icon_get_window(const wuss_icon_t *icon) +{ + return icon->window; +} diff --git a/libraries/wuss/icon/hit-test.c b/libraries/wuss/icon/hit-test.c new file mode 100644 index 00000000..d7579e21 --- /dev/null +++ b/libraries/wuss/icon/hit-test.c @@ -0,0 +1,28 @@ +/* hit-test.c -- wuss - work-area icon hit testing */ + +#include "geom/box.h" + +#include "../impl.h" + +wuss_icon_t *wuss__icon_hit_test(wuss_window_t *window, point_t doc_point) +{ + wuss_icon_t *it; + int i; + + /* last created wins, matching the draw order in redraw_window */ + for (i = window->nicons - 1; i >= 0; i--) + { + it = window->icons[i]; + + if (it->type != wuss_ICON_TYPE_BUTTON) + continue; + + if (it->flags & (wuss_ICON_FLAGS_HIDDEN | wuss_ICON_FLAGS_DISABLED)) + continue; + + if (box_contains_point(&it->bbox, doc_point.x, doc_point.y)) + return it; + } + + return NULL; +} diff --git a/libraries/wuss/icon/invalidate.c b/libraries/wuss/icon/invalidate.c new file mode 100644 index 00000000..91c9e47f --- /dev/null +++ b/libraries/wuss/icon/invalidate.c @@ -0,0 +1,10 @@ +/* invalidate.c -- wuss - mark a work-area icon's bbox dirty */ + +#include "../impl.h" + +void wuss__icon_invalidate(const wuss_icon_t *icon) +{ + /* the icon's bbox is already in the window-local (pre-scroll) coordinates + * wuss_window_invalidate expects */ + wuss_window_invalidate(icon->window, &icon->bbox); +} diff --git a/libraries/wuss/icon/screen-box.c b/libraries/wuss/icon/screen-box.c new file mode 100644 index 00000000..25440c7e --- /dev/null +++ b/libraries/wuss/icon/screen-box.c @@ -0,0 +1,17 @@ +/* screen-box.c -- wuss - work-area icon bbox to screen space */ + +#include "../impl.h" + +void wuss__icon_screen_box(const wuss_icon_t *icon, box_t *out) +{ + box_t content; + point_t scroll; + + wuss__content_box(icon->window, &content); + scroll = icon->window->scroll; + + out->x0 = content.x0 - scroll.x + icon->bbox.x0; + out->y0 = content.y0 - scroll.y + icon->bbox.y0; + out->x1 = content.x0 - scroll.x + icon->bbox.x1; + out->y1 = content.y0 - scroll.y + icon->bbox.y1; +} diff --git a/libraries/wuss/icon/set-hidden.c b/libraries/wuss/icon/set-hidden.c new file mode 100644 index 00000000..c76e35a8 --- /dev/null +++ b/libraries/wuss/icon/set-hidden.c @@ -0,0 +1,13 @@ +/* set-hidden.c -- wuss - show or hide a work-area icon */ + +#include "../impl.h" + +void wuss_icon_set_hidden(wuss_icon_t *icon, int hidden) +{ + if (hidden) + icon->flags |= wuss_ICON_FLAGS_HIDDEN; + else + icon->flags &= (wuss_icon_flags_t) ~wuss_ICON_FLAGS_HIDDEN; + + wuss__icon_invalidate(icon); +} diff --git a/libraries/wuss/icon/set-text.c b/libraries/wuss/icon/set-text.c new file mode 100644 index 00000000..3a5eca25 --- /dev/null +++ b/libraries/wuss/icon/set-text.c @@ -0,0 +1,32 @@ +/* set-text.c -- wuss - replace a work-area icon's label */ + +#include +#include + +#ifdef FORTIFY +#include "fortify/fortify.h" +#endif + +#include "../impl.h" + +result_t wuss_icon_set_text(wuss_icon_t *icon, const char *text) +{ + const char *src; + char *dup; + size_t len; + + src = (text != NULL) ? text : ""; + len = strlen(src); + + dup = malloc(len + 1); + if (dup == NULL) + return result_OOM; + memcpy(dup, src, len + 1); + + free(icon->text); + icon->text = dup; + + wuss__icon_invalidate(icon); + + return result_OK; +} diff --git a/libraries/wuss/impl.h b/libraries/wuss/impl.h index 7ab0ed0f..9a83a09d 100644 --- a/libraries/wuss/impl.h +++ b/libraries/wuss/impl.h @@ -14,6 +14,7 @@ #include "wuss/window.h" #include "furniture.h" +#include "icon.h" #define WUSS_TITLE_MAX 63 #define WUSS_DEFAULT_TITLEBAR_HEIGHT 20 @@ -25,7 +26,7 @@ * just some avoidable redraw work, never wrong */ #define WUSS_MAX_INVALIDATE_PIECES 32 -#define WUSS_ICON_INSET 3 /* shared by close/back/toggle/resize icons and scrollbar breadth */ +#define WUSS_BUTTON_INSET 3 /* shared by close/back/toggle/resize furniture buttons and scrollbar breadth */ #define WUSS_MIN_CONTENT 20 /* resize-drag floor: content can never be squeezed smaller than this */ #define WUSS_SCROLL_INSET 2 /* sausage cross-axis margin from its well's edges, purely cosmetic */ #define WUSS_DIVIDER_PX 1 /* interior rule between the content area and the furniture on its right/bottom */ @@ -47,6 +48,8 @@ struct wuss colour_t *palette; /* owned */ int npalette; wuss_palette_t furniture_colours; + 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 backdrop; /* wuss_NO_BACKGROUND for none */ int titlebar_height; list_t z_order; /* anchor; head = topmost window */ @@ -72,6 +75,9 @@ struct wuss_window wuss_window_state_t state; /* see wuss_window_state_t */ box_t pre_toggle; /* visible bounds to restore on the next toggle */ char title[WUSS_TITLE_MAX + 1]; + wuss_icon_t **icons; /* owned; array of owned icon pointers */ + int nicons; + int cap_icons; }; wuss_window_t *wuss__window_at(wuss_t *wuss, point_t p); @@ -169,22 +175,22 @@ static inline int wuss__outline_px(const wuss_window_t *window) * none, so NO_TITLEBAR windows that still opt into scrollbars/resize match * their titled siblings instead of a hardcoded size; the hardcoded default * is only a last-resort floor if even that isn't positive */ -static inline int wuss__icon_size_for(const wuss_t *wuss, wuss_window_flags_t flags) +static inline int wuss__button_size_for(const wuss_t *wuss, wuss_window_flags_t flags) { int size; - size = wuss__titlebar_height_for(wuss, flags) - 2 * WUSS_ICON_INSET; + size = wuss__titlebar_height_for(wuss, flags) - 2 * WUSS_BUTTON_INSET; if (size > 0) return size; - size = wuss->titlebar_height - 2 * WUSS_ICON_INSET; + size = wuss->titlebar_height - 2 * WUSS_BUTTON_INSET; - return (size > 0) ? size : WUSS_DEFAULT_TITLEBAR_HEIGHT - 2 * WUSS_ICON_INSET; + return (size > 0) ? size : WUSS_DEFAULT_TITLEBAR_HEIGHT - 2 * WUSS_BUTTON_INSET; } -static inline int wuss__icon_size(const wuss_window_t *window) +static inline int wuss__button_size(const wuss_window_t *window) { - return wuss__icon_size_for(window->wuss, window->flags); + return wuss__button_size_for(window->wuss, window->flags); } /* how much of a content box's width/height is furniture (scrollbars, the @@ -193,18 +199,18 @@ static inline int wuss__icon_size(const wuss_window_t *window) * visible) and window creation/resize (add it to visible up front) so the * two stay consistent with each other */ static inline void wuss__furniture_carve_for(wuss_window_flags_t flags, - int icon_size, + int button_size, point_t *carve) { - carve->x = (flags & wuss_WINDOW_NO_VSCROLL) ? 0 : icon_size; - carve->y = (flags & wuss_WINDOW_NO_HSCROLL) ? 0 : icon_size; + carve->x = (flags & wuss_WINDOW_NO_VSCROLL) ? 0 : button_size; + carve->y = (flags & wuss_WINDOW_NO_HSCROLL) ? 0 : button_size; if (!(flags & wuss_WINDOW_NO_RESIZE) && (flags & wuss_WINDOW_NO_VSCROLL) && (flags & wuss_WINDOW_NO_HSCROLL)) { - carve->x = icon_size; - carve->y = icon_size; + carve->x = button_size; + carve->y = button_size; } /* where furniture abuts the content area, a rule divides the two */ diff --git a/libraries/wuss/mouse-click.c b/libraries/wuss/mouse-click.c index 745f5e7b..634f907d 100644 --- a/libraries/wuss/mouse-click.c +++ b/libraries/wuss/mouse-click.c @@ -154,14 +154,40 @@ result_t wuss_mouse_click(wuss_t *wuss, if (win->task.handle != NULL) { - box_t content; + box_t content; + point_t doc_point; + wuss_icon_t *icon; wuss__content_box(win, &content); - event.kind = wuss_EVENT_MOUSE; - event.data.mouse.action = action; - event.data.mouse.point.x = x - content.x0 + win->scroll.x; - event.data.mouse.point.y = y - content.y0 + win->scroll.y; - event.data.mouse.button = button; + doc_point.x = x - content.x0 + win->scroll.x; + doc_point.y = y - content.y0 + win->scroll.y; + + icon = wuss__icon_hit_test(win, doc_point); + if (icon != NULL) + { + if (action == wuss_MOUSE_DOWN && + (button & (wuss_BUTTON_SELECT | wuss_BUTTON_ADJUST))) + { + icon->pressed = 1; + wuss__icon_invalidate(icon); + } + else if (action == wuss_MOUSE_UP && icon->pressed) + { + icon->pressed = 0; + wuss__icon_invalidate(icon); + } + + event.kind = wuss_EVENT_ICON; + event.data.icon.icon = icon; + event.data.icon.action = action; + event.data.icon.button = button; + return win->task.handle(win, &event, win->task.task_data); + } + + event.kind = wuss_EVENT_MOUSE; + event.data.mouse.action = action; + event.data.mouse.point = doc_point; + event.data.mouse.button = button; return win->task.handle(win, &event, win->task.task_data); } diff --git a/libraries/wuss/mouse-move.c b/libraries/wuss/mouse-move.c index 9b73076d..b5e644e3 100644 --- a/libraries/wuss/mouse-move.c +++ b/libraries/wuss/mouse-move.c @@ -52,14 +52,45 @@ result_t wuss_mouse_move(wuss_t *wuss, point_t p, wuss_window_t **hit) if (win->task.handle != NULL) { box_t content; + point_t doc_point; + wuss_icon_t *icon; wuss_event_t event; + int k; wuss__content_box(win, &content); - event.kind = wuss_EVENT_MOUSE; - event.data.mouse.action = wuss_MOUSE_MOVE; - event.data.mouse.point.x = x - content.x0 + win->scroll.x; - event.data.mouse.point.y = y - content.y0 + win->scroll.y; - event.data.mouse.button = wuss_BUTTON_SELECT; + doc_point.x = x - content.x0 + win->scroll.x; + doc_point.y = y - content.y0 + win->scroll.y; + + icon = wuss__icon_hit_test(win, doc_point); + + /* Clear the pressed state of any button the pointer has left. This does + * not re-press a button on drag-back-in, and does not track which mouse + * button is held -- wuss keeps no persistent "button down over content" + * state. */ + for (k = 0; k < win->nicons; k++) + { + wuss_icon_t *it = win->icons[k]; + + if (it->pressed && it != icon) + { + it->pressed = 0; + wuss__icon_invalidate(it); + } + } + + if (icon != NULL) + { + event.kind = wuss_EVENT_ICON; + event.data.icon.icon = icon; + event.data.icon.action = wuss_MOUSE_MOVE; + event.data.icon.button = wuss_BUTTON_SELECT; + return win->task.handle(win, &event, win->task.task_data); + } + + event.kind = wuss_EVENT_MOUSE; + event.data.mouse.action = wuss_MOUSE_MOVE; + event.data.mouse.point = doc_point; + event.data.mouse.button = wuss_BUTTON_SELECT; return win->task.handle(win, &event, win->task.task_data); } diff --git a/libraries/wuss/redraw.c b/libraries/wuss/redraw.c index 769257a9..e444b126 100644 --- a/libraries/wuss/redraw.c +++ b/libraries/wuss/redraw.c @@ -51,6 +51,15 @@ static void redraw_window(wuss_t *wuss, if (crc != result_OK) *rc = crc; } + + { + int k; + + /* draw in array order so later-created icons paint on top, matching + * wuss__icon_hit_test's reverse scan */ + for (k = 0; k < win->nicons; k++) + wuss__icon_draw(wuss, win->icons[k], &content, win->scroll); + } } } diff --git a/libraries/wuss/test/tasks/icons.c b/libraries/wuss/test/tasks/icons.c new file mode 100644 index 00000000..a16f7b47 --- /dev/null +++ b/libraries/wuss/test/tasks/icons.c @@ -0,0 +1,189 @@ +/* icons.c -- wuss test - work-area icons task */ + +#ifdef USE_SDL +#include "framebuf/palettes.h" + +#include +#include + +#ifdef FORTIFY +#include "fortify/fortify.h" +#endif + +#include "framebuf/palettes.h" +#include "framebuf/screen.h" +#include "geom/box.h" +#include "geom/point.h" +#include "geom/size.h" + +#include "icons.h" + +#define ICONS_DOC_W 220 +#define ICONS_DOC_H 520 /* taller than the window, so scrolling is exercised */ + +result_t icons_create(wuss_t *wuss, + const colour_t *palette, + bmfont_t *font, + icons_task_t *task) +{ + wuss_task_t delegate; + box_t box; + wuss_icon_spec_t spec; + result_t rc; + + task->font = font; + task->ink = palette[palette_PICO8_LAVENDER]; + task->window = NULL; + task->button = NULL; + task->counter = NULL; + task->count = 0; + + delegate = wuss_task_start(icons_handle, task); + box = (box_t) BOX_POS_SIZE(160, 120, ICONS_DOC_W, 160); + + rc = wuss_window_create(wuss, + &box, + "Icons", + wuss_WINDOW_NONE, + palette_PICO8_LIGHT_GREY, + &delegate, + SIZE2D(ICONS_DOC_W, ICONS_DOC_H), + SIZE2D(0, 0), + &task->window); + if (rc != result_OK) + return rc; + + memset(&spec, 0, sizeof(spec)); + + /* a heading label */ + spec.bbox = (box_t) BOX_POS_SIZE(8, 8, 180, 14); + spec.type = wuss_ICON_TYPE_LABEL; + spec.text = "Work-area icons:"; + spec.fg = palette_PICO8_DARK_BLUE; + spec.bg = wuss_NO_BACKGROUND; + spec.flags = wuss_ICON_FLAGS_NONE; + rc = wuss_icon_create(task->window, &spec, NULL); + if (rc != result_OK) + goto failure; + + /* the button that bumps the counter */ + spec.bbox = (box_t) BOX_POS_SIZE(8, 30, 80, 22); + spec.type = wuss_ICON_TYPE_BUTTON; + spec.text = "Press me"; + spec.fg = palette_PICO8_BLACK; + spec.bg = palette_PICO8_LIGHT_GREY; + rc = wuss_icon_create(task->window, &spec, &task->button); + if (rc != result_OK) + goto failure; + + /* the counter label beside it */ + spec.bbox = (box_t) BOX_POS_SIZE(100, 30, 140, 52); + spec.type = wuss_ICON_TYPE_LABEL; + spec.text = "0"; + spec.fg = palette_PICO8_DARK_BLUE; + spec.bg = wuss_NO_BACKGROUND; + rc = wuss_icon_create(task->window, &spec, &task->counter); + if (rc != result_OK) + goto failure; + + /* a button far down the document, to prove icons scroll and stay clickable */ + spec.bbox = (box_t) BOX_POS_SIZE(8, 460, 90, 52); + spec.type = wuss_ICON_TYPE_BUTTON; + spec.text = "Scrolled"; + spec.fg = palette_PICO8_BLACK; + spec.bg = palette_PICO8_LIGHT_GREY; + rc = wuss_icon_create(task->window, &spec, NULL); + if (rc != result_OK) + goto failure; + + return result_OK; + +failure: + wuss_window_close(task->window); + task->window = NULL; + return rc; +} + +void icons_destroy(icons_task_t *task) +{ + wuss_window_close(task->window); +} + +static result_t icons_redraw(const wuss_event_t *event, void *task_data) +{ + icons_task_t *tcx; + screen_t *scr; + const box_t *content; + const box_t *bounds; + point_t scroll; + int phase; + int first; + int x; + + tcx = task_data; + + scr = event->data.redraw.scr; + content = event->data.redraw.content; + bounds = event->data.redraw.bounds; + scroll = event->data.redraw.scroll; + + /* faint vertical rules every 16 document units, to show the task still + * paints under and around the wuss-managed icons -- anchored to the + * document so they track the scroll offset. The screen x of document + * x=d is bounds->x0 - scroll.x + d, so the rules land on screen + * columns congruent to (bounds->x0 - scroll.x) modulo 16. */ + phase = (bounds->x0 - scroll.x) % 16; + if (phase < 0) + phase += 16; + first = content->x0 - ((content->x0 - phase) % 16 + 16) % 16; + for (x = first; x < content->x1; x += 16) + screen_draw_line(scr, x, content->y0, x, content->y1 - 1, tcx->ink); + + return result_OK; +} + +static result_t icons_icon(const wuss_event_t *event, void *task_data) +{ + icons_task_t *tcx; + char buf[16]; + + tcx = task_data; + + if (event->data.icon.action != wuss_MOUSE_DOWN) + return result_OK; + if (event->data.icon.icon != tcx->button) + return result_OK; + + tcx->count++; + snprintf(buf, sizeof(buf), "%d", tcx->count); + + return wuss_icon_set_text(tcx->counter, buf); +} + +result_t icons_handle(wuss_window_t *window, + const wuss_event_t *event, + void *task_data) +{ + icons_task_t *tcx; + + tcx = task_data; + + switch (event->kind) + { + case wuss_EVENT_REDRAW: + return icons_redraw(event, task_data); + + case wuss_EVENT_ICON: + return icons_icon(event, task_data); + + case wuss_EVENT_CLOSE: + wuss_window_close(window); + tcx->window = NULL; + return result_OK; + + default: + return result_OK; + } +} + +#endif /* USE_SDL */ diff --git a/libraries/wuss/test/tasks/icons.h b/libraries/wuss/test/tasks/icons.h new file mode 100644 index 00000000..24da9a89 --- /dev/null +++ b/libraries/wuss/test/tasks/icons.h @@ -0,0 +1,40 @@ +/* icons.h -- wuss test - work-area icons task */ + +#ifndef TASKS_ICONS_H +#define TASKS_ICONS_H + +#ifdef USE_SDL + +#include "framebuf/bmfont.h" +#include "framebuf/colour.h" +#include "wuss/icon.h" +#include "wuss/window.h" + +/* demonstrates wuss-managed work-area icons: a couple of labels, a button that + * bumps a counter, and a second button placed far down the document to show + * icons scroll with the content and stay clickable */ +typedef struct icons_task +{ + wuss_window_t *window; + bmfont_t *font; + colour_t ink; + wuss_icon_t *button; /* "Press me" */ + wuss_icon_t *counter; /* label showing hit count */ + int count; +} +icons_task_t; + +wuss_event_fn_t icons_handle; + +/* create the icons window against the given wuss instance */ +result_t icons_create(wuss_t *wuss, + const colour_t *palette, + bmfont_t *font, + icons_task_t *task); + +/* destroy the icons window created by icons_create */ +void icons_destroy(icons_task_t *task); + +#endif /* USE_SDL */ + +#endif /* TASKS_ICONS_H */ diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 686cd217..05f44151 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -35,6 +35,7 @@ #include "tasks/checker.h" #include "tasks/curve.h" #include "tasks/gradient.h" +#include "tasks/icons.h" #include "tasks/image.h" #include "tasks/launcher.h" #include "tasks/palette.h" @@ -66,6 +67,7 @@ static checker_task_t g_checker_task; static curve_task_t g_curve_task; static sofa_task_t g_sofa_task; static gradient_task_t g_gradient_task; +static icons_task_t g_icons_task; static porter_duff_task_t g_porter_duff_task; static result_t spawn_ball(void) { return ball_create(g_wuss, g_palette, &g_ball_task); } @@ -78,6 +80,7 @@ static result_t spawn_checker(void) { return checker_create(g_wuss, g_palett static result_t spawn_curve(void) { return curve_create(g_wuss, g_palette, &g_curve_task); } static result_t spawn_sofa(void) { return sofa_create(g_wuss, g_palette, &g_sofa_task); } static result_t spawn_gradient(void) { return gradient_create(g_wuss, &g_gradient_task); } +static result_t spawn_icons(void) { return icons_create(g_wuss, g_palette, g_daydream_font, &g_icons_task); } static result_t spawn_porter_duff(void) { return porter_duff_create(g_wuss, g_palette, g_daydream_font, g_resources, &g_porter_duff_task); } static void destroy_ball(void) { ball_destroy(&g_ball_task); } @@ -90,6 +93,7 @@ static void destroy_checker(void) { checker_destroy(&g_checker_task); } static void destroy_curve(void) { curve_destroy(&g_curve_task); } static void destroy_sofa(void) { sofa_destroy(&g_sofa_task); } static void destroy_gradient(void) { gradient_destroy(&g_gradient_task); } +static void destroy_icons(void) { icons_destroy(&g_icons_task); } static void destroy_porter_duff(void) { porter_duff_destroy(&g_porter_duff_task); } static const launcher_entry_t g_launcher_entries[] = @@ -104,6 +108,7 @@ static const launcher_entry_t g_launcher_entries[] = { "Curve", spawn_curve, destroy_curve }, { "Sofa", spawn_sofa, destroy_sofa }, { "Gradient", spawn_gradient, destroy_gradient }, + { "Icons", spawn_icons, destroy_icons }, { "Porter-Duff", spawn_porter_duff, destroy_porter_duff } }; @@ -247,6 +252,8 @@ static result_t wuss_interactive_test(const char *resources) config.palette.scroll.arrows = palette_PICO8_BLUE; config.palette.scroll.wells = palette_PICO8_DARK_BLUE; config.palette.scroll.sausages = palette_PICO8_LIGHT_GREY; + config.bevel.light = palette_PICO8_WHITE; + config.bevel.dark = palette_PICO8_DARK_GREY; config.backdrop = palette_PICO8_LIGHT_GREY; rc = wuss_create(&scr, font, palette, NELEMS(palette), &config, &wuss); @@ -1496,7 +1503,7 @@ result_t wuss_test(const char *resources) goto Failure; /* toggle icon: top-right of the titlebar, inset by 3px, sized 20 - 2*3 - * (default titlebar height 20, WUSS_ICON_INSET 3), matching + * (default titlebar height 20, WUSS_BUTTON_INSET 3), matching * wuss__toggle_box's formula -- mirrored here since the test only sees * the public API */ outline_px = 1; diff --git a/libraries/wuss/window/close.c b/libraries/wuss/window/close.c index 252158cd..411b9e00 100644 --- a/libraries/wuss/window/close.c +++ b/libraries/wuss/window/close.c @@ -23,5 +23,7 @@ void wuss_window_close(wuss_window_t *doomed) list_remove(&wuss->z_order, &doomed->link); + wuss__icons_free(doomed); + free(doomed); } diff --git a/libraries/wuss/window/create.c b/libraries/wuss/window/create.c index 421ab08e..c29e87f2 100644 --- a/libraries/wuss/window/create.c +++ b/libraries/wuss/window/create.c @@ -40,7 +40,7 @@ result_t wuss_window_create(wuss_t *wuss, outline_px = wuss__outline_px_for(flags); titlebar_height = wuss__titlebar_height_for(wuss, flags); - wuss__furniture_carve_for(flags, wuss__icon_size_for(wuss, flags), &carve); + wuss__furniture_carve_for(flags, wuss__button_size_for(wuss, flags), &carve); win->wuss = wuss; win->visible.x0 = content->x0 - outline_px; @@ -79,6 +79,9 @@ result_t wuss_window_create(wuss_t *wuss, win->doc = doc; win->min_doc = min_doc; win->state = wuss_WINDOW_STATE_NONE; + win->icons = NULL; + win->nicons = 0; + win->cap_icons = 0; if (task != NULL) win->task = *task; diff --git a/libraries/wuss/window/resize.c b/libraries/wuss/window/resize.c index 46e6e7ff..4e0fd995 100644 --- a/libraries/wuss/window/resize.c +++ b/libraries/wuss/window/resize.c @@ -42,7 +42,7 @@ result_t wuss_window_resize(wuss_window_t *window, size2d_t size) outline_px = wuss__outline_px(window); titlebar_height = wuss__titlebar_height(window); before = window->visible; - wuss__furniture_carve_for(window->flags, wuss__icon_size(window), &carve); + wuss__furniture_carve_for(window->flags, wuss__button_size(window), &carve); window->visible.x1 = window->visible.x0 + size.w + 2 * outline_px + carve.x; window->visible.y1 = window->visible.y0 + size.h + titlebar_height + 2 * outline_px + carve.y; From 43af586a5d47545b27e6e0570b5be3d3c7631a2e Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sat, 29 Aug 2026 22:08:42 +0100 Subject: [PATCH 061/287] fix(wuss): scroll work-area grid and axis rulers with the document The icons task drew its backdrop grid and x/y axis rulers pinned to the window edge, so Wuss's scroll blit displaced the painted pixels without repainting them, smearing the labels. Anchor all task drawing to document space so it scrolls rigidly with the content, as the blit assumes; add a bmfont-drawn coordinate ruler along the document x=0 and y=0 lines. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/test/tasks/icons.c | 98 ++++++++++++++++++++++++++----- libraries/wuss/test/tasks/icons.h | 4 +- 2 files changed, 85 insertions(+), 17 deletions(-) diff --git a/libraries/wuss/test/tasks/icons.c b/libraries/wuss/test/tasks/icons.c index a16f7b47..bb10155a 100644 --- a/libraries/wuss/test/tasks/icons.c +++ b/libraries/wuss/test/tasks/icons.c @@ -33,6 +33,8 @@ result_t icons_create(wuss_t *wuss, task->font = font; task->ink = palette[palette_PICO8_LAVENDER]; + task->label = palette[palette_PICO8_DARK_BLUE]; + task->paper = palette[palette_PICO8_LIGHT_GREY]; /* the window bg, below */ task->window = NULL; task->button = NULL; task->counter = NULL; @@ -55,8 +57,11 @@ result_t icons_create(wuss_t *wuss, memset(&spec, 0, sizeof(spec)); + /* icons sit past the ruler gutter (see ICONS_GUTTER in icons_redraw) so the + * axis labels have the top/left strip to themselves */ + /* a heading label */ - spec.bbox = (box_t) BOX_POS_SIZE(8, 8, 180, 14); + spec.bbox = (box_t) BOX_POS_SIZE(28, 28, 180, 14); spec.type = wuss_ICON_TYPE_LABEL; spec.text = "Work-area icons:"; spec.fg = palette_PICO8_DARK_BLUE; @@ -67,7 +72,7 @@ result_t icons_create(wuss_t *wuss, goto failure; /* the button that bumps the counter */ - spec.bbox = (box_t) BOX_POS_SIZE(8, 30, 80, 22); + spec.bbox = (box_t) BOX_POS_SIZE(28, 50, 80, 22); spec.type = wuss_ICON_TYPE_BUTTON; spec.text = "Press me"; spec.fg = palette_PICO8_BLACK; @@ -77,7 +82,7 @@ result_t icons_create(wuss_t *wuss, goto failure; /* the counter label beside it */ - spec.bbox = (box_t) BOX_POS_SIZE(100, 30, 140, 52); + spec.bbox = (box_t) BOX_POS_SIZE(120, 50, 120, 22); spec.type = wuss_ICON_TYPE_LABEL; spec.text = "0"; spec.fg = palette_PICO8_DARK_BLUE; @@ -87,7 +92,7 @@ result_t icons_create(wuss_t *wuss, goto failure; /* a button far down the document, to prove icons scroll and stay clickable */ - spec.bbox = (box_t) BOX_POS_SIZE(8, 460, 90, 52); + spec.bbox = (box_t) BOX_POS_SIZE(28, 460, 90, 52); spec.type = wuss_ICON_TYPE_BUTTON; spec.text = "Scrolled"; spec.fg = palette_PICO8_BLACK; @@ -109,6 +114,24 @@ void icons_destroy(icons_task_t *task) wuss_window_close(task->window); } +#define ICONS_GRID 16 /* document-space pitch of the backdrop grid */ +#define ICONS_AXIS_LABEL 64 /* label every Nth grid line along each axis */ + +/* Screen coordinate of the first grid line at or after lo. Grid lines sit at + * document multiples of ICONS_GRID; the screen coordinate of document d is + * origin - scroll + d, so lines fall on screen coordinates congruent to + * (origin - scroll) modulo ICONS_GRID. */ +static int icons_grid_first(int origin, int scroll, int lo) +{ + int phase; + + phase = (origin - scroll) % ICONS_GRID; + if (phase < 0) + phase += ICONS_GRID; + + return lo - ((lo - phase) % ICONS_GRID + ICONS_GRID) % ICONS_GRID; +} + static result_t icons_redraw(const wuss_event_t *event, void *task_data) { icons_task_t *tcx; @@ -116,9 +139,13 @@ static result_t icons_redraw(const wuss_event_t *event, void *task_data) const box_t *content; const box_t *bounds; point_t scroll; - int phase; - int first; + point_t pos; + char buf[16]; + int ox; /* screen x of document x=0 */ + int oy; /* screen y of document y=0 */ + int doc; int x; + int y; tcx = task_data; @@ -127,18 +154,57 @@ static result_t icons_redraw(const wuss_event_t *event, void *task_data) bounds = event->data.redraw.bounds; scroll = event->data.redraw.scroll; - /* faint vertical rules every 16 document units, to show the task still - * paints under and around the wuss-managed icons -- anchored to the - * document so they track the scroll offset. The screen x of document - * x=d is bounds->x0 - scroll.x + d, so the rules land on screen - * columns congruent to (bounds->x0 - scroll.x) modulo 16. */ - phase = (bounds->x0 - scroll.x) % 16; - if (phase < 0) - phase += 16; - first = content->x0 - ((content->x0 - phase) % 16 + 16) % 16; - for (x = first; x < content->x1; x += 16) + ox = bounds->x0 - scroll.x; + oy = bounds->y0 - scroll.y; + + /* Everything this task paints is anchored to the document, not the window, + * so it scrolls rigidly with the content -- which is what Wuss's scroll + * blit assumes. Nothing here is pinned to a window edge. Every draw is + * clipped to the dirty rectangle (content) so partial redraws stay cheap. */ + + /* a faint grid across the whole work area */ + for (x = icons_grid_first(bounds->x0, scroll.x, content->x0); + x < content->x1; + x += ICONS_GRID) screen_draw_line(scr, x, content->y0, x, content->y1 - 1, tcx->ink); + for (y = icons_grid_first(bounds->y0, scroll.y, content->y0); + y < content->y1; + y += ICONS_GRID) + screen_draw_line(scr, content->x0, y, content->x1 - 1, y, tcx->ink); + + /* x-axis ruler: document x printed just below the y=0 line, at each + * labelled grid column. Scrolls with the document like the grid. */ + for (x = icons_grid_first(bounds->x0, scroll.x, content->x0); + x < content->x1; + x += ICONS_GRID) + { + doc = x - ox; + if (doc <= 0 || doc % ICONS_AXIS_LABEL != 0) + continue; + + snprintf(buf, sizeof(buf), "%d", doc); + pos = POINT(x + 2, oy + 2); + bmfont_draw(tcx->font, scr, buf, (int) strlen(buf), + tcx->label, tcx->paper, &pos, NULL); + } + + /* y-axis ruler: document y printed just right of the x=0 line, at each + * labelled grid row. */ + for (y = icons_grid_first(bounds->y0, scroll.y, content->y0); + y < content->y1; + y += ICONS_GRID) + { + doc = y - oy; + if (doc <= 0 || doc % ICONS_AXIS_LABEL != 0) + continue; + + snprintf(buf, sizeof(buf), "%d", doc); + pos = POINT(ox + 2, y + 2); + bmfont_draw(tcx->font, scr, buf, (int) strlen(buf), + tcx->label, tcx->paper, &pos, NULL); + } + return result_OK; } diff --git a/libraries/wuss/test/tasks/icons.h b/libraries/wuss/test/tasks/icons.h index 24da9a89..e683142f 100644 --- a/libraries/wuss/test/tasks/icons.h +++ b/libraries/wuss/test/tasks/icons.h @@ -17,7 +17,9 @@ typedef struct icons_task { wuss_window_t *window; bmfont_t *font; - colour_t ink; + colour_t ink; /* grid lines */ + colour_t label; /* axis coordinate text */ + colour_t paper; /* window bg, for bmfont_draw glyph blending */ wuss_icon_t *button; /* "Press me" */ wuss_icon_t *counter; /* label showing hit count */ int count; From 63a6fdea0318fec93c316367e052d18143ca0299 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sat, 29 Aug 2026 23:01:52 +0100 Subject: [PATCH 062/287] feat(wuss): add wuss_icon_create_array for bulk icon creation Loops wuss_icon_create over a spec array with all-or-nothing rollback: on the first failure any icons already created by the call are destroyed and no handles are written. Converts the icons test task to use it. Co-Authored-By: Claude Sonnet 5 --- CMakeLists.txt | 1 + include/wuss/icon.h | 20 ++++++++ libraries/wuss/icon/create-array.c | 43 ++++++++++++++++++ libraries/wuss/test/tasks/icons.c | 73 ++++++++++++++---------------- 4 files changed, 98 insertions(+), 39 deletions(-) create mode 100644 libraries/wuss/icon/create-array.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 8861523b..f71da178 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -338,6 +338,7 @@ set(WUSS_SOURCES libraries/wuss/furniture.h libraries/wuss/get-font.c libraries/wuss/icon/create.c + libraries/wuss/icon/create-array.c libraries/wuss/icon/delete.c libraries/wuss/icon/draw.c libraries/wuss/icon/free.c diff --git a/include/wuss/icon.h b/include/wuss/icon.h index ce8c3ea7..ee931169 100644 --- a/include/wuss/icon.h +++ b/include/wuss/icon.h @@ -109,6 +109,26 @@ result_t wuss_icon_create(wuss_window_t *window, const wuss_icon_spec_t *spec, wuss_icon_t **icon); +/** + * Create several icons on a window in one call, as if by \ref wuss_icon_create + * for each. Either all \c nspecs icons are created, or none are: on the first + * failure any icons already created by this call are destroyed and no handles + * are written. + * + * \param[in] window Window to attach the icons to. + * \param[in] specs Array of \c nspecs icon descriptions; each copied. + * \param[in] nspecs Number of entries in \c specs. Zero is a no-op. + * \param[out] icons Array of \c nspecs handles, filled in on success, or NULL + * if the caller does not need them. Untouched on failure. + * \return \ref result_OK on success, or the first failing \ref wuss_icon_create + * code (\ref result_OOM, \ref result_WUSS_BAD_COLOUR, \ref + * result_WUSS_BAD_ICON). + */ +result_t wuss_icon_create_array(wuss_window_t *window, + const wuss_icon_spec_t *specs, + int nspecs, + wuss_icon_t **icons); + /** * Destroy an icon, unlinking it from its window and invalidating its bounding * box so the next redraw clears it. Safe to pass NULL. diff --git a/libraries/wuss/icon/create-array.c b/libraries/wuss/icon/create-array.c new file mode 100644 index 00000000..5da88935 --- /dev/null +++ b/libraries/wuss/icon/create-array.c @@ -0,0 +1,43 @@ +/* create-array.c -- wuss - create several work-area icons at once */ + +#include +#include + +#ifdef FORTIFY +#include "fortify/fortify.h" +#endif + +#include "../impl.h" + +result_t wuss_icon_create_array(wuss_window_t *window, + const wuss_icon_spec_t *specs, + int nspecs, + wuss_icon_t **icons) +{ + result_t rc; + int i; + int j; + + assert(window != NULL); + assert(specs != NULL || nspecs == 0); + + for (i = 0; i < nspecs; i++) + { + wuss_icon_t *it; + + rc = wuss_icon_create(window, &specs[i], &it); + if (rc != result_OK) + { + /* all-or-nothing: unwind the icons this call already created. They are + * the last (i) entries on the window's icon list, newest last. */ + for (j = 0; j < i; j++) + wuss_icon_delete(window->icons[window->nicons - 1]); + return rc; + } + + if (icons != NULL) + icons[i] = it; + } + + return result_OK; +} diff --git a/libraries/wuss/test/tasks/icons.c b/libraries/wuss/test/tasks/icons.c index bb10155a..a183a0cb 100644 --- a/libraries/wuss/test/tasks/icons.c +++ b/libraries/wuss/test/tasks/icons.c @@ -28,7 +28,8 @@ result_t icons_create(wuss_t *wuss, { wuss_task_t delegate; box_t box; - wuss_icon_spec_t spec; + wuss_icon_spec_t specs[4]; + wuss_icon_t *made[4]; result_t rc; task->font = font; @@ -55,51 +56,45 @@ result_t icons_create(wuss_t *wuss, if (rc != result_OK) return rc; - memset(&spec, 0, sizeof(spec)); + memset(specs, 0, sizeof(specs)); /* icons sit past the ruler gutter (see ICONS_GUTTER in icons_redraw) so the * axis labels have the top/left strip to themselves */ - /* a heading label */ - spec.bbox = (box_t) BOX_POS_SIZE(28, 28, 180, 14); - spec.type = wuss_ICON_TYPE_LABEL; - spec.text = "Work-area icons:"; - spec.fg = palette_PICO8_DARK_BLUE; - spec.bg = wuss_NO_BACKGROUND; - spec.flags = wuss_ICON_FLAGS_NONE; - rc = wuss_icon_create(task->window, &spec, NULL); + /* [0] a heading label */ + specs[0].bbox = (box_t) BOX_POS_SIZE(28, 28, 180, 14); + specs[0].type = wuss_ICON_TYPE_LABEL; + specs[0].text = "Work-area icons:"; + specs[0].fg = palette_PICO8_DARK_BLUE; + specs[0].bg = wuss_NO_BACKGROUND; + + /* [1] the button that bumps the counter */ + specs[1].bbox = (box_t) BOX_POS_SIZE(28, 50, 80, 22); + specs[1].type = wuss_ICON_TYPE_BUTTON; + specs[1].text = "Press me"; + specs[1].fg = palette_PICO8_BLACK; + specs[1].bg = palette_PICO8_LIGHT_GREY; + + /* [2] the counter label beside it */ + specs[2].bbox = (box_t) BOX_POS_SIZE(120, 50, 120, 22); + specs[2].type = wuss_ICON_TYPE_LABEL; + specs[2].text = "0"; + specs[2].fg = palette_PICO8_DARK_BLUE; + specs[2].bg = wuss_NO_BACKGROUND; + + /* [3] a button far down the document, to prove icons scroll and stay clickable */ + specs[3].bbox = (box_t) BOX_POS_SIZE(28, 460, 90, 52); + specs[3].type = wuss_ICON_TYPE_BUTTON; + specs[3].text = "Scrolled"; + specs[3].fg = palette_PICO8_BLACK; + specs[3].bg = palette_PICO8_LIGHT_GREY; + + rc = wuss_icon_create_array(task->window, specs, 4, made); if (rc != result_OK) goto failure; - /* the button that bumps the counter */ - spec.bbox = (box_t) BOX_POS_SIZE(28, 50, 80, 22); - spec.type = wuss_ICON_TYPE_BUTTON; - spec.text = "Press me"; - spec.fg = palette_PICO8_BLACK; - spec.bg = palette_PICO8_LIGHT_GREY; - rc = wuss_icon_create(task->window, &spec, &task->button); - if (rc != result_OK) - goto failure; - - /* the counter label beside it */ - spec.bbox = (box_t) BOX_POS_SIZE(120, 50, 120, 22); - spec.type = wuss_ICON_TYPE_LABEL; - spec.text = "0"; - spec.fg = palette_PICO8_DARK_BLUE; - spec.bg = wuss_NO_BACKGROUND; - rc = wuss_icon_create(task->window, &spec, &task->counter); - if (rc != result_OK) - goto failure; - - /* a button far down the document, to prove icons scroll and stay clickable */ - spec.bbox = (box_t) BOX_POS_SIZE(28, 460, 90, 52); - spec.type = wuss_ICON_TYPE_BUTTON; - spec.text = "Scrolled"; - spec.fg = palette_PICO8_BLACK; - spec.bg = palette_PICO8_LIGHT_GREY; - rc = wuss_icon_create(task->window, &spec, NULL); - if (rc != result_OK) - goto failure; + task->button = made[1]; + task->counter = made[2]; return result_OK; From 85a004d525d8bce8cab806447fae1d3c2943d136 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sat, 29 Aug 2026 23:32:31 +0100 Subject: [PATCH 063/287] test(wuss): pad the image task with a border and min-doc floor Adds a BORDER margin around the bitmap, a min-doc resize floor, and a pink background so the border and transparent pixels are visible. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/test/tasks/image.c | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/libraries/wuss/test/tasks/image.c b/libraries/wuss/test/tasks/image.c index 22d5d801..a7a07b9a 100644 --- a/libraries/wuss/test/tasks/image.c +++ b/libraries/wuss/test/tasks/image.c @@ -15,6 +15,8 @@ #include "image.h" +#define BORDER 16 + result_t image_create(wuss_t *wuss, const colour_t *palette, const char *resources, @@ -25,6 +27,7 @@ result_t image_create(wuss_t *wuss, wuss_task_t delegate; box_t box; result_t rc; + size2d_t sz; leafname = path_join_leafname("jessica", "png"); filename = path_join_filename(resources, 3, "resources", "images", leafname); @@ -33,17 +36,21 @@ result_t image_create(wuss_t *wuss, return rc; delegate = wuss_task_start(image_handle, task); /* shows through the image's transparent pixels */ + + sz.w = task->bitmap.size.w + BORDER * 2; + sz.h = task->bitmap.size.h + BORDER * 2; + /* shorter than the bitmap so there's something to scroll through */ - box = (box_t) BOX_POS_SIZE(370, 10, task->bitmap.size.w, task->bitmap.size.h * 2 / 3); + box = (box_t) BOX_POS_SIZE(370, 10, sz.w, sz.h * 2 / 3); return wuss_window_create(wuss, &box, "Image", wuss_WINDOW_NONE, - palette_PICO8_BLACK, + palette_PICO8_PINK, &delegate, - SIZE2D(task->bitmap.size.w, task->bitmap.size.h), - SIZE2D(0, 0), + sz, + SIZE2D(32, 32), &task->window); } @@ -67,7 +74,7 @@ static result_t image_redraw(const wuss_event_t *event, void *task_data) sx = event->data.redraw.scroll.x; sy = event->data.redraw.scroll.y; - screen_draw_bitmap(scr, bounds->x0 - sx, bounds->y0 - sy, &ic->bitmap); + screen_draw_bitmap(scr, bounds->x0 - sx + BORDER, bounds->y0 - sy + BORDER, &ic->bitmap); return result_OK; } From ac5e5cc913a611bf33959c327c56f48e382438ef Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sat, 29 Aug 2026 23:35:23 +0100 Subject: [PATCH 064/287] refactor(wuss): pass the image path into image_create image_create took the repo root and built the PNG path itself; it now takes the full filename, with path assembly moved to the spawn_image caller in wuss-test.c. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/test/tasks/image.c | 9 ++------- libraries/wuss/test/tasks/image.h | 5 ++--- libraries/wuss/test/wuss-test.c | 11 ++++++++++- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/libraries/wuss/test/tasks/image.c b/libraries/wuss/test/tasks/image.c index a7a07b9a..7a6501e4 100644 --- a/libraries/wuss/test/tasks/image.c +++ b/libraries/wuss/test/tasks/image.c @@ -11,7 +11,6 @@ #include "base/utils.h" #include "framebuf/palettes.h" #include "geom/box.h" -#include "io/path.h" #include "image.h" @@ -19,19 +18,15 @@ result_t image_create(wuss_t *wuss, const colour_t *palette, - const char *resources, + const char *path, image_task_t *task) { - const char *leafname; - const char *filename; wuss_task_t delegate; box_t box; result_t rc; size2d_t sz; - leafname = path_join_leafname("jessica", "png"); - filename = path_join_filename(resources, 3, "resources", "images", leafname); - rc = bitmap_load_png(&task->bitmap, filename); + rc = bitmap_load_png(&task->bitmap, path); if (rc != result_OK) return rc; diff --git a/libraries/wuss/test/tasks/image.h b/libraries/wuss/test/tasks/image.h index c17aa0f5..87e1c53b 100644 --- a/libraries/wuss/test/tasks/image.h +++ b/libraries/wuss/test/tasks/image.h @@ -20,11 +20,10 @@ image_task_t; wuss_event_fn_t image_handle; -/* load the image and create its window against the given wuss instance; - * resources is the DPTLib repo root, for locating the bundled PNG */ +/* load the PNG at path and create its window against the given wuss instance */ result_t image_create(wuss_t *wuss, const colour_t *palette, - const char *resources, + const char *path, image_task_t *task); /* destroy the window and free the bitmap loaded by image_create */ diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 05f44151..682b3444 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -75,7 +75,16 @@ static result_t spawn_text(void) { return text_create(g_wuss, g_palette, static result_t spawn_blank(void) { return blank_create(g_wuss, g_npalette, &g_blank_task); } static result_t spawn_chars(void) { return chars_create(g_wuss, g_palette, &g_chars_task); } static result_t spawn_palette(void) { return palette_create(g_wuss, g_palette, g_npalette, &g_palette_task); } -static result_t spawn_image(void) { return image_create(g_wuss, g_palette, g_resources, &g_image_task); } +static result_t spawn_image(void) +{ + const char *leafname; + const char *filename; + + leafname = path_join_leafname("jessica", "png"); + filename = path_join_filename(g_resources, 3, "resources", "images", leafname); + + return image_create(g_wuss, g_palette, filename, &g_image_task); +} static result_t spawn_checker(void) { return checker_create(g_wuss, g_palette, &g_checker_task); } static result_t spawn_curve(void) { return curve_create(g_wuss, g_palette, &g_curve_task); } static result_t spawn_sofa(void) { return sofa_create(g_wuss, g_palette, &g_sofa_task); } From 0b3bff90df51932761e205211661fc6564c47f03 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 30 Aug 2026 00:04:46 +0100 Subject: [PATCH 065/287] feat(wuss): add wuss_window_create_placed for wuss-chosen positions New entry point taking a content size instead of a box: wuss packs the window (furniture included) into the first free screen region via a packer_t it owns, top-left order, tracking occupied area across calls so successive auto-placed windows tile. Cascades from the previous placement when no region fits. The slot is released back to the pool on close, and on the first wuss_window_move / wuss_window_resize (a titlebar drag counts as a move), after which wuss stops tracking the window's position. Adds packer_release() to geom/packer as the inverse of packer_place_*; released areas are not coalesced, which is sufficient for whole-window placement. Co-Authored-By: Claude Sonnet 5 --- CMakeLists.txt | 1 + include/geom/packer.h | 17 +++ include/wuss/window.h | 39 ++++++ libraries/geom/packer/packer.c | 15 +++ libraries/geom/packer/test/packer-test.c | 73 +++++++++++ libraries/wuss/create.c | 4 + libraries/wuss/destroy.c | 1 + libraries/wuss/impl.h | 22 ++++ libraries/wuss/test/wuss-test.c | 65 ++++++++++ libraries/wuss/window/close.c | 2 + libraries/wuss/window/create-placed.c | 149 +++++++++++++++++++++++ libraries/wuss/window/create.c | 2 + libraries/wuss/window/move.c | 4 + libraries/wuss/window/resize.c | 3 + 14 files changed, 397 insertions(+) create mode 100644 libraries/wuss/window/create-placed.c diff --git a/CMakeLists.txt b/CMakeLists.txt index f71da178..84556e01 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -363,6 +363,7 @@ set(WUSS_SOURCES libraries/wuss/task/stop.c libraries/wuss/window/at.c libraries/wuss/window/create.c + libraries/wuss/window/create-placed.c libraries/wuss/window/close.c libraries/wuss/window/get-content-bounds.c libraries/wuss/window/get-scroll.c diff --git a/include/geom/packer.h b/include/geom/packer.h index ea966075..16fb3d53 100644 --- a/include/geom/packer.h +++ b/include/geom/packer.h @@ -77,6 +77,23 @@ int packer_next_width(T *packer, packer_loc_t loc); result_t packer_place_at(T *packer, const box_t *area); +/** + * Returns a previously placed area to the free pool: the inverse of + * packer_place_at / packer_place_by. + * + * Adjacent released areas are not coalesced, so a single placement that would + * span two separately-released areas will not fit until the gap between them is + * also freed; placing a box that fits wholly within one free area is + * unaffected. packer_get_consumed_area is not narrowed by a release. + * + * \param[in] packer Packer to release into. + * \param[in] area Area to release. Clipped to the packer's margins. Copied. + * \return \ref result_OK, or \ref result_PACKER_EMPTY if 'area' lies entirely + * outside the margins. + */ +result_t packer_release(T *packer, + const box_t *area); + /** * Places a box of dimensions (w,h) in the next free area determined by location * 'loc'. diff --git a/include/wuss/window.h b/include/wuss/window.h index fa53be11..efdc7464 100644 --- a/include/wuss/window.h +++ b/include/wuss/window.h @@ -76,6 +76,45 @@ result_t wuss_window_create(wuss_t *wuss, size2d_t min_doc, wuss_window_t **window); +/** + * Create a window, letting Wuss choose its position. + * + * As wuss_window_create, but instead of a content box you pass just the content + * size; Wuss places the window (furniture included) in the first free screen + * region, packed towards the top-left, tracking occupied area across calls so + * successive auto-placed windows tile rather than stack. When no region is + * large enough the window is cascaded from the previous placement, stepping by + * a titlebar height and wrapping at the screen edge. + * + * The chosen slot is returned to the pool when the window is closed, or when it + * is first moved or resized via wuss_window_move / wuss_window_resize (after + * which Wuss no longer tracks its position). A window dragged by its titlebar + * counts as moved. + * + * \param[in] wuss Window manager to create the window on. + * \param[in] size Requested content-area size. Width and height must both + * be positive. + * \param[in] title Titlebar label, as wuss_window_create. + * \param[in] flags Appearance flags, as wuss_window_create. + * \param[in] bg Content background, as wuss_window_create. + * \param[in] task Content delegate, as wuss_window_create. + * \param[in] doc Virtual document extent, as wuss_window_create. + * \param[in] min_doc Minimum content size, as wuss_window_create. + * \param[out] window Newly created window. Becomes the topmost window. + * \return \ref result_OK on success, \ref result_WUSS_TOO_SMALL if size's width + * or height is not positive, \ref result_OOM if the layout tracker + * could not be created, or another result code from wuss_window_create. + */ +result_t wuss_window_create_placed(wuss_t *wuss, + size2d_t size, + const char *title, + wuss_window_flags_t flags, + wuss_colour_t bg, + const wuss_task_t *task, + size2d_t doc, + size2d_t min_doc, + wuss_window_t **window); + /** * Destroy a window. * diff --git a/libraries/geom/packer/packer.c b/libraries/geom/packer/packer.c index 59c19fe7..fc47e182 100644 --- a/libraries/geom/packer/packer.c +++ b/libraries/geom/packer/packer.c @@ -408,6 +408,21 @@ result_t packer_place_at(packer_t *packer, const box_t *area) return remove_area(packer, &b); } +result_t packer_release(packer_t *packer, const box_t *area) +{ + box_t b; + + (void) box_intersection(&packer->margins, area, &b); + + if (box_is_empty(&b)) + return result_PACKER_EMPTY; + + /* ponytail: no coalescing with neighbouring free areas -- a released + * fragment is usable on its own, which is all whole-box placement needs; + * tile-style reuse spanning two released areas would need a merge pass */ + return add_area(packer, &b); +} + result_t packer_place_by(packer_t *packer, packer_loc_t loc, int w, diff --git a/libraries/geom/packer/test/packer-test.c b/libraries/geom/packer/test/packer-test.c index ba744b0b..d93ba541 100644 --- a/libraries/geom/packer/test/packer-test.c +++ b/libraries/geom/packer/test/packer-test.c @@ -399,6 +399,75 @@ static int test2(void) return 1; } +/* packer_release: a slot handed back becomes available again. */ +static int test3(void) +{ + static const box_t pagedims = { 0, 0, 100, 100 }; + + packer_t *packer; + const box_t *a, *b, *c; + box_t freed; + result_t err; + + printf("test3: packer_release\n"); + + packer = packer_create(&pagedims); + if (packer == NULL) + return 1; + + /* fill the page with four 50x50 quads, top-left order */ + err = packer_place_by(packer, packer_LOC_TOP_LEFT, 50, 50, &a); + err |= packer_place_by(packer, packer_LOC_TOP_LEFT, 50, 50, &b); + err |= packer_place_by(packer, packer_LOC_TOP_LEFT, 50, 50, &c); + err |= packer_place_by(packer, packer_LOC_TOP_LEFT, 50, 50, NULL); + if (err) + goto failure; + + /* page is now full: a fifth 50x50 must not fit */ + if (packer_place_by(packer, packer_LOC_TOP_LEFT, 50, 50, NULL) + != result_PACKER_DIDNT_FIT) + { + printf("test3: expected DIDNT_FIT while full\n"); + goto failure; + } + + /* release the top-left quad, then a 50x50 must fit again, in that slot */ + freed.x0 = 0; freed.y0 = 0; freed.x1 = 50; freed.y1 = 50; + err = packer_release(packer, &freed); + if (err) + goto failure; + + err = packer_place_by(packer, packer_LOC_TOP_LEFT, 50, 50, &a); + if (err) + { + printf("test3: placement after release failed (%d)\n", err); + goto failure; + } + if (a->x0 != 0 || a->y0 != 0 || a->x1 != 50 || a->y1 != 50) + { + printf("test3: reused slot <%d,%d-%d,%d>, wanted <0,0-50,50>\n", + a->x0, a->y0, a->x1, a->y1); + goto failure; + } + + /* a box wholly outside the margins is rejected */ + freed.x0 = 200; freed.y0 = 200; freed.x1 = 250; freed.y1 = 250; + if (packer_release(packer, &freed) != result_PACKER_EMPTY) + { + printf("test3: out-of-bounds release not rejected\n"); + goto failure; + } + + packer_destroy(packer); + return 0; + + +failure: + + packer_destroy(packer); + return 1; +} + result_t packer_test(const char *resources) { result_t err; @@ -413,6 +482,10 @@ result_t packer_test(const char *resources) if (err) goto failure; + err = test3(); + if (err) + goto failure; + return result_TEST_PASSED; diff --git a/libraries/wuss/create.c b/libraries/wuss/create.c index 7e00e5fe..6b56de61 100644 --- a/libraries/wuss/create.c +++ b/libraries/wuss/create.c @@ -145,6 +145,10 @@ result_t wuss_create(screen_t *scr, w->ndirty = 0; + w->layout = NULL; + w->cascade.x = 0; + w->cascade.y = 0; + list_init(&w->z_order); *wuss = w; diff --git a/libraries/wuss/destroy.c b/libraries/wuss/destroy.c index 256b8791..ede75c1b 100644 --- a/libraries/wuss/destroy.c +++ b/libraries/wuss/destroy.c @@ -26,6 +26,7 @@ void wuss_destroy(wuss_t *doomed) e = next; } + packer_destroy(doomed->layout); free(doomed->palette); free(doomed); } diff --git a/libraries/wuss/impl.h b/libraries/wuss/impl.h index 9a83a09d..73692802 100644 --- a/libraries/wuss/impl.h +++ b/libraries/wuss/impl.h @@ -6,6 +6,8 @@ #include "base/utils.h" #include "datastruct/list.h" #include "geom/box.h" +#include "geom/packer.h" +#include "geom/point.h" #include "geom/size.h" #include "framebuf/screen.h" #include "framebuf/bmfont.h" @@ -56,6 +58,11 @@ struct wuss struct wuss__furniture furniture; box_t dirty[WUSS_MAX_DIRTY]; /* accumulated by wuss_invalidate; reset by a redraw */ int ndirty; + packer_t *layout; /* owned; occupied screen area for + * wuss_window_create_placed, lazily + * created on first auto-placement */ + point_t cascade; /* next cascade offset, used once the + * layout packer has no room left */ }; struct wuss_window @@ -73,6 +80,9 @@ struct wuss_window size2d_t min_doc; /* resize floor, set at creation; see * wuss__min_content */ wuss_window_state_t state; /* see wuss_window_state_t */ + box_t packed; /* footprint handed to wuss->layout by + * wuss_window_create_placed, or empty if + * not auto-placed or already released */ box_t pre_toggle; /* visible bounds to restore on the next toggle */ char title[WUSS_TITLE_MAX + 1]; wuss_icon_t **icons; /* owned; array of owned icon pointers */ @@ -125,6 +135,18 @@ static inline int wuss__size_ok(int width, int height) return width > 0 && height > 0; } +/* Give an auto-placed window's slot back to the layout packer and stop + * tracking it, so a later close/move/resize doesn't release it twice. A + * no-op for windows that were never auto-placed (empty "packed"). */ +static inline void wuss__release_packed(wuss_window_t *window) +{ + if (box_is_empty(&window->packed)) + return; + + (void) packer_release(window->wuss->layout, &window->packed); + box_reset(&window->packed); +} + /* The floor a resize-drag or toggle-size will shrink a window's content to: * the client's min_doc where it set one, but never below WUSS_MIN_CONTENT (a * window must stay big enough to grab) nor above the window's own doc extent diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 682b3444..c0de3242 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -2927,6 +2927,71 @@ result_t wuss_test(const char *resources) wuss_window_close(win_r); } + printf("test: wuss_window_create_placed tiles windows and reclaims a closed slot\n"); + + { + /* Windows created without a position are packed towards the top-left and + * must not overlap; closing one frees its slot for the next create. */ + test_task_t tc_p[4]; + wuss_task_t delegate_p; + wuss_window_t *win_p[4]; + box_t vis[4], probe; + int k, m; + + memset(tc_p, 0, sizeof(tc_p)); + delegate_p.handle = test_handle; + delegate_p.task_data = &tc_p[0]; + + for (k = 0; k < 4; k++) + { + rc = wuss_window_create_placed(wuss, + SIZE2D(40, 30), + "P", + wuss_WINDOW_NONE, + wuss_NO_BACKGROUND, + &delegate_p, + SIZE2D(40, 30), + SIZE2D(0, 0), + &win_p[k]); + if (rc != result_OK) + goto Failure; + wuss_window_get_visible_bounds(win_p[k], &vis[k]); + } + + for (k = 0; k < 4; k++) + for (m = k + 1; m < 4; m++) + if (box_intersects(&vis[k], &vis[m])) + goto Failure; /* auto-placed windows overlapped */ + + /* free the second window's slot, then a new placed window should land + * back in it rather than being pushed past the others */ + probe = vis[1]; + wuss_window_close(win_p[1]); + + rc = wuss_window_create_placed(wuss, + SIZE2D(40, 30), + "P", + wuss_WINDOW_NONE, + wuss_NO_BACKGROUND, + &delegate_p, + SIZE2D(40, 30), + SIZE2D(0, 0), + &win_p[1]); + if (rc != result_OK) + goto Failure; + wuss_window_get_visible_bounds(win_p[1], &vis[1]); + if (vis[1].x0 != probe.x0 || vis[1].y0 != probe.y0 || + vis[1].x1 != probe.x1 || vis[1].y1 != probe.y1) + goto Failure; /* freed slot not reused */ + + /* a manual move releases the slot: closing afterwards must not + * double-release (would corrupt the packer's free list) */ + wuss_window_move(win_p[0], POINT(200, 200)); + + for (k = 0; k < 4; k++) + wuss_window_close(win_p[k]); + } + printf("test: wuss_task_stop sends wuss_EVENT_QUIT to each window's task\n"); tc_a.stop_count = 0; diff --git a/libraries/wuss/window/close.c b/libraries/wuss/window/close.c index 411b9e00..058743fa 100644 --- a/libraries/wuss/window/close.c +++ b/libraries/wuss/window/close.c @@ -19,6 +19,8 @@ void wuss_window_close(wuss_window_t *doomed) if (wuss->furniture.dragging == doomed) wuss->furniture.dragging = NULL; + wuss__release_packed(doomed); + wuss__invalidate_clipped(doomed, &doomed->visible); list_remove(&wuss->z_order, &doomed->link); diff --git a/libraries/wuss/window/create-placed.c b/libraries/wuss/window/create-placed.c new file mode 100644 index 00000000..0550d2ba --- /dev/null +++ b/libraries/wuss/window/create-placed.c @@ -0,0 +1,149 @@ +/* create-placed.c -- wuss - window creation with wuss-chosen position */ + +#include + +#ifdef FORTIFY +#include "fortify/fortify.h" +#endif + +#include "geom/box.h" +#include "geom/packer.h" + +#include "../impl.h" + +/* Footprint padding around a content area of the given flags: the outline on + * every edge, the titlebar on top, and the scrollbar/resize carve on the + * right and bottom. Matches wuss_window_create's own visible-box maths so an + * auto-placed slot ends up exactly the size the window will occupy. */ +static void footprint_pad(const wuss_t *wuss, + wuss_window_flags_t flags, + int *left, + int *top, + int *right, + int *bottom) +{ + int outline_px, titlebar_height; + point_t carve; + + outline_px = wuss__outline_px_for(flags); + titlebar_height = wuss__titlebar_height_for(wuss, flags); + wuss__furniture_carve_for(flags, wuss__button_size_for(wuss, flags), &carve); + + *left = outline_px; + *top = outline_px + titlebar_height; + *right = outline_px + carve.x; + *bottom = outline_px + carve.y; +} + +/* Pick the next cascade position for a window of the given footprint size, + * once the layout packer has no room. Steps down/right by a titlebar each + * call, wrapping back to the top-left when the step would push the footprint + * off the screen. */ +static void next_cascade(wuss_t *wuss, int fw, int fh, point_t *pos) +{ + int scr_w, scr_h, step; + + scr_w = wuss->scr->size.w; + scr_h = wuss->scr->size.h; + step = wuss->titlebar_height; + if (step <= 0) + step = WUSS_DEFAULT_TITLEBAR_HEIGHT; + + if (wuss->cascade.x + fw > scr_w || wuss->cascade.y + fh > scr_h) + { + wuss->cascade.x = 0; + wuss->cascade.y = 0; + } + + *pos = wuss->cascade; + + wuss->cascade.x += step; + wuss->cascade.y += step; +} + +result_t wuss_window_create_placed(wuss_t *wuss, + size2d_t size, + const char *title, + wuss_window_flags_t flags, + wuss_colour_t bg, + const wuss_task_t *task, + size2d_t doc, + size2d_t min_doc, + wuss_window_t **window) +{ + result_t rc; + int left, top, right, bottom; + int fw, fh; + box_t screen, content; + point_t origin; + const box_t *slot; + int tracked; + + assert(wuss != NULL); + assert(window != NULL); + + if (!wuss__size_ok(size.w, size.h)) + return result_WUSS_TOO_SMALL; + + if (wuss->layout == NULL) + { + screen.x0 = 0; + screen.y0 = 0; + screen.x1 = wuss->scr->size.w; + screen.y1 = wuss->scr->size.h; + + wuss->layout = packer_create(&screen); + if (wuss->layout == NULL) + return result_OOM; + } + + footprint_pad(wuss, flags, &left, &top, &right, &bottom); + fw = left + size.w + right; + fh = top + size.h + bottom; + + /* packer's Y axis is reversed. bottom left here gives top left packing. */ + rc = packer_place_by(wuss->layout, packer_LOC_BOTTOM_LEFT, fw, fh, &slot); + if (rc == result_OK) + { + origin.x = slot->x0; + origin.y = slot->y0; + tracked = 1; + } + else if (rc == result_PACKER_DIDNT_FIT) + { + next_cascade(wuss, fw, fh, &origin); + tracked = 0; + } + else + { + return rc; + } + + /* content box = footprint origin plus the top/left furniture padding */ + content.x0 = origin.x + left; + content.y0 = origin.y + top; + content.x1 = content.x0 + size.w; + content.y1 = content.y0 + size.h; + + rc = wuss_window_create(wuss, &content, title, flags, bg, task, + doc, min_doc, window); + if (rc != result_OK) + { + if (tracked) + { + box_t placed; + + placed.x0 = origin.x; + placed.y0 = origin.y; + placed.x1 = origin.x + fw; + placed.y1 = origin.y + fh; + (void) packer_release(wuss->layout, &placed); + } + return rc; + } + + if (tracked) + (*window)->packed = *slot; + + return result_OK; +} diff --git a/libraries/wuss/window/create.c b/libraries/wuss/window/create.c index c29e87f2..7beb1ed5 100644 --- a/libraries/wuss/window/create.c +++ b/libraries/wuss/window/create.c @@ -83,6 +83,8 @@ result_t wuss_window_create(wuss_t *wuss, win->nicons = 0; win->cap_icons = 0; + box_reset(&win->packed); /* wuss_window_create_placed fills this in after */ + if (task != NULL) win->task = *task; else diff --git a/libraries/wuss/window/move.c b/libraries/wuss/window/move.c index 19c05a54..9acab623 100644 --- a/libraries/wuss/window/move.c +++ b/libraries/wuss/window/move.c @@ -78,6 +78,10 @@ void wuss_window_move(wuss_window_t *window, point_t p) box_t before, dirty, copied; int blit_failed; + /* a manual move desyncs the window from its layout-packer slot; hand the + * slot back and stop tracking this window's position */ + wuss__release_packed(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/window/resize.c b/libraries/wuss/window/resize.c index 4e0fd995..c43057a1 100644 --- a/libraries/wuss/window/resize.c +++ b/libraries/wuss/window/resize.c @@ -39,6 +39,9 @@ result_t wuss_window_resize(wuss_window_t *window, size2d_t size) if (!wuss__size_ok(size.w, size.h)) return result_WUSS_TOO_SMALL; + /* a manual resize desyncs the window from its layout-packer slot */ + wuss__release_packed(window); + outline_px = wuss__outline_px(window); titlebar_height = wuss__titlebar_height(window); before = window->visible; From 3232d2d92b8eb50db77bcf9f9f628cc2f56fc5c9 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 30 Aug 2026 00:04:51 +0100 Subject: [PATCH 066/287] refactor(wuss): let test tasks self-place via wuss_window_create_placed Switch every test task from a hardcoded BOX_POS_SIZE origin to wuss_window_create_placed, passing just the content size. Windows now tile from the top-left instead of landing at fixed coordinates. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/test/tasks/ball.c | 22 ++++++------- libraries/wuss/test/tasks/blank.c | 22 ++++++------- libraries/wuss/test/tasks/chars.c | 30 +++++++++--------- libraries/wuss/test/tasks/checker.c | 42 +++++++++++-------------- libraries/wuss/test/tasks/curve.c | 22 ++++++------- libraries/wuss/test/tasks/gradient.c | 22 ++++++------- libraries/wuss/test/tasks/icons.c | 22 ++++++------- libraries/wuss/test/tasks/image.c | 25 +++++++-------- libraries/wuss/test/tasks/launcher.c | 24 +++++++------- libraries/wuss/test/tasks/palette.c | 22 ++++++------- libraries/wuss/test/tasks/porter-duff.c | 22 ++++++------- libraries/wuss/test/tasks/sofa.c | 22 ++++++------- libraries/wuss/test/tasks/text.c | 30 +++++++++--------- 13 files changed, 152 insertions(+), 175 deletions(-) diff --git a/libraries/wuss/test/tasks/ball.c b/libraries/wuss/test/tasks/ball.c index 8d5fa59b..388f37b4 100644 --- a/libraries/wuss/test/tasks/ball.c +++ b/libraries/wuss/test/tasks/ball.c @@ -15,7 +15,6 @@ result_t ball_create(wuss_t *wuss, const colour_t *palette, ball_task_t *task) { wuss_task_t delegate; - box_t box; task->bg = palette[palette_PICO8_RED]; task->ball = palette[palette_PICO8_WHITE]; @@ -28,17 +27,16 @@ result_t ball_create(wuss_t *wuss, const colour_t *palette, ball_task_t *task) task->balls[0].radius = 8; delegate = wuss_task_start(ball_handle, task); /* ball_redraw paints its own background every frame */ - box = (box_t) BOX_POS_SIZE(20, 20, 200, 160); - - return wuss_window_create(wuss, - &box, - "Bouncing Ball", - wuss_WINDOW_NONE, - wuss_NO_BACKGROUND, - &delegate, - box_size(&box), - SIZE2D(0, 0), - &task->window); + + return wuss_window_create_placed(wuss, + SIZE2D(200, 160), + "Bouncing Ball", + wuss_WINDOW_NONE, + wuss_NO_BACKGROUND, + &delegate, + SIZE2D(200, 160), + SIZE2D(0, 0), + &task->window); } void ball_destroy(ball_task_t *task) diff --git a/libraries/wuss/test/tasks/blank.c b/libraries/wuss/test/tasks/blank.c index 344a59d3..7c95c9c8 100644 --- a/libraries/wuss/test/tasks/blank.c +++ b/libraries/wuss/test/tasks/blank.c @@ -18,24 +18,22 @@ result_t blank_create(wuss_t *wuss, int npalette, blank_task_t *task) { wuss_task_t delegate; - box_t box; task->npalette = npalette; task->index = palette_PICO8_GREEN; task->frame_count = 0; delegate = wuss_task_start(blank_handle, task); /* wuss fills the content area itself */ - box = (box_t) BOX_POS_SIZE(260, 60, 200, 160); - - return wuss_window_create(wuss, - &box, - NULL, - wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE, - palette_PICO8_GREEN, - &delegate, - box_size(&box), - SIZE2D(0, 0), - &task->window); + + return wuss_window_create_placed(wuss, + SIZE2D(200, 160), + NULL, + wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE, + palette_PICO8_GREEN, + &delegate, + SIZE2D(200, 160), + SIZE2D(0, 0), + &task->window); } void blank_destroy(blank_task_t *task) diff --git a/libraries/wuss/test/tasks/chars.c b/libraries/wuss/test/tasks/chars.c index 6f2d6603..572461ca 100644 --- a/libraries/wuss/test/tasks/chars.c +++ b/libraries/wuss/test/tasks/chars.c @@ -24,7 +24,7 @@ result_t chars_create(wuss_t *wuss, chars_task_t *task) { wuss_task_t delegate; - box_t box; + size2d_t sz; bmfont_t *font; int font_width, font_height, cell_w, cell_h; @@ -44,20 +44,20 @@ result_t chars_create(wuss_t *wuss, task->bg = palette[palette_PICO8_WHITE]; delegate = wuss_task_start(chars_handle, task); /* chars_redraw paints every cell itself */ - box = (box_t) BOX_POS_SIZE(500, 100, cell_w * CHARS_COLS, cell_h * CHARS_ROWS); - - return wuss_window_create(wuss, - &box, - "Chars", - wuss_WINDOW_NO_RESIZE | - wuss_WINDOW_NO_TOGGLE_SIZE | - wuss_WINDOW_NO_VSCROLL | - wuss_WINDOW_NO_HSCROLL, - wuss_NO_BACKGROUND, - &delegate, - box_size(&box), - SIZE2D(0, 0), - &task->window); + sz = SIZE2D(cell_w * CHARS_COLS, cell_h * CHARS_ROWS); + + return wuss_window_create_placed(wuss, + sz, + "Chars", + wuss_WINDOW_NO_RESIZE | + wuss_WINDOW_NO_TOGGLE_SIZE | + wuss_WINDOW_NO_VSCROLL | + wuss_WINDOW_NO_HSCROLL, + wuss_NO_BACKGROUND, + &delegate, + sz, + SIZE2D(0, 0), + &task->window); } void chars_destroy(chars_task_t *task) diff --git a/libraries/wuss/test/tasks/checker.c b/libraries/wuss/test/tasks/checker.c index d61baa56..dbd5e4e8 100644 --- a/libraries/wuss/test/tasks/checker.c +++ b/libraries/wuss/test/tasks/checker.c @@ -21,7 +21,6 @@ result_t checker_create(wuss_t *wuss, checker_task_t *task) { wuss_task_t delegate; - box_t box; result_t rc; task->black = palette[palette_PICO8_BLACK]; @@ -32,31 +31,28 @@ result_t checker_create(wuss_t *wuss, task->band2 = CHECKER_BAND_DEFAULT; delegate = wuss_task_start(checker_handle, task); /* checker_redraw paints every pixel itself */ - box = (box_t) BOX_POS_SIZE(440, 300, 160, 160); - - rc = wuss_window_create(wuss, - &box, - "Checker 1", - wuss_WINDOW_NONE, - wuss_NO_BACKGROUND, - &delegate, - box_size(&box), - SIZE2D(0, 0), - &task->window); + + rc = wuss_window_create_placed(wuss, + SIZE2D(160, 160), + "Checker 1", + wuss_WINDOW_NONE, + wuss_NO_BACKGROUND, + &delegate, + SIZE2D(160, 160), + SIZE2D(0, 0), + &task->window); if (rc != result_OK) return rc; - box = (box_t) BOX_POS_SIZE(440, 10, 160, 160); - - rc = wuss_window_create(wuss, - &box, - "Checker 2", - wuss_WINDOW_NONE, - wuss_NO_BACKGROUND, - &delegate, - box_size(&box), - SIZE2D(0, 0), - &task->window2); + rc = wuss_window_create_placed(wuss, + SIZE2D(160, 160), + "Checker 2", + wuss_WINDOW_NONE, + wuss_NO_BACKGROUND, + &delegate, + SIZE2D(160, 160), + SIZE2D(0, 0), + &task->window2); if (rc != result_OK) { wuss_window_close(task->window); diff --git a/libraries/wuss/test/tasks/curve.c b/libraries/wuss/test/tasks/curve.c index 24572802..2b9f44ef 100644 --- a/libraries/wuss/test/tasks/curve.c +++ b/libraries/wuss/test/tasks/curve.c @@ -26,7 +26,6 @@ result_t curve_create(wuss_t *wuss, curve_task_t *task) { wuss_task_t delegate; - box_t box; task->bg = palette[palette_PICO8_WHITE]; task->line = palette[palette_PICO8_BLACK]; @@ -40,17 +39,16 @@ result_t curve_create(wuss_t *wuss, task->points[3] = POINT(210, 140); delegate = wuss_task_start(curve_handle, task); /* curve_redraw paints its own background */ - box = (box_t) BOX_POS_SIZE(20, 260, 220, 160); - - return wuss_window_create(wuss, - &box, - "Curve", - wuss_WINDOW_NONE, - wuss_NO_BACKGROUND, - &delegate, - box_size(&box), - SIZE2D(0, 0), - &task->window); + + return wuss_window_create_placed(wuss, + SIZE2D(220, 160), + "Curve", + wuss_WINDOW_NONE, + wuss_NO_BACKGROUND, + &delegate, + SIZE2D(220, 160), + SIZE2D(0, 0), + &task->window); } void curve_destroy(curve_task_t *task) diff --git a/libraries/wuss/test/tasks/gradient.c b/libraries/wuss/test/tasks/gradient.c index 491e8460..0e3cc849 100644 --- a/libraries/wuss/test/tasks/gradient.c +++ b/libraries/wuss/test/tasks/gradient.c @@ -34,20 +34,18 @@ static int dither(int v, int x, int y) result_t gradient_create(wuss_t *wuss, gradient_task_t *task) { wuss_task_t delegate; - box_t box; delegate = wuss_task_start(gradient_handle, task); /* gradient_redraw paints every pixel itself */ - box = (box_t) BOX_POS_SIZE(620, 300, GRADIENT_OPEN_WIDTH, GRADIENT_OPEN_HEIGHT); - - return wuss_window_create(wuss, - &box, - "Gradient", - wuss_WINDOW_NONE, - wuss_NO_BACKGROUND, - &delegate, - SIZE2D(GRADIENT_DOC_WIDTH, GRADIENT_DOC_HEIGHT), - SIZE2D(0, 0), - &task->window); + + return wuss_window_create_placed(wuss, + SIZE2D(GRADIENT_OPEN_WIDTH, GRADIENT_OPEN_HEIGHT), + "Gradient", + wuss_WINDOW_NONE, + wuss_NO_BACKGROUND, + &delegate, + SIZE2D(GRADIENT_DOC_WIDTH, GRADIENT_DOC_HEIGHT), + SIZE2D(0, 0), + &task->window); } void gradient_destroy(gradient_task_t *task) diff --git a/libraries/wuss/test/tasks/icons.c b/libraries/wuss/test/tasks/icons.c index a183a0cb..70a247c7 100644 --- a/libraries/wuss/test/tasks/icons.c +++ b/libraries/wuss/test/tasks/icons.c @@ -27,7 +27,6 @@ result_t icons_create(wuss_t *wuss, icons_task_t *task) { wuss_task_t delegate; - box_t box; wuss_icon_spec_t specs[4]; wuss_icon_t *made[4]; result_t rc; @@ -42,17 +41,16 @@ result_t icons_create(wuss_t *wuss, task->count = 0; delegate = wuss_task_start(icons_handle, task); - box = (box_t) BOX_POS_SIZE(160, 120, ICONS_DOC_W, 160); - - rc = wuss_window_create(wuss, - &box, - "Icons", - wuss_WINDOW_NONE, - palette_PICO8_LIGHT_GREY, - &delegate, - SIZE2D(ICONS_DOC_W, ICONS_DOC_H), - SIZE2D(0, 0), - &task->window); + + rc = wuss_window_create_placed(wuss, + SIZE2D(ICONS_DOC_W, 160), + "Icons", + wuss_WINDOW_NONE, + palette_PICO8_LIGHT_GREY, + &delegate, + SIZE2D(ICONS_DOC_W, ICONS_DOC_H), + SIZE2D(0, 0), + &task->window); if (rc != result_OK) return rc; diff --git a/libraries/wuss/test/tasks/image.c b/libraries/wuss/test/tasks/image.c index 7a6501e4..0c7c9ee5 100644 --- a/libraries/wuss/test/tasks/image.c +++ b/libraries/wuss/test/tasks/image.c @@ -22,7 +22,6 @@ result_t image_create(wuss_t *wuss, image_task_t *task) { wuss_task_t delegate; - box_t box; result_t rc; size2d_t sz; @@ -34,19 +33,17 @@ result_t image_create(wuss_t *wuss, sz.w = task->bitmap.size.w + BORDER * 2; sz.h = task->bitmap.size.h + BORDER * 2; - - /* shorter than the bitmap so there's something to scroll through */ - box = (box_t) BOX_POS_SIZE(370, 10, sz.w, sz.h * 2 / 3); - - return wuss_window_create(wuss, - &box, - "Image", - wuss_WINDOW_NONE, - palette_PICO8_PINK, - &delegate, - sz, - SIZE2D(32, 32), - &task->window); + + return wuss_window_create_placed(wuss, + /* shorter than the bitmap so there's something to scroll through */ + SIZE2D(sz.w, sz.h * 2 / 3), + "Image", + wuss_WINDOW_NONE, + palette_PICO8_PINK, + &delegate, + sz, + SIZE2D(32, 32), + &task->window); } void image_destroy(image_task_t *task) diff --git a/libraries/wuss/test/tasks/launcher.c b/libraries/wuss/test/tasks/launcher.c index 3e3e93aa..69831975 100644 --- a/libraries/wuss/test/tasks/launcher.c +++ b/libraries/wuss/test/tasks/launcher.c @@ -28,7 +28,7 @@ result_t launcher_create(wuss_t *wuss, launcher_task_t *task) { wuss_task_t delegate; - box_t box; + size2d_t sz; assert(nentries <= LAUNCHER_MAX_ENTRIES); @@ -41,17 +41,17 @@ result_t launcher_create(wuss_t *wuss, task->running_fg = palette[palette_PICO8_LIGHT_GREY]; delegate = wuss_task_start(launcher_handle, task); /* launcher_redraw paints its own background */ - box = (box_t) BOX_POS_SIZE(10, 10, LAUNCHER_WIDTH, LAUNCHER_PAD * 2 + nentries * LAUNCHER_ROW_HEIGHT); - - return wuss_window_create(wuss, - &box, - "Launcher", - wuss_WINDOW_NO_CLOSE, - wuss_NO_BACKGROUND, - &delegate, - box_size(&box), - SIZE2D(0, 0), - &task->window); + sz = SIZE2D(LAUNCHER_WIDTH, LAUNCHER_PAD * 2 + nentries * LAUNCHER_ROW_HEIGHT); + + return wuss_window_create_placed(wuss, + sz, + "Launcher", + wuss_WINDOW_NO_CLOSE, + wuss_NO_BACKGROUND, + &delegate, + sz, + SIZE2D(0, 0), + &task->window); } void launcher_destroy(launcher_task_t *task) diff --git a/libraries/wuss/test/tasks/palette.c b/libraries/wuss/test/tasks/palette.c index 0ba14831..f98e18bd 100644 --- a/libraries/wuss/test/tasks/palette.c +++ b/libraries/wuss/test/tasks/palette.c @@ -18,23 +18,21 @@ result_t palette_create(wuss_t *wuss, palette_task_t *task) { wuss_task_t delegate; - box_t box; task->palette = palette; task->npalette = npalette; delegate = wuss_task_start(palette_handle, task); /* backdrop for any rounding gap around the grid */ - box = (box_t) BOX_POS_SIZE(380, 260, 100, 100); - - return wuss_window_create(wuss, - &box, - "Palette", - wuss_WINDOW_NO_RESIZE_BLIT, /* swatch grid is laid out across the whole window, so a resize must redraw all of it, not just the newly (un)covered edge */ - palette_PICO8_BLACK, - &delegate, - box_size(&box), - SIZE2D(0, 0), - &task->window); + + return wuss_window_create_placed(wuss, + SIZE2D(100, 100), + "Palette", + wuss_WINDOW_NO_RESIZE_BLIT, /* swatch grid is laid out across the whole window, so a resize must redraw all of it, not just the newly (un)covered edge */ + palette_PICO8_BLACK, + &delegate, + SIZE2D(100, 100), + SIZE2D(0, 0), + &task->window); } void palette_destroy(palette_task_t *task) diff --git a/libraries/wuss/test/tasks/porter-duff.c b/libraries/wuss/test/tasks/porter-duff.c index 8ff10b92..5d586693 100644 --- a/libraries/wuss/test/tasks/porter-duff.c +++ b/libraries/wuss/test/tasks/porter-duff.c @@ -163,7 +163,6 @@ result_t porter_duff_create(wuss_t *wuss, porter_duff_task_t *task) { wuss_task_t delegate; - box_t box; result_t rc; task->font = font; @@ -192,17 +191,16 @@ result_t porter_duff_create(wuss_t *wuss, goto free_src; delegate = wuss_task_start(porter_duff_handle, task); /* porter_duff_redraw paints every pixel itself */ - box = (box_t) BOX_POS_SIZE(60, 180, PD_SIZE, PD_SIZE + PD_LABEL_HEIGHT); - - rc = wuss_window_create(wuss, - &box, - "Porter-Duff", - wuss_WINDOW_NONE, - wuss_NO_BACKGROUND, - &delegate, - box_size(&box), - SIZE2D(0, 0), - &task->window); + + rc = wuss_window_create_placed(wuss, + SIZE2D(PD_SIZE, PD_SIZE + PD_LABEL_HEIGHT), + "Porter-Duff", + wuss_WINDOW_NONE, + wuss_NO_BACKGROUND, + &delegate, + SIZE2D(PD_SIZE, PD_SIZE + PD_LABEL_HEIGHT), + SIZE2D(0, 0), + &task->window); if (rc != result_OK) goto free_dst; diff --git a/libraries/wuss/test/tasks/sofa.c b/libraries/wuss/test/tasks/sofa.c index c006691e..0e143148 100644 --- a/libraries/wuss/test/tasks/sofa.c +++ b/libraries/wuss/test/tasks/sofa.c @@ -258,7 +258,6 @@ static fix8_point_t project(vec3_t v, int cx, int cy, double unit) result_t sofa_create(wuss_t *wuss, const colour_t *palette, sofa_task_t *task) { wuss_task_t delegate; - box_t box; task->bg = palette[palette_PICO8_DARK_PURPLE]; task->line = palette[palette_PICO8_ORANGE]; @@ -269,17 +268,16 @@ result_t sofa_create(wuss_t *wuss, const colour_t *palette, sofa_task_t *task) task->turns = 0; delegate = wuss_task_start(sofa_handle, task); /* sofa_redraw paints its own background every frame */ - box = (box_t) BOX_POS_SIZE(250, 260, 180, 160); - - return wuss_window_create(wuss, - &box, - "Sofa", - wuss_WINDOW_NONE, - wuss_NO_BACKGROUND, - &delegate, - box_size(&box), - SIZE2D(0, 0), - &task->window); + + return wuss_window_create_placed(wuss, + SIZE2D(180, 160), + "Sofa", + wuss_WINDOW_NONE, + wuss_NO_BACKGROUND, + &delegate, + SIZE2D(180, 160), + SIZE2D(0, 0), + &task->window); } void sofa_destroy(sofa_task_t *task) diff --git a/libraries/wuss/test/tasks/text.c b/libraries/wuss/test/tasks/text.c index c96d9274..2c2e9eb9 100644 --- a/libraries/wuss/test/tasks/text.c +++ b/libraries/wuss/test/tasks/text.c @@ -32,7 +32,7 @@ result_t text_create(wuss_t *wuss, text_task_t *task) { wuss_task_t delegate; - box_t box; + size2d_t sz; result_t rc; task->font = font; @@ -42,20 +42,20 @@ result_t text_create(wuss_t *wuss, task->resizing = true; delegate = wuss_task_start(text_handle, task); - box = (box_t) BOX_POS_SIZE(120, 100, 220, 180); - - task->base_width = box.x1 - box.x0; - task->base_height = box.y1 - box.y0; - - rc = wuss_window_create(wuss, - &box, - "Lorem Ipsum", - 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 */ - palette_PICO8_BLUE, - &delegate, - box_size(&box), - SIZE2D(0, 0), - &task->window); + sz = SIZE2D(220, 180); + + task->base_width = sz.w; + task->base_height = sz.h; + + rc = wuss_window_create_placed(wuss, + sz, + "Lorem Ipsum", + 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 */ + palette_PICO8_BLUE, + &delegate, + sz, + SIZE2D(0, 0), + &task->window); return rc; } From 34962d73418fb84deaa27270db24685f4662cd6b Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 30 Aug 2026 00:30:34 +0100 Subject: [PATCH 067/287] feat(packer): add packer_set_gutter for spacing between located boxes packer_place_by now searches for the box plus a configurable gutter strip along its two inner edges, so boxes placed by location never end up flush. The gutter defaults to 0, leaving every existing caller unchanged. The returned position is still the un-inflated box. wuss_window_create_placed sets a fixed WUSS_PLACE_GUTTER and stores the gutter-inflated region in wuss_window::packed so the whole reservation, not just the footprint, is handed back on close/move. Co-Authored-By: Claude Sonnet 5 --- include/geom/packer.h | 12 +++++++ libraries/geom/packer/impl.h | 3 ++ libraries/geom/packer/packer.c | 29 +++++++++++++-- libraries/geom/packer/test/packer-test.c | 45 ++++++++++++++++++++++++ libraries/wuss/impl.h | 9 +++-- libraries/wuss/window/create-placed.c | 28 +++++++++------ 6 files changed, 110 insertions(+), 16 deletions(-) diff --git a/include/geom/packer.h b/include/geom/packer.h index 16fb3d53..184f7d4f 100644 --- a/include/geom/packer.h +++ b/include/geom/packer.h @@ -77,6 +77,18 @@ int packer_next_width(T *packer, packer_loc_t loc); result_t packer_place_at(T *packer, const box_t *area); +/** + * Sets a gutter for packer_place_by: the width in pixels of a strip it + * additionally reserves along each placed box's two edges facing away from the + * search corner, so boxes placed by location never end up flush against each + * other. Default 0 (no gutter). Negative values are treated as 0. Does not + * affect packer_place_at. + * + * \param[in] packer Packer to configure. + * \param[in] gutter Gutter width in pixels. + */ +void packer_set_gutter(T *packer, int gutter); + /** * Returns a previously placed area to the free pool: the inverse of * packer_place_at / packer_place_by. diff --git a/libraries/geom/packer/impl.h b/libraries/geom/packer/impl.h index 2b3312a5..f8beff00 100644 --- a/libraries/geom/packer/impl.h +++ b/libraries/geom/packer/impl.h @@ -34,6 +34,9 @@ struct packer packer_sortdir_t order; /* order to which we have sorted */ int sorted; /* a bool */ + int gutter; /* px reserved past each placed box's + inner edges by packer_place_by; 0 = none */ + box_t consumed_area; /* total consumed area */ }; diff --git a/libraries/geom/packer/packer.c b/libraries/geom/packer/packer.c index fc47e182..a542c5e2 100644 --- a/libraries/geom/packer/packer.c +++ b/libraries/geom/packer/packer.c @@ -46,6 +46,8 @@ packer_t *packer_create(const box_t *dims) p->order = packer_SORT_TOP_LEFT; /* any will do */ p->sorted = 1; + p->gutter = 0; + p->consumed_area.x0 = INT_MAX; p->consumed_area.y0 = INT_MAX; p->consumed_area.x1 = INT_MIN; @@ -423,6 +425,11 @@ result_t packer_release(packer_t *packer, const box_t *area) return add_area(packer, &b); } +void packer_set_gutter(packer_t *packer, int gutter) +{ + packer->gutter = (gutter > 0) ? gutter : 0; +} + result_t packer_place_by(packer_t *packer, packer_loc_t loc, int w, @@ -431,6 +438,8 @@ result_t packer_place_by(packer_t *packer, { result_t err; const box_t *b; + int g, fw, fh; + box_t consume; if (pos) *pos = NULL; @@ -438,11 +447,17 @@ result_t packer_place_by(packer_t *packer, if (w == 0 || h == 0) return result_PACKER_EMPTY; + /* look for a free area big enough for the box plus the gutter strip it + * reserves along its two inner edges */ + g = packer->gutter; + fw = w + g; + fh = h + g; + for (b = packer_start(packer, (packer_sortdir_t) loc); b; b = packer_next(packer)) { - if (box_could_hold(b, w, h)) + if (box_could_hold(b, fw, fh)) { logf_debug("packer_place_by: %s", "fits"); break; @@ -455,18 +470,24 @@ result_t packer_place_by(packer_t *packer, return result_PACKER_DIDNT_FIT; } + /* the box sits flush in the chosen corner of the free area; the gutter is + * reserved on its two edges that face away from that corner */ switch (loc) { case packer_LOC_TOP_LEFT: case packer_LOC_BOTTOM_LEFT: packer->placed_area.x0 = b->x0; packer->placed_area.x1 = b->x0 + w; + consume.x0 = b->x0; + consume.x1 = b->x0 + fw; /* box + gutter to the right */ break; case packer_LOC_TOP_RIGHT: case packer_LOC_BOTTOM_RIGHT: packer->placed_area.x0 = b->x1 - w; packer->placed_area.x1 = b->x1; + consume.x0 = b->x1 - fw; /* box + gutter to the left */ + consume.x1 = b->x1; break; default: @@ -479,19 +500,23 @@ result_t packer_place_by(packer_t *packer, case packer_LOC_TOP_RIGHT: packer->placed_area.y0 = b->y1 - h; packer->placed_area.y1 = b->y1; + consume.y0 = b->y1 - fh; /* box + gutter below */ + consume.y1 = b->y1; break; case packer_LOC_BOTTOM_LEFT: case packer_LOC_BOTTOM_RIGHT: packer->placed_area.y0 = b->y0; packer->placed_area.y1 = b->y0 + h; + consume.y0 = b->y0; + consume.y1 = b->y0 + fh; /* box + gutter above */ break; default: break; } - err = remove_area(packer, &packer->placed_area); + err = remove_area(packer, &consume); if (err) return err; diff --git a/libraries/geom/packer/test/packer-test.c b/libraries/geom/packer/test/packer-test.c index d93ba541..523c4f50 100644 --- a/libraries/geom/packer/test/packer-test.c +++ b/libraries/geom/packer/test/packer-test.c @@ -458,6 +458,51 @@ static int test3(void) goto failure; } + packer_destroy(packer); + + + /* gutter: a column just wide enough for one box plus its gutter forces the + * second placement to stack above the first, gutter between them */ + { + static const box_t coldims = { 0, 0, 30, 200 }; + + box_t first; + + printf("test3: packer_set_gutter\n"); + + packer = packer_create(&coldims); + if (packer == NULL) + return 1; + + packer_set_gutter(packer, 10); + + /* pos points at the packer's single result buffer, so copy the first + * placement out before the second overwrites it */ + err = packer_place_by(packer, packer_LOC_BOTTOM_LEFT, 20, 20, &a); + if (err) + goto failure; + first = *a; + + err = packer_place_by(packer, packer_LOC_BOTTOM_LEFT, 20, 20, &b); + if (err) + goto failure; + + /* boxes are still 20x20 (the gutter is not added to the result)... */ + if (first.x1 - first.x0 != 20 || first.y1 - first.y0 != 20 || + b->x1 - b->x0 != 20 || b->y1 - b->y0 != 20) + { + printf("test3: gutter inflated the placed box\n"); + goto failure; + } + /* ...but the second sits a full gutter above the first, not flush */ + if (b->y0 - first.y1 != 10) + { + printf("test3: gap between placements was %d, wanted 10\n", + b->y0 - first.y1); + goto failure; + } + } + packer_destroy(packer); return 0; diff --git a/libraries/wuss/impl.h b/libraries/wuss/impl.h index 73692802..d5e5f139 100644 --- a/libraries/wuss/impl.h +++ b/libraries/wuss/impl.h @@ -28,6 +28,8 @@ * just some avoidable redraw work, never wrong */ #define WUSS_MAX_INVALIDATE_PIECES 32 +#define WUSS_PLACE_GUTTER 6 /* px left between windows auto-placed by wuss_window_create_placed */ + #define WUSS_BUTTON_INSET 3 /* shared by close/back/toggle/resize furniture buttons and scrollbar breadth */ #define WUSS_MIN_CONTENT 20 /* resize-drag floor: content can never be squeezed smaller than this */ #define WUSS_SCROLL_INSET 2 /* sausage cross-axis margin from its well's edges, purely cosmetic */ @@ -80,9 +82,10 @@ struct wuss_window size2d_t min_doc; /* resize floor, set at creation; see * wuss__min_content */ wuss_window_state_t state; /* see wuss_window_state_t */ - box_t packed; /* footprint handed to wuss->layout by - * wuss_window_create_placed, or empty if - * not auto-placed or already released */ + box_t packed; /* region wuss_window_create_placed took + * out of wuss->layout (footprint + gutter), + * to give back on close/move; empty if not + * auto-placed or already released */ box_t pre_toggle; /* visible bounds to restore on the next toggle */ char title[WUSS_TITLE_MAX + 1]; wuss_icon_t **icons; /* owned; array of owned icon pointers */ diff --git a/libraries/wuss/window/create-placed.c b/libraries/wuss/window/create-placed.c index 0550d2ba..7f884ace 100644 --- a/libraries/wuss/window/create-placed.c +++ b/libraries/wuss/window/create-placed.c @@ -74,7 +74,7 @@ result_t wuss_window_create_placed(wuss_t *wuss, result_t rc; int left, top, right, bottom; int fw, fh; - box_t screen, content; + box_t screen, content, consumed; point_t origin; const box_t *slot; int tracked; @@ -95,6 +95,8 @@ result_t wuss_window_create_placed(wuss_t *wuss, wuss->layout = packer_create(&screen); if (wuss->layout == NULL) return result_OOM; + + packer_set_gutter(wuss->layout, WUSS_PLACE_GUTTER); } footprint_pad(wuss, flags, &left, &top, &right, &bottom); @@ -125,25 +127,29 @@ result_t wuss_window_create_placed(wuss_t *wuss, content.x1 = content.x0 + size.w; content.y1 = content.y0 + size.h; + /* what packer_place_by actually took out of the free list: the footprint + * plus the gutter strip on its inner edges (right and, in packer space, + * top -- see packer_LOC_BOTTOM_LEFT). Releasing exactly this on close / + * move keeps the gutter from leaking away over a session. */ + if (tracked) + { + consumed.x0 = slot->x0; + consumed.y0 = slot->y0; + consumed.x1 = slot->x1 + WUSS_PLACE_GUTTER; + consumed.y1 = slot->y1 + WUSS_PLACE_GUTTER; + } + rc = wuss_window_create(wuss, &content, title, flags, bg, task, doc, min_doc, window); if (rc != result_OK) { if (tracked) - { - box_t placed; - - placed.x0 = origin.x; - placed.y0 = origin.y; - placed.x1 = origin.x + fw; - placed.y1 = origin.y + fh; - (void) packer_release(wuss->layout, &placed); - } + (void) packer_release(wuss->layout, &consumed); return rc; } if (tracked) - (*window)->packed = *slot; + (*window)->packed = consumed; return result_OK; } From 316fd6d8338451cc09a91cec872000bfe7bd8d40 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 30 Aug 2026 01:38:53 +0100 Subject: [PATCH 068/287] feat(wuss): mark sofa-task vertices with white dots Each wireframe model in the sofa test task now draws a small white square at every projected vertex, on top of the edges. Also adds a Cobra Mk III model alongside the existing ship. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/test/tasks/sofa.c | 123 +++++++++++++++++++++++++++++++ libraries/wuss/test/tasks/sofa.h | 3 +- 2 files changed, 125 insertions(+), 1 deletion(-) diff --git a/libraries/wuss/test/tasks/sofa.c b/libraries/wuss/test/tasks/sofa.c index 0e143148..7d0d0aff 100644 --- a/libraries/wuss/test/tasks/sofa.c +++ b/libraries/wuss/test/tasks/sofa.c @@ -15,6 +15,8 @@ #include "sofa.h" +#define SOFA_VERTEX_DOT 2 /* side, px, of the white marker square drawn at each vertex */ + #ifndef M_PI #define M_PI 3.14159265358979323846 #endif @@ -77,6 +79,81 @@ static const int ship_edges[18][2] = { 5, 8 }, { 8, 9 }, { 9, 5 }, /* right wing */ }; +/* a Cobra Mk. 3 from Elite */ +static const vec3_t cobra_vertices[28] = +{ + { 32/100.0, 0/100.0, 76/100.0 }, + { -32/100.0, 0/100.0, 76/100.0 }, + { 0/100.0, 26/100.0, 24/100.0 }, + {-120/100.0, -3/100.0, -8/100.0 }, + { 120/100.0, -3/100.0, -8/100.0 }, + { -88/100.0, 16/100.0, -40/100.0 }, + { 88/100.0, 16/100.0, -40/100.0 }, + { 128/100.0, -8/100.0, -40/100.0 }, + {-128/100.0, -8/100.0, -40/100.0 }, + { 0/100.0, 26/100.0, -40/100.0 }, + { -32/100.0, -24/100.0, -40/100.0 }, + { 32/100.0, -24/100.0, -40/100.0 }, + { -36/100.0, 8/100.0, -40/100.0 }, + { -8/100.0, 12/100.0, -40/100.0 }, + { 8/100.0, 12/100.0, -40/100.0 }, + { 36/100.0, 8/100.0, -40/100.0 }, + { 36/100.0, -12/100.0, -40/100.0 }, + { 8/100.0, -16/100.0, -40/100.0 }, + { -8/100.0, -16/100.0, -40/100.0 }, + { -36/100.0, -12/100.0, -40/100.0 }, + { 0/100.0, 0/100.0, 76/100.0 }, + { 0/100.0, 0/100.0, 90/100.0 }, + { -80/100.0, -6/100.0, -40/100.0 }, + { -80/100.0, 6/100.0, -40/100.0 }, + { -88/100.0, 0/100.0, -40/100.0 }, + { 80/100.0, 6/100.0, -40/100.0 }, + { 88/100.0, 0/100.0, -40/100.0 }, + { 80/100.0, -6/100.0, -40/100.0 }, +}; + +static const int cobra_edges[38][2] = +{ + { 0, 1 }, + { 0, 4 }, + { 1, 3 }, + { 3, 8 }, + { 4, 7 }, + { 6, 7 }, + { 6, 9 }, + { 5, 9 }, + { 5, 8 }, + { 2, 5 }, + { 2, 6 }, + { 3, 5 }, + { 4, 6 }, + { 1, 2 }, + { 0, 2 }, + { 8, 10 }, + { 10, 11 }, + { 7, 11 }, + { 1, 10 }, + { 0, 11 }, + { 1, 5 }, + { 0, 6 }, + { 20, 21 }, + { 12, 13 }, + { 18, 19 }, + { 14, 15 }, + { 16, 17 }, + { 15, 16 }, + { 14, 17 }, + { 13, 18 }, + { 12, 19 }, + { 2, 9 }, + { 22, 24 }, + { 23, 24 }, + { 22, 23 }, + { 25, 26 }, + { 26, 27 }, + { 25, 27 }, +}; + /* the five Platonic solids, vertices normalised to unit circumradius */ static const vec3_t tetra_vertices[4] = @@ -255,12 +332,33 @@ static fix8_point_t project(vec3_t v, int cx, int cy, double unit) return p; } +/* a marker square, SOFA_VERTEX_DOT on a side, centred on each projected + * vertex; drawn after the wireframe so the dots sit on top of the edges */ +static void draw_vertex_dots(screen_t *scr, + const fix8_point_t *screen, + int nvertices, + colour_t colour) +{ + int half; + int i; + + half = SOFA_VERTEX_DOT / 2; + + for (i = 0; i < nvertices; i++) + screen_draw_square(scr, + FIX8_ROUND_TO_INT(screen[i].x) - half, + FIX8_ROUND_TO_INT(screen[i].y) - half, + SOFA_VERTEX_DOT, + colour); +} + result_t sofa_create(wuss_t *wuss, const colour_t *palette, sofa_task_t *task) { wuss_task_t delegate; task->bg = palette[palette_PICO8_DARK_PURPLE]; task->line = palette[palette_PICO8_ORANGE]; + task->dot = palette[palette_PICO8_WHITE]; task->angle = 0.0; task->zoom = 1.0; task->spinning = true; @@ -331,6 +429,8 @@ static result_t sofa_redraw(const wuss_event_t *event, void *task_data) b = &screen[box_cube_edges[i][1]]; screen_draw_line_wu_fix8(scr, a->x, a->y, b->x, b->y, sc->line); } + + draw_vertex_dots(scr, screen, 8, sc->dot); } } else if (sc->shape == sofa_SHAPE_SHIP) @@ -349,6 +449,27 @@ static result_t sofa_redraw(const wuss_event_t *event, void *task_data) b = &screen[ship_edges[i][1]]; screen_draw_line_wu_fix8(scr, a->x, a->y, b->x, b->y, sc->line); } + + draw_vertex_dots(scr, screen, (int) NELEMS(ship_vertices), sc->dot); + } + else if (sc->shape == sofa_SHAPE_COBRA) + { + fix8_point_t screen[NELEMS(cobra_vertices)]; + int i; + + for (i = 0; i < (int) NELEMS(cobra_vertices); i++) + screen[i] = project(rotate_xy(cobra_vertices[i], SOFA_TILT, sc->angle), cx, cy, unit); + + for (i = 0; i < (int) NELEMS(cobra_edges); i++) + { + const fix8_point_t *a, *b; + + a = &screen[cobra_edges[i][0]]; + b = &screen[cobra_edges[i][1]]; + screen_draw_line_wu_fix8(scr, a->x, a->y, b->x, b->y, sc->line); + } + + draw_vertex_dots(scr, screen, (int) NELEMS(cobra_vertices), sc->dot); } else { @@ -369,6 +490,8 @@ static result_t sofa_redraw(const wuss_event_t *event, void *task_data) b = &screen[wf->edges[i][1]]; screen_draw_line_wu_fix8(scr, a->x, a->y, b->x, b->y, sc->line); } + + draw_vertex_dots(scr, screen, wf->nvertices, sc->dot); } return result_OK; diff --git a/libraries/wuss/test/tasks/sofa.h b/libraries/wuss/test/tasks/sofa.h index a092ecaf..7c4894e9 100644 --- a/libraries/wuss/test/tasks/sofa.h +++ b/libraries/wuss/test/tasks/sofa.h @@ -15,6 +15,7 @@ typedef enum sofa_shape { sofa_SHAPE_SOFA, sofa_SHAPE_SHIP, + sofa_SHAPE_COBRA, sofa_SHAPE_TETRAHEDRON, sofa_SHAPE_CUBE, sofa_SHAPE_OCTAHEDRON, @@ -30,7 +31,7 @@ sofa_shape_t; typedef struct sofa_task { wuss_window_t *window; - colour_t bg, line; + colour_t bg, line, dot; double angle; double zoom; /* scroll-adjustable */ bool spinning; From cbd439706f025256e180ef1fa987a7cccabb1f77 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 30 Aug 2026 10:28:35 +0100 Subject: [PATCH 069/287] fix(screen): remove signed-overflow and negative-shift UB in wu fix8 lines The anti-aliased fixed-point line rasteriser had three undefined behaviours, all reachable with long or off-screen endpoints: - FIX16_ONE * dy_f8 (and grad_f16 * dx) overflowed 32-bit int once a line exceeded ~32k fixed-point units; compute those in long long. - INT_TO_FIX8(iy) left-shifted pixel Y coordinates that can be negative; use a multiply instead. - the yf recurrence left-shifted a possibly-negative fix8 value before shifting back; form the fix16 sum by multiply, then arithmetic-shift down. Adds test_wu_fix8_extreme_coords covering large and off-screen endpoints, and fixes the test draw() helper's own negative left shift. Co-Authored-By: Claude Sonnet 5 --- libraries/framebuf/screen/screen-draw.c | 26 +++++++----- libraries/framebuf/screen/test/screen-test.c | 43 ++++++++++++++++++-- 2 files changed, 55 insertions(+), 14 deletions(-) diff --git a/libraries/framebuf/screen/screen-draw.c b/libraries/framebuf/screen/screen-draw.c index 50c9b453..2fc46547 100644 --- a/libraries/framebuf/screen/screen-draw.c +++ b/libraries/framebuf/screen/screen-draw.c @@ -483,18 +483,20 @@ void screen_draw_line_wu_fix8(screen_t *scr, SWAP(y0_f8, y1_f8); } - grad_f16 = (dx_f8 == 0) ? FIX16_ONE : FIX16_ONE * dy_f8 / dx_f8; + /* 64-bit intermediates: FIX16_ONE * dy_f8 and grad_f16 * dx overflow int. */ + grad_f16 = (dx_f8 == 0) ? FIX16_ONE : (fix16_t) ((long long) FIX16_ONE * dy_f8 / dx_f8); /* start point */ xend_i = FIX8_ROUND_TO_INT(x0_f8); - yend_f8 = y0_f8 + grad_f16 * (INT_TO_FIX8(xend_i) - x0_f8) / FIX16_ONE; + yend_f8 = y0_f8 + (fix8_t) ((long long) grad_f16 * (INT_TO_FIX8(xend_i) - x0_f8) / FIX16_ONE); xgap_f8 = INT_TO_FIX8(xend_i) + FIX8_ONE / 2 - x0_f8; assert(xgap_f8 >= 0 && xgap_f8 <= FIX8_ONE); ix0_i = xend_i; iy0_i = FIX8_FLOOR_TO_INT(yend_f8); - alpha1_i = (255 * (INT_TO_FIX8(iy0_i) + FIX8_ONE - yend_f8) * xgap_f8 / FIX8_ONE) / FIX8_ONE; - alpha2_i = (255 * -(INT_TO_FIX8(iy0_i) - yend_f8) * xgap_f8 / FIX8_ONE) / FIX8_ONE; + /* iy0_i may be negative; use multiply not INT_TO_FIX8's left shift. */ + alpha1_i = (255 * (iy0_i * FIX8_ONE + FIX8_ONE - yend_f8) * xgap_f8 / FIX8_ONE) / FIX8_ONE; + alpha2_i = (255 * -(iy0_i * FIX8_ONE - yend_f8) * xgap_f8 / FIX8_ONE) / FIX8_ONE; if (steep_b) { screen_blend_pixel(scr, iy0_i, ix0_i, colour, alpha1_i); @@ -506,18 +508,20 @@ void screen_draw_line_wu_fix8(screen_t *scr, screen_blend_pixel(scr, ix0_i, iy0_i + 1, colour, alpha2_i); } - yf_f8 = ((yend_f8 << (FIX16_SHIFT - FIX8_SHIFT)) + grad_f16) >> (FIX16_SHIFT - FIX8_SHIFT); + /* yend_f8 may be negative; form the fix16 sum by multiply (left-shifting a + * negative is UB) then arithmetic-shift back down. */ + yf_f8 = (yend_f8 * (FIX16_ONE / FIX8_ONE) + grad_f16) >> (FIX16_SHIFT - FIX8_SHIFT); /* end point */ xend_i = FIX8_ROUND_TO_INT(x1_f8); - yend_f8 = y1_f8 + grad_f16 * (INT_TO_FIX8(xend_i) - x1_f8) / FIX16_ONE; + yend_f8 = y1_f8 + (fix8_t) ((long long) grad_f16 * (INT_TO_FIX8(xend_i) - x1_f8) / FIX16_ONE); xgap_f8 = x1_f8 + FIX8_ONE / 2 - INT_TO_FIX8(xend_i); assert(xgap_f8 >= 0 && xgap_f8 < FIX8_ONE); ix1_i = xend_i; iy1_i = FIX8_FLOOR_TO_INT(yend_f8); - alpha1_i = (255 * (INT_TO_FIX8(iy1_i) + FIX8_ONE - yend_f8) * xgap_f8 / FIX8_ONE) / FIX8_ONE; - alpha2_i = (255 * -(INT_TO_FIX8(iy1_i) - yend_f8) * xgap_f8 / FIX8_ONE) / FIX8_ONE; + alpha1_i = (255 * (iy1_i * FIX8_ONE + FIX8_ONE - yend_f8) * xgap_f8 / FIX8_ONE) / FIX8_ONE; + alpha2_i = (255 * -(iy1_i * FIX8_ONE - yend_f8) * xgap_f8 / FIX8_ONE) / FIX8_ONE; if (steep_b) { screen_blend_pixel(scr, iy1_i, ix1_i, colour, alpha1_i); @@ -534,8 +538,8 @@ void screen_draw_line_wu_fix8(screen_t *scr, for (x_i = ix0_i + 1; x_i < ix1_i; x_i++) { y_i = FIX8_FLOOR_TO_INT(yf_f8); - alpha1_i = (255 * (INT_TO_FIX8(y_i) + FIX8_ONE - yf_f8)) / FIX8_ONE; - alpha2_i = (255 * -(INT_TO_FIX8(y_i) - yf_f8)) / FIX8_ONE; + alpha1_i = (255 * (y_i * FIX8_ONE + FIX8_ONE - yf_f8)) / FIX8_ONE; + alpha2_i = (255 * -(y_i * FIX8_ONE - yf_f8)) / FIX8_ONE; if (steep_b) { screen_blend_pixel(scr, y_i, x_i, colour, alpha1_i); @@ -546,7 +550,7 @@ void screen_draw_line_wu_fix8(screen_t *scr, screen_blend_pixel(scr, x_i, y_i, colour, alpha1_i); screen_blend_pixel(scr, x_i, y_i + 1, colour, alpha2_i); } - yf_f8 = ((yf_f8 << (FIX16_SHIFT - FIX8_SHIFT)) + grad_f16) >> (FIX16_SHIFT - FIX8_SHIFT); + yf_f8 = (yf_f8 * (FIX16_ONE / FIX8_ONE) + grad_f16) >> (FIX16_SHIFT - FIX8_SHIFT); } } diff --git a/libraries/framebuf/screen/test/screen-test.c b/libraries/framebuf/screen/test/screen-test.c index fb8cd919..a3ce2903 100644 --- a/libraries/framebuf/screen/test/screen-test.c +++ b/libraries/framebuf/screen/test/screen-test.c @@ -102,9 +102,10 @@ static void draw(screen_t *scr, linekind_t kind, const linetest_t *line, break; case linekind_WU_FIX8: + /* multiply, not INT_TO_FIX8: coords may be negative (left shift is UB). */ screen_draw_line_wu_fix8(scr, - INT_TO_FIX8(line->x0), INT_TO_FIX8(line->y0), - INT_TO_FIX8(line->x1), INT_TO_FIX8(line->y1), + line->x0 * FIX8_ONE, line->y0 * FIX8_ONE, + line->x1 * FIX8_ONE, line->y1 * FIX8_ONE, colour); break; @@ -202,6 +203,41 @@ static result_t test_clipping_still_happens(void) /* ----------------------------------------------------------------------- */ +/* Wu fix8 lines with large and off-screen endpoints must not trip + * UndefinedBehaviorSanitizer: the gradient maths once overflowed 32-bit int + * (FIX16_ONE * dy_f8) and left-shifted negative pixel coordinates. */ +static result_t test_wu_fix8_extreme_coords(void) +{ + /* fix8: value * 256, written out to avoid left-shifting negatives here too. */ + static const fix8_t endpoints[][4] = + { + { -1000 * 256, 32 * 256, 2000 * 256, 33 * 256 }, + { 32 * 256, -1000 * 256, 31 * 256, 2000 * 256 }, + { -5000 * 256, -5000 * 256, 5000 * 256, 5000 * 256 }, + { -32000 * 256, 10 * 256, 32000 * 256, 50 * 256 } + }; + + static testscreen_t ts; + + colour_t colour; + size_t i; + + colour = colour_rgb(255, 255, 255); + + for (i = 0; i < NELEMS(endpoints); i++) + { + testscreen_init(&ts); + screen_draw_line_wu_fix8(&ts.scr, + endpoints[i][0], endpoints[i][1], + endpoints[i][2], endpoints[i][3], + colour); + } + + return result_TEST_PASSED; +} + +/* ----------------------------------------------------------------------- */ + result_t screen_test(const char *resources) { typedef result_t (*screentestfn)(void); @@ -209,7 +245,8 @@ result_t screen_test(const char *resources) static const screentestfn tests[] = { test_clip_invariance, - test_clipping_still_happens + test_clipping_still_happens, + test_wu_fix8_extreme_coords }; result_t rc; From d6cf9d15e3afc15cfb1a698581cc972de2953fbe Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 30 Aug 2026 11:01:29 +0100 Subject: [PATCH 070/287] refactor(wuss): make launcher rows work-area button icons Replace the launcher's hand-drawn text rows and bespoke mouse hit-testing with wuss_ICON_TYPE_BUTTON icons created via wuss_icon_create_array; wuss now paints and hit-tests them and delivers clicks as wuss_EVENT_ICON. Each click spawns a fresh instance of the row's task, so a row can be clicked any number of times. Drops launcher_redraw/launcher_mouse, the running[] tint array, and the now-unused font and palette parameters. Shutdown calls every entry's destroy() unconditionally (NULL-safe). Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/test/tasks/launcher.c | 150 +++++++++++---------------- libraries/wuss/test/tasks/launcher.h | 18 ++-- libraries/wuss/test/wuss-test.c | 8 +- 3 files changed, 72 insertions(+), 104 deletions(-) diff --git a/libraries/wuss/test/tasks/launcher.c b/libraries/wuss/test/tasks/launcher.c index 69831975..d01b565e 100644 --- a/libraries/wuss/test/tasks/launcher.c +++ b/libraries/wuss/test/tasks/launcher.c @@ -12,46 +12,72 @@ #include "base/utils.h" #include "framebuf/palettes.h" #include "geom/box.h" -#include "geom/point.h" + +#include "wuss/icon.h" #include "launcher.h" -#define LAUNCHER_ROW_HEIGHT 20 +#define LAUNCHER_ROW_HEIGHT 22 #define LAUNCHER_PAD 4 #define LAUNCHER_WIDTH 160 result_t launcher_create(wuss_t *wuss, const launcher_entry_t *entries, int nentries, - bmfont_t *font, - const colour_t *palette, launcher_task_t *task) { - wuss_task_t delegate; - size2d_t sz; + wuss_task_t delegate; + wuss_icon_spec_t specs[LAUNCHER_MAX_ENTRIES]; + size2d_t sz; + int i; + result_t rc; assert(nentries <= LAUNCHER_MAX_ENTRIES); - task->entries = entries; - task->nentries = nentries; - memset(task->running, 0, sizeof(task->running)); - task->font = font; - task->fg = palette[palette_PICO8_BLACK]; - task->bg = palette[palette_PICO8_WHITE]; - task->running_fg = palette[palette_PICO8_LIGHT_GREY]; - - delegate = wuss_task_start(launcher_handle, task); /* launcher_redraw paints its own background */ - sz = SIZE2D(LAUNCHER_WIDTH, LAUNCHER_PAD * 2 + nentries * LAUNCHER_ROW_HEIGHT); - - return wuss_window_create_placed(wuss, - sz, - "Launcher", - wuss_WINDOW_NO_CLOSE, - wuss_NO_BACKGROUND, - &delegate, - sz, - SIZE2D(0, 0), - &task->window); + task->entries = entries; + task->nentries = nentries; + task->window = NULL; + memset(task->icons, 0, sizeof(task->icons)); + + delegate = wuss_task_start(launcher_handle, task); + sz = SIZE2D(LAUNCHER_WIDTH, + LAUNCHER_PAD * 2 + nentries * LAUNCHER_ROW_HEIGHT); + + rc = wuss_window_create_placed(wuss, + sz, + "Launcher", + wuss_WINDOW_NO_CLOSE, + palette_PICO8_LIGHT_GREY, + &delegate, + sz, + SIZE2D(0, 0), + &task->window); + if (rc != result_OK) + return rc; + + memset(specs, 0, sizeof(specs)); + + for (i = 0; i < nentries; i++) + { + specs[i].bbox = (box_t) BOX_POS_SIZE(LAUNCHER_PAD, + LAUNCHER_PAD + i * LAUNCHER_ROW_HEIGHT, + LAUNCHER_WIDTH - LAUNCHER_PAD * 2, + LAUNCHER_ROW_HEIGHT - 2); + specs[i].type = wuss_ICON_TYPE_BUTTON; + specs[i].text = entries[i].name; + specs[i].fg = palette_PICO8_BLACK; + specs[i].bg = palette_PICO8_LIGHT_GREY; + } + + rc = wuss_icon_create_array(task->window, specs, nentries, task->icons); + if (rc != result_OK) + { + wuss_window_close(task->window); + task->window = NULL; + return rc; + } + + return result_OK; } void launcher_destroy(launcher_task_t *task) @@ -59,83 +85,29 @@ void launcher_destroy(launcher_task_t *task) wuss_window_close(task->window); } -static result_t launcher_redraw(const wuss_event_t *event, void *task_data) +static result_t launcher_icon(launcher_task_t *lc, const wuss_icon_t *icon) { - launcher_task_t *lc; - screen_t *scr; - const box_t *content, *bounds; - int i, font_width, font_height, sx, sy; - point_t pos; - const launcher_entry_t *entry; - - lc = task_data; - - scr = event->data.redraw.scr; - content = event->data.redraw.content; - bounds = event->data.redraw.bounds; - sx = event->data.redraw.scroll.x; - sy = event->data.redraw.scroll.y; - - screen_draw_rect(scr, content->x0, content->y0, box_size(content), lc->bg); - - bmfont_get_info(lc->font, NULL, &font_height); + int i; for (i = 0; i < lc->nentries; i++) - { - entry = &lc->entries[i]; - - pos.x = bounds->x0 - sx + LAUNCHER_PAD; - pos.y = bounds->y0 - sy + LAUNCHER_PAD + i * LAUNCHER_ROW_HEIGHT + (LAUNCHER_ROW_HEIGHT - font_height) / 2; - - bmfont_draw(lc->font, scr, entry->name, (int) strlen(entry->name), - lc->running[i] ? lc->running_fg : lc->fg, lc->bg, &pos, NULL); - } + if (lc->icons[i] == icon) + return lc->entries[i].spawn(); return result_OK; } -static result_t launcher_mouse(wuss_window_t *window, int y, void *task_data) -{ - launcher_task_t *lc; - int i; - const launcher_entry_t *entry; - result_t rc; - - lc = task_data; - - /* y already arrives in virtual content space, which is how the rows are - * laid out, so the scroll offset must not be added again here. */ - i = (y - LAUNCHER_PAD) / LAUNCHER_ROW_HEIGHT; - if (i < 0 || i >= lc->nentries) - return result_OK; - - if (lc->running[i]) - return result_OK; - - entry = &lc->entries[i]; - rc = entry->spawn(); - if (rc == result_OK) - { - lc->running[i] = true; - wuss_window_invalidate_all(window); - } - - return rc; -} - result_t launcher_handle(wuss_window_t *window, const wuss_event_t *event, void *task_data) { + NOT_USED(window); + switch (event->kind) { - case wuss_EVENT_REDRAW: - return launcher_redraw(event, task_data); - - case wuss_EVENT_MOUSE: - if (event->data.mouse.action != wuss_MOUSE_DOWN) + case wuss_EVENT_ICON: + if (event->data.icon.action != wuss_MOUSE_DOWN) return result_OK; - return launcher_mouse(window, event->data.mouse.point.y, task_data); + return launcher_icon(task_data, event->data.icon.icon); default: return result_OK; diff --git a/libraries/wuss/test/tasks/launcher.h b/libraries/wuss/test/tasks/launcher.h index 4147975e..a72976b9 100644 --- a/libraries/wuss/test/tasks/launcher.h +++ b/libraries/wuss/test/tasks/launcher.h @@ -7,8 +7,7 @@ #include -#include "framebuf/bmfont.h" -#include "framebuf/colour.h" +#include "wuss/icon.h" #include "wuss/window.h" typedef result_t (*launcher_spawn_fn_t)(void); @@ -23,31 +22,26 @@ typedef struct launcher_entry } launcher_entry_t; -/* a row's task is spawned at most once: launcher_task's "running" array is - * set on the first click and never cleared, so a second click is a no-op -- - * relaunching a task after its window closes needs the test restarted */ +/* each row is a work-area button icon; clicking one spawns a fresh instance + * of its task, so a row may be clicked any number of times */ #define LAUNCHER_MAX_ENTRIES 32 typedef struct launcher_task { const launcher_entry_t *entries; /* owned by the caller, must outlive the window */ int nentries; - bool running[LAUNCHER_MAX_ENTRIES]; - bmfont_t *font; - colour_t fg, bg, running_fg; + wuss_icon_t *icons[LAUNCHER_MAX_ENTRIES]; wuss_window_t *window; } launcher_task_t; wuss_event_fn_t launcher_handle; -/* create a window listing "entries"; clicking a row calls its spawn - * function once */ +/* create a window of button icons, one per entry; each click on a button + * calls its spawn function */ result_t launcher_create(wuss_t *wuss, const launcher_entry_t *entries, int nentries, - bmfont_t *font, - const colour_t *palette, launcher_task_t *task); void launcher_destroy(launcher_task_t *task); diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index c0de3242..d9fe6da5 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -276,7 +276,7 @@ static result_t wuss_interactive_test(const char *resources) g_resources = resources; g_daydream_font = daydream_font; - rc = launcher_create(wuss, g_launcher_entries, NELEMS(g_launcher_entries), font, palette, &launcher_task); + rc = launcher_create(wuss, g_launcher_entries, NELEMS(g_launcher_entries), &launcher_task); if (rc != result_OK) goto Failure; @@ -429,9 +429,11 @@ static result_t wuss_interactive_test(const char *resources) SDL_Delay(1000 / 60); } + /* a task may have been spawned any number of times but each keeps only its + * latest window in a shared static; destroy() closes that window if still + * open and is a no-op otherwise */ for (i = 0; i < NELEMS(g_launcher_entries); i++) - if (launcher_task.running[i]) - g_launcher_entries[i].destroy(); + g_launcher_entries[i].destroy(); launcher_destroy(&launcher_task); wuss_destroy(wuss); From bac930dc581118b674db4df48a4bf9af27e71770 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 30 Aug 2026 11:39:45 +0100 Subject: [PATCH 071/287] feat(wuss): set an overall screen margin in window packer Reuses WUSS_PLACE_GUTTER to pad the whole screen. --- libraries/wuss/window/create-placed.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/libraries/wuss/window/create-placed.c b/libraries/wuss/window/create-placed.c index 7f884ace..59475c98 100644 --- a/libraries/wuss/window/create-placed.c +++ b/libraries/wuss/window/create-placed.c @@ -87,6 +87,10 @@ result_t wuss_window_create_placed(wuss_t *wuss, if (wuss->layout == NULL) { + static const box_t margins = { + WUSS_PLACE_GUTTER, WUSS_PLACE_GUTTER, WUSS_PLACE_GUTTER, WUSS_PLACE_GUTTER + }; + screen.x0 = 0; screen.y0 = 0; screen.x1 = wuss->scr->size.w; @@ -96,6 +100,7 @@ result_t wuss_window_create_placed(wuss_t *wuss, if (wuss->layout == NULL) return result_OOM; + packer_set_margins(wuss->layout, &margins); packer_set_gutter(wuss->layout, WUSS_PLACE_GUTTER); } From 1afeb0d14db34cc041355abefe9aa818a54b93bd Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 30 Aug 2026 11:51:56 +0100 Subject: [PATCH 072/287] refactor(wuss): give each launcher task per-instance heap state Every launcher task backed its state with a single file-scope static in wuss-test.c, so spawning a task twice made both windows share one state block and only the latest window's animation ran. Each spawn_* now calloc's a fresh per-instance block, passes it to the task's *_create, and frees it again if create fails or opens no window. Each task's wuss_EVENT_CLOSE handler frees its own block (and any owned bitmaps); the checker task, whose two windows share one block, frees it once both have closed. The now-redundant *_destroy functions, the twelve g_*_task statics, the destroy_* wrappers and the launcher_entry_t.destroy column are removed. wuss_idle already walks every window, so tasks now animate independently in as many windows as are open. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/test/tasks/ball.c | 8 +- libraries/wuss/test/tasks/ball.h | 6 +- libraries/wuss/test/tasks/blank.c | 14 +- libraries/wuss/test/tasks/blank.h | 2 - libraries/wuss/test/tasks/chars.c | 10 +- libraries/wuss/test/tasks/chars.h | 2 - libraries/wuss/test/tasks/checker.c | 11 +- libraries/wuss/test/tasks/checker.h | 2 - libraries/wuss/test/tasks/curve.c | 9 +- libraries/wuss/test/tasks/curve.h | 2 - libraries/wuss/test/tasks/gradient.c | 9 +- libraries/wuss/test/tasks/gradient.h | 2 - libraries/wuss/test/tasks/icons.c | 8 +- libraries/wuss/test/tasks/icons.h | 2 - libraries/wuss/test/tasks/image.c | 9 +- libraries/wuss/test/tasks/image.h | 2 - libraries/wuss/test/tasks/launcher.h | 6 +- libraries/wuss/test/tasks/palette.c | 9 +- libraries/wuss/test/tasks/palette.h | 2 - libraries/wuss/test/tasks/porter-duff.c | 15 +- libraries/wuss/test/tasks/porter-duff.h | 2 - libraries/wuss/test/tasks/sofa.c | 9 +- libraries/wuss/test/tasks/sofa.h | 2 - libraries/wuss/test/tasks/text.c | 9 +- libraries/wuss/test/tasks/text.h | 2 - libraries/wuss/test/wuss-test.c | 199 +++++++++++++++++------- 26 files changed, 189 insertions(+), 164 deletions(-) diff --git a/libraries/wuss/test/tasks/ball.c b/libraries/wuss/test/tasks/ball.c index 388f37b4..4e0127ba 100644 --- a/libraries/wuss/test/tasks/ball.c +++ b/libraries/wuss/test/tasks/ball.c @@ -2,6 +2,8 @@ #ifdef USE_SDL +#include + #ifdef FORTIFY #include "fortify/fortify.h" #endif @@ -39,10 +41,6 @@ result_t ball_create(wuss_t *wuss, const colour_t *palette, ball_task_t *task) &task->window); } -void ball_destroy(ball_task_t *task) -{ - wuss_window_close(task->window); -} static result_t ball_redraw(const wuss_event_t *event, void *task_data) { @@ -201,7 +199,7 @@ result_t ball_handle(wuss_window_t *window, case wuss_EVENT_CLOSE: wuss_window_close(window); - bc->window = NULL; + free(bc); /* task_data was calloc'd per instance by the spawner */ return result_OK; default: diff --git a/libraries/wuss/test/tasks/ball.h b/libraries/wuss/test/tasks/ball.h index 3d17ab1e..f42fb579 100644 --- a/libraries/wuss/test/tasks/ball.h +++ b/libraries/wuss/test/tasks/ball.h @@ -34,12 +34,10 @@ ball_task_t; wuss_event_fn_t ball_handle; -/* create the bouncing-ball window against the given wuss instance */ +/* create the bouncing-ball window against the given wuss instance; "task" is a + * per-instance block owned by the window and freed when it closes */ result_t ball_create(wuss_t *wuss, const colour_t *palette, ball_task_t *task); -/* destroy the bouncing-ball window created by ball_create */ -void ball_destroy(ball_task_t *task); - #endif /* USE_SDL */ #endif /* TASKS_BALL_H */ diff --git a/libraries/wuss/test/tasks/blank.c b/libraries/wuss/test/tasks/blank.c index 7c95c9c8..86d3365a 100644 --- a/libraries/wuss/test/tasks/blank.c +++ b/libraries/wuss/test/tasks/blank.c @@ -2,6 +2,8 @@ #ifdef USE_SDL +#include + #ifdef FORTIFY #include "fortify/fortify.h" #endif @@ -36,11 +38,6 @@ result_t blank_create(wuss_t *wuss, int npalette, blank_task_t *task) &task->window); } -void blank_destroy(blank_task_t *task) -{ - wuss_window_close(task->window); -} - static result_t blank_idle(void *task_data) { blank_task_t *bc; @@ -65,7 +62,12 @@ result_t blank_handle(wuss_window_t *window, const wuss_event_t *event, void *task_data) { - NOT_USED(window); + if (event->kind == wuss_EVENT_CLOSE) + { + wuss_window_close(window); + free(task_data); /* calloc'd per instance by the spawner */ + return result_OK; + } if (event->kind != wuss_EVENT_IDLE) return result_OK; diff --git a/libraries/wuss/test/tasks/blank.h b/libraries/wuss/test/tasks/blank.h index a2586304..b1ffd74f 100644 --- a/libraries/wuss/test/tasks/blank.h +++ b/libraries/wuss/test/tasks/blank.h @@ -25,8 +25,6 @@ wuss_event_fn_t blank_handle; /* create the colour-cycling blank window against the given wuss instance */ result_t blank_create(wuss_t *wuss, int npalette, blank_task_t *task); -/* destroy the colour-cycling window created by blank_create */ -void blank_destroy(blank_task_t *task); #endif /* USE_SDL */ diff --git a/libraries/wuss/test/tasks/chars.c b/libraries/wuss/test/tasks/chars.c index 572461ca..1680d005 100644 --- a/libraries/wuss/test/tasks/chars.c +++ b/libraries/wuss/test/tasks/chars.c @@ -2,6 +2,8 @@ #ifdef USE_SDL +#include + #include #ifdef FORTIFY @@ -60,12 +62,6 @@ result_t chars_create(wuss_t *wuss, &task->window); } -void chars_destroy(chars_task_t *task) -{ - if (task->window != NULL) - wuss_window_close(task->window); -} - static result_t chars_redraw(const wuss_event_t *event, void *task_data) { chars_task_t *cc; @@ -126,7 +122,7 @@ result_t chars_handle(wuss_window_t *window, if (event->kind == wuss_EVENT_CLOSE) { wuss_window_close(window); - ((chars_task_t *) task_data)->window = NULL; + free(task_data); /* calloc'd per instance by the spawner */ return result_OK; } diff --git a/libraries/wuss/test/tasks/chars.h b/libraries/wuss/test/tasks/chars.h index 80495809..5df4199c 100644 --- a/libraries/wuss/test/tasks/chars.h +++ b/libraries/wuss/test/tasks/chars.h @@ -27,8 +27,6 @@ result_t chars_create(wuss_t *wuss, const colour_t *palette, chars_task_t *task); -/* destroy the glyph-grid window created by chars_create, if any */ -void chars_destroy(chars_task_t *task); #endif /* USE_SDL */ diff --git a/libraries/wuss/test/tasks/checker.c b/libraries/wuss/test/tasks/checker.c index dbd5e4e8..71a77c9d 100644 --- a/libraries/wuss/test/tasks/checker.c +++ b/libraries/wuss/test/tasks/checker.c @@ -2,6 +2,8 @@ #ifdef USE_SDL +#include + #ifdef FORTIFY #include "fortify/fortify.h" #endif @@ -62,12 +64,6 @@ result_t checker_create(wuss_t *wuss, return result_OK; } -void checker_destroy(checker_task_t *task) -{ - wuss_window_close(task->window); - wuss_window_close(task->window2); -} - static result_t checker_redraw(wuss_window_t *window, const wuss_event_t *event, void *task_data) @@ -169,6 +165,9 @@ result_t checker_handle(wuss_window_t *window, cc->window2 = NULL; else cc->window = NULL; + /* one calloc'd block backs both windows; free it once both are gone */ + if (cc->window == NULL && cc->window2 == NULL) + free(cc); return result_OK; default: diff --git a/libraries/wuss/test/tasks/checker.h b/libraries/wuss/test/tasks/checker.h index 4df752a8..ca3983ce 100644 --- a/libraries/wuss/test/tasks/checker.h +++ b/libraries/wuss/test/tasks/checker.h @@ -37,8 +37,6 @@ result_t checker_create(wuss_t *wuss, const colour_t *palette, checker_task_t *task); -/* destroy the checkerboard windows created by checker_create */ -void checker_destroy(checker_task_t *task); #endif /* USE_SDL */ diff --git a/libraries/wuss/test/tasks/curve.c b/libraries/wuss/test/tasks/curve.c index 2b9f44ef..d7ae2971 100644 --- a/libraries/wuss/test/tasks/curve.c +++ b/libraries/wuss/test/tasks/curve.c @@ -2,6 +2,8 @@ #ifdef USE_SDL +#include + #include #ifdef FORTIFY @@ -51,11 +53,6 @@ result_t curve_create(wuss_t *wuss, &task->window); } -void curve_destroy(curve_task_t *task) -{ - wuss_window_close(task->window); -} - static int blob_hit(const point_t *p, int x, int y) { int half = CURVE_BLOBSZ / 2; @@ -181,7 +178,7 @@ result_t curve_handle(wuss_window_t *window, case wuss_EVENT_CLOSE: wuss_window_close(window); - task->window = NULL; + free(task); /* task_data was calloc'd per instance by the spawner */ return result_OK; default: diff --git a/libraries/wuss/test/tasks/curve.h b/libraries/wuss/test/tasks/curve.h index 2a411ffe..de7a2e80 100644 --- a/libraries/wuss/test/tasks/curve.h +++ b/libraries/wuss/test/tasks/curve.h @@ -30,8 +30,6 @@ result_t curve_create(wuss_t *wuss, const colour_t *palette, curve_task_t *task); -/* destroy the curve window created by curve_create */ -void curve_destroy(curve_task_t *task); #endif /* USE_SDL */ diff --git a/libraries/wuss/test/tasks/gradient.c b/libraries/wuss/test/tasks/gradient.c index 0e3cc849..0602325c 100644 --- a/libraries/wuss/test/tasks/gradient.c +++ b/libraries/wuss/test/tasks/gradient.c @@ -2,6 +2,8 @@ #ifdef USE_SDL +#include + #ifdef FORTIFY #include "fortify/fortify.h" #endif @@ -48,11 +50,6 @@ result_t gradient_create(wuss_t *wuss, gradient_task_t *task) &task->window); } -void gradient_destroy(gradient_task_t *task) -{ - wuss_window_close(task->window); -} - static result_t gradient_redraw(const wuss_event_t *event, void *task_data) { screen_t *scr; @@ -99,7 +96,7 @@ result_t gradient_handle(wuss_window_t *window, case wuss_EVENT_CLOSE: wuss_window_close(window); - gc->window = NULL; + free(gc); /* task_data was calloc'd per instance by the spawner */ return result_OK; default: diff --git a/libraries/wuss/test/tasks/gradient.h b/libraries/wuss/test/tasks/gradient.h index a37ff22a..8f720073 100644 --- a/libraries/wuss/test/tasks/gradient.h +++ b/libraries/wuss/test/tasks/gradient.h @@ -21,8 +21,6 @@ wuss_event_fn_t gradient_handle; /* create the gradient window against the given wuss instance */ result_t gradient_create(wuss_t *wuss, gradient_task_t *task); -/* destroy the gradient window created by gradient_create */ -void gradient_destroy(gradient_task_t *task); #endif /* USE_SDL */ diff --git a/libraries/wuss/test/tasks/icons.c b/libraries/wuss/test/tasks/icons.c index 70a247c7..5f184f38 100644 --- a/libraries/wuss/test/tasks/icons.c +++ b/libraries/wuss/test/tasks/icons.c @@ -4,6 +4,7 @@ #include "framebuf/palettes.h" #include +#include #include #ifdef FORTIFY @@ -102,11 +103,6 @@ result_t icons_create(wuss_t *wuss, return rc; } -void icons_destroy(icons_task_t *task) -{ - wuss_window_close(task->window); -} - #define ICONS_GRID 16 /* document-space pitch of the backdrop grid */ #define ICONS_AXIS_LABEL 64 /* label every Nth grid line along each axis */ @@ -237,7 +233,7 @@ result_t icons_handle(wuss_window_t *window, case wuss_EVENT_CLOSE: wuss_window_close(window); - tcx->window = NULL; + free(tcx); /* calloc'd per instance by the spawner */ return result_OK; default: diff --git a/libraries/wuss/test/tasks/icons.h b/libraries/wuss/test/tasks/icons.h index e683142f..aebf0ea2 100644 --- a/libraries/wuss/test/tasks/icons.h +++ b/libraries/wuss/test/tasks/icons.h @@ -34,8 +34,6 @@ result_t icons_create(wuss_t *wuss, bmfont_t *font, icons_task_t *task); -/* destroy the icons window created by icons_create */ -void icons_destroy(icons_task_t *task); #endif /* USE_SDL */ diff --git a/libraries/wuss/test/tasks/image.c b/libraries/wuss/test/tasks/image.c index 0c7c9ee5..70bd56da 100644 --- a/libraries/wuss/test/tasks/image.c +++ b/libraries/wuss/test/tasks/image.c @@ -46,12 +46,6 @@ result_t image_create(wuss_t *wuss, &task->window); } -void image_destroy(image_task_t *task) -{ - wuss_window_close(task->window); - free(task->bitmap.base); -} - static result_t image_redraw(const wuss_event_t *event, void *task_data) { image_task_t *ic; @@ -86,7 +80,8 @@ result_t image_handle(wuss_window_t *window, case wuss_EVENT_CLOSE: wuss_window_close(window); - ic->window = NULL; + free(ic->bitmap.base); + free(ic); /* task_data was calloc'd per instance by the spawner */ return result_OK; default: diff --git a/libraries/wuss/test/tasks/image.h b/libraries/wuss/test/tasks/image.h index 87e1c53b..3f767b4c 100644 --- a/libraries/wuss/test/tasks/image.h +++ b/libraries/wuss/test/tasks/image.h @@ -26,8 +26,6 @@ result_t image_create(wuss_t *wuss, const char *path, image_task_t *task); -/* destroy the window and free the bitmap loaded by image_create */ -void image_destroy(image_task_t *task); #endif /* USE_SDL */ diff --git a/libraries/wuss/test/tasks/launcher.h b/libraries/wuss/test/tasks/launcher.h index a72976b9..573f3e07 100644 --- a/libraries/wuss/test/tasks/launcher.h +++ b/libraries/wuss/test/tasks/launcher.h @@ -11,14 +11,12 @@ #include "wuss/window.h" typedef result_t (*launcher_spawn_fn_t)(void); -typedef void (*launcher_destroy_fn_t)(void); /* one clickable row */ typedef struct launcher_entry { - const char *name; - launcher_spawn_fn_t spawn; - launcher_destroy_fn_t destroy; + const char *name; + launcher_spawn_fn_t spawn; } launcher_entry_t; diff --git a/libraries/wuss/test/tasks/palette.c b/libraries/wuss/test/tasks/palette.c index f98e18bd..6aa16e7c 100644 --- a/libraries/wuss/test/tasks/palette.c +++ b/libraries/wuss/test/tasks/palette.c @@ -2,6 +2,8 @@ #ifdef USE_SDL +#include + #ifdef FORTIFY #include "fortify/fortify.h" #endif @@ -35,11 +37,6 @@ result_t palette_create(wuss_t *wuss, &task->window); } -void palette_destroy(palette_task_t *task) -{ - wuss_window_close(task->window); -} - static result_t palette_redraw(const wuss_event_t *event, void *task_data) { palette_task_t *pc; @@ -89,7 +86,7 @@ result_t palette_handle(wuss_window_t *window, if (event->kind == wuss_EVENT_CLOSE) { wuss_window_close(window); - ((palette_task_t *) task_data)->window = NULL; + free(task_data); /* calloc'd per instance by the spawner */ return result_OK; } diff --git a/libraries/wuss/test/tasks/palette.h b/libraries/wuss/test/tasks/palette.h index 8f7445b1..8463df9a 100644 --- a/libraries/wuss/test/tasks/palette.h +++ b/libraries/wuss/test/tasks/palette.h @@ -26,8 +26,6 @@ result_t palette_create(wuss_t *wuss, int npalette, palette_task_t *task); -/* destroy the palette-swatch-grid window created by palette_create */ -void palette_destroy(palette_task_t *task); #endif /* USE_SDL */ diff --git a/libraries/wuss/test/tasks/porter-duff.c b/libraries/wuss/test/tasks/porter-duff.c index 5d586693..8c6d1c91 100644 --- a/libraries/wuss/test/tasks/porter-duff.c +++ b/libraries/wuss/test/tasks/porter-duff.c @@ -218,15 +218,6 @@ result_t porter_duff_create(wuss_t *wuss, return rc; } -void porter_duff_destroy(porter_duff_task_t *task) -{ - wuss_window_close(task->window); - free(task->dst.base); - free(task->src.base); - free(task->b.base); - free(task->a.base); -} - /* ----------------------------------------------------------------------- */ /* Triangle ramp: 0 at the start of the rule's turn, 255 at its midpoint, back @@ -403,7 +394,11 @@ result_t porter_duff_handle(wuss_window_t *window, case wuss_EVENT_CLOSE: wuss_window_close(window); - pd->window = NULL; + free(pd->dst.base); + free(pd->src.base); + free(pd->b.base); + free(pd->a.base); + free(pd); /* calloc'd per instance by the spawner */ return result_OK; default: diff --git a/libraries/wuss/test/tasks/porter-duff.h b/libraries/wuss/test/tasks/porter-duff.h index d8dbd366..3a7f210e 100644 --- a/libraries/wuss/test/tasks/porter-duff.h +++ b/libraries/wuss/test/tasks/porter-duff.h @@ -42,8 +42,6 @@ result_t porter_duff_create(wuss_t *wuss, const char *resources, porter_duff_task_t *task); -/* destroy the window and free the bitmaps allocated by porter_duff_create */ -void porter_duff_destroy(porter_duff_task_t *task); #endif /* USE_SDL */ diff --git a/libraries/wuss/test/tasks/sofa.c b/libraries/wuss/test/tasks/sofa.c index 7d0d0aff..e0294768 100644 --- a/libraries/wuss/test/tasks/sofa.c +++ b/libraries/wuss/test/tasks/sofa.c @@ -2,6 +2,8 @@ #ifdef USE_SDL +#include + #include #ifdef FORTIFY @@ -378,11 +380,6 @@ result_t sofa_create(wuss_t *wuss, const colour_t *palette, sofa_task_t *task) &task->window); } -void sofa_destroy(sofa_task_t *task) -{ - wuss_window_close(task->window); -} - static result_t sofa_redraw(const wuss_event_t *event, void *task_data) { sofa_task_t *sc; @@ -582,7 +579,7 @@ result_t sofa_handle(wuss_window_t *window, case wuss_EVENT_CLOSE: wuss_window_close(window); - sc->window = NULL; + free(sc); /* task_data was calloc'd per instance by the spawner */ return result_OK; default: diff --git a/libraries/wuss/test/tasks/sofa.h b/libraries/wuss/test/tasks/sofa.h index 7c4894e9..1a8c9ffd 100644 --- a/libraries/wuss/test/tasks/sofa.h +++ b/libraries/wuss/test/tasks/sofa.h @@ -45,8 +45,6 @@ wuss_event_fn_t sofa_handle; /* create the sofa window against the given wuss instance */ result_t sofa_create(wuss_t *wuss, const colour_t *palette, sofa_task_t *task); -/* destroy the sofa window created by sofa_create */ -void sofa_destroy(sofa_task_t *task); #endif /* USE_SDL */ diff --git a/libraries/wuss/test/tasks/text.c b/libraries/wuss/test/tasks/text.c index 2c2e9eb9..1c5e8d0a 100644 --- a/libraries/wuss/test/tasks/text.c +++ b/libraries/wuss/test/tasks/text.c @@ -2,6 +2,8 @@ #ifdef USE_SDL +#include + #include #include #include @@ -60,11 +62,6 @@ result_t text_create(wuss_t *wuss, return rc; } -void text_destroy(text_task_t *task) -{ - wuss_window_close(task->window); -} - static result_t text_redraw(const wuss_event_t *event, void *task_data) { text_task_t *tcx; @@ -183,7 +180,7 @@ result_t text_handle(wuss_window_t *window, case wuss_EVENT_CLOSE: wuss_window_close(window); - tcx->window = NULL; + free(tcx); /* task_data was calloc'd per instance by the spawner */ return result_OK; case wuss_EVENT_IDLE: diff --git a/libraries/wuss/test/tasks/text.h b/libraries/wuss/test/tasks/text.h index cf1dd7fe..d7a985af 100644 --- a/libraries/wuss/test/tasks/text.h +++ b/libraries/wuss/test/tasks/text.h @@ -34,8 +34,6 @@ result_t text_create(wuss_t *wuss, bmfont_t *font, text_task_t *task); -/* destroy the paragraph-of-text window created by text_create */ -void text_destroy(text_task_t *task); #endif /* USE_SDL */ diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index d9fe6da5..e8dcc6ec 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -57,68 +57,154 @@ static int g_npalette; static const char *g_resources; static bmfont_t *g_daydream_font; -static ball_task_t g_ball_task; -static text_task_t g_text_task; -static blank_task_t g_blank_task; -static chars_task_t g_chars_task; -static palette_task_t g_palette_task; -static image_task_t g_image_task; -static checker_task_t g_checker_task; -static curve_task_t g_curve_task; -static sofa_task_t g_sofa_task; -static gradient_task_t g_gradient_task; -static icons_task_t g_icons_task; -static porter_duff_task_t g_porter_duff_task; - -static result_t spawn_ball(void) { return ball_create(g_wuss, g_palette, &g_ball_task); } -static result_t spawn_text(void) { return text_create(g_wuss, g_palette, g_daydream_font, &g_text_task); } -static result_t spawn_blank(void) { return blank_create(g_wuss, g_npalette, &g_blank_task); } -static result_t spawn_chars(void) { return chars_create(g_wuss, g_palette, &g_chars_task); } -static result_t spawn_palette(void) { return palette_create(g_wuss, g_palette, g_npalette, &g_palette_task); } +/* Each spawn allocates a fresh per-instance task block so a task may run in + * several windows at once; the block is owned by its window and freed by the + * task's wuss_EVENT_CLOSE handler. If create fails, or (for the font-less + * chars task) returns OK without opening a window, the block is freed here -- + * otherwise it would leak. */ + +static result_t spawn_ball(void) +{ + ball_task_t *t = calloc(1, sizeof(*t)); + result_t rc; + if (t == NULL) return result_OOM; + rc = ball_create(g_wuss, g_palette, t); + if (rc != result_OK || t->window == NULL) { free(t); return rc; } + return result_OK; +} + +static result_t spawn_text(void) +{ + text_task_t *t = calloc(1, sizeof(*t)); + result_t rc; + if (t == NULL) return result_OOM; + rc = text_create(g_wuss, g_palette, g_daydream_font, t); + if (rc != result_OK || t->window == NULL) { free(t); return rc; } + return result_OK; +} + +static result_t spawn_blank(void) +{ + blank_task_t *t = calloc(1, sizeof(*t)); + result_t rc; + if (t == NULL) return result_OOM; + rc = blank_create(g_wuss, g_npalette, t); + if (rc != result_OK || t->window == NULL) { free(t); return rc; } + return result_OK; +} + +static result_t spawn_chars(void) +{ + chars_task_t *t = calloc(1, sizeof(*t)); + result_t rc; + if (t == NULL) return result_OOM; + rc = chars_create(g_wuss, g_palette, t); + if (rc != result_OK || t->window == NULL) { free(t); return rc; } + return result_OK; +} + +static result_t spawn_palette(void) +{ + palette_task_t *t = calloc(1, sizeof(*t)); + result_t rc; + if (t == NULL) return result_OOM; + rc = palette_create(g_wuss, g_palette, g_npalette, t); + if (rc != result_OK || t->window == NULL) { free(t); return rc; } + return result_OK; +} + static result_t spawn_image(void) { - const char *leafname; - const char *filename; + image_task_t *t; + const char *leafname; + const char *filename; + result_t rc; + + t = calloc(1, sizeof(*t)); + if (t == NULL) return result_OOM; leafname = path_join_leafname("jessica", "png"); filename = path_join_filename(g_resources, 3, "resources", "images", leafname); - return image_create(g_wuss, g_palette, filename, &g_image_task); + rc = image_create(g_wuss, g_palette, filename, t); + if (rc != result_OK || t->window == NULL) { free(t); return rc; } + return result_OK; +} + +static result_t spawn_checker(void) +{ + checker_task_t *t = calloc(1, sizeof(*t)); + result_t rc; + if (t == NULL) return result_OOM; + rc = checker_create(g_wuss, g_palette, t); + if (rc != result_OK || t->window == NULL) { free(t); return rc; } + return result_OK; +} + +static result_t spawn_curve(void) +{ + curve_task_t *t = calloc(1, sizeof(*t)); + result_t rc; + if (t == NULL) return result_OOM; + rc = curve_create(g_wuss, g_palette, t); + if (rc != result_OK || t->window == NULL) { free(t); return rc; } + return result_OK; +} + +static result_t spawn_sofa(void) +{ + sofa_task_t *t = calloc(1, sizeof(*t)); + result_t rc; + if (t == NULL) return result_OOM; + rc = sofa_create(g_wuss, g_palette, t); + if (rc != result_OK || t->window == NULL) { free(t); return rc; } + return result_OK; +} + +static result_t spawn_gradient(void) +{ + gradient_task_t *t = calloc(1, sizeof(*t)); + result_t rc; + if (t == NULL) return result_OOM; + rc = gradient_create(g_wuss, t); + if (rc != result_OK || t->window == NULL) { free(t); return rc; } + return result_OK; +} + +static result_t spawn_icons(void) +{ + icons_task_t *t = calloc(1, sizeof(*t)); + result_t rc; + if (t == NULL) return result_OOM; + rc = icons_create(g_wuss, g_palette, g_daydream_font, t); + if (rc != result_OK || t->window == NULL) { free(t); return rc; } + return result_OK; +} + +static result_t spawn_porter_duff(void) +{ + porter_duff_task_t *t = calloc(1, sizeof(*t)); + result_t rc; + if (t == NULL) return result_OOM; + rc = porter_duff_create(g_wuss, g_palette, g_daydream_font, g_resources, t); + if (rc != result_OK || t->window == NULL) { free(t); return rc; } + return result_OK; } -static result_t spawn_checker(void) { return checker_create(g_wuss, g_palette, &g_checker_task); } -static result_t spawn_curve(void) { return curve_create(g_wuss, g_palette, &g_curve_task); } -static result_t spawn_sofa(void) { return sofa_create(g_wuss, g_palette, &g_sofa_task); } -static result_t spawn_gradient(void) { return gradient_create(g_wuss, &g_gradient_task); } -static result_t spawn_icons(void) { return icons_create(g_wuss, g_palette, g_daydream_font, &g_icons_task); } -static result_t spawn_porter_duff(void) { return porter_duff_create(g_wuss, g_palette, g_daydream_font, g_resources, &g_porter_duff_task); } - -static void destroy_ball(void) { ball_destroy(&g_ball_task); } -static void destroy_text(void) { text_destroy(&g_text_task); } -static void destroy_blank(void) { blank_destroy(&g_blank_task); } -static void destroy_chars(void) { chars_destroy(&g_chars_task); } -static void destroy_palette(void) { palette_destroy(&g_palette_task); } -static void destroy_image(void) { image_destroy(&g_image_task); } -static void destroy_checker(void) { checker_destroy(&g_checker_task); } -static void destroy_curve(void) { curve_destroy(&g_curve_task); } -static void destroy_sofa(void) { sofa_destroy(&g_sofa_task); } -static void destroy_gradient(void) { gradient_destroy(&g_gradient_task); } -static void destroy_icons(void) { icons_destroy(&g_icons_task); } -static void destroy_porter_duff(void) { porter_duff_destroy(&g_porter_duff_task); } static const launcher_entry_t g_launcher_entries[] = { - { "Ball", spawn_ball, destroy_ball }, - { "Text", spawn_text, destroy_text }, - { "Blank", spawn_blank, destroy_blank }, - { "Chars", spawn_chars, destroy_chars }, - { "Palette", spawn_palette, destroy_palette }, - { "Image", spawn_image, destroy_image }, - { "Checker", spawn_checker, destroy_checker }, - { "Curve", spawn_curve, destroy_curve }, - { "Sofa", spawn_sofa, destroy_sofa }, - { "Gradient", spawn_gradient, destroy_gradient }, - { "Icons", spawn_icons, destroy_icons }, - { "Porter-Duff", spawn_porter_duff, destroy_porter_duff } + { "Ball", spawn_ball }, + { "Text", spawn_text }, + { "Blank", spawn_blank }, + { "Chars", spawn_chars }, + { "Palette", spawn_palette }, + { "Image", spawn_image }, + { "Checker", spawn_checker }, + { "Curve", spawn_curve }, + { "Sofa", spawn_sofa }, + { "Gradient", spawn_gradient }, + { "Icons", spawn_icons }, + { "Porter-Duff", spawn_porter_duff } }; static wuss_button_t sdl_button_to_wuss(Uint8 button) @@ -429,11 +515,10 @@ static result_t wuss_interactive_test(const char *resources) SDL_Delay(1000 / 60); } - /* a task may have been spawned any number of times but each keeps only its - * latest window in a shared static; destroy() closes that window if still - * open and is a no-op otherwise */ - for (i = 0; i < NELEMS(g_launcher_entries); i++) - g_launcher_entries[i].destroy(); + /* ponytail: wuss_destroy() below frees every still-open window but not the + * per-instance task block hung off it, so any task window left open at quit + * leaks its block. Harmless at process exit; add a wuss close callback if a + * task ever needs deterministic teardown. */ launcher_destroy(&launcher_task); wuss_destroy(wuss); From d6a610367e1940577779b7e5f6a310f77d96477e Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 30 Aug 2026 13:06:48 +0100 Subject: [PATCH 073/287] build(tools): add wrap_protos.py and rewrap overlong prototypes New tools/wrap_protos.py rewraps C function prototypes and definitions onto one parameter per line, declarators aligned in a column (the style in screen-copy-rect.c), but only when the one-line form exceeds 80 columns. Conservative guards keep it off call statements, control-flow, macro bodies and comment text. Handles function-pointer and array parameters. Self-check in tools/test_wrap_protos.py. Applied across the tree: 35 files rewrapped. Builds clean (nosdl, sdl, asan); DPTLibTest 24/24. Co-Authored-By: Claude Sonnet 5 --- include/datastruct/bitfifo.h | 6 +- include/datastruct/cache.h | 10 +- include/datastruct/hash.h | 14 +- include/datastruct/ntree.h | 10 +- include/framebuf/screen.h | 28 +- include/text/txtfmt.h | 6 +- include/utils/array.h | 12 +- include/wuss/wuss.h | 12 +- libraries/databases/pickle/test/pickle-test.c | 20 +- libraries/datastruct/cache/cache.c | 10 +- libraries/datastruct/hash/create.c | 14 +- libraries/datastruct/ntree/test/ntree-test.c | 14 +- libraries/framebuf/bmfont/bmfont.c | 20 +- libraries/framebuf/screen/screen-copy-rect.c | 14 +- libraries/framebuf/screen/screen-draw.c | 34 +- libraries/framebuf/screen/test/screen-test.c | 6 +- libraries/io/stream/stream-packbitscomp.c | 4 +- libraries/text/txtfmt/get-line.c | 6 +- libraries/utils/array/grow.c | 12 +- libraries/wuss/create.c | 12 +- libraries/wuss/furniture.h | 3 +- libraries/wuss/impl.h | 5 +- libraries/wuss/mouse-click.c | 10 +- libraries/wuss/test/tasks/ball.c | 2 +- libraries/wuss/test/tasks/blank.c | 2 +- libraries/wuss/test/tasks/checker.c | 6 +- libraries/wuss/test/tasks/curve.c | 10 +- libraries/wuss/test/tasks/image.c | 2 +- libraries/wuss/test/tasks/palette.c | 2 +- libraries/wuss/test/tasks/porter-duff.c | 8 +- libraries/wuss/test/tasks/sofa.c | 6 +- libraries/wuss/test/tasks/text.c | 2 +- libraries/wuss/test/wuss-test.c | 2 +- libraries/wuss/window/create-placed.c | 12 +- libraries/wuss/window/move.c | 6 +- tools/test_wrap_protos.py | 191 ++++++++++ tools/wrap_protos.py | 328 ++++++++++++++++++ 37 files changed, 718 insertions(+), 143 deletions(-) create mode 100644 tools/test_wrap_protos.py create mode 100644 tools/wrap_protos.py diff --git a/include/datastruct/bitfifo.h b/include/datastruct/bitfifo.h index 509b6560..f7ec8e50 100644 --- a/include/datastruct/bitfifo.h +++ b/include/datastruct/bitfifo.h @@ -55,10 +55,10 @@ void bitfifo_destroy(T *doomed); /* writes bits to 'head' onwards, wrapping around if required */ /* fifo will reject attempts to store more bits than there is space for */ -result_t bitfifo_enqueue(T *fifo, +result_t bitfifo_enqueue(T *fifo, const unsigned int *newbits, - unsigned int newbitsoffset, - size_t nnewbits); + unsigned int newbitsoffset, + size_t nnewbits); /* reads bits from 'tail' onwards, wrapping around if required */ result_t bitfifo_dequeue(T *fifo, diff --git a/include/datastruct/cache.h b/include/datastruct/cache.h index a7983c82..164a18e6 100644 --- a/include/datastruct/cache.h +++ b/include/datastruct/cache.h @@ -109,11 +109,11 @@ void *cache_get(cache_t *cache, cachekey_t key); * * \return Error indication. */ -result_t cache_put(cache_t *cache, - cachekey_t key, - void *data, - size_t length, - void **inserted); +result_t cache_put(cache_t *cache, + cachekey_t key, + void *data, + size_t length, + void **inserted); /** * Print cache statistics to stdout. diff --git a/include/datastruct/hash.h b/include/datastruct/hash.h index 4d5b881f..75067575 100644 --- a/include/datastruct/hash.h +++ b/include/datastruct/hash.h @@ -75,13 +75,13 @@ hash_destroy_value_t hash_no_destroy_value; * * \return Error indication. */ -result_t hash_create(const void *default_value, - int nbins, - hash_fn_t *fn, - hash_compare_t *compare, - hash_destroy_key_t *destroy_key, - hash_destroy_value_t *destroy_value, - T **hash); +result_t hash_create(const void *default_value, + int nbins, + hash_fn_t *fn, + hash_compare_t *compare, + hash_destroy_key_t *destroy_key, + hash_destroy_value_t *destroy_value, + T **hash); /** * Destroy a hash. diff --git a/include/datastruct/ntree.h b/include/datastruct/ntree.h index 609833a2..9e7d54af 100644 --- a/include/datastruct/ntree.h +++ b/include/datastruct/ntree.h @@ -77,11 +77,11 @@ typedef unsigned int ntree_walk_flags_t; typedef result_t (ntree_walk_fn_t)(T *t, void *opaque); /* max_depth of 0 means 'walk all', 1..N just walk level 1..N */ -result_t ntree_walk(T *t, - ntree_walk_flags_t flags, - int max_depth, - ntree_walk_fn_t *fn, - void *opaque); +result_t ntree_walk(T *t, + ntree_walk_flags_t flags, + int max_depth, + ntree_walk_fn_t *fn, + void *opaque); /* ----------------------------------------------------------------------- */ diff --git a/include/framebuf/screen.h b/include/framebuf/screen.h index 41988ede..65af70f0 100644 --- a/include/framebuf/screen.h +++ b/include/framebuf/screen.h @@ -75,9 +75,10 @@ void screen_draw_pixel(screen_t *scr, int x, int y, colour_t colour); * \param[in] colour Colour of rectangle. */ void screen_draw_rect(screen_t *scr, - int x, int y, - size2d_t size, - colour_t colour); + int x, + int y, + size2d_t size, + colour_t colour); /** * Special case of `screen_draw_rect`. @@ -156,8 +157,11 @@ int screen_copy_rect(screen_t *scr, * \param[in] colour Colour of line. */ void screen_draw_line(screen_t *scr, - int x0, int y0, int x1, int y1, - colour_t colour); + int x0, + int y0, + int x1, + int y1, + colour_t colour); /** * Draws a line (fixed-point Wu version with anti-aliasing). @@ -173,8 +177,11 @@ void screen_draw_line(screen_t *scr, * \param[in] colour Colour of line. */ void screen_draw_line_wu_fix8(screen_t *scr, - fix8_t x0, fix8_t y0, fix8_t x1, fix8_t y1, - colour_t colour); + fix8_t x0, + fix8_t y0, + fix8_t x1, + fix8_t y1, + colour_t colour); /** * Draws a line (floating point Wu version with anti-aliasing). @@ -190,7 +197,10 @@ void screen_draw_line_wu_fix8(screen_t *scr, * \param[in] colour Colour of rectangle. */ void screen_draw_line_wu_float(screen_t *scr, - float x0, float y0, float x1, float y1, - colour_t colour); + float x0, + float y0, + float x1, + float y1, + colour_t colour); #endif /* FRAMEBUF_SCREEN_H */ diff --git a/include/text/txtfmt.h b/include/text/txtfmt.h index ab520886..dc087c06 100644 --- a/include/text/txtfmt.h +++ b/include/text/txtfmt.h @@ -104,9 +104,9 @@ int txtfmt_get_wrapped_width(const txtfmt_t *tx); * \return result_OK or result_BAD_ARG if index is out of range. */ result_t txtfmt_get_line(const txtfmt_t *tx, - int index, - const char **line, - int *length); + int index, + const char **line, + int *length); /* ----------------------------------------------------------------------- */ diff --git a/include/utils/array.h b/include/utils/array.h index eb5072b7..26ae20df 100644 --- a/include/utils/array.h +++ b/include/utils/array.h @@ -92,12 +92,12 @@ void array_squeeze2(unsigned char *base, * * \return 0 - ok, 1 - out of memory */ -int array_grow(void **block, - size_t elemsize, - int used, - int *allocated, - int need, - int minimum); +int array_grow(void **block, + size_t elemsize, + int used, + int *allocated, + int need, + int minimum); /** * Shrink a dynamically allocated array to have no free entries. diff --git a/include/wuss/wuss.h b/include/wuss/wuss.h index 9752036e..1195de5d 100644 --- a/include/wuss/wuss.h +++ b/include/wuss/wuss.h @@ -216,12 +216,12 @@ wuss_config_t; * config's palette entries are out of range for the palette, or another * appropriate result code. */ -result_t wuss_create(screen_t *scr, - bmfont_t *font, - const colour_t *palette, - int npalette, - const wuss_config_t *config, - wuss_t **wuss); +result_t wuss_create(screen_t *scr, + bmfont_t *font, + const colour_t *palette, + int npalette, + const wuss_config_t *config, + wuss_t **wuss); /** * Destroy a window manager, and any windows still open on it. diff --git a/libraries/databases/pickle/test/pickle-test.c b/libraries/databases/pickle/test/pickle-test.c index a72a268b..59ab3981 100644 --- a/libraries/databases/pickle/test/pickle-test.c +++ b/libraries/databases/pickle/test/pickle-test.c @@ -40,7 +40,10 @@ static char *my_strdup(const char *s) /* ----------------------------------------------------------------------- */ -static result_t test1_format_key(const void *key, char *buf, size_t len, void *opaque) +static result_t test1_format_key(const void *key, + char *buf, + size_t len, + void *opaque) { NOT_USED(len); NOT_USED(opaque); @@ -50,7 +53,10 @@ static result_t test1_format_key(const void *key, char *buf, size_t len, void *o return result_OK; } -static result_t test1_format_value(const void *key, char *buf, size_t len, void *opaque) +static result_t test1_format_value(const void *key, + char *buf, + size_t len, + void *opaque) { NOT_USED(len); NOT_USED(opaque); @@ -320,7 +326,10 @@ cheese_value_t; /* ----------------------------------------------------------------------- */ -static result_t cheese_format_key(const void *vkey, char *buf, size_t len, void *opaque) +static result_t cheese_format_key(const void *vkey, + char *buf, + size_t len, + void *opaque) { const cheese_key_t *key = vkey; @@ -332,7 +341,10 @@ static result_t cheese_format_key(const void *vkey, char *buf, size_t len, void return result_OK; } -static result_t cheese_format_value(const void *vvalue, char *buf, size_t len, void *opaque) +static result_t cheese_format_value(const void *vvalue, + char *buf, + size_t len, + void *opaque) { const cheese_value_t *value = vvalue; diff --git a/libraries/datastruct/cache/cache.c b/libraries/datastruct/cache/cache.c index 77a08af3..1a8c1cc1 100644 --- a/libraries/datastruct/cache/cache.c +++ b/libraries/datastruct/cache/cache.c @@ -573,11 +573,11 @@ static entry_t *evict(cache_t *c) return evictee; } -result_t cache_put(cache_t *c, - cachekey_t key, - void *data, - size_t length, - void **inserted) +result_t cache_put(cache_t *c, + cachekey_t key, + void *data, + size_t length, + void **inserted) { const size_t quantum = sizeof(free_t); diff --git a/libraries/datastruct/hash/create.c b/libraries/datastruct/hash/create.c index f5db0315..70f23800 100644 --- a/libraries/datastruct/hash/create.c +++ b/libraries/datastruct/hash/create.c @@ -74,13 +74,13 @@ void hash_no_destroy_value(void *string) /* ----------------------------------------------------------------------- */ -result_t hash_create(const void *default_value, - int nbins, - hash_fn_t *fn, - hash_compare_t *compare, - hash_destroy_key_t *destroy_key, - hash_destroy_value_t *destroy_value, - hash_t **ph) +result_t hash_create(const void *default_value, + int nbins, + hash_fn_t *fn, + hash_compare_t *compare, + hash_destroy_key_t *destroy_key, + hash_destroy_value_t *destroy_value, + hash_t **ph) { hash_t *h; hash_node_t **bins; diff --git a/libraries/datastruct/ntree/test/ntree-test.c b/libraries/datastruct/ntree/test/ntree-test.c index 63279894..8a9c8950 100644 --- a/libraries/datastruct/ntree/test/ntree-test.c +++ b/libraries/datastruct/ntree/test/ntree-test.c @@ -119,10 +119,10 @@ static result_t concat(ntree_t *t, void *opaque) return result_OK; } -static result_t tree_to_string(ntree_t *t, - ntree_walk_flags_t flags, - int depth, - char *buf) +static result_t tree_to_string(ntree_t *t, + ntree_walk_flags_t flags, + int depth, + char *buf) { result_t err; concat_data_t concat_data; @@ -137,9 +137,9 @@ static result_t tree_to_string(ntree_t *t, return err; } -static result_t walk_test(ntree_t *t, - ntree_walk_flags_t flags, - const char *expected[]) +static result_t walk_test(ntree_t *t, + ntree_walk_flags_t flags, + const char *expected[]) { result_t err = result_OK; int i; diff --git a/libraries/framebuf/bmfont/bmfont.c b/libraries/framebuf/bmfont/bmfont.c index b01d6508..00d3b87c 100644 --- a/libraries/framebuf/bmfont/bmfont.c +++ b/libraries/framebuf/bmfont/bmfont.c @@ -93,11 +93,11 @@ static int count_adw(unsigned char tab[256], pixelfmt_any_t adw_px) } /** Verify the font format and build the advance width table. */ -static result_t extract_advance_widths(bmfont_t *bmfont, - void *voidpixels, - png_uint_32 imgwidth, - png_uint_32 imgheight, - size_t rowbytes) +static result_t extract_advance_widths(bmfont_t *bmfont, + void *voidpixels, + png_uint_32 imgwidth, + png_uint_32 imgheight, + size_t rowbytes) { result_t rc = result_OK; unsigned char adwtab[256]; @@ -198,11 +198,11 @@ static void build_repack_tab(unsigned char tab[256], int idx) ((((i >> 6) & 3) == idx) << 3); } -static result_t extract_glyphs(bmfont_t *bmfont, - void *voidpixels, - png_uint_32 imgwidth, - png_uint_32 imgheight, - size_t rowbytes) +static result_t extract_glyphs(bmfont_t *bmfont, + void *voidpixels, + png_uint_32 imgwidth, + png_uint_32 imgheight, + size_t rowbytes) { result_t rc = result_OK; unsigned char repacktab[256]; diff --git a/libraries/framebuf/screen/screen-copy-rect.c b/libraries/framebuf/screen/screen-copy-rect.c index 87bd4284..5ae5231e 100644 --- a/libraries/framebuf/screen/screen-copy-rect.c +++ b/libraries/framebuf/screen/screen-copy-rect.c @@ -13,8 +13,13 @@ * columns within each row by the same rule applied to "dx" -- the standard * two-axis blit-direction trick, so every pixel is read before anything * that could overwrite it is written. */ -static int screen_copy_rect_p4(screen_t *scr, const box_t *s, const box_t *d, - int width, int height, int dx, int dy) +static int screen_copy_rect_p4(screen_t *scr, + const box_t *s, + const box_t *d, + int width, + int height, + int dx, + int dy) { unsigned char *base; int rowbytes; @@ -57,7 +62,10 @@ static int screen_copy_rect_p4(screen_t *scr, const box_t *s, const box_t *d, return 1; } -int screen_copy_rect(screen_t *scr, const box_t *src, point_t dst, box_t *copied_dst) +int screen_copy_rect(screen_t *scr, + const box_t *src, + point_t dst, + box_t *copied_dst) { box_t clip_box, s, d, d_clipped; int dx, dy, width, height, bpp; diff --git a/libraries/framebuf/screen/screen-draw.c b/libraries/framebuf/screen/screen-draw.c index 2fc46547..26523abe 100644 --- a/libraries/framebuf/screen/screen-draw.c +++ b/libraries/framebuf/screen/screen-draw.c @@ -85,8 +85,10 @@ void screen_draw_pixel(screen_t *scr, int x, int y, colour_t colour) } static void screen_blend_pixel(screen_t *scr, - int x, int y, - colour_t colour, int alpha) + int x, + int y, + colour_t colour, + int alpha) { box_t clip; @@ -137,9 +139,10 @@ static void screen_blend_pixel(screen_t *scr, /* ----------------------------------------------------------------------- */ void screen_draw_rect(screen_t *scr, - int x, int y, - size2d_t size, - colour_t colour) + int x, + int y, + size2d_t size, + colour_t colour) { box_t clip_box; box_t rect_box; @@ -364,8 +367,11 @@ static void screen_get_bounds(const screen_t *scr, box_t *bounds) } void screen_draw_line(screen_t *scr, - int x0, int y0, int x1, int y1, - colour_t colour) + int x0, + int y0, + int x1, + int y1, + colour_t colour) { box_t clip_box; box_t bounds; @@ -428,8 +434,11 @@ void screen_draw_line(screen_t *scr, } void screen_draw_line_wu_fix8(screen_t *scr, - fix8_t x0_f8, fix8_t y0_f8, fix8_t x1_f8, fix8_t y1_f8, - colour_t colour) + fix8_t x0_f8, + fix8_t y0_f8, + fix8_t x1_f8, + fix8_t y1_f8, + colour_t colour) { box_t clip_box_f8; box_t bounds_f8; @@ -561,8 +570,11 @@ static int my_lroundf(float x) } void screen_draw_line_wu_float(screen_t *scr, - float fx0, float fy0, float fx1, float fy1, - colour_t colour) + float fx0, + float fy0, + float fx1, + float fy1, + colour_t colour) { box_t clip_box; box_t bounds; diff --git a/libraries/framebuf/screen/test/screen-test.c b/libraries/framebuf/screen/test/screen-test.c index a3ce2903..e8c0b4d7 100644 --- a/libraries/framebuf/screen/test/screen-test.c +++ b/libraries/framebuf/screen/test/screen-test.c @@ -92,8 +92,10 @@ static void testscreen_init(testscreen_t *ts) ts->pixels); } -static void draw(screen_t *scr, linekind_t kind, const linetest_t *line, - colour_t colour) +static void draw(screen_t *scr, + linekind_t kind, + const linetest_t *line, + colour_t colour) { switch (kind) { diff --git a/libraries/io/stream/stream-packbitscomp.c b/libraries/io/stream/stream-packbitscomp.c index e8dab061..a9630c43 100644 --- a/libraries/io/stream/stream-packbitscomp.c +++ b/libraries/io/stream/stream-packbitscomp.c @@ -44,7 +44,9 @@ typedef struct stream_packbitscomp } stream_packbitscomp_t; -static result_t stream_packbitscomp_op(stream_t *s, stream_opcode_t op, void *arg) +static result_t stream_packbitscomp_op(stream_t *s, + stream_opcode_t op, + void *arg) { NOT_USED(s); NOT_USED(op); diff --git a/libraries/text/txtfmt/get-line.c b/libraries/text/txtfmt/get-line.c index 90023946..4be628fb 100644 --- a/libraries/text/txtfmt/get-line.c +++ b/libraries/text/txtfmt/get-line.c @@ -5,9 +5,9 @@ #include "impl.h" result_t txtfmt_get_line(const txtfmt_t *tx, - int index, - const char **line, - int *length) + int index, + const char **line, + int *length) { if (index < 0 || index >= tx->nspans) return result_BAD_ARG; diff --git a/libraries/utils/array/grow.c b/libraries/utils/array/grow.c index 267f81d3..42952902 100644 --- a/libraries/utils/array/grow.c +++ b/libraries/utils/array/grow.c @@ -11,12 +11,12 @@ #include "utils/barith.h" /* used, need, minimum - specified as number of elements (not bytes) */ -int array_grow(void **pblock, - size_t elemsize, - int used, - int *pallocated, - int need, - int minimum) +int array_grow(void **pblock, + size_t elemsize, + int used, + int *pallocated, + int need, + int minimum) { int to_allocate; void *block; diff --git a/libraries/wuss/create.c b/libraries/wuss/create.c index 6b56de61..aa5226c0 100644 --- a/libraries/wuss/create.c +++ b/libraries/wuss/create.c @@ -13,12 +13,12 @@ #include "impl.h" -result_t wuss_create(screen_t *scr, - bmfont_t *font, - const colour_t *palette, - int npalette, - const wuss_config_t *config, - wuss_t **wuss) +result_t wuss_create(screen_t *scr, + bmfont_t *font, + const colour_t *palette, + int npalette, + const wuss_config_t *config, + wuss_t **wuss) { wuss_t *w; wuss_palette_t pal; diff --git a/libraries/wuss/furniture.h b/libraries/wuss/furniture.h index 1bac5c97..a8216208 100644 --- a/libraries/wuss/furniture.h +++ b/libraries/wuss/furniture.h @@ -81,7 +81,8 @@ void wuss__furniture_draw(wuss_t *wuss, wuss_window_t *window, const box_t *full); void wuss__furniture_invalidate(wuss_window_t *window); -void wuss__furniture_invalidate_for(wuss_window_t *window, const box_t *visible); +void wuss__furniture_invalidate_for(wuss_window_t *window, + const box_t *visible); /* geometry: titlebar icons */ void wuss__back_box(const wuss_window_t *window, box_t *out); diff --git a/libraries/wuss/impl.h b/libraries/wuss/impl.h index d5e5f139..373af5e9 100644 --- a/libraries/wuss/impl.h +++ b/libraries/wuss/impl.h @@ -162,7 +162,7 @@ static inline void wuss__min_content(const wuss_window_t *window, size2d_t *min) WUSS_MIN_CONTENT)); } -static inline int wuss__titlebar_height_for(const wuss_t *wuss, +static inline int wuss__titlebar_height_for(const wuss_t *wuss, wuss_window_flags_t flags) { return (flags & wuss_WINDOW_NO_TITLEBAR) ? 0 : wuss->titlebar_height; @@ -200,7 +200,8 @@ static inline int wuss__outline_px(const wuss_window_t *window) * none, so NO_TITLEBAR windows that still opt into scrollbars/resize match * their titled siblings instead of a hardcoded size; the hardcoded default * is only a last-resort floor if even that isn't positive */ -static inline int wuss__button_size_for(const wuss_t *wuss, wuss_window_flags_t flags) +static inline int wuss__button_size_for(const wuss_t *wuss, + wuss_window_flags_t flags) { int size; diff --git a/libraries/wuss/mouse-click.c b/libraries/wuss/mouse-click.c index 634f907d..66eb1048 100644 --- a/libraries/wuss/mouse-click.c +++ b/libraries/wuss/mouse-click.c @@ -2,11 +2,11 @@ #include "impl.h" -result_t wuss_mouse_click(wuss_t *wuss, - point_t p, - wuss_button_t button, - wuss_mouse_action_t action, - wuss_window_t **hit) +result_t wuss_mouse_click(wuss_t *wuss, + point_t p, + wuss_button_t button, + wuss_mouse_action_t action, + wuss_window_t **hit) { wuss_window_t *win; wuss_furniture_region_t region; diff --git a/libraries/wuss/test/tasks/ball.c b/libraries/wuss/test/tasks/ball.c index 4e0127ba..2ee7a424 100644 --- a/libraries/wuss/test/tasks/ball.c +++ b/libraries/wuss/test/tasks/ball.c @@ -177,7 +177,7 @@ static result_t ball_idle(void *task_data) return result_OK; } -result_t ball_handle(wuss_window_t *window, +result_t ball_handle(wuss_window_t *window, const wuss_event_t *event, void *task_data) { diff --git a/libraries/wuss/test/tasks/blank.c b/libraries/wuss/test/tasks/blank.c index 86d3365a..603bec2d 100644 --- a/libraries/wuss/test/tasks/blank.c +++ b/libraries/wuss/test/tasks/blank.c @@ -58,7 +58,7 @@ static result_t blank_idle(void *task_data) return rc; } -result_t blank_handle(wuss_window_t *window, +result_t blank_handle(wuss_window_t *window, const wuss_event_t *event, void *task_data) { diff --git a/libraries/wuss/test/tasks/checker.c b/libraries/wuss/test/tasks/checker.c index 71a77c9d..cecbbc48 100644 --- a/libraries/wuss/test/tasks/checker.c +++ b/libraries/wuss/test/tasks/checker.c @@ -122,7 +122,9 @@ static result_t checker_mouse(wuss_window_t *window, void *task_data) return result_OK; } -static result_t checker_scroll(wuss_window_t *window, int delta, void *task_data) +static result_t checker_scroll(wuss_window_t *window, + int delta, + void *task_data) { checker_task_t *cc; int *band; @@ -138,7 +140,7 @@ static result_t checker_scroll(wuss_window_t *window, int delta, void *task_data return result_OK; } -result_t checker_handle(wuss_window_t *window, +result_t checker_handle(wuss_window_t *window, const wuss_event_t *event, void *task_data) { diff --git a/libraries/wuss/test/tasks/curve.c b/libraries/wuss/test/tasks/curve.c index d7ae2971..09456f0c 100644 --- a/libraries/wuss/test/tasks/curve.c +++ b/libraries/wuss/test/tasks/curve.c @@ -104,11 +104,11 @@ static result_t curve_redraw(const wuss_event_t *event, curve_task_t *task) return result_OK; } -static result_t curve_mouse(curve_task_t *task, - wuss_mouse_action_t action, - int x, - int y, - wuss_window_t *window) +static result_t curve_mouse(curve_task_t *task, + wuss_mouse_action_t action, + int x, + int y, + wuss_window_t *window) { int i; diff --git a/libraries/wuss/test/tasks/image.c b/libraries/wuss/test/tasks/image.c index 70bd56da..c5546cbc 100644 --- a/libraries/wuss/test/tasks/image.c +++ b/libraries/wuss/test/tasks/image.c @@ -65,7 +65,7 @@ static result_t image_redraw(const wuss_event_t *event, void *task_data) return result_OK; } -result_t image_handle(wuss_window_t *window, +result_t image_handle(wuss_window_t *window, const wuss_event_t *event, void *task_data) { diff --git a/libraries/wuss/test/tasks/palette.c b/libraries/wuss/test/tasks/palette.c index 6aa16e7c..dfcc2eee 100644 --- a/libraries/wuss/test/tasks/palette.c +++ b/libraries/wuss/test/tasks/palette.c @@ -79,7 +79,7 @@ static result_t palette_redraw(const wuss_event_t *event, void *task_data) return result_OK; } -result_t palette_handle(wuss_window_t *window, +result_t palette_handle(wuss_window_t *window, const wuss_event_t *event, void *task_data) { diff --git a/libraries/wuss/test/tasks/porter-duff.c b/libraries/wuss/test/tasks/porter-duff.c index 8c6d1c91..14fc1dc4 100644 --- a/libraries/wuss/test/tasks/porter-duff.c +++ b/libraries/wuss/test/tasks/porter-duff.c @@ -129,7 +129,8 @@ static const char *const rule_names[composite_RULE__LIMIT] = "XOR" }; -static result_t load_demo_png(bitmap_t *bm, const char *resources, +static result_t load_demo_png(bitmap_t *bm, + const char *resources, const char *leafname) { const char *leafname_ext; @@ -351,8 +352,9 @@ static result_t porter_duff_mouse(wuss_window_t *window, void *task_data) return result_OK; } -static result_t porter_duff_scroll(wuss_window_t *window, int delta, - void *task_data) +static result_t porter_duff_scroll(wuss_window_t *window, + int delta, + void *task_data) { porter_duff_task_t *pd; diff --git a/libraries/wuss/test/tasks/sofa.c b/libraries/wuss/test/tasks/sofa.c index e0294768..28cef250 100644 --- a/libraries/wuss/test/tasks/sofa.c +++ b/libraries/wuss/test/tasks/sofa.c @@ -494,7 +494,9 @@ static result_t sofa_redraw(const wuss_event_t *event, void *task_data) return result_OK; } -static result_t sofa_mouse(wuss_window_t *window, wuss_button_t button, void *task_data) +static result_t sofa_mouse(wuss_window_t *window, + wuss_button_t button, + void *task_data) { sofa_task_t *sc; @@ -553,7 +555,7 @@ static result_t sofa_idle(void *task_data) return result_OK; } -result_t sofa_handle(wuss_window_t *window, +result_t sofa_handle(wuss_window_t *window, const wuss_event_t *event, void *task_data) { diff --git a/libraries/wuss/test/tasks/text.c b/libraries/wuss/test/tasks/text.c index 1c5e8d0a..aa094435 100644 --- a/libraries/wuss/test/tasks/text.c +++ b/libraries/wuss/test/tasks/text.c @@ -160,7 +160,7 @@ static result_t text_idle(void *task_data) return rc; } -result_t text_handle(wuss_window_t *window, +result_t text_handle(wuss_window_t *window, const wuss_event_t *event, void *task_data) { diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index e8dcc6ec..52ea8d49 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -560,7 +560,7 @@ typedef struct test_task } test_task_t; -static result_t test_handle(wuss_window_t *window, +static result_t test_handle(wuss_window_t *window, const wuss_event_t *event, void *task_data) { diff --git a/libraries/wuss/window/create-placed.c b/libraries/wuss/window/create-placed.c index 59475c98..44f95ecb 100644 --- a/libraries/wuss/window/create-placed.c +++ b/libraries/wuss/window/create-placed.c @@ -15,12 +15,12 @@ * every edge, the titlebar on top, and the scrollbar/resize carve on the * right and bottom. Matches wuss_window_create's own visible-box maths so an * auto-placed slot ends up exactly the size the window will occupy. */ -static void footprint_pad(const wuss_t *wuss, - wuss_window_flags_t flags, - int *left, - int *top, - int *right, - int *bottom) +static void footprint_pad(const wuss_t *wuss, + wuss_window_flags_t flags, + int *left, + int *top, + int *right, + int *bottom) { int outline_px, titlebar_height; point_t carve; diff --git a/libraries/wuss/window/move.c b/libraries/wuss/window/move.c index 9acab623..ef2f8ace 100644 --- a/libraries/wuss/window/move.c +++ b/libraries/wuss/window/move.c @@ -25,8 +25,10 @@ static void translate_box(const box_t *box, int dx, int dy, box_t *out) * cycle, and rejecting those outright regressed plain corner-occlusion * drags into full fallback redraws. Only an actual cycle (i must precede j * and j must precede i) has no safe order and needs the fallback. */ -static int wuss__order_pieces(const box_t *clean, const box_t *dest, int n, - int *order) +static int wuss__order_pieces(const box_t *clean, + const box_t *dest, + int n, + int *order) { int adj[WUSS_MAX_INVALIDATE_PIECES][WUSS_MAX_INVALIDATE_PIECES]; int indeg[WUSS_MAX_INVALIDATE_PIECES]; diff --git a/tools/test_wrap_protos.py b/tools/test_wrap_protos.py new file mode 100644 index 00000000..abef8405 --- /dev/null +++ b/tools/test_wrap_protos.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python3 +"""assert-based self-check for wrap_protos.py""" +from wrap_protos import process, split_top_level + +W = 80 + + +def one(src): + out, changed = process(src, W) + return out if changed else None + + +def test_short_untouched(): + assert one("int f(int a, int b);\n") is None + + +def test_void_untouched(): + long = "static int " + "x" * 70 + "(void);\n" + assert one(long) is None + + +def test_basic_alignment(): + src = ("static int screen_copy_rect_p4(screen_t *scr, const box_t *s, " + "const box_t *d, int width, int height, int dx, int dy)\n") + assert one(src) == ( + "static int screen_copy_rect_p4(screen_t *scr,\n" + " const box_t *s,\n" + " const box_t *d,\n" + " int width,\n" + " int height,\n" + " int dx,\n" + " int dy)\n" + ) + + +def test_idempotent(): + src = ("static int screen_copy_rect_p4(screen_t *scr, const box_t *s, " + "const box_t *d, int width, int height, int dx, int dy)\n") + first = one(src) + assert process(first, W)[1] is False, "second pass should be a no-op" + + +def test_rewrap_bad_alignment(): + # joined form is >80, and it arrives already (badly) wrapped + src = ( + "static int screen_copy_rectangle_p4(screen_t *scr,\n" + " const box_t *s, const box_t *d,\n" + " int width, int height, int dx, int dy)\n" + ) + assert one(src) == ( + "static int screen_copy_rectangle_p4(screen_t *scr,\n" + " const box_t *s,\n" + " const box_t *d,\n" + " int width,\n" + " int height,\n" + " int dx,\n" + " int dy)\n" + ) + + +def test_short_multiline_untouched(): + # already multi-line but joins to <80 -- leave the hand grouping alone + src = ( + "void draw_rect(screen_t *scr,\n" + " int x, int y,\n" + " colour_t colour);\n" + ) + assert one(src) is None + + +def test_semicolon_tail_kept(): + src = ("extern void some_really_long_function_name_here(int first_argument, " + "int second_argument, int third_arg);\n") + got = one(src) + assert got.rstrip().endswith(");") + assert got.count("\n") == 3 + + +def test_brace_tail_spaced(): + src = ("static long another_long_one_that_is_over_the_limit(char *buffer, " + "unsigned long length, int flags) {\n") + assert one(src).rstrip().endswith(") {") + + +def test_func_pointer_param_aligned(): + src = ("int register_a_callback_with_a_longish_name(void *ctx, " + "int (*cb)(void *, int), unsigned long some_flags)\n") + got = one(src) + assert got == ( + "int register_a_callback_with_a_longish_name(void *ctx,\n" + " int (*cb)(void *, int),\n" + " unsigned long some_flags)\n" + ), got + + +def test_unnamed_params_aligned(): + src = ("int a_function_with_unnamed_parameters_that_is_long(const char *, " + "unsigned long, int);\n") + got = one(src) + assert got == ( + "int a_function_with_unnamed_parameters_that_is_long(const char *,\n" + " unsigned long,\n" + " int);\n" + ), got + + +def test_split_top_level_respects_nesting(): + assert split_top_level("int a, void (*f)(int, int), char *b") == \ + ["int a", "void (*f)(int, int)", "char *b"] + + +def test_variadic_bails(): + src = ("int a_printf_like_function_with_a_long_name(const char *fmt, " + "int count, ...)\n") + assert one(src) is None + + +def test_call_statement_not_touched(): + src = (" array_squeeze(v->base, v->used, v->width, width_argument_here); " + "// a call, way over 80 columns of course yes indeed\n") + assert one(src) is None + + +def test_bare_call_not_touched(): + src = (" some_function_call_that_is_quite_long(argument_one, argument_two, " + "argument_three, argument_four);\n") + assert one(src) is None + + +def test_if_statement_not_touched(): + src = (" if (some_long_condition_function(a, b) && another_condition(c, d) " + "&& yet_another_one(e))\n") + assert one(src) is None + + +def test_method_call_not_touched(): + src = (" obj->do_something_with_a_really_long_method_name(first_arg, " + "second_arg, third_arg, fourth);\n") + assert one(src) is None + + +def test_array_param_aligned(): + src = ("static result_t walk_test(ntree_t *t, ntree_walk_flags_t flags, " + "const char *expected[])\n{\n") + got = one(src) + assert got == ( + "static result_t walk_test(ntree_t *t,\n" + " ntree_walk_flags_t flags,\n" + " const char *expected[])\n" + "{\n" + ), got + # declarators all begin at the same column + lines = got.splitlines() + assert lines[0].index('*t') + 1 == lines[1].rindex('flags') \ + == lines[2].index('*expected') + 1 + + +def test_return_statement_not_touched(): + src = (" return atom_set(db->tags, db->counts[tag].index, " + "(const unsigned char *) name, strlen((char *) name) + 1);\n") + assert one(src) is None + + +def test_sizeof_call_not_touched(): + src = (" return some_allocator_function_with_a_long_name(sizeof(*p), " + "count_of_things, alignment_bytes);\n") + assert one(src) is None + + +def test_comment_line_not_touched(): + src = ( + "/** Mark it dirty. Shorthand for\n" + " * some_function_with_a_long_name(window, NULL, 0, extra_argument). */\n" + "#define M(w) some_function_with_a_long_name((w), NULL, 0, 0)\n" + ) + assert one(src) is None + + +def test_definition_still_wraps(): + src = ("result_t vector_set_width_with_a_longer_name(vector_t *v, " + "size_t width, int flags, int more)\n{\n") + got = one(src) + assert got is not None and got.splitlines()[0].endswith("(vector_t *v,") + + +if __name__ == '__main__': + for name, fn in sorted(globals().items()): + if name.startswith('test_'): + fn() + print(f'ok {name}') + print('all passed') diff --git a/tools/wrap_protos.py b/tools/wrap_protos.py new file mode 100644 index 00000000..03191596 --- /dev/null +++ b/tools/wrap_protos.py @@ -0,0 +1,328 @@ +#!/usr/bin/env python3 +"""wrap_protos.py -- rewrap C function prototypes/definitions that exceed a +column limit onto one parameter per line, with the parameter names aligned +in a column (the style already used across DPTLib, e.g. screen-copy-rect.c): + + static int screen_copy_rect_p4(screen_t *scr, + const box_t *s, + int width) + +Scope: + - Rewraps a header/definition whose parameter list ends in ")" then an + optional ";" or "{". Handles both a single overlong physical line and an + already-wrapped multi-line prototype (the list is joined, then re-split + and realigned) -- so re-running is idempotent and fixes bad alignment. + - Splits the parameter list on top-level commas only, so function-pointer + params and (T)(args) casts survive the split. + - Aligns the declarator (name, or the "(*name)" of a function pointer) + into one column; "*"s are pulled against the name. Unnamed params align + on their trailing token. void / empty / variadic-only lists are left + as-is (nothing to align). + - Only rewrites when the original spans >1 line OR exceeds the width. +""" +import argparse +import re +import sys + +# Match " (" ... ")" , across newlines. Non-greedy head stops +# at the first "(", the args group is everything up to the final ")". +# +# The head is deliberately strict -- " (" with +# nothing else -- so a bare call statement like "foo(a, b);" or "p->fn(x)" +# never matches. It still needs the is_prototype_head() checks below (>=2 +# space-separated words, a plausible return type, real param syntax). +DECL_RE = re.compile( + r'^(?P[ \t]*)' + r'(?P[A-Za-z_]\w*(?:[ \t]+(?:[A-Za-z_]\w*|\*+))*[ \t]*' + r'\*?[ \t]*[A-Za-z_]\w*\()' + r'(?P.*?)' + r'\)[ \t]*(?P[;{]?)[ \t]*$', + re.DOTALL) +# statement-start guard: the previous non-blank line must end like this for the +# candidate to be at the start of a statement (not mid-expression / mid-call). +STMT_END_RE = re.compile(r'(?:[;{}]|\*/|\)|,|^\s*#.*|^\s*/[/*].*)\s*$') +QUALIFIERS = {'static', 'extern', 'inline', '_Noreturn', 'auto', 'register', + '__inline', '__inline__', '__forceinline'} +# C keywords that can't appear in a function decl/def head -- their presence +# means the line is a statement (return/if/while/...), not a prototype. +STMT_KW = {'return', 'if', 'else', 'while', 'for', 'do', 'switch', 'case', + 'default', 'goto', 'break', 'continue', 'sizeof', 'typedef', + 'typeof', '__typeof__', 'defined'} +IDENT_TAIL_RE = re.compile(r'([A-Za-z_]\w*)\s*$') +# C type keywords -- a param made only of these (+ * and whitespace) is unnamed +TYPE_KW = {'void', 'char', 'short', 'int', 'long', 'float', 'double', + 'signed', 'unsigned', 'const', 'volatile', 'struct', 'union', + 'enum', '_Bool', 'size_t', 'ssize_t', 'ptrdiff_t', 'wchar_t'} +# function pointer / array-of-fn-pointer param: "TYPE (*name)(...)" / "(*name[])" +FNPTR_RE = re.compile(r'^(?P.*?)\(\s*\*\s*(?P\w*)\s*(?P(?:\[\s*\w*\s*\])*)\)\s*(?P\(.*\))\s*$', re.DOTALL) + + +def is_prototype_head(indent, head, args): + """Reject anything that isn't a real top-level function decl/def head. + + Guards against call statements ("foo(a,b);", "obj->method(x)"), macro + invocations, and control-flow ("if (...)"). Conservative: a false + negative just means a line is left unwrapped.""" + h = head[:-1].strip() # drop trailing "(" + if any(c in h for c in '->.=[]"\'+-/%<>&|!~?:'): + return False + words = h.split() + if len(words) < 2: # need at least " " + return False + name = words[-1].lstrip('*') + if not name.isidentifier() or name in QUALIFIERS: + return False + # every leading word is a type-ish token (keyword, identifier, or stars) + # and none is a statement keyword + for w in words: + bare = w.strip('*') + if bare in STMT_KW: + return False + for w in words[:-1]: + if w.strip('*') and not (w.strip('*').isidentifier()): + return False + # args must look like a parameter list: empty, "void", or comma-separated + # items that each contain a type token (an identifier or a "*"). + a = args.strip() + if a in ('', 'void'): + return True + for part in split_top_level(a): + p = part.strip() + if not p: + return False + if p == '...': + continue + if not re.search(r'[A-Za-z_]', p): # no type at all -> not a param + return False + if p[0] in '"\'' or '=' in p: # string arg / default -> a call + return False + return True + + +def split_top_level(args): + """Split on commas not nested in () or [].""" + parts, depth, start = [], 0, 0 + for i, c in enumerate(args): + if c in '([': + depth += 1 + elif c in ')]': + depth -= 1 + elif c == ',' and depth == 0: + parts.append(args[start:i]) + start = i + 1 + parts.append(args[start:]) + return [' '.join(p.split()) for p in parts] + + +def parse_param(param): + """Return (lhs, stars, decl) so that "lhs" + pad + stars + decl reflows the + param with "decl" (the name, or "(*name)(...)") in an aligned column. + + Returns None for void / "..." (nothing meaningful to align).""" + p = param.strip() + if p in ('', 'void', '...'): + return None + + m = FNPTR_RE.match(p) + if m: + ret = m.group('ret').rstrip() + stars = '' + while ret.endswith('*'): + stars += '*' + ret = ret[:-1].rstrip() + decl = f"(*{m.group('name')}{m.group('arr')}){m.group('rest')}" + return ret, stars, decl + + if '(' in p: # some other parenthesised form -- leave whole + return p, '', '' + + # peel a trailing array suffix ("[]", "[N]", "[N][M]") off the declarator + arr = '' + mt = re.search(r'((?:\[[^\]]*\])+)\s*$', p) + if mt: + arr = mt.group(1) + p = p[:mt.start()].rstrip() + + bare = {w for w in p.replace('*', ' ').split()} + m = IDENT_TAIL_RE.search(p) + if m and p[:m.start()].strip() and not bare <= TYPE_KW: # "TYPE name" + head = p[:m.start()].rstrip() + name = m.group(1) + else: # unnamed: "TYPE" / "TYPE *" + head, name = p, '' + stars = '' + while head.endswith('*'): + stars += '*' + head = head[:-1].rstrip() + return head, stars, name + arr + + +def reflow_parts(indent, head, args, tail): + params = split_top_level(args.strip()) + if len(params) == 1 and params[0].strip() in ('', 'void'): + return None + + parsed = [parse_param(p) for p in params] + if any(p is None for p in parsed): # void mixed in, or "..." -- bail + return None + + # DPTLib style (see screen-copy-rect.c, cache.c): the "*" sits immediately + # before the declarator, declarators line up in one column, and a "**" + # param's extra star hangs one place to the left. So the name column is + # one past the longest "type + space + stars". + name_col = max(len(l) + 1 + len(s) for l, s, _ in parsed) + cont = ' ' * len(indent + head) + close = ') ' + tail if tail == '{' else ')' + tail + + out = [] + for i, (lhs, stars, decl) in enumerate(parsed): + # right-align "stars + decl" so decl starts at name_col + tail_txt = stars + decl + piece = (lhs + ' ' * (name_col - len(stars) - len(lhs)) + tail_txt).rstrip() + prefix = (indent + head) if i == 0 else cont + sep = ',' if i < len(parsed) - 1 else close + out.append(prefix + piece + sep) + return '\n'.join(out) + '\n' + + +def _balanced(s): + """True if () and [] are balanced and never go negative in s.""" + depth = 0 + for c in s: + if c in '([': + depth += 1 + elif c in ')]': + depth -= 1 + if depth < 0: + return False + return depth == 0 + + +def _strip_comments(s): + """Blank out /* ... */ and // ... content so scanning ignores it. + Assumes s starts outside a comment.""" + out = [] + k = 0 + while k < len(s): + if s[k:k+2] == '//': + break + if s[k:k+2] == '/*': + end = s.find('*/', k + 2) + if end < 0: + return ''.join(out), True # unterminated -> rest is comment + out.append(' ') + out.append(' ' * (end + 2 - k - 2)) + k = end + 2 + continue + out.append(s[k]) + k += 1 + return ''.join(out), False + + +def process(text, width): + out = [] + changed = False + lines = text.splitlines(keepends=True) + prev_nonblank = '' # last source line already emitted + in_comment = False + i = 0 + while i < len(lines): + if in_comment: + out.append(lines[i]) + if '*/' in lines[i]: + in_comment = False + rest, cont = _strip_comments(lines[i].split('*/', 1)[1]) + in_comment = cont + i += 1 + continue + _, opened = _strip_comments(lines[i]) + if opened: # this line opens a block comment + out.append(lines[i]) + in_comment = True + i += 1 + continue + # a comment-continuation line (" * ...") never starts a prototype + if lines[i].lstrip().startswith('*'): + out.append(lines[i]) + i += 1 + continue + # Grow a candidate span up to the first line that closes the parens and + # ends in ) / ); / {. Stop early if a line ends in ; or { without + # balancing -- that means this was never a prototype. + j = i + span = '' + m = None + while j < len(lines) and j - i < 40: + span += lines[j] + body = span.rstrip() + if _balanced(body) and body.endswith((')', ');', ') {', '){')): + m = DECL_RE.match(body) + break + if body.endswith((';', '{', '}')) and _balanced(body): + break # closed, but not as a prototype + j += 1 + + ok = False + if m: + at_stmt_start = (prev_nonblank == '' + or STMT_END_RE.search(prev_nonblank)) + head_first = m.group('head').split()[0] + # a definition head may sit on its own line after "static\n" etc. + ok = (at_stmt_start + and is_prototype_head(m.group('indent'), m.group('head'), + m.group('args'))) + # reject macro-style ALL-CAPS "return type" + if head_first.isupper() and head_first not in TYPE_KW: + ok = False + + if ok: + one_line = (m.group('indent') + m.group('head') + + ', '.join(split_top_level(m.group('args').strip())) + + ')' + (' ' + m.group('tail') if m.group('tail') == '{' + else m.group('tail'))) + if len(one_line) > width: + new = reflow_parts(m.group('indent'), m.group('head'), + m.group('args'), m.group('tail')) + if new is not None and new != span: + out.append(new) + changed = True + prev_nonblank = new.rstrip('\n').rsplit('\n', 1)[-1] + i = j + 1 + continue + + out.append(lines[i]) + if lines[i].strip(): + prev_nonblank = lines[i].rstrip('\n') + i += 1 + return ''.join(out), changed + + +def main(): + ap = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument('files', nargs='+') + ap.add_argument('-w', '--width', type=int, default=80) + ap.add_argument('-n', '--dry-run', action='store_true', + help='report files that would change, write nothing') + args = ap.parse_args() + + rc = 0 + for path in args.files: + with open(path) as f: + text = f.read() + new, changed = process(text, args.width) + if not changed: + continue + if args.dry_run: + print(f'would rewrap: {path}') + rc = 1 + else: + with open(path, 'w') as f: + f.write(new) + print(f'rewrapped: {path}') + return rc + + +if __name__ == '__main__': + sys.exit(main()) From f679894c16191aed5048da80334a81c467c9122a Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 30 Aug 2026 15:34:54 +0100 Subject: [PATCH 074/287] feat(screen): add screen_draw_ninepatch for resizable 9-patch frames Draw a 3x3-grid source image into a destination box: corners at natural size, edges and centre tiled. Clipped to the destination and the screen clip, which is restored on return. Inherits pixel-format support and blending from screen_draw_bitmap. Exercised by a new screen test and by the wuss SDL image task, which now tiles resources/wuss/9tile.png behind the loaded PNG. Co-Authored-By: Claude Sonnet 5 --- CMakeLists.txt | 1 + include/framebuf/screen.h | 22 +++ .../framebuf/screen/screen-draw-ninepatch.c | 164 ++++++++++++++++++ libraries/framebuf/screen/test/screen-test.c | 139 ++++++++++++++- libraries/wuss/test/tasks/image.c | 19 ++ libraries/wuss/test/tasks/image.h | 7 +- libraries/wuss/test/wuss-test.c | 11 +- resources/wuss/9tile.png | Bin 0 -> 222 bytes 8 files changed, 357 insertions(+), 6 deletions(-) create mode 100644 libraries/framebuf/screen/screen-draw-ninepatch.c create mode 100644 resources/wuss/9tile.png diff --git a/CMakeLists.txt b/CMakeLists.txt index 84556e01..d81e15a5 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -235,6 +235,7 @@ set(FRAMEBUF_SOURCES libraries/framebuf/screen/screen.c libraries/framebuf/screen/screen-copy-rect.c libraries/framebuf/screen/screen-draw.c + libraries/framebuf/screen/screen-draw-ninepatch.c libraries/framebuf/span-registry/get.c libraries/framebuf/span-registry/regdata.h libraries/framebuf/span/all8888.c diff --git a/include/framebuf/screen.h b/include/framebuf/screen.h index 65af70f0..39a4f358 100644 --- a/include/framebuf/screen.h +++ b/include/framebuf/screen.h @@ -109,6 +109,28 @@ void screen_draw_square(screen_t *scr, */ void screen_draw_bitmap(screen_t *scr, int x, int y, const bitmap_t *src); +/** + * Draws a "9-patch": a resizable frame built from a source image that is a 3x3 + * grid of equal cells. The source width and height must each be a positive + * multiple of 3; the cell size is a third of each. Given a destination box, the + * four corner cells are drawn at their natural size in the destination corners, + * the four edge cells are tiled along the destination edges, and the centre + * cell is tiled across the interior. + * + * If the destination is narrower or shorter than two cells the opposing corners + * overlap and each is clipped to its own half; the edges and centre are then + * omitted. Drawing is clipped to both the destination box and the screen's clip + * region, which is restored on return. Cells are blended exactly as + * `screen_draw_bitmap` does. No scaling is performed. + * + * \param[in] scr Screen to draw upon. + * \param[in] dst Destination box to fill with the frame. + * \param[in] src Source image, a 3x3 grid of cells. + */ +void screen_draw_ninepatch(screen_t *scr, + const box_t *dst, + const bitmap_t *src); + /** * Copies a rectangular region of the screen to another position on the same * screen (e.g. sliding an already-rendered window's pixels to a new position diff --git a/libraries/framebuf/screen/screen-draw-ninepatch.c b/libraries/framebuf/screen/screen-draw-ninepatch.c new file mode 100644 index 00000000..fcefabfa --- /dev/null +++ b/libraries/framebuf/screen/screen-draw-ninepatch.c @@ -0,0 +1,164 @@ +/* screen-draw-ninepatch.c -- 9-patch bitmap drawing */ + +#include + +#include "geom/box.h" +#include "geom/size.h" + +#include "framebuf/bitmap.h" +#include "framebuf/pixelfmt.h" +#include "framebuf/screen.h" + +/* ----------------------------------------------------------------------- */ + +/* Build a bitmap_t view onto one cell (col,row) of a 3x3 grid within "src". + * The view shares "src"'s rowbytes, so screen_draw_bitmap walks the parent + * image correctly despite the narrower size. */ +static void ninepatch_cell(bitmap_t *cell, + const bitmap_t *src, + int col, + int row, + int pw, + int ph) +{ + int bpp; + + bpp = 1 << (pixelfmt_log2bpp(src->format) - 3); + + *cell = *src; + cell->size = SIZE2D(pw, ph); + cell->base = (unsigned char *) src->base + + row * ph * src->rowbytes + + col * pw * bpp; +} + +/* Tile "cell" across "area" (a screen-space box), with the screen clip set to + * the intersection of "area" and "saved". The draw origin starts at + * (area->x0, area->y0) and steps by (stepx, stepy); a zero step means a single + * row or column. Overhang past "area" is removed by the clip. */ +static void tile_area(screen_t *scr, + const box_t *saved, + const box_t *area, + const bitmap_t *cell, + int stepx, + int stepy) +{ + box_t clip; + int oy; + + if (box_is_empty(saved)) + clip = *area; + else if (box_intersection(saved, area, &clip)) + return; /* nothing visible */ + + if (box_is_empty(&clip)) + return; + + scr->clip = clip; + + for (oy = area->y0; oy < area->y1; oy += (stepy > 0) ? stepy : (area->y1 - oy)) + { + int ox; + + for (ox = area->x0; ox < area->x1; ox += (stepx > 0) ? stepx : (area->x1 - ox)) + screen_draw_bitmap(scr, ox, oy, cell); + } +} + +/* ----------------------------------------------------------------------- */ + +void screen_draw_ninepatch(screen_t *scr, + const box_t *dst, + const bitmap_t *src) +{ + box_t saved; + box_t orig_clip; + int pw, ph; + int lx, rx, ty, by; + bitmap_t cell; + + assert(src->size.w > 0 && src->size.w % 3 == 0); + assert(src->size.h > 0 && src->size.h % 3 == 0); + + if (box_is_empty(dst)) + return; + + pw = src->size.w / 3; + ph = src->size.h / 3; + + /* Corner column/row boundaries in the destination. When "dst" is narrower + * or shorter than two patches the near and far corners overlap; the clip in + * tile_area trims each to its own half. */ + lx = dst->x0 + pw; + rx = dst->x1 - pw; + ty = dst->y0 + ph; + by = dst->y1 - ph; + + /* Fold "dst" into the saved clip once, so every tile_area call is bounded by + * the destination rectangle as well as the caller's clip. An empty caller + * clip means "no clipping", so in that case the bound is "dst" alone. */ + orig_clip = scr->clip; + if (box_is_empty(&orig_clip)) + saved = *dst; + else if (box_intersection(&orig_clip, dst, &saved)) + return; /* dst entirely outside the clip */ + + /* Corners. */ + { + box_t b; + + ninepatch_cell(&cell, src, 0, 0, pw, ph); + b.x0 = dst->x0; b.y0 = dst->y0; b.x1 = lx; b.y1 = ty; + tile_area(scr, &saved, &b, &cell, 0, 0); + + ninepatch_cell(&cell, src, 2, 0, pw, ph); + b.x0 = rx; b.y0 = dst->y0; b.x1 = dst->x1; b.y1 = ty; + tile_area(scr, &saved, &b, &cell, 0, 0); + + ninepatch_cell(&cell, src, 0, 2, pw, ph); + b.x0 = dst->x0; b.y0 = by; b.x1 = lx; b.y1 = dst->y1; + tile_area(scr, &saved, &b, &cell, 0, 0); + + ninepatch_cell(&cell, src, 2, 2, pw, ph); + b.x0 = rx; b.y0 = by; b.x1 = dst->x1; b.y1 = dst->y1; + tile_area(scr, &saved, &b, &cell, 0, 0); + } + + /* Edges. */ + if (rx > lx) + { + box_t b; + + ninepatch_cell(&cell, src, 1, 0, pw, ph); + b.x0 = lx; b.y0 = dst->y0; b.x1 = rx; b.y1 = ty; + tile_area(scr, &saved, &b, &cell, pw, 0); + + ninepatch_cell(&cell, src, 1, 2, pw, ph); + b.x0 = lx; b.y0 = by; b.x1 = rx; b.y1 = dst->y1; + tile_area(scr, &saved, &b, &cell, pw, 0); + } + if (by > ty) + { + box_t b; + + ninepatch_cell(&cell, src, 0, 1, pw, ph); + b.x0 = dst->x0; b.y0 = ty; b.x1 = lx; b.y1 = by; + tile_area(scr, &saved, &b, &cell, 0, ph); + + ninepatch_cell(&cell, src, 2, 1, pw, ph); + b.x0 = rx; b.y0 = ty; b.x1 = dst->x1; b.y1 = by; + tile_area(scr, &saved, &b, &cell, 0, ph); + } + + /* Centre. */ + if (rx > lx && by > ty) + { + box_t b; + + ninepatch_cell(&cell, src, 1, 1, pw, ph); + b.x0 = lx; b.y0 = ty; b.x1 = rx; b.y1 = by; + tile_area(scr, &saved, &b, &cell, pw, ph); + } + + scr->clip = orig_clip; +} diff --git a/libraries/framebuf/screen/test/screen-test.c b/libraries/framebuf/screen/test/screen-test.c index e8c0b4d7..92e459e7 100644 --- a/libraries/framebuf/screen/test/screen-test.c +++ b/libraries/framebuf/screen/test/screen-test.c @@ -5,6 +5,7 @@ #include "base/result.h" #include "base/utils.h" +#include "framebuf/bitmap.h" #include "framebuf/colour.h" #include "framebuf/pixelfmt.h" #include "framebuf/screen.h" @@ -240,6 +241,141 @@ static result_t test_wu_fix8_extreme_coords(void) /* ----------------------------------------------------------------------- */ +/* 9x9 source: each 3x3 cell a distinct solid colour, indexed [row][col]. */ +#define NP_SRC 9 +#define NP_CELL 3 + +static const int np_rgb[3][3][3] = +{ + { { 255, 0, 0 }, { 255, 255, 0 }, { 0, 255, 0 } }, + { { 0, 255, 255 }, { 128, 128, 128 }, { 0, 0, 255 } }, + { { 255, 0, 255 }, { 255, 255, 255 }, { 64, 64, 64 } } +}; + +/* Encode an rgb colour to a screen pixel the same way the draw path does. */ +static pixelfmt_bgrx8888_t np_encode(testscreen_t *ts, int r, int g, int b) +{ + testscreen_init(ts); + screen_draw_pixel(&ts->scr, 0, 0, colour_rgb(r, g, b)); + return ts->pixels[0]; +} + +static void np_make_src(bitmap_t *src, pixelfmt_rgba8888_t *buf) +{ + int cx, cy, x, y; + + for (cy = 0; cy < 3; cy++) + for (cx = 0; cx < 3; cx++) + { + colour_t c; + + c = colour_rgb(np_rgb[cy][cx][0], np_rgb[cy][cx][1], np_rgb[cy][cx][2]); + + for (y = 0; y < NP_CELL; y++) + for (x = 0; x < NP_CELL; x++) + buf[(cy * NP_CELL + y) * NP_SRC + (cx * NP_CELL + x)] = c.primary; + } + + bitmap_init(src, SIZE2D(NP_SRC, NP_SRC), pixelfmt_rgba8888, + NP_SRC * (int) sizeof(buf[0]), NULL, buf); +} + +static int np_at(testscreen_t *ts, int x, int y) +{ + return (int) ts->pixels[y * WIDTH + x]; +} + +static result_t test_ninepatch(void) +{ + static testscreen_t ts; + static testscreen_t enc; + static pixelfmt_rgba8888_t srcbuf[NP_SRC * NP_SRC]; + + bitmap_t src; + box_t dst = { 5, 5, 45, 45 }; + int exp[3][3]; + int cx, cy; + + np_make_src(&src, srcbuf); + + for (cy = 0; cy < 3; cy++) + for (cx = 0; cx < 3; cx++) + exp[cy][cx] = (int) np_encode(&enc, + np_rgb[cy][cx][0], + np_rgb[cy][cx][1], + np_rgb[cy][cx][2]); + + /* Normal case. */ + testscreen_init(&ts); + screen_draw_ninepatch(&ts.scr, &dst, &src); + + /* Corners: the 3x3 block at each destination corner is that corner colour. */ + if (np_at(&ts, 5, 5) != exp[0][0] || np_at(&ts, 7, 7) != exp[0][0] || + np_at(&ts, 44, 5) != exp[0][2] || np_at(&ts, 42, 7) != exp[0][2] || + np_at(&ts, 5, 44) != exp[2][0] || np_at(&ts, 7, 42) != exp[2][0] || + np_at(&ts, 44, 44) != exp[2][2] || np_at(&ts, 42, 42) != exp[2][2]) + { + printf("screen: ninepatch corner mismatch\n"); + return result_TEST_FAILED; + } + + /* Mid-edge and interior. */ + if (np_at(&ts, 25, 6) != exp[0][1] || /* top edge */ + np_at(&ts, 25, 43) != exp[2][1] || /* bottom edge */ + np_at(&ts, 6, 25) != exp[1][0] || /* left edge */ + np_at(&ts, 43, 25) != exp[1][2] || /* right edge */ + np_at(&ts, 25, 25) != exp[1][1]) /* centre */ + { + printf("screen: ninepatch edge/centre mismatch\n"); + return result_TEST_FAILED; + } + + /* Clipping: a pixel just outside dst stays background. */ + if (np_at(&ts, 4, 4) != (int) (pixelfmt_bgrx8888_t) BACKGROUND || + np_at(&ts, 45, 45) != (int) (pixelfmt_bgrx8888_t) BACKGROUND) + { + printf("screen: ninepatch drew outside dst\n"); + return result_TEST_FAILED; + } + + /* Clip composition: restrict to the left half, the right half is untouched. */ + testscreen_init(&ts); + ts.scr.clip = (box_t) { 0, 0, 25, 64 }; + screen_draw_ninepatch(&ts.scr, &dst, &src); + if (np_at(&ts, 6, 25) != exp[1][0] || + np_at(&ts, 30, 25) != (int) (pixelfmt_bgrx8888_t) BACKGROUND) + { + printf("screen: ninepatch ignored the screen clip\n"); + return result_TEST_FAILED; + } + /* The clip is restored on return. */ + if (!box_is_empty(&ts.scr.clip) && + (ts.scr.clip.x0 != 0 || ts.scr.clip.x1 != 25)) + { + printf("screen: ninepatch did not restore the clip\n"); + return result_TEST_FAILED; + } + + /* Degenerate: dst exactly two cells each way -> only corners, no centre. */ + testscreen_init(&ts); + { + box_t small = { 10, 10, 10 + 2 * NP_CELL, 10 + 2 * NP_CELL }; + + screen_draw_ninepatch(&ts.scr, &small, &src); + if (np_at(&ts, 10, 10) != exp[0][0] || + np_at(&ts, 15, 15) != exp[2][2] || + np_at(&ts, 12, 12) == exp[1][1]) /* centre colour must NOT appear */ + { + printf("screen: ninepatch degenerate case wrong\n"); + return result_TEST_FAILED; + } + } + + return result_TEST_PASSED; +} + +/* ----------------------------------------------------------------------- */ + result_t screen_test(const char *resources) { typedef result_t (*screentestfn)(void); @@ -248,7 +384,8 @@ result_t screen_test(const char *resources) { test_clip_invariance, test_clipping_still_happens, - test_wu_fix8_extreme_coords + test_wu_fix8_extreme_coords, + test_ninepatch }; result_t rc; diff --git a/libraries/wuss/test/tasks/image.c b/libraries/wuss/test/tasks/image.c index c5546cbc..0df46c5d 100644 --- a/libraries/wuss/test/tasks/image.c +++ b/libraries/wuss/test/tasks/image.c @@ -19,6 +19,7 @@ result_t image_create(wuss_t *wuss, const colour_t *palette, const char *path, + const char *background_path, image_task_t *task) { wuss_task_t delegate; @@ -29,6 +30,13 @@ result_t image_create(wuss_t *wuss, if (rc != result_OK) return rc; + rc = bitmap_load_png(&task->ninepatch, background_path); + if (rc != result_OK) + { + free(task->bitmap.base); + return rc; + } + delegate = wuss_task_start(image_handle, task); /* shows through the image's transparent pixels */ sz.w = task->bitmap.size.w + BORDER * 2; @@ -60,6 +68,16 @@ static result_t image_redraw(const wuss_event_t *event, void *task_data) sx = event->data.redraw.scroll.x; sy = event->data.redraw.scroll.y; + { + box_t behind; + + behind.x0 = bounds->x0 - sx + BORDER - 9; + behind.y0 = bounds->y0 - sy + BORDER - 9; + behind.x1 = behind.x0 + ic->bitmap.size.w + 9 * 2; + behind.y1 = behind.y0 + ic->bitmap.size.h + 9 * 2; + screen_draw_ninepatch(scr, &behind, &ic->ninepatch); + } + screen_draw_bitmap(scr, bounds->x0 - sx + BORDER, bounds->y0 - sy + BORDER, &ic->bitmap); return result_OK; @@ -81,6 +99,7 @@ result_t image_handle(wuss_window_t *window, case wuss_EVENT_CLOSE: wuss_window_close(window); free(ic->bitmap.base); + free(ic->ninepatch.base); free(ic); /* task_data was calloc'd per instance by the spawner */ return result_OK; diff --git a/libraries/wuss/test/tasks/image.h b/libraries/wuss/test/tasks/image.h index 3f767b4c..f62eb928 100644 --- a/libraries/wuss/test/tasks/image.h +++ b/libraries/wuss/test/tasks/image.h @@ -14,16 +14,19 @@ typedef struct image_task { wuss_window_t *window; - bitmap_t bitmap; /* owned: base freed by the caller when done */ + bitmap_t bitmap; /* owned: base freed by the caller when done */ + bitmap_t ninepatch; /* owned: 9-patch tiled behind the main image */ } image_task_t; wuss_event_fn_t image_handle; -/* load the PNG at path and create its window against the given wuss instance */ +/* load the PNG at path (and the 9-patch PNG at background_path, drawn tiled + * behind it) and create its window against the given wuss instance */ result_t image_create(wuss_t *wuss, const colour_t *palette, const char *path, + const char *background_path, image_task_t *task); diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 52ea8d49..307d9929 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -118,15 +118,20 @@ static result_t spawn_image(void) image_task_t *t; const char *leafname; const char *filename; + char buf[DPTLIB_MAXPATH]; + const char *ninepatch; result_t rc; t = calloc(1, sizeof(*t)); if (t == NULL) return result_OOM; - leafname = path_join_leafname("jessica", "png"); - filename = path_join_filename(g_resources, 3, "resources", "images", leafname); + leafname = path_join_leafname("jessica", "png"); + filename = path_join_filename(g_resources, 3, "resources", "images", leafname); + strcpy(buf, filename); + ninepatch = path_join_filename(g_resources, 3, "resources", "wuss", + path_join_leafname("9tile", "png")); - rc = image_create(g_wuss, g_palette, filename, t); + rc = image_create(g_wuss, g_palette, buf, ninepatch, t); if (rc != result_OK || t->window == NULL) { free(t); return rc; } return result_OK; } diff --git a/resources/wuss/9tile.png b/resources/wuss/9tile.png new file mode 100644 index 0000000000000000000000000000000000000000..304e2af151478de591561302e36be15078355983 GIT binary patch literal 222 zcmeAS@N?(olHy`uVBq!ia0vp^(jd&i3?z4Pv7`ej#^NA%Cx&(BWL^R}1_3@Hu0UE= zJGf3Yn4vvzb&y$nxc{NU$NztP@t?u>|6+#!Z`2u{m}##4-&{85fBB04XI8yiY*ho) z6XWUP7@{%p?j&nI1_ci1$cQKZzMHyux%jZVThH9b@-u`jCBSNueyh~w*w&pBO(%OT z_!RHJsI^xoed9Ith>-QhJ3MDDJeYHu#UCI5-d*ydqpXEL)dH%WXb>X;3spSWQ R>_HA@@O1TaS?83{1OQgiQB?o{ literal 0 HcmV?d00001 From 52bc62f45adadcebdecb136f521f6c16070b58a6 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 30 Aug 2026 15:36:18 +0100 Subject: [PATCH 075/287] docs: update CLAUDE.md build/test/style guidance Co-Authored-By: Claude Sonnet 5 --- CLAUDE.md | 48 +++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 39 insertions(+), 9 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 340b704f..26c28807 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,26 +8,49 @@ DPTLib is a platform-independent C99 library (base, databases, datastruct, frame ## Build -Useful CMake options: -- `BUILD_TESTS=YES` — build the `DPTLibTest` self-test executable. -- `BUILD_SDL_TESTS=YES` — additionally build tests needing SDL2/SDL2_image. -- `USE_FORTIFY=YES` — link the bundled Fortify memory-debugging library. -- `DPTLIB_IMAGES_READ_ONLY=YES` — build libpng without write support. +Build directories are per-config and already created; there is **no** plain +`build/` — never invoke `./build/DPTLibTest` or `cmake --build build`. + +- `build-asan/` — **default**. Debug, core tests (`BUILD_TESTS=YES`). Use this + unless told otherwise. Despite the name its cache currently has + `USE_ASAN=OFF`; re-run cmake with `-DUSE_ASAN=YES` if you actually need the + sanitisers. +- `build-sdl/` — Release, SDL tests on (`BUILD_SDL_TESTS=ON`). Needed for the + interactive `wuss` driver and anything under `libraries/wuss/test/tasks/`. +- `build-nosdl/` — Release, core tests only. +- `build-riscos/`— GCCSDK cross build. Leave alone unless working on RISC OS. +- `build.xc/` — Xcode generator. + +CMake options: `BUILD_TESTS`, `BUILD_SDL_TESTS`, `USE_ASAN` (ASan + UBSan), +`USE_FORTIFY` (bundled Fortify), `DPTLIB_IMAGES_READ_ONLY` (libpng no write). + +(Re)configure a dir only if its `CMakeCache.txt` is missing or you are changing +options: +``` +cmake -B build-asan -G Ninja -DBUILD_TESTS=YES +``` + +Build: +``` +cmake --build build-asan --target DPTLibTest +``` Requires libpng (`find_package(PNG)` on non-RISC OS). On RISC OS the build fetches and patches zlib/libpng itself via `FetchContent` (see `cmake/*.patch`). ## Testing -Run all tests (needs `-resources` pointing at the repo root, for test fixture files): +Run from the **repo root** so `-resources .` resolves the fixture files: ``` -./build/DPTLibTest -resources /path/to/DPTLib +./build-asan/DPTLibTest -resources . ``` -Run a subset by naming tests (names come from the `tests[]` table in `apps/test/main.c`, e.g. `atom`, `bitvec`, `curve`, `pickle`, `stream`, `packer`): +Run a subset by naming tests (names come from the `tests[]` table in `apps/test/main.c`, e.g. `atom`, `bitvec`, `curve`, `pickle`, `stream`, `packer`, `wuss`): ``` -./build/DPTLibTest -resources /path/to/DPTLib atom bitvec +./build-asan/DPTLibTest -resources . atom bitvec ``` +SDL / interactive tests use the `build-sdl` binary instead. + Success prints `++ Tests completed in Ns: N of N tests passed.` ### Adding a new test @@ -62,7 +85,14 @@ New source/header files must be added by hand to the relevant `set(..._SOURCES . - 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. +- Edit `.c`/`.h` files with the Edit tool, never `sed -i` line-range splices — they corrupt the Allman/2-space layout and can't be verified without re-reading. To inspect exact bytes or indentation, Read the file; don't shell out to `cat -A`/`cat -v`. +- After editing any `.c`/`.h` function prototype or definition, run `python3 tools/wrap_protos.py `; after editing a header's Doxygen, run `python3 tools/wrap_doxygen.py ` (skip vendored headers). ## Commit messages Use [Conventional Commits](https://www.conventionalcommits.org/): `[optional scope]: `, e.g. `fix(pickle): handle zero-length blobs`. Common types: `feat`, `fix`, `docs`, `refactor`, `test`, `chore`, `ci`, `build`. Add a `!` before the colon (or a `BREAKING CHANGE:` footer) for breaking changes. + +When the user says "commit": stage the relevant files and `git commit` with the +message passed via repeated `-m` flags (subject, then body). Do **not** write a +`COMMIT_MSG` / `COMMIT_MSG_TMP` file. Never `git push` unless explicitly asked — +pushing prompts for an SSH key passphrase and will hang. From 7a0d2f6cf415038973d67b447694df047f0cdb24 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 30 Aug 2026 15:49:30 +0100 Subject: [PATCH 076/287] feat(screen): add screen_NINEPATCH_NO_CENTRE flag screen_draw_ninepatch gains a flags argument; passing screen_NINEPATCH_NO_CENTRE draws only the border and leaves the interior untouched. Existing callers pass 0. Co-Authored-By: Claude Sonnet 5 --- include/framebuf/screen.h | 18 +++++++++++---- .../framebuf/screen/screen-draw-ninepatch.c | 5 +++-- libraries/framebuf/screen/test/screen-test.c | 17 +++++++++++--- libraries/wuss/test/tasks/image.c | 22 ++++++++++--------- 4 files changed, 43 insertions(+), 19 deletions(-) diff --git a/include/framebuf/screen.h b/include/framebuf/screen.h index 39a4f358..da0b5325 100644 --- a/include/framebuf/screen.h +++ b/include/framebuf/screen.h @@ -109,6 +109,12 @@ void screen_draw_square(screen_t *scr, */ void screen_draw_bitmap(screen_t *scr, int x, int y, const bitmap_t *src); +/** Flags for `screen_draw_ninepatch`. */ +enum +{ + screen_NINEPATCH_NO_CENTRE = 1u << 0 /**< Leave the interior untouched. */ +}; + /** * Draws a "9-patch": a resizable frame built from a source image that is a 3x3 * grid of equal cells. The source width and height must each be a positive @@ -123,13 +129,17 @@ void screen_draw_bitmap(screen_t *scr, int x, int y, const bitmap_t *src); * region, which is restored on return. Cells are blended exactly as * `screen_draw_bitmap` does. No scaling is performed. * - * \param[in] scr Screen to draw upon. - * \param[in] dst Destination box to fill with the frame. - * \param[in] src Source image, a 3x3 grid of cells. + * \param[in] scr Screen to draw upon. + * \param[in] dst Destination box to fill with the frame. + * \param[in] src Source image, a 3x3 grid of cells. + * \param[in] flags Bitwise OR of `screen_NINEPATCH_*`, or 0. Pass + * `screen_NINEPATCH_NO_CENTRE` to draw only the border and + * leave the interior untouched. */ void screen_draw_ninepatch(screen_t *scr, const box_t *dst, - const bitmap_t *src); + const bitmap_t *src, + unsigned int flags); /** * Copies a rectangular region of the screen to another position on the same diff --git a/libraries/framebuf/screen/screen-draw-ninepatch.c b/libraries/framebuf/screen/screen-draw-ninepatch.c index fcefabfa..96ba082f 100644 --- a/libraries/framebuf/screen/screen-draw-ninepatch.c +++ b/libraries/framebuf/screen/screen-draw-ninepatch.c @@ -69,7 +69,8 @@ static void tile_area(screen_t *scr, void screen_draw_ninepatch(screen_t *scr, const box_t *dst, - const bitmap_t *src) + const bitmap_t *src, + unsigned int flags) { box_t saved; box_t orig_clip; @@ -151,7 +152,7 @@ void screen_draw_ninepatch(screen_t *scr, } /* Centre. */ - if (rx > lx && by > ty) + if (rx > lx && by > ty && !(flags & screen_NINEPATCH_NO_CENTRE)) { box_t b; diff --git a/libraries/framebuf/screen/test/screen-test.c b/libraries/framebuf/screen/test/screen-test.c index 92e459e7..dc8c3fd4 100644 --- a/libraries/framebuf/screen/test/screen-test.c +++ b/libraries/framebuf/screen/test/screen-test.c @@ -307,7 +307,7 @@ static result_t test_ninepatch(void) /* Normal case. */ testscreen_init(&ts); - screen_draw_ninepatch(&ts.scr, &dst, &src); + screen_draw_ninepatch(&ts.scr, &dst, &src, 0); /* Corners: the 3x3 block at each destination corner is that corner colour. */ if (np_at(&ts, 5, 5) != exp[0][0] || np_at(&ts, 7, 7) != exp[0][0] || @@ -341,7 +341,7 @@ static result_t test_ninepatch(void) /* Clip composition: restrict to the left half, the right half is untouched. */ testscreen_init(&ts); ts.scr.clip = (box_t) { 0, 0, 25, 64 }; - screen_draw_ninepatch(&ts.scr, &dst, &src); + screen_draw_ninepatch(&ts.scr, &dst, &src, 0); if (np_at(&ts, 6, 25) != exp[1][0] || np_at(&ts, 30, 25) != (int) (pixelfmt_bgrx8888_t) BACKGROUND) { @@ -361,7 +361,7 @@ static result_t test_ninepatch(void) { box_t small = { 10, 10, 10 + 2 * NP_CELL, 10 + 2 * NP_CELL }; - screen_draw_ninepatch(&ts.scr, &small, &src); + screen_draw_ninepatch(&ts.scr, &small, &src, 0); if (np_at(&ts, 10, 10) != exp[0][0] || np_at(&ts, 15, 15) != exp[2][2] || np_at(&ts, 12, 12) == exp[1][1]) /* centre colour must NOT appear */ @@ -371,6 +371,17 @@ static result_t test_ninepatch(void) } } + /* NO_CENTRE: border drawn, interior stays background. */ + testscreen_init(&ts); + screen_draw_ninepatch(&ts.scr, &dst, &src, screen_NINEPATCH_NO_CENTRE); + if (np_at(&ts, 5, 5) != exp[0][0] || /* corner still drawn */ + np_at(&ts, 25, 6) != exp[0][1] || /* edge still drawn */ + np_at(&ts, 25, 25) != (int) (pixelfmt_bgrx8888_t) BACKGROUND) /* centre skipped */ + { + printf("screen: ninepatch NO_CENTRE wrong\n"); + return result_TEST_FAILED; + } + return result_TEST_PASSED; } diff --git a/libraries/wuss/test/tasks/image.c b/libraries/wuss/test/tasks/image.c index 0df46c5d..943836e1 100644 --- a/libraries/wuss/test/tasks/image.c +++ b/libraries/wuss/test/tasks/image.c @@ -60,6 +60,8 @@ static result_t image_redraw(const wuss_event_t *event, void *task_data) screen_t *scr; const box_t *bounds; int sx, sy; + int bx, by; + box_t behind; ic = task_data; @@ -67,18 +69,18 @@ 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 + BORDER; + by = bounds->y0 - sy + BORDER; - { - box_t behind; - - behind.x0 = bounds->x0 - sx + BORDER - 9; - behind.y0 = bounds->y0 - sy + BORDER - 9; - behind.x1 = behind.x0 + ic->bitmap.size.w + 9 * 2; - behind.y1 = behind.y0 + ic->bitmap.size.h + 9 * 2; - screen_draw_ninepatch(scr, &behind, &ic->ninepatch); - } +#define NINEPATCHSZ 9 + + 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; + screen_draw_ninepatch(scr, &behind, &ic->ninepatch, 0); - screen_draw_bitmap(scr, bounds->x0 - sx + BORDER, bounds->y0 - sy + BORDER, &ic->bitmap); + screen_draw_bitmap(scr, bx, by, &ic->bitmap); return result_OK; } From 8a741b080bd078d344c1eec3427bc115fb1dce6c Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 30 Aug 2026 15:50:31 +0100 Subject: [PATCH 077/287] docs: start CHANGELOG.md Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..ce2e44f9 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,17 @@ +# Changelog + +All notable changes to DPTLib are recorded here. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). +This project does not yet publish versioned releases; entries are grouped under +_Unreleased_ until one is cut. + +## [Unreleased] + +### Added + +- `screen_draw_ninepatch()` — draws a resizable "9-patch" frame from a source + image that is a 3x3 grid of equal cells: corners at natural size, edges and + centre tiled, clipped to the destination box and the screen clip. +- `screen_NINEPATCH_NO_CENTRE` flag for `screen_draw_ninepatch()` to draw only + the border and leave the interior untouched. From 1d7d305dc9245e6bb6c9296b93a83e2d6a655e58 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 30 Aug 2026 17:46:29 +0100 Subject: [PATCH 078/287] fix(wuss): release held icon when a window opens over it A button icon pressed on MOUSE_DOWN stayed pressed if the client's ICON handler opened a window covering the icon's owner: the later MOUSE_UP resolved via wuss__window_at to the new window and never reached the pressed icon. Track the held icon on struct wuss as pressed_icon (mirroring furniture.dragging) and release it on any MOUSE_UP before the window hit-test. Also clear it on pointer-leave in mouse-move, and in wuss_window_close and wuss_icon_delete so it can't dangle. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/create.c | 1 + libraries/wuss/icon/delete.c | 3 +++ libraries/wuss/impl.h | 4 ++++ libraries/wuss/mouse-click.c | 21 +++++++++++++++++++-- libraries/wuss/mouse-move.c | 2 ++ libraries/wuss/window/close.c | 2 ++ 6 files changed, 31 insertions(+), 2 deletions(-) diff --git a/libraries/wuss/create.c b/libraries/wuss/create.c index aa5226c0..6eb6533f 100644 --- a/libraries/wuss/create.c +++ b/libraries/wuss/create.c @@ -140,6 +140,7 @@ result_t wuss_create(screen_t *scr, w->scr = scr; w->font = font; w->furniture.dragging = NULL; + w->pressed_icon = NULL; w->furniture.drag.x = 0; w->furniture.drag.y = 0; diff --git a/libraries/wuss/icon/delete.c b/libraries/wuss/icon/delete.c index d8cc7ee5..5f5932b7 100644 --- a/libraries/wuss/icon/delete.c +++ b/libraries/wuss/icon/delete.c @@ -18,6 +18,9 @@ void wuss_icon_delete(wuss_icon_t *icon) window = icon->window; + if (window->wuss->pressed_icon == icon) + window->wuss->pressed_icon = NULL; + wuss__icon_invalidate(icon); for (i = 0; i < window->nicons; i++) diff --git a/libraries/wuss/impl.h b/libraries/wuss/impl.h index 373af5e9..9d66b46a 100644 --- a/libraries/wuss/impl.h +++ b/libraries/wuss/impl.h @@ -65,6 +65,10 @@ struct wuss * created on first auto-placement */ point_t cascade; /* next cascade offset, used once the * layout packer has no room left */ + wuss_icon_t *pressed_icon; /* button icon held down, NULL when + * idle; released on any MOUSE_UP + * even if a new window now covers + * its owner */ }; struct wuss_window diff --git a/libraries/wuss/mouse-click.c b/libraries/wuss/mouse-click.c index 66eb1048..85e44374 100644 --- a/libraries/wuss/mouse-click.c +++ b/libraries/wuss/mouse-click.c @@ -16,6 +16,21 @@ result_t wuss_mouse_click(wuss_t *wuss, x = p.x; y = p.y; + /* Release a held button icon on any MOUSE_UP, before the hit-test picks a + * window: the up may land on a window that opened over the icon's owner on + * MOUSE_DOWN, so wuss__window_at would never reach the pressed icon. */ + if (action == wuss_MOUSE_UP && wuss->pressed_icon != NULL) + { + wuss_icon_t *pressed = wuss->pressed_icon; + + wuss->pressed_icon = NULL; + if (pressed->pressed) + { + pressed->pressed = 0; + wuss__icon_invalidate(pressed); + } + } + if (action == wuss_MOUSE_UP && wuss->furniture.dragging != NULL) { win = wuss->furniture.dragging; @@ -168,12 +183,14 @@ result_t wuss_mouse_click(wuss_t *wuss, if (action == wuss_MOUSE_DOWN && (button & (wuss_BUTTON_SELECT | wuss_BUTTON_ADJUST))) { - icon->pressed = 1; + icon->pressed = 1; + wuss->pressed_icon = icon; wuss__icon_invalidate(icon); } else if (action == wuss_MOUSE_UP && icon->pressed) { - icon->pressed = 0; + icon->pressed = 0; + wuss->pressed_icon = NULL; wuss__icon_invalidate(icon); } diff --git a/libraries/wuss/mouse-move.c b/libraries/wuss/mouse-move.c index b5e644e3..c56da51f 100644 --- a/libraries/wuss/mouse-move.c +++ b/libraries/wuss/mouse-move.c @@ -74,6 +74,8 @@ result_t wuss_mouse_move(wuss_t *wuss, point_t p, wuss_window_t **hit) if (it->pressed && it != icon) { it->pressed = 0; + if (wuss->pressed_icon == it) + wuss->pressed_icon = NULL; wuss__icon_invalidate(it); } } diff --git a/libraries/wuss/window/close.c b/libraries/wuss/window/close.c index 058743fa..8ff5fb9e 100644 --- a/libraries/wuss/window/close.c +++ b/libraries/wuss/window/close.c @@ -18,6 +18,8 @@ void wuss_window_close(wuss_window_t *doomed) wuss = doomed->wuss; if (wuss->furniture.dragging == doomed) wuss->furniture.dragging = NULL; + if (wuss->pressed_icon != NULL && wuss->pressed_icon->window == doomed) + wuss->pressed_icon = NULL; wuss__release_packed(doomed); From 74faa6572ca2526f31fdf7a4f4eac061902d55d1 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 30 Aug 2026 18:01:47 +0100 Subject: [PATCH 079/287] fix(wuss): don't repaint blitted pixels when a drag slides one clean piece onto another's vacated ground With an occluder biting a corner out of a window's pre-move footprint, the clean (non-occluded) pieces can overlap in destination: on a downward drag a full-width bottom band slides straight into the destination of the right-side band. The vacated-sliver invalidate only subtracted each piece's own destination, so that shared ground -- which the other piece's blit had already filled with valid pixels -- was invalidated and repainted across the full window width for nothing. Compute each sliver as clean[i] minus every clean piece's destination via wuss__subtract_boxes, not just full_dest[i]. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/window/move.c | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/libraries/wuss/window/move.c b/libraries/wuss/window/move.c index ef2f8ace..f9eeb611 100644 --- a/libraries/wuss/window/move.c +++ b/libraries/wuss/window/move.c @@ -181,9 +181,28 @@ void wuss_window_move(wuss_window_t *window, point_t p) * re-checking occlusion -- regardless of whether every pixel of that * new position actually got a blit above: the part that landed under an * occluder was skipped there, but the old position is vacated either - * way. */ + * way. + * + * The sliver is clean[i] minus *every* clean piece's destination, not + * just its own: with an occluder biting a corner out of "before", one + * clean piece can slide onto ground another clean piece just vacated + * (e.g. a full-width bottom band vacated straight into the destination + * of the right-side band on a downward drag). That overlap already got + * valid pixels from the other piece's blit, so invalidating it would + * just repaint good pixels. */ for (i = 0; i < nclean; i++) - wuss__invalidate_minus(window->wuss, &clean[i], &full_dest[i]); + { + box_t sliver[WUSS_MAX_INVALIDATE_PIECES]; + int nsliver, s; + + /* ponytail: nclean is a handful, so this can't approach the + * WUSS_MAX_INVALIDATE_PIECES cap; if that ever changes, a dropped + * piece here means a missed repaint (visible corruption), not just + * wasted work -- revisit then. */ + nsliver = wuss__subtract_boxes(&clean[i], full_dest, nclean, sliver); + for (s = 0; s < nsliver; s++) + wuss_invalidate(window->wuss, &sliver[s]); + } /* Whatever of "before" wasn't clean has no valid source pixels: its * translated destination needs a genuine repaint, clipped against From 277b4e878afef1e2662a708b3756c9ac63722cbb Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 30 Aug 2026 18:06:17 +0100 Subject: [PATCH 080/287] docs: fill in CHANGELOG for changes since 62a9193 Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce2e44f9..ff6845ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,8 +10,58 @@ _Unreleased_ until one is cut. ### Added +- `wuss/icon.h` — work-area icons: static labels and clickable bevelled + buttons drawn inside a window's content area. Icon boxes are in virtual + document space, so they scroll with the content. Wuss hit-tests buttons + before the content task sees a click and delivers them as `wuss_EVENT_ICON`; + labels and hidden or disabled icons fall through as `wuss_EVENT_MOUSE`. +- `wuss_icon_create_array()` — creates a batch of icons from a spec array with + all-or-nothing rollback: on the first failure any icons already created by + the call are destroyed and no handles are written. +- `wuss_window_create_placed()` — creates a window from a content size instead + of a box, letting Wuss pack it (furniture included) into the first free + screen region. Successive auto-placed windows tile; placement cascades when + no region fits. The slot is released on close and on the first + `wuss_window_move()` / `wuss_window_resize()`. An overall screen margin is + kept around all auto-placed windows. - `screen_draw_ninepatch()` — draws a resizable "9-patch" frame from a source image that is a 3x3 grid of equal cells: corners at natural size, edges and centre tiled, clipped to the destination box and the screen clip. - `screen_NINEPATCH_NO_CENTRE` flag for `screen_draw_ninepatch()` to draw only the border and leave the interior untouched. +- `packer_set_gutter()` — `packer_place_by()` now reserves a configurable + gutter strip along the box's two inner edges so located boxes are never + flush. Defaults to 0, leaving existing callers unchanged; the returned + position is still the un-inflated box. +- `packer_release()` — inverse of `packer_place_*`, returns an area to the + pool. Released areas are not coalesced. +- `POINT(x, y)` and `SIZE2D(w, h)` compound-literal macros in `geom/point.h` + and `geom/size.h`. + +### Changed + +- **Breaking:** `wuss_button_t` values are now flags (`wuss_BUTTON_SELECT` 4, + `wuss_BUTTON_MENU` 2, `wuss_BUTTON_ADJUST` 1, `wuss_BUTTON_NONE` 0) so + chords such as Select+Adjust can be reported. Client code comparing a + reported button for equality must now test with `&`. +- **Breaking:** `wuss_window_create()` takes a `min_doc` argument between + `doc` and `window`, the minimum content extent a resize-drag or toggle-size + will shrink to. It is clamped up to the built-in grab floor and down to + `doc`. Pass `(0, 0)` for the built-in floor. +- Adjust-clicking a scroll arrow now steps against the direction the arrow + points, so one arrow can be worked both ways without moving the pointer. + Toggle-size stays Select-only. + +### Fixed + +- `wuss_window_move()` no longer repaints already-blitted pixels when a drag + past an occluded corner slides one clean piece of the window onto ground + another clean piece just vacated. +- A work-area button held on mouse-down is now released if the click opens a + window that covers the button's owner, instead of staying stuck pressed. +- Resize-corner drag preserves where within the resize icon the mouse-down + landed, so the window's corner no longer jumps to the raw pointer position + on the first move. +- Removed signed-overflow and negative-shift undefined behaviour in the + anti-aliased fixed-point line rasteriser, reachable with long or off-screen + endpoints. From b860d831259411832f859159e8a5aaf73d8a1ac9 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 30 Aug 2026 18:11:39 +0100 Subject: [PATCH 081/287] fix(wuss): don't raise window when dragging a scrollbar well The resize icon and scrollbar wells shared a restack block, so grabbing a scroll well on Select popped the window to the front. Restrict the raise to FURNITURE_RESIZE. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/mouse-click.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libraries/wuss/mouse-click.c b/libraries/wuss/mouse-click.c index 85e44374..fadd05e3 100644 --- a/libraries/wuss/mouse-click.c +++ b/libraries/wuss/mouse-click.c @@ -141,7 +141,9 @@ result_t wuss_mouse_click(wuss_t *wuss, { point_t scroll; - if (button & wuss_BUTTON_SELECT) + /* Only resize raises the window; dragging a scrollbar well must not + * reorder the stack. */ + if ((button & wuss_BUTTON_SELECT) && region == wuss_FURNITURE_RESIZE) wuss_window_restack(win, wuss_ZORDER_FRONT); wuss_window_get_scroll(win, &scroll); From 834fa55ce93f06135142036d4fea99c086fe597b Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 30 Aug 2026 18:57:37 +0100 Subject: [PATCH 082/287] feat(wuss): make furniture and icon subsystems compile-time optional Two CMake options, WUSS_FURNITURE and WUSS_ICONS (both default ON, so current behaviour is unchanged), drop the respective furniture/*.c and icon/*.c files from the build and #ifdef-guard every core call site, struct field and inline helper that references them. With WUSS_FURNITURE off every window is chromeless: content box == visible box, no furniture drawn, hit-tested or dragged. wuss__scroll_clamp and wuss__scroll_step move to a new core file scroll-step.c so programmatic and wheel scrolling survive. With WUSS_ICONS off the wuss_icon_* API is not compiled and struct wuss/wuss_window lose their icon fields. The icon typedef moves to wuss.h (forward declaration) so wuss_event_t can name it regardless of the option; icon.h's body is wrapped in #ifdef WUSS_ICONS and dropped from PUBLIC_HEADERS when off. Tests stay ON-only: tasks/icons.c, tasks/launcher.c and the SDL interactive driver build only when both options are on. The core wuss_test is split - the existing body is gated on both options, with a compact core-only replacement exercising the chromeless window path (create, move, z-order, doc coordinates, scroll, invalidate, close) for the off configs. Co-Authored-By: Claude Sonnet 5 --- CMakeLists.txt | 93 ++++++--- include/wuss/icon.h | 11 +- include/wuss/wuss.h | 29 ++- libraries/wuss/create.c | 45 +++- libraries/wuss/destroy.c | 2 + libraries/wuss/furniture.h | 2 - libraries/wuss/furniture/scroll-action.c | 38 ---- libraries/wuss/impl.h | 87 ++++++++ libraries/wuss/mouse-click.c | 252 ++++++++++++----------- libraries/wuss/mouse-move.c | 55 ++--- libraries/wuss/redraw.c | 4 + libraries/wuss/scroll-step.c | 41 ++++ libraries/wuss/scroll.c | 17 +- libraries/wuss/test/wuss-test.c | 174 ++++++++++++++++ libraries/wuss/window/close.c | 6 + libraries/wuss/window/create-placed.c | 4 + libraries/wuss/window/create.c | 8 + libraries/wuss/window/resize.c | 2 + libraries/wuss/window/set-scroll.c | 2 + 19 files changed, 646 insertions(+), 226 deletions(-) create mode 100644 libraries/wuss/scroll-step.c diff --git a/CMakeLists.txt b/CMakeLists.txt index d81e15a5..0b62d49d 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -30,6 +30,8 @@ endif(CCACHE_FOUND) option(USE_FORTIFY "Use Fortify" OFF) option(USE_ASAN "Build with AddressSanitizer and UndefinedBehaviorSanitizer" OFF) option(DPTLIB_IMAGES_READ_ONLY "Remove libpng write support" OFF) +option(WUSS_FURNITURE "Build the wuss window-furniture subsystem" ON) +option(WUSS_ICONS "Build the wuss in-content icon subsystem" ON) # Referencing CMAKE_TOOLCHAIN_FILE avoids a warning on rebuilds. if(NOT ${CMAKE_TOOLCHAIN_FILE} STREQUAL "") @@ -112,11 +114,14 @@ set(PUBLIC_HEADERS include/utils/maths.h include/utils/pack.h include/utils/primes.h - include/wuss/icon.h include/wuss/task.h include/wuss/window.h include/wuss/wuss.h) +if(WUSS_ICONS) + list(APPEND PUBLIC_HEADERS include/wuss/icon.h) +endif() + # The public headers must be set as properties of the library, not as # target_sources. The quoting is essential. set_target_properties(DPTLib PROPERTIES PUBLIC_HEADER "${PUBLIC_HEADERS}") @@ -319,9 +324,35 @@ set(UTILS_SOURCES libraries/utils/pack/unpack.c libraries/utils/primes/primes.c) -set(WUSS_SOURCES +set(WUSS_CORE_SOURCES libraries/wuss/create.c libraries/wuss/destroy.c + libraries/wuss/get-font.c + libraries/wuss/idle.c + libraries/wuss/impl.h + libraries/wuss/invalidate.c + libraries/wuss/mouse-click.c + libraries/wuss/mouse-move.c + libraries/wuss/redraw.c + libraries/wuss/scroll.c + libraries/wuss/scroll-step.c + libraries/wuss/task/start.c + libraries/wuss/task/stop.c + libraries/wuss/window/at.c + libraries/wuss/window/create.c + libraries/wuss/window/create-placed.c + libraries/wuss/window/close.c + libraries/wuss/window/get-content-bounds.c + libraries/wuss/window/get-scroll.c + libraries/wuss/window/get-visible-bounds.c + libraries/wuss/window/invalidate.c + libraries/wuss/window/move.c + libraries/wuss/window/resize.c + libraries/wuss/window/restack.c + libraries/wuss/window/set-background.c + libraries/wuss/window/set-scroll.c) + +set(WUSS_FURNITURE_SOURCES libraries/wuss/furniture/back-box.c libraries/wuss/furniture/close-box.c libraries/wuss/furniture/content-box.c @@ -336,8 +367,9 @@ set(WUSS_SOURCES libraries/wuss/furniture/toggle-action.c libraries/wuss/furniture/toggle-box.c libraries/wuss/furniture/vscroll-box.c - libraries/wuss/furniture.h - libraries/wuss/get-font.c + libraries/wuss/furniture.h) + +set(WUSS_ICON_SOURCES libraries/wuss/icon/create.c libraries/wuss/icon/create-array.c libraries/wuss/icon/delete.c @@ -352,29 +384,15 @@ set(WUSS_SOURCES libraries/wuss/icon/screen-box.c libraries/wuss/icon/set-hidden.c libraries/wuss/icon/set-text.c - libraries/wuss/icon.h - libraries/wuss/idle.c - libraries/wuss/impl.h - libraries/wuss/invalidate.c - libraries/wuss/mouse-click.c - libraries/wuss/mouse-move.c - libraries/wuss/redraw.c - libraries/wuss/scroll.c - libraries/wuss/task/start.c - libraries/wuss/task/stop.c - libraries/wuss/window/at.c - libraries/wuss/window/create.c - libraries/wuss/window/create-placed.c - libraries/wuss/window/close.c - libraries/wuss/window/get-content-bounds.c - libraries/wuss/window/get-scroll.c - libraries/wuss/window/get-visible-bounds.c - libraries/wuss/window/invalidate.c - libraries/wuss/window/move.c - libraries/wuss/window/resize.c - libraries/wuss/window/restack.c - libraries/wuss/window/set-background.c - libraries/wuss/window/set-scroll.c) + libraries/wuss/icon.h) + +set(WUSS_SOURCES ${WUSS_CORE_SOURCES}) +if(WUSS_FURNITURE) + list(APPEND WUSS_SOURCES ${WUSS_FURNITURE_SOURCES}) +endif() +if(WUSS_ICONS) + list(APPEND WUSS_SOURCES ${WUSS_ICON_SOURCES}) +endif() set(ALL_SOURCES ${PUBLIC_HEADERS} @@ -426,6 +444,13 @@ if(DPTLIB_IMAGES_READ_ONLY) target_compile_definitions(DPTLib PRIVATE DPTLIB_IMAGES_READ_ONLY) endif() +if(WUSS_FURNITURE) + target_compile_definitions(DPTLib PUBLIC WUSS_FURNITURE) +endif() +if(WUSS_ICONS) + target_compile_definitions(DPTLib PUBLIC WUSS_ICONS) +endif() + if(NOT TARGET_RISCOS) set(CMAKE_FIND_FRAMEWORK NEVER) find_package(PNG REQUIRED MODULE) @@ -567,15 +592,21 @@ if(BUILD_TESTS) libraries/wuss/test/tasks/checker.c libraries/wuss/test/tasks/curve.c libraries/wuss/test/tasks/gradient.c - libraries/wuss/test/tasks/icons.c libraries/wuss/test/tasks/image.c - libraries/wuss/test/tasks/launcher.c libraries/wuss/test/tasks/palette.c libraries/wuss/test/tasks/porter-duff.c libraries/wuss/test/tasks/sofa.c libraries/wuss/test/tasks/text.c libraries/wuss/test/wuss-test.c) + # The icon-driven launcher and the icons task only build when both wuss + # subsystems are present; the SDL interactive driver needs them too. + if(WUSS_FURNITURE AND WUSS_ICONS) + list(APPEND TEST_SOURCES + libraries/wuss/test/tasks/icons.c + libraries/wuss/test/tasks/launcher.c) + endif() + source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" FILES ${TEST_SOURCES}) # Avoid a warning from CMake @@ -599,6 +630,10 @@ if(BUILD_TESTS) endif() if(BUILD_SDL_TESTS) + if(NOT (WUSS_FURNITURE AND WUSS_ICONS)) + message(FATAL_ERROR "BUILD_SDL_TESTS requires WUSS_FURNITURE and WUSS_ICONS") + endif() + find_package(SDL3 REQUIRED) find_package(SDL3_image REQUIRED) diff --git a/include/wuss/icon.h b/include/wuss/icon.h index ee931169..f09b5a0b 100644 --- a/include/wuss/icon.h +++ b/include/wuss/icon.h @@ -33,10 +33,15 @@ extern "C" #include "wuss/wuss.h" +/* The in-content icon subsystem is a compile-time option (CMake WUSS_ICONS). + * With it off the library has no wuss_icon_* symbols, so this header body is + * skipped. */ +#ifdef WUSS_ICONS + /* ----------------------------------------------------------------------- */ -/** An opaque work-area icon handle, owned by the window it is created on. */ -typedef struct wuss_icon wuss_icon_t; +/* wuss_icon_t (opaque, owned by the window it is created on) is forward-declared + * in wuss.h so wuss_event_t can name it regardless of this option. */ /** * What an icon looks like and how it behaves. The enum is left open so sprite @@ -190,6 +195,8 @@ const char *wuss_icon_get_text(const wuss_icon_t *icon); */ wuss_window_t *wuss_icon_get_window(const wuss_icon_t *icon); +#endif /* WUSS_ICONS */ + #ifdef __cplusplus } #endif diff --git a/include/wuss/wuss.h b/include/wuss/wuss.h index 1195de5d..de8d74bc 100644 --- a/include/wuss/wuss.h +++ b/include/wuss/wuss.h @@ -41,6 +41,10 @@ typedef struct wuss wuss_t; /** A window. Full API is in window.h. */ typedef struct wuss_window wuss_window_t; +/** A work-area icon. Full API is in icon.h, and is compiled only when the + * library is built with the WUSS_ICONS option on. */ +typedef struct wuss_icon wuss_icon_t; + /** * Mouse buttons, RISC OS-style: Select is the primary action, Adjust the * secondary action, Menu pops up a menu. @@ -80,7 +84,8 @@ typedef int wuss_colour_t; /** 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 - * flat colour. Each value is an index into the system palette (see + * flat colour. Ignored when the library is built with WUSS_FURNITURE off. + * Each value is an index into the system palette (see * wuss_create). */ typedef struct wuss_palette { @@ -104,7 +109,13 @@ typedef struct wuss_palette } wuss_palette_t; -/** Per-window appearance flags, combinable with bitwise OR. */ +/** + * Per-window appearance flags, combinable with bitwise OR. + * + * \note When the library is built with the WUSS_FURNITURE CMake option off, + * every window is chromeless regardless of these flags and the + * wuss_WINDOW_NO_* bits are ignored. + */ typedef enum wuss_window_flags { /** Default: every furniture region drawn. */ @@ -165,23 +176,29 @@ typedef enum wuss_zorder } wuss_zorder_t; -/** Optional creation-time configuration. */ +/** + * Optional creation-time configuration. + * + * \note titlebar_height and palette are ignored when the library is built with + * WUSS_FURNITURE off; bevel is ignored when built with both + * WUSS_FURNITURE and WUSS_ICONS off. backdrop is always honoured. + */ typedef struct wuss_config { /** * Titlebar height in pixels, or 0 to derive from font metrics (or a built-in - * fallback if no font). + * fallback if no font). Ignored when WUSS_FURNITURE is off. */ int titlebar_height; - /** Furniture chrome colours. */ + /** Furniture chrome colours. Ignored when WUSS_FURNITURE is off. */ wuss_palette_t palette; /** * 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. + * NULL. Ignored when both WUSS_FURNITURE and WUSS_ICONS are off. */ struct { diff --git a/libraries/wuss/create.c b/libraries/wuss/create.c index 6eb6533f..81a653fa 100644 --- a/libraries/wuss/create.c +++ b/libraries/wuss/create.c @@ -21,11 +21,17 @@ result_t wuss_create(screen_t *scr, wuss_t **wuss) { wuss_t *w; +#ifdef WUSS_FURNITURE wuss_palette_t pal; wuss_colour_t bg, fg; +#endif +#if defined(WUSS_FURNITURE) || defined(WUSS_ICONS) wuss_colour_t blight, bdark; +#endif +#ifdef WUSS_FURNITURE int font_height; int font_width; +#endif assert(scr != NULL); assert(wuss != NULL); @@ -63,6 +69,7 @@ result_t wuss_create(screen_t *scr, w->npalette = palette_PICO8__LENGTH; } +#ifdef WUSS_FURNITURE if (config != NULL) { pal = config->palette; @@ -136,13 +143,49 @@ result_t wuss_create(screen_t *scr, { w->titlebar_height = WUSS_DEFAULT_TITLEBAR_HEIGHT; } +#else /* !WUSS_FURNITURE */ + w->backdrop = (config != NULL) ? config->backdrop : wuss_NO_BACKGROUND; + if (w->backdrop != wuss_NO_BACKGROUND && + (w->backdrop < 0 || w->backdrop >= w->npalette)) + { + free(w->palette); + free(w); + return result_WUSS_BAD_COLOUR; + } + +#ifdef WUSS_ICONS + if (config != NULL) + { + blight = config->bevel.light; + bdark = config->bevel.dark; + } + else + { + blight = 0; + bdark = 0; + } + if (blight < 0 || blight >= w->npalette || + bdark < 0 || bdark >= w->npalette) + { + free(w->palette); + free(w); + return result_WUSS_BAD_COLOUR; + } + w->bevel_light = blight; + w->bevel_dark = bdark; +#endif +#endif /* WUSS_FURNITURE */ w->scr = scr; w->font = font; +#ifdef WUSS_FURNITURE w->furniture.dragging = NULL; - w->pressed_icon = NULL; w->furniture.drag.x = 0; w->furniture.drag.y = 0; +#endif +#ifdef WUSS_ICONS + w->pressed_icon = NULL; +#endif w->ndirty = 0; diff --git a/libraries/wuss/destroy.c b/libraries/wuss/destroy.c index ede75c1b..372fd517 100644 --- a/libraries/wuss/destroy.c +++ b/libraries/wuss/destroy.c @@ -21,7 +21,9 @@ void wuss_destroy(wuss_t *doomed) list_t *next; next = e->next; +#ifdef WUSS_ICONS wuss__icons_free((wuss_window_t *) e); +#endif free(e); e = next; } diff --git a/libraries/wuss/furniture.h b/libraries/wuss/furniture.h index a8216208..4b5b7bd8 100644 --- a/libraries/wuss/furniture.h +++ b/libraries/wuss/furniture.h @@ -105,8 +105,6 @@ int wuss__hscroll_well_px(const wuss_window_t *window); /* actions */ void wuss__furniture_toggle_size(wuss_window_t *window); -void wuss__furniture_scroll_step(wuss_window_t *window, point_t delta); -point_t wuss__scroll_clamp(const wuss_window_t *window, point_t desired); void wuss__furniture_drag_resize(wuss_window_t *window, point_t p); void wuss__furniture_drag_sausage(wuss_window_t *window, int delta_px, diff --git a/libraries/wuss/furniture/scroll-action.c b/libraries/wuss/furniture/scroll-action.c index 903b7648..6ac7a498 100644 --- a/libraries/wuss/furniture/scroll-action.c +++ b/libraries/wuss/furniture/scroll-action.c @@ -2,44 +2,6 @@ #include "../impl.h" -point_t wuss__scroll_clamp(const wuss_window_t *window, point_t desired) -{ - box_t content; - int max_x, max_y; - - wuss__content_box(window, &content); - - max_x = window->doc.w - (content.x1 - content.x0); - max_y = window->doc.h - (content.y1 - content.y0); - if (max_x < 0) - max_x = 0; - if (max_y < 0) - max_y = 0; - - if (desired.x < 0) - desired.x = 0; - else if (desired.x > max_x) - desired.x = max_x; - if (desired.y < 0) - desired.y = 0; - else if (desired.y > max_y) - desired.y = max_y; - - return desired; -} - -void wuss__furniture_scroll_step(wuss_window_t *window, point_t delta) -{ - point_t scroll; - - wuss_window_get_scroll(window, &scroll); - scroll.x += delta.x; - scroll.y += delta.y; - scroll = wuss__scroll_clamp(window, scroll); - - wuss_window_set_scroll(window, scroll); -} - void wuss__furniture_drag_sausage(wuss_window_t *window, int delta_px, int scroll_start, diff --git a/libraries/wuss/impl.h b/libraries/wuss/impl.h index 9d66b46a..fa8f52da 100644 --- a/libraries/wuss/impl.h +++ b/libraries/wuss/impl.h @@ -15,8 +15,12 @@ #include "wuss/wuss.h" #include "wuss/window.h" +#ifdef WUSS_FURNITURE #include "furniture.h" +#endif +#ifdef WUSS_ICONS #include "icon.h" +#endif #define WUSS_TITLE_MAX 63 #define WUSS_DEFAULT_TITLEBAR_HEIGHT 20 @@ -51,13 +55,21 @@ struct wuss bmfont_t *font; /* nullable, not owned */ colour_t *palette; /* owned */ int npalette; +#ifdef WUSS_FURNITURE wuss_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 */ +#endif wuss_colour_t backdrop; /* wuss_NO_BACKGROUND for none */ +#ifdef WUSS_FURNITURE int titlebar_height; +#endif list_t z_order; /* anchor; head = topmost window */ +#ifdef WUSS_FURNITURE struct wuss__furniture furniture; +#endif box_t dirty[WUSS_MAX_DIRTY]; /* accumulated by wuss_invalidate; reset by a redraw */ int ndirty; packer_t *layout; /* owned; occupied screen area for @@ -65,10 +77,12 @@ struct wuss * created on first auto-placement */ point_t cascade; /* next cascade offset, used once the * layout packer has no room left */ +#ifdef WUSS_ICONS wuss_icon_t *pressed_icon; /* button icon held down, NULL when * idle; released on any MOUSE_UP * even if a new window now covers * its owner */ +#endif }; struct wuss_window @@ -85,22 +99,43 @@ struct wuss_window size2d_t doc; /* virtual document extent, set at creation */ size2d_t min_doc; /* resize floor, set at creation; see * wuss__min_content */ +#ifdef WUSS_FURNITURE wuss_window_state_t state; /* see wuss_window_state_t */ +#endif box_t packed; /* region wuss_window_create_placed took * out of wuss->layout (footprint + gutter), * to give back on close/move; empty if not * auto-placed or already released */ +#ifdef WUSS_FURNITURE box_t pre_toggle; /* visible bounds to restore on the next toggle */ char title[WUSS_TITLE_MAX + 1]; +#endif +#ifdef WUSS_ICONS wuss_icon_t **icons; /* owned; array of owned icon pointers */ int nicons; int cap_icons; +#endif }; wuss_window_t *wuss__window_at(wuss_t *wuss, point_t p); + +/* clamp "desired" to the window's scrollable range; step the current offset + * by "delta" and apply it. Core (furniture-independent) -- used by the wheel + * and, when built, the scrollbar furniture. */ +point_t wuss__scroll_clamp(const wuss_window_t *window, point_t desired); +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__close_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. */ +static inline void wuss__content_box(const wuss_window_t *window, box_t *out) +{ + *out = window->visible; +} +#endif void wuss__invalidate_clipped(wuss_window_t *window, const box_t *box); void wuss__invalidate_minus(wuss_t *wuss, @@ -166,6 +201,7 @@ static inline void wuss__min_content(const wuss_window_t *window, size2d_t *min) WUSS_MIN_CONTENT)); } +#ifdef WUSS_FURNITURE static inline int wuss__titlebar_height_for(const wuss_t *wuss, wuss_window_flags_t flags) { @@ -249,5 +285,56 @@ static inline void wuss__furniture_carve_for(wuss_window_flags_t flags, if (carve->y > 0) carve->y += WUSS_DIVIDER_PX; } +#else /* !WUSS_FURNITURE */ +/* No furniture: every geometry helper collapses to "no chrome", so the core + * window create/move/resize maths still compiles and yields visible == + * content. */ +static inline int wuss__titlebar_height_for(const wuss_t *wuss, + wuss_window_flags_t flags) +{ + (void) wuss; (void) flags; + return 0; +} + +static inline int wuss__titlebar_height(const wuss_window_t *window) +{ + (void) window; + return 0; +} + +static inline int wuss__outline_px_for(wuss_window_flags_t flags) +{ + (void) flags; + return 0; +} + +static inline int wuss__outline_px(const wuss_window_t *window) +{ + (void) window; + return 0; +} + +static inline int wuss__button_size_for(const wuss_t *wuss, + wuss_window_flags_t flags) +{ + (void) wuss; (void) flags; + return 0; +} + +static inline int wuss__button_size(const wuss_window_t *window) +{ + (void) window; + return 0; +} + +static inline void wuss__furniture_carve_for(wuss_window_flags_t flags, + int button_size, + point_t *carve) +{ + (void) flags; (void) button_size; + carve->x = 0; + carve->y = 0; +} +#endif /* WUSS_FURNITURE */ #endif /* IMPL_H */ diff --git a/libraries/wuss/mouse-click.c b/libraries/wuss/mouse-click.c index fadd05e3..8028deb7 100644 --- a/libraries/wuss/mouse-click.c +++ b/libraries/wuss/mouse-click.c @@ -9,13 +9,13 @@ result_t wuss_mouse_click(wuss_t *wuss, wuss_window_t **hit) { wuss_window_t *win; - wuss_furniture_region_t region; wuss_event_t event; int x, y; x = p.x; y = p.y; +#ifdef WUSS_ICONS /* Release a held button icon on any MOUSE_UP, before the hit-test picks a * window: the up may land on a window that opened over the icon's owner on * MOUSE_DOWN, so wuss__window_at would never reach the pressed icon. */ @@ -30,7 +30,9 @@ result_t wuss_mouse_click(wuss_t *wuss, wuss__icon_invalidate(pressed); } } +#endif +#ifdef WUSS_FURNITURE if (action == wuss_MOUSE_UP && wuss->furniture.dragging != NULL) { win = wuss->furniture.dragging; @@ -41,6 +43,7 @@ result_t wuss_mouse_click(wuss_t *wuss, return result_OK; } +#endif win = wuss__window_at(wuss, p); if (hit != NULL) @@ -49,159 +52,170 @@ result_t wuss_mouse_click(wuss_t *wuss, if (win == NULL) return result_OK; - region = wuss__furniture_hit_test(win, POINT(x, y)); - - if (region == wuss_FURNITURE_CLOSE && - action == wuss_MOUSE_DOWN && - (button & wuss_BUTTON_SELECT)) +#ifdef WUSS_FURNITURE { - if (win->task.handle == NULL) - return result_OK; + wuss_furniture_region_t region; - event.kind = wuss_EVENT_CLOSE; - return win->task.handle(win, &event, win->task.task_data); - } + region = wuss__furniture_hit_test(win, POINT(x, y)); - if (region == wuss_FURNITURE_BACK && action == wuss_MOUSE_DOWN) - { - if (button & wuss_BUTTON_SELECT) - wuss_window_restack(win, wuss_ZORDER_BACK); - else if (button & wuss_BUTTON_ADJUST) - wuss_window_restack(win, wuss_ZORDER_FRONT); - return result_OK; - } - - if (region == wuss_FURNITURE_TOGGLE_SIZE || - region == wuss_FURNITURE_VSCROLL_UP || - region == wuss_FURNITURE_VSCROLL_DOWN || - region == wuss_FURNITURE_HSCROLL_LEFT || - region == wuss_FURNITURE_HSCROLL_RIGHT) - { - if (action == wuss_MOUSE_DOWN && - (button & (wuss_BUTTON_SELECT | wuss_BUTTON_ADJUST))) + if (region == wuss_FURNITURE_CLOSE && + action == wuss_MOUSE_DOWN && + (button & wuss_BUTTON_SELECT)) { - /* Adjust-clicking a scroll arrow steps the opposite way to the arrow it - * points, so one arrow can be worked in both directions without moving - * the pointer. Toggle-size stays Select-only. */ - int step; - - /* Select wins a Select+Adjust chord, so a chord never scrolls backwards - * unexpectedly. */ - step = (button & wuss_BUTTON_SELECT) ? WUSS_SCROLL_STEP - : -WUSS_SCROLL_STEP; + if (win->task.handle == NULL) + return result_OK; - switch (region) - { - case wuss_FURNITURE_TOGGLE_SIZE: - if (button & wuss_BUTTON_SELECT) - wuss__furniture_toggle_size(win); - break; - case wuss_FURNITURE_VSCROLL_UP: - wuss__furniture_scroll_step(win, POINT(0, -step)); - break; - case wuss_FURNITURE_VSCROLL_DOWN: - wuss__furniture_scroll_step(win, POINT(0, step)); - break; - case wuss_FURNITURE_HSCROLL_LEFT: - wuss__furniture_scroll_step(win, POINT(-step, 0)); - break; - case wuss_FURNITURE_HSCROLL_RIGHT: - wuss__furniture_scroll_step(win, POINT(step, 0)); - break; - default: - break; - } + event.kind = wuss_EVENT_CLOSE; + return win->task.handle(win, &event, win->task.task_data); } - return result_OK; - } - if (region == wuss_FURNITURE_CLOSE || region == wuss_FURNITURE_TITLE) - { - if (action == wuss_MOUSE_DOWN) + if (region == wuss_FURNITURE_BACK && action == wuss_MOUSE_DOWN) { - box_t content; - if (button & wuss_BUTTON_SELECT) + wuss_window_restack(win, wuss_ZORDER_BACK); + else if (button & wuss_BUTTON_ADJUST) wuss_window_restack(win, wuss_ZORDER_FRONT); - - wuss__content_box(win, &content); - wuss->furniture.dragging = win; - wuss->furniture.drag_kind = wuss_FURNITURE_DRAG_MOVE; - wuss->furniture.drag.x = x - content.x0; - wuss->furniture.drag.y = y - content.y0; + return result_OK; } - return result_OK; - } - if (region == wuss_FURNITURE_RESIZE || - region == wuss_FURNITURE_VSCROLL_WELL || - region == wuss_FURNITURE_HSCROLL_WELL) - { - if (action == wuss_MOUSE_DOWN) + if (region == wuss_FURNITURE_TOGGLE_SIZE || + region == wuss_FURNITURE_VSCROLL_UP || + region == wuss_FURNITURE_VSCROLL_DOWN || + region == wuss_FURNITURE_HSCROLL_LEFT || + region == wuss_FURNITURE_HSCROLL_RIGHT) { - point_t scroll; + if (action == wuss_MOUSE_DOWN && + (button & (wuss_BUTTON_SELECT | wuss_BUTTON_ADJUST))) + { + /* Adjust-clicking a scroll arrow steps the opposite way to the arrow + * it points, so one arrow can be worked in both directions without + * moving the pointer. Toggle-size stays Select-only. */ + int step; + + /* Select wins a Select+Adjust chord, so a chord never scrolls + * backwards unexpectedly. */ + step = (button & wuss_BUTTON_SELECT) ? WUSS_SCROLL_STEP + : -WUSS_SCROLL_STEP; + + switch (region) + { + case wuss_FURNITURE_TOGGLE_SIZE: + if (button & wuss_BUTTON_SELECT) + wuss__furniture_toggle_size(win); + break; + case wuss_FURNITURE_VSCROLL_UP: + wuss__scroll_step(win, POINT(0, -step)); + break; + case wuss_FURNITURE_VSCROLL_DOWN: + wuss__scroll_step(win, POINT(0, step)); + break; + case wuss_FURNITURE_HSCROLL_LEFT: + wuss__scroll_step(win, POINT(-step, 0)); + break; + case wuss_FURNITURE_HSCROLL_RIGHT: + wuss__scroll_step(win, POINT(step, 0)); + break; + default: + break; + } + } + return result_OK; + } - /* Only resize raises the window; dragging a scrollbar well must not - * reorder the stack. */ - if ((button & wuss_BUTTON_SELECT) && region == wuss_FURNITURE_RESIZE) - wuss_window_restack(win, wuss_ZORDER_FRONT); + if (region == wuss_FURNITURE_CLOSE || region == wuss_FURNITURE_TITLE) + { + if (action == wuss_MOUSE_DOWN) + { + box_t content; - wuss_window_get_scroll(win, &scroll); + if (button & wuss_BUTTON_SELECT) + wuss_window_restack(win, wuss_ZORDER_FRONT); - wuss->furniture.dragging = win; - wuss->furniture.drag_kind = wuss__furniture_drag_kind(region); - wuss->furniture.drag.x = x; - wuss->furniture.drag.y = y; - wuss->furniture.drag_scroll_start = (region == wuss_FURNITURE_VSCROLL_WELL) ? scroll.y : scroll.x; + wuss__content_box(win, &content); + wuss->furniture.dragging = win; + wuss->furniture.drag_kind = wuss_FURNITURE_DRAG_MOVE; + wuss->furniture.drag.x = x - content.x0; + wuss->furniture.drag.y = y - content.y0; + } + return result_OK; + } - /* Resize needs the pointer's offset from the content box's current - * bottom-right corner, so the point grabbed on the resize icon stays - * under the pointer as it moves, rather than that corner jumping to - * meet the pointer on the very first move. */ - if (region == wuss_FURNITURE_RESIZE) + if (region == wuss_FURNITURE_RESIZE || + region == wuss_FURNITURE_VSCROLL_WELL || + region == wuss_FURNITURE_HSCROLL_WELL) + { + if (action == wuss_MOUSE_DOWN) { - box_t content; - wuss__content_box(win, &content); - wuss->furniture.drag_offset.x = x - content.x1; - wuss->furniture.drag_offset.y = y - content.y1; + point_t scroll; + + /* Only resize raises the window; dragging a scrollbar well must not + * reorder the stack. */ + if ((button & wuss_BUTTON_SELECT) && region == wuss_FURNITURE_RESIZE) + wuss_window_restack(win, wuss_ZORDER_FRONT); + + wuss_window_get_scroll(win, &scroll); + + wuss->furniture.dragging = win; + wuss->furniture.drag_kind = wuss__furniture_drag_kind(region); + wuss->furniture.drag.x = x; + wuss->furniture.drag.y = y; + wuss->furniture.drag_scroll_start = (region == wuss_FURNITURE_VSCROLL_WELL) ? scroll.y : scroll.x; + + /* Resize needs the pointer's offset from the content box's current + * bottom-right corner, so the point grabbed on the resize icon stays + * under the pointer as it moves, rather than that corner jumping to + * meet the pointer on the very first move. */ + if (region == wuss_FURNITURE_RESIZE) + { + box_t content; + wuss__content_box(win, &content); + wuss->furniture.drag_offset.x = x - content.x1; + wuss->furniture.drag_offset.y = y - content.y1; + } } + return result_OK; } - return result_OK; } +#endif /* WUSS_FURNITURE */ if (win->task.handle != NULL) { box_t content; point_t doc_point; - wuss_icon_t *icon; wuss__content_box(win, &content); doc_point.x = x - content.x0 + win->scroll.x; doc_point.y = y - content.y0 + win->scroll.y; - icon = wuss__icon_hit_test(win, doc_point); - if (icon != NULL) +#ifdef WUSS_ICONS { - if (action == wuss_MOUSE_DOWN && - (button & (wuss_BUTTON_SELECT | wuss_BUTTON_ADJUST))) - { - icon->pressed = 1; - wuss->pressed_icon = icon; - wuss__icon_invalidate(icon); - } - else if (action == wuss_MOUSE_UP && icon->pressed) + wuss_icon_t *icon; + + icon = wuss__icon_hit_test(win, doc_point); + if (icon != NULL) { - icon->pressed = 0; - wuss->pressed_icon = NULL; - wuss__icon_invalidate(icon); + if (action == wuss_MOUSE_DOWN && + (button & (wuss_BUTTON_SELECT | wuss_BUTTON_ADJUST))) + { + icon->pressed = 1; + wuss->pressed_icon = icon; + wuss__icon_invalidate(icon); + } + else if (action == wuss_MOUSE_UP && icon->pressed) + { + icon->pressed = 0; + wuss->pressed_icon = NULL; + wuss__icon_invalidate(icon); + } + + event.kind = wuss_EVENT_ICON; + event.data.icon.icon = icon; + event.data.icon.action = action; + event.data.icon.button = button; + return win->task.handle(win, &event, win->task.task_data); } - - event.kind = wuss_EVENT_ICON; - event.data.icon.icon = icon; - event.data.icon.action = action; - event.data.icon.button = button; - return win->task.handle(win, &event, win->task.task_data); } +#endif event.kind = wuss_EVENT_MOUSE; event.data.mouse.action = action; diff --git a/libraries/wuss/mouse-move.c b/libraries/wuss/mouse-move.c index c56da51f..e07144ee 100644 --- a/libraries/wuss/mouse-move.c +++ b/libraries/wuss/mouse-move.c @@ -10,6 +10,7 @@ result_t wuss_mouse_move(wuss_t *wuss, point_t p, wuss_window_t **hit) x = p.x; y = p.y; +#ifdef WUSS_FURNITURE if (wuss->furniture.dragging != NULL) { win = wuss->furniture.dragging; @@ -38,6 +39,7 @@ result_t wuss_mouse_move(wuss_t *wuss, point_t p, wuss_window_t **hit) return result_OK; } +#endif win = wuss__window_at(wuss, p); if (hit != NULL) @@ -46,48 +48,55 @@ result_t wuss_mouse_move(wuss_t *wuss, point_t p, wuss_window_t **hit) if (win == NULL) return result_OK; +#ifdef WUSS_FURNITURE if (wuss__furniture_hit_test(win, POINT(x, y)) != wuss_FURNITURE_CONTENT) return result_OK; +#endif if (win->task.handle != NULL) { box_t content; point_t doc_point; - wuss_icon_t *icon; wuss_event_t event; - int k; wuss__content_box(win, &content); doc_point.x = x - content.x0 + win->scroll.x; doc_point.y = y - content.y0 + win->scroll.y; - icon = wuss__icon_hit_test(win, doc_point); - - /* Clear the pressed state of any button the pointer has left. This does - * not re-press a button on drag-back-in, and does not track which mouse - * button is held -- wuss keeps no persistent "button down over content" - * state. */ - for (k = 0; k < win->nicons; k++) +#ifdef WUSS_ICONS { - wuss_icon_t *it = win->icons[k]; + wuss_icon_t *icon; + int k; + + icon = wuss__icon_hit_test(win, doc_point); - if (it->pressed && it != icon) + /* Clear the pressed state of any button the pointer has left. This does + * not re-press a button on drag-back-in, and does not track which mouse + * button is held -- wuss keeps no persistent "button down over content" + * state. */ + for (k = 0; k < win->nicons; k++) { - it->pressed = 0; - if (wuss->pressed_icon == it) - wuss->pressed_icon = NULL; - wuss__icon_invalidate(it); + wuss_icon_t *it = win->icons[k]; + + if (it->pressed && it != icon) + { + it->pressed = 0; + if (wuss->pressed_icon == it) + wuss->pressed_icon = NULL; + wuss__icon_invalidate(it); + } } - } - if (icon != NULL) - { - event.kind = wuss_EVENT_ICON; - event.data.icon.icon = icon; - event.data.icon.action = wuss_MOUSE_MOVE; - event.data.icon.button = wuss_BUTTON_SELECT; - return win->task.handle(win, &event, win->task.task_data); + if (icon != NULL) + { + event.kind = wuss_EVENT_ICON; + event.data.icon.icon = icon; + event.data.icon.action = wuss_MOUSE_MOVE; + event.data.icon.button = wuss_BUTTON_SELECT; + return win->task.handle(win, &event, win->task.task_data); + } } +#endif event.kind = wuss_EVENT_MOUSE; event.data.mouse.action = wuss_MOUSE_MOVE; diff --git a/libraries/wuss/redraw.c b/libraries/wuss/redraw.c index e444b126..d86e3567 100644 --- a/libraries/wuss/redraw.c +++ b/libraries/wuss/redraw.c @@ -16,7 +16,9 @@ static void redraw_window(wuss_t *wuss, if (box_intersection(&win->visible, full, &visible_clipped)) return; /* offscreen */ +#ifdef WUSS_FURNITURE wuss__furniture_draw(wuss, win, full); +#endif wuss__content_box(win, &content); if (box_intersection(&content, full, &clipped)) @@ -52,6 +54,7 @@ static void redraw_window(wuss_t *wuss, *rc = crc; } +#ifdef WUSS_ICONS { int k; @@ -60,6 +63,7 @@ static void redraw_window(wuss_t *wuss, for (k = 0; k < win->nicons; k++) wuss__icon_draw(wuss, win->icons[k], &content, win->scroll); } +#endif } } diff --git a/libraries/wuss/scroll-step.c b/libraries/wuss/scroll-step.c new file mode 100644 index 00000000..c69766e8 --- /dev/null +++ b/libraries/wuss/scroll-step.c @@ -0,0 +1,41 @@ +/* scroll-step.c -- wuss - clamp and apply a scroll offset */ + +#include "impl.h" + +point_t wuss__scroll_clamp(const wuss_window_t *window, point_t desired) +{ + box_t content; + int max_x, max_y; + + wuss__content_box(window, &content); + + max_x = window->doc.w - (content.x1 - content.x0); + max_y = window->doc.h - (content.y1 - content.y0); + if (max_x < 0) + max_x = 0; + if (max_y < 0) + max_y = 0; + + if (desired.x < 0) + desired.x = 0; + else if (desired.x > max_x) + desired.x = max_x; + if (desired.y < 0) + desired.y = 0; + else if (desired.y > max_y) + desired.y = max_y; + + return desired; +} + +void wuss__scroll_step(wuss_window_t *window, point_t delta) +{ + point_t scroll; + + wuss_window_get_scroll(window, &scroll); + scroll.x += delta.x; + scroll.y += delta.y; + scroll = wuss__scroll_clamp(window, scroll); + + wuss_window_set_scroll(window, scroll); +} diff --git a/libraries/wuss/scroll.c b/libraries/wuss/scroll.c index 7be356e7..bcabbb43 100644 --- a/libraries/wuss/scroll.c +++ b/libraries/wuss/scroll.c @@ -5,7 +5,6 @@ result_t wuss_scroll(wuss_t *wuss, point_t p, int delta, wuss_window_t **hit) { wuss_window_t *win; - box_t titlebar; int x, y; x = p.x; @@ -18,9 +17,15 @@ result_t wuss_scroll(wuss_t *wuss, point_t p, int delta, wuss_window_t **hit) if (win == NULL) return result_OK; - wuss__titlebar_box(win, &titlebar); - if (box_contains_point(&titlebar, x, y)) - return result_OK; +#ifdef WUSS_FURNITURE + { + box_t titlebar; + + wuss__titlebar_box(win, &titlebar); + if (box_contains_point(&titlebar, x, y)) + return result_OK; + } +#endif if (win->task.handle != NULL) { @@ -36,12 +41,12 @@ result_t wuss_scroll(wuss_t *wuss, point_t p, int delta, wuss_window_t **hit) event.data.scroll.point.y = y - content.y0 + win->scroll.y; event.data.scroll.delta = delta; - wuss__furniture_scroll_step(win, POINT(0, delta)); + wuss__scroll_step(win, POINT(0, delta)); return win->task.handle(win, &event, win->task.task_data); } - wuss__furniture_scroll_step(win, POINT(0, delta)); + wuss__scroll_step(win, POINT(0, delta)); return result_OK; } diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 307d9929..6017f6f4 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -647,6 +647,8 @@ static int dirty_union_area(wuss_t *wuss, const box_t *bounds) return area; } +#if defined(WUSS_FURNITURE) && defined(WUSS_ICONS) + result_t wuss_test(const char *resources) { result_t rc; @@ -3114,3 +3116,175 @@ result_t wuss_test(const char *resources) return result_TEST_FAILED; } + +#else /* !(WUSS_FURNITURE && WUSS_ICONS) */ + +/* Compact core test for builds with furniture and/or icons compiled out. + * Exercises the chromeless window path: create, move, z-order, invalidate, + * redraw and programmatic scroll. No titlebar, no scrollbars, no icons. */ +result_t wuss_test(const char *resources) +{ + result_t rc; + int rowbytes; + void *pixels; + bitmap_t bm; + screen_t scr; + wuss_t *wuss; + test_task_t tc_a, tc_b; + wuss_task_t delegate_a, delegate_b; + box_t box_a, box_b, content; + wuss_window_t *win_a, *win_b, *hit; + point_t scroll; + + /* Force a chromeless window even in a build that still has furniture, so the + * assertions below (content box == visible box) hold in every config. */ + const wuss_window_flags_t chromeless = + 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; + + NOT_USED(resources); + + rowbytes = 200 * 4; + pixels = malloc((size_t) rowbytes * 200); + if (pixels == NULL) + goto Failure; + + rc = bitmap_init(&bm, SIZE2D(200, 200), pixelfmt_bgrx8888, rowbytes, NULL, + pixels); + if (rc != result_OK) + goto Failure; + + screen_for_bitmap(&scr, &bm); + + printf("test: wuss_create (core)\n"); + + rc = wuss_create(&scr, NULL, NULL, 0, NULL, &wuss); + if (rc != result_OK) + goto Failure; + + 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(wuss, &box_a, "toosmall", wuss_WINDOW_NONE, + wuss_NO_BACKGROUND, NULL, box_size(&box_a), + SIZE2D(0, 0), &win_a); + if (rc != result_WUSS_TOO_SMALL) + goto Failure; + + printf("test: create overlapping windows A and B\n"); + + memset(&tc_a, 0, sizeof(tc_a)); + memset(&tc_b, 0, sizeof(tc_b)); + delegate_a.handle = test_handle; delegate_a.task_data = &tc_a; + delegate_b.handle = test_handle; delegate_b.task_data = &tc_b; + + box_a.x0 = 0; box_a.y0 = 0; box_a.x1 = 100; box_a.y1 = 100; + rc = wuss_window_create(wuss, &box_a, "A", chromeless, + wuss_NO_BACKGROUND, &delegate_a, SIZE2D(400, 400), + SIZE2D(0, 0), &win_a); + if (rc != result_OK) + goto Failure; + + box_b.x0 = 50; box_b.y0 = 50; box_b.x1 = 150; box_b.y1 = 150; + rc = wuss_window_create(wuss, &box_b, "B", chromeless, + wuss_NO_BACKGROUND, &delegate_b, SIZE2D(400, 400), + SIZE2D(0, 0), &win_b); + if (rc != result_OK) + goto Failure; + + /* With furniture off the content box is the visible box verbatim. */ + wuss_window_get_content_bounds(win_a, &content); + if (content.x0 != 0 || content.y0 != 0 || + content.x1 != 100 || content.y1 != 100) + goto Failure; + + printf("test: redraw delivers REDRAW events\n"); + + wuss_redraw(wuss); + if (tc_a.redraw_count == 0 || tc_b.redraw_count == 0) + goto Failure; + + printf("test: z-order - click routes to topmost window\n"); + + /* B was created last so it is on top over the overlap at (75,75). */ + rc = wuss_mouse_click(wuss, POINT(75, 75), wuss_BUTTON_SELECT, + wuss_MOUSE_DOWN, &hit); + if (rc != result_OK || hit != win_b) + goto Failure; + if (tc_b.mouse_count != 1 || tc_a.mouse_count != 0) + goto Failure; + + wuss_window_restack(win_a, wuss_ZORDER_FRONT); + rc = wuss_mouse_click(wuss, POINT(75, 75), wuss_BUTTON_SELECT, + wuss_MOUSE_DOWN, &hit); + if (rc != result_OK || hit != win_a) + goto Failure; + if (tc_a.mouse_count != 1) + goto Failure; + + printf("test: mouse point is in document coordinates\n"); + + if (tc_a.last_x != 75 || tc_a.last_y != 75) + goto Failure; + + printf("test: window_move\n"); + + wuss_window_move(win_a, POINT(20, 20)); + wuss_window_get_content_bounds(win_a, &content); + if (content.x0 != 20 || content.y0 != 20) + goto Failure; + + printf("test: programmatic scroll offsets document coordinates\n"); + + wuss_window_set_scroll(win_a, POINT(10, 5)); + wuss_window_get_scroll(win_a, &scroll); + if (scroll.x != 10 || scroll.y != 5) + goto Failure; + + rc = wuss_mouse_click(wuss, POINT(20, 20), wuss_BUTTON_SELECT, + wuss_MOUSE_DOWN, &hit); + if (rc != result_OK || hit != win_a) + goto Failure; + if (tc_a.last_x != 10 || tc_a.last_y != 5) + goto Failure; + + printf("test: wheel scroll delivers SCROLL event and moves offset\n"); + + wuss_scroll(wuss, POINT(20, 20), 8, &hit); + if (hit != win_a) + goto Failure; + wuss_window_get_scroll(win_a, &scroll); + if (scroll.y != 13) + goto Failure; + + printf("test: invalidate marks dirty region\n"); + + wuss_window_invalidate(win_a, NULL); + if (wuss_get_dirty_count(wuss) == 0) + goto Failure; + wuss_redraw(wuss); + if (wuss_get_dirty_count(wuss) != 0) + goto Failure; + + printf("test: window_close delivers CLOSE and drops the window\n"); + + wuss_window_close(win_b); + rc = wuss_mouse_click(wuss, POINT(140, 140), wuss_BUTTON_SELECT, + wuss_MOUSE_DOWN, &hit); + if (rc != result_OK || hit != NULL) + goto Failure; + + wuss_destroy(wuss); + free(pixels); + + return result_TEST_PASSED; + +Failure: + + printf("wuss_test: failed\n"); + + return result_TEST_FAILED; +} + +#endif /* WUSS_FURNITURE && WUSS_ICONS */ diff --git a/libraries/wuss/window/close.c b/libraries/wuss/window/close.c index 8ff5fb9e..570425d0 100644 --- a/libraries/wuss/window/close.c +++ b/libraries/wuss/window/close.c @@ -16,10 +16,14 @@ void wuss_window_close(wuss_window_t *doomed) return; wuss = doomed->wuss; +#ifdef WUSS_FURNITURE if (wuss->furniture.dragging == doomed) wuss->furniture.dragging = NULL; +#endif +#ifdef WUSS_ICONS if (wuss->pressed_icon != NULL && wuss->pressed_icon->window == doomed) wuss->pressed_icon = NULL; +#endif wuss__release_packed(doomed); @@ -27,7 +31,9 @@ void wuss_window_close(wuss_window_t *doomed) list_remove(&wuss->z_order, &doomed->link); +#ifdef WUSS_ICONS wuss__icons_free(doomed); +#endif free(doomed); } diff --git a/libraries/wuss/window/create-placed.c b/libraries/wuss/window/create-placed.c index 44f95ecb..d3314f79 100644 --- a/libraries/wuss/window/create-placed.c +++ b/libraries/wuss/window/create-placed.c @@ -45,7 +45,11 @@ static void next_cascade(wuss_t *wuss, int fw, int fh, point_t *pos) scr_w = wuss->scr->size.w; scr_h = wuss->scr->size.h; +#ifdef WUSS_FURNITURE step = wuss->titlebar_height; +#else + step = 0; +#endif if (step <= 0) step = WUSS_DEFAULT_TITLEBAR_HEIGHT; diff --git a/libraries/wuss/window/create.c b/libraries/wuss/window/create.c index 7beb1ed5..11bb445b 100644 --- a/libraries/wuss/window/create.c +++ b/libraries/wuss/window/create.c @@ -78,10 +78,14 @@ result_t wuss_window_create(wuss_t *wuss, win->scroll.y = 0; win->doc = doc; win->min_doc = min_doc; +#ifdef WUSS_FURNITURE win->state = wuss_WINDOW_STATE_NONE; +#endif +#ifdef WUSS_ICONS win->icons = NULL; win->nicons = 0; win->cap_icons = 0; +#endif box_reset(&win->packed); /* wuss_window_create_placed fills this in after */ @@ -97,6 +101,7 @@ result_t wuss_window_create(wuss_t *wuss, } win->bg = bg; +#ifdef WUSS_FURNITURE if (title != NULL) { strncpy(win->title, title, WUSS_TITLE_MAX); @@ -106,6 +111,9 @@ result_t wuss_window_create(wuss_t *wuss, { win->title[0] = '\0'; } +#else + (void) title; +#endif list_add_to_head(&wuss->z_order, &win->link); diff --git a/libraries/wuss/window/resize.c b/libraries/wuss/window/resize.c index c43057a1..bcaca693 100644 --- a/libraries/wuss/window/resize.c +++ b/libraries/wuss/window/resize.c @@ -78,10 +78,12 @@ result_t wuss_window_resize(wuss_window_t *window, size2d_t size) * as already-valid and so never repaint. Force its old position dirty * too. Shrinking needs no such help: old furniture positions only ever * land outside the new, smaller box, already covered above. */ +#ifdef WUSS_FURNITURE if (window->visible.x1 - window->visible.x0 > before.x1 - before.x0 || window->visible.y1 - window->visible.y0 > before.y1 - before.y0) wuss__furniture_invalidate_for(window, &before); wuss__furniture_invalidate(window); +#endif } return result_OK; diff --git a/libraries/wuss/window/set-scroll.c b/libraries/wuss/window/set-scroll.c index 531665f4..e1371013 100644 --- a/libraries/wuss/window/set-scroll.c +++ b/libraries/wuss/window/set-scroll.c @@ -16,9 +16,11 @@ void wuss_window_set_scroll(wuss_window_t *window, point_t p) wuss__content_box(window, &content); +#ifdef WUSS_FURNITURE /* the scrollbar sausage position depends on scroll, so its well needs * redrawing too -- content invalidation alone never touches it */ wuss__furniture_invalidate(window); +#endif if (window->wuss->z_order.next == &window->link) { From 587bcc440e896b1a2d65fcc1b745d0a1a95aeb04 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 30 Aug 2026 21:06:29 +0100 Subject: [PATCH 083/287] feat(wuss): pattern-fill work-area icons Add screen_fill_pattern(), an 8x8 two-colour tile fill primitive in framebuf/screen with eight built-in patterns (solid, grey50, stripes, diagonal, dots, grid, crosshatch), phase-locked to a caller-supplied origin so a scrolling fill stays put. Add wuss_ICON_TYPE_PATTERN: a non-interactive icon whose bbox is filled with one of those patterns in fg/bg, aligned to document space. Clicks fall through as wuss_EVENT_MOUSE; disabled swatches fold fg into bg. Cover the primitive in screen-test.c (phase, clipping) and show every pattern in the interactive icons task. Co-Authored-By: Claude Sonnet 5 --- CMakeLists.txt | 1 + include/framebuf/screen.h | 44 +++++++ include/wuss/icon.h | 15 ++- .../framebuf/screen/screen-fill-pattern.c | 113 ++++++++++++++++++ libraries/framebuf/screen/test/screen-test.c | 65 +++++++++- libraries/wuss/icon.h | 1 + libraries/wuss/icon/create.c | 14 ++- libraries/wuss/icon/draw.c | 16 +++ libraries/wuss/test/tasks/icons.c | 20 +++- 9 files changed, 281 insertions(+), 8 deletions(-) create mode 100644 libraries/framebuf/screen/screen-fill-pattern.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 0b62d49d..a322d526 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -241,6 +241,7 @@ set(FRAMEBUF_SOURCES libraries/framebuf/screen/screen-copy-rect.c libraries/framebuf/screen/screen-draw.c libraries/framebuf/screen/screen-draw-ninepatch.c + libraries/framebuf/screen/screen-fill-pattern.c libraries/framebuf/span-registry/get.c libraries/framebuf/span-registry/regdata.h libraries/framebuf/span/all8888.c diff --git a/include/framebuf/screen.h b/include/framebuf/screen.h index da0b5325..309f205b 100644 --- a/include/framebuf/screen.h +++ b/include/framebuf/screen.h @@ -94,6 +94,50 @@ void screen_draw_square(screen_t *scr, int size, colour_t colour); +/** + * Built-in 8x8 fill patterns for `screen_fill_pattern`. Each is a 1-bit tile: + * set bits take the foreground colour, clear bits the background. + */ +typedef enum screen_pattern +{ + screen_PATTERN_SOLID = 0, /**< Every pixel foreground. */ + screen_PATTERN_GREY50, /**< 50% checkerboard. */ + screen_PATTERN_HSTRIPE, /**< Horizontal bars. */ + screen_PATTERN_VSTRIPE, /**< Vertical bars. */ + screen_PATTERN_DIAGONAL, /**< Diagonal lines. */ + screen_PATTERN_DOTS, /**< Sparse dots. */ + screen_PATTERN_GRID, /**< Thin grid lines. */ + screen_PATTERN_CROSSHATCH, /**< Crossed thin lines. */ + screen_PATTERN__LIMIT /**< Count of patterns; not itself a pattern. */ +} +screen_pattern_t; + +/** + * Fills a box with a repeating 8x8 two-colour pattern. + * + * The tile is phased against (`origin_x`, `origin_y`): that coordinate is the + * one that would map to the box's top-left corner. Passing the caller's own + * scroll origin keeps the pattern locked to content as the box moves, rather + * than crawling with it. + * + * Clipped to the screen's clip region. + * + * \param[in] scr Screen to draw upon. + * \param[in] box Box to fill, inclusive-exclusive. + * \param[in] pattern Pattern to fill with. + * \param[in] origin_x X coordinate mapping to `box->x0` for tile phase. + * \param[in] origin_y Y coordinate mapping to `box->y0` for tile phase. + * \param[in] fg Colour for set pattern bits. + * \param[in] bg Colour for clear pattern bits. + */ +void screen_fill_pattern(screen_t *scr, + const box_t *box, + screen_pattern_t pattern, + int origin_x, + int origin_y, + colour_t fg, + colour_t bg); + /** * Draws a bitmap, alpha-blending it against the screen where the bitmap has an * alpha channel. On paletted screens, which have no linear channel bits to diff --git a/include/wuss/icon.h b/include/wuss/icon.h index f09b5a0b..be9eaa18 100644 --- a/include/wuss/icon.h +++ b/include/wuss/icon.h @@ -29,6 +29,7 @@ extern "C" #endif #include "base/result.h" +#include "framebuf/screen.h" #include "geom/box.h" #include "wuss/wuss.h" @@ -52,10 +53,16 @@ typedef enum wuss_icon_type wuss_ICON_TYPE_LABEL = 0, /**< Static text drawn with the window manager's * font. Not interactive: clicks fall through to * the task as wuss_EVENT_MOUSE. */ - wuss_ICON_TYPE_BUTTON /**< Bevelled rectangle with a centred text label + wuss_ICON_TYPE_BUTTON, /**< Bevelled rectangle with a centred text label * and pressed-state visual feedback; clicks and * hovers are delivered to the task as * wuss_EVENT_ICON. */ + wuss_ICON_TYPE_PATTERN /**< Bounding box filled with a repeating two-colour + * 8x8 tile (spec.pattern) in fg/bg, phased to + * document space so it scrolls rigidly with + * content. Not interactive: clicks fall through + * to the task as wuss_EVENT_MOUSE. text is + * ignored. */ } wuss_icon_type_t; @@ -89,7 +96,11 @@ typedef struct wuss_icon_spec wuss_colour_t bg; /**< Fill/bevel base colour, as an index into the * system palette. A label may pass * wuss_NO_BACKGROUND for text with no fill; a - * button must pass a real index. */ + * button or pattern icon must pass a real + * index. */ + screen_pattern_t pattern; /**< Tile for wuss_ICON_TYPE_PATTERN; ignored by + * other types. Zero (screen_PATTERN_SOLID) is a + * safe default for zero-initialised specs. */ wuss_icon_flags_t flags; /**< Appearance/behaviour flags. */ } wuss_icon_spec_t; diff --git a/libraries/framebuf/screen/screen-fill-pattern.c b/libraries/framebuf/screen/screen-fill-pattern.c new file mode 100644 index 00000000..dd514224 --- /dev/null +++ b/libraries/framebuf/screen/screen-fill-pattern.c @@ -0,0 +1,113 @@ +/* screen-fill-pattern.c -- fill a box with a repeating 8x8 two-colour pattern */ + +#include + +#include "framebuf/colour.h" +#include "framebuf/pixelfmt.h" +#include "geom/box.h" + +#include "framebuf/screen.h" + +/* One byte per row, MSB = leftmost pixel. */ +static const unsigned char patterns[screen_PATTERN__LIMIT][8] = +{ + { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }, /* SOLID */ + { 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55 }, /* GREY50 */ + { 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00 }, /* HSTRIPE */ + { 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA }, /* VSTRIPE */ + { 0x88, 0x44, 0x22, 0x11, 0x88, 0x44, 0x22, 0x11 }, /* DIAGONAL */ + { 0x88, 0x00, 0x22, 0x00, 0x88, 0x00, 0x22, 0x00 }, /* DOTS */ + { 0xFF, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80 }, /* GRID */ + { 0x80, 0x41, 0x22, 0x14, 0x08, 0x14, 0x22, 0x41 } /* CROSSHATCH */ +}; + +/* ponytail: per-pixel loop. Fine for icon-sized fills; if a large pattern + * fill ever profiles hot, expand each pattern row to a colour run once per + * scanline instead. */ +void screen_fill_pattern(screen_t *scr, + const box_t *box, + screen_pattern_t pattern, + int origin_x, + int origin_y, + colour_t fg, + colour_t bg) +{ + const unsigned char *tile; + box_t clip_box; + box_t draw_box; + pixelfmt_any_t fg_fmt, bg_fmt; + int x, y; + + assert(pattern >= 0 && pattern < screen_PATTERN__LIMIT); + if (pattern < 0 || pattern >= screen_PATTERN__LIMIT) + return; + + tile = patterns[pattern]; + + if (screen_get_clip(scr, &clip_box)) + return; /* invalid clipped screen */ + + if (box_intersection(&clip_box, box, &draw_box)) + return; /* nothing visible */ + + fg_fmt = colour_to_pixel(scr->palette, + (scr->format == pixelfmt_p4) ? 16 : 0, + fg, scr->format); + bg_fmt = colour_to_pixel(scr->palette, + (scr->format == pixelfmt_p4) ? 16 : 0, + bg, scr->format); + + switch (pixelfmt_log2bpp(scr->format)) + { + case 2: + { + unsigned char *rowp; + + rowp = (unsigned char *) scr->base + draw_box.y0 * scr->rowbytes; + for (y = draw_box.y0; y < draw_box.y1; y++) + { + unsigned char bits; + + bits = tile[(y - origin_y) & 7]; + for (x = draw_box.x0; x < draw_box.x1; x++) + { + unsigned char *scrp; + int shift; + pixelfmt_any_t px; + + px = (bits & (0x80u >> ((x - origin_x) & 7))) ? fg_fmt : bg_fmt; + scrp = rowp + (x >> 1); + shift = (x & 1) * 4; + + *scrp = (unsigned char) ((*scrp & ~(0xF << shift)) | + ((px & 0xF) << shift)); + } + rowp += scr->rowbytes; + } + } + break; + + case 5: + { + unsigned char *rowp; + + rowp = (unsigned char *) scr->base + draw_box.y0 * scr->rowbytes; + for (y = draw_box.y0; y < draw_box.y1; y++) + { + pixelfmt_any32_t *scrp; + unsigned char bits; + + bits = tile[(y - origin_y) & 7]; + scrp = (pixelfmt_any32_t *) rowp + draw_box.x0; + for (x = draw_box.x0; x < draw_box.x1; x++) + *scrp++ = (bits & (0x80u >> ((x - origin_x) & 7))) ? fg_fmt : bg_fmt; + rowp += scr->rowbytes; + } + } + break; + + default: + assert(!"Unimplemented pixel format"); + break; + } +} diff --git a/libraries/framebuf/screen/test/screen-test.c b/libraries/framebuf/screen/test/screen-test.c index dc8c3fd4..2d14a006 100644 --- a/libraries/framebuf/screen/test/screen-test.c +++ b/libraries/framebuf/screen/test/screen-test.c @@ -387,6 +387,68 @@ static result_t test_ninepatch(void) /* ----------------------------------------------------------------------- */ +static result_t test_fill_pattern(void) +{ + static testscreen_t ts; + static testscreen_t enc; + + box_t box = { 8, 8, 24, 24 }; + int fg, bg; + + fg = (int) np_encode(&enc, 255, 0, 0); + bg = (int) np_encode(&enc, 0, 0, 255); + + /* GREY50 is 0xAA,0x55,... : at origin (0,0) pixel (x,y) is fg when + * ((x ^ y) & 1) == 0. */ + testscreen_init(&ts); + screen_fill_pattern(&ts.scr, &box, screen_PATTERN_GREY50, 0, 0, + colour_rgb(255, 0, 0), colour_rgb(0, 0, 255)); + + if (np_at(&ts, 8, 8) != fg || /* (0,0) phase -> set bit */ + np_at(&ts, 9, 8) != bg || + np_at(&ts, 8, 9) != bg || + np_at(&ts, 9, 9) != fg) + { + printf("screen: fill_pattern GREY50 wrong at origin 0\n"); + return result_TEST_FAILED; + } + + /* Outside the box stays background. */ + if (np_at(&ts, 7, 7) != (int) (pixelfmt_bgrx8888_t) BACKGROUND || + np_at(&ts, 24, 24) != (int) (pixelfmt_bgrx8888_t) BACKGROUND) + { + printf("screen: fill_pattern drew outside the box\n"); + return result_TEST_FAILED; + } + + /* Shift the origin by one in x: every pixel's phase flips, so the same + * screen coordinate takes the other colour. */ + testscreen_init(&ts); + screen_fill_pattern(&ts.scr, &box, screen_PATTERN_GREY50, 1, 0, + colour_rgb(255, 0, 0), colour_rgb(0, 0, 255)); + if (np_at(&ts, 8, 8) != bg || np_at(&ts, 9, 8) != fg) + { + printf("screen: fill_pattern ignored origin phase\n"); + return result_TEST_FAILED; + } + + /* Honours the screen clip. */ + testscreen_init(&ts); + ts.scr.clip = (box_t) { 0, 0, 16, 64 }; + screen_fill_pattern(&ts.scr, &box, screen_PATTERN_SOLID, 0, 0, + colour_rgb(255, 0, 0), colour_rgb(0, 0, 255)); + if (np_at(&ts, 10, 10) != fg || + np_at(&ts, 20, 10) != (int) (pixelfmt_bgrx8888_t) BACKGROUND) + { + printf("screen: fill_pattern ignored the screen clip\n"); + return result_TEST_FAILED; + } + + return result_TEST_PASSED; +} + +/* ----------------------------------------------------------------------- */ + result_t screen_test(const char *resources) { typedef result_t (*screentestfn)(void); @@ -396,7 +458,8 @@ result_t screen_test(const char *resources) test_clip_invariance, test_clipping_still_happens, test_wu_fix8_extreme_coords, - test_ninepatch + test_ninepatch, + test_fill_pattern }; result_t rc; diff --git a/libraries/wuss/icon.h b/libraries/wuss/icon.h index a2c415c9..84e3aead 100644 --- a/libraries/wuss/icon.h +++ b/libraries/wuss/icon.h @@ -19,6 +19,7 @@ struct wuss_icon char *text; /* owned; never NULL ("" instead) */ wuss_colour_t fg; wuss_colour_t bg; + screen_pattern_t pattern; /* wuss_ICON_TYPE_PATTERN tile; 0 otherwise */ wuss_icon_flags_t flags; int pressed; /* button: 1 while held with the pointer inside */ }; diff --git a/libraries/wuss/icon/create.c b/libraries/wuss/icon/create.c index 823f1860..86686b56 100644 --- a/libraries/wuss/icon/create.c +++ b/libraries/wuss/icon/create.c @@ -26,10 +26,18 @@ result_t wuss_icon_create(wuss_window_t *window, w = window->wuss; - if (spec->type != wuss_ICON_TYPE_LABEL && spec->type != wuss_ICON_TYPE_BUTTON) + if (spec->type != wuss_ICON_TYPE_LABEL && + spec->type != wuss_ICON_TYPE_BUTTON && + spec->type != wuss_ICON_TYPE_PATTERN) return result_WUSS_BAD_ICON; - if (spec->type == wuss_ICON_TYPE_BUTTON && spec->bg == wuss_NO_BACKGROUND) + if ((spec->type == wuss_ICON_TYPE_BUTTON || + spec->type == wuss_ICON_TYPE_PATTERN) && + spec->bg == wuss_NO_BACKGROUND) + return result_WUSS_BAD_ICON; + + if (spec->type == wuss_ICON_TYPE_PATTERN && + (spec->pattern < 0 || spec->pattern >= screen_PATTERN__LIMIT)) return result_WUSS_BAD_ICON; if (spec->fg < 0 || spec->fg >= w->npalette) @@ -68,6 +76,8 @@ result_t wuss_icon_create(wuss_window_t *window, it->type = spec->type; it->fg = spec->fg; it->bg = spec->bg; + it->pattern = (spec->type == wuss_ICON_TYPE_PATTERN) ? spec->pattern + : screen_PATTERN_SOLID; it->flags = spec->flags; it->pressed = 0; diff --git a/libraries/wuss/icon/draw.c b/libraries/wuss/icon/draw.c index 98606923..9fb2b950 100644 --- a/libraries/wuss/icon/draw.c +++ b/libraries/wuss/icon/draw.c @@ -65,6 +65,22 @@ void wuss__icon_draw(wuss_t *wuss, switch (icon->type) { + case wuss_ICON_TYPE_PATTERN: + { + colour_t pat_fg; + + /* disabled: fold the pattern into its own ground so it reads as greyed, + * mirroring the button's fg-swap */ + pat_fg = (icon->flags & wuss_ICON_FLAGS_DISABLED) + ? wuss->palette[icon->bg] + : fg; + + screen_fill_pattern(scr, &b, icon->pattern, + content->x0 - scroll.x, content->y0 - scroll.y, + pat_fg, wuss->palette[icon->bg]); + } + break; + case wuss_ICON_TYPE_LABEL: { colour_t bg; diff --git a/libraries/wuss/test/tasks/icons.c b/libraries/wuss/test/tasks/icons.c index 5f184f38..a9953354 100644 --- a/libraries/wuss/test/tasks/icons.c +++ b/libraries/wuss/test/tasks/icons.c @@ -27,9 +27,11 @@ result_t icons_create(wuss_t *wuss, bmfont_t *font, icons_task_t *task) { + /* [0..3] plus one swatch per built-in pattern */ wuss_task_t delegate; - wuss_icon_spec_t specs[4]; - wuss_icon_t *made[4]; + wuss_icon_spec_t specs[4 + screen_PATTERN__LIMIT]; + wuss_icon_t *made[4 + screen_PATTERN__LIMIT]; + int p; result_t rc; task->font = font; @@ -88,7 +90,19 @@ result_t icons_create(wuss_t *wuss, specs[3].fg = palette_PICO8_BLACK; specs[3].bg = palette_PICO8_LIGHT_GREY; - rc = wuss_icon_create_array(task->window, specs, 4, made); + /* [4..] one swatch per built-in pattern, in a column down the document so + * they scroll through the window and stay phase-locked while doing so */ + for (p = 0; p < screen_PATTERN__LIMIT; p++) + { + specs[4 + p].bbox = (box_t) BOX_POS_SIZE(28, 90 + p * 44, 90, 36); + specs[4 + p].type = wuss_ICON_TYPE_PATTERN; + specs[4 + p].fg = palette_PICO8_DARK_BLUE; + specs[4 + p].bg = palette_PICO8_LIGHT_GREY; + specs[4 + p].pattern = (screen_pattern_t) p; + } + + rc = wuss_icon_create_array(task->window, specs, + 4 + screen_PATTERN__LIMIT, made); if (rc != result_OK) goto failure; From 3a9a4bfebeebe3bb730521925d97f2d7512562b2 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 30 Aug 2026 21:11:09 +0100 Subject: [PATCH 084/287] perf(framebuf): expand screen_fill_pattern tile rows to colour runs The tile is 8x8 and repeats, so a fill has only eight distinct pixel rows. Expand each to a phase-shifted colour run once up front; the scanline loops then index the run with no per-pixel bit test. The 32bpp path memcpy's whole 8-pixel runs instead of storing pixel by pixel. Co-Authored-By: Claude Sonnet 5 --- .../framebuf/screen/screen-fill-pattern.c | 58 ++++++++++++------- 1 file changed, 38 insertions(+), 20 deletions(-) diff --git a/libraries/framebuf/screen/screen-fill-pattern.c b/libraries/framebuf/screen/screen-fill-pattern.c index dd514224..0d9b5696 100644 --- a/libraries/framebuf/screen/screen-fill-pattern.c +++ b/libraries/framebuf/screen/screen-fill-pattern.c @@ -1,6 +1,7 @@ /* screen-fill-pattern.c -- fill a box with a repeating 8x8 two-colour pattern */ #include +#include #include "framebuf/colour.h" #include "framebuf/pixelfmt.h" @@ -21,9 +22,6 @@ static const unsigned char patterns[screen_PATTERN__LIMIT][8] = { 0x80, 0x41, 0x22, 0x14, 0x08, 0x14, 0x22, 0x41 } /* CROSSHATCH */ }; -/* ponytail: per-pixel loop. Fine for icon-sized fills; if a large pattern - * fill ever profiles hot, expand each pattern row to a colour run once per - * scanline instead. */ void screen_fill_pattern(screen_t *scr, const box_t *box, screen_pattern_t pattern, @@ -36,7 +34,9 @@ void screen_fill_pattern(screen_t *scr, box_t clip_box; box_t draw_box; pixelfmt_any_t fg_fmt, bg_fmt; - int x, y; + pixelfmt_any_t runs[8][8]; /* one expanded colour run per tile row */ + int xphase; + int row, col, x, y; assert(pattern >= 0 && pattern < screen_PATTERN__LIMIT); if (pattern < 0 || pattern >= screen_PATTERN__LIMIT) @@ -57,6 +57,19 @@ void screen_fill_pattern(screen_t *scr, (scr->format == pixelfmt_p4) ? 16 : 0, bg, scr->format); + /* The tile is 8x8 and repeats, so there are only eight distinct pixel rows. + * Expand each to a colour run once here, already phase-shifted for x, then + * the scanline loops just index runs[row][col] with no per-pixel bit test. */ + xphase = ((draw_box.x0 - origin_x) & 7); + for (row = 0; row < 8; row++) + { + unsigned char bits = tile[row]; + + for (col = 0; col < 8; col++) + runs[row][col] = + (bits & (0x80u >> ((xphase + col) & 7))) ? fg_fmt : bg_fmt; + } + switch (pixelfmt_log2bpp(scr->format)) { case 2: @@ -66,21 +79,17 @@ void screen_fill_pattern(screen_t *scr, rowp = (unsigned char *) scr->base + draw_box.y0 * scr->rowbytes; for (y = draw_box.y0; y < draw_box.y1; y++) { - unsigned char bits; + const pixelfmt_any_t *run = runs[(y - origin_y) & 7]; - bits = tile[(y - origin_y) & 7]; + col = 0; for (x = draw_box.x0; x < draw_box.x1; x++) { - unsigned char *scrp; - int shift; - pixelfmt_any_t px; - - px = (bits & (0x80u >> ((x - origin_x) & 7))) ? fg_fmt : bg_fmt; - scrp = rowp + (x >> 1); - shift = (x & 1) * 4; + unsigned char *scrp = rowp + (x >> 1); + int shift = (x & 1) * 4; *scrp = (unsigned char) ((*scrp & ~(0xF << shift)) | - ((px & 0xF) << shift)); + ((run[col] & 0xF) << shift)); + col = (col + 1) & 7; } rowp += scr->rowbytes; } @@ -94,13 +103,22 @@ void screen_fill_pattern(screen_t *scr, rowp = (unsigned char *) scr->base + draw_box.y0 * scr->rowbytes; for (y = draw_box.y0; y < draw_box.y1; y++) { - pixelfmt_any32_t *scrp; - unsigned char bits; + const pixelfmt_any_t *run = runs[(y - origin_y) & 7]; + pixelfmt_any32_t *scrp = (pixelfmt_any32_t *) rowp + draw_box.x0; + int w = draw_box.x1 - draw_box.x0; - bits = tile[(y - origin_y) & 7]; - scrp = (pixelfmt_any32_t *) rowp + draw_box.x0; - for (x = draw_box.x0; x < draw_box.x1; x++) - *scrp++ = (bits & (0x80u >> ((x - origin_x) & 7))) ? fg_fmt : bg_fmt; + /* leading partial tile up to an 8-pixel boundary, then whole runs */ + col = 0; + while (w > 0) + { + int n = 8 - col; + if (n > w) + n = w; + memcpy(scrp, run + col, (size_t) n * sizeof(*scrp)); + scrp += n; + w -= n; + col = 0; + } rowp += scr->rowbytes; } } From e00cabf35545c1ccd2305a5bb5530cd0f552a655 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 30 Aug 2026 21:15:11 +0100 Subject: [PATCH 085/287] docs: fill in CHANGELOG for changes since 277b4e8 Covers screen_fill_pattern(), wuss_ICON_TYPE_PATTERN, the WUSS_FURNITURE/WUSS_ICONS compile-time options and the scrollbar-well raise fix. Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff6845ab..b50d2567 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,23 @@ _Unreleased_ until one is cut. no region fits. The slot is released on close and on the first `wuss_window_move()` / `wuss_window_resize()`. An overall screen margin is kept around all auto-placed windows. +- `screen_fill_pattern()` — 8x8 two-colour tile fill primitive in + `framebuf/screen` with eight built-in patterns (solid, grey50, stripes, + diagonal, dots, grid, crosshatch), phase-locked to a caller-supplied origin + so a scrolling fill stays put. The tile has only eight distinct rows, so each + is expanded to a phase-shifted colour run once up front and the scanline + loops index it with no per-pixel bit test; the 32bpp path memcpy's whole + 8-pixel runs. +- `wuss_ICON_TYPE_PATTERN` — a non-interactive work-area icon whose bbox is + filled with a `screen_fill_pattern()` pattern in fg/bg, aligned to document + space. Clicks fall through as `wuss_EVENT_MOUSE`; disabled swatches fold fg + into bg. +- `WUSS_FURNITURE` and `WUSS_ICONS` CMake options (both default ON) drop the + furniture/*.c and icon/*.c files and `#ifdef`-guard every core call site, + struct field and helper that references them. With `WUSS_FURNITURE` off every + window is chromeless (content box == visible box); with `WUSS_ICONS` off the + `wuss_icon_*` API is not compiled. Programmatic and wheel scrolling survive + either off via the new core `scroll-step.c`. - `screen_draw_ninepatch()` — draws a resizable "9-patch" frame from a source image that is a 3x3 grid of equal cells: corners at natural size, edges and centre tiled, clipped to the destination box and the screen clip. @@ -54,6 +71,8 @@ _Unreleased_ until one is cut. ### Fixed +- Dragging a scrollbar well with Select no longer raises the window; only a + resize-icon grab restacks it. - `wuss_window_move()` no longer repaints already-blitted pixels when a drag past an occluded corner slides one clean piece of the window onto ground another clean piece just vacated. From fcdeb889e0621555046f3ee74c01c5b916cbf4b2 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 30 Aug 2026 21:47:04 +0100 Subject: [PATCH 086/287] fix(screen): clamp ninepatch corners so they don't overlap on tiny boxes When dst was narrower or shorter than two patch cells the near and far corner boxes overlapped and each drew its cell full-size across the shared band, so the last one won. Clamp the corner column/row boundaries to the destination midpoint: the near corner keeps the near half, the far corner the far half, and the edge/centre runs between collapse. Also assert log2bpp >= 3 in ninepatch_cell so a sub-8bpp source aborts cleanly instead of hitting a negative shift, and hoist a mid-scope loop var. Co-Authored-By: Claude Sonnet 5 --- .../framebuf/screen/screen-draw-ninepatch.c | 37 +++++++++++++------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/libraries/framebuf/screen/screen-draw-ninepatch.c b/libraries/framebuf/screen/screen-draw-ninepatch.c index 96ba082f..b0b7a795 100644 --- a/libraries/framebuf/screen/screen-draw-ninepatch.c +++ b/libraries/framebuf/screen/screen-draw-ninepatch.c @@ -21,9 +21,13 @@ static void ninepatch_cell(bitmap_t *cell, int pw, int ph) { + int log2bpp; int bpp; - bpp = 1 << (pixelfmt_log2bpp(src->format) - 3); + /* Byte stride per pixel. Sub-byte formats have no meaningful cell offset. */ + log2bpp = pixelfmt_log2bpp(src->format); + assert(log2bpp >= 3); + bpp = 1 << (log2bpp - 3); *cell = *src; cell->size = SIZE2D(pw, ph); @@ -44,7 +48,6 @@ static void tile_area(screen_t *scr, int stepy) { box_t clip; - int oy; if (box_is_empty(saved)) clip = *area; @@ -56,12 +59,17 @@ static void tile_area(screen_t *scr, scr->clip = clip; - for (oy = area->y0; oy < area->y1; oy += (stepy > 0) ? stepy : (area->y1 - oy)) { + int oy; int ox; - for (ox = area->x0; ox < area->x1; ox += (stepx > 0) ? stepx : (area->x1 - ox)) - screen_draw_bitmap(scr, ox, oy, cell); + for (oy = area->y0; oy < area->y1; + oy += (stepy > 0) ? stepy : (area->y1 - oy)) + { + for (ox = area->x0; ox < area->x1; + ox += (stepx > 0) ? stepx : (area->x1 - ox)) + screen_draw_bitmap(scr, ox, oy, cell); + } } } @@ -75,6 +83,7 @@ void screen_draw_ninepatch(screen_t *scr, box_t saved; box_t orig_clip; int pw, ph; + int midx, midy; int lx, rx, ty, by; bitmap_t cell; @@ -87,13 +96,17 @@ void screen_draw_ninepatch(screen_t *scr, pw = src->size.w / 3; ph = src->size.h / 3; - /* Corner column/row boundaries in the destination. When "dst" is narrower - * or shorter than two patches the near and far corners overlap; the clip in - * tile_area trims each to its own half. */ - lx = dst->x0 + pw; - rx = dst->x1 - pw; - ty = dst->y0 + ph; - by = dst->y1 - ph; + /* Corner column/row boundaries in the destination. When "dst" is narrower or + * shorter than two patches the near and far corners would overlap, so each + * boundary is clamped to the destination midpoint: the near corner gets the + * near half, the far corner the far half, and the edge/centre runs between + * them collapse to nothing. */ + midx = (dst->x0 + dst->x1) / 2; + midy = (dst->y0 + dst->y1) / 2; + lx = dst->x0 + pw; if (lx > midx) lx = midx; + rx = dst->x1 - pw; if (rx < midx) rx = midx; + ty = dst->y0 + ph; if (ty > midy) ty = midy; + by = dst->y1 - ph; if (by < midy) by = midy; /* Fold "dst" into the saved clip once, so every tile_area call is bounded by * the destination rectangle as well as the caller's clip. An empty caller From 4d561b2a4ec9a78440cec172e685f23df772f893 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 30 Aug 2026 21:47:04 +0100 Subject: [PATCH 087/287] style(screen): group screen_fill_pattern declarations at top of scope Co-Authored-By: Claude Sonnet 5 --- .../framebuf/screen/screen-fill-pattern.c | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/libraries/framebuf/screen/screen-fill-pattern.c b/libraries/framebuf/screen/screen-fill-pattern.c index 0d9b5696..d0613b6b 100644 --- a/libraries/framebuf/screen/screen-fill-pattern.c +++ b/libraries/framebuf/screen/screen-fill-pattern.c @@ -63,8 +63,9 @@ void screen_fill_pattern(screen_t *scr, xphase = ((draw_box.x0 - origin_x) & 7); for (row = 0; row < 8; row++) { - unsigned char bits = tile[row]; + unsigned char bits; + bits = tile[row]; for (col = 0; col < 8; col++) runs[row][col] = (bits & (0x80u >> ((xphase + col) & 7))) ? fg_fmt : bg_fmt; @@ -74,18 +75,20 @@ void screen_fill_pattern(screen_t *scr, { case 2: { - unsigned char *rowp; + unsigned char *rowp; + const pixelfmt_any_t *run; + unsigned char *scrp; + int shift; 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 = runs[(y - origin_y) & 7]; - + run = runs[(y - origin_y) & 7]; col = 0; for (x = draw_box.x0; x < draw_box.x1; x++) { - unsigned char *scrp = rowp + (x >> 1); - int shift = (x & 1) * 4; + scrp = rowp + (x >> 1); + shift = (x & 1) * 4; *scrp = (unsigned char) ((*scrp & ~(0xF << shift)) | ((run[col] & 0xF) << shift)); @@ -98,20 +101,24 @@ void screen_fill_pattern(screen_t *scr, case 5: { - unsigned char *rowp; + unsigned char *rowp; + const pixelfmt_any_t *run; + pixelfmt_any32_t *scrp; + int w; + int n; 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 = runs[(y - origin_y) & 7]; - pixelfmt_any32_t *scrp = (pixelfmt_any32_t *) rowp + draw_box.x0; - int w = draw_box.x1 - draw_box.x0; + run = runs[(y - origin_y) & 7]; + scrp = (pixelfmt_any32_t *) rowp + draw_box.x0; + w = draw_box.x1 - draw_box.x0; /* leading partial tile up to an 8-pixel boundary, then whole runs */ col = 0; while (w > 0) { - int n = 8 - col; + n = 8 - col; if (n > w) n = w; memcpy(scrp, run + col, (size_t) n * sizeof(*scrp)); From 57e3c715485249eeeb5791b6726936b7a6b75110 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 30 Aug 2026 21:47:04 +0100 Subject: [PATCH 088/287] fix(wuss): don't wedge oversized auto-placed windows at the origin next_cascade had a single wrap check and no loop, so a footprint wider or taller than the screen made the wrap condition permanently true and every oversized auto-placed window landed at (0,0). Pin an over-screen footprint at the top-left and return without advancing the cascade counter, so later normal-sized windows still cascade. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/window/create-placed.c | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/libraries/wuss/window/create-placed.c b/libraries/wuss/window/create-placed.c index d3314f79..6088a289 100644 --- a/libraries/wuss/window/create-placed.c +++ b/libraries/wuss/window/create-placed.c @@ -38,7 +38,9 @@ static void footprint_pad(const wuss_t *wuss, /* Pick the next cascade position for a window of the given footprint size, * once the layout packer has no room. Steps down/right by a titlebar each * call, wrapping back to the top-left when the step would push the footprint - * off the screen. */ + * off the screen. A footprint that is itself larger than the screen can never + * fit; it is pinned at the top-left and the cascade counter is not advanced, + * so it does not wedge every later window at the origin too. */ static void next_cascade(wuss_t *wuss, int fw, int fh, point_t *pos) { int scr_w, scr_h, step; @@ -53,6 +55,13 @@ static void next_cascade(wuss_t *wuss, int fw, int fh, point_t *pos) if (step <= 0) step = WUSS_DEFAULT_TITLEBAR_HEIGHT; + if (fw > scr_w || fh > scr_h) + { + pos->x = 0; + pos->y = 0; + return; + } + if (wuss->cascade.x + fw > scr_w || wuss->cascade.y + fh > scr_h) { wuss->cascade.x = 0; From 55dc77d3346ba0dc38cf17c40f9d525584d33ed2 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 30 Aug 2026 21:47:04 +0100 Subject: [PATCH 089/287] refactor(wuss): share bevel/backdrop colour validation in wuss_create The backdrop and bevel-colour range check plus its free/free/return cleanup were duplicated across the WUSS_FURNITURE and WUSS_ICONS-only branches and had drifted: the two paths defaulted the bevel colours differently for a NULL config. Extract validate_bevel_backdrop() and default both bevels to 0. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/create.c | 45 +++++++++++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/libraries/wuss/create.c b/libraries/wuss/create.c index 81a653fa..086dd587 100644 --- a/libraries/wuss/create.c +++ b/libraries/wuss/create.c @@ -13,6 +13,24 @@ #include "impl.h" +#if defined(WUSS_FURNITURE) || defined(WUSS_ICONS) +/* Range-check the bevel colours and (when set) the backdrop against the + * palette. Shared by the furniture and icons-only paths so the accepted + * range, the error code and the freed-pointer set stay in one place. */ +static result_t validate_bevel_backdrop(const wuss_t *w, + wuss_colour_t blight, + wuss_colour_t bdark) +{ + if (blight < 0 || blight >= w->npalette || + bdark < 0 || bdark >= w->npalette || + (w->backdrop != wuss_NO_BACKGROUND && + (w->backdrop < 0 || w->backdrop >= w->npalette))) + return result_WUSS_BAD_COLOUR; + + return result_OK; +} +#endif + result_t wuss_create(screen_t *scr, bmfont_t *font, const colour_t *palette, @@ -102,8 +120,8 @@ result_t wuss_create(screen_t *scr, pal.scroll.wells = bg; pal.scroll.sausages = fg; - blight = pal.title.bg; - bdark = pal.title.bg; + blight = 0; + bdark = 0; } if (pal.title.bg < 0 || pal.title.bg >= w->npalette || @@ -115,10 +133,7 @@ result_t wuss_create(screen_t *scr, pal.scroll.arrows < 0 || pal.scroll.arrows >= w->npalette || pal.scroll.wells < 0 || pal.scroll.wells >= w->npalette || pal.scroll.sausages < 0 || pal.scroll.sausages >= w->npalette || - blight < 0 || blight >= w->npalette || - bdark < 0 || bdark >= w->npalette || - (w->backdrop != wuss_NO_BACKGROUND && - (w->backdrop < 0 || w->backdrop >= w->npalette))) + validate_bevel_backdrop(w, blight, bdark) != result_OK) { free(w->palette); free(w); @@ -145,13 +160,6 @@ result_t wuss_create(screen_t *scr, } #else /* !WUSS_FURNITURE */ w->backdrop = (config != NULL) ? config->backdrop : wuss_NO_BACKGROUND; - if (w->backdrop != wuss_NO_BACKGROUND && - (w->backdrop < 0 || w->backdrop >= w->npalette)) - { - free(w->palette); - free(w); - return result_WUSS_BAD_COLOUR; - } #ifdef WUSS_ICONS if (config != NULL) @@ -164,8 +172,7 @@ result_t wuss_create(screen_t *scr, blight = 0; bdark = 0; } - if (blight < 0 || blight >= w->npalette || - bdark < 0 || bdark >= w->npalette) + if (validate_bevel_backdrop(w, blight, bdark) != result_OK) { free(w->palette); free(w); @@ -173,6 +180,14 @@ result_t wuss_create(screen_t *scr, } w->bevel_light = blight; w->bevel_dark = bdark; +#else + if (w->backdrop != wuss_NO_BACKGROUND && + (w->backdrop < 0 || w->backdrop >= w->npalette)) + { + free(w->palette); + free(w); + return result_WUSS_BAD_COLOUR; + } #endif #endif /* WUSS_FURNITURE */ From e82cb39d3b72c05b731a606908fa961dfea7d14a Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 30 Aug 2026 21:47:04 +0100 Subject: [PATCH 090/287] refactor(wuss): replace dead wuss__icon_screen_box with a shared transform wuss__icon_screen_box was declared, compiled and linked with zero callers, re-deriving the content-to-screen bbox transform that wuss__icon_draw open-codes. Drop it for wuss__icon_box_to_screen(content, scroll, bbox, out), which wuss__icon_draw now routes through, so the transform exists once. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/icon.h | 11 +++++++---- libraries/wuss/icon/draw.c | 5 +---- libraries/wuss/icon/screen-box.c | 26 +++++++++++++++----------- 3 files changed, 23 insertions(+), 19 deletions(-) diff --git a/libraries/wuss/icon.h b/libraries/wuss/icon.h index 84e3aead..13c2c67f 100644 --- a/libraries/wuss/icon.h +++ b/libraries/wuss/icon.h @@ -24,10 +24,13 @@ struct wuss_icon int pressed; /* button: 1 while held with the pointer inside */ }; -/* Convert an icon's bbox (virtual document space) to a screen-space box, using - * the owning window's current content box and scroll offset: - * screen = content.x0 - scroll.x + bbox. Mirrors wuss_window_invalidate. */ -void wuss__icon_screen_box(const wuss_icon_t *icon, box_t *out); +/* Map an icon bbox (virtual document space) into screen space: + * screen = content.x0 - scroll.x + bbox. wuss__icon_draw paints through this + * so hit-testing and invalidation cannot drift from what is drawn. */ +void wuss__icon_box_to_screen(const box_t *content, + point_t scroll, + const box_t *bbox, + box_t *out); /* Invalidate exactly this icon's bbox, via wuss_window_invalidate, so a * set_text / pressed-state / hide change repaints just the icon. */ diff --git a/libraries/wuss/icon/draw.c b/libraries/wuss/icon/draw.c index 9fb2b950..2a06be2b 100644 --- a/libraries/wuss/icon/draw.c +++ b/libraries/wuss/icon/draw.c @@ -46,10 +46,7 @@ void wuss__icon_draw(wuss_t *wuss, scr = wuss->scr; - b.x0 = content->x0 - scroll.x + icon->bbox.x0; - b.y0 = content->y0 - scroll.y + icon->bbox.y0; - b.x1 = content->x0 - scroll.x + icon->bbox.x1; - b.y1 = content->y0 - scroll.y + icon->bbox.y1; + wuss__icon_box_to_screen(content, scroll, &icon->bbox, &b); if (b.x1 <= b.x0 || b.y1 <= b.y0) return; diff --git a/libraries/wuss/icon/screen-box.c b/libraries/wuss/icon/screen-box.c index 25440c7e..59e4e4e0 100644 --- a/libraries/wuss/icon/screen-box.c +++ b/libraries/wuss/icon/screen-box.c @@ -1,17 +1,21 @@ /* screen-box.c -- wuss - work-area icon bbox to screen space */ +#include "geom/box.h" +#include "geom/point.h" + #include "../impl.h" -void wuss__icon_screen_box(const wuss_icon_t *icon, box_t *out) +/* Map an icon bbox (virtual document space) into screen space, given the + * owning window's content box and scroll offset. wuss__icon_draw uses this + * for the box it paints into; keeping the transform here means hit-testing + * and invalidation cannot drift from what is drawn. */ +void wuss__icon_box_to_screen(const box_t *content, + point_t scroll, + const box_t *bbox, + box_t *out) { - box_t content; - point_t scroll; - - wuss__content_box(icon->window, &content); - scroll = icon->window->scroll; - - out->x0 = content.x0 - scroll.x + icon->bbox.x0; - out->y0 = content.y0 - scroll.y + icon->bbox.y0; - out->x1 = content.x0 - scroll.x + icon->bbox.x1; - out->y1 = content.y0 - scroll.y + icon->bbox.y1; + out->x0 = content->x0 - scroll.x + bbox->x0; + out->y0 = content->y0 - scroll.y + bbox->y0; + out->x1 = content->x0 - scroll.x + bbox->x1; + out->y1 = content->y0 - scroll.y + bbox->y1; } From 1ca951ab4fd79371954a971abe7ef0d23b8e3c36 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 30 Aug 2026 21:47:04 +0100 Subject: [PATCH 091/287] docs(wuss): correct the screen_copy_rect failure comment in window_move screen_copy_rect also returns 0 for an off-screen source or destination, and the blit pieces are only occlusion-clipped, so a later piece can fail after earlier pieces have already moved pixels. The frame still self-heals via the union fallback; the comment now says so instead of claiming the blit fails identically on the first piece. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/window/move.c | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/libraries/wuss/window/move.c b/libraries/wuss/window/move.c index f9eeb611..a23eaa17 100644 --- a/libraries/wuss/window/move.c +++ b/libraries/wuss/window/move.c @@ -154,10 +154,12 @@ void wuss_window_move(wuss_window_t *window, point_t p) POINT(blit_dest[idx].x0, blit_dest[idx].y0), &copied)) { - /* The screen format doesn't support the blit at all (e.g. paletted): - * this fails identically for every piece, so it fails on the first - * one, before any blit has happened. Bail out; the caller's fallback - * full invalidate repairs the window either way. */ + /* screen_copy_rect refused this piece: either the screen format has no + * blit path (e.g. paletted -- fails on the first piece, before anything + * has moved) or this piece's source/dest lies off-screen, which can + * happen part-way through after earlier pieces have already blitted. + * Either way, bail: the pieces done so far are self-consistent and the + * caller's fallback full invalidate repaints the whole union. */ blit_failed = 1; break; } From fea701586baec88ace718b436be74dc2e12d4972 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 30 Aug 2026 21:50:10 +0100 Subject: [PATCH 092/287] docs: note the ninepatch corner-clamp fix in CHANGELOG Covers fcdeb88; the other commits since e00cabf are docs/refactor/style with no user-facing change. Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b50d2567..cc9b7fb8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -84,3 +84,5 @@ _Unreleased_ until one is cut. - Removed signed-overflow and negative-shift undefined behaviour in the anti-aliased fixed-point line rasteriser, reachable with long or off-screen 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. From 00e84f90a4c7a4eba7465939da948e2ed0b70324 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 30 Aug 2026 22:29:21 +0100 Subject: [PATCH 093/287] fix(wuss): clamp scroll offset after a resize reveals past-extent content Enlarging a window's viewport (or growing it past the document extent) could scroll content that no longer exists into view. wuss_window_resize now re-clamps window->scroll via wuss__scroll_clamp after recomputing the visible box; if the offset moved, the whole content box (and the scrollbar well) is invalidated, since the grown/shrunk-sliver logic assumes an unchanged interior. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/window/resize.c | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/libraries/wuss/window/resize.c b/libraries/wuss/window/resize.c index bcaca693..1e95d901 100644 --- a/libraries/wuss/window/resize.c +++ b/libraries/wuss/window/resize.c @@ -35,6 +35,7 @@ result_t wuss_window_resize(wuss_window_t *window, size2d_t size) int outline_px, titlebar_height; box_t before; point_t carve; + point_t clamped; if (!wuss__size_ok(size.w, size.h)) return result_WUSS_TOO_SMALL; @@ -52,6 +53,24 @@ result_t wuss_window_resize(wuss_window_t *window, size2d_t size) wuss__notify_open(window); + /* Enlarging the viewport (or growing it past the doc extent) can scroll + * content that no longer exists into view; pull the offset back so only + * the document extent is ever shown. The interior has moved relative to + * the window, so the whole content box needs repainting -- the + * grown/shrunk-sliver logic below assumes an unchanged interior. */ + clamped = wuss__scroll_clamp(window, window->scroll); + if (clamped.x != window->scroll.x || clamped.y != window->scroll.y) + { + box_t content; + + window->scroll = clamped; + wuss__content_box(window, &content); + wuss__invalidate_clipped(window, &content); +#ifdef WUSS_FURNITURE + wuss__furniture_invalidate(window); +#endif + } + if (window->flags & wuss_WINDOW_NO_RESIZE_BLIT) { /* This task's content isn't just anchored positions plus furniture -- From dbcb8113aa699c857cc3ad2249f5054247944516 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 30 Aug 2026 23:02:55 +0100 Subject: [PATCH 094/287] feat(wuss): allow the desktop and window backgrounds to be a pattern fill Introduces wuss_backdrop_t ({ colour, pattern, pattern_bg }), replacing the bare wuss_colour_t for wuss_config_t::backdrop and for the bg parameter of wuss_window_create(), wuss_window_create_placed() and wuss_window_set_background(). A non-SOLID pattern is tiled with screen_fill_pattern(): the desktop phased to the screen origin, a window's content phased to its scroll origin so the fill stays locked to the content. wuss__validate_backdrop() and wuss__fill_backdrop() are shared out to a new backdrop.c. wuss_BACKDROP_COLOUR() / wuss_BACKDROP_PATTERN() build the struct as a compound literal; the flat-colour case is wuss_BACKDROP_COLOUR(old_value). The icons test task now uses a CROSSHATCH pattern backdrop instead of drawing its own grid. A transparent LABEL icon over a patterned window backdrop blends its glyphs against the pattern's background colour, not the foreground. Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 9 +++ CMakeLists.txt | 1 + docs/windowing/wuss.md | 8 ++- include/wuss/window.h | 37 ++++++----- include/wuss/wuss.h | 50 ++++++++++++-- libraries/wuss/backdrop.c | 43 +++++++++++++ libraries/wuss/create.c | 28 ++++---- libraries/wuss/icon/draw.c | 8 ++- libraries/wuss/impl.h | 21 +++++- libraries/wuss/redraw.c | 27 +++----- libraries/wuss/test/tasks/ball.c | 2 +- libraries/wuss/test/tasks/blank.c | 5 +- libraries/wuss/test/tasks/chars.c | 2 +- libraries/wuss/test/tasks/checker.c | 4 +- libraries/wuss/test/tasks/curve.c | 2 +- libraries/wuss/test/tasks/gradient.c | 2 +- libraries/wuss/test/tasks/icons.c | 18 ++---- libraries/wuss/test/tasks/icons.h | 1 - libraries/wuss/test/tasks/image.c | 2 +- libraries/wuss/test/tasks/launcher.c | 2 +- libraries/wuss/test/tasks/palette.c | 2 +- libraries/wuss/test/tasks/porter-duff.c | 2 +- libraries/wuss/test/tasks/sofa.c | 2 +- libraries/wuss/test/tasks/text.c | 2 +- libraries/wuss/test/wuss-test.c | 86 +++++++++++++------------ libraries/wuss/window/create-placed.c | 2 +- libraries/wuss/window/create.c | 4 +- libraries/wuss/window/set-background.c | 4 +- 28 files changed, 244 insertions(+), 132 deletions(-) create mode 100644 libraries/wuss/backdrop.c diff --git a/CHANGELOG.md b/CHANGELOG.md index cc9b7fb8..22d033b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,6 +57,15 @@ _Unreleased_ until one is cut. ### Changed +- **Breaking:** the window/desktop background is now a `wuss_backdrop_t` + (`{ colour, pattern, pattern_bg }`) instead of a bare `wuss_colour_t`: + `wuss_config_t::backdrop`, and the `bg` parameter of + `wuss_window_create()`, `wuss_window_create_placed()` and + `wuss_window_set_background()`. A non-`screen_PATTERN_SOLID` `pattern` fills + with `screen_fill_pattern()` — the desktop phased to the screen origin, a + window's content phased to its scroll origin so the pattern stays locked to + the content. `wuss_BACKDROP_COLOUR(c)` and `wuss_BACKDROP_PATTERN(c, p, b)` + build one; the flat-colour case is `wuss_BACKDROP_COLOUR(old_value)`. - **Breaking:** `wuss_button_t` values are now flags (`wuss_BUTTON_SELECT` 4, `wuss_BUTTON_MENU` 2, `wuss_BUTTON_ADJUST` 1, `wuss_BUTTON_NONE` 0) so chords such as Select+Adjust can be reported. Client code comparing a diff --git a/CMakeLists.txt b/CMakeLists.txt index a322d526..1df7a23a 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -326,6 +326,7 @@ set(UTILS_SOURCES libraries/utils/primes/primes.c) set(WUSS_CORE_SOURCES + libraries/wuss/backdrop.c libraries/wuss/create.c libraries/wuss/destroy.c libraries/wuss/get-font.c diff --git a/docs/windowing/wuss.md b/docs/windowing/wuss.md index 0871dd5c..a90913df 100644 --- a/docs/windowing/wuss.md +++ b/docs/windowing/wuss.md @@ -20,7 +20,9 @@ result_t wuss_create(screen_t *scr, wuss_t **wuss); ``` -`font` and `palette` may both be NULL, for unlabelled titlebars and a built-in default palette respectively. `config` may be NULL for default titlebar height/colours. `config->backdrop` sets a desktop background colour painted behind windows on every redraw, or `wuss_NO_BACKGROUND` (the default when `config` is NULL) to leave the background untouched and require the caller to repaint it itself. `wuss_get_font` reads back the font passed in (or NULL), for a task that wants to draw its own content in the same face as titlebars. +`font` and `palette` may both be NULL, for unlabelled titlebars and a built-in default palette respectively. `config` may be NULL for default titlebar height/colours. `config->backdrop` is a `wuss_backdrop_t` — a flat colour, or an 8x8 fill pattern — painted behind windows on every redraw; set its `colour` to `wuss_NO_BACKGROUND` (the default when `config` is NULL) to leave the background untouched and require the caller to repaint it itself. A non-`screen_PATTERN_SOLID` `pattern` tiles that pattern in `colour` over `pattern_bg`, phased to a fixed screen origin so it does not crawl between full and dirty-region redraws. `wuss_get_font` reads back the font passed in (or NULL), for a task that wants to draw its own content in the same face as titlebars. + +A `wuss_backdrop_t` is `{ colour, pattern, pattern_bg }`; the `wuss_BACKDROP_COLOUR(c)` and `wuss_BACKDROP_PATTERN(c, p, b)` macros build one as a compound literal. Destroy with `wuss_destroy`, which also destroys any windows still open on it. @@ -30,13 +32,13 @@ Create a window with a content bounding box, optional title, appearance flags, a ```C result_t wuss_window_create(wuss_t *wuss, const box_t *content, const char *title, - wuss_window_flags_t flags, wuss_colour_t bg, + wuss_window_flags_t flags, wuss_backdrop_t bg, const wuss_task_t *task, size2d_t doc, size2d_t min_doc, wuss_window_t **window); ``` -`bg` is filled in by wuss before each redraw event, or `wuss_NO_BACKGROUND` for the task to draw its own background (avoids a redundant fill behind an opaque task); changeable later via `wuss_window_set_background`. +`bg` is a `wuss_backdrop_t` (flat colour or 8x8 pattern, as for `config->backdrop`), filled in by wuss before each redraw event; set its `colour` to `wuss_NO_BACKGROUND` for the task to draw its own background (avoids a redundant fill behind an opaque task). Any pattern is phased to the window's scroll origin so it stays locked to the content as the window scrolls. Changeable later via `wuss_window_set_background`, which also takes a `wuss_backdrop_t`. `doc` is the virtual document extent behind the horizontal/vertical scrollbars' sausage proportion; pass `content`'s own width/height for a window with nothing to scroll. It is also the ceiling a resize-drag or toggle-size grows the content area to. Set once at creation, immutable thereafter. diff --git a/include/wuss/window.h b/include/wuss/window.h index efdc7464..601e51ea 100644 --- a/include/wuss/window.h +++ b/include/wuss/window.h @@ -47,10 +47,13 @@ extern "C" * \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. - * \param[in] bg Content background, filled by Wuss before each redraw, or - * wuss_NO_BACKGROUND for the task to draw its own - * background (avoids a redundant fill behind an opaque - * task). Changeable later via wuss_window_set_background. + * \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 for the task to draw + * its own background (avoids a redundant fill behind an + * opaque task). Any pattern is phased to the window's + * scroll origin so it stays locked to the content. + * Changeable later via wuss_window_set_background. * \param[in] task Content delegate. Copied in. May be NULL for a window * with no content handling. * \param[in] doc Virtual document extent, for the scrollbars' sausage @@ -63,14 +66,15 @@ extern "C" * window unusably small or larger than its document. * \param[out] window Newly created window. Becomes the topmost window. * \return \ref result_OK on success, \ref result_WUSS_TOO_SMALL if content's - * width or height is not positive, \ref result_WUSS_BAD_COLOUR if bg is - * out of range for the palette, or another appropriate result code. + * width or height is not positive, \ref result_WUSS_BAD_COLOUR if any + * of bg's colours are out of range for the palette, or another + * appropriate result code. */ result_t wuss_window_create(wuss_t *wuss, const box_t *content, const char *title, wuss_window_flags_t flags, - wuss_colour_t bg, + wuss_backdrop_t bg, const wuss_task_t *task, size2d_t doc, size2d_t min_doc, @@ -109,7 +113,7 @@ result_t wuss_window_create_placed(wuss_t *wuss, size2d_t size, const char *title, wuss_window_flags_t flags, - wuss_colour_t bg, + wuss_backdrop_t bg, const wuss_task_t *task, size2d_t doc, size2d_t min_doc, @@ -208,17 +212,18 @@ void wuss_window_set_scroll(wuss_window_t *window, point_t p); void wuss_window_get_scroll(const wuss_window_t *window, point_t *p); /** - * Change a window's background colour, invalidating its content area so the - * next redraw picks up the new fill. + * Change a window's background, invalidating its content area so the next + * redraw picks up the new fill. * * \param[in] window Window to change. - * \param[in] bg New content background, as an index into the system - * palette, or wuss_NO_BACKGROUND to hand background painting - * back to the task. - * \return \ref result_OK on success, \ref result_WUSS_BAD_COLOUR if bg is out - * of range for the palette. + * \param[in] bg New content background: a flat colour or an 8x8 fill + * pattern (see wuss_backdrop_t). Set its colour to + * wuss_NO_BACKGROUND to hand background painting back to the + * task. + * \return \ref result_OK on success, \ref result_WUSS_BAD_COLOUR if any of bg's + * colours are out of range for the palette. */ -result_t wuss_window_set_background(wuss_window_t *window, wuss_colour_t bg); +result_t wuss_window_set_background(wuss_window_t *window, wuss_backdrop_t bg); #ifdef __cplusplus } diff --git a/include/wuss/wuss.h b/include/wuss/wuss.h index de8d74bc..14b4bd6d 100644 --- a/include/wuss/wuss.h +++ b/include/wuss/wuss.h @@ -18,6 +18,7 @@ extern "C" #include "base/result.h" #include "framebuf/bmfont.h" +#include "framebuf/screen.h" #include "geom/box.h" #include "geom/point.h" #include "geom/size.h" @@ -176,12 +177,51 @@ typedef enum wuss_zorder } wuss_zorder_t; +/** + * Desktop background specification: a flat colour, or an 8x8 fill pattern. Used + * by wuss_config_t. Always honoured regardless of the WUSS_FURNITURE / + * WUSS_ICONS options. + */ +typedef struct wuss_backdrop +{ + /** + * Fill colour, or wuss_NO_BACKGROUND to leave the background untouched (the + * caller must then repaint it itself before wuss_redraw / wuss_redraw_dirty). + * When pattern is not screen_PATTERN_SOLID this is the pattern's foreground + * (set-bit) colour. + */ + wuss_colour_t colour; + + /** + * Fill pattern. screen_PATTERN_SOLID (the default) fills flat in colour; any + * other value tiles that pattern in colour over pattern_bg, phased to a fixed + * screen origin so it stays put across dirty-region redraws. Ignored when + * colour is wuss_NO_BACKGROUND. + */ + screen_pattern_t pattern; + + /** Pattern background (clear-bit) colour; used only when pattern is not + * screen_PATTERN_SOLID. */ + wuss_colour_t pattern_bg; +} +wuss_backdrop_t; + +/** A flat-colour wuss_backdrop_t (or wuss_NO_BACKGROUND for none), as a + * compound literal -- the common case where no fill pattern is wanted. */ +#define wuss_BACKDROP_COLOUR(c) \ + ((wuss_backdrop_t) { (c), screen_PATTERN_SOLID, wuss_NO_BACKGROUND }) + +/** A patterned wuss_backdrop_t: 8x8 pattern p tiled in colour c over + * background colour b. */ +#define wuss_BACKDROP_PATTERN(c, p, b) ((wuss_backdrop_t) { (c), (p), (b) }) + /** * Optional creation-time configuration. * * \note titlebar_height and palette are ignored when the library is built with * WUSS_FURNITURE off; bevel is ignored when built with both - * WUSS_FURNITURE and WUSS_ICONS off. backdrop is always honoured. + * WUSS_FURNITURE and WUSS_ICONS off. backdrop is always honoured. See the + * backdrop sub-struct for its own notes. */ typedef struct wuss_config { @@ -207,12 +247,8 @@ typedef struct wuss_config } bevel; - /** - * Desktop background colour, painted behind windows on every redraw, or - * wuss_NO_BACKGROUND to leave the background untouched (the caller must then - * repaint it itself before wuss_redraw / wuss_redraw_dirty). - */ - wuss_colour_t backdrop; + /** Desktop background, painted behind windows on every redraw. */ + wuss_backdrop_t backdrop; } wuss_config_t; diff --git a/libraries/wuss/backdrop.c b/libraries/wuss/backdrop.c new file mode 100644 index 00000000..7da138f8 --- /dev/null +++ b/libraries/wuss/backdrop.c @@ -0,0 +1,43 @@ +/* backdrop.c -- wuss - shared backdrop validation and fill */ + +#include "framebuf/screen.h" + +#include "impl.h" + +result_t wuss__validate_backdrop(const wuss_t *wuss, + const wuss_backdrop_t *backdrop) +{ + if (backdrop->colour == wuss_NO_BACKGROUND) + return result_OK; + + if (backdrop->colour < 0 || backdrop->colour >= wuss->npalette) + return result_WUSS_BAD_COLOUR; + + if (backdrop->pattern < 0 || backdrop->pattern >= screen_PATTERN__LIMIT) + return result_WUSS_BAD_COLOUR; + + if (backdrop->pattern != screen_PATTERN_SOLID && + (backdrop->pattern_bg < 0 || backdrop->pattern_bg >= wuss->npalette)) + return result_WUSS_BAD_COLOUR; + + return result_OK; +} + +void wuss__fill_backdrop(screen_t *scr, + const colour_t *palette, + const wuss_backdrop_t *backdrop, + const box_t *area, + int origin_x, + int origin_y) +{ + if (backdrop->colour == wuss_NO_BACKGROUND) + return; + + if (backdrop->pattern == screen_PATTERN_SOLID) + screen_draw_rect(scr, area->x0, area->y0, box_size(area), + palette[backdrop->colour]); + else + screen_fill_pattern(scr, area, backdrop->pattern, origin_x, origin_y, + palette[backdrop->colour], + palette[backdrop->pattern_bg]); +} diff --git a/libraries/wuss/create.c b/libraries/wuss/create.c index 086dd587..3553ef48 100644 --- a/libraries/wuss/create.c +++ b/libraries/wuss/create.c @@ -14,17 +14,16 @@ #include "impl.h" #if defined(WUSS_FURNITURE) || defined(WUSS_ICONS) -/* Range-check the bevel colours and (when set) the backdrop against the - * palette. Shared by the furniture and icons-only paths so the accepted - * range, the error code and the freed-pointer set stay in one place. */ +/* Range-check the bevel colours and the backdrop against the palette. Shared + * by the furniture and icons-only paths so the accepted range, the error code + * and the freed-pointer set stay in one place. */ static result_t validate_bevel_backdrop(const wuss_t *w, wuss_colour_t blight, wuss_colour_t bdark) { if (blight < 0 || blight >= w->npalette || bdark < 0 || bdark >= w->npalette || - (w->backdrop != wuss_NO_BACKGROUND && - (w->backdrop < 0 || w->backdrop >= w->npalette))) + wuss__validate_backdrop(w, &w->backdrop) != result_OK) return result_WUSS_BAD_COLOUR; return result_OK; @@ -87,18 +86,26 @@ result_t wuss_create(screen_t *scr, w->npalette = palette_PICO8__LENGTH; } + if (config != NULL) + { + w->backdrop = config->backdrop; + } + else + { + w->backdrop.colour = wuss_NO_BACKGROUND; + w->backdrop.pattern = screen_PATTERN_SOLID; + w->backdrop.pattern_bg = wuss_NO_BACKGROUND; + } + #ifdef WUSS_FURNITURE if (config != NULL) { pal = config->palette; blight = config->bevel.light; bdark = config->bevel.dark; - w->backdrop = config->backdrop; } else { - w->backdrop = wuss_NO_BACKGROUND; - if (palette == NULL) { bg = palette_PICO8_DARK_BLUE; @@ -159,8 +166,6 @@ result_t wuss_create(screen_t *scr, w->titlebar_height = WUSS_DEFAULT_TITLEBAR_HEIGHT; } #else /* !WUSS_FURNITURE */ - w->backdrop = (config != NULL) ? config->backdrop : wuss_NO_BACKGROUND; - #ifdef WUSS_ICONS if (config != NULL) { @@ -181,8 +186,7 @@ result_t wuss_create(screen_t *scr, w->bevel_light = blight; w->bevel_dark = bdark; #else - if (w->backdrop != wuss_NO_BACKGROUND && - (w->backdrop < 0 || w->backdrop >= w->npalette)) + if (wuss__validate_backdrop(w, &w->backdrop) != result_OK) { free(w->palette); free(w); diff --git a/libraries/wuss/icon/draw.c b/libraries/wuss/icon/draw.c index 2a06be2b..e6102eea 100644 --- a/libraries/wuss/icon/draw.c +++ b/libraries/wuss/icon/draw.c @@ -88,9 +88,13 @@ void wuss__icon_draw(wuss_t *wuss, screen_draw_rect(scr, b.x0, b.y0, SIZE2D(b.x1 - b.x0, b.y1 - b.y0), bg); } - else if (icon->window->bg != wuss_NO_BACKGROUND) + else if (icon->window->bg.colour != wuss_NO_BACKGROUND) { - bg = wuss->palette[icon->window->bg]; + /* blend against the dominant backdrop colour: for a pattern fill + * that's its background (clear-bit) colour, not the foreground */ + bg = (icon->window->bg.pattern != screen_PATTERN_SOLID) + ? wuss->palette[icon->window->bg.pattern_bg] + : wuss->palette[icon->window->bg.colour]; } else { diff --git a/libraries/wuss/impl.h b/libraries/wuss/impl.h index fa8f52da..5982e9ed 100644 --- a/libraries/wuss/impl.h +++ b/libraries/wuss/impl.h @@ -62,7 +62,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 */ #endif - wuss_colour_t backdrop; /* wuss_NO_BACKGROUND for none */ + wuss_backdrop_t backdrop; /* colour==wuss_NO_BACKGROUND: none */ #ifdef WUSS_FURNITURE int titlebar_height; #endif @@ -92,7 +92,7 @@ struct wuss_window box_t visible; /* full on-screen footprint: content expanded * outward by any titlebar/outline furniture */ wuss_task_t task; - wuss_colour_t bg; + wuss_backdrop_t bg; /* content background; colour==wuss_NO_BACKGROUND: none */ wuss_window_flags_t flags; point_t scroll; /* offset into virtual content space of the * content box's top-left; see wuss_window_set_scroll */ @@ -119,6 +119,23 @@ struct wuss_window wuss_window_t *wuss__window_at(wuss_t *wuss, point_t p); +/* Range-check a backdrop spec against the palette: its colour, and -- when a + * non-solid pattern is set -- its pattern index and background colour. + * Returns result_OK or result_WUSS_BAD_COLOUR. */ +result_t wuss__validate_backdrop(const wuss_t *wuss, + const wuss_backdrop_t *backdrop); + +/* Paint "backdrop" into "area" on "scr": a flat fill in the SOLID case, + * otherwise the 8x8 pattern tiled in colour over pattern_bg, phased against + * (origin_x, origin_y). No-op when colour is wuss_NO_BACKGROUND. Caller sets + * scr->clip. */ +void wuss__fill_backdrop(screen_t *scr, + const colour_t *palette, + const wuss_backdrop_t *backdrop, + const box_t *area, + int origin_x, + int origin_y); + /* clamp "desired" to the window's scrollable range; step the current offset * by "delta" and apply it. Core (furniture-independent) -- used by the wheel * and, when built, the scrollbar furniture. */ diff --git a/libraries/wuss/redraw.c b/libraries/wuss/redraw.c index d86e3567..aa6463bf 100644 --- a/libraries/wuss/redraw.c +++ b/libraries/wuss/redraw.c @@ -34,10 +34,11 @@ static void redraw_window(wuss_t *wuss, { wuss->scr->clip = pieces[i]; - if (win->bg != wuss_NO_BACKGROUND) - screen_draw_rect(wuss->scr, - content.x0, content.y0, box_size(&content), - wuss->palette[win->bg]); + /* phase any pattern against the scroll origin so it locks to the + * content rather than crawling as the window scrolls */ + wuss__fill_backdrop(wuss->scr, wuss->palette, &win->bg, &content, + content.x0 - win->scroll.x, + content.y0 - win->scroll.y); if (win->task.handle != NULL) { @@ -105,12 +106,8 @@ result_t wuss_redraw(wuss_t *wuss) full.x1 = wuss->scr->size.w; full.y1 = wuss->scr->size.h; - if (wuss->backdrop != wuss_NO_BACKGROUND) - { - wuss->scr->clip = full; - screen_draw_rect(wuss->scr, full.x0, full.y0, box_size(&full), - wuss->palette[wuss->backdrop]); - } + wuss->scr->clip = full; + wuss__fill_backdrop(wuss->scr, wuss->palette, &wuss->backdrop, &full, 0, 0); rc = result_OK; redraw_from(wuss, wuss->z_order.next, &full, &rc); @@ -137,13 +134,9 @@ result_t wuss_redraw_dirty(wuss_t *wuss) rc = result_OK; for (i = 0; i < wuss->ndirty; i++) { - if (wuss->backdrop != wuss_NO_BACKGROUND) - { - wuss->scr->clip = wuss->dirty[i]; - screen_draw_rect(wuss->scr, - wuss->dirty[i].x0, wuss->dirty[i].y0, SIZE2D(wuss->dirty[i].x1 - wuss->dirty[i].x0, wuss->dirty[i].y1 - wuss->dirty[i].y0), - wuss->palette[wuss->backdrop]); - } + wuss->scr->clip = wuss->dirty[i]; + wuss__fill_backdrop(wuss->scr, wuss->palette, &wuss->backdrop, + &wuss->dirty[i], 0, 0); redraw_from(wuss, wuss->z_order.next, &wuss->dirty[i], &rc); } diff --git a/libraries/wuss/test/tasks/ball.c b/libraries/wuss/test/tasks/ball.c index 2ee7a424..7c77ab67 100644 --- a/libraries/wuss/test/tasks/ball.c +++ b/libraries/wuss/test/tasks/ball.c @@ -34,7 +34,7 @@ result_t ball_create(wuss_t *wuss, const colour_t *palette, ball_task_t *task) SIZE2D(200, 160), "Bouncing Ball", wuss_WINDOW_NONE, - wuss_NO_BACKGROUND, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), &delegate, SIZE2D(200, 160), SIZE2D(0, 0), diff --git a/libraries/wuss/test/tasks/blank.c b/libraries/wuss/test/tasks/blank.c index 603bec2d..b54fa4c3 100644 --- a/libraries/wuss/test/tasks/blank.c +++ b/libraries/wuss/test/tasks/blank.c @@ -31,7 +31,7 @@ result_t blank_create(wuss_t *wuss, int npalette, blank_task_t *task) SIZE2D(200, 160), NULL, wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE, - palette_PICO8_GREEN, + wuss_BACKDROP_COLOUR(palette_PICO8_GREEN), &delegate, SIZE2D(200, 160), SIZE2D(0, 0), @@ -51,7 +51,8 @@ static result_t blank_idle(void *task_data) bc->frame_count = 0; bc->index = (bc->index + 1) % bc->npalette; - rc = wuss_window_set_background(bc->window, bc->index); + rc = wuss_window_set_background(bc->window, + wuss_BACKDROP_COLOUR(bc->index)); if (rc != result_OK) logf_warning("blank_idle: wuss_window_set_background(%d) failed", bc->index); diff --git a/libraries/wuss/test/tasks/chars.c b/libraries/wuss/test/tasks/chars.c index 1680d005..9a4b155b 100644 --- a/libraries/wuss/test/tasks/chars.c +++ b/libraries/wuss/test/tasks/chars.c @@ -55,7 +55,7 @@ result_t chars_create(wuss_t *wuss, wuss_WINDOW_NO_TOGGLE_SIZE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL, - wuss_NO_BACKGROUND, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), &delegate, sz, SIZE2D(0, 0), diff --git a/libraries/wuss/test/tasks/checker.c b/libraries/wuss/test/tasks/checker.c index cecbbc48..00d89499 100644 --- a/libraries/wuss/test/tasks/checker.c +++ b/libraries/wuss/test/tasks/checker.c @@ -38,7 +38,7 @@ result_t checker_create(wuss_t *wuss, SIZE2D(160, 160), "Checker 1", wuss_WINDOW_NONE, - wuss_NO_BACKGROUND, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), &delegate, SIZE2D(160, 160), SIZE2D(0, 0), @@ -50,7 +50,7 @@ result_t checker_create(wuss_t *wuss, SIZE2D(160, 160), "Checker 2", wuss_WINDOW_NONE, - wuss_NO_BACKGROUND, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), &delegate, SIZE2D(160, 160), SIZE2D(0, 0), diff --git a/libraries/wuss/test/tasks/curve.c b/libraries/wuss/test/tasks/curve.c index 09456f0c..74248e3c 100644 --- a/libraries/wuss/test/tasks/curve.c +++ b/libraries/wuss/test/tasks/curve.c @@ -46,7 +46,7 @@ result_t curve_create(wuss_t *wuss, SIZE2D(220, 160), "Curve", wuss_WINDOW_NONE, - wuss_NO_BACKGROUND, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), &delegate, SIZE2D(220, 160), SIZE2D(0, 0), diff --git a/libraries/wuss/test/tasks/gradient.c b/libraries/wuss/test/tasks/gradient.c index 0602325c..6f2d8e63 100644 --- a/libraries/wuss/test/tasks/gradient.c +++ b/libraries/wuss/test/tasks/gradient.c @@ -43,7 +43,7 @@ result_t gradient_create(wuss_t *wuss, gradient_task_t *task) SIZE2D(GRADIENT_OPEN_WIDTH, GRADIENT_OPEN_HEIGHT), "Gradient", wuss_WINDOW_NONE, - wuss_NO_BACKGROUND, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), &delegate, SIZE2D(GRADIENT_DOC_WIDTH, GRADIENT_DOC_HEIGHT), SIZE2D(0, 0), diff --git a/libraries/wuss/test/tasks/icons.c b/libraries/wuss/test/tasks/icons.c index a9953354..2e8084ac 100644 --- a/libraries/wuss/test/tasks/icons.c +++ b/libraries/wuss/test/tasks/icons.c @@ -35,7 +35,6 @@ result_t icons_create(wuss_t *wuss, result_t rc; task->font = font; - task->ink = palette[palette_PICO8_LAVENDER]; task->label = palette[palette_PICO8_DARK_BLUE]; task->paper = palette[palette_PICO8_LIGHT_GREY]; /* the window bg, below */ task->window = NULL; @@ -49,7 +48,9 @@ result_t icons_create(wuss_t *wuss, SIZE2D(ICONS_DOC_W, 160), "Icons", wuss_WINDOW_NONE, - palette_PICO8_LIGHT_GREY, + wuss_BACKDROP_PATTERN(palette_PICO8_LAVENDER, + screen_PATTERN_CROSSHATCH, + palette_PICO8_LIGHT_GREY), &delegate, SIZE2D(ICONS_DOC_W, ICONS_DOC_H), SIZE2D(0, 0), @@ -165,16 +166,9 @@ static result_t icons_redraw(const wuss_event_t *event, void *task_data) * blit assumes. Nothing here is pinned to a window edge. Every draw is * clipped to the dirty rectangle (content) so partial redraws stay cheap. */ - /* a faint grid across the whole work area */ - for (x = icons_grid_first(bounds->x0, scroll.x, content->x0); - x < content->x1; - x += ICONS_GRID) - screen_draw_line(scr, x, content->y0, x, content->y1 - 1, tcx->ink); - - for (y = icons_grid_first(bounds->y0, scroll.y, content->y0); - y < content->y1; - y += ICONS_GRID) - screen_draw_line(scr, content->x0, y, content->x1 - 1, y, tcx->ink); + /* the faint crosshatch backdrop is now the window's own bg (a + * wuss_backdrop_t pattern fill), painted by Wuss before this event and + * phase-locked to the scroll origin -- nothing to draw here */ /* x-axis ruler: document x printed just below the y=0 line, at each * labelled grid column. Scrolls with the document like the grid. */ diff --git a/libraries/wuss/test/tasks/icons.h b/libraries/wuss/test/tasks/icons.h index aebf0ea2..9e9fb820 100644 --- a/libraries/wuss/test/tasks/icons.h +++ b/libraries/wuss/test/tasks/icons.h @@ -17,7 +17,6 @@ typedef struct icons_task { wuss_window_t *window; bmfont_t *font; - colour_t ink; /* grid lines */ colour_t label; /* axis coordinate text */ colour_t paper; /* window bg, for bmfont_draw glyph blending */ wuss_icon_t *button; /* "Press me" */ diff --git a/libraries/wuss/test/tasks/image.c b/libraries/wuss/test/tasks/image.c index 943836e1..3ef7f763 100644 --- a/libraries/wuss/test/tasks/image.c +++ b/libraries/wuss/test/tasks/image.c @@ -47,7 +47,7 @@ result_t image_create(wuss_t *wuss, SIZE2D(sz.w, sz.h * 2 / 3), "Image", wuss_WINDOW_NONE, - palette_PICO8_PINK, + wuss_BACKDROP_COLOUR(palette_PICO8_PINK), &delegate, sz, SIZE2D(32, 32), diff --git a/libraries/wuss/test/tasks/launcher.c b/libraries/wuss/test/tasks/launcher.c index d01b565e..5cf882d9 100644 --- a/libraries/wuss/test/tasks/launcher.c +++ b/libraries/wuss/test/tasks/launcher.c @@ -47,7 +47,7 @@ result_t launcher_create(wuss_t *wuss, sz, "Launcher", wuss_WINDOW_NO_CLOSE, - palette_PICO8_LIGHT_GREY, + wuss_BACKDROP_COLOUR(palette_PICO8_LIGHT_GREY), &delegate, sz, SIZE2D(0, 0), diff --git a/libraries/wuss/test/tasks/palette.c b/libraries/wuss/test/tasks/palette.c index dfcc2eee..ac7da4e9 100644 --- a/libraries/wuss/test/tasks/palette.c +++ b/libraries/wuss/test/tasks/palette.c @@ -30,7 +30,7 @@ result_t palette_create(wuss_t *wuss, SIZE2D(100, 100), "Palette", wuss_WINDOW_NO_RESIZE_BLIT, /* swatch grid is laid out across the whole window, so a resize must redraw all of it, not just the newly (un)covered edge */ - palette_PICO8_BLACK, + wuss_BACKDROP_COLOUR(palette_PICO8_BLACK), &delegate, SIZE2D(100, 100), SIZE2D(0, 0), diff --git a/libraries/wuss/test/tasks/porter-duff.c b/libraries/wuss/test/tasks/porter-duff.c index 14fc1dc4..525db42f 100644 --- a/libraries/wuss/test/tasks/porter-duff.c +++ b/libraries/wuss/test/tasks/porter-duff.c @@ -197,7 +197,7 @@ result_t porter_duff_create(wuss_t *wuss, SIZE2D(PD_SIZE, PD_SIZE + PD_LABEL_HEIGHT), "Porter-Duff", wuss_WINDOW_NONE, - wuss_NO_BACKGROUND, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), &delegate, SIZE2D(PD_SIZE, PD_SIZE + PD_LABEL_HEIGHT), SIZE2D(0, 0), diff --git a/libraries/wuss/test/tasks/sofa.c b/libraries/wuss/test/tasks/sofa.c index 28cef250..ab8d951d 100644 --- a/libraries/wuss/test/tasks/sofa.c +++ b/libraries/wuss/test/tasks/sofa.c @@ -373,7 +373,7 @@ result_t sofa_create(wuss_t *wuss, const colour_t *palette, sofa_task_t *task) SIZE2D(180, 160), "Sofa", wuss_WINDOW_NONE, - wuss_NO_BACKGROUND, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), &delegate, SIZE2D(180, 160), SIZE2D(0, 0), diff --git a/libraries/wuss/test/tasks/text.c b/libraries/wuss/test/tasks/text.c index aa094435..719b96f7 100644 --- a/libraries/wuss/test/tasks/text.c +++ b/libraries/wuss/test/tasks/text.c @@ -53,7 +53,7 @@ result_t text_create(wuss_t *wuss, sz, "Lorem Ipsum", 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 */ - palette_PICO8_BLUE, + wuss_BACKDROP_COLOUR(palette_PICO8_BLUE), &delegate, sz, SIZE2D(0, 0), diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 6017f6f4..71f81948 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -354,7 +354,9 @@ static result_t wuss_interactive_test(const char *resources) config.palette.scroll.sausages = palette_PICO8_LIGHT_GREY; config.bevel.light = palette_PICO8_WHITE; config.bevel.dark = palette_PICO8_DARK_GREY; - config.backdrop = palette_PICO8_LIGHT_GREY; + config.backdrop.colour = palette_PICO8_LIGHT_GREY; + config.backdrop.pattern = screen_PATTERN_SOLID; + config.backdrop.pattern_bg = wuss_NO_BACKGROUND; rc = wuss_create(&scr, font, palette, NELEMS(palette), &config, &wuss); if (rc != result_OK) @@ -725,7 +727,7 @@ result_t wuss_test(const char *resources) &box_a, "toosmall", wuss_WINDOW_NONE, - wuss_NO_BACKGROUND, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), NULL, box_size(&box_a), SIZE2D(0, 0), @@ -751,7 +753,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_BACK | wuss_WINDOW_NO_TOGGLE_SIZE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, - wuss_NO_BACKGROUND, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), &delegate_a, box_size(&box_a), SIZE2D(0, 0), @@ -774,7 +776,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_BACK | wuss_WINDOW_NO_TOGGLE_SIZE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, - wuss_NO_BACKGROUND, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), &delegate_b, box_size(&box_b), SIZE2D(0, 0), @@ -1060,7 +1062,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, - wuss_NO_BACKGROUND, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), &delegate_d, box_size(&box_d), SIZE2D(0, 0), @@ -1108,7 +1110,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, - wuss_NO_BACKGROUND, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), &delegate_e, box_size(&box_e), SIZE2D(0, 0), @@ -1129,7 +1131,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, - wuss_NO_BACKGROUND, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), &delegate_f, box_size(&box_f), SIZE2D(0, 0), @@ -1165,11 +1167,12 @@ result_t wuss_test(const char *resources) printf("test: wuss_window_set_background\n"); - rc = wuss_window_set_background(win_d, 999); + rc = wuss_window_set_background(win_d, wuss_BACKDROP_COLOUR(999)); if (rc != result_WUSS_BAD_COLOUR) goto Failure; - rc = wuss_window_set_background(win_d, palette_PICO8_ORANGE); + rc = wuss_window_set_background(win_d, + wuss_BACKDROP_COLOUR(palette_PICO8_ORANGE)); if (rc != result_OK) goto Failure; @@ -1197,7 +1200,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, - wuss_NO_BACKGROUND, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), &delegate_h, box_size(&box_h), SIZE2D(0, 0), @@ -1218,7 +1221,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, - wuss_NO_BACKGROUND, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), &delegate_g, box_size(&box_g), SIZE2D(0, 0), @@ -1280,7 +1283,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, - wuss_NO_BACKGROUND, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), &delegate_i, box_size(&box_i), SIZE2D(0, 0), @@ -1301,7 +1304,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, - wuss_NO_BACKGROUND, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), &delegate_j, box_size(&box_j), SIZE2D(0, 0), @@ -1352,7 +1355,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, - wuss_NO_BACKGROUND, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), &delegate_m, box_size(&box_m), SIZE2D(0, 0), @@ -1405,7 +1408,7 @@ result_t wuss_test(const char *resources) &box_h, "H", wuss_WINDOW_NONE, - wuss_NO_BACKGROUND, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), &delegate_h, box_size(&box_h), SIZE2D(0, 0), @@ -1424,7 +1427,7 @@ result_t wuss_test(const char *resources) &box_g, "G", wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL, - wuss_NO_BACKGROUND, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), &delegate_g, box_size(&box_g), SIZE2D(0, 0), @@ -1544,7 +1547,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(wuss, &box_m, "M", wuss_WINDOW_NONE, - wuss_NO_BACKGROUND, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), &delegate_m, SIZE2D(200, 200), SIZE2D(80, 60), &win_m); @@ -1595,7 +1598,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 200x200 doc */ rc = wuss_window_create(wuss, &box_t_win, "T", wuss_WINDOW_NONE, - wuss_NO_BACKGROUND, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), &delegate_t, SIZE2D(200, 200), SIZE2D(0, 0), &win_t); if (rc != result_OK) @@ -1761,7 +1764,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(wuss, &box_r, "R", wuss_WINDOW_NONE, - wuss_NO_BACKGROUND, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), &delegate_r, SIZE2D(70, 70), SIZE2D(0, 0), &win_r); if (rc != result_OK) @@ -1859,7 +1862,7 @@ result_t wuss_test(const char *resources) box_nb.x0 = 10; box_nb.y0 = 10; box_nb.x1 = 50; box_nb.y1 = 50; /* 40x40 content, room to grow to a 200x200 doc */ rc = wuss_window_create(wuss, &box_nb, "NB", wuss_WINDOW_NO_RESIZE_BLIT, - wuss_NO_BACKGROUND, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), &delegate_nb, SIZE2D(200, 200), SIZE2D(0, 0), &win_nb); if (rc != result_OK) @@ -1938,7 +1941,8 @@ 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(wuss, &box_u, "U", wuss_WINDOW_NONE, wuss_NO_BACKGROUND, /* scrollbars on: carve.x/y = icon size */ + rc = wuss_window_create(wuss, &box_u, "U", wuss_WINDOW_NONE, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), /* scrollbars on: carve.x/y = icon size */ &delegate_u, 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) @@ -2046,7 +2050,7 @@ result_t wuss_test(const char *resources) * Doc big enough that maximize is * screen-limited, not doc-limited. */ rc = wuss_window_create(wuss, &box_v, "V", wuss_WINDOW_NONE, - wuss_NO_BACKGROUND, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), &delegate_v, SIZE2D(200, 200), SIZE2D(0, 0), &win_v); if (rc != result_OK) @@ -2159,7 +2163,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_BACK | wuss_WINDOW_NO_TOGGLE_SIZE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, - wuss_NO_BACKGROUND, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), &delegate_k, box_size(&box_k), SIZE2D(0, 0), @@ -2180,7 +2184,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_BACK | wuss_WINDOW_NO_TOGGLE_SIZE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, - wuss_NO_BACKGROUND, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), &delegate_l, box_size(&box_l), SIZE2D(0, 0), @@ -2247,7 +2251,7 @@ result_t wuss_test(const char *resources) "M2", wuss_WINDOW_NO_BACK | wuss_WINDOW_NO_TOGGLE_SIZE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL, - wuss_NO_BACKGROUND, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), &delegate_m2, box_size(&box_m2), SIZE2D(0, 0), @@ -2338,7 +2342,7 @@ result_t wuss_test(const char *resources) box_nb2.x0 = 0; box_nb2.y0 = 0; box_nb2.x1 = 40; box_nb2.y1 = 40; rc = wuss_window_create(wuss, &box_nb2, "NB2", wuss_WINDOW_NO_RESIZE_BLIT, - wuss_NO_BACKGROUND, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), &delegate_nb2, box_size(&box_nb2), SIZE2D(0, 0), @@ -2390,7 +2394,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_BACK | wuss_WINDOW_NO_TOGGLE_SIZE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, - wuss_NO_BACKGROUND, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), &delegate_n, box_size(&box_n), SIZE2D(0, 0), @@ -2409,7 +2413,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_BACK | wuss_WINDOW_NO_TOGGLE_SIZE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, - wuss_NO_BACKGROUND, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), &delegate_o, box_size(&box_o), SIZE2D(0, 0), @@ -2494,7 +2498,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, - wuss_NO_BACKGROUND, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), &delegate_b, box_size(&box_b), SIZE2D(0, 0), @@ -2513,7 +2517,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, - wuss_NO_BACKGROUND, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), &delegate_a, box_size(&box_a), SIZE2D(0, 0), @@ -2597,7 +2601,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, - wuss_NO_BACKGROUND, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), &delegate_b, box_size(&box_b), SIZE2D(0, 0), @@ -2616,7 +2620,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, - wuss_NO_BACKGROUND, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), &delegate_a, box_size(&box_a), SIZE2D(0, 0), @@ -2677,7 +2681,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, - wuss_NO_BACKGROUND, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), &delegate_b, box_size(&box_b), SIZE2D(0, 0), @@ -2696,7 +2700,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, - wuss_NO_BACKGROUND, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), &delegate_a, box_size(&box_a), SIZE2D(0, 0), @@ -2790,7 +2794,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, - wuss_NO_BACKGROUND, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), &delegate_b, box_size(&box_b), SIZE2D(0, 0), @@ -2809,7 +2813,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, - wuss_NO_BACKGROUND, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), &delegate_a, box_size(&box_a), SIZE2D(0, 0), @@ -2886,7 +2890,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_BACK | wuss_WINDOW_NO_TOGGLE_SIZE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, - wuss_NO_BACKGROUND, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), &delegate_c, box_size(&box_c), SIZE2D(0, 0), @@ -2921,7 +2925,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(wuss, &box_s, "S", wuss_WINDOW_NONE, - wuss_NO_BACKGROUND, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), &delegate_s, SIZE2D(200, 200), SIZE2D(0, 0), &win_s); if (rc != result_OK) @@ -2997,7 +3001,7 @@ result_t wuss_test(const char *resources) &box_r, "rules", wuss_WINDOW_NONE, /* all furniture present */ - wuss_NO_BACKGROUND, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), &delegate_r, SIZE2D(400, 400), SIZE2D(0, 0), @@ -3042,7 +3046,7 @@ result_t wuss_test(const char *resources) SIZE2D(40, 30), "P", wuss_WINDOW_NONE, - wuss_NO_BACKGROUND, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), &delegate_p, SIZE2D(40, 30), SIZE2D(0, 0), @@ -3066,7 +3070,7 @@ result_t wuss_test(const char *resources) SIZE2D(40, 30), "P", wuss_WINDOW_NONE, - wuss_NO_BACKGROUND, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), &delegate_p, SIZE2D(40, 30), SIZE2D(0, 0), diff --git a/libraries/wuss/window/create-placed.c b/libraries/wuss/window/create-placed.c index 6088a289..22ee483a 100644 --- a/libraries/wuss/window/create-placed.c +++ b/libraries/wuss/window/create-placed.c @@ -78,7 +78,7 @@ result_t wuss_window_create_placed(wuss_t *wuss, size2d_t size, const char *title, wuss_window_flags_t flags, - wuss_colour_t bg, + wuss_backdrop_t bg, const wuss_task_t *task, size2d_t doc, size2d_t min_doc, diff --git a/libraries/wuss/window/create.c b/libraries/wuss/window/create.c index 11bb445b..a7380175 100644 --- a/libraries/wuss/window/create.c +++ b/libraries/wuss/window/create.c @@ -14,7 +14,7 @@ result_t wuss_window_create(wuss_t *wuss, const box_t *content, const char *title, wuss_window_flags_t flags, - wuss_colour_t bg, + wuss_backdrop_t bg, const wuss_task_t *task, size2d_t doc, size2d_t min_doc, @@ -94,7 +94,7 @@ result_t wuss_window_create(wuss_t *wuss, else memset(&win->task, 0, sizeof(win->task)); - if (bg != wuss_NO_BACKGROUND && (bg < 0 || bg >= wuss->npalette)) + if (wuss__validate_backdrop(wuss, &bg) != result_OK) { free(win); return result_WUSS_BAD_COLOUR; diff --git a/libraries/wuss/window/set-background.c b/libraries/wuss/window/set-background.c index 8cfe7cb0..bd704c4a 100644 --- a/libraries/wuss/window/set-background.c +++ b/libraries/wuss/window/set-background.c @@ -2,11 +2,11 @@ #include "../impl.h" -result_t wuss_window_set_background(wuss_window_t *window, wuss_colour_t bg) +result_t wuss_window_set_background(wuss_window_t *window, wuss_backdrop_t bg) { box_t content; - if (bg != wuss_NO_BACKGROUND && (bg < 0 || bg >= window->wuss->npalette)) + if (wuss__validate_backdrop(window->wuss, &bg) != result_OK) return result_WUSS_BAD_COLOUR; window->bg = bg; From a4184246b7b82cad706be1bb4c6abba8cedaf567 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 30 Aug 2026 23:12:42 +0100 Subject: [PATCH 095/287] fix(wuss): keep a 2px gap at each end of the scrollbar well The sausage could butt right up against the arrow buttons at the scroll extremes. Scale and position it over the well minus a cosmetic end gap at each end, dropped when the well is too short to spare it. drag_sausage maps pointer movement over the same track so dragging stays 1:1. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/furniture.h | 5 +++-- libraries/wuss/furniture/hscroll-box.c | 22 ++++++++++++++-------- libraries/wuss/furniture/scroll-action.c | 11 ++++++++--- libraries/wuss/furniture/vscroll-box.c | 22 ++++++++++++++-------- 4 files changed, 39 insertions(+), 21 deletions(-) diff --git a/libraries/wuss/furniture.h b/libraries/wuss/furniture.h index 4b5b7bd8..1a90f180 100644 --- a/libraries/wuss/furniture.h +++ b/libraries/wuss/furniture.h @@ -8,8 +8,9 @@ #include "wuss/wuss.h" -#define WUSS_MIN_SAUSAGE 6 /* scrollbar sausage never shrinks below this, however small the content fraction */ -#define WUSS_SCROLL_STEP 20 /* pixels stepped per scrollbar arrow click */ +#define WUSS_MIN_SAUSAGE 6 /* scrollbar sausage never shrinks below this, however small the content fraction */ +#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 */ /* 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/hscroll-box.c b/libraries/wuss/furniture/hscroll-box.c index 31bbdc9b..ce452db0 100644 --- a/libraries/wuss/furniture/hscroll-box.c +++ b/libraries/wuss/furniture/hscroll-box.c @@ -70,27 +70,33 @@ int wuss__hscroll_well_px(const wuss_window_t *window) void wuss__hscroll_sausage_box(const wuss_window_t *window, box_t *out) { box_t well, content; - int well_px, content_size, doc_size, sausage_px, sausage_x0, inset; + int well_px, track_px, end_gap, content_size, doc_size, sausage_px, sausage_x0, inset; wuss__hscroll_well_box(window, &well); wuss__content_box(window, &content); - well_px = well.x1 - well.x0; + well_px = well.x1 - well.x0; + + /* Leave a cosmetic gap at each end of the well; drop it if the well is + * too short to spare two gaps plus a minimum sausage. */ + end_gap = (well_px > 2 * WUSS_SCROLL_END_GAP + WUSS_MIN_SAUSAGE) ? WUSS_SCROLL_END_GAP : 0; + track_px = well_px - 2 * end_gap; + content_size = content.x1 - content.x0; doc_size = window->doc.w; if (doc_size < content_size) doc_size = content_size; - sausage_px = (doc_size > 0) ? well_px * content_size / doc_size : well_px; + sausage_px = (doc_size > 0) ? track_px * content_size / doc_size : track_px; if (sausage_px < WUSS_MIN_SAUSAGE) sausage_px = WUSS_MIN_SAUSAGE; - if (sausage_px > well_px) - sausage_px = well_px; + if (sausage_px > track_px) + sausage_px = track_px; - if (doc_size > content_size && well_px > sausage_px) - sausage_x0 = well.x0 + (well_px - sausage_px) * window->scroll.x / (doc_size - content_size); + if (doc_size > content_size && track_px > sausage_px) + sausage_x0 = well.x0 + end_gap + (track_px - sausage_px) * window->scroll.x / (doc_size - content_size); else - sausage_x0 = well.x0; + sausage_x0 = well.x0 + end_gap; /* Inset the sausage from the well's long edges, but only while that * leaves something to draw: a tiny titlebar font can make the well diff --git a/libraries/wuss/furniture/scroll-action.c b/libraries/wuss/furniture/scroll-action.c index 6ac7a498..a8965eb3 100644 --- a/libraries/wuss/furniture/scroll-action.c +++ b/libraries/wuss/furniture/scroll-action.c @@ -8,7 +8,7 @@ void wuss__furniture_drag_sausage(wuss_window_t *window, int horizontal) { box_t content; - int well_px, content_size, doc_size, max_scroll, new_scroll; + int well_px, track_px, end_gap, content_size, doc_size, max_scroll, new_scroll; wuss__content_box(window, &content); @@ -25,14 +25,19 @@ void wuss__furniture_drag_sausage(wuss_window_t *window, doc_size = window->doc.h; } + /* Sausage travels over the well minus the cosmetic end gaps; must match + * wuss__*scroll_sausage_box so drag tracks the pointer 1:1. */ + end_gap = (well_px > 2 * WUSS_SCROLL_END_GAP + WUSS_MIN_SAUSAGE) ? WUSS_SCROLL_END_GAP : 0; + track_px = well_px - 2 * end_gap; + max_scroll = doc_size - content_size; if (max_scroll < 0) max_scroll = 0; - if (well_px <= 0 || doc_size <= content_size) + if (track_px <= 0 || doc_size <= content_size) new_scroll = 0; else - new_scroll = scroll_start + delta_px * doc_size / well_px; + new_scroll = scroll_start + delta_px * doc_size / track_px; if (new_scroll < 0) new_scroll = 0; diff --git a/libraries/wuss/furniture/vscroll-box.c b/libraries/wuss/furniture/vscroll-box.c index ba873db6..f87eda4b 100644 --- a/libraries/wuss/furniture/vscroll-box.c +++ b/libraries/wuss/furniture/vscroll-box.c @@ -72,27 +72,33 @@ int wuss__vscroll_well_px(const wuss_window_t *window) void wuss__vscroll_sausage_box(const wuss_window_t *window, box_t *out) { box_t well, content; - int well_px, content_size, doc_size, sausage_px, sausage_y0, inset; + int well_px, track_px, end_gap, content_size, doc_size, sausage_px, sausage_y0, inset; wuss__vscroll_well_box(window, &well); wuss__content_box(window, &content); - well_px = well.y1 - well.y0; + well_px = well.y1 - well.y0; + + /* Leave a cosmetic gap at each end of the well; drop it if the well is + * too short to spare two gaps plus a minimum sausage. */ + end_gap = (well_px > 2 * WUSS_SCROLL_END_GAP + WUSS_MIN_SAUSAGE) ? WUSS_SCROLL_END_GAP : 0; + track_px = well_px - 2 * end_gap; + content_size = content.y1 - content.y0; doc_size = window->doc.h; if (doc_size < content_size) doc_size = content_size; - sausage_px = (doc_size > 0) ? well_px * content_size / doc_size : well_px; + sausage_px = (doc_size > 0) ? track_px * content_size / doc_size : track_px; if (sausage_px < WUSS_MIN_SAUSAGE) sausage_px = WUSS_MIN_SAUSAGE; - if (sausage_px > well_px) - sausage_px = well_px; + if (sausage_px > track_px) + sausage_px = track_px; - if (doc_size > content_size && well_px > sausage_px) - sausage_y0 = well.y0 + (well_px - sausage_px) * window->scroll.y / (doc_size - content_size); + if (doc_size > content_size && track_px > sausage_px) + sausage_y0 = well.y0 + end_gap + (track_px - sausage_px) * window->scroll.y / (doc_size - content_size); else - sausage_y0 = well.y0; + sausage_y0 = well.y0 + end_gap; /* Inset the sausage from the well's long edges, but only while that * leaves something to draw: a tiny titlebar font can make the well From 28023e540cb4062d2cf8ac59b64917625c946ac2 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 30 Aug 2026 23:39:41 +0100 Subject: [PATCH 096/287] refactor(wuss): drop PICO-8 dependency from the NULL-palette default path wuss_create no longer includes framebuf/palettes.h or assumes a palette length. A NULL palette now falls back to a built-in two-entry black/white palette instead of define_pico8_palette, and the furniture default colours use the generic bg=0, fg=(npalette>1)?1:0 scheme unconditionally. The two alloc/copy branches are merged. wuss-test: set_background test indexed palette_PICO8_ORANGE against the default palette; use index 1, which is valid for the two-entry fallback. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/create.c | 60 ++++++++++++++------------------- libraries/wuss/test/wuss-test.c | 3 +- 2 files changed, 26 insertions(+), 37 deletions(-) diff --git a/libraries/wuss/create.c b/libraries/wuss/create.c index 3553ef48..767f415f 100644 --- a/libraries/wuss/create.c +++ b/libraries/wuss/create.c @@ -9,10 +9,17 @@ #endif #include "base/utils.h" -#include "framebuf/palettes.h" #include "impl.h" +/* Built-in fallback palette when the caller passes NULL: black and white. + * wuss makes no other assumptions about palette contents or length. */ +static const colour_t wuss__default_palette[] = +{ + { 0xFF000000 }, /* 0: black */ + { 0xFFFFFFFF } /* 1: white */ +}; + #if defined(WUSS_FURNITURE) || defined(WUSS_ICONS) /* Range-check the bevel colours and the backdrop against the palette. Shared * by the furniture and icons-only paths so the accepted range, the error code @@ -57,34 +64,25 @@ result_t wuss_create(screen_t *scr, if (w == NULL) return result_OOM; - if (palette != NULL) + if (palette == NULL) { - if (npalette <= 0) - { - free(w); - return result_BAD_ARG; - } - - w->palette = malloc(npalette * sizeof(*w->palette)); - if (w->palette == NULL) - { - free(w); - return result_OOM; - } - memcpy(w->palette, palette, npalette * sizeof(*w->palette)); - w->npalette = npalette; + palette = wuss__default_palette; + npalette = NELEMS(wuss__default_palette); } - else + else if (npalette <= 0) { - w->palette = malloc(palette_PICO8__LENGTH * sizeof(*w->palette)); - if (w->palette == NULL) - { - free(w); - return result_OOM; - } - define_pico8_palette(w->palette); - w->npalette = palette_PICO8__LENGTH; + free(w); + return result_BAD_ARG; + } + + w->palette = malloc(npalette * sizeof(*w->palette)); + if (w->palette == NULL) + { + free(w); + return result_OOM; } + memcpy(w->palette, palette, npalette * sizeof(*w->palette)); + w->npalette = npalette; if (config != NULL) { @@ -106,16 +104,8 @@ result_t wuss_create(screen_t *scr, } else { - if (palette == NULL) - { - bg = palette_PICO8_DARK_BLUE; - fg = palette_PICO8_WHITE; - } - else - { - bg = 0; - fg = (w->npalette > 1) ? 1 : 0; - } + bg = 0; + fg = (w->npalette > 1) ? 1 : 0; pal.title.bg = bg; pal.title.fg = fg; diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 71f81948..95d846c3 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -1171,8 +1171,7 @@ result_t wuss_test(const char *resources) if (rc != result_WUSS_BAD_COLOUR) goto Failure; - rc = wuss_window_set_background(win_d, - wuss_BACKDROP_COLOUR(palette_PICO8_ORANGE)); + rc = wuss_window_set_background(win_d, wuss_BACKDROP_COLOUR(1)); if (rc != result_OK) goto Failure; From cbc02d0457ea14278aef2c0ccb496e0291353930 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 30 Aug 2026 23:41:46 +0100 Subject: [PATCH 097/287] feat(palettes): add define_wimp16_palette and a WUSS_PALETTE selector Adds the RISC OS desktop (Wimp) 16-colour palette in native Wimp index order, with palette_WIMP16_* names and a palette_WIMP16__LENGTH. The interactive wuss test picks it up via WUSS_PALETTE=wimp16; the default stays PICO-8. Its desktop backdrop now uses a DOTS pattern over light grey instead of a flat fill. Co-Authored-By: Claude Sonnet 5 --- include/framebuf/palettes.h | 28 ++++++++++++++++++++++++++ libraries/framebuf/palettes/palettes.c | 22 ++++++++++++++++++++ libraries/wuss/test/wuss-test.c | 15 ++++++++++---- 3 files changed, 61 insertions(+), 4 deletions(-) diff --git a/include/framebuf/palettes.h b/include/framebuf/palettes.h index 3beabdc0..2829810b 100644 --- a/include/framebuf/palettes.h +++ b/include/framebuf/palettes.h @@ -25,6 +25,26 @@ #define palette_PICO8_LIGHT_PEACH (15) #define palette_PICO8__LENGTH (16) +/* RISC OS desktop (Wimp) 16-colour palette, in native Wimp index order */ + +#define palette_WIMP16_WHITE (0) +#define palette_WIMP16_GREY_87 (1) +#define palette_WIMP16_GREY_75 (2) +#define palette_WIMP16_GREY_62 (3) +#define palette_WIMP16_GREY_50 (4) +#define palette_WIMP16_GREY_37 (5) +#define palette_WIMP16_GREY_25 (6) +#define palette_WIMP16_BLACK (7) +#define palette_WIMP16_DARK_BLUE (8) +#define palette_WIMP16_YELLOW (9) +#define palette_WIMP16_GREEN (10) +#define palette_WIMP16_RED (11) +#define palette_WIMP16_CREAM (12) +#define palette_WIMP16_DARK_GREEN (13) +#define palette_WIMP16_ORANGE (14) +#define palette_WIMP16_LIGHT_BLUE (15) +#define palette_WIMP16__LENGTH (16) + /** * Define the PICO-8 palette. * @@ -32,4 +52,12 @@ */ void define_pico8_palette(colour_t palette[palette_PICO8__LENGTH]); +/** + * Define the RISC OS 16-colour desktop (Wimp) palette in native Wimp index + * order, addressed by the `palette_WIMP16_*` names. + * + * \param[out] palette RISC OS 16-colour palette. + */ +void define_wimp16_palette(colour_t palette[palette_WIMP16__LENGTH]); + #endif /* PALETTES_H */ diff --git a/libraries/framebuf/palettes/palettes.c b/libraries/framebuf/palettes/palettes.c index 46816b45..2f1b79b2 100644 --- a/libraries/framebuf/palettes/palettes.c +++ b/libraries/framebuf/palettes/palettes.c @@ -23,3 +23,25 @@ void define_pico8_palette(colour_t palette[palette_PICO8__LENGTH]) palette[palette_PICO8_PINK ] = colour_rgb(0xFF, 0x77, 0xA8); palette[palette_PICO8_LIGHT_PEACH] = colour_rgb(0xFF, 0xCC, 0xAA); } + +/* The standard RISC OS desktop (Wimp) 16-colour palette in native Wimp index + * order: a greyscale ramp (0-7) then eight colours. */ +void define_wimp16_palette(colour_t palette[palette_WIMP16__LENGTH]) +{ + palette[palette_WIMP16_WHITE ] = colour_rgb(0xFF, 0xFF, 0xFF); + palette[palette_WIMP16_GREY_87 ] = colour_rgb(0xDD, 0xDD, 0xDD); + palette[palette_WIMP16_GREY_75 ] = colour_rgb(0xBB, 0xBB, 0xBB); + palette[palette_WIMP16_GREY_62 ] = colour_rgb(0x99, 0x99, 0x99); + palette[palette_WIMP16_GREY_50 ] = colour_rgb(0x77, 0x77, 0x77); + palette[palette_WIMP16_GREY_37 ] = colour_rgb(0x55, 0x55, 0x55); + palette[palette_WIMP16_GREY_25 ] = colour_rgb(0x33, 0x33, 0x33); + palette[palette_WIMP16_BLACK ] = colour_rgb(0x00, 0x00, 0x00); + palette[palette_WIMP16_DARK_BLUE ] = colour_rgb(0x00, 0x44, 0x99); + palette[palette_WIMP16_YELLOW ] = colour_rgb(0xEE, 0xEE, 0x00); + palette[palette_WIMP16_GREEN ] = colour_rgb(0x00, 0xCC, 0x00); + palette[palette_WIMP16_RED ] = colour_rgb(0xDD, 0x00, 0x00); + palette[palette_WIMP16_CREAM ] = colour_rgb(0xEE, 0xEE, 0xBB); + palette[palette_WIMP16_DARK_GREEN] = colour_rgb(0x55, 0x88, 0x00); + palette[palette_WIMP16_ORANGE ] = colour_rgb(0xFF, 0xBB, 0x00); + palette[palette_WIMP16_LIGHT_BLUE] = colour_rgb(0x00, 0xBB, 0xFF); +} diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 95d846c3..9fb03dd6 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -277,7 +277,14 @@ static result_t wuss_interactive_test(const char *resources) bool pixel_stress_pending; int i; - define_pico8_palette(palette); + { + const char *palette_name = getenv("WUSS_PALETTE"); /* "riscos16" selects the RISC OS 16-colour palette; default is PICO-8 */ + + if (palette_name != NULL && strcmp(palette_name, "wimp16") == 0) + define_wimp16_palette(palette); + else + define_pico8_palette(palette); + } leafname = path_join_leafname("digits", "png"); filename = path_join_filename(resources, 3, "resources", "bmfonts", leafname); @@ -354,9 +361,9 @@ static result_t wuss_interactive_test(const char *resources) config.palette.scroll.sausages = palette_PICO8_LIGHT_GREY; config.bevel.light = palette_PICO8_WHITE; config.bevel.dark = palette_PICO8_DARK_GREY; - config.backdrop.colour = palette_PICO8_LIGHT_GREY; - config.backdrop.pattern = screen_PATTERN_SOLID; - config.backdrop.pattern_bg = wuss_NO_BACKGROUND; + config.backdrop.colour = palette_PICO8_WHITE; + config.backdrop.pattern = screen_PATTERN_DOTS; + config.backdrop.pattern_bg = palette_PICO8_LIGHT_GREY; rc = wuss_create(&scr, font, palette, NELEMS(palette), &config, &wuss); if (rc != result_OK) From 963f19e3f2d4df67e28684a41fb1c4bc7dd2f6cc Mon Sep 17 00:00:00 2001 From: David Thomas Date: Mon, 31 Aug 2026 00:17:37 +0100 Subject: [PATCH 098/287] fix(wuss): MENU on furniture should have no effect The title/close and resize/scrollbar-well drag-start branches were gated only on MOUSE_DOWN, so a middle-button (MENU) press on furniture would start a window move or resize drag. Gate them on SELECT|ADJUST as the other furniture branches already are. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/mouse-click.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/libraries/wuss/mouse-click.c b/libraries/wuss/mouse-click.c index 8028deb7..fa4ffa09 100644 --- a/libraries/wuss/mouse-click.c +++ b/libraries/wuss/mouse-click.c @@ -124,7 +124,8 @@ result_t wuss_mouse_click(wuss_t *wuss, if (region == wuss_FURNITURE_CLOSE || region == wuss_FURNITURE_TITLE) { - if (action == wuss_MOUSE_DOWN) + if (action == wuss_MOUSE_DOWN && + (button & (wuss_BUTTON_SELECT | wuss_BUTTON_ADJUST))) { box_t content; @@ -144,7 +145,8 @@ result_t wuss_mouse_click(wuss_t *wuss, region == wuss_FURNITURE_VSCROLL_WELL || region == wuss_FURNITURE_HSCROLL_WELL) { - if (action == wuss_MOUSE_DOWN) + if (action == wuss_MOUSE_DOWN && + (button & (wuss_BUTTON_SELECT | wuss_BUTTON_ADJUST))) { point_t scroll; From f971b8760290432d5042ad1ab4241da29cfe00ed Mon Sep 17 00:00:00 2001 From: David Thomas Date: Mon, 31 Aug 2026 01:08:36 +0100 Subject: [PATCH 099/287] fix(wuss): Fix missing text task content when scrolled horizontally --- libraries/wuss/test/tasks/text.c | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/libraries/wuss/test/tasks/text.c b/libraries/wuss/test/tasks/text.c index 719b96f7..028dd2f3 100644 --- a/libraries/wuss/test/tasks/text.c +++ b/libraries/wuss/test/tasks/text.c @@ -67,15 +67,16 @@ static result_t text_redraw(const wuss_event_t *event, void *task_data) text_task_t *tcx; screen_t *scr; const box_t *bounds; - int font_width, font_height, sy; + int font_width, font_height, sx, sy; const char *string; int stringlen; - point_t pos; + point_t pos0, pos1; tcx = task_data; scr = event->data.redraw.scr; bounds = event->data.redraw.bounds; + sx = event->data.redraw.scroll.x; sy = event->data.redraw.scroll.y; bmfont_get_info(tcx->font, &font_width, &font_height); @@ -83,15 +84,23 @@ static result_t text_redraw(const wuss_event_t *event, void *task_data) string = paragraph; stringlen = (int) strlen(paragraph); - pos.x = bounds->x0 + 4; - pos.y = bounds->y0 - sy + 4; +#define INSET 4 +#define LEADING 2 + + pos0.x = bounds->x0 - sx + INSET; + pos0.y = bounds->y0 - sy + INSET; + pos1.x = bounds->x1 - sx - INSET; + pos1.y = bounds->y1 - sy - INSET; - while (stringlen > 0 && pos.y + font_height <= bounds->y1) + while (stringlen) { - int absolute_break, friendly_break; + int target_width; + int absolute_break; bmfont_width_t width; + int friendly_break; - bmfont_measure(tcx->font, string, stringlen, bounds->x1 - 4 - pos.x, &absolute_break, &width); + target_width = pos1.x - pos0.x; + bmfont_measure(tcx->font, string, stringlen, target_width, &absolute_break, &width); friendly_break = absolute_break; if (absolute_break < stringlen) @@ -104,7 +113,7 @@ static result_t text_redraw(const wuss_event_t *event, void *task_data) friendly_break = absolute_break; /* no space to break at: hard break */ } - bmfont_draw(tcx->font, scr, string, friendly_break, tcx->fg, tcx->bg, &pos, NULL); + bmfont_draw(tcx->font, scr, string, friendly_break, tcx->fg, tcx->bg, &pos0, NULL); string += friendly_break; stringlen -= friendly_break; @@ -114,8 +123,7 @@ static result_t text_redraw(const wuss_event_t *event, void *task_data) stringlen--; } - pos.x = bounds->x0 + 4; - pos.y += font_height + 2; + pos0.y += font_height + LEADING; } return result_OK; From be48120239ae4377da8bea15098ecdd97e12886b Mon Sep 17 00:00:00 2001 From: David Thomas Date: Mon, 31 Aug 2026 10:27:36 +0100 Subject: [PATCH 100/287] docs(wuss): explain how redraw coordinate spaces relate Add a subsection under Scrolling with an ASCII nested-box diagram of screen origin / visible / content box / clip piece / scroll / doc extent, the fields each maps to, and the screen<->document conversion formulae. Co-Authored-By: Claude Sonnet 5 --- docs/windowing/wuss.md | 52 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/docs/windowing/wuss.md b/docs/windowing/wuss.md index a90913df..0ff6a7b5 100644 --- a/docs/windowing/wuss.md +++ b/docs/windowing/wuss.md @@ -127,6 +127,58 @@ Each window carries a scroll offset, `(0, 0)` by default: the point in the task' - window-local `x`/`y` delivered in mouse/scroll events (and expected in `wuss_window_invalidate`'s `local_box`) are in virtual content space, i.e. already shifted by the scroll offset. - a redraw event's `content` is still the on-screen (unscrolled) content box; a task reads the offset itself via `wuss_window_get_scroll` to work out which part of its content to paint there. +### How the coordinate spaces relate + +Everything a redraw callback gets is *screen space* except `scroll`, which is a +*virtual content space* offset. + +``` +screen (0,0) --- wuss->scr, origin top-left + +-------------------------------------------------+ + | win->visible (whole on-screen footprint) | + | +-----------------------------------------+ | + | | titlebar + 1px outline (carved off) | | + | +--------------------------------+--------+ | wuss__content_box() = + | | content box = redraw.bounds | vscroll| | visible + | | (bounds.x0,y0) . | (carve)| | - outline + | | . . +-------------+ .| | | - titlebar + | | . | redraw. | this .| | | - furniture carve + | | . | content | piece .| | | + | | . | (one clip | only .| | | + | | . | piece) | .| | | + | | . +-------------+ .| | | + | | . . . . . . . . . . . . . . .| | | + | +--------------------------------+--------+ | + | | hscroll (carve) | | + | +-----------------------------------------+ | + +-------------------------------------------------+ + +virtual content space: size win->doc, its own origin (0,0). + win->scroll = which point of doc sits at bounds.x0,y0. + doc extends past the content box (right + bottom); that + overhang is exactly what scroll ranges over. +``` + +- `win->visible` — window's whole on-screen box (screen space); set by Wuss at create/move/resize/toggle. `wuss_window_get_visible_bounds`. +- content box / `redraw.bounds` — `visible` with outline, titlebar and any scrollbar/resize carve removed (`wuss__content_box`). `wuss_window_get_content_bounds`. +- `clipped` (internal) — `bounds` intersected with the dirty rect, before subtracting windows above. +- `redraw.content` — `clipped` minus whatever higher windows cover, split into non-overlapping pieces; the callback fires once per piece with `scr->clip` already set to it. Touch only pixels inside it. +- `win->scroll` / `redraw.scroll` — document-space offset; the task sets it via `wuss_window_set_scroll`, Wuss clamps it to `[0, doc - viewport]`. +- `win->doc` — virtual content extent, fixed at window create; drives the scrollbar sausage size and the scroll clamp. + +Conversions (as used in `mouse-move.c`, `scroll.c`, `scroll-step.c`): + +``` +screen -> document: doc.x = screen.x - bounds.x0 + scroll.x (same for y) +document -> screen: screen.x = bounds.x0 - scroll.x + doc.x +scroll clamp: max = doc - (bounds.x1 - bounds.x0); clamp(scroll, 0, max) +``` + +In a redraw callback: start drawing at `bounds.x0 - scroll.x`, +`bounds.y0 - scroll.y`, then paint the whole content normally — the framebuffer +clip (`scr->clip` = `redraw.content`) discards anything outside the piece. See +`libraries/wuss/test/tasks/text.c`. + ## Redrawing - `wuss_redraw` repaints every window, back-to-front, unconditionally, having first painted the configured backdrop colour (see Setup) behind them, if any. From 7d4644ee18f6a33b958fe12289fd29a34f306f9c Mon Sep 17 00:00:00 2001 From: David Thomas Date: Mon, 31 Aug 2026 11:20:58 +0100 Subject: [PATCH 101/287] refactor(wuss): split the text task's layout and rendering text_redraw did line-breaking and drawing in one loop mixed with screen and scroll state. Pull the wrapping into a pure text_layout() that returns an array of spans, and the drawing into text_render(); text_redraw is now just glue. No behaviour change. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/test/tasks/text.c | 115 ++++++++++++++++++++++--------- 1 file changed, 82 insertions(+), 33 deletions(-) diff --git a/libraries/wuss/test/tasks/text.c b/libraries/wuss/test/tasks/text.c index 028dd2f3..48350352 100644 --- a/libraries/wuss/test/tasks/text.c +++ b/libraries/wuss/test/tasks/text.c @@ -62,45 +62,38 @@ result_t text_create(wuss_t *wuss, return rc; } -static result_t text_redraw(const wuss_event_t *event, void *task_data) -{ - text_task_t *tcx; - screen_t *scr; - const box_t *bounds; - int font_width, font_height, sx, sy; - const char *string; - int stringlen; - point_t pos0, pos1; - - tcx = task_data; - - scr = event->data.redraw.scr; - bounds = event->data.redraw.bounds; - sx = event->data.redraw.scroll.x; - sy = event->data.redraw.scroll.y; - - bmfont_get_info(tcx->font, &font_width, &font_height); +#define INSET 4 +#define LEADING 2 +#define MAX_LINES 64 /* paragraph is short and fixed; overflow is dropped */ - string = paragraph; - stringlen = (int) strlen(paragraph); +typedef struct text_line +{ + const char *str; + int len; +} +text_line_t; + +/* Break "string" into lines that each fit "wrap_width" pixels in "font", + * writing up to "max" of them to "lines". Returns the line count. Pure + * layout: no drawing, no screen or scroll state. */ +static int text_layout(bmfont_t *font, + const char *string, + int stringlen, + int wrap_width, + text_line_t *lines, + int max) +{ + int nlines; -#define INSET 4 -#define LEADING 2 - - pos0.x = bounds->x0 - sx + INSET; - pos0.y = bounds->y0 - sy + INSET; - pos1.x = bounds->x1 - sx - INSET; - pos1.y = bounds->y1 - sy - INSET; + nlines = 0; - while (stringlen) + while (stringlen > 0 && nlines < max) { - int target_width; int absolute_break; bmfont_width_t width; int friendly_break; - target_width = pos1.x - pos0.x; - bmfont_measure(tcx->font, string, stringlen, target_width, &absolute_break, &width); + bmfont_measure(font, string, stringlen, wrap_width, &absolute_break, &width); friendly_break = absolute_break; if (absolute_break < stringlen) @@ -113,7 +106,9 @@ static result_t text_redraw(const wuss_event_t *event, void *task_data) friendly_break = absolute_break; /* no space to break at: hard break */ } - bmfont_draw(tcx->font, scr, string, friendly_break, tcx->fg, tcx->bg, &pos0, NULL); + lines[nlines].str = string; + lines[nlines].len = friendly_break; + nlines++; string += friendly_break; stringlen -= friendly_break; @@ -122,9 +117,63 @@ static result_t text_redraw(const wuss_event_t *event, void *task_data) string++; stringlen--; } + } + + return nlines; +} + +/* Draw pre-laid-out "lines" stacked from "origin", advancing by the font + * height plus LEADING per line. */ +static void text_render(bmfont_t *font, + screen_t *scr, + const text_line_t *lines, + int nlines, + colour_t fg, + colour_t bg, + point_t origin) +{ + int font_width, font_height; + point_t pos; + int i; + + bmfont_get_info(font, &font_width, &font_height); - pos0.y += font_height + LEADING; + pos = origin; + for (i = 0; i < nlines; i++) + { + bmfont_draw(font, scr, lines[i].str, lines[i].len, fg, bg, &pos, NULL); + pos.y += font_height + LEADING; } +} + +static result_t text_redraw(const wuss_event_t *event, void *task_data) +{ + text_task_t *tcx; + screen_t *scr; + const box_t *bounds; + int sx, sy; + text_line_t lines[MAX_LINES]; + int nlines; + point_t origin; + + tcx = task_data; + + scr = event->data.redraw.scr; + bounds = event->data.redraw.bounds; + sx = event->data.redraw.scroll.x; + sy = event->data.redraw.scroll.y; + + nlines = text_layout(tcx->font, + paragraph, + (int) strlen(paragraph), + (bounds->x1 - INSET) - (bounds->x0 + INSET), + lines, + MAX_LINES); + + origin.x = bounds->x0 - sx + INSET; + origin.y = bounds->y0 - sy + INSET; + + text_render(tcx->font, scr, lines, nlines, tcx->fg, tcx->bg, origin); return result_OK; } From da29ca7e9895f7e21f76ec469d3130c6ffe44031 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Mon, 31 Aug 2026 11:39:14 +0100 Subject: [PATCH 102/287] feat(bmtext): factor pixel-wrapped text layout into a library New text/bmtext module: bmtext_layout() breaks a string into lines that fit a pixel width in a bmfont (via bmfont_measure, so proportional fonts wrap correctly), bmtext_draw() draws pre-laid-out lines stacked. The wuss text-task test now calls these instead of carrying its own copy. Co-Authored-By: Claude Sonnet 5 --- CMakeLists.txt | 3 + include/text/bmtext.h | 89 +++++++++++++++++++++++++ libraries/text/bmtext/draw.c | 32 +++++++++ libraries/text/bmtext/layout.c | 53 +++++++++++++++ libraries/wuss/test/tasks/text.c | 110 +++++-------------------------- 5 files changed, 192 insertions(+), 95 deletions(-) create mode 100644 include/text/bmtext.h create mode 100644 libraries/text/bmtext/draw.c create mode 100644 libraries/text/bmtext/layout.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 1df7a23a..69c7b6ab 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -105,6 +105,7 @@ set(PUBLIC_HEADERS include/io/stream.h include/test/all-tests.h include/test/txtscr.h + include/text/bmtext.h include/text/txtfmt.h include/utils/array.h include/utils/barith.h @@ -285,6 +286,8 @@ set(TESTLIB_SOURCES libraries/test/txtscr/txtscr.c) set(TEXT_SOURCES + libraries/text/bmtext/draw.c + libraries/text/bmtext/layout.c libraries/text/txtfmt/create.c libraries/text/txtfmt/destroy.c libraries/text/txtfmt/get-length.c diff --git a/include/text/bmtext.h b/include/text/bmtext.h new file mode 100644 index 00000000..f3b38113 --- /dev/null +++ b/include/text/bmtext.h @@ -0,0 +1,89 @@ +/* bmtext.h -- word-wrap and draw a paragraph in a bitmap font */ + +/** + * \file bmtext.h + * + * Splits a string into lines that each fit a given pixel width when drawn in a + * \ref bmfont_t, then draws those lines stacked. + * + * Unlike \ref txtfmt (which wraps at character counts, for monospaced text) + * this measures each candidate line with \ref bmfont_measure, so it wraps + * proportional fonts correctly. + * + * - Breaks at the last space that still fits; hard-breaks a word with no space + * in it. - Runs of whitespace at a break are swallowed. - Layout is pure: it + * touches no screen or scroll state. + */ + +#ifndef DPTLIB_BMTEXT_H +#define DPTLIB_BMTEXT_H + +#ifdef __cplusplus +extern "C" +{ +#endif + +#include "base/result.h" +#include "framebuf/bmfont.h" +#include "framebuf/screen.h" +#include "geom/point.h" + +/** + * One laid-out line: a pointer into the caller's string plus a length. Not + * NUL-terminated. + */ +typedef struct bmtext_line +{ + const char *str; + int len; +} +bmtext_line_t; + +/** + * Break \p string into lines that each fit \p wrap_width pixels in \p font. + * + * The line pointers refer into \p string itself, so it must outlive \p lines. + * + * \param[in] font Bitmap font the lines will be drawn in. + * \param[in] string Text to wrap. + * \param[in] stringlen Length of \p string in bytes. + * \param[in] wrap_width Width to wrap to, in pixels. + * \param[out] lines Filled with up to \p max lines. + * \param[in] max Capacity of \p lines. Lines past this are dropped. + * + * \return Number of lines written to \p lines. + */ +int bmtext_layout(bmfont_t *font, + const char *string, + int stringlen, + int wrap_width, + bmtext_line_t *lines, + int max); + +/** + * Draw \p nlines pre-laid-out \p lines stacked downward from \p origin, + * advancing by the font height plus \p leading pixels per line. + * + * \param[in] font Bitmap font to draw in. + * \param[in] scr Screen to draw on. + * \param[in] lines Lines from \ref bmtext_layout. + * \param[in] nlines Number of lines. + * \param[in] fg Foreground colour. + * \param[in] bg Background colour, for glyph blending. + * \param[in] leading Extra pixels between lines. + * \param[in] origin Top-left of the first line, in pixels. + */ +void bmtext_draw(bmfont_t *font, + screen_t *scr, + const bmtext_line_t *lines, + int nlines, + colour_t fg, + colour_t bg, + int leading, + point_t origin); + +#ifdef __cplusplus +} +#endif + +#endif /* DPTLIB_BMTEXT_H */ diff --git a/libraries/text/bmtext/draw.c b/libraries/text/bmtext/draw.c new file mode 100644 index 00000000..e8d9eba4 --- /dev/null +++ b/libraries/text/bmtext/draw.c @@ -0,0 +1,32 @@ +/* draw.c -- draw pre-laid-out bitmap-font lines */ + +#include + +#include "framebuf/bmfont.h" +#include "framebuf/screen.h" +#include "geom/point.h" + +#include "text/bmtext.h" + +void bmtext_draw(bmfont_t *font, + screen_t *scr, + const bmtext_line_t *lines, + int nlines, + colour_t fg, + colour_t bg, + int leading, + point_t origin) +{ + int font_width, font_height; + point_t pos; + int i; + + bmfont_get_info(font, &font_width, &font_height); + + pos = origin; + for (i = 0; i < nlines; i++) + { + bmfont_draw(font, scr, lines[i].str, lines[i].len, fg, bg, &pos, NULL); + pos.y += font_height + leading; + } +} diff --git a/libraries/text/bmtext/layout.c b/libraries/text/bmtext/layout.c new file mode 100644 index 00000000..2470d8cf --- /dev/null +++ b/libraries/text/bmtext/layout.c @@ -0,0 +1,53 @@ +/* layout.c -- break a string into pixel-fitted lines */ + +#include + +#include "framebuf/bmfont.h" + +#include "text/bmtext.h" + +int bmtext_layout(bmfont_t *font, + const char *string, + int stringlen, + int wrap_width, + bmtext_line_t *lines, + int max) +{ + int nlines; + + nlines = 0; + + while (stringlen > 0 && nlines < max) + { + int absolute_break; + bmfont_width_t width; + int friendly_break; + + bmfont_measure(font, string, stringlen, wrap_width, &absolute_break, &width); + + friendly_break = absolute_break; + if (absolute_break < stringlen) + { + /* line didn't fit whole: try to break at the last space within it */ + for (friendly_break = absolute_break - 1; friendly_break > 0; friendly_break--) + if (isspace((unsigned char) string[friendly_break])) + break; + if (friendly_break <= 0) + friendly_break = absolute_break; /* no space to break at: hard break */ + } + + lines[nlines].str = string; + lines[nlines].len = friendly_break; + nlines++; + + string += friendly_break; + stringlen -= friendly_break; + while (stringlen > 0 && isspace((unsigned char) *string)) + { + string++; + stringlen--; + } + } + + return nlines; +} diff --git a/libraries/wuss/test/tasks/text.c b/libraries/wuss/test/tasks/text.c index 48350352..80688ea1 100644 --- a/libraries/wuss/test/tasks/text.c +++ b/libraries/wuss/test/tasks/text.c @@ -4,7 +4,6 @@ #include -#include #include #include @@ -17,6 +16,7 @@ #include "framebuf/palettes.h" #include "geom/box.h" #include "geom/point.h" +#include "text/bmtext.h" #include "text.h" @@ -66,95 +66,15 @@ result_t text_create(wuss_t *wuss, #define LEADING 2 #define MAX_LINES 64 /* paragraph is short and fixed; overflow is dropped */ -typedef struct text_line -{ - const char *str; - int len; -} -text_line_t; - -/* Break "string" into lines that each fit "wrap_width" pixels in "font", - * writing up to "max" of them to "lines". Returns the line count. Pure - * layout: no drawing, no screen or scroll state. */ -static int text_layout(bmfont_t *font, - const char *string, - int stringlen, - int wrap_width, - text_line_t *lines, - int max) -{ - int nlines; - - nlines = 0; - - while (stringlen > 0 && nlines < max) - { - int absolute_break; - bmfont_width_t width; - int friendly_break; - - bmfont_measure(font, string, stringlen, wrap_width, &absolute_break, &width); - - friendly_break = absolute_break; - if (absolute_break < stringlen) - { - /* line didn't fit whole: try to break at the last space within it */ - for (friendly_break = absolute_break - 1; friendly_break > 0; friendly_break--) - if (isspace((unsigned char) string[friendly_break])) - break; - if (friendly_break <= 0) - friendly_break = absolute_break; /* no space to break at: hard break */ - } - - lines[nlines].str = string; - lines[nlines].len = friendly_break; - nlines++; - - string += friendly_break; - stringlen -= friendly_break; - while (stringlen > 0 && isspace((unsigned char) *string)) - { - string++; - stringlen--; - } - } - - return nlines; -} - -/* Draw pre-laid-out "lines" stacked from "origin", advancing by the font - * height plus LEADING per line. */ -static void text_render(bmfont_t *font, - screen_t *scr, - const text_line_t *lines, - int nlines, - colour_t fg, - colour_t bg, - point_t origin) -{ - int font_width, font_height; - point_t pos; - int i; - - bmfont_get_info(font, &font_width, &font_height); - - pos = origin; - for (i = 0; i < nlines; i++) - { - bmfont_draw(font, scr, lines[i].str, lines[i].len, fg, bg, &pos, NULL); - pos.y += font_height + LEADING; - } -} - static result_t text_redraw(const wuss_event_t *event, void *task_data) { - text_task_t *tcx; - screen_t *scr; - const box_t *bounds; - int sx, sy; - text_line_t lines[MAX_LINES]; - int nlines; - point_t origin; + text_task_t *tcx; + screen_t *scr; + const box_t *bounds; + int sx, sy; + bmtext_line_t lines[MAX_LINES]; + int nlines; + point_t origin; tcx = task_data; @@ -163,17 +83,17 @@ static result_t text_redraw(const wuss_event_t *event, void *task_data) sx = event->data.redraw.scroll.x; sy = event->data.redraw.scroll.y; - nlines = text_layout(tcx->font, - paragraph, - (int) strlen(paragraph), - (bounds->x1 - INSET) - (bounds->x0 + INSET), - lines, - MAX_LINES); + nlines = bmtext_layout(tcx->font, + paragraph, + (int) strlen(paragraph), + (bounds->x1 - INSET) - (bounds->x0 + INSET), + lines, + MAX_LINES); origin.x = bounds->x0 - sx + INSET; origin.y = bounds->y0 - sy + INSET; - text_render(tcx->font, scr, lines, nlines, tcx->fg, tcx->bg, origin); + bmtext_draw(tcx->font, scr, lines, nlines, tcx->fg, tcx->bg, LEADING, origin); return result_OK; } From e9ce0ae299467afb1812d545b4549b3145229e72 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Mon, 31 Aug 2026 11:40:27 +0100 Subject: [PATCH 103/287] docs(readme): add Text section for txtfmt and bmtext Co-Authored-By: Claude Sonnet 5 --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 2f17af0d..67c36fcb 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,11 @@ DPTLib is my platform independent C library. It contains a wide variety of funct - [`test/txtscr.h`](https://github.com/dpt/DPTLib/blob/master/include/test/txtscr.h) — text format 'screen' +### Text + +- [`text/txtfmt.h`](https://github.com/dpt/DPTLib/blob/master/include/text/txtfmt.h) — word-wrap a string to a character width (for monospaced text) +- [`text/bmtext.h`](https://github.com/dpt/DPTLib/blob/master/include/text/bmtext.h) — word-wrap and draw a paragraph to a pixel width in a `bmfont` + ### Utilities - [`utils/array.h`](https://github.com/dpt/DPTLib/blob/master/include/utils/array.h) — array utilities From d759a6cf6e20348e022fcf0ea1c8cf8fda9352c1 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Mon, 31 Aug 2026 18:35:42 +0100 Subject: [PATCH 104/287] refactor(wuss): extract the interactive demo into a standalone app The SDL interactive driver (spawn_* callbacks, launcher table and the main loop) lived inside libraries/wuss/test/wuss-test.c, reachable only as a tail call from the wuss unit test when built with USE_SDL. That dragged SDL3 linkage, a USE_SDL compile-def and all the tasks/*.c modules onto DPTLibTest. Move it to apps/wuss/main.c as its own `wuss` executable, gated by a new BUILD_APPS option (which requires WUSS_FURNITURE and WUSS_ICONS and pulls SDL3). wuss-test.c is now SDL-free and holds only the unit test, which DPTLibTest still runs. The old wuss_interactive_test is renamed run_wuss. Co-Authored-By: Claude Sonnet 5 --- CMakeLists.txt | 86 +++-- apps/wuss/main.c | 570 ++++++++++++++++++++++++++++++++ libraries/wuss/test/wuss-test.c | 541 +----------------------------- 3 files changed, 632 insertions(+), 565 deletions(-) create mode 100644 apps/wuss/main.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 69c7b6ab..6949ba1f 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -30,8 +30,8 @@ endif(CCACHE_FOUND) option(USE_FORTIFY "Use Fortify" OFF) option(USE_ASAN "Build with AddressSanitizer and UndefinedBehaviorSanitizer" OFF) option(DPTLIB_IMAGES_READ_ONLY "Remove libpng write support" OFF) -option(WUSS_FURNITURE "Build the wuss window-furniture subsystem" ON) -option(WUSS_ICONS "Build the wuss in-content icon subsystem" ON) +option(WUSS_FURNITURE "Build the Wuss window-furniture subsystem" ON) +option(WUSS_ICONS "Build the Wuss in-content icon subsystem" ON) # Referencing CMAKE_TOOLCHAIN_FILE avoids a warning on rebuilds. if(NOT ${CMAKE_TOOLCHAIN_FILE} STREQUAL "") @@ -564,6 +564,7 @@ install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/include option(BUILD_TESTS "Build test program" OFF) option(BUILD_SDL_TESTS "Build tests that use SDL" OFF) +option(BUILD_APPS "Build standalone apps (Wuss interactive demo)" OFF) if(BUILD_TESTS) set(TEST_SOURCES @@ -591,27 +592,8 @@ if(BUILD_TESTS) libraries/utils/bsearch/test/bsearch-test.c libraries/utils/pack/test/pack-test.c libraries/datastruct/vector/test/vector-test.c - libraries/wuss/test/tasks/ball.c - libraries/wuss/test/tasks/blank.c - libraries/wuss/test/tasks/chars.c - libraries/wuss/test/tasks/checker.c - libraries/wuss/test/tasks/curve.c - libraries/wuss/test/tasks/gradient.c - libraries/wuss/test/tasks/image.c - libraries/wuss/test/tasks/palette.c - libraries/wuss/test/tasks/porter-duff.c - libraries/wuss/test/tasks/sofa.c - libraries/wuss/test/tasks/text.c libraries/wuss/test/wuss-test.c) - # The icon-driven launcher and the icons task only build when both wuss - # subsystems are present; the SDL interactive driver needs them too. - if(WUSS_FURNITURE AND WUSS_ICONS) - list(APPEND TEST_SOURCES - libraries/wuss/test/tasks/icons.c - libraries/wuss/test/tasks/launcher.c) - endif() - source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" FILES ${TEST_SOURCES}) # Avoid a warning from CMake @@ -634,19 +616,69 @@ if(BUILD_TESTS) target_link_libraries(DPTLibTest m) endif() + # Some unit tests (e.g. bmfont-test) gate an interactive SDL mode on + # USE_SDL; BUILD_SDL_TESTS links SDL3 and defines it for the test binary. if(BUILD_SDL_TESTS) - if(NOT (WUSS_FURNITURE AND WUSS_ICONS)) - message(FATAL_ERROR "BUILD_SDL_TESTS requires WUSS_FURNITURE and WUSS_ICONS") - endif() - find_package(SDL3 REQUIRED) find_package(SDL3_image REQUIRED) target_link_libraries(DPTLibTest SDL3::SDL3 SDL3_image::SDL3_image) - - target_compile_definitions(DPTLibTest PUBLIC USE_SDL) + target_compile_definitions(DPTLibTest PRIVATE USE_SDL) endif() install(TARGETS DPTLibTest RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX}) endif() + + +# Standalone apps +# + +if(BUILD_APPS) + # The Wuss interactive demo: its own executable, built from the SDL task + # modules plus apps/wuss/main.c (which was the old in-test SDL driver). + if(NOT (WUSS_FURNITURE AND WUSS_ICONS)) + message(FATAL_ERROR "BUILD_APPS requires WUSS_FURNITURE and WUSS_ICONS") + endif() + + find_package(SDL3 REQUIRED) + find_package(SDL3_image REQUIRED) + + set(WUSS_APP_SOURCES + apps/wuss/main.c + libraries/wuss/test/tasks/ball.c + libraries/wuss/test/tasks/blank.c + libraries/wuss/test/tasks/chars.c + libraries/wuss/test/tasks/checker.c + libraries/wuss/test/tasks/curve.c + libraries/wuss/test/tasks/gradient.c + libraries/wuss/test/tasks/icons.c + libraries/wuss/test/tasks/image.c + libraries/wuss/test/tasks/launcher.c + libraries/wuss/test/tasks/palette.c + libraries/wuss/test/tasks/porter-duff.c + libraries/wuss/test/tasks/sofa.c + libraries/wuss/test/tasks/text.c) + + source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" FILES ${WUSS_APP_SOURCES}) + + add_executable(wuss ${WUSS_APP_SOURCES}) + + set_target_properties(wuss PROPERTIES + DESCRIPTION "DPTLib Wuss interactive demo" + C_STANDARD 99 + OUTPUT_NAME_DEBUG wuss-debug + OUTPUT_NAME_RELEASE wuss + OUTPUT_NAME_RELWITHDEBINFO wuss-relwithdebinfo + OUTPUT_NAME_MINSIZEREL wuss-minsizerel) + + target_include_directories(wuss PRIVATE libraries/wuss/test) + target_compile_definitions(wuss PRIVATE USE_SDL) + target_link_libraries(wuss DPTLib SDL3::SDL3 SDL3_image::SDL3_image) + if(NOT MSVC) + target_link_libraries(wuss m) + endif() + + install(TARGETS wuss RUNTIME + DESTINATION ${CMAKE_INSTALL_PREFIX}) +endif() diff --git a/apps/wuss/main.c b/apps/wuss/main.c new file mode 100644 index 00000000..b04bb258 --- /dev/null +++ b/apps/wuss/main.c @@ -0,0 +1,570 @@ +/* main.c -- Wuss - interactive minimal window manager demo */ + +#include +#include +#include + +#ifdef FORTIFY +#include "fortify/fortify.h" +#endif + +#include "base/result.h" +#include "base/utils.h" +#include "framebuf/bitmap.h" +#include "framebuf/bmfont.h" +#include "framebuf/colour.h" +#include "framebuf/palettes.h" +#include "framebuf/pixelfmt.h" +#include "framebuf/screen.h" +#include "geom/box.h" +#include "io/path.h" +#include "wuss/wuss.h" +#include "wuss/window.h" + +#include + +#include "tasks/ball.h" +#include "tasks/blank.h" +#include "tasks/chars.h" +#include "tasks/checker.h" +#include "tasks/curve.h" +#include "tasks/gradient.h" +#include "tasks/icons.h" +#include "tasks/image.h" +#include "tasks/launcher.h" +#include "tasks/palette.h" +#include "tasks/porter-duff.h" +#include "tasks/sofa.h" +#include "tasks/text.h" + +/* ----------------------------------------------------------------------- */ + +/* Screen pixel format for the interactive test: 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_TEST_32BPP 0 + +/* the launcher's spawn callbacks take no arguments, so the pieces they need + * are stashed here instead; run_wuss runs at most once per + * process, so file-scope statics are as good as a context struct */ +static wuss_t *g_wuss; +static const colour_t *g_palette; +static int g_npalette; +static const char *g_resources; +static bmfont_t *g_daydream_font; + +/* Each spawn allocates a fresh per-instance task block so a task may run in + * several windows at once; the block is owned by its window and freed by the + * task's wuss_EVENT_CLOSE handler. If create fails, or (for the font-less + * chars task) returns OK without opening a window, the block is freed here -- + * otherwise it would leak. */ + +static result_t spawn_ball(void) +{ + ball_task_t *t = calloc(1, sizeof(*t)); + result_t rc; + if (t == NULL) return result_OOM; + rc = ball_create(g_wuss, g_palette, t); + if (rc != result_OK || t->window == NULL) { free(t); return rc; } + return result_OK; +} + +static result_t spawn_text(void) +{ + text_task_t *t = calloc(1, sizeof(*t)); + result_t rc; + if (t == NULL) return result_OOM; + rc = text_create(g_wuss, g_palette, g_daydream_font, t); + if (rc != result_OK || t->window == NULL) { free(t); return rc; } + return result_OK; +} + +static result_t spawn_blank(void) +{ + blank_task_t *t = calloc(1, sizeof(*t)); + result_t rc; + if (t == NULL) return result_OOM; + rc = blank_create(g_wuss, g_npalette, t); + if (rc != result_OK || t->window == NULL) { free(t); return rc; } + return result_OK; +} + +static result_t spawn_chars(void) +{ + chars_task_t *t = calloc(1, sizeof(*t)); + result_t rc; + if (t == NULL) return result_OOM; + rc = chars_create(g_wuss, g_palette, t); + if (rc != result_OK || t->window == NULL) { free(t); return rc; } + return result_OK; +} + +static result_t spawn_palette(void) +{ + palette_task_t *t = calloc(1, sizeof(*t)); + result_t rc; + if (t == NULL) return result_OOM; + rc = palette_create(g_wuss, g_palette, g_npalette, t); + if (rc != result_OK || t->window == NULL) { free(t); return rc; } + return result_OK; +} + +static result_t spawn_image(void) +{ + image_task_t *t; + const char *leafname; + const char *filename; + char buf[DPTLIB_MAXPATH]; + const char *ninepatch; + result_t rc; + + t = calloc(1, sizeof(*t)); + if (t == NULL) return result_OOM; + + leafname = path_join_leafname("jessica", "png"); + filename = path_join_filename(g_resources, 3, "resources", "images", leafname); + strcpy(buf, filename); + ninepatch = path_join_filename(g_resources, 3, "resources", "wuss", + path_join_leafname("9tile", "png")); + + rc = image_create(g_wuss, g_palette, buf, ninepatch, t); + if (rc != result_OK || t->window == NULL) { free(t); return rc; } + return result_OK; +} + +static result_t spawn_checker(void) +{ + checker_task_t *t = calloc(1, sizeof(*t)); + result_t rc; + if (t == NULL) return result_OOM; + rc = checker_create(g_wuss, g_palette, t); + if (rc != result_OK || t->window == NULL) { free(t); return rc; } + return result_OK; +} + +static result_t spawn_curve(void) +{ + curve_task_t *t = calloc(1, sizeof(*t)); + result_t rc; + if (t == NULL) return result_OOM; + rc = curve_create(g_wuss, g_palette, t); + if (rc != result_OK || t->window == NULL) { free(t); return rc; } + return result_OK; +} + +static result_t spawn_sofa(void) +{ + sofa_task_t *t = calloc(1, sizeof(*t)); + result_t rc; + if (t == NULL) return result_OOM; + rc = sofa_create(g_wuss, g_palette, t); + if (rc != result_OK || t->window == NULL) { free(t); return rc; } + return result_OK; +} + +static result_t spawn_gradient(void) +{ + gradient_task_t *t = calloc(1, sizeof(*t)); + result_t rc; + if (t == NULL) return result_OOM; + rc = gradient_create(g_wuss, t); + if (rc != result_OK || t->window == NULL) { free(t); return rc; } + return result_OK; +} + +static result_t spawn_icons(void) +{ + icons_task_t *t = calloc(1, sizeof(*t)); + result_t rc; + if (t == NULL) return result_OOM; + rc = icons_create(g_wuss, g_palette, g_daydream_font, t); + if (rc != result_OK || t->window == NULL) { free(t); return rc; } + return result_OK; +} + +static result_t spawn_porter_duff(void) +{ + porter_duff_task_t *t = calloc(1, sizeof(*t)); + result_t rc; + if (t == NULL) return result_OOM; + rc = porter_duff_create(g_wuss, g_palette, g_daydream_font, g_resources, t); + if (rc != result_OK || t->window == NULL) { free(t); return rc; } + return result_OK; +} + +static const launcher_entry_t g_launcher_entries[] = +{ + { "Ball", spawn_ball }, + { "Text", spawn_text }, + { "Blank", spawn_blank }, + { "Chars", spawn_chars }, + { "Palette", spawn_palette }, + { "Image", spawn_image }, + { "Checker", spawn_checker }, + { "Curve", spawn_curve }, + { "Sofa", spawn_sofa }, + { "Gradient", spawn_gradient }, + { "Icons", spawn_icons }, + { "Porter-Duff", spawn_porter_duff } +}; + +static wuss_button_t sdl_button_to_wuss(Uint8 button) +{ + switch (button) + { + case SDL_BUTTON_MIDDLE: return wuss_BUTTON_MENU; + case SDL_BUTTON_RIGHT: return wuss_BUTTON_ADJUST; + default: return wuss_BUTTON_SELECT; + } +} + +/* SDL delivers mouse coordinates in window space, which F2 can scale away + * from the fixed-size Wuss screen; map back down to screen space */ +static void sdl_pos_to_scr(SDL_Window *window, + int scr_width, + int scr_height, + float in_x, + float in_y, + int *out_x, + int *out_y) +{ + int win_w, win_h; + + SDL_GetWindowSize(window, &win_w, &win_h); + + *out_x = (int) (in_x * scr_width / win_w); + *out_y = (int) (in_y * scr_height / win_h); +} + +/* click windows to bring to front, drag titlebars to move, resize the + * SDL window to see the Wuss screen scale; F2 doubles the SDL window size, + * Shift-F2 halves it; F3 redraws the whole screen one pixel at a time, to + * catch tasks whose drawing routines misbehave under a 1x1 clip; Q or + * close to quit */ +static result_t run_wuss(const char *resources) +{ + const int scr_width = 640; + const int scr_height = 480; +#if WUSS_TEST_32BPP + const int rowbytes = scr_width * 4; /* pixelfmt_bgrx8888: 4 bytes/pixel */ +#else + const int rowbytes = scr_width / 2; /* pixelfmt_p4: 2 pixels/byte */ +#endif + + result_t rc; + const char *leafname; + const char *filename; + bmfont_t *font; + bmfont_t *daydream_font; + void *pixels; + bitmap_t bm; +#if !WUSS_TEST_32BPP + bitmap_t *disp; +#endif + screen_t scr; + colour_t palette[16]; + wuss_t *wuss; + launcher_task_t launcher_task; + SDL_Window *window; + SDL_Renderer *renderer; + SDL_Texture *texture; + bool quit; + bool garbage_pending; + bool pixel_stress_pending; + int i; + + { + const char *palette_name = getenv("WUSS_PALETTE"); /* "riscos16" selects the RISC OS 16-colour palette; default is PICO-8 */ + + if (palette_name != NULL && strcmp(palette_name, "wimp16") == 0) + define_wimp16_palette(palette); + else + define_pico8_palette(palette); + } + + leafname = path_join_leafname("digits", "png"); + filename = path_join_filename(resources, 3, "resources", "bmfonts", leafname); + rc = bmfont_create(filename, &font); + if (rc != result_OK) + goto Failure; + + leafname = path_join_leafname("daydream", "png"); + filename = path_join_filename(resources, 3, "resources", "bmfonts", leafname); + rc = bmfont_create(filename, &daydream_font); + if (rc != result_OK) + goto Failure; + + pixels = malloc(rowbytes * scr_height); + if (pixels == NULL) + goto Failure; + +#if WUSS_TEST_32BPP + rc = bitmap_init(&bm, SIZE2D(scr_width, scr_height), pixelfmt_bgrx8888, rowbytes, palette, pixels); +#else + rc = bitmap_init(&bm, SIZE2D(scr_width, scr_height), pixelfmt_p4, rowbytes, palette, pixels); +#endif + if (rc != result_OK) + goto Failure; + + bitmap_clear(&bm, palette[palette_PICO8_WHITE]); + + screen_for_bitmap(&scr, &bm); + + if (!SDL_Init(SDL_INIT_VIDEO)) + { + fprintf(stderr, "Error: SDL_Init: %s\n", SDL_GetError()); + goto Failure; + } + + window = SDL_CreateWindow("Wuss", scr_width, scr_height, 0); + if (window == NULL) + { + fprintf(stderr, "Error: SDL_CreateWindow: %s\n", SDL_GetError()); + goto Failure; + } + + renderer = SDL_CreateRenderer(window, NULL); + if (renderer == NULL) + { + fprintf(stderr, "Error: SDL_CreateRenderer: %s\n", SDL_GetError()); + goto Failure; + } + + texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_ARGB8888, + SDL_TEXTUREACCESS_STREAMING, + scr_width, scr_height); + if (texture == NULL) + { + fprintf(stderr, "Error: SDL_CreateTexture: %s\n", SDL_GetError()); + goto Failure; + } + + SDL_SetTextureBlendMode(texture, SDL_BLENDMODE_NONE); + SDL_SetTextureScaleMode(texture, SDL_SCALEMODE_NEAREST); /* keep pixels crisp when F2 scales the window up */ + + { + wuss_config_t config; + + config.titlebar_height = 0; + config.palette.title.bg = palette_PICO8_DARK_BLUE; + config.palette.title.fg = palette_PICO8_WHITE; + config.palette.back = palette_PICO8_GREEN; + config.palette.close = palette_PICO8_RED; + config.palette.toggle = palette_PICO8_ORANGE; + config.palette.resize = palette_PICO8_LAVENDER; + config.palette.scroll.arrows = palette_PICO8_BLUE; + config.palette.scroll.wells = palette_PICO8_DARK_BLUE; + config.palette.scroll.sausages = palette_PICO8_LIGHT_GREY; + config.bevel.light = palette_PICO8_WHITE; + config.bevel.dark = palette_PICO8_DARK_GREY; + config.backdrop.colour = palette_PICO8_WHITE; + config.backdrop.pattern = screen_PATTERN_DOTS; + config.backdrop.pattern_bg = palette_PICO8_LIGHT_GREY; + + rc = wuss_create(&scr, font, palette, NELEMS(palette), &config, &wuss); + if (rc != result_OK) + goto Failure; + } + + g_wuss = wuss; + g_palette = palette; + g_npalette = NELEMS(palette); + g_resources = resources; + g_daydream_font = daydream_font; + + rc = launcher_create(wuss, g_launcher_entries, NELEMS(g_launcher_entries), &launcher_task); + if (rc != result_OK) + goto Failure; + + quit = false; + garbage_pending = false; + pixel_stress_pending = false; + + wuss_redraw(wuss); + + while (!quit) + { + SDL_Event event; + + while (SDL_PollEvent(&event)) + { + switch (event.type) + { + case SDL_EVENT_QUIT: + quit = true; + break; + + case SDL_EVENT_KEY_UP: + if (event.key.key == SDLK_Q) + quit = true; + else if (event.key.key == SDLK_F1 && (event.key.mod & SDL_KMOD_SHIFT)) + wuss_redraw(wuss); + else if (event.key.key == SDLK_F1) + garbage_pending = true; + else if (event.key.key == SDLK_F3) + pixel_stress_pending = true; + else if (event.key.key == SDLK_F2) + { + int w, h; + + SDL_GetWindowSize(window, &w, &h); + if (event.key.mod & SDL_KMOD_SHIFT) + SDL_SetWindowSize(window, w / 2, h / 2); + else + SDL_SetWindowSize(window, w * 2, h * 2); + } + break; + + case SDL_EVENT_MOUSE_BUTTON_DOWN: + { + int x, y; + + sdl_pos_to_scr(window, scr_width, scr_height, event.button.x, event.button.y, &x, &y); + wuss_mouse_click(wuss, POINT(x, y), sdl_button_to_wuss(event.button.button), wuss_MOUSE_DOWN, NULL); + } + break; + + case SDL_EVENT_MOUSE_BUTTON_UP: + { + int x, y; + + sdl_pos_to_scr(window, scr_width, scr_height, event.button.x, event.button.y, &x, &y); + wuss_mouse_click(wuss, POINT(x, y), sdl_button_to_wuss(event.button.button), wuss_MOUSE_UP, NULL); + } + break; + + case SDL_EVENT_MOUSE_MOTION: + { + int x, y; + + sdl_pos_to_scr(window, scr_width, scr_height, event.motion.x, event.motion.y, &x, &y); + wuss_mouse_move(wuss, POINT(x, y), NULL); + } + break; + + case SDL_EVENT_MOUSE_WHEEL: + { + int x, y; + + sdl_pos_to_scr(window, scr_width, scr_height, event.wheel.mouse_x, event.wheel.mouse_y, &x, &y); + wuss_scroll(wuss, POINT(x, y), (int) event.wheel.y, NULL); + } + break; + + default: + break; + } + } + + wuss_idle(wuss); + + if (garbage_pending) + { + /* full-screen corruption, drawn over whatever's already on screen + * (windows included) and left untouched: nothing here is invalidated, + * so it stays put until something actually redraws over it, e.g. the + * ball's own small per-frame dirty rect eating a trail through it, or + * a window being dragged across it */ + unsigned char *p; + size_t n; + size_t i; + + p = pixels; + n = (size_t) rowbytes * scr_height; + for (i = 0; i < n; i++) + p[i] = (unsigned char) rand(); + + garbage_pending = false; + } + else if (pixel_stress_pending) + { + /* redraw the whole screen one pixel at a time: each wuss_redraw_dirty + * call is flushed before the next pixel is invalidated, so no two + * pixels are ever coalesced into one redraw. A task whose drawing + * routine assumes it always gets handed a multi-pixel/aligned clip + * (rather than trusting scr->clip) will visibly misdraw here even + * though it looks fine under normal, larger dirty regions. */ + int x, y; + + for (y = 0; y < scr_height; y++) + { + for (x = 0; x < scr_width; x++) + { + box_t px; + + px.x0 = x; px.y0 = y; + px.x1 = x + 1; px.y1 = y + 1; + + wuss_invalidate(wuss, &px); + wuss_redraw_dirty(wuss); + } + } + + pixel_stress_pending = false; + } + else + { + wuss_redraw_dirty(wuss); + } + +#if WUSS_TEST_32BPP + SDL_UpdateTexture(texture, NULL, bm.base, bm.rowbytes); /* bm is already bgrx8888: no conversion needed */ +#else + rc = bitmap_convert(&bm, pixelfmt_bgrx8888, &disp); + if (rc == result_OK) + { + SDL_UpdateTexture(texture, NULL, disp->base, disp->rowbytes); + free(disp->base); + free(disp); + } +#endif + + SDL_RenderTexture(renderer, texture, NULL, NULL); + SDL_RenderPresent(renderer); + + SDL_Delay(1000 / 60); + } + + /* ponytail: wuss_destroy() below frees every still-open window but not the + * per-instance task block hung off it, so any task window left open at quit + * leaks its block. Harmless at process exit; add a wuss close callback if a + * task ever needs deterministic teardown. */ + launcher_destroy(&launcher_task); + + wuss_destroy(wuss); + bmfont_destroy(font); + bmfont_destroy(daydream_font); + + SDL_DestroyTexture(texture); + SDL_DestroyRenderer(renderer); + SDL_DestroyWindow(window); + SDL_Quit(); + + free(pixels); + + return result_TEST_PASSED; + + +Failure: + + printf("run_wuss: failed (rc=0x%X)\n", rc); + + return result_TEST_FAILED; +} + +/* ----------------------------------------------------------------------- */ + +int main(int argc, char *argv[]) +{ + const char *resources = "."; + int i; + result_t rc; + + for (i = 1; i < argc; i++) + if (strcmp(argv[i], "-resources") == 0 && i + 1 < argc) + resources = argv[++i]; + + rc = run_wuss(resources); + + return rc == result_TEST_PASSED ? EXIT_SUCCESS : EXIT_FAILURE; +} diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 9fb03dd6..b38be4e8 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -25,538 +25,9 @@ /* ----------------------------------------------------------------------- */ -#ifdef USE_SDL - -#include - -#include "tasks/ball.h" -#include "tasks/blank.h" -#include "tasks/chars.h" -#include "tasks/checker.h" -#include "tasks/curve.h" -#include "tasks/gradient.h" -#include "tasks/icons.h" -#include "tasks/image.h" -#include "tasks/launcher.h" -#include "tasks/palette.h" -#include "tasks/porter-duff.h" -#include "tasks/sofa.h" -#include "tasks/text.h" - -/* Screen pixel format for the interactive test: 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_TEST_32BPP 0 - -/* the launcher's spawn callbacks take no arguments, so the pieces they need - * are stashed here instead; wuss_interactive_test runs at most once per - * process, so file-scope statics are as good as a context struct */ -static wuss_t *g_wuss; -static const colour_t *g_palette; -static int g_npalette; -static const char *g_resources; -static bmfont_t *g_daydream_font; - -/* Each spawn allocates a fresh per-instance task block so a task may run in - * several windows at once; the block is owned by its window and freed by the - * task's wuss_EVENT_CLOSE handler. If create fails, or (for the font-less - * chars task) returns OK without opening a window, the block is freed here -- - * otherwise it would leak. */ - -static result_t spawn_ball(void) -{ - ball_task_t *t = calloc(1, sizeof(*t)); - result_t rc; - if (t == NULL) return result_OOM; - rc = ball_create(g_wuss, g_palette, t); - if (rc != result_OK || t->window == NULL) { free(t); return rc; } - return result_OK; -} - -static result_t spawn_text(void) -{ - text_task_t *t = calloc(1, sizeof(*t)); - result_t rc; - if (t == NULL) return result_OOM; - rc = text_create(g_wuss, g_palette, g_daydream_font, t); - if (rc != result_OK || t->window == NULL) { free(t); return rc; } - return result_OK; -} - -static result_t spawn_blank(void) -{ - blank_task_t *t = calloc(1, sizeof(*t)); - result_t rc; - if (t == NULL) return result_OOM; - rc = blank_create(g_wuss, g_npalette, t); - if (rc != result_OK || t->window == NULL) { free(t); return rc; } - return result_OK; -} - -static result_t spawn_chars(void) -{ - chars_task_t *t = calloc(1, sizeof(*t)); - result_t rc; - if (t == NULL) return result_OOM; - rc = chars_create(g_wuss, g_palette, t); - if (rc != result_OK || t->window == NULL) { free(t); return rc; } - return result_OK; -} - -static result_t spawn_palette(void) -{ - palette_task_t *t = calloc(1, sizeof(*t)); - result_t rc; - if (t == NULL) return result_OOM; - rc = palette_create(g_wuss, g_palette, g_npalette, t); - if (rc != result_OK || t->window == NULL) { free(t); return rc; } - return result_OK; -} - -static result_t spawn_image(void) -{ - image_task_t *t; - const char *leafname; - const char *filename; - char buf[DPTLIB_MAXPATH]; - const char *ninepatch; - result_t rc; - - t = calloc(1, sizeof(*t)); - if (t == NULL) return result_OOM; - - leafname = path_join_leafname("jessica", "png"); - filename = path_join_filename(g_resources, 3, "resources", "images", leafname); - strcpy(buf, filename); - ninepatch = path_join_filename(g_resources, 3, "resources", "wuss", - path_join_leafname("9tile", "png")); - - rc = image_create(g_wuss, g_palette, buf, ninepatch, t); - if (rc != result_OK || t->window == NULL) { free(t); return rc; } - return result_OK; -} - -static result_t spawn_checker(void) -{ - checker_task_t *t = calloc(1, sizeof(*t)); - result_t rc; - if (t == NULL) return result_OOM; - rc = checker_create(g_wuss, g_palette, t); - if (rc != result_OK || t->window == NULL) { free(t); return rc; } - return result_OK; -} - -static result_t spawn_curve(void) -{ - curve_task_t *t = calloc(1, sizeof(*t)); - result_t rc; - if (t == NULL) return result_OOM; - rc = curve_create(g_wuss, g_palette, t); - if (rc != result_OK || t->window == NULL) { free(t); return rc; } - return result_OK; -} - -static result_t spawn_sofa(void) -{ - sofa_task_t *t = calloc(1, sizeof(*t)); - result_t rc; - if (t == NULL) return result_OOM; - rc = sofa_create(g_wuss, g_palette, t); - if (rc != result_OK || t->window == NULL) { free(t); return rc; } - return result_OK; -} - -static result_t spawn_gradient(void) -{ - gradient_task_t *t = calloc(1, sizeof(*t)); - result_t rc; - if (t == NULL) return result_OOM; - rc = gradient_create(g_wuss, t); - if (rc != result_OK || t->window == NULL) { free(t); return rc; } - return result_OK; -} - -static result_t spawn_icons(void) -{ - icons_task_t *t = calloc(1, sizeof(*t)); - result_t rc; - if (t == NULL) return result_OOM; - rc = icons_create(g_wuss, g_palette, g_daydream_font, t); - if (rc != result_OK || t->window == NULL) { free(t); return rc; } - return result_OK; -} - -static result_t spawn_porter_duff(void) -{ - porter_duff_task_t *t = calloc(1, sizeof(*t)); - result_t rc; - if (t == NULL) return result_OOM; - rc = porter_duff_create(g_wuss, g_palette, g_daydream_font, g_resources, t); - if (rc != result_OK || t->window == NULL) { free(t); return rc; } - return result_OK; -} - -static const launcher_entry_t g_launcher_entries[] = -{ - { "Ball", spawn_ball }, - { "Text", spawn_text }, - { "Blank", spawn_blank }, - { "Chars", spawn_chars }, - { "Palette", spawn_palette }, - { "Image", spawn_image }, - { "Checker", spawn_checker }, - { "Curve", spawn_curve }, - { "Sofa", spawn_sofa }, - { "Gradient", spawn_gradient }, - { "Icons", spawn_icons }, - { "Porter-Duff", spawn_porter_duff } -}; - -static wuss_button_t sdl_button_to_wuss(Uint8 button) -{ - switch (button) - { - case SDL_BUTTON_MIDDLE: return wuss_BUTTON_MENU; - case SDL_BUTTON_RIGHT: return wuss_BUTTON_ADJUST; - default: return wuss_BUTTON_SELECT; - } -} - -/* SDL delivers mouse coordinates in window space, which F2 can scale away - * from the fixed-size Wuss screen; map back down to screen space */ -static void sdl_pos_to_scr(SDL_Window *window, - int scr_width, - int scr_height, - float in_x, - float in_y, - int *out_x, - int *out_y) -{ - int win_w, win_h; - - SDL_GetWindowSize(window, &win_w, &win_h); - - *out_x = (int) (in_x * scr_width / win_w); - *out_y = (int) (in_y * scr_height / win_h); -} - -/* click windows to bring to front, drag titlebars to move, resize the - * SDL window to see the Wuss screen scale; F2 doubles the SDL window size, - * Shift-F2 halves it; F3 redraws the whole screen one pixel at a time, to - * catch tasks whose drawing routines misbehave under a 1x1 clip; Q or - * close to quit */ -static result_t wuss_interactive_test(const char *resources) -{ - const int scr_width = 640; - const int scr_height = 480; -#if WUSS_TEST_32BPP - const int rowbytes = scr_width * 4; /* pixelfmt_bgrx8888: 4 bytes/pixel */ -#else - const int rowbytes = scr_width / 2; /* pixelfmt_p4: 2 pixels/byte */ -#endif - - result_t rc; - const char *leafname; - const char *filename; - bmfont_t *font; - bmfont_t *daydream_font; - void *pixels; - bitmap_t bm; -#if !WUSS_TEST_32BPP - bitmap_t *disp; -#endif - screen_t scr; - colour_t palette[16]; - wuss_t *wuss; - launcher_task_t launcher_task; - SDL_Window *window; - SDL_Renderer *renderer; - SDL_Texture *texture; - bool quit; - bool garbage_pending; - bool pixel_stress_pending; - int i; - - { - const char *palette_name = getenv("WUSS_PALETTE"); /* "riscos16" selects the RISC OS 16-colour palette; default is PICO-8 */ - - if (palette_name != NULL && strcmp(palette_name, "wimp16") == 0) - define_wimp16_palette(palette); - else - define_pico8_palette(palette); - } - - leafname = path_join_leafname("digits", "png"); - filename = path_join_filename(resources, 3, "resources", "bmfonts", leafname); - rc = bmfont_create(filename, &font); - if (rc != result_OK) - goto Failure; - - leafname = path_join_leafname("daydream", "png"); - filename = path_join_filename(resources, 3, "resources", "bmfonts", leafname); - rc = bmfont_create(filename, &daydream_font); - if (rc != result_OK) - goto Failure; - - pixels = malloc(rowbytes * scr_height); - if (pixels == NULL) - goto Failure; - -#if WUSS_TEST_32BPP - rc = bitmap_init(&bm, SIZE2D(scr_width, scr_height), pixelfmt_bgrx8888, rowbytes, palette, pixels); -#else - rc = bitmap_init(&bm, SIZE2D(scr_width, scr_height), pixelfmt_p4, rowbytes, palette, pixels); -#endif - if (rc != result_OK) - goto Failure; - - bitmap_clear(&bm, palette[palette_PICO8_WHITE]); - - screen_for_bitmap(&scr, &bm); - - if (!SDL_Init(SDL_INIT_VIDEO)) - { - fprintf(stderr, "Error: SDL_Init: %s\n", SDL_GetError()); - goto Failure; - } - - window = SDL_CreateWindow("DPTLib Wuss Test", scr_width, scr_height, 0); - if (window == NULL) - { - fprintf(stderr, "Error: SDL_CreateWindow: %s\n", SDL_GetError()); - goto Failure; - } - - renderer = SDL_CreateRenderer(window, NULL); - if (renderer == NULL) - { - fprintf(stderr, "Error: SDL_CreateRenderer: %s\n", SDL_GetError()); - goto Failure; - } - - texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_ARGB8888, - SDL_TEXTUREACCESS_STREAMING, - scr_width, scr_height); - if (texture == NULL) - { - fprintf(stderr, "Error: SDL_CreateTexture: %s\n", SDL_GetError()); - goto Failure; - } - - SDL_SetTextureBlendMode(texture, SDL_BLENDMODE_NONE); - SDL_SetTextureScaleMode(texture, SDL_SCALEMODE_NEAREST); /* keep pixels crisp when F2 scales the window up */ - - { - wuss_config_t config; - - config.titlebar_height = 0; - config.palette.title.bg = palette_PICO8_DARK_BLUE; - config.palette.title.fg = palette_PICO8_WHITE; - config.palette.back = palette_PICO8_GREEN; - config.palette.close = palette_PICO8_RED; - config.palette.toggle = palette_PICO8_ORANGE; - config.palette.resize = palette_PICO8_LAVENDER; - config.palette.scroll.arrows = palette_PICO8_BLUE; - config.palette.scroll.wells = palette_PICO8_DARK_BLUE; - config.palette.scroll.sausages = palette_PICO8_LIGHT_GREY; - config.bevel.light = palette_PICO8_WHITE; - config.bevel.dark = palette_PICO8_DARK_GREY; - config.backdrop.colour = palette_PICO8_WHITE; - config.backdrop.pattern = screen_PATTERN_DOTS; - config.backdrop.pattern_bg = palette_PICO8_LIGHT_GREY; - - rc = wuss_create(&scr, font, palette, NELEMS(palette), &config, &wuss); - if (rc != result_OK) - goto Failure; - } - - g_wuss = wuss; - g_palette = palette; - g_npalette = NELEMS(palette); - g_resources = resources; - g_daydream_font = daydream_font; - - rc = launcher_create(wuss, g_launcher_entries, NELEMS(g_launcher_entries), &launcher_task); - if (rc != result_OK) - goto Failure; - - quit = false; - garbage_pending = false; - pixel_stress_pending = false; - - wuss_redraw(wuss); - - while (!quit) - { - SDL_Event event; - - while (SDL_PollEvent(&event)) - { - switch (event.type) - { - case SDL_EVENT_QUIT: - quit = true; - break; - - case SDL_EVENT_KEY_UP: - if (event.key.key == SDLK_Q) - quit = true; - else if (event.key.key == SDLK_F1 && (event.key.mod & SDL_KMOD_SHIFT)) - wuss_redraw(wuss); - else if (event.key.key == SDLK_F1) - garbage_pending = true; - else if (event.key.key == SDLK_F3) - pixel_stress_pending = true; - else if (event.key.key == SDLK_F2) - { - int w, h; - - SDL_GetWindowSize(window, &w, &h); - if (event.key.mod & SDL_KMOD_SHIFT) - SDL_SetWindowSize(window, w / 2, h / 2); - else - SDL_SetWindowSize(window, w * 2, h * 2); - } - break; - - case SDL_EVENT_MOUSE_BUTTON_DOWN: - { - int x, y; - - sdl_pos_to_scr(window, scr_width, scr_height, event.button.x, event.button.y, &x, &y); - wuss_mouse_click(wuss, POINT(x, y), sdl_button_to_wuss(event.button.button), wuss_MOUSE_DOWN, NULL); - } - break; - - case SDL_EVENT_MOUSE_BUTTON_UP: - { - int x, y; - - sdl_pos_to_scr(window, scr_width, scr_height, event.button.x, event.button.y, &x, &y); - wuss_mouse_click(wuss, POINT(x, y), sdl_button_to_wuss(event.button.button), wuss_MOUSE_UP, NULL); - } - break; - - case SDL_EVENT_MOUSE_MOTION: - { - int x, y; - - sdl_pos_to_scr(window, scr_width, scr_height, event.motion.x, event.motion.y, &x, &y); - wuss_mouse_move(wuss, POINT(x, y), NULL); - } - break; - - case SDL_EVENT_MOUSE_WHEEL: - { - int x, y; - - sdl_pos_to_scr(window, scr_width, scr_height, event.wheel.mouse_x, event.wheel.mouse_y, &x, &y); - wuss_scroll(wuss, POINT(x, y), (int) event.wheel.y, NULL); - } - break; - - default: - break; - } - } - - wuss_idle(wuss); - - if (garbage_pending) - { - /* full-screen corruption, drawn over whatever's already on screen - * (windows included) and left untouched: nothing here is invalidated, - * so it stays put until something actually redraws over it, e.g. the - * ball's own small per-frame dirty rect eating a trail through it, or - * a window being dragged across it */ - unsigned char *p; - size_t n; - size_t i; - - p = pixels; - n = (size_t) rowbytes * scr_height; - for (i = 0; i < n; i++) - p[i] = (unsigned char) rand(); - - garbage_pending = false; - } - else if (pixel_stress_pending) - { - /* redraw the whole screen one pixel at a time: each wuss_redraw_dirty - * call is flushed before the next pixel is invalidated, so no two - * pixels are ever coalesced into one redraw. A task whose drawing - * routine assumes it always gets handed a multi-pixel/aligned clip - * (rather than trusting scr->clip) will visibly misdraw here even - * though it looks fine under normal, larger dirty regions. */ - int x, y; - - for (y = 0; y < scr_height; y++) - { - for (x = 0; x < scr_width; x++) - { - box_t px; - - px.x0 = x; px.y0 = y; - px.x1 = x + 1; px.y1 = y + 1; - - wuss_invalidate(wuss, &px); - wuss_redraw_dirty(wuss); - } - } - - pixel_stress_pending = false; - } - else - { - wuss_redraw_dirty(wuss); - } - -#if WUSS_TEST_32BPP - SDL_UpdateTexture(texture, NULL, bm.base, bm.rowbytes); /* bm is already bgrx8888: no conversion needed */ -#else - rc = bitmap_convert(&bm, pixelfmt_bgrx8888, &disp); - if (rc == result_OK) - { - SDL_UpdateTexture(texture, NULL, disp->base, disp->rowbytes); - free(disp->base); - free(disp); - } -#endif - - SDL_RenderTexture(renderer, texture, NULL, NULL); - SDL_RenderPresent(renderer); - - SDL_Delay(1000 / 60); - } - - /* ponytail: wuss_destroy() below frees every still-open window but not the - * per-instance task block hung off it, so any task window left open at quit - * leaks its block. Harmless at process exit; add a wuss close callback if a - * task ever needs deterministic teardown. */ - launcher_destroy(&launcher_task); - - wuss_destroy(wuss); - bmfont_destroy(font); - bmfont_destroy(daydream_font); - - SDL_DestroyTexture(texture); - SDL_DestroyRenderer(renderer); - SDL_DestroyWindow(window); - SDL_Quit(); - - free(pixels); - - return result_TEST_PASSED; - - -Failure: - - printf("wuss_interactive_test: failed (rc=0x%X)\n", rc); - - return result_TEST_FAILED; -} - -#endif /* USE_SDL */ +/* The SDL interactive driver that used to live here (spawn_* callbacks, the + * launcher table and the run_wuss loop) is now the standalone `wuss` app + * in apps/wuss/main.c. This file is the Wuss unit test only. */ /* ----------------------------------------------------------------------- */ @@ -3111,12 +2582,6 @@ result_t wuss_test(const char *resources) free(pixels); -#ifdef USE_SDL - rc = wuss_interactive_test(resources); - if (rc != result_TEST_PASSED) - goto Failure; -#endif - return result_TEST_PASSED; From 88f9172a78252220ee802bd28223a795045b6e18 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Mon, 31 Aug 2026 18:49:03 +0100 Subject: [PATCH 105/287] fix(framebuf): avoid undefined shifts flagged by UBSan curve: left-shifting negative point deltas by FIX16_SHIFT is undefined; multiply by FIX16_ONE instead (same codegen). bmfont: extract_advance_widths shifted currbits by bitsperchar without the width==32 guard that the glyph decoder already has, giving a shift-by-32 when charwidth is 16. Co-Authored-By: Claude Sonnet 5 --- libraries/framebuf/bmfont/bmfont.c | 2 +- libraries/framebuf/curve/curve.c | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/libraries/framebuf/bmfont/bmfont.c b/libraries/framebuf/bmfont/bmfont.c index 00d3b87c..f7581eb9 100644 --- a/libraries/framebuf/bmfont/bmfont.c +++ b/libraries/framebuf/bmfont/bmfont.c @@ -158,7 +158,7 @@ static result_t extract_advance_widths(bmfont_t *bmfont, assert(ncurrbits >= bitsperchar); adw_px = currbits & mask; /* extract high bits */ - currbits <<= bitsperchar; /* discard used bits */ + currbits = (bitsperchar == 32) ? 0 : currbits << bitsperchar; /* discard used bits */ ncurrbits -= bitsperchar; if (array_grow((void **) &bmfont->adw, diff --git a/libraries/framebuf/curve/curve.c b/libraries/framebuf/curve/curve.c index b3ce5fcb..fd244d12 100644 --- a/libraries/framebuf/curve/curve.c +++ b/libraries/framebuf/curve/curve.c @@ -328,14 +328,14 @@ void curve_bezier_cubic(point_t p0, #define MULFIX16(x,y) (int) (((long long) (x) * (y)) >> FIX16_SHIFT) - cx = 3 * ((p1.x - p0.x) << FIX16_SHIFT); - cy = 3 * ((p1.y - p0.y) << FIX16_SHIFT); + cx = 3 * ((p1.x - p0.x) * FIX16_ONE); + cy = 3 * ((p1.y - p0.y) * FIX16_ONE); - bx = 3 * ((p2.x - p1.x) << FIX16_SHIFT) - cx; - by = 3 * ((p2.y - p1.y) << FIX16_SHIFT) - cy; + bx = 3 * ((p2.x - p1.x) * FIX16_ONE) - cx; + by = 3 * ((p2.y - p1.y) * FIX16_ONE) - cy; - ax = ((p3.x - p0.x) << FIX16_SHIFT) - cx - bx; - ay = ((p3.y - p0.y) << FIX16_SHIFT) - cy - by; + ax = ((p3.x - p0.x) * FIX16_ONE) - cx - bx; + ay = ((p3.y - p0.y) * FIX16_ONE) - cy - by; h = FIX16_ONE / nsteps; hh = MULFIX16(h,h); @@ -355,8 +355,8 @@ void curve_bezier_cubic(point_t p0, point = &points[0]; - curx = p0.x << FIX16_SHIFT; - cury = p0.y << FIX16_SHIFT; + curx = p0.x * FIX16_ONE; + cury = p0.y * FIX16_ONE; for (i = 0; ; i++) { From b0e0f409fdf56987fd04b70b34301092389ef8d0 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Mon, 31 Aug 2026 18:55:20 +0100 Subject: [PATCH 106/287] test(curve): cut headless frame count from 1000 to 100 The no-SDL path looped 1000 frames of full curve calculation and software line rasterisation on an 800x600 buffer; under ASan/UBSan that ran ~5.5s. 100 frames covers every draw path and finishes in ~0.5s. Co-Authored-By: Claude Sonnet 5 --- libraries/framebuf/curve/test/curve-test.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/framebuf/curve/test/curve-test.c b/libraries/framebuf/curve/test/curve-test.c index 5cca0837..1c8e7f2f 100644 --- a/libraries/framebuf/curve/test/curve-test.c +++ b/libraries/framebuf/curve/test/curve-test.c @@ -688,7 +688,7 @@ static result_t curve_interactive_test(curveteststate_t *state) } } #else - if (frame > 1000) + if (frame > 100) /* headless: enough frames to cover every draw path */ quit = 1; #endif From 0c36e0afd51991bcd70eaae77e0b75750bec5247 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Mon, 31 Aug 2026 19:25:59 +0100 Subject: [PATCH 107/287] fix(packer): reclaim released space exactly instead of fragmenting packer_release only absorbed a freed slot when it sat wholly inside one existing free area. A window footprint straddles several free fragments, so it was appended as an overlapping box and remove_area never coalesced. Over repeated open/close cycles the free list decayed into slivers and large placements stopped fitting despite the pixels being free. Track every box carved out by packer_place_at / packer_place_by in a placed[] list. packer_release now drops the matching placed box and rebuilds the free list from the margins minus the still-live boxes -- exact reclaim, no coalescing heuristics. packer_clear resets the placed list since its swathe has no matching release. O(nplaced^2) per release; nplaced is a handful in practice. Adds packer test4: four rounds of placing six mixed-size boxes and releasing all of them, asserting the free list collapses back to the whole page each round and a full-page box then fits. --- include/geom/packer.h | 9 +- libraries/geom/packer/impl.h | 6 ++ libraries/geom/packer/packer.c | 88 ++++++++++++++++- libraries/geom/packer/test/packer-test.c | 117 +++++++++++++++++++++++ 4 files changed, 211 insertions(+), 9 deletions(-) diff --git a/include/geom/packer.h b/include/geom/packer.h index 184f7d4f..c9360098 100644 --- a/include/geom/packer.h +++ b/include/geom/packer.h @@ -93,10 +93,11 @@ void packer_set_gutter(T *packer, int gutter); * Returns a previously placed area to the free pool: the inverse of * packer_place_at / packer_place_by. * - * Adjacent released areas are not coalesced, so a single placement that would - * span two separately-released areas will not fit until the gap between them is - * also freed; placing a box that fits wholly within one free area is - * unaffected. packer_get_consumed_area is not narrowed by a release. + * Free areas that share a full edge are coalesced after each release, so + * repeated place/release cycles reclaim the whole page rather than fragmenting + * it. A placement spanning two free areas that only partially overlap + * (staggered edges) still will not fit until the gap between them is freed too. + * packer_get_consumed_area is not narrowed by a release. * * \param[in] packer Packer to release into. * \param[in] area Area to release. Clipped to the packer's margins. Copied. diff --git a/libraries/geom/packer/impl.h b/libraries/geom/packer/impl.h index f8beff00..e17c62c1 100644 --- a/libraries/geom/packer/impl.h +++ b/libraries/geom/packer/impl.h @@ -21,6 +21,12 @@ struct packer int allocedareas; int usedareas; + box_t *placed; /* boxes carved out by packer_place_*, kept + so packer_release can rebuild the free + list minus the ones still live */ + int allocedplaced; + int nplaced; + box_t dims; /* page size */ box_t margins; /* page size minus margins (but not the diff --git a/libraries/geom/packer/packer.c b/libraries/geom/packer/packer.c index a542c5e2..b0715b18 100644 --- a/libraries/geom/packer/packer.c +++ b/libraries/geom/packer/packer.c @@ -40,6 +40,10 @@ packer_t *packer_create(const box_t *dims) p->areas[0] = *dims; p->usedareas = 1; + p->placed = NULL; + p->allocedplaced = 0; + p->nplaced = 0; + p->dims = *dims; p->margins = *dims; @@ -61,6 +65,7 @@ void packer_destroy(packer_t *doomed) if (doomed == NULL) return; + free(doomed->placed); free(doomed->areas); free(doomed); } @@ -396,9 +401,12 @@ int packer_next_width(packer_t *packer, packer_loc_t loc) /* ----------------------------------------------------------------------- */ +static result_t note_placed(packer_t *packer, const box_t *area); + result_t packer_place_at(packer_t *packer, const box_t *area) { - box_t b; + result_t err; + box_t b; /* subtract the margins */ @@ -407,21 +415,82 @@ result_t packer_place_at(packer_t *packer, const box_t *area) if (box_is_empty(&b)) return result_PACKER_EMPTY; - return remove_area(packer, &b); + err = remove_area(packer, &b); + if (err) + return err; + + return note_placed(packer, &b); +} + +/* Rebuild the free list from scratch: the whole margin, minus every box + * currently recorded as placed. Called after a release so repeated + * place/release cycles reclaim the whole page exactly, rather than leaving + * the free list fragmented into unusable slivers. O(placed^2); 'placed' is + * the live box count, a handful in practice. */ +static result_t rebuild_free_list(packer_t *packer) +{ + result_t err; + int i; + + packer->areas[0] = packer->margins; + packer->usedareas = 1; + packer->sorted = 0; + + for (i = 0; i < packer->nplaced; i++) + { + err = remove_area(packer, &packer->placed[i]); + if (err) + return err; + } + + return result_OK; +} + +/* Record 'area' (a rect just carved out of the free list) as placed, so a + * later packer_release can rebuild the free list without it. */ +static result_t note_placed(packer_t *packer, const box_t *area) +{ + if (packer->nplaced + 1 > packer->allocedplaced) + { + int n; + box_t *p; + + n = packer->allocedplaced ? packer->allocedplaced * 2 : INITIALAREAS; + p = realloc(packer->placed, n * sizeof(*p)); + if (p == NULL) + return result_OOM; + + packer->placed = p; + packer->allocedplaced = n; + } + + packer->placed[packer->nplaced++] = *area; + + return result_OK; } result_t packer_release(packer_t *packer, const box_t *area) { box_t b; + int i; (void) box_intersection(&packer->margins, area, &b); if (box_is_empty(&b)) return result_PACKER_EMPTY; - /* ponytail: no coalescing with neighbouring free areas -- a released - * fragment is usable on its own, which is all whole-box placement needs; - * tile-style reuse spanning two released areas would need a merge pass */ + /* forget the matching placed box (exact match, else closest by origin); + * a release with no recorded placement just adds free space directly */ + for (i = 0; i < packer->nplaced; i++) + { + if (packer->placed[i].x0 == b.x0 && packer->placed[i].y0 == b.y0 && + packer->placed[i].x1 == b.x1 && packer->placed[i].y1 == b.y1) + { + packer->placed[i] = packer->placed[--packer->nplaced]; + return rebuild_free_list(packer); + } + } + return add_area(packer, &b); } @@ -520,6 +589,10 @@ result_t packer_place_by(packer_t *packer, if (err) return err; + err = note_placed(packer, &consume); + if (err) + return err; + if (pos) *pos = &packer->placed_area; @@ -579,6 +652,11 @@ result_t packer_clear(packer_t *packer, packer_cleardir_t clear) if (err) return err; + /* the cleared swathe crosses placed boxes without a matching release; drop + * the placed list so a later packer_release falls back to adding free + * space directly rather than resurrecting cleared area on a rebuild */ + packer->nplaced = 0; + return result_OK; } diff --git a/libraries/geom/packer/test/packer-test.c b/libraries/geom/packer/test/packer-test.c index 523c4f50..dcece14b 100644 --- a/libraries/geom/packer/test/packer-test.c +++ b/libraries/geom/packer/test/packer-test.c @@ -507,6 +507,119 @@ static int test3(void) return 0; +failure: + + packer_destroy(packer); + return 1; +} + +/* count_and_last: packer_map callback recording the free-area count and the + * last area visited. */ +struct scan +{ + int count; + box_t last; +}; + +static result_t count_and_last(const box_t *area, void *opaque) +{ + struct scan *s = opaque; + + s->count++; + s->last = *area; + + return result_OK; +} + +/* packer_release coalescing: filling a page with mixed sizes then releasing + * every slot must collapse the free list back to the whole page, so a + * full-page box fits again. */ +static int test4(void) +{ + static const box_t pagedims = { 0, 0, 300, 300 }; + + static const int w[6] = { 40, 55, 40, 70, 40, 50 }; + static const int h[6] = { 30, 40, 30, 35, 30, 45 }; + + packer_t *packer; + const box_t *slot; + box_t held[6]; + box_t full; + struct scan s; + result_t err; + int cycle; + int i; + + printf("test4: packer_release coalescing\n"); + + packer = packer_create(&pagedims); + if (packer == NULL) + return 1; + + packer_set_gutter(packer, 4); + + /* several open/close rounds: the free list must not drift */ + for (cycle = 0; cycle < 4; cycle++) + { + for (i = 0; i < 6; i++) + { + err = packer_place_by(packer, packer_LOC_BOTTOM_LEFT, w[i], h[i], &slot); + if (err) + { + printf("test4: cycle %d place %d failed (%d)\n", cycle, i, err); + goto failure; + } + + /* release what place_by consumed: the box plus its gutter strip */ + held[i] = *slot; + held[i].x1 = slot->x1 + 4; + held[i].y1 = slot->y1 + 4; + } + + for (i = 5; i >= 0; i--) + { + err = packer_release(packer, &held[i]); + if (err) + { + printf("test4: cycle %d release %d failed (%d)\n", cycle, i, err); + goto failure; + } + } + + /* everything is back: exactly one free area, the whole margin */ + s.count = 0; + err = packer_map(packer, count_and_last, &s); + if (err) + goto failure; + + if (s.count != 1) + { + printf("test4: cycle %d left %d free areas, wanted 1\n", cycle, s.count); + goto failure; + } + + if (s.last.x0 != 0 || s.last.y0 != 0 || + s.last.x1 != 300 || s.last.y1 != 300) + { + printf("test4: cycle %d free area <%d,%d-%d,%d>, wanted <0,0-300,300>\n", + cycle, s.last.x0, s.last.y0, s.last.x1, s.last.y1); + goto failure; + } + } + + /* and a box spanning the whole page now fits */ + full.x0 = 0; full.y0 = 0; full.x1 = 300; full.y1 = 300; + err = packer_place_at(packer, &full); + if (err) + { + printf("test4: full-page placement after reclaim failed (%d)\n", err); + goto failure; + } + + packer_destroy(packer); + return 0; + + failure: packer_destroy(packer); @@ -531,6 +644,10 @@ result_t packer_test(const char *resources) if (err) goto failure; + err = test4(); + if (err) + goto failure; + return result_TEST_PASSED; From 0f6c5ef7e7837dac19b954142ff0463de390e321 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Mon, 31 Aug 2026 20:04:38 +0100 Subject: [PATCH 108/287] docs(packer): document packer_release and exact space reclaim --- docs/geom/packer.md | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/docs/geom/packer.md b/docs/geom/packer.md index e9e65e03..6cb5a7c2 100644 --- a/docs/geom/packer.md +++ b/docs/geom/packer.md @@ -64,6 +64,27 @@ result_t packer_place_at(packer_t *packer, This places an element at a specific location, ignoring margins. +## Releasing Elements + +Use `packer_release()` to hand a previously placed area back to the free pool: + +```C +result_t packer_release(packer_t *packer, + const box_t *area); +``` + +Pass the same box that `packer_place_at()` placed, or the area consumed by +`packer_place_by()` (the returned footprint plus its gutter strip). The packer +matches it against its record of placed boxes and rebuilds the free list from +the container minus every box still live, so repeated place/release cycles +reclaim the whole area exactly rather than fragmenting it into unusable +slivers. `area` is clipped to the margins and copied. + +A release whose box matches no recorded placement (for example after +`packer_clear()`, which drops the placement record) simply adds the area to +the free pool without a rebuild. `packer_get_consumed_area()` is not narrowed +by a release. + ## Layout Management Use `packer_clear()` to move past consumed areas: @@ -106,7 +127,8 @@ typedef result_t (packer_map_fn_t)(const box_t *area, void *opaque); The packer can return these specific result codes: - `result_PACKER_DIDNT_FIT` - Element couldn't be placed in available space -- `result_PACKER_EMPTY` - No elements have been placed yet +- `result_PACKER_EMPTY` - No elements have been placed yet, or a released area + lay entirely outside the margins ## Cleanup From 7f4dc1cefb707513d875a7b1d1bd3e78530d8cdf Mon Sep 17 00:00:00 2001 From: David Thomas Date: Mon, 31 Aug 2026 20:13:37 +0100 Subject: [PATCH 109/287] docs: Update --- docs/geom/packer.md | 17 +++---------- docs/windowing/wuss.md | 56 ++++++++++++++++++++---------------------- 2 files changed, 30 insertions(+), 43 deletions(-) diff --git a/docs/geom/packer.md b/docs/geom/packer.md index 6cb5a7c2..d18002f5 100644 --- a/docs/geom/packer.md +++ b/docs/geom/packer.md @@ -73,17 +73,9 @@ result_t packer_release(packer_t *packer, const box_t *area); ``` -Pass the same box that `packer_place_at()` placed, or the area consumed by -`packer_place_by()` (the returned footprint plus its gutter strip). The packer -matches it against its record of placed boxes and rebuilds the free list from -the container minus every box still live, so repeated place/release cycles -reclaim the whole area exactly rather than fragmenting it into unusable -slivers. `area` is clipped to the margins and copied. - -A release whose box matches no recorded placement (for example after -`packer_clear()`, which drops the placement record) simply adds the area to -the free pool without a rebuild. `packer_get_consumed_area()` is not narrowed -by a release. +Pass the same box that `packer_place_at()` placed, or the area consumed by `packer_place_by()` (the returned footprint plus its gutter strip). The packer matches it against its record of placed boxes and rebuilds the free list from the container minus every box still live, so repeated place/release cycles reclaim the whole area exactly rather than fragmenting it into unusable slivers. `area` is clipped to the margins and copied. + +A release whose box matches no recorded placement (for example after `packer_clear()`, which drops the placement record) simply adds the area to the free pool without a rebuild. `packer_get_consumed_area()` is not narrowed by a release. ## Layout Management @@ -127,8 +119,7 @@ typedef result_t (packer_map_fn_t)(const box_t *area, void *opaque); The packer can return these specific result codes: - `result_PACKER_DIDNT_FIT` - Element couldn't be placed in available space -- `result_PACKER_EMPTY` - No elements have been placed yet, or a released area - lay entirely outside the margins +- `result_PACKER_EMPTY` - No elements have been placed yet, or a released area lay entirely outside the margins ## Cleanup diff --git a/docs/windowing/wuss.md b/docs/windowing/wuss.md index 0ff6a7b5..b1d0ef9d 100644 --- a/docs/windowing/wuss.md +++ b/docs/windowing/wuss.md @@ -95,10 +95,10 @@ typedef struct wuss_event wuss_event_kind_t kind; union { - struct { screen_t *scr; const box_t *content; } redraw; - struct { wuss_mouse_action_t action; point_t point; wuss_button_t button; } mouse; + struct { screen_t *scr; const box_t *content; } redraw; + struct { wuss_mouse_action_t action; point_t point; wuss_button_t button; } mouse; struct { wuss_icon_t *icon; wuss_mouse_action_t action; wuss_button_t button; } icon; - struct { point_t point; int delta; } scroll; + struct { point_t point; int delta; } scroll; } data; } @@ -129,29 +129,28 @@ Each window carries a scroll offset, `(0, 0)` by default: the point in the task' ### How the coordinate spaces relate -Everything a redraw callback gets is *screen space* except `scroll`, which is a -*virtual content space* offset. +Everything a redraw callback gets is _screen space_ except `scroll`, which is a _virtual content space_ offset. ``` screen (0,0) --- wuss->scr, origin top-left - +-------------------------------------------------+ - | win->visible (whole on-screen footprint) | - | +-----------------------------------------+ | - | | titlebar + 1px outline (carved off) | | - | +--------------------------------+--------+ | wuss__content_box() = - | | content box = redraw.bounds | vscroll| | visible - | | (bounds.x0,y0) . | (carve)| | - outline - | | . . +-------------+ .| | | - titlebar - | | . | redraw. | this .| | | - furniture carve - | | . | content | piece .| | | - | | . | (one clip | only .| | | - | | . | piece) | .| | | - | | . +-------------+ .| | | - | | . . . . . . . . . . . . . . .| | | - | +--------------------------------+--------+ | - | | hscroll (carve) | | - | +-----------------------------------------+ | - +-------------------------------------------------+ + +-----------------------------------------------+ + | win->visible (whole on-screen footprint) | + | +-----------------------------------------+ | + | | titlebar + 1px outline (carved off) | | + | +--------------------------------+--------+ | wuss__content_box() = + | | content box = redraw.bounds | vscroll| | visible + | | (bounds.x0,y0) . | (carve)| | - outline + | | . . +-------------+ .| | | - titlebar + | | . | redraw. | this .| | | - furniture carve + | | . | content | piece .| | | + | | . | (one clip | only .| | | + | | . | piece) | .| | | + | | . +-------------+ .| | | + | | . . . . . . . . . . . . . . .| | | + | +--------------------------------+--------+ | + | | hscroll (carve) | | + | +-----------------------------------------+ | + +-----------------------------------------------+ virtual content space: size win->doc, its own origin (0,0). win->scroll = which point of doc sits at bounds.x0,y0. @@ -169,15 +168,12 @@ virtual content space: size win->doc, its own origin (0,0). Conversions (as used in `mouse-move.c`, `scroll.c`, `scroll-step.c`): ``` -screen -> document: doc.x = screen.x - bounds.x0 + scroll.x (same for y) -document -> screen: screen.x = bounds.x0 - scroll.x + doc.x -scroll clamp: max = doc - (bounds.x1 - bounds.x0); clamp(scroll, 0, max) +screen -> document: doc.x = screen.x - bounds.x0 + scroll.x (same for y) +document -> screen: screen.x = bounds.x0 - scroll.x + doc.x +scroll clamp: max = doc - (bounds.x1 - bounds.x0); clamp(scroll, 0, max) ``` -In a redraw callback: start drawing at `bounds.x0 - scroll.x`, -`bounds.y0 - scroll.y`, then paint the whole content normally — the framebuffer -clip (`scr->clip` = `redraw.content`) discards anything outside the piece. See -`libraries/wuss/test/tasks/text.c`. +In a redraw callback: start drawing at `bounds.x0 - scroll.x`, `bounds.y0 - scroll.y`, then paint the whole content normally — the framebuffer clip (`scr->clip` = `redraw.content`) discards anything outside the piece. See `libraries/wuss/test/tasks/text.c`. ## Redrawing From a28eecd806937b7233c2e8540ed5750b38ddc466 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Mon, 31 Aug 2026 21:48:47 +0100 Subject: [PATCH 110/287] feat(wuss): add grouping-frame icon type and label justification New wuss_ICON_TYPE_FRAME draws a one-pixel grouping box with the top edge broken around an optional caption, RISC OS group-box style. Not interactive: clicks fall through as wuss_EVENT_MOUSE (hit-test already matches BUTTON only, so no change there). wuss_ICON_TYPE_LABEL gains wuss_ICON_FLAGS_JUSTIFY_RIGHT / _CENTRE; the label draw path now measures the text and positions it left (default), right or centred within the bbox. Frame caption geometry is WUSS_FRAME_CAPTION_INSET / _PAD in impl.h. The icons test task shows a frame with a right- and a centre-justified label. First stage of the staged wuss UI toolkit plan. Co-Authored-By: Claude Sonnet 5 --- include/wuss/icon.h | 31 +++++++++---- libraries/wuss/icon/create.c | 3 +- libraries/wuss/icon/draw.c | 76 ++++++++++++++++++++++++++++++- libraries/wuss/impl.h | 5 ++ libraries/wuss/test/tasks/icons.c | 36 +++++++++++++-- 5 files changed, 134 insertions(+), 17 deletions(-) diff --git a/include/wuss/icon.h b/include/wuss/icon.h index be9eaa18..ad41b81f 100644 --- a/include/wuss/icon.h +++ b/include/wuss/icon.h @@ -57,23 +57,36 @@ typedef enum wuss_icon_type * and pressed-state visual feedback; clicks and * hovers are delivered to the task as * wuss_EVENT_ICON. */ - wuss_ICON_TYPE_PATTERN /**< Bounding box filled with a repeating two-colour + wuss_ICON_TYPE_PATTERN, /**< Bounding box filled with a repeating two-colour * 8x8 tile (spec.pattern) in fg/bg, phased to * document space so it scrolls rigidly with * content. Not interactive: clicks fall through * to the task as wuss_EVENT_MOUSE. text is * ignored. */ + wuss_ICON_TYPE_FRAME /**< A grouping box: a one-pixel rectangle in fg + * around the bounding box, broken at the top-left + * for an optional caption (text) drawn over the + * window background. Not interactive: clicks fall + * through to the task as wuss_EVENT_MOUSE. */ } wuss_icon_type_t; /** Icon appearance and behaviour flags, combinable with bitwise OR. */ typedef enum wuss_icon_flags { - wuss_ICON_FLAGS_NONE = 0, - wuss_ICON_FLAGS_HIDDEN = 1 << 0, /**< Not drawn, not hit-tested. */ - wuss_ICON_FLAGS_DISABLED = 1 << 1 /**< Drawn greyed; clicks fall through to - * the task as wuss_EVENT_MOUSE rather - * than raising wuss_EVENT_ICON. */ + wuss_ICON_FLAGS_NONE = 0, + wuss_ICON_FLAGS_HIDDEN = 1 << 0, /**< Not drawn, not hit-tested. */ + wuss_ICON_FLAGS_DISABLED = 1 << 1, /**< Drawn greyed; clicks fall through + * to the task as wuss_EVENT_MOUSE + * rather than raising + * wuss_EVENT_ICON. */ + wuss_ICON_FLAGS_JUSTIFY_RIGHT = 1 << 2, /**< wuss_ICON_TYPE_LABEL: right-align + * the text in the bounding box + * instead of the default left. */ + wuss_ICON_FLAGS_JUSTIFY_CENTRE = 1 << 3 /**< wuss_ICON_TYPE_LABEL: centre the + * text in the bounding box. Takes + * precedence over + * wuss_ICON_FLAGS_JUSTIFY_RIGHT. */ } wuss_icon_flags_t; @@ -94,9 +107,9 @@ typedef struct wuss_icon_spec wuss_colour_t fg; /**< Text colour, as an index into the system * palette. */ wuss_colour_t bg; /**< Fill/bevel base colour, as an index into the - * system palette. A label may pass - * wuss_NO_BACKGROUND for text with no fill; a - * button or pattern icon must pass a real + * system palette. A label or frame may pass + * wuss_NO_BACKGROUND for text/outline with no + * fill; a button or pattern icon must pass a real * index. */ screen_pattern_t pattern; /**< Tile for wuss_ICON_TYPE_PATTERN; ignored by * other types. Zero (screen_PATTERN_SOLID) is a diff --git a/libraries/wuss/icon/create.c b/libraries/wuss/icon/create.c index 86686b56..96b6f294 100644 --- a/libraries/wuss/icon/create.c +++ b/libraries/wuss/icon/create.c @@ -28,7 +28,8 @@ result_t wuss_icon_create(wuss_window_t *window, if (spec->type != wuss_ICON_TYPE_LABEL && spec->type != wuss_ICON_TYPE_BUTTON && - spec->type != wuss_ICON_TYPE_PATTERN) + spec->type != wuss_ICON_TYPE_PATTERN && + spec->type != wuss_ICON_TYPE_FRAME) return result_WUSS_BAD_ICON; if ((spec->type == wuss_ICON_TYPE_BUTTON || diff --git a/libraries/wuss/icon/draw.c b/libraries/wuss/icon/draw.c index e6102eea..e4edb3f0 100644 --- a/libraries/wuss/icon/draw.c +++ b/libraries/wuss/icon/draw.c @@ -103,10 +103,82 @@ void wuss__icon_draw(wuss_t *wuss, if (have_font) { - point_t pos; + point_t pos; + int interior_w, split_point; + bmfont_width_t width; + + interior_w = (b.x1 - b.x0) - 2; + if (interior_w < 1) + interior_w = 1; + + bmfont_measure(wuss->font, icon->text, (int) strlen(icon->text), + interior_w, &split_point, &width); - pos.x = b.x0 + 1; + if (icon->flags & wuss_ICON_FLAGS_JUSTIFY_CENTRE) + pos.x = b.x0 + ((b.x1 - b.x0) - width) / 2; + else if (icon->flags & wuss_ICON_FLAGS_JUSTIFY_RIGHT) + pos.x = b.x1 - 1 - width; + else + pos.x = b.x0 + 1; pos.y = b.y0 + (b.y1 - b.y0 - font_height) / 2; + + bmfont_draw(wuss->font, scr, icon->text, (int) strlen(icon->text), + fg, bg, &pos, NULL); + } + } + break; + + case wuss_ICON_TYPE_FRAME: + { + colour_t bg; + int cap_w, cap_x, gap_x0, gap_x1, mid_y; + + /* the caption sits over whatever ground the label case would blend + * against: an explicit bg, else the window's dominant backdrop, else + * fall back to fg so bmfont still has a blend colour */ + if (icon->bg != wuss_NO_BACKGROUND) + bg = wuss->palette[icon->bg]; + else if (icon->window->bg.colour != wuss_NO_BACKGROUND) + bg = (icon->window->bg.pattern != screen_PATTERN_SOLID) + ? wuss->palette[icon->window->bg.pattern_bg] + : wuss->palette[icon->window->bg.colour]; + else + bg = fg; + + cap_w = 0; + if (have_font) + { + int split_point; + bmfont_width_t width; + + bmfont_measure(wuss->font, icon->text, (int) strlen(icon->text), + (b.x1 - b.x0) - WUSS_FRAME_CAPTION_INSET * 2, + &split_point, &width); + cap_w = width; + } + + mid_y = b.y0 + font_height / 2; + + /* left, right and bottom edges are unbroken */ + screen_draw_line(scr, b.x0, mid_y, b.x0, b.y1 - 1, fg); + screen_draw_line(scr, b.x1 - 1, mid_y, b.x1 - 1, b.y1 - 1, fg); + screen_draw_line(scr, b.x0, b.y1 - 1, b.x1 - 1, b.y1 - 1, fg); + + /* the top edge is broken around the caption */ + 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; + if (gap_x0 > b.x0) + screen_draw_line(scr, b.x0, mid_y, gap_x0, mid_y, fg); + if (gap_x1 < b.x1 - 1) + screen_draw_line(scr, gap_x1, mid_y, b.x1 - 1, mid_y, fg); + + if (have_font && cap_w > 0) + { + point_t pos; + + pos.x = cap_x; + pos.y = b.y0; bmfont_draw(wuss->font, scr, icon->text, (int) strlen(icon->text), fg, bg, &pos, NULL); } diff --git a/libraries/wuss/impl.h b/libraries/wuss/impl.h index 5982e9ed..71a1c856 100644 --- a/libraries/wuss/impl.h +++ b/libraries/wuss/impl.h @@ -35,6 +35,11 @@ #define WUSS_PLACE_GUTTER 6 /* px left between windows auto-placed by wuss_window_create_placed */ #define WUSS_BUTTON_INSET 3 /* shared by close/back/toggle/resize furniture buttons and scrollbar breadth */ + +#ifdef WUSS_ICONS +#define WUSS_FRAME_CAPTION_INSET 8 /* x offset of a wuss_ICON_TYPE_FRAME caption from the frame's left edge */ +#define WUSS_FRAME_CAPTION_PAD 2 /* gap left in the frame's top edge either side of the caption */ +#endif #define WUSS_MIN_CONTENT 20 /* resize-drag floor: content can never be squeezed smaller than this */ #define WUSS_SCROLL_INSET 2 /* sausage cross-axis margin from its well's edges, purely cosmetic */ #define WUSS_DIVIDER_PX 1 /* interior rule between the content area and the furniture on its right/bottom */ diff --git a/libraries/wuss/test/tasks/icons.c b/libraries/wuss/test/tasks/icons.c index 2e8084ac..a3e150ca 100644 --- a/libraries/wuss/test/tasks/icons.c +++ b/libraries/wuss/test/tasks/icons.c @@ -27,11 +27,14 @@ result_t icons_create(wuss_t *wuss, bmfont_t *font, icons_task_t *task) { - /* [0..3] plus one swatch per built-in pattern */ + /* [0..3], one swatch per built-in pattern, then [.. +3] a grouping frame + * with two differently-justified labels inside it */ + enum { ICONS_NSPECS = 4 + screen_PATTERN__LIMIT + 3 }; wuss_task_t delegate; - wuss_icon_spec_t specs[4 + screen_PATTERN__LIMIT]; - wuss_icon_t *made[4 + screen_PATTERN__LIMIT]; + wuss_icon_spec_t specs[ICONS_NSPECS]; + wuss_icon_t *made[ICONS_NSPECS]; int p; + int g; /* index of the first frame spec */ result_t rc; task->font = font; @@ -102,8 +105,31 @@ result_t icons_create(wuss_t *wuss, specs[4 + p].pattern = (screen_pattern_t) p; } - rc = wuss_icon_create_array(task->window, specs, - 4 + screen_PATTERN__LIMIT, made); + /* [g] a grouping frame down the document, with [g+1] a right-justified and + * [g+2] a centred label sat inside it */ + g = 4 + screen_PATTERN__LIMIT; + + specs[g].bbox = (box_t) BOX_POS_SIZE(28, 300, 170, 70); + specs[g].type = wuss_ICON_TYPE_FRAME; + specs[g].text = "Grouping frame"; + specs[g].fg = palette_PICO8_DARK_BLUE; + specs[g].bg = wuss_NO_BACKGROUND; + + specs[g + 1].bbox = (box_t) BOX_POS_SIZE(38, 320, 150, 14); + specs[g + 1].type = wuss_ICON_TYPE_LABEL; + specs[g + 1].text = "right"; + specs[g + 1].fg = palette_PICO8_DARK_BLUE; + specs[g + 1].bg = wuss_NO_BACKGROUND; + specs[g + 1].flags = wuss_ICON_FLAGS_JUSTIFY_RIGHT; + + specs[g + 2].bbox = (box_t) BOX_POS_SIZE(38, 342, 150, 14); + specs[g + 2].type = wuss_ICON_TYPE_LABEL; + specs[g + 2].text = "centre"; + specs[g + 2].fg = palette_PICO8_DARK_BLUE; + specs[g + 2].bg = wuss_NO_BACKGROUND; + specs[g + 2].flags = wuss_ICON_FLAGS_JUSTIFY_CENTRE; + + rc = wuss_icon_create_array(task->window, specs, ICONS_NSPECS, made); if (rc != result_OK) goto failure; From 16fb5d631f9dd1c147e221877bc3ba9e0c1f0eab Mon Sep 17 00:00:00 2001 From: David Thomas Date: Mon, 31 Aug 2026 21:52:01 +0100 Subject: [PATCH 111/287] feat(wuss): add default action button styling for work-area buttons A wuss_ICON_TYPE_BUTTON created with the new wuss_ICON_FLAGS_DEFAULT flag draws as a flat accent-filled rectangle inside a one-pixel accent-text border, standing out from the bevelled ordinary buttons around it (RISC OS's default action button). The accent colours come from the new wuss_config_t::accent {bg, fg} pair, cached on struct wuss beside the bevel shades and validated the same way; both default to the titlebar colours when config is NULL. apps/wuss/main.c sets config.accent (its config is assigned field by field, so an unset sub-struct would be indeterminate). The icons test task marks its 'Press me' button as the default. Second stage of the staged wuss UI toolkit plan. Co-Authored-By: Claude Sonnet 5 --- apps/wuss/main.c | 2 ++ include/wuss/icon.h | 12 +++++++++--- include/wuss/wuss.h | 16 +++++++++++++++- libraries/wuss/create.c | 23 +++++++++++++++++++--- libraries/wuss/icon/draw.c | 32 +++++++++++++++++++++++-------- libraries/wuss/impl.h | 2 ++ libraries/wuss/test/tasks/icons.c | 14 ++++++++------ 7 files changed, 80 insertions(+), 21 deletions(-) diff --git a/apps/wuss/main.c b/apps/wuss/main.c index b04bb258..02f5414f 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -357,6 +357,8 @@ static result_t run_wuss(const char *resources) config.palette.scroll.sausages = palette_PICO8_LIGHT_GREY; config.bevel.light = palette_PICO8_WHITE; config.bevel.dark = palette_PICO8_DARK_GREY; + config.accent.bg = palette_PICO8_DARK_BLUE; + config.accent.fg = palette_PICO8_WHITE; config.backdrop.colour = palette_PICO8_WHITE; config.backdrop.pattern = screen_PATTERN_DOTS; config.backdrop.pattern_bg = palette_PICO8_LIGHT_GREY; diff --git a/include/wuss/icon.h b/include/wuss/icon.h index ad41b81f..3b693577 100644 --- a/include/wuss/icon.h +++ b/include/wuss/icon.h @@ -83,10 +83,16 @@ typedef enum wuss_icon_flags wuss_ICON_FLAGS_JUSTIFY_RIGHT = 1 << 2, /**< wuss_ICON_TYPE_LABEL: right-align * the text in the bounding box * instead of the default left. */ - wuss_ICON_FLAGS_JUSTIFY_CENTRE = 1 << 3 /**< wuss_ICON_TYPE_LABEL: centre the - * text in the bounding box. Takes - * precedence over + wuss_ICON_FLAGS_JUSTIFY_CENTRE = 1 << 3, /**< wuss_ICON_TYPE_LABEL: centre + * the text in the bounding box. + * Takes precedence over * wuss_ICON_FLAGS_JUSTIFY_RIGHT. */ + wuss_ICON_FLAGS_DEFAULT = 1 << 4 /**< 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_flags_t; diff --git a/include/wuss/wuss.h b/include/wuss/wuss.h index 14b4bd6d..b36a36af 100644 --- a/include/wuss/wuss.h +++ b/include/wuss/wuss.h @@ -219,7 +219,7 @@ wuss_backdrop_t; * Optional creation-time configuration. * * \note titlebar_height and palette are ignored when the library is built with - * WUSS_FURNITURE off; bevel is ignored when built with both + * 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. */ @@ -247,6 +247,20 @@ typedef struct wuss_config } 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. + */ + struct + { + wuss_colour_t bg; /**< Default-button fill. */ + wuss_colour_t fg; /**< Default-button text. */ + } + accent; + /** Desktop background, painted behind windows on every redraw. */ wuss_backdrop_t backdrop; } diff --git a/libraries/wuss/create.c b/libraries/wuss/create.c index 767f415f..13172f64 100644 --- a/libraries/wuss/create.c +++ b/libraries/wuss/create.c @@ -26,10 +26,14 @@ static const colour_t wuss__default_palette[] = * and the freed-pointer set stay in one place. */ static result_t validate_bevel_backdrop(const wuss_t *w, wuss_colour_t blight, - wuss_colour_t bdark) + wuss_colour_t bdark, + wuss_colour_t abg, + wuss_colour_t afg) { if (blight < 0 || blight >= w->npalette || bdark < 0 || bdark >= w->npalette || + abg < 0 || abg >= w->npalette || + afg < 0 || afg >= w->npalette || wuss__validate_backdrop(w, &w->backdrop) != result_OK) return result_WUSS_BAD_COLOUR; @@ -51,6 +55,7 @@ result_t wuss_create(screen_t *scr, #endif #if defined(WUSS_FURNITURE) || defined(WUSS_ICONS) wuss_colour_t blight, bdark; + wuss_colour_t abg, afg; #endif #ifdef WUSS_FURNITURE int font_height; @@ -101,6 +106,8 @@ result_t wuss_create(screen_t *scr, pal = config->palette; blight = config->bevel.light; bdark = config->bevel.dark; + abg = config->accent.bg; + afg = config->accent.fg; } else { @@ -119,6 +126,8 @@ result_t wuss_create(screen_t *scr, blight = 0; bdark = 0; + abg = bg; /* default action button: the titlebar colours */ + afg = fg; } if (pal.title.bg < 0 || pal.title.bg >= w->npalette || @@ -130,7 +139,7 @@ result_t wuss_create(screen_t *scr, pal.scroll.arrows < 0 || pal.scroll.arrows >= w->npalette || pal.scroll.wells < 0 || pal.scroll.wells >= w->npalette || pal.scroll.sausages < 0 || pal.scroll.sausages >= w->npalette || - validate_bevel_backdrop(w, blight, bdark) != result_OK) + validate_bevel_backdrop(w, blight, bdark, abg, afg) != result_OK) { free(w->palette); free(w); @@ -140,6 +149,8 @@ result_t wuss_create(screen_t *scr, w->furniture_colours = pal; w->bevel_light = blight; w->bevel_dark = bdark; + w->accent_bg = abg; + w->accent_fg = afg; if (config != NULL && config->titlebar_height > 0) { @@ -161,13 +172,17 @@ result_t wuss_create(screen_t *scr, { blight = config->bevel.light; bdark = config->bevel.dark; + abg = config->accent.bg; + afg = config->accent.fg; } else { blight = 0; bdark = 0; + abg = 0; + afg = (w->npalette > 1) ? 1 : 0; } - if (validate_bevel_backdrop(w, blight, bdark) != result_OK) + if (validate_bevel_backdrop(w, blight, bdark, abg, afg) != result_OK) { free(w->palette); free(w); @@ -175,6 +190,8 @@ result_t wuss_create(screen_t *scr, } w->bevel_light = blight; w->bevel_dark = bdark; + w->accent_bg = abg; + w->accent_fg = afg; #else if (wuss__validate_backdrop(w, &w->backdrop) != result_OK) { diff --git a/libraries/wuss/icon/draw.c b/libraries/wuss/icon/draw.c index e4edb3f0..902ff2da 100644 --- a/libraries/wuss/icon/draw.c +++ b/libraries/wuss/icon/draw.c @@ -187,16 +187,32 @@ void wuss__icon_draw(wuss_t *wuss, case wuss_ICON_TYPE_BUTTON: { - colour_t light, dark, base; - int pressed; + colour_t light, dark, base, label; + int pressed, is_default; - base = wuss->palette[icon->bg]; - light = wuss->palette[wuss->bevel_light]; - dark = wuss->palette[wuss->bevel_dark]; - pressed = icon->pressed; + pressed = icon->pressed; + is_default = (icon->flags & wuss_ICON_FLAGS_DEFAULT) != 0; + + if (is_default) + { + /* default action button: a flat accent-filled rectangle inside a + * one-pixel accent-text border, distinct from the bevelled ordinary + * buttons around it */ + base = wuss->palette[wuss->accent_bg]; + light = wuss->palette[wuss->accent_fg]; + dark = light; + label = wuss->palette[wuss->accent_fg]; + } + else + { + base = wuss->palette[icon->bg]; + light = wuss->palette[wuss->bevel_light]; + dark = wuss->palette[wuss->bevel_dark]; + label = fg; + } if (icon->flags & wuss_ICON_FLAGS_DISABLED) - fg = dark; /* greyed: label sinks toward the dark bevel shade */ + label = wuss->palette[wuss->bevel_dark]; /* greyed: sink toward dark */ if (pressed) icon_bevel(scr, &b, base, dark, light); @@ -225,7 +241,7 @@ void wuss__icon_draw(wuss_t *wuss, } bmfont_draw(wuss->font, scr, icon->text, (int) strlen(icon->text), - fg, base, &pos, NULL); + label, base, &pos, NULL); } } break; diff --git a/libraries/wuss/impl.h b/libraries/wuss/impl.h index 71a1c856..169ab7d2 100644 --- a/libraries/wuss/impl.h +++ b/libraries/wuss/impl.h @@ -66,6 +66,8 @@ struct wuss #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 */ #endif wuss_backdrop_t backdrop; /* colour==wuss_NO_BACKGROUND: none */ #ifdef WUSS_FURNITURE diff --git a/libraries/wuss/test/tasks/icons.c b/libraries/wuss/test/tasks/icons.c index a3e150ca..ff8612e5 100644 --- a/libraries/wuss/test/tasks/icons.c +++ b/libraries/wuss/test/tasks/icons.c @@ -73,12 +73,14 @@ result_t icons_create(wuss_t *wuss, specs[0].fg = palette_PICO8_DARK_BLUE; specs[0].bg = wuss_NO_BACKGROUND; - /* [1] the button that bumps the counter */ - specs[1].bbox = (box_t) BOX_POS_SIZE(28, 50, 80, 22); - specs[1].type = wuss_ICON_TYPE_BUTTON; - specs[1].text = "Press me"; - specs[1].fg = palette_PICO8_BLACK; - specs[1].bg = palette_PICO8_LIGHT_GREY; + /* [1] the button that bumps the counter -- a default action button, so it + * shows the accent styling */ + specs[1].bbox = (box_t) BOX_POS_SIZE(28, 50, 80, 22); + specs[1].type = wuss_ICON_TYPE_BUTTON; + specs[1].text = "Press me"; + specs[1].fg = palette_PICO8_BLACK; + specs[1].bg = palette_PICO8_LIGHT_GREY; + specs[1].flags = wuss_ICON_FLAGS_DEFAULT; /* [2] the counter label beside it */ specs[2].bbox = (box_t) BOX_POS_SIZE(120, 50, 120, 22); From 408df3f24f3e77f5b7cb5cbfcd9ddd43ea95b4e3 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Mon, 31 Aug 2026 22:07:22 +0100 Subject: [PATCH 112/287] feat(wuss): add radio and option button icons New icon types wuss_ICON_TYPE_RADIO and wuss_ICON_TYPE_OPTION with latched selected state on the icon (not the task). Radios take an int group; selecting one clears every other selected radio on the same window sharing that non-zero group. Options toggle independently. New public API wuss_icon_get_selected / wuss_icon_set_selected; the latter respects group exclusivity but delivers no task event. A user click latches the new state on MOUSE_UP before wuss_EVENT_ICON fires, so the handler sees it; ADJUST-click toggles a radio. Glyphs are primitive-drawn (square ring + centre fill for radio, box + tick for option), font-height, at the left of the bbox with the label to the right. hit-test.c makes both types interactive. The icons test task grows a three-radio group, a standalone option and a label echoing the last change. Co-Authored-By: Claude Sonnet 5 --- CMakeLists.txt | 2 + include/wuss/icon.h | 49 +++++++++++++++-- libraries/wuss/icon.h | 11 +++- libraries/wuss/icon/create.c | 14 +++-- libraries/wuss/icon/draw.c | 88 ++++++++++++++++++++++++++++++ libraries/wuss/icon/get-selected.c | 12 ++++ libraries/wuss/icon/hit-test.c | 4 +- libraries/wuss/icon/set-selected.c | 51 +++++++++++++++++ libraries/wuss/mouse-click.c | 9 +++ libraries/wuss/test/tasks/icons.c | 54 ++++++++++++++++-- libraries/wuss/test/tasks/icons.h | 2 + 11 files changed, 278 insertions(+), 18 deletions(-) create mode 100644 libraries/wuss/icon/get-selected.c create mode 100644 libraries/wuss/icon/set-selected.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 6949ba1f..573c36d0 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -381,6 +381,7 @@ set(WUSS_ICON_SOURCES libraries/wuss/icon/draw.c libraries/wuss/icon/free.c libraries/wuss/icon/get-bbox.c + libraries/wuss/icon/get-selected.c libraries/wuss/icon/get-text.c libraries/wuss/icon/get-type.c libraries/wuss/icon/get-window.c @@ -388,6 +389,7 @@ set(WUSS_ICON_SOURCES libraries/wuss/icon/invalidate.c libraries/wuss/icon/screen-box.c libraries/wuss/icon/set-hidden.c + libraries/wuss/icon/set-selected.c libraries/wuss/icon/set-text.c libraries/wuss/icon.h) diff --git a/include/wuss/icon.h b/include/wuss/icon.h index 3b693577..7deab927 100644 --- a/include/wuss/icon.h +++ b/include/wuss/icon.h @@ -63,11 +63,23 @@ typedef enum wuss_icon_type * content. Not interactive: clicks fall through * to the task as wuss_EVENT_MOUSE. text is * ignored. */ - wuss_ICON_TYPE_FRAME /**< A grouping box: a one-pixel rectangle in fg + wuss_ICON_TYPE_FRAME, /**< A grouping box: a one-pixel rectangle in fg * around the bounding box, broken at the top-left * for an optional caption (text) drawn over the * window background. Not interactive: clicks fall * through to the task as wuss_EVENT_MOUSE. */ + wuss_ICON_TYPE_RADIO, /**< A radio button: a small ring at the left of the + * bounding box, filled when selected, with the + * label (text) to its right. Interactive: a click + * selects it and clears every other selected + * radio sharing its non-zero group, then the task + * is told via wuss_EVENT_ICON. */ + wuss_ICON_TYPE_OPTION /**< An option button: a small box at the left of + * the bounding box, ticked when selected, with + * the label (text) to its right. Interactive: a + * click toggles its own selected state (group is + * ignored), then the task is told via + * wuss_EVENT_ICON. */ } wuss_icon_type_t; @@ -113,13 +125,19 @@ typedef struct wuss_icon_spec wuss_colour_t fg; /**< Text colour, as an index into the system * palette. */ wuss_colour_t bg; /**< Fill/bevel base colour, as an index into the - * system palette. A label or frame may pass - * wuss_NO_BACKGROUND for text/outline with no - * fill; a button or pattern icon must pass a real - * index. */ + * system palette. A label, frame, radio or option + * icon may pass wuss_NO_BACKGROUND for no fill + * behind its text/glyph; a button or pattern icon + * must pass a real index. */ screen_pattern_t pattern; /**< Tile for wuss_ICON_TYPE_PATTERN; ignored by * other types. Zero (screen_PATTERN_SOLID) is a * safe default for zero-initialised specs. */ + int group; /**< 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 clears another. + * Ignored by all other icon types. */ wuss_icon_flags_t flags; /**< Appearance/behaviour flags. */ } wuss_icon_spec_t; @@ -225,6 +243,27 @@ const char *wuss_icon_get_text(const wuss_icon_t *icon); */ wuss_window_t *wuss_icon_get_window(const wuss_icon_t *icon); +/** + * Fetch a radio or option icon's selected (latched) state. + * + * \param[in] icon Icon to query. + * \return Non-zero if selected, zero otherwise. Always zero for icon types that + * have no latched state. + */ +int wuss_icon_get_selected(const wuss_icon_t *icon); + +/** + * Set a radio or option icon's selected state, invalidating it so the next + * redraw repaints it. For a radio with a non-zero group, selecting it (passing + * non-zero) also clears every other selected radio on the same window with that + * group. No task event is delivered -- this is the programmatic path, distinct + * from a user click. A no-op for icon types with no latched state. + * + * \param[in] icon Icon to change. + * \param[in] selected Non-zero to select, zero to deselect. + */ +void wuss_icon_set_selected(wuss_icon_t *icon, int selected); + #endif /* WUSS_ICONS */ #ifdef __cplusplus diff --git a/libraries/wuss/icon.h b/libraries/wuss/icon.h index 13c2c67f..66740565 100644 --- a/libraries/wuss/icon.h +++ b/libraries/wuss/icon.h @@ -19,11 +19,18 @@ struct wuss_icon char *text; /* owned; never NULL ("" instead) */ wuss_colour_t fg; wuss_colour_t bg; - screen_pattern_t pattern; /* wuss_ICON_TYPE_PATTERN tile; 0 otherwise */ + screen_pattern_t pattern; /* wuss_ICON_TYPE_PATTERN tile; 0 otherwise */ + int group; /* radio: exclusive-selection group; 0 = none */ wuss_icon_flags_t flags; - int pressed; /* button: 1 while held with the pointer inside */ + int pressed; /* button: 1 while held with the pointer inside */ + int selected; /* radio/option: 1 while latched on */ }; +/* Set icon->selected, invalidating it. For a radio with a non-zero group, + * selecting it also clears every other selected radio on the same window with + * that group. Ignored for types with no latched state. */ +void wuss__icon_select(wuss_icon_t *icon, int selected); + /* Map an icon bbox (virtual document space) into screen space: * screen = content.x0 - scroll.x + bbox. wuss__icon_draw paints through this * so hit-testing and invalidation cannot drift from what is drawn. */ diff --git a/libraries/wuss/icon/create.c b/libraries/wuss/icon/create.c index 96b6f294..2f3026dd 100644 --- a/libraries/wuss/icon/create.c +++ b/libraries/wuss/icon/create.c @@ -29,7 +29,9 @@ result_t wuss_icon_create(wuss_window_t *window, if (spec->type != wuss_ICON_TYPE_LABEL && spec->type != wuss_ICON_TYPE_BUTTON && spec->type != wuss_ICON_TYPE_PATTERN && - spec->type != wuss_ICON_TYPE_FRAME) + spec->type != wuss_ICON_TYPE_FRAME && + spec->type != wuss_ICON_TYPE_RADIO && + spec->type != wuss_ICON_TYPE_OPTION) return result_WUSS_BAD_ICON; if ((spec->type == wuss_ICON_TYPE_BUTTON || @@ -77,10 +79,12 @@ result_t wuss_icon_create(wuss_window_t *window, it->type = spec->type; it->fg = spec->fg; it->bg = spec->bg; - it->pattern = (spec->type == wuss_ICON_TYPE_PATTERN) ? spec->pattern - : screen_PATTERN_SOLID; - it->flags = spec->flags; - it->pressed = 0; + it->pattern = (spec->type == wuss_ICON_TYPE_PATTERN) ? spec->pattern + : screen_PATTERN_SOLID; + it->group = (spec->type == wuss_ICON_TYPE_RADIO) ? spec->group : 0; + it->flags = spec->flags; + it->pressed = 0; + it->selected = 0; window->icons[window->nicons++] = it; diff --git a/libraries/wuss/icon/draw.c b/libraries/wuss/icon/draw.c index 902ff2da..63f2f566 100644 --- a/libraries/wuss/icon/draw.c +++ b/libraries/wuss/icon/draw.c @@ -245,5 +245,93 @@ void wuss__icon_draw(wuss_t *wuss, } } break; + + case wuss_ICON_TYPE_RADIO: + case wuss_ICON_TYPE_OPTION: + { + colour_t glyph, bg; + box_t g; + int gsz, gy, tx; + + /* the glyph is a square the height of the font (min 8), sat at the left + * of the box and vertically centred; the label follows to its right */ + gsz = (font_height >= 8) ? font_height : 8; + if (gsz > b.y1 - b.y0) + gsz = b.y1 - b.y0; + gy = b.y0 + (b.y1 - b.y0 - gsz) / 2; + + g.x0 = b.x0; + g.y0 = gy; + g.x1 = b.x0 + gsz; + g.y1 = gy + gsz; + + glyph = (icon->flags & wuss_ICON_FLAGS_DISABLED) + ? wuss->palette[wuss->bevel_dark] + : fg; + + /* ground the glyph blends against: explicit bg, else the window backdrop, + * else fg (bmfont still needs a colour) -- same ladder as the label case */ + if (icon->bg != wuss_NO_BACKGROUND) + bg = wuss->palette[icon->bg]; + else if (icon->window->bg.colour != wuss_NO_BACKGROUND) + bg = (icon->window->bg.pattern != screen_PATTERN_SOLID) + ? wuss->palette[icon->window->bg.pattern_bg] + : wuss->palette[icon->window->bg.colour]; + else + bg = glyph; + + if (icon->bg != wuss_NO_BACKGROUND) + screen_draw_rect(scr, b.x0, b.y0, + SIZE2D(b.x1 - b.x0, b.y1 - b.y0), bg); + + if (icon->type == wuss_ICON_TYPE_RADIO) + { + /* a square ring (no circle primitive); a solid centre when selected */ + screen_draw_line(scr, g.x0 + 2, g.y0, g.x1 - 3, g.y0, glyph); + screen_draw_line(scr, g.x0 + 2, g.y1 - 1, g.x1 - 3, g.y1 - 1, glyph); + screen_draw_line(scr, g.x0, g.y0 + 2, g.x0, g.y1 - 3, glyph); + screen_draw_line(scr, g.x1 - 1, g.y0 + 2, g.x1 - 1, g.y1 - 3, glyph); + if (icon->selected) + screen_draw_rect(scr, g.x0 + 3, g.y0 + 3, + SIZE2D(gsz - 6, gsz - 6), glyph); + } + else + { + /* a box; a tick (two strokes) when selected */ + screen_draw_line(scr, g.x0, g.y0, g.x1 - 1, g.y0, glyph); + screen_draw_line(scr, g.x0, g.y1 - 1, g.x1 - 1, g.y1 - 1, glyph); + screen_draw_line(scr, g.x0, g.y0, g.x0, g.y1 - 1, glyph); + screen_draw_line(scr, g.x1 - 1, g.y0, g.x1 - 1, g.y1 - 1, glyph); + if (icon->selected) + { + screen_draw_line(scr, g.x0 + 2, g.y0 + gsz / 2, + g.x0 + gsz / 2 - 1, g.y1 - 3, glyph); + screen_draw_line(scr, g.x0 + gsz / 2 - 1, g.y1 - 3, + g.x1 - 3, g.y0 + 2, glyph); + } + } + + if (have_font) + { + point_t pos; + int interior_w, split_point; + bmfont_width_t width; + + tx = g.x1 + 4; + interior_w = (b.x1 - tx) - 1; + if (interior_w < 1) + interior_w = 1; + + bmfont_measure(wuss->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 - font_height) / 2; + bmfont_draw(wuss->font, scr, icon->text, (int) strlen(icon->text), + glyph, bg, &pos, NULL); + } + } + break; } } diff --git a/libraries/wuss/icon/get-selected.c b/libraries/wuss/icon/get-selected.c new file mode 100644 index 00000000..6d8e06f0 --- /dev/null +++ b/libraries/wuss/icon/get-selected.c @@ -0,0 +1,12 @@ +/* get-selected.c -- wuss - query a radio/option icon's latched state */ + +#include + +#include "../impl.h" + +int wuss_icon_get_selected(const wuss_icon_t *icon) +{ + assert(icon != NULL); + + return icon->selected; +} diff --git a/libraries/wuss/icon/hit-test.c b/libraries/wuss/icon/hit-test.c index d7579e21..ac63f889 100644 --- a/libraries/wuss/icon/hit-test.c +++ b/libraries/wuss/icon/hit-test.c @@ -14,7 +14,9 @@ wuss_icon_t *wuss__icon_hit_test(wuss_window_t *window, point_t doc_point) { it = window->icons[i]; - if (it->type != wuss_ICON_TYPE_BUTTON) + if (it->type != wuss_ICON_TYPE_BUTTON && + it->type != wuss_ICON_TYPE_RADIO && + it->type != wuss_ICON_TYPE_OPTION) continue; if (it->flags & (wuss_ICON_FLAGS_HIDDEN | wuss_ICON_FLAGS_DISABLED)) diff --git a/libraries/wuss/icon/set-selected.c b/libraries/wuss/icon/set-selected.c new file mode 100644 index 00000000..72ec17c4 --- /dev/null +++ b/libraries/wuss/icon/set-selected.c @@ -0,0 +1,51 @@ +/* set-selected.c -- wuss - latch a radio/option icon's selected state */ + +#include + +#include "../impl.h" + +void wuss__icon_select(wuss_icon_t *icon, int selected) +{ + wuss_window_t *window; + wuss_icon_t *other; + int i; + + assert(icon != NULL); + + if (icon->type != wuss_ICON_TYPE_RADIO && + icon->type != wuss_ICON_TYPE_OPTION) + return; + + selected = selected ? 1 : 0; + + /* selecting a grouped radio clears its siblings first */ + if (selected && + icon->type == wuss_ICON_TYPE_RADIO && + icon->group != 0) + { + window = icon->window; + for (i = 0; i < window->nicons; i++) + { + other = window->icons[i]; + if (other == icon) + continue; + if (other->type != wuss_ICON_TYPE_RADIO || other->group != icon->group) + continue; + if (!other->selected) + continue; + other->selected = 0; + wuss__icon_invalidate(other); + } + } + + if (icon->selected == selected) + return; + + icon->selected = selected; + wuss__icon_invalidate(icon); +} + +void wuss_icon_set_selected(wuss_icon_t *icon, int selected) +{ + wuss__icon_select(icon, selected); +} diff --git a/libraries/wuss/mouse-click.c b/libraries/wuss/mouse-click.c index fa4ffa09..ec35940b 100644 --- a/libraries/wuss/mouse-click.c +++ b/libraries/wuss/mouse-click.c @@ -208,6 +208,15 @@ result_t wuss_mouse_click(wuss_t *wuss, icon->pressed = 0; wuss->pressed_icon = NULL; wuss__icon_invalidate(icon); + + /* a completed click latches radio/option state before the task is + * told, so the wuss_EVENT_ICON handler sees the new value */ + if (icon->type == wuss_ICON_TYPE_OPTION) + wuss__icon_select(icon, !icon->selected); + else if (icon->type == wuss_ICON_TYPE_RADIO) + wuss__icon_select(icon, + (button & wuss_BUTTON_ADJUST) ? !icon->selected + : 1); } event.kind = wuss_EVENT_ICON; diff --git a/libraries/wuss/test/tasks/icons.c b/libraries/wuss/test/tasks/icons.c index ff8612e5..feda2626 100644 --- a/libraries/wuss/test/tasks/icons.c +++ b/libraries/wuss/test/tasks/icons.c @@ -28,13 +28,15 @@ result_t icons_create(wuss_t *wuss, icons_task_t *task) { /* [0..3], one swatch per built-in pattern, then [.. +3] a grouping frame - * with two differently-justified labels inside it */ - enum { ICONS_NSPECS = 4 + screen_PATTERN__LIMIT + 3 }; + * with two differently-justified labels inside it, then [.. +5] three grouped + * radios, a standalone option and a label echoing the selection */ + enum { ICONS_NSPECS = 4 + screen_PATTERN__LIMIT + 3 + 5 }; wuss_task_t delegate; wuss_icon_spec_t specs[ICONS_NSPECS]; wuss_icon_t *made[ICONS_NSPECS]; int p; int g; /* index of the first frame spec */ + int r; /* radio index */ result_t rc; task->font = font; @@ -44,6 +46,8 @@ result_t icons_create(wuss_t *wuss, task->button = NULL; task->counter = NULL; task->count = 0; + task->opt = NULL; + task->state = NULL; delegate = wuss_task_start(icons_handle, task); @@ -131,12 +135,38 @@ result_t icons_create(wuss_t *wuss, specs[g + 2].bg = wuss_NO_BACKGROUND; specs[g + 2].flags = wuss_ICON_FLAGS_JUSTIFY_CENTRE; + /* [g+3..g+5] three radios sharing group 1, [g+6] a standalone option, + * [g+7] a label echoing whichever control last changed */ + for (r = 0; r < 3; r++) + { + specs[g + 3 + r].bbox = (box_t) BOX_POS_SIZE(28, 380 + r * 20, 150, 16); + specs[g + 3 + r].type = wuss_ICON_TYPE_RADIO; + specs[g + 3 + r].text = (r == 0) ? "Red" : (r == 1) ? "Green" : "Blue"; + specs[g + 3 + r].fg = palette_PICO8_DARK_BLUE; + specs[g + 3 + r].bg = wuss_NO_BACKGROUND; + specs[g + 3 + r].group = 1; + } + + specs[g + 6].bbox = (box_t) BOX_POS_SIZE(28, 444, 150, 16); + specs[g + 6].type = wuss_ICON_TYPE_OPTION; + specs[g + 6].text = "Wireframe"; + specs[g + 6].fg = palette_PICO8_DARK_BLUE; + specs[g + 6].bg = wuss_NO_BACKGROUND; + + specs[g + 7].bbox = (box_t) BOX_POS_SIZE(28, 464, 180, 14); + specs[g + 7].type = wuss_ICON_TYPE_LABEL; + specs[g + 7].text = "(no selection)"; + specs[g + 7].fg = palette_PICO8_DARK_BLUE; + specs[g + 7].bg = wuss_NO_BACKGROUND; + rc = wuss_icon_create_array(task->window, specs, ICONS_NSPECS, made); if (rc != result_OK) goto failure; task->button = made[1]; task->counter = made[2]; + task->opt = made[g + 6]; + task->state = made[g + 7]; return result_OK; @@ -236,13 +266,27 @@ static result_t icons_redraw(const wuss_event_t *event, void *task_data) static result_t icons_icon(const wuss_event_t *event, void *task_data) { icons_task_t *tcx; - char buf[16]; + wuss_icon_t *icon; + char buf[48]; - tcx = task_data; + tcx = task_data; + icon = event->data.icon.icon; + + /* a radio/option latches on MOUSE_UP -- report the state then */ + if (event->data.icon.action == wuss_MOUSE_UP && + (wuss_icon_get_type(icon) == wuss_ICON_TYPE_RADIO || + wuss_icon_get_type(icon) == wuss_ICON_TYPE_OPTION)) + { + snprintf(buf, sizeof(buf), "%s: %s%s", + wuss_icon_get_text(icon), + wuss_icon_get_selected(icon) ? "on" : "off", + (icon == tcx->opt) ? "" : " (radio)"); + return wuss_icon_set_text(tcx->state, buf); + } if (event->data.icon.action != wuss_MOUSE_DOWN) return result_OK; - if (event->data.icon.icon != tcx->button) + if (icon != tcx->button) return result_OK; tcx->count++; diff --git a/libraries/wuss/test/tasks/icons.h b/libraries/wuss/test/tasks/icons.h index 9e9fb820..29da3f22 100644 --- a/libraries/wuss/test/tasks/icons.h +++ b/libraries/wuss/test/tasks/icons.h @@ -22,6 +22,8 @@ typedef struct icons_task wuss_icon_t *button; /* "Press me" */ wuss_icon_t *counter; /* label showing hit count */ int count; + wuss_icon_t *opt; /* standalone option button */ + wuss_icon_t *state; /* label echoing radio/option selection */ } icons_task_t; From 6018ff6ad92a91dc6ab2e5ae6e0380368e467556 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Mon, 31 Aug 2026 22:31:06 +0100 Subject: [PATCH 113/287] feat(wuss): add plain bitmap icons New icon type wuss_ICON_TYPE_BITMAP draws a caller-owned const bitmap_t * at the top-left of the bbox, alpha-blended and clipped to the box, no scaling. The bitmap is borrowed, not copied, and must outlive the icon (unlike text). New flag wuss_ICON_FLAGS_INTERACTIVE makes a bitmap icon hit-testable and raise wuss_EVENT_ICON like a button; without it the icon is pure decoration and clicks fall through as wuss_EVENT_MOUSE. Other icon types ignore the flag. draw.c narrows a stack copy of the screen's clip to the icon box before the blit. create.c rejects a bitmap spec with no bitmap. hit-test.c treats an interactive bitmap like the other clickable types. The icons test task loads resources/wuss/9tile.png and shows one decorative and one interactive bitmap icon; icons_create gains a resources argument. Co-Authored-By: Claude Sonnet 5 --- apps/wuss/main.c | 2 +- include/wuss/icon.h | 31 +++++++++++-- libraries/wuss/icon.h | 1 + libraries/wuss/icon/create.c | 17 ++++--- libraries/wuss/icon/draw.c | 23 ++++++++++ libraries/wuss/icon/hit-test.c | 15 +++++-- libraries/wuss/test/tasks/icons.c | 75 ++++++++++++++++++++++++------- libraries/wuss/test/tasks/icons.h | 5 +++ 8 files changed, 139 insertions(+), 30 deletions(-) diff --git a/apps/wuss/main.c b/apps/wuss/main.c index 02f5414f..2e623285 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -177,7 +177,7 @@ static result_t spawn_icons(void) icons_task_t *t = calloc(1, sizeof(*t)); result_t rc; if (t == NULL) return result_OOM; - rc = icons_create(g_wuss, g_palette, g_daydream_font, t); + rc = icons_create(g_wuss, g_palette, g_daydream_font, g_resources, t); if (rc != result_OK || t->window == NULL) { free(t); return rc; } return result_OK; } diff --git a/include/wuss/icon.h b/include/wuss/icon.h index 7deab927..17cf28f8 100644 --- a/include/wuss/icon.h +++ b/include/wuss/icon.h @@ -74,12 +74,22 @@ typedef enum wuss_icon_type * selects it and clears every other selected * radio sharing its non-zero group, then the task * is told via wuss_EVENT_ICON. */ - wuss_ICON_TYPE_OPTION /**< An option button: a small box at the left of + wuss_ICON_TYPE_OPTION, /**< An option button: a small box at the left of * the bounding box, ticked when selected, with * the label (text) to its right. Interactive: a * click toggles its own selected state (group is * ignored), then the task is told via * wuss_EVENT_ICON. */ + wuss_ICON_TYPE_BITMAP /**< A caller-owned bitmap (spec.bitmap) drawn at + * the top-left of the bounding box, alpha-blended + * against what is already there, clipped to the + * box; no scaling. The bitmap is borrowed, not + * copied, and must outlive the icon (unlike + * text). fg, bg, text and pattern are ignored. + * Not interactive unless + * wuss_ICON_FLAGS_INTERACTIVE is set, in which + * case clicks raise wuss_EVENT_ICON like a + * button. */ } wuss_icon_type_t; @@ -99,12 +109,20 @@ typedef enum wuss_icon_flags * the text in the bounding box. * Takes precedence over * wuss_ICON_FLAGS_JUSTIFY_RIGHT. */ - wuss_ICON_FLAGS_DEFAULT = 1 << 4 /**< wuss_ICON_TYPE_BUTTON: draw as a + wuss_ICON_FLAGS_DEFAULT = 1 << 4, /**< 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_FLAGS_INTERACTIVE = 1 << 5 /**< 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 clicks fall through + * as wuss_EVENT_MOUSE. Ignored by + * other icon types (interactive or + * not by their nature). */ } wuss_icon_flags_t; @@ -132,6 +150,11 @@ typedef struct wuss_icon_spec screen_pattern_t pattern; /**< Tile for wuss_ICON_TYPE_PATTERN; ignored by * other types. Zero (screen_PATTERN_SOLID) is a * safe default for zero-initialised specs. */ + const bitmap_t *bitmap; /**< 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. */ int group; /**< wuss_ICON_TYPE_RADIO: exclusive-selection group. * Selecting a radio clears every other selected * radio on the same window with the same group. @@ -155,8 +178,8 @@ wuss_icon_spec_t; * does not need it. * \return \ref result_OK on success, \ref result_OOM on allocation failure, * \ref result_WUSS_BAD_COLOUR if fg or bg is out of range for the - * palette, or \ref result_WUSS_BAD_ICON if type is unknown or a button - * spec has no fill colour. + * palette, or \ref result_WUSS_BAD_ICON if type is unknown, a button or + * pattern spec has no fill colour, or a bitmap spec has no bitmap. */ result_t wuss_icon_create(wuss_window_t *window, const wuss_icon_spec_t *spec, diff --git a/libraries/wuss/icon.h b/libraries/wuss/icon.h index 66740565..a638b49f 100644 --- a/libraries/wuss/icon.h +++ b/libraries/wuss/icon.h @@ -20,6 +20,7 @@ struct wuss_icon wuss_colour_t fg; wuss_colour_t bg; screen_pattern_t pattern; /* wuss_ICON_TYPE_PATTERN tile; 0 otherwise */ + const bitmap_t *bitmap; /* wuss_ICON_TYPE_BITMAP image; borrowed, or NULL */ int group; /* radio: exclusive-selection group; 0 = none */ wuss_icon_flags_t flags; int pressed; /* button: 1 while held with the pointer inside */ diff --git a/libraries/wuss/icon/create.c b/libraries/wuss/icon/create.c index 2f3026dd..2a53b1dd 100644 --- a/libraries/wuss/icon/create.c +++ b/libraries/wuss/icon/create.c @@ -31,7 +31,8 @@ result_t wuss_icon_create(wuss_window_t *window, spec->type != wuss_ICON_TYPE_PATTERN && spec->type != wuss_ICON_TYPE_FRAME && spec->type != wuss_ICON_TYPE_RADIO && - spec->type != wuss_ICON_TYPE_OPTION) + spec->type != wuss_ICON_TYPE_OPTION && + spec->type != wuss_ICON_TYPE_BITMAP) return result_WUSS_BAD_ICON; if ((spec->type == wuss_ICON_TYPE_BUTTON || @@ -39,6 +40,9 @@ result_t wuss_icon_create(wuss_window_t *window, spec->bg == wuss_NO_BACKGROUND) return result_WUSS_BAD_ICON; + if (spec->type == wuss_ICON_TYPE_BITMAP && spec->bitmap == NULL) + return result_WUSS_BAD_ICON; + if (spec->type == wuss_ICON_TYPE_PATTERN && (spec->pattern < 0 || spec->pattern >= screen_PATTERN__LIMIT)) return result_WUSS_BAD_ICON; @@ -74,13 +78,14 @@ result_t wuss_icon_create(wuss_window_t *window, } memcpy(it->text, src, len + 1); - it->window = window; - it->bbox = spec->bbox; - it->type = spec->type; - it->fg = spec->fg; - it->bg = spec->bg; + it->window = window; + it->bbox = spec->bbox; + it->type = spec->type; + it->fg = spec->fg; + it->bg = spec->bg; it->pattern = (spec->type == wuss_ICON_TYPE_PATTERN) ? spec->pattern : screen_PATTERN_SOLID; + it->bitmap = (spec->type == wuss_ICON_TYPE_BITMAP) ? spec->bitmap : NULL; it->group = (spec->type == wuss_ICON_TYPE_RADIO) ? spec->group : 0; it->flags = spec->flags; it->pressed = 0; diff --git a/libraries/wuss/icon/draw.c b/libraries/wuss/icon/draw.c index 63f2f566..eb1861a3 100644 --- a/libraries/wuss/icon/draw.c +++ b/libraries/wuss/icon/draw.c @@ -333,5 +333,28 @@ void wuss__icon_draw(wuss_t *wuss, } } break; + + case wuss_ICON_TYPE_BITMAP: + { + screen_t clipped; + + if (icon->bitmap == NULL) + break; + + /* screen_draw_bitmap clips to scr->clip and does not scale, so narrow + * the clip to the icon box (intersected with whatever redraw already + * set) and blit at the box's top-left */ + clipped = *scr; + if (clipped.clip.x0 < b.x0) clipped.clip.x0 = b.x0; + if (clipped.clip.y0 < b.y0) clipped.clip.y0 = b.y0; + if (clipped.clip.x1 > b.x1) clipped.clip.x1 = b.x1; + if (clipped.clip.y1 > b.y1) clipped.clip.y1 = b.y1; + if (clipped.clip.x1 <= clipped.clip.x0 || + clipped.clip.y1 <= clipped.clip.y0) + break; + + screen_draw_bitmap(&clipped, b.x0, b.y0, icon->bitmap); + } + break; } } diff --git a/libraries/wuss/icon/hit-test.c b/libraries/wuss/icon/hit-test.c index ac63f889..9cb66bf2 100644 --- a/libraries/wuss/icon/hit-test.c +++ b/libraries/wuss/icon/hit-test.c @@ -14,10 +14,19 @@ wuss_icon_t *wuss__icon_hit_test(wuss_window_t *window, point_t doc_point) { it = window->icons[i]; - if (it->type != wuss_ICON_TYPE_BUTTON && - it->type != wuss_ICON_TYPE_RADIO && - it->type != wuss_ICON_TYPE_OPTION) + /* a bitmap icon is interactive only when it asks to be; the other + * clickable types always are */ + if (it->type == wuss_ICON_TYPE_BITMAP) + { + if (!(it->flags & wuss_ICON_FLAGS_INTERACTIVE)) + continue; + } + else if (it->type != wuss_ICON_TYPE_BUTTON && + it->type != wuss_ICON_TYPE_RADIO && + it->type != wuss_ICON_TYPE_OPTION) + { continue; + } if (it->flags & (wuss_ICON_FLAGS_HIDDEN | wuss_ICON_FLAGS_DISABLED)) continue; diff --git a/libraries/wuss/test/tasks/icons.c b/libraries/wuss/test/tasks/icons.c index feda2626..2a188b1a 100644 --- a/libraries/wuss/test/tasks/icons.c +++ b/libraries/wuss/test/tasks/icons.c @@ -11,11 +11,13 @@ #include "fortify/fortify.h" #endif +#include "framebuf/bitmap.h" #include "framebuf/palettes.h" #include "framebuf/screen.h" #include "geom/box.h" #include "geom/point.h" #include "geom/size.h" +#include "io/path.h" #include "icons.h" @@ -25,29 +27,40 @@ result_t icons_create(wuss_t *wuss, const colour_t *palette, bmfont_t *font, + const char *resources, icons_task_t *task) { /* [0..3], one swatch per built-in pattern, then [.. +3] a grouping frame * with two differently-justified labels inside it, then [.. +5] three grouped - * radios, a standalone option and a label echoing the selection */ - enum { ICONS_NSPECS = 4 + screen_PATTERN__LIMIT + 3 + 5 }; + * radios, a standalone option and a label echoing the selection, then [.. +2] + * a decorative bitmap icon and an interactive one that bumps the counter */ + enum { ICONS_NSPECS = 4 + screen_PATTERN__LIMIT + 3 + 5 + 2 }; wuss_task_t delegate; wuss_icon_spec_t specs[ICONS_NSPECS]; wuss_icon_t *made[ICONS_NSPECS]; + const char *sprite_path; int p; - int g; /* index of the first frame spec */ - int r; /* radio index */ + int g; /* index of the first frame spec */ + int r; /* radio index */ + int nspecs; /* live spec count (sprite icons are optional) */ result_t rc; - task->font = font; - task->label = palette[palette_PICO8_DARK_BLUE]; - task->paper = palette[palette_PICO8_LIGHT_GREY]; /* the window bg, below */ - task->window = NULL; - task->button = NULL; - task->counter = NULL; - task->count = 0; - task->opt = NULL; - task->state = NULL; + task->font = font; + task->label = palette[palette_PICO8_DARK_BLUE]; + task->paper = palette[palette_PICO8_LIGHT_GREY]; /* the window bg, below */ + task->window = NULL; + task->button = NULL; + task->counter = NULL; + task->count = 0; + task->opt = NULL; + task->state = NULL; + task->has_sprite = 0; + task->hotspot = NULL; + + sprite_path = path_join_filename(resources, 3, "resources", "wuss", + path_join_leafname("9tile", "png")); + if (bitmap_load_png(&task->sprite, sprite_path) == result_OK) + task->has_sprite = 1; delegate = wuss_task_start(icons_handle, task); @@ -63,7 +76,11 @@ result_t icons_create(wuss_t *wuss, SIZE2D(0, 0), &task->window); if (rc != result_OK) + { + if (task->has_sprite) + free(task->sprite.base); return rc; + } memset(specs, 0, sizeof(specs)); @@ -159,7 +176,27 @@ result_t icons_create(wuss_t *wuss, specs[g + 7].fg = palette_PICO8_DARK_BLUE; specs[g + 7].bg = wuss_NO_BACKGROUND; - rc = wuss_icon_create_array(task->window, specs, ICONS_NSPECS, made); + nspecs = g + 8; + + /* [g+8] a decorative bitmap, [g+9] an interactive one that bumps the + * counter -- only if the sprite loaded */ + if (task->has_sprite) + { + specs[g + 8].bbox = (box_t) BOX_POS_SIZE(28, 484, task->sprite.size.w, + task->sprite.size.h); + specs[g + 8].type = wuss_ICON_TYPE_BITMAP; + specs[g + 8].bitmap = &task->sprite; + + specs[g + 9].bbox = (box_t) BOX_POS_SIZE(120, 484, task->sprite.size.w, + task->sprite.size.h); + specs[g + 9].type = wuss_ICON_TYPE_BITMAP; + specs[g + 9].bitmap = &task->sprite; + specs[g + 9].flags = wuss_ICON_FLAGS_INTERACTIVE; + + nspecs = g + 10; + } + + rc = wuss_icon_create_array(task->window, specs, nspecs, made); if (rc != result_OK) goto failure; @@ -167,12 +204,16 @@ result_t icons_create(wuss_t *wuss, task->counter = made[2]; task->opt = made[g + 6]; task->state = made[g + 7]; + if (task->has_sprite) + task->hotspot = made[g + 9]; return result_OK; failure: wuss_window_close(task->window); task->window = NULL; + if (task->has_sprite) + free(task->sprite.base); return rc; } @@ -286,7 +327,7 @@ static result_t icons_icon(const wuss_event_t *event, void *task_data) if (event->data.icon.action != wuss_MOUSE_DOWN) return result_OK; - if (icon != tcx->button) + if (icon != tcx->button && icon != tcx->hotspot) return result_OK; tcx->count++; @@ -312,7 +353,9 @@ result_t icons_handle(wuss_window_t *window, return icons_icon(event, task_data); case wuss_EVENT_CLOSE: - wuss_window_close(window); + wuss_window_close(window); /* frees the icons, which only borrowed sprite */ + if (tcx->has_sprite) + free(tcx->sprite.base); free(tcx); /* calloc'd per instance by the spawner */ return result_OK; diff --git a/libraries/wuss/test/tasks/icons.h b/libraries/wuss/test/tasks/icons.h index 29da3f22..6d91d956 100644 --- a/libraries/wuss/test/tasks/icons.h +++ b/libraries/wuss/test/tasks/icons.h @@ -5,6 +5,7 @@ #ifdef USE_SDL +#include "framebuf/bitmap.h" #include "framebuf/bmfont.h" #include "framebuf/colour.h" #include "wuss/icon.h" @@ -24,6 +25,9 @@ typedef struct icons_task int count; wuss_icon_t *opt; /* standalone option button */ wuss_icon_t *state; /* label echoing radio/option selection */ + bitmap_t sprite; /* borrowed by the two BITMAP icons; freed on close */ + int has_sprite; + wuss_icon_t *hotspot; /* the interactive BITMAP icon */ } icons_task_t; @@ -33,6 +37,7 @@ wuss_event_fn_t icons_handle; result_t icons_create(wuss_t *wuss, const colour_t *palette, bmfont_t *font, + const char *resources, icons_task_t *task); From f1fba1b29c2bc1c61f69e8e8d0a8e91b4745b6db Mon Sep 17 00:00:00 2001 From: David Thomas Date: Mon, 31 Aug 2026 22:41:39 +0100 Subject: [PATCH 114/287] feat(wuss): add menu-entry icons and hover tracking New icon type wuss_ICON_TYPE_MENU_ENTRY: a full-width row drawn in fg over the window background, inverting to the accent colours while the pointer is over it. Reuses the selected flag for a left-edge tick; wuss_ICON_FLAGS_SUBMENU adds a right-edge arrow; wuss_ICON_FLAGS_SEPARATOR draws a horizontal rule instead of text and makes the row inert. Disabled entries never highlight. Hover tracking: struct wuss gains hover_icon, struct wuss_icon gains hovered. wuss__icon_set_hover swaps the flag between the old and new hovered icon and invalidates whichever changed. mouse-move.c calls it with the icon under the pointer, or NULL at every early-out (no window, over furniture, no task). Cleared on window close and icon delete. This is the hook the menu helper (WUSS_MENUS) will use for submenu-on-hover. The icons test task grows a four-row menu strip: plain, ticked, submenu, separator. Co-Authored-By: Claude Sonnet 5 --- CMakeLists.txt | 1 + include/wuss/icon.h | 27 ++++++++++- libraries/wuss/create.c | 1 + libraries/wuss/icon.h | 9 +++- libraries/wuss/icon/create.c | 4 +- libraries/wuss/icon/delete.c | 2 + libraries/wuss/icon/draw.c | 77 +++++++++++++++++++++++++++++++ libraries/wuss/icon/hit-test.c | 9 +++- libraries/wuss/icon/set-hover.c | 28 +++++++++++ libraries/wuss/impl.h | 4 ++ libraries/wuss/mouse-move.c | 21 ++++++++- libraries/wuss/test/tasks/icons.c | 40 ++++++++++++++-- libraries/wuss/window/close.c | 2 + 13 files changed, 215 insertions(+), 10 deletions(-) create mode 100644 libraries/wuss/icon/set-hover.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 573c36d0..7f691915 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -389,6 +389,7 @@ set(WUSS_ICON_SOURCES libraries/wuss/icon/invalidate.c libraries/wuss/icon/screen-box.c libraries/wuss/icon/set-hidden.c + libraries/wuss/icon/set-hover.c libraries/wuss/icon/set-selected.c libraries/wuss/icon/set-text.c libraries/wuss/icon.h) diff --git a/include/wuss/icon.h b/include/wuss/icon.h index 17cf28f8..50203b54 100644 --- a/include/wuss/icon.h +++ b/include/wuss/icon.h @@ -80,7 +80,7 @@ typedef enum wuss_icon_type * click toggles its own selected state (group is * ignored), then the task is told via * wuss_EVENT_ICON. */ - wuss_ICON_TYPE_BITMAP /**< A caller-owned bitmap (spec.bitmap) drawn at + wuss_ICON_TYPE_BITMAP, /**< A caller-owned bitmap (spec.bitmap) drawn at * the top-left of the bounding box, alpha-blended * against what is already there, clipped to the * box; no scaling. The bitmap is borrowed, not @@ -90,6 +90,19 @@ typedef enum wuss_icon_type * wuss_ICON_FLAGS_INTERACTIVE is set, in which * case clicks raise wuss_EVENT_ICON like a * button. */ + wuss_ICON_TYPE_MENU_ENTRY /**< A menu row: text left-justified across the + * bounding box, drawn in fg over the window + * background, or inverted (window manager's title + * colours) while the pointer is over it. An + * optional tick at the left edge when the icon is + * selected (see wuss_icon_set_selected), and an + * optional submenu arrow at the right edge with + * wuss_ICON_FLAGS_SUBMENU. + * wuss_ICON_FLAGS_SEPARATOR draws a horizontal + * rule instead of text. Interactive: a click + * raises wuss_EVENT_ICON like a button; a + * disabled entry never highlights and its clicks + * fall through. */ } wuss_icon_type_t; @@ -115,7 +128,7 @@ typedef enum wuss_icon_flags * (see wuss_config_t::accent) * instead of the ordinary bevel. * Ignored by other icon types. */ - wuss_ICON_FLAGS_INTERACTIVE = 1 << 5 /**< wuss_ICON_TYPE_BITMAP: hit-test + wuss_ICON_FLAGS_INTERACTIVE = 1 << 5, /**< 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 @@ -123,6 +136,16 @@ typedef enum wuss_icon_flags * as wuss_EVENT_MOUSE. Ignored by * other icon types (interactive or * not by their nature). */ + wuss_ICON_FLAGS_SUBMENU = 1 << 6, /**< wuss_ICON_TYPE_MENU_ENTRY: draw a + * right-pointing arrow at the right + * edge, marking an entry that opens + * a submenu. Ignored by other + * types. */ + wuss_ICON_FLAGS_SEPARATOR = 1 << 7 /**< wuss_ICON_TYPE_MENU_ENTRY: draw a + * horizontal rule across the + * bounding box instead of text; the + * entry is inert (not hit-tested). + * Ignored by other types. */ } wuss_icon_flags_t; diff --git a/libraries/wuss/create.c b/libraries/wuss/create.c index 13172f64..db80d01b 100644 --- a/libraries/wuss/create.c +++ b/libraries/wuss/create.c @@ -211,6 +211,7 @@ result_t wuss_create(screen_t *scr, #endif #ifdef WUSS_ICONS w->pressed_icon = NULL; + w->hover_icon = NULL; #endif w->ndirty = 0; diff --git a/libraries/wuss/icon.h b/libraries/wuss/icon.h index a638b49f..7794f4e7 100644 --- a/libraries/wuss/icon.h +++ b/libraries/wuss/icon.h @@ -24,7 +24,9 @@ struct wuss_icon int group; /* radio: exclusive-selection group; 0 = none */ wuss_icon_flags_t flags; int pressed; /* button: 1 while held with the pointer inside */ - int selected; /* radio/option: 1 while latched on */ + int selected; /* radio/option: 1 while latched on; menu entry: + * 1 draws a tick */ + int hovered; /* menu entry: 1 while the pointer is over it */ }; /* Set icon->selected, invalidating it. For a radio with a non-zero group, @@ -32,6 +34,11 @@ struct wuss_icon * that group. Ignored for types with no latched state. */ void wuss__icon_select(wuss_icon_t *icon, int selected); +/* Make "icon" (may be NULL) the hovered icon: clears the hovered flag on the + * previous wuss->hover_icon and sets it on the new one, invalidating whichever + * of the two changed. A no-op if nothing changed. */ +void wuss__icon_set_hover(wuss_t *wuss, wuss_icon_t *icon); + /* Map an icon bbox (virtual document space) into screen space: * screen = content.x0 - scroll.x + bbox. wuss__icon_draw paints through this * so hit-testing and invalidation cannot drift from what is drawn. */ diff --git a/libraries/wuss/icon/create.c b/libraries/wuss/icon/create.c index 2a53b1dd..72588646 100644 --- a/libraries/wuss/icon/create.c +++ b/libraries/wuss/icon/create.c @@ -32,7 +32,8 @@ result_t wuss_icon_create(wuss_window_t *window, spec->type != wuss_ICON_TYPE_FRAME && spec->type != wuss_ICON_TYPE_RADIO && spec->type != wuss_ICON_TYPE_OPTION && - spec->type != wuss_ICON_TYPE_BITMAP) + spec->type != wuss_ICON_TYPE_BITMAP && + spec->type != wuss_ICON_TYPE_MENU_ENTRY) return result_WUSS_BAD_ICON; if ((spec->type == wuss_ICON_TYPE_BUTTON || @@ -90,6 +91,7 @@ result_t wuss_icon_create(wuss_window_t *window, it->flags = spec->flags; it->pressed = 0; it->selected = 0; + it->hovered = 0; window->icons[window->nicons++] = it; diff --git a/libraries/wuss/icon/delete.c b/libraries/wuss/icon/delete.c index 5f5932b7..0642faca 100644 --- a/libraries/wuss/icon/delete.c +++ b/libraries/wuss/icon/delete.c @@ -20,6 +20,8 @@ void wuss_icon_delete(wuss_icon_t *icon) if (window->wuss->pressed_icon == icon) window->wuss->pressed_icon = NULL; + if (window->wuss->hover_icon == icon) + window->wuss->hover_icon = NULL; wuss__icon_invalidate(icon); diff --git a/libraries/wuss/icon/draw.c b/libraries/wuss/icon/draw.c index eb1861a3..dafacd49 100644 --- a/libraries/wuss/icon/draw.c +++ b/libraries/wuss/icon/draw.c @@ -356,5 +356,82 @@ void wuss__icon_draw(wuss_t *wuss, screen_draw_bitmap(&clipped, b.x0, b.y0, icon->bitmap); } break; + + case wuss_ICON_TYPE_MENU_ENTRY: + { + colour_t ink, ground; + int disabled, highlit, pad, mid_y; + + disabled = (icon->flags & wuss_ICON_FLAGS_DISABLED) != 0; + highlit = icon->hovered && !disabled; + pad = 4; + + /* highlight inverts to the accent colours (the window manager's + * emphasis pair); otherwise ink over the row's own/inherited ground */ + if (highlit) + { + ground = wuss->palette[wuss->accent_bg]; + ink = wuss->palette[wuss->accent_fg]; + } + else + { + if (icon->bg != wuss_NO_BACKGROUND) + ground = wuss->palette[icon->bg]; + else if (icon->window->bg.colour != wuss_NO_BACKGROUND) + ground = (icon->window->bg.pattern != screen_PATTERN_SOLID) + ? wuss->palette[icon->window->bg.pattern_bg] + : wuss->palette[icon->window->bg.colour]; + else + ground = fg; /* bmfont still needs a blend colour */ + ink = disabled ? wuss->palette[wuss->bevel_dark] : fg; + } + + if (highlit || icon->bg != wuss_NO_BACKGROUND) + screen_draw_rect(scr, b.x0, b.y0, + SIZE2D(b.x1 - b.x0, b.y1 - b.y0), ground); + + if (icon->flags & wuss_ICON_FLAGS_SEPARATOR) + { + mid_y = b.y0 + (b.y1 - b.y0) / 2; + screen_draw_line(scr, b.x0 + pad, mid_y, b.x1 - 1 - pad, mid_y, ink); + break; + } + + /* left-edge tick when selected */ + if (icon->selected) + { + int cx, cy, h; + + h = (font_height >= 8) ? font_height : 8; + cx = b.x0 + pad; + cy = b.y0 + (b.y1 - b.y0 - h) / 2; + screen_draw_line(scr, cx, cy + h / 2, cx + h / 2 - 1, cy + h - 2, ink); + screen_draw_line(scr, cx + h / 2 - 1, cy + h - 2, cx + h - 2, cy, ink); + } + + /* right-edge arrow for a submenu entry */ + if (icon->flags & wuss_ICON_FLAGS_SUBMENU) + { + int ax, ay, r, dy; + + r = (font_height >= 8) ? font_height / 3 : 3; + ax = b.x1 - 1 - pad - r; + ay = b.y0 + (b.y1 - b.y0) / 2; + for (dy = -r; dy <= r; dy++) + screen_draw_line(scr, ax, ay + dy, + ax + (r - (dy < 0 ? -dy : dy)), ay + dy, ink); + } + + if (have_font) + { + point_t pos; + + pos.x = b.x0 + pad + font_height; /* leave room for a tick */ + pos.y = b.y0 + (b.y1 - b.y0 - font_height) / 2; + bmfont_draw(wuss->font, scr, icon->text, (int) strlen(icon->text), + ink, ground, &pos, NULL); + } + } + break; } } diff --git a/libraries/wuss/icon/hit-test.c b/libraries/wuss/icon/hit-test.c index 9cb66bf2..e0a12f85 100644 --- a/libraries/wuss/icon/hit-test.c +++ b/libraries/wuss/icon/hit-test.c @@ -14,13 +14,18 @@ wuss_icon_t *wuss__icon_hit_test(wuss_window_t *window, point_t doc_point) { it = window->icons[i]; - /* a bitmap icon is interactive only when it asks to be; the other - * clickable types always are */ + /* a bitmap icon is interactive only when it asks to be; a menu separator + * never is; the other clickable types always are */ if (it->type == wuss_ICON_TYPE_BITMAP) { if (!(it->flags & wuss_ICON_FLAGS_INTERACTIVE)) continue; } + else if (it->type == wuss_ICON_TYPE_MENU_ENTRY) + { + if (it->flags & wuss_ICON_FLAGS_SEPARATOR) + continue; + } else if (it->type != wuss_ICON_TYPE_BUTTON && it->type != wuss_ICON_TYPE_RADIO && it->type != wuss_ICON_TYPE_OPTION) diff --git a/libraries/wuss/icon/set-hover.c b/libraries/wuss/icon/set-hover.c new file mode 100644 index 00000000..6ae8d734 --- /dev/null +++ b/libraries/wuss/icon/set-hover.c @@ -0,0 +1,28 @@ +/* set-hover.c -- wuss - track which work-area icon the pointer is over */ + +#include + +#include "../impl.h" + +void wuss__icon_set_hover(wuss_t *wuss, wuss_icon_t *icon) +{ + wuss_icon_t *prev; + + prev = wuss->hover_icon; + if (prev == icon) + return; + + if (prev != NULL) + { + prev->hovered = 0; + wuss__icon_invalidate(prev); + } + + wuss->hover_icon = icon; + + if (icon != NULL) + { + icon->hovered = 1; + wuss__icon_invalidate(icon); + } +} diff --git a/libraries/wuss/impl.h b/libraries/wuss/impl.h index 169ab7d2..92d5a4b8 100644 --- a/libraries/wuss/impl.h +++ b/libraries/wuss/impl.h @@ -89,6 +89,10 @@ struct wuss * idle; released on any MOUSE_UP * even if a new window now covers * its owner */ + wuss_icon_t *hover_icon; /* icon the pointer is currently + * over, NULL when none; drives + * hover-highlight repaint of + * menu-entry icons */ #endif }; diff --git a/libraries/wuss/mouse-move.c b/libraries/wuss/mouse-move.c index e07144ee..fa1c9eaf 100644 --- a/libraries/wuss/mouse-move.c +++ b/libraries/wuss/mouse-move.c @@ -46,14 +46,31 @@ result_t wuss_mouse_move(wuss_t *wuss, point_t p, wuss_window_t **hit) *hit = win; if (win == NULL) + { +#ifdef WUSS_ICONS + wuss__icon_set_hover(wuss, NULL); +#endif return result_OK; + } #ifdef WUSS_FURNITURE if (wuss__furniture_hit_test(win, POINT(x, y)) != wuss_FURNITURE_CONTENT) + { +#ifdef WUSS_ICONS + wuss__icon_set_hover(wuss, NULL); +#endif return result_OK; + } #endif - if (win->task.handle != NULL) + if (win->task.handle == NULL) + { +#ifdef WUSS_ICONS + wuss__icon_set_hover(wuss, NULL); +#endif + return result_OK; + } + { box_t content; point_t doc_point; @@ -70,6 +87,8 @@ result_t wuss_mouse_move(wuss_t *wuss, point_t p, wuss_window_t **hit) icon = wuss__icon_hit_test(win, doc_point); + wuss__icon_set_hover(wuss, icon); + /* Clear the pressed state of any button the pointer has left. This does * not re-press a button on drag-back-in, and does not track which mouse * button is held -- wuss keeps no persistent "button down over content" diff --git a/libraries/wuss/test/tasks/icons.c b/libraries/wuss/test/tasks/icons.c index 2a188b1a..7a1dc385 100644 --- a/libraries/wuss/test/tasks/icons.c +++ b/libraries/wuss/test/tasks/icons.c @@ -22,7 +22,7 @@ #include "icons.h" #define ICONS_DOC_W 220 -#define ICONS_DOC_H 520 /* taller than the window, so scrolling is exercised */ +#define ICONS_DOC_H 640 /* taller than the window, so scrolling is exercised */ result_t icons_create(wuss_t *wuss, const colour_t *palette, @@ -33,8 +33,9 @@ result_t icons_create(wuss_t *wuss, /* [0..3], one swatch per built-in pattern, then [.. +3] a grouping frame * with two differently-justified labels inside it, then [.. +5] three grouped * radios, a standalone option and a label echoing the selection, then [.. +2] - * a decorative bitmap icon and an interactive one that bumps the counter */ - enum { ICONS_NSPECS = 4 + screen_PATTERN__LIMIT + 3 + 5 + 2 }; + * a decorative bitmap icon and an interactive one that bumps the counter, + * then [.. +4] a strip of menu-entry icons that hover-highlight */ + enum { ICONS_NSPECS = 4 + screen_PATTERN__LIMIT + 3 + 5 + 2 + 4 }; wuss_task_t delegate; wuss_icon_spec_t specs[ICONS_NSPECS]; wuss_icon_t *made[ICONS_NSPECS]; @@ -42,6 +43,7 @@ result_t icons_create(wuss_t *wuss, int p; int g; /* index of the first frame spec */ int r; /* radio index */ + int m; /* index of the first menu-entry spec */ int nspecs; /* live spec count (sprite icons are optional) */ result_t rc; @@ -196,6 +198,37 @@ result_t icons_create(wuss_t *wuss, nspecs = g + 10; } + /* [nspecs..nspecs+3] a menu-entry strip: plain, ticked, submenu, separator. + * Hover the pointer over them to see the highlight track. */ + m = nspecs; + + specs[m].bbox = (box_t) BOX_POS_SIZE(28, 524, 160, 16); + specs[m].type = wuss_ICON_TYPE_MENU_ENTRY; + specs[m].text = "Open"; + specs[m].fg = palette_PICO8_DARK_BLUE; + specs[m].bg = wuss_NO_BACKGROUND; + + specs[m + 1].bbox = (box_t) BOX_POS_SIZE(28, 542, 160, 16); + specs[m + 1].type = wuss_ICON_TYPE_MENU_ENTRY; + specs[m + 1].text = "Show grid"; + specs[m + 1].fg = palette_PICO8_DARK_BLUE; + specs[m + 1].bg = wuss_NO_BACKGROUND; + + specs[m + 2].bbox = (box_t) BOX_POS_SIZE(28, 560, 160, 16); + specs[m + 2].type = wuss_ICON_TYPE_MENU_ENTRY; + specs[m + 2].text = "Export"; + specs[m + 2].fg = palette_PICO8_DARK_BLUE; + specs[m + 2].bg = wuss_NO_BACKGROUND; + specs[m + 2].flags = wuss_ICON_FLAGS_SUBMENU; + + specs[m + 3].bbox = (box_t) BOX_POS_SIZE(28, 578, 160, 10); + specs[m + 3].type = wuss_ICON_TYPE_MENU_ENTRY; + specs[m + 3].fg = palette_PICO8_DARK_BLUE; + specs[m + 3].bg = wuss_NO_BACKGROUND; + specs[m + 3].flags = wuss_ICON_FLAGS_SEPARATOR; + + nspecs = m + 4; + rc = wuss_icon_create_array(task->window, specs, nspecs, made); if (rc != result_OK) goto failure; @@ -206,6 +239,7 @@ result_t icons_create(wuss_t *wuss, task->state = made[g + 7]; if (task->has_sprite) task->hotspot = made[g + 9]; + wuss_icon_set_selected(made[m + 1], 1); /* "Show grid" starts ticked */ return result_OK; diff --git a/libraries/wuss/window/close.c b/libraries/wuss/window/close.c index 570425d0..759572fe 100644 --- a/libraries/wuss/window/close.c +++ b/libraries/wuss/window/close.c @@ -23,6 +23,8 @@ void wuss_window_close(wuss_window_t *doomed) #ifdef WUSS_ICONS if (wuss->pressed_icon != NULL && wuss->pressed_icon->window == doomed) wuss->pressed_icon = NULL; + if (wuss->hover_icon != NULL && wuss->hover_icon->window == doomed) + wuss->hover_icon = NULL; #endif wuss__release_packed(doomed); From f19964ceb75f8c92f2ddd8d45e56fb5dea2cbae6 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Mon, 31 Aug 2026 22:52:55 +0100 Subject: [PATCH 115/287] feat(wuss): add WUSS_MENUS pop-up menu helper New compile-time option WUSS_MENUS (implies WUSS_ICONS) guarding a thin helper for RISC OS-style pop-up menus: a menu is a borderless window of wuss_ICON_TYPE_MENU_ENTRY icons, but wuss owns layout, on-screen placement, submenu chaining on hover and whole-chain dismissal. - include/wuss/menu.h: caller-owned immutable wuss_menu_t / wuss_menu_item_t data model; wuss_menu_open / wuss_menu_close / wuss_menu_is_open; wuss_menu_select_fn_t leaf-selection callback (SELECT closes the chain, ADJUST keeps it open). - libraries/wuss/menu/menu.c + libraries/wuss/menu.h: struct wuss__menu chain nodes (window + per-item icon handles + parent/child links). Measures the widest label, sizes and places a borderless window, maps item flags (DASHED/DISABLED/submenu/TICKED) onto icon flags, and runs the chain from an internal window delegate. - struct wuss gains menu_chain; mouse-click.c dismisses the chain on a MOUSE_DOWN outside every menu window; create.c inits it, destroy.c tears down any still-open chain. - CMakeLists.txt: WUSS_MENUS option, WUSS_MENU_SOURCES, PUBLIC_HEADERS. - apps/wuss/main.c: a "Menu" launcher entry opening a static demo tree. Co-Authored-By: Claude Sonnet 5 --- CMakeLists.txt | 17 ++ apps/wuss/main.c | 48 ++++- include/wuss/menu.h | 114 +++++++++++ libraries/wuss/create.c | 3 + libraries/wuss/destroy.c | 6 + libraries/wuss/impl.h | 9 + libraries/wuss/menu.h | 34 +++ libraries/wuss/menu/menu.c | 387 +++++++++++++++++++++++++++++++++++ libraries/wuss/mouse-click.c | 8 + 9 files changed, 625 insertions(+), 1 deletion(-) create mode 100644 include/wuss/menu.h create mode 100644 libraries/wuss/menu.h create mode 100644 libraries/wuss/menu/menu.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 7f691915..a9f5f699 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,6 +32,7 @@ option(USE_ASAN "Build with AddressSanitizer and UndefinedBehaviorSanitizer" OFF option(DPTLIB_IMAGES_READ_ONLY "Remove libpng write support" OFF) option(WUSS_FURNITURE "Build the Wuss window-furniture subsystem" ON) option(WUSS_ICONS "Build the Wuss in-content icon subsystem" ON) +option(WUSS_MENUS "Build the Wuss pop-up menu helper (implies WUSS_ICONS)" ON) # Referencing CMAKE_TOOLCHAIN_FILE avoids a warning on rebuilds. if(NOT ${CMAKE_TOOLCHAIN_FILE} STREQUAL "") @@ -122,6 +123,9 @@ set(PUBLIC_HEADERS if(WUSS_ICONS) list(APPEND PUBLIC_HEADERS include/wuss/icon.h) endif() +if(WUSS_MENUS) + list(APPEND PUBLIC_HEADERS include/wuss/menu.h) +endif() # The public headers must be set as properties of the library, not as # target_sources. The quoting is essential. @@ -394,13 +398,23 @@ set(WUSS_ICON_SOURCES libraries/wuss/icon/set-text.c libraries/wuss/icon.h) +set(WUSS_MENU_SOURCES + libraries/wuss/menu/menu.c + libraries/wuss/menu.h) + set(WUSS_SOURCES ${WUSS_CORE_SOURCES}) if(WUSS_FURNITURE) list(APPEND WUSS_SOURCES ${WUSS_FURNITURE_SOURCES}) endif() +if(WUSS_MENUS) + set(WUSS_ICONS ON) # the menu helper needs the icon subsystem +endif() if(WUSS_ICONS) list(APPEND WUSS_SOURCES ${WUSS_ICON_SOURCES}) endif() +if(WUSS_MENUS) + list(APPEND WUSS_SOURCES ${WUSS_MENU_SOURCES}) +endif() set(ALL_SOURCES ${PUBLIC_HEADERS} @@ -458,6 +472,9 @@ endif() if(WUSS_ICONS) target_compile_definitions(DPTLib PUBLIC WUSS_ICONS) endif() +if(WUSS_MENUS) + target_compile_definitions(DPTLib PUBLIC WUSS_MENUS) +endif() if(NOT TARGET_RISCOS) set(CMAKE_FIND_FRAMEWORK NEVER) diff --git a/apps/wuss/main.c b/apps/wuss/main.c index 2e623285..f9cf4b7e 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -20,6 +20,7 @@ #include "io/path.h" #include "wuss/wuss.h" #include "wuss/window.h" +#include "wuss/menu.h" #include @@ -192,6 +193,50 @@ 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 dashed separator. 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 = +{ + g_menu_export_items, NELEMS(g_menu_export_items) +}; + +static const wuss_menu_item_t g_menu_items[] = +{ + { "Open", wuss_MENU_ITEM_NONE, NULL }, + { "Show grid", wuss_MENU_ITEM_TICKED, NULL }, + { "Wireframe", wuss_MENU_ITEM_TICKED, NULL }, + { "Export", wuss_MENU_ITEM_NONE, &g_menu_export }, + { NULL, wuss_MENU_ITEM_DASHED, NULL }, + { "Quit", wuss_MENU_ITEM_NONE, NULL } +}; +static const wuss_menu_t g_menu = +{ + g_menu_items, NELEMS(g_menu_items) +}; + +static void menu_selected(const wuss_menu_t *menu, + int index, + wuss_button_t button, + void *ctx) +{ + NOT_USED(button); + NOT_USED(ctx); + printf("menu: picked \"%s\"\n", + menu->items[index].text ? menu->items[index].text : "(sep)"); +} + +static result_t spawn_menu(void) +{ + return wuss_menu_open(g_wuss, &g_menu, POINT(120, 120), + menu_selected, NULL, NULL); +} + static const launcher_entry_t g_launcher_entries[] = { { "Ball", spawn_ball }, @@ -205,7 +250,8 @@ static const launcher_entry_t g_launcher_entries[] = { "Sofa", spawn_sofa }, { "Gradient", spawn_gradient }, { "Icons", spawn_icons }, - { "Porter-Duff", spawn_porter_duff } + { "Porter-Duff", spawn_porter_duff }, + { "Menu", spawn_menu } }; static wuss_button_t sdl_button_to_wuss(Uint8 button) diff --git a/include/wuss/menu.h b/include/wuss/menu.h new file mode 100644 index 00000000..9322f48c --- /dev/null +++ b/include/wuss/menu.h @@ -0,0 +1,114 @@ +/* menu.h -- wuss pop-up menu helper */ + +/** + * \file menu.h + * + * A thin helper for RISC OS-style pop-up menus: a menu is a borderless window + * populated with wuss_ICON_TYPE_MENU_ENTRY icons, but wuss owns the plumbing -- + * layout, placement, submenu chaining on hover and whole-chain dismissal on a + * click outside or a leaf selection. + * + * Menus are described by caller-owned, immutable wuss_menu_t / wuss_menu_item_t + * structures (which may be static). The helper never mutates them; a task that + * wants a tick to change just edits its own array and reopens the menu. + * + * Built only when WUSS_MENUS is defined (which implies WUSS_ICONS). + */ + +#ifndef WUSS_MENU_H +#define WUSS_MENU_H + +#ifdef __cplusplus +extern "C" +{ +#endif + +#include "base/result.h" +#include "geom/point.h" + +#include "wuss/wuss.h" + +/* ----------------------------------------------------------------------- */ + +/** Per-item flags for a wuss_menu_item_t. OR'd together. */ +typedef enum wuss_menu_item_flags +{ + wuss_MENU_ITEM_NONE = 0, + wuss_MENU_ITEM_TICKED = 1 << 0, /**< draw a tick at the item's left edge */ + wuss_MENU_ITEM_DASHED = 1 << 1, /**< this item is a separator rule; text + * is ignored */ + wuss_MENU_ITEM_DISABLED = 1 << 2 /**< greyed, never highlights, not + * selectable */ +} +wuss_menu_item_flags_t; + +/** One row of a menu. */ +typedef struct wuss_menu_item +{ + const char *text; /**< row label; NULL is treated as "" */ + wuss_menu_item_flags_t flags; /**< see wuss_menu_item_flags_t */ + const struct wuss_menu *submenu; /**< non-NULL: draw a right arrow and open + * this menu to the right on hover */ +} +wuss_menu_item_t; + +/** A menu: an array of items the caller owns. */ +typedef struct wuss_menu +{ + const wuss_menu_item_t *items; + int nitems; +} +wuss_menu_t; + +/** Opaque handle to an open menu chain. */ +typedef struct wuss__menu *wuss_menu_handle_t; + +/** + * Called when the pointer is released over a selectable leaf item. + * + * \param[in] menu The menu the item belongs to (a submenu, if nested). + * \param[in] index Index of the item within \c menu->items. + * \param[in] button wuss_button_t flags for the release; test with '&'. ADJUST + * keeps the chain open, SELECT closes it. + * \param[in] ctx As passed to wuss_menu_open. + */ +typedef void (wuss_menu_select_fn_t)(const wuss_menu_t *menu, + int index, + wuss_button_t button, + void *ctx); + +/* ----------------------------------------------------------------------- */ + +/** + * Open \p menu as a pop-up at \p at (screen space), nudged to stay on screen. + * Any menu chain already open is closed first. The chain lives until a leaf is + * SELECT-picked, a click lands outside every menu window, or wuss_menu_close is + * called. + * + * \param[in] wuss Window manager. + * \param[in] menu Menu to show; borrowed, must outlive the open chain. + * \param[in] at Where to put the menu's top-left, screen space. + * \param[in] on_select Leaf-selection callback, or NULL. + * \param[in] ctx Opaque pointer passed back to \p on_select. + * \param[out] out Filled with the chain handle, or NULL if not wanted. + * \return \ref result_OK, \ref result_OOM, or a wuss_window_create code. + */ +result_t wuss_menu_open(wuss_t *wuss, + const wuss_menu_t *menu, + point_t at, + wuss_menu_select_fn_t *on_select, + void *ctx, + wuss_menu_handle_t *out); + +/** Close a menu chain and every window in it. Safe to pass a stale or NULL + * handle. */ +void wuss_menu_close(wuss_menu_handle_t handle); + +/** Non-zero while \p handle refers to a currently open chain. */ +int wuss_menu_is_open(wuss_menu_handle_t handle); + +#ifdef __cplusplus +} +#endif + +#endif /* WUSS_MENU_H */ diff --git a/libraries/wuss/create.c b/libraries/wuss/create.c index db80d01b..acb6ba48 100644 --- a/libraries/wuss/create.c +++ b/libraries/wuss/create.c @@ -213,6 +213,9 @@ result_t wuss_create(screen_t *scr, w->pressed_icon = NULL; w->hover_icon = NULL; #endif +#ifdef WUSS_MENUS + w->menu_chain = NULL; +#endif w->ndirty = 0; diff --git a/libraries/wuss/destroy.c b/libraries/wuss/destroy.c index 372fd517..7bc4bf08 100644 --- a/libraries/wuss/destroy.c +++ b/libraries/wuss/destroy.c @@ -15,6 +15,12 @@ void wuss_destroy(wuss_t *doomed) if (doomed == NULL) return; +#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. */ + wuss_menu_close(doomed->menu_chain); +#endif + e = doomed->z_order.next; while (e != NULL) { diff --git a/libraries/wuss/impl.h b/libraries/wuss/impl.h index 92d5a4b8..49d004be 100644 --- a/libraries/wuss/impl.h +++ b/libraries/wuss/impl.h @@ -21,6 +21,9 @@ #ifdef WUSS_ICONS #include "icon.h" #endif +#ifdef WUSS_MENUS +#include "menu.h" +#endif #define WUSS_TITLE_MAX 63 #define WUSS_DEFAULT_TITLEBAR_HEIGHT 20 @@ -94,6 +97,12 @@ struct wuss * hover-highlight repaint of * menu-entry icons */ #endif +#ifdef WUSS_MENUS + struct wuss__menu *menu_chain; /* head (root) of the open pop-up + * menu chain, NULL when none open; + * consulted by mouse-click.c to + * dismiss on a click-away */ +#endif }; struct wuss_window diff --git a/libraries/wuss/menu.h b/libraries/wuss/menu.h new file mode 100644 index 00000000..4012f6d5 --- /dev/null +++ b/libraries/wuss/menu.h @@ -0,0 +1,34 @@ +/* menu.h -- wuss pop-up menu helper, internal */ + +#ifndef WUSS_MENU_IMPL_H +#define WUSS_MENU_IMPL_H + +#include "geom/point.h" + +#include "wuss/wuss.h" +#include "wuss/menu.h" + +/* One open level of a menu chain: its window and per-item icon handles, plus a + * link to the level it opened from. The head (root) is wuss->menu_chain; each + * child points at its parent so wuss_menu_close can walk leaf-to-root. */ +struct wuss__menu +{ + wuss_t *wuss; + wuss_window_t *window; /* the borderless menu window */ + const wuss_menu_t *menu; /* borrowed source description */ + wuss_icon_t **icons; /* owned array, menu->nitems entries, in + * item order */ + struct wuss__menu *parent; /* level this one opened from, NULL at root */ + struct wuss__menu *child; /* open submenu level, NULL when none */ + wuss_menu_select_fn_t *on_select; + void *ctx; + int open_index; /* item index whose submenu `child` is, + * -1 when no child open */ +}; + +/* Called from wuss_mouse_click on a MOUSE_DOWN before the window hit-test: if a + * menu chain is open and `hit` is not one of its windows, close the whole chain + * 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); + +#endif /* WUSS_MENU_IMPL_H */ diff --git a/libraries/wuss/menu/menu.c b/libraries/wuss/menu/menu.c new file mode 100644 index 00000000..40b150b6 --- /dev/null +++ b/libraries/wuss/menu/menu.c @@ -0,0 +1,387 @@ +/* menu.c -- wuss pop-up menu helper */ + +#include +#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/point.h" +#include "geom/size.h" + +#include "wuss/icon.h" +#include "wuss/task.h" +#include "wuss/window.h" +#include "wuss/wuss.h" + +#include "../impl.h" + +/* Row padding above/below the glyph, and the gutters left for the tick (left) + * and the submenu arrow (right). All in pixels. */ +#define WUSS_MENU_ROW_PAD 3 +#define WUSS_MENU_TICK_W 14 +#define WUSS_MENU_ARROW_W 14 +#define WUSS_MENU_TEXT_PAD 6 +#define WUSS_MENU_SUBMENU_OVERLAP 2 /* px a submenu overlaps its parent's right edge */ + +/* ----------------------------------------------------------------------- */ + +static result_t wuss__menu_spawn(wuss_t *wuss, + const wuss_menu_t *menu, + point_t at, + struct wuss__menu *parent, + wuss_menu_select_fn_t *on_select, + void *ctx, + struct wuss__menu **out); + +/* Close `node` and everything it opened, leaf-first. The caller clears any + * parent's child pointer. */ +static void wuss__menu_close_from(struct wuss__menu *node) +{ + if (node == NULL) + return; + + wuss__menu_close_from(node->child); + node->child = NULL; + + wuss_window_close(node->window); + free(node->icons); + free(node); +} + +/* ----------------------------------------------------------------------- */ + +/* The menu window's task delegate. task_data is the struct wuss__menu. */ +static result_t wuss__menu_handle(wuss_window_t *window, + const wuss_event_t *event, + void *task_data) +{ + struct wuss__menu *self; + const wuss_icon_t *icon; + const wuss_menu_item_t *item; + int index; + int i; + + NOT_USED(window); + + self = task_data; + + if (event->kind != wuss_EVENT_ICON) + return result_OK; + + icon = event->data.icon.icon; + index = -1; + for (i = 0; i < self->menu->nitems; i++) + { + if (self->icons[i] == icon) + { + index = i; + break; + } + } + if (index < 0) + return result_OK; + + item = &self->menu->items[index]; + + if (item->flags & (wuss_MENU_ITEM_DASHED | wuss_MENU_ITEM_DISABLED)) + return result_OK; + + if (event->data.icon.action == wuss_MOUSE_MOVE) + { + box_t ib; + box_t wb; + point_t at; + + /* Hovering a different row closes any submenu the previous row opened. */ + if (self->child != NULL && self->open_index != index) + { + wuss__menu_close_from(self->child); + self->child = NULL; + self->open_index = -1; + } + + if (item->submenu == NULL || self->child != NULL) + return result_OK; + + wuss_window_get_visible_bounds(self->window, &wb); + wuss_icon_get_bbox(icon, &ib); + + /* The menu is unscrolled, so a row's document y equals its screen y + * offset from the window's top. */ + at.x = wb.x1 - WUSS_MENU_SUBMENU_OVERLAP; + at.y = wb.y0 + ib.y0; + + if (wuss__menu_spawn(self->wuss, item->submenu, at, self, + self->on_select, self->ctx, &self->child) == result_OK) + self->open_index = index; + + return result_OK; + } + + if (event->data.icon.action == wuss_MOUSE_UP) + { + wuss_button_t button; + struct wuss__menu *root; + + button = event->data.icon.button; + + if (item->submenu != NULL) + return result_OK; /* a submenu row opens on hover, it is not a pick */ + + if (self->on_select != NULL) + self->on_select(self->menu, index, button, self->ctx); + + if (button & wuss_BUTTON_ADJUST) + return result_OK; /* ADJUST keeps the chain open */ + + root = self; + while (root->parent != NULL) + root = root->parent; + + root->wuss->menu_chain = NULL; + wuss__menu_close_from(root); + } + + return result_OK; +} + +/* ----------------------------------------------------------------------- */ + +/* Measure the menu, create its borderless window and one MENU_ENTRY icon per + * item, and hang the node off *out. */ +static result_t wuss__menu_spawn(wuss_t *wuss, + const wuss_menu_t *menu, + point_t at, + struct wuss__menu *parent, + wuss_menu_select_fn_t *on_select, + void *ctx, + struct wuss__menu **out) +{ + struct wuss__menu *node; + wuss_icon_spec_t *specs; + wuss_task_t task; + result_t rc; + int fw, fh; + int pitch; + int widest; + int width, height; + int i; + size2d_t size; + + assert(wuss != NULL); + assert(menu != NULL); + assert(wuss->font != NULL); + + if (menu->nitems <= 0) + return result_WUSS_BAD_ICON; + + bmfont_get_info(wuss->font, &fw, &fh); + NOT_USED(fw); + pitch = fh + 2 * WUSS_MENU_ROW_PAD; + + widest = 0; + for (i = 0; i < menu->nitems; i++) + { + const char *text; + int split; + bmfont_width_t w; + + text = menu->items[i].text ? menu->items[i].text : ""; + if (bmfont_measure(wuss->font, text, (int) strlen(text), + INT_MAX, &split, &w) == result_OK && (int) w > widest) + widest = (int) w; + } + + width = WUSS_MENU_TICK_W + widest + WUSS_MENU_TEXT_PAD + WUSS_MENU_ARROW_W; + height = pitch * menu->nitems; + + /* Nudge onto the screen where it can be, but keep the top-left on screen. */ + if (at.x + width > wuss->scr->size.w) + at.x = wuss->scr->size.w - width; + if (at.y + height > wuss->scr->size.h) + at.y = wuss->scr->size.h - height; + if (at.x < 0) + at.x = 0; + if (at.y < 0) + at.y = 0; + + node = malloc(sizeof(*node)); + if (node == NULL) + return result_OOM; + + node->icons = calloc((size_t) menu->nitems, sizeof(*node->icons)); + specs = calloc((size_t) menu->nitems, sizeof(*specs)); + if (node->icons == NULL || specs == NULL) + { + free(specs); + free(node->icons); + free(node); + return result_OOM; + } + + node->wuss = wuss; + node->window = NULL; + node->menu = menu; + node->parent = parent; + node->child = NULL; + node->on_select = on_select; + node->ctx = ctx; + node->open_index = -1; + + for (i = 0; i < menu->nitems; i++) + { + const wuss_menu_item_t *item; + wuss_icon_flags_t flags; + + item = &menu->items[i]; + flags = wuss_ICON_FLAGS_NONE; + + if (item->flags & wuss_MENU_ITEM_DASHED) + flags |= wuss_ICON_FLAGS_SEPARATOR; + if (item->flags & wuss_MENU_ITEM_DISABLED) + flags |= wuss_ICON_FLAGS_DISABLED; + if (item->submenu != NULL) + flags |= wuss_ICON_FLAGS_SUBMENU; + + specs[i].type = wuss_ICON_TYPE_MENU_ENTRY; + specs[i].bbox.x0 = 0; + specs[i].bbox.y0 = i * pitch; + specs[i].bbox.x1 = width; + specs[i].bbox.y1 = i * pitch + pitch; + specs[i].text = item->text ? item->text : ""; + specs[i].fg = 0; + specs[i].bg = wuss_NO_BACKGROUND; + specs[i].flags = flags; + } + + size = SIZE2D(width, height); + task = wuss_task_start(wuss__menu_handle, node); + + rc = wuss_window_create_placed(wuss, size, NULL, + 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(1), + &task, size, size, &node->window); + if (rc != result_OK) + { + free(specs); + free(node->icons); + free(node); + return rc; + } + + wuss_window_move(node->window, at); + + rc = wuss_icon_create_array(node->window, specs, menu->nitems, node->icons); + free(specs); + if (rc != result_OK) + { + wuss_window_close(node->window); + free(node->icons); + free(node); + return rc; + } + + for (i = 0; i < menu->nitems; i++) + { + if (menu->items[i].flags & wuss_MENU_ITEM_TICKED) + wuss_icon_set_selected(node->icons[i], 1); + } + + *out = node; + return result_OK; +} + +/* ----------------------------------------------------------------------- */ + +result_t wuss_menu_open(wuss_t *wuss, + const wuss_menu_t *menu, + point_t at, + wuss_menu_select_fn_t *on_select, + void *ctx, + wuss_menu_handle_t *out) +{ + struct wuss__menu *root; + result_t rc; + + assert(wuss != NULL); + assert(menu != NULL); + + if (wuss->menu_chain != NULL) + { + wuss__menu_close_from(wuss->menu_chain); + wuss->menu_chain = NULL; + } + + rc = wuss__menu_spawn(wuss, menu, at, NULL, on_select, ctx, &root); + if (rc != result_OK) + return rc; + + wuss->menu_chain = root; + if (out != NULL) + *out = root; + return result_OK; +} + +void wuss_menu_close(wuss_menu_handle_t handle) +{ + struct wuss__menu *root; + + if (handle == NULL) + return; + + root = handle; + while (root->parent != NULL) + root = root->parent; + + if (root->wuss->menu_chain != root) + return; /* stale handle */ + + root->wuss->menu_chain = NULL; + wuss__menu_close_from(root); +} + +int wuss_menu_is_open(wuss_menu_handle_t handle) +{ + struct wuss__menu *root; + + if (handle == NULL) + return 0; + + root = handle; + while (root->parent != NULL) + root = root->parent; + + return root->wuss->menu_chain == root; +} + +/* ----------------------------------------------------------------------- */ + +int wuss__menu_click_outside(wuss_t *wuss, const wuss_window_t *hit) +{ + struct wuss__menu *node; + + if (wuss->menu_chain == NULL) + return 0; + + for (node = wuss->menu_chain; node != NULL; node = node->child) + { + if (node->window == hit) + return 0; + } + + wuss__menu_close_from(wuss->menu_chain); + wuss->menu_chain = NULL; + return 1; +} diff --git a/libraries/wuss/mouse-click.c b/libraries/wuss/mouse-click.c index ec35940b..cf9c7356 100644 --- a/libraries/wuss/mouse-click.c +++ b/libraries/wuss/mouse-click.c @@ -49,6 +49,14 @@ result_t wuss_mouse_click(wuss_t *wuss, if (hit != NULL) *hit = win; +#ifdef WUSS_MENUS + /* A press outside every window in the open menu chain dismisses the chain + * and is spent doing so. Presses inside the chain fall through to the menu + * window's own delegate. */ + if (action == wuss_MOUSE_DOWN && wuss__menu_click_outside(wuss, win)) + return result_OK; +#endif + if (win == NULL) return result_OK; From db6e02d377249013b49f61298072fff991eba072 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Mon, 31 Aug 2026 23:14:52 +0100 Subject: [PATCH 116/287] feat(wuss): add wuss_menu_create_from_desc descriptor parser Port PrivateEye's menu_create_from_desc token machine to build a heap wuss_menu_t tree from a compact string: - ',' separates items, '|' marks a dashed separator above - '{ ... }' after an item is its submenu (first token is a title, discarded for source compatibility with the Wimp descriptors) - per-token '!' ticks, '~' shades, '>' pulls a const wuss_menu_t * submenu from the varargs - '%s' substitutes the next const char * vararg '>' submenus are deep-copied so the whole result tree is owned uniformly and freed by one wuss_menu_destroy. getname() trims trailing whitespace so "Foo { " yields "Foo". Also fix a Stage 6a crash: wuss__menu_spawn passed an empty label to bmfont_measure (asserts textlen > 0) for a separator row; skip empty labels when measuring and when drawing a MENU_ENTRY. Guarded by WUSS_MENUS. wuss-test.c gains a parser self-check. Co-Authored-By: Claude Sonnet 5 --- CMakeLists.txt | 1 + include/wuss/menu.h | 38 ++ libraries/wuss/icon/draw.c | 2 +- libraries/wuss/menu/create-from-desc.c | 471 +++++++++++++++++++++++++ libraries/wuss/menu/menu.c | 6 +- libraries/wuss/test/wuss-test.c | 73 ++++ 6 files changed, 589 insertions(+), 2 deletions(-) create mode 100644 libraries/wuss/menu/create-from-desc.c diff --git a/CMakeLists.txt b/CMakeLists.txt index a9f5f699..8c1ba83c 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -400,6 +400,7 @@ set(WUSS_ICON_SOURCES set(WUSS_MENU_SOURCES libraries/wuss/menu/menu.c + libraries/wuss/menu/create-from-desc.c libraries/wuss/menu.h) set(WUSS_SOURCES ${WUSS_CORE_SOURCES}) diff --git a/include/wuss/menu.h b/include/wuss/menu.h index 9322f48c..ca286f9b 100644 --- a/include/wuss/menu.h +++ b/include/wuss/menu.h @@ -107,6 +107,44 @@ void wuss_menu_close(wuss_menu_handle_t handle); /** Non-zero while \p handle refers to a currently open chain. */ int wuss_menu_is_open(wuss_menu_handle_t handle); +/* ----------------------------------------------------------------------- */ + +/** + * Build a wuss_menu_t tree from a compact descriptor string. The syntax is + * lifted from PrivateEye's menu_create_from_desc. A comma separates items. A + * leading '|' marks an item a dashed separator above it + * (wuss_MENU_ITEM_DASHED). A '{ ... }' group after an item is that item's + * submenu; as in the Wimp original the first token inside the braces is a title + * and is discarded, so the same descriptor strings port across unchanged. A + * per-token prefix '!' ticks the item, '~' shades (disables) it, and '>' + * attaches a submenu pulled as a const wuss_menu_t * from the varargs + * rather than from a following '{ }' block. A "%s" in a token substitutes the + * next const char * vararg. The '>' and "%s" varargs are consumed in + * the order they are encountered scanning left to right. + * + * Example: wuss_menu_create_from_desc(&m, "App, %s { %s, !Grid, ~Export, + * |Quit }", "File", "File"). + * + * The whole tree, including copied label text, is one heap allocation graph + * owned by the caller; free it with wuss_menu_destroy. wuss_menu_open treats a + * desc-built tree exactly like a static literal. + * + * \param[out] out Filled with the root menu on success, untouched on failure. + * \param[in] desc Descriptor string. + * \return \ref result_OK, \ref result_OOM, or \ref result_BAD_ARG for a + * malformed descriptor (unbalanced braces, empty token, too deep). + */ +result_t wuss_menu_create_from_desc(wuss_menu_t **out, const char *desc, ...); + +/** + * Free a tree built by wuss_menu_create_from_desc, including every submenu and + * copied label. Safe to pass NULL. Never call this on a static or + * caller-assembled wuss_menu_t. + * + * \param[in] menu Root menu returned by wuss_menu_create_from_desc, or NULL. + */ +void wuss_menu_destroy(wuss_menu_t *menu); + #ifdef __cplusplus } #endif diff --git a/libraries/wuss/icon/draw.c b/libraries/wuss/icon/draw.c index dafacd49..60a48266 100644 --- a/libraries/wuss/icon/draw.c +++ b/libraries/wuss/icon/draw.c @@ -422,7 +422,7 @@ void wuss__icon_draw(wuss_t *wuss, ax + (r - (dy < 0 ? -dy : dy)), ay + dy, ink); } - if (have_font) + if (have_font && icon->text != NULL && icon->text[0] != '\0') { point_t pos; diff --git a/libraries/wuss/menu/create-from-desc.c b/libraries/wuss/menu/create-from-desc.c new file mode 100644 index 00000000..80bbaefa --- /dev/null +++ b/libraries/wuss/menu/create-from-desc.c @@ -0,0 +1,471 @@ +/* create-from-desc.c -- wuss - build a menu tree from a descriptor string */ + +/* The token machine (Token / Parser / getopt / getname / getnext / isdelim) + * and the explicit menu stack are ported from PrivateEye's + * libs/appengine/wimp/menu/create-from-desc.c. What changed for wuss: the + * output is a heap wuss_menu_t tree rather than a wimp_menu, there is no title + * row (the first token of a { } block is parsed then discarded, so descriptor + * strings stay source-compatible), and a '>' submenu is deep-copied from the + * const wuss_menu_t * vararg so the whole result tree is owned uniformly and + * freed by one wuss_menu_destroy. */ + +#include +#include +#include +#include +#include + +#ifdef FORTIFY +#include "fortify/fortify.h" +#endif + +#include "base/result.h" + +#include "wuss/menu.h" + +/* ----------------------------------------------------------------------- */ + +enum { WUSS_MENU_DESC_DEPTH = 8 }; /* nested { } limit, as the Wimp original */ +enum { WUSS_MENU_DESC_TEXT = 64 }; /* per-token expansion buffer */ + +/* ----------------------------------------------------------------------- */ + +typedef enum { Name, Sep, DashSep, Push, Pop, End, Error } Token; + +enum { Tick = 1 << 0, Shade = 1 << 1, SubMenu = 1 << 2 }; + +typedef struct +{ + const char *p; + const char *start, *end; /* end exclusive */ + unsigned int flags; +} +Parser; + +static void getopt_(Parser *parser) +{ + const char *p; + int c; + + parser->flags = 0; + p = parser->p; + + for (;;) + { + c = *p; + if (c == '!') parser->flags |= Tick; + else if (c == '>') parser->flags |= SubMenu; + else if (c == '~') parser->flags |= Shade; + else { parser->p = p; return; } + p++; + } +} + +static int isdelim(int c) +{ + return c == ',' || c == '|' || c == '{' || c == '}' || c == '\0'; +} + +static void getname(Parser *parser) +{ + const char *p; + + getopt_(parser); + p = parser->p; + parser->start = p; + + while (!isdelim(*p)) + p++; + + parser->p = p; + + /* trim trailing whitespace so "Foo { " yields the name "Foo", not "Foo " */ + while (p > parser->start && isspace((unsigned char) p[-1])) + p--; + + parser->end = p; +} + +static Token getnext(Parser *parser) +{ + while (isspace((unsigned char) *parser->p)) + parser->p++; + + if (!isdelim(*parser->p)) + { + getname(parser); + if (parser->start == parser->end) + return Error; + return Name; + } + + switch (*parser->p++) + { + case ',': return Sep; + case '|': return DashSep; + case '{': return Push; + case '}': return Pop; + } + + assert(parser->p[-1] == '\0'); + return End; +} + +/* ----------------------------------------------------------------------- */ + +void wuss_menu_destroy(wuss_menu_t *menu) +{ + int i; + + if (menu == NULL) + return; + + for (i = 0; i < menu->nitems; i++) + { + free((void *) menu->items[i].text); + wuss_menu_destroy((wuss_menu_t *) menu->items[i].submenu); + } + free((void *) menu->items); + free(menu); +} + +/* Deep-copy a caller-supplied menu tree so a '>' submenu can be owned by, and + * 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) +{ + wuss_menu_t *m; + wuss_menu_item_t *items; + int i; + result_t rc; + + m = malloc(sizeof(*m)); + if (m == NULL) + return result_OOM; + + items = (src->nitems > 0) + ? calloc((size_t) src->nitems, sizeof(*items)) + : NULL; + if (src->nitems > 0 && items == NULL) + { + free(m); + return result_OOM; + } + + m->items = items; + m->nitems = src->nitems; + + for (i = 0; i < src->nitems; i++) + { + items[i].text = strdup(src->items[i].text ? src->items[i].text : ""); + items[i].flags = src->items[i].flags; + items[i].submenu = NULL; + + if (items[i].text == NULL) + { + m->nitems = i; /* only [0, i) are populated -- keeps destroy correct */ + wuss_menu_destroy(m); + return result_OOM; + } + + if (src->items[i].submenu != NULL) + { + wuss_menu_t *child; + + rc = menu_deep_copy(src->items[i].submenu, &child); + if (rc != result_OK) + { + m->nitems = i + 1; + wuss_menu_destroy(m); + return rc; + } + items[i].submenu = child; + } + } + + *out = m; + return result_OK; +} + +/* ----------------------------------------------------------------------- */ + +/* A menu under construction: a growable wuss_menu_item_t array plus the + * separator flag owed to the next-emitted item. */ +typedef struct +{ + wuss_menu_item_t *items; + int n; + int cap; + int pending_dash; +} +Building; + +static result_t build_add(Building *b, + const char *text, + unsigned int opt, + wuss_menu_t *submenu) +{ + wuss_menu_item_t *it; + char *copy; + + if (b->n == b->cap) + { + int cap; + wuss_menu_item_t *grown; + + /* ponytail: double on demand, no shrink-to-fit -- the array is small and + * freed as one block, trailing slack costs nothing */ + cap = b->cap ? b->cap * 2 : 4; + grown = realloc(b->items, (size_t) cap * sizeof(*grown)); + if (grown == NULL) + return result_OOM; + b->items = grown; + b->cap = cap; + } + + copy = strdup(text ? text : ""); + if (copy == NULL) + return result_OOM; + + it = &b->items[b->n++]; + it->text = copy; + it->flags = wuss_MENU_ITEM_NONE; + it->submenu = submenu; + + if (b->pending_dash) + { + it->flags |= wuss_MENU_ITEM_DASHED; + b->pending_dash = 0; + } + if (opt & Tick) + it->flags |= wuss_MENU_ITEM_TICKED; + if (opt & Shade) + it->flags |= wuss_MENU_ITEM_DISABLED; + + return result_OK; +} + +/* Seal a Building into a heap wuss_menu_t, transferring its item array. */ +static wuss_menu_t *build_seal(Building *b) +{ + wuss_menu_t *m; + + m = malloc(sizeof(*m)); + if (m == NULL) + return NULL; + + m->items = b->items; + m->nitems = b->n; + + b->items = NULL; + b->n = b->cap = 0; + + return m; +} + +/* Free an unsealed Building (error path): everything in it is ours. */ +static void build_discard(Building *b) +{ + int i; + + for (i = 0; i < b->n; i++) + { + free((void *) b->items[i].text); + wuss_menu_destroy((wuss_menu_t *) b->items[i].submenu); + } + free(b->items); + b->items = NULL; + b->n = b->cap = 0; +} + +/* ----------------------------------------------------------------------- */ + +result_t wuss_menu_create_from_desc(wuss_menu_t **out, const char *desc, ...) +{ + Parser parser; + Building stack[WUSS_MENU_DESC_DEPTH]; + int need_title[WUSS_MENU_DESC_DEPTH]; + int sp; + va_list ap; + result_t rc; + char text[WUSS_MENU_DESC_TEXT]; + int i; + + assert(out != NULL); + assert(desc != NULL); + + parser.p = desc; + + for (i = 0; i < WUSS_MENU_DESC_DEPTH; i++) + { + stack[i].items = NULL; + stack[i].n = stack[i].cap = stack[i].pending_dash = 0; + need_title[i] = 0; + } + + sp = 0; + rc = result_OK; + + va_start(ap, desc); + + for (;;) + { + Token tok; + + tok = getnext(&parser); + + if (tok == End) + break; + + if (tok == Error) + { + rc = result_BAD_ARG; + break; + } + + if (tok == Sep) + continue; + + if (tok == DashSep) + { + stack[sp].pending_dash = 1; + continue; + } + + if (tok == Push) + { + if (sp + 1 >= WUSS_MENU_DESC_DEPTH) + { + rc = result_BAD_ARG; /* too deep */ + break; + } + sp++; + need_title[sp] = 1; /* first token inside { } is a title, discard it */ + continue; + } + + if (tok == Pop) + { + wuss_menu_t *sub; + + if (sp == 0) + { + rc = result_BAD_ARG; /* } without { */ + break; + } + + sub = build_seal(&stack[sp]); + if (sub == NULL) + { + rc = result_OOM; + break; + } + sp--; + + if (stack[sp].n == 0) + { + wuss_menu_destroy(sub); + rc = result_BAD_ARG; /* { } with no preceding item */ + break; + } + stack[sp].items[stack[sp].n - 1].submenu = sub; + continue; + } + + /* tok == Name */ + { + char *q; + const char *s; + unsigned int opt; + int overflow; + + opt = parser.flags; + q = text; + overflow = 0; + + for (s = parser.start; s != parser.end; s++) + { + if (*s == '%' && (s + 1) != parser.end && s[1] == 's') + { + const char *arg; + size_t len; + + arg = va_arg(ap, const char *); + len = strlen(arg); + if ((size_t) (q - text) + len >= sizeof(text)) + { + overflow = 1; + break; + } + memcpy(q, arg, len); + q += len; + s++; + } + else + { + if ((size_t) (q - text) + 1 >= sizeof(text)) + { + overflow = 1; + break; + } + *q++ = *s; + } + } + + if (overflow) + { + rc = result_BAD_ARG; + break; + } + *q = '\0'; + + if (need_title[sp]) + { + need_title[sp] = 0; /* the submenu title -- parsed, not emitted */ + continue; + } + + { + wuss_menu_t *submenu; + + submenu = NULL; + if (opt & SubMenu) + { + const wuss_menu_t *borrowed; + + borrowed = va_arg(ap, const wuss_menu_t *); + rc = menu_deep_copy(borrowed, &submenu); + if (rc != result_OK) + break; + } + + rc = build_add(&stack[sp], text, opt, submenu); + if (rc != result_OK) + { + wuss_menu_destroy(submenu); + break; + } + } + } + } + + va_end(ap); + + if (rc == result_OK && sp != 0) + rc = result_BAD_ARG; /* unclosed { */ + + if (rc != result_OK) + { + for (i = 0; i < WUSS_MENU_DESC_DEPTH; i++) + build_discard(&stack[i]); + return rc; + } + + *out = build_seal(&stack[0]); + if (*out == NULL) + { + build_discard(&stack[0]); + return result_OOM; + } + + return result_OK; +} diff --git a/libraries/wuss/menu/menu.c b/libraries/wuss/menu/menu.c index 40b150b6..c8e5a8d4 100644 --- a/libraries/wuss/menu/menu.c +++ b/libraries/wuss/menu/menu.c @@ -191,11 +191,15 @@ static result_t wuss__menu_spawn(wuss_t *wuss, for (i = 0; i < menu->nitems; i++) { const char *text; + int len; int split; bmfont_width_t w; text = menu->items[i].text ? menu->items[i].text : ""; - if (bmfont_measure(wuss->font, text, (int) strlen(text), + len = (int) strlen(text); + if (len == 0) + continue; /* separator row, nothing to measure */ + if (bmfont_measure(wuss->font, text, len, INT_MAX, &split, &w) == result_OK && (int) w > widest) widest = (int) w; } diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index b38be4e8..ce1ec4a2 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -20,6 +20,9 @@ #include "io/path.h" #include "wuss/wuss.h" #include "wuss/window.h" +#ifdef WUSS_MENUS +#include "wuss/menu.h" +#endif #include "test/all-tests.h" @@ -2578,6 +2581,76 @@ result_t wuss_test(const char *resources) if (tc_a.stop_count != 1 || tc_b.stop_count != 1) goto Failure; +#ifdef WUSS_MENUS + printf("test: wuss_menu_create_from_desc parses a descriptor tree\n"); + { + static const wuss_menu_item_t borrowed_items[] = + { + { "Info", wuss_MENU_ITEM_NONE, NULL }, + { "About", wuss_MENU_ITEM_NONE, NULL } + }; + static const wuss_menu_t borrowed = + { + borrowed_items, NELEMS(borrowed_items) + }; + wuss_menu_t *m; + const wuss_menu_t *sub; + + /* root: App, Grid(!), Export(~), Quit(|), Help(> borrowed) + * with "File" substituted for both %s. */ + rc = wuss_menu_create_from_desc(&m, + "App, %s { %s, !Grid, ~Export, |Quit }, >Help", + "File", "File", &borrowed); + if (rc != result_OK) + goto Failure; + + if (m->nitems != 3) goto MenuFail; + if (strcmp(m->items[0].text, "App") != 0) goto MenuFail; + if (m->items[0].submenu != NULL) goto MenuFail; + + /* items[1] "File" carries the { } submenu; its first token was the title + * and is not emitted, so the submenu has 3 rows */ + if (strcmp(m->items[1].text, "File") != 0) goto MenuFail; + sub = m->items[1].submenu; + printf(" sub1=%p nitems=%d\n", (void *) sub, sub ? sub->nitems : -1); + if (sub != NULL) + printf(" sub1[0]=%s f=%u [1]=%s f=%u [2]=%s f=%u\n", + sub->items[0].text, sub->items[0].flags, + sub->nitems > 1 ? sub->items[1].text : "?", + sub->nitems > 1 ? sub->items[1].flags : 0, + sub->nitems > 2 ? sub->items[2].text : "?", + sub->nitems > 2 ? sub->items[2].flags : 0); + if (sub == NULL || sub->nitems != 3) goto MenuFail; + if (strcmp(sub->items[0].text, "Grid") != 0) goto MenuFail; + if (!(sub->items[0].flags & wuss_MENU_ITEM_TICKED)) goto MenuFail; + if (!(sub->items[1].flags & wuss_MENU_ITEM_DISABLED)) goto MenuFail; + if (!(sub->items[2].flags & wuss_MENU_ITEM_DASHED)) goto MenuFail; + if (strcmp(sub->items[2].text, "Quit") != 0) goto MenuFail; + + /* items[2] "Help" got a deep copy of the borrowed menu */ + if (strcmp(m->items[2].text, "Help") != 0) goto MenuFail; + sub = m->items[2].submenu; + if (sub == NULL || sub == &borrowed) goto MenuFail; + if (sub->nitems != 2) goto MenuFail; + if (strcmp(sub->items[1].text, "About") != 0) goto MenuFail; + + wuss_menu_destroy(m); + + /* malformed: unbalanced brace */ + rc = wuss_menu_create_from_desc(&m, "A { B, C"); + if (rc != result_BAD_ARG) + goto MenuFail; + + rc = result_OK; + goto MenuOK; + +MenuFail: + printf("wuss_test: menu-desc check failed\n"); + return result_TEST_FAILED; +MenuOK: ; + } +#endif + wuss_destroy(wuss); free(pixels); From e69f71ad90f00f6f8ca0aa97662b393271b7c9b2 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Tue, 1 Sep 2026 00:02:54 +0100 Subject: [PATCH 117/287] feat(wuss): menu appearance fixes and screen_draw_dashed_line - menus default to a white (nearest-palette) backdrop - menus keep their titlebar furniture (drop wuss_WINDOW_NO_TITLEBAR); submenu positioning switched to content bounds so rows still align under the titlebar - add screen_draw_dashed_line (Bresenham + dash-period counter) and use it for menu separator rows; covered by a new screen test - menus open under the pointer: cache wuss->pointer in both mouse entry points, expose wuss_get_pointer, shift the root menu content top-left so the pointer lands over row 0 - menu-entry highlight now swaps the row's own resolved fg/bg instead of pulling the window-manager accent pair - drop stray debug printfs left in wuss-test.c Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- CMakeLists.txt | 1 + apps/wuss/main.c | 2 +- include/framebuf/screen.h | 25 +++++++ include/wuss/wuss.h | 11 +++ libraries/framebuf/screen/screen-draw.c | 75 ++++++++++++++++++++ libraries/framebuf/screen/test/screen-test.c | 68 +++++++++++++++++- libraries/wuss/create.c | 30 ++++++++ libraries/wuss/get-pointer.c | 12 ++++ libraries/wuss/icon/draw.c | 36 +++++----- libraries/wuss/impl.h | 3 + libraries/wuss/menu/menu.c | 22 ++++-- libraries/wuss/mouse-click.c | 2 + libraries/wuss/mouse-move.c | 2 + libraries/wuss/test/wuss-test.c | 8 --- 14 files changed, 263 insertions(+), 34 deletions(-) create mode 100644 libraries/wuss/get-pointer.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 8c1ba83c..a341d931 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -337,6 +337,7 @@ set(WUSS_CORE_SOURCES libraries/wuss/create.c libraries/wuss/destroy.c libraries/wuss/get-font.c + libraries/wuss/get-pointer.c libraries/wuss/idle.c libraries/wuss/impl.h libraries/wuss/invalidate.c diff --git a/apps/wuss/main.c b/apps/wuss/main.c index f9cf4b7e..5ff8af36 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -233,7 +233,7 @@ static void menu_selected(const wuss_menu_t *menu, static result_t spawn_menu(void) { - return wuss_menu_open(g_wuss, &g_menu, POINT(120, 120), + return wuss_menu_open(g_wuss, &g_menu, wuss_get_pointer(g_wuss), menu_selected, NULL, NULL); } diff --git a/include/framebuf/screen.h b/include/framebuf/screen.h index 309f205b..12bdd4a3 100644 --- a/include/framebuf/screen.h +++ b/include/framebuf/screen.h @@ -239,6 +239,31 @@ void screen_draw_line(screen_t *scr, int y1, colour_t colour); +/** + * Draws a stippled line: `on` pixels drawn, then `off` skipped, repeating along + * the line. Bresenham stepping, so the dash period is measured in steps not + * Euclidean distance. `on` <= 0 draws nothing; `off` <= 0 gives a solid line. + * + * Coordinates are `int`s and inclusive. + * + * \param[in] scr Screen to draw upon. + * \param[in] x0 X coordinate of first point of line. + * \param[in] y0 Y coordinate of first point of line. + * \param[in] x1 X coordinate of second point of line. + * \param[in] y1 Y coordinate of second point of line. + * \param[in] on Length of each dash, in steps. + * \param[in] off Gap between dashes, in steps. + * \param[in] colour Colour of line. + */ +void screen_draw_dashed_line(screen_t *scr, + int x0, + int y0, + int x1, + int y1, + int on, + int off, + colour_t colour); + /** * Draws a line (fixed-point Wu version with anti-aliasing). * diff --git a/include/wuss/wuss.h b/include/wuss/wuss.h index b36a36af..439b1d58 100644 --- a/include/wuss/wuss.h +++ b/include/wuss/wuss.h @@ -306,6 +306,17 @@ void wuss_destroy(wuss_t *doomed); */ bmfont_t *wuss_get_font(const wuss_t *wuss); +/** + * The last pointer position seen by wuss_mouse_click or wuss_mouse_move, screen + * space. (0,0) until the first mouse event. Handy for opening a pop-up menu + * under the pointer from a task's icon handler, which gets no coordinate of its + * own. + * + * \param[in] wuss Window manager. + * \return Last pointer position, screen space. + */ +point_t wuss_get_pointer(const wuss_t *wuss); + /** * Redraw every window, back-to-front, having first painted the configured * backdrop colour (see wuss_config_t::backdrop) behind them, if any. diff --git a/libraries/framebuf/screen/screen-draw.c b/libraries/framebuf/screen/screen-draw.c index 26523abe..40a2f01a 100644 --- a/libraries/framebuf/screen/screen-draw.c +++ b/libraries/framebuf/screen/screen-draw.c @@ -433,6 +433,81 @@ void screen_draw_line(screen_t *scr, } } +void screen_draw_dashed_line(screen_t *scr, + int x0, + int y0, + int x1, + int y1, + int on, + int off, + colour_t colour) +{ + box_t clip_box; + box_t bounds; + int rx0, ry0, rx1, ry1; + int dx, dy; + int adx, ady; + int sx, sy; + int error, e2; + int period; + int phase; + + if (on <= 0) + return; + if (off < 0) + off = 0; + period = on + off; + + if (screen_get_clip(scr, &clip_box)) + return; /* invalid clipped screen */ + + rx0 = x0; + ry0 = y0; + rx1 = x1; + ry1 = y1; + if (line_clip(&clip_box, &rx0, &ry0, &rx1, &ry1) == 0) + return; + + screen_get_bounds(scr, &bounds); + (void) line_clip(&bounds, &x0, &y0, &x1, &y1); + + dx = x1 - x0; + adx = abs(dx); + sx = SGN(dx); + + dy = y1 - y0; + ady = -abs(dy); + sy = SGN(dy); + + error = adx + ady; + phase = 0; + + for (;;) + { + if (phase < on) + screen_draw_pixel(scr, x0, y0, colour); + if (++phase >= period) + phase = 0; + + if (x0 == x1 && y0 == y1) + break; + + e2 = 2 * error; + if (e2 >= ady) + { + if (x0 == x1) { break; } + error += ady; + x0 += sx; + } + if (e2 <= adx) + { + if (y0 == y1) { break; } + error += adx; + y0 += sy; + } + } +} + void screen_draw_line_wu_fix8(screen_t *scr, fix8_t x0_f8, fix8_t y0_f8, diff --git a/libraries/framebuf/screen/test/screen-test.c b/libraries/framebuf/screen/test/screen-test.c index 2d14a006..3d903ef2 100644 --- a/libraries/framebuf/screen/test/screen-test.c +++ b/libraries/framebuf/screen/test/screen-test.c @@ -449,6 +449,71 @@ static result_t test_fill_pattern(void) /* ----------------------------------------------------------------------- */ +static result_t test_dashed_line(void) +{ + static testscreen_t ts; + static testscreen_t enc; + + int fg; + int x; + int on_count, off_count; + + fg = (int) np_encode(&enc, 255, 0, 0); + + /* Horizontal line y=10, x in [0,19], on=2 off=2: phase cycles + * 0,1 (drawn) 2,3 (skipped) starting at x=0. */ + testscreen_init(&ts); + screen_draw_dashed_line(&ts.scr, 0, 10, 19, 10, 2, 2, colour_rgb(255, 0, 0)); + + on_count = off_count = 0; + for (x = 0; x <= 19; x++) + { + int lit = (np_at(&ts, x, 10) == fg); + int want = ((x % 4) < 2); + if (lit != want) + { + printf("screen: dashed_line wrong at x=%d (lit=%d want=%d)\n", + x, lit, want); + return result_TEST_FAILED; + } + if (lit) on_count++; else off_count++; + } + if (on_count != 10 || off_count != 10) + { + printf("screen: dashed_line dash ratio off (on=%d off=%d)\n", + on_count, off_count); + return result_TEST_FAILED; + } + + /* off <= 0 gives a solid line. */ + testscreen_init(&ts); + screen_draw_dashed_line(&ts.scr, 0, 5, 9, 5, 3, 0, colour_rgb(255, 0, 0)); + for (x = 0; x <= 9; x++) + { + if (np_at(&ts, x, 5) != fg) + { + printf("screen: dashed_line with off=0 left a gap at x=%d\n", x); + return result_TEST_FAILED; + } + } + + /* on <= 0 draws nothing. */ + testscreen_init(&ts); + screen_draw_dashed_line(&ts.scr, 0, 7, 9, 7, 0, 4, colour_rgb(255, 0, 0)); + for (x = 0; x <= 9; x++) + { + if (np_at(&ts, x, 7) != (int) (pixelfmt_bgrx8888_t) BACKGROUND) + { + printf("screen: dashed_line with on=0 drew a pixel at x=%d\n", x); + return result_TEST_FAILED; + } + } + + return result_TEST_PASSED; +} + +/* ----------------------------------------------------------------------- */ + result_t screen_test(const char *resources) { typedef result_t (*screentestfn)(void); @@ -459,7 +524,8 @@ result_t screen_test(const char *resources) test_clipping_still_happens, test_wu_fix8_extreme_coords, test_ninepatch, - test_fill_pattern + test_fill_pattern, + test_dashed_line }; result_t rc; diff --git a/libraries/wuss/create.c b/libraries/wuss/create.c index acb6ba48..f1b559ba 100644 --- a/libraries/wuss/create.c +++ b/libraries/wuss/create.c @@ -89,6 +89,34 @@ result_t wuss_create(screen_t *scr, memcpy(w->palette, palette, npalette * sizeof(*w->palette)); w->npalette = npalette; + /* Cache the palette index nearest to opaque white, for menu backdrops and + * anything else wanting "paper". Sum-of-squared-channel-distance to white; + * ties keep the first. */ + { + unsigned long best_d; + int i; + + w->white = 0; + best_d = ~0UL; + for (i = 0; i < npalette; i++) + { + unsigned int px; + long dr, dg, db; + unsigned long d; + + px = w->palette[i].primary; + dr = 255 - (long) ((px >> 16) & 0xFF); + dg = 255 - (long) ((px >> 8) & 0xFF); + db = 255 - (long) ( px & 0xFF); + d = (unsigned long) (dr * dr + dg * dg + db * db); + if (d < best_d) + { + best_d = d; + w->white = i; + } + } + } + if (config != NULL) { w->backdrop = config->backdrop; @@ -222,6 +250,8 @@ result_t wuss_create(screen_t *scr, w->layout = NULL; w->cascade.x = 0; w->cascade.y = 0; + w->pointer.x = 0; + w->pointer.y = 0; list_init(&w->z_order); diff --git a/libraries/wuss/get-pointer.c b/libraries/wuss/get-pointer.c new file mode 100644 index 00000000..ee538d84 --- /dev/null +++ b/libraries/wuss/get-pointer.c @@ -0,0 +1,12 @@ +/* get-pointer.c -- wuss - minimal window manager */ + +#include <assert.h> + +#include "impl.h" + +point_t wuss_get_pointer(const wuss_t *wuss) +{ + assert(wuss != NULL); + + return wuss->pointer; +} diff --git a/libraries/wuss/icon/draw.c b/libraries/wuss/icon/draw.c index 60a48266..0de3fdd7 100644 --- a/libraries/wuss/icon/draw.c +++ b/libraries/wuss/icon/draw.c @@ -359,31 +359,30 @@ void wuss__icon_draw(wuss_t *wuss, case wuss_ICON_TYPE_MENU_ENTRY: { - colour_t ink, ground; + colour_t ink, ground, tmp; int disabled, highlit, pad, mid_y; disabled = (icon->flags & wuss_ICON_FLAGS_DISABLED) != 0; highlit = icon->hovered && !disabled; pad = 4; - /* highlight inverts to the accent colours (the window manager's - * emphasis pair); otherwise ink over the row's own/inherited ground */ - if (highlit) - { - ground = wuss->palette[wuss->accent_bg]; - ink = wuss->palette[wuss->accent_fg]; - } + /* resolve the row's own ink over its own/inherited ground */ + if (icon->bg != wuss_NO_BACKGROUND) + ground = wuss->palette[icon->bg]; + else if (icon->window->bg.colour != wuss_NO_BACKGROUND) + ground = (icon->window->bg.pattern != screen_PATTERN_SOLID) + ? wuss->palette[icon->window->bg.pattern_bg] + : wuss->palette[icon->window->bg.colour]; else + ground = fg; /* bmfont still needs a blend colour */ + ink = disabled ? wuss->palette[wuss->bevel_dark] : fg; + + /* highlight simply swaps the row's fg/bg */ + if (highlit) { - if (icon->bg != wuss_NO_BACKGROUND) - ground = wuss->palette[icon->bg]; - else if (icon->window->bg.colour != wuss_NO_BACKGROUND) - ground = (icon->window->bg.pattern != screen_PATTERN_SOLID) - ? wuss->palette[icon->window->bg.pattern_bg] - : wuss->palette[icon->window->bg.colour]; - else - ground = fg; /* bmfont still needs a blend colour */ - ink = disabled ? wuss->palette[wuss->bevel_dark] : fg; + tmp = ink; + ink = ground; + ground = tmp; } if (highlit || icon->bg != wuss_NO_BACKGROUND) @@ -393,7 +392,8 @@ void wuss__icon_draw(wuss_t *wuss, if (icon->flags & wuss_ICON_FLAGS_SEPARATOR) { mid_y = b.y0 + (b.y1 - b.y0) / 2; - screen_draw_line(scr, b.x0 + pad, mid_y, b.x1 - 1 - pad, mid_y, ink); + screen_draw_dashed_line(scr, b.x0 + pad, mid_y, b.x1 - 1 - pad, mid_y, + 2, 2, ink); break; } diff --git a/libraries/wuss/impl.h b/libraries/wuss/impl.h index 49d004be..5944123f 100644 --- a/libraries/wuss/impl.h +++ b/libraries/wuss/impl.h @@ -63,6 +63,7 @@ struct wuss bmfont_t *font; /* nullable, not owned */ colour_t *palette; /* owned */ int npalette; + wuss_colour_t white; /* palette index nearest to white */ #ifdef WUSS_FURNITURE wuss_palette_t furniture_colours; #endif @@ -87,6 +88,8 @@ struct wuss * created on first auto-placement */ point_t cascade; /* next cascade offset, used once the * layout packer has no room left */ + point_t pointer; /* last pointer position, screen + * space, from any mouse click/move */ #ifdef WUSS_ICONS wuss_icon_t *pressed_icon; /* button icon held down, NULL when * idle; released on any MOUSE_UP diff --git a/libraries/wuss/menu/menu.c b/libraries/wuss/menu/menu.c index c8e5a8d4..e7ea9f14 100644 --- a/libraries/wuss/menu/menu.c +++ b/libraries/wuss/menu/menu.c @@ -111,11 +111,12 @@ static result_t wuss__menu_handle(wuss_window_t *window, if (item->submenu == NULL || self->child != NULL) return result_OK; - wuss_window_get_visible_bounds(self->window, &wb); + wuss_window_get_content_bounds(self->window, &wb); wuss_icon_get_bbox(icon, &ib); - /* The menu is unscrolled, so a row's document y equals its screen y - * offset from the window's top. */ + /* wb is the content box in screen space; the menu is unscrolled, so a + * row's document y is its offset from the content top. The child's own + * titlebar sits above `at`, so a submenu row lines up with its parent. */ at.x = wb.x1 - WUSS_MENU_SUBMENU_OVERLAP; at.y = wb.y0 + ib.y0; @@ -269,13 +270,16 @@ static result_t wuss__menu_spawn(wuss_t *wuss, size = SIZE2D(width, height); task = wuss_task_start(wuss__menu_handle, node); - rc = wuss_window_create_placed(wuss, size, NULL, - wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE + /* ponytail: menus carry no title string yet; a bare titlebar is enough for + * the drag/knock-back affordance. Add wuss_menu_t.title if a caption is + * wanted. */ + rc = wuss_window_create_placed(wuss, size, "", + 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(1), + wuss_BACKDROP_COLOUR(wuss->white), &task, size, size, &node->window); if (rc != result_OK) { @@ -328,6 +332,12 @@ result_t wuss_menu_open(wuss_t *wuss, wuss->menu_chain = NULL; } + /* 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 + * left so `at` (the pointer) lands over row 0. */ + at.x -= WUSS_MENU_TICK_W; + at.y -= WUSS_MENU_ROW_PAD; + rc = wuss__menu_spawn(wuss, menu, at, NULL, on_select, ctx, &root); if (rc != result_OK) return rc; diff --git a/libraries/wuss/mouse-click.c b/libraries/wuss/mouse-click.c index cf9c7356..d513a7f5 100644 --- a/libraries/wuss/mouse-click.c +++ b/libraries/wuss/mouse-click.c @@ -15,6 +15,8 @@ result_t wuss_mouse_click(wuss_t *wuss, x = p.x; y = p.y; + wuss->pointer = p; + #ifdef WUSS_ICONS /* Release a held button icon on any MOUSE_UP, before the hit-test picks a * window: the up may land on a window that opened over the icon's owner on diff --git a/libraries/wuss/mouse-move.c b/libraries/wuss/mouse-move.c index fa1c9eaf..99f0e724 100644 --- a/libraries/wuss/mouse-move.c +++ b/libraries/wuss/mouse-move.c @@ -10,6 +10,8 @@ result_t wuss_mouse_move(wuss_t *wuss, point_t p, wuss_window_t **hit) x = p.x; y = p.y; + wuss->pointer = p; + #ifdef WUSS_FURNITURE if (wuss->furniture.dragging != NULL) { diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index ce1ec4a2..8c9d5d18 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -2612,14 +2612,6 @@ result_t wuss_test(const char *resources) * and is not emitted, so the submenu has 3 rows */ if (strcmp(m->items[1].text, "File") != 0) goto MenuFail; sub = m->items[1].submenu; - printf(" sub1=%p nitems=%d\n", (void *) sub, sub ? sub->nitems : -1); - if (sub != NULL) - printf(" sub1[0]=%s f=%u [1]=%s f=%u [2]=%s f=%u\n", - sub->items[0].text, sub->items[0].flags, - sub->nitems > 1 ? sub->items[1].text : "?", - sub->nitems > 1 ? sub->items[1].flags : 0, - sub->nitems > 2 ? sub->items[2].text : "?", - sub->nitems > 2 ? sub->items[2].flags : 0); if (sub == NULL || sub->nitems != 3) goto MenuFail; if (strcmp(sub->items[0].text, "Grid") != 0) goto MenuFail; if (!(sub->items[0].flags & wuss_MENU_ITEM_TICKED)) goto MenuFail; From a50c2bae768b9c05f77f6fd728719afd6d659c33 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Tue, 1 Sep 2026 00:16:53 +0100 Subject: [PATCH 118/287] fix(wuss): draw menu titlebars The menu helper created its window at a packer slot then wuss_window_move'd it to the pointer. wuss_window_move blits the window's already-rendered pixels to the new spot, but a just-created window has none, so the titlebar landed unpainted (a thin sliver was all that got invalidated). - wuss__menu_spawn now creates the window at the target point directly with wuss_window_create; no post-move - add wuss_menu_t.title (first field) so the titlebar has a caption; menu.c passes it, wuss_menu_create_from_desc leaves it NULL (still discards the descriptor's { } title token), menu_deep_copy carries it, wuss_menu_destroy frees it - demo menus in apps/wuss titled; test literals updated for the new field Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- apps/wuss/main.c | 4 ++-- include/wuss/menu.h | 3 +++ libraries/wuss/menu/create-from-desc.c | 13 +++++++++++ libraries/wuss/menu/menu.c | 32 +++++++++++++++----------- libraries/wuss/test/wuss-test.c | 2 +- 5 files changed, 38 insertions(+), 16 deletions(-) diff --git a/apps/wuss/main.c b/apps/wuss/main.c index 5ff8af36..6086d1a5 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -203,7 +203,7 @@ static const wuss_menu_item_t g_menu_export_items[] = }; static const wuss_menu_t g_menu_export = { - g_menu_export_items, NELEMS(g_menu_export_items) + "Export", g_menu_export_items, NELEMS(g_menu_export_items) }; static const wuss_menu_item_t g_menu_items[] = @@ -217,7 +217,7 @@ static const wuss_menu_item_t g_menu_items[] = }; static const wuss_menu_t g_menu = { - g_menu_items, NELEMS(g_menu_items) + "Display", g_menu_items, NELEMS(g_menu_items) }; static void menu_selected(const wuss_menu_t *menu, diff --git a/include/wuss/menu.h b/include/wuss/menu.h index ca286f9b..4a348c4b 100644 --- a/include/wuss/menu.h +++ b/include/wuss/menu.h @@ -55,6 +55,9 @@ wuss_menu_item_t; /** A menu: an array of items the caller owns. */ typedef struct wuss_menu { + const char *title; /**< titlebar caption; NULL is treated as "" + * (wuss_menu_create_from_desc leaves this + * NULL) */ const wuss_menu_item_t *items; int nitems; } diff --git a/libraries/wuss/menu/create-from-desc.c b/libraries/wuss/menu/create-from-desc.c index 80bbaefa..3b8e3078 100644 --- a/libraries/wuss/menu/create-from-desc.c +++ b/libraries/wuss/menu/create-from-desc.c @@ -125,6 +125,7 @@ void wuss_menu_destroy(wuss_menu_t *menu) free((void *) menu->items[i].text); wuss_menu_destroy((wuss_menu_t *) menu->items[i].submenu); } + free((void *) menu->title); free((void *) menu->items); free(menu); } @@ -151,6 +152,14 @@ static result_t menu_deep_copy(const wuss_menu_t *src, wuss_menu_t **out) return result_OOM; } + m->title = src->title ? strdup(src->title) : NULL; + if (src->title != NULL && m->title == NULL) + { + free(items); + free(m); + return result_OOM; + } + m->items = items; m->nitems = src->nitems; @@ -253,6 +262,10 @@ static wuss_menu_t *build_seal(Building *b) if (m == NULL) return NULL; + /* ponytail: the descriptor's { } title token is still discarded, so a + * parser-built menu has no caption. Wire it through Building if that + * changes. */ + m->title = NULL; m->items = b->items; m->nitems = b->n; diff --git a/libraries/wuss/menu/menu.c b/libraries/wuss/menu/menu.c index e7ea9f14..4ff8c13e 100644 --- a/libraries/wuss/menu/menu.c +++ b/libraries/wuss/menu/menu.c @@ -176,6 +176,7 @@ static result_t wuss__menu_spawn(wuss_t *wuss, int width, height; int i; size2d_t size; + box_t content; assert(wuss != NULL); assert(menu != NULL); @@ -270,17 +271,24 @@ static result_t wuss__menu_spawn(wuss_t *wuss, size = SIZE2D(width, height); task = wuss_task_start(wuss__menu_handle, node); - /* ponytail: menus carry no title string yet; a bare titlebar is enough for - * the drag/knock-back affordance. Add wuss_menu_t.title if a caption is - * wanted. */ - rc = wuss_window_create_placed(wuss, size, "", - 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->white), - &task, size, size, &node->window); + /* `at` is the content top-left, already clamped on screen above. Create the + * window there directly -- creating it elsewhere and wuss_window_move-ing it + * afterwards would blit its not-yet-rendered pixels and leave the titlebar + * unpainted. */ + content.x0 = at.x; + content.y0 = at.y; + content.x1 = at.x + width; + content.y1 = at.y + height; + + rc = wuss_window_create(wuss, &content, + menu->title ? menu->title : "", + 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->white), + &task, size, size, &node->window); if (rc != result_OK) { free(specs); @@ -289,8 +297,6 @@ static result_t wuss__menu_spawn(wuss_t *wuss, return rc; } - wuss_window_move(node->window, at); - rc = wuss_icon_create_array(node->window, specs, menu->nitems, node->icons); free(specs); if (rc != result_OK) diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 8c9d5d18..d0512bd5 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -2591,7 +2591,7 @@ result_t wuss_test(const char *resources) }; static const wuss_menu_t borrowed = { - borrowed_items, NELEMS(borrowed_items) + "Help", borrowed_items, NELEMS(borrowed_items) }; wuss_menu_t *m; const wuss_menu_t *sub; From 98b2dbb634168f849761988b8f1ab78a47978878 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Tue, 1 Sep 2026 00:44:40 +0100 Subject: [PATCH 119/287] feat(wuss): dashed menu items draw a rule above and stay pickable wuss_MENU_ITEM_DASHED / wuss_ICON_FLAGS_SEPARATOR previously meant "this entry IS the rule, its text is ignored". Flip it: a dashed entry now draws a dashed rule along its own top edge and then renders its label as normal, remaining hit-testable. An entry that carries the flag with no text is still a bare, inert rule. - create-from-desc: '|' sets DASHED on the following item itself, no synthetic rule row - menu.c: DASHED entries are no longer skipped by the pick guard - icon/hit-test: a SEPARATOR MENU_ENTRY is inert only when its text is empty - icon/draw: SEPARATOR draws the rule in the row's ink (visible when inverted) then falls through to tick/arrow/text - apps/wuss: drop the standalone rule row, mark Quit DASHED - also lands the wuss_menu_create_from_desc SDL launcher test and the root-title-from-first-token descriptor change Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- apps/wuss/main.c | 46 +++++++++++++++++++++++--- include/wuss/icon.h | 17 ++++++---- include/wuss/menu.h | 38 +++++++++++---------- libraries/wuss/icon/draw.c | 9 ++--- libraries/wuss/icon/hit-test.c | 8 +++-- libraries/wuss/menu/create-from-desc.c | 29 +++++++++++++++- libraries/wuss/menu/menu.c | 4 +-- libraries/wuss/test/wuss-test.c | 15 +++++---- 8 files changed, 122 insertions(+), 44 deletions(-) diff --git a/apps/wuss/main.c b/apps/wuss/main.c index 6086d1a5..b06613c5 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -194,7 +194,8 @@ static result_t spawn_porter_duff(void) } /* A static demo menu tree for the pop-up helper: a submenu, a couple of - * ticked rows and a dashed separator. wuss_menu_open never mutates it. */ + * ticked rows and a dashed rule 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 }, @@ -212,8 +213,7 @@ static const wuss_menu_item_t g_menu_items[] = { "Show grid", wuss_MENU_ITEM_TICKED, NULL }, { "Wireframe", wuss_MENU_ITEM_TICKED, NULL }, { "Export", wuss_MENU_ITEM_NONE, &g_menu_export }, - { NULL, wuss_MENU_ITEM_DASHED, NULL }, - { "Quit", wuss_MENU_ITEM_NONE, NULL } + { "Quit", wuss_MENU_ITEM_DASHED, NULL } }; static const wuss_menu_t g_menu = { @@ -237,6 +237,41 @@ static result_t spawn_menu(void) menu_selected, NULL, 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 + * run_wuss's 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_wuss, g_menu_desc, wuss_get_pointer(g_wuss), + menu_selected, NULL, NULL); +} + static const launcher_entry_t g_launcher_entries[] = { { "Ball", spawn_ball }, @@ -251,7 +286,8 @@ static const launcher_entry_t g_launcher_entries[] = { "Gradient", spawn_gradient }, { "Icons", spawn_icons }, { "Porter-Duff", spawn_porter_duff }, - { "Menu", spawn_menu } + { "Menu", spawn_menu }, + { "Menu (desc)", spawn_menu_desc } }; static wuss_button_t sdl_button_to_wuss(Uint8 button) @@ -579,6 +615,8 @@ static result_t run_wuss(const char *resources) * task ever needs deterministic teardown. */ launcher_destroy(&launcher_task); + wuss_menu_destroy(g_menu_desc); + wuss_destroy(wuss); bmfont_destroy(font); bmfont_destroy(daydream_font); diff --git a/include/wuss/icon.h b/include/wuss/icon.h index 50203b54..13c709b8 100644 --- a/include/wuss/icon.h +++ b/include/wuss/icon.h @@ -98,9 +98,12 @@ typedef enum wuss_icon_type * selected (see wuss_icon_set_selected), and an * optional submenu arrow at the right edge with * wuss_ICON_FLAGS_SUBMENU. - * wuss_ICON_FLAGS_SEPARATOR draws a horizontal - * rule instead of text. Interactive: a click - * raises wuss_EVENT_ICON like a button; a + * wuss_ICON_FLAGS_SEPARATOR draws a dashed rule + * along the top edge before the text, marking a + * group boundary; the entry keeps its label and + * stays interactive (an entry with the flag and + * no text is just a bare rule). Interactive: a + * click raises wuss_EVENT_ICON like a button; a * disabled entry never highlights and its clicks * fall through. */ } @@ -142,9 +145,11 @@ typedef enum wuss_icon_flags * a submenu. Ignored by other * types. */ wuss_ICON_FLAGS_SEPARATOR = 1 << 7 /**< wuss_ICON_TYPE_MENU_ENTRY: draw a - * horizontal rule across the - * bounding box instead of text; the - * entry is inert (not hit-tested). + * dashed rule along the entry's top + * edge, then the text as normal; the + * entry stays interactive. An entry + * carrying the flag with no text is + * a bare rule and is not hit-tested. * Ignored by other types. */ } wuss_icon_flags_t; diff --git a/include/wuss/menu.h b/include/wuss/menu.h index 4a348c4b..7d28c746 100644 --- a/include/wuss/menu.h +++ b/include/wuss/menu.h @@ -35,8 +35,9 @@ typedef enum wuss_menu_item_flags { wuss_MENU_ITEM_NONE = 0, wuss_MENU_ITEM_TICKED = 1 << 0, /**< draw a tick at the item's left edge */ - wuss_MENU_ITEM_DASHED = 1 << 1, /**< this item is a separator rule; text - * is ignored */ + wuss_MENU_ITEM_DASHED = 1 << 1, /**< draw a dashed separator immediately + * above this item; the item keeps its + * own text and stays selectable */ wuss_MENU_ITEM_DISABLED = 1 << 2 /**< greyed, never highlights, not * selectable */ } @@ -55,9 +56,7 @@ wuss_menu_item_t; /** A menu: an array of items the caller owns. */ typedef struct wuss_menu { - const char *title; /**< titlebar caption; NULL is treated as "" - * (wuss_menu_create_from_desc leaves this - * NULL) */ + const char *title; /**< titlebar caption; NULL treated as "" */ const wuss_menu_item_t *items; int nitems; } @@ -114,26 +113,29 @@ int wuss_menu_is_open(wuss_menu_handle_t handle); /** * Build a wuss_menu_t tree from a compact descriptor string. The syntax is - * lifted from PrivateEye's menu_create_from_desc. A comma separates items. A - * leading '|' marks an item a dashed separator above it - * (wuss_MENU_ITEM_DASHED). A '{ ... }' group after an item is that item's - * submenu; as in the Wimp original the first token inside the braces is a title - * and is discarded, so the same descriptor strings port across unchanged. A - * per-token prefix '!' ticks the item, '~' shades (disables) it, and '>' - * attaches a submenu pulled as a <tt>const wuss_menu_t *</tt> from the varargs - * rather than from a following '{ }' block. A "%s" in a token substitutes the - * next <tt>const char *</tt> vararg. The '>' and "%s" varargs are consumed in - * the order they are encountered scanning left to right. + * lifted from PrivateEye's menu_create_from_desc. A comma separates items. The + * very first token is the root menu's titlebar caption, not an item -- exactly + * as the first token inside a '{ }' is that submenu's title -- so the same + * descriptor strings port across unchanged. Submenus keep the Wimp behaviour of + * discarding their title token; only the root's is kept. A leading '|' on an + * item sets wuss_MENU_ITEM_DASHED on it: a dashed rule is drawn above the item + * while the item keeps its own label and stays selectable. A '{ ... }' group + * after an item is that item's submenu. A per-token prefix '!' ticks the item, + * '~' shades (disables) it, and '>' attaches a submenu pulled as a <tt>const + * wuss_menu_t *</tt> from the varargs rather than from a following '{ }' block. + * A "%s" in a token substitutes the next <tt>const char *</tt> vararg. The '>' + * and "%s" varargs are consumed in the order they are encountered scanning left + * to right. * - * Example: <tt>wuss_menu_create_from_desc(&m, "App, %s { %s, !Grid, ~Export, - * |Quit }", "File", "File")</tt>. + * Example: <tt>wuss_menu_create_from_desc(&m, "Display, Open, !Grid, ~Export, + * |Quit")</tt> -- "Display" is the caption, the menu has four rows. * * The whole tree, including copied label text, is one heap allocation graph * owned by the caller; free it with wuss_menu_destroy. wuss_menu_open treats a * desc-built tree exactly like a static literal. * * \param[out] out Filled with the root menu on success, untouched on failure. - * \param[in] desc Descriptor string. + * \param[in] desc Descriptor string; its first token is the root caption. * \return \ref result_OK, \ref result_OOM, or \ref result_BAD_ARG for a * malformed descriptor (unbalanced braces, empty token, too deep). */ diff --git a/libraries/wuss/icon/draw.c b/libraries/wuss/icon/draw.c index 0de3fdd7..33ab5cc0 100644 --- a/libraries/wuss/icon/draw.c +++ b/libraries/wuss/icon/draw.c @@ -389,12 +389,13 @@ void wuss__icon_draw(wuss_t *wuss, screen_draw_rect(scr, b.x0, b.y0, SIZE2D(b.x1 - b.x0, b.y1 - b.y0), ground); + /* a dashed rule along the entry's top edge, then the text as normal; + * drawn in the row's ink so it stays visible when the row is inverted */ if (icon->flags & wuss_ICON_FLAGS_SEPARATOR) { - mid_y = b.y0 + (b.y1 - b.y0) / 2; - screen_draw_dashed_line(scr, b.x0 + pad, mid_y, b.x1 - 1 - pad, mid_y, - 2, 2, ink); - break; + mid_y = b.y0 + 1; + screen_draw_dashed_line(scr, b.x0 + pad, mid_y, + b.x1 - 1 - pad, mid_y, 2, 2, ink); } /* left-edge tick when selected */ diff --git a/libraries/wuss/icon/hit-test.c b/libraries/wuss/icon/hit-test.c index e0a12f85..79fb799d 100644 --- a/libraries/wuss/icon/hit-test.c +++ b/libraries/wuss/icon/hit-test.c @@ -14,8 +14,10 @@ wuss_icon_t *wuss__icon_hit_test(wuss_window_t *window, point_t doc_point) { it = window->icons[i]; - /* a bitmap icon is interactive only when it asks to be; a menu separator - * never is; the other clickable types always are */ + /* a bitmap icon is interactive only when it asks to be; a menu entry that + * is a bare rule (SEPARATOR flag, no text) is inert, but one that only + * carries a rule above its own label stays pickable; the other clickable + * types always are */ if (it->type == wuss_ICON_TYPE_BITMAP) { if (!(it->flags & wuss_ICON_FLAGS_INTERACTIVE)) @@ -23,7 +25,7 @@ wuss_icon_t *wuss__icon_hit_test(wuss_window_t *window, point_t doc_point) } else if (it->type == wuss_ICON_TYPE_MENU_ENTRY) { - if (it->flags & wuss_ICON_FLAGS_SEPARATOR) + if ((it->flags & wuss_ICON_FLAGS_SEPARATOR) && it->text[0] == '\0') continue; } else if (it->type != wuss_ICON_TYPE_BUTTON && diff --git a/libraries/wuss/menu/create-from-desc.c b/libraries/wuss/menu/create-from-desc.c index 3b8e3078..94ed53a8 100644 --- a/libraries/wuss/menu/create-from-desc.c +++ b/libraries/wuss/menu/create-from-desc.c @@ -240,11 +240,14 @@ static result_t build_add(Building *b, it->flags = wuss_MENU_ITEM_NONE; it->submenu = submenu; + /* '|' before this item means "draw a dashed rule above it"; the item keeps + * its own text and stays pickable. */ if (b->pending_dash) { it->flags |= wuss_MENU_ITEM_DASHED; b->pending_dash = 0; } + if (opt & Tick) it->flags |= wuss_MENU_ITEM_TICKED; if (opt & Shade) @@ -297,6 +300,7 @@ result_t wuss_menu_create_from_desc(wuss_menu_t **out, const char *desc, ...) 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; @@ -313,8 +317,14 @@ result_t wuss_menu_create_from_desc(wuss_menu_t **out, const char *desc, ...) stack[i].items = NULL; stack[i].n = stack[i].cap = stack[i].pending_dash = 0; need_title[i] = 0; + titles[i] = NULL; } + /* the descriptor's first token is the root menu's title, exactly as the + * first token inside a { } is that submenu's -- so the same strings port + * from PrivateEye unchanged */ + need_title[0] = 1; + sp = 0; rc = result_OK; @@ -433,7 +443,18 @@ result_t wuss_menu_create_from_desc(wuss_menu_t **out, const char *desc, ...) if (need_title[sp]) { - need_title[sp] = 0; /* the submenu title -- parsed, not emitted */ + /* the menu's title token -- captured, not emitted as an item; kept + * only for the root (sp 0), submenus get no caption */ + need_title[sp] = 0; + if (sp == 0) + { + titles[0] = strdup(text); + if (titles[0] == NULL) + { + rc = result_OOM; + break; + } + } continue; } @@ -469,7 +490,10 @@ result_t wuss_menu_create_from_desc(wuss_menu_t **out, const char *desc, ...) if (rc != result_OK) { for (i = 0; i < WUSS_MENU_DESC_DEPTH; i++) + { build_discard(&stack[i]); + free(titles[i]); + } return rc; } @@ -477,8 +501,11 @@ result_t wuss_menu_create_from_desc(wuss_menu_t **out, const char *desc, ...) if (*out == NULL) { build_discard(&stack[0]); + free(titles[0]); return result_OOM; } + (*out)->title = titles[0]; /* ownership transferred; NULL if none given */ + return result_OK; } diff --git a/libraries/wuss/menu/menu.c b/libraries/wuss/menu/menu.c index 4ff8c13e..c085598e 100644 --- a/libraries/wuss/menu/menu.c +++ b/libraries/wuss/menu/menu.c @@ -91,7 +91,7 @@ static result_t wuss__menu_handle(wuss_window_t *window, item = &self->menu->items[index]; - if (item->flags & (wuss_MENU_ITEM_DASHED | wuss_MENU_ITEM_DISABLED)) + if (item->flags & wuss_MENU_ITEM_DISABLED) return result_OK; if (event->data.icon.action == wuss_MOUSE_MOVE) @@ -200,7 +200,7 @@ static result_t wuss__menu_spawn(wuss_t *wuss, text = menu->items[i].text ? menu->items[i].text : ""; len = (int) strlen(text); if (len == 0) - continue; /* separator row, nothing to measure */ + continue; /* empty label (bare rule row); nothing to measure */ if (bmfont_measure(wuss->font, text, len, INT_MAX, &split, &w) == result_OK && (int) w > widest) widest = (int) w; diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index d0512bd5..9335870c 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -2596,28 +2596,31 @@ result_t wuss_test(const char *resources) wuss_menu_t *m; const wuss_menu_t *sub; - /* root: App, <title discarded> Grid(!), Export(~), Quit(|), Help(> borrowed) - * with "File" substituted for both %s. */ + /* "Root" is the root caption (first token), then items App, + * File{ %s title discarded, Grid(!), Export(~), |Quit }, Help(> borrowed), + * with "File" substituted for both %s. '|Quit' sets DASHED on Quit itself + * (rule drawn above it, text kept), so the submenu holds 3 rows. */ rc = wuss_menu_create_from_desc(&m, - "App, %s { %s, !Grid, ~Export, |Quit }, >Help", + "Root, App, %s { %s, !Grid, ~Export, |Quit }, >Help", "File", "File", &borrowed); if (rc != result_OK) goto Failure; if (m->nitems != 3) goto MenuFail; + if (m->title == NULL || strcmp(m->title, "Root") != 0) goto MenuFail; if (strcmp(m->items[0].text, "App") != 0) goto MenuFail; if (m->items[0].submenu != NULL) goto MenuFail; /* items[1] "File" carries the { } submenu; its first token was the title - * and is not emitted, so the submenu has 3 rows */ + * and is not emitted; '|Quit' keeps Quit's text and flags it DASHED */ if (strcmp(m->items[1].text, "File") != 0) goto MenuFail; sub = m->items[1].submenu; if (sub == NULL || sub->nitems != 3) goto MenuFail; if (strcmp(sub->items[0].text, "Grid") != 0) goto MenuFail; if (!(sub->items[0].flags & wuss_MENU_ITEM_TICKED)) goto MenuFail; if (!(sub->items[1].flags & wuss_MENU_ITEM_DISABLED)) goto MenuFail; - if (!(sub->items[2].flags & wuss_MENU_ITEM_DASHED)) goto MenuFail; if (strcmp(sub->items[2].text, "Quit") != 0) goto MenuFail; + if (!(sub->items[2].flags & wuss_MENU_ITEM_DASHED)) goto MenuFail; /* items[2] "Help" got a deep copy of the borrowed menu */ if (strcmp(m->items[2].text, "Help") != 0) goto MenuFail; @@ -2629,7 +2632,7 @@ result_t wuss_test(const char *resources) wuss_menu_destroy(m); /* malformed: unbalanced brace */ - rc = wuss_menu_create_from_desc(&m, "A { B, C"); + rc = wuss_menu_create_from_desc(&m, "T, A { B, C"); if (rc != result_BAD_ARG) goto MenuFail; From bfca21293cdc53d9b9c08ed1ef3465edd8eb0587 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Tue, 1 Sep 2026 09:19:05 +0100 Subject: [PATCH 120/287] feat(wuss): pluggable allocator via wuss_alloc_t Add wuss_alloc_t (malloc/realloc/free hooks) and a wuss_alloc stdlib default. wuss_create takes a const wuss_alloc_t * (NULL selects wuss_alloc); the hooks are copied into struct wuss and reached through wuss__malloc/ wuss__realloc/wuss__free helpers in impl.h. Every heap block a wuss_t owns -- the instance, its palette, windows, icons, icon-pointer arrays and menu nodes -- now allocates through them. menu/create-from-desc.c is left on stdlib: its API takes no wuss_t. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- apps/wuss/main.c | 3 ++- include/wuss/wuss.h | 24 ++++++++++++++++++++++ libraries/wuss/create.c | 29 +++++++++++++++++--------- libraries/wuss/destroy.c | 6 +++--- libraries/wuss/icon/create.c | 8 ++++---- libraries/wuss/icon/delete.c | 4 ++-- libraries/wuss/icon/free.c | 11 ++++++---- libraries/wuss/icon/set-text.c | 6 ++++-- libraries/wuss/impl.h | 21 +++++++++++++++++++ libraries/wuss/menu/menu.c | 36 +++++++++++++++++++-------------- libraries/wuss/test/wuss-test.c | 8 ++++---- libraries/wuss/window/close.c | 2 +- libraries/wuss/window/create.c | 4 ++-- 13 files changed, 114 insertions(+), 48 deletions(-) diff --git a/apps/wuss/main.c b/apps/wuss/main.c index b06613c5..98fd0747 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -445,7 +445,8 @@ static result_t run_wuss(const char *resources) config.backdrop.pattern = screen_PATTERN_DOTS; config.backdrop.pattern_bg = palette_PICO8_LIGHT_GREY; - rc = wuss_create(&scr, font, palette, NELEMS(palette), &config, &wuss); + rc = wuss_create(&scr, font, palette, NELEMS(palette), &config, NULL, + &wuss); if (rc != result_OK) goto Failure; } diff --git a/include/wuss/wuss.h b/include/wuss/wuss.h index 439b1d58..b57259c8 100644 --- a/include/wuss/wuss.h +++ b/include/wuss/wuss.h @@ -16,6 +16,8 @@ extern "C" { #endif +#include <stddef.h> + #include "base/result.h" #include "framebuf/bmfont.h" #include "framebuf/screen.h" @@ -46,6 +48,24 @@ typedef struct wuss_window wuss_window_t; * library is built with the WUSS_ICONS option on. */ typedef struct wuss_icon wuss_icon_t; +/** + * Allocator hooks used by a wuss_t for every heap block it owns (the instance + * itself, windows, icons, menu nodes). The three members must behave like the C + * library malloc / realloc / free -- same argument and return conventions, + * realloc(NULL, n) == malloc(n), free(NULL) a no-op. Passed to wuss_create and + * copied in; NULL there selects wuss_alloc (plain stdlib). + */ +typedef struct wuss_alloc +{ + void *(*malloc)(size_t size); + void *(*realloc)(void *ptr, size_t size); + void (*free)(void *ptr); +} +wuss_alloc_t; + +/** The default allocator: the C library malloc / realloc / free. */ +extern const wuss_alloc_t wuss_alloc; + /** * Mouse buttons, RISC OS-style: Select is the primary action, Adjust the * secondary action, Menu pops up a menu. @@ -278,6 +298,9 @@ wuss_config_t; * \param[in] npalette Number of entries in palette. Ignored if palette is * NULL. * \param[in] config Creation-time configuration, or NULL for defaults. + * \param[in] alloc Allocator hooks, copied in, or NULL for \ref wuss_alloc + * (plain stdlib). Must outlive nothing -- only the three + * function pointers are kept. * \param[out] wuss Newly created window manager. * \return \ref result_OK on success, \ref result_WUSS_BAD_COLOUR if any of * config's palette entries are out of range for the palette, or another @@ -288,6 +311,7 @@ result_t wuss_create(screen_t *scr, const colour_t *palette, int npalette, const wuss_config_t *config, + const wuss_alloc_t *alloc, wuss_t **wuss); /** diff --git a/libraries/wuss/create.c b/libraries/wuss/create.c index f1b559ba..f1282b8e 100644 --- a/libraries/wuss/create.c +++ b/libraries/wuss/create.c @@ -12,6 +12,10 @@ #include "impl.h" +/* The default allocator: plain stdlib. wuss_create copies this in when its + * alloc argument is NULL. */ +const wuss_alloc_t wuss_alloc = { malloc, realloc, free }; + /* Built-in fallback palette when the caller passes NULL: black and white. * wuss makes no other assumptions about palette contents or length. */ static const colour_t wuss__default_palette[] = @@ -46,8 +50,10 @@ result_t wuss_create(screen_t *scr, const colour_t *palette, int npalette, const wuss_config_t *config, + const wuss_alloc_t *alloc, wuss_t **wuss) { + wuss_alloc_t al; wuss_t *w; #ifdef WUSS_FURNITURE wuss_palette_t pal; @@ -65,9 +71,12 @@ result_t wuss_create(screen_t *scr, assert(scr != NULL); assert(wuss != NULL); - w = malloc(sizeof(*w)); + al = (alloc != NULL) ? *alloc : wuss_alloc; + + w = al.malloc(sizeof(*w)); if (w == NULL) return result_OOM; + w->alloc = al; if (palette == NULL) { @@ -76,14 +85,14 @@ result_t wuss_create(screen_t *scr, } else if (npalette <= 0) { - free(w); + wuss__free(w, w); return result_BAD_ARG; } - w->palette = malloc(npalette * sizeof(*w->palette)); + w->palette = wuss__malloc(w, npalette * sizeof(*w->palette)); if (w->palette == NULL) { - free(w); + wuss__free(w, w); return result_OOM; } memcpy(w->palette, palette, npalette * sizeof(*w->palette)); @@ -169,8 +178,8 @@ result_t wuss_create(screen_t *scr, pal.scroll.sausages < 0 || pal.scroll.sausages >= w->npalette || validate_bevel_backdrop(w, blight, bdark, abg, afg) != result_OK) { - free(w->palette); - free(w); + wuss__free(w, w->palette); + wuss__free(w, w); return result_WUSS_BAD_COLOUR; } @@ -212,8 +221,8 @@ result_t wuss_create(screen_t *scr, } if (validate_bevel_backdrop(w, blight, bdark, abg, afg) != result_OK) { - free(w->palette); - free(w); + wuss__free(w, w->palette); + wuss__free(w, w); return result_WUSS_BAD_COLOUR; } w->bevel_light = blight; @@ -223,8 +232,8 @@ result_t wuss_create(screen_t *scr, #else if (wuss__validate_backdrop(w, &w->backdrop) != result_OK) { - free(w->palette); - free(w); + wuss__free(w, w->palette); + wuss__free(w, w); return result_WUSS_BAD_COLOUR; } #endif diff --git a/libraries/wuss/destroy.c b/libraries/wuss/destroy.c index 7bc4bf08..9ea33274 100644 --- a/libraries/wuss/destroy.c +++ b/libraries/wuss/destroy.c @@ -30,11 +30,11 @@ void wuss_destroy(wuss_t *doomed) #ifdef WUSS_ICONS wuss__icons_free((wuss_window_t *) e); #endif - free(e); + wuss__free(doomed, e); e = next; } packer_destroy(doomed->layout); - free(doomed->palette); - free(doomed); + wuss__free(doomed, doomed->palette); + wuss__free(doomed, doomed); /* reads doomed->alloc.free before freeing */ } diff --git a/libraries/wuss/icon/create.c b/libraries/wuss/icon/create.c index 72588646..d29f815d 100644 --- a/libraries/wuss/icon/create.c +++ b/libraries/wuss/icon/create.c @@ -58,23 +58,23 @@ result_t wuss_icon_create(wuss_window_t *window, if (window->nicons == window->cap_icons) { newcap = (window->cap_icons == 0) ? 4 : window->cap_icons * 2; - grown = realloc(window->icons, newcap * sizeof(*window->icons)); + grown = wuss__realloc(w, window->icons, newcap * sizeof(*window->icons)); if (grown == NULL) return result_OOM; window->icons = grown; window->cap_icons = newcap; } - it = malloc(sizeof(*it)); + it = wuss__malloc(w, sizeof(*it)); if (it == NULL) return result_OOM; src = (spec->text != NULL) ? spec->text : ""; len = strlen(src); - it->text = malloc(len + 1); + it->text = wuss__malloc(w, len + 1); if (it->text == NULL) { - free(it); + wuss__free(w, it); return result_OOM; } memcpy(it->text, src, len + 1); diff --git a/libraries/wuss/icon/delete.c b/libraries/wuss/icon/delete.c index 0642faca..35d495de 100644 --- a/libraries/wuss/icon/delete.c +++ b/libraries/wuss/icon/delete.c @@ -34,6 +34,6 @@ void wuss_icon_delete(wuss_icon_t *icon) } } - free(icon->text); - free(icon); + wuss__free(window->wuss, icon->text); + wuss__free(window->wuss, icon); } diff --git a/libraries/wuss/icon/free.c b/libraries/wuss/icon/free.c index d7cbac7e..2630b266 100644 --- a/libraries/wuss/icon/free.c +++ b/libraries/wuss/icon/free.c @@ -10,15 +10,18 @@ void wuss__icons_free(wuss_window_t *window) { - int i; + wuss_t *w; + int i; + + w = window->wuss; for (i = 0; i < window->nicons; i++) { - free(window->icons[i]->text); - free(window->icons[i]); + wuss__free(w, window->icons[i]->text); + wuss__free(w, window->icons[i]); } - free(window->icons); + wuss__free(w, window->icons); window->icons = NULL; window->nicons = 0; window->cap_icons = 0; diff --git a/libraries/wuss/icon/set-text.c b/libraries/wuss/icon/set-text.c index 3a5eca25..3f67a6cf 100644 --- a/libraries/wuss/icon/set-text.c +++ b/libraries/wuss/icon/set-text.c @@ -11,19 +11,21 @@ result_t wuss_icon_set_text(wuss_icon_t *icon, const char *text) { + wuss_t *w; const char *src; char *dup; size_t len; + w = icon->window->wuss; src = (text != NULL) ? text : ""; len = strlen(src); - dup = malloc(len + 1); + dup = wuss__malloc(w, len + 1); if (dup == NULL) return result_OOM; memcpy(dup, src, len + 1); - free(icon->text); + wuss__free(w, icon->text); icon->text = dup; wuss__icon_invalidate(icon); diff --git a/libraries/wuss/impl.h b/libraries/wuss/impl.h index 5944123f..97eecd38 100644 --- a/libraries/wuss/impl.h +++ b/libraries/wuss/impl.h @@ -61,6 +61,9 @@ struct wuss { screen_t *scr; bmfont_t *font; /* nullable, not owned */ + wuss_alloc_t alloc; /* malloc/realloc/free hooks, copied in + * by wuss_create; used for every heap + * block this wuss_t owns */ colour_t *palette; /* owned */ int npalette; wuss_colour_t white; /* palette index nearest to white */ @@ -142,6 +145,24 @@ struct wuss_window wuss_window_t *wuss__window_at(wuss_t *wuss, point_t p); +/* Allocation through a wuss_t's configured hooks (see struct wuss::alloc). + * Every heap block a wuss_t owns -- windows, icons, icon-pointer arrays, menu + * nodes -- goes through these so a caller can supply its own allocator. */ +static inline void *wuss__malloc(const wuss_t *wuss, size_t size) +{ + return wuss->alloc.malloc(size); +} + +static inline void *wuss__realloc(const wuss_t *wuss, void *ptr, size_t size) +{ + return wuss->alloc.realloc(ptr, size); +} + +static inline void wuss__free(const wuss_t *wuss, void *ptr) +{ + wuss->alloc.free(ptr); +} + /* Range-check a backdrop spec against the palette: its colour, and -- when a * non-solid pattern is set -- its pattern index and background colour. * Returns result_OK or result_WUSS_BAD_COLOUR. */ diff --git a/libraries/wuss/menu/menu.c b/libraries/wuss/menu/menu.c index c085598e..a55ce443 100644 --- a/libraries/wuss/menu/menu.c +++ b/libraries/wuss/menu/menu.c @@ -51,9 +51,14 @@ static void wuss__menu_close_from(struct wuss__menu *node) wuss__menu_close_from(node->child); node->child = NULL; - wuss_window_close(node->window); - free(node->icons); - free(node); + { + wuss_t *w; + + w = node->wuss; + wuss_window_close(node->window); + wuss__free(w, node->icons); + wuss__free(w, node); + } } /* ----------------------------------------------------------------------- */ @@ -219,19 +224,20 @@ static result_t wuss__menu_spawn(wuss_t *wuss, if (at.y < 0) at.y = 0; - node = malloc(sizeof(*node)); + node = wuss__malloc(wuss, sizeof(*node)); if (node == NULL) return result_OOM; - node->icons = calloc((size_t) menu->nitems, sizeof(*node->icons)); - specs = calloc((size_t) menu->nitems, sizeof(*specs)); + node->icons = wuss__malloc(wuss, (size_t) menu->nitems * sizeof(*node->icons)); + specs = wuss__malloc(wuss, (size_t) menu->nitems * sizeof(*specs)); if (node->icons == NULL || specs == NULL) { - free(specs); - free(node->icons); - free(node); + wuss__free(wuss, specs); + wuss__free(wuss, node->icons); + wuss__free(wuss, node); return result_OOM; } + memset(node->icons, 0, (size_t) menu->nitems * sizeof(*node->icons)); node->wuss = wuss; node->window = NULL; @@ -291,19 +297,19 @@ static result_t wuss__menu_spawn(wuss_t *wuss, &task, size, size, &node->window); if (rc != result_OK) { - free(specs); - free(node->icons); - free(node); + wuss__free(wuss, specs); + wuss__free(wuss, node->icons); + wuss__free(wuss, node); return rc; } rc = wuss_icon_create_array(node->window, specs, menu->nitems, node->icons); - free(specs); + wuss__free(wuss, specs); if (rc != result_OK) { wuss_window_close(node->window); - free(node->icons); - free(node); + wuss__free(wuss, node->icons); + wuss__free(wuss, node); return rc; } diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 9335870c..46018d29 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -177,7 +177,7 @@ result_t wuss_test(const char *resources) bad_config.palette.scroll.arrows = 0; bad_config.palette.scroll.wells = 0; bad_config.palette.scroll.sausages = 0; - rc = wuss_create(&scr, NULL, NULL, 0, &bad_config, &bad_wuss); + rc = wuss_create(&scr, NULL, NULL, 0, &bad_config, NULL, &bad_wuss); if (rc != result_WUSS_BAD_COLOUR) goto Failure; @@ -186,7 +186,7 @@ result_t wuss_test(const char *resources) { wuss_t *custom_wuss; - rc = wuss_create(&scr, NULL, custom_palette, 2, NULL, &custom_wuss); + rc = wuss_create(&scr, NULL, custom_palette, 2, NULL, NULL, &custom_wuss); if (rc != result_OK) goto Failure; wuss_destroy(custom_wuss); @@ -194,7 +194,7 @@ result_t wuss_test(const char *resources) printf("test: wuss_create with default palette\n"); - rc = wuss_create(&scr, NULL, NULL, 0, NULL, &wuss); + rc = wuss_create(&scr, NULL, NULL, 0, NULL, NULL, &wuss); if (rc != result_OK) goto Failure; @@ -2702,7 +2702,7 @@ result_t wuss_test(const char *resources) printf("test: wuss_create (core)\n"); - rc = wuss_create(&scr, NULL, NULL, 0, NULL, &wuss); + rc = wuss_create(&scr, NULL, NULL, 0, NULL, NULL, &wuss); if (rc != result_OK) goto Failure; diff --git a/libraries/wuss/window/close.c b/libraries/wuss/window/close.c index 759572fe..e24e1379 100644 --- a/libraries/wuss/window/close.c +++ b/libraries/wuss/window/close.c @@ -37,5 +37,5 @@ void wuss_window_close(wuss_window_t *doomed) wuss__icons_free(doomed); #endif - free(doomed); + wuss__free(wuss, doomed); } diff --git a/libraries/wuss/window/create.c b/libraries/wuss/window/create.c index a7380175..5935d85e 100644 --- a/libraries/wuss/window/create.c +++ b/libraries/wuss/window/create.c @@ -34,7 +34,7 @@ result_t wuss_window_create(wuss_t *wuss, if (!wuss__size_ok(width, height)) return result_WUSS_TOO_SMALL; - win = malloc(sizeof(*win)); + win = wuss__malloc(wuss, sizeof(*win)); if (win == NULL) return result_OOM; @@ -96,7 +96,7 @@ result_t wuss_window_create(wuss_t *wuss, if (wuss__validate_backdrop(wuss, &bg) != result_OK) { - free(win); + wuss__free(wuss, win); return result_WUSS_BAD_COLOUR; } win->bg = bg; From 2725ceed5ac6c4efbfbe7bd432218d6314417e38 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Tue, 1 Sep 2026 09:37:33 +0100 Subject: [PATCH 121/287] refactor(wuss): split wuss__icon_draw into per-type sub-functions Each switch case in wuss__icon_draw now delegates to a static wuss__icon_draw_* helper taking a shared icon_draw_ctx_t. The explicit-bg / window-backdrop / fallback blend-ground ladder that was repeated across the label, frame, radio/option and menu-entry cases is factored into icon_blend_ground. Behaviour is unchanged; the pattern case still receives the content box and scroll so its tile origin does not move. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- libraries/wuss/icon/draw.c | 781 ++++++++++++++++++++----------------- 1 file changed, 416 insertions(+), 365 deletions(-) diff --git a/libraries/wuss/icon/draw.c b/libraries/wuss/icon/draw.c index 33ab5cc0..f60220cc 100644 --- a/libraries/wuss/icon/draw.c +++ b/libraries/wuss/icon/draw.c @@ -13,6 +13,24 @@ /* ----------------------------------------------------------------------- */ +/* Shared per-draw state, resolved once by wuss__icon_draw and handed to each + * type's draw helper. "b" is the icon's screen box; "fg" is its resolved + * foreground; "font_height" is 0 and "have_font" is 0 when there is no font or + * no text to draw. */ +typedef struct icon_draw_ctx +{ + wuss_t *wuss; + screen_t *scr; + const wuss_icon_t *icon; + box_t b; + colour_t fg; + int font_height; + int have_font; +} +icon_draw_ctx_t; + +/* ----------------------------------------------------------------------- */ + static void icon_bevel(screen_t *scr, const box_t *b, colour_t fill, @@ -28,6 +46,385 @@ static void icon_bevel(screen_t *scr, screen_draw_line(scr, b->x1 - 1, b->y0, b->x1 - 1, b->y1 - 1, 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 + * blend colour. */ +static colour_t icon_blend_ground(const icon_draw_ctx_t *c, colour_t fallback) +{ + const wuss_icon_t *icon = c->icon; + + if (icon->bg != wuss_NO_BACKGROUND) + return c->wuss->palette[icon->bg]; + + if (icon->window->bg.colour != wuss_NO_BACKGROUND) + return (icon->window->bg.pattern != screen_PATTERN_SOLID) + ? c->wuss->palette[icon->window->bg.pattern_bg] + : c->wuss->palette[icon->window->bg.colour]; + + return fallback; +} + +/* ----------------------------------------------------------------------- */ + +static void wuss__icon_draw_pattern(const icon_draw_ctx_t *c, + const box_t *content, + point_t scroll) +{ + const wuss_icon_t *icon = c->icon; + colour_t pat_fg; + + /* disabled: fold the pattern into its own ground so it reads as greyed, + * mirroring the button's fg-swap */ + pat_fg = (icon->flags & wuss_ICON_FLAGS_DISABLED) + ? c->wuss->palette[icon->bg] + : c->fg; + + screen_fill_pattern(c->scr, &c->b, icon->pattern, + content->x0 - scroll.x, content->y0 - scroll.y, + pat_fg, c->wuss->palette[icon->bg]); +} + +/* ----------------------------------------------------------------------- */ + +static void wuss__icon_draw_label(const icon_draw_ctx_t *c) +{ + const wuss_icon_t *icon = c->icon; + 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) + { + bg = c->wuss->palette[icon->bg]; + screen_draw_rect(c->scr, b->x0, b->y0, + SIZE2D(b->x1 - b->x0, b->y1 - b->y0), bg); + } + else + { + bg = icon_blend_ground(c, c->fg); + } + + if (!c->have_font) + return; + + interior_w = (b->x1 - b->x0) - 2; + if (interior_w < 1) + interior_w = 1; + + bmfont_measure(c->wuss->font, icon->text, (int) strlen(icon->text), + interior_w, &split_point, &width); + + if (icon->flags & wuss_ICON_FLAGS_JUSTIFY_CENTRE) + pos.x = b->x0 + ((b->x1 - b->x0) - width) / 2; + else if (icon->flags & wuss_ICON_FLAGS_JUSTIFY_RIGHT) + pos.x = b->x1 - 1 - width; + else + pos.x = b->x0 + 1; + pos.y = b->y0 + (b->y1 - b->y0 - c->font_height) / 2; + + bmfont_draw(c->wuss->font, c->scr, icon->text, (int) strlen(icon->text), + c->fg, bg, &pos, NULL); +} + +/* ----------------------------------------------------------------------- */ + +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; + + bg = icon_blend_ground(c, c->fg); + + cap_w = 0; + if (c->have_font) + { + int split_point; + bmfont_width_t width; + + bmfont_measure(c->wuss->font, icon->text, (int) strlen(icon->text), + (b->x1 - b->x0) - WUSS_FRAME_CAPTION_INSET * 2, + &split_point, &width); + 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 top edge is broken around the caption */ + 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; + if (gap_x0 > b->x0) + 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 (c->have_font && cap_w > 0) + { + point_t pos; + + pos.x = cap_x; + pos.y = b->y0; + bmfont_draw(c->wuss->font, c->scr, icon->text, (int) strlen(icon->text), + c->fg, bg, &pos, NULL); + } +} + +/* ----------------------------------------------------------------------- */ + +static void wuss__icon_draw_button(const icon_draw_ctx_t *c) +{ + const wuss_icon_t *icon = c->icon; + const box_t *b = &c->b; + colour_t light, dark, base, label; + int pressed, is_default; + + pressed = icon->pressed; + is_default = (icon->flags & wuss_ICON_FLAGS_DEFAULT) != 0; + + if (is_default) + { + /* default action button: a flat accent-filled rectangle inside a + * one-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]; + } + 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; + } + + 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; + int interior_w, split_point; + bmfont_width_t width; + + interior_w = (b->x1 - b->x0) - 2; + if (interior_w < 1) + interior_w = 1; + + bmfont_measure(c->wuss->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; + if (pressed) + { + pos.x += 1; + pos.y += 1; + } + + bmfont_draw(c->wuss->font, c->scr, icon->text, (int) strlen(icon->text), + label, base, &pos, NULL); + } +} + +/* ----------------------------------------------------------------------- */ + +/* 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. */ +static void wuss__icon_draw_radio_option(const icon_draw_ctx_t *c) +{ + const wuss_icon_t *icon = c->icon; + const box_t *b = &c->b; + colour_t glyph, bg; + box_t g; + int gsz, gy, tx; + + gsz = (c->font_height >= 8) ? c->font_height : 8; + if (gsz > b->y1 - b->y0) + gsz = b->y1 - b->y0; + gy = b->y0 + (b->y1 - b->y0 - gsz) / 2; + + g.x0 = b->x0; + g.y0 = gy; + g.x1 = b->x0 + gsz; + g.y1 = gy + gsz; + + glyph = (icon->flags & wuss_ICON_FLAGS_DISABLED) + ? c->wuss->palette[c->wuss->bevel_dark] + : c->fg; + + bg = icon_blend_ground(c, glyph); + + if (icon->bg != wuss_NO_BACKGROUND) + screen_draw_rect(c->scr, b->x0, b->y0, + SIZE2D(b->x1 - b->x0, b->y1 - b->y0), bg); + + 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); + screen_draw_line(c->scr, g.x0 + 2, g.y1 - 1, g.x1 - 3, g.y1 - 1, glyph); + screen_draw_line(c->scr, g.x0, g.y0 + 2, g.x0, g.y1 - 3, glyph); + screen_draw_line(c->scr, g.x1 - 1, g.y0 + 2, g.x1 - 1, g.y1 - 3, glyph); + if (icon->selected) + screen_draw_rect(c->scr, g.x0 + 3, g.y0 + 3, + SIZE2D(gsz - 6, gsz - 6), glyph); + } + else + { + /* a box; a tick (two strokes) when selected */ + screen_draw_line(c->scr, g.x0, g.y0, g.x1 - 1, g.y0, glyph); + screen_draw_line(c->scr, g.x0, g.y1 - 1, g.x1 - 1, g.y1 - 1, glyph); + screen_draw_line(c->scr, g.x0, g.y0, g.x0, g.y1 - 1, glyph); + screen_draw_line(c->scr, g.x1 - 1, g.y0, g.x1 - 1, g.y1 - 1, glyph); + if (icon->selected) + { + screen_draw_line(c->scr, g.x0 + 2, g.y0 + gsz / 2, + g.x0 + gsz / 2 - 1, g.y1 - 3, glyph); + screen_draw_line(c->scr, g.x0 + gsz / 2 - 1, g.y1 - 3, + g.x1 - 3, g.y0 + 2, glyph); + } + } + + if (c->have_font) + { + point_t pos; + int interior_w, split_point; + bmfont_width_t width; + + tx = g.x1 + 4; + interior_w = (b->x1 - tx) - 1; + if (interior_w < 1) + interior_w = 1; + + bmfont_measure(c->wuss->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->wuss->font, c->scr, icon->text, (int) strlen(icon->text), + glyph, bg, &pos, NULL); + } +} + +/* ----------------------------------------------------------------------- */ + +static void wuss__icon_draw_bitmap(const icon_draw_ctx_t *c) +{ + const box_t *b = &c->b; + screen_t clipped; + + if (c->icon->bitmap == NULL) + return; + + /* screen_draw_bitmap clips to scr->clip and does not scale, so narrow the + * clip to the icon box (intersected with whatever redraw already set) and + * blit at the box's top-left */ + clipped = *c->scr; + if (clipped.clip.x0 < b->x0) clipped.clip.x0 = b->x0; + if (clipped.clip.y0 < b->y0) clipped.clip.y0 = b->y0; + if (clipped.clip.x1 > b->x1) clipped.clip.x1 = b->x1; + if (clipped.clip.y1 > b->y1) clipped.clip.y1 = b->y1; + if (clipped.clip.x1 <= clipped.clip.x0 || + clipped.clip.y1 <= clipped.clip.y0) + return; + + screen_draw_bitmap(&clipped, b->x0, b->y0, c->icon->bitmap); +} + +/* ----------------------------------------------------------------------- */ + +static void wuss__icon_draw_menu_entry(const icon_draw_ctx_t *c) +{ + const wuss_icon_t *icon = c->icon; + const box_t *b = &c->b; + colour_t ink, ground, tmp; + int disabled, highlit, pad, mid_y; + + disabled = (icon->flags & wuss_ICON_FLAGS_DISABLED) != 0; + highlit = icon->hovered && !disabled; + pad = 4; + + /* resolve the row's own ink over its own/inherited ground */ + ground = icon_blend_ground(c, c->fg); + ink = disabled ? c->wuss->palette[c->wuss->bevel_dark] : c->fg; + + /* highlight simply swaps the row's fg/bg */ + if (highlit) + { + tmp = ink; + ink = ground; + ground = tmp; + } + + if (highlit || icon->bg != wuss_NO_BACKGROUND) + screen_draw_rect(c->scr, b->x0, b->y0, + SIZE2D(b->x1 - b->x0, b->y1 - b->y0), ground); + + /* a dashed rule along the entry's top edge, then the text as normal; drawn in + * the row's ink so it stays visible when the row is inverted */ + if (icon->flags & wuss_ICON_FLAGS_SEPARATOR) + { + mid_y = b->y0 + 1; + screen_draw_dashed_line(c->scr, b->x0 + pad, mid_y, + b->x1 - 1 - pad, mid_y, 2, 2, ink); + } + + /* left-edge tick when selected */ + if (icon->selected) + { + int cx, cy, h; + + h = (c->font_height >= 8) ? c->font_height : 8; + cx = b->x0 + pad; + cy = b->y0 + (b->y1 - b->y0 - h) / 2; + screen_draw_line(c->scr, cx, cy + h / 2, cx + h / 2 - 1, cy + h - 2, ink); + screen_draw_line(c->scr, cx + h / 2 - 1, cy + h - 2, cx + h - 2, cy, ink); + } + + /* right-edge arrow for a submenu entry */ + if (icon->flags & wuss_ICON_FLAGS_SUBMENU) + { + int ax, ay, r, dy; + + r = (c->font_height >= 8) ? c->font_height / 3 : 3; + ax = b->x1 - 1 - pad - r; + ay = b->y0 + (b->y1 - b->y0) / 2; + for (dy = -r; dy <= r; dy++) + screen_draw_line(c->scr, ax, ay + dy, + ax + (r - (dy < 0 ? -dy : dy)), ay + dy, ink); + } + + if (c->have_font && icon->text != NULL && icon->text[0] != '\0') + { + point_t pos; + + pos.x = b->x0 + pad + c->font_height; /* leave room for a tick */ + pos.y = b->y0 + (b->y1 - b->y0 - c->font_height) / 2; + bmfont_draw(c->wuss->font, c->scr, icon->text, (int) strlen(icon->text), + ink, ground, &pos, NULL); + } +} + /* ----------------------------------------------------------------------- */ void wuss__icon_draw(wuss_t *wuss, @@ -35,404 +432,58 @@ void wuss__icon_draw(wuss_t *wuss, const box_t *content, point_t scroll) { - screen_t *scr; - box_t b; - colour_t fg; - int font_width, font_height; - int have_font; + icon_draw_ctx_t c; + int font_width; if (icon->flags & wuss_ICON_FLAGS_HIDDEN) return; - scr = wuss->scr; - - wuss__icon_box_to_screen(content, scroll, &icon->bbox, &b); + wuss__icon_box_to_screen(content, scroll, &icon->bbox, &c.b); - if (b.x1 <= b.x0 || b.y1 <= b.y0) + if (c.b.x1 <= c.b.x0 || c.b.y1 <= c.b.y0) return; - fg = wuss->palette[icon->fg]; + c.wuss = wuss; + c.scr = wuss->scr; + c.icon = icon; + c.fg = wuss->palette[icon->fg]; - have_font = (wuss->font != NULL && icon->text[0] != '\0'); - if (have_font) - bmfont_get_info(wuss->font, &font_width, &font_height); + c.have_font = (wuss->font != NULL && icon->text[0] != '\0'); + if (c.have_font) + bmfont_get_info(wuss->font, &font_width, &c.font_height); else - font_width = font_height = 0; + font_width = c.font_height = 0; NOT_USED(font_width); switch (icon->type) { case wuss_ICON_TYPE_PATTERN: - { - colour_t pat_fg; - - /* disabled: fold the pattern into its own ground so it reads as greyed, - * mirroring the button's fg-swap */ - pat_fg = (icon->flags & wuss_ICON_FLAGS_DISABLED) - ? wuss->palette[icon->bg] - : fg; - - screen_fill_pattern(scr, &b, icon->pattern, - content->x0 - scroll.x, content->y0 - scroll.y, - pat_fg, wuss->palette[icon->bg]); - } + wuss__icon_draw_pattern(&c, content, scroll); break; case wuss_ICON_TYPE_LABEL: - { - colour_t bg; - - if (icon->bg != wuss_NO_BACKGROUND) - { - bg = wuss->palette[icon->bg]; - screen_draw_rect(scr, b.x0, b.y0, - SIZE2D(b.x1 - b.x0, b.y1 - b.y0), bg); - } - else if (icon->window->bg.colour != wuss_NO_BACKGROUND) - { - /* blend against the dominant backdrop colour: for a pattern fill - * that's its background (clear-bit) colour, not the foreground */ - bg = (icon->window->bg.pattern != screen_PATTERN_SOLID) - ? wuss->palette[icon->window->bg.pattern_bg] - : wuss->palette[icon->window->bg.colour]; - } - else - { - bg = fg; /* bmfont needs a blend colour; nothing better to offer */ - } - - if (have_font) - { - point_t pos; - int interior_w, split_point; - bmfont_width_t width; - - interior_w = (b.x1 - b.x0) - 2; - if (interior_w < 1) - interior_w = 1; - - bmfont_measure(wuss->font, icon->text, (int) strlen(icon->text), - interior_w, &split_point, &width); - - if (icon->flags & wuss_ICON_FLAGS_JUSTIFY_CENTRE) - pos.x = b.x0 + ((b.x1 - b.x0) - width) / 2; - else if (icon->flags & wuss_ICON_FLAGS_JUSTIFY_RIGHT) - pos.x = b.x1 - 1 - width; - else - pos.x = b.x0 + 1; - pos.y = b.y0 + (b.y1 - b.y0 - font_height) / 2; - - bmfont_draw(wuss->font, scr, icon->text, (int) strlen(icon->text), - fg, bg, &pos, NULL); - } - } + wuss__icon_draw_label(&c); break; case wuss_ICON_TYPE_FRAME: - { - colour_t bg; - int cap_w, cap_x, gap_x0, gap_x1, mid_y; - - /* the caption sits over whatever ground the label case would blend - * against: an explicit bg, else the window's dominant backdrop, else - * fall back to fg so bmfont still has a blend colour */ - if (icon->bg != wuss_NO_BACKGROUND) - bg = wuss->palette[icon->bg]; - else if (icon->window->bg.colour != wuss_NO_BACKGROUND) - bg = (icon->window->bg.pattern != screen_PATTERN_SOLID) - ? wuss->palette[icon->window->bg.pattern_bg] - : wuss->palette[icon->window->bg.colour]; - else - bg = fg; - - cap_w = 0; - if (have_font) - { - int split_point; - bmfont_width_t width; - - bmfont_measure(wuss->font, icon->text, (int) strlen(icon->text), - (b.x1 - b.x0) - WUSS_FRAME_CAPTION_INSET * 2, - &split_point, &width); - cap_w = width; - } - - mid_y = b.y0 + font_height / 2; - - /* left, right and bottom edges are unbroken */ - screen_draw_line(scr, b.x0, mid_y, b.x0, b.y1 - 1, fg); - screen_draw_line(scr, b.x1 - 1, mid_y, b.x1 - 1, b.y1 - 1, fg); - screen_draw_line(scr, b.x0, b.y1 - 1, b.x1 - 1, b.y1 - 1, fg); - - /* the top edge is broken around the caption */ - 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; - if (gap_x0 > b.x0) - screen_draw_line(scr, b.x0, mid_y, gap_x0, mid_y, fg); - if (gap_x1 < b.x1 - 1) - screen_draw_line(scr, gap_x1, mid_y, b.x1 - 1, mid_y, fg); - - if (have_font && cap_w > 0) - { - point_t pos; - - pos.x = cap_x; - pos.y = b.y0; - bmfont_draw(wuss->font, scr, icon->text, (int) strlen(icon->text), - fg, bg, &pos, NULL); - } - } + wuss__icon_draw_frame(&c); break; case wuss_ICON_TYPE_BUTTON: - { - colour_t light, dark, base, label; - int pressed, is_default; - - pressed = icon->pressed; - is_default = (icon->flags & wuss_ICON_FLAGS_DEFAULT) != 0; - - if (is_default) - { - /* default action button: a flat accent-filled rectangle inside a - * one-pixel accent-text border, distinct from the bevelled ordinary - * buttons around it */ - base = wuss->palette[wuss->accent_bg]; - light = wuss->palette[wuss->accent_fg]; - dark = light; - label = wuss->palette[wuss->accent_fg]; - } - else - { - base = wuss->palette[icon->bg]; - light = wuss->palette[wuss->bevel_light]; - dark = wuss->palette[wuss->bevel_dark]; - label = fg; - } - - if (icon->flags & wuss_ICON_FLAGS_DISABLED) - label = wuss->palette[wuss->bevel_dark]; /* greyed: sink toward dark */ - - if (pressed) - icon_bevel(scr, &b, base, dark, light); - else - icon_bevel(scr, &b, base, light, dark); - - if (have_font) - { - point_t pos; - int interior_w, split_point; - bmfont_width_t width; - - interior_w = (b.x1 - b.x0) - 2; - if (interior_w < 1) - interior_w = 1; - - bmfont_measure(wuss->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 - font_height) / 2; - if (pressed) - { - pos.x += 1; - pos.y += 1; - } - - bmfont_draw(wuss->font, scr, icon->text, (int) strlen(icon->text), - label, base, &pos, NULL); - } - } + wuss__icon_draw_button(&c); break; case wuss_ICON_TYPE_RADIO: case wuss_ICON_TYPE_OPTION: - { - colour_t glyph, bg; - box_t g; - int gsz, gy, tx; - - /* the glyph is a square the height of the font (min 8), sat at the left - * of the box and vertically centred; the label follows to its right */ - gsz = (font_height >= 8) ? font_height : 8; - if (gsz > b.y1 - b.y0) - gsz = b.y1 - b.y0; - gy = b.y0 + (b.y1 - b.y0 - gsz) / 2; - - g.x0 = b.x0; - g.y0 = gy; - g.x1 = b.x0 + gsz; - g.y1 = gy + gsz; - - glyph = (icon->flags & wuss_ICON_FLAGS_DISABLED) - ? wuss->palette[wuss->bevel_dark] - : fg; - - /* ground the glyph blends against: explicit bg, else the window backdrop, - * else fg (bmfont still needs a colour) -- same ladder as the label case */ - if (icon->bg != wuss_NO_BACKGROUND) - bg = wuss->palette[icon->bg]; - else if (icon->window->bg.colour != wuss_NO_BACKGROUND) - bg = (icon->window->bg.pattern != screen_PATTERN_SOLID) - ? wuss->palette[icon->window->bg.pattern_bg] - : wuss->palette[icon->window->bg.colour]; - else - bg = glyph; - - if (icon->bg != wuss_NO_BACKGROUND) - screen_draw_rect(scr, b.x0, b.y0, - SIZE2D(b.x1 - b.x0, b.y1 - b.y0), bg); - - if (icon->type == wuss_ICON_TYPE_RADIO) - { - /* a square ring (no circle primitive); a solid centre when selected */ - screen_draw_line(scr, g.x0 + 2, g.y0, g.x1 - 3, g.y0, glyph); - screen_draw_line(scr, g.x0 + 2, g.y1 - 1, g.x1 - 3, g.y1 - 1, glyph); - screen_draw_line(scr, g.x0, g.y0 + 2, g.x0, g.y1 - 3, glyph); - screen_draw_line(scr, g.x1 - 1, g.y0 + 2, g.x1 - 1, g.y1 - 3, glyph); - if (icon->selected) - screen_draw_rect(scr, g.x0 + 3, g.y0 + 3, - SIZE2D(gsz - 6, gsz - 6), glyph); - } - else - { - /* a box; a tick (two strokes) when selected */ - screen_draw_line(scr, g.x0, g.y0, g.x1 - 1, g.y0, glyph); - screen_draw_line(scr, g.x0, g.y1 - 1, g.x1 - 1, g.y1 - 1, glyph); - screen_draw_line(scr, g.x0, g.y0, g.x0, g.y1 - 1, glyph); - screen_draw_line(scr, g.x1 - 1, g.y0, g.x1 - 1, g.y1 - 1, glyph); - if (icon->selected) - { - screen_draw_line(scr, g.x0 + 2, g.y0 + gsz / 2, - g.x0 + gsz / 2 - 1, g.y1 - 3, glyph); - screen_draw_line(scr, g.x0 + gsz / 2 - 1, g.y1 - 3, - g.x1 - 3, g.y0 + 2, glyph); - } - } - - if (have_font) - { - point_t pos; - int interior_w, split_point; - bmfont_width_t width; - - tx = g.x1 + 4; - interior_w = (b.x1 - tx) - 1; - if (interior_w < 1) - interior_w = 1; - - bmfont_measure(wuss->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 - font_height) / 2; - bmfont_draw(wuss->font, scr, icon->text, (int) strlen(icon->text), - glyph, bg, &pos, NULL); - } - } + wuss__icon_draw_radio_option(&c); break; case wuss_ICON_TYPE_BITMAP: - { - screen_t clipped; - - if (icon->bitmap == NULL) - break; - - /* screen_draw_bitmap clips to scr->clip and does not scale, so narrow - * the clip to the icon box (intersected with whatever redraw already - * set) and blit at the box's top-left */ - clipped = *scr; - if (clipped.clip.x0 < b.x0) clipped.clip.x0 = b.x0; - if (clipped.clip.y0 < b.y0) clipped.clip.y0 = b.y0; - if (clipped.clip.x1 > b.x1) clipped.clip.x1 = b.x1; - if (clipped.clip.y1 > b.y1) clipped.clip.y1 = b.y1; - if (clipped.clip.x1 <= clipped.clip.x0 || - clipped.clip.y1 <= clipped.clip.y0) - break; - - screen_draw_bitmap(&clipped, b.x0, b.y0, icon->bitmap); - } + wuss__icon_draw_bitmap(&c); break; case wuss_ICON_TYPE_MENU_ENTRY: - { - colour_t ink, ground, tmp; - int disabled, highlit, pad, mid_y; - - disabled = (icon->flags & wuss_ICON_FLAGS_DISABLED) != 0; - highlit = icon->hovered && !disabled; - pad = 4; - - /* resolve the row's own ink over its own/inherited ground */ - if (icon->bg != wuss_NO_BACKGROUND) - ground = wuss->palette[icon->bg]; - else if (icon->window->bg.colour != wuss_NO_BACKGROUND) - ground = (icon->window->bg.pattern != screen_PATTERN_SOLID) - ? wuss->palette[icon->window->bg.pattern_bg] - : wuss->palette[icon->window->bg.colour]; - else - ground = fg; /* bmfont still needs a blend colour */ - ink = disabled ? wuss->palette[wuss->bevel_dark] : fg; - - /* highlight simply swaps the row's fg/bg */ - if (highlit) - { - tmp = ink; - ink = ground; - ground = tmp; - } - - if (highlit || icon->bg != wuss_NO_BACKGROUND) - screen_draw_rect(scr, b.x0, b.y0, - SIZE2D(b.x1 - b.x0, b.y1 - b.y0), ground); - - /* a dashed rule along the entry's top edge, then the text as normal; - * drawn in the row's ink so it stays visible when the row is inverted */ - if (icon->flags & wuss_ICON_FLAGS_SEPARATOR) - { - mid_y = b.y0 + 1; - screen_draw_dashed_line(scr, b.x0 + pad, mid_y, - b.x1 - 1 - pad, mid_y, 2, 2, ink); - } - - /* left-edge tick when selected */ - if (icon->selected) - { - int cx, cy, h; - - h = (font_height >= 8) ? font_height : 8; - cx = b.x0 + pad; - cy = b.y0 + (b.y1 - b.y0 - h) / 2; - screen_draw_line(scr, cx, cy + h / 2, cx + h / 2 - 1, cy + h - 2, ink); - screen_draw_line(scr, cx + h / 2 - 1, cy + h - 2, cx + h - 2, cy, ink); - } - - /* right-edge arrow for a submenu entry */ - if (icon->flags & wuss_ICON_FLAGS_SUBMENU) - { - int ax, ay, r, dy; - - r = (font_height >= 8) ? font_height / 3 : 3; - ax = b.x1 - 1 - pad - r; - ay = b.y0 + (b.y1 - b.y0) / 2; - for (dy = -r; dy <= r; dy++) - screen_draw_line(scr, ax, ay + dy, - ax + (r - (dy < 0 ? -dy : dy)), ay + dy, ink); - } - - if (have_font && icon->text != NULL && icon->text[0] != '\0') - { - point_t pos; - - pos.x = b.x0 + pad + font_height; /* leave room for a tick */ - pos.y = b.y0 + (b.y1 - b.y0 - font_height) / 2; - bmfont_draw(wuss->font, scr, icon->text, (int) strlen(icon->text), - ink, ground, &pos, NULL); - } - } + wuss__icon_draw_menu_entry(&c); break; } } From 0c38ad60a18ec9acb6c927bb00e69e9ddd82daef Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Tue, 1 Sep 2026 09:52:33 +0100 Subject: [PATCH 122/287] chore: path-relative header comments across all sources The first-line header comment of every first-party .c/.h file now carries its path relative to libraries/, include/ or apps/ instead of a bare basename: /* draw.c -- wuss - draw a work-area icon */ -> /* wuss/icon/draw.c -- draw a work-area icon */ The description after '--' is preserved; a redundant "<module> - " (or "<module> test - ") prefix is dropped, except where that prefix plus the module's own tagline is the whole description (kept as "wuss - minimal window manager"). Files with no header, or only a stale/bare filename, gain a path-only header. Vendored trees (libraries/fortify, include/fortify) are untouched. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- apps/test/main.c | 2 +- apps/wuss/main.c | 2 +- include/base/debug.h | 2 +- include/base/result.h | 2 +- include/base/types.h | 2 +- include/base/utils.h | 2 +- include/databases/digest-db.h | 2 +- include/databases/filename-db.h | 2 +- include/databases/pickle-reader-hash.h | 2 +- include/databases/pickle-writer-hash.h | 2 +- include/databases/pickle.h | 2 +- include/databases/tag-db.h | 2 +- include/datastruct/atom.h | 2 +- include/datastruct/bitarr.h | 2 +- include/datastruct/bitfifo.h | 2 +- include/datastruct/bitvec.h | 2 +- include/datastruct/cache.h | 2 +- include/datastruct/hash.h | 2 +- include/datastruct/hlist.h | 2 +- include/datastruct/list.h | 2 +- include/datastruct/ntree.h | 2 +- include/datastruct/vector.h | 2 +- include/framebuf/bitmap-set.h | 2 +- include/framebuf/bitmap.h | 2 +- include/framebuf/bmfont.h | 2 +- include/framebuf/colour.h | 2 +- include/framebuf/composite.h | 2 +- include/framebuf/curve.h | 2 +- include/framebuf/palettes.h | 2 +- include/framebuf/pixelfmt.h | 2 +- include/framebuf/screen.h | 2 +- include/framebuf/span-bgrx8888.h | 2 +- include/framebuf/span-p4.h | 2 +- include/framebuf/span-registry.h | 2 +- include/framebuf/span-rgba8888.h | 2 +- include/framebuf/span-rgbx8888.h | 2 +- include/framebuf/span-xbgr8888.h | 2 +- include/framebuf/span.h | 2 +- include/geom/box.h | 2 +- include/geom/layout.h | 2 +- include/geom/line.h | 2 +- include/geom/packer.h | 2 +- include/geom/point.h | 2 +- include/geom/size.h | 2 +- include/io/path.h | 2 +- include/io/stream-mem.h | 2 +- include/io/stream-mtfcomp.h | 2 +- include/io/stream-packbits.h | 2 +- include/io/stream-stdio.h | 2 +- include/io/stream.h | 2 +- include/test/all-tests.h | 2 +- include/test/txtscr.h | 2 +- include/text/bmtext.h | 2 +- include/text/txtfmt.h | 2 +- include/utils/array.h | 2 +- include/utils/barith.h | 2 +- include/utils/bsearch.h | 2 +- include/utils/bytesex.h | 2 +- include/utils/fxp.h | 2 +- include/utils/maths.h | 2 +- include/utils/pack.h | 2 +- include/utils/primes.h | 2 +- include/wuss/icon.h | 2 +- include/wuss/menu.h | 2 +- include/wuss/task.h | 2 +- include/wuss/window.h | 2 +- include/wuss/wuss.h | 2 +- libraries/databases/digest-db/digest-db.c | 2 +- libraries/databases/filename-db/filename-db.c | 2 +- libraries/databases/pickle/delete.c | 2 +- libraries/databases/pickle/hash-reader.c | 2 +- libraries/databases/pickle/hash-writer.c | 2 +- libraries/databases/pickle/pickle.c | 2 +- libraries/databases/pickle/test/pickle-test.c | 1 + libraries/databases/pickle/unpickle.c | 2 +- libraries/databases/tag-db/tag-db.c | 2 +- libraries/databases/tag-db/test/tag-db-test.c | 1 + libraries/datastruct/atom/create.c | 2 +- libraries/datastruct/atom/delete-block.c | 2 +- libraries/datastruct/atom/delete.c | 2 +- libraries/datastruct/atom/destroy.c | 2 +- libraries/datastruct/atom/for-block.c | 2 +- libraries/datastruct/atom/get.c | 2 +- libraries/datastruct/atom/impl.h | 2 +- libraries/datastruct/atom/new.c | 2 +- libraries/datastruct/atom/set.c | 2 +- libraries/datastruct/atom/test/atom-test.c | 1 + libraries/datastruct/bitarr/count.c | 2 +- libraries/datastruct/bitarr/test/bitarr-test.c | 1 + libraries/datastruct/bitfifo/bitfifo.c | 2 +- libraries/datastruct/bitfifo/test/bitfifo-test.c | 1 + libraries/datastruct/bitvec/and.c | 2 +- libraries/datastruct/bitvec/clear-all.c | 2 +- libraries/datastruct/bitvec/clear.c | 2 +- libraries/datastruct/bitvec/count.c | 2 +- libraries/datastruct/bitvec/create.c | 2 +- libraries/datastruct/bitvec/destroy.c | 2 +- libraries/datastruct/bitvec/ensure.c | 2 +- libraries/datastruct/bitvec/eq.c | 2 +- libraries/datastruct/bitvec/get.c | 2 +- libraries/datastruct/bitvec/impl.h | 2 +- libraries/datastruct/bitvec/length.c | 2 +- libraries/datastruct/bitvec/next.c | 2 +- libraries/datastruct/bitvec/or.c | 2 +- libraries/datastruct/bitvec/set-all.c | 2 +- libraries/datastruct/bitvec/set.c | 2 +- libraries/datastruct/bitvec/test/bitvec-test.c | 1 + libraries/datastruct/bitvec/toggle.c | 2 +- libraries/datastruct/cache/cache.c | 2 +- libraries/datastruct/cache/test/cache-test.c | 1 + libraries/datastruct/hash/count.c | 2 +- libraries/datastruct/hash/create.c | 2 +- libraries/datastruct/hash/destroy.c | 2 +- libraries/datastruct/hash/impl.h | 2 +- libraries/datastruct/hash/insert.c | 2 +- libraries/datastruct/hash/lookup-node.c | 2 +- libraries/datastruct/hash/lookup.c | 2 +- libraries/datastruct/hash/remove.c | 2 +- libraries/datastruct/hash/test/hash-test.c | 1 + libraries/datastruct/hash/walk-cont.c | 2 +- libraries/datastruct/hash/walk.c | 2 +- libraries/datastruct/hlist/append.c | 1 + libraries/datastruct/hlist/copy.c | 1 + libraries/datastruct/hlist/free.c | 1 + libraries/datastruct/hlist/impl.h | 2 +- libraries/datastruct/hlist/length.c | 1 + libraries/datastruct/hlist/list.c | 1 + libraries/datastruct/hlist/map.c | 1 + libraries/datastruct/hlist/pop.c | 1 + libraries/datastruct/hlist/push.c | 1 + libraries/datastruct/hlist/reverse.c | 1 + libraries/datastruct/hlist/to-array.c | 1 + libraries/datastruct/list/add-head.c | 1 + libraries/datastruct/list/add-tail.c | 1 + libraries/datastruct/list/find.c | 1 + libraries/datastruct/list/init.c | 1 + libraries/datastruct/list/remove.c | 1 + libraries/datastruct/list/test/list-test.c | 1 + libraries/datastruct/list/walk.c | 1 + libraries/datastruct/ntree/copy.c | 2 +- libraries/datastruct/ntree/delete.c | 2 +- libraries/datastruct/ntree/depth.c | 2 +- libraries/datastruct/ntree/first-child.c | 2 +- libraries/datastruct/ntree/free.c | 2 +- libraries/datastruct/ntree/get-data.c | 2 +- libraries/datastruct/ntree/impl.h | 2 +- libraries/datastruct/ntree/insert-after.c | 2 +- libraries/datastruct/ntree/insert-before.c | 2 +- libraries/datastruct/ntree/insert.c | 2 +- libraries/datastruct/ntree/last-child.c | 2 +- libraries/datastruct/ntree/max-height.c | 2 +- libraries/datastruct/ntree/n-nodes.c | 2 +- libraries/datastruct/ntree/new.c | 2 +- libraries/datastruct/ntree/next-sibling.c | 2 +- libraries/datastruct/ntree/nth-child.c | 2 +- libraries/datastruct/ntree/parent.c | 2 +- libraries/datastruct/ntree/prepend.c | 2 +- libraries/datastruct/ntree/prev-sibling.c | 2 +- libraries/datastruct/ntree/set-data.c | 2 +- libraries/datastruct/ntree/test/ntree-test.c | 1 + libraries/datastruct/ntree/unlink.c | 2 +- libraries/datastruct/ntree/walk.c | 2 +- libraries/datastruct/vector/clear.c | 2 +- libraries/datastruct/vector/create.c | 2 +- libraries/datastruct/vector/destroy.c | 2 +- libraries/datastruct/vector/ensure.c | 2 +- libraries/datastruct/vector/get.c | 2 +- libraries/datastruct/vector/impl.h | 2 +- libraries/datastruct/vector/insert.c | 2 +- libraries/datastruct/vector/length.c | 2 +- libraries/datastruct/vector/set-length.c | 2 +- libraries/datastruct/vector/set-width.c | 2 +- libraries/datastruct/vector/set.c | 2 +- libraries/datastruct/vector/test/vector-test.c | 1 + libraries/datastruct/vector/width.c | 2 +- libraries/framebuf/bitmap/bitmap.c | 2 +- libraries/framebuf/bitmap/load.c | 2 +- libraries/framebuf/bitmap/save.c | 2 +- libraries/framebuf/bmfont/bmfont.c | 2 +- libraries/framebuf/bmfont/test/bmfont-test.c | 2 +- libraries/framebuf/colour/colour.c | 2 +- libraries/framebuf/composite/composite.c | 2 +- libraries/framebuf/composite/test/composite-test.c | 2 +- libraries/framebuf/curve/curve.c | 2 +- libraries/framebuf/curve/test/curve-test.c | 2 +- libraries/framebuf/palettes/palettes.c | 2 +- libraries/framebuf/pixelfmt/log2bpp.c | 2 +- libraries/framebuf/screen/screen-copy-rect.c | 2 +- libraries/framebuf/screen/screen-draw-ninepatch.c | 2 +- libraries/framebuf/screen/screen-draw.c | 2 +- libraries/framebuf/screen/screen-fill-pattern.c | 2 +- libraries/framebuf/screen/screen.c | 2 +- libraries/framebuf/screen/test/screen-test.c | 2 +- libraries/framebuf/span-registry/get.c | 2 +- libraries/framebuf/span-registry/regdata.h | 2 +- libraries/framebuf/span/all8888-generic.c | 2 +- libraries/framebuf/span/all8888.c | 2 +- libraries/framebuf/span/all8888.h | 2 +- libraries/framebuf/span/bgrx8888.c | 2 +- libraries/framebuf/span/p4.c | 2 +- libraries/framebuf/span/rgbx8888.c | 2 +- libraries/framebuf/span/xbgr8888.c | 2 +- libraries/geom/box/clipped.c | 2 +- libraries/geom/box/contains-box.c | 2 +- libraries/geom/box/contains-point.c | 2 +- libraries/geom/box/could-hold.c | 2 +- libraries/geom/box/extend.c | 2 +- libraries/geom/box/grow.c | 2 +- libraries/geom/box/intersection.c | 2 +- libraries/geom/box/intersects.c | 2 +- libraries/geom/box/is-empty.c | 2 +- libraries/geom/box/reset.c | 2 +- libraries/geom/box/round.c | 2 +- libraries/geom/box/round4.c | 2 +- libraries/geom/box/size.c | 2 +- libraries/geom/box/test/box-test.c | 1 + libraries/geom/box/translated.c | 2 +- libraries/geom/box/union.c | 2 +- libraries/geom/layout/layout.c | 2 +- libraries/geom/layout/test/layout-test.c | 1 + libraries/geom/line/line.c | 2 +- libraries/geom/packer/impl.h | 2 +- libraries/geom/packer/packer.c | 2 +- libraries/geom/packer/test/packer-test.c | 1 + libraries/io/path/path.c | 2 +- libraries/io/stream/stream-mem.c | 2 +- libraries/io/stream/stream-mtfcomp.c | 2 +- libraries/io/stream/stream-packbitscomp.c | 2 +- libraries/io/stream/stream-packbitsdecomp.c | 2 +- libraries/io/stream/stream-stdio.c | 2 +- libraries/io/stream/stream.c | 2 +- libraries/io/stream/test/stream-test.c | 1 + libraries/test/txtscr/txtscr.c | 2 +- libraries/text/bmtext/draw.c | 2 +- libraries/text/bmtext/layout.c | 2 +- libraries/text/txtfmt/create.c | 2 +- libraries/text/txtfmt/destroy.c | 2 +- libraries/text/txtfmt/get-length.c | 2 +- libraries/text/txtfmt/get-line.c | 2 +- libraries/text/txtfmt/get-nlines.c | 2 +- libraries/text/txtfmt/get-wrapped-width.c | 2 +- libraries/text/txtfmt/impl.h | 2 +- libraries/text/txtfmt/print.c | 2 +- libraries/text/txtfmt/test/txtfmt-test.c | 2 +- libraries/text/txtfmt/wrap.c | 2 +- libraries/utils/array/delelem.c | 1 + libraries/utils/array/delelems.c | 1 + libraries/utils/array/grow.c | 2 +- libraries/utils/array/shrink.c | 2 +- libraries/utils/array/squeeze.c | 1 + libraries/utils/array/stretch.c | 1 + libraries/utils/array/test/array-test.c | 1 + libraries/utils/barith/barith.c | 2 +- libraries/utils/bsearch/bsearch-impl.h | 2 +- libraries/utils/bsearch/bsearch-int.c | 2 +- libraries/utils/bsearch/bsearch-short.c | 2 +- libraries/utils/bsearch/bsearch-uint.c | 2 +- libraries/utils/bsearch/bsearch-ushort.c | 2 +- libraries/utils/bsearch/test/bsearch-test.c | 1 + libraries/utils/bytesex/rev-l-block.c | 2 +- libraries/utils/bytesex/rev-l-m.c | 2 +- libraries/utils/bytesex/rev-l.c | 2 +- libraries/utils/bytesex/rev-s-block.c | 2 +- libraries/utils/bytesex/rev-s-m.c | 2 +- libraries/utils/bytesex/rev-s-pair-m.c | 2 +- libraries/utils/bytesex/rev-s-pair.c | 2 +- libraries/utils/bytesex/rev-s.c | 2 +- libraries/utils/bytesex/util.h | 2 +- libraries/utils/fxp/smull-fxp16.c | 2 +- libraries/utils/fxp/umull-fxp16.c | 2 +- libraries/utils/maths/degs-to-rads.c | 2 +- libraries/utils/maths/gcd.c | 2 +- libraries/utils/pack/pack.c | 2 +- libraries/utils/pack/test/pack-test.c | 2 +- libraries/utils/pack/unpack.c | 2 +- libraries/utils/primes/primes.c | 2 +- libraries/wuss/backdrop.c | 2 +- libraries/wuss/create.c | 2 +- libraries/wuss/destroy.c | 2 +- libraries/wuss/furniture.h | 2 +- libraries/wuss/furniture/back-box.c | 2 +- libraries/wuss/furniture/close-box.c | 2 +- libraries/wuss/furniture/content-box.c | 2 +- libraries/wuss/furniture/drag-resize.c | 2 +- libraries/wuss/furniture/draw.c | 2 +- libraries/wuss/furniture/hit-test.c | 2 +- libraries/wuss/furniture/hscroll-box.c | 2 +- libraries/wuss/furniture/invalidate.c | 2 +- libraries/wuss/furniture/resize-box.c | 2 +- libraries/wuss/furniture/scroll-action.c | 2 +- libraries/wuss/furniture/titlebar-box.c | 2 +- libraries/wuss/furniture/toggle-action.c | 2 +- libraries/wuss/furniture/toggle-box.c | 2 +- libraries/wuss/furniture/vscroll-box.c | 2 +- libraries/wuss/get-font.c | 2 +- libraries/wuss/get-pointer.c | 2 +- libraries/wuss/icon.h | 2 +- libraries/wuss/icon/create-array.c | 2 +- libraries/wuss/icon/create.c | 2 +- libraries/wuss/icon/delete.c | 2 +- libraries/wuss/icon/draw.c | 2 +- libraries/wuss/icon/free.c | 2 +- libraries/wuss/icon/get-bbox.c | 2 +- libraries/wuss/icon/get-selected.c | 2 +- libraries/wuss/icon/get-text.c | 2 +- libraries/wuss/icon/get-type.c | 2 +- libraries/wuss/icon/get-window.c | 2 +- libraries/wuss/icon/hit-test.c | 2 +- libraries/wuss/icon/invalidate.c | 2 +- libraries/wuss/icon/screen-box.c | 2 +- libraries/wuss/icon/set-hidden.c | 2 +- libraries/wuss/icon/set-hover.c | 2 +- libraries/wuss/icon/set-selected.c | 2 +- libraries/wuss/icon/set-text.c | 2 +- libraries/wuss/idle.c | 2 +- libraries/wuss/impl.h | 2 +- libraries/wuss/invalidate.c | 2 +- libraries/wuss/menu.h | 2 +- libraries/wuss/menu/create-from-desc.c | 2 +- libraries/wuss/menu/menu.c | 2 +- libraries/wuss/mouse-click.c | 2 +- libraries/wuss/mouse-move.c | 2 +- libraries/wuss/redraw.c | 2 +- libraries/wuss/scroll-step.c | 2 +- libraries/wuss/scroll.c | 2 +- libraries/wuss/task/start.c | 2 +- libraries/wuss/task/stop.c | 2 +- libraries/wuss/test/tasks/ball.c | 2 +- libraries/wuss/test/tasks/ball.h | 2 +- libraries/wuss/test/tasks/blank.c | 2 +- libraries/wuss/test/tasks/blank.h | 2 +- libraries/wuss/test/tasks/chars.c | 2 +- libraries/wuss/test/tasks/chars.h | 2 +- libraries/wuss/test/tasks/checker.c | 2 +- libraries/wuss/test/tasks/checker.h | 2 +- libraries/wuss/test/tasks/curve.c | 2 +- libraries/wuss/test/tasks/curve.h | 2 +- libraries/wuss/test/tasks/gradient.c | 2 +- libraries/wuss/test/tasks/gradient.h | 2 +- libraries/wuss/test/tasks/icons.c | 2 +- libraries/wuss/test/tasks/icons.h | 2 +- libraries/wuss/test/tasks/image.c | 2 +- libraries/wuss/test/tasks/image.h | 2 +- libraries/wuss/test/tasks/launcher.c | 2 +- libraries/wuss/test/tasks/launcher.h | 2 +- libraries/wuss/test/tasks/palette.c | 2 +- libraries/wuss/test/tasks/palette.h | 2 +- libraries/wuss/test/tasks/porter-duff.c | 2 +- libraries/wuss/test/tasks/porter-duff.h | 2 +- libraries/wuss/test/tasks/sofa.c | 2 +- libraries/wuss/test/tasks/sofa.h | 2 +- libraries/wuss/test/tasks/text.c | 2 +- libraries/wuss/test/tasks/text.h | 2 +- libraries/wuss/test/wuss-test.c | 2 +- libraries/wuss/window/at.c | 2 +- libraries/wuss/window/close.c | 2 +- libraries/wuss/window/create-placed.c | 2 +- libraries/wuss/window/create.c | 2 +- libraries/wuss/window/get-content-bounds.c | 2 +- libraries/wuss/window/get-scroll.c | 2 +- libraries/wuss/window/get-visible-bounds.c | 2 +- libraries/wuss/window/invalidate.c | 2 +- libraries/wuss/window/move.c | 2 +- libraries/wuss/window/resize.c | 2 +- libraries/wuss/window/restack.c | 2 +- libraries/wuss/window/set-background.c | 2 +- libraries/wuss/window/set-scroll.c | 2 +- 367 files changed, 367 insertions(+), 330 deletions(-) diff --git a/apps/test/main.c b/apps/test/main.c index 435b3b82..aaf9f21d 100644 --- a/apps/test/main.c +++ b/apps/test/main.c @@ -1,4 +1,4 @@ -// +/* test/main.c */ // main.c // DPTLibTest // diff --git a/apps/wuss/main.c b/apps/wuss/main.c index 98fd0747..0c7f4046 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -1,4 +1,4 @@ -/* main.c -- Wuss - interactive minimal window manager demo */ +/* wuss/main.c -- Wuss - interactive minimal window manager demo */ #include <stdio.h> #include <stdlib.h> diff --git a/include/base/debug.h b/include/base/debug.h index fd7249f8..ca74f437 100644 --- a/include/base/debug.h +++ b/include/base/debug.h @@ -1,4 +1,4 @@ -/* debug.h -- debugging and logging macros */ +/* base/debug.h -- debugging and logging macros */ #ifndef BASE_DEBUG_H #define BASE_DEBUG_H diff --git a/include/base/result.h b/include/base/result.h index 5ae5f834..d8021f31 100644 --- a/include/base/result.h +++ b/include/base/result.h @@ -1,4 +1,4 @@ -/* result.h -- generic function return values */ +/* base/result.h -- generic function return values */ #ifndef BASE_RESULT_H #define BASE_RESULT_H diff --git a/include/base/types.h b/include/base/types.h index 77570e39..f53e2f9c 100644 --- a/include/base/types.h +++ b/include/base/types.h @@ -1,4 +1,4 @@ -/* types.h -- fixed-width integer types */ +/* base/types.h -- fixed-width integer types */ #ifndef BASE_TYPES_H #define BASE_TYPES_H diff --git a/include/base/utils.h b/include/base/utils.h index 3d3cbd7a..8e6b389c 100644 --- a/include/base/utils.h +++ b/include/base/utils.h @@ -1,4 +1,4 @@ -/* utils.h -- various utilities */ +/* base/utils.h -- various utilities */ #ifndef BASE_UTILS_H #define BASE_UTILS_H diff --git a/include/databases/digest-db.h b/include/databases/digest-db.h index e91eaca2..287ffc57 100644 --- a/include/databases/digest-db.h +++ b/include/databases/digest-db.h @@ -1,4 +1,4 @@ -/* digest-db.h -- digest database */ +/* databases/digest-db.h -- digest database */ /** * \file digest-db.h diff --git a/include/databases/filename-db.h b/include/databases/filename-db.h index 1a68715a..d0a2bf54 100644 --- a/include/databases/filename-db.h +++ b/include/databases/filename-db.h @@ -1,4 +1,4 @@ -/* filename-db.h -- filename database */ +/* databases/filename-db.h -- filename database */ /** * \file filename-db.h diff --git a/include/databases/pickle-reader-hash.h b/include/databases/pickle-reader-hash.h index 86c6d778..e43b3ee5 100644 --- a/include/databases/pickle-reader-hash.h +++ b/include/databases/pickle-reader-hash.h @@ -1,4 +1,4 @@ -/* pickle-reader-hash.h -- glue methods to let pickle read from hashes */ +/* databases/pickle-reader-hash.h -- glue methods to let pickle read from hashes */ #ifndef DATABASES_PICKLE_READER_HASH_H #define DATABASES_PICKLE_READER_HASH_H diff --git a/include/databases/pickle-writer-hash.h b/include/databases/pickle-writer-hash.h index 560e5f5b..e0888a17 100644 --- a/include/databases/pickle-writer-hash.h +++ b/include/databases/pickle-writer-hash.h @@ -1,4 +1,4 @@ -/* pickle-writer-hash.h -- glue methods to let pickle write to hashes */ +/* databases/pickle-writer-hash.h -- glue methods to let pickle write to hashes */ #ifndef DATABASES_PICKLE_WRITER_HASH_H #define DATABASES_PICKLE_WRITER_HASH_H diff --git a/include/databases/pickle.h b/include/databases/pickle.h index eaa48b19..88c1d7ac 100644 --- a/include/databases/pickle.h +++ b/include/databases/pickle.h @@ -1,4 +1,4 @@ -/* pickle.h -- (de-)serialising associative arrays */ +/* databases/pickle.h -- (de-)serialising associative arrays */ /** * \file pickle.h diff --git a/include/databases/tag-db.h b/include/databases/tag-db.h index 3160812c..dcbc01d7 100644 --- a/include/databases/tag-db.h +++ b/include/databases/tag-db.h @@ -1,4 +1,4 @@ -/* tag-db.h -- tag database */ +/* databases/tag-db.h -- tag database */ /** * \file tag-db.h diff --git a/include/datastruct/atom.h b/include/datastruct/atom.h index 3d5b9e9f..285c0ee3 100644 --- a/include/datastruct/atom.h +++ b/include/datastruct/atom.h @@ -1,4 +1,4 @@ -/* atom.h -- indexed data block storage */ +/* datastruct/atom.h -- indexed data block storage */ /** * \file atom.h diff --git a/include/datastruct/bitarr.h b/include/datastruct/bitarr.h index 56b082ff..e7398bfc 100644 --- a/include/datastruct/bitarr.h +++ b/include/datastruct/bitarr.h @@ -1,4 +1,4 @@ -/* bitarr.h -- arrays of bits */ +/* datastruct/bitarr.h -- arrays of bits */ /** * \file bitarr.h diff --git a/include/datastruct/bitfifo.h b/include/datastruct/bitfifo.h index f7ec8e50..c73182b5 100644 --- a/include/datastruct/bitfifo.h +++ b/include/datastruct/bitfifo.h @@ -1,4 +1,4 @@ -/* bitfifo.h -- fifo which stores bits */ +/* datastruct/bitfifo.h -- fifo which stores bits */ /** * \file bitfifo.h diff --git a/include/datastruct/bitvec.h b/include/datastruct/bitvec.h index 7fdbdf1e..8d5d641f 100644 --- a/include/datastruct/bitvec.h +++ b/include/datastruct/bitvec.h @@ -1,4 +1,4 @@ -/* bitvec.h -- flexible arrays of bits */ +/* datastruct/bitvec.h -- flexible arrays of bits */ /** * \file bitvec.h diff --git a/include/datastruct/cache.h b/include/datastruct/cache.h index 164a18e6..ce0aeb4b 100644 --- a/include/datastruct/cache.h +++ b/include/datastruct/cache.h @@ -1,4 +1,4 @@ -/* cache.h -- generic single-block cache */ +/* datastruct/cache.h -- generic single-block cache */ /** * \file cache.h diff --git a/include/datastruct/hash.h b/include/datastruct/hash.h index 75067575..bf68ed23 100644 --- a/include/datastruct/hash.h +++ b/include/datastruct/hash.h @@ -1,4 +1,4 @@ -/* hash.h -- associative arrays */ +/* datastruct/hash.h -- associative arrays */ /** * \file hash.h diff --git a/include/datastruct/hlist.h b/include/datastruct/hlist.h index a8ffb6b6..5b73388c 100644 --- a/include/datastruct/hlist.h +++ b/include/datastruct/hlist.h @@ -1,4 +1,4 @@ -/* hlist.h -- "Hanson" linked list library */ +/* datastruct/hlist.h -- "Hanson" linked list library */ /* See chapter 5 of C Interfaces and Implementations. */ diff --git a/include/datastruct/list.h b/include/datastruct/list.h index ff8cdf84..ebdb986c 100644 --- a/include/datastruct/list.h +++ b/include/datastruct/list.h @@ -1,4 +1,4 @@ -/* list.h -- linked lists */ +/* datastruct/list.h -- linked lists */ /** * \file list.h diff --git a/include/datastruct/ntree.h b/include/datastruct/ntree.h index 9e7d54af..a2c160cf 100644 --- a/include/datastruct/ntree.h +++ b/include/datastruct/ntree.h @@ -1,4 +1,4 @@ -/* ntree.h -- n-ary trees */ +/* datastruct/ntree.h -- n-ary trees */ /** * \file ntree.h diff --git a/include/datastruct/vector.h b/include/datastruct/vector.h index 2ed1752a..5750e37a 100644 --- a/include/datastruct/vector.h +++ b/include/datastruct/vector.h @@ -1,4 +1,4 @@ -/* vector.h -- flexible arrays */ +/* datastruct/vector.h -- flexible arrays */ /** * \file vector.h diff --git a/include/framebuf/bitmap-set.h b/include/framebuf/bitmap-set.h index 809ea432..33631177 100644 --- a/include/framebuf/bitmap-set.h +++ b/include/framebuf/bitmap-set.h @@ -1,4 +1,4 @@ -/* bitmap-set.h -- a set of bitmap images */ +/* framebuf/bitmap-set.h -- a set of bitmap images */ #ifndef FRAMEBUF_BITMAP_SET_H #define FRAMEBUF_BITMAP_SET_H diff --git a/include/framebuf/bitmap.h b/include/framebuf/bitmap.h index 59305c41..2fecfd6f 100644 --- a/include/framebuf/bitmap.h +++ b/include/framebuf/bitmap.h @@ -1,4 +1,4 @@ -/* bitmap.h -- bitmap image type */ +/* framebuf/bitmap.h -- bitmap image type */ #ifndef FRAMEBUF_BITMAP_H #define FRAMEBUF_BITMAP_H diff --git a/include/framebuf/bmfont.h b/include/framebuf/bmfont.h index e46cd9f4..b8f7d9b8 100644 --- a/include/framebuf/bmfont.h +++ b/include/framebuf/bmfont.h @@ -1,4 +1,4 @@ -/* bmfont.h -- proportional bitmap font engine */ +/* framebuf/bmfont.h -- proportional bitmap font engine */ #ifndef DPTLIB_BMFONT_H #define DPTLIB_BMFONT_H diff --git a/include/framebuf/colour.h b/include/framebuf/colour.h index 53ba312b..452f1be4 100644 --- a/include/framebuf/colour.h +++ b/include/framebuf/colour.h @@ -1,4 +1,4 @@ -/* colour.h -- colour type */ +/* framebuf/colour.h -- colour type */ #ifndef FRAMEBUF_COLOUR_H #define FRAMEBUF_COLOUR_H diff --git a/include/framebuf/composite.h b/include/framebuf/composite.h index cd577fc0..219878ef 100644 --- a/include/framebuf/composite.h +++ b/include/framebuf/composite.h @@ -1,4 +1,4 @@ -/* composite.h -- Porter-Duff image compositing */ +/* framebuf/composite.h -- Porter-Duff image compositing */ #ifndef COMPOSITE_H #define COMPOSITE_H diff --git a/include/framebuf/curve.h b/include/framebuf/curve.h index ecc5dc63..a8d171c9 100644 --- a/include/framebuf/curve.h +++ b/include/framebuf/curve.h @@ -1,4 +1,4 @@ -/* curve.h -- bezier calculations */ +/* framebuf/curve.h -- bezier calculations */ #ifndef DPTLIB_CURVE_H #define DPTLIB_CURVE_H diff --git a/include/framebuf/palettes.h b/include/framebuf/palettes.h index 2829810b..41500e0b 100644 --- a/include/framebuf/palettes.h +++ b/include/framebuf/palettes.h @@ -1,4 +1,4 @@ -/* palettes.h -- standard palettes */ +/* framebuf/palettes.h -- standard palettes */ #ifndef PALETTES_H #define PALETTES_H diff --git a/include/framebuf/pixelfmt.h b/include/framebuf/pixelfmt.h index b20b2882..c8b0f638 100644 --- a/include/framebuf/pixelfmt.h +++ b/include/framebuf/pixelfmt.h @@ -1,4 +1,4 @@ -/* pixelfmt.h -- pixel formats */ +/* framebuf/pixelfmt.h -- pixel formats */ #ifndef FRAMEBUF_PIXELFMT_H #define FRAMEBUF_PIXELFMT_H diff --git a/include/framebuf/screen.h b/include/framebuf/screen.h index 12bdd4a3..25fa4d19 100644 --- a/include/framebuf/screen.h +++ b/include/framebuf/screen.h @@ -1,4 +1,4 @@ -/* screen.h -- screen type */ +/* framebuf/screen.h -- screen type */ #ifndef FRAMEBUF_SCREEN_H #define FRAMEBUF_SCREEN_H diff --git a/include/framebuf/span-bgrx8888.h b/include/framebuf/span-bgrx8888.h index e4bf23c1..f956fda9 100644 --- a/include/framebuf/span-bgrx8888.h +++ b/include/framebuf/span-bgrx8888.h @@ -1,4 +1,4 @@ -/* span-bgrx8888.h -- BGRX8888 format plot methods */ +/* framebuf/span-bgrx8888.h -- BGRX8888 format plot methods */ #ifndef SPAN_BGRX8888_H #define SPAN_BGRX8888_H diff --git a/include/framebuf/span-p4.h b/include/framebuf/span-p4.h index f49edf53..776f7f6f 100644 --- a/include/framebuf/span-p4.h +++ b/include/framebuf/span-p4.h @@ -1,4 +1,4 @@ -/* span-p4.h -- P4 (4bpp paletted) format plot methods */ +/* framebuf/span-p4.h -- P4 (4bpp paletted) format plot methods */ #ifndef SPAN_P4_H #define SPAN_P4_H diff --git a/include/framebuf/span-registry.h b/include/framebuf/span-registry.h index b1eb34ff..77685f14 100644 --- a/include/framebuf/span-registry.h +++ b/include/framebuf/span-registry.h @@ -1,4 +1,4 @@ -/* span-registry.h -- registry of plotting methods */ +/* framebuf/span-registry.h -- registry of plotting methods */ #ifndef SPAN_REGISTRY_H #define SPAN_REGISTRY_H diff --git a/include/framebuf/span-rgba8888.h b/include/framebuf/span-rgba8888.h index 4aec7fd6..bad2cc32 100644 --- a/include/framebuf/span-rgba8888.h +++ b/include/framebuf/span-rgba8888.h @@ -1,4 +1,4 @@ -/* span-rgba8888.h -- RGBA8888 format plot methods */ +/* framebuf/span-rgba8888.h -- RGBA8888 format plot methods */ #ifndef SPAN_RGBA8888_H #define SPAN_RGBA8888_H diff --git a/include/framebuf/span-rgbx8888.h b/include/framebuf/span-rgbx8888.h index e516b636..0c12e03e 100644 --- a/include/framebuf/span-rgbx8888.h +++ b/include/framebuf/span-rgbx8888.h @@ -1,4 +1,4 @@ -/* span-rgbx8888.h -- RGBX8888 format plot methods */ +/* framebuf/span-rgbx8888.h -- RGBX8888 format plot methods */ #ifndef SPAN_RGBX8888_H #define SPAN_RGBX8888_H diff --git a/include/framebuf/span-xbgr8888.h b/include/framebuf/span-xbgr8888.h index 67d29fe1..8640c97c 100644 --- a/include/framebuf/span-xbgr8888.h +++ b/include/framebuf/span-xbgr8888.h @@ -1,4 +1,4 @@ -/* span-xbgr8888.h -- XBGR8888 format plot methods */ +/* framebuf/span-xbgr8888.h -- XBGR8888 format plot methods */ #ifndef SPAN_XBGR8888_H #define SPAN_XBGR8888_H diff --git a/include/framebuf/span.h b/include/framebuf/span.h index 724ff622..6b1a3f43 100644 --- a/include/framebuf/span.h +++ b/include/framebuf/span.h @@ -1,4 +1,4 @@ -/* span.h -- interface of plotting methods */ +/* framebuf/span.h -- interface of plotting methods */ #ifndef SPAN_H #define SPAN_H diff --git a/include/geom/box.h b/include/geom/box.h index a2fd0736..1c618697 100644 --- a/include/geom/box.h +++ b/include/geom/box.h @@ -1,4 +1,4 @@ -/* box.h -- box type */ +/* geom/box.h -- box type */ #ifndef GEOM_BOX_H #define GEOM_BOX_H diff --git a/include/geom/layout.h b/include/geom/layout.h index 131c8768..e56a94c7 100644 --- a/include/geom/layout.h +++ b/include/geom/layout.h @@ -1,4 +1,4 @@ -/* layout.h -- laying out elements using the packer */ +/* geom/layout.h -- laying out elements using the packer */ #ifndef GEOM_LAYOUT_H #define GEOM_LAYOUT_H diff --git a/include/geom/line.h b/include/geom/line.h index c57849d8..73dadabe 100644 --- a/include/geom/line.h +++ b/include/geom/line.h @@ -1,4 +1,4 @@ -/* line.h -- lines */ +/* geom/line.h -- lines */ #ifndef GEOM_LINE_H #define GEOM_LINE_H diff --git a/include/geom/packer.h b/include/geom/packer.h index c9360098..20ff3976 100644 --- a/include/geom/packer.h +++ b/include/geom/packer.h @@ -1,4 +1,4 @@ -/* packer.h -- box packing for layout */ +/* geom/packer.h -- box packing for layout */ #ifndef GEOM_PACKER_H #define GEOM_PACKER_H diff --git a/include/geom/point.h b/include/geom/point.h index a1360842..36ee4be9 100644 --- a/include/geom/point.h +++ b/include/geom/point.h @@ -1,4 +1,4 @@ -/* point.h -- point type */ +/* geom/point.h -- point type */ #ifndef GEOM_POINT_H #define GEOM_POINT_H diff --git a/include/geom/size.h b/include/geom/size.h index 5d66d4c0..0210cc4c 100644 --- a/include/geom/size.h +++ b/include/geom/size.h @@ -1,4 +1,4 @@ -/* size.h -- size type */ +/* geom/size.h -- size type */ #ifndef GEOM_SIZE_H #define GEOM_SIZE_H diff --git a/include/io/path.h b/include/io/path.h index 8a12df5c..0cb354f4 100644 --- a/include/io/path.h +++ b/include/io/path.h @@ -1,4 +1,4 @@ -/* path.h -- filename path handling */ +/* io/path.h -- filename path handling */ #ifndef DPTLIB_PATH #define DPTLIB_PATH diff --git a/include/io/stream-mem.h b/include/io/stream-mem.h index 599f973e..bf48c14a 100644 --- a/include/io/stream-mem.h +++ b/include/io/stream-mem.h @@ -1,4 +1,4 @@ -/* stream-mem.c -- memory block IO stream implementation */ +/* io/stream-mem.h -- memory block IO stream implementation */ #ifndef STREAM_MEM_H #define STREAM_MEM_H diff --git a/include/io/stream-mtfcomp.h b/include/io/stream-mtfcomp.h index ffe7f6de..7d17d036 100644 --- a/include/io/stream-mtfcomp.h +++ b/include/io/stream-mtfcomp.h @@ -1,4 +1,4 @@ -/* stream-mtfcomp.h -- "move to front" adaptive compression stream */ +/* io/stream-mtfcomp.h -- "move to front" adaptive compression stream */ #ifndef STREAM_MTFCOMP_H #define STREAM_MTFCOMP_H diff --git a/include/io/stream-packbits.h b/include/io/stream-packbits.h index 836066bb..cdd70e2b 100644 --- a/include/io/stream-packbits.h +++ b/include/io/stream-packbits.h @@ -1,4 +1,4 @@ -/* stream-packbits.h -- PackBits compression */ +/* io/stream-packbits.h -- PackBits compression */ #ifndef STREAM_PACKBITS_H #define STREAM_PACKBITS_H diff --git a/include/io/stream-stdio.h b/include/io/stream-stdio.h index 71bcd4f9..a769c70a 100644 --- a/include/io/stream-stdio.h +++ b/include/io/stream-stdio.h @@ -1,4 +1,4 @@ -/* stream-stdio.c -- C standard IO stream implementation */ +/* io/stream-stdio.h -- C standard IO stream implementation */ #ifndef STREAM_STDIO_H #define STREAM_STDIO_H diff --git a/include/io/stream.h b/include/io/stream.h index 7e676fd4..6eed9217 100644 --- a/include/io/stream.h +++ b/include/io/stream.h @@ -1,4 +1,4 @@ -/* stream.h -- stream system */ +/* io/stream.h -- stream system */ /** * \file Stream (interface). diff --git a/include/test/all-tests.h b/include/test/all-tests.h index 9986c3a7..aca2be79 100644 --- a/include/test/all-tests.h +++ b/include/test/all-tests.h @@ -1,4 +1,4 @@ -/* all-tests.h */ +/* test/all-tests.h */ #ifndef TESTS_ALL_TESTS_H #define TESTS_ALL_TESTS_H diff --git a/include/test/txtscr.h b/include/test/txtscr.h index 0dcf6191..40c6c6fd 100644 --- a/include/test/txtscr.h +++ b/include/test/txtscr.h @@ -1,4 +1,4 @@ -/* txtscr.h -- text format 'screen' */ +/* test/txtscr.h -- text format 'screen' */ #ifndef TEST_TXTSCR_H #define TEST_TXTSCR_H diff --git a/include/text/bmtext.h b/include/text/bmtext.h index f3b38113..adee453d 100644 --- a/include/text/bmtext.h +++ b/include/text/bmtext.h @@ -1,4 +1,4 @@ -/* bmtext.h -- word-wrap and draw a paragraph in a bitmap font */ +/* text/bmtext.h -- word-wrap and draw a paragraph in a bitmap font */ /** * \file bmtext.h diff --git a/include/text/txtfmt.h b/include/text/txtfmt.h index dc087c06..68937037 100644 --- a/include/text/txtfmt.h +++ b/include/text/txtfmt.h @@ -1,4 +1,4 @@ -/* txtfmt.h -- text formatting */ +/* text/txtfmt.h -- text formatting */ /** * \file txtfmt.h diff --git a/include/utils/array.h b/include/utils/array.h index 26ae20df..b25d3661 100644 --- a/include/utils/array.h +++ b/include/utils/array.h @@ -1,4 +1,4 @@ -/* array.h -- array utilities */ +/* utils/array.h -- array utilities */ /** * \file array.h diff --git a/include/utils/barith.h b/include/utils/barith.h index 053ccd77..68008063 100644 --- a/include/utils/barith.h +++ b/include/utils/barith.h @@ -1,4 +1,4 @@ -/* barith.h -- binary arithmetic */ +/* utils/barith.h -- binary arithmetic */ #ifndef UTILS_BARITH_H #define UTILS_BARITH_H diff --git a/include/utils/bsearch.h b/include/utils/bsearch.h index 7405f2e2..2fed3371 100644 --- a/include/utils/bsearch.h +++ b/include/utils/bsearch.h @@ -1,4 +1,4 @@ -/* bsearch.h -- binary searching arrays */ +/* utils/bsearch.h -- binary searching arrays */ #ifndef UTILS_BSEARCH_H #define UTILS_BSEARCH_H diff --git a/include/utils/bytesex.h b/include/utils/bytesex.h index 71508c01..d3af2d89 100644 --- a/include/utils/bytesex.h +++ b/include/utils/bytesex.h @@ -1,4 +1,4 @@ -/* bytesex.h -- reversing bytesex */ +/* utils/bytesex.h -- reversing bytesex */ #ifndef UTILS_BYTESEX_H #define UTILS_BYTESEX_H diff --git a/include/utils/fxp.h b/include/utils/fxp.h index 4043cb9b..d5bfe9a5 100644 --- a/include/utils/fxp.h +++ b/include/utils/fxp.h @@ -1,4 +1,4 @@ -/* fxp.h -- fixed point helpers */ +/* utils/fxp.h -- fixed point helpers */ #ifndef UTILS_FXP_H #define UTILS_FXP_H diff --git a/include/utils/maths.h b/include/utils/maths.h index 50df4a1f..aef58a0a 100644 --- a/include/utils/maths.h +++ b/include/utils/maths.h @@ -1,4 +1,4 @@ -/* maths.h -- maths utils */ +/* utils/maths.h -- maths utils */ #ifndef UTILS_MATHS_H #define UTILS_MATHS_H diff --git a/include/utils/pack.h b/include/utils/pack.h index 43bf89a2..d61f4bbb 100644 --- a/include/utils/pack.h +++ b/include/utils/pack.h @@ -1,4 +1,4 @@ -/* pack.h -- structure packing and unpacking helpers */ +/* utils/pack.h -- structure packing and unpacking helpers */ /** * \file Pack (interface). diff --git a/include/utils/primes.h b/include/utils/primes.h index 8948abf5..1937c849 100644 --- a/include/utils/primes.h +++ b/include/utils/primes.h @@ -1,4 +1,4 @@ -/* primes.h -- cache of prime numbers */ +/* utils/primes.h -- cache of prime numbers */ #ifndef UTILS_PRIMES_H #define UTILS_PRIMES_H diff --git a/include/wuss/icon.h b/include/wuss/icon.h index 13c709b8..8a6f9b43 100644 --- a/include/wuss/icon.h +++ b/include/wuss/icon.h @@ -1,4 +1,4 @@ -/* icon.h -- wuss work-area icons */ +/* wuss/icon.h -- wuss work-area icons */ /** * \file icon.h diff --git a/include/wuss/menu.h b/include/wuss/menu.h index 7d28c746..463fad80 100644 --- a/include/wuss/menu.h +++ b/include/wuss/menu.h @@ -1,4 +1,4 @@ -/* menu.h -- wuss pop-up menu helper */ +/* wuss/menu.h -- wuss pop-up menu helper */ /** * \file menu.h diff --git a/include/wuss/task.h b/include/wuss/task.h index 02b65a4a..b2cf1d66 100644 --- a/include/wuss/task.h +++ b/include/wuss/task.h @@ -1,4 +1,4 @@ -/* task.h -- wuss task API */ +/* wuss/task.h -- wuss task API */ /** * \file task.h diff --git a/include/wuss/window.h b/include/wuss/window.h index 601e51ea..1d830d9d 100644 --- a/include/wuss/window.h +++ b/include/wuss/window.h @@ -1,4 +1,4 @@ -/* window.h -- wuss window API */ +/* wuss/window.h -- wuss window API */ /** * \file window.h diff --git a/include/wuss/wuss.h b/include/wuss/wuss.h index b57259c8..fa43c69e 100644 --- a/include/wuss/wuss.h +++ b/include/wuss/wuss.h @@ -1,4 +1,4 @@ -/* wuss.h -- minimal window manager */ +/* wuss/wuss.h -- minimal window manager */ /** * \file wuss.h diff --git a/libraries/databases/digest-db/digest-db.c b/libraries/databases/digest-db/digest-db.c index c09e0e9e..ed22e84b 100644 --- a/libraries/databases/digest-db/digest-db.c +++ b/libraries/databases/digest-db/digest-db.c @@ -1,4 +1,4 @@ -/* digest-db.c -- digest database */ +/* databases/digest-db/digest-db.c -- digest database */ #include <limits.h> #include <stddef.h> diff --git a/libraries/databases/filename-db/filename-db.c b/libraries/databases/filename-db/filename-db.c index abe5202c..9ba41f73 100644 --- a/libraries/databases/filename-db/filename-db.c +++ b/libraries/databases/filename-db/filename-db.c @@ -1,4 +1,4 @@ -/* filename-db.c -- filename database */ +/* databases/filename-db/filename-db.c -- filename database */ /* filenamedb maps md5 digests to filenames so that we can search for files * and retrieve filenames */ diff --git a/libraries/databases/pickle/delete.c b/libraries/databases/pickle/delete.c index a517f4ef..e4553874 100644 --- a/libraries/databases/pickle/delete.c +++ b/libraries/databases/pickle/delete.c @@ -1,4 +1,4 @@ -/* delete.c -- delete a pickle file */ +/* databases/pickle/delete.c -- delete a pickle file */ #include <assert.h> #include <stdio.h> diff --git a/libraries/databases/pickle/hash-reader.c b/libraries/databases/pickle/hash-reader.c index d878c83c..e1ee74b5 100644 --- a/libraries/databases/pickle/hash-reader.c +++ b/libraries/databases/pickle/hash-reader.c @@ -1,4 +1,4 @@ -/* hash-reader.c -- glue methods to let pickle operate on hashes */ +/* databases/pickle/hash-reader.c -- glue methods to let pickle operate on hashes */ #include <assert.h> #include <ctype.h> diff --git a/libraries/databases/pickle/hash-writer.c b/libraries/databases/pickle/hash-writer.c index 2949dcfb..3b892f71 100644 --- a/libraries/databases/pickle/hash-writer.c +++ b/libraries/databases/pickle/hash-writer.c @@ -1,4 +1,4 @@ -/* hash-writer.c -- glue methods to let pickle operate on hashes */ +/* databases/pickle/hash-writer.c -- glue methods to let pickle operate on hashes */ #include <assert.h> #include <ctype.h> diff --git a/libraries/databases/pickle/pickle.c b/libraries/databases/pickle/pickle.c index 55bcf06b..eadb172f 100644 --- a/libraries/databases/pickle/pickle.c +++ b/libraries/databases/pickle/pickle.c @@ -1,4 +1,4 @@ -/* pickle.c -- serialise an associative array to file */ +/* databases/pickle/pickle.c -- serialise an associative array to file */ #include <assert.h> #include <ctype.h> diff --git a/libraries/databases/pickle/test/pickle-test.c b/libraries/databases/pickle/test/pickle-test.c index 59ab3981..8b38f707 100644 --- a/libraries/databases/pickle/test/pickle-test.c +++ b/libraries/databases/pickle/test/pickle-test.c @@ -1,3 +1,4 @@ +/* databases/pickle/test/pickle-test.c */ #include <assert.h> #include <stdio.h> diff --git a/libraries/databases/pickle/unpickle.c b/libraries/databases/pickle/unpickle.c index fce030be..a0c2be20 100644 --- a/libraries/databases/pickle/unpickle.c +++ b/libraries/databases/pickle/unpickle.c @@ -1,4 +1,4 @@ -/* unpickle.c -- deserialise an associative array from file */ +/* databases/pickle/unpickle.c -- deserialise an associative array from file */ #include <assert.h> #include <ctype.h> diff --git a/libraries/databases/tag-db/tag-db.c b/libraries/databases/tag-db/tag-db.c index 5397b361..b6ce93ab 100644 --- a/libraries/databases/tag-db/tag-db.c +++ b/libraries/databases/tag-db/tag-db.c @@ -1,4 +1,4 @@ -/* tag-db.c -- tag database */ +/* databases/tag-db/tag-db.c -- tag database */ // TODO // cope with tags with spaces (quoted for saving and loading) diff --git a/libraries/databases/tag-db/test/tag-db-test.c b/libraries/databases/tag-db/test/tag-db-test.c index 076b1191..8e7a777f 100644 --- a/libraries/databases/tag-db/test/tag-db-test.c +++ b/libraries/databases/tag-db/test/tag-db-test.c @@ -1,3 +1,4 @@ +/* databases/tag-db/test/tag-db-test.c */ #include <assert.h> #include <stdio.h> diff --git a/libraries/datastruct/atom/create.c b/libraries/datastruct/atom/create.c index 90d51b6b..1a61b68c 100644 --- a/libraries/datastruct/atom/create.c +++ b/libraries/datastruct/atom/create.c @@ -1,4 +1,4 @@ -/* create.c -- atoms */ +/* datastruct/atom/create.c -- atoms */ #include <stdlib.h> diff --git a/libraries/datastruct/atom/delete-block.c b/libraries/datastruct/atom/delete-block.c index 1a9ca722..6ededecc 100644 --- a/libraries/datastruct/atom/delete-block.c +++ b/libraries/datastruct/atom/delete-block.c @@ -1,4 +1,4 @@ -/* delete-block.c -- atoms */ +/* datastruct/atom/delete-block.c -- atoms */ #include <stdlib.h> diff --git a/libraries/datastruct/atom/delete.c b/libraries/datastruct/atom/delete.c index a7be93f9..a6735de5 100644 --- a/libraries/datastruct/atom/delete.c +++ b/libraries/datastruct/atom/delete.c @@ -1,4 +1,4 @@ -/* delete.c -- atoms */ +/* datastruct/atom/delete.c -- atoms */ #include <assert.h> #include <string.h> diff --git a/libraries/datastruct/atom/destroy.c b/libraries/datastruct/atom/destroy.c index 3dcf6470..493587c4 100644 --- a/libraries/datastruct/atom/destroy.c +++ b/libraries/datastruct/atom/destroy.c @@ -1,4 +1,4 @@ -/* destroy.c -- atoms */ +/* datastruct/atom/destroy.c -- atoms */ #include <stdlib.h> diff --git a/libraries/datastruct/atom/for-block.c b/libraries/datastruct/atom/for-block.c index c67db912..c8f27f42 100644 --- a/libraries/datastruct/atom/for-block.c +++ b/libraries/datastruct/atom/for-block.c @@ -1,4 +1,4 @@ -/* for-block.c -- atoms */ +/* datastruct/atom/for-block.c -- atoms */ #include <assert.h> #include <limits.h> diff --git a/libraries/datastruct/atom/get.c b/libraries/datastruct/atom/get.c index ef63a7f0..6b423075 100644 --- a/libraries/datastruct/atom/get.c +++ b/libraries/datastruct/atom/get.c @@ -1,4 +1,4 @@ -/* get.c -- atoms */ +/* datastruct/atom/get.c -- atoms */ #include <assert.h> #include <stddef.h> diff --git a/libraries/datastruct/atom/impl.h b/libraries/datastruct/atom/impl.h index 35c7e13c..12373308 100644 --- a/libraries/datastruct/atom/impl.h +++ b/libraries/datastruct/atom/impl.h @@ -1,4 +1,4 @@ -/* impl.h -- atoms */ +/* datastruct/atom/impl.h -- atoms */ /* An atom_set consists of a series of block pools and location pools. * diff --git a/libraries/datastruct/atom/new.c b/libraries/datastruct/atom/new.c index 58663d69..026ebebe 100644 --- a/libraries/datastruct/atom/new.c +++ b/libraries/datastruct/atom/new.c @@ -1,4 +1,4 @@ -/* new.c -- atoms */ +/* datastruct/atom/new.c -- atoms */ #include <assert.h> #include <stdlib.h> diff --git a/libraries/datastruct/atom/set.c b/libraries/datastruct/atom/set.c index 0b702451..68d1a8d9 100644 --- a/libraries/datastruct/atom/set.c +++ b/libraries/datastruct/atom/set.c @@ -1,4 +1,4 @@ -/* set.c -- atoms */ +/* datastruct/atom/set.c -- atoms */ #include <assert.h> #include <string.h> diff --git a/libraries/datastruct/atom/test/atom-test.c b/libraries/datastruct/atom/test/atom-test.c index 0fbabed5..78743dea 100644 --- a/libraries/datastruct/atom/test/atom-test.c +++ b/libraries/datastruct/atom/test/atom-test.c @@ -1,3 +1,4 @@ +/* datastruct/atom/test/atom-test.c */ #include <stdio.h> #include <stdlib.h> diff --git a/libraries/datastruct/bitarr/count.c b/libraries/datastruct/bitarr/count.c index 475562c9..b3a89652 100644 --- a/libraries/datastruct/bitarr/count.c +++ b/libraries/datastruct/bitarr/count.c @@ -1,4 +1,4 @@ -/* count.c -- arrays of bits */ +/* datastruct/bitarr/count.c -- arrays of bits */ #include <stddef.h> diff --git a/libraries/datastruct/bitarr/test/bitarr-test.c b/libraries/datastruct/bitarr/test/bitarr-test.c index 91a566bb..e8403c05 100644 --- a/libraries/datastruct/bitarr/test/bitarr-test.c +++ b/libraries/datastruct/bitarr/test/bitarr-test.c @@ -1,3 +1,4 @@ +/* datastruct/bitarr/test/bitarr-test.c */ #include <stdio.h> #include <stdlib.h> diff --git a/libraries/datastruct/bitfifo/bitfifo.c b/libraries/datastruct/bitfifo/bitfifo.c index cd0e6b13..ba215515 100644 --- a/libraries/datastruct/bitfifo/bitfifo.c +++ b/libraries/datastruct/bitfifo/bitfifo.c @@ -1,4 +1,4 @@ -/* bitfifo.c -- fifo which stores bits */ +/* datastruct/bitfifo/bitfifo.c -- fifo which stores bits */ #include <assert.h> #include <string.h> diff --git a/libraries/datastruct/bitfifo/test/bitfifo-test.c b/libraries/datastruct/bitfifo/test/bitfifo-test.c index d46bf447..d346539e 100644 --- a/libraries/datastruct/bitfifo/test/bitfifo-test.c +++ b/libraries/datastruct/bitfifo/test/bitfifo-test.c @@ -1,3 +1,4 @@ +/* datastruct/bitfifo/test/bitfifo-test.c */ #include <limits.h> #include <stdio.h> diff --git a/libraries/datastruct/bitvec/and.c b/libraries/datastruct/bitvec/and.c index b291e995..d6eceab4 100644 --- a/libraries/datastruct/bitvec/and.c +++ b/libraries/datastruct/bitvec/and.c @@ -1,4 +1,4 @@ -/* and.c -- bit vectors */ +/* datastruct/bitvec/and.c -- bit vectors */ #include <stdlib.h> diff --git a/libraries/datastruct/bitvec/clear-all.c b/libraries/datastruct/bitvec/clear-all.c index f17259ed..8b24e7ea 100644 --- a/libraries/datastruct/bitvec/clear-all.c +++ b/libraries/datastruct/bitvec/clear-all.c @@ -1,4 +1,4 @@ -/* clear-all.c -- bit vectors */ +/* datastruct/bitvec/clear-all.c -- bit vectors */ #include <string.h> diff --git a/libraries/datastruct/bitvec/clear.c b/libraries/datastruct/bitvec/clear.c index 166b232a..982ac63b 100644 --- a/libraries/datastruct/bitvec/clear.c +++ b/libraries/datastruct/bitvec/clear.c @@ -1,4 +1,4 @@ -/* clear.c -- bit vectors */ +/* datastruct/bitvec/clear.c -- bit vectors */ #include "datastruct/bitvec.h" diff --git a/libraries/datastruct/bitvec/count.c b/libraries/datastruct/bitvec/count.c index 51e548aa..6834dc26 100644 --- a/libraries/datastruct/bitvec/count.c +++ b/libraries/datastruct/bitvec/count.c @@ -1,4 +1,4 @@ -/* count.c -- bit vectors */ +/* datastruct/bitvec/count.c -- bit vectors */ #include "utils/barith.h" #include "datastruct/bitvec.h" diff --git a/libraries/datastruct/bitvec/create.c b/libraries/datastruct/bitvec/create.c index 56f2b454..459797c7 100644 --- a/libraries/datastruct/bitvec/create.c +++ b/libraries/datastruct/bitvec/create.c @@ -1,4 +1,4 @@ -/* create.c -- bit vectors */ +/* datastruct/bitvec/create.c -- bit vectors */ #include <assert.h> #include <stdlib.h> diff --git a/libraries/datastruct/bitvec/destroy.c b/libraries/datastruct/bitvec/destroy.c index ece3d5c3..70a1eb09 100644 --- a/libraries/datastruct/bitvec/destroy.c +++ b/libraries/datastruct/bitvec/destroy.c @@ -1,4 +1,4 @@ -/* destroy.c -- bit vectors */ +/* datastruct/bitvec/destroy.c -- bit vectors */ #include <stdlib.h> diff --git a/libraries/datastruct/bitvec/ensure.c b/libraries/datastruct/bitvec/ensure.c index 62adab48..02f74c0c 100644 --- a/libraries/datastruct/bitvec/ensure.c +++ b/libraries/datastruct/bitvec/ensure.c @@ -1,4 +1,4 @@ -/* ensure.c -- bit vectors */ +/* datastruct/bitvec/ensure.c -- bit vectors */ #include <stdlib.h> #include <string.h> diff --git a/libraries/datastruct/bitvec/eq.c b/libraries/datastruct/bitvec/eq.c index b7e4d500..3e852c1d 100644 --- a/libraries/datastruct/bitvec/eq.c +++ b/libraries/datastruct/bitvec/eq.c @@ -1,4 +1,4 @@ -/* eq.c -- bit vectors */ +/* datastruct/bitvec/eq.c -- bit vectors */ #include <string.h> diff --git a/libraries/datastruct/bitvec/get.c b/libraries/datastruct/bitvec/get.c index e2b270ef..a548b47c 100644 --- a/libraries/datastruct/bitvec/get.c +++ b/libraries/datastruct/bitvec/get.c @@ -1,4 +1,4 @@ -/* get.c -- bit vectors */ +/* datastruct/bitvec/get.c -- bit vectors */ #include "datastruct/bitvec.h" diff --git a/libraries/datastruct/bitvec/impl.h b/libraries/datastruct/bitvec/impl.h index e94f1fb6..7e312de6 100644 --- a/libraries/datastruct/bitvec/impl.h +++ b/libraries/datastruct/bitvec/impl.h @@ -1,4 +1,4 @@ -/* impl.h -- bit vectors */ +/* datastruct/bitvec/impl.h -- bit vectors */ #ifndef DATASTRUCT_BITVEC_IMPL_H #define DATASTRUCT_BITVEC_IMPL_H diff --git a/libraries/datastruct/bitvec/length.c b/libraries/datastruct/bitvec/length.c index 401d6748..3c484190 100644 --- a/libraries/datastruct/bitvec/length.c +++ b/libraries/datastruct/bitvec/length.c @@ -1,4 +1,4 @@ -/* length.c -- bit vectors */ +/* datastruct/bitvec/length.c -- bit vectors */ #include <stddef.h> diff --git a/libraries/datastruct/bitvec/next.c b/libraries/datastruct/bitvec/next.c index 67c540b3..0bb2d992 100644 --- a/libraries/datastruct/bitvec/next.c +++ b/libraries/datastruct/bitvec/next.c @@ -1,4 +1,4 @@ -/* next.c -- bit vectors */ +/* datastruct/bitvec/next.c -- bit vectors */ #include "utils/barith.h" #include "datastruct/bitvec.h" diff --git a/libraries/datastruct/bitvec/or.c b/libraries/datastruct/bitvec/or.c index 5724ef37..cc06de1e 100644 --- a/libraries/datastruct/bitvec/or.c +++ b/libraries/datastruct/bitvec/or.c @@ -1,4 +1,4 @@ -/* or.c -- bit vectors */ +/* datastruct/bitvec/or.c -- bit vectors */ #include <stdlib.h> diff --git a/libraries/datastruct/bitvec/set-all.c b/libraries/datastruct/bitvec/set-all.c index d913afa0..368aa710 100644 --- a/libraries/datastruct/bitvec/set-all.c +++ b/libraries/datastruct/bitvec/set-all.c @@ -1,4 +1,4 @@ -/* set-all.c -- bit vectors */ +/* datastruct/bitvec/set-all.c -- bit vectors */ #include <string.h> diff --git a/libraries/datastruct/bitvec/set.c b/libraries/datastruct/bitvec/set.c index 2cefd9b4..58c4a699 100644 --- a/libraries/datastruct/bitvec/set.c +++ b/libraries/datastruct/bitvec/set.c @@ -1,4 +1,4 @@ -/* set.c -- bit vectors */ +/* datastruct/bitvec/set.c -- bit vectors */ #include "base/result.h" diff --git a/libraries/datastruct/bitvec/test/bitvec-test.c b/libraries/datastruct/bitvec/test/bitvec-test.c index 97165452..fb708dd3 100644 --- a/libraries/datastruct/bitvec/test/bitvec-test.c +++ b/libraries/datastruct/bitvec/test/bitvec-test.c @@ -1,3 +1,4 @@ +/* datastruct/bitvec/test/bitvec-test.c */ #include <limits.h> #include <stdio.h> diff --git a/libraries/datastruct/bitvec/toggle.c b/libraries/datastruct/bitvec/toggle.c index 94a29bc4..4105b9bb 100644 --- a/libraries/datastruct/bitvec/toggle.c +++ b/libraries/datastruct/bitvec/toggle.c @@ -1,4 +1,4 @@ -/* toggle.c -- bit vectors */ +/* datastruct/bitvec/toggle.c -- bit vectors */ #include "base/result.h" diff --git a/libraries/datastruct/cache/cache.c b/libraries/datastruct/cache/cache.c index 1a8c1cc1..0b25c784 100644 --- a/libraries/datastruct/cache/cache.c +++ b/libraries/datastruct/cache/cache.c @@ -1,4 +1,4 @@ -/* cache.c -- generic single block cache */ +/* datastruct/cache/cache.c -- generic single block cache */ #include <assert.h> #include <limits.h> diff --git a/libraries/datastruct/cache/test/cache-test.c b/libraries/datastruct/cache/test/cache-test.c index c1704c90..e4fbaaa8 100644 --- a/libraries/datastruct/cache/test/cache-test.c +++ b/libraries/datastruct/cache/test/cache-test.c @@ -1,3 +1,4 @@ +/* datastruct/cache/test/cache-test.c */ #include <limits.h> #include <stdio.h> diff --git a/libraries/datastruct/hash/count.c b/libraries/datastruct/hash/count.c index 0d24af29..02fbe318 100644 --- a/libraries/datastruct/hash/count.c +++ b/libraries/datastruct/hash/count.c @@ -1,4 +1,4 @@ -/* count.c -- hash */ +/* datastruct/hash/count.c -- hash */ #include "datastruct/hash.h" diff --git a/libraries/datastruct/hash/create.c b/libraries/datastruct/hash/create.c index 70f23800..eb2387f4 100644 --- a/libraries/datastruct/hash/create.c +++ b/libraries/datastruct/hash/create.c @@ -1,4 +1,4 @@ -/* create.c -- hash */ +/* datastruct/hash/create.c -- hash */ #include <stdlib.h> #include <string.h> diff --git a/libraries/datastruct/hash/destroy.c b/libraries/datastruct/hash/destroy.c index 8aa8252a..2c06ca98 100644 --- a/libraries/datastruct/hash/destroy.c +++ b/libraries/datastruct/hash/destroy.c @@ -1,4 +1,4 @@ -/* destroy.c -- hash */ +/* datastruct/hash/destroy.c -- hash */ #include <stdlib.h> diff --git a/libraries/datastruct/hash/impl.h b/libraries/datastruct/hash/impl.h index 808a7901..9403e832 100644 --- a/libraries/datastruct/hash/impl.h +++ b/libraries/datastruct/hash/impl.h @@ -1,4 +1,4 @@ -/* impl.h -- hash */ +/* datastruct/hash/impl.h -- hash */ #ifndef DATASTRUCT_HASH_IMPL_H #define DATASTRUCT_HASH_IMPL_H diff --git a/libraries/datastruct/hash/insert.c b/libraries/datastruct/hash/insert.c index df661758..0644aee1 100644 --- a/libraries/datastruct/hash/insert.c +++ b/libraries/datastruct/hash/insert.c @@ -1,4 +1,4 @@ -/* insert.c -- hash */ +/* datastruct/hash/insert.c -- hash */ #include <stdlib.h> diff --git a/libraries/datastruct/hash/lookup-node.c b/libraries/datastruct/hash/lookup-node.c index 6b6c4357..42ee1445 100644 --- a/libraries/datastruct/hash/lookup-node.c +++ b/libraries/datastruct/hash/lookup-node.c @@ -1,4 +1,4 @@ -/* lookup-node.c -- hash */ +/* datastruct/hash/lookup-node.c -- hash */ #include <stdlib.h> diff --git a/libraries/datastruct/hash/lookup.c b/libraries/datastruct/hash/lookup.c index 8f665037..3671c38b 100644 --- a/libraries/datastruct/hash/lookup.c +++ b/libraries/datastruct/hash/lookup.c @@ -1,4 +1,4 @@ -/* lookup.c -- hash */ +/* datastruct/hash/lookup.c -- hash */ #include <stdlib.h> diff --git a/libraries/datastruct/hash/remove.c b/libraries/datastruct/hash/remove.c index a3a1ce8a..8222d6ae 100644 --- a/libraries/datastruct/hash/remove.c +++ b/libraries/datastruct/hash/remove.c @@ -1,4 +1,4 @@ -/* remove.c -- hash */ +/* datastruct/hash/remove.c -- hash */ #include <stdlib.h> diff --git a/libraries/datastruct/hash/test/hash-test.c b/libraries/datastruct/hash/test/hash-test.c index 7d807b29..c896d5d3 100644 --- a/libraries/datastruct/hash/test/hash-test.c +++ b/libraries/datastruct/hash/test/hash-test.c @@ -1,3 +1,4 @@ +/* datastruct/hash/test/hash-test.c */ #include <stdio.h> #include <string.h> diff --git a/libraries/datastruct/hash/walk-cont.c b/libraries/datastruct/hash/walk-cont.c index 4fa822fd..db3b44c6 100644 --- a/libraries/datastruct/hash/walk-cont.c +++ b/libraries/datastruct/hash/walk-cont.c @@ -1,4 +1,4 @@ -/* walk-cont.c -- hash */ +/* datastruct/hash/walk-cont.c -- hash */ #include <assert.h> #include <stdlib.h> diff --git a/libraries/datastruct/hash/walk.c b/libraries/datastruct/hash/walk.c index 6388c627..d876d441 100644 --- a/libraries/datastruct/hash/walk.c +++ b/libraries/datastruct/hash/walk.c @@ -1,4 +1,4 @@ -/* walk.c -- hash */ +/* datastruct/hash/walk.c -- hash */ #include <stdlib.h> diff --git a/libraries/datastruct/hlist/append.c b/libraries/datastruct/hlist/append.c index 601242b5..3bcfbd50 100644 --- a/libraries/datastruct/hlist/append.c +++ b/libraries/datastruct/hlist/append.c @@ -1,3 +1,4 @@ +/* datastruct/hlist/append.c */ #include <stdarg.h> #include <stddef.h> diff --git a/libraries/datastruct/hlist/copy.c b/libraries/datastruct/hlist/copy.c index 49fdd4cc..12240d40 100644 --- a/libraries/datastruct/hlist/copy.c +++ b/libraries/datastruct/hlist/copy.c @@ -1,3 +1,4 @@ +/* datastruct/hlist/copy.c */ #include <stdarg.h> #include <stddef.h> diff --git a/libraries/datastruct/hlist/free.c b/libraries/datastruct/hlist/free.c index 5823b503..3e8dfc38 100644 --- a/libraries/datastruct/hlist/free.c +++ b/libraries/datastruct/hlist/free.c @@ -1,3 +1,4 @@ +/* datastruct/hlist/free.c */ #include <assert.h> #include <stdarg.h> diff --git a/libraries/datastruct/hlist/impl.h b/libraries/datastruct/hlist/impl.h index 32b30fa7..b3e533d8 100644 --- a/libraries/datastruct/hlist/impl.h +++ b/libraries/datastruct/hlist/impl.h @@ -1,3 +1,3 @@ -/* impl.h -- hlist */ +/* datastruct/hlist/impl.h -- hlist */ #define T hlist_t diff --git a/libraries/datastruct/hlist/length.c b/libraries/datastruct/hlist/length.c index b52c1615..41535767 100644 --- a/libraries/datastruct/hlist/length.c +++ b/libraries/datastruct/hlist/length.c @@ -1,3 +1,4 @@ +/* datastruct/hlist/length.c */ #include <stdarg.h> #include <stddef.h> diff --git a/libraries/datastruct/hlist/list.c b/libraries/datastruct/hlist/list.c index a8178dff..07cd0ffb 100644 --- a/libraries/datastruct/hlist/list.c +++ b/libraries/datastruct/hlist/list.c @@ -1,3 +1,4 @@ +/* datastruct/hlist/list.c */ #include <stdarg.h> #include <stddef.h> diff --git a/libraries/datastruct/hlist/map.c b/libraries/datastruct/hlist/map.c index 183567b0..d0a3acd7 100644 --- a/libraries/datastruct/hlist/map.c +++ b/libraries/datastruct/hlist/map.c @@ -1,3 +1,4 @@ +/* datastruct/hlist/map.c */ #include <assert.h> #include <stdarg.h> diff --git a/libraries/datastruct/hlist/pop.c b/libraries/datastruct/hlist/pop.c index 03a09025..9d3d3e4a 100644 --- a/libraries/datastruct/hlist/pop.c +++ b/libraries/datastruct/hlist/pop.c @@ -1,3 +1,4 @@ +/* datastruct/hlist/pop.c */ #include <stdarg.h> #include <stddef.h> diff --git a/libraries/datastruct/hlist/push.c b/libraries/datastruct/hlist/push.c index fefe1cd2..2e5add1b 100644 --- a/libraries/datastruct/hlist/push.c +++ b/libraries/datastruct/hlist/push.c @@ -1,3 +1,4 @@ +/* datastruct/hlist/push.c */ #include <stdarg.h> #include <stddef.h> diff --git a/libraries/datastruct/hlist/reverse.c b/libraries/datastruct/hlist/reverse.c index 49467f1f..31647c87 100644 --- a/libraries/datastruct/hlist/reverse.c +++ b/libraries/datastruct/hlist/reverse.c @@ -1,3 +1,4 @@ +/* datastruct/hlist/reverse.c */ #include <stdarg.h> #include <stddef.h> diff --git a/libraries/datastruct/hlist/to-array.c b/libraries/datastruct/hlist/to-array.c index 26da4b1f..be907e24 100644 --- a/libraries/datastruct/hlist/to-array.c +++ b/libraries/datastruct/hlist/to-array.c @@ -1,3 +1,4 @@ +/* datastruct/hlist/to-array.c */ #include <stdarg.h> #include <stddef.h> diff --git a/libraries/datastruct/list/add-head.c b/libraries/datastruct/list/add-head.c index f7e1d6ba..59b9f6c7 100644 --- a/libraries/datastruct/list/add-head.c +++ b/libraries/datastruct/list/add-head.c @@ -1,3 +1,4 @@ +/* datastruct/list/add-head.c */ #include "datastruct/list.h" diff --git a/libraries/datastruct/list/add-tail.c b/libraries/datastruct/list/add-tail.c index bb602371..b500ba57 100644 --- a/libraries/datastruct/list/add-tail.c +++ b/libraries/datastruct/list/add-tail.c @@ -1,3 +1,4 @@ +/* datastruct/list/add-tail.c */ #include "datastruct/list.h" diff --git a/libraries/datastruct/list/find.c b/libraries/datastruct/list/find.c index 7db71f05..e283cb87 100644 --- a/libraries/datastruct/list/find.c +++ b/libraries/datastruct/list/find.c @@ -1,3 +1,4 @@ +/* datastruct/list/find.c */ #include "datastruct/list.h" diff --git a/libraries/datastruct/list/init.c b/libraries/datastruct/list/init.c index c9cfcac3..d91c25df 100644 --- a/libraries/datastruct/list/init.c +++ b/libraries/datastruct/list/init.c @@ -1,3 +1,4 @@ +/* datastruct/list/init.c */ #include "datastruct/list.h" diff --git a/libraries/datastruct/list/remove.c b/libraries/datastruct/list/remove.c index 532a7223..bf73e24f 100644 --- a/libraries/datastruct/list/remove.c +++ b/libraries/datastruct/list/remove.c @@ -1,3 +1,4 @@ +/* datastruct/list/remove.c */ #include <assert.h> diff --git a/libraries/datastruct/list/test/list-test.c b/libraries/datastruct/list/test/list-test.c index a6e959cd..ed2964ec 100644 --- a/libraries/datastruct/list/test/list-test.c +++ b/libraries/datastruct/list/test/list-test.c @@ -1,3 +1,4 @@ +/* datastruct/list/test/list-test.c */ #include <stdio.h> diff --git a/libraries/datastruct/list/walk.c b/libraries/datastruct/list/walk.c index 37c7ed9a..bbbee8c2 100644 --- a/libraries/datastruct/list/walk.c +++ b/libraries/datastruct/list/walk.c @@ -1,3 +1,4 @@ +/* datastruct/list/walk.c */ #include "datastruct/list.h" diff --git a/libraries/datastruct/ntree/copy.c b/libraries/datastruct/ntree/copy.c index 6214491d..3870dad9 100644 --- a/libraries/datastruct/ntree/copy.c +++ b/libraries/datastruct/ntree/copy.c @@ -1,4 +1,4 @@ -/* copy.c -- n-ary tree */ +/* datastruct/ntree/copy.c -- n-ary tree */ #include <stdlib.h> diff --git a/libraries/datastruct/ntree/delete.c b/libraries/datastruct/ntree/delete.c index 8730160b..3c640ad7 100644 --- a/libraries/datastruct/ntree/delete.c +++ b/libraries/datastruct/ntree/delete.c @@ -1,4 +1,4 @@ -/* delete.c -- n-ary tree */ +/* datastruct/ntree/delete.c -- n-ary tree */ #include <assert.h> diff --git a/libraries/datastruct/ntree/depth.c b/libraries/datastruct/ntree/depth.c index e64d4e37..491e4d2f 100644 --- a/libraries/datastruct/ntree/depth.c +++ b/libraries/datastruct/ntree/depth.c @@ -1,4 +1,4 @@ -/* depth.c -- n-ary tree */ +/* datastruct/ntree/depth.c -- n-ary tree */ #include "datastruct/ntree.h" diff --git a/libraries/datastruct/ntree/first-child.c b/libraries/datastruct/ntree/first-child.c index 8bcbf63a..2e896803 100644 --- a/libraries/datastruct/ntree/first-child.c +++ b/libraries/datastruct/ntree/first-child.c @@ -1,4 +1,4 @@ -/* first-child.c -- n-ary tree */ +/* datastruct/ntree/first-child.c -- n-ary tree */ #include <stdlib.h> diff --git a/libraries/datastruct/ntree/free.c b/libraries/datastruct/ntree/free.c index 06b345eb..06b0323c 100644 --- a/libraries/datastruct/ntree/free.c +++ b/libraries/datastruct/ntree/free.c @@ -1,4 +1,4 @@ -/* free.c -- n-ary tree */ +/* datastruct/ntree/free.c -- n-ary tree */ #include <assert.h> #include <stdlib.h> diff --git a/libraries/datastruct/ntree/get-data.c b/libraries/datastruct/ntree/get-data.c index 07647d9e..f842bdeb 100644 --- a/libraries/datastruct/ntree/get-data.c +++ b/libraries/datastruct/ntree/get-data.c @@ -1,4 +1,4 @@ -/* get-data.c -- n-ary tree */ +/* datastruct/ntree/get-data.c -- n-ary tree */ #include "datastruct/ntree.h" diff --git a/libraries/datastruct/ntree/impl.h b/libraries/datastruct/ntree/impl.h index dec9802e..21c6ecc8 100644 --- a/libraries/datastruct/ntree/impl.h +++ b/libraries/datastruct/ntree/impl.h @@ -1,4 +1,4 @@ -/* impl.h -- n-ary tree */ +/* datastruct/ntree/impl.h -- n-ary tree */ #ifndef NTREE_IMPL_H #define NTREE_IMPL_H diff --git a/libraries/datastruct/ntree/insert-after.c b/libraries/datastruct/ntree/insert-after.c index 6ef8f7fb..94d78899 100644 --- a/libraries/datastruct/ntree/insert-after.c +++ b/libraries/datastruct/ntree/insert-after.c @@ -1,4 +1,4 @@ -/* insert-after.c -- n-ary tree */ +/* datastruct/ntree/insert-after.c -- n-ary tree */ #include <assert.h> #include <stdlib.h> diff --git a/libraries/datastruct/ntree/insert-before.c b/libraries/datastruct/ntree/insert-before.c index ae44f4aa..5afd5627 100644 --- a/libraries/datastruct/ntree/insert-before.c +++ b/libraries/datastruct/ntree/insert-before.c @@ -1,4 +1,4 @@ -/* insert-before.c -- n-ary tree */ +/* datastruct/ntree/insert-before.c -- n-ary tree */ #include <assert.h> #include <stdlib.h> diff --git a/libraries/datastruct/ntree/insert.c b/libraries/datastruct/ntree/insert.c index 26e90199..2bb87f1d 100644 --- a/libraries/datastruct/ntree/insert.c +++ b/libraries/datastruct/ntree/insert.c @@ -1,4 +1,4 @@ -/* insert.c -- n-ary tree */ +/* datastruct/ntree/insert.c -- n-ary tree */ #include <stdlib.h> diff --git a/libraries/datastruct/ntree/last-child.c b/libraries/datastruct/ntree/last-child.c index 5dd9a037..800eac4f 100644 --- a/libraries/datastruct/ntree/last-child.c +++ b/libraries/datastruct/ntree/last-child.c @@ -1,4 +1,4 @@ -/* last-child.c -- n-ary tree */ +/* datastruct/ntree/last-child.c -- n-ary tree */ #include "datastruct/ntree.h" diff --git a/libraries/datastruct/ntree/max-height.c b/libraries/datastruct/ntree/max-height.c index 6735c76c..30df91fe 100644 --- a/libraries/datastruct/ntree/max-height.c +++ b/libraries/datastruct/ntree/max-height.c @@ -1,4 +1,4 @@ -/* max-height.c -- n-ary tree */ +/* datastruct/ntree/max-height.c -- n-ary tree */ #include "datastruct/ntree.h" diff --git a/libraries/datastruct/ntree/n-nodes.c b/libraries/datastruct/ntree/n-nodes.c index 76a18124..80189c9a 100644 --- a/libraries/datastruct/ntree/n-nodes.c +++ b/libraries/datastruct/ntree/n-nodes.c @@ -1,4 +1,4 @@ -/* n-nodes.c -- n-ary tree */ +/* datastruct/ntree/n-nodes.c -- n-ary tree */ #include "base/utils.h" diff --git a/libraries/datastruct/ntree/new.c b/libraries/datastruct/ntree/new.c index 24aeb2b4..fa1dd1ad 100644 --- a/libraries/datastruct/ntree/new.c +++ b/libraries/datastruct/ntree/new.c @@ -1,4 +1,4 @@ -/* new.c -- n-ary tree */ +/* datastruct/ntree/new.c -- n-ary tree */ #include <stdlib.h> diff --git a/libraries/datastruct/ntree/next-sibling.c b/libraries/datastruct/ntree/next-sibling.c index 57d3d73e..617c234f 100644 --- a/libraries/datastruct/ntree/next-sibling.c +++ b/libraries/datastruct/ntree/next-sibling.c @@ -1,4 +1,4 @@ -/* next-sibling.c -- n-ary tree */ +/* datastruct/ntree/next-sibling.c -- n-ary tree */ #include <stdlib.h> diff --git a/libraries/datastruct/ntree/nth-child.c b/libraries/datastruct/ntree/nth-child.c index feef026f..cf11b395 100644 --- a/libraries/datastruct/ntree/nth-child.c +++ b/libraries/datastruct/ntree/nth-child.c @@ -1,4 +1,4 @@ -/* nth-child.c -- n-ary tree */ +/* datastruct/ntree/nth-child.c -- n-ary tree */ #include "datastruct/ntree.h" diff --git a/libraries/datastruct/ntree/parent.c b/libraries/datastruct/ntree/parent.c index a947a76d..ed756cf2 100644 --- a/libraries/datastruct/ntree/parent.c +++ b/libraries/datastruct/ntree/parent.c @@ -1,4 +1,4 @@ -/* parent.c -- n-ary tree */ +/* datastruct/ntree/parent.c -- n-ary tree */ #include <stdlib.h> diff --git a/libraries/datastruct/ntree/prepend.c b/libraries/datastruct/ntree/prepend.c index 42d0a912..e5820a9a 100644 --- a/libraries/datastruct/ntree/prepend.c +++ b/libraries/datastruct/ntree/prepend.c @@ -1,4 +1,4 @@ -/* prepend.c -- n-ary tree */ +/* datastruct/ntree/prepend.c -- n-ary tree */ #include <stdlib.h> diff --git a/libraries/datastruct/ntree/prev-sibling.c b/libraries/datastruct/ntree/prev-sibling.c index 48526b5a..04866359 100644 --- a/libraries/datastruct/ntree/prev-sibling.c +++ b/libraries/datastruct/ntree/prev-sibling.c @@ -1,4 +1,4 @@ -/* prev-sibling.c -- n-ary tree */ +/* datastruct/ntree/prev-sibling.c -- n-ary tree */ #include <stdlib.h> diff --git a/libraries/datastruct/ntree/set-data.c b/libraries/datastruct/ntree/set-data.c index 611b418c..533a6696 100644 --- a/libraries/datastruct/ntree/set-data.c +++ b/libraries/datastruct/ntree/set-data.c @@ -1,4 +1,4 @@ -/* set-data.c -- n-ary tree */ +/* datastruct/ntree/set-data.c -- n-ary tree */ #include "datastruct/ntree.h" diff --git a/libraries/datastruct/ntree/test/ntree-test.c b/libraries/datastruct/ntree/test/ntree-test.c index 8a9c8950..9aa5417b 100644 --- a/libraries/datastruct/ntree/test/ntree-test.c +++ b/libraries/datastruct/ntree/test/ntree-test.c @@ -1,3 +1,4 @@ +/* datastruct/ntree/test/ntree-test.c */ #include <stdio.h> #include <stdlib.h> diff --git a/libraries/datastruct/ntree/unlink.c b/libraries/datastruct/ntree/unlink.c index c9df723c..94838fd6 100644 --- a/libraries/datastruct/ntree/unlink.c +++ b/libraries/datastruct/ntree/unlink.c @@ -1,4 +1,4 @@ -/* unlink.c -- n-ary tree */ +/* datastruct/ntree/unlink.c -- n-ary tree */ #include <assert.h> #include <stdlib.h> diff --git a/libraries/datastruct/ntree/walk.c b/libraries/datastruct/ntree/walk.c index e9e4e219..22320720 100644 --- a/libraries/datastruct/ntree/walk.c +++ b/libraries/datastruct/ntree/walk.c @@ -1,4 +1,4 @@ -/* walk.c -- n-ary tree */ +/* datastruct/ntree/walk.c -- n-ary tree */ #include <assert.h> #include <stdlib.h> diff --git a/libraries/datastruct/vector/clear.c b/libraries/datastruct/vector/clear.c index 0345e94a..da5096a7 100644 --- a/libraries/datastruct/vector/clear.c +++ b/libraries/datastruct/vector/clear.c @@ -1,4 +1,4 @@ -/* clear.c -- vector - flexible array */ +/* datastruct/vector/clear.c -- flexible array */ #include <stdlib.h> diff --git a/libraries/datastruct/vector/create.c b/libraries/datastruct/vector/create.c index bdc03a38..ab861228 100644 --- a/libraries/datastruct/vector/create.c +++ b/libraries/datastruct/vector/create.c @@ -1,4 +1,4 @@ -/* create.c -- vector - flexible array */ +/* datastruct/vector/create.c -- flexible array */ #include <stdlib.h> diff --git a/libraries/datastruct/vector/destroy.c b/libraries/datastruct/vector/destroy.c index b4fce1a0..1cf7a36f 100644 --- a/libraries/datastruct/vector/destroy.c +++ b/libraries/datastruct/vector/destroy.c @@ -1,4 +1,4 @@ -/* destroy.c -- vector - flexible array */ +/* datastruct/vector/destroy.c -- flexible array */ #include <stdlib.h> diff --git a/libraries/datastruct/vector/ensure.c b/libraries/datastruct/vector/ensure.c index 29bcf67b..39b44e80 100644 --- a/libraries/datastruct/vector/ensure.c +++ b/libraries/datastruct/vector/ensure.c @@ -1,4 +1,4 @@ -/* ensure.c -- vector - flexible array */ +/* datastruct/vector/ensure.c -- flexible array */ #include <stdlib.h> diff --git a/libraries/datastruct/vector/get.c b/libraries/datastruct/vector/get.c index 817ddc1b..b6d995a9 100644 --- a/libraries/datastruct/vector/get.c +++ b/libraries/datastruct/vector/get.c @@ -1,4 +1,4 @@ -/* get.c -- vector - flexible array */ +/* datastruct/vector/get.c -- flexible array */ #include <assert.h> #include <stdlib.h> diff --git a/libraries/datastruct/vector/impl.h b/libraries/datastruct/vector/impl.h index 7ffc5935..143a0fcb 100644 --- a/libraries/datastruct/vector/impl.h +++ b/libraries/datastruct/vector/impl.h @@ -1,4 +1,4 @@ -/* impl.h -- vector - flexible array */ +/* datastruct/vector/impl.h -- flexible array */ #ifndef IMPL_H #define IMPL_H diff --git a/libraries/datastruct/vector/insert.c b/libraries/datastruct/vector/insert.c index f171d2e8..5a09f628 100644 --- a/libraries/datastruct/vector/insert.c +++ b/libraries/datastruct/vector/insert.c @@ -1,4 +1,4 @@ -/* insert.c -- vector - flexible array */ +/* datastruct/vector/insert.c -- flexible array */ #include <assert.h> #include <stdlib.h> diff --git a/libraries/datastruct/vector/length.c b/libraries/datastruct/vector/length.c index 87e33c8d..29a17a21 100644 --- a/libraries/datastruct/vector/length.c +++ b/libraries/datastruct/vector/length.c @@ -1,4 +1,4 @@ -/* length.c -- vector - flexible array */ +/* datastruct/vector/length.c -- flexible array */ #include <stdlib.h> diff --git a/libraries/datastruct/vector/set-length.c b/libraries/datastruct/vector/set-length.c index 3e9a0d84..4d20b92d 100644 --- a/libraries/datastruct/vector/set-length.c +++ b/libraries/datastruct/vector/set-length.c @@ -1,4 +1,4 @@ -/* set-length.c -- vector - flexible array */ +/* datastruct/vector/set-length.c -- flexible array */ #include <stdlib.h> diff --git a/libraries/datastruct/vector/set-width.c b/libraries/datastruct/vector/set-width.c index 7a222e9e..26d4fd61 100644 --- a/libraries/datastruct/vector/set-width.c +++ b/libraries/datastruct/vector/set-width.c @@ -1,4 +1,4 @@ -/* set-width.c -- vector - flexible array */ +/* datastruct/vector/set-width.c -- flexible array */ #include <stdlib.h> diff --git a/libraries/datastruct/vector/set.c b/libraries/datastruct/vector/set.c index 7f5e5c94..0a2a39db 100644 --- a/libraries/datastruct/vector/set.c +++ b/libraries/datastruct/vector/set.c @@ -1,4 +1,4 @@ -/* set.c -- vector - flexible array */ +/* datastruct/vector/set.c -- flexible array */ #include <assert.h> #include <string.h> diff --git a/libraries/datastruct/vector/test/vector-test.c b/libraries/datastruct/vector/test/vector-test.c index 2b7ec064..48fba96c 100644 --- a/libraries/datastruct/vector/test/vector-test.c +++ b/libraries/datastruct/vector/test/vector-test.c @@ -1,3 +1,4 @@ +/* datastruct/vector/test/vector-test.c */ #include <stdio.h> diff --git a/libraries/datastruct/vector/width.c b/libraries/datastruct/vector/width.c index 24930510..4d3081b5 100644 --- a/libraries/datastruct/vector/width.c +++ b/libraries/datastruct/vector/width.c @@ -1,4 +1,4 @@ -/* width.c -- vector - flexible array */ +/* datastruct/vector/width.c -- flexible array */ #include <stdlib.h> diff --git a/libraries/framebuf/bitmap/bitmap.c b/libraries/framebuf/bitmap/bitmap.c index af839a25..bef5f884 100644 --- a/libraries/framebuf/bitmap/bitmap.c +++ b/libraries/framebuf/bitmap/bitmap.c @@ -1,4 +1,4 @@ -/* bitmap.c */ +/* framebuf/bitmap/bitmap.c */ #include <assert.h> #include <stddef.h> diff --git a/libraries/framebuf/bitmap/load.c b/libraries/framebuf/bitmap/load.c index b4b3fbaa..85f3be70 100644 --- a/libraries/framebuf/bitmap/load.c +++ b/libraries/framebuf/bitmap/load.c @@ -1,4 +1,4 @@ -/* load.c */ +/* framebuf/bitmap/load.c */ #include <assert.h> #include <stdlib.h> diff --git a/libraries/framebuf/bitmap/save.c b/libraries/framebuf/bitmap/save.c index 7be936e5..4de7ee23 100755 --- a/libraries/framebuf/bitmap/save.c +++ b/libraries/framebuf/bitmap/save.c @@ -1,4 +1,4 @@ -/* save.c */ +/* framebuf/bitmap/save.c */ #include <assert.h> #include <stdlib.h> diff --git a/libraries/framebuf/bmfont/bmfont.c b/libraries/framebuf/bmfont/bmfont.c index f7581eb9..534c9996 100644 --- a/libraries/framebuf/bmfont/bmfont.c +++ b/libraries/framebuf/bmfont/bmfont.c @@ -1,4 +1,4 @@ -/* bmfont.c */ +/* framebuf/bmfont/bmfont.c */ /* TODOS / IDEAS * diff --git a/libraries/framebuf/bmfont/test/bmfont-test.c b/libraries/framebuf/bmfont/test/bmfont-test.c index c9caed05..86457d8f 100644 --- a/libraries/framebuf/bmfont/test/bmfont-test.c +++ b/libraries/framebuf/bmfont/test/bmfont-test.c @@ -1,4 +1,4 @@ -/* bmfont-test.c */ +/* framebuf/bmfont/test/bmfont-test.c */ #include <assert.h> #include <ctype.h> diff --git a/libraries/framebuf/colour/colour.c b/libraries/framebuf/colour/colour.c index 1b77838f..e7b58b86 100644 --- a/libraries/framebuf/colour/colour.c +++ b/libraries/framebuf/colour/colour.c @@ -1,4 +1,4 @@ -/* colour.c */ +/* framebuf/colour/colour.c */ #include <assert.h> #include <stddef.h> diff --git a/libraries/framebuf/composite/composite.c b/libraries/framebuf/composite/composite.c index 3aa29acb..b2859cd0 100644 --- a/libraries/framebuf/composite/composite.c +++ b/libraries/framebuf/composite/composite.c @@ -1,4 +1,4 @@ -/* composite.c +/* framebuf/composite/composite.c * * An implementation of Porter-Duff image compositing. * diff --git a/libraries/framebuf/composite/test/composite-test.c b/libraries/framebuf/composite/test/composite-test.c index fdb0fe95..eac2cdc8 100644 --- a/libraries/framebuf/composite/test/composite-test.c +++ b/libraries/framebuf/composite/test/composite-test.c @@ -1,4 +1,4 @@ -/* composite-test.c */ +/* framebuf/composite/test/composite-test.c */ #include <assert.h> #include <stdio.h> diff --git a/libraries/framebuf/curve/curve.c b/libraries/framebuf/curve/curve.c index fd244d12..dc74c5ef 100644 --- a/libraries/framebuf/curve/curve.c +++ b/libraries/framebuf/curve/curve.c @@ -1,4 +1,4 @@ -/* bezier.c */ +/* framebuf/curve/curve.c */ // TODO // diff --git a/libraries/framebuf/curve/test/curve-test.c b/libraries/framebuf/curve/test/curve-test.c index 1c8e7f2f..2a926448 100644 --- a/libraries/framebuf/curve/test/curve-test.c +++ b/libraries/framebuf/curve/test/curve-test.c @@ -1,4 +1,4 @@ -/* curve-test.c */ +/* framebuf/curve/test/curve-test.c */ #include <assert.h> #include <ctype.h> diff --git a/libraries/framebuf/palettes/palettes.c b/libraries/framebuf/palettes/palettes.c index 2f1b79b2..ac3b2586 100644 --- a/libraries/framebuf/palettes/palettes.c +++ b/libraries/framebuf/palettes/palettes.c @@ -1,4 +1,4 @@ -/* palettes.c */ +/* framebuf/palettes/palettes.c */ #include "framebuf/colour.h" diff --git a/libraries/framebuf/pixelfmt/log2bpp.c b/libraries/framebuf/pixelfmt/log2bpp.c index dbc78a38..1d593519 100644 --- a/libraries/framebuf/pixelfmt/log2bpp.c +++ b/libraries/framebuf/pixelfmt/log2bpp.c @@ -1,4 +1,4 @@ -/* log2bpp.c -- given a pixel format return its log2 bits-per-pixel size */ +/* framebuf/pixelfmt/log2bpp.c -- given a pixel format return its log2 bits-per-pixel size */ #include <assert.h> diff --git a/libraries/framebuf/screen/screen-copy-rect.c b/libraries/framebuf/screen/screen-copy-rect.c index 5ae5231e..46c5f559 100644 --- a/libraries/framebuf/screen/screen-copy-rect.c +++ b/libraries/framebuf/screen/screen-copy-rect.c @@ -1,4 +1,4 @@ -/* screen-copy-rect.c -- screen - same-screen rectangle copy */ +/* framebuf/screen/screen-copy-rect.c -- same-screen rectangle copy */ #include <string.h> diff --git a/libraries/framebuf/screen/screen-draw-ninepatch.c b/libraries/framebuf/screen/screen-draw-ninepatch.c index b0b7a795..6dac6608 100644 --- a/libraries/framebuf/screen/screen-draw-ninepatch.c +++ b/libraries/framebuf/screen/screen-draw-ninepatch.c @@ -1,4 +1,4 @@ -/* screen-draw-ninepatch.c -- 9-patch bitmap drawing */ +/* framebuf/screen/screen-draw-ninepatch.c -- 9-patch bitmap drawing */ #include <assert.h> diff --git a/libraries/framebuf/screen/screen-draw.c b/libraries/framebuf/screen/screen-draw.c index 40a2f01a..5c77046c 100644 --- a/libraries/framebuf/screen/screen-draw.c +++ b/libraries/framebuf/screen/screen-draw.c @@ -1,4 +1,4 @@ -/* screen-draw.c */ +/* framebuf/screen/screen-draw.c */ #include <assert.h> #include <string.h> diff --git a/libraries/framebuf/screen/screen-fill-pattern.c b/libraries/framebuf/screen/screen-fill-pattern.c index d0613b6b..47fda849 100644 --- a/libraries/framebuf/screen/screen-fill-pattern.c +++ b/libraries/framebuf/screen/screen-fill-pattern.c @@ -1,4 +1,4 @@ -/* screen-fill-pattern.c -- fill a box with a repeating 8x8 two-colour pattern */ +/* framebuf/screen/screen-fill-pattern.c -- fill a box with a repeating 8x8 two-colour pattern */ #include <assert.h> #include <string.h> diff --git a/libraries/framebuf/screen/screen.c b/libraries/framebuf/screen/screen.c index e15fafdc..7cac1f59 100644 --- a/libraries/framebuf/screen/screen.c +++ b/libraries/framebuf/screen/screen.c @@ -1,4 +1,4 @@ -/* screen.c */ +/* framebuf/screen/screen.c */ #include <assert.h> #include <string.h> diff --git a/libraries/framebuf/screen/test/screen-test.c b/libraries/framebuf/screen/test/screen-test.c index 3d903ef2..8cf267fc 100644 --- a/libraries/framebuf/screen/test/screen-test.c +++ b/libraries/framebuf/screen/test/screen-test.c @@ -1,4 +1,4 @@ -/* screen-test.c -- test screen drawing */ +/* framebuf/screen/test/screen-test.c -- test screen drawing */ #include <stdio.h> #include <string.h> diff --git a/libraries/framebuf/span-registry/get.c b/libraries/framebuf/span-registry/get.c index b68e5501..52f99df7 100644 --- a/libraries/framebuf/span-registry/get.c +++ b/libraries/framebuf/span-registry/get.c @@ -1,4 +1,4 @@ -/* get.c */ +/* framebuf/span-registry/get.c */ #include <stddef.h> diff --git a/libraries/framebuf/span-registry/regdata.h b/libraries/framebuf/span-registry/regdata.h index aae1c27f..37b3fe15 100644 --- a/libraries/framebuf/span-registry/regdata.h +++ b/libraries/framebuf/span-registry/regdata.h @@ -1,4 +1,4 @@ -/* regdata.h */ +/* framebuf/span-registry/regdata.h */ #ifndef SPAN_REGISTRY_REGDATA_H #define SPAN_REGISTRY_REGDATA_H diff --git a/libraries/framebuf/span/all8888-generic.c b/libraries/framebuf/span/all8888-generic.c index 0306fed6..9d34b5f1 100644 --- a/libraries/framebuf/span/all8888-generic.c +++ b/libraries/framebuf/span/all8888-generic.c @@ -1,4 +1,4 @@ -/* all8888-blend.c -- alpha blending common to all 8888 formats */ +/* framebuf/span/all8888-generic.c -- alpha blending common to all 8888 formats */ #include "base/utils.h" diff --git a/libraries/framebuf/span/all8888.c b/libraries/framebuf/span/all8888.c index 9f9f2f04..51922e33 100644 --- a/libraries/framebuf/span/all8888.c +++ b/libraries/framebuf/span/all8888.c @@ -1,4 +1,4 @@ -/* all8888.c */ +/* framebuf/span/all8888.c */ #include <string.h> diff --git a/libraries/framebuf/span/all8888.h b/libraries/framebuf/span/all8888.h index f5b285b4..028e41c7 100644 --- a/libraries/framebuf/span/all8888.h +++ b/libraries/framebuf/span/all8888.h @@ -1,4 +1,4 @@ -/* all8888.h -- span handlers common to all 8888 formats */ +/* framebuf/span/all8888.h -- span handlers common to all 8888 formats */ #ifndef SPAN_ALL8888_H #define SPAN_ALL8888_H diff --git a/libraries/framebuf/span/bgrx8888.c b/libraries/framebuf/span/bgrx8888.c index 25aaea02..bafceaff 100644 --- a/libraries/framebuf/span/bgrx8888.c +++ b/libraries/framebuf/span/bgrx8888.c @@ -1,4 +1,4 @@ -/* bgrx8888.c */ +/* framebuf/span/bgrx8888.c */ #include <string.h> diff --git a/libraries/framebuf/span/p4.c b/libraries/framebuf/span/p4.c index eb401a8d..80cfca35 100644 --- a/libraries/framebuf/span/p4.c +++ b/libraries/framebuf/span/p4.c @@ -1,4 +1,4 @@ -/* p4.c -- P4 (4bpp paletted) format plot methods */ +/* framebuf/span/p4.c -- P4 (4bpp paletted) format plot methods */ #include <stddef.h> diff --git a/libraries/framebuf/span/rgbx8888.c b/libraries/framebuf/span/rgbx8888.c index 01424443..6f1efe32 100644 --- a/libraries/framebuf/span/rgbx8888.c +++ b/libraries/framebuf/span/rgbx8888.c @@ -1,4 +1,4 @@ -/* rgbx8888.c */ +/* framebuf/span/rgbx8888.c */ #include <string.h> diff --git a/libraries/framebuf/span/xbgr8888.c b/libraries/framebuf/span/xbgr8888.c index 1dc85442..a2c8acde 100644 --- a/libraries/framebuf/span/xbgr8888.c +++ b/libraries/framebuf/span/xbgr8888.c @@ -1,4 +1,4 @@ -/* xbgr8888.c */ +/* framebuf/span/xbgr8888.c */ #include <string.h> diff --git a/libraries/geom/box/clipped.c b/libraries/geom/box/clipped.c index 0348ba90..fc575f6b 100644 --- a/libraries/geom/box/clipped.c +++ b/libraries/geom/box/clipped.c @@ -1,4 +1,4 @@ -/* clipped.c -- calculate clipped away edge sizes */ +/* geom/box/clipped.c -- calculate clipped away edge sizes */ #include "base/utils.h" diff --git a/libraries/geom/box/contains-box.c b/libraries/geom/box/contains-box.c index 868ac77e..83b206b7 100644 --- a/libraries/geom/box/contains-box.c +++ b/libraries/geom/box/contains-box.c @@ -1,4 +1,4 @@ -/* contains-box.c -- return true if box "inside" is contained by box "outside" */ +/* geom/box/contains-box.c -- return true if box "inside" is contained by box "outside" */ #include "geom/box.h" diff --git a/libraries/geom/box/contains-point.c b/libraries/geom/box/contains-point.c index b13ebf97..650f225c 100644 --- a/libraries/geom/box/contains-point.c +++ b/libraries/geom/box/contains-point.c @@ -1,4 +1,4 @@ -/* contains-point.c -- return true if "box" contains the point (x,y) */ +/* geom/box/contains-point.c -- return true if "box" contains the point (x,y) */ #include "base/utils.h" diff --git a/libraries/geom/box/could-hold.c b/libraries/geom/box/could-hold.c index 203c5a5c..f787ec9d 100644 --- a/libraries/geom/box/could-hold.c +++ b/libraries/geom/box/could-hold.c @@ -1,4 +1,4 @@ -/* could-hold.c -- return true if "box" can hold a box of size (w,h) */ +/* geom/box/could-hold.c -- return true if "box" can hold a box of size (w,h) */ #include "geom/box.h" diff --git a/libraries/geom/box/extend.c b/libraries/geom/box/extend.c index 39005c06..9d9f390c 100644 --- a/libraries/geom/box/extend.c +++ b/libraries/geom/box/extend.c @@ -1,4 +1,4 @@ -/* extend.c -- extend box "b" to include (x,y). */ +/* geom/box/extend.c -- extend box "b" to include (x,y). */ #include <stdarg.h> diff --git a/libraries/geom/box/grow.c b/libraries/geom/box/grow.c index dfbcf6d7..e07aee9a 100644 --- a/libraries/geom/box/grow.c +++ b/libraries/geom/box/grow.c @@ -1,4 +1,4 @@ -/* grow.c -- increases the size of "box" by "change" */ +/* geom/box/grow.c -- increases the size of "box" by "change" */ #include "geom/box.h" diff --git a/libraries/geom/box/intersection.c b/libraries/geom/box/intersection.c index 4d1bdf7a..d8af0083 100644 --- a/libraries/geom/box/intersection.c +++ b/libraries/geom/box/intersection.c @@ -1,4 +1,4 @@ -/* intersection.c -- compute "c" the result of intersecting boxes "a" and "b" */ +/* geom/box/intersection.c -- compute "c" the result of intersecting boxes "a" and "b" */ #include "base/utils.h" diff --git a/libraries/geom/box/intersects.c b/libraries/geom/box/intersects.c index c9e6b686..e4360008 100644 --- a/libraries/geom/box/intersects.c +++ b/libraries/geom/box/intersects.c @@ -1,4 +1,4 @@ -/* intersects.c -- return true if box "a" overlaps box "b" */ +/* geom/box/intersects.c -- return true if box "a" overlaps box "b" */ #include "geom/box.h" diff --git a/libraries/geom/box/is-empty.c b/libraries/geom/box/is-empty.c index 8fcaedb2..57d5f14e 100644 --- a/libraries/geom/box/is-empty.c +++ b/libraries/geom/box/is-empty.c @@ -1,4 +1,4 @@ -/* is-empty.c -- return whether the specified box is empty */ +/* geom/box/is-empty.c -- return whether the specified box is empty */ #include "geom/box.h" diff --git a/libraries/geom/box/reset.c b/libraries/geom/box/reset.c index 0a785b2f..88286d20 100644 --- a/libraries/geom/box/reset.c +++ b/libraries/geom/box/reset.c @@ -1,4 +1,4 @@ -/* reset.c -- set box 'b' to be invalid */ +/* geom/box/reset.c -- set box 'b' to be invalid */ #include <limits.h> diff --git a/libraries/geom/box/round.c b/libraries/geom/box/round.c index 6979b8a5..e9838983 100644 --- a/libraries/geom/box/round.c +++ b/libraries/geom/box/round.c @@ -1,4 +1,4 @@ -/* round.c -- round a box's coords so they're a multiple of log2 x,y */ +/* geom/box/round.c -- round a box's coords so they're a multiple of log2 x,y */ #include "base/utils.h" diff --git a/libraries/geom/box/round4.c b/libraries/geom/box/round4.c index 89ed7bf1..66ee929d 100644 --- a/libraries/geom/box/round4.c +++ b/libraries/geom/box/round4.c @@ -1,4 +1,4 @@ -/* round4.c -- round a box's coords so they're a multiple of 4 */ +/* geom/box/round4.c -- round a box's coords so they're a multiple of 4 */ #include "geom/box.h" diff --git a/libraries/geom/box/size.c b/libraries/geom/box/size.c index d5b0fd28..4a4d7710 100644 --- a/libraries/geom/box/size.c +++ b/libraries/geom/box/size.c @@ -1,4 +1,4 @@ -/* size.c -- return the size of the specified box */ +/* geom/box/size.c -- return the size of the specified box */ #include "geom/box.h" #include "geom/size.h" diff --git a/libraries/geom/box/test/box-test.c b/libraries/geom/box/test/box-test.c index efd8c69f..9b474abd 100644 --- a/libraries/geom/box/test/box-test.c +++ b/libraries/geom/box/test/box-test.c @@ -1,3 +1,4 @@ +/* geom/box/test/box-test.c */ #include <limits.h> #include <stdio.h> diff --git a/libraries/geom/box/translated.c b/libraries/geom/box/translated.c index f8d2d195..c84d56d4 100644 --- a/libraries/geom/box/translated.c +++ b/libraries/geom/box/translated.c @@ -1,4 +1,4 @@ -/* translated.c -- translate box "b" by (x,y) producing new box "t" */ +/* geom/box/translated.c -- translate box "b" by (x,y) producing new box "t" */ #include "base/utils.h" diff --git a/libraries/geom/box/union.c b/libraries/geom/box/union.c index 36b431f5..9944ffe9 100644 --- a/libraries/geom/box/union.c +++ b/libraries/geom/box/union.c @@ -1,4 +1,4 @@ -/* union.c -- return a box "c" that contains both boxes "a" and "b" */ +/* geom/box/union.c -- return a box "c" that contains both boxes "a" and "b" */ #include "base/utils.h" diff --git a/libraries/geom/layout/layout.c b/libraries/geom/layout/layout.c index 39081501..2c23fe1a 100644 --- a/libraries/geom/layout/layout.c +++ b/libraries/geom/layout/layout.c @@ -1,4 +1,4 @@ -/* layout.c -- laying out elements using the packer */ +/* geom/layout/layout.c -- laying out elements using the packer */ #include <stddef.h> diff --git a/libraries/geom/layout/test/layout-test.c b/libraries/geom/layout/test/layout-test.c index b1db8331..cad978ff 100644 --- a/libraries/geom/layout/test/layout-test.c +++ b/libraries/geom/layout/test/layout-test.c @@ -1,3 +1,4 @@ +/* geom/layout/test/layout-test.c */ #include <limits.h> #include <stdio.h> diff --git a/libraries/geom/line/line.c b/libraries/geom/line/line.c index 107b84a9..3d925bb7 100644 --- a/libraries/geom/line/line.c +++ b/libraries/geom/line/line.c @@ -1,4 +1,4 @@ -/* line.c -- Cohen-Sutherland line clipping algorithm */ +/* geom/line/line.c -- Cohen-Sutherland line clipping algorithm */ #include "base/utils.h" #include "geom/box.h" diff --git a/libraries/geom/packer/impl.h b/libraries/geom/packer/impl.h index e17c62c1..119ce176 100644 --- a/libraries/geom/packer/impl.h +++ b/libraries/geom/packer/impl.h @@ -1,4 +1,4 @@ -/* impl.h -- box packing for layout */ +/* geom/packer/impl.h -- box packing for layout */ #ifndef IMPL_H #define IMPL_H diff --git a/libraries/geom/packer/packer.c b/libraries/geom/packer/packer.c index b0715b18..324919fc 100644 --- a/libraries/geom/packer/packer.c +++ b/libraries/geom/packer/packer.c @@ -1,4 +1,4 @@ -/* packer.c -- box packing for layout */ +/* geom/packer/packer.c -- box packing for layout */ #include <assert.h> #include <limits.h> diff --git a/libraries/geom/packer/test/packer-test.c b/libraries/geom/packer/test/packer-test.c index dcece14b..ac0015bc 100644 --- a/libraries/geom/packer/test/packer-test.c +++ b/libraries/geom/packer/test/packer-test.c @@ -1,3 +1,4 @@ +/* geom/packer/test/packer-test.c */ #include <limits.h> #include <stdio.h> diff --git a/libraries/io/path/path.c b/libraries/io/path/path.c index d1bb657c..42acdc1c 100644 --- a/libraries/io/path/path.c +++ b/libraries/io/path/path.c @@ -1,4 +1,4 @@ -/* path.c */ +/* io/path/path.c */ #include <assert.h> #include <stdio.h> diff --git a/libraries/io/stream/stream-mem.c b/libraries/io/stream/stream-mem.c index c490504f..81ac0c0f 100644 --- a/libraries/io/stream/stream-mem.c +++ b/libraries/io/stream/stream-mem.c @@ -1,4 +1,4 @@ -/* stream-mem.c -- memory block IO stream implementation */ +/* io/stream/stream-mem.c -- memory block IO stream implementation */ #include <assert.h> #include <stddef.h> diff --git a/libraries/io/stream/stream-mtfcomp.c b/libraries/io/stream/stream-mtfcomp.c index 3c0ea8b4..e8971594 100644 --- a/libraries/io/stream/stream-mtfcomp.c +++ b/libraries/io/stream/stream-mtfcomp.c @@ -1,4 +1,4 @@ -/* stream-mtfcomp.c -- "Move to front" adaptive compression stream */ +/* io/stream/stream-mtfcomp.c -- "Move to front" adaptive compression stream */ #include <assert.h> #include <stddef.h> diff --git a/libraries/io/stream/stream-packbitscomp.c b/libraries/io/stream/stream-packbitscomp.c index a9630c43..3cb529a9 100644 --- a/libraries/io/stream/stream-packbitscomp.c +++ b/libraries/io/stream/stream-packbitscomp.c @@ -1,4 +1,4 @@ -/* stream-packbitscomp.c -- PackBits compression */ +/* io/stream/stream-packbitscomp.c -- PackBits compression */ #include <assert.h> #include <stddef.h> diff --git a/libraries/io/stream/stream-packbitsdecomp.c b/libraries/io/stream/stream-packbitsdecomp.c index d4a75004..7828e41b 100644 --- a/libraries/io/stream/stream-packbitsdecomp.c +++ b/libraries/io/stream/stream-packbitsdecomp.c @@ -1,4 +1,4 @@ -/* stream-packbitsdecomp.c -- PackBits decompression */ +/* io/stream/stream-packbitsdecomp.c -- PackBits decompression */ #include <assert.h> #include <stddef.h> diff --git a/libraries/io/stream/stream-stdio.c b/libraries/io/stream/stream-stdio.c index 8d6074bb..8ed97dcf 100644 --- a/libraries/io/stream/stream-stdio.c +++ b/libraries/io/stream/stream-stdio.c @@ -1,4 +1,4 @@ -/* stream-stdio.c -- C standard IO stream implementation */ +/* io/stream/stream-stdio.c -- C standard IO stream implementation */ #include <assert.h> #include <stddef.h> diff --git a/libraries/io/stream/stream.c b/libraries/io/stream/stream.c index 6101315f..b28966f6 100644 --- a/libraries/io/stream/stream.c +++ b/libraries/io/stream/stream.c @@ -1,4 +1,4 @@ -/* stream.c -- stream system support functions */ +/* io/stream/stream.c -- stream system support functions */ #include <assert.h> #include <stdio.h> diff --git a/libraries/io/stream/test/stream-test.c b/libraries/io/stream/test/stream-test.c index d100a674..696dc19d 100644 --- a/libraries/io/stream/test/stream-test.c +++ b/libraries/io/stream/test/stream-test.c @@ -1,3 +1,4 @@ +/* io/stream/test/stream-test.c */ #include <assert.h> #include <stdio.h> diff --git a/libraries/test/txtscr/txtscr.c b/libraries/test/txtscr/txtscr.c index 2a0c5377..d5670868 100644 --- a/libraries/test/txtscr/txtscr.c +++ b/libraries/test/txtscr/txtscr.c @@ -1,4 +1,4 @@ -/* txtscr.c -- text format 'screen' */ +/* test/txtscr/txtscr.c -- text format 'screen' */ #include <stdio.h> #include <stdlib.h> diff --git a/libraries/text/bmtext/draw.c b/libraries/text/bmtext/draw.c index e8d9eba4..1f508354 100644 --- a/libraries/text/bmtext/draw.c +++ b/libraries/text/bmtext/draw.c @@ -1,4 +1,4 @@ -/* draw.c -- draw pre-laid-out bitmap-font lines */ +/* text/bmtext/draw.c -- draw pre-laid-out bitmap-font lines */ #include <stddef.h> diff --git a/libraries/text/bmtext/layout.c b/libraries/text/bmtext/layout.c index 2470d8cf..b6f30b67 100644 --- a/libraries/text/bmtext/layout.c +++ b/libraries/text/bmtext/layout.c @@ -1,4 +1,4 @@ -/* layout.c -- break a string into pixel-fitted lines */ +/* text/bmtext/layout.c -- break a string into pixel-fitted lines */ #include <ctype.h> diff --git a/libraries/text/txtfmt/create.c b/libraries/text/txtfmt/create.c index 19f2999a..47516ba7 100644 --- a/libraries/text/txtfmt/create.c +++ b/libraries/text/txtfmt/create.c @@ -1,4 +1,4 @@ -/* create.c -- txtfmt - text formatting */ +/* text/txtfmt/create.c -- text formatting */ #include <stdlib.h> #include <string.h> diff --git a/libraries/text/txtfmt/destroy.c b/libraries/text/txtfmt/destroy.c index b176e220..aaed4e33 100644 --- a/libraries/text/txtfmt/destroy.c +++ b/libraries/text/txtfmt/destroy.c @@ -1,4 +1,4 @@ -/* destroy.c -- txtfmt - text formatting */ +/* text/txtfmt/destroy.c -- text formatting */ #include <stdlib.h> diff --git a/libraries/text/txtfmt/get-length.c b/libraries/text/txtfmt/get-length.c index 21e0a231..b3e2fd68 100644 --- a/libraries/text/txtfmt/get-length.c +++ b/libraries/text/txtfmt/get-length.c @@ -1,4 +1,4 @@ -/* get-length.c -- txtfmt - text formatting */ +/* text/txtfmt/get-length.c -- text formatting */ #include "text/txtfmt.h" diff --git a/libraries/text/txtfmt/get-line.c b/libraries/text/txtfmt/get-line.c index 4be628fb..247bd8f7 100644 --- a/libraries/text/txtfmt/get-line.c +++ b/libraries/text/txtfmt/get-line.c @@ -1,4 +1,4 @@ -/* get-line.c -- txtfmt - text formatting */ +/* text/txtfmt/get-line.c -- text formatting */ #include "text/txtfmt.h" diff --git a/libraries/text/txtfmt/get-nlines.c b/libraries/text/txtfmt/get-nlines.c index 03c0ba6a..c9bc3417 100644 --- a/libraries/text/txtfmt/get-nlines.c +++ b/libraries/text/txtfmt/get-nlines.c @@ -1,4 +1,4 @@ -/* get-nlines.c -- txtfmt - text formatting */ +/* text/txtfmt/get-nlines.c -- text formatting */ #include "text/txtfmt.h" diff --git a/libraries/text/txtfmt/get-wrapped-width.c b/libraries/text/txtfmt/get-wrapped-width.c index 06f1d8ce..2b104cd6 100644 --- a/libraries/text/txtfmt/get-wrapped-width.c +++ b/libraries/text/txtfmt/get-wrapped-width.c @@ -1,4 +1,4 @@ -/* get-wrapped-width.c -- txtfmt - text formatting */ +/* text/txtfmt/get-wrapped-width.c -- text formatting */ #include "text/txtfmt.h" diff --git a/libraries/text/txtfmt/impl.h b/libraries/text/txtfmt/impl.h index 7a4ced4d..3725bfa3 100644 --- a/libraries/text/txtfmt/impl.h +++ b/libraries/text/txtfmt/impl.h @@ -1,4 +1,4 @@ -/* impl.h -- txtfmt - text formatting */ +/* text/txtfmt/impl.h -- text formatting */ #ifndef IMPL_H #define IMPL_H diff --git a/libraries/text/txtfmt/print.c b/libraries/text/txtfmt/print.c index a6a898e0..13ef73de 100644 --- a/libraries/text/txtfmt/print.c +++ b/libraries/text/txtfmt/print.c @@ -1,4 +1,4 @@ -/* print.c -- txtfmt - text formatting */ +/* text/txtfmt/print.c -- text formatting */ #include <stdio.h> diff --git a/libraries/text/txtfmt/test/txtfmt-test.c b/libraries/text/txtfmt/test/txtfmt-test.c index 9cc0edbd..a1f28b5f 100644 --- a/libraries/text/txtfmt/test/txtfmt-test.c +++ b/libraries/text/txtfmt/test/txtfmt-test.c @@ -1,4 +1,4 @@ -/* txtfmt-test.c -- txtfmt - text formatting */ +/* text/txtfmt/test/txtfmt-test.c -- text formatting */ #include <stdio.h> diff --git a/libraries/text/txtfmt/wrap.c b/libraries/text/txtfmt/wrap.c index dc7632e3..8d64842d 100644 --- a/libraries/text/txtfmt/wrap.c +++ b/libraries/text/txtfmt/wrap.c @@ -1,4 +1,4 @@ -/* wrap.c -- txtfmt - text formatting */ +/* text/txtfmt/wrap.c -- text formatting */ #include <stdlib.h> #include <string.h> diff --git a/libraries/utils/array/delelem.c b/libraries/utils/array/delelem.c index 35493cc9..5f5d55ea 100644 --- a/libraries/utils/array/delelem.c +++ b/libraries/utils/array/delelem.c @@ -1,3 +1,4 @@ +/* utils/array/delelem.c */ #include <stdlib.h> #include <string.h> diff --git a/libraries/utils/array/delelems.c b/libraries/utils/array/delelems.c index aac00640..2b3ad7fc 100644 --- a/libraries/utils/array/delelems.c +++ b/libraries/utils/array/delelems.c @@ -1,3 +1,4 @@ +/* utils/array/delelems.c */ #include <stdlib.h> #include <string.h> diff --git a/libraries/utils/array/grow.c b/libraries/utils/array/grow.c index 42952902..5dbb51a6 100644 --- a/libraries/utils/array/grow.c +++ b/libraries/utils/array/grow.c @@ -1,4 +1,4 @@ -/* grow.c -- grow an array */ +/* utils/array/grow.c -- grow an array */ #include <assert.h> #include <stdlib.h> diff --git a/libraries/utils/array/shrink.c b/libraries/utils/array/shrink.c index 0b8de40d..98828cd3 100644 --- a/libraries/utils/array/shrink.c +++ b/libraries/utils/array/shrink.c @@ -1,4 +1,4 @@ -/* shrink.c -- shrink wrap an array */ +/* utils/array/shrink.c -- shrink wrap an array */ #include <assert.h> #include <stdlib.h> diff --git a/libraries/utils/array/squeeze.c b/libraries/utils/array/squeeze.c index eef61de2..d4a71937 100644 --- a/libraries/utils/array/squeeze.c +++ b/libraries/utils/array/squeeze.c @@ -1,3 +1,4 @@ +/* utils/array/squeeze.c */ #include <assert.h> #include <string.h> diff --git a/libraries/utils/array/stretch.c b/libraries/utils/array/stretch.c index b1064005..eac7377a 100644 --- a/libraries/utils/array/stretch.c +++ b/libraries/utils/array/stretch.c @@ -1,3 +1,4 @@ +/* utils/array/stretch.c */ #include <assert.h> #include <string.h> diff --git a/libraries/utils/array/test/array-test.c b/libraries/utils/array/test/array-test.c index 537ee49f..c9e998fd 100644 --- a/libraries/utils/array/test/array-test.c +++ b/libraries/utils/array/test/array-test.c @@ -1,3 +1,4 @@ +/* utils/array/test/array-test.c */ #include <stdio.h> #include <stdlib.h> diff --git a/libraries/utils/barith/barith.c b/libraries/utils/barith/barith.c index 07e95d44..711a3a17 100644 --- a/libraries/utils/barith/barith.c +++ b/libraries/utils/barith/barith.c @@ -1,4 +1,4 @@ -/* barith.c -- define all of barith */ +/* utils/barith/barith.c -- define all of barith */ #define BARITH_INLINE diff --git a/libraries/utils/bsearch/bsearch-impl.h b/libraries/utils/bsearch/bsearch-impl.h index 914142e3..6ee9ec54 100644 --- a/libraries/utils/bsearch/bsearch-impl.h +++ b/libraries/utils/bsearch/bsearch-impl.h @@ -1,4 +1,4 @@ -/* bsearch-impl.h -- binary searching arrays */ +/* utils/bsearch/bsearch-impl.h -- binary searching arrays */ #if !defined(TYPE) || !defined(NAME) #error TYPE and NAME must be defined. diff --git a/libraries/utils/bsearch/bsearch-int.c b/libraries/utils/bsearch/bsearch-int.c index eb0549ce..93b3fc07 100644 --- a/libraries/utils/bsearch/bsearch-int.c +++ b/libraries/utils/bsearch/bsearch-int.c @@ -1,4 +1,4 @@ -/* bsearch-int.c -- binary searching arrays */ +/* utils/bsearch/bsearch-int.c -- binary searching arrays */ #define TYPE int #define NAME bsearch_int diff --git a/libraries/utils/bsearch/bsearch-short.c b/libraries/utils/bsearch/bsearch-short.c index 528ca7d7..aa7c22c4 100644 --- a/libraries/utils/bsearch/bsearch-short.c +++ b/libraries/utils/bsearch/bsearch-short.c @@ -1,4 +1,4 @@ -/* bsearch-short.c -- binary searching arrays */ +/* utils/bsearch/bsearch-short.c -- binary searching arrays */ #define TYPE short #define NAME bsearch_short diff --git a/libraries/utils/bsearch/bsearch-uint.c b/libraries/utils/bsearch/bsearch-uint.c index 725cc581..1b73bd9b 100644 --- a/libraries/utils/bsearch/bsearch-uint.c +++ b/libraries/utils/bsearch/bsearch-uint.c @@ -1,4 +1,4 @@ -/* bsearch-uint.c -- binary searching arrays */ +/* utils/bsearch/bsearch-uint.c -- binary searching arrays */ #define TYPE unsigned int #define NAME bsearch_uint diff --git a/libraries/utils/bsearch/bsearch-ushort.c b/libraries/utils/bsearch/bsearch-ushort.c index 0cdc9304..eb30e4b8 100644 --- a/libraries/utils/bsearch/bsearch-ushort.c +++ b/libraries/utils/bsearch/bsearch-ushort.c @@ -1,4 +1,4 @@ -/* bsearch-ushort.c -- binary searching arrays */ +/* utils/bsearch/bsearch-ushort.c -- binary searching arrays */ #define TYPE unsigned short #define NAME bsearch_ushort diff --git a/libraries/utils/bsearch/test/bsearch-test.c b/libraries/utils/bsearch/test/bsearch-test.c index f2798af9..cbe6b7db 100644 --- a/libraries/utils/bsearch/test/bsearch-test.c +++ b/libraries/utils/bsearch/test/bsearch-test.c @@ -1,3 +1,4 @@ +/* utils/bsearch/test/bsearch-test.c */ #include <limits.h> #include <stdio.h> diff --git a/libraries/utils/bytesex/rev-l-block.c b/libraries/utils/bytesex/rev-l-block.c index dd652f0d..887fa50f 100644 --- a/libraries/utils/bytesex/rev-l-block.c +++ b/libraries/utils/bytesex/rev-l-block.c @@ -1,4 +1,4 @@ -/* rev-l-block.c -- reversing bytesex */ +/* utils/bytesex/rev-l-block.c -- reversing bytesex */ #include <assert.h> diff --git a/libraries/utils/bytesex/rev-l-m.c b/libraries/utils/bytesex/rev-l-m.c index 3c9befb4..822fc495 100644 --- a/libraries/utils/bytesex/rev-l-m.c +++ b/libraries/utils/bytesex/rev-l-m.c @@ -1,4 +1,4 @@ -/* rev-l-m.c -- reversing bytesex */ +/* utils/bytesex/rev-l-m.c -- reversing bytesex */ #include "utils/bytesex.h" diff --git a/libraries/utils/bytesex/rev-l.c b/libraries/utils/bytesex/rev-l.c index 9124cf4e..39367bc6 100644 --- a/libraries/utils/bytesex/rev-l.c +++ b/libraries/utils/bytesex/rev-l.c @@ -1,4 +1,4 @@ -/* rev-l.c -- reversing bytesex */ +/* utils/bytesex/rev-l.c -- reversing bytesex */ #include "utils/bytesex.h" diff --git a/libraries/utils/bytesex/rev-s-block.c b/libraries/utils/bytesex/rev-s-block.c index 118fa580..cd24f235 100644 --- a/libraries/utils/bytesex/rev-s-block.c +++ b/libraries/utils/bytesex/rev-s-block.c @@ -1,4 +1,4 @@ -/* rev-s-block.c -- reversing bytesex */ +/* utils/bytesex/rev-s-block.c -- reversing bytesex */ #include <assert.h> diff --git a/libraries/utils/bytesex/rev-s-m.c b/libraries/utils/bytesex/rev-s-m.c index d5bf7e46..8b181e3d 100644 --- a/libraries/utils/bytesex/rev-s-m.c +++ b/libraries/utils/bytesex/rev-s-m.c @@ -1,4 +1,4 @@ -/* rev-s-m.c -- reversing bytesex */ +/* utils/bytesex/rev-s-m.c -- reversing bytesex */ #include "utils/bytesex.h" diff --git a/libraries/utils/bytesex/rev-s-pair-m.c b/libraries/utils/bytesex/rev-s-pair-m.c index c4f0327d..23104771 100644 --- a/libraries/utils/bytesex/rev-s-pair-m.c +++ b/libraries/utils/bytesex/rev-s-pair-m.c @@ -1,4 +1,4 @@ -/* rev-s-pair-m.c -- reversing bytesex */ +/* utils/bytesex/rev-s-pair-m.c -- reversing bytesex */ #include "utils/bytesex.h" diff --git a/libraries/utils/bytesex/rev-s-pair.c b/libraries/utils/bytesex/rev-s-pair.c index b251a683..c98cab4d 100644 --- a/libraries/utils/bytesex/rev-s-pair.c +++ b/libraries/utils/bytesex/rev-s-pair.c @@ -1,4 +1,4 @@ -/* rev-s-pair.c -- reversing bytesex */ +/* utils/bytesex/rev-s-pair.c -- reversing bytesex */ #include "utils/bytesex.h" diff --git a/libraries/utils/bytesex/rev-s.c b/libraries/utils/bytesex/rev-s.c index 392e910f..646e0da0 100644 --- a/libraries/utils/bytesex/rev-s.c +++ b/libraries/utils/bytesex/rev-s.c @@ -1,4 +1,4 @@ -/* rev-s.c -- reversing bytesex */ +/* utils/bytesex/rev-s.c -- reversing bytesex */ #include "utils/bytesex.h" diff --git a/libraries/utils/bytesex/util.h b/libraries/utils/bytesex/util.h index f41523e9..c6b153a3 100644 --- a/libraries/utils/bytesex/util.h +++ b/libraries/utils/bytesex/util.h @@ -1,4 +1,4 @@ -/* util.h -- utils for reversing bytesex */ +/* utils/bytesex/util.h -- utils for reversing bytesex */ #include <limits.h> diff --git a/libraries/utils/fxp/smull-fxp16.c b/libraries/utils/fxp/smull-fxp16.c index 6c4509ab..44ecd19d 100644 --- a/libraries/utils/fxp/smull-fxp16.c +++ b/libraries/utils/fxp/smull-fxp16.c @@ -1,4 +1,4 @@ -/* smull-fxp16.c -- fixed point helpers */ +/* utils/fxp/smull-fxp16.c -- fixed point helpers */ #include "utils/fxp.h" diff --git a/libraries/utils/fxp/umull-fxp16.c b/libraries/utils/fxp/umull-fxp16.c index e15a846c..30377304 100644 --- a/libraries/utils/fxp/umull-fxp16.c +++ b/libraries/utils/fxp/umull-fxp16.c @@ -1,4 +1,4 @@ -/* umull-fxp16.c -- fixed point helpers */ +/* utils/fxp/umull-fxp16.c -- fixed point helpers */ #include "utils/fxp.h" diff --git a/libraries/utils/maths/degs-to-rads.c b/libraries/utils/maths/degs-to-rads.c index 891ffe26..1e1f0f92 100644 --- a/libraries/utils/maths/degs-to-rads.c +++ b/libraries/utils/maths/degs-to-rads.c @@ -1,4 +1,4 @@ -/* degs-to-rads.c -- degrees to radians */ +/* utils/maths/degs-to-rads.c -- degrees to radians */ #include "utils/maths.h" diff --git a/libraries/utils/maths/gcd.c b/libraries/utils/maths/gcd.c index 9216dbe7..f4e2d531 100644 --- a/libraries/utils/maths/gcd.c +++ b/libraries/utils/maths/gcd.c @@ -1,4 +1,4 @@ -/* gcd.c -- greatest common divisor */ +/* utils/maths/gcd.c -- greatest common divisor */ #include "utils/maths.h" diff --git a/libraries/utils/pack/pack.c b/libraries/utils/pack/pack.c index aacce624..26dd7bbf 100644 --- a/libraries/utils/pack/pack.c +++ b/libraries/utils/pack/pack.c @@ -1,4 +1,4 @@ -/* pack.c -- structure packing */ +/* utils/pack/pack.c -- structure packing */ #include <assert.h> #include <ctype.h> diff --git a/libraries/utils/pack/test/pack-test.c b/libraries/utils/pack/test/pack-test.c index bed208b3..64d7e04a 100644 --- a/libraries/utils/pack/test/pack-test.c +++ b/libraries/utils/pack/test/pack-test.c @@ -1,4 +1,4 @@ -/* pack-test.c -- tests for pack/unpack */ +/* utils/pack/test/pack-test.c -- tests for pack/unpack */ #include <limits.h> #include <stdio.h> diff --git a/libraries/utils/pack/unpack.c b/libraries/utils/pack/unpack.c index 01f2911e..3ffec99a 100644 --- a/libraries/utils/pack/unpack.c +++ b/libraries/utils/pack/unpack.c @@ -1,4 +1,4 @@ -/* unpack.c -- structure unpacking */ +/* utils/pack/unpack.c -- structure unpacking */ #include <assert.h> #include <ctype.h> diff --git a/libraries/utils/primes/primes.c b/libraries/utils/primes/primes.c index b39fcd64..9d25d619 100644 --- a/libraries/utils/primes/primes.c +++ b/libraries/utils/primes/primes.c @@ -1,4 +1,4 @@ -/* primes.c -- cache of prime numbers */ +/* utils/primes/primes.c -- cache of prime numbers */ #include "base/utils.h" diff --git a/libraries/wuss/backdrop.c b/libraries/wuss/backdrop.c index 7da138f8..ae691866 100644 --- a/libraries/wuss/backdrop.c +++ b/libraries/wuss/backdrop.c @@ -1,4 +1,4 @@ -/* backdrop.c -- wuss - shared backdrop validation and fill */ +/* wuss/backdrop.c -- shared backdrop validation and fill */ #include "framebuf/screen.h" diff --git a/libraries/wuss/create.c b/libraries/wuss/create.c index f1282b8e..ad11546d 100644 --- a/libraries/wuss/create.c +++ b/libraries/wuss/create.c @@ -1,4 +1,4 @@ -/* create.c -- wuss - minimal window manager */ +/* wuss/create.c -- wuss - minimal window manager */ #include <assert.h> #include <stdlib.h> diff --git a/libraries/wuss/destroy.c b/libraries/wuss/destroy.c index 9ea33274..dd974e56 100644 --- a/libraries/wuss/destroy.c +++ b/libraries/wuss/destroy.c @@ -1,4 +1,4 @@ -/* destroy.c -- wuss - minimal window manager */ +/* wuss/destroy.c -- wuss - minimal window manager */ #include <stdlib.h> diff --git a/libraries/wuss/furniture.h b/libraries/wuss/furniture.h index 1a90f180..22744f08 100644 --- a/libraries/wuss/furniture.h +++ b/libraries/wuss/furniture.h @@ -1,4 +1,4 @@ -/* furniture.h -- wuss - minimal window manager */ +/* wuss/furniture.h -- wuss - minimal window manager */ #ifndef WUSS_FURNITURE_IMPL_H #define WUSS_FURNITURE_IMPL_H diff --git a/libraries/wuss/furniture/back-box.c b/libraries/wuss/furniture/back-box.c index deab1238..c2d92f25 100644 --- a/libraries/wuss/furniture/back-box.c +++ b/libraries/wuss/furniture/back-box.c @@ -1,4 +1,4 @@ -/* back-box.c -- wuss - minimal window manager */ +/* wuss/furniture/back-box.c -- wuss - minimal window manager */ #include "../impl.h" diff --git a/libraries/wuss/furniture/close-box.c b/libraries/wuss/furniture/close-box.c index 424af7c4..3972fd02 100644 --- a/libraries/wuss/furniture/close-box.c +++ b/libraries/wuss/furniture/close-box.c @@ -1,4 +1,4 @@ -/* close-box.c -- wuss - minimal window manager */ +/* wuss/furniture/close-box.c -- wuss - minimal window manager */ #include "../impl.h" diff --git a/libraries/wuss/furniture/content-box.c b/libraries/wuss/furniture/content-box.c index 72f379eb..14249a4f 100644 --- a/libraries/wuss/furniture/content-box.c +++ b/libraries/wuss/furniture/content-box.c @@ -1,4 +1,4 @@ -/* content-box.c -- wuss - minimal window manager */ +/* wuss/furniture/content-box.c -- wuss - minimal window manager */ #include "../impl.h" diff --git a/libraries/wuss/furniture/drag-resize.c b/libraries/wuss/furniture/drag-resize.c index 9a92a3a3..3e0d02c6 100644 --- a/libraries/wuss/furniture/drag-resize.c +++ b/libraries/wuss/furniture/drag-resize.c @@ -1,4 +1,4 @@ -/* drag-resize.c -- wuss - minimal window manager */ +/* wuss/furniture/drag-resize.c -- wuss - minimal window manager */ #include "base/utils.h" diff --git a/libraries/wuss/furniture/draw.c b/libraries/wuss/furniture/draw.c index a30cb44d..98a7c657 100644 --- a/libraries/wuss/furniture/draw.c +++ b/libraries/wuss/furniture/draw.c @@ -1,4 +1,4 @@ -/* draw.c -- wuss - minimal window manager */ +/* wuss/furniture/draw.c -- wuss - minimal window manager */ #include <string.h> diff --git a/libraries/wuss/furniture/hit-test.c b/libraries/wuss/furniture/hit-test.c index d6e0705b..15eb1972 100644 --- a/libraries/wuss/furniture/hit-test.c +++ b/libraries/wuss/furniture/hit-test.c @@ -1,4 +1,4 @@ -/* hit-test.c -- wuss - minimal window manager */ +/* wuss/furniture/hit-test.c -- wuss - minimal window manager */ #include "../impl.h" diff --git a/libraries/wuss/furniture/hscroll-box.c b/libraries/wuss/furniture/hscroll-box.c index ce452db0..b3b2cf81 100644 --- a/libraries/wuss/furniture/hscroll-box.c +++ b/libraries/wuss/furniture/hscroll-box.c @@ -1,4 +1,4 @@ -/* hscroll-box.c -- wuss - minimal window manager */ +/* wuss/furniture/hscroll-box.c -- wuss - minimal window manager */ #include "../impl.h" diff --git a/libraries/wuss/furniture/invalidate.c b/libraries/wuss/furniture/invalidate.c index dc2e3ba7..0b7d2103 100644 --- a/libraries/wuss/furniture/invalidate.c +++ b/libraries/wuss/furniture/invalidate.c @@ -1,4 +1,4 @@ -/* invalidate.c -- wuss - minimal window manager */ +/* wuss/furniture/invalidate.c -- wuss - minimal window manager */ #include "../impl.h" diff --git a/libraries/wuss/furniture/resize-box.c b/libraries/wuss/furniture/resize-box.c index 143ab98c..789a2e67 100644 --- a/libraries/wuss/furniture/resize-box.c +++ b/libraries/wuss/furniture/resize-box.c @@ -1,4 +1,4 @@ -/* resize-box.c -- wuss - minimal window manager */ +/* wuss/furniture/resize-box.c -- wuss - minimal window manager */ #include "../impl.h" diff --git a/libraries/wuss/furniture/scroll-action.c b/libraries/wuss/furniture/scroll-action.c index a8965eb3..8b84d759 100644 --- a/libraries/wuss/furniture/scroll-action.c +++ b/libraries/wuss/furniture/scroll-action.c @@ -1,4 +1,4 @@ -/* scroll-action.c -- wuss - minimal window manager */ +/* wuss/furniture/scroll-action.c -- wuss - minimal window manager */ #include "../impl.h" diff --git a/libraries/wuss/furniture/titlebar-box.c b/libraries/wuss/furniture/titlebar-box.c index 52a9dade..0d1faa49 100644 --- a/libraries/wuss/furniture/titlebar-box.c +++ b/libraries/wuss/furniture/titlebar-box.c @@ -1,4 +1,4 @@ -/* titlebar-box.c -- wuss - minimal window manager */ +/* wuss/furniture/titlebar-box.c -- wuss - minimal window manager */ #include "../impl.h" diff --git a/libraries/wuss/furniture/toggle-action.c b/libraries/wuss/furniture/toggle-action.c index 6a330d6f..7e2724b6 100644 --- a/libraries/wuss/furniture/toggle-action.c +++ b/libraries/wuss/furniture/toggle-action.c @@ -1,4 +1,4 @@ -/* toggle-action.c -- wuss - minimal window manager */ +/* wuss/furniture/toggle-action.c -- wuss - minimal window manager */ #include "base/utils.h" diff --git a/libraries/wuss/furniture/toggle-box.c b/libraries/wuss/furniture/toggle-box.c index 4d9cf3f4..2a77a588 100644 --- a/libraries/wuss/furniture/toggle-box.c +++ b/libraries/wuss/furniture/toggle-box.c @@ -1,4 +1,4 @@ -/* toggle-box.c -- wuss - minimal window manager */ +/* wuss/furniture/toggle-box.c -- wuss - minimal window manager */ #include "../impl.h" diff --git a/libraries/wuss/furniture/vscroll-box.c b/libraries/wuss/furniture/vscroll-box.c index f87eda4b..1d936b71 100644 --- a/libraries/wuss/furniture/vscroll-box.c +++ b/libraries/wuss/furniture/vscroll-box.c @@ -1,4 +1,4 @@ -/* vscroll-box.c -- wuss - minimal window manager */ +/* wuss/furniture/vscroll-box.c -- wuss - minimal window manager */ #include "../impl.h" diff --git a/libraries/wuss/get-font.c b/libraries/wuss/get-font.c index 206cd958..f54fabd4 100644 --- a/libraries/wuss/get-font.c +++ b/libraries/wuss/get-font.c @@ -1,4 +1,4 @@ -/* get-font.c -- wuss - minimal window manager */ +/* wuss/get-font.c -- wuss - minimal window manager */ #include <assert.h> diff --git a/libraries/wuss/get-pointer.c b/libraries/wuss/get-pointer.c index ee538d84..db0b6d45 100644 --- a/libraries/wuss/get-pointer.c +++ b/libraries/wuss/get-pointer.c @@ -1,4 +1,4 @@ -/* get-pointer.c -- wuss - minimal window manager */ +/* wuss/get-pointer.c -- wuss - minimal window manager */ #include <assert.h> diff --git a/libraries/wuss/icon.h b/libraries/wuss/icon.h index 7794f4e7..bc5dc48d 100644 --- a/libraries/wuss/icon.h +++ b/libraries/wuss/icon.h @@ -1,4 +1,4 @@ -/* icon.h -- wuss - work-area icons, internal */ +/* wuss/icon.h -- work-area icons, internal */ #ifndef WUSS_ICON_IMPL_H #define WUSS_ICON_IMPL_H diff --git a/libraries/wuss/icon/create-array.c b/libraries/wuss/icon/create-array.c index 5da88935..07bdef4d 100644 --- a/libraries/wuss/icon/create-array.c +++ b/libraries/wuss/icon/create-array.c @@ -1,4 +1,4 @@ -/* create-array.c -- wuss - create several work-area icons at once */ +/* wuss/icon/create-array.c -- create several work-area icons at once */ #include <assert.h> #include <stddef.h> diff --git a/libraries/wuss/icon/create.c b/libraries/wuss/icon/create.c index d29f815d..a9d481be 100644 --- a/libraries/wuss/icon/create.c +++ b/libraries/wuss/icon/create.c @@ -1,4 +1,4 @@ -/* create.c -- wuss - create a work-area icon */ +/* wuss/icon/create.c -- create a work-area icon */ #include <assert.h> #include <stdlib.h> diff --git a/libraries/wuss/icon/delete.c b/libraries/wuss/icon/delete.c index 35d495de..097690cd 100644 --- a/libraries/wuss/icon/delete.c +++ b/libraries/wuss/icon/delete.c @@ -1,4 +1,4 @@ -/* delete.c -- wuss - destroy a work-area icon */ +/* wuss/icon/delete.c -- destroy a work-area icon */ #include <stdlib.h> diff --git a/libraries/wuss/icon/draw.c b/libraries/wuss/icon/draw.c index f60220cc..5f15307e 100644 --- a/libraries/wuss/icon/draw.c +++ b/libraries/wuss/icon/draw.c @@ -1,4 +1,4 @@ -/* draw.c -- wuss - draw a work-area icon */ +/* wuss/icon/draw.c -- draw a work-area icon */ #include <string.h> diff --git a/libraries/wuss/icon/free.c b/libraries/wuss/icon/free.c index 2630b266..d8a08432 100644 --- a/libraries/wuss/icon/free.c +++ b/libraries/wuss/icon/free.c @@ -1,4 +1,4 @@ -/* free.c -- wuss - free a window's whole icon store */ +/* wuss/icon/free.c -- free a window's whole icon store */ #include <stdlib.h> diff --git a/libraries/wuss/icon/get-bbox.c b/libraries/wuss/icon/get-bbox.c index 55bdbd8d..700f96d9 100644 --- a/libraries/wuss/icon/get-bbox.c +++ b/libraries/wuss/icon/get-bbox.c @@ -1,4 +1,4 @@ -/* get-bbox.c -- wuss - read a work-area icon's bounding box */ +/* wuss/icon/get-bbox.c -- read a work-area icon's bounding box */ #include "../impl.h" diff --git a/libraries/wuss/icon/get-selected.c b/libraries/wuss/icon/get-selected.c index 6d8e06f0..cb2add43 100644 --- a/libraries/wuss/icon/get-selected.c +++ b/libraries/wuss/icon/get-selected.c @@ -1,4 +1,4 @@ -/* get-selected.c -- wuss - query a radio/option icon's latched state */ +/* wuss/icon/get-selected.c -- query a radio/option icon's latched state */ #include <assert.h> diff --git a/libraries/wuss/icon/get-text.c b/libraries/wuss/icon/get-text.c index 2978cdb7..783f685f 100644 --- a/libraries/wuss/icon/get-text.c +++ b/libraries/wuss/icon/get-text.c @@ -1,4 +1,4 @@ -/* get-text.c -- wuss - read a work-area icon's label */ +/* wuss/icon/get-text.c -- read a work-area icon's label */ #include "../impl.h" diff --git a/libraries/wuss/icon/get-type.c b/libraries/wuss/icon/get-type.c index 84b34091..2335286b 100644 --- a/libraries/wuss/icon/get-type.c +++ b/libraries/wuss/icon/get-type.c @@ -1,4 +1,4 @@ -/* get-type.c -- wuss - read a work-area icon's type */ +/* wuss/icon/get-type.c -- read a work-area icon's type */ #include "../impl.h" diff --git a/libraries/wuss/icon/get-window.c b/libraries/wuss/icon/get-window.c index 9921af6d..43fa0127 100644 --- a/libraries/wuss/icon/get-window.c +++ b/libraries/wuss/icon/get-window.c @@ -1,4 +1,4 @@ -/* get-window.c -- wuss - read a work-area icon's owning window */ +/* wuss/icon/get-window.c -- read a work-area icon's owning window */ #include "../impl.h" diff --git a/libraries/wuss/icon/hit-test.c b/libraries/wuss/icon/hit-test.c index 79fb799d..b4045fbb 100644 --- a/libraries/wuss/icon/hit-test.c +++ b/libraries/wuss/icon/hit-test.c @@ -1,4 +1,4 @@ -/* hit-test.c -- wuss - work-area icon hit testing */ +/* wuss/icon/hit-test.c -- work-area icon hit testing */ #include "geom/box.h" diff --git a/libraries/wuss/icon/invalidate.c b/libraries/wuss/icon/invalidate.c index 91c9e47f..d702fb36 100644 --- a/libraries/wuss/icon/invalidate.c +++ b/libraries/wuss/icon/invalidate.c @@ -1,4 +1,4 @@ -/* invalidate.c -- wuss - mark a work-area icon's bbox dirty */ +/* wuss/icon/invalidate.c -- mark a work-area icon's bbox dirty */ #include "../impl.h" diff --git a/libraries/wuss/icon/screen-box.c b/libraries/wuss/icon/screen-box.c index 59e4e4e0..f1abad26 100644 --- a/libraries/wuss/icon/screen-box.c +++ b/libraries/wuss/icon/screen-box.c @@ -1,4 +1,4 @@ -/* screen-box.c -- wuss - work-area icon bbox to screen space */ +/* wuss/icon/screen-box.c -- work-area icon bbox to screen space */ #include "geom/box.h" #include "geom/point.h" diff --git a/libraries/wuss/icon/set-hidden.c b/libraries/wuss/icon/set-hidden.c index c76e35a8..7adfee0d 100644 --- a/libraries/wuss/icon/set-hidden.c +++ b/libraries/wuss/icon/set-hidden.c @@ -1,4 +1,4 @@ -/* set-hidden.c -- wuss - show or hide a work-area icon */ +/* wuss/icon/set-hidden.c -- show or hide a work-area icon */ #include "../impl.h" diff --git a/libraries/wuss/icon/set-hover.c b/libraries/wuss/icon/set-hover.c index 6ae8d734..cb5deb8b 100644 --- a/libraries/wuss/icon/set-hover.c +++ b/libraries/wuss/icon/set-hover.c @@ -1,4 +1,4 @@ -/* set-hover.c -- wuss - track which work-area icon the pointer is over */ +/* wuss/icon/set-hover.c -- track which work-area icon the pointer is over */ #include <stddef.h> diff --git a/libraries/wuss/icon/set-selected.c b/libraries/wuss/icon/set-selected.c index 72ec17c4..ea6c3561 100644 --- a/libraries/wuss/icon/set-selected.c +++ b/libraries/wuss/icon/set-selected.c @@ -1,4 +1,4 @@ -/* set-selected.c -- wuss - latch a radio/option icon's selected state */ +/* wuss/icon/set-selected.c -- latch a radio/option icon's selected state */ #include <assert.h> diff --git a/libraries/wuss/icon/set-text.c b/libraries/wuss/icon/set-text.c index 3f67a6cf..2c5d517a 100644 --- a/libraries/wuss/icon/set-text.c +++ b/libraries/wuss/icon/set-text.c @@ -1,4 +1,4 @@ -/* set-text.c -- wuss - replace a work-area icon's label */ +/* wuss/icon/set-text.c -- replace a work-area icon's label */ #include <stdlib.h> #include <string.h> diff --git a/libraries/wuss/idle.c b/libraries/wuss/idle.c index c8de3625..e0be5fb6 100644 --- a/libraries/wuss/idle.c +++ b/libraries/wuss/idle.c @@ -1,4 +1,4 @@ -/* idle.c -- wuss - minimal window manager */ +/* wuss/idle.c -- wuss - minimal window manager */ #include "impl.h" diff --git a/libraries/wuss/impl.h b/libraries/wuss/impl.h index 97eecd38..7046e5c5 100644 --- a/libraries/wuss/impl.h +++ b/libraries/wuss/impl.h @@ -1,4 +1,4 @@ -/* impl.h -- wuss - minimal window manager */ +/* wuss/impl.h -- wuss - minimal window manager */ #ifndef IMPL_H #define IMPL_H diff --git a/libraries/wuss/invalidate.c b/libraries/wuss/invalidate.c index 1bca4190..1b027431 100644 --- a/libraries/wuss/invalidate.c +++ b/libraries/wuss/invalidate.c @@ -1,4 +1,4 @@ -/* invalidate.c -- wuss - minimal window manager */ +/* wuss/invalidate.c -- wuss - minimal window manager */ #include <assert.h> diff --git a/libraries/wuss/menu.h b/libraries/wuss/menu.h index 4012f6d5..9df3ca27 100644 --- a/libraries/wuss/menu.h +++ b/libraries/wuss/menu.h @@ -1,4 +1,4 @@ -/* menu.h -- wuss pop-up menu helper, internal */ +/* wuss/menu.h -- wuss pop-up menu helper, internal */ #ifndef WUSS_MENU_IMPL_H #define WUSS_MENU_IMPL_H diff --git a/libraries/wuss/menu/create-from-desc.c b/libraries/wuss/menu/create-from-desc.c index 94ed53a8..1d8c16ab 100644 --- a/libraries/wuss/menu/create-from-desc.c +++ b/libraries/wuss/menu/create-from-desc.c @@ -1,4 +1,4 @@ -/* create-from-desc.c -- wuss - build a menu tree from a descriptor string */ +/* wuss/menu/create-from-desc.c -- build a menu tree from a descriptor string */ /* The token machine (Token / Parser / getopt / getname / getnext / isdelim) * and the explicit menu stack are ported from PrivateEye's diff --git a/libraries/wuss/menu/menu.c b/libraries/wuss/menu/menu.c index a55ce443..15cdaa8e 100644 --- a/libraries/wuss/menu/menu.c +++ b/libraries/wuss/menu/menu.c @@ -1,4 +1,4 @@ -/* menu.c -- wuss pop-up menu helper */ +/* wuss/menu/menu.c -- wuss pop-up menu helper */ #include <assert.h> #include <limits.h> diff --git a/libraries/wuss/mouse-click.c b/libraries/wuss/mouse-click.c index d513a7f5..f6f961fb 100644 --- a/libraries/wuss/mouse-click.c +++ b/libraries/wuss/mouse-click.c @@ -1,4 +1,4 @@ -/* mouse-click.c -- wuss - minimal window manager */ +/* wuss/mouse-click.c -- wuss - minimal window manager */ #include "impl.h" diff --git a/libraries/wuss/mouse-move.c b/libraries/wuss/mouse-move.c index 99f0e724..d1546b65 100644 --- a/libraries/wuss/mouse-move.c +++ b/libraries/wuss/mouse-move.c @@ -1,4 +1,4 @@ -/* mouse-move.c -- wuss - minimal window manager */ +/* wuss/mouse-move.c -- wuss - minimal window manager */ #include "impl.h" diff --git a/libraries/wuss/redraw.c b/libraries/wuss/redraw.c index aa6463bf..52ad93f0 100644 --- a/libraries/wuss/redraw.c +++ b/libraries/wuss/redraw.c @@ -1,4 +1,4 @@ -/* redraw.c -- wuss - minimal window manager */ +/* wuss/redraw.c -- wuss - minimal window manager */ #include "impl.h" diff --git a/libraries/wuss/scroll-step.c b/libraries/wuss/scroll-step.c index c69766e8..ab01d5f3 100644 --- a/libraries/wuss/scroll-step.c +++ b/libraries/wuss/scroll-step.c @@ -1,4 +1,4 @@ -/* scroll-step.c -- wuss - clamp and apply a scroll offset */ +/* wuss/scroll-step.c -- clamp and apply a scroll offset */ #include "impl.h" diff --git a/libraries/wuss/scroll.c b/libraries/wuss/scroll.c index bcabbb43..1eb54af5 100644 --- a/libraries/wuss/scroll.c +++ b/libraries/wuss/scroll.c @@ -1,4 +1,4 @@ -/* scroll.c -- wuss - minimal window manager */ +/* wuss/scroll.c -- wuss - minimal window manager */ #include "impl.h" diff --git a/libraries/wuss/task/start.c b/libraries/wuss/task/start.c index 65eb812b..f9736b4a 100644 --- a/libraries/wuss/task/start.c +++ b/libraries/wuss/task/start.c @@ -1,4 +1,4 @@ -/* start.c -- wuss - minimal window manager */ +/* wuss/task/start.c -- wuss - minimal window manager */ #include <stddef.h> diff --git a/libraries/wuss/task/stop.c b/libraries/wuss/task/stop.c index b8cbd80b..deffd847 100644 --- a/libraries/wuss/task/stop.c +++ b/libraries/wuss/task/stop.c @@ -1,4 +1,4 @@ -/* stop.c -- wuss - minimal window manager */ +/* wuss/task/stop.c -- wuss - minimal window manager */ #include "../impl.h" diff --git a/libraries/wuss/test/tasks/ball.c b/libraries/wuss/test/tasks/ball.c index 7c77ab67..e1f34970 100644 --- a/libraries/wuss/test/tasks/ball.c +++ b/libraries/wuss/test/tasks/ball.c @@ -1,4 +1,4 @@ -/* ball.c -- wuss test - bouncing ball task */ +/* wuss/test/tasks/ball.c -- bouncing ball task */ #ifdef USE_SDL diff --git a/libraries/wuss/test/tasks/ball.h b/libraries/wuss/test/tasks/ball.h index f42fb579..ac87f603 100644 --- a/libraries/wuss/test/tasks/ball.h +++ b/libraries/wuss/test/tasks/ball.h @@ -1,4 +1,4 @@ -/* ball.h -- wuss test - bouncing ball task */ +/* wuss/test/tasks/ball.h -- bouncing ball task */ #ifndef TASKS_BALL_H #define TASKS_BALL_H diff --git a/libraries/wuss/test/tasks/blank.c b/libraries/wuss/test/tasks/blank.c index b54fa4c3..b97f67aa 100644 --- a/libraries/wuss/test/tasks/blank.c +++ b/libraries/wuss/test/tasks/blank.c @@ -1,4 +1,4 @@ -/* blank.c -- wuss test - colour-cycling task */ +/* wuss/test/tasks/blank.c -- colour-cycling task */ #ifdef USE_SDL diff --git a/libraries/wuss/test/tasks/blank.h b/libraries/wuss/test/tasks/blank.h index b1ffd74f..44739617 100644 --- a/libraries/wuss/test/tasks/blank.h +++ b/libraries/wuss/test/tasks/blank.h @@ -1,4 +1,4 @@ -/* blank.h -- wuss test - colour-cycling task */ +/* wuss/test/tasks/blank.h -- colour-cycling task */ #ifndef TASKS_BLANK_H #define TASKS_BLANK_H diff --git a/libraries/wuss/test/tasks/chars.c b/libraries/wuss/test/tasks/chars.c index 9a4b155b..ec530665 100644 --- a/libraries/wuss/test/tasks/chars.c +++ b/libraries/wuss/test/tasks/chars.c @@ -1,4 +1,4 @@ -/* chars.c -- wuss test - system font glyph grid task */ +/* wuss/test/tasks/chars.c -- system font glyph grid task */ #ifdef USE_SDL diff --git a/libraries/wuss/test/tasks/chars.h b/libraries/wuss/test/tasks/chars.h index 5df4199c..277b6691 100644 --- a/libraries/wuss/test/tasks/chars.h +++ b/libraries/wuss/test/tasks/chars.h @@ -1,4 +1,4 @@ -/* chars.h -- wuss test - system font glyph grid task */ +/* wuss/test/tasks/chars.h -- system font glyph grid task */ #ifndef TASKS_CHARS_H #define TASKS_CHARS_H diff --git a/libraries/wuss/test/tasks/checker.c b/libraries/wuss/test/tasks/checker.c index 00d89499..04f2bfda 100644 --- a/libraries/wuss/test/tasks/checker.c +++ b/libraries/wuss/test/tasks/checker.c @@ -1,4 +1,4 @@ -/* checker.c -- wuss test - checkerboard task */ +/* wuss/test/tasks/checker.c -- checkerboard task */ #ifdef USE_SDL diff --git a/libraries/wuss/test/tasks/checker.h b/libraries/wuss/test/tasks/checker.h index ca3983ce..07937138 100644 --- a/libraries/wuss/test/tasks/checker.h +++ b/libraries/wuss/test/tasks/checker.h @@ -1,4 +1,4 @@ -/* checker.h -- wuss test - checkerboard task */ +/* wuss/test/tasks/checker.h -- checkerboard task */ #ifndef TASKS_CHECKER_H #define TASKS_CHECKER_H diff --git a/libraries/wuss/test/tasks/curve.c b/libraries/wuss/test/tasks/curve.c index 74248e3c..ce17708f 100644 --- a/libraries/wuss/test/tasks/curve.c +++ b/libraries/wuss/test/tasks/curve.c @@ -1,4 +1,4 @@ -/* curve.c -- wuss test - draggable Bezier curve task */ +/* wuss/test/tasks/curve.c -- draggable Bezier curve task */ #ifdef USE_SDL diff --git a/libraries/wuss/test/tasks/curve.h b/libraries/wuss/test/tasks/curve.h index de7a2e80..3d3ad505 100644 --- a/libraries/wuss/test/tasks/curve.h +++ b/libraries/wuss/test/tasks/curve.h @@ -1,4 +1,4 @@ -/* curve.h -- wuss test - draggable Bezier curve task */ +/* wuss/test/tasks/curve.h -- draggable Bezier curve task */ #ifndef TASKS_CURVE_H #define TASKS_CURVE_H diff --git a/libraries/wuss/test/tasks/gradient.c b/libraries/wuss/test/tasks/gradient.c index 6f2d8e63..e536128b 100644 --- a/libraries/wuss/test/tasks/gradient.c +++ b/libraries/wuss/test/tasks/gradient.c @@ -1,4 +1,4 @@ -/* gradient.c -- wuss test - gradient fill task */ +/* wuss/test/tasks/gradient.c -- gradient fill task */ #ifdef USE_SDL diff --git a/libraries/wuss/test/tasks/gradient.h b/libraries/wuss/test/tasks/gradient.h index 8f720073..9f475596 100644 --- a/libraries/wuss/test/tasks/gradient.h +++ b/libraries/wuss/test/tasks/gradient.h @@ -1,4 +1,4 @@ -/* gradient.h -- wuss test - gradient fill task */ +/* wuss/test/tasks/gradient.h -- gradient fill task */ #ifndef TASKS_GRADIENT_H #define TASKS_GRADIENT_H diff --git a/libraries/wuss/test/tasks/icons.c b/libraries/wuss/test/tasks/icons.c index 7a1dc385..5b923656 100644 --- a/libraries/wuss/test/tasks/icons.c +++ b/libraries/wuss/test/tasks/icons.c @@ -1,4 +1,4 @@ -/* icons.c -- wuss test - work-area icons task */ +/* wuss/test/tasks/icons.c -- work-area icons task */ #ifdef USE_SDL #include "framebuf/palettes.h" diff --git a/libraries/wuss/test/tasks/icons.h b/libraries/wuss/test/tasks/icons.h index 6d91d956..98ec0744 100644 --- a/libraries/wuss/test/tasks/icons.h +++ b/libraries/wuss/test/tasks/icons.h @@ -1,4 +1,4 @@ -/* icons.h -- wuss test - work-area icons task */ +/* wuss/test/tasks/icons.h -- work-area icons task */ #ifndef TASKS_ICONS_H #define TASKS_ICONS_H diff --git a/libraries/wuss/test/tasks/image.c b/libraries/wuss/test/tasks/image.c index 3ef7f763..16e159b2 100644 --- a/libraries/wuss/test/tasks/image.c +++ b/libraries/wuss/test/tasks/image.c @@ -1,4 +1,4 @@ -/* image.c -- wuss test - static bitmap image task */ +/* wuss/test/tasks/image.c -- static bitmap image task */ #ifdef USE_SDL diff --git a/libraries/wuss/test/tasks/image.h b/libraries/wuss/test/tasks/image.h index f62eb928..7b9ff9ce 100644 --- a/libraries/wuss/test/tasks/image.h +++ b/libraries/wuss/test/tasks/image.h @@ -1,4 +1,4 @@ -/* image.h -- wuss test - static bitmap image task */ +/* wuss/test/tasks/image.h -- static bitmap image task */ #ifndef TASKS_IMAGE_H #define TASKS_IMAGE_H diff --git a/libraries/wuss/test/tasks/launcher.c b/libraries/wuss/test/tasks/launcher.c index 5cf882d9..db967404 100644 --- a/libraries/wuss/test/tasks/launcher.c +++ b/libraries/wuss/test/tasks/launcher.c @@ -1,4 +1,4 @@ -/* launcher.c -- wuss test - clickable list of names that spawn other tasks */ +/* wuss/test/tasks/launcher.c -- clickable list of names that spawn other tasks */ #ifdef USE_SDL diff --git a/libraries/wuss/test/tasks/launcher.h b/libraries/wuss/test/tasks/launcher.h index 573f3e07..8dd2b5cf 100644 --- a/libraries/wuss/test/tasks/launcher.h +++ b/libraries/wuss/test/tasks/launcher.h @@ -1,4 +1,4 @@ -/* launcher.h -- wuss test - clickable list of names that spawn other tasks */ +/* wuss/test/tasks/launcher.h -- clickable list of names that spawn other tasks */ #ifndef TASKS_LAUNCHER_H #define TASKS_LAUNCHER_H diff --git a/libraries/wuss/test/tasks/palette.c b/libraries/wuss/test/tasks/palette.c index ac7da4e9..69cb38fa 100644 --- a/libraries/wuss/test/tasks/palette.c +++ b/libraries/wuss/test/tasks/palette.c @@ -1,4 +1,4 @@ -/* palette.c -- wuss test - desktop palette swatch grid task */ +/* wuss/test/tasks/palette.c -- desktop palette swatch grid task */ #ifdef USE_SDL diff --git a/libraries/wuss/test/tasks/palette.h b/libraries/wuss/test/tasks/palette.h index 8463df9a..ea8b3cc6 100644 --- a/libraries/wuss/test/tasks/palette.h +++ b/libraries/wuss/test/tasks/palette.h @@ -1,4 +1,4 @@ -/* palette.h -- wuss test - desktop palette swatch grid task */ +/* wuss/test/tasks/palette.h -- desktop palette swatch grid task */ #ifndef TASKS_PALETTE_H #define TASKS_PALETTE_H diff --git a/libraries/wuss/test/tasks/porter-duff.c b/libraries/wuss/test/tasks/porter-duff.c index 525db42f..d961388a 100644 --- a/libraries/wuss/test/tasks/porter-duff.c +++ b/libraries/wuss/test/tasks/porter-duff.c @@ -1,4 +1,4 @@ -/* porter-duff.c -- wuss test - animated Porter-Duff compositing task */ +/* wuss/test/tasks/porter-duff.c -- animated Porter-Duff compositing task */ #ifdef USE_SDL diff --git a/libraries/wuss/test/tasks/porter-duff.h b/libraries/wuss/test/tasks/porter-duff.h index 3a7f210e..361722f7 100644 --- a/libraries/wuss/test/tasks/porter-duff.h +++ b/libraries/wuss/test/tasks/porter-duff.h @@ -1,4 +1,4 @@ -/* porter-duff.h -- wuss test - animated Porter-Duff compositing task */ +/* wuss/test/tasks/porter-duff.h -- animated Porter-Duff compositing task */ #ifndef TASKS_PORTER_DUFF_H #define TASKS_PORTER_DUFF_H diff --git a/libraries/wuss/test/tasks/sofa.c b/libraries/wuss/test/tasks/sofa.c index ab8d951d..a8521b57 100644 --- a/libraries/wuss/test/tasks/sofa.c +++ b/libraries/wuss/test/tasks/sofa.c @@ -1,4 +1,4 @@ -/* sofa.c -- wuss test - rotating wireframe sofa and spaceship task */ +/* wuss/test/tasks/sofa.c -- rotating wireframe sofa and spaceship task */ #ifdef USE_SDL diff --git a/libraries/wuss/test/tasks/sofa.h b/libraries/wuss/test/tasks/sofa.h index 1a8c9ffd..35c28c76 100644 --- a/libraries/wuss/test/tasks/sofa.h +++ b/libraries/wuss/test/tasks/sofa.h @@ -1,4 +1,4 @@ -/* sofa.h -- wuss test - rotating wireframe sofa task */ +/* wuss/test/tasks/sofa.h -- rotating wireframe sofa task */ #ifndef TASKS_SOFA_H #define TASKS_SOFA_H diff --git a/libraries/wuss/test/tasks/text.c b/libraries/wuss/test/tasks/text.c index 80688ea1..c079d9fe 100644 --- a/libraries/wuss/test/tasks/text.c +++ b/libraries/wuss/test/tasks/text.c @@ -1,4 +1,4 @@ -/* text.c -- wuss test - static paragraph task */ +/* wuss/test/tasks/text.c -- static paragraph task */ #ifdef USE_SDL diff --git a/libraries/wuss/test/tasks/text.h b/libraries/wuss/test/tasks/text.h index d7a985af..2b94ae96 100644 --- a/libraries/wuss/test/tasks/text.h +++ b/libraries/wuss/test/tasks/text.h @@ -1,4 +1,4 @@ -/* text.h -- wuss test - static paragraph task */ +/* wuss/test/tasks/text.h -- static paragraph task */ #ifndef TASKS_TEXT_H #define TASKS_TEXT_H diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 46018d29..13824f17 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -1,4 +1,4 @@ -/* wuss-test.c -- wuss - minimal window manager */ +/* wuss/test/wuss-test.c -- wuss - minimal window manager */ #include <stdio.h> #include <stdlib.h> diff --git a/libraries/wuss/window/at.c b/libraries/wuss/window/at.c index 963f428c..bad019d1 100644 --- a/libraries/wuss/window/at.c +++ b/libraries/wuss/window/at.c @@ -1,4 +1,4 @@ -/* at.c -- wuss - minimal window manager */ +/* wuss/window/at.c -- wuss - minimal window manager */ #include "../impl.h" diff --git a/libraries/wuss/window/close.c b/libraries/wuss/window/close.c index e24e1379..91c32ede 100644 --- a/libraries/wuss/window/close.c +++ b/libraries/wuss/window/close.c @@ -1,4 +1,4 @@ -/* close.c -- wuss - minimal window manager */ +/* wuss/window/close.c -- wuss - minimal window manager */ #include <stdlib.h> diff --git a/libraries/wuss/window/create-placed.c b/libraries/wuss/window/create-placed.c index 22ee483a..2ff3e00c 100644 --- a/libraries/wuss/window/create-placed.c +++ b/libraries/wuss/window/create-placed.c @@ -1,4 +1,4 @@ -/* create-placed.c -- wuss - window creation with wuss-chosen position */ +/* wuss/window/create-placed.c -- window creation with wuss-chosen position */ #include <assert.h> diff --git a/libraries/wuss/window/create.c b/libraries/wuss/window/create.c index 5935d85e..1a5119e7 100644 --- a/libraries/wuss/window/create.c +++ b/libraries/wuss/window/create.c @@ -1,4 +1,4 @@ -/* create.c -- wuss - minimal window manager */ +/* wuss/window/create.c -- wuss - minimal window manager */ #include <assert.h> #include <stdlib.h> diff --git a/libraries/wuss/window/get-content-bounds.c b/libraries/wuss/window/get-content-bounds.c index be2187d7..9eda68e7 100644 --- a/libraries/wuss/window/get-content-bounds.c +++ b/libraries/wuss/window/get-content-bounds.c @@ -1,4 +1,4 @@ -/* get-content-bounds.c -- wuss - minimal window manager */ +/* wuss/window/get-content-bounds.c -- wuss - minimal window manager */ #include "../impl.h" diff --git a/libraries/wuss/window/get-scroll.c b/libraries/wuss/window/get-scroll.c index 18d09d8d..b2345194 100644 --- a/libraries/wuss/window/get-scroll.c +++ b/libraries/wuss/window/get-scroll.c @@ -1,4 +1,4 @@ -/* get-scroll.c -- wuss - minimal window manager */ +/* wuss/window/get-scroll.c -- wuss - minimal window manager */ #include "../impl.h" diff --git a/libraries/wuss/window/get-visible-bounds.c b/libraries/wuss/window/get-visible-bounds.c index b33f3bed..e7e08765 100644 --- a/libraries/wuss/window/get-visible-bounds.c +++ b/libraries/wuss/window/get-visible-bounds.c @@ -1,4 +1,4 @@ -/* get-visible-bounds.c -- wuss - minimal window manager */ +/* wuss/window/get-visible-bounds.c -- wuss - minimal window manager */ #include "../impl.h" diff --git a/libraries/wuss/window/invalidate.c b/libraries/wuss/window/invalidate.c index accaa784..55564967 100644 --- a/libraries/wuss/window/invalidate.c +++ b/libraries/wuss/window/invalidate.c @@ -1,4 +1,4 @@ -/* invalidate.c -- wuss - minimal window manager */ +/* wuss/window/invalidate.c -- wuss - minimal window manager */ #include <string.h> diff --git a/libraries/wuss/window/move.c b/libraries/wuss/window/move.c index a23eaa17..916bd36b 100644 --- a/libraries/wuss/window/move.c +++ b/libraries/wuss/window/move.c @@ -1,4 +1,4 @@ -/* move.c -- wuss - minimal window manager */ +/* wuss/window/move.c -- wuss - minimal window manager */ #include "../impl.h" diff --git a/libraries/wuss/window/resize.c b/libraries/wuss/window/resize.c index 1e95d901..d66664ba 100644 --- a/libraries/wuss/window/resize.c +++ b/libraries/wuss/window/resize.c @@ -1,4 +1,4 @@ -/* resize.c -- wuss - minimal window manager */ +/* wuss/window/resize.c -- wuss - minimal window manager */ #include "base/utils.h" diff --git a/libraries/wuss/window/restack.c b/libraries/wuss/window/restack.c index 5192bdfb..d4d7ee9e 100644 --- a/libraries/wuss/window/restack.c +++ b/libraries/wuss/window/restack.c @@ -1,4 +1,4 @@ -/* restack.c -- wuss - minimal window manager */ +/* wuss/window/restack.c -- wuss - minimal window manager */ #include "../impl.h" diff --git a/libraries/wuss/window/set-background.c b/libraries/wuss/window/set-background.c index bd704c4a..1c5cf95c 100644 --- a/libraries/wuss/window/set-background.c +++ b/libraries/wuss/window/set-background.c @@ -1,4 +1,4 @@ -/* set-background.c -- wuss - minimal window manager */ +/* wuss/window/set-background.c -- wuss - minimal window manager */ #include "../impl.h" diff --git a/libraries/wuss/window/set-scroll.c b/libraries/wuss/window/set-scroll.c index e1371013..6a0107b1 100644 --- a/libraries/wuss/window/set-scroll.c +++ b/libraries/wuss/window/set-scroll.c @@ -1,4 +1,4 @@ -/* set-scroll.c -- wuss - minimal window manager */ +/* wuss/window/set-scroll.c -- wuss - minimal window manager */ #include "../impl.h" From 429bbabf122f10748861aef81b99b6e58ab59ecc Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Tue, 1 Sep 2026 09:59:46 +0100 Subject: [PATCH 123/287] perf(wuss): skip icon redraw on hover when appearance is hover-independent wuss__icon_set_hover invalidated every icon the pointer entered or left, but only wuss_ICON_TYPE_MENU_ENTRY changes appearance on hover. Buttons and all other types ignore icon->hovered, so the redraw was wasted work. Gate the invalidate calls behind a type check; the hovered flag still updates for all types. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- libraries/wuss/icon/set-hover.c | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/libraries/wuss/icon/set-hover.c b/libraries/wuss/icon/set-hover.c index cb5deb8b..8fc1ec76 100644 --- a/libraries/wuss/icon/set-hover.c +++ b/libraries/wuss/icon/set-hover.c @@ -4,6 +4,14 @@ #include "../impl.h" +/* Only wuss_ICON_TYPE_MENU_ENTRY changes appearance on hover (see + * wuss__icon_draw_menu_entry). Buttons and every other type ignore icon->hovered, + * so redrawing them on pointer enter/leave is wasted work. */ +static int wuss__icon_hover_visible(const wuss_icon_t *icon) +{ + return icon->type == wuss_ICON_TYPE_MENU_ENTRY; +} + void wuss__icon_set_hover(wuss_t *wuss, wuss_icon_t *icon) { wuss_icon_t *prev; @@ -15,7 +23,8 @@ void wuss__icon_set_hover(wuss_t *wuss, wuss_icon_t *icon) if (prev != NULL) { prev->hovered = 0; - wuss__icon_invalidate(prev); + if (wuss__icon_hover_visible(prev)) + wuss__icon_invalidate(prev); } wuss->hover_icon = icon; @@ -23,6 +32,7 @@ void wuss__icon_set_hover(wuss_t *wuss, wuss_icon_t *icon) if (icon != NULL) { icon->hovered = 1; - wuss__icon_invalidate(icon); + if (wuss__icon_hover_visible(icon)) + wuss__icon_invalidate(icon); } } From 8d191a070a9ad044e0a7c7396fb0985f4fd21608 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Tue, 1 Sep 2026 10:18:03 +0100 Subject: [PATCH 124/287] fix(wuss): re-resolve the hovered icon after a scroll Scrolling moves content under a stationary pointer, but wuss_scroll never re-ran the icon hit-test, so hover_icon stayed pinned to the icon that was under the pointer before the scroll -- a highlighted wuss_ICON_TYPE_MENU_ENTRY kept its highlight after scrolling out from under the pointer. Add wuss__scroll_rehover to recompute the hovered icon on both scroll paths. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- libraries/wuss/scroll.c | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/libraries/wuss/scroll.c b/libraries/wuss/scroll.c index 1eb54af5..d956954c 100644 --- a/libraries/wuss/scroll.c +++ b/libraries/wuss/scroll.c @@ -2,6 +2,30 @@ #include "impl.h" +/* Scrolling moves content under a stationary pointer, so the icon the pointer + * now sits over may differ from before. Re-resolve the hovered icon so a + * highlighted wuss_ICON_TYPE_MENU_ENTRY does not keep its highlight after + * scrolling out from under the pointer. */ +static void wuss__scroll_rehover(wuss_t *wuss, + wuss_window_t *win, + point_t screen_point) +{ +#ifdef WUSS_ICONS + box_t content; + point_t doc_point; + + wuss__content_box(win, &content); + doc_point.x = screen_point.x - content.x0 + win->scroll.x; + doc_point.y = screen_point.y - content.y0 + win->scroll.y; + + wuss__icon_set_hover(wuss, wuss__icon_hit_test(win, doc_point)); +#else + (void) wuss; + (void) win; + (void) screen_point; +#endif +} + result_t wuss_scroll(wuss_t *wuss, point_t p, int delta, wuss_window_t **hit) { wuss_window_t *win; @@ -42,11 +66,13 @@ result_t wuss_scroll(wuss_t *wuss, point_t p, int delta, wuss_window_t **hit) event.data.scroll.delta = delta; wuss__scroll_step(win, POINT(0, delta)); + wuss__scroll_rehover(wuss, win, POINT(x, y)); return win->task.handle(win, &event, win->task.task_data); } wuss__scroll_step(win, POINT(0, delta)); + wuss__scroll_rehover(wuss, win, POINT(x, y)); return result_OK; } From 8510756cf5a433f1e6330cedc83ef5284a002fda Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Tue, 1 Sep 2026 10:18:11 +0100 Subject: [PATCH 125/287] perf(wuss): blit the still-valid content when a resize re-clamps the scroll Growing a window that was scrolled to its limit pulls the scroll offset back so only the document extent stays in view. wuss_window_resize repainted the whole content box on that re-clamp, matching the toggle-size path's pessimism -- but that path runs after the window has already been resized on screen, whereas a direct wuss_window_resize (interactive drag-resize) runs with the screen still showing this window's content at the old geometry, so the old content pixels are a valid blit source. Blit the old content box, minus whatever windows above it were covering (wuss__clip_to_visible), shifted by the scroll delta; the clip bounds every destination to the new content box. Only the occluded columns and the newly-revealed strip then need a real repaint. Falls back to a full content-box invalidate when the pixel format can't blit. Adds two tests: the topmost case and the not-topmost (occluded) case. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- libraries/wuss/test/wuss-test.c | 209 ++++++++++++++++++++++++++++++++ libraries/wuss/window/resize.c | 52 +++++++- 2 files changed, 256 insertions(+), 5 deletions(-) diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 13824f17..c2901c1c 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -1324,6 +1324,215 @@ result_t wuss_test(const char *resources) wuss_window_close(win_r); } + printf("test: wuss_window_resize on a topmost window at max scroll blits the still-valid content rather than redrawing it all\n"); + + { + /* Unlike the toggle-size path above, a direct wuss_window_resize (the + * interactive drag-resize path) runs with the screen still showing this + * window's content at the old geometry and old scroll offset. When the + * grow forces a scroll re-clamp, the overlap of the old and new content + * boxes is genuine on-screen content that just needs sliding by the + * scroll delta -- only the newly-exposed strip needs a real repaint. */ + test_task_t tc_d; + wuss_task_t delegate_d; + box_t box_d, content_before; + wuss_window_t *win_d; + point_t scroll_d; + int i; + int top_x, top_y, mid_x, mid_y, bottom_x, bottom_y; + int top_dirty, mid_dirty, bottom_dirty; + + tc_d.redraw_count = 0; + tc_d.mouse_count = 0; + delegate_d.handle = test_handle; + delegate_d.task_data = &tc_d; + + 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(wuss, &box_d, "D", wuss_WINDOW_NONE, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + &delegate_d, + SIZE2D(80, 140), SIZE2D(0, 0), &win_d); + if (rc != result_OK) + goto Failure; + + rc = wuss_redraw_dirty(wuss); /* flush the create's own invalidate */ + if (rc != result_OK) + goto Failure; + + wuss_window_get_content_bounds(win_d, &content_before); + + /* scroll to the bottom: max_y = doc.h (140) - content height (80) = 60 */ + wuss_window_set_scroll(win_d, POINT(0, 60)); + rc = wuss_redraw_dirty(wuss); /* flush the scroll's own invalidate */ + if (rc != result_OK) + goto Failure; + + wuss_window_get_scroll(win_d, &scroll_d); + if (scroll_d.y != 60) + goto Failure; + + /* Three probe points at the same x, well clear of the scrollbar column. + * The re-clamp drops scroll.y from 60 to 0, so on-screen content slides + * DOWN by 60px: the top ~60px of the content box is newly revealed and + * must be repainted, everything below it was already on screen and must + * be reused by the blit. */ + top_x = content_before.x0 + 8; + top_y = content_before.y0 + 8; /* < 60px down: in the revealed strip */ + mid_x = content_before.x0 + 8; + mid_y = content_before.y0 + 70; /* > 60px down: reused */ + bottom_x = content_before.x0 + 8; + bottom_y = content_before.y1 - 8; /* near old viewport bottom: reused */ + + /* grow the content taller than the doc: content height goes to >= 140, + * so max_y drops to 0 and scroll.y is clamped back from 60 to 0 */ + rc = wuss_window_resize(win_d, SIZE2D(80, 160)); + if (rc != result_OK) + goto Failure; + + wuss_window_get_scroll(win_d, &scroll_d); + if (scroll_d.y != 0) + goto Failure; /* the grow must have re-clamped the offset */ + + if (wuss_get_dirty_count(wuss) == 0) + goto Failure; + + top_dirty = 0; + mid_dirty = 0; + bottom_dirty = 0; + for (i = 0; i < wuss_get_dirty_count(wuss); i++) + { + box_t region; + + wuss_get_dirty(wuss, i, ®ion); + if (box_contains_point(®ion, top_x, top_y)) + top_dirty = 1; + if (box_contains_point(®ion, mid_x, mid_y)) + mid_dirty = 1; + if (box_contains_point(®ion, bottom_x, bottom_y)) + bottom_dirty = 1; + } + + if (!top_dirty) + goto Failure; /* the newly-revealed strip must be repainted */ + + if (mid_dirty || bottom_dirty) + goto Failure; /* content that stayed on screen must be reused by the + * blit-and-shift, not redrawn -- a full content-box + * invalidate has regressed the optimisation */ + + rc = wuss_redraw_dirty(wuss); + if (rc != result_OK) + goto Failure; + + wuss_window_close(win_d); + } + + printf("test: wuss_window_resize blits the un-occluded content even when the window is not topmost\n"); + + { + /* Same as above, but another window covers the right half of D's + * content. The blit source is the old content box MINUS what the + * occluder was covering (that area holds the occluder's pixels, not + * D's): the un-occluded left half is still slid into place, only the + * occluded columns and the newly-revealed top strip need a repaint. */ + test_task_t tc_d, tc_o; + wuss_task_t delegate_d, delegate_o; + box_t box_d, box_o, content_before; + wuss_window_t *win_d, *win_o; + point_t scroll_d; + int i, split_x; + int left_x, left_y, right_x, right_y; + int left_dirty, right_dirty; + + tc_d.redraw_count = 0; tc_d.mouse_count = 0; + tc_o.redraw_count = 0; tc_o.mouse_count = 0; + delegate_d.handle = test_handle; delegate_d.task_data = &tc_d; + delegate_o.handle = test_handle; delegate_o.task_data = &tc_o; + + 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(wuss, &box_d, "D", wuss_WINDOW_NONE, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + &delegate_d, + SIZE2D(100, 140), SIZE2D(0, 0), &win_d); + if (rc != result_OK) + goto Failure; + + wuss_window_get_content_bounds(win_d, &content_before); + split_x = (content_before.x0 + content_before.x1) / 2; + + /* 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(wuss, &box_o, "O", wuss_WINDOW_NONE, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + &delegate_o, + SIZE2D(box_o.x1 - box_o.x0, box_o.y1 - box_o.y0), + SIZE2D(0, 0), &win_o); + if (rc != result_OK) + goto Failure; + + rc = wuss_redraw_dirty(wuss); /* flush both creates */ + if (rc != result_OK) + goto Failure; + + wuss_window_set_scroll(win_d, POINT(0, 60)); /* max_y = 140 - 80 = 60 */ + rc = wuss_redraw_dirty(wuss); + if (rc != result_OK) + goto Failure; + + wuss_window_get_scroll(win_d, &scroll_d); + if (scroll_d.y != 60) + goto Failure; + + /* left probe: un-occluded, below the ~60px revealed strip -- must be + * reused by the blit. right probe: under the occluder -- was never D's + * pixels on screen, so it must be repainted. Both at the same y. */ + left_x = content_before.x0 + 8; + left_y = content_before.y1 - 8; + right_x = split_x + 8; + right_y = content_before.y1 - 8; + + rc = wuss_window_resize(win_d, SIZE2D(100, 160)); /* grow past doc height */ + if (rc != result_OK) + goto Failure; + + wuss_window_get_scroll(win_d, &scroll_d); + if (scroll_d.y != 0) + goto Failure; + + if (wuss_get_dirty_count(wuss) == 0) + goto Failure; + + left_dirty = 0; + right_dirty = 0; + for (i = 0; i < wuss_get_dirty_count(wuss); i++) + { + box_t region; + + wuss_get_dirty(wuss, i, ®ion); + if (box_contains_point(®ion, left_x, left_y)) + left_dirty = 1; + if (box_contains_point(®ion, right_x, right_y)) + right_dirty = 1; + } + + if (left_dirty) + goto Failure; /* un-occluded content must be reused by the blit */ + + if (!right_dirty) + goto Failure; /* content that was hidden behind the occluder holds no + * valid D pixels on screen -- it must be repainted */ + + rc = wuss_redraw_dirty(wuss); + if (rc != result_OK) + goto Failure; + + wuss_window_close(win_o); + wuss_window_close(win_d); + } + printf("test: wuss_WINDOW_NO_RESIZE_BLIT redraws the whole window instead of blitting\n"); { diff --git a/libraries/wuss/window/resize.c b/libraries/wuss/window/resize.c index d66664ba..67f31339 100644 --- a/libraries/wuss/window/resize.c +++ b/libraries/wuss/window/resize.c @@ -1,6 +1,7 @@ /* wuss/window/resize.c -- wuss - minimal window manager */ #include "base/utils.h" +#include "geom/box.h" #include "../impl.h" @@ -33,8 +34,9 @@ static void invalidate_grown_or_shrunk(wuss_window_t *window, result_t wuss_window_resize(wuss_window_t *window, size2d_t size) { int outline_px, titlebar_height; - box_t before; + box_t before, before_content; point_t carve; + point_t old_scroll; point_t clamped; if (!wuss__size_ok(size.w, size.h)) @@ -46,6 +48,8 @@ result_t wuss_window_resize(wuss_window_t *window, size2d_t size) outline_px = wuss__outline_px(window); titlebar_height = wuss__titlebar_height(window); before = window->visible; + old_scroll = window->scroll; + wuss__content_box(window, &before_content); wuss__furniture_carve_for(window->flags, wuss__button_size(window), &carve); window->visible.x1 = window->visible.x0 + size.w + 2 * outline_px + carve.x; @@ -55,17 +59,55 @@ result_t wuss_window_resize(wuss_window_t *window, size2d_t size) /* Enlarging the viewport (or growing it past the doc extent) can scroll * content that no longer exists into view; pull the offset back so only - * the document extent is ever shown. The interior has moved relative to - * the window, so the whole content box needs repainting -- the - * grown/shrunk-sliver logic below assumes an unchanged interior. */ + * the document extent is ever shown. The content pixels already on screen + * are still this window's own rendering, just at the old scroll offset -- + * so slide them by the scroll delta and only the newly-exposed strip(s) + * need a real repaint. The valid source is the old content box minus + * whatever windows above it were covering (those areas hold occluder + * pixels, not this window's), so blit it piece by piece; the clip bounds + * every destination to the new content box, discarding anything sliding + * off the old box's far edge rather than dragging stale pixels in. If the + * screen format can't blit, fall back to repainting the whole content + * box. Either way the grown/shrunk-sliver logic below still assumes an + * unchanged interior, which now holds. */ clamped = wuss__scroll_clamp(window, window->scroll); if (clamped.x != window->scroll.x || clamped.y != window->scroll.y) { box_t content; + box_t src[WUSS_MAX_INVALIDATE_PIECES]; + box_t copied[WUSS_MAX_INVALIDATE_PIECES]; + box_t dirty[WUSS_MAX_INVALIDATE_PIECES]; + int dx, dy, nsrc, ncopied, ndirty, i; + dx = clamped.x - old_scroll.x; + dy = clamped.y - old_scroll.y; window->scroll = clamped; wuss__content_box(window, &content); - wuss__invalidate_clipped(window, &content); + + nsrc = wuss__clip_to_visible(window, &before_content, src); + ncopied = 0; + window->wuss->scr->clip = content; + for (i = 0; i < nsrc; i++) + { + box_t got; + + if (screen_copy_rect(window->wuss->scr, &src[i], + POINT(src[i].x0 - dx, src[i].y0 - dy), + &got) != 0 && + ncopied < WUSS_MAX_INVALIDATE_PIECES) + copied[ncopied++] = got; + } + + if (ncopied > 0) + { + ndirty = wuss__subtract_boxes(&content, copied, ncopied, dirty); + for (i = 0; i < ndirty; i++) + wuss_invalidate(window->wuss, &dirty[i]); + } + else + { + wuss__invalidate_clipped(window, &content); + } #ifdef WUSS_FURNITURE wuss__furniture_invalidate(window); #endif From 5d952421199b817e02365d1e15c7bbc3e5a77ad9 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Tue, 1 Sep 2026 10:19:33 +0100 Subject: [PATCH 126/287] perf(wuss): blit un-occluded content on scroll when the window is not topmost wuss_window_set_scroll only took its blit-and-shift path when the window was topmost; otherwise it repainted the whole content box. Windows above occlude only part of the content, and wuss__clip_to_visible already computes the un-occluded remainder -- blit those pieces shifted by the scroll delta, then repaint only the occluded regions plus the newly-exposed edge strip (wuss__subtract_boxes). Falls back to a full invalidate when the pixel format can't blit. Mirrors the resize re-clamp path. Adds a not-topmost (occluded) set-scroll test. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- libraries/wuss/test/wuss-test.c | 86 ++++++++++++++++++++++++++++++ libraries/wuss/window/set-scroll.c | 44 ++++++++++----- 2 files changed, 116 insertions(+), 14 deletions(-) diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index c2901c1c..244293a3 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -1533,6 +1533,92 @@ result_t wuss_test(const char *resources) wuss_window_close(win_d); } + printf("test: wuss_window_set_scroll blits the un-occluded content even when the window is not topmost\n"); + + { + /* A window covered on its right half by another still blits its + * un-occluded left half on a programmatic scroll: only the occluded + * columns and the newly-exposed edge strip need a repaint. */ + test_task_t tc_s, tc_o; + wuss_task_t delegate_s, delegate_o; + box_t box_s, box_o, content; + wuss_window_t *win_s, *win_o; + int i, split_x; + int left_x, left_y, right_x, right_y; + int left_dirty, right_dirty; + + tc_s.redraw_count = 0; tc_s.mouse_count = 0; + tc_o.redraw_count = 0; tc_o.mouse_count = 0; + delegate_s.handle = test_handle; delegate_s.task_data = &tc_s; + delegate_o.handle = test_handle; delegate_o.task_data = &tc_o; + + 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(wuss, &box_s, "S", wuss_WINDOW_NONE, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + &delegate_s, + SIZE2D(100, 200), SIZE2D(0, 0), &win_s); + if (rc != result_OK) + goto Failure; + + wuss_window_get_content_bounds(win_s, &content); + split_x = (content.x0 + content.x1) / 2; + + 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(wuss, &box_o, "O", wuss_WINDOW_NONE, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + &delegate_o, + SIZE2D(box_o.x1 - box_o.x0, box_o.y1 - box_o.y0), + SIZE2D(0, 0), &win_o); + if (rc != result_OK) + goto Failure; + + rc = wuss_redraw_dirty(wuss); /* flush both creates */ + if (rc != result_OK) + goto Failure; + + /* left probe: un-occluded, near the bottom -- content there just slides + * up by the scroll delta, so the blit must reuse it. right probe: under + * the occluder -- never S's pixels on screen, so it must be repainted. */ + left_x = content.x0 + 8; + left_y = content.y1 - 20; + right_x = split_x + 8; + right_y = content.y1 - 20; + + wuss_window_set_scroll(win_s, POINT(0, 12)); + + if (wuss_get_dirty_count(wuss) == 0) + goto Failure; + + left_dirty = 0; + right_dirty = 0; + for (i = 0; i < wuss_get_dirty_count(wuss); i++) + { + box_t region; + + wuss_get_dirty(wuss, i, ®ion); + if (box_contains_point(®ion, left_x, left_y)) + left_dirty = 1; + if (box_contains_point(®ion, right_x, right_y)) + right_dirty = 1; + } + + if (left_dirty) + goto Failure; /* un-occluded content must be reused by the blit */ + + if (!right_dirty) + goto Failure; /* content hidden behind the occluder holds no valid S + * pixels on screen -- it must be repainted */ + + rc = wuss_redraw_dirty(wuss); + if (rc != result_OK) + goto Failure; + + wuss_window_close(win_o); + wuss_window_close(win_s); + } + printf("test: wuss_WINDOW_NO_RESIZE_BLIT redraws the whole window instead of blitting\n"); { diff --git a/libraries/wuss/window/set-scroll.c b/libraries/wuss/window/set-scroll.c index 6a0107b1..2aca6587 100644 --- a/libraries/wuss/window/set-scroll.c +++ b/libraries/wuss/window/set-scroll.c @@ -4,8 +4,11 @@ void wuss_window_set_scroll(wuss_window_t *window, point_t p) { - box_t content, copied; - int dx, dy; + box_t content; + box_t src[WUSS_MAX_INVALIDATE_PIECES]; + box_t copied[WUSS_MAX_INVALIDATE_PIECES]; + box_t dirty[WUSS_MAX_INVALIDATE_PIECES]; + int dx, dy, nsrc, ncopied, ndirty, i; dx = p.x - window->scroll.x; dy = p.y - window->scroll.y; @@ -22,19 +25,32 @@ void wuss_window_set_scroll(wuss_window_t *window, point_t p) wuss__furniture_invalidate(window); #endif - if (window->wuss->z_order.next == &window->link) + /* The valid blit source is the content box minus whatever windows above it + * were covering (those areas hold occluder pixels, not this window's), so + * slide it piece by piece by the scroll delta. The clip keeps every + * destination inside the content box, so a piece near the trailing edge + * shifts partly out and that area falls into the repaint set below along + * with the occluded regions. Falls back to a full content invalidate when + * the pixel format can't blit. */ + nsrc = wuss__clip_to_visible(window, &content, src); + ncopied = 0; + window->wuss->scr->clip = content; + for (i = 0; i < nsrc; i++) { - /* Topmost, so every pixel in "content" is genuinely this window's own - * rendering: slide it by the scroll delta (clipped to the viewport, so - * source and destination both stay inside "content") and only the - * newly-exposed edge strip(s) need an actual repaint. */ - window->wuss->scr->clip = content; - if (screen_copy_rect(window->wuss->scr, &content, - POINT(content.x0 - dx, content.y0 - dy), &copied)) - { - wuss__invalidate_minus(window->wuss, &content, &copied); - return; - } + box_t got; + + if (screen_copy_rect(window->wuss->scr, &src[i], + POINT(src[i].x0 - dx, src[i].y0 - dy), &got) != 0 && + ncopied < WUSS_MAX_INVALIDATE_PIECES) + copied[ncopied++] = got; + } + + if (ncopied > 0) + { + ndirty = wuss__subtract_boxes(&content, copied, ncopied, dirty); + for (i = 0; i < ndirty; i++) + wuss_invalidate(window->wuss, &dirty[i]); + return; } wuss__invalidate_clipped(window, &content); From 4c54556802bcddc46078ef7cd4a7edb8419a4c97 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Tue, 1 Sep 2026 10:28:08 +0100 Subject: [PATCH 127/287] fix(wuss): clip scroll/resize blit repaint set to visible area The blit-and-shift optimisation in wuss_window_set_scroll and wuss_window_resize computed its repaint remainder as the content box minus the blitted pieces and queued each survivor via raw wuss_invalidate. That is screen-space and does not subtract occluders, so the columns hidden behind a window above got marked dirty and the occluding window was repainted for nothing -- its pixels were already correct. Route the remainder through wuss__invalidate_clipped instead, which subtracts windows above this one before queueing. Rewrite the two not-topmost tests to assert the occluded region stays clean while the un-occluded newly-exposed strip still repaints. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- libraries/wuss/test/wuss-test.c | 114 +++++++++++++++++------------ libraries/wuss/window/resize.c | 6 +- libraries/wuss/window/set-scroll.c | 11 ++- 3 files changed, 79 insertions(+), 52 deletions(-) diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 244293a3..9bfa3259 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -1428,22 +1428,23 @@ result_t wuss_test(const char *resources) wuss_window_close(win_d); } - printf("test: wuss_window_resize blits the un-occluded content even when the window is not topmost\n"); + printf("test: wuss_window_resize on a non-topmost window blits the un-occluded content and never dirties the occluder\n"); { - /* Same as above, but another window covers the right half of D's - * content. The blit source is the old content box MINUS what the - * occluder was covering (that area holds the occluder's pixels, not - * D's): the un-occluded left half is still slid into place, only the - * occluded columns and the newly-revealed top strip need a repaint. */ + /* Another window covers the right half of D's content. The blit source + * is the old content box MINUS what the occluder covered, so the + * un-occluded left half is slid into place; the right half still shows + * the occluder's own correct pixels, so it must stay clean -- dirtying + * it would redraw the occluding window for nothing. Only the un-occluded + * newly-revealed top strip needs a repaint. */ test_task_t tc_d, tc_o; wuss_task_t delegate_d, delegate_o; box_t box_d, box_o, content_before; wuss_window_t *win_d, *win_o; point_t scroll_d; int i, split_x; - int left_x, left_y, right_x, right_y; - int left_dirty, right_dirty; + int occluded_x, occluded_y, kept_x, kept_y, strip_x, strip_y; + int occluded_dirty, kept_dirty, strip_dirty; tc_d.redraw_count = 0; tc_d.mouse_count = 0; tc_o.redraw_count = 0; tc_o.mouse_count = 0; @@ -1486,13 +1487,15 @@ result_t wuss_test(const char *resources) if (scroll_d.y != 60) goto Failure; - /* left probe: un-occluded, below the ~60px revealed strip -- must be - * reused by the blit. right probe: under the occluder -- was never D's - * pixels on screen, so it must be repainted. Both at the same y. */ - left_x = content_before.x0 + 8; - left_y = content_before.y1 - 8; - right_x = split_x + 8; - right_y = content_before.y1 - 8; + /* occluded: under O -- must stay clean. kept: un-occluded, below the + * ~60px revealed strip -- reused by the blit, must stay clean. strip: + * un-occluded, in the newly-revealed top band -- must be repainted. */ + occluded_x = split_x + 8; + occluded_y = content_before.y1 - 8; + kept_x = content_before.x0 + 8; + kept_y = content_before.y1 - 8; + strip_x = content_before.x0 + 8; + strip_y = content_before.y0 + 8; rc = wuss_window_resize(win_d, SIZE2D(100, 160)); /* grow past doc height */ if (rc != result_OK) @@ -1505,25 +1508,31 @@ result_t wuss_test(const char *resources) if (wuss_get_dirty_count(wuss) == 0) goto Failure; - left_dirty = 0; - right_dirty = 0; + occluded_dirty = 0; + kept_dirty = 0; + strip_dirty = 0; for (i = 0; i < wuss_get_dirty_count(wuss); i++) { box_t region; wuss_get_dirty(wuss, i, ®ion); - if (box_contains_point(®ion, left_x, left_y)) - left_dirty = 1; - if (box_contains_point(®ion, right_x, right_y)) - right_dirty = 1; + if (box_contains_point(®ion, occluded_x, occluded_y)) + occluded_dirty = 1; + if (box_contains_point(®ion, kept_x, kept_y)) + kept_dirty = 1; + if (box_contains_point(®ion, strip_x, strip_y)) + strip_dirty = 1; } - if (left_dirty) + if (occluded_dirty) + goto Failure; /* under the occluder: its pixels are already correct, + * dirtying this would redraw the occluding window */ + + if (kept_dirty) goto Failure; /* un-occluded content must be reused by the blit */ - if (!right_dirty) - goto Failure; /* content that was hidden behind the occluder holds no - * valid D pixels on screen -- it must be repainted */ + if (!strip_dirty) + goto Failure; /* the un-occluded newly-revealed strip must be repainted */ rc = wuss_redraw_dirty(wuss); if (rc != result_OK) @@ -1533,19 +1542,21 @@ result_t wuss_test(const char *resources) wuss_window_close(win_d); } - printf("test: wuss_window_set_scroll blits the un-occluded content even when the window is not topmost\n"); + printf("test: wuss_window_set_scroll on a non-topmost window blits the un-occluded content and never dirties the occluder\n"); { /* A window covered on its right half by another still blits its - * un-occluded left half on a programmatic scroll: only the occluded - * columns and the newly-exposed edge strip need a repaint. */ + * un-occluded left half on a programmatic scroll. The right half shows + * the occluder's own correct pixels, so it must stay clean -- dirtying + * it would redraw the occluding window. Only the un-occluded + * newly-exposed edge strip needs a repaint. */ test_task_t tc_s, tc_o; wuss_task_t delegate_s, delegate_o; box_t box_s, box_o, content; wuss_window_t *win_s, *win_o; int i, split_x; - int left_x, left_y, right_x, right_y; - int left_dirty, right_dirty; + int occluded_x, occluded_y, kept_x, kept_y, strip_x, strip_y; + int occluded_dirty, kept_dirty, strip_dirty; tc_s.redraw_count = 0; tc_s.mouse_count = 0; tc_o.redraw_count = 0; tc_o.mouse_count = 0; @@ -1578,38 +1589,47 @@ result_t wuss_test(const char *resources) if (rc != result_OK) goto Failure; - /* left probe: un-occluded, near the bottom -- content there just slides - * up by the scroll delta, so the blit must reuse it. right probe: under - * the occluder -- never S's pixels on screen, so it must be repainted. */ - left_x = content.x0 + 8; - left_y = content.y1 - 20; - right_x = split_x + 8; - right_y = content.y1 - 20; + /* kept: un-occluded, near the bottom -- content there just slides up by + * the scroll delta, so the blit must reuse it. occluded: under O -- must + * stay clean. strip: un-occluded, in the newly-exposed bottom band -- + * must be repainted. */ + kept_x = content.x0 + 8; + kept_y = content.y0 + 8; + occluded_x = split_x + 8; + occluded_y = content.y1 - 20; + strip_x = content.x0 + 8; + strip_y = content.y1 - 6; wuss_window_set_scroll(win_s, POINT(0, 12)); if (wuss_get_dirty_count(wuss) == 0) goto Failure; - left_dirty = 0; - right_dirty = 0; + kept_dirty = 0; + occluded_dirty = 0; + strip_dirty = 0; for (i = 0; i < wuss_get_dirty_count(wuss); i++) { box_t region; wuss_get_dirty(wuss, i, ®ion); - if (box_contains_point(®ion, left_x, left_y)) - left_dirty = 1; - if (box_contains_point(®ion, right_x, right_y)) - right_dirty = 1; + if (box_contains_point(®ion, kept_x, kept_y)) + kept_dirty = 1; + if (box_contains_point(®ion, occluded_x, occluded_y)) + occluded_dirty = 1; + if (box_contains_point(®ion, strip_x, strip_y)) + strip_dirty = 1; } - if (left_dirty) + if (occluded_dirty) + goto Failure; /* under the occluder: its pixels are already correct, + * dirtying this would redraw the occluding window */ + + if (kept_dirty) goto Failure; /* un-occluded content must be reused by the blit */ - if (!right_dirty) - goto Failure; /* content hidden behind the occluder holds no valid S - * pixels on screen -- it must be repainted */ + if (!strip_dirty) + goto Failure; /* the un-occluded newly-exposed strip must be repainted */ rc = wuss_redraw_dirty(wuss); if (rc != result_OK) diff --git a/libraries/wuss/window/resize.c b/libraries/wuss/window/resize.c index 67f31339..e6a961cb 100644 --- a/libraries/wuss/window/resize.c +++ b/libraries/wuss/window/resize.c @@ -100,9 +100,13 @@ result_t wuss_window_resize(wuss_window_t *window, size2d_t size) if (ncopied > 0) { + /* Repaint the content box minus what the blit reused, each survivor + * clipped to this window's visible area: parts that were behind an + * occluder still show the occluder's own correct pixels, so leaving + * them out keeps the resize from redrawing the occluding window. */ ndirty = wuss__subtract_boxes(&content, copied, ncopied, dirty); for (i = 0; i < ndirty; i++) - wuss_invalidate(window->wuss, &dirty[i]); + wuss__invalidate_clipped(window, &dirty[i]); } else { diff --git a/libraries/wuss/window/set-scroll.c b/libraries/wuss/window/set-scroll.c index 2aca6587..4e7f4732 100644 --- a/libraries/wuss/window/set-scroll.c +++ b/libraries/wuss/window/set-scroll.c @@ -29,9 +29,8 @@ void wuss_window_set_scroll(wuss_window_t *window, point_t p) * were covering (those areas hold occluder pixels, not this window's), so * slide it piece by piece by the scroll delta. The clip keeps every * destination inside the content box, so a piece near the trailing edge - * shifts partly out and that area falls into the repaint set below along - * with the occluded regions. Falls back to a full content invalidate when - * the pixel format can't blit. */ + * shifts partly out and that area falls into the repaint set below. Falls + * back to a full content invalidate when the pixel format can't blit. */ nsrc = wuss__clip_to_visible(window, &content, src); ncopied = 0; window->wuss->scr->clip = content; @@ -47,9 +46,13 @@ void wuss_window_set_scroll(wuss_window_t *window, point_t p) if (ncopied > 0) { + /* Repaint the content box minus what the blit reused, then clip each + * survivor to this window's visible area: the parts that were behind an + * occluder still show the occluder's own correct pixels, so leaving + * them out keeps the scroll from redrawing the occluding window. */ ndirty = wuss__subtract_boxes(&content, copied, ncopied, dirty); for (i = 0; i < ndirty; i++) - wuss_invalidate(window->wuss, &dirty[i]); + wuss__invalidate_clipped(window, &dirty[i]); return; } From 8a65a20b6e8ddea1695a0815f1ab1555631fc076 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Tue, 1 Sep 2026 10:34:34 +0100 Subject: [PATCH 128/287] feat(wuss): add Lissajous figure client Plots x=sin(a*t+phase), y=sin(b*t) as a ring of dots; the phase drifts each idle tick so the figure morphs. Select cycles the frequency pair, Adjust reverses the drift. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- CMakeLists.txt | 1 + apps/wuss/main.c | 12 ++ libraries/wuss/test/tasks/lissajous.c | 168 ++++++++++++++++++++++++++ libraries/wuss/test/tasks/lissajous.h | 37 ++++++ 4 files changed, 218 insertions(+) create mode 100644 libraries/wuss/test/tasks/lissajous.c create mode 100644 libraries/wuss/test/tasks/lissajous.h diff --git a/CMakeLists.txt b/CMakeLists.txt index a341d931..d862008f 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -677,6 +677,7 @@ if(BUILD_APPS) libraries/wuss/test/tasks/icons.c libraries/wuss/test/tasks/image.c libraries/wuss/test/tasks/launcher.c + libraries/wuss/test/tasks/lissajous.c libraries/wuss/test/tasks/palette.c libraries/wuss/test/tasks/porter-duff.c libraries/wuss/test/tasks/sofa.c diff --git a/apps/wuss/main.c b/apps/wuss/main.c index 0c7f4046..2de23bc4 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -33,6 +33,7 @@ #include "tasks/icons.h" #include "tasks/image.h" #include "tasks/launcher.h" +#include "tasks/lissajous.h" #include "tasks/palette.h" #include "tasks/porter-duff.h" #include "tasks/sofa.h" @@ -153,6 +154,16 @@ static result_t spawn_curve(void) return result_OK; } +static result_t spawn_lissajous(void) +{ + lissajous_task_t *t = calloc(1, sizeof(*t)); + result_t rc; + if (t == NULL) return result_OOM; + rc = lissajous_create(g_wuss, g_palette, t); + if (rc != result_OK || t->window == NULL) { free(t); return rc; } + return result_OK; +} + static result_t spawn_sofa(void) { sofa_task_t *t = calloc(1, sizeof(*t)); @@ -282,6 +293,7 @@ static const launcher_entry_t g_launcher_entries[] = { "Image", spawn_image }, { "Checker", spawn_checker }, { "Curve", spawn_curve }, + { "Lissajous", spawn_lissajous }, { "Sofa", spawn_sofa }, { "Gradient", spawn_gradient }, { "Icons", spawn_icons }, diff --git a/libraries/wuss/test/tasks/lissajous.c b/libraries/wuss/test/tasks/lissajous.c new file mode 100644 index 00000000..bcf94b00 --- /dev/null +++ b/libraries/wuss/test/tasks/lissajous.c @@ -0,0 +1,168 @@ +/* wuss/test/tasks/lissajous.c -- Lissajous figure task */ + +#ifdef USE_SDL + +#include <math.h> +#include <stdlib.h> + +#ifdef FORTIFY +#include "fortify/fortify.h" +#endif + +#include "base/utils.h" +#include "framebuf/palettes.h" +#include "geom/box.h" + +#include "lissajous.h" + +/* frequency pairs cycled by a Select click */ +static const int lissajous_freqs[][2] = +{ + { 3, 2 }, { 5, 4 }, { 3, 4 }, { 5, 6 }, { 1, 2 }, { 7, 4 } +}; + +result_t lissajous_create(wuss_t *wuss, + const colour_t *palette, + lissajous_task_t *task) +{ + wuss_task_t delegate; + + task->bg = palette[palette_PICO8_DARK_BLUE]; + task->fg = palette[palette_PICO8_YELLOW]; + task->freq_index = 0; + task->a = lissajous_freqs[0][0]; + task->b = lissajous_freqs[0][1]; + task->phase = 0.0; + task->drift = 0.01; + + delegate = wuss_task_start(lissajous_handle, task); /* redraw paints its own background every frame */ + + return wuss_window_create_placed(wuss, + SIZE2D(220, 220), + "Lissajous", + wuss_WINDOW_NONE, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + &delegate, + SIZE2D(220, 220), + SIZE2D(0, 0), + &task->window); +} + +static result_t lissajous_redraw(const wuss_event_t *event, void *task_data) +{ + lissajous_task_t *lc; + screen_t *scr; + const box_t *content, *bounds; + int sx, sy; + int width, height, cx, cy, rx, ry; + int i; + + lc = task_data; + + scr = event->data.redraw.scr; + content = event->data.redraw.content; + bounds = event->data.redraw.bounds; + sx = event->data.redraw.scroll.x; + sy = event->data.redraw.scroll.y; + + screen_draw_rect(scr, content->x0, content->y0, box_size(content), lc->bg); + + width = bounds->x1 - bounds->x0; + height = bounds->y1 - bounds->y0; + cx = bounds->x0 - sx + width / 2; + cy = bounds->y0 - sy + height / 2; + rx = width / 2 - 10; + ry = height / 2 - 10; + + for (i = 0; i < LISSAJOUS_POINTS; i++) + { + double t; + int px, py; + + t = (double) i / LISSAJOUS_POINTS * 2.0 * M_PI; + px = cx + (int) (rx * sin(lc->a * t + lc->phase)); + py = cy + (int) (ry * sin(lc->b * t)); + + screen_draw_pixel(scr, px, py, lc->fg); + } + + return result_OK; +} + +static result_t lissajous_mouse(wuss_window_t *window, + wuss_mouse_action_t action, + wuss_button_t button, + void *task_data) +{ + lissajous_task_t *lc; + + NOT_USED(window); + + lc = task_data; + + if (action != wuss_MOUSE_DOWN) + return result_OK; + + if (button & wuss_BUTTON_SELECT) + { + 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 */ + } + else if (button & wuss_BUTTON_ADJUST) + { + lc->drift = -lc->drift; + } + + return result_OK; +} + +static result_t lissajous_idle(void *task_data) +{ + lissajous_task_t *lc; + + lc = task_data; + + lc->phase += lc->drift; + if (lc->phase > 2.0 * M_PI) + lc->phase -= 2.0 * M_PI; + 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 */ + + return result_OK; +} + +result_t lissajous_handle(wuss_window_t *window, + const wuss_event_t *event, + void *task_data) +{ + lissajous_task_t *lc; + + lc = task_data; + + switch (event->kind) + { + case wuss_EVENT_REDRAW: + return lissajous_redraw(event, task_data); + + case wuss_EVENT_MOUSE: + return lissajous_mouse(window, event->data.mouse.action, + event->data.mouse.button, task_data); + + case wuss_EVENT_IDLE: + return lissajous_idle(task_data); + + case wuss_EVENT_CLOSE: + wuss_window_close(window); + free(lc); /* task_data was calloc'd per instance by the spawner */ + return result_OK; + + default: + return result_OK; + } +} + +#endif /* USE_SDL */ diff --git a/libraries/wuss/test/tasks/lissajous.h b/libraries/wuss/test/tasks/lissajous.h new file mode 100644 index 00000000..d9b85be6 --- /dev/null +++ b/libraries/wuss/test/tasks/lissajous.h @@ -0,0 +1,37 @@ +/* wuss/test/tasks/lissajous.h -- Lissajous figure task */ + +#ifndef TASKS_LISSAJOUS_H +#define TASKS_LISSAJOUS_H + +#ifdef USE_SDL + +#include "framebuf/colour.h" +#include "wuss/window.h" + +#define LISSAJOUS_POINTS 512 /* samples plotted along the curve */ + +/* window task: a Lissajous figure x=sin(a*t+phase), y=sin(b*t) plotted as a + * ring of dots. The phase drifts each idle tick so the figure slowly morphs. + * Select cycles the frequency pair (a,b); Adjust reverses the drift. */ +typedef struct lissajous_task +{ + wuss_window_t *window; + colour_t bg, fg; + int a, b; /* frequency ratio */ + double phase; + double drift; + int freq_index; +} +lissajous_task_t; + +wuss_event_fn_t lissajous_handle; + +/* create the Lissajous window against the given wuss instance; "task" is a + * per-instance block owned by the window and freed when it closes */ +result_t lissajous_create(wuss_t *wuss, + const colour_t *palette, + lissajous_task_t *task); + +#endif /* USE_SDL */ + +#endif /* TASKS_LISSAJOUS_H */ From ff7d341baa03c575befcd6096290c84f4a83d843 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Tue, 1 Sep 2026 10:44:37 +0100 Subject: [PATCH 129/287] refactor(wuss): use box lib primitives for icon/invalidate box ops Replace open-coded box clamps and translates with box_intersection, box_is_empty and box_translated. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- libraries/wuss/icon/draw.c | 9 ++------- libraries/wuss/icon/screen-box.c | 5 +---- libraries/wuss/window/invalidate.c | 6 ++---- 3 files changed, 5 insertions(+), 15 deletions(-) diff --git a/libraries/wuss/icon/draw.c b/libraries/wuss/icon/draw.c index 5f15307e..29d5f320 100644 --- a/libraries/wuss/icon/draw.c +++ b/libraries/wuss/icon/draw.c @@ -340,12 +340,7 @@ static void wuss__icon_draw_bitmap(const icon_draw_ctx_t *c) * clip to the icon box (intersected with whatever redraw already set) and * blit at the box's top-left */ clipped = *c->scr; - if (clipped.clip.x0 < b->x0) clipped.clip.x0 = b->x0; - if (clipped.clip.y0 < b->y0) clipped.clip.y0 = b->y0; - if (clipped.clip.x1 > b->x1) clipped.clip.x1 = b->x1; - if (clipped.clip.y1 > b->y1) clipped.clip.y1 = b->y1; - if (clipped.clip.x1 <= clipped.clip.x0 || - clipped.clip.y1 <= clipped.clip.y0) + if (box_intersection(&c->scr->clip, b, &clipped.clip)) return; screen_draw_bitmap(&clipped, b->x0, b->y0, c->icon->bitmap); @@ -440,7 +435,7 @@ void wuss__icon_draw(wuss_t *wuss, wuss__icon_box_to_screen(content, scroll, &icon->bbox, &c.b); - if (c.b.x1 <= c.b.x0 || c.b.y1 <= c.b.y0) + if (box_is_empty(&c.b)) return; c.wuss = wuss; diff --git a/libraries/wuss/icon/screen-box.c b/libraries/wuss/icon/screen-box.c index f1abad26..3f630519 100644 --- a/libraries/wuss/icon/screen-box.c +++ b/libraries/wuss/icon/screen-box.c @@ -14,8 +14,5 @@ void wuss__icon_box_to_screen(const box_t *content, const box_t *bbox, box_t *out) { - out->x0 = content->x0 - scroll.x + bbox->x0; - out->y0 = content->y0 - scroll.y + bbox->y0; - out->x1 = content->x0 - scroll.x + bbox->x1; - out->y1 = content->y0 - scroll.y + bbox->y1; + box_translated(bbox, content->x0 - scroll.x, content->y0 - scroll.y, out); } diff --git a/libraries/wuss/window/invalidate.c b/libraries/wuss/window/invalidate.c index 55564967..71079112 100644 --- a/libraries/wuss/window/invalidate.c +++ b/libraries/wuss/window/invalidate.c @@ -220,10 +220,8 @@ void wuss_window_invalidate(wuss_window_t *window, const box_t *local_box) local_box = &whole; } - screen_box.x0 = content.x0 - window->scroll.x + local_box->x0; - screen_box.y0 = content.y0 - window->scroll.y + local_box->y0; - screen_box.x1 = content.x0 - window->scroll.x + local_box->x1; - screen_box.y1 = content.y0 - window->scroll.y + local_box->y1; + box_translated(local_box, content.x0 - window->scroll.x, + content.y0 - window->scroll.y, &screen_box); wuss__invalidate_clipped(window, &screen_box); } From def9ef932836075031812b789de57f723bee7715 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Tue, 1 Sep 2026 11:32:59 +0100 Subject: [PATCH 130/287] feat(screen): add screen_draw_lines and unfilled screen_draw_rect Add screen_draw_lines (connected polyline) and repurpose screen_draw_rect as a one-pixel unfilled rectangle outline built on it, with a filled fallback for degenerate sizes. Cover both with unit tests in screen-test. Rename the primitives so draw/fill intent is explicit: screen_draw_pixel -> screen_set_pixel screen_draw_rect -> screen_fill_rect screen_draw_square -> screen_fill_square Use the new outline in the OPTION icon glyph (4 draw_line calls -> 1). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- include/framebuf/screen.h | 45 ++++++- libraries/framebuf/curve/test/curve-test.c | 6 +- libraries/framebuf/screen/screen-draw.c | 61 +++++++-- libraries/framebuf/screen/test/screen-test.c | 124 ++++++++++++++++++- libraries/framebuf/span/p4.c | 2 +- libraries/wuss/backdrop.c | 2 +- libraries/wuss/furniture/draw.c | 38 +++--- libraries/wuss/icon/draw.c | 16 ++- libraries/wuss/test/tasks/ball.c | 4 +- libraries/wuss/test/tasks/chars.c | 2 +- libraries/wuss/test/tasks/checker.c | 2 +- libraries/wuss/test/tasks/curve.c | 4 +- libraries/wuss/test/tasks/gradient.c | 2 +- libraries/wuss/test/tasks/lissajous.c | 4 +- libraries/wuss/test/tasks/palette.c | 2 +- libraries/wuss/test/tasks/porter-duff.c | 2 +- libraries/wuss/test/tasks/sofa.c | 4 +- 17 files changed, 260 insertions(+), 60 deletions(-) diff --git a/include/framebuf/screen.h b/include/framebuf/screen.h index 25fa4d19..2c89ec30 100644 --- a/include/framebuf/screen.h +++ b/include/framebuf/screen.h @@ -63,7 +63,7 @@ int screen_get_clip(const screen_t *scr, box_t *clip); * \param[in] y Y coordinate of pixel to draw. * \param[in] colour Colour of pixel. */ -void screen_draw_pixel(screen_t *scr, int x, int y, colour_t colour); +void screen_set_pixel(screen_t *scr, int x, int y, colour_t colour); /** * Draws a solid rectangle. @@ -74,14 +74,14 @@ void screen_draw_pixel(screen_t *scr, int x, int y, colour_t colour); * \param[in] size Width and height of rectangle. * \param[in] colour Colour of rectangle. */ -void screen_draw_rect(screen_t *scr, +void screen_fill_rect(screen_t *scr, int x, int y, size2d_t size, colour_t colour); /** - * Special case of `screen_draw_rect`. + * Special case of `screen_fill_rect`. * * \param[in] scr Screen to draw upon. * \param[in] x X coordinate of leftmost point of rectangle. @@ -89,7 +89,7 @@ void screen_draw_rect(screen_t *scr, * \param[in] size Size of rectangle. * \param[in] colour Colour of rectangle. */ -void screen_draw_square(screen_t *scr, +void screen_fill_square(screen_t *scr, int x, int y, int size, colour_t colour); @@ -239,6 +239,43 @@ void screen_draw_line(screen_t *scr, int y1, colour_t colour); +/** + * Draws a connected polyline through `npoints` points: a `screen_draw_line` + * segment between each adjacent pair. For a closed shape repeat the first point + * as the last. Fewer than 2 points draws nothing. + * + * Segments share their joint pixel, which is plotted by both adjacent segments; + * harmless for a solid colour. + * + * Coordinates are `int`s and inclusive. + * + * \param[in] scr Screen to draw upon. + * \param[in] points Array of `npoints` points. + * \param[in] npoints Number of points in `points`. + * \param[in] colour Colour of the polyline. + */ +void screen_draw_lines(screen_t *scr, + const point_t *points, + int npoints, + colour_t colour); + +/** + * Draws a one-pixel unfilled rectangle outline. `size` is inclusive of both + * edges, matching `screen_fill_rect`. A degenerate size (<= 1 in either axis) + * falls back to a filled `screen_fill_rect`. + * + * \param[in] scr Screen to draw upon. + * \param[in] x X coordinate of leftmost point of rectangle. + * \param[in] y Y coordinate of topmost point of rectangle. + * \param[in] size Width and height of rectangle. + * \param[in] colour Colour of the outline. + */ +void screen_draw_rect(screen_t *scr, + int x, + int y, + size2d_t size, + colour_t colour); + /** * Draws a stippled line: `on` pixels drawn, then `off` skipped, repeating along * the line. Bresenham stepping, so the dash period is measured in steps not diff --git a/libraries/framebuf/curve/test/curve-test.c b/libraries/framebuf/curve/test/curve-test.c index 2a926448..a78021ed 100644 --- a/libraries/framebuf/curve/test/curve-test.c +++ b/libraries/framebuf/curve/test/curve-test.c @@ -336,7 +336,7 @@ static void draw_all_control_points(curveteststate_t *state) colour = state->palette[8 + set]; - screen_draw_square(&state->scr, b.x0, b.y0, BLOBSZ, colour); + screen_fill_square(&state->scr, b.x0, b.y0, BLOBSZ, colour); box_union(&b, &state->overalldirty, &state->overalldirty); @@ -366,8 +366,8 @@ static void draw_a_curve(curveteststate_t *state) if (state->opt.draw_endpoints) { - screen_draw_pixel(&state->scr, b.x0, b.y0, state->palette[palette_PICO8_GREEN]); - screen_draw_pixel(&state->scr, b.x1, b.y1, state->palette[palette_PICO8_RED]); + screen_set_pixel(&state->scr, b.x0, b.y0, state->palette[palette_PICO8_GREEN]); + screen_set_pixel(&state->scr, b.x1, b.y1, state->palette[palette_PICO8_RED]); } switch (state->opt.method) diff --git a/libraries/framebuf/screen/screen-draw.c b/libraries/framebuf/screen/screen-draw.c index 5c77046c..91bf7d1c 100644 --- a/libraries/framebuf/screen/screen-draw.c +++ b/libraries/framebuf/screen/screen-draw.c @@ -20,7 +20,7 @@ * blow the stack (relevant on RISC OS). */ #define BITMAP_BLIT_CHUNK 256 -void screen_draw_pixel(screen_t *scr, int x, int y, colour_t colour) +void screen_set_pixel(screen_t *scr, int x, int y, colour_t colour) { box_t clip; pixelfmt_any_t pxl; @@ -138,7 +138,7 @@ static void screen_blend_pixel(screen_t *scr, /* ----------------------------------------------------------------------- */ -void screen_draw_rect(screen_t *scr, +void screen_fill_rect(screen_t *scr, int x, int y, size2d_t size, @@ -215,9 +215,9 @@ void screen_draw_rect(screen_t *scr, } } -void screen_draw_square(screen_t *scr, int x, int y, int size, colour_t colour) +void screen_fill_square(screen_t *scr, int x, int y, int size, colour_t colour) { - screen_draw_rect(scr, x, y, SIZE2D(size, size), colour); + screen_fill_rect(scr, x, y, SIZE2D(size, size), colour); } /* ----------------------------------------------------------------------- */ @@ -254,7 +254,7 @@ void screen_draw_bitmap(screen_t *scr, int x, int y, const bitmap_t *src) { /* Paletted screen: no linear channel bits to blend, so fall back to * alpha-tested (skip fully transparent, else nearest palette match) - * rather than true blending, matching screen_draw_pixel's case 2. */ + * rather than true blending, matching screen_set_pixel's case 2. */ const unsigned char *srcrow; unsigned char *dstbase; int yy; @@ -412,7 +412,7 @@ void screen_draw_line(screen_t *scr, for (;;) { - screen_draw_pixel(scr, x0, y0, colour); + screen_set_pixel(scr, x0, y0, colour); if (x0 == x1 && y0 == y1) break; @@ -433,6 +433,51 @@ void screen_draw_line(screen_t *scr, } } +void screen_draw_lines(screen_t *scr, + const point_t *points, + int npoints, + colour_t colour) +{ + int i; + + if (points == NULL || npoints < 2) + return; + + /* ponytail: per-segment call; joint pixels double-plot, fine for solid fill */ + for (i = 1; i < npoints; i++) + screen_draw_line(scr, + points[i - 1].x, points[i - 1].y, + points[i].x, points[i].y, + colour); +} + +void screen_draw_rect(screen_t *scr, + int x, + int y, + size2d_t size, + colour_t colour) +{ + int x1, y1; + point_t p[5]; + + if (size.w <= 1 || size.h <= 1) + { + screen_fill_rect(scr, x, y, size, colour); + return; + } + + x1 = x + size.w - 1; + y1 = y + size.h - 1; + + p[0].x = x; p[0].y = y; + p[1].x = x1; p[1].y = y; + p[2].x = x1; p[2].y = y1; + p[3].x = x; p[3].y = y1; + p[4].x = x; p[4].y = y; + + screen_draw_lines(scr, p, 5, colour); +} + void screen_draw_dashed_line(screen_t *scr, int x0, int y0, @@ -485,7 +530,7 @@ void screen_draw_dashed_line(screen_t *scr, for (;;) { if (phase < on) - screen_draw_pixel(scr, x0, y0, colour); + screen_set_pixel(scr, x0, y0, colour); if (++phase >= period) phase = 0; @@ -672,7 +717,7 @@ void screen_draw_line_wu_float(screen_t *scr, return; /* invalid clipped screen */ /* This discards the fractional part of the coordinates so for now just use it - * to discard lines. screen_draw_pixel() will be doing clipping too later. */ + * to discard lines. screen_set_pixel() will be doing clipping too later. */ x0 = fx0; y0 = fy0; x1 = fx1; diff --git a/libraries/framebuf/screen/test/screen-test.c b/libraries/framebuf/screen/test/screen-test.c index 8cf267fc..2ffcdb38 100644 --- a/libraries/framebuf/screen/test/screen-test.c +++ b/libraries/framebuf/screen/test/screen-test.c @@ -10,6 +10,7 @@ #include "framebuf/pixelfmt.h" #include "framebuf/screen.h" #include "geom/box.h" +#include "geom/point.h" #include "utils/fxp.h" #include "test/all-tests.h" @@ -256,7 +257,7 @@ static const int np_rgb[3][3][3] = static pixelfmt_bgrx8888_t np_encode(testscreen_t *ts, int r, int g, int b) { testscreen_init(ts); - screen_draw_pixel(&ts->scr, 0, 0, colour_rgb(r, g, b)); + screen_set_pixel(&ts->scr, 0, 0, colour_rgb(r, g, b)); return ts->pixels[0]; } @@ -514,6 +515,123 @@ static result_t test_dashed_line(void) /* ----------------------------------------------------------------------- */ +static result_t test_draw_lines(void) +{ + static testscreen_t ts; + static testscreen_t enc; + + const point_t chain[] = { { 4, 4 }, { 20, 4 }, { 20, 20 } }; + int fg, bg; + int x, y; + + fg = (int) np_encode(&enc, 255, 0, 0); + bg = (int) (pixelfmt_bgrx8888_t) BACKGROUND; + + /* An open two-segment polyline: both segments drawn, the shared joint + * pixel lit, nothing else. */ + testscreen_init(&ts); + screen_draw_lines(&ts.scr, chain, NELEMS(chain), colour_rgb(255, 0, 0)); + + for (x = 4; x <= 20; x++) + if (np_at(&ts, x, 4) != fg) + { + printf("screen: draw_lines gap on segment 1 at x=%d\n", x); + return result_TEST_FAILED; + } + for (y = 4; y <= 20; y++) + if (np_at(&ts, 20, y) != fg) + { + printf("screen: draw_lines gap on segment 2 at y=%d\n", y); + return result_TEST_FAILED; + } + if (np_at(&ts, 5, 5) != bg || np_at(&ts, 4, 20) != bg) + { + printf("screen: draw_lines drew outside the polyline\n"); + return result_TEST_FAILED; + } + + /* Fewer than two points is a no-op. */ + testscreen_init(&ts); + screen_draw_lines(&ts.scr, chain, 1, colour_rgb(255, 0, 0)); + screen_draw_lines(&ts.scr, NULL, 5, colour_rgb(255, 0, 0)); + for (y = 0; y < HEIGHT; y++) + for (x = 0; x < WIDTH; x++) + if (np_at(&ts, x, y) != bg) + { + printf("screen: draw_lines with <2 points drew at (%d,%d)\n", x, y); + return result_TEST_FAILED; + } + + return result_TEST_PASSED; +} + +/* ----------------------------------------------------------------------- */ + +static result_t test_draw_rect(void) +{ + static testscreen_t ts; + static testscreen_t enc; + + int fg, bg; + int x, y; + + fg = (int) np_encode(&enc, 0, 255, 0); + bg = (int) (pixelfmt_bgrx8888_t) BACKGROUND; + + /* 10x8 outline at (5,5): edges lit, interior and exterior background. + * size is inclusive, so the far edges are at x=14, y=12. */ + testscreen_init(&ts); + screen_draw_rect(&ts.scr, 5, 5, SIZE2D(10, 8), colour_rgb(0, 255, 0)); + + for (x = 5; x <= 14; x++) + if (np_at(&ts, x, 5) != fg || np_at(&ts, x, 12) != fg) + { + printf("screen: draw_rect horizontal edge gap at x=%d\n", x); + return result_TEST_FAILED; + } + for (y = 5; y <= 12; y++) + if (np_at(&ts, 5, y) != fg || np_at(&ts, 14, y) != fg) + { + printf("screen: draw_rect vertical edge gap at y=%d\n", y); + return result_TEST_FAILED; + } + if (np_at(&ts, 9, 8) != bg) /* interior */ + { + printf("screen: draw_rect filled its interior\n"); + return result_TEST_FAILED; + } + if (np_at(&ts, 4, 5) != bg || np_at(&ts, 15, 5) != bg || + np_at(&ts, 5, 4) != bg || np_at(&ts, 5, 13) != bg) + { + printf("screen: draw_rect drew outside the rect\n"); + return result_TEST_FAILED; + } + + /* Degenerate size (<= 1 in an axis) falls back to a filled rect. */ + testscreen_init(&ts); + screen_draw_rect(&ts.scr, 3, 3, SIZE2D(1, 6), colour_rgb(0, 255, 0)); + for (y = 3; y <= 8; y++) + if (np_at(&ts, 3, y) != fg) + { + printf("screen: draw_rect degenerate fallback gap at y=%d\n", y); + return result_TEST_FAILED; + } + + /* Honours the screen clip. */ + testscreen_init(&ts); + ts.scr.clip = (box_t) { 0, 0, 10, 64 }; + screen_draw_rect(&ts.scr, 5, 5, SIZE2D(10, 8), colour_rgb(0, 255, 0)); + if (np_at(&ts, 5, 5) != fg || np_at(&ts, 14, 5) != bg) + { + printf("screen: draw_rect ignored the screen clip\n"); + return result_TEST_FAILED; + } + + return result_TEST_PASSED; +} + +/* ----------------------------------------------------------------------- */ + result_t screen_test(const char *resources) { typedef result_t (*screentestfn)(void); @@ -525,7 +643,9 @@ result_t screen_test(const char *resources) test_wu_fix8_extreme_coords, test_ninepatch, test_fill_pattern, - test_dashed_line + test_dashed_line, + test_draw_lines, + test_draw_rect }; result_t rc; diff --git a/libraries/framebuf/span/p4.c b/libraries/framebuf/span/p4.c index 80cfca35..d35e0834 100644 --- a/libraries/framebuf/span/p4.c +++ b/libraries/framebuf/span/p4.c @@ -14,7 +14,7 @@ /* pixel values here are unpacked palette indices, one per byte (not the * two-nibbles-per-byte layout used in screen memory); packing/unpacking * into the actual screen bytes is the caller's job, as with - * screen_draw_pixel's other pixel formats. src2 is an array of colour_t, + * 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_p4_blendconst(void *vdst, diff --git a/libraries/wuss/backdrop.c b/libraries/wuss/backdrop.c index ae691866..cc7071ca 100644 --- a/libraries/wuss/backdrop.c +++ b/libraries/wuss/backdrop.c @@ -34,7 +34,7 @@ void wuss__fill_backdrop(screen_t *scr, return; if (backdrop->pattern == screen_PATTERN_SOLID) - screen_draw_rect(scr, area->x0, area->y0, box_size(area), + screen_fill_rect(scr, area->x0, area->y0, box_size(area), palette[backdrop->colour]); else screen_fill_pattern(scr, area, backdrop->pattern, origin_x, origin_y, diff --git a/libraries/wuss/furniture/draw.c b/libraries/wuss/furniture/draw.c index 98a7c657..d7433bf6 100644 --- a/libraries/wuss/furniture/draw.c +++ b/libraries/wuss/furniture/draw.c @@ -22,7 +22,7 @@ void wuss__furniture_draw(wuss_t *wuss, if (!box_intersection(&titlebar, full, &clipped)) { wuss->scr->clip = clipped; - screen_draw_rect(wuss->scr, + screen_fill_rect(wuss->scr, titlebar.x0, titlebar.y0, box_size(&titlebar), wuss->palette[wuss->furniture_colours.title.bg]); @@ -86,7 +86,7 @@ void wuss__furniture_draw(wuss_t *wuss, box_t close; wuss__close_box(window, &close); - screen_draw_rect(wuss->scr, + screen_fill_rect(wuss->scr, close.x0, close.y0, box_size(&close), wuss->palette[wuss->furniture_colours.close]); } @@ -96,7 +96,7 @@ void wuss__furniture_draw(wuss_t *wuss, box_t back; wuss__back_box(window, &back); - screen_draw_rect(wuss->scr, + screen_fill_rect(wuss->scr, back.x0, back.y0, box_size(&back), wuss->palette[wuss->furniture_colours.back]); } @@ -106,7 +106,7 @@ void wuss__furniture_draw(wuss_t *wuss, box_t toggle; wuss__toggle_box(window, &toggle); - screen_draw_rect(wuss->scr, + screen_fill_rect(wuss->scr, toggle.x0, toggle.y0, box_size(&toggle), wuss->palette[wuss->furniture_colours.toggle]); } @@ -120,7 +120,7 @@ void wuss__furniture_draw(wuss_t *wuss, if (!box_intersection(&resize, full, &clipped)) { wuss->scr->clip = clipped; - screen_draw_rect(wuss->scr, + screen_fill_rect(wuss->scr, resize.x0, resize.y0, box_size(&resize), wuss->palette[wuss->furniture_colours.resize]); } @@ -134,7 +134,7 @@ void wuss__furniture_draw(wuss_t *wuss, if (!box_intersection(&up, full, &clipped)) { wuss->scr->clip = clipped; - screen_draw_rect(wuss->scr, up.x0, up.y0, box_size(&up), + screen_fill_rect(wuss->scr, up.x0, up.y0, box_size(&up), wuss->palette[wuss->furniture_colours.scroll.arrows]); } @@ -142,7 +142,7 @@ void wuss__furniture_draw(wuss_t *wuss, if (!box_intersection(&down, full, &clipped)) { wuss->scr->clip = clipped; - screen_draw_rect(wuss->scr, down.x0, down.y0, box_size(&down), + screen_fill_rect(wuss->scr, down.x0, down.y0, box_size(&down), wuss->palette[wuss->furniture_colours.scroll.arrows]); } @@ -150,7 +150,7 @@ void wuss__furniture_draw(wuss_t *wuss, if (!box_intersection(&well, full, &clipped)) { wuss->scr->clip = clipped; - screen_draw_rect(wuss->scr, well.x0, well.y0, box_size(&well), + screen_fill_rect(wuss->scr, well.x0, well.y0, box_size(&well), wuss->palette[wuss->furniture_colours.scroll.wells]); } @@ -158,7 +158,7 @@ void wuss__furniture_draw(wuss_t *wuss, if (!box_intersection(&sausage, full, &clipped)) { wuss->scr->clip = clipped; - screen_draw_rect(wuss->scr, sausage.x0, sausage.y0, box_size(&sausage), + screen_fill_rect(wuss->scr, sausage.x0, sausage.y0, box_size(&sausage), wuss->palette[wuss->furniture_colours.scroll.sausages]); } } @@ -171,7 +171,7 @@ void wuss__furniture_draw(wuss_t *wuss, if (!box_intersection(&left, full, &clipped)) { wuss->scr->clip = clipped; - screen_draw_rect(wuss->scr, left.x0, left.y0, box_size(&left), + screen_fill_rect(wuss->scr, left.x0, left.y0, box_size(&left), wuss->palette[wuss->furniture_colours.scroll.arrows]); } @@ -179,7 +179,7 @@ void wuss__furniture_draw(wuss_t *wuss, if (!box_intersection(&right, full, &clipped)) { wuss->scr->clip = clipped; - screen_draw_rect(wuss->scr, right.x0, right.y0, box_size(&right), + screen_fill_rect(wuss->scr, right.x0, right.y0, box_size(&right), wuss->palette[wuss->furniture_colours.scroll.arrows]); } @@ -187,7 +187,7 @@ void wuss__furniture_draw(wuss_t *wuss, if (!box_intersection(&well, full, &clipped)) { wuss->scr->clip = clipped; - screen_draw_rect(wuss->scr, well.x0, well.y0, box_size(&well), + screen_fill_rect(wuss->scr, well.x0, well.y0, box_size(&well), wuss->palette[wuss->furniture_colours.scroll.wells]); } @@ -195,7 +195,7 @@ void wuss__furniture_draw(wuss_t *wuss, if (!box_intersection(&sausage, full, &clipped)) { wuss->scr->clip = clipped; - screen_draw_rect(wuss->scr, sausage.x0, sausage.y0, box_size(&sausage), + screen_fill_rect(wuss->scr, sausage.x0, sausage.y0, box_size(&sausage), wuss->palette[wuss->furniture_colours.scroll.sausages]); } } @@ -218,7 +218,7 @@ void wuss__furniture_draw(wuss_t *wuss, if (!box_intersection(&rule, full, &clipped)) { wuss->scr->clip = clipped; - screen_draw_rect(wuss->scr, rule.x0, rule.y0, box_size(&rule), + screen_fill_rect(wuss->scr, rule.x0, rule.y0, box_size(&rule), wuss->palette[wuss->furniture_colours.title.bg]); } } @@ -232,7 +232,7 @@ void wuss__furniture_draw(wuss_t *wuss, if (!box_intersection(&rule, full, &clipped)) { wuss->scr->clip = clipped; - screen_draw_rect(wuss->scr, rule.x0, rule.y0, box_size(&rule), + screen_fill_rect(wuss->scr, rule.x0, rule.y0, box_size(&rule), wuss->palette[wuss->furniture_colours.title.bg]); } } @@ -248,9 +248,9 @@ void wuss__furniture_draw(wuss_t *wuss, border = wuss->palette[wuss->furniture_colours.title.bg]; /* no dedicated outline class; matches titlebar chrome */ wuss->scr->clip = visible_clipped; - screen_draw_rect(wuss->scr, window->visible.x0, window->visible.y0, SIZE2D(width, 1), border); - screen_draw_rect(wuss->scr, window->visible.x0, window->visible.y1 - 1, SIZE2D(width, 1), border); - screen_draw_rect(wuss->scr, window->visible.x0, window->visible.y0, SIZE2D(1, height), border); - screen_draw_rect(wuss->scr, window->visible.x1 - 1, window->visible.y0, SIZE2D(1, height), border); + 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); } } diff --git a/libraries/wuss/icon/draw.c b/libraries/wuss/icon/draw.c index 29d5f320..729aa7f0 100644 --- a/libraries/wuss/icon/draw.c +++ b/libraries/wuss/icon/draw.c @@ -37,7 +37,7 @@ static void icon_bevel(screen_t *scr, colour_t light, colour_t dark) { - screen_draw_rect(scr, b->x0, b->y0, + 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); @@ -99,7 +99,7 @@ static void wuss__icon_draw_label(const icon_draw_ctx_t *c) if (icon->bg != wuss_NO_BACKGROUND) { bg = c->wuss->palette[icon->bg]; - screen_draw_rect(c->scr, b->x0, b->y0, + screen_fill_rect(c->scr, b->x0, b->y0, SIZE2D(b->x1 - b->x0, b->y1 - b->y0), bg); } else @@ -274,7 +274,7 @@ static void wuss__icon_draw_radio_option(const icon_draw_ctx_t *c) bg = icon_blend_ground(c, glyph); if (icon->bg != wuss_NO_BACKGROUND) - screen_draw_rect(c->scr, b->x0, b->y0, + 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) @@ -285,16 +285,14 @@ static void wuss__icon_draw_radio_option(const icon_draw_ctx_t *c) screen_draw_line(c->scr, g.x0, g.y0 + 2, g.x0, g.y1 - 3, glyph); screen_draw_line(c->scr, g.x1 - 1, g.y0 + 2, g.x1 - 1, g.y1 - 3, glyph); if (icon->selected) - screen_draw_rect(c->scr, g.x0 + 3, g.y0 + 3, + screen_fill_rect(c->scr, g.x0 + 3, g.y0 + 3, SIZE2D(gsz - 6, gsz - 6), glyph); } else { /* a box; a tick (two strokes) when selected */ - screen_draw_line(c->scr, g.x0, g.y0, g.x1 - 1, g.y0, glyph); - screen_draw_line(c->scr, g.x0, g.y1 - 1, g.x1 - 1, g.y1 - 1, glyph); - screen_draw_line(c->scr, g.x0, g.y0, g.x0, g.y1 - 1, glyph); - screen_draw_line(c->scr, g.x1 - 1, g.y0, g.x1 - 1, g.y1 - 1, glyph); + screen_draw_rect(c->scr, g.x0, g.y0, + SIZE2D(g.x1 - g.x0, g.y1 - g.y0), glyph); if (icon->selected) { screen_draw_line(c->scr, g.x0 + 2, g.y0 + gsz / 2, @@ -372,7 +370,7 @@ static void wuss__icon_draw_menu_entry(const icon_draw_ctx_t *c) } if (highlit || icon->bg != wuss_NO_BACKGROUND) - screen_draw_rect(c->scr, b->x0, b->y0, + screen_fill_rect(c->scr, b->x0, b->y0, SIZE2D(b->x1 - b->x0, b->y1 - b->y0), ground); /* a dashed rule along the entry's top edge, then the text as normal; drawn in diff --git a/libraries/wuss/test/tasks/ball.c b/libraries/wuss/test/tasks/ball.c index e1f34970..12cd4083 100644 --- a/libraries/wuss/test/tasks/ball.c +++ b/libraries/wuss/test/tasks/ball.c @@ -57,7 +57,7 @@ static result_t ball_redraw(const wuss_event_t *event, void *task_data) sx = event->data.redraw.scroll.x; sy = event->data.redraw.scroll.y; - screen_draw_rect(scr, content->x0, content->y0, box_size(content), + screen_fill_rect(scr, content->x0, content->y0, box_size(content), bc->bg); for (i = 0; i < bc->nballs; i++) @@ -66,7 +66,7 @@ static result_t ball_redraw(const wuss_event_t *event, void *task_data) b = &bc->balls[i]; - screen_draw_rect(scr, bounds->x0 - sx + b->x - b->radius, bounds->y0 - sy + b->y - b->radius, SIZE2D(b->radius * 2, b->radius * 2), bc->ball); + screen_fill_rect(scr, bounds->x0 - sx + b->x - b->radius, bounds->y0 - sy + b->y - b->radius, SIZE2D(b->radius * 2, b->radius * 2), bc->ball); } return result_OK; diff --git a/libraries/wuss/test/tasks/chars.c b/libraries/wuss/test/tasks/chars.c index ec530665..5b12ea80 100644 --- a/libraries/wuss/test/tasks/chars.c +++ b/libraries/wuss/test/tasks/chars.c @@ -101,7 +101,7 @@ static result_t chars_redraw(const wuss_event_t *event, void *task_data) x = bounds->x0 - sx + col * cell_w; y = bounds->y0 - sy + row * cell_h; - screen_draw_rect(scr, x, y, SIZE2D(cell_w, cell_h), cc->bg); + screen_fill_rect(scr, x, y, SIZE2D(cell_w, cell_h), cc->bg); if (i < first || i >= first + count) continue; /* no glyph for this byte value: leave the cell blank */ diff --git a/libraries/wuss/test/tasks/checker.c b/libraries/wuss/test/tasks/checker.c index 04f2bfda..f8163a3d 100644 --- a/libraries/wuss/test/tasks/checker.c +++ b/libraries/wuss/test/tasks/checker.c @@ -100,7 +100,7 @@ static result_t checker_redraw(wuss_window_t *window, default: band = lx / band_px + ly / band_px; break; /* CHECKERBOARD */ } - screen_draw_pixel(scr, x, y, (band & 1) ? cc->black : cc->white); + screen_set_pixel(scr, x, y, (band & 1) ? cc->black : cc->white); } } diff --git a/libraries/wuss/test/tasks/curve.c b/libraries/wuss/test/tasks/curve.c index ce17708f..d99b430b 100644 --- a/libraries/wuss/test/tasks/curve.c +++ b/libraries/wuss/test/tasks/curve.c @@ -75,7 +75,7 @@ static result_t curve_redraw(const wuss_event_t *event, curve_task_t *task) sx = event->data.redraw.scroll.x; sy = event->data.redraw.scroll.y; - screen_draw_rect(scr, content->x0, content->y0, box_size(content), + screen_fill_rect(scr, content->x0, content->y0, box_size(content), task->bg); prev = task->points[0]; @@ -98,7 +98,7 @@ static result_t curve_redraw(const wuss_event_t *event, curve_task_t *task) { cur = task->points[i]; cur.x += bounds->x0 - sx; cur.y += bounds->y0 - sy; - screen_draw_square(scr, cur.x - half, cur.y - half, CURVE_BLOBSZ, task->blob); + screen_fill_square(scr, cur.x - half, cur.y - half, CURVE_BLOBSZ, task->blob); } return result_OK; diff --git a/libraries/wuss/test/tasks/gradient.c b/libraries/wuss/test/tasks/gradient.c index e536128b..b8b746ee 100644 --- a/libraries/wuss/test/tasks/gradient.c +++ b/libraries/wuss/test/tasks/gradient.c @@ -71,7 +71,7 @@ static result_t gradient_redraw(const wuss_event_t *event, void *task_data) lx = x - bounds->x0 + sx; ly = y - bounds->y0 + sy; - screen_draw_pixel(scr, x, y, + 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))); diff --git a/libraries/wuss/test/tasks/lissajous.c b/libraries/wuss/test/tasks/lissajous.c index bcf94b00..cae15c91 100644 --- a/libraries/wuss/test/tasks/lissajous.c +++ b/libraries/wuss/test/tasks/lissajous.c @@ -65,7 +65,7 @@ static result_t lissajous_redraw(const wuss_event_t *event, void *task_data) sx = event->data.redraw.scroll.x; sy = event->data.redraw.scroll.y; - screen_draw_rect(scr, content->x0, content->y0, box_size(content), lc->bg); + screen_fill_rect(scr, content->x0, content->y0, box_size(content), lc->bg); width = bounds->x1 - bounds->x0; height = bounds->y1 - bounds->y0; @@ -83,7 +83,7 @@ static result_t lissajous_redraw(const wuss_event_t *event, void *task_data) px = cx + (int) (rx * sin(lc->a * t + lc->phase)); py = cy + (int) (ry * sin(lc->b * t)); - screen_draw_pixel(scr, px, py, lc->fg); + screen_set_pixel(scr, px, py, lc->fg); } return result_OK; diff --git a/libraries/wuss/test/tasks/palette.c b/libraries/wuss/test/tasks/palette.c index 69cb38fa..0bf9cde2 100644 --- a/libraries/wuss/test/tasks/palette.c +++ b/libraries/wuss/test/tasks/palette.c @@ -73,7 +73,7 @@ static result_t palette_redraw(const wuss_event_t *event, void *task_data) x = bounds->x0 - sx + col * cell_w; y = bounds->y0 - sy + row * cell_h; - screen_draw_rect(scr, x, y, SIZE2D(cell_w, cell_h), pc->palette[i]); + screen_fill_rect(scr, x, y, SIZE2D(cell_w, cell_h), pc->palette[i]); } return result_OK; diff --git a/libraries/wuss/test/tasks/porter-duff.c b/libraries/wuss/test/tasks/porter-duff.c index d961388a..d7d82902 100644 --- a/libraries/wuss/test/tasks/porter-duff.c +++ b/libraries/wuss/test/tasks/porter-duff.c @@ -275,7 +275,7 @@ static void porter_duff_draw_checkerboard(const porter_duff_task_t *pd, ly = y - bounds->y0; band = lx / PD_CHECKER_BAND + ly / PD_CHECKER_BAND; - screen_draw_pixel(scr, x, y, (band & 1) ? pd->dark : pd->light); + screen_set_pixel(scr, x, y, (band & 1) ? pd->dark : pd->light); } } diff --git a/libraries/wuss/test/tasks/sofa.c b/libraries/wuss/test/tasks/sofa.c index a8521b57..c3341764 100644 --- a/libraries/wuss/test/tasks/sofa.c +++ b/libraries/wuss/test/tasks/sofa.c @@ -347,7 +347,7 @@ static void draw_vertex_dots(screen_t *scr, half = SOFA_VERTEX_DOT / 2; for (i = 0; i < nvertices; i++) - screen_draw_square(scr, + screen_fill_square(scr, FIX8_ROUND_TO_INT(screen[i].x) - half, FIX8_ROUND_TO_INT(screen[i].y) - half, SOFA_VERTEX_DOT, @@ -397,7 +397,7 @@ static result_t sofa_redraw(const wuss_event_t *event, void *task_data) sx = event->data.redraw.scroll.x; sy = event->data.redraw.scroll.y; - screen_draw_rect(scr, + screen_fill_rect(scr, content->x0, content->y0, box_size(content), sc->bg); From 143654d4f5f807b0d914ca12da6d8514cc4d7f93 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Tue, 1 Sep 2026 12:11:27 +0100 Subject: [PATCH 131/287] refactor(wuss): route core->furniture calls through a dispatch table Introduce wuss__furniture_ops_t, a struct of the seven furniture entry points the wuss core calls (hit_test, draw, invalidate, invalidate_for, toggle_size, drag_resize, drag_sausage). struct wuss holds a pointer to one, defaulted by wuss_create to wuss__furniture_default_ops (the existing built-in furniture, now named in furniture/ops.c). The twelve core call sites in redraw, mouse-click, mouse-move and window/{resize,set-scroll} dispatch through the table instead of calling the functions by name. The inline geometry helpers in impl.h and the wuss__furniture_drag_kind switch stay direct: they are the compile-time furniture/no-furniture seam, not a runtime policy seam. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- CMakeLists.txt | 1 + libraries/wuss/create.c | 1 + libraries/wuss/furniture.h | 25 +++++++++++++++++++++++++ libraries/wuss/furniture/ops.c | 16 ++++++++++++++++ libraries/wuss/impl.h | 5 ++++- libraries/wuss/mouse-click.c | 4 ++-- libraries/wuss/mouse-move.c | 8 ++++---- libraries/wuss/redraw.c | 2 +- libraries/wuss/window/resize.c | 6 +++--- libraries/wuss/window/set-scroll.c | 2 +- 10 files changed, 58 insertions(+), 12 deletions(-) create mode 100644 libraries/wuss/furniture/ops.c diff --git a/CMakeLists.txt b/CMakeLists.txt index d862008f..b01414d8 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -371,6 +371,7 @@ set(WUSS_FURNITURE_SOURCES libraries/wuss/furniture/hit-test.c libraries/wuss/furniture/hscroll-box.c libraries/wuss/furniture/invalidate.c + libraries/wuss/furniture/ops.c libraries/wuss/furniture/resize-box.c libraries/wuss/furniture/scroll-action.c libraries/wuss/furniture/titlebar-box.c diff --git a/libraries/wuss/create.c b/libraries/wuss/create.c index ad11546d..027e3ec6 100644 --- a/libraries/wuss/create.c +++ b/libraries/wuss/create.c @@ -245,6 +245,7 @@ result_t wuss_create(screen_t *scr, w->furniture.dragging = NULL; w->furniture.drag.x = 0; w->furniture.drag.y = 0; + w->furniture_ops = &wuss__furniture_default_ops; #endif #ifdef WUSS_ICONS w->pressed_icon = NULL; diff --git a/libraries/wuss/furniture.h b/libraries/wuss/furniture.h index 22744f08..0bf378b9 100644 --- a/libraries/wuss/furniture.h +++ b/libraries/wuss/furniture.h @@ -85,6 +85,31 @@ void wuss__furniture_invalidate(wuss_window_t *window); void wuss__furniture_invalidate_for(wuss_window_t *window, const box_t *visible); +/* Dispatch table the wuss core reaches window furniture through: everything + * the core (z-order, mouse routing, redraw, window lifecycle) calls that + * lives in the furniture .c files. A caller could point wuss::furniture_ops at its + * own table to replace the furniture wholesale; wuss_create defaults it to + * wuss__furniture_default_ops. The geometry helpers in impl.h + * (wuss__titlebar_box, wuss__content_box, wuss__furniture_carve_for, the + * titlebar-height/button-size inlines) are a compile-time seam between the + * furniture and no-furniture builds and stay out of this table. */ +typedef struct wuss__furniture_ops +{ + wuss_furniture_region_t (*hit_test)(const wuss_window_t *window, point_t p); + void (*draw)(wuss_t *wuss, wuss_window_t *window, const box_t *full); + void (*invalidate)(wuss_window_t *window); + void (*invalidate_for)(wuss_window_t *window, const box_t *visible); + void (*toggle_size)(wuss_window_t *window); + void (*drag_resize)(wuss_window_t *window, point_t p); + void (*drag_sausage)(wuss_window_t *window, + int delta_px, + int scroll_start, + int horizontal); +} +wuss__furniture_ops_t; + +extern const wuss__furniture_ops_t wuss__furniture_default_ops; + /* geometry: titlebar icons */ void wuss__back_box(const wuss_window_t *window, box_t *out); void wuss__toggle_box(const wuss_window_t *window, box_t *out); diff --git a/libraries/wuss/furniture/ops.c b/libraries/wuss/furniture/ops.c new file mode 100644 index 00000000..15789161 --- /dev/null +++ b/libraries/wuss/furniture/ops.c @@ -0,0 +1,16 @@ +/* wuss/furniture/ops.c -- wuss - core-to-furniture dispatch table */ + +#include "../impl.h" + +/* The built-in furniture: wuss_create points every window's furniture_ops + * here unless a caller overrides it. */ +const wuss__furniture_ops_t wuss__furniture_default_ops = +{ + wuss__furniture_hit_test, + wuss__furniture_draw, + wuss__furniture_invalidate, + wuss__furniture_invalidate_for, + wuss__furniture_toggle_size, + wuss__furniture_drag_resize, + wuss__furniture_drag_sausage +}; diff --git a/libraries/wuss/impl.h b/libraries/wuss/impl.h index 7046e5c5..9e308f8e 100644 --- a/libraries/wuss/impl.h +++ b/libraries/wuss/impl.h @@ -82,7 +82,10 @@ struct wuss #endif list_t z_order; /* anchor; head = topmost window */ #ifdef WUSS_FURNITURE - struct wuss__furniture furniture; + struct wuss__furniture furniture; /* drag state */ + const wuss__furniture_ops_t *furniture_ops; /* core->furniture dispatch; + * wuss_create defaults it to + * wuss__furniture_default_ops */ #endif box_t dirty[WUSS_MAX_DIRTY]; /* accumulated by wuss_invalidate; reset by a redraw */ int ndirty; diff --git a/libraries/wuss/mouse-click.c b/libraries/wuss/mouse-click.c index f6f961fb..d0bf85da 100644 --- a/libraries/wuss/mouse-click.c +++ b/libraries/wuss/mouse-click.c @@ -66,7 +66,7 @@ result_t wuss_mouse_click(wuss_t *wuss, { wuss_furniture_region_t region; - region = wuss__furniture_hit_test(win, POINT(x, y)); + region = wuss->furniture_ops->hit_test(win, POINT(x, y)); if (region == wuss_FURNITURE_CLOSE && action == wuss_MOUSE_DOWN && @@ -111,7 +111,7 @@ result_t wuss_mouse_click(wuss_t *wuss, { case wuss_FURNITURE_TOGGLE_SIZE: if (button & wuss_BUTTON_SELECT) - wuss__furniture_toggle_size(win); + wuss->furniture_ops->toggle_size(win); break; case wuss_FURNITURE_VSCROLL_UP: wuss__scroll_step(win, POINT(0, -step)); diff --git a/libraries/wuss/mouse-move.c b/libraries/wuss/mouse-move.c index d1546b65..a0495cf5 100644 --- a/libraries/wuss/mouse-move.c +++ b/libraries/wuss/mouse-move.c @@ -22,15 +22,15 @@ result_t wuss_mouse_move(wuss_t *wuss, point_t p, wuss_window_t **hit) switch (wuss->furniture.drag_kind) { case wuss_FURNITURE_DRAG_RESIZE: - wuss__furniture_drag_resize(win, POINT(x, y)); + wuss->furniture_ops->drag_resize(win, POINT(x, y)); break; case wuss_FURNITURE_DRAG_VSCROLL_SAUSAGE: - wuss__furniture_drag_sausage(win, y - wuss->furniture.drag.y, wuss->furniture.drag_scroll_start, 0); + wuss->furniture_ops->drag_sausage(win, y - wuss->furniture.drag.y, wuss->furniture.drag_scroll_start, 0); break; case wuss_FURNITURE_DRAG_HSCROLL_SAUSAGE: - wuss__furniture_drag_sausage(win, x - wuss->furniture.drag.x, wuss->furniture.drag_scroll_start, 1); + wuss->furniture_ops->drag_sausage(win, x - wuss->furniture.drag.x, wuss->furniture.drag_scroll_start, 1); break; case wuss_FURNITURE_DRAG_MOVE: @@ -56,7 +56,7 @@ result_t wuss_mouse_move(wuss_t *wuss, point_t p, wuss_window_t **hit) } #ifdef WUSS_FURNITURE - if (wuss__furniture_hit_test(win, POINT(x, y)) != wuss_FURNITURE_CONTENT) + if (wuss->furniture_ops->hit_test(win, POINT(x, y)) != wuss_FURNITURE_CONTENT) { #ifdef WUSS_ICONS wuss__icon_set_hover(wuss, NULL); diff --git a/libraries/wuss/redraw.c b/libraries/wuss/redraw.c index 52ad93f0..cca90011 100644 --- a/libraries/wuss/redraw.c +++ b/libraries/wuss/redraw.c @@ -17,7 +17,7 @@ static void redraw_window(wuss_t *wuss, return; /* offscreen */ #ifdef WUSS_FURNITURE - wuss__furniture_draw(wuss, win, full); + wuss->furniture_ops->draw(wuss, win, full); #endif wuss__content_box(win, &content); diff --git a/libraries/wuss/window/resize.c b/libraries/wuss/window/resize.c index e6a961cb..9c8e7368 100644 --- a/libraries/wuss/window/resize.c +++ b/libraries/wuss/window/resize.c @@ -113,7 +113,7 @@ result_t wuss_window_resize(wuss_window_t *window, size2d_t size) wuss__invalidate_clipped(window, &content); } #ifdef WUSS_FURNITURE - wuss__furniture_invalidate(window); + window->wuss->furniture_ops->invalidate(window); #endif } @@ -146,8 +146,8 @@ result_t wuss_window_resize(wuss_window_t *window, size2d_t size) #ifdef WUSS_FURNITURE if (window->visible.x1 - window->visible.x0 > before.x1 - before.x0 || window->visible.y1 - window->visible.y0 > before.y1 - before.y0) - wuss__furniture_invalidate_for(window, &before); - wuss__furniture_invalidate(window); + window->wuss->furniture_ops->invalidate_for(window, &before); + window->wuss->furniture_ops->invalidate(window); #endif } diff --git a/libraries/wuss/window/set-scroll.c b/libraries/wuss/window/set-scroll.c index 4e7f4732..0acf264c 100644 --- a/libraries/wuss/window/set-scroll.c +++ b/libraries/wuss/window/set-scroll.c @@ -22,7 +22,7 @@ void wuss_window_set_scroll(wuss_window_t *window, point_t p) #ifdef WUSS_FURNITURE /* the scrollbar sausage position depends on scroll, so its well needs * redrawing too -- content invalidation alone never touches it */ - wuss__furniture_invalidate(window); + window->wuss->furniture_ops->invalidate(window); #endif /* The valid blit source is the content box minus whatever windows above it From 74d23fed26f179e09888c8fc96b3fa686e3edfea Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Tue, 1 Sep 2026 12:34:07 +0100 Subject: [PATCH 132/287] feat(wuss): make menu separators their own short inert row A separator is no longer a dashed rule drawn above a still-pickable label. The '|' descriptor prefix now emits a standalone DASHED item (empty text) before the real one, laid out at its own short height and skipped by hit-testing, so the pointer never interacts with it. Also route create-from-desc's item array growth through array_grow instead of a hand-rolled doubling realloc. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- include/wuss/icon.h | 12 +++--- include/wuss/menu.h | 25 ++++++------ libraries/wuss/icon/draw.c | 6 +-- libraries/wuss/icon/hit-test.c | 3 +- libraries/wuss/menu/create-from-desc.c | 53 ++++++++++++++------------ libraries/wuss/menu/menu.c | 23 +++++++++-- libraries/wuss/test/wuss-test.c | 13 ++++--- 7 files changed, 77 insertions(+), 58 deletions(-) diff --git a/include/wuss/icon.h b/include/wuss/icon.h index 8a6f9b43..ff3bcd94 100644 --- a/include/wuss/icon.h +++ b/include/wuss/icon.h @@ -144,13 +144,11 @@ typedef enum wuss_icon_flags * edge, marking an entry that opens * a submenu. Ignored by other * types. */ - wuss_ICON_FLAGS_SEPARATOR = 1 << 7 /**< wuss_ICON_TYPE_MENU_ENTRY: draw a - * dashed rule along the entry's top - * edge, then the text as normal; the - * entry stays interactive. An entry - * carrying the flag with no text is - * a bare rule and is not hit-tested. - * Ignored by other types. */ + wuss_ICON_FLAGS_SEPARATOR = 1 << 7 /**< wuss_ICON_TYPE_MENU_ENTRY: the + * entry is a separator -- a centred + * dashed rule, no text, and not + * hit-tested. Ignored by other + * types. */ } wuss_icon_flags_t; diff --git a/include/wuss/menu.h b/include/wuss/menu.h index 463fad80..759fd645 100644 --- a/include/wuss/menu.h +++ b/include/wuss/menu.h @@ -35,9 +35,10 @@ typedef enum wuss_menu_item_flags { wuss_MENU_ITEM_NONE = 0, wuss_MENU_ITEM_TICKED = 1 << 0, /**< draw a tick at the item's left edge */ - wuss_MENU_ITEM_DASHED = 1 << 1, /**< draw a dashed separator immediately - * above this item; the item keeps its - * own text and stays selectable */ + wuss_MENU_ITEM_DASHED = 1 << 1, /**< this item is a separator: its own + * short row holding a centred dashed + * rule. The text is ignored and the row + * does not respond to the pointer */ wuss_MENU_ITEM_DISABLED = 1 << 2 /**< greyed, never highlights, not * selectable */ } @@ -118,17 +119,17 @@ int wuss_menu_is_open(wuss_menu_handle_t handle); * as the first token inside a '{ }' is that submenu's title -- so the same * descriptor strings port across unchanged. Submenus keep the Wimp behaviour of * discarding their title token; only the root's is kept. A leading '|' on an - * item sets wuss_MENU_ITEM_DASHED on it: a dashed rule is drawn above the item - * while the item keeps its own label and stays selectable. A '{ ... }' group - * after an item is that item's submenu. A per-token prefix '!' ticks the item, - * '~' shades (disables) it, and '>' attaches a submenu pulled as a <tt>const - * wuss_menu_t *</tt> from the varargs rather than from a following '{ }' block. - * A "%s" in a token substitutes the next <tt>const char *</tt> vararg. The '>' - * and "%s" varargs are consumed in the order they are encountered scanning left - * to right. + * item inserts a separator row above it: its own short, inert row holding a + * centred dashed rule. A '{ ... }' group after an item is that item's submenu. + * A per-token prefix '!' ticks the item, '~' shades (disables) it, and '>' + * attaches a submenu pulled as a <tt>const wuss_menu_t *</tt> from the varargs + * rather than from a following '{ }' block. A "%s" in a token substitutes the + * next <tt>const char *</tt> vararg. The '>' and "%s" varargs are consumed in + * the order they are encountered scanning left to right. * * Example: <tt>wuss_menu_create_from_desc(&m, "Display, Open, !Grid, ~Export, - * |Quit")</tt> -- "Display" is the caption, the menu has four rows. + * |Quit")</tt> -- "Display" is the caption; the menu has four items plus a + * separator row before "Quit". * * The whole tree, including copied label text, is one heap allocation graph * owned by the caller; free it with wuss_menu_destroy. wuss_menu_open treats a diff --git a/libraries/wuss/icon/draw.c b/libraries/wuss/icon/draw.c index 729aa7f0..796017ea 100644 --- a/libraries/wuss/icon/draw.c +++ b/libraries/wuss/icon/draw.c @@ -373,11 +373,11 @@ static void wuss__icon_draw_menu_entry(const icon_draw_ctx_t *c) screen_fill_rect(c->scr, b->x0, b->y0, SIZE2D(b->x1 - b->x0, b->y1 - b->y0), ground); - /* a dashed rule along the entry's top edge, then the text as normal; drawn in - * the row's ink so it stays visible when the row is inverted */ + /* a separator is its own short, textless row: a dashed rule centred in it, + * drawn in the row's ink so it stays visible if the row is inverted */ if (icon->flags & wuss_ICON_FLAGS_SEPARATOR) { - mid_y = b->y0 + 1; + mid_y = b->y0 + (b->y1 - b->y0) / 2; screen_draw_dashed_line(c->scr, b->x0 + pad, mid_y, b->x1 - 1 - pad, mid_y, 2, 2, ink); } diff --git a/libraries/wuss/icon/hit-test.c b/libraries/wuss/icon/hit-test.c index b4045fbb..78b87618 100644 --- a/libraries/wuss/icon/hit-test.c +++ b/libraries/wuss/icon/hit-test.c @@ -15,8 +15,7 @@ wuss_icon_t *wuss__icon_hit_test(wuss_window_t *window, point_t doc_point) it = window->icons[i]; /* a bitmap icon is interactive only when it asks to be; a menu entry that - * is a bare rule (SEPARATOR flag, no text) is inert, but one that only - * carries a rule above its own label stays pickable; the other clickable + * is a separator (SEPARATOR flag, no text) is inert; the other clickable * types always are */ if (it->type == wuss_ICON_TYPE_BITMAP) { diff --git a/libraries/wuss/menu/create-from-desc.c b/libraries/wuss/menu/create-from-desc.c index 1d8c16ab..f0a6f0c9 100644 --- a/libraries/wuss/menu/create-from-desc.c +++ b/libraries/wuss/menu/create-from-desc.c @@ -20,6 +20,7 @@ #endif #include "base/result.h" +#include "utils/array.h" #include "wuss/menu.h" @@ -208,28 +209,17 @@ typedef struct } Building; -static result_t build_add(Building *b, - const char *text, - unsigned int opt, - wuss_menu_t *submenu) +static result_t build_emit(Building *b, + const char *text, + unsigned int flags, + wuss_menu_t *submenu) { wuss_menu_item_t *it; char *copy; - if (b->n == b->cap) - { - int cap; - wuss_menu_item_t *grown; - - /* ponytail: double on demand, no shrink-to-fit -- the array is small and - * freed as one block, trailing slack costs nothing */ - cap = b->cap ? b->cap * 2 : 4; - grown = realloc(b->items, (size_t) cap * sizeof(*grown)); - if (grown == NULL) - return result_OOM; - b->items = grown; - b->cap = cap; - } + if (array_grow((void **) &b->items, sizeof(*b->items), + b->n, &b->cap, 1, 4)) + return result_OOM; copy = strdup(text ? text : ""); if (copy == NULL) @@ -237,23 +227,36 @@ static result_t build_add(Building *b, it = &b->items[b->n++]; it->text = copy; - it->flags = wuss_MENU_ITEM_NONE; + it->flags = flags; it->submenu = submenu; + return result_OK; +} - /* '|' before this item means "draw a dashed rule above it"; the item keeps - * its own text and stays pickable. */ +static result_t build_add(Building *b, + const char *text, + unsigned int opt, + wuss_menu_t *submenu) +{ + unsigned int flags; + result_t rc; + + /* '|' before this item means "insert a separator row above it": a distinct + * DASHED item with no text of its own, emitted before the real one. */ if (b->pending_dash) { - it->flags |= wuss_MENU_ITEM_DASHED; b->pending_dash = 0; + rc = build_emit(b, "", wuss_MENU_ITEM_DASHED, NULL); + if (rc != result_OK) + return rc; } + flags = wuss_MENU_ITEM_NONE; if (opt & Tick) - it->flags |= wuss_MENU_ITEM_TICKED; + flags |= wuss_MENU_ITEM_TICKED; if (opt & Shade) - it->flags |= wuss_MENU_ITEM_DISABLED; + flags |= wuss_MENU_ITEM_DISABLED; - return result_OK; + return build_emit(b, text, flags, submenu); } /* Seal a Building into a heap wuss_menu_t, transferring its item array. */ diff --git a/libraries/wuss/menu/menu.c b/libraries/wuss/menu/menu.c index 15cdaa8e..6fdccc8d 100644 --- a/libraries/wuss/menu/menu.c +++ b/libraries/wuss/menu/menu.c @@ -177,6 +177,8 @@ static result_t wuss__menu_spawn(wuss_t *wuss, result_t rc; int fw, fh; int pitch; + int sep_h; + int y; int widest; int width, height; int i; @@ -193,6 +195,7 @@ static result_t wuss__menu_spawn(wuss_t *wuss, bmfont_get_info(wuss->font, &fw, &fh); NOT_USED(fw); pitch = fh + 2 * WUSS_MENU_ROW_PAD; + sep_h = 2 * WUSS_MENU_ROW_PAD; widest = 0; for (i = 0; i < menu->nitems; i++) @@ -212,7 +215,10 @@ static result_t wuss__menu_spawn(wuss_t *wuss, } width = WUSS_MENU_TICK_W + widest + WUSS_MENU_TEXT_PAD + WUSS_MENU_ARROW_W; - height = pitch * menu->nitems; + + height = 0; + for (i = 0; i < menu->nitems; i++) + height += (menu->items[i].flags & wuss_MENU_ITEM_DASHED) ? sep_h : pitch; /* Nudge onto the screen where it can be, but keep the top-left on screen. */ if (at.x + width > wuss->scr->size.w) @@ -248,10 +254,12 @@ static result_t wuss__menu_spawn(wuss_t *wuss, node->ctx = ctx; node->open_index = -1; + y = 0; for (i = 0; i < menu->nitems; i++) { const wuss_menu_item_t *item; wuss_icon_flags_t flags; + int row_h; item = &menu->items[i]; flags = wuss_ICON_FLAGS_NONE; @@ -263,15 +271,22 @@ static result_t wuss__menu_spawn(wuss_t *wuss, if (item->submenu != NULL) flags |= wuss_ICON_FLAGS_SUBMENU; + /* a separator is its own short, inert row: force the text empty so + * wuss__icon_hit_test treats it as a bare rule */ + row_h = (flags & wuss_ICON_FLAGS_SEPARATOR) ? sep_h : pitch; + specs[i].type = wuss_ICON_TYPE_MENU_ENTRY; specs[i].bbox.x0 = 0; - specs[i].bbox.y0 = i * pitch; + specs[i].bbox.y0 = y; specs[i].bbox.x1 = width; - specs[i].bbox.y1 = i * pitch + pitch; - specs[i].text = item->text ? item->text : ""; + specs[i].bbox.y1 = y + row_h; + specs[i].text = (flags & wuss_ICON_FLAGS_SEPARATOR) + ? "" : (item->text ? item->text : ""); specs[i].fg = 0; specs[i].bg = wuss_NO_BACKGROUND; specs[i].flags = flags; + + y += row_h; } size = SIZE2D(width, height); diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 9bfa3259..eefbca56 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -2913,8 +2913,8 @@ result_t wuss_test(const char *resources) /* "Root" is the root caption (first token), then items App, * File{ %s title discarded, Grid(!), Export(~), |Quit }, Help(> borrowed), - * with "File" substituted for both %s. '|Quit' sets DASHED on Quit itself - * (rule drawn above it, text kept), so the submenu holds 3 rows. */ + * with "File" substituted for both %s. '|Quit' inserts a standalone DASHED + * separator item before Quit, so the submenu holds 4 rows. */ rc = wuss_menu_create_from_desc(&m, "Root, App, %s { %s, !Grid, ~Export, |Quit }, >Help", "File", "File", &borrowed); @@ -2927,15 +2927,18 @@ result_t wuss_test(const char *resources) if (m->items[0].submenu != NULL) goto MenuFail; /* items[1] "File" carries the { } submenu; its first token was the title - * and is not emitted; '|Quit' keeps Quit's text and flags it DASHED */ + * and is not emitted; '|Quit' inserts a separate DASHED separator row + * before Quit, which keeps its own text and no DASHED flag */ if (strcmp(m->items[1].text, "File") != 0) goto MenuFail; sub = m->items[1].submenu; - if (sub == NULL || sub->nitems != 3) goto MenuFail; + if (sub == NULL || sub->nitems != 4) goto MenuFail; if (strcmp(sub->items[0].text, "Grid") != 0) goto MenuFail; if (!(sub->items[0].flags & wuss_MENU_ITEM_TICKED)) goto MenuFail; if (!(sub->items[1].flags & wuss_MENU_ITEM_DISABLED)) goto MenuFail; - if (strcmp(sub->items[2].text, "Quit") != 0) goto MenuFail; + if (sub->items[2].text[0] != '\0') goto MenuFail; if (!(sub->items[2].flags & wuss_MENU_ITEM_DASHED)) goto MenuFail; + if (strcmp(sub->items[3].text, "Quit") != 0) goto MenuFail; + if (sub->items[3].flags & wuss_MENU_ITEM_DASHED) goto MenuFail; /* items[2] "Help" got a deep copy of the borrowed menu */ if (strcmp(m->items[2].text, "Help") != 0) goto MenuFail; From 76aaee2b340e7cd0f719ea2d47183d433a7cf96b Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Tue, 1 Sep 2026 12:53:15 +0100 Subject: [PATCH 133/287] refactor(wuss): condense icon pressed/selected/hovered into a flags word Replace the three loose int fields on struct wuss_icon with a single wuss_icon_state_t bitflags word and wuss__icon_pressed/selected/hovered/ set_state accessors, mirroring wuss_window_state_t. Leaves room for more per-instance state without growing the struct. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- libraries/wuss/icon.h | 52 ++++++++++++++++++++++++------ libraries/wuss/icon/create.c | 4 +-- libraries/wuss/icon/draw.c | 10 +++--- libraries/wuss/icon/get-selected.c | 2 +- libraries/wuss/icon/set-hover.c | 6 ++-- libraries/wuss/icon/set-selected.c | 8 ++--- libraries/wuss/mouse-click.c | 14 ++++---- libraries/wuss/mouse-move.c | 4 +-- 8 files changed, 66 insertions(+), 34 deletions(-) diff --git a/libraries/wuss/icon.h b/libraries/wuss/icon.h index bc5dc48d..366aa84f 100644 --- a/libraries/wuss/icon.h +++ b/libraries/wuss/icon.h @@ -11,24 +11,58 @@ #include "wuss/wuss.h" #include "wuss/icon.h" +/* Per-instance transient state, kept as bitflags with wuss__icon_* accessors + * (mirrors wuss_window_state_t) so more can be added without growing the + * struct. Not part of the public appearance API. */ +typedef enum wuss_icon_state +{ + wuss_ICON_STATE_NONE = 0, + wuss_ICON_STATE_PRESSED = 1 << 0, /* button: held with the pointer inside */ + wuss_ICON_STATE_SELECTED = 1 << 1, /* radio/option: latched on; menu entry: + * draw a tick */ + wuss_ICON_STATE_HOVERED = 1 << 2 /* menu entry: pointer is over it */ +} +wuss_icon_state_t; + struct wuss_icon { wuss_window_t *window; /* owner; back-pointer for invalidate/get_window */ box_t bbox; /* virtual document space */ wuss_icon_type_t type; char *text; /* owned; never NULL ("" instead) */ - wuss_colour_t fg; - wuss_colour_t bg; - screen_pattern_t pattern; /* wuss_ICON_TYPE_PATTERN tile; 0 otherwise */ - const bitmap_t *bitmap; /* wuss_ICON_TYPE_BITMAP image; borrowed, or NULL */ - int group; /* radio: exclusive-selection group; 0 = none */ + wuss_colour_t fg, bg; + screen_pattern_t pattern; /* wuss_ICON_TYPE_PATTERN tile; 0 otherwise */ + const bitmap_t *bitmap; /* wuss_ICON_TYPE_BITMAP image; borrowed, or NULL */ + int group; /* radio: exclusive-selection group; 0 = none */ wuss_icon_flags_t flags; - int pressed; /* button: 1 while held with the pointer inside */ - int selected; /* radio/option: 1 while latched on; menu entry: - * 1 draws a tick */ - int hovered; /* menu entry: 1 while the pointer is over it */ + wuss_icon_state_t state; }; +static inline int wuss__icon_pressed(const wuss_icon_t *icon) +{ + return (icon->state & wuss_ICON_STATE_PRESSED) != 0; +} + +static inline int wuss__icon_selected(const wuss_icon_t *icon) +{ + return (icon->state & wuss_ICON_STATE_SELECTED) != 0; +} + +static inline int wuss__icon_hovered(const wuss_icon_t *icon) +{ + return (icon->state & wuss_ICON_STATE_HOVERED) != 0; +} + +static inline void wuss__icon_set_state(wuss_icon_t *icon, + wuss_icon_state_t bit, + int on) +{ + if (on) + icon->state |= bit; + else + icon->state &= (wuss_icon_state_t) ~bit; +} + /* Set icon->selected, invalidating it. For a radio with a non-zero group, * selecting it also clears every other selected radio on the same window with * that group. Ignored for types with no latched state. */ diff --git a/libraries/wuss/icon/create.c b/libraries/wuss/icon/create.c index a9d481be..7cd21c70 100644 --- a/libraries/wuss/icon/create.c +++ b/libraries/wuss/icon/create.c @@ -89,9 +89,7 @@ result_t wuss_icon_create(wuss_window_t *window, it->bitmap = (spec->type == wuss_ICON_TYPE_BITMAP) ? spec->bitmap : NULL; it->group = (spec->type == wuss_ICON_TYPE_RADIO) ? spec->group : 0; it->flags = spec->flags; - it->pressed = 0; - it->selected = 0; - it->hovered = 0; + it->state = wuss_ICON_STATE_NONE; window->icons[window->nicons++] = it; diff --git a/libraries/wuss/icon/draw.c b/libraries/wuss/icon/draw.c index 796017ea..d1de16eb 100644 --- a/libraries/wuss/icon/draw.c +++ b/libraries/wuss/icon/draw.c @@ -188,7 +188,7 @@ static void wuss__icon_draw_button(const icon_draw_ctx_t *c) colour_t light, dark, base, label; int pressed, is_default; - pressed = icon->pressed; + pressed = wuss__icon_pressed(icon); is_default = (icon->flags & wuss_ICON_FLAGS_DEFAULT) != 0; if (is_default) @@ -284,7 +284,7 @@ static void wuss__icon_draw_radio_option(const icon_draw_ctx_t *c) screen_draw_line(c->scr, g.x0 + 2, g.y1 - 1, g.x1 - 3, g.y1 - 1, glyph); screen_draw_line(c->scr, g.x0, g.y0 + 2, g.x0, g.y1 - 3, glyph); screen_draw_line(c->scr, g.x1 - 1, g.y0 + 2, g.x1 - 1, g.y1 - 3, glyph); - if (icon->selected) + if (wuss__icon_selected(icon)) screen_fill_rect(c->scr, g.x0 + 3, g.y0 + 3, SIZE2D(gsz - 6, gsz - 6), glyph); } @@ -293,7 +293,7 @@ static void wuss__icon_draw_radio_option(const icon_draw_ctx_t *c) /* a box; a tick (two strokes) when selected */ screen_draw_rect(c->scr, g.x0, g.y0, SIZE2D(g.x1 - g.x0, g.y1 - g.y0), glyph); - if (icon->selected) + if (wuss__icon_selected(icon)) { screen_draw_line(c->scr, g.x0 + 2, g.y0 + gsz / 2, g.x0 + gsz / 2 - 1, g.y1 - 3, glyph); @@ -354,7 +354,7 @@ static void wuss__icon_draw_menu_entry(const icon_draw_ctx_t *c) int disabled, highlit, pad, mid_y; disabled = (icon->flags & wuss_ICON_FLAGS_DISABLED) != 0; - highlit = icon->hovered && !disabled; + highlit = wuss__icon_hovered(icon) && !disabled; pad = 4; /* resolve the row's own ink over its own/inherited ground */ @@ -383,7 +383,7 @@ static void wuss__icon_draw_menu_entry(const icon_draw_ctx_t *c) } /* left-edge tick when selected */ - if (icon->selected) + if (wuss__icon_selected(icon)) { int cx, cy, h; diff --git a/libraries/wuss/icon/get-selected.c b/libraries/wuss/icon/get-selected.c index cb2add43..3029e5d9 100644 --- a/libraries/wuss/icon/get-selected.c +++ b/libraries/wuss/icon/get-selected.c @@ -8,5 +8,5 @@ int wuss_icon_get_selected(const wuss_icon_t *icon) { assert(icon != NULL); - return icon->selected; + return wuss__icon_selected(icon); } diff --git a/libraries/wuss/icon/set-hover.c b/libraries/wuss/icon/set-hover.c index 8fc1ec76..08ef518a 100644 --- a/libraries/wuss/icon/set-hover.c +++ b/libraries/wuss/icon/set-hover.c @@ -5,7 +5,7 @@ #include "../impl.h" /* Only wuss_ICON_TYPE_MENU_ENTRY changes appearance on hover (see - * wuss__icon_draw_menu_entry). Buttons and every other type ignore icon->hovered, + * wuss__icon_draw_menu_entry). Buttons and every other type ignore the hover state, * so redrawing them on pointer enter/leave is wasted work. */ static int wuss__icon_hover_visible(const wuss_icon_t *icon) { @@ -22,7 +22,7 @@ void wuss__icon_set_hover(wuss_t *wuss, wuss_icon_t *icon) if (prev != NULL) { - prev->hovered = 0; + wuss__icon_set_state(prev, wuss_ICON_STATE_HOVERED, 0); if (wuss__icon_hover_visible(prev)) wuss__icon_invalidate(prev); } @@ -31,7 +31,7 @@ void wuss__icon_set_hover(wuss_t *wuss, wuss_icon_t *icon) if (icon != NULL) { - icon->hovered = 1; + wuss__icon_set_state(icon, wuss_ICON_STATE_HOVERED, 1); if (wuss__icon_hover_visible(icon)) wuss__icon_invalidate(icon); } diff --git a/libraries/wuss/icon/set-selected.c b/libraries/wuss/icon/set-selected.c index ea6c3561..6344ac65 100644 --- a/libraries/wuss/icon/set-selected.c +++ b/libraries/wuss/icon/set-selected.c @@ -31,17 +31,17 @@ void wuss__icon_select(wuss_icon_t *icon, int selected) continue; if (other->type != wuss_ICON_TYPE_RADIO || other->group != icon->group) continue; - if (!other->selected) + if (!wuss__icon_selected(other)) continue; - other->selected = 0; + wuss__icon_set_state(other, wuss_ICON_STATE_SELECTED, 0); wuss__icon_invalidate(other); } } - if (icon->selected == selected) + if (wuss__icon_selected(icon) == selected) return; - icon->selected = selected; + wuss__icon_set_state(icon, wuss_ICON_STATE_SELECTED, selected); wuss__icon_invalidate(icon); } diff --git a/libraries/wuss/mouse-click.c b/libraries/wuss/mouse-click.c index d0bf85da..91c4ef6e 100644 --- a/libraries/wuss/mouse-click.c +++ b/libraries/wuss/mouse-click.c @@ -26,9 +26,9 @@ result_t wuss_mouse_click(wuss_t *wuss, wuss_icon_t *pressed = wuss->pressed_icon; wuss->pressed_icon = NULL; - if (pressed->pressed) + if (wuss__icon_pressed(pressed)) { - pressed->pressed = 0; + wuss__icon_set_state(pressed, wuss_ICON_STATE_PRESSED, 0); wuss__icon_invalidate(pressed); } } @@ -209,23 +209,23 @@ result_t wuss_mouse_click(wuss_t *wuss, if (action == wuss_MOUSE_DOWN && (button & (wuss_BUTTON_SELECT | wuss_BUTTON_ADJUST))) { - icon->pressed = 1; + wuss__icon_set_state(icon, wuss_ICON_STATE_PRESSED, 1); wuss->pressed_icon = icon; wuss__icon_invalidate(icon); } - else if (action == wuss_MOUSE_UP && icon->pressed) + else if (action == wuss_MOUSE_UP && wuss__icon_pressed(icon)) { - icon->pressed = 0; + wuss__icon_set_state(icon, wuss_ICON_STATE_PRESSED, 0); wuss->pressed_icon = NULL; wuss__icon_invalidate(icon); /* a completed click latches radio/option state before the task is * told, so the wuss_EVENT_ICON handler sees the new value */ if (icon->type == wuss_ICON_TYPE_OPTION) - wuss__icon_select(icon, !icon->selected); + wuss__icon_select(icon, !wuss__icon_selected(icon)); else if (icon->type == wuss_ICON_TYPE_RADIO) wuss__icon_select(icon, - (button & wuss_BUTTON_ADJUST) ? !icon->selected + (button & wuss_BUTTON_ADJUST) ? !wuss__icon_selected(icon) : 1); } diff --git a/libraries/wuss/mouse-move.c b/libraries/wuss/mouse-move.c index a0495cf5..32453285 100644 --- a/libraries/wuss/mouse-move.c +++ b/libraries/wuss/mouse-move.c @@ -99,9 +99,9 @@ result_t wuss_mouse_move(wuss_t *wuss, point_t p, wuss_window_t **hit) { wuss_icon_t *it = win->icons[k]; - if (it->pressed && it != icon) + if (wuss__icon_pressed(it) && it != icon) { - it->pressed = 0; + wuss__icon_set_state(it, wuss_ICON_STATE_PRESSED, 0); if (wuss->pressed_icon == it) wuss->pressed_icon = NULL; wuss__icon_invalidate(it); From 94cdbf289ae1524d7b7ecf86372d19b78e94a1b2 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Tue, 1 Sep 2026 13:06:36 +0100 Subject: [PATCH 134/287] feat(screen): add 17-level Bayer dither fill patterns screen_pattern_t gains screen_PATTERN_BAYER0..BAYER16, a 4x4 ordered dither ramp from empty to solid, indexable as BAYER0 + level. BAYER8 duplicates GREY50 and BAYER16 duplicates SOLID; both kept so the level arithmetic stays simple. Tiles are the 4x4 matrix repeated across the existing 8x8 tile rows. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- include/framebuf/screen.h | 25 +++++++++++++++++++ .../framebuf/screen/screen-fill-pattern.c | 24 +++++++++++++++++- 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/include/framebuf/screen.h b/include/framebuf/screen.h index 2c89ec30..6483b6f2 100644 --- a/include/framebuf/screen.h +++ b/include/framebuf/screen.h @@ -108,6 +108,31 @@ typedef enum screen_pattern screen_PATTERN_DOTS, /**< Sparse dots. */ screen_PATTERN_GRID, /**< Thin grid lines. */ screen_PATTERN_CROSSHATCH, /**< Crossed thin lines. */ + + /** + * 4x4 ordered (Bayer) dither, coverage levels 0 (empty) to 16 (solid). Index + * by level as `screen_PATTERN_BAYER0 + level`. `screen_PATTERN_BAYER8` is + * identical to `screen_PATTERN_GREY50` and `screen_PATTERN_BAYER16` to + * `screen_PATTERN_SOLID`; both are kept so the level arithmetic stays simple. + */ + screen_PATTERN_BAYER0, + screen_PATTERN_BAYER1, + screen_PATTERN_BAYER2, + screen_PATTERN_BAYER3, + screen_PATTERN_BAYER4, + screen_PATTERN_BAYER5, + screen_PATTERN_BAYER6, + screen_PATTERN_BAYER7, + screen_PATTERN_BAYER8, + screen_PATTERN_BAYER9, + screen_PATTERN_BAYER10, + screen_PATTERN_BAYER11, + screen_PATTERN_BAYER12, + screen_PATTERN_BAYER13, + screen_PATTERN_BAYER14, + screen_PATTERN_BAYER15, + screen_PATTERN_BAYER16, + screen_PATTERN__LIMIT /**< Count of patterns; not itself a pattern. */ } screen_pattern_t; diff --git a/libraries/framebuf/screen/screen-fill-pattern.c b/libraries/framebuf/screen/screen-fill-pattern.c index 47fda849..84b8427c 100644 --- a/libraries/framebuf/screen/screen-fill-pattern.c +++ b/libraries/framebuf/screen/screen-fill-pattern.c @@ -19,7 +19,29 @@ static const unsigned char patterns[screen_PATTERN__LIMIT][8] = { 0x88, 0x44, 0x22, 0x11, 0x88, 0x44, 0x22, 0x11 }, /* DIAGONAL */ { 0x88, 0x00, 0x22, 0x00, 0x88, 0x00, 0x22, 0x00 }, /* DOTS */ { 0xFF, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80 }, /* GRID */ - { 0x80, 0x41, 0x22, 0x14, 0x08, 0x14, 0x22, 0x41 } /* CROSSHATCH */ + { 0x80, 0x41, 0x22, 0x14, 0x08, 0x14, 0x22, 0x41 }, /* CROSSHATCH */ + + /* 4x4 ordered (Bayer) dither, levels 0..16. Level N sets the pixels whose + * threshold in the 4x4 matrix is < N, tiled twice to fill the 8x8 row. + * BAYER8 equals GREY50 and BAYER16 equals SOLID; kept in the run so a + * caller can index by level as screen_PATTERN_BAYER0 + level. */ + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* BAYER0 */ + { 0x88, 0x00, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00 }, /* BAYER1 */ + { 0x88, 0x00, 0x22, 0x00, 0x88, 0x00, 0x22, 0x00 }, /* BAYER2 */ + { 0xAA, 0x00, 0x22, 0x00, 0xAA, 0x00, 0x22, 0x00 }, /* BAYER3 */ + { 0xAA, 0x00, 0xAA, 0x00, 0xAA, 0x00, 0xAA, 0x00 }, /* BAYER4 */ + { 0xAA, 0x44, 0xAA, 0x00, 0xAA, 0x44, 0xAA, 0x00 }, /* BAYER5 */ + { 0xAA, 0x44, 0xAA, 0x11, 0xAA, 0x44, 0xAA, 0x11 }, /* BAYER6 */ + { 0xAA, 0x55, 0xAA, 0x11, 0xAA, 0x55, 0xAA, 0x11 }, /* BAYER7 */ + { 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55 }, /* BAYER8 */ + { 0xEE, 0x55, 0xAA, 0x55, 0xEE, 0x55, 0xAA, 0x55 }, /* BAYER9 */ + { 0xEE, 0x55, 0xBB, 0x55, 0xEE, 0x55, 0xBB, 0x55 }, /* BAYER10 */ + { 0xFF, 0x55, 0xBB, 0x55, 0xFF, 0x55, 0xBB, 0x55 }, /* BAYER11 */ + { 0xFF, 0x55, 0xFF, 0x55, 0xFF, 0x55, 0xFF, 0x55 }, /* BAYER12 */ + { 0xFF, 0xDD, 0xFF, 0x55, 0xFF, 0xDD, 0xFF, 0x55 }, /* BAYER13 */ + { 0xFF, 0xDD, 0xFF, 0x77, 0xFF, 0xDD, 0xFF, 0x77 }, /* BAYER14 */ + { 0xFF, 0xFF, 0xFF, 0x77, 0xFF, 0xFF, 0xFF, 0x77 }, /* BAYER15 */ + { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF } /* BAYER16 */ }; void screen_fill_pattern(screen_t *scr, From ecbb75df41ad822d57bcfeb3e84443dda493253d Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Tue, 1 Sep 2026 13:16:25 +0100 Subject: [PATCH 135/287] refactor(wuss): show pattern swatches as a 16x16 grid The icons task laid its fill-pattern swatches out as a single tall column of 90x36 cells. With the pattern set now 25 entries that column ran far past the document; show them as 16x16 swatches on a 6-wide grid instead. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- libraries/wuss/test/tasks/icons.c | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/libraries/wuss/test/tasks/icons.c b/libraries/wuss/test/tasks/icons.c index 5b923656..41b144c9 100644 --- a/libraries/wuss/test/tasks/icons.c +++ b/libraries/wuss/test/tasks/icons.c @@ -30,11 +30,12 @@ result_t icons_create(wuss_t *wuss, const char *resources, icons_task_t *task) { - /* [0..3], one swatch per built-in pattern, then [.. +3] a grouping frame - * with two differently-justified labels inside it, then [.. +5] three grouped - * radios, a standalone option and a label echoing the selection, then [.. +2] - * a decorative bitmap icon and an interactive one that bumps the counter, - * then [.. +4] a strip of menu-entry icons that hover-highlight */ + /* [0..3] heading, counter button, counter label and a scrolled-away button, + * then [.. +N] one swatch per built-in fill pattern, then [.. +3] a grouping + * frame with two differently-justified labels inside it, then [.. +5] three + * grouped radios, a standalone option and a label echoing the selection, + * then [.. +2] a decorative bitmap icon and an interactive one that bumps + * the counter, then [.. +4] a strip of menu-entry icons that hover-highlight */ enum { ICONS_NSPECS = 4 + screen_PATTERN__LIMIT + 3 + 5 + 2 + 4 }; wuss_task_t delegate; wuss_icon_spec_t specs[ICONS_NSPECS]; @@ -119,11 +120,20 @@ result_t icons_create(wuss_t *wuss, specs[3].fg = palette_PICO8_BLACK; specs[3].bg = palette_PICO8_LIGHT_GREY; - /* [4..] one swatch per built-in pattern, in a column down the document so - * they scroll through the window and stay phase-locked while doing so */ + /* [4..] one 16x16 swatch per built-in fill pattern, laid out as a grid down + * the document so it scrolls through the window and stays phase-locked while + * doing so */ for (p = 0; p < screen_PATTERN__LIMIT; p++) { - specs[4 + p].bbox = (box_t) BOX_POS_SIZE(28, 90 + p * 44, 90, 36); + enum { ICONS_SWATCH_COLS = 6, ICONS_SWATCH_PITCH = 20 }; + int col, row; + + col = p % ICONS_SWATCH_COLS; + row = p / ICONS_SWATCH_COLS; + + specs[4 + p].bbox = (box_t) BOX_POS_SIZE(28 + col * ICONS_SWATCH_PITCH, + 90 + row * ICONS_SWATCH_PITCH, + 16, 16); specs[4 + p].type = wuss_ICON_TYPE_PATTERN; specs[4 + p].fg = palette_PICO8_DARK_BLUE; specs[4 + p].bg = palette_PICO8_LIGHT_GREY; From d105a9d18ffdd853ceaba7683efda0e0c54fd1c5 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Tue, 1 Sep 2026 13:24:13 +0100 Subject: [PATCH 136/287] feat(screen): use the full 8x8 65-level Bayer dither ramp Replaces the 17-level 4x4 Bayer ramp with the full 8x8 one: 65 coverage levels, one per threshold in the recursively-built 8x8 matrix, still indexed as screen_PATTERN_BAYER0 + level. The 65 levels past BAYER0 are left unnamed in the enum with BAYER_LIMIT one past the end; a static assert pins the tile table length to screen_PATTERN__LIMIT. BAYER32 equals GREY50 and BAYER64 equals SOLID, kept for simple arithmetic. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- include/framebuf/screen.h | 31 ++----- .../framebuf/screen/screen-fill-pattern.c | 92 +++++++++++++++---- 2 files changed, 83 insertions(+), 40 deletions(-) diff --git a/include/framebuf/screen.h b/include/framebuf/screen.h index 6483b6f2..f8e8f084 100644 --- a/include/framebuf/screen.h +++ b/include/framebuf/screen.h @@ -110,30 +110,19 @@ typedef enum screen_pattern screen_PATTERN_CROSSHATCH, /**< Crossed thin lines. */ /** - * 4x4 ordered (Bayer) dither, coverage levels 0 (empty) to 16 (solid). Index - * by level as `screen_PATTERN_BAYER0 + level`. `screen_PATTERN_BAYER8` is - * identical to `screen_PATTERN_GREY50` and `screen_PATTERN_BAYER16` to - * `screen_PATTERN_SOLID`; both are kept so the level arithmetic stays simple. + * 8x8 ordered (Bayer) dither, one entry per coverage level 0 (empty) to 64 + * (solid). Index by level as `screen_PATTERN_BAYER0 + level`; + * `screen_PATTERN_BAYER_LIMIT` is one past the last. `screen_PATTERN_BAYER0` + * matches nothing else here, `screen_PATTERN_BAYER32` equals + * `screen_PATTERN_GREY50` and `screen_PATTERN_BAYER64` equals + * `screen_PATTERN_SOLID`; the duplicates are kept so the level arithmetic + * stays simple. */ screen_PATTERN_BAYER0, - screen_PATTERN_BAYER1, - screen_PATTERN_BAYER2, - screen_PATTERN_BAYER3, - screen_PATTERN_BAYER4, - screen_PATTERN_BAYER5, - screen_PATTERN_BAYER6, - screen_PATTERN_BAYER7, - screen_PATTERN_BAYER8, - screen_PATTERN_BAYER9, - screen_PATTERN_BAYER10, - screen_PATTERN_BAYER11, - screen_PATTERN_BAYER12, - screen_PATTERN_BAYER13, - screen_PATTERN_BAYER14, - screen_PATTERN_BAYER15, - screen_PATTERN_BAYER16, + screen_PATTERN_BAYER_LIMIT = screen_PATTERN_BAYER0 + 65, - screen_PATTERN__LIMIT /**< Count of patterns; not itself a pattern. */ + screen_PATTERN__LIMIT = screen_PATTERN_BAYER_LIMIT + /**< Count of patterns; not itself a pattern. */ } screen_pattern_t; diff --git a/libraries/framebuf/screen/screen-fill-pattern.c b/libraries/framebuf/screen/screen-fill-pattern.c index 84b8427c..5d5ed8a7 100644 --- a/libraries/framebuf/screen/screen-fill-pattern.c +++ b/libraries/framebuf/screen/screen-fill-pattern.c @@ -21,29 +21,83 @@ static const unsigned char patterns[screen_PATTERN__LIMIT][8] = { 0xFF, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80 }, /* GRID */ { 0x80, 0x41, 0x22, 0x14, 0x08, 0x14, 0x22, 0x41 }, /* CROSSHATCH */ - /* 4x4 ordered (Bayer) dither, levels 0..16. Level N sets the pixels whose - * threshold in the 4x4 matrix is < N, tiled twice to fill the 8x8 row. - * BAYER8 equals GREY50 and BAYER16 equals SOLID; kept in the run so a + /* 8x8 ordered (Bayer) dither, one row per coverage level 0..64. Level N sets + * the pixels whose threshold in the recursively-built 8x8 matrix is < N. + * BAYER32 equals GREY50 and BAYER64 equals SOLID; kept in the run so a * caller can index by level as screen_PATTERN_BAYER0 + level. */ { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* BAYER0 */ - { 0x88, 0x00, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00 }, /* BAYER1 */ - { 0x88, 0x00, 0x22, 0x00, 0x88, 0x00, 0x22, 0x00 }, /* BAYER2 */ - { 0xAA, 0x00, 0x22, 0x00, 0xAA, 0x00, 0x22, 0x00 }, /* BAYER3 */ - { 0xAA, 0x00, 0xAA, 0x00, 0xAA, 0x00, 0xAA, 0x00 }, /* BAYER4 */ - { 0xAA, 0x44, 0xAA, 0x00, 0xAA, 0x44, 0xAA, 0x00 }, /* BAYER5 */ - { 0xAA, 0x44, 0xAA, 0x11, 0xAA, 0x44, 0xAA, 0x11 }, /* BAYER6 */ - { 0xAA, 0x55, 0xAA, 0x11, 0xAA, 0x55, 0xAA, 0x11 }, /* BAYER7 */ - { 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55 }, /* BAYER8 */ - { 0xEE, 0x55, 0xAA, 0x55, 0xEE, 0x55, 0xAA, 0x55 }, /* BAYER9 */ - { 0xEE, 0x55, 0xBB, 0x55, 0xEE, 0x55, 0xBB, 0x55 }, /* BAYER10 */ - { 0xFF, 0x55, 0xBB, 0x55, 0xFF, 0x55, 0xBB, 0x55 }, /* BAYER11 */ - { 0xFF, 0x55, 0xFF, 0x55, 0xFF, 0x55, 0xFF, 0x55 }, /* BAYER12 */ - { 0xFF, 0xDD, 0xFF, 0x55, 0xFF, 0xDD, 0xFF, 0x55 }, /* BAYER13 */ - { 0xFF, 0xDD, 0xFF, 0x77, 0xFF, 0xDD, 0xFF, 0x77 }, /* BAYER14 */ - { 0xFF, 0xFF, 0xFF, 0x77, 0xFF, 0xFF, 0xFF, 0x77 }, /* BAYER15 */ - { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF } /* BAYER16 */ + { 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* BAYER1 */ + { 0x80, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00 }, /* BAYER2 */ + { 0x80, 0x00, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00 }, /* BAYER3 */ + { 0x88, 0x00, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00 }, /* BAYER4 */ + { 0x88, 0x00, 0x20, 0x00, 0x88, 0x00, 0x00, 0x00 }, /* BAYER5 */ + { 0x88, 0x00, 0x20, 0x00, 0x88, 0x00, 0x02, 0x00 }, /* BAYER6 */ + { 0x88, 0x00, 0x20, 0x00, 0x88, 0x00, 0x22, 0x00 }, /* BAYER7 */ + { 0x88, 0x00, 0x22, 0x00, 0x88, 0x00, 0x22, 0x00 }, /* BAYER8 */ + { 0x88, 0x00, 0xA2, 0x00, 0x88, 0x00, 0x22, 0x00 }, /* BAYER9 */ + { 0x88, 0x00, 0xA2, 0x00, 0x88, 0x00, 0x2A, 0x00 }, /* BAYER10 */ + { 0x88, 0x00, 0xA2, 0x00, 0x88, 0x00, 0xAA, 0x00 }, /* BAYER11 */ + { 0x88, 0x00, 0xAA, 0x00, 0x88, 0x00, 0xAA, 0x00 }, /* BAYER12 */ + { 0xA8, 0x00, 0xAA, 0x00, 0x88, 0x00, 0xAA, 0x00 }, /* BAYER13 */ + { 0xA8, 0x00, 0xAA, 0x00, 0x8A, 0x00, 0xAA, 0x00 }, /* BAYER14 */ + { 0xA8, 0x00, 0xAA, 0x00, 0xAA, 0x00, 0xAA, 0x00 }, /* BAYER15 */ + { 0xAA, 0x00, 0xAA, 0x00, 0xAA, 0x00, 0xAA, 0x00 }, /* BAYER16 */ + { 0xAA, 0x40, 0xAA, 0x00, 0xAA, 0x00, 0xAA, 0x00 }, /* BAYER17 */ + { 0xAA, 0x40, 0xAA, 0x00, 0xAA, 0x04, 0xAA, 0x00 }, /* BAYER18 */ + { 0xAA, 0x40, 0xAA, 0x00, 0xAA, 0x44, 0xAA, 0x00 }, /* BAYER19 */ + { 0xAA, 0x44, 0xAA, 0x00, 0xAA, 0x44, 0xAA, 0x00 }, /* BAYER20 */ + { 0xAA, 0x44, 0xAA, 0x10, 0xAA, 0x44, 0xAA, 0x00 }, /* BAYER21 */ + { 0xAA, 0x44, 0xAA, 0x10, 0xAA, 0x44, 0xAA, 0x01 }, /* BAYER22 */ + { 0xAA, 0x44, 0xAA, 0x10, 0xAA, 0x44, 0xAA, 0x11 }, /* BAYER23 */ + { 0xAA, 0x44, 0xAA, 0x11, 0xAA, 0x44, 0xAA, 0x11 }, /* BAYER24 */ + { 0xAA, 0x44, 0xAA, 0x51, 0xAA, 0x44, 0xAA, 0x11 }, /* BAYER25 */ + { 0xAA, 0x44, 0xAA, 0x51, 0xAA, 0x44, 0xAA, 0x15 }, /* BAYER26 */ + { 0xAA, 0x44, 0xAA, 0x51, 0xAA, 0x44, 0xAA, 0x55 }, /* BAYER27 */ + { 0xAA, 0x44, 0xAA, 0x55, 0xAA, 0x44, 0xAA, 0x55 }, /* BAYER28 */ + { 0xAA, 0x54, 0xAA, 0x55, 0xAA, 0x44, 0xAA, 0x55 }, /* BAYER29 */ + { 0xAA, 0x54, 0xAA, 0x55, 0xAA, 0x45, 0xAA, 0x55 }, /* BAYER30 */ + { 0xAA, 0x54, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55 }, /* BAYER31 */ + { 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55 }, /* BAYER32 */ + { 0xAA, 0xD5, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55 }, /* BAYER33 */ + { 0xAA, 0xD5, 0xAA, 0x55, 0xAA, 0x5D, 0xAA, 0x55 }, /* BAYER34 */ + { 0xAA, 0xD5, 0xAA, 0x55, 0xAA, 0xDD, 0xAA, 0x55 }, /* BAYER35 */ + { 0xAA, 0xDD, 0xAA, 0x55, 0xAA, 0xDD, 0xAA, 0x55 }, /* BAYER36 */ + { 0xAA, 0xDD, 0xAA, 0x75, 0xAA, 0xDD, 0xAA, 0x55 }, /* BAYER37 */ + { 0xAA, 0xDD, 0xAA, 0x75, 0xAA, 0xDD, 0xAA, 0x57 }, /* BAYER38 */ + { 0xAA, 0xDD, 0xAA, 0x75, 0xAA, 0xDD, 0xAA, 0x77 }, /* BAYER39 */ + { 0xAA, 0xDD, 0xAA, 0x77, 0xAA, 0xDD, 0xAA, 0x77 }, /* BAYER40 */ + { 0xAA, 0xDD, 0xAA, 0xF7, 0xAA, 0xDD, 0xAA, 0x77 }, /* BAYER41 */ + { 0xAA, 0xDD, 0xAA, 0xF7, 0xAA, 0xDD, 0xAA, 0x7F }, /* BAYER42 */ + { 0xAA, 0xDD, 0xAA, 0xF7, 0xAA, 0xDD, 0xAA, 0xFF }, /* BAYER43 */ + { 0xAA, 0xDD, 0xAA, 0xFF, 0xAA, 0xDD, 0xAA, 0xFF }, /* BAYER44 */ + { 0xAA, 0xFD, 0xAA, 0xFF, 0xAA, 0xDD, 0xAA, 0xFF }, /* BAYER45 */ + { 0xAA, 0xFD, 0xAA, 0xFF, 0xAA, 0xDF, 0xAA, 0xFF }, /* BAYER46 */ + { 0xAA, 0xFD, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF }, /* BAYER47 */ + { 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF }, /* BAYER48 */ + { 0xEA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF }, /* BAYER49 */ + { 0xEA, 0xFF, 0xAA, 0xFF, 0xAE, 0xFF, 0xAA, 0xFF }, /* BAYER50 */ + { 0xEA, 0xFF, 0xAA, 0xFF, 0xEE, 0xFF, 0xAA, 0xFF }, /* BAYER51 */ + { 0xEE, 0xFF, 0xAA, 0xFF, 0xEE, 0xFF, 0xAA, 0xFF }, /* BAYER52 */ + { 0xEE, 0xFF, 0xBA, 0xFF, 0xEE, 0xFF, 0xAA, 0xFF }, /* BAYER53 */ + { 0xEE, 0xFF, 0xBA, 0xFF, 0xEE, 0xFF, 0xAB, 0xFF }, /* BAYER54 */ + { 0xEE, 0xFF, 0xBA, 0xFF, 0xEE, 0xFF, 0xBB, 0xFF }, /* BAYER55 */ + { 0xEE, 0xFF, 0xBB, 0xFF, 0xEE, 0xFF, 0xBB, 0xFF }, /* BAYER56 */ + { 0xEE, 0xFF, 0xFB, 0xFF, 0xEE, 0xFF, 0xBB, 0xFF }, /* BAYER57 */ + { 0xEE, 0xFF, 0xFB, 0xFF, 0xEE, 0xFF, 0xBF, 0xFF }, /* BAYER58 */ + { 0xEE, 0xFF, 0xFB, 0xFF, 0xEE, 0xFF, 0xFF, 0xFF }, /* BAYER59 */ + { 0xEE, 0xFF, 0xFF, 0xFF, 0xEE, 0xFF, 0xFF, 0xFF }, /* BAYER60 */ + { 0xFE, 0xFF, 0xFF, 0xFF, 0xEE, 0xFF, 0xFF, 0xFF }, /* BAYER61 */ + { 0xFE, 0xFF, 0xFF, 0xFF, 0xEF, 0xFF, 0xFF, 0xFF }, /* BAYER62 */ + { 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }, /* BAYER63 */ + { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF } /* BAYER64 */ }; +/* the enum leaves the 65 Bayer levels unnamed past BAYER0, so keep the table + * length pinned to it here */ +typedef char + patterns_len_matches_enum[(sizeof patterns / sizeof patterns[0]) == + screen_PATTERN__LIMIT ? 1 : -1]; + void screen_fill_pattern(screen_t *scr, const box_t *box, screen_pattern_t pattern, From 9bcf4ad5623abc0635a81dd5da172846cb255660 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Tue, 1 Sep 2026 14:03:33 +0100 Subject: [PATCH 137/287] feat(wuss): clamp windows so they can never grow larger than the screen New wuss__max_content_on_screen helper gives the largest content size whose visible box still fits the screen from the window's current top-left, floored at WUSS_MIN_CONTENT so a window jammed against the far edge stays grabbable. wuss_window_create and wuss_window_resize both clamp through it, covering client calls, drag-resize and creation; toggle-size already had an equivalent bound of its own. Adds create/resize clamp tests to both wuss_test bodies. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- libraries/wuss/impl.h | 27 ++++++++ libraries/wuss/test/wuss-test.c | 106 ++++++++++++++++++++++++++++++++ libraries/wuss/window/create.c | 16 +++++ libraries/wuss/window/resize.c | 11 ++++ 4 files changed, 160 insertions(+) diff --git a/libraries/wuss/impl.h b/libraries/wuss/impl.h index 9e308f8e..84be1268 100644 --- a/libraries/wuss/impl.h +++ b/libraries/wuss/impl.h @@ -401,4 +401,31 @@ static inline void wuss__furniture_carve_for(wuss_window_flags_t flags, } #endif /* WUSS_FURNITURE */ +/* Largest content width/height whose visible box (content + outline + + * titlebar + scrollbar/resize carve) still fits the screen from the + * window's current top-left. Never returns below WUSS_MIN_CONTENT: a + * window jammed hard against the far edge stays grabbable even though it + * then overhangs. Shared by wuss_window_create and wuss_window_resize so + * no path can produce a window larger than the screen. */ +static inline void wuss__max_content_on_screen(const wuss_window_t *window, + size2d_t *max) +{ + int outline_px, titlebar_height; + point_t carve; + + outline_px = wuss__outline_px(window); + titlebar_height = wuss__titlebar_height(window); + wuss__furniture_carve_for(window->flags, wuss__button_size(window), &carve); + + max->w = window->wuss->scr->size.w - window->visible.x0 + - 2 * outline_px - carve.x; + max->h = window->wuss->scr->size.h - window->visible.y0 + - 2 * outline_px - titlebar_height - carve.y; + + if (max->w < WUSS_MIN_CONTENT) + max->w = WUSS_MIN_CONTENT; + if (max->h < WUSS_MIN_CONTENT) + max->h = WUSS_MIN_CONTENT; +} + #endif /* IMPL_H */ diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index eefbca56..f203cab2 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -528,6 +528,59 @@ result_t wuss_test(const char *resources) if (width != 52 || height != 72) goto Failure; + printf("test: window_resize can never grow a window past the screen\n"); + + /* screen is 200x200; win_a's visible box sits at (0,25) with a 1px + * outline all round and a 20px titlebar. asking for a 500x500 content + * area must clamp so the visible box still fits: width 200-0-2, + * height 200-25-2-20. */ + rc = wuss_window_resize(win_a, SIZE2D(500, 500)); + if (rc != result_OK) + goto Failure; + wuss_window_get_content_bounds(win_a, &content); + if (content.x1 - content.x0 != 198 || content.y1 - content.y0 != 153) + goto Failure; + wuss_window_get_visible_bounds(win_a, &visible); + if (visible.x1 != 200 || visible.y1 != 200) + goto Failure; + + /* a request that already fits is honoured verbatim */ + rc = wuss_window_resize(win_a, SIZE2D(60, 40)); + if (rc != result_OK) + goto Failure; + wuss_window_get_content_bounds(win_a, &content); + if (content.x1 - content.x0 != 60 || content.y1 - content.y0 != 40) + goto Failure; + + /* restore for the tests that follow */ + rc = wuss_window_resize(win_a, SIZE2D(50, 50)); + if (rc != result_OK) + goto Failure; + + printf("test: window_create can never make a window bigger than the " + "screen\n"); + + { + box_t box_big; + wuss_window_t *win_big; + + /* content box asks for 10,10..400,400; the on-screen nudge pulls the + * visible top-left (10-1 outline) back to (0,0), then the clamp caps + * the content at 200 - 2*1 - 20(titlebar) tall, 200 - 2*1 wide. */ + box_big.x0 = 10; box_big.y0 = 10; box_big.x1 = 400; box_big.y1 = 400; + rc = wuss_window_create(wuss, &box_big, "BIG", wuss_WINDOW_NO_VSCROLL | + wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), NULL, + SIZE2D(400, 400), SIZE2D(0, 0), &win_big); + if (rc != result_OK) + goto Failure; + wuss_window_get_visible_bounds(win_big, &visible); + if (visible.x0 != 0 || visible.y0 != 0 || + visible.x1 != 200 || visible.y1 != 200) + goto Failure; + wuss_window_close(win_big); + } + printf("test: title-less, no-outline window has no furniture, so visible == content\n"); tc_d.redraw_count = 0; @@ -3060,6 +3113,59 @@ result_t wuss_test(const char *resources) content.x1 != 100 || content.y1 != 100) goto Failure; + printf("test: window_resize can never grow a window past the screen\n"); + + /* screen is 200x200; win_a sits at (0,0), chromeless. asking for a + * 500x500 content area must clamp to the 200x200 screen. */ + rc = wuss_window_resize(win_a, SIZE2D(500, 500)); + if (rc != result_OK) + goto Failure; + wuss_window_get_content_bounds(win_a, &content); + if (content.x1 - content.x0 != 200 || content.y1 - content.y0 != 200) + goto Failure; + + /* a request that already fits is left exactly as asked */ + rc = wuss_window_resize(win_a, SIZE2D(120, 90)); + if (rc != result_OK) + goto Failure; + wuss_window_get_content_bounds(win_a, &content); + if (content.x1 - content.x0 != 120 || content.y1 - content.y0 != 90) + goto Failure; + + /* a window whose top-left is offset only gets the space that is left */ + wuss_window_move(win_a, POINT(60, 40)); + rc = wuss_window_resize(win_a, SIZE2D(500, 500)); + if (rc != result_OK) + goto Failure; + wuss_window_get_content_bounds(win_a, &content); + if (content.x1 - content.x0 != 140 || content.y1 - content.y0 != 160) + goto Failure; + wuss_window_move(win_a, POINT(0, 0)); + rc = wuss_window_resize(win_a, SIZE2D(100, 100)); + if (rc != result_OK) + goto Failure; + + printf("test: window_create can never make a window bigger than the " + "screen\n"); + + { + box_t box_big; + wuss_window_t *win_big; + + /* chromeless and the on-screen nudge drags the top-left back to (0,0), + * so the clamp caps content at the full 200x200 screen */ + box_big.x0 = 10; box_big.y0 = 10; box_big.x1 = 400; box_big.y1 = 400; + rc = wuss_window_create(wuss, &box_big, "BIG", chromeless, + wuss_NO_BACKGROUND, NULL, SIZE2D(400, 400), + SIZE2D(0, 0), &win_big); + if (rc != result_OK) + goto Failure; + wuss_window_get_content_bounds(win_big, &content); + if (content.x1 - content.x0 != 200 || content.y1 - content.y0 != 200) + goto Failure; + wuss_window_close(win_big); + } + printf("test: redraw delivers REDRAW events\n"); wuss_redraw(wuss); diff --git a/libraries/wuss/window/create.c b/libraries/wuss/window/create.c index 1a5119e7..130659f9 100644 --- a/libraries/wuss/window/create.c +++ b/libraries/wuss/window/create.c @@ -74,6 +74,22 @@ result_t wuss_window_create(wuss_t *wuss, win->visible.y0 += dy; win->visible.y1 += dy; win->flags = flags; + + /* clamp so the window can never be born larger than the screen; done + * after the on-screen nudge above so it measures from the final + * top-left. only the bottom-right corner moves. */ + { + size2d_t max; + + wuss__max_content_on_screen(win, &max); + if (win->visible.x1 - win->visible.x0 - 2 * outline_px - carve.x > max.w) + win->visible.x1 = win->visible.x0 + 2 * outline_px + carve.x + max.w; + if (win->visible.y1 - win->visible.y0 - 2 * outline_px + - titlebar_height - carve.y > max.h) + win->visible.y1 = win->visible.y0 + 2 * outline_px + titlebar_height + + carve.y + max.h; + } + win->scroll.x = 0; win->scroll.y = 0; win->doc = doc; diff --git a/libraries/wuss/window/resize.c b/libraries/wuss/window/resize.c index 9c8e7368..5c4a46af 100644 --- a/libraries/wuss/window/resize.c +++ b/libraries/wuss/window/resize.c @@ -45,6 +45,17 @@ result_t wuss_window_resize(wuss_window_t *window, size2d_t size) /* a manual resize desyncs the window from its layout-packer slot */ wuss__release_packed(window); + /* a window can never grow larger than the screen */ + { + size2d_t max; + + wuss__max_content_on_screen(window, &max); + if (size.w > max.w) + size.w = max.w; + if (size.h > max.h) + size.h = max.h; + } + outline_px = wuss__outline_px(window); titlebar_height = wuss__titlebar_height(window); before = window->visible; From c7322c40dd590f0eedd2fd775f56e2d4802e45a9 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Tue, 1 Sep 2026 14:27:16 +0100 Subject: [PATCH 138/287] fix(wuss): give menus an outline wuss__menu_spawn passed wuss_WINDOW_NO_OUTLINE, so furniture drawing skipped the 1px border. Drop the flag; the outline sits outside the content box and does not overlap the menu rows. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- libraries/wuss/menu/menu.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/libraries/wuss/menu/menu.c b/libraries/wuss/menu/menu.c index 6fdccc8d..5934e1ef 100644 --- a/libraries/wuss/menu/menu.c +++ b/libraries/wuss/menu/menu.c @@ -303,8 +303,7 @@ static result_t wuss__menu_spawn(wuss_t *wuss, rc = wuss_window_create(wuss, &content, menu->title ? menu->title : "", - wuss_WINDOW_NO_OUTLINE - | wuss_WINDOW_NO_CLOSE | wuss_WINDOW_NO_BACK + 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, From 90418b08323511ca4bdd27694a5f473cfced2426 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Tue, 1 Sep 2026 14:30:04 +0100 Subject: [PATCH 139/287] fix(wuss): stop the menu outline cropping the last row Since menu windows gained an outline, wuss__menu_spawn's on-screen clamp still measured only the content box. The window's visible box also spans the outline on four sides and the titlebar above, so a menu opened near a screen edge had its visible box pushed past the screen, and wuss_window_create's own size clamp trimmed the content -- dropping the bottom row (e.g. a trailing "Quit"). Clamp against the full visible extent instead. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- libraries/wuss/menu/menu.c | 38 +++++++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/libraries/wuss/menu/menu.c b/libraries/wuss/menu/menu.c index 5934e1ef..6d8a9d10 100644 --- a/libraries/wuss/menu/menu.c +++ b/libraries/wuss/menu/menu.c @@ -182,6 +182,9 @@ static result_t wuss__menu_spawn(wuss_t *wuss, int widest; int width, height; int i; + int outline_px; + int titlebar_height; + wuss_window_flags_t menu_flags; size2d_t size; box_t content; @@ -192,6 +195,11 @@ static result_t wuss__menu_spawn(wuss_t *wuss, if (menu->nitems <= 0) return result_WUSS_BAD_ICON; + menu_flags = 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; + bmfont_get_info(wuss->font, &fw, &fh); NOT_USED(fw); pitch = fh + 2 * WUSS_MENU_ROW_PAD; @@ -220,15 +228,22 @@ static result_t wuss__menu_spawn(wuss_t *wuss, for (i = 0; i < menu->nitems; i++) height += (menu->items[i].flags & wuss_MENU_ITEM_DASHED) ? sep_h : pitch; - /* Nudge onto the screen where it can be, but keep the top-left on screen. */ - if (at.x + width > wuss->scr->size.w) - at.x = wuss->scr->size.w - width; - if (at.y + height > wuss->scr->size.h) - at.y = wuss->scr->size.h - height; - if (at.x < 0) - at.x = 0; - if (at.y < 0) - at.y = 0; + /* Nudge onto the screen where it can be, but keep the top-left on screen. + * `at` is the content top-left; the window's visible box also spans the + * outline on all four sides and the titlebar above, so clamp against that + * extent -- otherwise wuss_window_create's own on-screen size clamp trims + * the content and the last row (e.g. a trailing "Quit") is cropped. */ + outline_px = wuss__outline_px_for(menu_flags); + titlebar_height = wuss__titlebar_height_for(wuss, menu_flags); + + if (at.x + width + outline_px > wuss->scr->size.w) + at.x = wuss->scr->size.w - width - outline_px; + if (at.y + height + outline_px > wuss->scr->size.h) + at.y = wuss->scr->size.h - height - outline_px; + if (at.x - outline_px < 0) + at.x = outline_px; + if (at.y - outline_px - titlebar_height < 0) + at.y = outline_px + titlebar_height; node = wuss__malloc(wuss, sizeof(*node)); if (node == NULL) @@ -303,10 +318,7 @@ static result_t wuss__menu_spawn(wuss_t *wuss, rc = wuss_window_create(wuss, &content, menu->title ? menu->title : "", - 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, + menu_flags, wuss_BACKDROP_COLOUR(wuss->white), &task, size, size, &node->window); if (rc != result_OK) From b5561a60ee4c14acce3b68c283ead85b825ed7c7 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Tue, 1 Sep 2026 14:34:11 +0100 Subject: [PATCH 140/287] Revert "fix(wuss): stop the menu outline cropping the last row" This reverts commit 90418b08323511ca4bdd27694a5f473cfced2426. --- libraries/wuss/menu/menu.c | 38 +++++++++++++------------------------- 1 file changed, 13 insertions(+), 25 deletions(-) diff --git a/libraries/wuss/menu/menu.c b/libraries/wuss/menu/menu.c index 6d8a9d10..5934e1ef 100644 --- a/libraries/wuss/menu/menu.c +++ b/libraries/wuss/menu/menu.c @@ -182,9 +182,6 @@ static result_t wuss__menu_spawn(wuss_t *wuss, int widest; int width, height; int i; - int outline_px; - int titlebar_height; - wuss_window_flags_t menu_flags; size2d_t size; box_t content; @@ -195,11 +192,6 @@ static result_t wuss__menu_spawn(wuss_t *wuss, if (menu->nitems <= 0) return result_WUSS_BAD_ICON; - menu_flags = 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; - bmfont_get_info(wuss->font, &fw, &fh); NOT_USED(fw); pitch = fh + 2 * WUSS_MENU_ROW_PAD; @@ -228,22 +220,15 @@ static result_t wuss__menu_spawn(wuss_t *wuss, for (i = 0; i < menu->nitems; i++) height += (menu->items[i].flags & wuss_MENU_ITEM_DASHED) ? sep_h : pitch; - /* Nudge onto the screen where it can be, but keep the top-left on screen. - * `at` is the content top-left; the window's visible box also spans the - * outline on all four sides and the titlebar above, so clamp against that - * extent -- otherwise wuss_window_create's own on-screen size clamp trims - * the content and the last row (e.g. a trailing "Quit") is cropped. */ - outline_px = wuss__outline_px_for(menu_flags); - titlebar_height = wuss__titlebar_height_for(wuss, menu_flags); - - if (at.x + width + outline_px > wuss->scr->size.w) - at.x = wuss->scr->size.w - width - outline_px; - if (at.y + height + outline_px > wuss->scr->size.h) - at.y = wuss->scr->size.h - height - outline_px; - if (at.x - outline_px < 0) - at.x = outline_px; - if (at.y - outline_px - titlebar_height < 0) - at.y = outline_px + titlebar_height; + /* Nudge onto the screen where it can be, but keep the top-left on screen. */ + if (at.x + width > wuss->scr->size.w) + at.x = wuss->scr->size.w - width; + if (at.y + height > wuss->scr->size.h) + at.y = wuss->scr->size.h - height; + if (at.x < 0) + at.x = 0; + if (at.y < 0) + at.y = 0; node = wuss__malloc(wuss, sizeof(*node)); if (node == NULL) @@ -318,7 +303,10 @@ static result_t wuss__menu_spawn(wuss_t *wuss, rc = wuss_window_create(wuss, &content, menu->title ? menu->title : "", - menu_flags, + 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->white), &task, size, size, &node->window); if (rc != result_OK) From 5689b2ad364e0f422fd1d5d836ededb533b6e659 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Tue, 1 Sep 2026 14:34:11 +0100 Subject: [PATCH 141/287] Revert "fix(wuss): give menus an outline" This reverts commit c7322c40dd590f0eedd2fd775f56e2d4802e45a9. --- libraries/wuss/menu/menu.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libraries/wuss/menu/menu.c b/libraries/wuss/menu/menu.c index 5934e1ef..6fdccc8d 100644 --- a/libraries/wuss/menu/menu.c +++ b/libraries/wuss/menu/menu.c @@ -303,7 +303,8 @@ static result_t wuss__menu_spawn(wuss_t *wuss, rc = wuss_window_create(wuss, &content, menu->title ? menu->title : "", - wuss_WINDOW_NO_CLOSE | wuss_WINDOW_NO_BACK + 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, From ccfdef56f69233c1eb495aa139108e1f09e7f8b8 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Tue, 1 Sep 2026 14:41:39 +0100 Subject: [PATCH 142/287] fix(wuss): restore the Quit row in the app demo menu 74d23fe redefined wuss_MENU_ITEM_DASHED from "pickable item with a rule above it" to "textless short separator row". The descriptor parser was updated to match, but the app's hardcoded g_menu still set DASHED on the Quit item itself, so menu.c discarded its text and drew a 6px blank rule instead of a Quit entry. Split it into a standalone DASHED separator followed by a plain Quit item. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- apps/wuss/main.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/wuss/main.c b/apps/wuss/main.c index 2de23bc4..fc43c76e 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -205,8 +205,8 @@ static result_t spawn_porter_duff(void) } /* A static demo menu tree for the pop-up helper: a submenu, a couple of - * ticked rows and a dashed rule above the final entry. wuss_menu_open never - * mutates it. */ + * 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 }, @@ -224,7 +224,8 @@ static const wuss_menu_item_t g_menu_items[] = { "Show grid", wuss_MENU_ITEM_TICKED, NULL }, { "Wireframe", wuss_MENU_ITEM_TICKED, NULL }, { "Export", wuss_MENU_ITEM_NONE, &g_menu_export }, - { "Quit", wuss_MENU_ITEM_DASHED, NULL } + { "", wuss_MENU_ITEM_DASHED, NULL }, + { "Quit", wuss_MENU_ITEM_NONE, NULL } }; static const wuss_menu_t g_menu = { From ea0413de743f5ccf7f81b6df7f560649995c82f4 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Tue, 1 Sep 2026 14:41:43 +0100 Subject: [PATCH 143/287] Reapply "fix(wuss): give menus an outline" This reverts commit 5689b2ad364e0f422fd1d5d836ededb533b6e659. --- libraries/wuss/menu/menu.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/libraries/wuss/menu/menu.c b/libraries/wuss/menu/menu.c index 6fdccc8d..5934e1ef 100644 --- a/libraries/wuss/menu/menu.c +++ b/libraries/wuss/menu/menu.c @@ -303,8 +303,7 @@ static result_t wuss__menu_spawn(wuss_t *wuss, rc = wuss_window_create(wuss, &content, menu->title ? menu->title : "", - wuss_WINDOW_NO_OUTLINE - | wuss_WINDOW_NO_CLOSE | wuss_WINDOW_NO_BACK + 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, From 158490ff944878d18be00b680cbdae1a3d4d5652 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Tue, 1 Sep 2026 14:41:43 +0100 Subject: [PATCH 144/287] Reapply "fix(wuss): stop the menu outline cropping the last row" This reverts commit b5561a60ee4c14acce3b68c283ead85b825ed7c7. --- libraries/wuss/menu/menu.c | 38 +++++++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/libraries/wuss/menu/menu.c b/libraries/wuss/menu/menu.c index 5934e1ef..6d8a9d10 100644 --- a/libraries/wuss/menu/menu.c +++ b/libraries/wuss/menu/menu.c @@ -182,6 +182,9 @@ static result_t wuss__menu_spawn(wuss_t *wuss, int widest; int width, height; int i; + int outline_px; + int titlebar_height; + wuss_window_flags_t menu_flags; size2d_t size; box_t content; @@ -192,6 +195,11 @@ static result_t wuss__menu_spawn(wuss_t *wuss, if (menu->nitems <= 0) return result_WUSS_BAD_ICON; + menu_flags = 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; + bmfont_get_info(wuss->font, &fw, &fh); NOT_USED(fw); pitch = fh + 2 * WUSS_MENU_ROW_PAD; @@ -220,15 +228,22 @@ static result_t wuss__menu_spawn(wuss_t *wuss, for (i = 0; i < menu->nitems; i++) height += (menu->items[i].flags & wuss_MENU_ITEM_DASHED) ? sep_h : pitch; - /* Nudge onto the screen where it can be, but keep the top-left on screen. */ - if (at.x + width > wuss->scr->size.w) - at.x = wuss->scr->size.w - width; - if (at.y + height > wuss->scr->size.h) - at.y = wuss->scr->size.h - height; - if (at.x < 0) - at.x = 0; - if (at.y < 0) - at.y = 0; + /* Nudge onto the screen where it can be, but keep the top-left on screen. + * `at` is the content top-left; the window's visible box also spans the + * outline on all four sides and the titlebar above, so clamp against that + * extent -- otherwise wuss_window_create's own on-screen size clamp trims + * the content and the last row (e.g. a trailing "Quit") is cropped. */ + outline_px = wuss__outline_px_for(menu_flags); + titlebar_height = wuss__titlebar_height_for(wuss, menu_flags); + + if (at.x + width + outline_px > wuss->scr->size.w) + at.x = wuss->scr->size.w - width - outline_px; + if (at.y + height + outline_px > wuss->scr->size.h) + at.y = wuss->scr->size.h - height - outline_px; + if (at.x - outline_px < 0) + at.x = outline_px; + if (at.y - outline_px - titlebar_height < 0) + at.y = outline_px + titlebar_height; node = wuss__malloc(wuss, sizeof(*node)); if (node == NULL) @@ -303,10 +318,7 @@ static result_t wuss__menu_spawn(wuss_t *wuss, rc = wuss_window_create(wuss, &content, menu->title ? menu->title : "", - 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, + menu_flags, wuss_BACKDROP_COLOUR(wuss->white), &task, size, size, &node->window); if (rc != result_OK) From a299ebcdac15088e251d4f17945596476e17f528 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Tue, 1 Sep 2026 15:44:58 +0100 Subject: [PATCH 145/287] refactor(wuss): make menu separators a flag on a real entry plus a RULE icon wuss_ICON_FLAGS_SEPARATOR / wuss_MENU_ITEM_DASHED no longer turn a row into a standalone inert separator. The flagged item stays a normal interactive entry with its own label; the dashed line is drawn by a new inert wuss_ICON_TYPE_RULE icon laid out separately above the entry. - icon.h: add wuss_ICON_TYPE_RULE; reword SEPARATOR flag and MENU_ENTRY docs - icon/create.c: accept the new type - icon/draw.c: add wuss__icon_draw_rule; drop the dashed-rule branch from wuss__icon_draw_menu_entry - icon/hit-test.c: RULE falls through as inert; drop the dead SEPARATOR/empty special case - menu/menu.c: emit a RULE icon of height sep_h before each dashed item; entries are now full-height; node->icons stays item-indexed (rule icons are owned by the window and not stored) - menu/create-from-desc.c: '|' sets DASHED on the prefixed item instead of emitting a blank separator item - tests and the wuss demo app updated to the new model Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- apps/wuss/main.c | 3 +- include/wuss/icon.h | 37 ++++++++---- include/wuss/menu.h | 28 +++++---- libraries/wuss/icon/create.c | 3 +- libraries/wuss/icon/draw.c | 30 ++++++---- libraries/wuss/icon/hit-test.c | 13 ++-- libraries/wuss/menu/create-from-desc.c | 14 ++--- libraries/wuss/menu/menu.c | 83 +++++++++++++++++++------- libraries/wuss/test/tasks/icons.c | 19 ++++-- libraries/wuss/test/wuss-test.c | 14 ++--- 10 files changed, 151 insertions(+), 93 deletions(-) diff --git a/apps/wuss/main.c b/apps/wuss/main.c index fc43c76e..a11a8c81 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -224,8 +224,7 @@ static const wuss_menu_item_t g_menu_items[] = { "Show grid", wuss_MENU_ITEM_TICKED, NULL }, { "Wireframe", wuss_MENU_ITEM_TICKED, NULL }, { "Export", wuss_MENU_ITEM_NONE, &g_menu_export }, - { "", wuss_MENU_ITEM_DASHED, NULL }, - { "Quit", wuss_MENU_ITEM_NONE, NULL } + { "Quit", wuss_MENU_ITEM_DASHED, NULL } }; static const wuss_menu_t g_menu = { diff --git a/include/wuss/icon.h b/include/wuss/icon.h index ff3bcd94..92aa5920 100644 --- a/include/wuss/icon.h +++ b/include/wuss/icon.h @@ -90,7 +90,7 @@ typedef enum wuss_icon_type * wuss_ICON_FLAGS_INTERACTIVE is set, in which * case clicks raise wuss_EVENT_ICON like a * button. */ - wuss_ICON_TYPE_MENU_ENTRY /**< A menu row: text left-justified across the + wuss_ICON_TYPE_MENU_ENTRY, /**< A menu row: text left-justified across the * bounding box, drawn in fg over the window * background, or inverted (window manager's title * colours) while the pointer is over it. An @@ -98,14 +98,22 @@ typedef enum wuss_icon_type * selected (see wuss_icon_set_selected), and an * optional submenu arrow at the right edge with * wuss_ICON_FLAGS_SUBMENU. - * wuss_ICON_FLAGS_SEPARATOR draws a dashed rule - * along the top edge before the text, marking a - * group boundary; the entry keeps its label and - * stays interactive (an entry with the flag and - * no text is just a bare rule). Interactive: a - * click raises wuss_EVENT_ICON like a button; a - * disabled entry never highlights and its clicks - * fall through. */ + * wuss_ICON_FLAGS_SEPARATOR marks the entry as + * following a group boundary: the dashed rule + * itself is a separate wuss_ICON_TYPE_RULE icon + * laid out above the entry, not drawn by the + * entry. The entry keeps its label and stays + * fully interactive. Interactive: a click raises + * wuss_EVENT_ICON like a button; a disabled + * entry never highlights and its clicks fall + * through. */ + wuss_ICON_TYPE_RULE /**< A horizontal dashed rule centred in the + * bounding box, drawn in fg over the window + * background. Purely decorative: never + * hit-tested, never highlights. text, bg and + * pattern are ignored. Used between menu rows to + * render the line a wuss_ICON_FLAGS_SEPARATOR + * entry sits below. */ } wuss_icon_type_t; @@ -145,10 +153,13 @@ typedef enum wuss_icon_flags * a submenu. Ignored by other * types. */ wuss_ICON_FLAGS_SEPARATOR = 1 << 7 /**< wuss_ICON_TYPE_MENU_ENTRY: the - * entry is a separator -- a centred - * dashed rule, no text, and not - * hit-tested. Ignored by other - * types. */ + * entry follows a group boundary. It + * stays a normal interactive row + * with its own label; the dashed + * rule above it is laid out and + * drawn as a separate + * wuss_ICON_TYPE_RULE icon. Ignored + * by other types. */ } wuss_icon_flags_t; diff --git a/include/wuss/menu.h b/include/wuss/menu.h index 759fd645..bbb36d93 100644 --- a/include/wuss/menu.h +++ b/include/wuss/menu.h @@ -35,10 +35,12 @@ typedef enum wuss_menu_item_flags { wuss_MENU_ITEM_NONE = 0, wuss_MENU_ITEM_TICKED = 1 << 0, /**< draw a tick at the item's left edge */ - wuss_MENU_ITEM_DASHED = 1 << 1, /**< this item is a separator: its own - * short row holding a centred dashed - * rule. The text is ignored and the row - * does not respond to the pointer */ + wuss_MENU_ITEM_DASHED = 1 << 1, /**< draw a dashed rule above this item, + * marking a group boundary. The item is + * otherwise an ordinary row: it keeps + * its label and responds to the pointer. + * The rule is laid out and drawn + * separately and is not interactive */ wuss_MENU_ITEM_DISABLED = 1 << 2 /**< greyed, never highlights, not * selectable */ } @@ -119,17 +121,17 @@ int wuss_menu_is_open(wuss_menu_handle_t handle); * as the first token inside a '{ }' is that submenu's title -- so the same * descriptor strings port across unchanged. Submenus keep the Wimp behaviour of * discarding their title token; only the root's is kept. A leading '|' on an - * item inserts a separator row above it: its own short, inert row holding a - * centred dashed rule. A '{ ... }' group after an item is that item's submenu. - * A per-token prefix '!' ticks the item, '~' shades (disables) it, and '>' - * attaches a submenu pulled as a <tt>const wuss_menu_t *</tt> from the varargs - * rather than from a following '{ }' block. A "%s" in a token substitutes the - * next <tt>const char *</tt> vararg. The '>' and "%s" varargs are consumed in - * the order they are encountered scanning left to right. + * item draws a dashed rule above it, marking a group boundary; the item itself + * stays an ordinary interactive row. A '{ ... }' group after an item is that + * item's submenu. A per-token prefix '!' ticks the item, '~' shades (disables) + * it, and '>' attaches a submenu pulled as a <tt>const wuss_menu_t *</tt> from + * the varargs rather than from a following '{ }' block. A "%s" in a token + * substitutes the next <tt>const char *</tt> vararg. The '>' and "%s" varargs + * are consumed in the order they are encountered scanning left to right. * * Example: <tt>wuss_menu_create_from_desc(&m, "Display, Open, !Grid, ~Export, - * |Quit")</tt> -- "Display" is the caption; the menu has four items plus a - * separator row before "Quit". + * |Quit")</tt> -- "Display" is the caption; the menu has four items, with a + * dashed rule above "Quit". * * The whole tree, including copied label text, is one heap allocation graph * owned by the caller; free it with wuss_menu_destroy. wuss_menu_open treats a diff --git a/libraries/wuss/icon/create.c b/libraries/wuss/icon/create.c index 7cd21c70..c45c8cbc 100644 --- a/libraries/wuss/icon/create.c +++ b/libraries/wuss/icon/create.c @@ -33,7 +33,8 @@ result_t wuss_icon_create(wuss_window_t *window, spec->type != wuss_ICON_TYPE_RADIO && spec->type != wuss_ICON_TYPE_OPTION && spec->type != wuss_ICON_TYPE_BITMAP && - spec->type != wuss_ICON_TYPE_MENU_ENTRY) + spec->type != wuss_ICON_TYPE_MENU_ENTRY && + spec->type != wuss_ICON_TYPE_RULE) return result_WUSS_BAD_ICON; if ((spec->type == wuss_ICON_TYPE_BUTTON || diff --git a/libraries/wuss/icon/draw.c b/libraries/wuss/icon/draw.c index d1de16eb..8a9b0873 100644 --- a/libraries/wuss/icon/draw.c +++ b/libraries/wuss/icon/draw.c @@ -351,7 +351,7 @@ static void wuss__icon_draw_menu_entry(const icon_draw_ctx_t *c) const wuss_icon_t *icon = c->icon; const box_t *b = &c->b; colour_t ink, ground, tmp; - int disabled, highlit, pad, mid_y; + int disabled, highlit, pad; disabled = (icon->flags & wuss_ICON_FLAGS_DISABLED) != 0; highlit = wuss__icon_hovered(icon) && !disabled; @@ -373,15 +373,6 @@ static void wuss__icon_draw_menu_entry(const icon_draw_ctx_t *c) screen_fill_rect(c->scr, b->x0, b->y0, SIZE2D(b->x1 - b->x0, b->y1 - b->y0), ground); - /* a separator is its own short, textless row: a dashed rule centred in it, - * drawn in the row's ink so it stays visible if the row is inverted */ - if (icon->flags & wuss_ICON_FLAGS_SEPARATOR) - { - mid_y = b->y0 + (b->y1 - b->y0) / 2; - screen_draw_dashed_line(c->scr, b->x0 + pad, mid_y, - b->x1 - 1 - pad, mid_y, 2, 2, ink); - } - /* left-edge tick when selected */ if (wuss__icon_selected(icon)) { @@ -420,6 +411,21 @@ static void wuss__icon_draw_menu_entry(const icon_draw_ctx_t *c) /* ----------------------------------------------------------------------- */ +/* wuss_ICON_TYPE_RULE: a dashed rule centred in the box, drawn in the icon's + * fg over whatever ground it inherits. Inert -- see wuss__icon_hit_test. */ +static void wuss__icon_draw_rule(const icon_draw_ctx_t *c) +{ + const box_t *b = &c->b; + int pad, mid_y; + + pad = 4; + mid_y = b->y0 + (b->y1 - b->y0) / 2; + screen_draw_dashed_line(c->scr, b->x0 + pad, mid_y, + b->x1 - 1 - pad, mid_y, 2, 2, c->fg); +} + +/* ----------------------------------------------------------------------- */ + void wuss__icon_draw(wuss_t *wuss, const wuss_icon_t *icon, const box_t *content, @@ -478,5 +484,9 @@ void wuss__icon_draw(wuss_t *wuss, case wuss_ICON_TYPE_MENU_ENTRY: wuss__icon_draw_menu_entry(&c); break; + + case wuss_ICON_TYPE_RULE: + wuss__icon_draw_rule(&c); + break; } } diff --git a/libraries/wuss/icon/hit-test.c b/libraries/wuss/icon/hit-test.c index 78b87618..2e9ab7c8 100644 --- a/libraries/wuss/icon/hit-test.c +++ b/libraries/wuss/icon/hit-test.c @@ -14,22 +14,17 @@ wuss_icon_t *wuss__icon_hit_test(wuss_window_t *window, point_t doc_point) { it = window->icons[i]; - /* a bitmap icon is interactive only when it asks to be; a menu entry that - * is a separator (SEPARATOR flag, no text) is inert; the other clickable - * types always are */ + /* a bitmap icon is interactive only when it asks to be; RULE and the + * static types are always inert; the other types always click */ if (it->type == wuss_ICON_TYPE_BITMAP) { if (!(it->flags & wuss_ICON_FLAGS_INTERACTIVE)) continue; } - else if (it->type == wuss_ICON_TYPE_MENU_ENTRY) - { - if ((it->flags & wuss_ICON_FLAGS_SEPARATOR) && it->text[0] == '\0') - continue; - } else if (it->type != wuss_ICON_TYPE_BUTTON && it->type != wuss_ICON_TYPE_RADIO && - it->type != wuss_ICON_TYPE_OPTION) + it->type != wuss_ICON_TYPE_OPTION && + it->type != wuss_ICON_TYPE_MENU_ENTRY) { continue; } diff --git a/libraries/wuss/menu/create-from-desc.c b/libraries/wuss/menu/create-from-desc.c index f0a6f0c9..5418e284 100644 --- a/libraries/wuss/menu/create-from-desc.c +++ b/libraries/wuss/menu/create-from-desc.c @@ -199,7 +199,7 @@ static result_t menu_deep_copy(const wuss_menu_t *src, wuss_menu_t **out) /* ----------------------------------------------------------------------- */ /* A menu under construction: a growable wuss_menu_item_t array plus the - * separator flag owed to the next-emitted item. */ + * pending '|' dash flag owed to the next-added item. */ typedef struct { wuss_menu_item_t *items; @@ -238,19 +238,17 @@ static result_t build_add(Building *b, wuss_menu_t *submenu) { unsigned int flags; - result_t rc; - /* '|' before this item means "insert a separator row above it": a distinct - * DASHED item with no text of its own, emitted before the real one. */ + flags = wuss_MENU_ITEM_NONE; + + /* '|' before this item draws a dashed rule above it: set DASHED on the item + * itself, which stays an ordinary interactive row. */ if (b->pending_dash) { b->pending_dash = 0; - rc = build_emit(b, "", wuss_MENU_ITEM_DASHED, NULL); - if (rc != result_OK) - return rc; + flags |= wuss_MENU_ITEM_DASHED; } - flags = wuss_MENU_ITEM_NONE; if (opt & Tick) flags |= wuss_MENU_ITEM_TICKED; if (opt & Shade) diff --git a/libraries/wuss/menu/menu.c b/libraries/wuss/menu/menu.c index 6d8a9d10..9cd73ef6 100644 --- a/libraries/wuss/menu/menu.c +++ b/libraries/wuss/menu/menu.c @@ -173,6 +173,7 @@ static result_t wuss__menu_spawn(wuss_t *wuss, { struct wuss__menu *node; wuss_icon_spec_t *specs; + wuss_icon_t **made; wuss_task_t task; result_t rc; int fw, fh; @@ -182,6 +183,9 @@ static result_t wuss__menu_spawn(wuss_t *wuss, int widest; int width, height; int i; + int ndashed; + int nspecs; + int s; int outline_px; int titlebar_height; wuss_window_flags_t menu_flags; @@ -205,6 +209,12 @@ static result_t wuss__menu_spawn(wuss_t *wuss, pitch = fh + 2 * WUSS_MENU_ROW_PAD; sep_h = 2 * WUSS_MENU_ROW_PAD; + ndashed = 0; + for (i = 0; i < menu->nitems; i++) + if (menu->items[i].flags & wuss_MENU_ITEM_DASHED) + ndashed++; + nspecs = menu->nitems + ndashed; + widest = 0; for (i = 0; i < menu->nitems; i++) { @@ -224,9 +234,8 @@ static result_t wuss__menu_spawn(wuss_t *wuss, width = WUSS_MENU_TICK_W + widest + WUSS_MENU_TEXT_PAD + WUSS_MENU_ARROW_W; - height = 0; - for (i = 0; i < menu->nitems; i++) - height += (menu->items[i].flags & wuss_MENU_ITEM_DASHED) ? sep_h : pitch; + /* every item is a full row now; a dashed item also gets a sep_h rule above */ + height = menu->nitems * pitch + ndashed * sep_h; /* Nudge onto the screen where it can be, but keep the top-left on screen. * `at` is the content top-left; the window's visible box also spans the @@ -250,9 +259,11 @@ static result_t wuss__menu_spawn(wuss_t *wuss, return result_OOM; node->icons = wuss__malloc(wuss, (size_t) menu->nitems * sizeof(*node->icons)); - specs = wuss__malloc(wuss, (size_t) menu->nitems * sizeof(*specs)); - if (node->icons == NULL || specs == NULL) + specs = wuss__malloc(wuss, (size_t) nspecs * sizeof(*specs)); + made = wuss__malloc(wuss, (size_t) nspecs * sizeof(*made)); + if (node->icons == NULL || specs == NULL || made == NULL) { + wuss__free(wuss, made); wuss__free(wuss, specs); wuss__free(wuss, node->icons); wuss__free(wuss, node); @@ -269,39 +280,53 @@ static result_t wuss__menu_spawn(wuss_t *wuss, node->ctx = ctx; node->open_index = -1; + /* One MENU_ENTRY icon per item, in item order, preceded by an inert + * wuss_ICON_TYPE_RULE icon for each dashed item. specs[] therefore holds + * nspecs entries; s walks it while i walks the items. */ y = 0; + s = 0; for (i = 0; i < menu->nitems; i++) { const wuss_menu_item_t *item; wuss_icon_flags_t flags; - int row_h; item = &menu->items[i]; flags = wuss_ICON_FLAGS_NONE; if (item->flags & wuss_MENU_ITEM_DASHED) + { flags |= wuss_ICON_FLAGS_SEPARATOR; + + specs[s].type = wuss_ICON_TYPE_RULE; + specs[s].bbox.x0 = 0; + specs[s].bbox.y0 = y; + specs[s].bbox.x1 = width; + specs[s].bbox.y1 = y + sep_h; + specs[s].text = ""; + specs[s].fg = 0; + specs[s].bg = wuss_NO_BACKGROUND; + specs[s].flags = wuss_ICON_FLAGS_NONE; + s++; + y += sep_h; + } + if (item->flags & wuss_MENU_ITEM_DISABLED) flags |= wuss_ICON_FLAGS_DISABLED; if (item->submenu != NULL) flags |= wuss_ICON_FLAGS_SUBMENU; - /* a separator is its own short, inert row: force the text empty so - * wuss__icon_hit_test treats it as a bare rule */ - row_h = (flags & wuss_ICON_FLAGS_SEPARATOR) ? sep_h : pitch; - - specs[i].type = wuss_ICON_TYPE_MENU_ENTRY; - specs[i].bbox.x0 = 0; - specs[i].bbox.y0 = y; - specs[i].bbox.x1 = width; - specs[i].bbox.y1 = y + row_h; - specs[i].text = (flags & wuss_ICON_FLAGS_SEPARATOR) - ? "" : (item->text ? item->text : ""); - specs[i].fg = 0; - specs[i].bg = wuss_NO_BACKGROUND; - specs[i].flags = flags; - - y += row_h; + specs[s].type = wuss_ICON_TYPE_MENU_ENTRY; + specs[s].bbox.x0 = 0; + specs[s].bbox.y0 = y; + specs[s].bbox.x1 = width; + specs[s].bbox.y1 = y + pitch; + specs[s].text = item->text ? item->text : ""; + specs[s].fg = 0; + specs[s].bg = wuss_NO_BACKGROUND; + specs[s].flags = flags; + s++; + + y += pitch; } size = SIZE2D(width, height); @@ -323,27 +348,39 @@ static result_t wuss__menu_spawn(wuss_t *wuss, &task, size, size, &node->window); if (rc != result_OK) { + wuss__free(wuss, made); wuss__free(wuss, specs); wuss__free(wuss, node->icons); wuss__free(wuss, node); return rc; } - rc = wuss_icon_create_array(node->window, specs, menu->nitems, node->icons); + rc = wuss_icon_create_array(node->window, specs, nspecs, made); wuss__free(wuss, specs); if (rc != result_OK) { + wuss__free(wuss, made); wuss_window_close(node->window); wuss__free(wuss, node->icons); wuss__free(wuss, node); return rc; } + /* keep only the entry handles, item-indexed; the rule icons stay owned by + * the window and need no further handling. Walk made[] with the same + * rule-then-entry interleave as the specs loop. */ + s = 0; for (i = 0; i < menu->nitems; i++) { + if (menu->items[i].flags & wuss_MENU_ITEM_DASHED) + s++; + node->icons[i] = made[s]; + s++; + if (menu->items[i].flags & wuss_MENU_ITEM_TICKED) wuss_icon_set_selected(node->icons[i], 1); } + wuss__free(wuss, made); *out = node; return result_OK; diff --git a/libraries/wuss/test/tasks/icons.c b/libraries/wuss/test/tasks/icons.c index 41b144c9..f8cb6c06 100644 --- a/libraries/wuss/test/tasks/icons.c +++ b/libraries/wuss/test/tasks/icons.c @@ -36,7 +36,7 @@ result_t icons_create(wuss_t *wuss, * grouped radios, a standalone option and a label echoing the selection, * then [.. +2] a decorative bitmap icon and an interactive one that bumps * the counter, then [.. +4] a strip of menu-entry icons that hover-highlight */ - enum { ICONS_NSPECS = 4 + screen_PATTERN__LIMIT + 3 + 5 + 2 + 4 }; + enum { ICONS_NSPECS = 4 + screen_PATTERN__LIMIT + 3 + 5 + 2 + 5 }; wuss_task_t delegate; wuss_icon_spec_t specs[ICONS_NSPECS]; wuss_icon_t *made[ICONS_NSPECS]; @@ -208,8 +208,9 @@ result_t icons_create(wuss_t *wuss, nspecs = g + 10; } - /* [nspecs..nspecs+3] a menu-entry strip: plain, ticked, submenu, separator. - * Hover the pointer over them to see the highlight track. */ + /* [nspecs..nspecs+4] a menu-entry strip: plain, ticked, submenu, then a + * standalone dashed rule and a SEPARATOR-flagged entry below it. Hover the + * pointer over them to see the highlight track; the rule stays inert. */ m = nspecs; specs[m].bbox = (box_t) BOX_POS_SIZE(28, 524, 160, 16); @@ -232,12 +233,18 @@ result_t icons_create(wuss_t *wuss, specs[m + 2].flags = wuss_ICON_FLAGS_SUBMENU; specs[m + 3].bbox = (box_t) BOX_POS_SIZE(28, 578, 160, 10); - specs[m + 3].type = wuss_ICON_TYPE_MENU_ENTRY; + specs[m + 3].type = wuss_ICON_TYPE_RULE; specs[m + 3].fg = palette_PICO8_DARK_BLUE; specs[m + 3].bg = wuss_NO_BACKGROUND; - specs[m + 3].flags = wuss_ICON_FLAGS_SEPARATOR; - nspecs = m + 4; + specs[m + 4].bbox = (box_t) BOX_POS_SIZE(28, 588, 160, 16); + specs[m + 4].type = wuss_ICON_TYPE_MENU_ENTRY; + specs[m + 4].text = "Quit"; + specs[m + 4].fg = palette_PICO8_DARK_BLUE; + specs[m + 4].bg = wuss_NO_BACKGROUND; + specs[m + 4].flags = wuss_ICON_FLAGS_SEPARATOR; + + nspecs = m + 5; rc = wuss_icon_create_array(task->window, specs, nspecs, made); if (rc != result_OK) diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index f203cab2..4d80377f 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -2966,8 +2966,8 @@ result_t wuss_test(const char *resources) /* "Root" is the root caption (first token), then items App, * File{ %s title discarded, Grid(!), Export(~), |Quit }, Help(> borrowed), - * with "File" substituted for both %s. '|Quit' inserts a standalone DASHED - * separator item before Quit, so the submenu holds 4 rows. */ + * with "File" substituted for both %s. '|Quit' sets DASHED on Quit itself, + * so the submenu holds 3 rows. */ rc = wuss_menu_create_from_desc(&m, "Root, App, %s { %s, !Grid, ~Export, |Quit }, >Help", "File", "File", &borrowed); @@ -2980,18 +2980,16 @@ result_t wuss_test(const char *resources) if (m->items[0].submenu != NULL) goto MenuFail; /* items[1] "File" carries the { } submenu; its first token was the title - * and is not emitted; '|Quit' inserts a separate DASHED separator row - * before Quit, which keeps its own text and no DASHED flag */ + * and is not emitted; '|Quit' sets DASHED on the Quit row itself, which + * keeps its label */ if (strcmp(m->items[1].text, "File") != 0) goto MenuFail; sub = m->items[1].submenu; - if (sub == NULL || sub->nitems != 4) goto MenuFail; + if (sub == NULL || sub->nitems != 3) goto MenuFail; if (strcmp(sub->items[0].text, "Grid") != 0) goto MenuFail; if (!(sub->items[0].flags & wuss_MENU_ITEM_TICKED)) goto MenuFail; if (!(sub->items[1].flags & wuss_MENU_ITEM_DISABLED)) goto MenuFail; - if (sub->items[2].text[0] != '\0') goto MenuFail; + if (strcmp(sub->items[2].text, "Quit") != 0) goto MenuFail; if (!(sub->items[2].flags & wuss_MENU_ITEM_DASHED)) goto MenuFail; - if (strcmp(sub->items[3].text, "Quit") != 0) goto MenuFail; - if (sub->items[3].flags & wuss_MENU_ITEM_DASHED) goto MenuFail; /* items[2] "Help" got a deep copy of the borrowed menu */ if (strcmp(m->items[2].text, "Help") != 0) goto MenuFail; From cdef3f72d19618d23fdcc7caad706ce3b4172c5d Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Tue, 1 Sep 2026 15:56:38 +0100 Subject: [PATCH 146/287] fix(wuss): stop scroll blitting content over a mid-content occluder wuss_window_set_scroll clipped the blit source against occluding windows but not the destination. A source slice below a smaller window on top, slid by the scroll delta, could land its destination behind that window; screen_copy_rect only clips to the content box, so it painted the scrolling window's content over the occluder and marked the area 'copied', excluding it from the repaint set so nothing restored it. Clip each blit destination against the occluders too and copy only the surviving sub-pieces, each with its own matching source offset. Adds a wuss test that paints two windows in distinct colours and reads back the framebuffer behind the occluder after a scroll. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- libraries/wuss/test/wuss-test.c | 112 +++++++++++++++++++++++++++++ libraries/wuss/window/set-scroll.c | 34 ++++++--- 2 files changed, 135 insertions(+), 11 deletions(-) diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 4d80377f..bac8d4f2 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -1,5 +1,6 @@ /* wuss/test/wuss-test.c -- wuss - minimal window manager */ +#include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h> @@ -96,6 +97,33 @@ static result_t test_handle(wuss_window_t *window, return result_OK; } +/* A redraw handler that floods its dirty box with a fixed colour, so a test + * can read back the framebuffer and tell whose pixels ended up where. The + * colour is passed via task_data (a pointer to a colour_t). */ +static result_t paint_handle(wuss_window_t *window, + const wuss_event_t *event, + void *task_data) +{ + const colour_t *col; + + NOT_USED(window); + + if (event->kind != wuss_EVENT_REDRAW) + return result_OK; + + col = task_data; + screen_fill_rect(event->data.redraw.scr, + event->data.redraw.content->x0, + event->data.redraw.content->y0, + SIZE2D(event->data.redraw.content->x1 + - event->data.redraw.content->x0, + event->data.redraw.content->y1 + - event->data.redraw.content->y0), + *col); + + return result_OK; +} + /* ----------------------------------------------------------------------- */ /* Total area covered by the dirty list, counting overlapped pixels once. @@ -1692,6 +1720,90 @@ result_t wuss_test(const char *resources) wuss_window_close(win_s); } + printf("test: wuss_window_set_scroll never blits a scrolled window's content over a mid-content occluder\n"); + + { + /* Regression: a small window O sits in the middle of a larger scrollable + * window M's content, covering neither M's top nor bottom edge. A + * programmatic vertical scroll of M takes a blit source slice from below + * O and shifts it up by the scroll delta, so its destination overlaps O. + * screen_copy_rect only clips to the content box, so without the fix it + * paints M's content over O and marks that area "copied" -- excluding it + * from the repaint set, so O's pixels stay overpainted with M's colour. + * With the fix each destination is clipped against the occluders and only + * the un-occluded sub-pieces are blitted, leaving O's own pixels intact. + * Read the framebuffer behind O to check. */ + colour_t cm, co; + uint32_t fb_m, fb_o; + test_task_t tc_m, tc_o; + wuss_task_t delegate_m, delegate_o; + box_t box_m, box_o, content; + wuss_window_t *win_m, *win_o; + int occ_x, occ_y, mid_x, mid_y, delta; + + cm = colour_rgb(0x11, 0x22, 0x33); + co = colour_rgb(0xcc, 0xdd, 0xee); + tc_m.redraw_count = 0; tc_m.mouse_count = 0; + tc_o.redraw_count = 0; tc_o.mouse_count = 0; + delegate_m.handle = paint_handle; delegate_m.task_data = &cm; + delegate_o.handle = paint_handle; delegate_o.task_data = &co; + + 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(wuss, &box_m, "M", wuss_WINDOW_NONE, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + &delegate_m, + SIZE2D(100, 300), SIZE2D(0, 0), &win_m); + if (rc != result_OK) + goto Failure; + + wuss_window_get_content_bounds(win_m, &content); + + /* occluder spanning M's full content width, a band in the vertical + * middle -- so wuss__clip_to_visible splits M's blit source into a top + * and a bottom band with nothing behind O. Scrolling down slides the + * bottom band up; part of its destination lands behind O, yet no dirty + * 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(wuss, &box_o, "O", wuss_WINDOW_NONE, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + &delegate_o, + SIZE2D(box_o.x1 - box_o.x0, box_o.y1 - box_o.y0), + SIZE2D(0, 0), &win_o); + if (rc != result_OK) + goto Failure; + + rc = wuss_redraw_dirty(wuss); /* flush both creates: M then O paint */ + if (rc != result_OK) + goto Failure; + + /* sample the framebuffer: occ_* sits behind O, mid_* sits in M's content + * clear of O -- record what each colour actually renders as */ + occ_x = (box_o.x0 + box_o.x1) / 2; + occ_y = box_o.y1 - 3; /* near O's bottom: the bottom band's blit + * destination reaches up to here */ + mid_x = content.x0 + 5; + mid_y = content.y0 + 5; + fb_o = ((const uint32_t *) pixels)[occ_y * 200 + occ_x]; + fb_m = ((const uint32_t *) pixels)[mid_y * 200 + mid_x]; + if (fb_o == fb_m) + goto Failure; /* the two windows must render distinguishable pixels */ + + delta = 20; + wuss_window_set_scroll(win_m, POINT(0, delta)); + rc = wuss_redraw_dirty(wuss); /* apply the scroll's blit + any repaint */ + if (rc != result_OK) + goto Failure; + + /* the pixel behind O must still be O's, not M's content blitted over it */ + if (((const uint32_t *) pixels)[occ_y * 200 + occ_x] != fb_o) + goto Failure; + + wuss_window_close(win_o); + wuss_window_close(win_m); + } + printf("test: wuss_WINDOW_NO_RESIZE_BLIT redraws the whole window instead of blitting\n"); { diff --git a/libraries/wuss/window/set-scroll.c b/libraries/wuss/window/set-scroll.c index 0acf264c..3aec5846 100644 --- a/libraries/wuss/window/set-scroll.c +++ b/libraries/wuss/window/set-scroll.c @@ -8,7 +8,8 @@ void wuss_window_set_scroll(wuss_window_t *window, point_t p) box_t src[WUSS_MAX_INVALIDATE_PIECES]; box_t copied[WUSS_MAX_INVALIDATE_PIECES]; box_t dirty[WUSS_MAX_INVALIDATE_PIECES]; - int dx, dy, nsrc, ncopied, ndirty, i; + box_t vis[WUSS_MAX_INVALIDATE_PIECES]; + int dx, dy, nsrc, ncopied, ndirty, nvis, i, j; dx = p.x - window->scroll.x; dy = p.y - window->scroll.y; @@ -26,22 +27,33 @@ void wuss_window_set_scroll(wuss_window_t *window, point_t p) #endif /* The valid blit source is the content box minus whatever windows above it - * were covering (those areas hold occluder pixels, not this window's), so - * slide it piece by piece by the scroll delta. The clip keeps every - * destination inside the content box, so a piece near the trailing edge - * shifts partly out and that area falls into the repaint set below. Falls - * back to a full content invalidate when the pixel format can't blit. */ + * were covering (those areas hold occluder pixels, not this window's). + * Sliding a source piece by the scroll delta can still land its + * destination on screen a higher window owns -- screen_copy_rect only + * clips to the content box, so it would paint over the occluder there. + * Clip each destination against the occluders too and blit only the + * surviving sub-pieces, each with its own matching source offset; the + * rest falls into the repaint set below. Falls back to a full content + * invalidate when the pixel format can't blit. */ nsrc = wuss__clip_to_visible(window, &content, src); ncopied = 0; window->wuss->scr->clip = content; for (i = 0; i < nsrc; i++) { - box_t got; + box_t want; - if (screen_copy_rect(window->wuss->scr, &src[i], - POINT(src[i].x0 - dx, src[i].y0 - dy), &got) != 0 && - ncopied < WUSS_MAX_INVALIDATE_PIECES) - copied[ncopied++] = got; + box_translated(&src[i], -dx, -dy, &want); + nvis = wuss__clip_to_visible(window, &want, vis); + for (j = 0; j < nvis; j++) + { + box_t ssub, got; + + box_translated(&vis[j], dx, dy, &ssub); /* source for this dest piece */ + if (screen_copy_rect(window->wuss->scr, &ssub, + POINT(ssub.x0 - dx, ssub.y0 - dy), &got) != 0 && + ncopied < WUSS_MAX_INVALIDATE_PIECES) + copied[ncopied++] = got; + } } if (ncopied > 0) From b1db2c65dfa0b5d4c9989e1ae619368e9fa0cad0 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Tue, 1 Sep 2026 16:21:03 +0100 Subject: [PATCH 147/287] fix(wuss): order scroll blit sub-pieces so one doesn't clobber another's source When a small window floats mid-content over a larger scrollable window with a gap all round it, wuss__clip_to_visible carves the scrolled window's blittable content into bands around the occluder. A vertical scroll shifts every band by the same delta, and one band's shifted destination can land on another band's still-unread source. Blitting the bands in clip-emit order then double-shifts the content near the occluder's bottom edge. Collect all blit sub-pieces first, order them with wuss__order_pieces (the same topological sort wuss_window_move already used, now promoted from static in move.c to a shared internal in invalidate.c), and fall back to a full content invalidate on a cycle or piece-budget overflow. Adds a regression test: paint_handle now paints one-pixel rows whose blue channel encodes document Y, so the test can read the gap column beside the occluder back and assert each row slid by exactly the scroll delta. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- libraries/wuss/impl.h | 10 +++ libraries/wuss/test/wuss-test.c | 131 +++++++++++++++++++++++++---- libraries/wuss/window/invalidate.c | 54 ++++++++++++ libraries/wuss/window/move.c | 54 ------------ libraries/wuss/window/set-scroll.c | 61 ++++++++++---- 5 files changed, 224 insertions(+), 86 deletions(-) diff --git a/libraries/wuss/impl.h b/libraries/wuss/impl.h index 84be1268..a74a5b11 100644 --- a/libraries/wuss/impl.h +++ b/libraries/wuss/impl.h @@ -222,6 +222,16 @@ int wuss__subtract_boxes(const box_t *whole, int ncuts, box_t *out); +/* Given "n" single-rect blits, each moving "clean[i]" to "dest[i]", find an + * order in which no blit's destination overwrites a still-unread source of a + * later blit. Writes the piece indices to "order" (capacity + * WUSS_MAX_INVALIDATE_PIECES) and returns non-zero on success; returns zero + * when the overlap graph has a cycle and no safe order exists. */ +int wuss__order_pieces(const box_t *clean, + const box_t *dest, + int n, + int *order); + /* 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/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index bac8d4f2..addd00ef 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -97,29 +97,45 @@ static result_t test_handle(wuss_window_t *window, return result_OK; } -/* A redraw handler that floods its dirty box with a fixed colour, so a test - * can read back the framebuffer and tell whose pixels ended up where. The - * colour is passed via task_data (a pointer to a colour_t). */ +/* A redraw handler that paints its content as one-pixel horizontal lines + * whose colour encodes the document-space Y of each row, so a test can read + * the framebuffer back and tell not just whose pixels are where but whether + * a blit slid them by the right amount. task_data points to a colour_t used + * only as a base hue (its blue channel is replaced per row). NULL task_data + * means flood with a single fixed colour instead. */ static result_t paint_handle(wuss_window_t *window, const wuss_event_t *event, void *task_data) { - const colour_t *col; + const box_t *clip; + const box_t *bounds; + point_t scroll; + int y, doc_y; NOT_USED(window); if (event->kind != wuss_EVENT_REDRAW) return result_OK; - col = task_data; - screen_fill_rect(event->data.redraw.scr, - event->data.redraw.content->x0, - event->data.redraw.content->y0, - SIZE2D(event->data.redraw.content->x1 - - event->data.redraw.content->x0, - event->data.redraw.content->y1 - - event->data.redraw.content->y0), - *col); + clip = event->data.redraw.content; + bounds = event->data.redraw.bounds; + scroll = event->data.redraw.scroll; + + if (task_data == NULL) + { + screen_fill_rect(event->data.redraw.scr, clip->x0, clip->y0, + SIZE2D(clip->x1 - clip->x0, clip->y1 - clip->y0), + colour_rgb(0xcc, 0xdd, 0xee)); + return result_OK; + } + + for (y = clip->y0; y < clip->y1; y++) + { + doc_y = y - bounds->y0 + scroll.y; + screen_fill_rect(event->data.redraw.scr, clip->x0, y, + SIZE2D(clip->x1 - clip->x0, 1), + colour_rgb(0x20, 0x40, doc_y & 0xff)); + } return result_OK; } @@ -1733,7 +1749,7 @@ result_t wuss_test(const char *resources) * With the fix each destination is clipped against the occluders and only * the un-occluded sub-pieces are blitted, leaving O's own pixels intact. * Read the framebuffer behind O to check. */ - colour_t cm, co; + colour_t cm; uint32_t fb_m, fb_o; test_task_t tc_m, tc_o; wuss_task_t delegate_m, delegate_o; @@ -1742,11 +1758,10 @@ result_t wuss_test(const char *resources) int occ_x, occ_y, mid_x, mid_y, delta; cm = colour_rgb(0x11, 0x22, 0x33); - co = colour_rgb(0xcc, 0xdd, 0xee); tc_m.redraw_count = 0; tc_m.mouse_count = 0; tc_o.redraw_count = 0; tc_o.mouse_count = 0; delegate_m.handle = paint_handle; delegate_m.task_data = &cm; - delegate_o.handle = paint_handle; delegate_o.task_data = &co; + delegate_o.handle = paint_handle; delegate_o.task_data = NULL; box_m.x0 = 10; box_m.y0 = 10; box_m.x1 = 110; box_m.y1 = 110; /* 100x100 content; doc taller, so scrollable */ @@ -1804,6 +1819,90 @@ result_t wuss_test(const char *resources) wuss_window_close(win_m); } + printf("test: wuss_window_set_scroll orders its blit sub-pieces so one never clobbers another's source\n"); + + { + /* Regression: a small window O floats in the middle of a larger + * scrollable window M, with a gap all round it. wuss__clip_to_visible + * carves M's blittable content into bands around O; a vertical scroll + * shifts each band by the same delta, and one band's shifted + * destination lands on another band's still-unread source. Blitting the + * bands in clip-emit order corrupts M's own content -- pixels near O's + * bottom edge end up double-shifted. The blit must be ordered (or fall + * back). paint_handle paints M as one-pixel rows whose blue channel + * encodes document Y, so a mis-shifted row is detectable exactly. */ + colour_t cm; + test_task_t tc_m, tc_o; + wuss_task_t delegate_m, delegate_o; + box_t box_m, box_o, content, ovis; + wuss_window_t *win_m, *win_o; + int gx, gy, delta, bad; + + cm = colour_rgb(0x11, 0x22, 0x33); + tc_m.redraw_count = 0; tc_m.mouse_count = 0; + tc_o.redraw_count = 0; tc_o.mouse_count = 0; + delegate_m.handle = paint_handle; delegate_m.task_data = &cm; + delegate_o.handle = paint_handle; delegate_o.task_data = NULL; + + 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(wuss, &box_m, "M", wuss_WINDOW_NONE, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + &delegate_m, + SIZE2D(100, 300), SIZE2D(0, 0), &win_m); + if (rc != result_OK) + goto Failure; + + wuss_window_get_content_bounds(win_m, &content); + + /* 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(wuss, &box_o, "O", wuss_WINDOW_NONE, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + &delegate_o, + SIZE2D(box_o.x1 - box_o.x0, box_o.y1 - box_o.y0), + SIZE2D(0, 0), &win_o); + if (rc != result_OK) + goto Failure; + + rc = wuss_redraw_dirty(wuss); /* flush both creates */ + if (rc != result_OK) + goto Failure; + + /* probe the gap column just right of O: pure M content, must slide up by + * exactly the scroll delta with no discontinuity */ + wuss_window_get_visible_bounds(win_o, &ovis); + gx = (ovis.x1 + content.x1) / 2; + delta = 20; + + wuss_window_set_scroll(win_m, POINT(0, delta)); + rc = wuss_redraw_dirty(wuss); + if (rc != result_OK) + goto Failure; + + /* every row in the gap column, across O's vertical span, must show the + * document Y that scrolling put there: doc_y = (screen_y - content.y0) + + * delta, low byte carried in blue by paint_handle */ + bad = 0; + for (gy = ovis.y0 + 2; gy < ovis.y1 - 2; gy++) + { + uint32_t px, blue; + + if (gy < content.y0 || gy >= content.y1) + continue; + px = ((const uint32_t *) pixels)[gy * 200 + gx]; + blue = px & 0xff; + if (blue != (uint32_t) ((gy - content.y0 + delta) & 0xff)) + bad = 1; + } + if (bad) + goto Failure; /* a band blit clobbered another band's source */ + + wuss_window_close(win_o); + wuss_window_close(win_m); + } + printf("test: wuss_WINDOW_NO_RESIZE_BLIT redraws the whole window instead of blitting\n"); { diff --git a/libraries/wuss/window/invalidate.c b/libraries/wuss/window/invalidate.c index 71079112..1d4bb5da 100644 --- a/libraries/wuss/window/invalidate.c +++ b/libraries/wuss/window/invalidate.c @@ -148,6 +148,60 @@ int wuss__subtract_boxes(const box_t *whole, return ncur; } +/* Sequential single-rect blits (each a self-consistent memmove) can still + * corrupt each other when one piece's destination lands on another piece's + * still-unread source -- but that only actually matters if no blit order + * avoids it. Build the "must happen before" graph (piece j before piece i + * whenever dest[i] would overwrite clean[j]'s still-unread source) and + * topologically sort it: any window with more than one occluder-carved + * piece near a shared edge -- e.g. two bands split by a corner occluder -- + * routinely has one such pairwise overlap without there being a genuine + * cycle, and rejecting those outright regressed plain corner-occlusion + * drags into full fallback redraws. Only an actual cycle (i must precede j + * and j must precede i) has no safe order and needs the fallback. */ +int wuss__order_pieces(const box_t *clean, + const box_t *dest, + int n, + int *order) +{ + int adj[WUSS_MAX_INVALIDATE_PIECES][WUSS_MAX_INVALIDATE_PIECES]; + int indeg[WUSS_MAX_INVALIDATE_PIECES]; + int queue[WUSS_MAX_INVALIDATE_PIECES]; + int i, j, head, tail, nout, u; + + for (i = 0; i < n; i++) + indeg[i] = 0; + for (j = 0; j < n; j++) + for (i = 0; i < n; i++) + adj[j][i] = 0; + + for (i = 0; i < n; i++) + for (j = 0; j < n; j++) + if (i != j && !adj[j][i] && box_intersects(&dest[i], &clean[j])) + { + adj[j][i] = 1; /* j must be blitted before i */ + indeg[i]++; + } + + tail = 0; + for (i = 0; i < n; i++) + if (indeg[i] == 0) + queue[tail++] = i; + + head = nout = 0; + while (head < tail) + { + u = queue[head++]; + order[nout++] = u; + + for (i = 0; i < n; i++) + if (adj[u][i] && --indeg[i] == 0) + queue[tail++] = i; + } + + return nout == n; +} + /* Invalidate the parts of "window"'s footprint that are hidden behind other * windows at the current z-order -- the rest of its footprint is already * showing its own correct pixels, so redrawing that too would just be diff --git a/libraries/wuss/window/move.c b/libraries/wuss/window/move.c index 916bd36b..6bb923d4 100644 --- a/libraries/wuss/window/move.c +++ b/libraries/wuss/window/move.c @@ -14,60 +14,6 @@ static void translate_box(const box_t *box, int dx, int dy, box_t *out) /* p is the window's content top-left; the furniture offset (outline plus * any titlebar) is constant for a given window, so the footprint just * follows it */ -/* Sequential single-rect blits (each a self-consistent memmove) can still - * corrupt each other when one piece's destination lands on another piece's - * still-unread source -- but that only actually matters if no blit order - * avoids it. Build the "must happen before" graph (piece j before piece i - * whenever dest[i] would overwrite clean[j]'s still-unread source) and - * topologically sort it: any window with more than one occluder-carved - * piece near a shared edge -- e.g. two bands split by a corner occluder -- - * routinely has one such pairwise overlap without there being a genuine - * cycle, and rejecting those outright regressed plain corner-occlusion - * drags into full fallback redraws. Only an actual cycle (i must precede j - * and j must precede i) has no safe order and needs the fallback. */ -static int wuss__order_pieces(const box_t *clean, - const box_t *dest, - int n, - int *order) -{ - int adj[WUSS_MAX_INVALIDATE_PIECES][WUSS_MAX_INVALIDATE_PIECES]; - int indeg[WUSS_MAX_INVALIDATE_PIECES]; - int queue[WUSS_MAX_INVALIDATE_PIECES]; - int i, j, head, tail, nout, u; - - for (i = 0; i < n; i++) - indeg[i] = 0; - for (j = 0; j < n; j++) - for (i = 0; i < n; i++) - adj[j][i] = 0; - - for (i = 0; i < n; i++) - for (j = 0; j < n; j++) - if (i != j && !adj[j][i] && box_intersects(&dest[i], &clean[j])) - { - adj[j][i] = 1; /* j must be blitted before i */ - indeg[i]++; - } - - tail = 0; - for (i = 0; i < n; i++) - if (indeg[i] == 0) - queue[tail++] = i; - - head = nout = 0; - while (head < tail) - { - u = queue[head++]; - order[nout++] = u; - - for (i = 0; i < n; i++) - if (adj[u][i] && --indeg[i] == 0) - queue[tail++] = i; - } - - return nout == n; -} - void wuss_window_move(wuss_window_t *window, point_t p) { box_t clean[WUSS_MAX_INVALIDATE_PIECES]; diff --git a/libraries/wuss/window/set-scroll.c b/libraries/wuss/window/set-scroll.c index 3aec5846..df1552ae 100644 --- a/libraries/wuss/window/set-scroll.c +++ b/libraries/wuss/window/set-scroll.c @@ -6,10 +6,14 @@ void wuss_window_set_scroll(wuss_window_t *window, point_t p) { box_t content; box_t src[WUSS_MAX_INVALIDATE_PIECES]; + box_t blit_src[WUSS_MAX_INVALIDATE_PIECES]; + box_t blit_dest[WUSS_MAX_INVALIDATE_PIECES]; box_t copied[WUSS_MAX_INVALIDATE_PIECES]; box_t dirty[WUSS_MAX_INVALIDATE_PIECES]; box_t vis[WUSS_MAX_INVALIDATE_PIECES]; - int dx, dy, nsrc, ncopied, ndirty, nvis, i, j; + int order[WUSS_MAX_INVALIDATE_PIECES]; + int dx, dy, nsrc, nblit, ncopied, ndirty, nvis, i, j, idx; + int overflow, blit_failed; dx = p.x - window->scroll.x; dy = p.y - window->scroll.y; @@ -31,14 +35,12 @@ void wuss_window_set_scroll(wuss_window_t *window, point_t p) * Sliding a source piece by the scroll delta can still land its * destination on screen a higher window owns -- screen_copy_rect only * clips to the content box, so it would paint over the occluder there. - * Clip each destination against the occluders too and blit only the - * surviving sub-pieces, each with its own matching source offset; the - * rest falls into the repaint set below. Falls back to a full content - * invalidate when the pixel format can't blit. */ - nsrc = wuss__clip_to_visible(window, &content, src); - ncopied = 0; - window->wuss->scr->clip = content; - for (i = 0; i < nsrc; i++) + * Clip each destination against the occluders too and keep only the + * surviving sub-pieces, each with its own matching source offset. */ + nsrc = wuss__clip_to_visible(window, &content, src); + nblit = 0; + overflow = 0; + for (i = 0; i < nsrc && !overflow; i++) { box_t want; @@ -46,17 +48,44 @@ void wuss_window_set_scroll(wuss_window_t *window, point_t p) nvis = wuss__clip_to_visible(window, &want, vis); for (j = 0; j < nvis; j++) { - box_t ssub, got; + if (nblit == WUSS_MAX_INVALIDATE_PIECES) + { + overflow = 1; + break; + } + blit_dest[nblit] = vis[j]; + box_translated(&vis[j], dx, dy, &blit_src[nblit]); + nblit++; + } + } + + /* Each sub-piece blit is a self-consistent memmove, but one piece's + * destination can land on another piece's still-unread source (e.g. the + * bands carved around a floating occluder overlap once shifted). Order + * them so that never happens; fall back to a full content invalidate if + * no safe order exists or the piece budget overflowed. */ + window->wuss->scr->clip = content; + ncopied = 0; + blit_failed = nsrc == 0 || overflow || + !wuss__order_pieces(blit_src, blit_dest, nblit, order); + + for (i = 0; i < nblit && !blit_failed; i++) + { + box_t got; - box_translated(&vis[j], dx, dy, &ssub); /* source for this dest piece */ - if (screen_copy_rect(window->wuss->scr, &ssub, - POINT(ssub.x0 - dx, ssub.y0 - dy), &got) != 0 && - ncopied < WUSS_MAX_INVALIDATE_PIECES) - copied[ncopied++] = got; + idx = order[i]; + if (screen_copy_rect(window->wuss->scr, &blit_src[idx], + POINT(blit_dest[idx].x0, blit_dest[idx].y0), + &got) == 0) + { + blit_failed = 1; + break; } + if (ncopied < WUSS_MAX_INVALIDATE_PIECES) + copied[ncopied++] = got; } - if (ncopied > 0) + if (!blit_failed && ncopied > 0) { /* Repaint the content box minus what the blit reused, then clip each * survivor to this window's visible area: the parts that were behind an From 12214977eb4b551ef1a2daea46ed8c4d69e54015 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Tue, 1 Sep 2026 19:03:26 +0100 Subject: [PATCH 148/287] docs(wuss): put icon enum/struct member docs before each member Move the trailing /**< member comments in wuss_icon_type, wuss_icon_flags and wuss_icon_spec to /** blocks preceding each member. Also lower the wrap tooling's default width from 80 to 77 columns (wrap_doxygen.py and wrap_protos.py, plus their tests), rewrap every public header's Doxygen blocks to the new limit, and run wrap_protos.py across the tree to rewrap prototypes that now exceed it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- include/databases/digest-db.h | 6 +- include/databases/filename-db.h | 4 +- include/databases/pickle.h | 19 +- include/datastruct/atom.h | 31 +- include/datastruct/bitarr.h | 23 +- include/datastruct/cache.h | 13 +- include/datastruct/hash.h | 4 +- include/datastruct/vector.h | 22 +- include/framebuf/bmfont.h | 6 +- include/framebuf/curve.h | 15 +- include/framebuf/screen.h | 97 +++--- include/framebuf/span.h | 20 +- include/geom/box.h | 8 +- include/geom/layout.h | 4 +- include/geom/line.h | 15 +- include/geom/packer.h | 27 +- include/io/stream-packbits.h | 8 +- include/io/stream.h | 3 +- include/text/bmtext.h | 13 +- include/text/txtfmt.h | 8 +- include/utils/array.h | 8 +- include/utils/pack.h | 18 +- include/wuss/icon.h | 323 +++++++++----------- include/wuss/menu.h | 69 +++-- include/wuss/task.h | 12 +- include/wuss/window.h | 131 ++++---- include/wuss/wuss.h | 171 ++++++----- libraries/datastruct/ntree/insert-before.c | 4 +- libraries/framebuf/bitmap/bitmap.c | 4 +- libraries/framebuf/screen/screen-draw.c | 6 +- libraries/io/stream/stream-packbitsdecomp.c | 4 +- libraries/wuss/furniture/invalidate.c | 3 +- libraries/wuss/icon/draw.c | 3 +- libraries/wuss/impl.h | 6 +- libraries/wuss/test/tasks/ball.c | 4 +- libraries/wuss/test/tasks/ball.h | 4 +- libraries/wuss/test/tasks/porter-duff.c | 3 +- libraries/wuss/test/tasks/sofa.c | 8 +- libraries/wuss/test/tasks/sofa.h | 4 +- libraries/wuss/window/get-content-bounds.c | 3 +- libraries/wuss/window/get-visible-bounds.c | 3 +- libraries/wuss/window/invalidate.c | 8 +- libraries/wuss/window/set-background.c | 3 +- tools/test_wrap_doxygen.py | 6 +- tools/test_wrap_protos.py | 8 +- tools/wrap_doxygen.py | 2 +- tools/wrap_protos.py | 2 +- 47 files changed, 609 insertions(+), 557 deletions(-) diff --git a/include/databases/digest-db.h b/include/databases/digest-db.h index 287ffc57..d4002c8c 100644 --- a/include/databases/digest-db.h +++ b/include/databases/digest-db.h @@ -5,9 +5,9 @@ * * Digest database. * - * digestdb is a wrapper around an atom set specifically for holding (128-bit) - * digests. It is used by tagdb and filenamedb to share the cost of storing - * digest values by replacing 128-bit digests with smaller indices. + * digestdb is a wrapper around an atom set specifically for holding + * (128-bit) digests. It is used by tagdb and filenamedb to share the cost of + * storing digest values by replacing 128-bit digests with smaller indices. */ #ifndef DATABASES_DIGEST_DB_H diff --git a/include/databases/filename-db.h b/include/databases/filename-db.h index d0a2bf54..fd02e3de 100644 --- a/include/databases/filename-db.h +++ b/include/databases/filename-db.h @@ -5,8 +5,8 @@ * * Filename database. * - * The filenamedb is an associative array which maps keys (such as digests) to - * filenames. The data is stored on disc. + * The filenamedb is an associative array which maps keys (such as digests) + * to filenames. The data is stored on disc. * * PrivateEye uses this to find out where an image lives given its digest. */ diff --git a/include/databases/pickle.h b/include/databases/pickle.h index 88c1d7ac..0026ca7a 100644 --- a/include/databases/pickle.h +++ b/include/databases/pickle.h @@ -6,8 +6,8 @@ * Storage of associative arrays. * * Its name borrowed from Python, this module provides pickle_pickle() and - * pickle_unpickle() which respectively serialise or deserialise an associative - * array to a file of the form: + * pickle_unpickle() which respectively serialise or deserialise an + * associative array to a file of the form: * * #<comments> <version> <key><separator><value> (zero or more) * @@ -240,13 +240,14 @@ pickle_unformat_methods_t; /* ----------------------------------------------------------------------- */ /** - * Serialise associative array 'assocarr' to the file 'filename'. Interpret the - * contents of the associative array using the methods in 'reader'. Format the - * keys and values for storage using the methods in 'format'. + * Serialise associative array 'assocarr' to the file 'filename'. Interpret + * the contents of the associative array using the methods in 'reader'. + * Format the keys and values for storage using the methods in 'format'. * * \param[in] filename Filename to save to. * \param[in] assocarr Associative array to pickle. - * \param[in] reader Interfaces for reading from the associative array. + * \param[in] reader Interfaces for reading from the associative + * array. * \param[in] format Interfaces for formatting the retrieved values. * \param[in] opaque Opaque pointer passed into interfaces. * @@ -259,9 +260,9 @@ result_t pickle_pickle(const char *filename, void *opaque); /** - * Populate associative array 'assocarr' from the file 'filename'. Insert into - * the associative array using the methods in 'writer'. Parse the keys and - * values from storage using the methods in 'unformat'. + * Populate associative array 'assocarr' from the file 'filename'. Insert + * into the associative array using the methods in 'writer'. Parse the keys + * and values from storage using the methods in 'unformat'. * * \param[in] filename Filename to read from. * \param[in] assocarr Associative array to pickle. diff --git a/include/datastruct/atom.h b/include/datastruct/atom.h index 285c0ee3..e4a173b1 100644 --- a/include/datastruct/atom.h +++ b/include/datastruct/atom.h @@ -6,15 +6,16 @@ * Indexed data block store. * * Atoms are indices assigned to blocks of stored data. Identical data blocks - * are assigned the same atom. Atoms belonging to the same set can be directly - * compared avoiding the need to memcmp, strcmp, or otherwise linearly compare - * the contents of the respective data blocks. + * are assigned the same atom. Atoms belonging to the same set can be + * directly compared avoiding the need to memcmp, strcmp, or otherwise + * linearly compare the contents of the respective data blocks. * - * Data blocks can be retrieved by quoting an atom to atom_get. This returns a - * pointer to the data block along with its length. + * Data blocks can be retrieved by quoting an atom to atom_get. This returns + * a pointer to the data block along with its length. * - * To avoid heap overhead, atoms are stored in a series of fixed-size pools of - * memory. The size of the pools may be specified when the set is first created. + * To avoid heap overhead, atoms are stored in a series of fixed-size pools + * of memory. The size of the pools may be specified when the set is first + * created. */ #ifndef DATASTRUCT_ATOM_H @@ -56,13 +57,15 @@ atom_set_t *atom_create(void); /** * Create a new atom set using the specified data sizes. * - * \param locpoolsz Size of a location pool, or zero for the default. Set this - * to the number of entries you typically expect to store. - * \param blkpoolsz Size of a block pool, or zero for the default. No inserted - * data block may be larger than this. Increasing this value - * will use fewer individual block pools, reducing heap - * overhead, at the expense of potentially greater wasted space - * should the block pool remain not fully allocated. + * \param locpoolsz Size of a location pool, or zero for the default. Set + * this to the number of entries you typically expect to + * store. + * \param blkpoolsz Size of a block pool, or zero for the default. No + * inserted data block may be larger than this. Increasing + * this value will use fewer individual block pools, + * reducing heap overhead, at the expense of potentially + * greater wasted space should the block pool remain not + * fully allocated. * * \return New atom set, or NULL if out of memory. */ diff --git a/include/datastruct/bitarr.h b/include/datastruct/bitarr.h index e7398bfc..783f3d4b 100644 --- a/include/datastruct/bitarr.h +++ b/include/datastruct/bitarr.h @@ -5,9 +5,9 @@ * * Arrays of bits. * - * Bit arrays are an array of bits. They are of a fixed length and allocated by - * the client. The bit array library provides functions to manipulate bit arrays - * but not allocate them. + * Bit arrays are an array of bits. They are of a fixed length and allocated + * by the client. The bit array library provides functions to manipulate bit + * arrays but not allocate them. * * \see Bit Vector for manipulating a dynamically allocated bit array. * @@ -54,7 +54,8 @@ typedef unsigned int bitarr_elem_t; #define BITARR_MASK (BITARR_BITS - 1) /** - * Return the number of elements required to store the specified number of bits. + * Return the number of elements required to store the specified number of + * bits. */ #define BITARR_ELEMS(nbits) ((nbits + BITARR_BITS - 1) >> BITARR_SHIFT) @@ -62,15 +63,15 @@ typedef unsigned int bitarr_elem_t; * Declare a bit array with the specified number of bits. * * This uses the OSLib trick of declaring a macro to define a type and also - * declaring an 'equivalent' struct. In practice they're not actually equivalent - * as the former, bitarr_ARRAY, is an anonymous struct of a given length and the - * bitarr_t is a struct containing an array of 'UNKNOWN' length, which is - * actually defined as 1. + * declaring an 'equivalent' struct. In practice they're not actually + * equivalent as the former, bitarr_ARRAY, is an anonymous struct of a given + * length and the bitarr_t is a struct containing an array of 'UNKNOWN' + * length, which is actually defined as 1. * * This works out all right, letting us declare bit arrays by specifying the - * number of bits we require, but we end up needing to cast out declared entries - * to (bitarr_t *) when calling a 'real' function, e.g. bitarr_count which looks - * awkward. + * number of bits we require, but we end up needing to cast out declared + * entries to (bitarr_t *) when calling a 'real' function, e.g. bitarr_count + * which looks awkward. */ #define bitarr_ARRAY(nbits) \ struct { \ diff --git a/include/datastruct/cache.h b/include/datastruct/cache.h index ce0aeb4b..6bbd5481 100644 --- a/include/datastruct/cache.h +++ b/include/datastruct/cache.h @@ -46,8 +46,8 @@ typedef unsigned int cachekey_t; /** * Create a cache. * - * \param[in] config Pointer to cache parameters, or NULL for default cache - * parameters. + * \param[in] config Pointer to cache parameters, or NULL for default + * cache parameters. * \param[in] length Byte length of the cache to allocate. * \param[out] cache Returned pointer to the created cache. * @@ -67,8 +67,8 @@ void cache_destroy(cache_t *doomed); /** * Create a cache in a supplied block of memory. * - * \param[in] config Pointer to cache parameters, or NULL for default cache - * parameters. + * \param[in] config Pointer to cache parameters, or NULL for default + * cache parameters. * \param[in] block Pointer to memory to use. * \param[in] length Byte length of block. * \param[out] cache Returned pointer to the created cache. @@ -104,8 +104,9 @@ void *cache_get(cache_t *cache, cachekey_t key); * \param[in] key Key. * \param[in] data Pointer to data to store. * \param[in] length Length of data. - * \param[out] inserted Returned pointer to the inserted data, or NULL if not - * wanted. This valid until the next cache_put operation. + * \param[out] inserted Returned pointer to the inserted data, or NULL if + * not wanted. This valid until the next cache_put + * operation. * * \return Error indication. */ diff --git a/include/datastruct/hash.h b/include/datastruct/hash.h index bf68ed23..02794d2d 100644 --- a/include/datastruct/hash.h +++ b/include/datastruct/hash.h @@ -5,8 +5,8 @@ * * Hash is an associative array. * - * The interface presently forces you to malloc all keys, and values passed in, - * yourself. + * The interface presently forces you to malloc all keys, and values passed + * in, yourself. */ #ifndef DATASTRUCT_HASH_H diff --git a/include/datastruct/vector.h b/include/datastruct/vector.h index 5750e37a..4a4b9c66 100644 --- a/include/datastruct/vector.h +++ b/include/datastruct/vector.h @@ -3,10 +3,11 @@ /** * \file vector.h * - * Vector is an abstracted array which can be resized by both length and element - * width. + * Vector is an abstracted array which can be resized by both length and + * element width. * - * Elements are of a fixed size, stored contiguously and are addressed by index. + * Elements are of a fixed size, stored contiguously and are addressed by + * index. * * \warning If the vector is altered then pointers into the vector may be * invalidated (should the block move when reallocated). @@ -82,8 +83,8 @@ result_t vector_set_length(vector_t *vector, unsigned int length); /* ----------------------------------------------------------------------- */ /** - * Reserve space for at least the specified number of elements in the specified - * vector. + * Reserve space for at least the specified number of elements in the + * specified vector. * * \param[in] vector Vector to change. * \param[in] need Required length. @@ -106,8 +107,8 @@ size_t vector_width(const vector_t *vector); /** * Change the byte width of element stored in the specified vector. * - * If the element width is reduced then any extra bytes are lost. If increased, - * then zeroes are inserted. + * If the element width is reduced then any extra bytes are lost. If + * increased, then zeroes are inserted. * * \param[in] vector Vector to change. * \param[in] width New element width. @@ -154,7 +155,8 @@ void vector_set(vector_t *vector, unsigned int index, const void *value); result_t vector_insert(vector_t *vector, const void *value); /** - * Insert many elements at the end of the vector, allocating memory if required. + * Insert many elements at the end of the vector, allocating memory if + * required. * * \param[in] vector Vector to change. * \param[in] values Array of values to insert. @@ -162,7 +164,9 @@ result_t vector_insert(vector_t *vector, const void *value); * * \return result_OK or result_OOM. */ -result_t vector_insert_many(vector_t *vector, const void *values, int nvalues); +result_t vector_insert_many(vector_t *vector, + const void *values, + int nvalues); /* ----------------------------------------------------------------------- */ diff --git a/include/framebuf/bmfont.h b/include/framebuf/bmfont.h index b8f7d9b8..417cfa17 100644 --- a/include/framebuf/bmfont.h +++ b/include/framebuf/bmfont.h @@ -39,9 +39,9 @@ void bmfont_destroy(bmfont_t *bmfont); void bmfont_get_info(bmfont_t *bmfont, int *width, int *height); /** - * Read the number of glyphs in the specified bitmap font. Glyphs are laid out - * contiguously starting at ' ' (space, 0x20), so a char c has a glyph iff c >= - * ' ' and c < ' ' + bmfont_get_count(bmfont). + * Read the number of glyphs in the specified bitmap font. Glyphs are laid + * out contiguously starting at ' ' (space, 0x20), so a char c has a glyph + * iff c >= ' ' and c < ' ' + bmfont_get_count(bmfont). * * \param[in] bmfont Bitmap font to query. * \return Number of glyphs in the font. diff --git a/include/framebuf/curve.h b/include/framebuf/curve.h index a8d171c9..d8e9b15f 100644 --- a/include/framebuf/curve.h +++ b/include/framebuf/curve.h @@ -11,8 +11,8 @@ /** * Return the point on the line defined by points `p0` and `p1` at time `t`. * - * This is a straight linear interpolation between the two points: there is no - * curvature. + * This is a straight linear interpolation between the two points: there is + * no curvature. * * \param[in] p0 Start point. * \param[in] p1 End point. @@ -116,8 +116,8 @@ point_t curve_bezier_point_on_cubic_r(point_t p0, fix16_t t); /** - * As for \ref curve_bezier_point_on_quartic but is written in terms of cubics - * (and in turn of quads). + * As for \ref curve_bezier_point_on_quartic but is written in terms of + * cubics (and in turn of quads). * * \param[in] p0 Start point. * \param[in] p1 Control point 1. @@ -135,8 +135,8 @@ point_t curve_bezier_point_on_quartic_r(point_t p0, fix16_t t); /** - * As for \ref curve_bezier_point_on_quintic but is written in terms of quartics - * (and in turn of cubics, etc.). + * As for \ref curve_bezier_point_on_quintic but is written in terms of + * quartics (and in turn of cubics, etc.). * * \param[in] p0 Start point. * \param[in] p1 Control point 1. @@ -156,7 +156,8 @@ point_t curve_bezier_point_on_quintic_r(point_t p0, fix16_t t); /** - * As for \ref curve_bezier_cubic but uses forward differencing (float version). + * As for \ref curve_bezier_cubic but uses forward differencing (float + * version). * * \param[in] p0 Start point. * \param[in] p1 Control point 1. diff --git a/include/framebuf/screen.h b/include/framebuf/screen.h index f8e8f084..455a786e 100644 --- a/include/framebuf/screen.h +++ b/include/framebuf/screen.h @@ -90,13 +90,14 @@ void screen_fill_rect(screen_t *scr, * \param[in] colour Colour of rectangle. */ void screen_fill_square(screen_t *scr, - int x, int y, - int size, - colour_t colour); + int x, + int y, + int size, + colour_t colour); /** - * Built-in 8x8 fill patterns for `screen_fill_pattern`. Each is a 1-bit tile: - * set bits take the foreground colour, clear bits the background. + * Built-in 8x8 fill patterns for `screen_fill_pattern`. Each is a 1-bit + * tile: set bits take the foreground colour, clear bits the background. */ typedef enum screen_pattern { @@ -112,11 +113,11 @@ typedef enum screen_pattern /** * 8x8 ordered (Bayer) dither, one entry per coverage level 0 (empty) to 64 * (solid). Index by level as `screen_PATTERN_BAYER0 + level`; - * `screen_PATTERN_BAYER_LIMIT` is one past the last. `screen_PATTERN_BAYER0` - * matches nothing else here, `screen_PATTERN_BAYER32` equals - * `screen_PATTERN_GREY50` and `screen_PATTERN_BAYER64` equals - * `screen_PATTERN_SOLID`; the duplicates are kept so the level arithmetic - * stays simple. + * `screen_PATTERN_BAYER_LIMIT` is one past the last. + * `screen_PATTERN_BAYER0` matches nothing else here, + * `screen_PATTERN_BAYER32` equals `screen_PATTERN_GREY50` and + * `screen_PATTERN_BAYER64` equals `screen_PATTERN_SOLID`; the duplicates + * are kept so the level arithmetic stays simple. */ screen_PATTERN_BAYER0, screen_PATTERN_BAYER_LIMIT = screen_PATTERN_BAYER0 + 65, @@ -129,10 +130,10 @@ screen_pattern_t; /** * Fills a box with a repeating 8x8 two-colour pattern. * - * The tile is phased against (`origin_x`, `origin_y`): that coordinate is the - * one that would map to the box's top-left corner. Passing the caller's own - * scroll origin keeps the pattern locked to content as the box moves, rather - * than crawling with it. + * The tile is phased against (`origin_x`, `origin_y`): that coordinate is + * the one that would map to the box's top-left corner. Passing the caller's + * own scroll origin keeps the pattern locked to content as the box moves, + * rather than crawling with it. * * Clipped to the screen's clip region. * @@ -153,12 +154,13 @@ void screen_fill_pattern(screen_t *scr, colour_t bg); /** - * Draws a bitmap, alpha-blending it against the screen where the bitmap has an - * alpha channel. On paletted screens, which have no linear channel bits to - * blend, this falls back to alpha-tested transparency instead (drawn at full - * strength, or not at all). + * Draws a bitmap, alpha-blending it against the screen where the bitmap has + * an alpha channel. On paletted screens, which have no linear channel bits + * to blend, this falls back to alpha-tested transparency instead (drawn at + * full strength, or not at all). * - * The bitmap is clipped to the screen's clip region. No scaling is performed. + * The bitmap is clipped to the screen's clip region. No scaling is + * performed. * * \param[in] scr Screen to draw upon. * \param[in] x X coordinate of leftmost point to draw bitmap at. @@ -174,18 +176,18 @@ enum }; /** - * Draws a "9-patch": a resizable frame built from a source image that is a 3x3 - * grid of equal cells. The source width and height must each be a positive - * multiple of 3; the cell size is a third of each. Given a destination box, the - * four corner cells are drawn at their natural size in the destination corners, - * the four edge cells are tiled along the destination edges, and the centre - * cell is tiled across the interior. + * Draws a "9-patch": a resizable frame built from a source image that is a + * 3x3 grid of equal cells. The source width and height must each be a + * positive multiple of 3; the cell size is a third of each. Given a + * destination box, the four corner cells are drawn at their natural size in + * the destination corners, the four edge cells are tiled along the + * destination edges, and the centre cell is tiled across the interior. * - * If the destination is narrower or shorter than two cells the opposing corners - * overlap and each is clipped to its own half; the edges and centre are then - * omitted. Drawing is clipped to both the destination box and the screen's clip - * region, which is restored on return. Cells are blended exactly as - * `screen_draw_bitmap` does. No scaling is performed. + * If the destination is narrower or shorter than two cells the opposing + * corners overlap and each is clipped to its own half; the edges and centre + * are then omitted. Drawing is clipped to both the destination box and the + * screen's clip region, which is restored on return. Cells are blended + * exactly as `screen_draw_bitmap` does. No scaling is performed. * * \param[in] scr Screen to draw upon. * \param[in] dst Destination box to fill with the frame. @@ -206,18 +208,18 @@ void screen_draw_ninepatch(screen_t *scr, * copying is done in the correct row order to handle that safely. * * Both the source and destination are clipped to the screen's clip region, - * shrinking together so the copied area always maps source pixel to destination - * pixel 1:1. + * shrinking together so the copied area always maps source pixel to + * destination pixel 1:1. * * Callers must check the return value and fall back to a normal * invalidate/redraw when it's false (e.g. out of memory, or an unknown pixel * format), since a declined copy leaves the destination untouched. * * If "src" or the intended destination falls partly off-screen, the actual - * copied area shrinks to what both ends have in common on-screen: callers must - * invalidate whatever part of their intended (unclipped) destination falls - * outside "copied_dst", since it has no valid source pixels to have been copied - * from and so is left untouched, not merely stale. + * copied area shrinks to what both ends have in common on-screen: callers + * must invalidate whatever part of their intended (unclipped) destination + * falls outside "copied_dst", since it has no valid source pixels to have + * been copied from and so is left untouched, not merely stale. * * \param[in] scr Screen to copy within. * \param[in] src Screen-space region to copy from. @@ -226,8 +228,8 @@ void screen_draw_ninepatch(screen_t *scr, * smaller than intended if either end was partly * off-screen). Pass NULL if not needed. Left unset if * the copy was declined. - * \return True if the copy was performed, false if declined (unsupported pixel - * format). + * \return True if the copy was performed, false if declined (unsupported + * pixel format). */ int screen_copy_rect(screen_t *scr, const box_t *src, @@ -255,11 +257,11 @@ void screen_draw_line(screen_t *scr, /** * Draws a connected polyline through `npoints` points: a `screen_draw_line` - * segment between each adjacent pair. For a closed shape repeat the first point - * as the last. Fewer than 2 points draws nothing. + * segment between each adjacent pair. For a closed shape repeat the first + * point as the last. Fewer than 2 points draws nothing. * - * Segments share their joint pixel, which is plotted by both adjacent segments; - * harmless for a solid colour. + * Segments share their joint pixel, which is plotted by both adjacent + * segments; harmless for a solid colour. * * Coordinates are `int`s and inclusive. * @@ -275,8 +277,8 @@ void screen_draw_lines(screen_t *scr, /** * Draws a one-pixel unfilled rectangle outline. `size` is inclusive of both - * edges, matching `screen_fill_rect`. A degenerate size (<= 1 in either axis) - * falls back to a filled `screen_fill_rect`. + * edges, matching `screen_fill_rect`. A degenerate size (<= 1 in either + * axis) falls back to a filled `screen_fill_rect`. * * \param[in] scr Screen to draw upon. * \param[in] x X coordinate of leftmost point of rectangle. @@ -291,9 +293,10 @@ void screen_draw_rect(screen_t *scr, colour_t colour); /** - * Draws a stippled line: `on` pixels drawn, then `off` skipped, repeating along - * the line. Bresenham stepping, so the dash period is measured in steps not - * Euclidean distance. `on` <= 0 draws nothing; `off` <= 0 gives a solid line. + * Draws a stippled line: `on` pixels drawn, then `off` skipped, repeating + * along the line. Bresenham stepping, so the dash period is measured in + * steps not Euclidean distance. `on` <= 0 draws nothing; `off` <= 0 gives a + * solid line. * * Coordinates are `int`s and inclusive. * diff --git a/include/framebuf/span.h b/include/framebuf/span.h index 6b1a3f43..bda935e8 100644 --- a/include/framebuf/span.h +++ b/include/framebuf/span.h @@ -17,17 +17,17 @@ typedef void (span_copy_t)(void *dst, const void *src, int length); /** * Type of a "blend constant pixels" function. * - * This will blend the respective source pixels by the specified constant alpha - * value, writing the results to the destination buffer (like Porter-Duff Source - * Over Destination). + * This will blend the respective source pixels by the specified constant + * alpha value, writing the results to the destination buffer (like + * Porter-Duff Source Over Destination). * * \param[out] dst Destination pixels. * \param[in] src1 Source pixels 1. * \param[in] src2 Source pixels 2. * \param[in] length Length of pixels to blend. * \param[in] alpha Constant alpha value (0..255). - * \param[in] context Format-specific extra data (e.g. a palette for an indexed - * format); ignored where not needed, pass NULL. + * \param[in] context Format-specific extra data (e.g. a palette for an + * indexed format); ignored where not needed, pass NULL. */ typedef void (span_blendconst_t)(void *dst, const void *src1, @@ -39,9 +39,9 @@ typedef void (span_blendconst_t)(void *dst, /** * Type of a "blend array of pixels" function. * - * This will blend the respective source pixels by the specified alpha values, - * writing the results to the destination buffer (like Porter-Duff Source Over - * Destination). + * This will blend the respective source pixels by the specified alpha + * values, writing the results to the destination buffer (like Porter-Duff + * Source Over Destination). * * \param[out] dst Destination pixels. * \param[in] src1 Source pixels 1. @@ -58,8 +58,8 @@ typedef void (span_blendarray_t)(void *dst, /** * Defines a span. * - * A span is a group of functions that combine runs of pixels. They are keyed by - * pixel format. + * A span is a group of functions that combine runs of pixels. They are keyed + * by pixel format. */ typedef struct span { diff --git a/include/geom/box.h b/include/geom/box.h index 1c618697..da7751c6 100644 --- a/include/geom/box.h +++ b/include/geom/box.h @@ -30,8 +30,8 @@ typedef os_box box_t; #endif /** - * Initialises a box to an invalid state that will still produce a valid result - * when intersected with. + * Initialises a box to an invalid state that will still produce a valid + * result when intersected with. */ #define BOX_INIT { INT_MAX, INT_MAX, INT_MIN, INT_MIN } @@ -49,8 +49,8 @@ size2d_t box_size(const box_t *b); /** * Reset the box to an invalid state. * - * This sets x0,y0 to INT_MAX and the x1,y1 to INT_MIN. This is an invalid box - * but will still produce a valid result when intersected with. + * This sets x0,y0 to INT_MAX and the x1,y1 to INT_MIN. This is an invalid + * box but will still produce a valid result when intersected with. * * \param[in] b The box to reset. */ diff --git a/include/geom/layout.h b/include/geom/layout.h index e56a94c7..ded5e37e 100644 --- a/include/geom/layout.h +++ b/include/geom/layout.h @@ -61,8 +61,8 @@ layout_spec_t; * \param[in] nelements Number of layout elements given. * \param[out] boxes An array of boxes to be populated. * \param[in] nboxes Number of boxes available. - * \return \ref result_OK on success, result_LAYOUT_BUFFER_FULL if too few boxes - * were supplied, or appropriate result code otherwise. + * \return \ref result_OK on success, result_LAYOUT_BUFFER_FULL if too few + * boxes were supplied, or appropriate result code otherwise. */ result_t layout_place(const layout_spec_t *spec, const layout_element_t *elements, diff --git a/include/geom/line.h b/include/geom/line.h index 73dadabe..a8cc7668 100644 --- a/include/geom/line.h +++ b/include/geom/line.h @@ -11,14 +11,15 @@ extern "C" #include "geom/box.h" /** - * Clips the line (x0,y0)-(x1,y1) by box `clip` and returns the clipped points - * in `x0` and co. + * Clips the line (x0,y0)-(x1,y1) by box `clip` and returns the clipped + * points in `x0` and co. * - * The returned points are rounded to the nearest integer position on the clip - * boundary, so they vary with the clip box given. Callers which step along the - * line incrementally (Bresenham, Wu, etc.) must not seed their error terms from - * them if the pixels drawn are to be independent of the clip box passed in; use - * the return value to reject and step from the original endpoints. + * The returned points are rounded to the nearest integer position on the + * clip boundary, so they vary with the clip box given. Callers which step + * along the line incrementally (Bresenham, Wu, etc.) must not seed their + * error terms from them if the pixels drawn are to be independent of the + * clip box passed in; use the return value to reject and step from the + * original endpoints. * * \param[in] clip Rectangular clip region. * \param[in,out] x0 X coordinate of first point of line (modified). diff --git a/include/geom/packer.h b/include/geom/packer.h index 20ff3976..cc7aaa33 100644 --- a/include/geom/packer.h +++ b/include/geom/packer.h @@ -79,10 +79,10 @@ result_t packer_place_at(T *packer, /** * Sets a gutter for packer_place_by: the width in pixels of a strip it - * additionally reserves along each placed box's two edges facing away from the - * search corner, so boxes placed by location never end up flush against each - * other. Default 0 (no gutter). Negative values are treated as 0. Does not - * affect packer_place_at. + * additionally reserves along each placed box's two edges facing away from + * the search corner, so boxes placed by location never end up flush against + * each other. Default 0 (no gutter). Negative values are treated as 0. Does + * not affect packer_place_at. * * \param[in] packer Packer to configure. * \param[in] gutter Gutter width in pixels. @@ -94,22 +94,23 @@ void packer_set_gutter(T *packer, int gutter); * packer_place_at / packer_place_by. * * Free areas that share a full edge are coalesced after each release, so - * repeated place/release cycles reclaim the whole page rather than fragmenting - * it. A placement spanning two free areas that only partially overlap - * (staggered edges) still will not fit until the gap between them is freed too. - * packer_get_consumed_area is not narrowed by a release. + * repeated place/release cycles reclaim the whole page rather than + * fragmenting it. A placement spanning two free areas that only partially + * overlap (staggered edges) still will not fit until the gap between them is + * freed too. packer_get_consumed_area is not narrowed by a release. * * \param[in] packer Packer to release into. - * \param[in] area Area to release. Clipped to the packer's margins. Copied. - * \return \ref result_OK, or \ref result_PACKER_EMPTY if 'area' lies entirely - * outside the margins. + * \param[in] area Area to release. Clipped to the packer's margins. + * Copied. + * \return \ref result_OK, or \ref result_PACKER_EMPTY if 'area' lies + * entirely outside the margins. */ result_t packer_release(T *packer, const box_t *area); /** - * Places a box of dimensions (w,h) in the next free area determined by location - * 'loc'. + * Places a box of dimensions (w,h) in the next free area determined by + * location 'loc'. * * \param[in] packer Packer to place box. * \param[in] loc Direction to search from for the next available area. diff --git a/include/io/stream-packbits.h b/include/io/stream-packbits.h index cdd70e2b..10b25ca6 100644 --- a/include/io/stream-packbits.h +++ b/include/io/stream-packbits.h @@ -20,7 +20,9 @@ extern "C" * stream. * \return \ref result_OK on success, or appropriate result code otherwise. */ -result_t stream_packbitscomp_create(stream_t *input, int bufsz, stream_t **s); +result_t stream_packbitscomp_create(stream_t *input, + int bufsz, + stream_t **s); /** * Create a PackBits decompression stream. @@ -31,7 +33,9 @@ result_t stream_packbitscomp_create(stream_t *input, int bufsz, stream_t **s); * stream. * \return \ref result_OK on success, or appropriate result code otherwise. */ -result_t stream_packbitsdecomp_create(stream_t *input, int bufsz, stream_t **s); +result_t stream_packbitsdecomp_create(stream_t *input, + int bufsz, + stream_t **s); #ifdef __cplusplus } diff --git a/include/io/stream.h b/include/io/stream.h index 6eed9217..ff1e353d 100644 --- a/include/io/stream.h +++ b/include/io/stream.h @@ -3,7 +3,8 @@ /** * \file Stream (interface). * - * A stream is a generic interface which can be used to wrap sources of bytes. + * A stream is a generic interface which can be used to wrap sources of + * bytes. * * Single byte and block operations are supported. Byte access is efficient: * implemented as a macro. diff --git a/include/text/bmtext.h b/include/text/bmtext.h index adee453d..8bc6f4dd 100644 --- a/include/text/bmtext.h +++ b/include/text/bmtext.h @@ -3,16 +3,16 @@ /** * \file bmtext.h * - * Splits a string into lines that each fit a given pixel width when drawn in a - * \ref bmfont_t, then draws those lines stacked. + * Splits a string into lines that each fit a given pixel width when drawn in + * a \ref bmfont_t, then draws those lines stacked. * * Unlike \ref txtfmt (which wraps at character counts, for monospaced text) * this measures each candidate line with \ref bmfont_measure, so it wraps * proportional fonts correctly. * - * - Breaks at the last space that still fits; hard-breaks a word with no space - * in it. - Runs of whitespace at a break are swallowed. - Layout is pure: it - * touches no screen or scroll state. + * - Breaks at the last space that still fits; hard-breaks a word with no + * space in it. - Runs of whitespace at a break are swallowed. - Layout is + * pure: it touches no screen or scroll state. */ #ifndef DPTLIB_BMTEXT_H @@ -42,7 +42,8 @@ bmtext_line_t; /** * Break \p string into lines that each fit \p wrap_width pixels in \p font. * - * The line pointers refer into \p string itself, so it must outlive \p lines. + * The line pointers refer into \p string itself, so it must outlive \p + * lines. * * \param[in] font Bitmap font the lines will be drawn in. * \param[in] string Text to wrap. diff --git a/include/text/txtfmt.h b/include/text/txtfmt.h index 68937037..65867ce1 100644 --- a/include/text/txtfmt.h +++ b/include/text/txtfmt.h @@ -80,8 +80,8 @@ int txtfmt_get_nlines(const txtfmt_t *tx); /** * Returns the wrapped width of a txtfmt. * - * e.g. If you wrap some text containing a word 10 characters long it'll never - * get any thinner than 10. + * e.g. If you wrap some text containing a word 10 characters long it'll + * never get any thinner than 10. * * \param[in] tx Txtfmt to query. * @@ -92,8 +92,8 @@ int txtfmt_get_wrapped_width(const txtfmt_t *tx); /** * Retrieve a line produced by the last wrap. * - * The returned pointer refers into the txtfmt's own copy of the string and is - * valid until the next call to txtfmt_wrap or txtfmt_destroy. It is not + * The returned pointer refers into the txtfmt's own copy of the string and + * is valid until the next call to txtfmt_wrap or txtfmt_destroy. It is not * NUL-terminated: use the returned length. * * \param[in] tx Txtfmt to query. diff --git a/include/utils/array.h b/include/utils/array.h index b25d3661..228c33b4 100644 --- a/include/utils/array.h +++ b/include/utils/array.h @@ -19,8 +19,8 @@ extern "C" /* ----------------------------------------------------------------------- */ /** - * Signifies an array of unknown length, e.g. when used as the size of an array - * which is the final member of a struct. + * Signifies an array of unknown length, e.g. when used as the size of an + * array which is the final member of a struct. */ #define UNKNOWN 1 @@ -80,8 +80,8 @@ void array_squeeze2(unsigned char *base, * * Presently the growth strategy is doubling. * - * 'block' can be NULL to perform an initial alloc. Start with used == allocated - * == 0. + * 'block' can be NULL to perform an initial alloc. Start with used == + * allocated == 0. * * \param block Pointer to pointer to block. Updated on success. * \param elemsize Element size in bytes. diff --git a/include/utils/pack.h b/include/utils/pack.h index d61f4bbb..dc41271e 100644 --- a/include/utils/pack.h +++ b/include/utils/pack.h @@ -5,9 +5,9 @@ * * Structure packing and unpacking routines. * - * Inspired by printf, scanf and the Python 'struct' module these allow a string - * composed of formatting character to specify how data should be marshalled - * into memory. + * Inspired by printf, scanf and the Python 'struct' module these allow a + * string composed of formatting character to specify how data should be + * marshalled into memory. */ #ifndef UTILS_PACK_H @@ -42,8 +42,8 @@ extern "C" * pack(outbuf, "2si", 0x2000, 12345, 1 << 31); * * - * Using '*' instead of a count invokes array mode: the next argument is used as - * an array length and the next after that as an array base pointer. + * Using '*' instead of a count invokes array mode: the next argument is used + * as an array length and the next after that as an array base pointer. * * Example: * @@ -74,8 +74,8 @@ size_t pack(unsigned char *outbuf, const char *fmt, ...); * Retrieves three characters and an int from 'inbuf'. * * - * Using '*' instead of a count invokes array mode: the next argument is used as - * an array length and the next after that as an array base pointer. + * Using '*' instead of a count invokes array mode: the next argument is used + * as an array length and the next after that as an array base pointer. * * Example: * @@ -92,8 +92,8 @@ size_t pack(unsigned char *outbuf, const char *fmt, ...); * (Note that these specifiers are all different than the formatting * characters). * - * With these qualifers, sign becomes important. You can write CSIQ for unsigned - * arguments, or csiq for signed arguments. + * With these qualifers, sign becomes important. You can write CSIQ for + * unsigned arguments, or csiq for signed arguments. * * Example: * diff --git a/include/wuss/icon.h b/include/wuss/icon.h index 92aa5920..39636e8b 100644 --- a/include/wuss/icon.h +++ b/include/wuss/icon.h @@ -3,9 +3,9 @@ /** * \file icon.h * - * Work-area icons: static labels and clickable bevelled buttons that Wuss draws - * inside a window's content area and hit-tests before the content task sees a - * click. + * Work-area icons: static labels and clickable bevelled buttons that Wuss + * draws inside a window's content area and hit-tests before the content task + * sees a click. * * An icon's bounding box is given in virtual document space -- the same * coordinate space as wuss_EVENT_MOUSE's point and wuss_window_invalidate's @@ -14,9 +14,9 @@ * content bounds and scroll offset. * * Wuss fills a window's background, draws its icons, then delivers - * wuss_EVENT_REDRAW, so a task is free to paint over or around icon pixels. A - * click on a wuss_ICON_TYPE_BUTTON reaches the task as wuss_EVENT_ICON; clicks - * on a label, or on a hidden or disabled icon, fall through as + * wuss_EVENT_REDRAW, so a task is free to paint over or around icon pixels. + * A click on a wuss_ICON_TYPE_BUTTON reaches the task as wuss_EVENT_ICON; + * clicks on a label, or on a hidden or disabled icon, fall through as * wuss_EVENT_MOUSE. */ @@ -45,75 +45,62 @@ extern "C" * in wuss.h so wuss_event_t can name it regardless of this option. */ /** - * What an icon looks like and how it behaves. The enum is left open so sprite - * and editable-text icons can be added later without breaking existing specs. + * What an icon looks like and how it behaves. The enum is left open so + * sprite and editable-text icons can be added later without breaking + * existing specs. */ typedef enum wuss_icon_type { - wuss_ICON_TYPE_LABEL = 0, /**< Static text drawn with the window manager's - * font. Not interactive: clicks fall through to - * the task as wuss_EVENT_MOUSE. */ - wuss_ICON_TYPE_BUTTON, /**< Bevelled rectangle with a centred text label - * and pressed-state visual feedback; clicks and - * hovers are delivered to the task as - * wuss_EVENT_ICON. */ - wuss_ICON_TYPE_PATTERN, /**< Bounding box filled with a repeating two-colour - * 8x8 tile (spec.pattern) in fg/bg, phased to - * document space so it scrolls rigidly with - * content. Not interactive: clicks fall through - * to the task as wuss_EVENT_MOUSE. text is - * ignored. */ - wuss_ICON_TYPE_FRAME, /**< A grouping box: a one-pixel rectangle in fg - * around the bounding box, broken at the top-left - * for an optional caption (text) drawn over the - * window background. Not interactive: clicks fall - * through to the task as wuss_EVENT_MOUSE. */ - wuss_ICON_TYPE_RADIO, /**< A radio button: a small ring at the left of the - * bounding box, filled when selected, with the - * label (text) to its right. Interactive: a click - * selects it and clears every other selected - * radio sharing its non-zero group, then the task - * is told via wuss_EVENT_ICON. */ - wuss_ICON_TYPE_OPTION, /**< An option button: a small box at the left of - * the bounding box, ticked when selected, with - * the label (text) to its right. Interactive: a - * click toggles its own selected state (group is - * ignored), then the task is told via - * wuss_EVENT_ICON. */ - wuss_ICON_TYPE_BITMAP, /**< A caller-owned bitmap (spec.bitmap) drawn at - * the top-left of the bounding box, alpha-blended - * against what is already there, clipped to the - * box; no scaling. The bitmap is borrowed, not - * copied, and must outlive the icon (unlike - * text). fg, bg, text and pattern are ignored. - * Not interactive unless - * wuss_ICON_FLAGS_INTERACTIVE is set, in which - * case clicks raise wuss_EVENT_ICON like a - * button. */ - wuss_ICON_TYPE_MENU_ENTRY, /**< A menu row: text left-justified across the - * bounding box, drawn in fg over the window - * background, or inverted (window manager's title - * colours) while the pointer is over it. An - * optional tick at the left edge when the icon is - * selected (see wuss_icon_set_selected), and an - * optional submenu arrow at the right edge with - * wuss_ICON_FLAGS_SUBMENU. - * wuss_ICON_FLAGS_SEPARATOR marks the entry as - * following a group boundary: the dashed rule - * itself is a separate wuss_ICON_TYPE_RULE icon - * laid out above the entry, not drawn by the - * entry. The entry keeps its label and stays - * fully interactive. Interactive: a click raises - * wuss_EVENT_ICON like a button; a disabled - * entry never highlights and its clicks fall - * through. */ - wuss_ICON_TYPE_RULE /**< A horizontal dashed rule centred in the - * bounding box, drawn in fg over the window - * background. Purely decorative: never - * hit-tested, never highlights. text, bg and - * pattern are ignored. Used between menu rows to - * render the line a wuss_ICON_FLAGS_SEPARATOR - * entry sits below. */ + /** Static text drawn with the window manager's font. Not interactive: clicks + * fall through to the task as wuss_EVENT_MOUSE. */ + wuss_ICON_TYPE_LABEL = 0, + /** Bevelled rectangle with a centred text label and pressed-state visual + * feedback; clicks and hovers are delivered to the task as + * wuss_EVENT_ICON. */ + wuss_ICON_TYPE_BUTTON, + /** Bounding box filled with a repeating two-colour 8x8 tile (spec.pattern) in + * fg/bg, phased to document space so it scrolls rigidly with content. Not + * interactive: clicks fall through to the task as wuss_EVENT_MOUSE. text is + * ignored. */ + wuss_ICON_TYPE_PATTERN, + /** A grouping box: a one-pixel rectangle in fg around the bounding box, + * broken at the top-left for an optional caption (text) drawn over the + * window background. Not interactive: clicks fall through to the task as + * wuss_EVENT_MOUSE. */ + wuss_ICON_TYPE_FRAME, + /** A radio button: a small ring at the left of the bounding box, filled when + * selected, with the label (text) to its right. Interactive: a click selects + * it and clears every other selected radio sharing its non-zero group, then + * the task is told via wuss_EVENT_ICON. */ + wuss_ICON_TYPE_RADIO, + /** An option button: a small box at the left of the bounding box, ticked when + * selected, with the label (text) to its right. Interactive: a click toggles + * its own selected state (group is ignored), then the task is told via + * wuss_EVENT_ICON. */ + wuss_ICON_TYPE_OPTION, + /** A caller-owned bitmap (spec.bitmap) drawn at the top-left of the bounding + * box, alpha-blended against what is already there, clipped to the box; no + * scaling. The bitmap is borrowed, not copied, and must outlive the icon + * (unlike text). fg, bg, text and pattern are ignored. Not interactive + * unless wuss_ICON_FLAGS_INTERACTIVE is set, in which case clicks raise + * wuss_EVENT_ICON like a button. */ + wuss_ICON_TYPE_BITMAP, + /** A menu row: text left-justified across the bounding box, drawn in fg over + * the window background, or inverted (window manager's title colours) while + * the pointer is over it. An optional tick at the left edge when the icon is + * selected (see wuss_icon_set_selected), and an optional submenu arrow at + * the right edge with wuss_ICON_FLAGS_SUBMENU. wuss_ICON_FLAGS_SEPARATOR + * marks the entry as following a group boundary: the dashed rule itself is a + * separate wuss_ICON_TYPE_RULE icon laid out above the entry, not drawn by + * the entry. The entry keeps its label and stays fully interactive. + * Interactive: a click raises wuss_EVENT_ICON like a button; a disabled + * entry never highlights and its clicks fall through. */ + wuss_ICON_TYPE_MENU_ENTRY, + /** A horizontal dashed rule centred in the bounding box, drawn in fg over the + * window background. Purely decorative: never hit-tested, never highlights. + * text, bg and pattern are ignored. Used between menu rows to render the + * line a wuss_ICON_FLAGS_SEPARATOR entry sits below. */ + wuss_ICON_TYPE_RULE } wuss_icon_type_t; @@ -121,84 +108,73 @@ wuss_icon_type_t; typedef enum wuss_icon_flags { wuss_ICON_FLAGS_NONE = 0, - wuss_ICON_FLAGS_HIDDEN = 1 << 0, /**< Not drawn, not hit-tested. */ - wuss_ICON_FLAGS_DISABLED = 1 << 1, /**< Drawn greyed; clicks fall through - * to the task as wuss_EVENT_MOUSE - * rather than raising - * wuss_EVENT_ICON. */ - wuss_ICON_FLAGS_JUSTIFY_RIGHT = 1 << 2, /**< wuss_ICON_TYPE_LABEL: right-align - * the text in the bounding box - * instead of the default left. */ - wuss_ICON_FLAGS_JUSTIFY_CENTRE = 1 << 3, /**< wuss_ICON_TYPE_LABEL: centre - * the text in the bounding box. - * Takes precedence over - * wuss_ICON_FLAGS_JUSTIFY_RIGHT. */ - wuss_ICON_FLAGS_DEFAULT = 1 << 4, /**< 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_FLAGS_INTERACTIVE = 1 << 5, /**< 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 clicks fall through - * as wuss_EVENT_MOUSE. Ignored by - * other icon types (interactive or - * not by their nature). */ - wuss_ICON_FLAGS_SUBMENU = 1 << 6, /**< wuss_ICON_TYPE_MENU_ENTRY: draw a - * right-pointing arrow at the right - * edge, marking an entry that opens - * a submenu. Ignored by other - * types. */ - wuss_ICON_FLAGS_SEPARATOR = 1 << 7 /**< wuss_ICON_TYPE_MENU_ENTRY: the - * entry follows a group boundary. It - * stays a normal interactive row - * with its own label; the dashed - * rule above it is laid out and - * drawn as a separate - * wuss_ICON_TYPE_RULE icon. Ignored - * by other types. */ + /** Not drawn, not hit-tested. */ + wuss_ICON_FLAGS_HIDDEN = 1 << 0, + /** Drawn greyed; clicks fall through to the task as wuss_EVENT_MOUSE rather + * than raising wuss_EVENT_ICON. */ + wuss_ICON_FLAGS_DISABLED = 1 << 1, + /** wuss_ICON_TYPE_LABEL: right-align the text in the bounding box instead of + * the default left. */ + wuss_ICON_FLAGS_JUSTIFY_RIGHT = 1 << 2, + /** 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_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 + * clicks fall through as wuss_EVENT_MOUSE. Ignored by other icon types + * (interactive or not by their nature). */ + wuss_ICON_FLAGS_INTERACTIVE = 1 << 5, + /** wuss_ICON_TYPE_MENU_ENTRY: draw a right-pointing arrow at the right edge, + * marking an entry that opens a submenu. Ignored by other types. */ + wuss_ICON_FLAGS_SUBMENU = 1 << 6, + /** wuss_ICON_TYPE_MENU_ENTRY: the entry follows a group boundary. It stays a + * normal interactive row with its own label; the dashed rule above it is + * laid out and drawn as a separate wuss_ICON_TYPE_RULE icon. Ignored by + * other types. */ + wuss_ICON_FLAGS_SEPARATOR = 1 << 7 } wuss_icon_flags_t; /** - * Description of an icon at creation. Copied by value into the icon; the caller - * keeps ownership of \c text, which is copied. + * Description of an icon at creation. Copied by value into the icon; the + * caller keeps ownership of \c text, which is copied. * - * A RISC OS-style validation string is deliberately omitted for now; a later \c - * validation field would stay source-compatible for callers that + * A RISC OS-style validation string is deliberately omitted for now; a later + * \c validation field would stay source-compatible for callers that * zero-initialise the spec. */ typedef struct wuss_icon_spec { - box_t bbox; /**< Bounding box, virtual document space, - * inclusive-exclusive. */ - wuss_icon_type_t type; /**< Icon type. */ - const char *text; /**< NUL-terminated label; copied. NULL means "". */ - wuss_colour_t fg; /**< Text colour, as an index into the system - * palette. */ - wuss_colour_t bg; /**< Fill/bevel base colour, as an index into the - * system palette. A label, frame, radio or option - * icon may pass wuss_NO_BACKGROUND for no fill - * behind its text/glyph; a button or pattern icon - * must pass a real index. */ - screen_pattern_t pattern; /**< Tile for wuss_ICON_TYPE_PATTERN; ignored by - * other types. Zero (screen_PATTERN_SOLID) is a - * safe default for zero-initialised specs. */ - const bitmap_t *bitmap; /**< 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. */ - int group; /**< 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 clears another. - * Ignored by all other icon types. */ - wuss_icon_flags_t flags; /**< Appearance/behaviour flags. */ + /** Bounding box, virtual document space, inclusive-exclusive. */ + box_t bbox; + /** Icon type. */ + wuss_icon_type_t type; + /** NUL-terminated label; copied. NULL means "". */ + const char *text; + /** Text colour, as an index into the system palette. */ + wuss_colour_t fg; + /** Fill/bevel base colour, as an index into the system palette. A label, + * frame, radio or option icon may pass wuss_NO_BACKGROUND for no fill behind + * its text/glyph; a button or pattern icon must pass a real index. */ + wuss_colour_t bg; + /** Tile for wuss_ICON_TYPE_PATTERN; ignored by other types. Zero + * (screen_PATTERN_SOLID) is a safe default for zero-initialised specs. */ + 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. */ + const bitmap_t *bitmap; + /** 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 + * clears another. Ignored by all other icon types. */ + int group; + /** Appearance/behaviour flags. */ + wuss_icon_flags_t flags; } wuss_icon_spec_t; @@ -206,36 +182,38 @@ wuss_icon_spec_t; /** * Create an icon on a window. The icon is owned by the window and freed when - * the window is closed (or the window manager destroyed). Its bounding box is - * invalidated so the next redraw paints it. + * the window is closed (or the window manager destroyed). Its bounding box + * is invalidated so the next redraw paints it. * * \param[in] window Window to attach the icon to. * \param[in] spec Icon description; copied. - * \param[out] icon Filled in with the new icon handle, or NULL if the caller - * does not need it. + * \param[out] icon Filled in with the new icon handle, or NULL if the + * caller does not need it. * \return \ref result_OK on success, \ref result_OOM on allocation failure, * \ref result_WUSS_BAD_COLOUR if fg or bg is out of range for the - * palette, or \ref result_WUSS_BAD_ICON if type is unknown, a button or - * pattern spec has no fill colour, or a bitmap spec has no bitmap. + * palette, or \ref result_WUSS_BAD_ICON if type is unknown, a button + * or pattern spec has no fill colour, or a bitmap spec has no + * bitmap. */ result_t wuss_icon_create(wuss_window_t *window, const wuss_icon_spec_t *spec, wuss_icon_t **icon); /** - * Create several icons on a window in one call, as if by \ref wuss_icon_create - * for each. Either all \c nspecs icons are created, or none are: on the first - * failure any icons already created by this call are destroyed and no handles - * are written. + * Create several icons on a window in one call, as if by \ref + * wuss_icon_create for each. Either all \c nspecs icons are created, or none + * are: on the first failure any icons already created by this call are + * destroyed and no handles are written. * * \param[in] window Window to attach the icons to. * \param[in] specs Array of \c nspecs icon descriptions; each copied. * \param[in] nspecs Number of entries in \c specs. Zero is a no-op. - * \param[out] icons Array of \c nspecs handles, filled in on success, or NULL - * if the caller does not need them. Untouched on failure. - * \return \ref result_OK on success, or the first failing \ref wuss_icon_create - * code (\ref result_OOM, \ref result_WUSS_BAD_COLOUR, \ref - * result_WUSS_BAD_ICON). + * \param[out] icons Array of \c nspecs handles, filled in on success, or + * NULL if the caller does not need them. Untouched on + * failure. + * \return \ref result_OK on success, or the first failing \ref + * wuss_icon_create code (\ref result_OOM, \ref + * result_WUSS_BAD_COLOUR, \ref result_WUSS_BAD_ICON). */ result_t wuss_icon_create_array(wuss_window_t *window, const wuss_icon_spec_t *specs, @@ -243,27 +221,27 @@ result_t wuss_icon_create_array(wuss_window_t *window, wuss_icon_t **icons); /** - * Destroy an icon, unlinking it from its window and invalidating its bounding - * box so the next redraw clears it. Safe to pass NULL. + * Destroy an icon, unlinking it from its window and invalidating its + * bounding box so the next redraw clears it. Safe to pass NULL. * * \param[in] icon Icon to destroy, or NULL. */ void wuss_icon_delete(wuss_icon_t *icon); /** - * Replace an icon's label text. The new text is copied. Invalidates the icon's - * bounding box. + * Replace an icon's label text. The new text is copied. Invalidates the + * icon's bounding box. * * \param[in] icon Icon to change. * \param[in] text New NUL-terminated label; copied. NULL means "". - * \return \ref result_OK on success, \ref result_OOM on allocation failure (the - * icon keeps its old text). + * \return \ref result_OK on success, \ref result_OOM on allocation failure + * (the icon keeps its old text). */ result_t wuss_icon_set_text(wuss_icon_t *icon, const char *text); /** - * Show or hide an icon, toggling wuss_ICON_FLAGS_HIDDEN. Invalidates the icon's - * bounding box. + * Show or hide an icon, toggling wuss_ICON_FLAGS_HIDDEN. Invalidates the + * icon's bounding box. * * \param[in] icon Icon to change. * \param[in] hidden Non-zero to hide the icon, zero to show it. @@ -290,8 +268,8 @@ wuss_icon_type_t wuss_icon_get_type(const wuss_icon_t *icon); * Fetch an icon's current label text. * * \param[in] icon Icon to query. - * \return The label, never NULL (may be ""). Owned by the icon; valid until the - * next wuss_icon_set_text or wuss_icon_delete on it. + * \return The label, never NULL (may be ""). Owned by the icon; valid until + * the next wuss_icon_set_text or wuss_icon_delete on it. */ const char *wuss_icon_get_text(const wuss_icon_t *icon); @@ -307,17 +285,18 @@ wuss_window_t *wuss_icon_get_window(const wuss_icon_t *icon); * Fetch a radio or option icon's selected (latched) state. * * \param[in] icon Icon to query. - * \return Non-zero if selected, zero otherwise. Always zero for icon types that - * have no latched state. + * \return Non-zero if selected, zero otherwise. Always zero for icon types + * that have no latched state. */ int wuss_icon_get_selected(const wuss_icon_t *icon); /** * Set a radio or option icon's selected state, invalidating it so the next - * redraw repaints it. For a radio with a non-zero group, selecting it (passing - * non-zero) also clears every other selected radio on the same window with that - * group. No task event is delivered -- this is the programmatic path, distinct - * from a user click. A no-op for icon types with no latched state. + * redraw repaints it. For a radio with a non-zero group, selecting it + * (passing non-zero) also clears every other selected radio on the same + * window with that group. No task event is delivered -- this is the + * programmatic path, distinct from a user click. A no-op for icon types with + * no latched state. * * \param[in] icon Icon to change. * \param[in] selected Non-zero to select, zero to deselect. diff --git a/include/wuss/menu.h b/include/wuss/menu.h index bbb36d93..ead3d27b 100644 --- a/include/wuss/menu.h +++ b/include/wuss/menu.h @@ -3,14 +3,15 @@ /** * \file menu.h * - * A thin helper for RISC OS-style pop-up menus: a menu is a borderless window - * populated with wuss_ICON_TYPE_MENU_ENTRY icons, but wuss owns the plumbing -- - * layout, placement, submenu chaining on hover and whole-chain dismissal on a - * click outside or a leaf selection. + * A thin helper for RISC OS-style pop-up menus: a menu is a borderless + * window populated with wuss_ICON_TYPE_MENU_ENTRY icons, but wuss owns the + * plumbing -- layout, placement, submenu chaining on hover and whole-chain + * dismissal on a click outside or a leaf selection. * - * Menus are described by caller-owned, immutable wuss_menu_t / wuss_menu_item_t - * structures (which may be static). The helper never mutates them; a task that - * wants a tick to change just edits its own array and reopens the menu. + * Menus are described by caller-owned, immutable wuss_menu_t / + * wuss_menu_item_t structures (which may be static). The helper never + * mutates them; a task that wants a tick to change just edits its own array + * and reopens the menu. * * Built only when WUSS_MENUS is defined (which implies WUSS_ICONS). */ @@ -73,8 +74,8 @@ typedef struct wuss__menu *wuss_menu_handle_t; * * \param[in] menu The menu the item belongs to (a submenu, if nested). * \param[in] index Index of the item within \c menu->items. - * \param[in] button wuss_button_t flags for the release; test with '&'. ADJUST - * keeps the chain open, SELECT closes it. + * \param[in] button wuss_button_t flags for the release; test with '&'. + * ADJUST keeps the chain open, SELECT closes it. * \param[in] ctx As passed to wuss_menu_open. */ typedef void (wuss_menu_select_fn_t)(const wuss_menu_t *menu, @@ -85,10 +86,10 @@ typedef void (wuss_menu_select_fn_t)(const wuss_menu_t *menu, /* ----------------------------------------------------------------------- */ /** - * Open \p menu as a pop-up at \p at (screen space), nudged to stay on screen. - * Any menu chain already open is closed first. The chain lives until a leaf is - * SELECT-picked, a click lands outside every menu window, or wuss_menu_close is - * called. + * Open \p menu as a pop-up at \p at (screen space), nudged to stay on + * screen. Any menu chain already open is closed first. The chain lives until + * a leaf is SELECT-picked, a click lands outside every menu window, or + * wuss_menu_close is called. * * \param[in] wuss Window manager. * \param[in] menu Menu to show; borrowed, must outlive the open chain. @@ -116,28 +117,30 @@ int wuss_menu_is_open(wuss_menu_handle_t handle); /** * Build a wuss_menu_t tree from a compact descriptor string. The syntax is - * lifted from PrivateEye's menu_create_from_desc. A comma separates items. The - * very first token is the root menu's titlebar caption, not an item -- exactly - * as the first token inside a '{ }' is that submenu's title -- so the same - * descriptor strings port across unchanged. Submenus keep the Wimp behaviour of - * discarding their title token; only the root's is kept. A leading '|' on an - * item draws a dashed rule above it, marking a group boundary; the item itself - * stays an ordinary interactive row. A '{ ... }' group after an item is that - * item's submenu. A per-token prefix '!' ticks the item, '~' shades (disables) - * it, and '>' attaches a submenu pulled as a <tt>const wuss_menu_t *</tt> from - * the varargs rather than from a following '{ }' block. A "%s" in a token - * substitutes the next <tt>const char *</tt> vararg. The '>' and "%s" varargs - * are consumed in the order they are encountered scanning left to right. + * lifted from PrivateEye's menu_create_from_desc. A comma separates items. + * The very first token is the root menu's titlebar caption, not an item -- + * exactly as the first token inside a '{ }' is that submenu's title -- so + * the same descriptor strings port across unchanged. Submenus keep the Wimp + * behaviour of discarding their title token; only the root's is kept. A + * leading '|' on an item draws a dashed rule above it, marking a group + * boundary; the item itself stays an ordinary interactive row. A '{ ... }' + * group after an item is that item's submenu. A per-token prefix '!' ticks + * the item, '~' shades (disables) it, and '>' attaches a submenu pulled as a + * <tt>const wuss_menu_t *</tt> from the varargs rather than from a following + * '{ }' block. A "%s" in a token substitutes the next <tt>const char *</tt> + * vararg. The '>' and "%s" varargs are consumed in the order they are + * encountered scanning left to right. * - * Example: <tt>wuss_menu_create_from_desc(&m, "Display, Open, !Grid, ~Export, - * |Quit")</tt> -- "Display" is the caption; the menu has four items, with a - * dashed rule above "Quit". + * Example: <tt>wuss_menu_create_from_desc(&m, "Display, Open, !Grid, + * ~Export, |Quit")</tt> -- "Display" is the caption; the menu has four + * items, with a dashed rule above "Quit". * * The whole tree, including copied label text, is one heap allocation graph - * owned by the caller; free it with wuss_menu_destroy. wuss_menu_open treats a - * desc-built tree exactly like a static literal. + * owned by the caller; free it with wuss_menu_destroy. wuss_menu_open treats + * a desc-built tree exactly like a static literal. * - * \param[out] out Filled with the root menu on success, untouched on failure. + * \param[out] out Filled with the root menu on success, untouched on + * failure. * \param[in] desc Descriptor string; its first token is the root caption. * \return \ref result_OK, \ref result_OOM, or \ref result_BAD_ARG for a * malformed descriptor (unbalanced braces, empty token, too deep). @@ -145,8 +148,8 @@ int wuss_menu_is_open(wuss_menu_handle_t handle); result_t wuss_menu_create_from_desc(wuss_menu_t **out, const char *desc, ...); /** - * Free a tree built by wuss_menu_create_from_desc, including every submenu and - * copied label. Safe to pass NULL. Never call this on a static or + * Free a tree built by wuss_menu_create_from_desc, including every submenu + * and copied label. Safe to pass NULL. Never call this on a static or * caller-assembled wuss_menu_t. * * \param[in] menu Root menu returned by wuss_menu_create_from_desc, or NULL. diff --git a/include/wuss/task.h b/include/wuss/task.h index b2cf1d66..37e4159f 100644 --- a/include/wuss/task.h +++ b/include/wuss/task.h @@ -3,8 +3,8 @@ /** * \file task.h * - * A Wuss task: the content delegate a window hands its drawing and input events - * to, and the events themselves. + * A Wuss task: the content delegate a window hands its drawing and input + * events to, and the events themselves. */ #ifndef WUSS_TASK_H @@ -25,7 +25,9 @@ extern "C" /* ----------------------------------------------------------------------- */ -/** Which kind of event a wuss_event_t carries; more will be added over time. */ +/** + * Which kind of event a wuss_event_t carries; more will be added over time. + */ typedef enum wuss_event_kind { wuss_EVENT_IDLE, /**< Wuss has finished its pending tasks. */ @@ -131,7 +133,9 @@ typedef result_t (wuss_event_fn_t)(wuss_window_t *window, const wuss_event_t *event, void *task_data); -/** A window's content delegate. Copied by value into the window at creation. */ +/** + * A window's content delegate. Copied by value into the window at creation. + */ typedef struct wuss_task { /** diff --git a/include/wuss/window.h b/include/wuss/window.h index 1d830d9d..8319742a 100644 --- a/include/wuss/window.h +++ b/include/wuss/window.h @@ -3,8 +3,8 @@ /** * \file window.h * - * A Wuss window: creation, destruction, positioning, sizing and task delegation - * of content drawing and mouse handling. + * A Wuss window: creation, destruction, positioning, sizing and task + * delegation of content drawing and mouse handling. */ #ifndef WUSS_WINDOW_H @@ -29,45 +29,49 @@ extern "C" /** * Create a window. * - * Furniture (titlebar/outline) is added outside \p content, not carved out of - * it: before clamping, the window's content area is exactly \p content, and its - * on-screen footprint (see wuss_window_get_visible_bounds) is \p content - * expanded outward by whatever furniture flags request. If that footprint would - * then fall off the top or left edge of the screen, the window (content - * included) is nudged right/down just enough to bring it flush with the edge, - * so the titlebar/close icon stay reachable; a window wider or taller than the - * screen keeps its top-left corner on-screen instead. The bottom/right edges - * are not clamped. + * Furniture (titlebar/outline) is added outside \p content, not carved out + * of it: before clamping, the window's content area is exactly \p content, + * and its on-screen footprint (see wuss_window_get_visible_bounds) is \p + * content expanded outward by whatever furniture flags request. If that + * footprint would then fall off the top or left edge of the screen, the + * window (content included) is nudged right/down just enough to bring it + * flush with the edge, so the titlebar/close icon stay reachable; a window + * wider or taller than the screen keeps its top-left corner on-screen + * instead. The bottom/right edges are not clamped. * * \param[in] wuss Window manager to create the window on. - * \param[in] content Requested content-area bounds, screen space. Copied in. - * \param[in] title Titlebar label, or NULL for none. Copied in, truncated if - * too long. Ignored if flags includes + * \param[in] content Requested content-area bounds, screen space. Copied + * in. + * \param[in] title Titlebar label, or NULL for none. Copied in, truncated + * if too long. Ignored if flags includes * 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. - * \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 for the task to draw - * its own background (avoids a redundant fill behind an - * opaque task). Any pattern is phased to the window's - * scroll origin so it stays locked to the content. - * Changeable later via wuss_window_set_background. + * \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 + * for the task to draw its own background (avoids a + * redundant fill behind an opaque task). Any pattern is + * phased to the window's scroll origin so it stays + * locked to the content. Changeable later via + * wuss_window_set_background. * \param[in] task Content delegate. Copied in. May be NULL for a window * with no content handling. * \param[in] doc Virtual document extent, for the scrollbars' sausage * proportions; pass content's own width and height for a * window with nothing to scroll. Also the ceiling a - * resize-drag or toggle-size will grow the content area to. + * resize-drag or toggle-size will grow the content area + * to. * \param[in] min_doc Minimum content size a resize-drag or toggle-size will - * shrink to. Pass (0,0) for the built-in floor. Clamped to - * doc, and to the built-in floor, so it can never make a - * window unusably small or larger than its document. + * shrink to. Pass (0,0) for the built-in floor. Clamped + * to doc, and to the built-in floor, so it can never + * make a window unusably small or larger than its + * document. * \param[out] window Newly created window. Becomes the topmost window. * \return \ref result_OK on success, \ref result_WUSS_TOO_SMALL if content's - * width or height is not positive, \ref result_WUSS_BAD_COLOUR if any - * of bg's colours are out of range for the palette, or another + * width or height is not positive, \ref result_WUSS_BAD_COLOUR if + * any of bg's colours are out of range for the palette, or another * appropriate result code. */ result_t wuss_window_create(wuss_t *wuss, @@ -83,21 +87,21 @@ result_t wuss_window_create(wuss_t *wuss, /** * Create a window, letting Wuss choose its position. * - * As wuss_window_create, but instead of a content box you pass just the content - * size; Wuss places the window (furniture included) in the first free screen - * region, packed towards the top-left, tracking occupied area across calls so - * successive auto-placed windows tile rather than stack. When no region is - * large enough the window is cascaded from the previous placement, stepping by - * a titlebar height and wrapping at the screen edge. + * As wuss_window_create, but instead of a content box you pass just the + * content size; Wuss places the window (furniture included) in the first + * free screen region, packed towards the top-left, tracking occupied area + * across calls so successive auto-placed windows tile rather than stack. + * When no region is large enough the window is cascaded from the previous + * placement, stepping by a titlebar height and wrapping at the screen edge. * - * The chosen slot is returned to the pool when the window is closed, or when it - * is first moved or resized via wuss_window_move / wuss_window_resize (after - * which Wuss no longer tracks its position). A window dragged by its titlebar - * counts as moved. + * The chosen slot is returned to the pool when the window is closed, or when + * it is first moved or resized via wuss_window_move / wuss_window_resize + * (after which Wuss no longer tracks its position). A window dragged by its + * titlebar counts as moved. * * \param[in] wuss Window manager to create the window on. - * \param[in] size Requested content-area size. Width and height must both - * be positive. + * \param[in] size Requested content-area size. Width and height must + * both be positive. * \param[in] title Titlebar label, as wuss_window_create. * \param[in] flags Appearance flags, as wuss_window_create. * \param[in] bg Content background, as wuss_window_create. @@ -105,9 +109,10 @@ result_t wuss_window_create(wuss_t *wuss, * \param[in] doc Virtual document extent, as wuss_window_create. * \param[in] min_doc Minimum content size, as wuss_window_create. * \param[out] window Newly created window. Becomes the topmost window. - * \return \ref result_OK on success, \ref result_WUSS_TOO_SMALL if size's width - * or height is not positive, \ref result_OOM if the layout tracker - * could not be created, or another result code from wuss_window_create. + * \return \ref result_OK on success, \ref result_WUSS_TOO_SMALL if size's + * width or height is not positive, \ref result_OOM if the layout + * tracker could not be created, or another result code from + * wuss_window_create. */ result_t wuss_window_create_placed(wuss_t *wuss, size2d_t size, @@ -139,8 +144,8 @@ void wuss_window_move(wuss_window_t *window, point_t p); * * \param[in] window Window to resize. * \param[in] size New content size. - * \return \ref result_OK on success, \ref result_WUSS_TOO_SMALL if size's width - * or height is not positive. + * \return \ref result_OK on success, \ref result_WUSS_TOO_SMALL if size's + * width or height is not positive. */ result_t wuss_window_resize(wuss_window_t *window, size2d_t size); @@ -163,8 +168,8 @@ void wuss_window_get_visible_bounds(const wuss_window_t *window, box_t *visible); /** - * Fetch a window's current content-area bounds, screen space (as requested at - * creation, or adjusted by a subsequent move/resize; excludes any + * Fetch a window's current content-area bounds, screen space (as requested + * at creation, or adjusted by a subsequent move/resize; excludes any * titlebar/outline furniture). Useful for computing invalidation regions * outside of a redraw callback. * @@ -175,14 +180,15 @@ void wuss_window_get_content_bounds(const wuss_window_t *window, box_t *content); /** - * Mark a region of a window's content as dirty, for the next wuss_redraw_dirty - * call. Content changes are opaque to Wuss, so tasks must call this themselves - * (e.g. the union of an animated element's old and new positions). + * Mark a region of a window's content as dirty, for the next + * wuss_redraw_dirty call. Content changes are opaque to Wuss, so tasks must + * call this themselves (e.g. the union of an animated element's old and new + * positions). * * \param[in] window Window whose content changed. - * \param[in] local_box Region, in window-local content coordinates (as passed - * to the task's mouse callback), or NULL to mark the whole - * content area dirty. + * \param[in] local_box Region, in window-local content coordinates (as + * passed to the task's mouse callback), or NULL to mark + * the whole content area dirty. */ void wuss_window_invalidate(wuss_window_t *window, const box_t *local_box); @@ -192,11 +198,11 @@ void wuss_window_invalidate(wuss_window_t *window, const box_t *local_box); wuss_window_invalidate((window), NULL) /** - * Set a window's scroll offset: the point in virtual content space that appears - * at the content area's top-left. Larger offsets bring later content into view. - * Invalidates the content area so the next redraw picks up the new offset; the - * task's redraw callback is responsible for using the offset (via - * wuss_window_get_scroll) to draw the right portion of its content. + * Set a window's scroll offset: the point in virtual content space that + * appears at the content area's top-left. Larger offsets bring later content + * into view. Invalidates the content area so the next redraw picks up the + * new offset; the task's redraw callback is responsible for using the offset + * (via wuss_window_get_scroll) to draw the right portion of its content. * * \param[in] window Window to scroll. * \param[in] p New scroll offset. @@ -218,12 +224,13 @@ void wuss_window_get_scroll(const wuss_window_t *window, point_t *p); * \param[in] window Window to change. * \param[in] bg New content background: a flat colour or an 8x8 fill * pattern (see wuss_backdrop_t). Set its colour to - * wuss_NO_BACKGROUND to hand background painting back to the - * task. - * \return \ref result_OK on success, \ref result_WUSS_BAD_COLOUR if any of bg's - * colours are out of range for the palette. + * wuss_NO_BACKGROUND to hand background painting back to + * the task. + * \return \ref result_OK on success, \ref result_WUSS_BAD_COLOUR if any of + * bg's colours are out of range for the palette. */ -result_t wuss_window_set_background(wuss_window_t *window, wuss_backdrop_t bg); +result_t wuss_window_set_background(wuss_window_t *window, + wuss_backdrop_t bg); #ifdef __cplusplus } diff --git a/include/wuss/wuss.h b/include/wuss/wuss.h index fa43c69e..358b3f83 100644 --- a/include/wuss/wuss.h +++ b/include/wuss/wuss.h @@ -32,7 +32,8 @@ extern "C" /** A palette index was out of range. */ #define result_WUSS_BAD_COLOUR (result_BASE_WUSS + 1) /** - * An icon spec was malformed (unknown type, or BUTTON without a fill colour). + * An icon spec was malformed (unknown type, or BUTTON without a fill + * colour). */ #define result_WUSS_BAD_ICON (result_BASE_WUSS + 2) @@ -49,11 +50,12 @@ typedef struct wuss_window wuss_window_t; typedef struct wuss_icon wuss_icon_t; /** - * Allocator hooks used by a wuss_t for every heap block it owns (the instance - * itself, windows, icons, menu nodes). The three members must behave like the C - * library malloc / realloc / free -- same argument and return conventions, - * realloc(NULL, n) == malloc(n), free(NULL) a no-op. Passed to wuss_create and - * copied in; NULL there selects wuss_alloc (plain stdlib). + * Allocator hooks used by a wuss_t for every heap block it owns (the + * instance itself, windows, icons, menu nodes). The three members must + * behave like the C library malloc / realloc / free -- same argument and + * return conventions, realloc(NULL, n) == malloc(n), free(NULL) a no-op. + * Passed to wuss_create and copied in; NULL there selects wuss_alloc (plain + * stdlib). */ typedef struct wuss_alloc { @@ -72,8 +74,8 @@ extern const wuss_alloc_t wuss_alloc; * * These are flags, OR'd together, so that chords (e.g. Select and Adjust * pressed together) can be reported. The bit values match the RISC OS button - * order. Test them with '&' rather than comparing for equality, or a chord will - * match no button at all. + * order. Test them with '&' rather than comparing for equality, or a chord + * will match no button at all. */ typedef enum wuss_button { @@ -99,7 +101,8 @@ wuss_mouse_action_t; typedef int wuss_colour_t; /** - * Sentinel for wuss_window_create's bg meaning "no automatic background fill". + * Sentinel for wuss_window_create's bg meaning "no automatic background + * fill". */ #define wuss_NO_BACKGROUND ((wuss_colour_t) -1) @@ -181,8 +184,8 @@ typedef enum wuss_window_flags /** * 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 the window's size in ways redraw can't patch incrementally (e.g. - * a palette that lays itself out across the whole window). + * depends on the window's size in ways redraw can't patch incrementally + * (e.g. a palette that lays itself out across the whole window). */ wuss_WINDOW_NO_RESIZE_BLIT = 1 << 8 } @@ -198,25 +201,25 @@ typedef enum wuss_zorder wuss_zorder_t; /** - * Desktop background specification: a flat colour, or an 8x8 fill pattern. Used - * by wuss_config_t. Always honoured regardless of the WUSS_FURNITURE / + * Desktop background specification: a flat colour, or an 8x8 fill pattern. + * Used by wuss_config_t. Always honoured regardless of the WUSS_FURNITURE / * WUSS_ICONS options. */ typedef struct wuss_backdrop { /** - * Fill colour, or wuss_NO_BACKGROUND to leave the background untouched (the - * caller must then repaint it itself before wuss_redraw / wuss_redraw_dirty). - * When pattern is not screen_PATTERN_SOLID this is the pattern's foreground - * (set-bit) colour. + * Fill colour, or wuss_NO_BACKGROUND to leave the background untouched + * (the caller must then repaint it itself before wuss_redraw / + * wuss_redraw_dirty). When pattern is not screen_PATTERN_SOLID this is the + * pattern's foreground (set-bit) colour. */ wuss_colour_t colour; /** - * Fill pattern. screen_PATTERN_SOLID (the default) fills flat in colour; any - * other value tiles that pattern in colour over pattern_bg, phased to a fixed - * screen origin so it stays put across dirty-region redraws. Ignored when - * colour is wuss_NO_BACKGROUND. + * Fill pattern. screen_PATTERN_SOLID (the default) fills flat in colour; + * any other value tiles that pattern in colour over pattern_bg, phased to + * a fixed screen origin so it stays put across dirty-region redraws. + * Ignored when colour is wuss_NO_BACKGROUND. */ screen_pattern_t pattern; @@ -238,16 +241,16 @@ 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 is always honoured. See the - * backdrop sub-struct for its own notes. + * \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. */ typedef struct wuss_config { /** - * Titlebar height in pixels, or 0 to derive from font metrics (or a built-in - * fallback if no font). Ignored when WUSS_FURNITURE is off. + * Titlebar height in pixels, or 0 to derive from font metrics (or a + * built-in fallback if no font). Ignored when WUSS_FURNITURE is off. */ int titlebar_height; @@ -255,10 +258,11 @@ typedef struct wuss_config wuss_palette_t palette; /** - * 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. + * 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. */ struct { @@ -271,8 +275,8 @@ typedef struct wuss_config * 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. + * Both default to the titlebar colours (bg / fg) when config is NULL. + * Ignored when both WUSS_FURNITURE and WUSS_ICONS are off. */ struct { @@ -289,8 +293,8 @@ wuss_config_t; /** * Create a window manager. * - * \param[in] scr Screen to draw windows onto. Not owned; must outlive the - * wuss_t. + * \param[in] scr Screen to draw windows onto. Not owned; must outlive + * the wuss_t. * \param[in] font Font used to draw titlebar labels, or NULL to draw * titlebars unlabelled. Not owned. * \param[in] palette System palette, copied in, or NULL to use a built-in @@ -298,13 +302,13 @@ wuss_config_t; * \param[in] npalette Number of entries in palette. Ignored if palette is * NULL. * \param[in] config Creation-time configuration, or NULL for defaults. - * \param[in] alloc Allocator hooks, copied in, or NULL for \ref wuss_alloc - * (plain stdlib). Must outlive nothing -- only the three - * function pointers are kept. + * \param[in] alloc Allocator hooks, copied in, or NULL for \ref + * wuss_alloc (plain stdlib). Must outlive nothing -- + * only the three function pointers are kept. * \param[out] wuss Newly created window manager. * \return \ref result_OK on success, \ref result_WUSS_BAD_COLOUR if any of - * config's palette entries are out of range for the palette, or another - * appropriate result code. + * config's palette entries are out of range for the palette, or + * another appropriate result code. */ result_t wuss_create(screen_t *scr, bmfont_t *font, @@ -322,8 +326,8 @@ result_t wuss_create(screen_t *scr, void wuss_destroy(wuss_t *doomed); /** - * Fetch the system font (see wuss_create), for tasks to draw their own content - * in the same face as window titlebars. + * Fetch the system font (see wuss_create), for tasks to draw their own + * content in the same face as window titlebars. * * \param[in] wuss Window manager. * \return System font, or NULL if none was given to wuss_create. @@ -331,10 +335,10 @@ void wuss_destroy(wuss_t *doomed); bmfont_t *wuss_get_font(const wuss_t *wuss); /** - * The last pointer position seen by wuss_mouse_click or wuss_mouse_move, screen - * space. (0,0) until the first mouse event. Handy for opening a pop-up menu - * under the pointer from a task's icon handler, which gets no coordinate of its - * own. + * The last pointer position seen by wuss_mouse_click or wuss_mouse_move, + * screen space. (0,0) until the first mouse event. Handy for opening a + * pop-up menu under the pointer from a task's icon handler, which gets no + * coordinate of its own. * * \param[in] wuss Window manager. * \return Last pointer position, screen space. @@ -353,10 +357,11 @@ point_t wuss_get_pointer(const wuss_t *wuss); result_t wuss_redraw(wuss_t *wuss); /** - * Mark a screen-space region dirty. Window creation, destruction, move, resize - * and bring-to-front invalidate their own affected regions automatically; tasks - * must call this themselves when their content changes (e.g. an animation), - * passing the union of the old and new screen-space areas that need repainting. + * Mark a screen-space region dirty. Window creation, destruction, move, + * resize and bring-to-front invalidate their own affected regions + * automatically; tasks must call this themselves when their content changes + * (e.g. an animation), passing the union of the old and new screen-space + * areas that need repainting. * * \param[in] wuss Window manager. * \param[in] box Screen-space region to mark dirty. @@ -367,8 +372,8 @@ result_t wuss_invalidate(wuss_t *wuss, const box_t *box); /** * Redraw only the region accumulated by wuss_invalidate calls (and any * automatic invalidation from window management) since the last redraw, - * back-to-front, then clear the dirty region. Does nothing if nothing is dirty. - * Each dirty region has the configured backdrop colour (see + * back-to-front, then clear the dirty region. Does nothing if nothing is + * dirty. Each dirty region has the configured backdrop colour (see * wuss_config_t::backdrop) painted into it first, if any. * * \param[in] wuss Window manager. @@ -380,9 +385,9 @@ result_t wuss_redraw_dirty(wuss_t *wuss); /** * Fetch the number of dirty regions currently accumulated (see * wuss_invalidate). Regions are self-coalescing: an invalidation already - * covered by an existing region is discarded, and one sharing a complete edge - * with an existing region extends it in place, so this stays small under most - * usage. + * covered by an existing region is discarded, and one sharing a complete + * edge with an existing region extends it in place, so this stays small + * under most usage. * * \param[in] wuss Window manager. * \return Number of dirty regions, 0 if nothing is dirty. @@ -390,28 +395,28 @@ result_t wuss_redraw_dirty(wuss_t *wuss); int wuss_get_dirty_count(const wuss_t *wuss); /** - * Fetch one of the current accumulated dirty regions (see wuss_invalidate). If - * no backdrop colour was configured (see wuss_config_t::backdrop), wuss only - * repaints windows, not background between/behind them, so a caller whose - * invalidations can expose background (e.g. after a window move) should clear - * these regions itself before calling wuss_redraw_dirty. + * Fetch one of the current accumulated dirty regions (see wuss_invalidate). + * If no backdrop colour was configured (see wuss_config_t::backdrop), wuss + * only repaints windows, not background between/behind them, so a caller + * whose invalidations can expose background (e.g. after a window move) + * should clear these regions itself before calling wuss_redraw_dirty. * * \param[in] wuss Window manager. - * \param[in] index Index of the region to fetch, 0 to wuss_get_dirty_count() - - * 1. + * \param[in] index Index of the region to fetch, 0 to + * wuss_get_dirty_count() - 1. * \param[out] out Filled in with the dirty region. */ void wuss_get_dirty(const wuss_t *wuss, int index, box_t *out); /** * Deliver a mouse-down or mouse-up event (action must be wuss_MOUSE_DOWN or - * wuss_MOUSE_UP). Hit-tests the topmost window at (x,y). On a down, a titlebar - * click brings the window to front if button is Select (Adjust and Menu leave - * the z-order unchanged) and starts a drag; on an up, an in-progress drag is - * ended instead of hit-testing (an Adjust click with no move in between sends - * the window to the back rather than dragging it). A click on the window's - * content never changes the z-order and is delivered to the task in - * window-local content coordinates. + * wuss_MOUSE_UP). Hit-tests the topmost window at (x,y). On a down, a + * titlebar click brings the window to front if button is Select (Adjust and + * Menu leave the z-order unchanged) and starts a drag; on an up, an + * in-progress drag is ended instead of hit-testing (an Adjust click with no + * move in between sends the window to the back rather than dragging it). A + * click on the window's content never changes the z-order and is delivered + * to the task in window-local content coordinates. * * \param[in] wuss Window manager. * \param[in] p Screen coordinate. @@ -429,10 +434,10 @@ result_t wuss_mouse_click(wuss_t *wuss, wuss_window_t **hit); /** - * Deliver a mouse-move event. Updates the dragged window's position if a drag - * is active (invalidating the affected region; call wuss_redraw_dirty to - * actually repaint it), otherwise hit-tests and delivers to the window's task - * as per wuss_mouse_click. + * Deliver a mouse-move event. Updates the dragged window's position if a + * drag is active (invalidating the affected region; call wuss_redraw_dirty + * to actually repaint it), otherwise hit-tests and delivers to the window's + * task as per wuss_mouse_click. * * \param[in] wuss Window manager. * \param[in] p Screen coordinate. @@ -445,15 +450,15 @@ result_t wuss_mouse_move(wuss_t *wuss, point_t p, wuss_window_t **hit); /** * Deliver a scroll event. Hit-tests the topmost window at p as per - * wuss_mouse_click, and delivers to the window's task in window-local content - * coordinates; dropped if the hit window has no scroll callback, or the pointer - * is over its titlebar. + * wuss_mouse_click, and delivers to the window's task in window-local + * content coordinates; dropped if the hit window has no scroll callback, or + * the pointer is over its titlebar. * * \param[in] wuss Window manager. * \param[in] p Screen coordinate. * \param[in] delta Scroll amount; sign and units are caller-defined. - * \param[out] hit Window under the pointer, or NULL if none. May be NULL if - * not needed. + * \param[out] hit Window under the pointer, or NULL if none. May be NULL + * if not needed. * \return \ref result_OK, or a result code returned by the task's scroll * callback. */ @@ -464,13 +469,13 @@ result_t wuss_scroll(wuss_t *wuss, /** * Broadcast a wuss_EVENT_IDLE event to every window's task, in z-order. - * Intended to be called once per main-loop iteration, after other pending input - * has been handled, so tasks can drive their own animation/timers without the - * caller stepping each one individually. + * Intended to be called once per main-loop iteration, after other pending + * input has been handled, so tasks can drive their own animation/timers + * without the caller stepping each one individually. * * \param[in] wuss Window manager whose windows' tasks should go idle. - * \return \ref result_OK on success, else the first non-OK result returned by a - * task's handle callback. + * \return \ref result_OK on success, else the first non-OK result returned + * by a task's handle callback. */ result_t wuss_idle(wuss_t *wuss); diff --git a/libraries/datastruct/ntree/insert-before.c b/libraries/datastruct/ntree/insert-before.c index 5afd5627..0a2ccc49 100644 --- a/libraries/datastruct/ntree/insert-before.c +++ b/libraries/datastruct/ntree/insert-before.c @@ -7,7 +7,9 @@ #include "impl.h" -result_t ntree_insert_before(ntree_t *parent, ntree_t *sibling, ntree_t *node) +result_t ntree_insert_before(ntree_t *parent, + ntree_t *sibling, + ntree_t *node) { assert(node->parent == NULL); diff --git a/libraries/framebuf/bitmap/bitmap.c b/libraries/framebuf/bitmap/bitmap.c index bef5f884..c8004b0a 100644 --- a/libraries/framebuf/bitmap/bitmap.c +++ b/libraries/framebuf/bitmap/bitmap.c @@ -167,7 +167,9 @@ static result_t bmconv_p4_to_bgrx8888(const bitmap_t *src, bitmap_t **pdst) return rc; } -result_t bitmap_convert(const bitmap_t *src, pixelfmt_t newfmt, bitmap_t **dst) +result_t bitmap_convert(const bitmap_t *src, + pixelfmt_t newfmt, + bitmap_t **dst) { *dst = NULL; diff --git a/libraries/framebuf/screen/screen-draw.c b/libraries/framebuf/screen/screen-draw.c index 91bf7d1c..b3409057 100644 --- a/libraries/framebuf/screen/screen-draw.c +++ b/libraries/framebuf/screen/screen-draw.c @@ -215,7 +215,11 @@ void screen_fill_rect(screen_t *scr, } } -void screen_fill_square(screen_t *scr, int x, int y, int size, colour_t colour) +void screen_fill_square(screen_t *scr, + int x, + int y, + int size, + colour_t colour) { screen_fill_rect(scr, x, y, SIZE2D(size, size), colour); } diff --git a/libraries/io/stream/stream-packbitsdecomp.c b/libraries/io/stream/stream-packbitsdecomp.c index 7828e41b..3dde4a9b 100644 --- a/libraries/io/stream/stream-packbitsdecomp.c +++ b/libraries/io/stream/stream-packbitsdecomp.c @@ -170,7 +170,9 @@ static stream_size_t stream_packbitsdecomp_fill(stream_t *s) return stream_remaining(s); } -result_t stream_packbitsdecomp_create(stream_t *input, int bufsz, stream_t **s) +result_t stream_packbitsdecomp_create(stream_t *input, + int bufsz, + stream_t **s) { stream_packbitsdecomp_t *sp; diff --git a/libraries/wuss/furniture/invalidate.c b/libraries/wuss/furniture/invalidate.c index 0b7d2103..ad058751 100644 --- a/libraries/wuss/furniture/invalidate.c +++ b/libraries/wuss/furniture/invalidate.c @@ -7,7 +7,8 @@ * (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. */ -void wuss__furniture_invalidate_for(wuss_window_t *window, const box_t *visible) +void wuss__furniture_invalidate_for(wuss_window_t *window, + const box_t *visible) { int outline_px; point_t carve; diff --git a/libraries/wuss/icon/draw.c b/libraries/wuss/icon/draw.c index 8a9b0873..c4bdcf6a 100644 --- a/libraries/wuss/icon/draw.c +++ b/libraries/wuss/icon/draw.c @@ -50,7 +50,8 @@ static void icon_bevel(screen_t *scr, * 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 * blend colour. */ -static colour_t icon_blend_ground(const icon_draw_ctx_t *c, colour_t fallback) +static colour_t icon_blend_ground(const icon_draw_ctx_t *c, + colour_t fallback) { const wuss_icon_t *icon = c->icon; diff --git a/libraries/wuss/impl.h b/libraries/wuss/impl.h index a74a5b11..6e3bccb6 100644 --- a/libraries/wuss/impl.h +++ b/libraries/wuss/impl.h @@ -267,7 +267,8 @@ static inline void wuss__release_packed(wuss_window_t *window) * the client's min_doc where it set one, but never below WUSS_MIN_CONTENT (a * window must stay big enough to grab) nor above the window's own doc extent * (a window can't be forced larger than the document it shows). */ -static inline void wuss__min_content(const wuss_window_t *window, size2d_t *min) +static inline void wuss__min_content(const wuss_window_t *window, + size2d_t *min) { min->w = CLAMP(window->min_doc.w, WUSS_MIN_CONTENT, MAX(window->doc.w, WUSS_MIN_CONTENT)); @@ -292,7 +293,8 @@ static inline int wuss__window_toggled(const wuss_window_t *window) return (window->state & wuss_WINDOW_STATE_TOGGLED) != 0; } -static inline void wuss__window_set_toggled(wuss_window_t *window, int toggled) +static inline void wuss__window_set_toggled(wuss_window_t *window, + int toggled) { if (toggled) window->state |= wuss_WINDOW_STATE_TOGGLED; diff --git a/libraries/wuss/test/tasks/ball.c b/libraries/wuss/test/tasks/ball.c index 12cd4083..9923b12e 100644 --- a/libraries/wuss/test/tasks/ball.c +++ b/libraries/wuss/test/tasks/ball.c @@ -14,7 +14,9 @@ #include "ball.h" -result_t ball_create(wuss_t *wuss, const colour_t *palette, ball_task_t *task) +result_t ball_create(wuss_t *wuss, + const colour_t *palette, + ball_task_t *task) { wuss_task_t delegate; diff --git a/libraries/wuss/test/tasks/ball.h b/libraries/wuss/test/tasks/ball.h index ac87f603..5e54df59 100644 --- a/libraries/wuss/test/tasks/ball.h +++ b/libraries/wuss/test/tasks/ball.h @@ -36,7 +36,9 @@ wuss_event_fn_t ball_handle; /* create the bouncing-ball window against the given wuss instance; "task" is a * per-instance block owned by the window and freed when it closes */ -result_t ball_create(wuss_t *wuss, const colour_t *palette, ball_task_t *task); +result_t ball_create(wuss_t *wuss, + const colour_t *palette, + ball_task_t *task); #endif /* USE_SDL */ diff --git a/libraries/wuss/test/tasks/porter-duff.c b/libraries/wuss/test/tasks/porter-duff.c index d7d82902..15db71fb 100644 --- a/libraries/wuss/test/tasks/porter-duff.c +++ b/libraries/wuss/test/tasks/porter-duff.c @@ -279,7 +279,8 @@ 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) +static result_t porter_duff_redraw(const wuss_event_t *event, + void *task_data) { porter_duff_task_t *pd; screen_t *scr; diff --git a/libraries/wuss/test/tasks/sofa.c b/libraries/wuss/test/tasks/sofa.c index c3341764..266117a9 100644 --- a/libraries/wuss/test/tasks/sofa.c +++ b/libraries/wuss/test/tasks/sofa.c @@ -354,7 +354,9 @@ static void draw_vertex_dots(screen_t *scr, colour); } -result_t sofa_create(wuss_t *wuss, const colour_t *palette, sofa_task_t *task) +result_t sofa_create(wuss_t *wuss, + const colour_t *palette, + sofa_task_t *task) { wuss_task_t delegate; @@ -516,7 +518,9 @@ static result_t sofa_mouse(wuss_window_t *window, return result_OK; } -static result_t sofa_scroll(wuss_window_t *window, int delta, void *task_data) +static result_t sofa_scroll(wuss_window_t *window, + int delta, + void *task_data) { sofa_task_t *sc; diff --git a/libraries/wuss/test/tasks/sofa.h b/libraries/wuss/test/tasks/sofa.h index 35c28c76..08aeb6df 100644 --- a/libraries/wuss/test/tasks/sofa.h +++ b/libraries/wuss/test/tasks/sofa.h @@ -43,7 +43,9 @@ sofa_task_t; wuss_event_fn_t sofa_handle; /* create the sofa window against the given wuss instance */ -result_t sofa_create(wuss_t *wuss, const colour_t *palette, sofa_task_t *task); +result_t sofa_create(wuss_t *wuss, + const colour_t *palette, + sofa_task_t *task); #endif /* USE_SDL */ diff --git a/libraries/wuss/window/get-content-bounds.c b/libraries/wuss/window/get-content-bounds.c index 9eda68e7..4a8cbab5 100644 --- a/libraries/wuss/window/get-content-bounds.c +++ b/libraries/wuss/window/get-content-bounds.c @@ -2,7 +2,8 @@ #include "../impl.h" -void wuss_window_get_content_bounds(const wuss_window_t *window, box_t *content) +void wuss_window_get_content_bounds(const wuss_window_t *window, + box_t *content) { wuss__content_box(window, content); } diff --git a/libraries/wuss/window/get-visible-bounds.c b/libraries/wuss/window/get-visible-bounds.c index e7e08765..d305ad4e 100644 --- a/libraries/wuss/window/get-visible-bounds.c +++ b/libraries/wuss/window/get-visible-bounds.c @@ -2,7 +2,8 @@ #include "../impl.h" -void wuss_window_get_visible_bounds(const wuss_window_t *window, box_t *visible) +void wuss_window_get_visible_bounds(const wuss_window_t *window, + box_t *visible) { *visible = window->visible; } diff --git a/libraries/wuss/window/invalidate.c b/libraries/wuss/window/invalidate.c index 1d4bb5da..33986c6d 100644 --- a/libraries/wuss/window/invalidate.c +++ b/libraries/wuss/window/invalidate.c @@ -45,7 +45,9 @@ static void box_subtract_into(const box_t *piece, /* Clip "box" (screen space) down to the parts not already covered by * windows above "window" in the z-order, writing the surviving pieces to * "out" (capacity WUSS_MAX_INVALIDATE_PIECES) and returning their count. */ -int wuss__clip_to_visible(wuss_window_t *window, const box_t *box, box_t *out) +int wuss__clip_to_visible(wuss_window_t *window, + const box_t *box, + box_t *out) { box_t scratch[WUSS_MAX_INVALIDATE_PIECES]; box_t *cur, *nxt, *tmp; @@ -240,7 +242,9 @@ void wuss__invalidate_clipped(wuss_window_t *window, const box_t *box) /* Invalidate the part of "whole" not already covered by "keep" -- used * after a blit has slid a window's pixels from "whole" to "keep", so only * the vacated sliver still needs an actual repaint. */ -void wuss__invalidate_minus(wuss_t *wuss, const box_t *whole, const box_t *keep) +void wuss__invalidate_minus(wuss_t *wuss, + const box_t *whole, + const box_t *keep) { box_t pieces[WUSS_MAX_INVALIDATE_PIECES]; box_t cut; diff --git a/libraries/wuss/window/set-background.c b/libraries/wuss/window/set-background.c index 1c5cf95c..5e846a33 100644 --- a/libraries/wuss/window/set-background.c +++ b/libraries/wuss/window/set-background.c @@ -2,7 +2,8 @@ #include "../impl.h" -result_t wuss_window_set_background(wuss_window_t *window, wuss_backdrop_t bg) +result_t wuss_window_set_background(wuss_window_t *window, + wuss_backdrop_t bg) { box_t content; diff --git a/tools/test_wrap_doxygen.py b/tools/test_wrap_doxygen.py index b0fe1a5e..f252a2e7 100644 --- a/tools/test_wrap_doxygen.py +++ b/tools/test_wrap_doxygen.py @@ -16,10 +16,10 @@ int g(void); '''.splitlines() -out, overflow = process(src, width=80) +out, overflow = process(src, width=77) assert overflow == [], overflow -assert all(len(l) <= 80 for l in out), [l for l in out if len(l) > 80] +assert all(len(l) <= 77 for l in out), [l for l in out if len(l) > 77] assert out[0] == '/**' assert out[1] == ' * A short line.' # continuation lines must keep the "*" one column right of "/**", with a @@ -31,7 +31,7 @@ assert out.count('/**') == 2 # original block + the promoted single-liner # idempotent: reformatting already-reformatted text changes nothing -out2, overflow2 = process(out, width=80) +out2, overflow2 = process(out, width=77) assert out2 == out assert overflow2 == [] diff --git a/tools/test_wrap_protos.py b/tools/test_wrap_protos.py index abef8405..c09d3eaf 100644 --- a/tools/test_wrap_protos.py +++ b/tools/test_wrap_protos.py @@ -2,7 +2,7 @@ """assert-based self-check for wrap_protos.py""" from wrap_protos import process, split_top_level -W = 80 +W = 77 def one(src): @@ -41,7 +41,7 @@ def test_idempotent(): def test_rewrap_bad_alignment(): - # joined form is >80, and it arrives already (badly) wrapped + # joined form is >77, and it arrives already (badly) wrapped src = ( "static int screen_copy_rectangle_p4(screen_t *scr,\n" " const box_t *s, const box_t *d,\n" @@ -59,7 +59,7 @@ def test_rewrap_bad_alignment(): def test_short_multiline_untouched(): - # already multi-line but joins to <80 -- leave the hand grouping alone + # already multi-line but joins to <77 -- leave the hand grouping alone src = ( "void draw_rect(screen_t *scr,\n" " int x, int y,\n" @@ -117,7 +117,7 @@ def test_variadic_bails(): def test_call_statement_not_touched(): src = (" array_squeeze(v->base, v->used, v->width, width_argument_here); " - "// a call, way over 80 columns of course yes indeed\n") + "// a call, way over 77 columns of course yes indeed\n") assert one(src) is None diff --git a/tools/wrap_doxygen.py b/tools/wrap_doxygen.py index f64906ba..4c6b2e8a 100644 --- a/tools/wrap_doxygen.py +++ b/tools/wrap_doxygen.py @@ -131,7 +131,7 @@ def flush(): def main(): ap = argparse.ArgumentParser(description=__doc__.splitlines()[0]) ap.add_argument('files', nargs='+') - ap.add_argument('--width', type=int, default=80) + ap.add_argument('--width', type=int, default=77) ap.add_argument('--check', action='store_true', help="don't write; exit 1 if any file would change") args = ap.parse_args() diff --git a/tools/wrap_protos.py b/tools/wrap_protos.py index 03191596..2787af3a 100644 --- a/tools/wrap_protos.py +++ b/tools/wrap_protos.py @@ -302,7 +302,7 @@ def main(): description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) ap.add_argument('files', nargs='+') - ap.add_argument('-w', '--width', type=int, default=80) + ap.add_argument('-w', '--width', type=int, default=77) ap.add_argument('-n', '--dry-run', action='store_true', help='report files that would change, write nothing') args = ap.parse_args() From cddfdf8cac342d293782c0197ed9ef0d72651054 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Tue, 1 Sep 2026 19:26:25 +0100 Subject: [PATCH 149/287] refactor(wuss): replace the demo launcher window with a backdrop menu The interactive wuss app opened a persistent Launcher window full of button icons, one per task. Drop it and launcher.c/.h entirely; a MENU-button click on the bare backdrop now opens a "Tasks" pop-up menu at the pointer via wuss_menu_open. g_task_items and a parallel g_task_spawn table drive it: picking item i calls g_task_spawn[i]. The two menu-helper demo entries sit below a dashed rule. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- CMakeLists.txt | 1 - apps/wuss/main.c | 79 ++++++++++++------ libraries/wuss/test/tasks/launcher.c | 117 --------------------------- libraries/wuss/test/tasks/launcher.h | 49 ----------- 4 files changed, 53 insertions(+), 193 deletions(-) delete mode 100644 libraries/wuss/test/tasks/launcher.c delete mode 100644 libraries/wuss/test/tasks/launcher.h diff --git a/CMakeLists.txt b/CMakeLists.txt index b01414d8..bd70cd46 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -677,7 +677,6 @@ if(BUILD_APPS) libraries/wuss/test/tasks/gradient.c libraries/wuss/test/tasks/icons.c libraries/wuss/test/tasks/image.c - libraries/wuss/test/tasks/launcher.c libraries/wuss/test/tasks/lissajous.c libraries/wuss/test/tasks/palette.c libraries/wuss/test/tasks/porter-duff.c diff --git a/apps/wuss/main.c b/apps/wuss/main.c index a11a8c81..a55c743d 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -32,7 +32,6 @@ #include "tasks/gradient.h" #include "tasks/icons.h" #include "tasks/image.h" -#include "tasks/launcher.h" #include "tasks/lissajous.h" #include "tasks/palette.h" #include "tasks/porter-duff.h" @@ -283,25 +282,52 @@ static result_t spawn_menu_desc(void) menu_selected, NULL, NULL); } -static const launcher_entry_t g_launcher_entries[] = +/* The task launcher is a MENU-button pop-up over the backdrop rather than a + * window of buttons. g_task_items and g_task_spawn run in lock-step: picking + * item i calls g_task_spawn[i]. */ +typedef result_t (*task_spawn_fn_t)(void); + +static const wuss_menu_item_t g_task_items[] = +{ + { "Ball", wuss_MENU_ITEM_NONE, NULL }, + { "Text", wuss_MENU_ITEM_NONE, NULL }, + { "Blank", wuss_MENU_ITEM_NONE, NULL }, + { "Chars", wuss_MENU_ITEM_NONE, NULL }, + { "Palette", wuss_MENU_ITEM_NONE, NULL }, + { "Image", wuss_MENU_ITEM_NONE, NULL }, + { "Checker", wuss_MENU_ITEM_NONE, NULL }, + { "Curve", wuss_MENU_ITEM_NONE, NULL }, + { "Lissajous", wuss_MENU_ITEM_NONE, NULL }, + { "Sofa", wuss_MENU_ITEM_NONE, NULL }, + { "Gradient", wuss_MENU_ITEM_NONE, NULL }, + { "Icons", wuss_MENU_ITEM_NONE, NULL }, + { "Porter-Duff", wuss_MENU_ITEM_NONE, NULL }, + { "Menu", wuss_MENU_ITEM_DASHED, NULL }, + { "Menu (desc)", wuss_MENU_ITEM_NONE, NULL } +}; +static const task_spawn_fn_t g_task_spawn[] = +{ + spawn_ball, spawn_text, spawn_blank, spawn_chars, spawn_palette, + spawn_image, spawn_checker, spawn_curve, spawn_lissajous, spawn_sofa, + spawn_gradient, spawn_icons, spawn_porter_duff, spawn_menu, spawn_menu_desc +}; +static const wuss_menu_t g_task_menu = { - { "Ball", spawn_ball }, - { "Text", spawn_text }, - { "Blank", spawn_blank }, - { "Chars", spawn_chars }, - { "Palette", spawn_palette }, - { "Image", spawn_image }, - { "Checker", spawn_checker }, - { "Curve", spawn_curve }, - { "Lissajous", spawn_lissajous }, - { "Sofa", spawn_sofa }, - { "Gradient", spawn_gradient }, - { "Icons", spawn_icons }, - { "Porter-Duff", spawn_porter_duff }, - { "Menu", spawn_menu }, - { "Menu (desc)", spawn_menu_desc } + "Tasks", g_task_items, NELEMS(g_task_items) }; +static void task_menu_selected(const wuss_menu_t *menu, + int index, + wuss_button_t button, + void *ctx) +{ + NOT_USED(menu); + NOT_USED(button); + NOT_USED(ctx); + if (index >= 0 && index < (int) NELEMS(g_task_spawn)) + (void) g_task_spawn[index](); +} + static wuss_button_t sdl_button_to_wuss(Uint8 button) { switch (button) @@ -358,7 +384,6 @@ static result_t run_wuss(const char *resources) screen_t scr; colour_t palette[16]; wuss_t *wuss; - launcher_task_t launcher_task; SDL_Window *window; SDL_Renderer *renderer; SDL_Texture *texture; @@ -469,10 +494,6 @@ static result_t run_wuss(const char *resources) g_resources = resources; g_daydream_font = daydream_font; - rc = launcher_create(wuss, g_launcher_entries, NELEMS(g_launcher_entries), &launcher_task); - if (rc != result_OK) - goto Failure; - quit = false; garbage_pending = false; pixel_stress_pending = false; @@ -514,10 +535,18 @@ static result_t run_wuss(const char *resources) case SDL_EVENT_MOUSE_BUTTON_DOWN: { - int x, y; + int x, y; + wuss_button_t button; + wuss_window_t *hit; sdl_pos_to_scr(window, scr_width, scr_height, event.button.x, event.button.y, &x, &y); - wuss_mouse_click(wuss, POINT(x, y), sdl_button_to_wuss(event.button.button), wuss_MOUSE_DOWN, NULL); + button = sdl_button_to_wuss(event.button.button); + wuss_mouse_click(wuss, POINT(x, y), button, wuss_MOUSE_DOWN, &hit); + + /* MENU click on bare backdrop opens the task launcher there */ + if (hit == NULL && (button & wuss_BUTTON_MENU)) + wuss_menu_open(wuss, &g_task_menu, POINT(x, y), + task_menu_selected, NULL, NULL); } break; @@ -626,8 +655,6 @@ static result_t run_wuss(const char *resources) * per-instance task block hung off it, so any task window left open at quit * leaks its block. Harmless at process exit; add a wuss close callback if a * task ever needs deterministic teardown. */ - launcher_destroy(&launcher_task); - wuss_menu_destroy(g_menu_desc); wuss_destroy(wuss); diff --git a/libraries/wuss/test/tasks/launcher.c b/libraries/wuss/test/tasks/launcher.c deleted file mode 100644 index db967404..00000000 --- a/libraries/wuss/test/tasks/launcher.c +++ /dev/null @@ -1,117 +0,0 @@ -/* wuss/test/tasks/launcher.c -- clickable list of names that spawn other tasks */ - -#ifdef USE_SDL - -#include <assert.h> -#include <string.h> - -#ifdef FORTIFY -#include "fortify/fortify.h" -#endif - -#include "base/utils.h" -#include "framebuf/palettes.h" -#include "geom/box.h" - -#include "wuss/icon.h" - -#include "launcher.h" - -#define LAUNCHER_ROW_HEIGHT 22 -#define LAUNCHER_PAD 4 -#define LAUNCHER_WIDTH 160 - -result_t launcher_create(wuss_t *wuss, - const launcher_entry_t *entries, - int nentries, - launcher_task_t *task) -{ - wuss_task_t delegate; - wuss_icon_spec_t specs[LAUNCHER_MAX_ENTRIES]; - size2d_t sz; - int i; - result_t rc; - - assert(nentries <= LAUNCHER_MAX_ENTRIES); - - task->entries = entries; - task->nentries = nentries; - task->window = NULL; - memset(task->icons, 0, sizeof(task->icons)); - - delegate = wuss_task_start(launcher_handle, task); - sz = SIZE2D(LAUNCHER_WIDTH, - LAUNCHER_PAD * 2 + nentries * LAUNCHER_ROW_HEIGHT); - - rc = wuss_window_create_placed(wuss, - sz, - "Launcher", - wuss_WINDOW_NO_CLOSE, - wuss_BACKDROP_COLOUR(palette_PICO8_LIGHT_GREY), - &delegate, - sz, - SIZE2D(0, 0), - &task->window); - if (rc != result_OK) - return rc; - - memset(specs, 0, sizeof(specs)); - - for (i = 0; i < nentries; i++) - { - specs[i].bbox = (box_t) BOX_POS_SIZE(LAUNCHER_PAD, - LAUNCHER_PAD + i * LAUNCHER_ROW_HEIGHT, - LAUNCHER_WIDTH - LAUNCHER_PAD * 2, - LAUNCHER_ROW_HEIGHT - 2); - specs[i].type = wuss_ICON_TYPE_BUTTON; - specs[i].text = entries[i].name; - specs[i].fg = palette_PICO8_BLACK; - specs[i].bg = palette_PICO8_LIGHT_GREY; - } - - rc = wuss_icon_create_array(task->window, specs, nentries, task->icons); - if (rc != result_OK) - { - wuss_window_close(task->window); - task->window = NULL; - return rc; - } - - return result_OK; -} - -void launcher_destroy(launcher_task_t *task) -{ - wuss_window_close(task->window); -} - -static result_t launcher_icon(launcher_task_t *lc, const wuss_icon_t *icon) -{ - int i; - - for (i = 0; i < lc->nentries; i++) - if (lc->icons[i] == icon) - return lc->entries[i].spawn(); - - return result_OK; -} - -result_t launcher_handle(wuss_window_t *window, - const wuss_event_t *event, - void *task_data) -{ - NOT_USED(window); - - switch (event->kind) - { - case wuss_EVENT_ICON: - if (event->data.icon.action != wuss_MOUSE_DOWN) - return result_OK; - return launcher_icon(task_data, event->data.icon.icon); - - default: - return result_OK; - } -} - -#endif /* USE_SDL */ diff --git a/libraries/wuss/test/tasks/launcher.h b/libraries/wuss/test/tasks/launcher.h deleted file mode 100644 index 8dd2b5cf..00000000 --- a/libraries/wuss/test/tasks/launcher.h +++ /dev/null @@ -1,49 +0,0 @@ -/* wuss/test/tasks/launcher.h -- clickable list of names that spawn other tasks */ - -#ifndef TASKS_LAUNCHER_H -#define TASKS_LAUNCHER_H - -#ifdef USE_SDL - -#include <stdbool.h> - -#include "wuss/icon.h" -#include "wuss/window.h" - -typedef result_t (*launcher_spawn_fn_t)(void); - -/* one clickable row */ -typedef struct launcher_entry -{ - const char *name; - launcher_spawn_fn_t spawn; -} -launcher_entry_t; - -/* each row is a work-area button icon; clicking one spawns a fresh instance - * of its task, so a row may be clicked any number of times */ -#define LAUNCHER_MAX_ENTRIES 32 - -typedef struct launcher_task -{ - const launcher_entry_t *entries; /* owned by the caller, must outlive the window */ - int nentries; - wuss_icon_t *icons[LAUNCHER_MAX_ENTRIES]; - wuss_window_t *window; -} -launcher_task_t; - -wuss_event_fn_t launcher_handle; - -/* create a window of button icons, one per entry; each click on a button - * calls its spawn function */ -result_t launcher_create(wuss_t *wuss, - const launcher_entry_t *entries, - int nentries, - launcher_task_t *task); - -void launcher_destroy(launcher_task_t *task); - -#endif /* USE_SDL */ - -#endif /* TASKS_LAUNCHER_H */ From 9d817271e1afd2d218656d110c3804f3e3427fa5 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Tue, 1 Sep 2026 19:39:33 +0100 Subject: [PATCH 150/287] refactor(wuss): validate icon type with a switch not a chain of != Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- libraries/wuss/icon/create.c | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/libraries/wuss/icon/create.c b/libraries/wuss/icon/create.c index c45c8cbc..9393061e 100644 --- a/libraries/wuss/icon/create.c +++ b/libraries/wuss/icon/create.c @@ -26,16 +26,22 @@ result_t wuss_icon_create(wuss_window_t *window, w = window->wuss; - if (spec->type != wuss_ICON_TYPE_LABEL && - spec->type != wuss_ICON_TYPE_BUTTON && - spec->type != wuss_ICON_TYPE_PATTERN && - spec->type != wuss_ICON_TYPE_FRAME && - spec->type != wuss_ICON_TYPE_RADIO && - spec->type != wuss_ICON_TYPE_OPTION && - spec->type != wuss_ICON_TYPE_BITMAP && - spec->type != wuss_ICON_TYPE_MENU_ENTRY && - spec->type != wuss_ICON_TYPE_RULE) + switch (spec->type) + { + case wuss_ICON_TYPE_LABEL: + case wuss_ICON_TYPE_BUTTON: + case wuss_ICON_TYPE_PATTERN: + case wuss_ICON_TYPE_FRAME: + case wuss_ICON_TYPE_RADIO: + case wuss_ICON_TYPE_OPTION: + case wuss_ICON_TYPE_BITMAP: + case wuss_ICON_TYPE_MENU_ENTRY: + case wuss_ICON_TYPE_RULE: + break; + + default: return result_WUSS_BAD_ICON; + } if ((spec->type == wuss_ICON_TYPE_BUTTON || spec->type == wuss_ICON_TYPE_PATTERN) && From 8cff9e48a57f3cc05d05404b783fcb3ad9005368 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Tue, 1 Sep 2026 19:39:33 +0100 Subject: [PATCH 151/287] refactor(wuss): pass NULL for the unused width out-param of bmfont_get_info Drops the fw local and its NOT_USED in wuss__menu_spawn. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- libraries/wuss/menu/menu.c | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/libraries/wuss/menu/menu.c b/libraries/wuss/menu/menu.c index 9cd73ef6..2e2619eb 100644 --- a/libraries/wuss/menu/menu.c +++ b/libraries/wuss/menu/menu.c @@ -25,10 +25,10 @@ /* Row padding above/below the glyph, and the gutters left for the tick (left) * and the submenu arrow (right). All in pixels. */ -#define WUSS_MENU_ROW_PAD 3 -#define WUSS_MENU_TICK_W 14 -#define WUSS_MENU_ARROW_W 14 -#define WUSS_MENU_TEXT_PAD 6 +#define WUSS_MENU_ROW_PAD 4 +#define WUSS_MENU_TICK_W 14 +#define WUSS_MENU_ARROW_W 14 +#define WUSS_MENU_TEXT_PAD 6 #define WUSS_MENU_SUBMENU_OVERLAP 2 /* px a submenu overlaps its parent's right edge */ /* ----------------------------------------------------------------------- */ @@ -176,7 +176,7 @@ static result_t wuss__menu_spawn(wuss_t *wuss, wuss_icon_t **made; wuss_task_t task; result_t rc; - int fw, fh; + int fh; int pitch; int sep_h; int y; @@ -204,8 +204,7 @@ static result_t wuss__menu_spawn(wuss_t *wuss, | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE; - bmfont_get_info(wuss->font, &fw, &fh); - NOT_USED(fw); + bmfont_get_info(wuss->font, NULL, &fh); pitch = fh + 2 * WUSS_MENU_ROW_PAD; sep_h = 2 * WUSS_MENU_ROW_PAD; From de2058841d2949c3929d9ff7aa5953a722495e05 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Tue, 1 Sep 2026 19:39:33 +0100 Subject: [PATCH 152/287] feat(wuss): add a 'Quit Wuss' task-menu entry that shuts the app down Also space out the menu-table declarations one blank line apart. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- apps/wuss/main.c | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/apps/wuss/main.c b/apps/wuss/main.c index a55c743d..a8be5490 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -53,6 +53,7 @@ static const colour_t *g_palette; static int g_npalette; static const char *g_resources; static bmfont_t *g_daydream_font; +static bool g_quit; /* set by the "Quit Wuss" task-menu entry */ /* Each spawn allocates a fresh per-instance task block so a task may run in * several windows at once; the block is owned by its window and freed by the @@ -212,6 +213,7 @@ static const wuss_menu_item_t g_menu_export_items[] = { "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) @@ -225,6 +227,7 @@ static const wuss_menu_item_t g_menu_items[] = { "Export", wuss_MENU_ITEM_NONE, &g_menu_export }, { "Quit", wuss_MENU_ITEM_DASHED, NULL } }; + static const wuss_menu_t g_menu = { "Display", g_menu_items, NELEMS(g_menu_items) @@ -259,6 +262,7 @@ static const wuss_menu_item_t g_menu_desc_export_items[] = { "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) @@ -303,14 +307,24 @@ static const wuss_menu_item_t g_task_items[] = { "Icons", wuss_MENU_ITEM_NONE, NULL }, { "Porter-Duff", wuss_MENU_ITEM_NONE, NULL }, { "Menu", wuss_MENU_ITEM_DASHED, NULL }, - { "Menu (desc)", wuss_MENU_ITEM_NONE, NULL } + { "Menu (desc)", wuss_MENU_ITEM_NONE, NULL }, + { "Quit Wuss", wuss_MENU_ITEM_DASHED, NULL } }; + +static result_t spawn_quit(void) +{ + g_quit = true; + return result_OK; +} + static const task_spawn_fn_t g_task_spawn[] = { spawn_ball, spawn_text, spawn_blank, spawn_chars, spawn_palette, spawn_image, spawn_checker, spawn_curve, spawn_lissajous, spawn_sofa, - spawn_gradient, spawn_icons, spawn_porter_duff, spawn_menu, spawn_menu_desc + spawn_gradient, spawn_icons, spawn_porter_duff, spawn_menu, spawn_menu_desc, + spawn_quit }; + static const wuss_menu_t g_task_menu = { "Tasks", g_task_items, NELEMS(g_task_items) @@ -495,12 +509,13 @@ static result_t run_wuss(const char *resources) g_daydream_font = daydream_font; quit = false; + g_quit = false; garbage_pending = false; pixel_stress_pending = false; wuss_redraw(wuss); - while (!quit) + while (!quit && !g_quit) { SDL_Event event; From 1b94b14320d752f5e23e26b13fac35b8792f1e92 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Tue, 1 Sep 2026 21:12:18 +0100 Subject: [PATCH 153/287] fix(wuss): close menu chain before invoking on_select The MOUSE_UP handler in wuss__menu_handle called the on_select callback while the menu node was still live, then continued to touch `self`. A callback that opens a new menu (via wuss_menu_open) frees the whole chain, including the node the dispatcher was running inside, causing a heap-use-after-free on return. Capture the callback, menu and ctx up front, tear the chain down first, then invoke the callback. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- libraries/wuss/menu/menu.c | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/libraries/wuss/menu/menu.c b/libraries/wuss/menu/menu.c index 2e2619eb..81c187e9 100644 --- a/libraries/wuss/menu/menu.c +++ b/libraries/wuss/menu/menu.c @@ -134,26 +134,36 @@ static result_t wuss__menu_handle(wuss_window_t *window, if (event->data.icon.action == wuss_MOUSE_UP) { - wuss_button_t button; - struct wuss__menu *root; + wuss_button_t button; + struct wuss__menu *root; + wuss_menu_select_fn_t *on_select; + const wuss_menu_t *menu; + void *ctx; button = event->data.icon.button; if (item->submenu != NULL) return result_OK; /* a submenu row opens on hover, it is not a pick */ - if (self->on_select != NULL) - self->on_select(self->menu, index, button, self->ctx); + /* Capture what the callback needs, then tear the chain down *before* + * invoking it: on_select may itself open a new menu (freeing this one), + * so `self` must not be touched afterwards. */ + on_select = self->on_select; + menu = self->menu; + ctx = self->ctx; - if (button & wuss_BUTTON_ADJUST) - return result_OK; /* ADJUST keeps the chain open */ + if (!(button & wuss_BUTTON_ADJUST)) + { + root = self; + while (root->parent != NULL) + root = root->parent; - root = self; - while (root->parent != NULL) - root = root->parent; + root->wuss->menu_chain = NULL; + wuss__menu_close_from(root); + } - root->wuss->menu_chain = NULL; - wuss__menu_close_from(root); + if (on_select != NULL) + on_select(menu, index, button, ctx); } return result_OK; From dc90dc7a59e07d5f9633eed5cf19b14bc35716d7 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Tue, 1 Sep 2026 21:12:50 +0100 Subject: [PATCH 154/287] style(wuss): drop horizontal padding from menu rule lines Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- 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 c4bdcf6a..cc48f56f 100644 --- a/libraries/wuss/icon/draw.c +++ b/libraries/wuss/icon/draw.c @@ -417,12 +417,12 @@ static void wuss__icon_draw_menu_entry(const icon_draw_ctx_t *c) static void wuss__icon_draw_rule(const icon_draw_ctx_t *c) { const box_t *b = &c->b; - int pad, mid_y; + int mid_y; - pad = 4; mid_y = b->y0 + (b->y1 - b->y0) / 2; - screen_draw_dashed_line(c->scr, b->x0 + pad, mid_y, - b->x1 - 1 - pad, mid_y, 2, 2, c->fg); + screen_draw_dashed_line(c->scr, + b->x0, mid_y, b->x1 - 1, mid_y, + 2, 2, c->fg); } /* ----------------------------------------------------------------------- */ From 0686a5041fb5619e824c05e824d0d01607c5c7ff Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Tue, 1 Sep 2026 21:23:45 +0100 Subject: [PATCH 155/287] refactor(wuss): bunch g_* runtime statics into a single struct The six mutable file-scope statics that carry per-run context to the argument-less spawn callbacks are now fields of one anonymous struct g rather than loose g_wuss/g_palette/... globals. The const menu tables are left as they were. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- apps/wuss/main.c | 68 +++++++++++++++++++++++++----------------------- 1 file changed, 36 insertions(+), 32 deletions(-) diff --git a/apps/wuss/main.c b/apps/wuss/main.c index a8be5490..ba8cba22 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -47,13 +47,17 @@ /* the launcher's spawn callbacks take no arguments, so the pieces they need * are stashed here instead; run_wuss runs at most once per - * process, so file-scope statics are as good as a context struct */ -static wuss_t *g_wuss; -static const colour_t *g_palette; -static int g_npalette; -static const char *g_resources; -static bmfont_t *g_daydream_font; -static bool g_quit; /* set by the "Quit Wuss" task-menu entry */ + * process, so a file-scope struct is as good as a passed-around context */ +static struct +{ + wuss_t *wuss; + const colour_t *palette; + int npalette; + const char *resources; + bmfont_t *daydream_font; + bool quit; /* set by the "Quit Wuss" task-menu entry */ +} +g; /* Each spawn allocates a fresh per-instance task block so a task may run in * several windows at once; the block is owned by its window and freed by the @@ -66,7 +70,7 @@ static result_t spawn_ball(void) ball_task_t *t = calloc(1, sizeof(*t)); result_t rc; if (t == NULL) return result_OOM; - rc = ball_create(g_wuss, g_palette, t); + rc = ball_create(g.wuss, g.palette, t); if (rc != result_OK || t->window == NULL) { free(t); return rc; } return result_OK; } @@ -76,7 +80,7 @@ static result_t spawn_text(void) text_task_t *t = calloc(1, sizeof(*t)); result_t rc; if (t == NULL) return result_OOM; - rc = text_create(g_wuss, g_palette, g_daydream_font, t); + rc = text_create(g.wuss, g.palette, g.daydream_font, t); if (rc != result_OK || t->window == NULL) { free(t); return rc; } return result_OK; } @@ -86,7 +90,7 @@ static result_t spawn_blank(void) blank_task_t *t = calloc(1, sizeof(*t)); result_t rc; if (t == NULL) return result_OOM; - rc = blank_create(g_wuss, g_npalette, t); + rc = blank_create(g.wuss, g.npalette, t); if (rc != result_OK || t->window == NULL) { free(t); return rc; } return result_OK; } @@ -96,7 +100,7 @@ static result_t spawn_chars(void) chars_task_t *t = calloc(1, sizeof(*t)); result_t rc; if (t == NULL) return result_OOM; - rc = chars_create(g_wuss, g_palette, t); + rc = chars_create(g.wuss, g.palette, t); if (rc != result_OK || t->window == NULL) { free(t); return rc; } return result_OK; } @@ -106,7 +110,7 @@ static result_t spawn_palette(void) palette_task_t *t = calloc(1, sizeof(*t)); result_t rc; if (t == NULL) return result_OOM; - rc = palette_create(g_wuss, g_palette, g_npalette, t); + rc = palette_create(g.wuss, g.palette, g.npalette, t); if (rc != result_OK || t->window == NULL) { free(t); return rc; } return result_OK; } @@ -124,12 +128,12 @@ static result_t spawn_image(void) if (t == NULL) return result_OOM; leafname = path_join_leafname("jessica", "png"); - filename = path_join_filename(g_resources, 3, "resources", "images", leafname); + filename = path_join_filename(g.resources, 3, "resources", "images", leafname); strcpy(buf, filename); - ninepatch = path_join_filename(g_resources, 3, "resources", "wuss", + ninepatch = path_join_filename(g.resources, 3, "resources", "wuss", path_join_leafname("9tile", "png")); - rc = image_create(g_wuss, g_palette, buf, ninepatch, t); + rc = image_create(g.wuss, g.palette, buf, ninepatch, t); if (rc != result_OK || t->window == NULL) { free(t); return rc; } return result_OK; } @@ -139,7 +143,7 @@ static result_t spawn_checker(void) checker_task_t *t = calloc(1, sizeof(*t)); result_t rc; if (t == NULL) return result_OOM; - rc = checker_create(g_wuss, g_palette, t); + rc = checker_create(g.wuss, g.palette, t); if (rc != result_OK || t->window == NULL) { free(t); return rc; } return result_OK; } @@ -149,7 +153,7 @@ static result_t spawn_curve(void) curve_task_t *t = calloc(1, sizeof(*t)); result_t rc; if (t == NULL) return result_OOM; - rc = curve_create(g_wuss, g_palette, t); + rc = curve_create(g.wuss, g.palette, t); if (rc != result_OK || t->window == NULL) { free(t); return rc; } return result_OK; } @@ -159,7 +163,7 @@ static result_t spawn_lissajous(void) lissajous_task_t *t = calloc(1, sizeof(*t)); result_t rc; if (t == NULL) return result_OOM; - rc = lissajous_create(g_wuss, g_palette, t); + rc = lissajous_create(g.wuss, g.palette, t); if (rc != result_OK || t->window == NULL) { free(t); return rc; } return result_OK; } @@ -169,7 +173,7 @@ static result_t spawn_sofa(void) sofa_task_t *t = calloc(1, sizeof(*t)); result_t rc; if (t == NULL) return result_OOM; - rc = sofa_create(g_wuss, g_palette, t); + rc = sofa_create(g.wuss, g.palette, t); if (rc != result_OK || t->window == NULL) { free(t); return rc; } return result_OK; } @@ -179,7 +183,7 @@ static result_t spawn_gradient(void) gradient_task_t *t = calloc(1, sizeof(*t)); result_t rc; if (t == NULL) return result_OOM; - rc = gradient_create(g_wuss, t); + rc = gradient_create(g.wuss, t); if (rc != result_OK || t->window == NULL) { free(t); return rc; } return result_OK; } @@ -189,7 +193,7 @@ static result_t spawn_icons(void) icons_task_t *t = calloc(1, sizeof(*t)); result_t rc; if (t == NULL) return result_OOM; - rc = icons_create(g_wuss, g_palette, g_daydream_font, g_resources, t); + rc = icons_create(g.wuss, g.palette, g.daydream_font, g.resources, t); if (rc != result_OK || t->window == NULL) { free(t); return rc; } return result_OK; } @@ -199,7 +203,7 @@ static result_t spawn_porter_duff(void) porter_duff_task_t *t = calloc(1, sizeof(*t)); result_t rc; if (t == NULL) return result_OOM; - rc = porter_duff_create(g_wuss, g_palette, g_daydream_font, g_resources, t); + rc = porter_duff_create(g.wuss, g.palette, g.daydream_font, g.resources, t); if (rc != result_OK || t->window == NULL) { free(t); return rc; } return result_OK; } @@ -246,7 +250,7 @@ static void menu_selected(const wuss_menu_t *menu, static result_t spawn_menu(void) { - return wuss_menu_open(g_wuss, &g_menu, wuss_get_pointer(g_wuss), + return wuss_menu_open(g.wuss, &g_menu, wuss_get_pointer(g.wuss), menu_selected, NULL, NULL); } @@ -282,7 +286,7 @@ static result_t spawn_menu_desc(void) wuss_menu_destroy(g_menu_desc); g_menu_desc = m; - return wuss_menu_open(g_wuss, g_menu_desc, wuss_get_pointer(g_wuss), + return wuss_menu_open(g.wuss, g_menu_desc, wuss_get_pointer(g.wuss), menu_selected, NULL, NULL); } @@ -313,7 +317,7 @@ static const wuss_menu_item_t g_task_items[] = static result_t spawn_quit(void) { - g_quit = true; + g.quit = true; return result_OK; } @@ -502,20 +506,20 @@ static result_t run_wuss(const char *resources) goto Failure; } - g_wuss = wuss; - g_palette = palette; - g_npalette = NELEMS(palette); - g_resources = resources; - g_daydream_font = daydream_font; + g.wuss = wuss; + g.palette = palette; + g.npalette = NELEMS(palette); + g.resources = resources; + g.daydream_font = daydream_font; quit = false; - g_quit = false; + g.quit = false; garbage_pending = false; pixel_stress_pending = false; wuss_redraw(wuss); - while (!quit && !g_quit) + while (!quit && !g.quit) { SDL_Event event; From cd4fd216aefba588c350015dd32cf35b8714f718 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Tue, 1 Sep 2026 21:29:18 +0100 Subject: [PATCH 156/287] fix(wuss): don't wheel-scroll windows that declared an axis non-scrollable wuss__scroll_step now zeroes the delta on any axis whose window carries wuss_WINDOW_NO_HSCROLL / wuss_WINDOW_NO_VSCROLL. Pop-up menus set both and have no scrollbar to clamp against; their geometry (submenu placement, hit testing) assumes scroll == 0, so a wheel turn over a menu taller than the screen was corrupting the display. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- libraries/wuss/scroll-step.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/libraries/wuss/scroll-step.c b/libraries/wuss/scroll-step.c index ab01d5f3..5267b006 100644 --- a/libraries/wuss/scroll-step.c +++ b/libraries/wuss/scroll-step.c @@ -32,6 +32,16 @@ void wuss__scroll_step(wuss_window_t *window, point_t delta) { point_t scroll; + /* A window that declared an axis non-scrollable (e.g. a pop-up menu) has no + * scrollbar to clamp against and its geometry assumes scroll == 0, so a wheel + * turn over it must not move the content -- doing so corrupts the display. */ + if (window->flags & wuss_WINDOW_NO_HSCROLL) + delta.x = 0; + if (window->flags & wuss_WINDOW_NO_VSCROLL) + delta.y = 0; + if (delta.x == 0 && delta.y == 0) + return; + wuss_window_get_scroll(window, &scroll); scroll.x += delta.x; scroll.y += delta.y; From ab858641ca23a7e04ee59351455185c1800bd68e Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Tue, 1 Sep 2026 21:37:40 +0100 Subject: [PATCH 157/287] feat(wuss): scroll over-tall pop-up menus instead of cropping them When a menu's full height exceeds what the screen leaves for it, cap the window's content height at that maximum and clear wuss_WINDOW_NO_VSCROLL so the window gets a real vertical scrollbar. The document extent passed to wuss_window_create stays the full menu height, so the existing scroll-aware icon draw and hit test move the rows under the viewport with no further work. Submenu placement now subtracts the parent menu's scroll offset when siting a child, and the on-screen nudge accounts for the scrollbar carve on the right edge. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- libraries/wuss/menu/menu.c | 71 ++++++++++++++++++++++++++++---------- 1 file changed, 52 insertions(+), 19 deletions(-) diff --git a/libraries/wuss/menu/menu.c b/libraries/wuss/menu/menu.c index 81c187e9..ea59c7b7 100644 --- a/libraries/wuss/menu/menu.c +++ b/libraries/wuss/menu/menu.c @@ -119,11 +119,17 @@ static result_t wuss__menu_handle(wuss_window_t *window, wuss_window_get_content_bounds(self->window, &wb); wuss_icon_get_bbox(icon, &ib); - /* wb is the content box in screen space; the menu is unscrolled, so a - * row's document y is its offset from the content top. The child's own - * titlebar sits above `at`, so a submenu row lines up with its parent. */ - at.x = wb.x1 - WUSS_MENU_SUBMENU_OVERLAP; - at.y = wb.y0 + ib.y0; + /* wb is the content box in screen space; ib is in document space, so the + * row's screen y is its document y less however far the menu is scrolled. + * The child's own titlebar sits above `at`, so a submenu row lines up with + * its parent. */ + { + point_t scroll; + + wuss_window_get_scroll(self->window, &scroll); + at.x = wb.x1 - WUSS_MENU_SUBMENU_OVERLAP; + at.y = wb.y0 + ib.y0 - scroll.y; + } if (wuss__menu_spawn(self->wuss, item->submenu, at, self, self->on_select, self->ctx, &self->child) == result_OK) @@ -192,14 +198,18 @@ static result_t wuss__menu_spawn(wuss_t *wuss, int y; int widest; int width, height; + int doc_h; + int max_h; int i; int ndashed; int nspecs; int s; int outline_px; int titlebar_height; + point_t carve; wuss_window_flags_t menu_flags; - size2d_t size; + size2d_t doc; + size2d_t min_doc; box_t content; assert(wuss != NULL); @@ -214,6 +224,9 @@ static result_t wuss__menu_spawn(wuss_t *wuss, | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE; + outline_px = wuss__outline_px_for(menu_flags); + titlebar_height = wuss__titlebar_height_for(wuss, menu_flags); + bmfont_get_info(wuss->font, NULL, &fh); pitch = fh + 2 * WUSS_MENU_ROW_PAD; sep_h = 2 * WUSS_MENU_ROW_PAD; @@ -243,19 +256,36 @@ static result_t wuss__menu_spawn(wuss_t *wuss, width = WUSS_MENU_TICK_W + widest + WUSS_MENU_TEXT_PAD + WUSS_MENU_ARROW_W; - /* every item is a full row now; a dashed item also gets a sep_h rule above */ - height = menu->nitems * pitch + ndashed * sep_h; + /* every item is a full row now; a dashed item also gets a sep_h rule above. + * doc_h is the whole menu; `height` is what the window actually shows. When + * the menu is taller than the space the screen leaves for it, cap `height` + * and give the window a real vertical scrollbar -- the icon draw and hit + * test already honour window->scroll, so the rows just move under it. */ + doc_h = menu->nitems * pitch + ndashed * sep_h; + + max_h = wuss->scr->size.h - 2 * outline_px - titlebar_height; + + if (doc_h > max_h) + { + height = max_h; + menu_flags &= (wuss_window_flags_t) ~wuss_WINDOW_NO_VSCROLL; + } + else + { + height = doc_h; + } + + wuss__furniture_carve_for(menu_flags, wuss__button_size_for(wuss, menu_flags), + &carve); /* Nudge onto the screen where it can be, but keep the top-left on screen. * `at` is the content top-left; the window's visible box also spans the - * outline on all four sides and the titlebar above, so clamp against that - * extent -- otherwise wuss_window_create's own on-screen size clamp trims - * the content and the last row (e.g. a trailing "Quit") is cropped. */ - outline_px = wuss__outline_px_for(menu_flags); - titlebar_height = wuss__titlebar_height_for(wuss, menu_flags); - - if (at.x + width + outline_px > wuss->scr->size.w) - at.x = wuss->scr->size.w - width - outline_px; + * outline on all four sides, the titlebar above and (for a scrolling menu) + * the scrollbar carve on the right, so clamp against that whole extent -- + * otherwise wuss_window_create's own on-screen size clamp trims the content + * and the last row (e.g. a trailing "Quit") is cropped. */ + if (at.x + width + outline_px + carve.x > wuss->scr->size.w) + at.x = wuss->scr->size.w - width - outline_px - carve.x; if (at.y + height + outline_px > wuss->scr->size.h) at.y = wuss->scr->size.h - height - outline_px; if (at.x - outline_px < 0) @@ -338,8 +368,11 @@ static result_t wuss__menu_spawn(wuss_t *wuss, y += pitch; } - size = SIZE2D(width, height); - task = wuss_task_start(wuss__menu_handle, node); + /* doc spans the whole menu so the window can scroll it; min_doc is what the + * window shows, so its own resize floor never exceeds what fits. */ + doc = SIZE2D(width, doc_h); + min_doc = SIZE2D(width, height); + task = wuss_task_start(wuss__menu_handle, node); /* `at` is the content top-left, already clamped on screen above. Create the * window there directly -- creating it elsewhere and wuss_window_move-ing it @@ -354,7 +387,7 @@ static result_t wuss__menu_spawn(wuss_t *wuss, menu->title ? menu->title : "", menu_flags, wuss_BACKDROP_COLOUR(wuss->white), - &task, size, size, &node->window); + &task, doc, min_doc, &node->window); if (rc != result_OK) { wuss__free(wuss, made); From 631af4c6977908fae9d30fb763c4a038deb978e3 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Tue, 1 Sep 2026 21:48:06 +0100 Subject: [PATCH 158/287] refactor(wuss): split pattern swatches out of icons task The icons task baked a swatch-per-fill-pattern grid into its icon-spec array alongside its labels, buttons, radios and menu entries. Move that grid into a standalone "swatches" task and drop it from icons, so each demonstrates one thing. Adds it to the interactive demo's task menu and CMake source list. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- CMakeLists.txt | 1 + apps/wuss/main.c | 16 ++++- libraries/wuss/test/tasks/icons.c | 35 ++-------- libraries/wuss/test/tasks/swatches.c | 101 +++++++++++++++++++++++++++ libraries/wuss/test/tasks/swatches.h | 27 +++++++ 5 files changed, 150 insertions(+), 30 deletions(-) create mode 100644 libraries/wuss/test/tasks/swatches.c create mode 100644 libraries/wuss/test/tasks/swatches.h diff --git a/CMakeLists.txt b/CMakeLists.txt index bd70cd46..845d523c 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -681,6 +681,7 @@ if(BUILD_APPS) libraries/wuss/test/tasks/palette.c libraries/wuss/test/tasks/porter-duff.c libraries/wuss/test/tasks/sofa.c + libraries/wuss/test/tasks/swatches.c libraries/wuss/test/tasks/text.c) source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" FILES ${WUSS_APP_SOURCES}) diff --git a/apps/wuss/main.c b/apps/wuss/main.c index ba8cba22..c23e698f 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -36,6 +36,7 @@ #include "tasks/palette.h" #include "tasks/porter-duff.h" #include "tasks/sofa.h" +#include "tasks/swatches.h" #include "tasks/text.h" /* ----------------------------------------------------------------------- */ @@ -198,6 +199,16 @@ static result_t spawn_icons(void) return result_OK; } +static result_t spawn_swatches(void) +{ + swatches_task_t *t = calloc(1, sizeof(*t)); + result_t rc; + if (t == NULL) return result_OOM; + rc = swatches_create(g.wuss, t); + if (rc != result_OK || t->window == NULL) { free(t); return rc; } + return result_OK; +} + static result_t spawn_porter_duff(void) { porter_duff_task_t *t = calloc(1, sizeof(*t)); @@ -309,6 +320,7 @@ static const wuss_menu_item_t g_task_items[] = { "Sofa", wuss_MENU_ITEM_NONE, NULL }, { "Gradient", wuss_MENU_ITEM_NONE, NULL }, { "Icons", wuss_MENU_ITEM_NONE, NULL }, + { "Swatches", wuss_MENU_ITEM_NONE, NULL }, { "Porter-Duff", wuss_MENU_ITEM_NONE, NULL }, { "Menu", wuss_MENU_ITEM_DASHED, NULL }, { "Menu (desc)", wuss_MENU_ITEM_NONE, NULL }, @@ -325,8 +337,8 @@ static const task_spawn_fn_t g_task_spawn[] = { spawn_ball, spawn_text, spawn_blank, spawn_chars, spawn_palette, spawn_image, spawn_checker, spawn_curve, spawn_lissajous, spawn_sofa, - spawn_gradient, spawn_icons, spawn_porter_duff, spawn_menu, spawn_menu_desc, - spawn_quit + spawn_gradient, spawn_icons, spawn_swatches, spawn_porter_duff, spawn_menu, + spawn_menu_desc, spawn_quit }; static const wuss_menu_t g_task_menu = diff --git a/libraries/wuss/test/tasks/icons.c b/libraries/wuss/test/tasks/icons.c index f8cb6c06..4adbdb38 100644 --- a/libraries/wuss/test/tasks/icons.c +++ b/libraries/wuss/test/tasks/icons.c @@ -31,17 +31,16 @@ result_t icons_create(wuss_t *wuss, icons_task_t *task) { /* [0..3] heading, counter button, counter label and a scrolled-away button, - * then [.. +N] one swatch per built-in fill pattern, then [.. +3] a grouping - * frame with two differently-justified labels inside it, then [.. +5] three - * grouped radios, a standalone option and a label echoing the selection, - * then [.. +2] a decorative bitmap icon and an interactive one that bumps - * the counter, then [.. +4] a strip of menu-entry icons that hover-highlight */ - enum { ICONS_NSPECS = 4 + screen_PATTERN__LIMIT + 3 + 5 + 2 + 5 }; + * then [.. +3] a grouping frame with two differently-justified labels inside + * it, then [.. +5] three grouped radios, a standalone option and a label + * echoing the selection, then [.. +2] a decorative bitmap icon and an + * interactive one that bumps the counter, then [.. +4] a strip of menu-entry + * icons that hover-highlight */ + enum { ICONS_NSPECS = 4 + 3 + 5 + 2 + 5 }; wuss_task_t delegate; wuss_icon_spec_t specs[ICONS_NSPECS]; wuss_icon_t *made[ICONS_NSPECS]; const char *sprite_path; - int p; int g; /* index of the first frame spec */ int r; /* radio index */ int m; /* index of the first menu-entry spec */ @@ -120,29 +119,9 @@ result_t icons_create(wuss_t *wuss, specs[3].fg = palette_PICO8_BLACK; specs[3].bg = palette_PICO8_LIGHT_GREY; - /* [4..] one 16x16 swatch per built-in fill pattern, laid out as a grid down - * the document so it scrolls through the window and stays phase-locked while - * doing so */ - for (p = 0; p < screen_PATTERN__LIMIT; p++) - { - enum { ICONS_SWATCH_COLS = 6, ICONS_SWATCH_PITCH = 20 }; - int col, row; - - col = p % ICONS_SWATCH_COLS; - row = p / ICONS_SWATCH_COLS; - - specs[4 + p].bbox = (box_t) BOX_POS_SIZE(28 + col * ICONS_SWATCH_PITCH, - 90 + row * ICONS_SWATCH_PITCH, - 16, 16); - specs[4 + p].type = wuss_ICON_TYPE_PATTERN; - specs[4 + p].fg = palette_PICO8_DARK_BLUE; - specs[4 + p].bg = palette_PICO8_LIGHT_GREY; - specs[4 + p].pattern = (screen_pattern_t) p; - } - /* [g] a grouping frame down the document, with [g+1] a right-justified and * [g+2] a centred label sat inside it */ - g = 4 + screen_PATTERN__LIMIT; + g = 4; specs[g].bbox = (box_t) BOX_POS_SIZE(28, 300, 170, 70); specs[g].type = wuss_ICON_TYPE_FRAME; diff --git a/libraries/wuss/test/tasks/swatches.c b/libraries/wuss/test/tasks/swatches.c new file mode 100644 index 00000000..0092d176 --- /dev/null +++ b/libraries/wuss/test/tasks/swatches.c @@ -0,0 +1,101 @@ +/* wuss/test/tasks/swatches.c -- fill-pattern swatch grid task */ + +#ifdef USE_SDL + +#include <stdlib.h> +#include <string.h> + +#ifdef FORTIFY +#include "fortify/fortify.h" +#endif + +#include "framebuf/palettes.h" +#include "framebuf/screen.h" +#include "geom/box.h" +#include "geom/size.h" +#include "wuss/icon.h" + +#include "swatches.h" + +#define SWATCHES_DOC_W 160 +#define SWATCHES_DOC_H 320 /* taller than the window, so scrolling is exercised */ + +result_t swatches_create(wuss_t *wuss, swatches_task_t *task) +{ + enum { SWATCHES_NSPECS = 1 + screen_PATTERN__LIMIT }; + wuss_task_t delegate; + wuss_icon_spec_t specs[SWATCHES_NSPECS]; + int p; + result_t rc; + + task->window = NULL; + + delegate = wuss_task_start(swatches_handle, task); + + rc = wuss_window_create_placed(wuss, + SIZE2D(SWATCHES_DOC_W, 140), + "Swatches", + wuss_WINDOW_NONE, + wuss_BACKDROP_COLOUR(palette_PICO8_LIGHT_GREY), + &delegate, + SIZE2D(SWATCHES_DOC_W, SWATCHES_DOC_H), + SIZE2D(0, 0), + &task->window); + if (rc != result_OK) + return rc; + + memset(specs, 0, sizeof(specs)); + + /* [0] a heading label */ + specs[0].bbox = (box_t) BOX_POS_SIZE(12, 12, 140, 14); + specs[0].type = wuss_ICON_TYPE_LABEL; + specs[0].text = "Fill patterns:"; + specs[0].fg = palette_PICO8_DARK_BLUE; + specs[0].bg = wuss_NO_BACKGROUND; + + /* [1..] one 16x16 swatch per built-in fill pattern, laid out as a grid down + * the document so it scrolls through the window and stays phase-locked while + * doing so */ + for (p = 0; p < screen_PATTERN__LIMIT; p++) + { + enum { SWATCHES_COLS = 6, SWATCHES_PITCH = 20 }; + int col, row; + + col = p % SWATCHES_COLS; + row = p / SWATCHES_COLS; + + specs[1 + p].bbox = (box_t) BOX_POS_SIZE(12 + col * SWATCHES_PITCH, + 34 + row * SWATCHES_PITCH, + 16, 16); + specs[1 + p].type = wuss_ICON_TYPE_PATTERN; + specs[1 + p].fg = palette_PICO8_DARK_BLUE; + specs[1 + p].bg = palette_PICO8_LIGHT_GREY; + specs[1 + p].pattern = (screen_pattern_t) p; + } + + rc = wuss_icon_create_array(task->window, specs, SWATCHES_NSPECS, NULL); + if (rc != result_OK) + { + wuss_window_close(task->window); + task->window = NULL; + return rc; + } + + return result_OK; +} + +result_t swatches_handle(wuss_window_t *window, + const wuss_event_t *event, + void *task_data) +{ + if (event->kind == wuss_EVENT_CLOSE) + { + wuss_window_close(window); + free(task_data); /* calloc'd per instance by the spawner */ + return result_OK; + } + + return result_OK; +} + +#endif /* USE_SDL */ diff --git a/libraries/wuss/test/tasks/swatches.h b/libraries/wuss/test/tasks/swatches.h new file mode 100644 index 00000000..c30d7a50 --- /dev/null +++ b/libraries/wuss/test/tasks/swatches.h @@ -0,0 +1,27 @@ +/* wuss/test/tasks/swatches.h -- fill-pattern swatch grid task */ + +#ifndef TASKS_SWATCHES_H +#define TASKS_SWATCHES_H + +#ifdef USE_SDL + +#include "wuss/window.h" + +/* one 16x16 PATTERN icon per built-in screen fill pattern, laid out as a + * grid down a document taller than the window so the swatches scroll + * through it and stay phase-locked while doing so */ +typedef struct swatches_task +{ + wuss_window_t *window; +} +swatches_task_t; + +wuss_event_fn_t swatches_handle; + +/* create the swatches window against the given wuss instance */ +result_t swatches_create(wuss_t *wuss, swatches_task_t *task); + + +#endif /* USE_SDL */ + +#endif /* TASKS_SWATCHES_H */ From 3d437ff33d6ae3eed6e8da1a7271f60604b7858d Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Tue, 1 Sep 2026 23:41:16 +0100 Subject: [PATCH 159/287] feat(wuss): add wuss_nearest_colour Find the system-palette entry closest to a given RGB by squared Euclidean distance, ties to the lower index. Also renames the config's furniture-colour struct wuss_palette_t -> wuss_furniture_palette_t and narrows wuss_colour_t to unsigned char. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- CMakeLists.txt | 1 + include/wuss/wuss.h | 21 +++++++++++++---- libraries/wuss/nearest-colour.c | 40 +++++++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 4 deletions(-) create mode 100644 libraries/wuss/nearest-colour.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 845d523c..3e044bbb 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -343,6 +343,7 @@ set(WUSS_CORE_SOURCES libraries/wuss/invalidate.c libraries/wuss/mouse-click.c libraries/wuss/mouse-move.c + libraries/wuss/nearest-colour.c libraries/wuss/redraw.c libraries/wuss/scroll.c libraries/wuss/scroll-step.c diff --git a/include/wuss/wuss.h b/include/wuss/wuss.h index 358b3f83..c1b9c8bb 100644 --- a/include/wuss/wuss.h +++ b/include/wuss/wuss.h @@ -98,7 +98,7 @@ wuss_mouse_action_t; /** * An index into a wuss_t's system palette (see wuss_create). Not a colour_t. */ -typedef int wuss_colour_t; +typedef unsigned char wuss_colour_t; /** * Sentinel for wuss_window_create's bg meaning "no automatic background @@ -111,7 +111,7 @@ typedef int wuss_colour_t; * flat colour. Ignored when the library is built with WUSS_FURNITURE off. * Each value is an index into the system palette (see * wuss_create). */ -typedef struct wuss_palette +typedef struct wuss_furniture_palette { struct { @@ -131,7 +131,7 @@ typedef struct wuss_palette } scroll; } -wuss_palette_t; +wuss_furniture_palette_t; /** * Per-window appearance flags, combinable with bitwise OR. @@ -255,7 +255,7 @@ typedef struct wuss_config int titlebar_height; /** Furniture chrome colours. Ignored when WUSS_FURNITURE is off. */ - wuss_palette_t palette; + wuss_furniture_palette_t furniture; /** * Bevelled work-area button edge shades, as indices into the system @@ -345,6 +345,19 @@ bmfont_t *wuss_get_font(const wuss_t *wuss); */ point_t wuss_get_pointer(const wuss_t *wuss); +/** + * Find the system palette entry (see wuss_create) closest to an RGB value, + * by squared Euclidean distance in RGB space. Alpha is ignored. Ties keep + * the lower index. + * + * \param[in] wuss Window manager. + * \param[in] r Red component, 0..255. + * \param[in] g Green component, 0..255. + * \param[in] b Blue component, 0..255. + * \return Palette index, 0..npalette-1. + */ +wuss_colour_t wuss_nearest_colour(const wuss_t *wuss, int r, int g, int b); + /** * Redraw every window, back-to-front, having first painted the configured * backdrop colour (see wuss_config_t::backdrop) behind them, if any. diff --git a/libraries/wuss/nearest-colour.c b/libraries/wuss/nearest-colour.c new file mode 100644 index 00000000..39fe3800 --- /dev/null +++ b/libraries/wuss/nearest-colour.c @@ -0,0 +1,40 @@ +/* wuss/nearest-colour.c -- wuss - minimal window manager */ + +#include <assert.h> + +#include "framebuf/pixelfmt.h" + +#include "impl.h" + +wuss_colour_t wuss_nearest_colour(const wuss_t *wuss, int r, int g, int b) +{ + unsigned long best_d; + wuss_colour_t best_i; + int i; + + assert(wuss != NULL); + assert(wuss->npalette > 0); + + best_d = ~0UL; + best_i = 0; + for (i = 0; i < wuss->npalette; i++) + { + unsigned int px; + long dr, dg, db; + unsigned long d; + + /* rgba8888 is 0xAABBGGRR (see pixelfmt.h). */ + px = wuss->palette[i].primary; + dr = r - (long) ( px & 0xFF); + dg = g - (long) ((px >> 8) & 0xFF); + db = b - (long) ((px >> 16) & 0xFF); + d = (unsigned long) (dr * dr + dg * dg + db * db); + if (d < best_d) + { + best_d = d; + best_i = i; + } + } + + return best_i; +} From bdcfd3615f1fb909bf79e0c57d135e8c8c5c964a Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Tue, 1 Sep 2026 23:41:23 +0100 Subject: [PATCH 160/287] fix(wuss): draw menu text in the nearest-black palette entry Menu item and rule specs hardcoded fg = palette index 0, which is only black in the default palette; a custom palette with white at index 0 gave white-on-white menu text. Use a new palettecache.black (nearest to RGB 0,0,0), alongside the existing white cache now moved into the same sub-struct. The white-cache loop in create.c is replaced by wuss_nearest_colour, which also fixes its swapped R/B channel read. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- libraries/wuss/create.c | 35 ++++++--------------------------- libraries/wuss/impl.h | 9 +++++++-- libraries/wuss/menu/menu.c | 6 +++--- libraries/wuss/test/wuss-test.c | 18 ++++++++--------- 4 files changed, 25 insertions(+), 43 deletions(-) diff --git a/libraries/wuss/create.c b/libraries/wuss/create.c index 027e3ec6..1675d3bd 100644 --- a/libraries/wuss/create.c +++ b/libraries/wuss/create.c @@ -56,7 +56,7 @@ result_t wuss_create(screen_t *scr, wuss_alloc_t al; wuss_t *w; #ifdef WUSS_FURNITURE - wuss_palette_t pal; + wuss_furniture_palette_t pal; wuss_colour_t bg, fg; #endif #if defined(WUSS_FURNITURE) || defined(WUSS_ICONS) @@ -98,33 +98,10 @@ result_t wuss_create(screen_t *scr, memcpy(w->palette, palette, npalette * sizeof(*w->palette)); w->npalette = npalette; - /* Cache the palette index nearest to opaque white, for menu backdrops and - * anything else wanting "paper". Sum-of-squared-channel-distance to white; - * ties keep the first. */ - { - unsigned long best_d; - int i; - - w->white = 0; - best_d = ~0UL; - for (i = 0; i < npalette; i++) - { - unsigned int px; - long dr, dg, db; - unsigned long d; - - px = w->palette[i].primary; - dr = 255 - (long) ((px >> 16) & 0xFF); - dg = 255 - (long) ((px >> 8) & 0xFF); - db = 255 - (long) ( px & 0xFF); - d = (unsigned long) (dr * dr + dg * dg + db * db); - if (d < best_d) - { - best_d = d; - w->white = i; - } - } - } + /* Cache the palette indices nearest to opaque white and black, for menu + * backdrops/text and anything else wanting "paper" or "ink". */ + w->palettecache.white = wuss_nearest_colour(w, 255, 255, 255); + w->palettecache.black = wuss_nearest_colour(w, 0, 0, 0); if (config != NULL) { @@ -140,7 +117,7 @@ result_t wuss_create(screen_t *scr, #ifdef WUSS_FURNITURE if (config != NULL) { - pal = config->palette; + pal = config->furniture; blight = config->bevel.light; bdark = config->bevel.dark; abg = config->accent.bg; diff --git a/libraries/wuss/impl.h b/libraries/wuss/impl.h index 6e3bccb6..0c3355d4 100644 --- a/libraries/wuss/impl.h +++ b/libraries/wuss/impl.h @@ -66,9 +66,14 @@ struct wuss * block this wuss_t owns */ colour_t *palette; /* owned */ int npalette; - wuss_colour_t white; /* palette index nearest to white */ + struct + { + wuss_colour_t white; /* palette index nearest to white */ + wuss_colour_t black; /* palette index nearest to black */ + } + palettecache; #ifdef WUSS_FURNITURE - wuss_palette_t furniture_colours; + 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 */ diff --git a/libraries/wuss/menu/menu.c b/libraries/wuss/menu/menu.c index ea59c7b7..c4b66447 100644 --- a/libraries/wuss/menu/menu.c +++ b/libraries/wuss/menu/menu.c @@ -342,7 +342,7 @@ static result_t wuss__menu_spawn(wuss_t *wuss, specs[s].bbox.x1 = width; specs[s].bbox.y1 = y + sep_h; specs[s].text = ""; - specs[s].fg = 0; + specs[s].fg = wuss->palettecache.black; specs[s].bg = wuss_NO_BACKGROUND; specs[s].flags = wuss_ICON_FLAGS_NONE; s++; @@ -360,7 +360,7 @@ static result_t wuss__menu_spawn(wuss_t *wuss, specs[s].bbox.x1 = width; specs[s].bbox.y1 = y + pitch; specs[s].text = item->text ? item->text : ""; - specs[s].fg = 0; + specs[s].fg = wuss->palettecache.black; specs[s].bg = wuss_NO_BACKGROUND; specs[s].flags = flags; s++; @@ -386,7 +386,7 @@ static result_t wuss__menu_spawn(wuss_t *wuss, rc = wuss_window_create(wuss, &content, menu->title ? menu->title : "", menu_flags, - wuss_BACKDROP_COLOUR(wuss->white), + wuss_BACKDROP_COLOUR(wuss->palettecache.white), &task, doc, min_doc, &node->window); if (rc != result_OK) { diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index addd00ef..29781ebf 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -212,15 +212,15 @@ result_t wuss_test(const char *resources) printf("test: wuss_create with bad titlebar colour index\n"); bad_config.titlebar_height = 0; - bad_config.palette.title.bg = 999; - bad_config.palette.title.fg = 0; - bad_config.palette.back = 0; - bad_config.palette.close = 0; - bad_config.palette.toggle = 0; - bad_config.palette.resize = 0; - bad_config.palette.scroll.arrows = 0; - bad_config.palette.scroll.wells = 0; - bad_config.palette.scroll.sausages = 0; + bad_config.furniture.title.bg = 999; + bad_config.furniture.title.fg = 0; + bad_config.furniture.back = 0; + bad_config.furniture.close = 0; + bad_config.furniture.toggle = 0; + bad_config.furniture.resize = 0; + bad_config.furniture.scroll.arrows = 0; + bad_config.furniture.scroll.wells = 0; + bad_config.furniture.scroll.sausages = 0; rc = wuss_create(&scr, NULL, NULL, 0, &bad_config, NULL, &bad_wuss); if (rc != result_WUSS_BAD_COLOUR) goto Failure; From da8633d75cbaa0f9b914a3ff614db7d8a26a3f08 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Tue, 1 Sep 2026 23:41:27 +0100 Subject: [PATCH 161/287] refactor(wuss): common out ball invalidation box Three copies of the ball -> window-local invalidation box calc collapse into ball_local_box(); the two click sites pass a degenerate box, idle passes the swept MIN/MAX range. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- libraries/wuss/test/tasks/ball.c | 53 ++++++++++++++++++++------------ 1 file changed, 33 insertions(+), 20 deletions(-) diff --git a/libraries/wuss/test/tasks/ball.c b/libraries/wuss/test/tasks/ball.c index 9923b12e..29352abe 100644 --- a/libraries/wuss/test/tasks/ball.c +++ b/libraries/wuss/test/tasks/ball.c @@ -14,14 +14,31 @@ #include "ball.h" -result_t ball_create(wuss_t *wuss, - const colour_t *palette, - ball_task_t *task) +/* Window-local invalidation box covering a ball (or its swept range) whose + * centre spans [vx0,vx1] x [vy0,vy1] in virtual content space. */ +static box_t ball_local_box(int vx0, + int vy0, + int vx1, + int vy1, + int radius, + point_t scroll) +{ + box_t local; + + local.x0 = vx0 - radius - scroll.x; + local.y0 = vy0 - radius - scroll.y; + local.x1 = vx1 + radius - scroll.x; + local.y1 = vy1 + radius - scroll.y; + + return local; +} + +result_t ball_create(wuss_t *wuss, ball_task_t *task) { wuss_task_t delegate; - task->bg = palette[palette_PICO8_RED]; - task->ball = palette[palette_PICO8_WHITE]; + task->bg = colour_rgb(0xFF, 0x00, 0x00); + task->ball = colour_rgb(0xFF, 0xFF, 0xFF); task->nballs = 1; task->balls[0].x = 50; @@ -43,13 +60,13 @@ result_t ball_create(wuss_t *wuss, &task->window); } - static result_t ball_redraw(const wuss_event_t *event, void *task_data) { ball_task_t *bc; screen_t *scr; const box_t *content, *bounds; - int i, sx, sy; + int sx, sy; + int i; bc = task_data; @@ -68,7 +85,10 @@ static result_t ball_redraw(const wuss_event_t *event, void *task_data) b = &bc->balls[i]; - screen_fill_rect(scr, bounds->x0 - sx + b->x - b->radius, bounds->y0 - sy + b->y - b->radius, SIZE2D(b->radius * 2, b->radius * 2), bc->ball); + screen_fill_rect(scr, bounds->x0 - sx + b->x - b->radius, + bounds->y0 - sy + b->y - b->radius, + SIZE2D(b->radius * 2, b->radius * 2), + bc->ball); } return result_OK; @@ -109,10 +129,7 @@ static result_t ball_mouse(wuss_window_t *window, b->dy = (bc->nballs & 2) ? 2 : -2; b->radius = 8; - local.x0 = b->x - b->radius - scroll.x; - local.y0 = b->y - b->radius - scroll.y; - local.x1 = b->x + b->radius - scroll.x; - local.y1 = b->y + b->radius - scroll.y; + local = ball_local_box(b->x, b->y, b->x, b->y, b->radius, scroll); wuss_window_invalidate(bc->window, &local); } else if (button & wuss_BUTTON_ADJUST) @@ -124,10 +141,7 @@ static result_t ball_mouse(wuss_window_t *window, b = &bc->balls[--bc->nballs]; - local.x0 = b->x - b->radius - scroll.x; - local.y0 = b->y - b->radius - scroll.y; - local.x1 = b->x + b->radius - scroll.x; - local.y1 = b->y + b->radius - scroll.y; + local = ball_local_box(b->x, b->y, b->x, b->y, b->radius, scroll); wuss_window_invalidate(bc->window, &local); } @@ -168,10 +182,9 @@ static result_t ball_idle(void *task_data) if (b->y - b->radius < scroll.y) { b->y = scroll.y + b->radius; b->dy = -b->dy; } else if (b->y + b->radius > scroll.y + height){ b->y = scroll.y + height - b->radius; b->dy = -b->dy; } - local.x0 = MIN(old_x, b->x) - b->radius - scroll.x; - local.y0 = MIN(old_y, b->y) - b->radius - scroll.y; - local.x1 = MAX(old_x, b->x) + b->radius - scroll.x; - local.y1 = MAX(old_y, b->y) + b->radius - scroll.y; + local = ball_local_box(MIN(old_x, b->x), MIN(old_y, b->y), + MAX(old_x, b->x), MAX(old_y, b->y), + b->radius, scroll); wuss_window_invalidate(bc->window, &local); } From accb16092fb9e8a1b27977c774919b7024d5e3d5 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Tue, 1 Sep 2026 23:42:53 +0100 Subject: [PATCH 162/287] refactor(wuss): drop palette args from test-task constructors The SDL test tasks took palette/npalette just to index a PICO8 lookup for two or three fixed colours; use colour_rgb() literals directly and simplify each *_create signature, with apps/wuss/main.c updated to match. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- apps/wuss/main.c | 81 +++++++++++++++++---------- libraries/wuss/test/tasks/ball.h | 4 +- libraries/wuss/test/tasks/blank.c | 10 ++-- libraries/wuss/test/tasks/blank.h | 2 +- libraries/wuss/test/tasks/chars.c | 8 +-- libraries/wuss/test/tasks/chars.h | 11 ++-- libraries/wuss/test/tasks/checker.c | 8 +-- libraries/wuss/test/tasks/checker.h | 5 +- libraries/wuss/test/tasks/curve.c | 10 ++-- libraries/wuss/test/tasks/curve.h | 5 +- libraries/wuss/test/tasks/icons.c | 56 +++++++++--------- libraries/wuss/test/tasks/icons.h | 10 ++-- libraries/wuss/test/tasks/image.c | 9 ++- libraries/wuss/test/tasks/image.h | 10 ++-- libraries/wuss/test/tasks/lissajous.c | 8 +-- libraries/wuss/test/tasks/lissajous.h | 4 +- libraries/wuss/test/tasks/sofa.c | 10 ++-- libraries/wuss/test/tasks/sofa.h | 5 +- libraries/wuss/test/tasks/swatches.c | 12 ++-- libraries/wuss/test/tasks/text.c | 13 ++--- libraries/wuss/test/tasks/text.h | 2 - 21 files changed, 140 insertions(+), 143 deletions(-) diff --git a/apps/wuss/main.c b/apps/wuss/main.c index c23e698f..11ad04aa 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -71,7 +71,7 @@ static result_t spawn_ball(void) ball_task_t *t = calloc(1, sizeof(*t)); result_t rc; if (t == NULL) return result_OOM; - rc = ball_create(g.wuss, g.palette, t); + rc = ball_create(g.wuss, t); if (rc != result_OK || t->window == NULL) { free(t); return rc; } return result_OK; } @@ -81,7 +81,7 @@ static result_t spawn_text(void) text_task_t *t = calloc(1, sizeof(*t)); result_t rc; if (t == NULL) return result_OOM; - rc = text_create(g.wuss, g.palette, g.daydream_font, t); + rc = text_create(g.wuss, g.daydream_font, t); if (rc != result_OK || t->window == NULL) { free(t); return rc; } return result_OK; } @@ -91,7 +91,7 @@ static result_t spawn_blank(void) blank_task_t *t = calloc(1, sizeof(*t)); result_t rc; if (t == NULL) return result_OOM; - rc = blank_create(g.wuss, g.npalette, t); + rc = blank_create(g.wuss, t); if (rc != result_OK || t->window == NULL) { free(t); return rc; } return result_OK; } @@ -101,7 +101,7 @@ static result_t spawn_chars(void) chars_task_t *t = calloc(1, sizeof(*t)); result_t rc; if (t == NULL) return result_OOM; - rc = chars_create(g.wuss, g.palette, t); + rc = chars_create(g.wuss, t); if (rc != result_OK || t->window == NULL) { free(t); return rc; } return result_OK; } @@ -134,7 +134,7 @@ static result_t spawn_image(void) ninepatch = path_join_filename(g.resources, 3, "resources", "wuss", path_join_leafname("9tile", "png")); - rc = image_create(g.wuss, g.palette, buf, ninepatch, t); + rc = image_create(g.wuss, buf, ninepatch, t); if (rc != result_OK || t->window == NULL) { free(t); return rc; } return result_OK; } @@ -144,7 +144,7 @@ static result_t spawn_checker(void) checker_task_t *t = calloc(1, sizeof(*t)); result_t rc; if (t == NULL) return result_OOM; - rc = checker_create(g.wuss, g.palette, t); + rc = checker_create(g.wuss, t); if (rc != result_OK || t->window == NULL) { free(t); return rc; } return result_OK; } @@ -154,7 +154,7 @@ static result_t spawn_curve(void) curve_task_t *t = calloc(1, sizeof(*t)); result_t rc; if (t == NULL) return result_OOM; - rc = curve_create(g.wuss, g.palette, t); + rc = curve_create(g.wuss, t); if (rc != result_OK || t->window == NULL) { free(t); return rc; } return result_OK; } @@ -164,7 +164,7 @@ static result_t spawn_lissajous(void) lissajous_task_t *t = calloc(1, sizeof(*t)); result_t rc; if (t == NULL) return result_OOM; - rc = lissajous_create(g.wuss, g.palette, t); + rc = lissajous_create(g.wuss, t); if (rc != result_OK || t->window == NULL) { free(t); return rc; } return result_OK; } @@ -174,7 +174,7 @@ static result_t spawn_sofa(void) sofa_task_t *t = calloc(1, sizeof(*t)); result_t rc; if (t == NULL) return result_OOM; - rc = sofa_create(g.wuss, g.palette, t); + rc = sofa_create(g.wuss, t); if (rc != result_OK || t->window == NULL) { free(t); return rc; } return result_OK; } @@ -194,7 +194,7 @@ static result_t spawn_icons(void) icons_task_t *t = calloc(1, sizeof(*t)); result_t rc; if (t == NULL) return result_OOM; - rc = icons_create(g.wuss, g.palette, g.daydream_font, g.resources, t); + rc = icons_create(g.wuss, g.daydream_font, g.resources, t); if (rc != result_OK || t->window == NULL) { free(t); return rc; } return result_OK; } @@ -421,11 +421,14 @@ static result_t run_wuss(const char *resources) bool garbage_pending; bool pixel_stress_pending; int i; + bool use_wimp16; { - const char *palette_name = getenv("WUSS_PALETTE"); /* "riscos16" selects the RISC OS 16-colour palette; default is PICO-8 */ + /* "riscos16" selects the RISC OS 16-colour palette; default is PICO-8 */ + const char *palette_name = getenv("WUSS_PALETTE"); - if (palette_name != NULL && strcmp(palette_name, "wimp16") == 0) + use_wimp16 = (palette_name != NULL && strcmp(palette_name, "wimp16") == 0); + if (use_wimp16) define_wimp16_palette(palette); else define_pico8_palette(palette); @@ -494,23 +497,43 @@ static result_t run_wuss(const char *resources) { wuss_config_t config; - config.titlebar_height = 0; - config.palette.title.bg = palette_PICO8_DARK_BLUE; - config.palette.title.fg = palette_PICO8_WHITE; - config.palette.back = palette_PICO8_GREEN; - config.palette.close = palette_PICO8_RED; - config.palette.toggle = palette_PICO8_ORANGE; - config.palette.resize = palette_PICO8_LAVENDER; - config.palette.scroll.arrows = palette_PICO8_BLUE; - config.palette.scroll.wells = palette_PICO8_DARK_BLUE; - config.palette.scroll.sausages = palette_PICO8_LIGHT_GREY; - config.bevel.light = palette_PICO8_WHITE; - config.bevel.dark = palette_PICO8_DARK_GREY; - config.accent.bg = palette_PICO8_DARK_BLUE; - config.accent.fg = palette_PICO8_WHITE; - config.backdrop.colour = palette_PICO8_WHITE; - config.backdrop.pattern = screen_PATTERN_DOTS; - config.backdrop.pattern_bg = palette_PICO8_LIGHT_GREY; + if (!use_wimp16) { + config.titlebar_height = 0; + config.furniture.title.bg = palette_PICO8_DARK_BLUE; + config.furniture.title.fg = palette_PICO8_WHITE; + 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.accent.bg = palette_PICO8_DARK_BLUE; + config.accent.fg = palette_PICO8_WHITE; + config.backdrop.colour = palette_PICO8_WHITE; + config.backdrop.pattern = screen_PATTERN_DOTS; + config.backdrop.pattern_bg = palette_PICO8_LIGHT_GREY; + } else { + config.titlebar_height = 0; + config.furniture.title.bg = palette_WIMP16_GREY_75; + config.furniture.title.fg = 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.accent.bg = palette_WIMP16_ORANGE; + config.accent.fg = palette_WIMP16_BLACK; + config.backdrop.colour = palette_WIMP16_GREY_50; + config.backdrop.pattern = screen_PATTERN_DOTS; + config.backdrop.pattern_bg = palette_WIMP16_GREY_37; + } rc = wuss_create(&scr, font, palette, NELEMS(palette), &config, NULL, &wuss); diff --git a/libraries/wuss/test/tasks/ball.h b/libraries/wuss/test/tasks/ball.h index 5e54df59..ae373f25 100644 --- a/libraries/wuss/test/tasks/ball.h +++ b/libraries/wuss/test/tasks/ball.h @@ -36,9 +36,7 @@ wuss_event_fn_t ball_handle; /* create the bouncing-ball window against the given wuss instance; "task" is a * per-instance block owned by the window and freed when it closes */ -result_t ball_create(wuss_t *wuss, - const colour_t *palette, - ball_task_t *task); +result_t ball_create(wuss_t *wuss, ball_task_t *task); #endif /* USE_SDL */ diff --git a/libraries/wuss/test/tasks/blank.c b/libraries/wuss/test/tasks/blank.c index b97f67aa..55d651e5 100644 --- a/libraries/wuss/test/tasks/blank.c +++ b/libraries/wuss/test/tasks/blank.c @@ -17,12 +17,12 @@ #define BLANK_CYCLE_FRAMES 30 /* colour advances every half-second at 60fps */ -result_t blank_create(wuss_t *wuss, int npalette, blank_task_t *task) +result_t blank_create(wuss_t *wuss, blank_task_t *task) { wuss_task_t delegate; - task->npalette = npalette; - task->index = palette_PICO8_GREEN; + task->npalette = 16; // TODO: Read max palette index from wuss + task->index = 0; task->frame_count = 0; delegate = wuss_task_start(blank_handle, task); /* wuss fills the content area itself */ @@ -31,7 +31,7 @@ result_t blank_create(wuss_t *wuss, int npalette, blank_task_t *task) SIZE2D(200, 160), NULL, wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE, - wuss_BACKDROP_COLOUR(palette_PICO8_GREEN), + wuss_BACKDROP_COLOUR(task->index), &delegate, SIZE2D(200, 160), SIZE2D(0, 0), @@ -52,7 +52,7 @@ static result_t blank_idle(void *task_data) bc->index = (bc->index + 1) % bc->npalette; rc = wuss_window_set_background(bc->window, - wuss_BACKDROP_COLOUR(bc->index)); + wuss_BACKDROP_COLOUR(bc->index)); if (rc != result_OK) logf_warning("blank_idle: wuss_window_set_background(%d) failed", bc->index); diff --git a/libraries/wuss/test/tasks/blank.h b/libraries/wuss/test/tasks/blank.h index 44739617..3a42f270 100644 --- a/libraries/wuss/test/tasks/blank.h +++ b/libraries/wuss/test/tasks/blank.h @@ -23,7 +23,7 @@ blank_task_t; wuss_event_fn_t blank_handle; /* create the colour-cycling blank window against the given wuss instance */ -result_t blank_create(wuss_t *wuss, int npalette, blank_task_t *task); +result_t blank_create(wuss_t *wuss, blank_task_t *task); #endif /* USE_SDL */ diff --git a/libraries/wuss/test/tasks/chars.c b/libraries/wuss/test/tasks/chars.c index 5b12ea80..cf98bece 100644 --- a/libraries/wuss/test/tasks/chars.c +++ b/libraries/wuss/test/tasks/chars.c @@ -21,9 +21,7 @@ #define CHARS_ROWS 8 #define CHARS_PAD 2 -result_t chars_create(wuss_t *wuss, - const colour_t *palette, - chars_task_t *task) +result_t chars_create(wuss_t *wuss, chars_task_t *task) { wuss_task_t delegate; size2d_t sz; @@ -42,8 +40,8 @@ result_t chars_create(wuss_t *wuss, cell_h = font_height + CHARS_PAD * 2; task->font = font; - task->fg = palette[palette_PICO8_BLACK]; - task->bg = palette[palette_PICO8_WHITE]; + task->fg = colour_rgb(0x00, 0x00, 0x00); + task->bg = colour_rgb(0xFF, 0xFF, 0xFF); delegate = wuss_task_start(chars_handle, task); /* chars_redraw paints every cell itself */ sz = SIZE2D(cell_w * CHARS_COLS, cell_h * CHARS_ROWS); diff --git a/libraries/wuss/test/tasks/chars.h b/libraries/wuss/test/tasks/chars.h index 277b6691..d43fa125 100644 --- a/libraries/wuss/test/tasks/chars.h +++ b/libraries/wuss/test/tasks/chars.h @@ -13,9 +13,9 @@ * so the whole font can be eyeballed at a glance */ typedef struct chars_task { - wuss_window_t *window; - bmfont_t *font; - colour_t fg, bg; + wuss_window_t *window; + bmfont_t *font; + colour_t fg, bg; } chars_task_t; @@ -23,10 +23,7 @@ wuss_event_fn_t chars_handle; /* create the glyph-grid window against the given wuss instance; does * nothing and returns result_OK if wuss has no system font */ -result_t chars_create(wuss_t *wuss, - const colour_t *palette, - chars_task_t *task); - +result_t chars_create(wuss_t *wuss, chars_task_t *task); #endif /* USE_SDL */ diff --git a/libraries/wuss/test/tasks/checker.c b/libraries/wuss/test/tasks/checker.c index f8163a3d..7fc5c957 100644 --- a/libraries/wuss/test/tasks/checker.c +++ b/libraries/wuss/test/tasks/checker.c @@ -18,15 +18,13 @@ #define CHECKER_BAND_MIN 1 #define CHECKER_BAND_MAX 32 -result_t checker_create(wuss_t *wuss, - const colour_t *palette, - checker_task_t *task) +result_t checker_create(wuss_t*wuss, checker_task_t *task) { wuss_task_t delegate; result_t rc; - task->black = palette[palette_PICO8_BLACK]; - task->white = palette[palette_PICO8_WHITE]; + task->black = colour_rgb(0x00, 0x00, 0x00); + task->white = colour_rgb(0xFF, 0xFF, 0xFF); task->pattern = checker_PATTERN_CHECKERBOARD; task->pattern2 = checker_PATTERN_VERTICAL; task->band = CHECKER_BAND_DEFAULT; diff --git a/libraries/wuss/test/tasks/checker.h b/libraries/wuss/test/tasks/checker.h index 07937138..9983ecc3 100644 --- a/libraries/wuss/test/tasks/checker.h +++ b/libraries/wuss/test/tasks/checker.h @@ -33,10 +33,7 @@ checker_task_t; wuss_event_fn_t checker_handle; /* create the two checkerboard windows against the given wuss instance */ -result_t checker_create(wuss_t *wuss, - const colour_t *palette, - checker_task_t *task); - +result_t checker_create(wuss_t *wuss, checker_task_t *task); #endif /* USE_SDL */ diff --git a/libraries/wuss/test/tasks/curve.c b/libraries/wuss/test/tasks/curve.c index d99b430b..0f91fb05 100644 --- a/libraries/wuss/test/tasks/curve.c +++ b/libraries/wuss/test/tasks/curve.c @@ -23,15 +23,13 @@ #define CURVE_SEGMENTS_MIN 4 #define CURVE_SEGMENTS_MAX 128 -result_t curve_create(wuss_t *wuss, - const colour_t *palette, - curve_task_t *task) +result_t curve_create(wuss_t *wuss, curve_task_t *task) { wuss_task_t delegate; - task->bg = palette[palette_PICO8_WHITE]; - task->line = palette[palette_PICO8_BLACK]; - task->blob = palette[palette_PICO8_RED]; + task->bg = colour_rgb(0xFF, 0xFF, 0xFF); + task->line = colour_rgb(0x00, 0x00, 0x00); + task->blob = colour_rgb(0xFF, 0x00, 0x00); task->nsegments = CURVE_SEGMENTS_DEFAULT; task->dragging = -1; diff --git a/libraries/wuss/test/tasks/curve.h b/libraries/wuss/test/tasks/curve.h index 3d3ad505..59ce2516 100644 --- a/libraries/wuss/test/tasks/curve.h +++ b/libraries/wuss/test/tasks/curve.h @@ -26,10 +26,7 @@ curve_task_t; wuss_event_fn_t curve_handle; /* create the curve window against the given wuss instance */ -result_t curve_create(wuss_t *wuss, - const colour_t *palette, - curve_task_t *task); - +result_t curve_create(wuss_t *wuss, curve_task_t *task); #endif /* USE_SDL */ diff --git a/libraries/wuss/test/tasks/icons.c b/libraries/wuss/test/tasks/icons.c index 4adbdb38..e60703a0 100644 --- a/libraries/wuss/test/tasks/icons.c +++ b/libraries/wuss/test/tasks/icons.c @@ -24,11 +24,10 @@ #define ICONS_DOC_W 220 #define ICONS_DOC_H 640 /* taller than the window, so scrolling is exercised */ -result_t icons_create(wuss_t *wuss, - const colour_t *palette, - bmfont_t *font, - const char *resources, - icons_task_t *task) +result_t icons_create(wuss_t *wuss, + bmfont_t *font, + const char *resources, + icons_task_t *task) { /* [0..3] heading, counter button, counter label and a scrolled-away button, * then [.. +3] a grouping frame with two differently-justified labels inside @@ -38,6 +37,7 @@ result_t icons_create(wuss_t *wuss, * icons that hover-highlight */ enum { ICONS_NSPECS = 4 + 3 + 5 + 2 + 5 }; wuss_task_t delegate; + wuss_colour_t black, grey5, grey6; wuss_icon_spec_t specs[ICONS_NSPECS]; wuss_icon_t *made[ICONS_NSPECS]; const char *sprite_path; @@ -48,8 +48,8 @@ result_t icons_create(wuss_t *wuss, result_t rc; task->font = font; - task->label = palette[palette_PICO8_DARK_BLUE]; - task->paper = palette[palette_PICO8_LIGHT_GREY]; /* the window bg, below */ + task->label = colour_rgb(0x00, 0x00, 0x00); + task->paper = colour_rgb(0xDD, 0xDD, 0xDD); /* the window bg, below */ task->window = NULL; task->button = NULL; task->counter = NULL; @@ -65,14 +65,18 @@ result_t icons_create(wuss_t *wuss, task->has_sprite = 1; delegate = wuss_task_start(icons_handle, task); + + black = wuss_nearest_colour(wuss, 0x00, 0x00, 0x00); + grey5 = wuss_nearest_colour(wuss, 0xBB, 0xBB, 0xBB); + grey6 = wuss_nearest_colour(wuss, 0xDD, 0xDD, 0xDD); rc = wuss_window_create_placed(wuss, SIZE2D(ICONS_DOC_W, 160), "Icons", wuss_WINDOW_NONE, - wuss_BACKDROP_PATTERN(palette_PICO8_LAVENDER, + wuss_BACKDROP_PATTERN(grey5, screen_PATTERN_CROSSHATCH, - palette_PICO8_LIGHT_GREY), + grey6), &delegate, SIZE2D(ICONS_DOC_W, ICONS_DOC_H), SIZE2D(0, 0), @@ -93,7 +97,7 @@ result_t icons_create(wuss_t *wuss, specs[0].bbox = (box_t) BOX_POS_SIZE(28, 28, 180, 14); specs[0].type = wuss_ICON_TYPE_LABEL; specs[0].text = "Work-area icons:"; - specs[0].fg = palette_PICO8_DARK_BLUE; + specs[0].fg = black; specs[0].bg = wuss_NO_BACKGROUND; /* [1] the button that bumps the counter -- a default action button, so it @@ -101,23 +105,23 @@ result_t icons_create(wuss_t *wuss, specs[1].bbox = (box_t) BOX_POS_SIZE(28, 50, 80, 22); specs[1].type = wuss_ICON_TYPE_BUTTON; specs[1].text = "Press me"; - specs[1].fg = palette_PICO8_BLACK; - specs[1].bg = palette_PICO8_LIGHT_GREY; + specs[1].fg = black; + specs[1].bg = grey6; specs[1].flags = wuss_ICON_FLAGS_DEFAULT; /* [2] the counter label beside it */ specs[2].bbox = (box_t) BOX_POS_SIZE(120, 50, 120, 22); specs[2].type = wuss_ICON_TYPE_LABEL; specs[2].text = "0"; - specs[2].fg = palette_PICO8_DARK_BLUE; + specs[2].fg = black; specs[2].bg = wuss_NO_BACKGROUND; /* [3] a button far down the document, to prove icons scroll and stay clickable */ specs[3].bbox = (box_t) BOX_POS_SIZE(28, 460, 90, 52); specs[3].type = wuss_ICON_TYPE_BUTTON; specs[3].text = "Scrolled"; - specs[3].fg = palette_PICO8_BLACK; - specs[3].bg = palette_PICO8_LIGHT_GREY; + specs[3].fg = black; + specs[3].bg = grey6; /* [g] a grouping frame down the document, with [g+1] a right-justified and * [g+2] a centred label sat inside it */ @@ -126,20 +130,20 @@ result_t icons_create(wuss_t *wuss, specs[g].bbox = (box_t) BOX_POS_SIZE(28, 300, 170, 70); specs[g].type = wuss_ICON_TYPE_FRAME; specs[g].text = "Grouping frame"; - specs[g].fg = palette_PICO8_DARK_BLUE; + specs[g].fg = black; specs[g].bg = wuss_NO_BACKGROUND; specs[g + 1].bbox = (box_t) BOX_POS_SIZE(38, 320, 150, 14); specs[g + 1].type = wuss_ICON_TYPE_LABEL; specs[g + 1].text = "right"; - specs[g + 1].fg = palette_PICO8_DARK_BLUE; + specs[g + 1].fg = black; specs[g + 1].bg = wuss_NO_BACKGROUND; specs[g + 1].flags = wuss_ICON_FLAGS_JUSTIFY_RIGHT; specs[g + 2].bbox = (box_t) BOX_POS_SIZE(38, 342, 150, 14); specs[g + 2].type = wuss_ICON_TYPE_LABEL; specs[g + 2].text = "centre"; - specs[g + 2].fg = palette_PICO8_DARK_BLUE; + specs[g + 2].fg = black; specs[g + 2].bg = wuss_NO_BACKGROUND; specs[g + 2].flags = wuss_ICON_FLAGS_JUSTIFY_CENTRE; @@ -150,7 +154,7 @@ result_t icons_create(wuss_t *wuss, specs[g + 3 + r].bbox = (box_t) BOX_POS_SIZE(28, 380 + r * 20, 150, 16); specs[g + 3 + r].type = wuss_ICON_TYPE_RADIO; specs[g + 3 + r].text = (r == 0) ? "Red" : (r == 1) ? "Green" : "Blue"; - specs[g + 3 + r].fg = palette_PICO8_DARK_BLUE; + specs[g + 3 + r].fg = black; specs[g + 3 + r].bg = wuss_NO_BACKGROUND; specs[g + 3 + r].group = 1; } @@ -158,13 +162,13 @@ result_t icons_create(wuss_t *wuss, specs[g + 6].bbox = (box_t) BOX_POS_SIZE(28, 444, 150, 16); specs[g + 6].type = wuss_ICON_TYPE_OPTION; specs[g + 6].text = "Wireframe"; - specs[g + 6].fg = palette_PICO8_DARK_BLUE; + specs[g + 6].fg = black; specs[g + 6].bg = wuss_NO_BACKGROUND; specs[g + 7].bbox = (box_t) BOX_POS_SIZE(28, 464, 180, 14); specs[g + 7].type = wuss_ICON_TYPE_LABEL; specs[g + 7].text = "(no selection)"; - specs[g + 7].fg = palette_PICO8_DARK_BLUE; + specs[g + 7].fg = black; specs[g + 7].bg = wuss_NO_BACKGROUND; nspecs = g + 8; @@ -195,31 +199,31 @@ result_t icons_create(wuss_t *wuss, specs[m].bbox = (box_t) BOX_POS_SIZE(28, 524, 160, 16); specs[m].type = wuss_ICON_TYPE_MENU_ENTRY; specs[m].text = "Open"; - specs[m].fg = palette_PICO8_DARK_BLUE; + specs[m].fg = black; specs[m].bg = wuss_NO_BACKGROUND; specs[m + 1].bbox = (box_t) BOX_POS_SIZE(28, 542, 160, 16); specs[m + 1].type = wuss_ICON_TYPE_MENU_ENTRY; specs[m + 1].text = "Show grid"; - specs[m + 1].fg = palette_PICO8_DARK_BLUE; + specs[m + 1].fg = black; specs[m + 1].bg = wuss_NO_BACKGROUND; specs[m + 2].bbox = (box_t) BOX_POS_SIZE(28, 560, 160, 16); specs[m + 2].type = wuss_ICON_TYPE_MENU_ENTRY; specs[m + 2].text = "Export"; - specs[m + 2].fg = palette_PICO8_DARK_BLUE; + specs[m + 2].fg = black; specs[m + 2].bg = wuss_NO_BACKGROUND; specs[m + 2].flags = wuss_ICON_FLAGS_SUBMENU; specs[m + 3].bbox = (box_t) BOX_POS_SIZE(28, 578, 160, 10); specs[m + 3].type = wuss_ICON_TYPE_RULE; - specs[m + 3].fg = palette_PICO8_DARK_BLUE; + specs[m + 3].fg = black; specs[m + 3].bg = wuss_NO_BACKGROUND; specs[m + 4].bbox = (box_t) BOX_POS_SIZE(28, 588, 160, 16); specs[m + 4].type = wuss_ICON_TYPE_MENU_ENTRY; specs[m + 4].text = "Quit"; - specs[m + 4].fg = palette_PICO8_DARK_BLUE; + specs[m + 4].fg = black; specs[m + 4].bg = wuss_NO_BACKGROUND; specs[m + 4].flags = wuss_ICON_FLAGS_SEPARATOR; diff --git a/libraries/wuss/test/tasks/icons.h b/libraries/wuss/test/tasks/icons.h index 98ec0744..9707ad61 100644 --- a/libraries/wuss/test/tasks/icons.h +++ b/libraries/wuss/test/tasks/icons.h @@ -34,12 +34,10 @@ icons_task_t; wuss_event_fn_t icons_handle; /* create the icons window against the given wuss instance */ -result_t icons_create(wuss_t *wuss, - const colour_t *palette, - bmfont_t *font, - const char *resources, - icons_task_t *task); - +result_t icons_create(wuss_t *wuss, + bmfont_t *font, + const char *resources, + icons_task_t *task); #endif /* USE_SDL */ diff --git a/libraries/wuss/test/tasks/image.c b/libraries/wuss/test/tasks/image.c index 16e159b2..46b2c3b0 100644 --- a/libraries/wuss/test/tasks/image.c +++ b/libraries/wuss/test/tasks/image.c @@ -16,11 +16,10 @@ #define BORDER 16 -result_t image_create(wuss_t *wuss, - const colour_t *palette, - const char *path, - const char *background_path, - image_task_t *task) +result_t image_create(wuss_t *wuss, + const char *path, + const char *background_path, + image_task_t *task) { wuss_task_t delegate; result_t rc; diff --git a/libraries/wuss/test/tasks/image.h b/libraries/wuss/test/tasks/image.h index 7b9ff9ce..8943564c 100644 --- a/libraries/wuss/test/tasks/image.h +++ b/libraries/wuss/test/tasks/image.h @@ -23,12 +23,10 @@ wuss_event_fn_t image_handle; /* load the PNG at path (and the 9-patch PNG at background_path, drawn tiled * behind it) and create its window against the given wuss instance */ -result_t image_create(wuss_t *wuss, - const colour_t *palette, - const char *path, - const char *background_path, - image_task_t *task); - +result_t image_create(wuss_t *wuss, + const char *path, + const char *background_path, + image_task_t *task); #endif /* USE_SDL */ diff --git a/libraries/wuss/test/tasks/lissajous.c b/libraries/wuss/test/tasks/lissajous.c index cae15c91..8c26a674 100644 --- a/libraries/wuss/test/tasks/lissajous.c +++ b/libraries/wuss/test/tasks/lissajous.c @@ -21,14 +21,12 @@ static const int lissajous_freqs[][2] = { 3, 2 }, { 5, 4 }, { 3, 4 }, { 5, 6 }, { 1, 2 }, { 7, 4 } }; -result_t lissajous_create(wuss_t *wuss, - const colour_t *palette, - lissajous_task_t *task) +result_t lissajous_create(wuss_t *wuss, lissajous_task_t *task) { wuss_task_t delegate; - task->bg = palette[palette_PICO8_DARK_BLUE]; - task->fg = palette[palette_PICO8_YELLOW]; + task->bg = colour_rgb(0x00, 0x00, 0x00); + task->fg = colour_rgb(0x00, 0xFF, 0x00); task->freq_index = 0; task->a = lissajous_freqs[0][0]; task->b = lissajous_freqs[0][1]; diff --git a/libraries/wuss/test/tasks/lissajous.h b/libraries/wuss/test/tasks/lissajous.h index d9b85be6..36e359a4 100644 --- a/libraries/wuss/test/tasks/lissajous.h +++ b/libraries/wuss/test/tasks/lissajous.h @@ -28,9 +28,7 @@ wuss_event_fn_t lissajous_handle; /* create the Lissajous window against the given wuss instance; "task" is a * per-instance block owned by the window and freed when it closes */ -result_t lissajous_create(wuss_t *wuss, - const colour_t *palette, - lissajous_task_t *task); +result_t lissajous_create(wuss_t *wuss, lissajous_task_t *task); #endif /* USE_SDL */ diff --git a/libraries/wuss/test/tasks/sofa.c b/libraries/wuss/test/tasks/sofa.c index 266117a9..70870bd9 100644 --- a/libraries/wuss/test/tasks/sofa.c +++ b/libraries/wuss/test/tasks/sofa.c @@ -354,15 +354,13 @@ static void draw_vertex_dots(screen_t *scr, colour); } -result_t sofa_create(wuss_t *wuss, - const colour_t *palette, - sofa_task_t *task) +result_t sofa_create(wuss_t*wuss, sofa_task_t*task) { wuss_task_t delegate; - task->bg = palette[palette_PICO8_DARK_PURPLE]; - task->line = palette[palette_PICO8_ORANGE]; - task->dot = palette[palette_PICO8_WHITE]; + task->bg = colour_rgb(0x7E, 0x25, 0x53); + task->line = colour_rgb(0xFF, 0xA3, 0x00); + task->dot = colour_rgb(0xFF, 0xFF, 0xFF); task->angle = 0.0; task->zoom = 1.0; task->spinning = true; diff --git a/libraries/wuss/test/tasks/sofa.h b/libraries/wuss/test/tasks/sofa.h index 08aeb6df..280f2793 100644 --- a/libraries/wuss/test/tasks/sofa.h +++ b/libraries/wuss/test/tasks/sofa.h @@ -43,10 +43,7 @@ sofa_task_t; wuss_event_fn_t sofa_handle; /* create the sofa window against the given wuss instance */ -result_t sofa_create(wuss_t *wuss, - const colour_t *palette, - sofa_task_t *task); - +result_t sofa_create(wuss_t*wuss, sofa_task_t*task); #endif /* USE_SDL */ diff --git a/libraries/wuss/test/tasks/swatches.c b/libraries/wuss/test/tasks/swatches.c index 0092d176..b5f436a5 100644 --- a/libraries/wuss/test/tasks/swatches.c +++ b/libraries/wuss/test/tasks/swatches.c @@ -26,6 +26,7 @@ result_t swatches_create(wuss_t *wuss, swatches_task_t *task) wuss_task_t delegate; wuss_icon_spec_t specs[SWATCHES_NSPECS]; int p; + wuss_colour_t fg, bg; result_t rc; task->window = NULL; @@ -36,13 +37,16 @@ result_t swatches_create(wuss_t *wuss, swatches_task_t *task) SIZE2D(SWATCHES_DOC_W, 140), "Swatches", wuss_WINDOW_NONE, - wuss_BACKDROP_COLOUR(palette_PICO8_LIGHT_GREY), + wuss_BACKDROP_COLOUR(wuss_nearest_colour(wuss, 0xDD, 0xDD, 0xDD)), &delegate, SIZE2D(SWATCHES_DOC_W, SWATCHES_DOC_H), SIZE2D(0, 0), &task->window); if (rc != result_OK) return rc; + + fg = wuss_nearest_colour(wuss, 0x00, 0x00, 0x00); + bg = wuss_nearest_colour(wuss, 0xFF, 0xFF, 0xFF); memset(specs, 0, sizeof(specs)); @@ -50,7 +54,7 @@ result_t swatches_create(wuss_t *wuss, swatches_task_t *task) specs[0].bbox = (box_t) BOX_POS_SIZE(12, 12, 140, 14); specs[0].type = wuss_ICON_TYPE_LABEL; specs[0].text = "Fill patterns:"; - specs[0].fg = palette_PICO8_DARK_BLUE; + specs[0].fg = fg; specs[0].bg = wuss_NO_BACKGROUND; /* [1..] one 16x16 swatch per built-in fill pattern, laid out as a grid down @@ -68,8 +72,8 @@ result_t swatches_create(wuss_t *wuss, swatches_task_t *task) 34 + row * SWATCHES_PITCH, 16, 16); specs[1 + p].type = wuss_ICON_TYPE_PATTERN; - specs[1 + p].fg = palette_PICO8_DARK_BLUE; - specs[1 + p].bg = palette_PICO8_LIGHT_GREY; + specs[1 + p].fg = fg; + specs[1 + p].bg = bg; specs[1 + p].pattern = (screen_pattern_t) p; } diff --git a/libraries/wuss/test/tasks/text.c b/libraries/wuss/test/tasks/text.c index c079d9fe..65a2b233 100644 --- a/libraries/wuss/test/tasks/text.c +++ b/libraries/wuss/test/tasks/text.c @@ -28,18 +28,17 @@ static const char paragraph[] = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, " "sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."; -result_t text_create(wuss_t *wuss, - const colour_t *palette, - bmfont_t *font, - text_task_t *task) +result_t text_create(wuss_t *wuss, + bmfont_t *font, + text_task_t *task) { wuss_task_t delegate; size2d_t sz; result_t rc; task->font = font; - task->bg = palette[palette_PICO8_BLUE]; /* matches the "Lorem Ipsum" window's bg, for bmfont_draw's glyph blending */ - task->fg = palette[palette_PICO8_WHITE]; + task->bg = colour_rgb(0xFF, 0xFF, 0xFF); + task->fg = colour_rgb(0x00, 0x00, 0x00); task->frame_count = 0; task->resizing = true; @@ -53,7 +52,7 @@ result_t text_create(wuss_t *wuss, sz, "Lorem Ipsum", 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(palette_PICO8_BLUE), + wuss_BACKDROP_COLOUR(wuss_nearest_colour(wuss, 0xFF, 0xFF, 0xFF)), &delegate, sz, SIZE2D(0, 0), diff --git a/libraries/wuss/test/tasks/text.h b/libraries/wuss/test/tasks/text.h index 2b94ae96..f729b786 100644 --- a/libraries/wuss/test/tasks/text.h +++ b/libraries/wuss/test/tasks/text.h @@ -30,11 +30,9 @@ wuss_event_fn_t text_handle; /* create the paragraph-of-text window against the given wuss instance */ result_t text_create(wuss_t *wuss, - const colour_t *palette, bmfont_t *font, text_task_t *task); - #endif /* USE_SDL */ #endif /* TASKS_TEXT_H */ From 5c3bfffa31a16ec5e99f0044a1f8bd7c7e11c8ca Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Tue, 1 Sep 2026 23:49:32 +0100 Subject: [PATCH 163/287] refactor(wuss): table-drive the demo chrome palette, tidy task colour setup run_wuss had two near-identical wuss_config_t fill blocks (PICO-8 vs wimp16) differing only in colour constants; fold them into a 2-row index table so the field wiring exists once. Also hoist the wuss_nearest_colour calls in the icons and swatches tasks above their window-create call and drop stray trailing whitespace. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- apps/wuss/main.c | 76 ++++++++++++++-------------- libraries/wuss/test/tasks/icons.c | 8 +-- libraries/wuss/test/tasks/image.c | 4 +- libraries/wuss/test/tasks/swatches.c | 6 +-- 4 files changed, 46 insertions(+), 48 deletions(-) diff --git a/apps/wuss/main.c b/apps/wuss/main.c index 11ad04aa..e400c379 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -495,45 +495,43 @@ static result_t run_wuss(const char *resources) SDL_SetTextureScaleMode(texture, SDL_SCALEMODE_NEAREST); /* keep pixels crisp when F2 scales the window up */ { - wuss_config_t config; - - if (!use_wimp16) { - config.titlebar_height = 0; - config.furniture.title.bg = palette_PICO8_DARK_BLUE; - config.furniture.title.fg = palette_PICO8_WHITE; - 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.accent.bg = palette_PICO8_DARK_BLUE; - config.accent.fg = palette_PICO8_WHITE; - config.backdrop.colour = palette_PICO8_WHITE; - config.backdrop.pattern = screen_PATTERN_DOTS; - config.backdrop.pattern_bg = palette_PICO8_LIGHT_GREY; - } else { - config.titlebar_height = 0; - config.furniture.title.bg = palette_WIMP16_GREY_75; - config.furniture.title.fg = 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.accent.bg = palette_WIMP16_ORANGE; - config.accent.fg = palette_WIMP16_BLACK; - config.backdrop.colour = palette_WIMP16_GREY_50; - config.backdrop.pattern = screen_PATTERN_DOTS; - config.backdrop.pattern_bg = palette_WIMP16_GREY_37; - } + /* Furniture/bevel/accent/backdrop colour indices, one row per palette. + * Same field order as the assignments below. */ + static const wuss_colour_t chrome[2][15] = + { + /* 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 }, + /* 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 } + }; + const wuss_colour_t *c = chrome[use_wimp16 ? 1 : 0]; + wuss_config_t config; + + 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.backdrop.pattern = screen_PATTERN_DOTS; + config.backdrop.pattern_bg = c[14]; rc = wuss_create(&scr, font, palette, NELEMS(palette), &config, NULL, &wuss); diff --git a/libraries/wuss/test/tasks/icons.c b/libraries/wuss/test/tasks/icons.c index e60703a0..97bf3bce 100644 --- a/libraries/wuss/test/tasks/icons.c +++ b/libraries/wuss/test/tasks/icons.c @@ -47,6 +47,10 @@ result_t icons_create(wuss_t *wuss, int nspecs; /* live spec count (sprite icons are optional) */ result_t rc; + black = wuss_nearest_colour(wuss, 0x00, 0x00, 0x00); + grey5 = wuss_nearest_colour(wuss, 0xBB, 0xBB, 0xBB); + grey6 = wuss_nearest_colour(wuss, 0xDD, 0xDD, 0xDD); + task->font = font; task->label = colour_rgb(0x00, 0x00, 0x00); task->paper = colour_rgb(0xDD, 0xDD, 0xDD); /* the window bg, below */ @@ -65,10 +69,6 @@ result_t icons_create(wuss_t *wuss, task->has_sprite = 1; delegate = wuss_task_start(icons_handle, task); - - black = wuss_nearest_colour(wuss, 0x00, 0x00, 0x00); - grey5 = wuss_nearest_colour(wuss, 0xBB, 0xBB, 0xBB); - grey6 = wuss_nearest_colour(wuss, 0xDD, 0xDD, 0xDD); rc = wuss_window_create_placed(wuss, SIZE2D(ICONS_DOC_W, 160), diff --git a/libraries/wuss/test/tasks/image.c b/libraries/wuss/test/tasks/image.c index 46b2c3b0..381590db 100644 --- a/libraries/wuss/test/tasks/image.c +++ b/libraries/wuss/test/tasks/image.c @@ -37,7 +37,7 @@ result_t image_create(wuss_t *wuss, } delegate = wuss_task_start(image_handle, task); /* shows through the image's transparent pixels */ - + sz.w = task->bitmap.size.w + BORDER * 2; sz.h = task->bitmap.size.h + BORDER * 2; @@ -72,7 +72,7 @@ static result_t image_redraw(const wuss_event_t *event, void *task_data) by = bounds->y0 - sy + BORDER; #define NINEPATCHSZ 9 - + behind.x0 = bx - NINEPATCHSZ; behind.y0 = by - NINEPATCHSZ; behind.x1 = behind.x0 + ic->bitmap.size.w + NINEPATCHSZ * 2; diff --git a/libraries/wuss/test/tasks/swatches.c b/libraries/wuss/test/tasks/swatches.c index b5f436a5..087c5064 100644 --- a/libraries/wuss/test/tasks/swatches.c +++ b/libraries/wuss/test/tasks/swatches.c @@ -29,6 +29,9 @@ result_t swatches_create(wuss_t *wuss, swatches_task_t *task) wuss_colour_t fg, bg; result_t rc; + fg = wuss_nearest_colour(wuss, 0x00, 0x00, 0x00); + bg = wuss_nearest_colour(wuss, 0xFF, 0xFF, 0xFF); + task->window = NULL; delegate = wuss_task_start(swatches_handle, task); @@ -44,9 +47,6 @@ result_t swatches_create(wuss_t *wuss, swatches_task_t *task) &task->window); if (rc != result_OK) return rc; - - fg = wuss_nearest_colour(wuss, 0x00, 0x00, 0x00); - bg = wuss_nearest_colour(wuss, 0xFF, 0xFF, 0xFF); memset(specs, 0, sizeof(specs)); From 259c0d51445397552f7cb3709af590867ce30c98 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Wed, 2 Sep 2026 00:00:19 +0100 Subject: [PATCH 164/287] fix(wuss): don't blit stale pixels when scrolls arrive faster than redraws A fast wheel spin delivers several wuss_scroll calls before wuss_redraw_dirty runs. wuss_window_set_scroll's incremental blit is a framebuffer memmove, so a second scroll in the same frame would slide the strip the first scroll exposed -- still marked dirty, not yet repainted -- into the window interior, smearing stale content there permanently. Before the blit, collect the pending dirty rects that overlap the content box (sampled ahead of the furniture invalidate so its unrelated rects are excluded) and subtract them from every clean blit-source piece. The stale areas are no longer blitted; they fall through to the normal content-minus-copied repaint instead. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- libraries/wuss/window/set-scroll.c | 43 ++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/libraries/wuss/window/set-scroll.c b/libraries/wuss/window/set-scroll.c index df1552ae..721d23f5 100644 --- a/libraries/wuss/window/set-scroll.c +++ b/libraries/wuss/window/set-scroll.c @@ -5,6 +5,8 @@ void wuss_window_set_scroll(wuss_window_t *window, point_t p) { box_t content; + box_t stale[WUSS_MAX_DIRTY]; + box_t clean[WUSS_MAX_INVALIDATE_PIECES]; box_t src[WUSS_MAX_INVALIDATE_PIECES]; box_t blit_src[WUSS_MAX_INVALIDATE_PIECES]; box_t blit_dest[WUSS_MAX_INVALIDATE_PIECES]; @@ -12,7 +14,7 @@ void wuss_window_set_scroll(wuss_window_t *window, point_t p) box_t dirty[WUSS_MAX_INVALIDATE_PIECES]; box_t vis[WUSS_MAX_INVALIDATE_PIECES]; int order[WUSS_MAX_INVALIDATE_PIECES]; - int dx, dy, nsrc, nblit, ncopied, ndirty, nvis, i, j, idx; + int dx, dy, nstale, nclean, nsrc, nblit, ncopied, ndirty, nvis, i, j, idx; int overflow, blit_failed; dx = p.x - window->scroll.x; @@ -24,6 +26,18 @@ void wuss_window_set_scroll(wuss_window_t *window, point_t p) wuss__content_box(window, &content); + /* Any content-box area already sitting in the dirty list holds stale pixels + * (e.g. the strip exposed by an earlier scroll this frame, not yet + * redrawn): a wheel spin can deliver several scrolls before wuss_redraw_dirty + * runs, and the blit below is a framebuffer memmove -- feeding it a stale + * source would smear that stale content into the interior. Collect those + * regions now, before the furniture invalidate adds its own unrelated + * rects, and exclude them from the blit source further down. */ + nstale = 0; + for (i = 0; i < window->wuss->ndirty; i++) + if (box_intersects(&window->wuss->dirty[i], &content)) + stale[nstale++] = window->wuss->dirty[i]; + #ifdef WUSS_FURNITURE /* the scrollbar sausage position depends on scroll, so its well needs * redrawing too -- content invalidation alone never touches it */ @@ -37,9 +51,34 @@ void wuss_window_set_scroll(wuss_window_t *window, point_t p) * clips to the content box, so it would paint over the occluder there. * Clip each destination against the occluders too and keep only the * surviving sub-pieces, each with its own matching source offset. */ - nsrc = wuss__clip_to_visible(window, &content, src); + nclean = wuss__clip_to_visible(window, &content, clean); nblit = 0; overflow = 0; + + /* Carve the stale regions out of each clean source piece. Each subtract + * can split a piece into up to four bands, so this can overflow the piece + * budget on a badly fragmented window -- treat that as "no safe fast + * path" and let the blit_failed branch below invalidate what's left. */ + nsrc = 0; + for (i = 0; i < nclean; i++) + { + box_t kept[WUSS_MAX_INVALIDATE_PIECES]; + int nkept, k; + + nkept = wuss__subtract_boxes(&clean[i], stale, nstale, kept); + for (k = 0; k < nkept; k++) + { + if (nsrc == WUSS_MAX_INVALIDATE_PIECES) + { + overflow = 1; + break; + } + src[nsrc++] = kept[k]; + } + if (overflow) + break; + } + for (i = 0; i < nsrc && !overflow; i++) { box_t want; From 0ec6602db88cbad339fa2389372ec4967b5a7776 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Wed, 2 Sep 2026 00:20:04 +0100 Subject: [PATCH 165/287] feat(wuss): swap the system palette mid-session wuss_set_palette copies a new palette over the old, refreshes the cached nearest-black/white indices, broadcasts a new wuss_EVENT_PALETTE to every window's task so they can recache wuss_nearest_colour selections, then invalidates the whole screen. Length must match the palette given to wuss_create. The swatches test task rebuilds its swatch icons on wuss_EVENT_PALETTE; the interactive wuss driver cycles PICO-8 <-> Wimp16 on F4, pushing the new palette into the framebuffer bitmap and then into wuss. Also add bitmap_set_palette, which reuses an already-allocated palette buffer, and refactor bitmap_init to build its palette through it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- CMakeLists.txt | 1 + apps/wuss/main.c | 28 +++++++--- include/framebuf/bitmap.h | 13 +++++ include/wuss/task.h | 8 +-- include/wuss/wuss.h | 25 +++++++++ libraries/framebuf/bitmap/bitmap.c | 48 +++++++++++------ libraries/wuss/set-palette.c | 56 ++++++++++++++++++++ libraries/wuss/test/tasks/swatches.c | 78 ++++++++++++++++++---------- libraries/wuss/test/tasks/swatches.h | 6 +++ libraries/wuss/test/wuss-test.c | 32 ++++++++++++ 10 files changed, 244 insertions(+), 51 deletions(-) create mode 100644 libraries/wuss/set-palette.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 3e044bbb..d133fc86 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -347,6 +347,7 @@ set(WUSS_CORE_SOURCES libraries/wuss/redraw.c libraries/wuss/scroll.c libraries/wuss/scroll-step.c + libraries/wuss/set-palette.c libraries/wuss/task/start.c libraries/wuss/task/stop.c libraries/wuss/window/at.c diff --git a/apps/wuss/main.c b/apps/wuss/main.c index e400c379..e3162a9e 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -368,6 +368,13 @@ static wuss_button_t sdl_button_to_wuss(Uint8 button) } } +/* Palettes F4 cycles through, in order. Each fills a colour_t[16]. */ +static void (*const g_palettes[])(colour_t *) = +{ + define_pico8_palette, + define_wimp16_palette +}; + /* SDL delivers mouse coordinates in window space, which F2 can scale away * from the fixed-size Wuss screen; map back down to screen space */ static void sdl_pos_to_scr(SDL_Window *window, @@ -389,8 +396,8 @@ static void sdl_pos_to_scr(SDL_Window *window, /* click windows to bring to front, drag titlebars to move, resize the * SDL window to see the Wuss screen scale; F2 doubles the SDL window size, * Shift-F2 halves it; F3 redraws the whole screen one pixel at a time, to - * catch tasks whose drawing routines misbehave under a 1x1 clip; Q or - * close to quit */ + * catch tasks whose drawing routines misbehave under a 1x1 clip; F4 cycles + * the system palette live (wuss_set_palette); Q or close to quit */ static result_t run_wuss(const char *resources) { const int scr_width = 640; @@ -422,16 +429,15 @@ static result_t run_wuss(const char *resources) bool pixel_stress_pending; int i; bool use_wimp16; + int palette_index; { /* "riscos16" selects the RISC OS 16-colour palette; default is PICO-8 */ const char *palette_name = getenv("WUSS_PALETTE"); use_wimp16 = (palette_name != NULL && strcmp(palette_name, "wimp16") == 0); - if (use_wimp16) - define_wimp16_palette(palette); - else - define_pico8_palette(palette); + palette_index = use_wimp16 ? 1 : 0; + g_palettes[palette_index](palette); } leafname = path_join_leafname("digits", "png"); @@ -573,6 +579,16 @@ static result_t run_wuss(const char *resources) garbage_pending = true; else if (event.key.key == SDLK_F3) pixel_stress_pending = true; + else if (event.key.key == SDLK_F4) + { + /* cycle the system palette live: rebuild the palette, push it into + * the framebuffer bitmap, then tell wuss, which refreshes its own + * copy and pokes every task to recache */ + palette_index = (palette_index + 1) % (int) NELEMS(g_palettes); + g_palettes[palette_index](palette); + bitmap_set_palette(&bm, palette); + wuss_set_palette(wuss, palette, NELEMS(palette)); + } else if (event.key.key == SDLK_F2) { int w, h; diff --git a/include/framebuf/bitmap.h b/include/framebuf/bitmap.h index 2fecfd6f..a11361ec 100644 --- a/include/framebuf/bitmap.h +++ b/include/framebuf/bitmap.h @@ -47,6 +47,19 @@ result_t bitmap_init(bitmap_t *bm, const colour_t *palette, void *base); +/** + * Replace a bitmap's palette, reusing its existing palette buffer when one + * is already allocated and it is large enough. A no-op returning \ref + * result_OK for formats with no palette (log2bpp > 3). + * + * \param[in] bm Bitmap to repalette. + * \param[in] palette New palette, copied in, or NULL to drop the bitmap's + * palette entirely. + * \return \ref result_OK on success, \ref result_OOM if a palette buffer + * could not be allocated (the bitmap keeps its old palette). + */ +result_t bitmap_set_palette(bitmap_t *bm, const colour_t *palette); + /** * Clear the given bitmap to the specified colour. * diff --git a/include/wuss/task.h b/include/wuss/task.h index 37e4159f..b41e650b 100644 --- a/include/wuss/task.h +++ b/include/wuss/task.h @@ -37,7 +37,9 @@ typedef enum wuss_event_kind wuss_EVENT_MOUSE, /**< Button down/up over the window's content. */ wuss_EVENT_ICON, /**< A work-area button icon was clicked or hovered. */ wuss_EVENT_SCROLL, /**< Mouse wheel used over the window's content. */ - wuss_EVENT_QUIT /**< Task shutting down, via wuss_task_stop. */ + wuss_EVENT_QUIT, /**< Task shutting down, via wuss_task_stop. */ + wuss_EVENT_PALETTE /**< System palette changed, via wuss_set_palette; + recache any wuss_nearest_colour selections. */ } wuss_event_kind_t; @@ -114,8 +116,8 @@ typedef struct wuss_event } scroll; - /* wuss_EVENT_IDLE, wuss_EVENT_CLOSE, wuss_EVENT_QUIT and - * wuss_EVENT_OPEN carry no data. */ + /* wuss_EVENT_IDLE, wuss_EVENT_CLOSE, wuss_EVENT_QUIT, + * wuss_EVENT_OPEN and wuss_EVENT_PALETTE carry no data. */ } data; } diff --git a/include/wuss/wuss.h b/include/wuss/wuss.h index c1b9c8bb..1eb2a897 100644 --- a/include/wuss/wuss.h +++ b/include/wuss/wuss.h @@ -318,6 +318,31 @@ result_t wuss_create(screen_t *scr, const wuss_alloc_t *alloc, wuss_t **wuss); +/** + * Replace the system palette partway through a session. + * + * Copies \p palette in over the existing one (same semantics as + * wuss_create's palette argument), refreshes the cached nearest-black / + * nearest-white indices, broadcasts a \ref wuss_EVENT_PALETTE event to every + * window's task so they can recache any wuss_nearest_colour selections, then + * invalidates the whole screen. The caller is still responsible for the next + * wuss_redraw / wuss_redraw_dirty, and -- on a paletted screen -- for + * updating the screen bitmap's own palette to match. + * + * \param[in] wuss Window manager. + * \param[in] palette New system palette, copied in. + * \param[in] npalette Number of entries in palette. Must equal the count + * passed to wuss_create. + * \return \ref result_OK on success, \ref result_BAD_ARG if npalette does + * not match the current palette length, \ref result_WUSS_BAD_COLOUR + * if a configured furniture/bevel/backdrop colour index is now out + * of range (in which case the palette is left unchanged), else the + * first non-OK result returned by a task's handle callback. + */ +result_t wuss_set_palette(wuss_t *wuss, + const colour_t *palette, + int npalette); + /** * Destroy a window manager, and any windows still open on it. * diff --git a/libraries/framebuf/bitmap/bitmap.c b/libraries/framebuf/bitmap/bitmap.c index c8004b0a..139d70ae 100644 --- a/libraries/framebuf/bitmap/bitmap.c +++ b/libraries/framebuf/bitmap/bitmap.c @@ -15,8 +15,6 @@ result_t bitmap_init(bitmap_t *bm, const colour_t *palette, void *base) { - int log2bpp; - assert(bm); bm->size = size; @@ -26,25 +24,43 @@ result_t bitmap_init(bitmap_t *bm, bm->span = spanregistry_get(fmt); bm->base = base; - if (palette) - { - log2bpp = pixelfmt_log2bpp(fmt); - if (log2bpp <= 3) - { - int nentries; - colour_t *newpal; + return bitmap_set_palette(bm, palette); +} - nentries = 1 << (1 << log2bpp); - newpal = malloc(nentries * sizeof(*newpal)); - if (newpal == NULL) - return result_OOM; +result_t bitmap_set_palette(bitmap_t *bm, const colour_t *palette) +{ + int log2bpp; + int nentries; + colour_t *pal; - memcpy(newpal, palette, nentries * sizeof(*newpal)); + assert(bm); - bm->palette = newpal; - } + log2bpp = pixelfmt_log2bpp(bm->format); + if (log2bpp > 3) + return result_OK; /* no palette for this format */ + + if (palette == NULL) + { + free(bm->palette); + bm->palette = NULL; + return result_OK; } + nentries = 1 << (1 << log2bpp); + + /* the entry count is fixed by the format, so an existing buffer is always + * the right size to reuse */ + pal = bm->palette; + if (pal == NULL) + { + pal = malloc(nentries * sizeof(*pal)); + if (pal == NULL) + return result_OOM; + } + + memcpy(pal, palette, nentries * sizeof(*pal)); + bm->palette = pal; + return result_OK; } diff --git a/libraries/wuss/set-palette.c b/libraries/wuss/set-palette.c new file mode 100644 index 00000000..6a550a7a --- /dev/null +++ b/libraries/wuss/set-palette.c @@ -0,0 +1,56 @@ +/* wuss/set-palette.c -- swap the system palette mid-session */ + +#include <assert.h> +#include <string.h> + +#include "geom/box.h" + +#include "impl.h" + +result_t wuss_set_palette(wuss_t *wuss, + const colour_t *palette, + int npalette) +{ + wuss_event_t event; + box_t screen; + result_t rc; + list_t *e; + + assert(wuss != NULL); + assert(palette != NULL); + + /* Same length only: keeps every stored furniture/bevel/backdrop colour + * index in range, and matches the fixed-size framebuffer palette on a + * paletted screen. */ + if (npalette != wuss->npalette) + return result_BAD_ARG; + + memcpy(wuss->palette, palette, npalette * sizeof(*wuss->palette)); + + wuss->palettecache.white = wuss_nearest_colour(wuss, 255, 255, 255); + wuss->palettecache.black = wuss_nearest_colour(wuss, 0, 0, 0); + + rc = result_OK; + event.kind = wuss_EVENT_PALETTE; + for (e = wuss->z_order.next; e != NULL; e = e->next) + { + wuss_window_t *win; + result_t crc; + + win = (wuss_window_t *) e; + if (win->task.handle == NULL) + continue; + + crc = win->task.handle(win, &event, win->task.task_data); + if (crc != result_OK && rc == result_OK) + rc = crc; + } + + screen.x0 = 0; + screen.y0 = 0; + screen.x1 = wuss->scr->size.w; + screen.y1 = wuss->scr->size.h; + wuss_invalidate(wuss, &screen); + + return rc; +} diff --git a/libraries/wuss/test/tasks/swatches.c b/libraries/wuss/test/tasks/swatches.c index 087c5064..22cdd12c 100644 --- a/libraries/wuss/test/tasks/swatches.c +++ b/libraries/wuss/test/tasks/swatches.c @@ -20,33 +20,23 @@ #define SWATCHES_DOC_W 160 #define SWATCHES_DOC_H 320 /* taller than the window, so scrolling is exercised */ -result_t swatches_create(wuss_t *wuss, swatches_task_t *task) +/* Delete the current swatch icons and lay a fresh set out, resolving the + * heading and swatch colours through the palette as it stands now. Called at + * create time and again on wuss_EVENT_PALETTE. */ +static result_t swatches_build(swatches_task_t *task) { - enum { SWATCHES_NSPECS = 1 + screen_PATTERN__LIMIT }; - wuss_task_t delegate; wuss_icon_spec_t specs[SWATCHES_NSPECS]; - int p; wuss_colour_t fg, bg; - result_t rc; - - fg = wuss_nearest_colour(wuss, 0x00, 0x00, 0x00); - bg = wuss_nearest_colour(wuss, 0xFF, 0xFF, 0xFF); - - task->window = NULL; + int p; - delegate = wuss_task_start(swatches_handle, task); + for (p = 0; p < SWATCHES_NSPECS; p++) + { + wuss_icon_delete(task->icons[p]); + task->icons[p] = NULL; + } - rc = wuss_window_create_placed(wuss, - SIZE2D(SWATCHES_DOC_W, 140), - "Swatches", - wuss_WINDOW_NONE, - wuss_BACKDROP_COLOUR(wuss_nearest_colour(wuss, 0xDD, 0xDD, 0xDD)), - &delegate, - SIZE2D(SWATCHES_DOC_W, SWATCHES_DOC_H), - SIZE2D(0, 0), - &task->window); - if (rc != result_OK) - return rc; + fg = wuss_nearest_colour(task->wuss, 0x00, 0x00, 0x00); + bg = wuss_nearest_colour(task->wuss, 0xFF, 0xFF, 0xFF); memset(specs, 0, sizeof(specs)); @@ -77,7 +67,34 @@ result_t swatches_create(wuss_t *wuss, swatches_task_t *task) specs[1 + p].pattern = (screen_pattern_t) p; } - rc = wuss_icon_create_array(task->window, specs, SWATCHES_NSPECS, NULL); + return wuss_icon_create_array(task->window, specs, SWATCHES_NSPECS, + task->icons); +} + +result_t swatches_create(wuss_t *wuss, swatches_task_t *task) +{ + wuss_task_t delegate; + result_t rc; + + task->wuss = wuss; + task->window = NULL; + memset(task->icons, 0, sizeof(task->icons)); + + delegate = wuss_task_start(swatches_handle, task); + + rc = wuss_window_create_placed(wuss, + SIZE2D(SWATCHES_DOC_W, 140), + "Swatches", + wuss_WINDOW_NONE, + wuss_BACKDROP_COLOUR(wuss_nearest_colour(wuss, 0xDD, 0xDD, 0xDD)), + &delegate, + SIZE2D(SWATCHES_DOC_W, SWATCHES_DOC_H), + SIZE2D(0, 0), + &task->window); + if (rc != result_OK) + return rc; + + rc = swatches_build(task); if (rc != result_OK) { wuss_window_close(task->window); @@ -92,14 +109,23 @@ result_t swatches_handle(wuss_window_t *window, const wuss_event_t *event, void *task_data) { - if (event->kind == wuss_EVENT_CLOSE) + swatches_task_t *task; + + task = task_data; + + switch (event->kind) { + case wuss_EVENT_PALETTE: + return swatches_build(task); + + case wuss_EVENT_CLOSE: wuss_window_close(window); free(task_data); /* calloc'd per instance by the spawner */ return result_OK; - } - return result_OK; + default: + return result_OK; + } } #endif /* USE_SDL */ diff --git a/libraries/wuss/test/tasks/swatches.h b/libraries/wuss/test/tasks/swatches.h index c30d7a50..96253d6e 100644 --- a/libraries/wuss/test/tasks/swatches.h +++ b/libraries/wuss/test/tasks/swatches.h @@ -5,14 +5,20 @@ #ifdef USE_SDL +#include "framebuf/screen.h" +#include "wuss/icon.h" #include "wuss/window.h" /* one 16x16 PATTERN icon per built-in screen fill pattern, laid out as a * grid down a document taller than the window so the swatches scroll * through it and stay phase-locked while doing so */ +enum { SWATCHES_NSPECS = 1 + screen_PATTERN__LIMIT }; + typedef struct swatches_task { + wuss_t *wuss; /* for re-resolving swatch colours on a palette swap */ wuss_window_t *window; + wuss_icon_t *icons[SWATCHES_NSPECS]; /* current icons, deleted on rebuild */ } swatches_task_t; diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 29781ebf..3de12433 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -46,6 +46,7 @@ typedef struct test_task int close_count; int stop_count; int open_count; + int palette_count; } test_task_t; @@ -90,6 +91,10 @@ static result_t test_handle(wuss_window_t *window, tc->open_count++; break; + case wuss_EVENT_PALETTE: + tc->palette_count++; + break; + default: break; } @@ -3149,6 +3154,33 @@ result_t wuss_test(const char *resources) wuss_window_close(win_p[k]); } + printf("test: wuss_set_palette broadcasts wuss_EVENT_PALETTE and rejects a length mismatch\n"); + { + /* the wuss under test was made with a 2-entry palette (see top of this + * function); a fresh 2-entry palette must broadcast to every task, a + * wrong-length one must be refused without broadcasting */ + static const colour_t swapped[2] = { { 0xFF202020 }, { 0xFFE0E0E0 } }; + static const colour_t too_long[3] = + { { 0xFF000000 }, { 0xFF808080 }, { 0xFFFFFFFF } }; + + tc_a.palette_count = 0; + tc_b.palette_count = 0; + + rc = wuss_set_palette(wuss, swapped, NELEMS(swapped)); + if (rc != result_OK) + goto Failure; + if (tc_a.palette_count != 1 || tc_b.palette_count != 1) + goto Failure; + + rc = wuss_set_palette(wuss, too_long, NELEMS(too_long)); + if (rc != result_BAD_ARG) + goto Failure; + if (tc_a.palette_count != 1 || tc_b.palette_count != 1) + goto Failure; /* rejected call must not have broadcast */ + + rc = result_OK; + } + printf("test: wuss_task_stop sends wuss_EVENT_QUIT to each window's task\n"); tc_a.stop_count = 0; From fe6e0834bfc82849fe379b270bcde07d18dbd749 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Wed, 2 Sep 2026 00:39:22 +0100 Subject: [PATCH 166/287] refactor(wuss): sort the launch menu tasks by name Alphabetise the 14 spawn tasks in g_task_items and reorder g_task_spawn to match. The trailing Menu, Menu (desc) and Quit Wuss entries stay pinned below the separator. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- apps/wuss/main.c | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/apps/wuss/main.c b/apps/wuss/main.c index e3162a9e..9103cb3d 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -309,19 +309,19 @@ typedef result_t (*task_spawn_fn_t)(void); static const wuss_menu_item_t g_task_items[] = { { "Ball", wuss_MENU_ITEM_NONE, NULL }, - { "Text", wuss_MENU_ITEM_NONE, NULL }, { "Blank", wuss_MENU_ITEM_NONE, NULL }, { "Chars", wuss_MENU_ITEM_NONE, NULL }, - { "Palette", wuss_MENU_ITEM_NONE, NULL }, - { "Image", wuss_MENU_ITEM_NONE, NULL }, { "Checker", wuss_MENU_ITEM_NONE, NULL }, { "Curve", wuss_MENU_ITEM_NONE, NULL }, - { "Lissajous", wuss_MENU_ITEM_NONE, NULL }, - { "Sofa", wuss_MENU_ITEM_NONE, NULL }, { "Gradient", wuss_MENU_ITEM_NONE, NULL }, { "Icons", wuss_MENU_ITEM_NONE, NULL }, - { "Swatches", wuss_MENU_ITEM_NONE, NULL }, + { "Image", wuss_MENU_ITEM_NONE, NULL }, + { "Lissajous", wuss_MENU_ITEM_NONE, NULL }, + { "Palette", wuss_MENU_ITEM_NONE, NULL }, { "Porter-Duff", wuss_MENU_ITEM_NONE, NULL }, + { "Sofa", wuss_MENU_ITEM_NONE, NULL }, + { "Swatches", wuss_MENU_ITEM_NONE, NULL }, + { "Text", wuss_MENU_ITEM_NONE, NULL }, { "Menu", wuss_MENU_ITEM_DASHED, NULL }, { "Menu (desc)", wuss_MENU_ITEM_NONE, NULL }, { "Quit Wuss", wuss_MENU_ITEM_DASHED, NULL } @@ -335,9 +335,9 @@ static result_t spawn_quit(void) static const task_spawn_fn_t g_task_spawn[] = { - spawn_ball, spawn_text, spawn_blank, spawn_chars, spawn_palette, - spawn_image, spawn_checker, spawn_curve, spawn_lissajous, spawn_sofa, - spawn_gradient, spawn_icons, spawn_swatches, spawn_porter_duff, spawn_menu, + spawn_ball, spawn_blank, spawn_chars, spawn_checker, spawn_curve, + spawn_gradient, spawn_icons, spawn_image, spawn_lissajous, spawn_palette, + spawn_porter_duff, spawn_sofa, spawn_swatches, spawn_text, spawn_menu, spawn_menu_desc, spawn_quit }; From 27df2fd265f0d9c372909c81746518ed0fa0a57d Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Wed, 2 Sep 2026 01:00:52 +0100 Subject: [PATCH 167/287] fix(wuss): ignore the mouse-up that follows a menu-opening press A menu is opened from a task's MOUSE_DOWN handler. The matching MOUSE_UP was landing on the fresh menu's row 0 and immediately picking it. Add a wuss_t.menu_eat_up flag set by wuss_menu_open and consumed by the next MOUSE_UP in wuss_mouse_click; any fresh MOUSE_DOWN clears it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- libraries/wuss/create.c | 1 + libraries/wuss/impl.h | 4 ++++ libraries/wuss/menu/menu.c | 7 +++++++ libraries/wuss/mouse-click.c | 13 +++++++++++++ 4 files changed, 25 insertions(+) diff --git a/libraries/wuss/create.c b/libraries/wuss/create.c index 1675d3bd..d7040f95 100644 --- a/libraries/wuss/create.c +++ b/libraries/wuss/create.c @@ -230,6 +230,7 @@ result_t wuss_create(screen_t *scr, #endif #ifdef WUSS_MENUS w->menu_chain = NULL; + w->menu_eat_up = 0; #endif w->ndirty = 0; diff --git a/libraries/wuss/impl.h b/libraries/wuss/impl.h index 0c3355d4..8c857328 100644 --- a/libraries/wuss/impl.h +++ b/libraries/wuss/impl.h @@ -116,6 +116,10 @@ struct wuss * menu chain, NULL when none open; * consulted by mouse-click.c to * dismiss on a click-away */ + int menu_eat_up; /* a menu opened on the last + * MOUSE_DOWN; swallow its matching + * MOUSE_UP so the release does not + * immediately pick row 0 */ #endif }; diff --git a/libraries/wuss/menu/menu.c b/libraries/wuss/menu/menu.c index c4b66447..2bd068dd 100644 --- a/libraries/wuss/menu/menu.c +++ b/libraries/wuss/menu/menu.c @@ -460,6 +460,13 @@ result_t wuss_menu_open(wuss_t *wuss, return rc; wuss->menu_chain = root; + + /* A menu is opened from a task's MOUSE_DOWN handler; the matching MOUSE_UP is + * still to come and would land on the fresh menu's row 0. Mark it to be + * eaten. wuss_mouse_click clears this on the next MOUSE_UP whether or not it + * hit the menu. */ + wuss->menu_eat_up = 1; + if (out != NULL) *out = root; return result_OK; diff --git a/libraries/wuss/mouse-click.c b/libraries/wuss/mouse-click.c index 91c4ef6e..68037d55 100644 --- a/libraries/wuss/mouse-click.c +++ b/libraries/wuss/mouse-click.c @@ -52,6 +52,19 @@ result_t wuss_mouse_click(wuss_t *wuss, *hit = win; #ifdef WUSS_MENUS + /* The MOUSE_UP that follows the MENU press which opened the chain is spent: + * without this it would land on the new menu's row 0 and pick it. */ + if (action == wuss_MOUSE_UP && wuss->menu_eat_up) + { + wuss->menu_eat_up = 0; + return result_OK; + } + + /* Any fresh press ends the eat-up window: the release it pairs with is a + * real click, not the tail of the menu-opening press. */ + if (action == wuss_MOUSE_DOWN) + wuss->menu_eat_up = 0; + /* A press outside every window in the open menu chain dismisses the chain * and is spent doing so. Presses inside the chain fall through to the menu * window's own delegate. */ From b48fa2802c6c23eae0c2dc3acf8a08691563976b Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Wed, 2 Sep 2026 01:08:30 +0100 Subject: [PATCH 168/287] docs(wuss): changelog the menu mouse-up fix Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22d033b9..285ff167 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -80,6 +80,8 @@ _Unreleased_ until one is cut. ### Fixed +- Opening a menu from a task's mouse-down handler no longer picks the menu's + first row on the matching mouse-up: that release is now swallowed. - Dragging a scrollbar well with Select no longer raises the window; only a resize-icon grab restacks it. - `wuss_window_move()` no longer repaints already-blitted pixels when a drag From 75042220b8420e59661a1b2f23e85ea9c55683e8 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Wed, 2 Sep 2026 01:11:25 +0100 Subject: [PATCH 169/287] docs: fill in CHANGELOG for changes since 671cb53d Menus (wuss/menu.h + descriptor parser), mid-session palette swap, wuss_nearest_colour, pluggable allocator, bmtext, the extended icon-type set, screen polyline/outline/dashed-line primitives, 8x8 Bayer dither, the Wimp16 palette, plus the draw/fill primitive renames and assorted scroll-redraw fixes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- CHANGELOG.md | 85 ++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 80 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 285ff167..30b41f9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,14 +10,64 @@ _Unreleased_ until one is cut. ### Added -- `wuss/icon.h` — work-area icons: static labels and clickable bevelled - buttons drawn inside a window's content area. Icon boxes are in virtual - document space, so they scroll with the content. Wuss hit-tests buttons - before the content task sees a click and delivers them as `wuss_EVENT_ICON`; - labels and hidden or disabled icons fall through as `wuss_EVENT_MOUSE`. +- `wuss/menu.h` (new `WUSS_MENUS` option, implies `WUSS_ICONS`) — RISC OS-style + pop-up menus. `wuss_menu_open()` shows a caller-owned, immutable + `wuss_menu_t` (title plus an array of `wuss_menu_item_t`) as a borderless + window, nudged to stay on screen and opened under the pointer; wuss owns + layout, submenu chaining on hover and whole-chain dismissal. Per-item flags + cover ticks, disabled rows, dashed separators and submenus. + `wuss_menu_select_fn_t` reports a leaf pick — SELECT closes the chain, ADJUST + keeps it open. `wuss_menu_close()` / `wuss_menu_is_open()` manage a chain by + handle. An over-tall menu gets a real vertical scrollbar instead of being + cropped. +- `wuss_menu_create_from_desc()` / `wuss_menu_destroy()` — build a heap + `wuss_menu_t` tree from a compact descriptor string (PrivateEye's + `menu_create_from_desc` syntax: `,` between items, leading `|` for a dashed + separator, `{ ... }` submenus, `!` tick, `~` shade, `>` and `%s` varargs). + The whole tree is one owned allocation graph freed by `wuss_menu_destroy()`. +- `wuss_set_palette()` — swap the system palette mid-session. Copies the new + palette in, refreshes the cached nearest-black/white indices, broadcasts a + new `wuss_EVENT_PALETTE` to every window's task so it can recache + `wuss_nearest_colour()` selections, then invalidates the whole screen. + Length must match `wuss_create()`'s; a now-out-of-range furniture/bevel/ + backdrop index is rejected with the palette left unchanged. +- `wuss_nearest_colour()` — the system-palette index closest to an RGB value by + squared Euclidean distance, ties to the lower index. +- `wuss_alloc_t` and the `wuss_alloc` stdlib default — pluggable malloc/realloc/ + free hooks. `wuss_create()` takes a new `const wuss_alloc_t *` argument + (NULL selects `wuss_alloc`); every heap block a `wuss_t` owns goes through + the hooks. +- `wuss/icon.h` — work-area icons drawn inside a window's content area, in + virtual document space so they scroll with the content. Wuss hit-tests + interactive icons before the content task sees a click and delivers them as + `wuss_EVENT_ICON`; labels and hidden or disabled icons fall through as + `wuss_EVENT_MOUSE`. Icon types: `LABEL` (with `JUSTIFY_RIGHT` / `_CENTRE` + flags), bevelled `BUTTON` (`DEFAULT` flag draws it as the default action + button), `RADIO` and `OPTION` latching buttons (radios with a non-zero + `group` are mutually exclusive), `FRAME` grouping box, `BITMAP` (a + caller-owned image, hit-tested only with the `INTERACTIVE` flag), `PATTERN` + swatch, `MENU_ENTRY` and inert `RULE` rows. +- `wuss_icon_get_selected()` / `wuss_icon_set_selected()` — query and set a + radio or option icon's latched state; setting a grouped radio clears the + others in its group. No task event — the programmatic path. - `wuss_icon_create_array()` — creates a batch of icons from a spec array with all-or-nothing rollback: on the first failure any icons already created by the call are destroyed and no handles are written. +- `text/bmtext.h` — `bmtext_layout()` word-wraps a string to a pixel width in a + `bmfont_t` (measuring each candidate line, so proportional fonts wrap + correctly); `bmtext_draw()` draws the laid-out lines stacked. Layout is pure. +- `screen_draw_lines()` — connected polyline; a `screen_draw_line()` segment + between each adjacent pair. +- `screen_draw_rect()` — one-pixel unfilled rectangle outline (falls back to a + fill for a degenerate size). See _Changed_ for the fill-primitive renames. +- `screen_draw_dashed_line()` — Bresenham line with a dash-period counter. +- `screen_PATTERN_BAYER0` .. `screen_PATTERN_BAYER0 + 64` — 8x8 ordered dither, + one fill pattern per coverage level 0 (empty) to 64 (solid), indexed as + `screen_PATTERN_BAYER0 + level`. `BAYER32` == `GREY50`, `BAYER64` == `SOLID`. +- `define_wimp16_palette()` and the `palette_WIMP16_*` names — the RISC OS + desktop 16-colour palette in native Wimp index order. +- `bitmap_set_palette()` — replace a bitmap's palette in place, reusing the + existing palette buffer when it is large enough; NULL drops the palette. - `wuss_window_create_placed()` — creates a window from a content size instead of a box, letting Wuss pack it (furniture included) into the first free screen region. Successive auto-placed windows tile; placement cascades when @@ -57,6 +107,15 @@ _Unreleased_ until one is cut. ### Changed +- **Breaking:** `wuss_create()` takes a `const wuss_alloc_t *alloc` argument + after `config`; pass NULL for the stdlib allocator. +- **Breaking:** `wuss_config_t::palette` is renamed `furniture`, and its type + `wuss_palette_t` renamed `wuss_furniture_palette_t`. `wuss_colour_t` narrows + from `int` to `unsigned char`. +- **Breaking:** the framebuf draw primitives split draw/fill in their names: + `screen_draw_pixel` → `screen_set_pixel`, `screen_draw_rect` → + `screen_fill_rect`, `screen_draw_square` → `screen_fill_square`. `screen_draw_rect` + now names a one-pixel outline. - **Breaking:** the window/desktop background is now a `wuss_backdrop_t` (`{ colour, pattern, pattern_bg }`) instead of a bare `wuss_colour_t`: `wuss_config_t::backdrop`, and the `bg` parameter of @@ -77,16 +136,32 @@ _Unreleased_ until one is cut. - Adjust-clicking a scroll arrow now steps against the direction the arrow points, so one arrow can be worked both ways without moving the pointer. Toggle-size stays Select-only. +- A window can no longer be resized larger than the screen. ### Fixed - Opening a menu from a task's mouse-down handler no longer picks the menu's first row on the matching mouse-up: that release is now swallowed. +- A menu chain is now closed before its `on_select` callback runs, so a + callback that opens another menu no longer fights the one being torn down. +- Menu text is drawn in the nearest-black palette entry rather than assuming + a fixed index, so menus stay legible under any system palette. +- The mouse wheel no longer scrolls a window on an axis it declared + non-scrollable. +- A MENU-button click on window furniture now has no effect, instead of being + routed to the content task. +- A window's scroll offset is re-clamped after a resize reveals content past + the document extent. +- The scrollbar well keeps a 2px gap at each end. - Dragging a scrollbar well with Select no longer raises the window; only a resize-icon grab restacks it. - `wuss_window_move()` no longer repaints already-blitted pixels when a drag past an occluded corner slides one clean piece of the window onto ground another clean piece just vacated. +- Several scroll-redraw glitches fixed: stale pixels when scroll events arrive + faster than redraws, content blitted over a mid-content occluder, blit + sub-pieces clobbering each other's source region, and repaint sets not + clipped to the visible area. The hovered icon is re-resolved after a scroll. - A work-area button held on mouse-down is now released if the click opens a window that covers the button's owner, instead of staying stuck pressed. - Resize-corner drag preserves where within the resize icon the mouse-down From db8d9afc3593ebc0fc6da1f234966abbf857fe2a Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Wed, 2 Sep 2026 01:43:18 +0100 Subject: [PATCH 170/287] feat(wuss): allow a window as a menu sub-menu A wuss_menu_item_t may now carry a caller-owned wuss_window_t in a new `window` field (mutually exclusive with `submenu`). Hovering the row shows that window where a sub-menu would open, using the same anchor maths; moving off the row, a click outside, a leaf SELECT elsewhere, or wuss_destroy all hide it again rather than close it, so the same handle is reused next time. Backing this is a new core wuss_WINDOW_HIDDEN flag plus wuss_window_set_hidden(): a hidden window keeps its z-order slot but is not drawn, not hit-tested and occludes nothing; wuss_window_move still works on it so it can be parked and re-shown in position. Honoured in window/at.c, redraw.c, window/invalidate.c (occlusion), window/create.c (born hidden) and window/move.c (translate only, no blit). create-from-desc.c now initialises the new `window` field -- build_emit grows its array uninitialised, so a desc-built menu was dereferencing garbage on hover. apps/wuss adds a "Details" row wired to a lazily-created hidden window to exercise it interactively. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- CMakeLists.txt | 1 + apps/wuss/main.c | 47 +++++++++++-- include/wuss/menu.h | 11 +++ include/wuss/window.h | 15 ++++ include/wuss/wuss.h | 13 +++- libraries/wuss/menu.h | 13 +++- libraries/wuss/menu/create-from-desc.c | 2 + libraries/wuss/menu/menu.c | 96 ++++++++++++++++++++------ libraries/wuss/redraw.c | 3 + libraries/wuss/window/at.c | 2 + libraries/wuss/window/create.c | 3 +- libraries/wuss/window/invalidate.c | 2 + libraries/wuss/window/move.c | 12 ++++ libraries/wuss/window/set-hidden.c | 25 +++++++ 14 files changed, 213 insertions(+), 32 deletions(-) create mode 100644 libraries/wuss/window/set-hidden.c diff --git a/CMakeLists.txt b/CMakeLists.txt index d133fc86..472fc25a 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -362,6 +362,7 @@ set(WUSS_CORE_SOURCES libraries/wuss/window/resize.c libraries/wuss/window/restack.c libraries/wuss/window/set-background.c + libraries/wuss/window/set-hidden.c libraries/wuss/window/set-scroll.c) set(WUSS_FURNITURE_SOURCES diff --git a/apps/wuss/main.c b/apps/wuss/main.c index 9103cb3d..8a2d5f7e 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -234,13 +234,19 @@ static const wuss_menu_t g_menu_export = "Export", g_menu_export_items, NELEMS(g_menu_export_items) }; -static const wuss_menu_item_t g_menu_items[] = -{ - { "Open", wuss_MENU_ITEM_NONE, NULL }, - { "Show grid", wuss_MENU_ITEM_TICKED, NULL }, - { "Wireframe", wuss_MENU_ITEM_TICKED, NULL }, - { "Export", wuss_MENU_ITEM_NONE, &g_menu_export }, - { "Quit", wuss_MENU_ITEM_DASHED, NULL } +/* 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 = @@ -259,8 +265,35 @@ static void menu_selected(const wuss_menu_t *menu, menu->items[index].text ? menu->items[index].text : "(sep)"); } +/* 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.wuss, &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), + NULL, + 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.wuss, &g_menu, wuss_get_pointer(g.wuss), menu_selected, NULL, NULL); } diff --git a/include/wuss/menu.h b/include/wuss/menu.h index ead3d27b..6619b380 100644 --- a/include/wuss/menu.h +++ b/include/wuss/menu.h @@ -28,6 +28,7 @@ extern "C" #include "geom/point.h" #include "wuss/wuss.h" +#include "wuss/window.h" /* ----------------------------------------------------------------------- */ @@ -54,6 +55,16 @@ typedef struct wuss_menu_item wuss_menu_item_flags_t flags; /**< see wuss_menu_item_flags_t */ const struct wuss_menu *submenu; /**< non-NULL: draw a right arrow and open * this menu to the right on hover */ + wuss_window_t *window; /**< non-NULL: draw a right arrow and, on + * hover, show this caller-owned window + * where a submenu would open, hiding it + * again when the pointer leaves the row + * or the chain is dismissed. Create it + * with wuss_WINDOW_HIDDEN. Mutually + * exclusive with \c submenu. The window + * must outlive the open chain -- do not + * wuss_window_close it while its menu is + * open. */ } wuss_menu_item_t; diff --git a/include/wuss/window.h b/include/wuss/window.h index 8319742a..cdcca47a 100644 --- a/include/wuss/window.h +++ b/include/wuss/window.h @@ -139,6 +139,21 @@ void wuss_window_close(wuss_window_t *doomed); */ void wuss_window_move(wuss_window_t *window, point_t p); +/** + * Show or hide a window without destroying it. + * + * A hidden window keeps its z-order slot and all its state, but is not drawn + * and not hit-tested: it neither occludes lower windows nor catches the + * pointer. wuss_window_move still works while hidden, so a caller can park a + * window off to one side and re-show it in position later. Toggling + * visibility invalidates the window's footprint so the next redraw picks up + * the change. + * + * \param[in] window Window to show or hide. + * \param[in] hidden Non-zero to hide, zero to show. + */ +void wuss_window_set_hidden(wuss_window_t *window, int hidden); + /** * Resize a window's content area, preserving its top-left position. * diff --git a/include/wuss/wuss.h b/include/wuss/wuss.h index 1eb2a897..83d2758a 100644 --- a/include/wuss/wuss.h +++ b/include/wuss/wuss.h @@ -187,7 +187,18 @@ typedef enum wuss_window_flags * depends on the window's size in ways redraw can't patch incrementally * (e.g. a palette that lays itself out across the whole window). */ - wuss_WINDOW_NO_RESIZE_BLIT = 1 << 8 + wuss_WINDOW_NO_RESIZE_BLIT = 1 << 8, + + /** + * Created hidden, or hidden later via wuss_window_set_hidden: the window + * keeps its place in the z-order but is not drawn and not hit-tested, so + * it neither occludes other windows nor catches the pointer. Its position + * can still be changed with wuss_window_move while hidden, ready for when + * it is shown again. Unlike the wuss_WINDOW_NO_* bits this one is toggled + * at runtime, and it is honoured regardless of the WUSS_FURNITURE build + * option. + */ + wuss_WINDOW_HIDDEN = 1 << 9 } wuss_window_flags_t; diff --git a/libraries/wuss/menu.h b/libraries/wuss/menu.h index 9df3ca27..4d5a5b1e 100644 --- a/libraries/wuss/menu.h +++ b/libraries/wuss/menu.h @@ -14,16 +14,23 @@ struct wuss__menu { wuss_t *wuss; - wuss_window_t *window; /* the borderless menu window */ - const wuss_menu_t *menu; /* borrowed source description */ + wuss_window_t *window; /* the borderless menu window; for a + * borrowed-window level (see borrowed) the + * caller's own window instead */ + const wuss_menu_t *menu; /* borrowed source description; NULL for a + * borrowed-window level */ wuss_icon_t **icons; /* owned array, menu->nitems entries, in - * item order */ + * item order; NULL for a borrowed-window + * level */ struct wuss__menu *parent; /* level this one opened from, NULL at root */ struct wuss__menu *child; /* open submenu level, NULL when none */ wuss_menu_select_fn_t *on_select; void *ctx; int open_index; /* item index whose submenu `child` is, * -1 when no child open */ + int borrowed; /* 1: `window` is a caller-owned window + * shown in place of a submenu -- hide it + * rather than close it on teardown */ }; /* Called from wuss_mouse_click on a MOUSE_DOWN before the window hit-test: if a diff --git a/libraries/wuss/menu/create-from-desc.c b/libraries/wuss/menu/create-from-desc.c index 5418e284..74dc441c 100644 --- a/libraries/wuss/menu/create-from-desc.c +++ b/libraries/wuss/menu/create-from-desc.c @@ -169,6 +169,7 @@ static result_t menu_deep_copy(const wuss_menu_t *src, wuss_menu_t **out) items[i].text = strdup(src->items[i].text ? src->items[i].text : ""); items[i].flags = src->items[i].flags; items[i].submenu = NULL; + items[i].window = src->items[i].window; /* borrowed, as in the source */ if (items[i].text == NULL) { @@ -229,6 +230,7 @@ static result_t build_emit(Building *b, it->text = copy; it->flags = flags; it->submenu = submenu; + it->window = NULL; /* array_grow leaves this uninitialised otherwise */ return result_OK; } diff --git a/libraries/wuss/menu/menu.c b/libraries/wuss/menu/menu.c index 2bd068dd..9db1d39e 100644 --- a/libraries/wuss/menu/menu.c +++ b/libraries/wuss/menu/menu.c @@ -55,12 +55,70 @@ static void wuss__menu_close_from(struct wuss__menu *node) wuss_t *w; w = node->wuss; - wuss_window_close(node->window); - wuss__free(w, node->icons); + if (node->borrowed) + wuss_window_set_hidden(node->window, 1); /* caller's window: hide, keep */ + else + wuss_window_close(node->window); + wuss__free(w, node->icons); /* NULL for a borrowed level */ wuss__free(w, node); } } +/* 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 + * up. Mirrors the maths in wuss__menu_handle's MOUSE_MOVE branch. */ +static point_t wuss__submenu_anchor(struct wuss__menu *self, + const wuss_icon_t *icon) +{ + box_t wb; + box_t ib; + point_t scroll; + point_t at; + + wuss_window_get_content_bounds(self->window, &wb); + wuss_icon_get_bbox(icon, &ib); + wuss_window_get_scroll(self->window, &scroll); + + at.x = wb.x1 - WUSS_MENU_SUBMENU_OVERLAP; + at.y = wb.y0 + ib.y0 - scroll.y; + return at; +} + +/* Open item `index`'s borrowed window as level `self->child`: position it + * where a submenu would appear, un-hide it, bring it to the front. */ +static result_t wuss__menu_open_window(struct wuss__menu *self, int index) +{ + struct wuss__menu *node; + wuss_window_t *win; + point_t at; + + win = self->menu->items[index].window; + + node = wuss__malloc(self->wuss, sizeof(*node)); + if (node == NULL) + return result_OOM; + + node->wuss = self->wuss; + node->window = win; + node->menu = NULL; + node->icons = NULL; + node->parent = self; + node->child = NULL; + node->on_select = self->on_select; + node->ctx = self->ctx; + node->open_index = -1; + node->borrowed = 1; + + at = wuss__submenu_anchor(self, self->icons[index]); + wuss_window_move(win, at); + wuss_window_set_hidden(win, 0); + wuss_window_restack(win, wuss_ZORDER_FRONT); + + self->child = node; + return result_OK; +} + /* ----------------------------------------------------------------------- */ /* The menu window's task delegate. task_data is the struct wuss__menu. */ @@ -101,8 +159,6 @@ static result_t wuss__menu_handle(wuss_window_t *window, if (event->data.icon.action == wuss_MOUSE_MOVE) { - box_t ib; - box_t wb; point_t at; /* Hovering a different row closes any submenu the previous row opened. */ @@ -113,24 +169,23 @@ static result_t wuss__menu_handle(wuss_window_t *window, self->open_index = -1; } - if (item->submenu == NULL || self->child != NULL) + if (self->child != NULL) return result_OK; - wuss_window_get_content_bounds(self->window, &wb); - wuss_icon_get_bbox(icon, &ib); - - /* wb is the content box in screen space; ib is in document space, so the - * row's screen y is its document y less however far the menu is scrolled. - * The child's own titlebar sits above `at`, so a submenu row lines up with - * its parent. */ + /* A borrowed window opens where a submenu would; a submenu spawns as a + * fresh menu level. Either lines its row 0 up with this row -- the + * child's own titlebar sits above `at`. */ + if (item->window != NULL) { - point_t scroll; - - wuss_window_get_scroll(self->window, &scroll); - at.x = wb.x1 - WUSS_MENU_SUBMENU_OVERLAP; - at.y = wb.y0 + ib.y0 - scroll.y; + if (wuss__menu_open_window(self, index) == result_OK) + self->open_index = index; + return result_OK; } + if (item->submenu == NULL) + return result_OK; + + at = wuss__submenu_anchor(self, icon); if (wuss__menu_spawn(self->wuss, item->submenu, at, self, self->on_select, self->ctx, &self->child) == result_OK) self->open_index = index; @@ -148,8 +203,8 @@ static result_t wuss__menu_handle(wuss_window_t *window, button = event->data.icon.button; - if (item->submenu != NULL) - return result_OK; /* a submenu row opens on hover, it is not a pick */ + if (item->submenu != NULL || item->window != NULL) + return result_OK; /* a submenu/window row opens on hover, not a pick */ /* Capture what the callback needs, then tear the chain down *before* * invoking it: on_select may itself open a new menu (freeing this one), @@ -318,6 +373,7 @@ static result_t wuss__menu_spawn(wuss_t *wuss, node->on_select = on_select; node->ctx = ctx; node->open_index = -1; + node->borrowed = 0; /* One MENU_ENTRY icon per item, in item order, preceded by an inert * wuss_ICON_TYPE_RULE icon for each dashed item. specs[] therefore holds @@ -351,7 +407,7 @@ static result_t wuss__menu_spawn(wuss_t *wuss, if (item->flags & wuss_MENU_ITEM_DISABLED) flags |= wuss_ICON_FLAGS_DISABLED; - if (item->submenu != NULL) + if (item->submenu != NULL || item->window != NULL) flags |= wuss_ICON_FLAGS_SUBMENU; specs[s].type = wuss_ICON_TYPE_MENU_ENTRY; diff --git a/libraries/wuss/redraw.c b/libraries/wuss/redraw.c index cca90011..38a0cafc 100644 --- a/libraries/wuss/redraw.c +++ b/libraries/wuss/redraw.c @@ -13,6 +13,9 @@ static void redraw_window(wuss_t *wuss, box_t pieces[WUSS_MAX_INVALIDATE_PIECES]; int npieces, i; + if (win->flags & wuss_WINDOW_HIDDEN) + return; /* not drawn while hidden */ + if (box_intersection(&win->visible, full, &visible_clipped)) return; /* offscreen */ diff --git a/libraries/wuss/window/at.c b/libraries/wuss/window/at.c index bad019d1..c450a8fb 100644 --- a/libraries/wuss/window/at.c +++ b/libraries/wuss/window/at.c @@ -11,6 +11,8 @@ wuss_window_t *wuss__window_at(wuss_t *wuss, point_t p) wuss_window_t *win; win = (wuss_window_t *) e; + if (win->flags & wuss_WINDOW_HIDDEN) + continue; if (box_contains_point(&win->visible, p.x, p.y)) return win; } diff --git a/libraries/wuss/window/create.c b/libraries/wuss/window/create.c index 130659f9..29936b2a 100644 --- a/libraries/wuss/window/create.c +++ b/libraries/wuss/window/create.c @@ -133,7 +133,8 @@ result_t wuss_window_create(wuss_t *wuss, list_add_to_head(&wuss->z_order, &win->link); - wuss_invalidate(wuss, &win->visible); + if (!(flags & wuss_WINDOW_HIDDEN)) + wuss_invalidate(wuss, &win->visible); *window = win; diff --git a/libraries/wuss/window/invalidate.c b/libraries/wuss/window/invalidate.c index 33986c6d..ac2c4c53 100644 --- a/libraries/wuss/window/invalidate.c +++ b/libraries/wuss/window/invalidate.c @@ -65,6 +65,8 @@ int wuss__clip_to_visible(wuss_window_t *window, int nnext, p; occluder = (wuss_window_t *) e; + if (occluder->flags & wuss_WINDOW_HIDDEN) + continue; /* a hidden window occludes nothing */ nnext = 0; for (p = 0; p < ncur; p++) diff --git a/libraries/wuss/window/move.c b/libraries/wuss/window/move.c index 6bb923d4..036dd655 100644 --- a/libraries/wuss/window/move.c +++ b/libraries/wuss/window/move.c @@ -36,6 +36,18 @@ void wuss_window_move(wuss_window_t *window, point_t p) titlebar_height = wuss__titlebar_height(window); before = window->visible; + /* a hidden window has nothing on screen to slide and must paint nothing; + * just translate its footprint so it is in place when shown again */ + if (window->flags & wuss_WINDOW_HIDDEN) + { + window->visible.x0 = p.x - outline_px; + window->visible.y0 = p.y - outline_px - titlebar_height; + window->visible.x1 = window->visible.x0 + width; + window->visible.y1 = window->visible.y0 + height; + wuss__notify_open(window); + return; + } + /* The clean (non-occluded) pieces of "before" are genuinely this * window's own rendering; whatever isn't clean is hidden behind some * other window and has no valid pixels of this window's content to diff --git a/libraries/wuss/window/set-hidden.c b/libraries/wuss/window/set-hidden.c new file mode 100644 index 00000000..76de1528 --- /dev/null +++ b/libraries/wuss/window/set-hidden.c @@ -0,0 +1,25 @@ +/* wuss/window/set-hidden.c -- wuss - minimal window manager */ + +#include "../impl.h" + +void wuss_window_set_hidden(wuss_window_t *window, int hidden) +{ + int was_hidden; + + was_hidden = (window->flags & wuss_WINDOW_HIDDEN) != 0; + if (!was_hidden == !hidden) + return; /* no change */ + + if (hidden) + { + /* still on screen: repaint its footprint now, then mark it gone */ + wuss_invalidate(window->wuss, &window->visible); + window->flags |= wuss_WINDOW_HIDDEN; + } + else + { + /* mark it back, then repaint its footprint so it appears */ + window->flags &= (wuss_window_flags_t) ~wuss_WINDOW_HIDDEN; + wuss_invalidate(window->wuss, &window->visible); + } +} From 0b16dae861bf95c7c3a3954259257d9086180a2c Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Wed, 2 Sep 2026 11:08:14 +0100 Subject: [PATCH 171/287] docs(wuss): changelog the window-as-submenu feature Covers db8d9af: wuss_menu_item_t::window, wuss_window_set_hidden() and the wuss_WINDOW_HIDDEN create flag. --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 30b41f9c..92684e80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,16 @@ _Unreleased_ until one is cut. `menu_create_from_desc` syntax: `,` between items, leading `|` for a dashed separator, `{ ... }` submenus, `!` tick, `~` shade, `>` and `%s` varargs). The whole tree is one owned allocation graph freed by `wuss_menu_destroy()`. +- `wuss_menu_item_t::window` — a menu row may carry a caller-owned + `wuss_window_t` instead of a `submenu` (the two are mutually exclusive). + Hovering the row shows that window where a submenu would open, using the same + anchor maths; leaving the row, a click outside, a leaf SELECT elsewhere or + `wuss_destroy()` hide it again rather than close it, so the same handle is + reused on the next hover. Create the window with `wuss_WINDOW_HIDDEN`. +- `wuss_window_set_hidden()` and the `wuss_WINDOW_HIDDEN` create flag — a hidden + window keeps its z-order slot but is not drawn, not hit-tested and occludes + nothing. `wuss_window_move()` still works on it (translate only, no blit) so + it can be parked and re-shown in position. - `wuss_set_palette()` — swap the system palette mid-session. Copies the new palette in, refreshes the cached nearest-black/white indices, broadcasts a new `wuss_EVENT_PALETTE` to every window's task so it can recache From 161ddc5bdb7b7bed18b28fd2d44fef953ff6a06b Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Wed, 2 Sep 2026 12:06:53 +0100 Subject: [PATCH 172/287] refactor(wuss)!: make wuss_task a registered event target Turn wuss_task_t into an opaque, heap-allocated object that owns its windows and is the sole delivery target for their events. Introduces a master wuss_event_kind_t enum with per-recipient subset views, the single dispatch chokepoint wuss__deliver, and QUIT delivered once per wuss_task_destroy. wuss_task_start / wuss_task_stop and the by-value wuss_task_t struct are replaced by wuss_task_create / wuss_task_destroy taking a wuss_task_desc_t. wuss_create initialises the task list; wuss_destroy sweeps any tasks the caller left registered. BREAKING CHANGE: wuss_task_t is now opaque; wuss_task_start/stop and wuss_event_fn_t are gone. Create tasks with wuss_task_create and a wuss_task_desc_t; destroy with wuss_task_destroy. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- CMakeLists.txt | 5 +- include/wuss/task.h | 189 +++++++++++++++++++++++++--------- include/wuss/wuss.h | 31 +++--- libraries/wuss/create.c | 2 + libraries/wuss/destroy.c | 13 +++ libraries/wuss/impl.h | 51 +++++++-- libraries/wuss/task/create.c | 39 +++++++ libraries/wuss/task/deliver.c | 54 ++++++++++ libraries/wuss/task/destroy.c | 36 +++++++ libraries/wuss/task/start.c | 16 --- libraries/wuss/task/stop.c | 15 --- 11 files changed, 352 insertions(+), 99 deletions(-) create mode 100644 libraries/wuss/task/create.c create mode 100644 libraries/wuss/task/deliver.c create mode 100644 libraries/wuss/task/destroy.c delete mode 100644 libraries/wuss/task/start.c delete mode 100644 libraries/wuss/task/stop.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 472fc25a..10304d9f 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -348,8 +348,9 @@ set(WUSS_CORE_SOURCES libraries/wuss/scroll.c libraries/wuss/scroll-step.c libraries/wuss/set-palette.c - libraries/wuss/task/start.c - libraries/wuss/task/stop.c + libraries/wuss/task/create.c + libraries/wuss/task/destroy.c + libraries/wuss/task/deliver.c libraries/wuss/window/at.c libraries/wuss/window/create.c libraries/wuss/window/create-placed.c diff --git a/include/wuss/task.h b/include/wuss/task.h index b41e650b..eaa971ab 100644 --- a/include/wuss/task.h +++ b/include/wuss/task.h @@ -3,8 +3,14 @@ /** * \file task.h * - * A Wuss task: the content delegate a window hands its drawing and input - * events to, and the events themselves. + * A Wuss task: a registered object that owns windows and is the single + * delivery target for their events, plus the events themselves. + * + * A task is a mini-instance: wuss_task_create / wuss_task_destroy rhyme with + * wuss_create / wuss_destroy. Every window is created against a task (see + * window.h), and that task's one handle callback receives all events for all + * its windows, as well as the app-wide IDLE / PALETTE / MENU_SELECT + * notifications. */ #ifndef WUSS_TASK_H @@ -26,26 +32,67 @@ extern "C" /* ----------------------------------------------------------------------- */ /** - * Which kind of event a wuss_event_t carries; more will be added over time. + * The master event kind. Every event delivered to any recipient carries one + * of these values; the per-recipient views below are strict subsets of it + * with the same integer values, so no translation happens at dispatch. */ typedef enum wuss_event_kind { - wuss_EVENT_IDLE, /**< Wuss has finished its pending tasks. */ - wuss_EVENT_REDRAW, /**< Part of the window's content needs repainting. */ - wuss_EVENT_OPEN, /**< Window moved or resized. */ - wuss_EVENT_CLOSE, /**< Close button clicked; Wuss takes no action itself. */ - wuss_EVENT_MOUSE, /**< Button down/up over the window's content. */ - wuss_EVENT_ICON, /**< A work-area button icon was clicked or hovered. */ - wuss_EVENT_SCROLL, /**< Mouse wheel used over the window's content. */ - wuss_EVENT_QUIT, /**< Task shutting down, via wuss_task_stop. */ - wuss_EVENT_PALETTE /**< System palette changed, via wuss_set_palette; - recache any wuss_nearest_colour selections. */ + wuss_EVENT_REDRAW, /**< Part of a window's content needs repaint. */ + wuss_EVENT_MOUSE, /**< Button down/up/move over window content. */ + wuss_EVENT_ICON, /**< A work-area button icon was clicked or hovered + (window view); or, in the task view, a future + shared/dock element -- reserved, nothing emits + it yet. */ + wuss_EVENT_SCROLL, /**< Mouse wheel used over a window's content. */ + wuss_EVENT_OPEN, /**< A visible window moved or resized. */ + wuss_EVENT_PRE_SHOW, /**< A hidden window is about to become visible; a + non-OK return vetoes the reveal. */ + wuss_EVENT_SHOW, /**< A window has become visible (hidden->visible + transition only, not at create). */ + wuss_EVENT_PRE_CLOSE, /**< A window is about to close via + wuss_window_try_close; a non-OK return vetoes + it. Never fired by wuss_window_close. */ + wuss_EVENT_CLOSE, /**< A window has closed after a successful + wuss_window_try_close. Never fired by + wuss_window_close. */ + wuss_EVENT_IDLE, /**< Wuss has finished its pending work; once per + task per wuss_idle. */ + wuss_EVENT_QUIT, /**< The task is shutting down, via + wuss_task_destroy; its windows are still + alive. */ + wuss_EVENT_PALETTE, /**< System palette changed, via wuss_set_palette; + once per task. Recache any wuss_nearest_colour + selections. */ + wuss_EVENT_MENU_SELECT /**< A leaf menu item was picked; delivered to the + task that opened the menu. */ } wuss_event_kind_t; /** - * An event delivered to a task's handle callback. Only the union member - * matching \c kind is valid. + * The subset of wuss_event_kind_t a window's handle callback can receive. + * Same integer values as the master enum; listed separately for + * documentation and the debug-only dispatch assert. + * + * Members: wuss_EVENT_REDRAW, wuss_EVENT_MOUSE, wuss_EVENT_ICON, + * wuss_EVENT_SCROLL, wuss_EVENT_OPEN, wuss_EVENT_PRE_SHOW, wuss_EVENT_SHOW, + * wuss_EVENT_PRE_CLOSE, wuss_EVENT_CLOSE. + */ +typedef wuss_event_kind_t wuss_window_event_kind_t; + +/** + * The subset of wuss_event_kind_t a task's handle callback can receive with + * 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). + */ +typedef wuss_event_kind_t wuss_task_event_kind_t; + +/** + * An event delivered to a handle callback. Only the union member matching \c + * kind is valid; several kinds carry no data. */ typedef struct wuss_event { @@ -94,11 +141,13 @@ typedef struct wuss_event mouse; /** wuss_EVENT_ICON: delivered instead of wuss_EVENT_MOUSE while the - * pointer is inside a wuss_ICON_TYPE_BUTTON icon's bounding box. Label, - * hidden and disabled icons never raise this -- those clicks fall through - * as wuss_EVENT_MOUSE. action is DOWN/UP/MOVE; button is a set of - * wuss_button_t flags, so test it with '&' rather than comparing for - * equality. */ + * pointer is inside a wuss_ICON_TYPE_BUTTON icon's bounding box. + * Label, hidden and disabled icons never raise this; those clicks + * fall through as wuss_EVENT_MOUSE. action is DOWN/UP/MOVE; button + * is a set of wuss_button_t flags, so test it with '&' rather than + * comparing for equality. In the task view (window == NULL) this is + * reserved for a future shared/dock element and is never currently + * emitted. */ struct { wuss_icon_t *icon; @@ -116,61 +165,105 @@ typedef struct wuss_event } scroll; - /* wuss_EVENT_IDLE, wuss_EVENT_CLOSE, wuss_EVENT_QUIT, - * wuss_EVENT_OPEN and wuss_EVENT_PALETTE carry no data. */ + /** wuss_EVENT_MENU_SELECT: delivered to the task that called + * wuss_menu_open. menu is the (sub)menu the item belongs to; index is + * its position in menu->items; button is the wuss_button_t flags for the + * release -- ADJUST keeps the chain open, SELECT closes it; test with + * '&'. */ + struct + { + const struct wuss_menu *menu; + int index; + wuss_button_t button; + } + menu_select; + + /* 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. */ } data; } wuss_event_t; /** - * Task event callback. + * A window's event callback: receives the wuss_window_event_kind_t subset. * * \param[in] window The window receiving the event. * \param[in] event The event; see wuss_event_t. - * \param[in] task_data As passed to wuss_window_create. + * \param[in] task_data As passed to wuss_task_create. + * \return \ref result_OK on success, else an appropriate result code. For + * wuss_EVENT_PRE_SHOW / wuss_EVENT_PRE_CLOSE a non-OK return vetoes + * the transition. + */ +typedef result_t (wuss_window_fn_t)(wuss_window_t *window, + const wuss_event_t *event, + void *task_data); + +/** + * A task's event callback: receives the wuss_task_event_kind_t subset, + * always with window == NULL. + * + * \param[in] window Always NULL for task-view events; the parameter is + * kept so a single handle can serve both views. + * \param[in] event The event; see wuss_event_t. + * \param[in] task_data As passed to wuss_task_create. * \return \ref result_OK on success, else an appropriate result code. */ -typedef result_t (wuss_event_fn_t)(wuss_window_t *window, - const wuss_event_t *event, - void *task_data); +typedef result_t (wuss_task_fn_t)(wuss_window_t *window, + const wuss_event_t *event, + void *task_data); /** - * A window's content delegate. Copied by value into the window at creation. + * Task creation descriptor. handle serves both the window-view and task-view + * events for every window the task owns. */ -typedef struct wuss_task +typedef struct wuss_task_desc { /** - * NULL => task receives no events; Wuss still fills the content background - * per wuss_window_create's bg. + * Event callback, or NULL for a task that receives no events (its windows + * still get their background filled per wuss_window_create's bg). The one + * callback is invoked for both wuss_window_fn_t-shaped and + * wuss_task_fn_t-shaped events; the two typedefs have the same signature. */ - wuss_event_fn_t *handle; + wuss_window_fn_t *handle; + + /** Opaque pointer passed back to handle. */ + void *task_data; - void *task_data; + /** Optional name, for debugging/tracing; borrowed, not copied. */ + const char *name; } -wuss_task_t; +wuss_task_desc_t; /** - * Build a wuss_task_t from its fields. + * Register a task on a window manager. * - * \param[in] handle Event callback, or NULL for a task that receives no - * events. - * \param[in] task_data Opaque pointer passed back to the callback. - * \return The populated task. + * The task is appended to the manager's task list; app-wide notifications + * (wuss_idle, wuss_set_palette) are delivered to tasks in registration + * order. + * + * \param[in] wuss Window manager. + * \param[in] desc Task descriptor; copied in (name is borrowed). + * \param[out] task Newly registered task. + * \return \ref result_OK on success, \ref result_OOM if the task node could + * not be allocated. */ -wuss_task_t wuss_task_start(wuss_event_fn_t *handle, - void *task_data); +result_t wuss_task_create(wuss_t *wuss, + const wuss_task_desc_t *desc, + wuss_task_t **task); /** - * Notify a window's task that it is shutting down, via wuss_EVENT_QUIT. Not - * called automatically by wuss_window_close; call it first if the task needs - * notice before its window is torn down. + * Unregister and free a task. + * + * Fires one wuss_EVENT_QUIT to the task's handle (its windows are still + * alive), then force-closes every window the task owns via wuss_window_close + * (no wuss_EVENT_PRE_CLOSE / wuss_EVENT_CLOSE), unlinks the task from the + * manager and frees it. * - * \param[in] window Window whose task should be stopped. - * \return \ref result_OK on success, else the result returned by the task's - * handle callback. + * \param[in] doomed Task to destroy. NULL is a no-op. */ -result_t wuss_task_stop(wuss_window_t *window); +void wuss_task_destroy(wuss_task_t *doomed); #ifdef __cplusplus } diff --git a/include/wuss/wuss.h b/include/wuss/wuss.h index 83d2758a..144f6d23 100644 --- a/include/wuss/wuss.h +++ b/include/wuss/wuss.h @@ -45,6 +45,10 @@ typedef struct wuss wuss_t; /** A window. Full API is in window.h. */ typedef struct wuss_window wuss_window_t; +/** A registered task: owns windows, receives their events. Full API is in + * task.h. */ +typedef struct wuss_task wuss_task_t; + /** A work-area icon. Full API is in icon.h, and is compiled only when the * library is built with the WUSS_ICONS option on. */ typedef struct wuss_icon wuss_icon_t; @@ -334,11 +338,12 @@ result_t wuss_create(screen_t *scr, * * Copies \p palette in over the existing one (same semantics as * wuss_create's palette argument), refreshes the cached nearest-black / - * nearest-white indices, broadcasts a \ref wuss_EVENT_PALETTE event to every - * window's task so they can recache any wuss_nearest_colour selections, then - * invalidates the whole screen. The caller is still responsible for the next - * wuss_redraw / wuss_redraw_dirty, and -- on a paletted screen -- for - * updating the screen bitmap's own palette to match. + * nearest-white indices, broadcasts a \ref wuss_EVENT_PALETTE event once to + * every registered task (in registration order, window == NULL) so they can + * recache any wuss_nearest_colour selections, then invalidates the whole + * screen. The caller is still responsible for the next wuss_redraw / + * wuss_redraw_dirty, and -- on a paletted screen -- for updating the screen + * bitmap's own palette to match. * * \param[in] wuss Window manager. * \param[in] palette New system palette, copied in. @@ -348,7 +353,8 @@ result_t wuss_create(screen_t *scr, * not match the current palette length, \ref result_WUSS_BAD_COLOUR * if a configured furniture/bevel/backdrop colour index is now out * of range (in which case the palette is left unchanged), else the - * first non-OK result returned by a task's handle callback. + * first non-OK result returned by a task's handle callback + * (iteration still continues past it). */ result_t wuss_set_palette(wuss_t *wuss, const colour_t *palette, @@ -517,14 +523,15 @@ result_t wuss_scroll(wuss_t *wuss, wuss_window_t **hit); /** - * Broadcast a wuss_EVENT_IDLE event to every window's task, in z-order. - * Intended to be called once per main-loop iteration, after other pending - * input has been handled, so tasks can drive their own animation/timers - * without the caller stepping each one individually. + * Broadcast a wuss_EVENT_IDLE event once to every registered task, in + * registration order (window == NULL). Intended to be called once per + * main-loop iteration, after other pending input has been handled, so tasks + * can drive their own animation/timers -- iterating their own window lists + * -- without the caller stepping each one individually. * - * \param[in] wuss Window manager whose windows' tasks should go idle. + * \param[in] wuss Window manager whose tasks should go idle. * \return \ref result_OK on success, else the first non-OK result returned - * by a task's handle callback. + * by a task's handle callback (iteration still continues past it). */ result_t wuss_idle(wuss_t *wuss); diff --git a/libraries/wuss/create.c b/libraries/wuss/create.c index d7040f95..e8457616 100644 --- a/libraries/wuss/create.c +++ b/libraries/wuss/create.c @@ -230,6 +230,7 @@ result_t wuss_create(screen_t *scr, #endif #ifdef WUSS_MENUS w->menu_chain = NULL; + w->menu_task = NULL; w->menu_eat_up = 0; #endif @@ -242,6 +243,7 @@ result_t wuss_create(screen_t *scr, w->pointer.y = 0; list_init(&w->z_order); + list_init(&w->tasks); *wuss = w; diff --git a/libraries/wuss/destroy.c b/libraries/wuss/destroy.c index dd974e56..769534a9 100644 --- a/libraries/wuss/destroy.c +++ b/libraries/wuss/destroy.c @@ -34,6 +34,19 @@ void wuss_destroy(wuss_t *doomed) e = next; } + /* Windows are gone; free any tasks the caller left registered. No QUIT -- + * wuss_destroy is the whole-manager teardown, past the point of + * notification. */ + e = doomed->tasks.next; + while (e != NULL) + { + list_t *next; + + next = e->next; + 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 */ diff --git a/libraries/wuss/impl.h b/libraries/wuss/impl.h index 8c857328..f4d8e988 100644 --- a/libraries/wuss/impl.h +++ b/libraries/wuss/impl.h @@ -3,6 +3,8 @@ #ifndef IMPL_H #define IMPL_H +#include <stddef.h> + #include "base/utils.h" #include "datastruct/list.h" #include "geom/box.h" @@ -14,6 +16,7 @@ #include "wuss/wuss.h" #include "wuss/window.h" +#include "wuss/task.h" #ifdef WUSS_FURNITURE #include "furniture.h" @@ -57,6 +60,20 @@ typedef enum wuss_window_state } wuss_window_state_t; +/* A registered task. Owns a list of windows (linked through each window's + * second embedded list_t, task_link) and is the single delivery target for + * their events plus the app-wide IDLE/PALETTE/MENU_SELECT notifications. + * Tasks are chained through wuss::tasks in registration order. */ +struct wuss_task +{ + list_t link; /* anchor node in wuss::tasks; must be first */ + wuss_t *wuss; + wuss_window_fn_t *handle; /* nullable: no events delivered */ + void *task_data; + const char *name; /* borrowed, may be NULL */ + list_t windows; /* anchor; nodes are window->task_link */ +}; + struct wuss { screen_t *scr; @@ -86,6 +103,8 @@ struct wuss int titlebar_height; #endif list_t z_order; /* anchor; head = topmost window */ + list_t tasks; /* anchor; registered tasks, in + * wuss_task_create order */ #ifdef WUSS_FURNITURE struct wuss__furniture furniture; /* drag state */ const wuss__furniture_ops_t *furniture_ops; /* core->furniture dispatch; @@ -116,6 +135,11 @@ struct wuss * menu chain, NULL when none open; * consulted by mouse-click.c to * dismiss on a click-away */ + wuss_task_t *menu_task; /* internal task owning every + * borderless menu window; created + * lazily on first wuss_menu_open, + * freed by wuss_destroy's task + * sweep */ int menu_eat_up; /* a menu opened on the last * MOUSE_DOWN; swallow its matching * MOUSE_UP so the release does not @@ -125,11 +149,14 @@ struct wuss struct wuss_window { - list_t link; /* must be first member */ + list_t link; /* z-order chain; must be first member */ + list_t task_link; /* chain through the owning task's window + * list (wuss_task::windows) */ wuss_t *wuss; box_t visible; /* full on-screen footprint: content expanded * outward by any titlebar/outline furniture */ - wuss_task_t task; + wuss_task_t *task; /* owning task, set at create, immutable; never + * NULL */ wuss_backdrop_t bg; /* content background; colour==wuss_NO_BACKGROUND: none */ wuss_window_flags_t flags; point_t scroll; /* offset into virtual content space of the @@ -241,6 +268,21 @@ int wuss__order_pieces(const box_t *clean, int n, int *order); +/* Recover the owning wuss_window_t from a node in a task's window list + * (window->task_link). task_link is not the first member, so a plain cast + * won't do. */ +#define wuss__window_from_task_link(node) \ + ((wuss_window_t *) ((char *) (node) - offsetof(struct wuss_window, task_link))) + +/* The single dispatch chokepoint. Delivers "ev" to "task"'s handle, passing + * "win_or_null" as the window (NULL for task-view events). No-op (returns + * result_OK) when task is NULL or has no handle. A debug build also asserts + * ev->kind is valid for the recipient view. Every emit site routes through + * here. */ +result_t wuss__deliver(wuss_task_t *task, + wuss_window_t *win_or_null, + const wuss_event_t *ev); + /* 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. */ @@ -248,11 +290,8 @@ static inline void wuss__notify_open(wuss_window_t *window) { wuss_event_t event; - if (window->task.handle == NULL) - return; - event.kind = wuss_EVENT_OPEN; - (void) window->task.handle(window, &event, window->task.task_data); + (void) wuss__deliver(window->task, window, &event); } static inline int wuss__size_ok(int width, int height) diff --git a/libraries/wuss/task/create.c b/libraries/wuss/task/create.c new file mode 100644 index 00000000..935fbad6 --- /dev/null +++ b/libraries/wuss/task/create.c @@ -0,0 +1,39 @@ +/* wuss/task/create.c -- register a task on a window manager */ + +#include <assert.h> +#include <stddef.h> + +#ifdef FORTIFY +#include "fortify/fortify.h" +#endif + +#include "wuss/task.h" + +#include "../impl.h" + +result_t wuss_task_create(wuss_t *wuss, + const wuss_task_desc_t *desc, + wuss_task_t **task) +{ + wuss_task_t *t; + + assert(wuss != NULL); + assert(desc != NULL); + assert(task != NULL); + + t = wuss__malloc(wuss, sizeof(*t)); + if (t == NULL) + return result_OOM; + + t->wuss = wuss; + t->handle = desc->handle; + t->task_data = desc->task_data; + t->name = desc->name; + list_init(&t->windows); + + list_add_to_tail(&wuss->tasks, &t->link); + + *task = t; + + return result_OK; +} diff --git a/libraries/wuss/task/deliver.c b/libraries/wuss/task/deliver.c new file mode 100644 index 00000000..6be99e25 --- /dev/null +++ b/libraries/wuss/task/deliver.c @@ -0,0 +1,54 @@ +/* wuss/task/deliver.c -- the single event-dispatch chokepoint */ + +#include <assert.h> +#include <stddef.h> + +#include "wuss/task.h" + +#include "../impl.h" + +#ifndef NDEBUG +/* Is "kind" valid for delivery to a window (win != NULL) or to a task + * (win == NULL)? The two sets are the wuss_window_event_kind_t / + * wuss_task_event_kind_t views documented in task.h. */ +static int wuss__kind_ok_for(wuss_event_kind_t kind, int have_window) +{ + switch (kind) + { + case wuss_EVENT_REDRAW: + case wuss_EVENT_MOUSE: + case wuss_EVENT_SCROLL: + case wuss_EVENT_OPEN: + case wuss_EVENT_PRE_SHOW: + case wuss_EVENT_SHOW: + case wuss_EVENT_PRE_CLOSE: + case wuss_EVENT_CLOSE: + return have_window; + + case wuss_EVENT_IDLE: + case wuss_EVENT_QUIT: + case wuss_EVENT_PALETTE: + case wuss_EVENT_MENU_SELECT: + return !have_window; + + case wuss_EVENT_ICON: + return 1; /* window view: real icon; task view: reserved shared element */ + } + + return 0; +} +#endif + +result_t wuss__deliver(wuss_task_t *task, + wuss_window_t *win_or_null, + const wuss_event_t *ev) +{ + if (task == NULL || task->handle == NULL) + return result_OK; + +#ifndef NDEBUG + assert(wuss__kind_ok_for(ev->kind, win_or_null != NULL)); +#endif + + return task->handle(win_or_null, ev, task->task_data); +} diff --git a/libraries/wuss/task/destroy.c b/libraries/wuss/task/destroy.c new file mode 100644 index 00000000..d34dec79 --- /dev/null +++ b/libraries/wuss/task/destroy.c @@ -0,0 +1,36 @@ +/* wuss/task/destroy.c -- unregister and free a task */ + +#include <stddef.h> + +#ifdef FORTIFY +#include "fortify/fortify.h" +#endif + +#include "wuss/task.h" + +#include "../impl.h" + +void wuss_task_destroy(wuss_task_t *doomed) +{ + wuss_event_t event; + wuss_t *wuss; + list_t *e; + + if (doomed == NULL) + return; + + wuss = doomed->wuss; + + /* One QUIT while every window is still alive. */ + event.kind = wuss_EVENT_QUIT; + (void) wuss__deliver(doomed, NULL, &event); + + /* Force-close every window the task owns, in task-list order. No + * PRE_CLOSE/CLOSE -- wuss_window_close is the unvetoable teardown. Each + * close unlinks the window's task_link, so re-read the head each time. */ + while ((e = doomed->windows.next) != NULL) + wuss_window_close(wuss__window_from_task_link(e)); + + list_remove(&wuss->tasks, &doomed->link); + wuss__free(wuss, doomed); +} diff --git a/libraries/wuss/task/start.c b/libraries/wuss/task/start.c deleted file mode 100644 index f9736b4a..00000000 --- a/libraries/wuss/task/start.c +++ /dev/null @@ -1,16 +0,0 @@ -/* wuss/task/start.c -- wuss - minimal window manager */ - -#include <stddef.h> - -#include "wuss/task.h" - -wuss_task_t wuss_task_start(wuss_event_fn_t *handle, - void *task_data) -{ - wuss_task_t task; - - task.handle = handle; - task.task_data = task_data; - - return task; -} diff --git a/libraries/wuss/task/stop.c b/libraries/wuss/task/stop.c deleted file mode 100644 index deffd847..00000000 --- a/libraries/wuss/task/stop.c +++ /dev/null @@ -1,15 +0,0 @@ -/* wuss/task/stop.c -- wuss - minimal window manager */ - -#include "../impl.h" - -result_t wuss_task_stop(wuss_window_t *window) -{ - wuss_event_t event; - - if (window->task.handle == NULL) - return result_OK; - - event.kind = wuss_EVENT_QUIT; - - return window->task.handle(window, &event, window->task.task_data); -} From 0bee48e0e2bd50ec6b7fde91e829b9e9ac43a5a7 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Wed, 2 Sep 2026 12:07:00 +0100 Subject: [PATCH 173/287] refactor(wuss)!: route all window events through wuss__deliver REDRAW, MOUSE, ICON, SCROLL and OPEN now go through the window's owning task via wuss__deliver instead of a per-window handle. IDLE and PALETTE broadcast once per registered task in registration order, first non-OK return winning. wuss_window_create / wuss_window_create_placed take the owning wuss_task_t (dropping the wuss_t and per-window task-delegate arguments) and link the new window onto the task's window list; close unlinks it. BREAKING CHANGE: wuss_window_create and wuss_window_create_placed no longer take a wuss_t or a task delegate; pass the owning wuss_task_t instead. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- libraries/wuss/idle.c | 14 ++++++-------- libraries/wuss/mouse-click.c | 15 +++++++-------- libraries/wuss/mouse-move.c | 6 +++--- libraries/wuss/redraw.c | 3 +-- libraries/wuss/scroll.c | 8 +------- libraries/wuss/set-palette.c | 12 +++++------- libraries/wuss/window/close.c | 1 + libraries/wuss/window/create-placed.c | 10 ++++++---- libraries/wuss/window/create.c | 14 +++++++------- 9 files changed, 37 insertions(+), 46 deletions(-) diff --git a/libraries/wuss/idle.c b/libraries/wuss/idle.c index e0be5fb6..c4d4165b 100644 --- a/libraries/wuss/idle.c +++ b/libraries/wuss/idle.c @@ -11,17 +11,15 @@ result_t wuss_idle(wuss_t *wuss) event.kind = wuss_EVENT_IDLE; rc = result_OK; - for (e = wuss->z_order.next; e != NULL; e = e->next) + for (e = wuss->tasks.next; e != NULL; e = e->next) { - wuss_window_t *win; - result_t crc; + wuss_task_t *task; + result_t crc; - win = (wuss_window_t *) e; - if (win->task.handle == NULL) - continue; + task = (wuss_task_t *) e; - crc = win->task.handle(win, &event, win->task.task_data); - if (crc != result_OK) + crc = wuss__deliver(task, NULL, &event); + if (crc != result_OK && rc == result_OK) rc = crc; } diff --git a/libraries/wuss/mouse-click.c b/libraries/wuss/mouse-click.c index 68037d55..0868de1d 100644 --- a/libraries/wuss/mouse-click.c +++ b/libraries/wuss/mouse-click.c @@ -85,11 +85,10 @@ result_t wuss_mouse_click(wuss_t *wuss, action == wuss_MOUSE_DOWN && (button & wuss_BUTTON_SELECT)) { - if (win->task.handle == NULL) - return result_OK; - - event.kind = wuss_EVENT_CLOSE; - return win->task.handle(win, &event, win->task.task_data); + /* User close-icon path: routes through try_close so the task gets + * PRE_CLOSE (may veto) then CLOSE. wuss takes no teardown action + * itself -- the task closes the window if it wants to. */ + return wuss_window_try_close(win); } if (region == wuss_FURNITURE_BACK && action == wuss_MOUSE_DOWN) @@ -203,7 +202,7 @@ result_t wuss_mouse_click(wuss_t *wuss, } #endif /* WUSS_FURNITURE */ - if (win->task.handle != NULL) + if (win->task->handle != NULL) { box_t content; point_t doc_point; @@ -246,7 +245,7 @@ result_t wuss_mouse_click(wuss_t *wuss, event.data.icon.icon = icon; event.data.icon.action = action; event.data.icon.button = button; - return win->task.handle(win, &event, win->task.task_data); + return wuss__deliver(win->task, win, &event); } } #endif @@ -255,7 +254,7 @@ result_t wuss_mouse_click(wuss_t *wuss, event.data.mouse.action = action; event.data.mouse.point = doc_point; event.data.mouse.button = button; - return win->task.handle(win, &event, win->task.task_data); + return wuss__deliver(win->task, win, &event); } return result_OK; diff --git a/libraries/wuss/mouse-move.c b/libraries/wuss/mouse-move.c index 32453285..98675359 100644 --- a/libraries/wuss/mouse-move.c +++ b/libraries/wuss/mouse-move.c @@ -65,7 +65,7 @@ result_t wuss_mouse_move(wuss_t *wuss, point_t p, wuss_window_t **hit) } #endif - if (win->task.handle == NULL) + if (win->task->handle == NULL) { #ifdef WUSS_ICONS wuss__icon_set_hover(wuss, NULL); @@ -114,7 +114,7 @@ result_t wuss_mouse_move(wuss_t *wuss, point_t p, wuss_window_t **hit) event.data.icon.icon = icon; event.data.icon.action = wuss_MOUSE_MOVE; event.data.icon.button = wuss_BUTTON_SELECT; - return win->task.handle(win, &event, win->task.task_data); + return wuss__deliver(win->task, win, &event); } } #endif @@ -123,7 +123,7 @@ result_t wuss_mouse_move(wuss_t *wuss, point_t p, wuss_window_t **hit) event.data.mouse.action = wuss_MOUSE_MOVE; event.data.mouse.point = doc_point; event.data.mouse.button = wuss_BUTTON_SELECT; - return win->task.handle(win, &event, win->task.task_data); + return wuss__deliver(win->task, win, &event); } return result_OK; diff --git a/libraries/wuss/redraw.c b/libraries/wuss/redraw.c index 38a0cafc..2b8ce1fb 100644 --- a/libraries/wuss/redraw.c +++ b/libraries/wuss/redraw.c @@ -43,7 +43,6 @@ static void redraw_window(wuss_t *wuss, content.x0 - win->scroll.x, content.y0 - win->scroll.y); - if (win->task.handle != NULL) { wuss_event_t event; result_t crc; @@ -53,7 +52,7 @@ static void redraw_window(wuss_t *wuss, event.data.redraw.content = &pieces[i]; event.data.redraw.bounds = &content; event.data.redraw.scroll = win->scroll; - crc = win->task.handle(win, &event, win->task.task_data); + crc = wuss__deliver(win->task, win, &event); if (crc != result_OK) *rc = crc; } diff --git a/libraries/wuss/scroll.c b/libraries/wuss/scroll.c index d956954c..45dff36d 100644 --- a/libraries/wuss/scroll.c +++ b/libraries/wuss/scroll.c @@ -51,7 +51,6 @@ result_t wuss_scroll(wuss_t *wuss, point_t p, int delta, wuss_window_t **hit) } #endif - if (win->task.handle != NULL) { box_t content; wuss_event_t event; @@ -68,11 +67,6 @@ result_t wuss_scroll(wuss_t *wuss, point_t p, int delta, wuss_window_t **hit) wuss__scroll_step(win, POINT(0, delta)); wuss__scroll_rehover(wuss, win, POINT(x, y)); - return win->task.handle(win, &event, win->task.task_data); + return wuss__deliver(win->task, win, &event); } - - wuss__scroll_step(win, POINT(0, delta)); - wuss__scroll_rehover(wuss, win, POINT(x, y)); - - return result_OK; } diff --git a/libraries/wuss/set-palette.c b/libraries/wuss/set-palette.c index 6a550a7a..b1a7645b 100644 --- a/libraries/wuss/set-palette.c +++ b/libraries/wuss/set-palette.c @@ -32,16 +32,14 @@ result_t wuss_set_palette(wuss_t *wuss, rc = result_OK; event.kind = wuss_EVENT_PALETTE; - for (e = wuss->z_order.next; e != NULL; e = e->next) + for (e = wuss->tasks.next; e != NULL; e = e->next) { - wuss_window_t *win; - result_t crc; + wuss_task_t *task; + result_t crc; - win = (wuss_window_t *) e; - if (win->task.handle == NULL) - continue; + task = (wuss_task_t *) e; - crc = win->task.handle(win, &event, win->task.task_data); + crc = wuss__deliver(task, NULL, &event); if (crc != result_OK && rc == result_OK) rc = crc; } diff --git a/libraries/wuss/window/close.c b/libraries/wuss/window/close.c index 91c32ede..15e10e4b 100644 --- a/libraries/wuss/window/close.c +++ b/libraries/wuss/window/close.c @@ -32,6 +32,7 @@ void wuss_window_close(wuss_window_t *doomed) wuss__invalidate_clipped(doomed, &doomed->visible); list_remove(&wuss->z_order, &doomed->link); + list_remove(&doomed->task->windows, &doomed->task_link); #ifdef WUSS_ICONS wuss__icons_free(doomed); diff --git a/libraries/wuss/window/create-placed.c b/libraries/wuss/window/create-placed.c index 2ff3e00c..f3484177 100644 --- a/libraries/wuss/window/create-placed.c +++ b/libraries/wuss/window/create-placed.c @@ -74,16 +74,16 @@ static void next_cascade(wuss_t *wuss, int fw, int fh, point_t *pos) wuss->cascade.y += step; } -result_t wuss_window_create_placed(wuss_t *wuss, +result_t wuss_window_create_placed(wuss_task_t *task, size2d_t size, const char *title, wuss_window_flags_t flags, wuss_backdrop_t bg, - const wuss_task_t *task, size2d_t doc, size2d_t min_doc, wuss_window_t **window) { + wuss_t *wuss; result_t rc; int left, top, right, bottom; int fw, fh; @@ -92,9 +92,11 @@ result_t wuss_window_create_placed(wuss_t *wuss, const box_t *slot; int tracked; - assert(wuss != NULL); + assert(task != NULL); assert(window != NULL); + wuss = task->wuss; + if (!wuss__size_ok(size.w, size.h)) return result_WUSS_TOO_SMALL; @@ -157,7 +159,7 @@ result_t wuss_window_create_placed(wuss_t *wuss, consumed.y1 = slot->y1 + WUSS_PLACE_GUTTER; } - rc = wuss_window_create(wuss, &content, title, flags, bg, task, + rc = wuss_window_create(task, &content, title, flags, bg, doc, min_doc, window); if (rc != result_OK) { diff --git a/libraries/wuss/window/create.c b/libraries/wuss/window/create.c index 29936b2a..ee8c7ad7 100644 --- a/libraries/wuss/window/create.c +++ b/libraries/wuss/window/create.c @@ -10,25 +10,27 @@ #include "../impl.h" -result_t wuss_window_create(wuss_t *wuss, +result_t wuss_window_create(wuss_task_t *task, const box_t *content, const char *title, wuss_window_flags_t flags, wuss_backdrop_t bg, - const wuss_task_t *task, size2d_t doc, size2d_t min_doc, wuss_window_t **window) { + wuss_t *wuss; wuss_window_t *win; int width, height, outline_px, titlebar_height; int scr_width, scr_height, dx, dy; point_t carve; - assert(wuss != NULL); + assert(task != NULL); assert(content != NULL); assert(window != NULL); + wuss = task->wuss; + width = content->x1 - content->x0; height = content->y1 - content->y0; if (!wuss__size_ok(width, height)) @@ -105,10 +107,7 @@ result_t wuss_window_create(wuss_t *wuss, box_reset(&win->packed); /* wuss_window_create_placed fills this in after */ - if (task != NULL) - win->task = *task; - else - memset(&win->task, 0, sizeof(win->task)); + win->task = task; if (wuss__validate_backdrop(wuss, &bg) != result_OK) { @@ -132,6 +131,7 @@ result_t wuss_window_create(wuss_t *wuss, #endif list_add_to_head(&wuss->z_order, &win->link); + list_add_to_tail(&task->windows, &win->task_link); if (!(flags & wuss_WINDOW_HIDDEN)) wuss_invalidate(wuss, &win->visible); From 5b12cca627cb5e082263715cf21c81680607d1db Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Wed, 2 Sep 2026 12:07:09 +0100 Subject: [PATCH 174/287] feat(wuss)!: veto-able PRE_SHOW and PRE_CLOSE window events wuss_window_set_hidden now returns result_t: revealing a hidden window first fires PRE_SHOW, and a non-OK return from the task vetoes the reveal (the window stays hidden); a successful reveal is followed by SHOW. New wuss_window_try_close fires PRE_CLOSE (non-OK vetoes), then CLOSE while the window is still alive, then tears the window down. The close furniture icon now calls wuss_window_try_close. wuss_window_close stays the forced, unvetoable teardown and fires neither pre-event. BREAKING CHANGE: wuss_window_set_hidden returns result_t instead of void. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- CMakeLists.txt | 3 +- include/wuss/window.h | 49 ++++++++++++++++++++++-------- libraries/wuss/window/set-hidden.c | 34 +++++++++++++++------ libraries/wuss/window/try-close.c | 31 +++++++++++++++++++ 4 files changed, 95 insertions(+), 22 deletions(-) create mode 100644 libraries/wuss/window/try-close.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 10304d9f..b2152729 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -364,7 +364,8 @@ set(WUSS_CORE_SOURCES libraries/wuss/window/restack.c libraries/wuss/window/set-background.c libraries/wuss/window/set-hidden.c - libraries/wuss/window/set-scroll.c) + libraries/wuss/window/set-scroll.c + libraries/wuss/window/try-close.c) set(WUSS_FURNITURE_SOURCES libraries/wuss/furniture/back-box.c diff --git a/include/wuss/window.h b/include/wuss/window.h index cdcca47a..b00d734c 100644 --- a/include/wuss/window.h +++ b/include/wuss/window.h @@ -39,7 +39,9 @@ extern "C" * wider or taller than the screen keeps its top-left corner on-screen * instead. The bottom/right edges are not clamped. * - * \param[in] wuss Window manager to create the window on. + * \param[in] task Owning task; the window is created on task's window + * manager and delivers all its events to task's handle. + * Immutable once set. * \param[in] content Requested content-area bounds, screen space. Copied * in. * \param[in] title Titlebar label, or NULL for none. Copied in, truncated @@ -56,8 +58,6 @@ extern "C" * phased to the window's scroll origin so it stays * locked to the content. Changeable later via * wuss_window_set_background. - * \param[in] task Content delegate. Copied in. May be NULL for a window - * with no content handling. * \param[in] doc Virtual document extent, for the scrollbars' sausage * proportions; pass content's own width and height for a * window with nothing to scroll. Also the ceiling a @@ -74,12 +74,11 @@ extern "C" * any of bg's colours are out of range for the palette, or another * appropriate result code. */ -result_t wuss_window_create(wuss_t *wuss, +result_t wuss_window_create(wuss_task_t *task, const box_t *content, const char *title, wuss_window_flags_t flags, wuss_backdrop_t bg, - const wuss_task_t *task, size2d_t doc, size2d_t min_doc, wuss_window_t **window); @@ -99,13 +98,12 @@ result_t wuss_window_create(wuss_t *wuss, * (after which Wuss no longer tracks its position). A window dragged by its * titlebar counts as moved. * - * \param[in] wuss Window manager to create the window on. + * \param[in] task Owning task, as wuss_window_create. * \param[in] size Requested content-area size. Width and height must * both be positive. * \param[in] title Titlebar label, as wuss_window_create. * \param[in] flags Appearance flags, as wuss_window_create. * \param[in] bg Content background, as wuss_window_create. - * \param[in] task Content delegate, as wuss_window_create. * \param[in] doc Virtual document extent, as wuss_window_create. * \param[in] min_doc Minimum content size, as wuss_window_create. * \param[out] window Newly created window. Becomes the topmost window. @@ -114,23 +112,42 @@ result_t wuss_window_create(wuss_t *wuss, * tracker could not be created, or another result code from * wuss_window_create. */ -result_t wuss_window_create_placed(wuss_t *wuss, +result_t wuss_window_create_placed(wuss_task_t *task, size2d_t size, const char *title, wuss_window_flags_t flags, wuss_backdrop_t bg, - const wuss_task_t *task, size2d_t doc, size2d_t min_doc, wuss_window_t **window); /** - * Destroy a window. + * Destroy a window. The forced, unvetoable teardown: fires no + * wuss_EVENT_PRE_CLOSE / wuss_EVENT_CLOSE. Used by wuss_task_destroy, tests + * and error paths. For the user close-icon path, which the task can veto, + * see wuss_window_try_close. * - * \param[in] doomed Window to destroy. + * \param[in] doomed Window to destroy. NULL is a no-op. */ void wuss_window_close(wuss_window_t *doomed); +/** + * Attempt to close a window, giving its task a chance to veto. + * + * Fires wuss_EVENT_PRE_CLOSE to the task; a non-OK return vetoes the close, + * the window stays open and that result is returned. Otherwise fires + * wuss_EVENT_CLOSE (while the window is still alive) and then destroys the + * window as per wuss_window_close. + * + * This is the path the titlebar close icon takes. + * + * \param[in] window Window to close. NULL is a no-op returning \ref + * result_OK. + * \return \ref result_OK if the window was closed (or was NULL), else the + * non-OK result the task returned from wuss_EVENT_PRE_CLOSE. + */ +result_t wuss_window_try_close(wuss_window_t *window); + /** * Move a window, preserving its size. * @@ -149,10 +166,18 @@ void wuss_window_move(wuss_window_t *window, point_t p); * visibility invalidates the window's footprint so the next redraw picks up * the change. * + * Showing a hidden window fires wuss_EVENT_PRE_SHOW to the task first: a + * non-OK return vetoes the reveal, the window stays hidden and that result + * is returned. On a successful reveal wuss_EVENT_SHOW follows. Hiding a + * visible window, and any call that is a no-op (already in the requested + * state), fires nothing and returns \ref result_OK. + * * \param[in] window Window to show or hide. * \param[in] hidden Non-zero to hide, zero to show. + * \return \ref result_OK on success or a no-op, else the non-OK result the + * task returned from wuss_EVENT_PRE_SHOW. */ -void wuss_window_set_hidden(wuss_window_t *window, int hidden); +result_t wuss_window_set_hidden(wuss_window_t *window, int hidden); /** * Resize a window's content area, preserving its top-left position. diff --git a/libraries/wuss/window/set-hidden.c b/libraries/wuss/window/set-hidden.c index 76de1528..373c2477 100644 --- a/libraries/wuss/window/set-hidden.c +++ b/libraries/wuss/window/set-hidden.c @@ -1,25 +1,41 @@ /* wuss/window/set-hidden.c -- wuss - minimal window manager */ +#include <stddef.h> + +#include "wuss/task.h" + #include "../impl.h" -void wuss_window_set_hidden(wuss_window_t *window, int hidden) +result_t wuss_window_set_hidden(wuss_window_t *window, int hidden) { - int was_hidden; + wuss_event_t event; + result_t rc; + int was_hidden; was_hidden = (window->flags & wuss_WINDOW_HIDDEN) != 0; if (!was_hidden == !hidden) - return; /* no change */ + return result_OK; /* no change */ if (hidden) { /* still on screen: repaint its footprint now, then mark it gone */ wuss_invalidate(window->wuss, &window->visible); window->flags |= wuss_WINDOW_HIDDEN; + return result_OK; } - else - { - /* mark it back, then repaint its footprint so it appears */ - window->flags &= (wuss_window_flags_t) ~wuss_WINDOW_HIDDEN; - wuss_invalidate(window->wuss, &window->visible); - } + + /* hidden -> visible: PRE_SHOW may veto, leaving the window hidden. */ + event.kind = wuss_EVENT_PRE_SHOW; + rc = wuss__deliver(window->task, window, &event); + if (rc != result_OK) + return rc; + + /* mark it back, repaint its footprint so it appears, then SHOW */ + window->flags &= (wuss_window_flags_t) ~wuss_WINDOW_HIDDEN; + wuss_invalidate(window->wuss, &window->visible); + + event.kind = wuss_EVENT_SHOW; + (void) wuss__deliver(window->task, window, &event); + + return result_OK; } diff --git a/libraries/wuss/window/try-close.c b/libraries/wuss/window/try-close.c new file mode 100644 index 00000000..d8a14a4f --- /dev/null +++ b/libraries/wuss/window/try-close.c @@ -0,0 +1,31 @@ +/* wuss/window/try-close.c -- veto-checked window close */ + +#include <stddef.h> + +#include "wuss/task.h" + +#include "../impl.h" + +result_t wuss_window_try_close(wuss_window_t *window) +{ + wuss_event_t event; + result_t rc; + + if (window == NULL) + return result_OK; + + /* PRE_CLOSE: a non-OK return from the task vetoes the close; the window + * stays open and no CLOSE is sent. */ + event.kind = wuss_EVENT_PRE_CLOSE; + rc = wuss__deliver(window->task, window, &event); + if (rc != result_OK) + return rc; + + /* CLOSE while the window is still alive, then the unvetoable teardown. */ + event.kind = wuss_EVENT_CLOSE; + (void) wuss__deliver(window->task, window, &event); + + wuss_window_close(window); + + return result_OK; +} From b42797cc9c34604745d9b1853156a138590bdaa7 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Wed, 2 Sep 2026 12:07:15 +0100 Subject: [PATCH 175/287] refactor(wuss)!: fold menu selection into the task handler A picked leaf menu item is now delivered as wuss_EVENT_MENU_SELECT to the task that opened the menu, carrying the (sub)menu, item index and release button in data.menu_select. wuss_menu_open takes that wuss_task_t instead of a wuss_menu_select_fn_t callback and context. Menu windows are owned by a single lazily-created internal task (wuss->menu_task); the firing menu node is recovered by walking the open chain for the matching window. BREAKING CHANGE: wuss_menu_open takes a wuss_task_t, not a select callback; wuss_menu_select_fn_t is gone. Handle selection in the task's wuss_EVENT_MENU_SELECT case. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- include/wuss/menu.h | 39 +++------ libraries/wuss/menu.h | 6 +- libraries/wuss/menu/menu.c | 163 ++++++++++++++++++++++++------------- 3 files changed, 124 insertions(+), 84 deletions(-) diff --git a/include/wuss/menu.h b/include/wuss/menu.h index 6619b380..6b8b07b4 100644 --- a/include/wuss/menu.h +++ b/include/wuss/menu.h @@ -80,20 +80,6 @@ wuss_menu_t; /** Opaque handle to an open menu chain. */ typedef struct wuss__menu *wuss_menu_handle_t; -/** - * Called when the pointer is released over a selectable leaf item. - * - * \param[in] menu The menu the item belongs to (a submenu, if nested). - * \param[in] index Index of the item within \c menu->items. - * \param[in] button wuss_button_t flags for the release; test with '&'. - * ADJUST keeps the chain open, SELECT closes it. - * \param[in] ctx As passed to wuss_menu_open. - */ -typedef void (wuss_menu_select_fn_t)(const wuss_menu_t *menu, - int index, - wuss_button_t button, - void *ctx); - /* ----------------------------------------------------------------------- */ /** @@ -102,20 +88,21 @@ typedef void (wuss_menu_select_fn_t)(const wuss_menu_t *menu, * a leaf is SELECT-picked, a click lands outside every menu window, or * wuss_menu_close is called. * - * \param[in] wuss Window manager. - * \param[in] menu Menu to show; borrowed, must outlive the open chain. - * \param[in] at Where to put the menu's top-left, screen space. - * \param[in] on_select Leaf-selection callback, or NULL. - * \param[in] ctx Opaque pointer passed back to \p on_select. - * \param[out] out Filled with the chain handle, or NULL if not wanted. + * When a leaf item is released over, a wuss_EVENT_MENU_SELECT event is + * delivered to \p task's handle (with window == NULL); its data.menu_select + * carries the (sub)menu, the item index and the release button. + * + * \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. + * \param[in] at Where to put the menu's top-left, screen space. + * \param[out] out Filled with the chain handle, or NULL if not wanted. * \return \ref result_OK, \ref result_OOM, or a wuss_window_create code. */ -result_t wuss_menu_open(wuss_t *wuss, - const wuss_menu_t *menu, - point_t at, - wuss_menu_select_fn_t *on_select, - void *ctx, - wuss_menu_handle_t *out); +result_t wuss_menu_open(wuss_task_t *task, + const wuss_menu_t *menu, + point_t at, + wuss_menu_handle_t *out); /** Close a menu chain and every window in it. Safe to pass a stale or NULL * handle. */ diff --git a/libraries/wuss/menu.h b/libraries/wuss/menu.h index 4d5a5b1e..6d435ab8 100644 --- a/libraries/wuss/menu.h +++ b/libraries/wuss/menu.h @@ -14,6 +14,10 @@ struct wuss__menu { wuss_t *wuss; + wuss_task_t *owner; /* task that opened the chain (from + * wuss_menu_open); MENU_SELECT is + * delivered here. Only meaningful on the + * root; children copy it for convenience */ wuss_window_t *window; /* the borderless menu window; for a * borrowed-window level (see borrowed) the * caller's own window instead */ @@ -24,8 +28,6 @@ struct wuss__menu * level */ struct wuss__menu *parent; /* level this one opened from, NULL at root */ struct wuss__menu *child; /* open submenu level, NULL when none */ - wuss_menu_select_fn_t *on_select; - void *ctx; int open_index; /* item index whose submenu `child` is, * -1 when no child open */ int borrowed; /* 1: `window` is a caller-owned window diff --git a/libraries/wuss/menu/menu.c b/libraries/wuss/menu/menu.c index 9db1d39e..78e326e9 100644 --- a/libraries/wuss/menu/menu.c +++ b/libraries/wuss/menu/menu.c @@ -2,6 +2,7 @@ #include <assert.h> #include <limits.h> +#include <stddef.h> #include <stdlib.h> #include <string.h> @@ -33,13 +34,51 @@ /* ----------------------------------------------------------------------- */ -static result_t wuss__menu_spawn(wuss_t *wuss, - const wuss_menu_t *menu, - point_t at, - struct wuss__menu *parent, - wuss_menu_select_fn_t *on_select, - void *ctx, - struct wuss__menu **out); +static result_t wuss__menu_handle(wuss_window_t *window, + const wuss_event_t *event, + void *task_data); + +static result_t wuss__menu_spawn(wuss_t *wuss, + wuss_task_t *owner, + const wuss_menu_t *menu, + point_t at, + struct wuss__menu *parent, + struct wuss__menu **out); + +/* The internal task that owns every borderless menu window, created on the + * first wuss_menu_open of a session. */ +static wuss_task_t *wuss__menu_task(wuss_t *wuss) +{ + wuss_task_desc_t desc; + + if (wuss->menu_task != NULL) + return wuss->menu_task; + + desc.handle = wuss__menu_handle; + desc.task_data = NULL; /* per-window node is found by walking the chain */ + desc.name = "wuss:menu"; + + if (wuss_task_create(wuss, &desc, &wuss->menu_task) != result_OK) + return NULL; + + return wuss->menu_task; +} + +/* Find the open chain node whose window is `window` (the menu window whose + * delegate fired), or NULL. Borrowed-window levels never fire this handler + * (their windows keep the caller's task), so a match is always a real menu + * level with icons. */ +static struct wuss__menu *wuss__menu_node_for(wuss_t *wuss, + const wuss_window_t *window) +{ + struct wuss__menu *node; + + for (node = wuss->menu_chain; node != NULL; node = node->child) + if (node->window == window) + return node; + + return NULL; +} /* Close `node` and everything it opened, leaf-first. The caller clears any * parent's child pointer. */ @@ -86,7 +125,8 @@ static point_t wuss__submenu_anchor(struct wuss__menu *self, } /* Open item `index`'s borrowed window as level `self->child`: position it - * where a submenu would appear, un-hide it, bring it to the front. */ + * where a submenu would appear, un-hide it, bring it to the front. A + * PRE_SHOW veto from the window's own task leaves the row unopened. */ static result_t wuss__menu_open_window(struct wuss__menu *self, int index) { struct wuss__menu *node; @@ -100,19 +140,22 @@ static result_t wuss__menu_open_window(struct wuss__menu *self, int index) return result_OOM; node->wuss = self->wuss; + node->owner = self->owner; node->window = win; node->menu = NULL; node->icons = NULL; node->parent = self; node->child = NULL; - node->on_select = self->on_select; - node->ctx = self->ctx; node->open_index = -1; node->borrowed = 1; at = wuss__submenu_anchor(self, self->icons[index]); wuss_window_move(win, at); - wuss_window_set_hidden(win, 0); + if (wuss_window_set_hidden(win, 0) != result_OK) + { + wuss__free(self->wuss, node); /* vetoed: row stays unopened */ + return result_OK; + } wuss_window_restack(win, wuss_ZORDER_FRONT); self->child = node; @@ -121,7 +164,8 @@ static result_t wuss__menu_open_window(struct wuss__menu *self, int index) /* ----------------------------------------------------------------------- */ -/* The menu window's task delegate. task_data is the struct wuss__menu. */ +/* The menu window's task delegate. Shared across every borderless menu + * window; the firing level is located by its window. */ static result_t wuss__menu_handle(wuss_window_t *window, const wuss_event_t *event, void *task_data) @@ -129,16 +173,20 @@ static result_t wuss__menu_handle(wuss_window_t *window, struct wuss__menu *self; const wuss_icon_t *icon; const wuss_menu_item_t *item; + wuss_t *wuss; int index; int i; - NOT_USED(window); - - self = task_data; + NOT_USED(task_data); if (event->kind != wuss_EVENT_ICON) return result_OK; + wuss = window->wuss; + self = wuss__menu_node_for(wuss, window); + if (self == NULL) + return result_OK; + icon = event->data.icon.icon; index = -1; for (i = 0; i < self->menu->nitems; i++) @@ -177,7 +225,7 @@ static result_t wuss__menu_handle(wuss_window_t *window, * child's own titlebar sits above `at`. */ if (item->window != NULL) { - if (wuss__menu_open_window(self, index) == result_OK) + if (wuss__menu_open_window(self, index) == result_OK && self->child != NULL) self->open_index = index; return result_OK; } @@ -186,8 +234,8 @@ static result_t wuss__menu_handle(wuss_window_t *window, return result_OK; at = wuss__submenu_anchor(self, icon); - if (wuss__menu_spawn(self->wuss, item->submenu, at, self, - self->on_select, self->ctx, &self->child) == result_OK) + if (wuss__menu_spawn(self->wuss, self->owner, item->submenu, at, self, + &self->child) == result_OK) self->open_index = index; return result_OK; @@ -195,36 +243,37 @@ static result_t wuss__menu_handle(wuss_window_t *window, if (event->data.icon.action == wuss_MOUSE_UP) { - wuss_button_t button; - struct wuss__menu *root; - wuss_menu_select_fn_t *on_select; - const wuss_menu_t *menu; - void *ctx; + wuss_button_t button; + struct wuss__menu *root; + wuss_task_t *owner; + const wuss_menu_t *menu; + wuss_event_t sel; button = event->data.icon.button; if (item->submenu != NULL || item->window != NULL) return result_OK; /* a submenu/window row opens on hover, not a pick */ - /* Capture what the callback needs, then tear the chain down *before* - * invoking it: on_select may itself open a new menu (freeing this one), - * so `self` must not be touched afterwards. */ - on_select = self->on_select; - menu = self->menu; - ctx = self->ctx; + /* Capture what the notification needs, then tear the chain down *before* + * delivering it: the task's MENU_SELECT handler may itself open a new + * menu (freeing this one), so `self` must not be touched afterwards. */ + root = self; + while (root->parent != NULL) + root = root->parent; + owner = root->owner; + menu = self->menu; if (!(button & wuss_BUTTON_ADJUST)) { - root = self; - while (root->parent != NULL) - root = root->parent; - root->wuss->menu_chain = NULL; wuss__menu_close_from(root); } - if (on_select != NULL) - on_select(menu, index, button, ctx); + sel.kind = wuss_EVENT_MENU_SELECT; + sel.data.menu_select.menu = menu; + sel.data.menu_select.index = index; + sel.data.menu_select.button = button; + (void) wuss__deliver(owner, NULL, &sel); } return result_OK; @@ -234,18 +283,17 @@ static result_t wuss__menu_handle(wuss_window_t *window, /* Measure the menu, create its borderless window and one MENU_ENTRY icon per * item, and hang the node off *out. */ -static result_t wuss__menu_spawn(wuss_t *wuss, - const wuss_menu_t *menu, - point_t at, - struct wuss__menu *parent, - wuss_menu_select_fn_t *on_select, - void *ctx, - struct wuss__menu **out) +static result_t wuss__menu_spawn(wuss_t *wuss, + wuss_task_t *owner, + const wuss_menu_t *menu, + point_t at, + struct wuss__menu *parent, + struct wuss__menu **out) { struct wuss__menu *node; + wuss_task_t *menu_task; wuss_icon_spec_t *specs; wuss_icon_t **made; - wuss_task_t task; result_t rc; int fh; int pitch; @@ -274,6 +322,10 @@ static result_t wuss__menu_spawn(wuss_t *wuss, if (menu->nitems <= 0) return result_WUSS_BAD_ICON; + menu_task = wuss__menu_task(wuss); + if (menu_task == NULL) + return result_OOM; + menu_flags = wuss_WINDOW_NO_CLOSE | wuss_WINDOW_NO_BACK | wuss_WINDOW_NO_TOGGLE_SIZE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL @@ -366,12 +418,11 @@ static result_t wuss__menu_spawn(wuss_t *wuss, memset(node->icons, 0, (size_t) menu->nitems * sizeof(*node->icons)); node->wuss = wuss; + node->owner = owner; node->window = NULL; node->menu = menu; node->parent = parent; node->child = NULL; - node->on_select = on_select; - node->ctx = ctx; node->open_index = -1; node->borrowed = 0; @@ -428,7 +479,6 @@ static result_t wuss__menu_spawn(wuss_t *wuss, * window shows, so its own resize floor never exceeds what fits. */ doc = SIZE2D(width, doc_h); min_doc = SIZE2D(width, height); - task = wuss_task_start(wuss__menu_handle, node); /* `at` is the content top-left, already clamped on screen above. Create the * window there directly -- creating it elsewhere and wuss_window_move-ing it @@ -439,11 +489,11 @@ static result_t wuss__menu_spawn(wuss_t *wuss, content.x1 = at.x + width; content.y1 = at.y + height; - rc = wuss_window_create(wuss, &content, + rc = wuss_window_create(menu_task, &content, menu->title ? menu->title : "", menu_flags, wuss_BACKDROP_COLOUR(wuss->palettecache.white), - &task, doc, min_doc, &node->window); + doc, min_doc, &node->window); if (rc != result_OK) { wuss__free(wuss, made); @@ -486,19 +536,20 @@ static result_t wuss__menu_spawn(wuss_t *wuss, /* ----------------------------------------------------------------------- */ -result_t wuss_menu_open(wuss_t *wuss, - const wuss_menu_t *menu, - point_t at, - wuss_menu_select_fn_t *on_select, - void *ctx, - wuss_menu_handle_t *out) +result_t wuss_menu_open(wuss_task_t *task, + const wuss_menu_t *menu, + point_t at, + wuss_menu_handle_t *out) { struct wuss__menu *root; + wuss_t *wuss; result_t rc; - assert(wuss != NULL); + assert(task != NULL); assert(menu != NULL); + wuss = task->wuss; + if (wuss->menu_chain != NULL) { wuss__menu_close_from(wuss->menu_chain); @@ -511,7 +562,7 @@ result_t wuss_menu_open(wuss_t *wuss, at.x -= WUSS_MENU_TICK_W; at.y -= WUSS_MENU_ROW_PAD; - rc = wuss__menu_spawn(wuss, menu, at, NULL, on_select, ctx, &root); + rc = wuss__menu_spawn(wuss, task, menu, at, NULL, &root); if (rc != result_OK) return rc; From 52e3bca79ed18a621a89938683a95c84304ded46 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Wed, 2 Sep 2026 12:59:12 +0100 Subject: [PATCH 176/287] test(wuss): migrate wuss-test to the registered-task event API Port every test task off the removed wuss_task_start/stop pair onto wuss_task_create/wuss_task_destroy, and add coverage for the reworked dispatch: - wuss_window_set_hidden fires a veto-able PRE_SHOW then SHOW; a veto keeps the window hidden and propagates. - The close-icon path runs wuss_window_try_close; a PRE_CLOSE veto keeps the window. - wuss_task_destroy sends one QUIT and force-closes the task's windows. - wuss_set_palette and wuss_idle broadcast once per registered task. Block-local test_task_t delegates are now static: wuss_set_palette and wuss_idle deliver to registered tasks, so a stack-scoped task_data would dangle once its block exits. mouse-click.c gains a comment noting the close-icon path routes through wuss_window_try_close. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- libraries/wuss/mouse-click.c | 6 +- libraries/wuss/test/wuss-test.c | 722 ++++++++++++++++++++------------ 2 files changed, 446 insertions(+), 282 deletions(-) diff --git a/libraries/wuss/mouse-click.c b/libraries/wuss/mouse-click.c index 0868de1d..be676367 100644 --- a/libraries/wuss/mouse-click.c +++ b/libraries/wuss/mouse-click.c @@ -85,9 +85,9 @@ result_t wuss_mouse_click(wuss_t *wuss, action == wuss_MOUSE_DOWN && (button & wuss_BUTTON_SELECT)) { - /* User close-icon path: routes through try_close so the task gets - * PRE_CLOSE (may veto) then CLOSE. wuss takes no teardown action - * itself -- the task closes the window if it wants to. */ + /* User close-icon path: routes through try_close, so the task gets + * PRE_CLOSE (may veto) then CLOSE and, if not vetoed, wuss tears the + * window down. A veto's non-OK return propagates to the caller. */ return wuss_window_try_close(win); } diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 3de12433..deb8d54b 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -20,6 +20,7 @@ #include "geom/box.h" #include "io/path.h" #include "wuss/wuss.h" +#include "wuss/task.h" #include "wuss/window.h" #ifdef WUSS_MENUS #include "wuss/menu.h" @@ -44,9 +45,17 @@ typedef struct test_task wuss_button_t last_button; int last_scroll_x, last_scroll_y; int close_count; + int pre_close_count; + int show_count; + int pre_show_count; int stop_count; int open_count; int palette_count; + int idle_count; + int veto_pre_close; + int veto_pre_show; + int menu_select_count; + int last_menu_index; } test_task_t; @@ -83,6 +92,27 @@ static result_t test_handle(wuss_window_t *window, tc->close_count++; break; + case wuss_EVENT_PRE_CLOSE: + tc->pre_close_count++; + if (tc->veto_pre_close) + return result_BAD_ARG; /* any non-OK return vetoes the close */ + break; + + case wuss_EVENT_SHOW: + tc->show_count++; + break; + + case wuss_EVENT_PRE_SHOW: + tc->pre_show_count++; + if (tc->veto_pre_show) + return result_BAD_ARG; /* any non-OK return vetoes the reveal */ + break; + + case wuss_EVENT_MENU_SELECT: + tc->menu_select_count++; + tc->last_menu_index = event->data.menu_select.index; + break; + case wuss_EVENT_QUIT: tc->stop_count++; break; @@ -95,6 +125,10 @@ static result_t test_handle(wuss_window_t *window, tc->palette_count++; break; + case wuss_EVENT_IDLE: + tc->idle_count++; + break; + default: break; } @@ -147,6 +181,52 @@ static result_t paint_handle(wuss_window_t *window, /* ----------------------------------------------------------------------- */ +/* Most tests declare their test_task_t on the block stack. A registered + * task outlives that block (wuss only sweeps at wuss_destroy), so once the + * block exits the task's task_data dangles. Broadcast events (PALETTE, + * IDLE) walk every registered task and would read that freed stack. + * + * mk_task records every task it makes; reap_test_tasks destroys them all. + * Call reap_test_tasks() before any broadcast test and before + * wuss_destroy, so no stale stack is ever delivered to. */ +#define MK_TASK_MAX 64 +static wuss_task_t *mk_task_reg[MK_TASK_MAX]; +static int mk_task_count; + +/* Register a task in one call. Returns NULL on OOM; callers goto Failure. */ +static wuss_task_t *mk_task(wuss_t *wuss, + wuss_window_fn_t *handle, + void *task_data) +{ + wuss_task_desc_t desc; + wuss_task_t *task; + + desc.handle = handle; + desc.task_data = task_data; + desc.name = "wuss-test"; + + if (wuss_task_create(wuss, &desc, &task) != result_OK) + return NULL; + + if (mk_task_count < MK_TASK_MAX) + mk_task_reg[mk_task_count++] = task; + + return task; +} + +/* Destroy every task mk_task created and forget them. Each + * wuss_task_destroy closes that task's windows and fires one QUIT. */ +static void reap_test_tasks(void) +{ + int i; + + for (i = 0; i < mk_task_count; i++) + wuss_task_destroy(mk_task_reg[i]); + mk_task_count = 0; +} + +/* ----------------------------------------------------------------------- */ + /* Total area covered by the dirty list, counting overlapped pixels once. * Summing each region's area instead would double-count wherever two * invalidations overlap, which they legitimately do. */ @@ -192,7 +272,7 @@ result_t wuss_test(const char *resources) wuss_config_t bad_config; wuss_t *bad_wuss; test_task_t tc_a, tc_b, tc_c, tc_d; - wuss_task_t delegate_a, delegate_b, delegate_c, delegate_d; + wuss_task_t *delegate_a, *delegate_b, *delegate_c, *delegate_d; box_t box_a, box_b, box_c, box_d; wuss_window_t *win_a, *win_b, *win_c, *win_d; wuss_window_t *hit; @@ -253,12 +333,11 @@ result_t wuss_test(const char *resources) box_a.y0 = 0; box_a.x1 = 100; box_a.y1 = 0; /* zero-height content is invalid regardless of furniture */ - rc = wuss_window_create(wuss, + rc = wuss_window_create(mk_task(wuss, NULL, NULL), &box_a, "toosmall", wuss_WINDOW_NONE, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), - NULL, box_size(&box_a), SIZE2D(0, 0), &win_a); @@ -267,47 +346,42 @@ result_t wuss_test(const char *resources) printf("test: create overlapping windows A and B\n"); - tc_a.redraw_count = 0; - tc_a.mouse_count = 0; - tc_a.open_count = 0; - delegate_a.handle = test_handle; - delegate_a.task_data = &tc_a; + memset(&tc_a, 0, sizeof(tc_a)); + delegate_a = mk_task(wuss, test_handle, &tc_a); + if (delegate_a == NULL) goto Failure; box_a.x0 = 0; box_a.y0 = 0; box_a.x1 = 100; box_a.y1 = 100; - rc = wuss_window_create(wuss, + rc = wuss_window_create(delegate_a, &box_a, "A", 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), - &delegate_a, box_size(&box_a), SIZE2D(0, 0), &win_a); if (rc != result_OK) goto Failure; - tc_b.redraw_count = 0; - tc_b.mouse_count = 0; - delegate_b.handle = test_handle; - delegate_b.task_data = &tc_b; + memset(&tc_b, 0, sizeof(tc_b)); + delegate_b = mk_task(wuss, test_handle, &tc_b); + if (delegate_b == NULL) goto Failure; box_b.x0 = 50; box_b.y0 = 50; box_b.x1 = 150; box_b.y1 = 150; - rc = wuss_window_create(wuss, + rc = wuss_window_create(delegate_b, &box_b, "B", 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), - &delegate_b, box_size(&box_b), SIZE2D(0, 0), &win_b); @@ -392,25 +466,32 @@ result_t wuss_test(const char *resources) if (rc != result_OK) goto Failure; - printf("test: clicking a window's close icon sends wuss_EVENT_CLOSE, not a drag\n"); + printf("test: close icon runs wuss_window_try_close; a PRE_CLOSE veto keeps the window\n"); - tc_a.close_count = 0; + tc_a.pre_close_count = 0; + tc_a.close_count = 0; + tc_a.veto_pre_close = 1; rc = wuss_mouse_click(wuss, POINT(6, 11), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* A's close icon */ - if (rc != result_OK) - goto Failure; + if (rc != result_BAD_ARG) + goto Failure; /* the veto's non-OK return propagates out of the click */ if (hit != win_a) goto Failure; - if (tc_a.close_count != 1) - goto Failure; rc = wuss_mouse_click(wuss, POINT(31, 36), wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); /* if the close click had started a drag, this would move A */ if (rc != result_OK) goto Failure; - wuss_window_get_visible_bounds(win_a, &visible); + if (tc_a.pre_close_count != 1) + goto Failure; /* the close icon asked the task first */ + if (tc_a.close_count != 0) + goto Failure; /* vetoed: no CLOSE, window not torn down */ + + wuss_window_get_visible_bounds(win_a, &visible); /* still alive */ if (visible.x0 != 0 || visible.y0 != 0) goto Failure; /* unmoved: no drag was started by the close click */ + tc_a.veto_pre_close = 0; + printf("test: click-to-front changes subsequent overlap hits\n"); tc_a.mouse_count = 0; @@ -617,9 +698,9 @@ result_t wuss_test(const char *resources) * visible top-left (10-1 outline) back to (0,0), then the clamp caps * the content at 200 - 2*1 - 20(titlebar) tall, 200 - 2*1 wide. */ box_big.x0 = 10; box_big.y0 = 10; box_big.x1 = 400; box_big.y1 = 400; - rc = wuss_window_create(wuss, &box_big, "BIG", wuss_WINDOW_NO_VSCROLL | + rc = wuss_window_create(mk_task(wuss, NULL, NULL), &box_big, "BIG", wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), NULL, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), SIZE2D(400, 400), SIZE2D(0, 0), &win_big); if (rc != result_OK) goto Failure; @@ -634,19 +715,18 @@ result_t wuss_test(const char *resources) tc_d.redraw_count = 0; tc_d.mouse_count = 0; - delegate_d.handle = test_handle; - delegate_d.task_data = &tc_d; + delegate_d = mk_task(wuss, test_handle, &tc_d); + if (delegate_d == NULL) goto Failure; box_d.x0 = 0; box_d.y0 = 160; box_d.x1 = 30; box_d.y1 = 175; /* shorter than the 20px titlebar_height, still valid: no titlebar to fit */ - rc = wuss_window_create(wuss, + rc = wuss_window_create(delegate_d, &box_d, "ignored", wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), - &delegate_d, box_size(&box_d), SIZE2D(0, 0), &win_d); @@ -675,26 +755,25 @@ result_t wuss_test(const char *resources) printf("test: content click on a title-less window does not change z-order\n"); { - test_task_t tc_e, tc_f; - wuss_task_t delegate_e, delegate_f; + static test_task_t tc_e, tc_f; + wuss_task_t *delegate_e, *delegate_f; box_t box_e, box_f; wuss_window_t *win_e, *win_f; tc_e.redraw_count = 0; tc_e.mouse_count = 0; - delegate_e.handle = test_handle; - delegate_e.task_data = &tc_e; + delegate_e = mk_task(wuss, test_handle, &tc_e); + if (delegate_e == NULL) goto Failure; box_e.x0 = 100; box_e.y0 = 0; box_e.x1 = 150; box_e.y1 = 50; - rc = wuss_window_create(wuss, + rc = wuss_window_create(delegate_e, &box_e, NULL, wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), - &delegate_e, box_size(&box_e), SIZE2D(0, 0), &win_e); @@ -703,19 +782,18 @@ result_t wuss_test(const char *resources) tc_f.redraw_count = 0; tc_f.mouse_count = 0; - delegate_f.handle = test_handle; - delegate_f.task_data = &tc_f; + delegate_f = mk_task(wuss, test_handle, &tc_f); + if (delegate_f == NULL) goto Failure; box_f.x0 = 130; box_f.y0 = 20; box_f.x1 = 180; box_f.y1 = 70; - rc = wuss_window_create(wuss, + rc = wuss_window_create(delegate_f, &box_f, NULL, wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), - &delegate_f, box_size(&box_f), SIZE2D(0, 0), &win_f); @@ -763,27 +841,26 @@ result_t wuss_test(const char *resources) printf("test: moving/resizing a window entirely behind an occluder has no visible effect\n"); { - test_task_t tc_h, tc_g; - wuss_task_t delegate_h, delegate_g; + static test_task_t tc_h, tc_g; + wuss_task_t *delegate_h, *delegate_g; box_t box_h, box_g; wuss_window_t *win_h, *win_g; int before_h, before_g; tc_h.redraw_count = 0; tc_h.mouse_count = 0; - delegate_h.handle = test_handle; - delegate_h.task_data = &tc_h; + delegate_h = mk_task(wuss, test_handle, &tc_h); + if (delegate_h == NULL) goto Failure; box_h.x0 = 10; box_h.y0 = 10; box_h.x1 = 30; box_h.y1 = 30; - rc = wuss_window_create(wuss, + rc = wuss_window_create(delegate_h, &box_h, NULL, wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), - &delegate_h, box_size(&box_h), SIZE2D(0, 0), &win_h); @@ -792,19 +869,18 @@ result_t wuss_test(const char *resources) tc_g.redraw_count = 0; tc_g.mouse_count = 0; - delegate_g.handle = test_handle; - delegate_g.task_data = &tc_g; + delegate_g = mk_task(wuss, test_handle, &tc_g); + if (delegate_g == NULL) goto Failure; box_g.x0 = 0; box_g.y0 = 0; box_g.x1 = 150; box_g.y1 = 150; /* G is created after H, so G is topmost and fully covers H */ - rc = wuss_window_create(wuss, + rc = wuss_window_create(delegate_g, &box_g, NULL, wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), - &delegate_g, box_size(&box_g), SIZE2D(0, 0), &win_g); @@ -847,26 +923,25 @@ result_t wuss_test(const char *resources) printf("test: bring-to-front only invalidates the newly-uncovered part\n"); { - test_task_t tc_i, tc_j; - wuss_task_t delegate_i, delegate_j; + static test_task_t tc_i, tc_j; + wuss_task_t *delegate_i, *delegate_j; box_t box_i, box_j, dirty; wuss_window_t *win_i, *win_j; tc_i.redraw_count = 0; tc_i.mouse_count = 0; - delegate_i.handle = test_handle; - delegate_i.task_data = &tc_i; + delegate_i = mk_task(wuss, test_handle, &tc_i); + if (delegate_i == NULL) goto Failure; box_i.x0 = 0; box_i.y0 = 0; box_i.x1 = 100; box_i.y1 = 100; - rc = wuss_window_create(wuss, + rc = wuss_window_create(delegate_i, &box_i, NULL, wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), - &delegate_i, box_size(&box_i), SIZE2D(0, 0), &win_i); @@ -875,19 +950,18 @@ result_t wuss_test(const char *resources) tc_j.redraw_count = 0; tc_j.mouse_count = 0; - delegate_j.handle = test_handle; - delegate_j.task_data = &tc_j; + delegate_j = mk_task(wuss, test_handle, &tc_j); + if (delegate_j == NULL) goto Failure; box_j.x0 = 50; box_j.y0 = 0; box_j.x1 = 150; box_j.y1 = 100; /* J created after I, so J is topmost, covering I's right half */ - rc = wuss_window_create(wuss, + rc = wuss_window_create(delegate_j, &box_j, NULL, wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), - &delegate_j, box_size(&box_j), SIZE2D(0, 0), &win_j); @@ -918,27 +992,26 @@ result_t wuss_test(const char *resources) printf("test: dragging off-screen and back on repaints the reappearing edge\n"); { - test_task_t tc_m; - wuss_task_t delegate_m; + static test_task_t tc_m; + wuss_task_t *delegate_m; box_t box_m; wuss_window_t *win_m; int before_m; tc_m.redraw_count = 0; tc_m.mouse_count = 0; - delegate_m.handle = test_handle; - delegate_m.task_data = &tc_m; + delegate_m = mk_task(wuss, test_handle, &tc_m); + if (delegate_m == NULL) goto Failure; box_m.x0 = 10; box_m.y0 = 10; box_m.x1 = 60; box_m.y1 = 60; /* 50x50, fully on-screen, topmost (created last) */ - rc = wuss_window_create(wuss, + rc = wuss_window_create(delegate_m, &box_m, NULL, wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), - &delegate_m, box_size(&box_m), SIZE2D(0, 0), &win_m); @@ -974,24 +1047,23 @@ result_t wuss_test(const char *resources) printf("test: Adjust-click on a window's back icon brings it to front\n"); { - test_task_t tc_g, tc_h; - wuss_task_t delegate_g, delegate_h; + static test_task_t tc_g, tc_h; + wuss_task_t *delegate_g, *delegate_h; box_t box_g, box_h; wuss_window_t *win_g, *win_h; tc_h.redraw_count = 0; tc_h.mouse_count = 0; - delegate_h.handle = test_handle; - delegate_h.task_data = &tc_h; + delegate_h = mk_task(wuss, test_handle, &tc_h); + if (delegate_h == NULL) goto Failure; box_h.x0 = 130; box_h.y0 = 50; box_h.x1 = 190; box_h.y1 = 100; - rc = wuss_window_create(wuss, + rc = wuss_window_create(delegate_h, &box_h, "H", wuss_WINDOW_NONE, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), - &delegate_h, box_size(&box_h), SIZE2D(0, 0), &win_h); @@ -1000,17 +1072,16 @@ result_t wuss_test(const char *resources) tc_g.redraw_count = 0; tc_g.mouse_count = 0; - delegate_g.handle = test_handle; - delegate_g.task_data = &tc_g; + delegate_g = mk_task(wuss, test_handle, &tc_g); + if (delegate_g == NULL) goto Failure; box_g.x0 = 110; box_g.y0 = 30; box_g.x1 = 160; box_g.y1 = 80; - rc = wuss_window_create(wuss, + rc = wuss_window_create(delegate_g, &box_g, "G", wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), - &delegate_g, box_size(&box_g), SIZE2D(0, 0), &win_g); @@ -1116,21 +1187,20 @@ result_t wuss_test(const char *resources) printf("test: drag-resize stops at min_doc, not just WUSS_MIN_CONTENT\n"); { - test_task_t tc_m; - wuss_task_t delegate_m; + static test_task_t tc_m; + wuss_task_t *delegate_m; box_t box_m, content, visible; wuss_window_t *win_m; tc_m.redraw_count = 0; tc_m.mouse_count = 0; - delegate_m.handle = test_handle; - delegate_m.task_data = &tc_m; + delegate_m = mk_task(wuss, test_handle, &tc_m); + if (delegate_m == NULL) goto Failure; 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(wuss, &box_m, "M", wuss_WINDOW_NONE, + rc = wuss_window_create(delegate_m, &box_m, "M", wuss_WINDOW_NONE, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), - &delegate_m, SIZE2D(200, 200), SIZE2D(80, 60), &win_m); if (rc != result_OK) @@ -1163,8 +1233,8 @@ result_t wuss_test(const char *resources) printf("test: toggle-size blits rather than redrawing the whole window\n"); { - test_task_t tc_t; - wuss_task_t delegate_t; + static test_task_t tc_t; + wuss_task_t *delegate_t; box_t box_t_win, before, after, titlebar, toggle; wuss_window_t *win_t; int outline_px, titlebar_height, inset, icon; @@ -1174,14 +1244,13 @@ result_t wuss_test(const char *resources) tc_t.redraw_count = 0; tc_t.mouse_count = 0; - delegate_t.handle = test_handle; - delegate_t.task_data = &tc_t; + delegate_t = mk_task(wuss, test_handle, &tc_t); + if (delegate_t == NULL) goto Failure; 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 200x200 doc */ - rc = wuss_window_create(wuss, &box_t_win, "T", wuss_WINDOW_NONE, + rc = wuss_window_create(delegate_t, &box_t_win, "T", wuss_WINDOW_NONE, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), - &delegate_t, SIZE2D(200, 200), SIZE2D(0, 0), &win_t); if (rc != result_OK) goto Failure; @@ -1331,8 +1400,8 @@ result_t wuss_test(const char *resources) printf("test: toggle-size that forces a scroll re-clamp invalidates the content it's about to redraw at the new offset, not just the blit's edge sliver\n"); { - test_task_t tc_r; - wuss_task_t delegate_r; + static test_task_t tc_r; + wuss_task_t *delegate_r; box_t box_r, before, titlebar, toggle; wuss_window_t *win_r; int outline_px, titlebar_height, inset, icon; @@ -1340,14 +1409,13 @@ result_t wuss_test(const char *resources) tc_r.redraw_count = 0; tc_r.mouse_count = 0; - delegate_r.handle = test_handle; - delegate_r.task_data = &tc_r; + delegate_r = mk_task(wuss, test_handle, &tc_r); + if (delegate_r == NULL) goto Failure; 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(wuss, &box_r, "R", wuss_WINDOW_NONE, + rc = wuss_window_create(delegate_r, &box_r, "R", wuss_WINDOW_NONE, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), - &delegate_r, SIZE2D(70, 70), SIZE2D(0, 0), &win_r); if (rc != result_OK) goto Failure; @@ -1435,8 +1503,8 @@ result_t wuss_test(const char *resources) * grow forces a scroll re-clamp, the overlap of the old and new content * boxes is genuine on-screen content that just needs sliding by the * scroll delta -- only the newly-exposed strip needs a real repaint. */ - test_task_t tc_d; - wuss_task_t delegate_d; + static test_task_t tc_d; + wuss_task_t *delegate_d; box_t box_d, content_before; wuss_window_t *win_d; point_t scroll_d; @@ -1446,14 +1514,13 @@ result_t wuss_test(const char *resources) tc_d.redraw_count = 0; tc_d.mouse_count = 0; - delegate_d.handle = test_handle; - delegate_d.task_data = &tc_d; + delegate_d = mk_task(wuss, test_handle, &tc_d); + if (delegate_d == NULL) goto Failure; 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(wuss, &box_d, "D", wuss_WINDOW_NONE, + rc = wuss_window_create(delegate_d, &box_d, "D", wuss_WINDOW_NONE, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), - &delegate_d, SIZE2D(80, 140), SIZE2D(0, 0), &win_d); if (rc != result_OK) goto Failure; @@ -1539,8 +1606,8 @@ result_t wuss_test(const char *resources) * the occluder's own correct pixels, so it must stay clean -- dirtying * it would redraw the occluding window for nothing. Only the un-occluded * newly-revealed top strip needs a repaint. */ - test_task_t tc_d, tc_o; - wuss_task_t delegate_d, delegate_o; + static test_task_t tc_d, tc_o; + wuss_task_t *delegate_d, *delegate_o; box_t box_d, box_o, content_before; wuss_window_t *win_d, *win_o; point_t scroll_d; @@ -1550,14 +1617,15 @@ result_t wuss_test(const char *resources) tc_d.redraw_count = 0; tc_d.mouse_count = 0; tc_o.redraw_count = 0; tc_o.mouse_count = 0; - delegate_d.handle = test_handle; delegate_d.task_data = &tc_d; - delegate_o.handle = test_handle; delegate_o.task_data = &tc_o; + delegate_d = mk_task(wuss, test_handle, &tc_d); + if (delegate_d == NULL) goto Failure; + delegate_o = mk_task(wuss, test_handle, &tc_o); + if (delegate_o == NULL) goto Failure; 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(wuss, &box_d, "D", wuss_WINDOW_NONE, + rc = wuss_window_create(delegate_d, &box_d, "D", wuss_WINDOW_NONE, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), - &delegate_d, SIZE2D(100, 140), SIZE2D(0, 0), &win_d); if (rc != result_OK) goto Failure; @@ -1568,9 +1636,8 @@ 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(wuss, &box_o, "O", wuss_WINDOW_NONE, + rc = wuss_window_create(delegate_o, &box_o, "O", wuss_WINDOW_NONE, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), - &delegate_o, SIZE2D(box_o.x1 - box_o.x0, box_o.y1 - box_o.y0), SIZE2D(0, 0), &win_o); if (rc != result_OK) @@ -1652,8 +1719,8 @@ result_t wuss_test(const char *resources) * the occluder's own correct pixels, so it must stay clean -- dirtying * it would redraw the occluding window. Only the un-occluded * newly-exposed edge strip needs a repaint. */ - test_task_t tc_s, tc_o; - wuss_task_t delegate_s, delegate_o; + static test_task_t tc_s, tc_o; + wuss_task_t *delegate_s, *delegate_o; box_t box_s, box_o, content; wuss_window_t *win_s, *win_o; int i, split_x; @@ -1662,14 +1729,15 @@ result_t wuss_test(const char *resources) tc_s.redraw_count = 0; tc_s.mouse_count = 0; tc_o.redraw_count = 0; tc_o.mouse_count = 0; - delegate_s.handle = test_handle; delegate_s.task_data = &tc_s; - delegate_o.handle = test_handle; delegate_o.task_data = &tc_o; + delegate_s = mk_task(wuss, test_handle, &tc_s); + if (delegate_s == NULL) goto Failure; + delegate_o = mk_task(wuss, test_handle, &tc_o); + if (delegate_o == NULL) goto Failure; 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(wuss, &box_s, "S", wuss_WINDOW_NONE, + rc = wuss_window_create(delegate_s, &box_s, "S", wuss_WINDOW_NONE, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), - &delegate_s, SIZE2D(100, 200), SIZE2D(0, 0), &win_s); if (rc != result_OK) goto Failure; @@ -1679,9 +1747,8 @@ 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(wuss, &box_o, "O", wuss_WINDOW_NONE, + rc = wuss_window_create(delegate_o, &box_o, "O", wuss_WINDOW_NONE, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), - &delegate_o, SIZE2D(box_o.x1 - box_o.x0, box_o.y1 - box_o.y0), SIZE2D(0, 0), &win_o); if (rc != result_OK) @@ -1756,8 +1823,8 @@ result_t wuss_test(const char *resources) * Read the framebuffer behind O to check. */ colour_t cm; uint32_t fb_m, fb_o; - test_task_t tc_m, tc_o; - wuss_task_t delegate_m, delegate_o; + static test_task_t tc_m, tc_o; + wuss_task_t *delegate_m, *delegate_o; box_t box_m, box_o, content; wuss_window_t *win_m, *win_o; int occ_x, occ_y, mid_x, mid_y, delta; @@ -1765,14 +1832,15 @@ result_t wuss_test(const char *resources) cm = colour_rgb(0x11, 0x22, 0x33); tc_m.redraw_count = 0; tc_m.mouse_count = 0; tc_o.redraw_count = 0; tc_o.mouse_count = 0; - delegate_m.handle = paint_handle; delegate_m.task_data = &cm; - delegate_o.handle = paint_handle; delegate_o.task_data = NULL; + delegate_m = mk_task(wuss, paint_handle, &cm); + if (delegate_m == NULL) goto Failure; + delegate_o = mk_task(wuss, paint_handle, NULL); + if (delegate_o == NULL) goto Failure; 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(wuss, &box_m, "M", wuss_WINDOW_NONE, + rc = wuss_window_create(delegate_m, &box_m, "M", wuss_WINDOW_NONE, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), - &delegate_m, SIZE2D(100, 300), SIZE2D(0, 0), &win_m); if (rc != result_OK) goto Failure; @@ -1786,9 +1854,8 @@ 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(wuss, &box_o, "O", wuss_WINDOW_NONE, + rc = wuss_window_create(delegate_o, &box_o, "O", wuss_WINDOW_NONE, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), - &delegate_o, SIZE2D(box_o.x1 - box_o.x0, box_o.y1 - box_o.y0), SIZE2D(0, 0), &win_o); if (rc != result_OK) @@ -1837,8 +1904,8 @@ result_t wuss_test(const char *resources) * back). paint_handle paints M as one-pixel rows whose blue channel * encodes document Y, so a mis-shifted row is detectable exactly. */ colour_t cm; - test_task_t tc_m, tc_o; - wuss_task_t delegate_m, delegate_o; + static test_task_t tc_m, tc_o; + wuss_task_t *delegate_m, *delegate_o; box_t box_m, box_o, content, ovis; wuss_window_t *win_m, *win_o; int gx, gy, delta, bad; @@ -1846,14 +1913,15 @@ result_t wuss_test(const char *resources) cm = colour_rgb(0x11, 0x22, 0x33); tc_m.redraw_count = 0; tc_m.mouse_count = 0; tc_o.redraw_count = 0; tc_o.mouse_count = 0; - delegate_m.handle = paint_handle; delegate_m.task_data = &cm; - delegate_o.handle = paint_handle; delegate_o.task_data = NULL; + delegate_m = mk_task(wuss, paint_handle, &cm); + if (delegate_m == NULL) goto Failure; + delegate_o = mk_task(wuss, paint_handle, NULL); + if (delegate_o == NULL) goto Failure; 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(wuss, &box_m, "M", wuss_WINDOW_NONE, + rc = wuss_window_create(delegate_m, &box_m, "M", wuss_WINDOW_NONE, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), - &delegate_m, SIZE2D(100, 300), SIZE2D(0, 0), &win_m); if (rc != result_OK) goto Failure; @@ -1863,9 +1931,8 @@ 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(wuss, &box_o, "O", wuss_WINDOW_NONE, + rc = wuss_window_create(delegate_o, &box_o, "O", wuss_WINDOW_NONE, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), - &delegate_o, SIZE2D(box_o.x1 - box_o.x0, box_o.y1 - box_o.y0), SIZE2D(0, 0), &win_o); if (rc != result_OK) @@ -1911,8 +1978,8 @@ result_t wuss_test(const char *resources) printf("test: wuss_WINDOW_NO_RESIZE_BLIT redraws the whole window instead of blitting\n"); { - test_task_t tc_nb; - wuss_task_t delegate_nb; + static test_task_t tc_nb; + wuss_task_t *delegate_nb; box_t box_nb, before, titlebar, toggle; wuss_window_t *win_nb; int outline_px, titlebar_height, inset, icon; @@ -1920,14 +1987,13 @@ result_t wuss_test(const char *resources) tc_nb.redraw_count = 0; tc_nb.mouse_count = 0; - delegate_nb.handle = test_handle; - delegate_nb.task_data = &tc_nb; + delegate_nb = mk_task(wuss, test_handle, &tc_nb); + if (delegate_nb == NULL) goto Failure; box_nb.x0 = 10; box_nb.y0 = 10; box_nb.x1 = 50; box_nb.y1 = 50; /* 40x40 content, room to grow to a 200x200 doc */ - rc = wuss_window_create(wuss, &box_nb, "NB", wuss_WINDOW_NO_RESIZE_BLIT, + rc = wuss_window_create(delegate_nb, &box_nb, "NB", wuss_WINDOW_NO_RESIZE_BLIT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), - &delegate_nb, SIZE2D(200, 200), SIZE2D(0, 0), &win_nb); if (rc != result_OK) goto Failure; @@ -1991,8 +2057,8 @@ result_t wuss_test(const char *resources) printf("test: toggle-size maximize accounts for scrollbar furniture, not just outline/titlebar\n"); { - test_task_t tc_u; - wuss_task_t delegate_u; + static test_task_t tc_u; + wuss_task_t *delegate_u; box_t box_u, ub, titlebar, toggle; wuss_window_t *win_u; int outline_px, titlebar_height, inset, icon; @@ -2000,14 +2066,13 @@ result_t wuss_test(const char *resources) tc_u.redraw_count = 0; tc_u.mouse_count = 0; - delegate_u.handle = test_handle; - delegate_u.task_data = &tc_u; + delegate_u = mk_task(wuss, test_handle, &tc_u); + if (delegate_u == NULL) goto Failure; box_u.x0 = 80; box_u.y0 = 80; box_u.x1 = 120; box_u.y1 = 120; /* 40x40 content */ - rc = wuss_window_create(wuss, &box_u, "U", wuss_WINDOW_NONE, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), /* scrollbars on: carve.x/y = icon size */ - &delegate_u, + rc = wuss_window_create(mk_task(wuss, NULL, NULL), &box_u, "U", wuss_WINDOW_NONE, + 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) goto Failure; @@ -2090,17 +2155,20 @@ result_t wuss_test(const char *resources) printf("test: toggle-size maximize stays a valid box for a window dragged off the right/bottom edge\n"); { - test_task_t tc_v; - wuss_task_t delegate_v; + static test_task_t tc_v; + wuss_task_t *delegate_v; box_t box_v, vb, titlebar, toggle; wuss_window_t *win_v; int outline_px, titlebar_height, inset, icon; int cx, cy; - tc_v.redraw_count = 0; - tc_v.mouse_count = 0; - delegate_v.handle = test_handle; - delegate_v.task_data = &tc_v; + memset(&tc_v, 0, sizeof(tc_v)); + tc_v.veto_pre_close = 1; /* this test pokes furniture with a window in a + * deliberately broken (inverted) box; a stray + * hit on the close icon must not tear win_v down + * mid-test */ + delegate_v = mk_task(wuss, test_handle, &tc_v); + if (delegate_v == NULL) goto Failure; box_v.x0 = 10; box_v.y0 = 10; box_v.x1 = 70; box_v.y1 = 70; /* 60x60 content -- wide enough titlebar that @@ -2113,9 +2181,8 @@ 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(wuss, &box_v, "V", wuss_WINDOW_NONE, + rc = wuss_window_create(delegate_v, &box_v, "V", wuss_WINDOW_NONE, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), - &delegate_v, SIZE2D(200, 200), SIZE2D(0, 0), &win_v); if (rc != result_OK) goto Failure; @@ -2171,9 +2238,13 @@ result_t wuss_test(const char *resources) if (rc != result_OK) goto Failure; - /* toggle back: shrink. Confirm the window is still reachable at all -- - * this alone would already fail (rc != result_OK from a wrong hit) if - * the icon had become permanently unhittable above. */ + /* toggle back: shrink. With the window clipped this narrow its toggle + * icon overlaps close's box (see the box_v comment above), so the click + * meant for toggle may land on close instead; hit-test checks close + * first. That routes through wuss_window_try_close, and this task's + * PRE_CLOSE veto (set at task creation) both keeps the window alive and + * makes the click return the veto's non-OK -- accept that here. The + * point of the check is line-below: the box must still be valid. */ wuss_window_get_visible_bounds(win_v, &vb); titlebar.x0 = vb.x0 + outline_px; titlebar.x1 = vb.x1 - outline_px; @@ -2186,12 +2257,12 @@ result_t wuss_test(const char *resources) cy = (toggle.y0 + toggle.y1) / 2; rc = wuss_mouse_click(wuss, POINT(cx, cy), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* V's toggle-size icon: shrink back */ - if (rc != result_OK) + if (rc != result_OK && rc != result_BAD_ARG) goto Failure; if (hit != win_v) goto Failure; rc = wuss_mouse_click(wuss, POINT(cx, cy), wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); - if (rc != result_OK) + if (rc != result_OK && rc != result_BAD_ARG) goto Failure; wuss_window_get_visible_bounds(win_v, &vb); @@ -2208,27 +2279,26 @@ result_t wuss_test(const char *resources) printf("test: dragging a back-most window with nothing above it still blits\n"); { - test_task_t tc_k, tc_l; - wuss_task_t delegate_k, delegate_l; + static test_task_t tc_k, tc_l; + wuss_task_t *delegate_k, *delegate_l; box_t box_k, box_l; wuss_window_t *win_k, *win_l; int before_k, before_l; tc_k.redraw_count = 0; tc_k.mouse_count = 0; - delegate_k.handle = test_handle; - delegate_k.task_data = &tc_k; + delegate_k = mk_task(wuss, test_handle, &tc_k); + if (delegate_k == NULL) goto Failure; box_k.x0 = 0; box_k.y0 = 140; /* clear of the still-open A/B windows above */ box_k.x1 = 50; box_k.y1 = 175; - rc = wuss_window_create(wuss, + rc = wuss_window_create(delegate_k, &box_k, "K", 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), - &delegate_k, box_size(&box_k), SIZE2D(0, 0), &win_k); @@ -2237,19 +2307,18 @@ result_t wuss_test(const char *resources) tc_l.redraw_count = 0; tc_l.mouse_count = 0; - delegate_l.handle = test_handle; - delegate_l.task_data = &tc_l; + delegate_l = mk_task(wuss, test_handle, &tc_l); + if (delegate_l == NULL) goto Failure; box_l.x0 = 120; box_l.y0 = 140; /* well clear of K, so never overlaps it */ box_l.x1 = 170; box_l.y1 = 175; - rc = wuss_window_create(wuss, + rc = wuss_window_create(delegate_l, &box_l, "L", 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), - &delegate_l, box_size(&box_l), SIZE2D(0, 0), &win_l); @@ -2297,26 +2366,25 @@ result_t wuss_test(const char *resources) printf("test: resizing a window only invalidates the grown/shrunk sliver\n"); { - test_task_t tc_m2; - wuss_task_t delegate_m2; + static test_task_t tc_m2; + wuss_task_t *delegate_m2; box_t box_m2, before2, after2, region; wuss_window_t *win_m2; int i, dirty_area, full_area, interior_x, interior_y, interior_dirty; tc_m2.redraw_count = 0; tc_m2.mouse_count = 0; - delegate_m2.handle = test_handle; - delegate_m2.task_data = &tc_m2; + delegate_m2 = mk_task(wuss, test_handle, &tc_m2); + if (delegate_m2 == NULL) goto Failure; box_m2.x0 = 0; box_m2.y0 = 0; box_m2.x1 = 40; box_m2.y1 = 40; - rc = wuss_window_create(wuss, + rc = wuss_window_create(delegate_m2, &box_m2, "M2", wuss_WINDOW_NO_BACK | wuss_WINDOW_NO_TOGGLE_SIZE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), - &delegate_m2, box_size(&box_m2), SIZE2D(0, 0), &win_m2); @@ -2392,22 +2460,21 @@ result_t wuss_test(const char *resources) printf("test: resizing a wuss_WINDOW_NO_RESIZE_BLIT window redraws it fully\n"); { - test_task_t tc_nb2; - wuss_task_t delegate_nb2; + static test_task_t tc_nb2; + wuss_task_t *delegate_nb2; box_t box_nb2, before3, after3, region; wuss_window_t *win_nb2; int i, dirty_area, full_area; tc_nb2.redraw_count = 0; tc_nb2.mouse_count = 0; - delegate_nb2.handle = test_handle; - delegate_nb2.task_data = &tc_nb2; + delegate_nb2 = mk_task(wuss, test_handle, &tc_nb2); + if (delegate_nb2 == NULL) goto Failure; box_nb2.x0 = 0; box_nb2.y0 = 0; box_nb2.x1 = 40; box_nb2.y1 = 40; - rc = wuss_window_create(wuss, &box_nb2, "NB2", wuss_WINDOW_NO_RESIZE_BLIT, + rc = wuss_window_create(delegate_nb2, &box_nb2, "NB2", wuss_WINDOW_NO_RESIZE_BLIT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), - &delegate_nb2, box_size(&box_nb2), SIZE2D(0, 0), &win_nb2); @@ -2441,25 +2508,24 @@ result_t wuss_test(const char *resources) printf("test: dragging a clear window onto an occluder leaves the occluder untouched\n"); { - test_task_t tc_n, tc_o; - wuss_task_t delegate_n, delegate_o; + static test_task_t tc_n, tc_o; + wuss_task_t *delegate_n, *delegate_o; box_t box_n, box_o, visible_o, exposed, occluded, region; wuss_window_t *win_n, *win_o; int i, exposed_dirty, occluded_dirty; tc_n.redraw_count = 0; tc_n.mouse_count = 0; - delegate_n.handle = test_handle; - delegate_n.task_data = &tc_n; + delegate_n = mk_task(wuss, test_handle, &tc_n); + if (delegate_n == NULL) goto Failure; box_n.x0 = 0; box_n.y0 = 140; /* clear of any occluder to start */ box_n.x1 = 60; box_n.y1 = 170; - rc = wuss_window_create(wuss, &box_n, "N", + rc = wuss_window_create(delegate_n, &box_n, "N", 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), - &delegate_n, box_size(&box_n), SIZE2D(0, 0), &win_n); @@ -2468,17 +2534,16 @@ result_t wuss_test(const char *resources) tc_o.redraw_count = 0; tc_o.mouse_count = 0; - delegate_o.handle = test_handle; - delegate_o.task_data = &tc_o; + delegate_o = mk_task(wuss, test_handle, &tc_o); + if (delegate_o == NULL) goto Failure; box_o.x0 = 90; box_o.y0 = 140; /* N will be dragged partly on top of O */ box_o.x1 = 130; box_o.y1 = 180; - rc = wuss_window_create(wuss, &box_o, "O", + rc = wuss_window_create(delegate_o, &box_o, "O", 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), - &delegate_o, box_size(&box_o), SIZE2D(0, 0), &win_o); @@ -2544,8 +2609,8 @@ result_t wuss_test(const char *resources) printf("test: moving a partly-occluded window blits its clean part and only repaints the occluded part\n"); { - test_task_t tc_a, tc_b; - wuss_task_t delegate_a, delegate_b; + static test_task_t tc_a, tc_b; + wuss_task_t *delegate_a, *delegate_b; box_t box_a, box_b, visible_a, visible_b_before; box_t clean_new, hidden_new, region; wuss_window_t *win_a, *win_b; @@ -2553,17 +2618,16 @@ result_t wuss_test(const char *resources) tc_b.redraw_count = 0; tc_b.mouse_count = 0; - delegate_b.handle = test_handle; - delegate_b.task_data = &tc_b; + delegate_b = mk_task(wuss, test_handle, &tc_b); + if (delegate_b == NULL) goto Failure; box_b.x0 = 20; box_b.y0 = 10; /* left half will sit under A */ box_b.x1 = 80; box_b.y1 = 50; - rc = wuss_window_create(wuss, &box_b, "B", + rc = wuss_window_create(delegate_b, &box_b, "B", wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), - &delegate_b, box_size(&box_b), SIZE2D(0, 0), &win_b); @@ -2572,17 +2636,16 @@ result_t wuss_test(const char *resources) tc_a.redraw_count = 0; tc_a.mouse_count = 0; - delegate_a.handle = test_handle; - delegate_a.task_data = &tc_a; + delegate_a = mk_task(wuss, test_handle, &tc_a); + if (delegate_a == NULL) goto Failure; box_a.x0 = 0; box_a.y0 = 0; /* created after B, so A is topmost */ box_a.x1 = 40; box_a.y1 = 100; - rc = wuss_window_create(wuss, &box_a, "A", + rc = wuss_window_create(delegate_a, &box_a, "A", wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), - &delegate_a, box_size(&box_a), SIZE2D(0, 0), &win_a); @@ -2647,8 +2710,8 @@ result_t wuss_test(const char *resources) printf("test: moving a window whose occluded piece was never blitted doesn't redraw the occluder\n"); { - test_task_t tc_a, tc_b; - wuss_task_t delegate_a, delegate_b; + static test_task_t tc_a, tc_b; + wuss_task_t *delegate_a, *delegate_b; box_t box_a, box_b, visible_a, visible_b_before; box_t region; wuss_window_t *win_a, *win_b; @@ -2656,17 +2719,16 @@ result_t wuss_test(const char *resources) tc_b.redraw_count = 0; tc_b.mouse_count = 0; - delegate_b.handle = test_handle; - delegate_b.task_data = &tc_b; + delegate_b = mk_task(wuss, test_handle, &tc_b); + if (delegate_b == NULL) goto Failure; box_b.x0 = 0; box_b.y0 = 0; /* right part sits under A throughout */ box_b.x1 = 60; box_b.y1 = 40; - rc = wuss_window_create(wuss, &box_b, "B", + rc = wuss_window_create(delegate_b, &box_b, "B", wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), - &delegate_b, box_size(&box_b), SIZE2D(0, 0), &win_b); @@ -2675,17 +2737,16 @@ result_t wuss_test(const char *resources) tc_a.redraw_count = 0; tc_a.mouse_count = 0; - delegate_a.handle = test_handle; - delegate_a.task_data = &tc_a; + delegate_a = mk_task(wuss, test_handle, &tc_a); + if (delegate_a == NULL) goto Failure; box_a.x0 = 40; box_a.y0 = 0; /* created after B, so A is topmost */ box_a.x1 = 100; box_a.y1 = 40; - rc = wuss_window_create(wuss, &box_a, "A", + rc = wuss_window_create(delegate_a, &box_a, "A", wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), - &delegate_a, box_size(&box_a), SIZE2D(0, 0), &win_a); @@ -2727,8 +2788,8 @@ result_t wuss_test(const char *resources) printf("test: moving a window split by a mid-band occluder past the gap between bands blits both bands in a safe order\n"); { - test_task_t tc_a, tc_b; - wuss_task_t delegate_a, delegate_b; + static test_task_t tc_a, tc_b; + wuss_task_t *delegate_a, *delegate_b; box_t box_a, box_b, visible_b_before; box_t occluded_overlap, hidden_new, region; wuss_window_t *win_a, *win_b; @@ -2736,17 +2797,16 @@ result_t wuss_test(const char *resources) tc_b.redraw_count = 0; tc_b.mouse_count = 0; - delegate_b.handle = test_handle; - delegate_b.task_data = &tc_b; + delegate_b = mk_task(wuss, test_handle, &tc_b); + if (delegate_b == NULL) goto Failure; box_b.x0 = 0; box_b.y0 = 0; /* middle band sits under A */ box_b.x1 = 60; box_b.y1 = 60; - rc = wuss_window_create(wuss, &box_b, "B", + rc = wuss_window_create(delegate_b, &box_b, "B", wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), - &delegate_b, box_size(&box_b), SIZE2D(0, 0), &win_b); @@ -2755,17 +2815,16 @@ result_t wuss_test(const char *resources) tc_a.redraw_count = 0; tc_a.mouse_count = 0; - delegate_a.handle = test_handle; - delegate_a.task_data = &tc_a; + delegate_a = mk_task(wuss, test_handle, &tc_a); + if (delegate_a == NULL) goto Failure; box_a.x0 = 0; box_a.y0 = 20; /* created after B, so A is topmost */ box_a.x1 = 60; box_a.y1 = 40; - rc = wuss_window_create(wuss, &box_a, "A", + rc = wuss_window_create(delegate_a, &box_a, "A", wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), - &delegate_a, box_size(&box_a), SIZE2D(0, 0), &win_a); @@ -2841,25 +2900,24 @@ result_t wuss_test(const char *resources) printf("test: dragging a window deeper under a corner occluder blits both L-shaped pieces in a safe order\n"); { - test_task_t tc_a, tc_b; - wuss_task_t delegate_a, delegate_b; + static test_task_t tc_a, tc_b; + wuss_task_t *delegate_a, *delegate_b; box_t box_a, box_b, visible_b_before, visible_a, region; wuss_window_t *win_a, *win_b; int i; tc_b.redraw_count = 0; tc_b.mouse_count = 0; - delegate_b.handle = test_handle; - delegate_b.task_data = &tc_b; + delegate_b = mk_task(wuss, test_handle, &tc_b); + if (delegate_b == NULL) goto Failure; box_b.x0 = 80; box_b.y0 = 80; /* corner already under A */ box_b.x1 = 140; box_b.y1 = 140; - rc = wuss_window_create(wuss, &box_b, "B", + rc = wuss_window_create(delegate_b, &box_b, "B", wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), - &delegate_b, box_size(&box_b), SIZE2D(0, 0), &win_b); @@ -2868,17 +2926,16 @@ result_t wuss_test(const char *resources) tc_a.redraw_count = 0; tc_a.mouse_count = 0; - delegate_a.handle = test_handle; - delegate_a.task_data = &tc_a; + delegate_a = mk_task(wuss, test_handle, &tc_a); + if (delegate_a == NULL) goto Failure; box_a.x0 = 0; box_a.y0 = 0; /* created after B, so A is topmost */ box_a.x1 = 100; box_a.y1 = 100; - rc = wuss_window_create(wuss, &box_a, "A", + rc = wuss_window_create(delegate_a, &box_a, "A", wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), - &delegate_a, box_size(&box_a), SIZE2D(0, 0), &win_a); @@ -2941,21 +2998,20 @@ result_t wuss_test(const char *resources) tc_c.redraw_count = 0; tc_c.mouse_count = 0; - delegate_c.handle = test_handle; - delegate_c.task_data = &tc_c; + delegate_c = mk_task(wuss, test_handle, &tc_c); + if (delegate_c == NULL) goto Failure; box_c.x0 = 0; box_c.y0 = 0; box_c.x1 = 40; box_c.y1 = 40; - rc = wuss_window_create(wuss, + rc = wuss_window_create(delegate_c, &box_c, "C", 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), - &delegate_c, box_size(&box_c), SIZE2D(0, 0), &win_c); @@ -2977,20 +3033,19 @@ result_t wuss_test(const char *resources) printf("test: mouse and scroll events arrive in virtual content space, with the scroll offset applied exactly once\n"); { - test_task_t tc_s = { 0 }; - wuss_task_t delegate_s; + static test_task_t tc_s = { 0 }; + wuss_task_t *delegate_s; box_t box_s, content_s; wuss_window_t *win_s; point_t scroll; - delegate_s.handle = test_handle; - delegate_s.task_data = &tc_s; + delegate_s = mk_task(wuss, test_handle, &tc_s); + if (delegate_s == NULL) goto Failure; 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(wuss, &box_s, "S", wuss_WINDOW_NONE, + rc = wuss_window_create(delegate_s, &box_s, "S", wuss_WINDOW_NONE, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), - &delegate_s, SIZE2D(200, 200), SIZE2D(0, 0), &win_s); if (rc != result_OK) goto Failure; @@ -3048,25 +3103,24 @@ result_t wuss_test(const char *resources) * content from them -- is added outside the requested content box, never * carved out of it, so what the caller asks for is what it gets, both at * creation and after a resize. */ - test_task_t tc_r; - wuss_task_t delegate_r; + static test_task_t tc_r; + wuss_task_t *delegate_r; box_t box_r, content_r; wuss_window_t *win_r; memset(&tc_r, 0, sizeof(tc_r)); - delegate_r.handle = test_handle; - delegate_r.task_data = &tc_r; + delegate_r = mk_task(wuss, test_handle, &tc_r); + if (delegate_r == NULL) goto Failure; box_r.x0 = 20; box_r.y0 = 30; box_r.x1 = 120; box_r.y1 = 110; - rc = wuss_window_create(wuss, + rc = wuss_window_create(delegate_r, &box_r, "rules", wuss_WINDOW_NONE, /* all furniture present */ wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), - &delegate_r, SIZE2D(400, 400), SIZE2D(0, 0), &win_r); @@ -3094,24 +3148,23 @@ result_t wuss_test(const char *resources) { /* Windows created without a position are packed towards the top-left and * must not overlap; closing one frees its slot for the next create. */ - test_task_t tc_p[4]; - wuss_task_t delegate_p; + static test_task_t tc_p[4]; + wuss_task_t *delegate_p; wuss_window_t *win_p[4]; box_t vis[4], probe; int k, m; memset(tc_p, 0, sizeof(tc_p)); - delegate_p.handle = test_handle; - delegate_p.task_data = &tc_p[0]; + delegate_p = mk_task(wuss, test_handle, &tc_p[0]); + if (delegate_p == NULL) goto Failure; for (k = 0; k < 4; k++) { - rc = wuss_window_create_placed(wuss, + rc = wuss_window_create_placed(delegate_p, SIZE2D(40, 30), "P", wuss_WINDOW_NONE, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), - &delegate_p, SIZE2D(40, 30), SIZE2D(0, 0), &win_p[k]); @@ -3130,12 +3183,11 @@ result_t wuss_test(const char *resources) probe = vis[1]; wuss_window_close(win_p[1]); - rc = wuss_window_create_placed(wuss, + rc = wuss_window_create_placed(delegate_p, SIZE2D(40, 30), "P", wuss_WINDOW_NONE, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), - &delegate_p, SIZE2D(40, 30), SIZE2D(0, 0), &win_p[1]); @@ -3154,7 +3206,7 @@ result_t wuss_test(const char *resources) wuss_window_close(win_p[k]); } - printf("test: wuss_set_palette broadcasts wuss_EVENT_PALETTE and rejects a length mismatch\n"); + printf("test: wuss_set_palette and wuss_idle broadcast once per registered task\n"); { /* the wuss under test was made with a 2-entry palette (see top of this * function); a fresh 2-entry palette must broadcast to every task, a @@ -3163,34 +3215,144 @@ result_t wuss_test(const char *resources) static const colour_t too_long[3] = { { 0xFF000000 }, { 0xFF808080 }, { 0xFFFFFFFF } }; - tc_a.palette_count = 0; - tc_b.palette_count = 0; + static test_task_t tc_pa, tc_pb; + wuss_task_t *delegate_pa, *delegate_pb; + + /* every earlier block-local test_task_t is out of scope now; drop the + * tasks that still point at it so the broadcast counts are exactly the + * two made here */ + reap_test_tasks(); + + memset(&tc_pa, 0, sizeof(tc_pa)); + memset(&tc_pb, 0, sizeof(tc_pb)); + delegate_pa = mk_task(wuss, test_handle, &tc_pa); + delegate_pb = mk_task(wuss, test_handle, &tc_pb); + if (delegate_pa == NULL || delegate_pb == NULL) + goto Failure; rc = wuss_set_palette(wuss, swapped, NELEMS(swapped)); if (rc != result_OK) goto Failure; - if (tc_a.palette_count != 1 || tc_b.palette_count != 1) - goto Failure; + if (tc_pa.palette_count != 1 || tc_pb.palette_count != 1) + goto Failure; /* one PALETTE per registered task */ rc = wuss_set_palette(wuss, too_long, NELEMS(too_long)); if (rc != result_BAD_ARG) goto Failure; - if (tc_a.palette_count != 1 || tc_b.palette_count != 1) + if (tc_pa.palette_count != 1 || tc_pb.palette_count != 1) goto Failure; /* rejected call must not have broadcast */ + /* wuss_idle broadcasts a single wuss_EVENT_IDLE to each task too; a + * task with no window still gets it (delivery is per registered task, + * not per window) */ + rc = wuss_idle(wuss); + if (rc != result_OK) + goto Failure; + if (tc_pa.idle_count != 1 || tc_pb.idle_count != 1) + goto Failure; + + reap_test_tasks(); /* tc_pa/tc_pb die with this block too */ rc = result_OK; } - printf("test: wuss_task_stop sends wuss_EVENT_QUIT to each window's task\n"); + printf("test: wuss_window_set_hidden fires PRE_SHOW; a veto keeps the window hidden\n"); + { + static test_task_t tc_ps; + wuss_task_t *delegate_ps; + box_t box_ps; + wuss_window_t *win_ps; + + memset(&tc_ps, 0, sizeof(tc_ps)); + delegate_ps = mk_task(wuss, test_handle, &tc_ps); + if (delegate_ps == NULL) + goto Failure; - tc_a.stop_count = 0; - tc_b.stop_count = 0; - wuss_task_stop(win_a); - wuss_task_stop(win_b); - wuss_window_close(win_a); - wuss_window_close(win_b); - if (tc_a.stop_count != 1 || tc_b.stop_count != 1) - goto Failure; + 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, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + box_size(&box_ps), SIZE2D(0, 0), &win_ps); + if (rc != result_OK) + goto Failure; + + /* hide it: no event, and it must not catch the pointer any more */ + rc = wuss_window_set_hidden(win_ps, 1); + if (rc != result_OK || tc_ps.pre_show_count != 0 || tc_ps.show_count != 0) + goto Failure; + rc = wuss_mouse_click(wuss, POINT(10, 10), wuss_BUTTON_SELECT, + wuss_MOUSE_DOWN, &hit); + if (rc != result_OK || hit == win_ps) + goto Failure; /* hidden window is not hit-tested */ + (void) wuss_mouse_click(wuss, POINT(10, 10), wuss_BUTTON_SELECT, + wuss_MOUSE_UP, &hit); + + /* vetoed reveal: PRE_SHOW fires, returns the veto rc, no SHOW, window + * stays hidden */ + tc_ps.veto_pre_show = 1; + rc = wuss_window_set_hidden(win_ps, 0); + if (rc != result_BAD_ARG) + goto Failure; + if (tc_ps.pre_show_count != 1 || tc_ps.show_count != 0) + goto Failure; + rc = wuss_mouse_click(wuss, POINT(10, 10), wuss_BUTTON_SELECT, + wuss_MOUSE_DOWN, &hit); + if (rc != result_OK || hit == win_ps) + goto Failure; /* still hidden */ + (void) wuss_mouse_click(wuss, POINT(10, 10), wuss_BUTTON_SELECT, + wuss_MOUSE_UP, &hit); + + /* allow it: PRE_SHOW then SHOW, and now it catches the pointer */ + tc_ps.veto_pre_show = 0; + rc = wuss_window_set_hidden(win_ps, 0); + if (rc != result_OK) + goto Failure; + if (tc_ps.pre_show_count != 2 || tc_ps.show_count != 1) + goto Failure; + rc = wuss_mouse_click(wuss, POINT(10, 10), wuss_BUTTON_SELECT, + wuss_MOUSE_DOWN, &hit); + if (rc != result_OK || hit != win_ps) + goto Failure; + (void) wuss_mouse_click(wuss, POINT(10, 10), wuss_BUTTON_SELECT, + wuss_MOUSE_UP, &hit); + + wuss_task_destroy(delegate_ps); + mk_task_count = 0; /* delegate_ps is gone; drop the stale registry entry */ + rc = result_OK; + } + + printf("test: wuss_task_destroy sends one wuss_EVENT_QUIT and closes the task's windows\n"); + { + static test_task_t tc_q; + wuss_task_t *delegate_q; + box_t box_q; + wuss_window_t *win_q1, *win_q2; + + memset(&tc_q, 0, sizeof(tc_q)); + delegate_q = mk_task(wuss, test_handle, &tc_q); + if (delegate_q == NULL) goto Failure; + + 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, + 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, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + box_size(&box_q), SIZE2D(0, 0), &win_q2); + if (rc != result_OK) + goto Failure; + NOT_USED(win_q1); + NOT_USED(win_q2); + + /* one QUIT for the task, regardless of how many windows it owns */ + wuss_task_destroy(delegate_q); + if (tc_q.stop_count != 1) + goto Failure; + + mk_task_count = 0; /* delegate_q is gone; drop the stale registry entry */ + } #ifdef WUSS_MENUS printf("test: wuss_menu_create_from_desc parses a descriptor tree\n"); @@ -3286,7 +3448,7 @@ result_t wuss_test(const char *resources) screen_t scr; wuss_t *wuss; test_task_t tc_a, tc_b; - wuss_task_t delegate_a, delegate_b; + wuss_task_t *delegate_a, *delegate_b; box_t box_a, box_b, content; wuss_window_t *win_a, *win_b, *hit; point_t scroll; @@ -3321,8 +3483,8 @@ 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(wuss, &box_a, "toosmall", wuss_WINDOW_NONE, - wuss_NO_BACKGROUND, NULL, box_size(&box_a), + rc = wuss_window_create(mk_task(wuss, NULL, NULL), &box_a, "toosmall", wuss_WINDOW_NONE, + wuss_NO_BACKGROUND, box_size(&box_a), SIZE2D(0, 0), &win_a); if (rc != result_WUSS_TOO_SMALL) goto Failure; @@ -3331,19 +3493,21 @@ result_t wuss_test(const char *resources) memset(&tc_a, 0, sizeof(tc_a)); memset(&tc_b, 0, sizeof(tc_b)); - delegate_a.handle = test_handle; delegate_a.task_data = &tc_a; - delegate_b.handle = test_handle; delegate_b.task_data = &tc_b; + delegate_a = mk_task(wuss, test_handle, &tc_a); + if (delegate_a == NULL) goto Failure; + delegate_b = mk_task(wuss, test_handle, &tc_b); + if (delegate_b == NULL) goto Failure; box_a.x0 = 0; box_a.y0 = 0; box_a.x1 = 100; box_a.y1 = 100; - rc = wuss_window_create(wuss, &box_a, "A", chromeless, - wuss_NO_BACKGROUND, &delegate_a, SIZE2D(400, 400), + rc = wuss_window_create(delegate_a, &box_a, "A", chromeless, + wuss_NO_BACKGROUND, SIZE2D(400, 400), SIZE2D(0, 0), &win_a); if (rc != result_OK) goto Failure; box_b.x0 = 50; box_b.y0 = 50; box_b.x1 = 150; box_b.y1 = 150; - rc = wuss_window_create(wuss, &box_b, "B", chromeless, - wuss_NO_BACKGROUND, &delegate_b, SIZE2D(400, 400), + rc = wuss_window_create(delegate_b, &box_b, "B", chromeless, + wuss_NO_BACKGROUND, SIZE2D(400, 400), SIZE2D(0, 0), &win_b); if (rc != result_OK) goto Failure; @@ -3396,8 +3560,8 @@ result_t wuss_test(const char *resources) /* chromeless and the on-screen nudge drags the top-left back to (0,0), * so the clamp caps content at the full 200x200 screen */ box_big.x0 = 10; box_big.y0 = 10; box_big.x1 = 400; box_big.y1 = 400; - rc = wuss_window_create(wuss, &box_big, "BIG", chromeless, - wuss_NO_BACKGROUND, NULL, SIZE2D(400, 400), + rc = wuss_window_create(mk_task(wuss, NULL, NULL), &box_big, "BIG", chromeless, + wuss_NO_BACKGROUND, SIZE2D(400, 400), SIZE2D(0, 0), &win_big); if (rc != result_OK) goto Failure; From fc65709cd0d82c98991d672bb2df4a83d4350793 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Wed, 2 Sep 2026 12:59:24 +0100 Subject: [PATCH 177/287] test(wuss): port the SDL task drivers to the registered-task API Migrate the 14 interactive task drivers and apps/wuss/main.c off the removed by-value task struct: - wuss_task_desc_t + wuss_task_create replace wuss_task_start; handlers are wuss_window_fn_t. - wuss_window_create_placed drops its leading wuss_t *; the owning task comes first. - apps/wuss routes menu picks through a menu_task whose handler takes wuss_EVENT_MENU_SELECT, replacing the removed wuss_menu_select_fn_t callbacks. Drop the wuss_window_close(window) call from every task's CLOSE handler: wuss_window_try_close now tears the window down itself after delivering CLOSE, so the handler call was a double free. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- apps/wuss/main.c | 87 ++++++++++++++++--------- libraries/wuss/test/tasks/ball.c | 16 +++-- libraries/wuss/test/tasks/ball.h | 2 +- libraries/wuss/test/tasks/blank.c | 16 +++-- libraries/wuss/test/tasks/blank.h | 3 +- libraries/wuss/test/tasks/chars.c | 16 +++-- libraries/wuss/test/tasks/chars.h | 2 +- libraries/wuss/test/tasks/checker.c | 18 +++-- libraries/wuss/test/tasks/checker.h | 2 +- libraries/wuss/test/tasks/curve.c | 16 +++-- libraries/wuss/test/tasks/curve.h | 2 +- libraries/wuss/test/tasks/gradient.c | 20 ++++-- libraries/wuss/test/tasks/gradient.h | 2 +- libraries/wuss/test/tasks/icons.c | 14 ++-- libraries/wuss/test/tasks/icons.h | 2 +- libraries/wuss/test/tasks/image.c | 15 +++-- libraries/wuss/test/tasks/image.h | 2 +- libraries/wuss/test/tasks/lissajous.c | 16 +++-- libraries/wuss/test/tasks/lissajous.h | 2 +- libraries/wuss/test/tasks/palette.c | 16 +++-- libraries/wuss/test/tasks/palette.h | 2 +- libraries/wuss/test/tasks/porter-duff.c | 15 +++-- libraries/wuss/test/tasks/porter-duff.h | 2 +- libraries/wuss/test/tasks/sofa.c | 16 +++-- libraries/wuss/test/tasks/sofa.h | 2 +- libraries/wuss/test/tasks/swatches.c | 14 ++-- libraries/wuss/test/tasks/swatches.h | 2 +- libraries/wuss/test/tasks/text.c | 14 ++-- libraries/wuss/test/tasks/text.h | 2 +- 29 files changed, 218 insertions(+), 120 deletions(-) diff --git a/apps/wuss/main.c b/apps/wuss/main.c index 8a2d5f7e..1f6df726 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -18,6 +18,7 @@ #include "framebuf/screen.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" @@ -52,6 +53,7 @@ static struct { wuss_t *wuss; + wuss_task_t *menu_task; /* owns the menus and the hidden Details window */ const colour_t *palette; int npalette; const char *resources; @@ -254,16 +256,13 @@ static const wuss_menu_t g_menu = "Display", g_menu_items, NELEMS(g_menu_items) }; -static void menu_selected(const wuss_menu_t *menu, - int index, - wuss_button_t button, - void *ctx) -{ - NOT_USED(button); - NOT_USED(ctx); - printf("menu: picked \"%s\"\n", - menu->items[index].text ? menu->items[index].text : "(sep)"); -} +/* g_task_menu picks are dispatched by index; g_menu / g_menu_desc picks just + * print. All three menus are opened by g.menu_task, so one handler sees every + * wuss_EVENT_MENU_SELECT and tells them apart by data.menu_select.menu. + * Defined after g_task_menu / g_task_spawn, which it needs. */ +static result_t menu_handle(wuss_window_t *window, + const wuss_event_t *event, + void *task_data); /* index of the "Details" row in g_menu_items */ #define G_MENU_DETAILS_INDEX 4 @@ -280,13 +279,12 @@ static result_t spawn_menu(void) content.y0 = 0; content.x1 = 180; content.y1 = 120; - rc = wuss_window_create(g.wuss, &content, "Details", + 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), - NULL, SIZE2D(180, 120), SIZE2D(0, 0), &g_menu_details_window); if (rc != result_OK) @@ -294,8 +292,7 @@ static result_t spawn_menu(void) g_menu_items[G_MENU_DETAILS_INDEX].window = g_menu_details_window; } - return wuss_menu_open(g.wuss, &g_menu, wuss_get_pointer(g.wuss), - menu_selected, NULL, NULL); + 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 @@ -330,8 +327,8 @@ static result_t spawn_menu_desc(void) wuss_menu_destroy(g_menu_desc); g_menu_desc = m; - return wuss_menu_open(g.wuss, g_menu_desc, wuss_get_pointer(g.wuss), - menu_selected, NULL, NULL); + 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 @@ -379,16 +376,32 @@ static const wuss_menu_t g_task_menu = "Tasks", g_task_items, NELEMS(g_task_items) }; -static void task_menu_selected(const wuss_menu_t *menu, - int index, - wuss_button_t button, - void *ctx) +static result_t menu_handle(wuss_window_t *window, + const wuss_event_t *event, + void *task_data) { - NOT_USED(menu); - NOT_USED(button); - NOT_USED(ctx); - if (index >= 0 && index < (int) NELEMS(g_task_spawn)) - (void) g_task_spawn[index](); + const wuss_menu_t *menu; + int index; + + NOT_USED(window); + NOT_USED(task_data); + + if (event->kind != wuss_EVENT_MENU_SELECT) + return result_OK; + + menu = event->data.menu_select.menu; + index = event->data.menu_select.index; + + if (menu == &g_task_menu) + { + if (index >= 0 && index < (int) NELEMS(g_task_spawn)) + (void) g_task_spawn[index](); + return result_OK; + } + + printf("menu: picked \"%s\"\n", + menu->items[index].text ? menu->items[index].text : "(sep)"); + return result_OK; } static wuss_button_t sdl_button_to_wuss(Uint8 button) @@ -584,6 +597,17 @@ static result_t run_wuss(const char *resources) g.resources = resources; g.daydream_font = daydream_font; + { + wuss_task_desc_t desc; + + desc.handle = menu_handle; + desc.task_data = NULL; + desc.name = "menu"; + rc = wuss_task_create(wuss, &desc, &g.menu_task); + if (rc != result_OK) + goto Failure; + } + quit = false; g.quit = false; garbage_pending = false; @@ -646,8 +670,7 @@ static result_t run_wuss(const char *resources) /* MENU click on bare backdrop opens the task launcher there */ if (hit == NULL && (button & wuss_BUTTON_MENU)) - wuss_menu_open(wuss, &g_task_menu, POINT(x, y), - task_menu_selected, NULL, NULL); + wuss_menu_open(g.menu_task, &g_task_menu, POINT(x, y), NULL); } break; @@ -752,13 +775,13 @@ static result_t run_wuss(const char *resources) SDL_Delay(1000 / 60); } - /* ponytail: wuss_destroy() below frees every still-open window but not the - * per-instance task block hung off it, so any task window left open at quit - * leaks its block. Harmless at process exit; add a wuss close callback if a - * task ever needs deterministic teardown. */ + /* ponytail: wuss_destroy() below force-closes every still-open window and + * frees every registered task node, but not the per-instance task_data + * block a spawn_* calloc'd, so any task window left open at quit leaks that + * block. Harmless at process exit. */ wuss_menu_destroy(g_menu_desc); - wuss_destroy(wuss); + wuss_destroy(wuss); /* also sweeps g.menu_task */ bmfont_destroy(font); bmfont_destroy(daydream_font); diff --git a/libraries/wuss/test/tasks/ball.c b/libraries/wuss/test/tasks/ball.c index 29352abe..0f700ecc 100644 --- a/libraries/wuss/test/tasks/ball.c +++ b/libraries/wuss/test/tasks/ball.c @@ -35,7 +35,9 @@ static box_t ball_local_box(int vx0, result_t ball_create(wuss_t *wuss, ball_task_t *task) { - wuss_task_t delegate; + wuss_task_t *delegate; + wuss_task_desc_t delegate_desc; + result_t rc; task->bg = colour_rgb(0xFF, 0x00, 0x00); task->ball = colour_rgb(0xFF, 0xFF, 0xFF); @@ -47,14 +49,19 @@ result_t ball_create(wuss_t *wuss, ball_task_t *task) task->balls[0].dy = 2; task->balls[0].radius = 8; - delegate = wuss_task_start(ball_handle, task); /* ball_redraw paints its own background every frame */ + /* ball_redraw paints its own background every frame */ + delegate_desc.handle = ball_handle; + delegate_desc.task_data = task; + delegate_desc.name = "ball"; + rc = wuss_task_create(wuss, &delegate_desc, &delegate); + if (rc != result_OK) + return rc; - return wuss_window_create_placed(wuss, + return wuss_window_create_placed(delegate, SIZE2D(200, 160), "Bouncing Ball", wuss_WINDOW_NONE, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), - &delegate, SIZE2D(200, 160), SIZE2D(0, 0), &task->window); @@ -213,7 +220,6 @@ result_t ball_handle(wuss_window_t *window, return ball_idle(task_data); case wuss_EVENT_CLOSE: - wuss_window_close(window); free(bc); /* task_data was calloc'd per instance by the spawner */ return result_OK; diff --git a/libraries/wuss/test/tasks/ball.h b/libraries/wuss/test/tasks/ball.h index ae373f25..c6605d6e 100644 --- a/libraries/wuss/test/tasks/ball.h +++ b/libraries/wuss/test/tasks/ball.h @@ -32,7 +32,7 @@ typedef struct ball_task } ball_task_t; -wuss_event_fn_t ball_handle; +wuss_window_fn_t ball_handle; /* create the bouncing-ball window against the given wuss instance; "task" is a * per-instance block owned by the window and freed when it closes */ diff --git a/libraries/wuss/test/tasks/blank.c b/libraries/wuss/test/tasks/blank.c index 55d651e5..2e1fc3cd 100644 --- a/libraries/wuss/test/tasks/blank.c +++ b/libraries/wuss/test/tasks/blank.c @@ -19,20 +19,27 @@ result_t blank_create(wuss_t *wuss, blank_task_t *task) { - wuss_task_t delegate; + 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; task->frame_count = 0; - delegate = wuss_task_start(blank_handle, task); /* wuss fills the content area itself */ + /* wuss fills the content area itself */ + delegate_desc.handle = blank_handle; + delegate_desc.task_data = task; + delegate_desc.name = "blank"; + rc = wuss_task_create(wuss, &delegate_desc, &delegate); + if (rc != result_OK) + return rc; - return wuss_window_create_placed(wuss, + return wuss_window_create_placed(delegate, SIZE2D(200, 160), NULL, wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE, wuss_BACKDROP_COLOUR(task->index), - &delegate, SIZE2D(200, 160), SIZE2D(0, 0), &task->window); @@ -65,7 +72,6 @@ result_t blank_handle(wuss_window_t *window, { if (event->kind == wuss_EVENT_CLOSE) { - wuss_window_close(window); free(task_data); /* calloc'd per instance by the spawner */ return result_OK; } diff --git a/libraries/wuss/test/tasks/blank.h b/libraries/wuss/test/tasks/blank.h index 3a42f270..c1b94ebe 100644 --- a/libraries/wuss/test/tasks/blank.h +++ b/libraries/wuss/test/tasks/blank.h @@ -6,6 +6,7 @@ #ifdef USE_SDL #include "wuss/window.h" +#include "wuss/task.h" #include "wuss/wuss.h" /* window C's task: no redraw callback at all, relying entirely on wuss's @@ -20,7 +21,7 @@ typedef struct blank_task } blank_task_t; -wuss_event_fn_t blank_handle; +wuss_window_fn_t blank_handle; /* create the colour-cycling blank window against the given wuss instance */ result_t blank_create(wuss_t *wuss, blank_task_t *task); diff --git a/libraries/wuss/test/tasks/chars.c b/libraries/wuss/test/tasks/chars.c index cf98bece..1bee8476 100644 --- a/libraries/wuss/test/tasks/chars.c +++ b/libraries/wuss/test/tasks/chars.c @@ -23,7 +23,9 @@ result_t chars_create(wuss_t *wuss, chars_task_t *task) { - wuss_task_t delegate; + wuss_task_t *delegate; + wuss_task_desc_t delegate_desc; + result_t rc; size2d_t sz; bmfont_t *font; int font_width, font_height, cell_w, cell_h; @@ -43,10 +45,16 @@ result_t chars_create(wuss_t *wuss, chars_task_t *task) task->fg = colour_rgb(0x00, 0x00, 0x00); task->bg = colour_rgb(0xFF, 0xFF, 0xFF); - delegate = wuss_task_start(chars_handle, task); /* chars_redraw paints every cell itself */ + /* chars_redraw paints every cell itself */ + delegate_desc.handle = chars_handle; + delegate_desc.task_data = task; + delegate_desc.name = "chars"; + rc = wuss_task_create(wuss, &delegate_desc, &delegate); + if (rc != result_OK) + return rc; sz = SIZE2D(cell_w * CHARS_COLS, cell_h * CHARS_ROWS); - return wuss_window_create_placed(wuss, + return wuss_window_create_placed(delegate, sz, "Chars", wuss_WINDOW_NO_RESIZE | @@ -54,7 +62,6 @@ result_t chars_create(wuss_t *wuss, chars_task_t *task) wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), - &delegate, sz, SIZE2D(0, 0), &task->window); @@ -119,7 +126,6 @@ result_t chars_handle(wuss_window_t *window, { if (event->kind == wuss_EVENT_CLOSE) { - wuss_window_close(window); free(task_data); /* calloc'd per instance by the spawner */ return result_OK; } diff --git a/libraries/wuss/test/tasks/chars.h b/libraries/wuss/test/tasks/chars.h index d43fa125..eb2dc367 100644 --- a/libraries/wuss/test/tasks/chars.h +++ b/libraries/wuss/test/tasks/chars.h @@ -19,7 +19,7 @@ typedef struct chars_task } chars_task_t; -wuss_event_fn_t chars_handle; +wuss_window_fn_t chars_handle; /* create the glyph-grid window against the given wuss instance; does * nothing and returns result_OK if wuss has no system font */ diff --git a/libraries/wuss/test/tasks/checker.c b/libraries/wuss/test/tasks/checker.c index 7fc5c957..6ed98497 100644 --- a/libraries/wuss/test/tasks/checker.c +++ b/libraries/wuss/test/tasks/checker.c @@ -20,7 +20,8 @@ result_t checker_create(wuss_t*wuss, checker_task_t *task) { - wuss_task_t delegate; + wuss_task_t *delegate; + wuss_task_desc_t delegate_desc; result_t rc; task->black = colour_rgb(0x00, 0x00, 0x00); @@ -30,26 +31,30 @@ result_t checker_create(wuss_t*wuss, checker_task_t *task) task->band = CHECKER_BAND_DEFAULT; task->band2 = CHECKER_BAND_DEFAULT; - delegate = wuss_task_start(checker_handle, task); /* checker_redraw paints every pixel itself */ + /* checker_redraw paints every pixel itself */ + delegate_desc.handle = checker_handle; + delegate_desc.task_data = task; + delegate_desc.name = "checker"; + rc = wuss_task_create(wuss, &delegate_desc, &delegate); + if (rc != result_OK) + return rc; - rc = wuss_window_create_placed(wuss, + rc = wuss_window_create_placed(delegate, SIZE2D(160, 160), "Checker 1", wuss_WINDOW_NONE, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), - &delegate, SIZE2D(160, 160), SIZE2D(0, 0), &task->window); if (rc != result_OK) return rc; - rc = wuss_window_create_placed(wuss, + rc = wuss_window_create_placed(delegate, SIZE2D(160, 160), "Checker 2", wuss_WINDOW_NONE, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), - &delegate, SIZE2D(160, 160), SIZE2D(0, 0), &task->window2); @@ -160,7 +165,6 @@ result_t checker_handle(wuss_window_t *window, return checker_scroll(window, event->data.scroll.delta, task_data); case wuss_EVENT_CLOSE: - wuss_window_close(window); if (window == cc->window2) cc->window2 = NULL; else diff --git a/libraries/wuss/test/tasks/checker.h b/libraries/wuss/test/tasks/checker.h index 9983ecc3..565fe7a9 100644 --- a/libraries/wuss/test/tasks/checker.h +++ b/libraries/wuss/test/tasks/checker.h @@ -30,7 +30,7 @@ typedef struct checker_task } checker_task_t; -wuss_event_fn_t checker_handle; +wuss_window_fn_t checker_handle; /* create the two checkerboard windows against the given wuss instance */ result_t checker_create(wuss_t *wuss, checker_task_t *task); diff --git a/libraries/wuss/test/tasks/curve.c b/libraries/wuss/test/tasks/curve.c index 0f91fb05..fe586d62 100644 --- a/libraries/wuss/test/tasks/curve.c +++ b/libraries/wuss/test/tasks/curve.c @@ -25,7 +25,9 @@ result_t curve_create(wuss_t *wuss, curve_task_t *task) { - wuss_task_t delegate; + 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); @@ -38,14 +40,19 @@ result_t curve_create(wuss_t *wuss, curve_task_t *task) task->points[2] = POINT(210, 10); task->points[3] = POINT(210, 140); - delegate = wuss_task_start(curve_handle, task); /* curve_redraw paints its own background */ + /* curve_redraw paints its own background */ + delegate_desc.handle = curve_handle; + delegate_desc.task_data = task; + delegate_desc.name = "curve"; + rc = wuss_task_create(wuss, &delegate_desc, &delegate); + if (rc != result_OK) + return rc; - return wuss_window_create_placed(wuss, + return wuss_window_create_placed(delegate, SIZE2D(220, 160), "Curve", wuss_WINDOW_NONE, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), - &delegate, SIZE2D(220, 160), SIZE2D(0, 0), &task->window); @@ -175,7 +182,6 @@ result_t curve_handle(wuss_window_t *window, return curve_scroll(task, event->data.scroll.delta, window); case wuss_EVENT_CLOSE: - wuss_window_close(window); free(task); /* task_data was calloc'd per instance by the spawner */ return result_OK; diff --git a/libraries/wuss/test/tasks/curve.h b/libraries/wuss/test/tasks/curve.h index 59ce2516..f8db1d99 100644 --- a/libraries/wuss/test/tasks/curve.h +++ b/libraries/wuss/test/tasks/curve.h @@ -23,7 +23,7 @@ typedef struct curve_task } curve_task_t; -wuss_event_fn_t curve_handle; +wuss_window_fn_t curve_handle; /* create the curve window against the given wuss instance */ result_t curve_create(wuss_t *wuss, curve_task_t *task); diff --git a/libraries/wuss/test/tasks/gradient.c b/libraries/wuss/test/tasks/gradient.c index b8b746ee..8df6f1a6 100644 --- a/libraries/wuss/test/tasks/gradient.c +++ b/libraries/wuss/test/tasks/gradient.c @@ -35,16 +35,23 @@ static int dither(int v, int x, int y) result_t gradient_create(wuss_t *wuss, gradient_task_t *task) { - wuss_task_t delegate; - - delegate = wuss_task_start(gradient_handle, task); /* gradient_redraw paints every pixel itself */ - - return wuss_window_create_placed(wuss, + wuss_task_t *delegate; + wuss_task_desc_t delegate_desc; + result_t rc; + + /* gradient_redraw paints every pixel itself */ + delegate_desc.handle = gradient_handle; + delegate_desc.task_data = task; + delegate_desc.name = "gradient"; + rc = wuss_task_create(wuss, &delegate_desc, &delegate); + if (rc != result_OK) + return rc; + + return wuss_window_create_placed(delegate, SIZE2D(GRADIENT_OPEN_WIDTH, GRADIENT_OPEN_HEIGHT), "Gradient", wuss_WINDOW_NONE, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), - &delegate, SIZE2D(GRADIENT_DOC_WIDTH, GRADIENT_DOC_HEIGHT), SIZE2D(0, 0), &task->window); @@ -95,7 +102,6 @@ result_t gradient_handle(wuss_window_t *window, return gradient_redraw(event, task_data); case wuss_EVENT_CLOSE: - wuss_window_close(window); 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 9f475596..db7898c0 100644 --- a/libraries/wuss/test/tasks/gradient.h +++ b/libraries/wuss/test/tasks/gradient.h @@ -16,7 +16,7 @@ typedef struct gradient_task } gradient_task_t; -wuss_event_fn_t gradient_handle; +wuss_window_fn_t gradient_handle; /* create the gradient window against the given wuss instance */ result_t gradient_create(wuss_t *wuss, gradient_task_t *task); diff --git a/libraries/wuss/test/tasks/icons.c b/libraries/wuss/test/tasks/icons.c index 97bf3bce..226643d5 100644 --- a/libraries/wuss/test/tasks/icons.c +++ b/libraries/wuss/test/tasks/icons.c @@ -36,7 +36,8 @@ result_t icons_create(wuss_t *wuss, * interactive one that bumps the counter, then [.. +4] a strip of menu-entry * icons that hover-highlight */ enum { ICONS_NSPECS = 4 + 3 + 5 + 2 + 5 }; - wuss_task_t delegate; + wuss_task_t *delegate; + wuss_task_desc_t delegate_desc; wuss_colour_t black, grey5, grey6; wuss_icon_spec_t specs[ICONS_NSPECS]; wuss_icon_t *made[ICONS_NSPECS]; @@ -68,16 +69,20 @@ result_t icons_create(wuss_t *wuss, if (bitmap_load_png(&task->sprite, sprite_path) == result_OK) task->has_sprite = 1; - delegate = wuss_task_start(icons_handle, task); + delegate_desc.handle = icons_handle; + delegate_desc.task_data = task; + delegate_desc.name = "icons"; + rc = wuss_task_create(wuss, &delegate_desc, &delegate); + if (rc != result_OK) + return rc; - rc = wuss_window_create_placed(wuss, + rc = wuss_window_create_placed(delegate, SIZE2D(ICONS_DOC_W, 160), "Icons", wuss_WINDOW_NONE, wuss_BACKDROP_PATTERN(grey5, screen_PATTERN_CROSSHATCH, grey6), - &delegate, SIZE2D(ICONS_DOC_W, ICONS_DOC_H), SIZE2D(0, 0), &task->window); @@ -387,7 +392,6 @@ result_t icons_handle(wuss_window_t *window, return icons_icon(event, task_data); case wuss_EVENT_CLOSE: - wuss_window_close(window); /* frees the icons, which only borrowed sprite */ if (tcx->has_sprite) free(tcx->sprite.base); free(tcx); /* calloc'd per instance by the spawner */ diff --git a/libraries/wuss/test/tasks/icons.h b/libraries/wuss/test/tasks/icons.h index 9707ad61..1780f4df 100644 --- a/libraries/wuss/test/tasks/icons.h +++ b/libraries/wuss/test/tasks/icons.h @@ -31,7 +31,7 @@ typedef struct icons_task } icons_task_t; -wuss_event_fn_t icons_handle; +wuss_window_fn_t icons_handle; /* create the icons window against the given wuss instance */ result_t icons_create(wuss_t *wuss, diff --git a/libraries/wuss/test/tasks/image.c b/libraries/wuss/test/tasks/image.c index 381590db..a29edede 100644 --- a/libraries/wuss/test/tasks/image.c +++ b/libraries/wuss/test/tasks/image.c @@ -21,7 +21,8 @@ result_t image_create(wuss_t *wuss, const char *background_path, image_task_t *task) { - wuss_task_t delegate; + wuss_task_t *delegate; + wuss_task_desc_t delegate_desc; result_t rc; size2d_t sz; @@ -36,18 +37,23 @@ result_t image_create(wuss_t *wuss, return rc; } - delegate = wuss_task_start(image_handle, task); /* shows through the image's transparent pixels */ + /* shows through the image's transparent pixels */ + delegate_desc.handle = image_handle; + delegate_desc.task_data = task; + delegate_desc.name = "image"; + rc = wuss_task_create(wuss, &delegate_desc, &delegate); + if (rc != result_OK) + return rc; sz.w = task->bitmap.size.w + BORDER * 2; sz.h = task->bitmap.size.h + BORDER * 2; - return wuss_window_create_placed(wuss, + return wuss_window_create_placed(delegate, /* shorter than the bitmap so there's something to scroll through */ SIZE2D(sz.w, sz.h * 2 / 3), "Image", wuss_WINDOW_NONE, wuss_BACKDROP_COLOUR(palette_PICO8_PINK), - &delegate, sz, SIZE2D(32, 32), &task->window); @@ -98,7 +104,6 @@ result_t image_handle(wuss_window_t *window, return image_redraw(event, task_data); case wuss_EVENT_CLOSE: - wuss_window_close(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 8943564c..906f3157 100644 --- a/libraries/wuss/test/tasks/image.h +++ b/libraries/wuss/test/tasks/image.h @@ -19,7 +19,7 @@ typedef struct image_task } image_task_t; -wuss_event_fn_t image_handle; +wuss_window_fn_t image_handle; /* load the PNG at path (and the 9-patch PNG at background_path, drawn tiled * behind it) and create its window against the given wuss instance */ diff --git a/libraries/wuss/test/tasks/lissajous.c b/libraries/wuss/test/tasks/lissajous.c index 8c26a674..36b6fabe 100644 --- a/libraries/wuss/test/tasks/lissajous.c +++ b/libraries/wuss/test/tasks/lissajous.c @@ -23,7 +23,9 @@ static const int lissajous_freqs[][2] = result_t lissajous_create(wuss_t *wuss, lissajous_task_t *task) { - wuss_task_t delegate; + 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); @@ -33,14 +35,19 @@ result_t lissajous_create(wuss_t *wuss, lissajous_task_t *task) task->phase = 0.0; task->drift = 0.01; - delegate = wuss_task_start(lissajous_handle, task); /* redraw paints its own background every frame */ + /* redraw paints its own background every frame */ + delegate_desc.handle = lissajous_handle; + delegate_desc.task_data = task; + delegate_desc.name = "lissajous"; + rc = wuss_task_create(wuss, &delegate_desc, &delegate); + if (rc != result_OK) + return rc; - return wuss_window_create_placed(wuss, + return wuss_window_create_placed(delegate, SIZE2D(220, 220), "Lissajous", wuss_WINDOW_NONE, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), - &delegate, SIZE2D(220, 220), SIZE2D(0, 0), &task->window); @@ -154,7 +161,6 @@ result_t lissajous_handle(wuss_window_t *window, return lissajous_idle(task_data); case wuss_EVENT_CLOSE: - wuss_window_close(window); free(lc); /* task_data was calloc'd per instance by the spawner */ return result_OK; diff --git a/libraries/wuss/test/tasks/lissajous.h b/libraries/wuss/test/tasks/lissajous.h index 36e359a4..be6ea5ea 100644 --- a/libraries/wuss/test/tasks/lissajous.h +++ b/libraries/wuss/test/tasks/lissajous.h @@ -24,7 +24,7 @@ typedef struct lissajous_task } lissajous_task_t; -wuss_event_fn_t lissajous_handle; +wuss_window_fn_t lissajous_handle; /* create the Lissajous window against the given wuss instance; "task" is a * per-instance block owned by the window and freed when it closes */ diff --git a/libraries/wuss/test/tasks/palette.c b/libraries/wuss/test/tasks/palette.c index 0bf9cde2..ddc30625 100644 --- a/libraries/wuss/test/tasks/palette.c +++ b/libraries/wuss/test/tasks/palette.c @@ -19,19 +19,26 @@ result_t palette_create(wuss_t *wuss, int npalette, palette_task_t *task) { - wuss_task_t delegate; + wuss_task_t *delegate; + wuss_task_desc_t delegate_desc; + result_t rc; task->palette = palette; task->npalette = npalette; - delegate = wuss_task_start(palette_handle, task); /* backdrop for any rounding gap around the grid */ + /* backdrop for any rounding gap around the grid */ + delegate_desc.handle = palette_handle; + delegate_desc.task_data = task; + delegate_desc.name = "palette"; + rc = wuss_task_create(wuss, &delegate_desc, &delegate); + if (rc != result_OK) + return rc; - return wuss_window_create_placed(wuss, + return wuss_window_create_placed(delegate, SIZE2D(100, 100), "Palette", wuss_WINDOW_NO_RESIZE_BLIT, /* swatch grid is laid out across the whole window, so a resize must redraw all of it, not just the newly (un)covered edge */ wuss_BACKDROP_COLOUR(palette_PICO8_BLACK), - &delegate, SIZE2D(100, 100), SIZE2D(0, 0), &task->window); @@ -85,7 +92,6 @@ result_t palette_handle(wuss_window_t *window, { if (event->kind == wuss_EVENT_CLOSE) { - wuss_window_close(window); free(task_data); /* calloc'd per instance by the spawner */ return result_OK; } diff --git a/libraries/wuss/test/tasks/palette.h b/libraries/wuss/test/tasks/palette.h index ea8b3cc6..6b445e62 100644 --- a/libraries/wuss/test/tasks/palette.h +++ b/libraries/wuss/test/tasks/palette.h @@ -18,7 +18,7 @@ typedef struct palette_task } palette_task_t; -wuss_event_fn_t palette_handle; +wuss_window_fn_t palette_handle; /* create the palette-swatch-grid window against the given wuss instance */ result_t palette_create(wuss_t *wuss, diff --git a/libraries/wuss/test/tasks/porter-duff.c b/libraries/wuss/test/tasks/porter-duff.c index 15db71fb..b2974393 100644 --- a/libraries/wuss/test/tasks/porter-duff.c +++ b/libraries/wuss/test/tasks/porter-duff.c @@ -163,7 +163,8 @@ result_t porter_duff_create(wuss_t *wuss, const char *resources, porter_duff_task_t *task) { - wuss_task_t delegate; + wuss_task_t *delegate; + wuss_task_desc_t delegate_desc; result_t rc; task->font = font; @@ -191,14 +192,19 @@ result_t porter_duff_create(wuss_t *wuss, if (rc != result_OK) goto free_src; - delegate = wuss_task_start(porter_duff_handle, task); /* porter_duff_redraw paints every pixel itself */ + /* porter_duff_redraw paints every pixel itself */ + delegate_desc.handle = porter_duff_handle; + delegate_desc.task_data = task; + delegate_desc.name = "porter-duff"; + rc = wuss_task_create(wuss, &delegate_desc, &delegate); + if (rc != result_OK) + return rc; - rc = wuss_window_create_placed(wuss, + rc = wuss_window_create_placed(delegate, SIZE2D(PD_SIZE, PD_SIZE + PD_LABEL_HEIGHT), "Porter-Duff", wuss_WINDOW_NONE, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), - &delegate, SIZE2D(PD_SIZE, PD_SIZE + PD_LABEL_HEIGHT), SIZE2D(0, 0), &task->window); @@ -396,7 +402,6 @@ result_t porter_duff_handle(wuss_window_t *window, return porter_duff_idle(task_data); case wuss_EVENT_CLOSE: - wuss_window_close(window); free(pd->dst.base); free(pd->src.base); free(pd->b.base); diff --git a/libraries/wuss/test/tasks/porter-duff.h b/libraries/wuss/test/tasks/porter-duff.h index 361722f7..ce58ea7e 100644 --- a/libraries/wuss/test/tasks/porter-duff.h +++ b/libraries/wuss/test/tasks/porter-duff.h @@ -32,7 +32,7 @@ typedef struct porter_duff_task } porter_duff_task_t; -wuss_event_fn_t porter_duff_handle; +wuss_window_fn_t porter_duff_handle; /* load the two demo images and create the window against the given wuss * instance; resources is the DPTLib repo root, for locating the bundled PNGs */ diff --git a/libraries/wuss/test/tasks/sofa.c b/libraries/wuss/test/tasks/sofa.c index 70870bd9..27e0b493 100644 --- a/libraries/wuss/test/tasks/sofa.c +++ b/libraries/wuss/test/tasks/sofa.c @@ -356,7 +356,9 @@ static void draw_vertex_dots(screen_t *scr, result_t sofa_create(wuss_t*wuss, sofa_task_t*task) { - wuss_task_t delegate; + 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); @@ -367,14 +369,19 @@ result_t sofa_create(wuss_t*wuss, sofa_task_t*task) task->shape = sofa_SHAPE_SOFA; task->turns = 0; - delegate = wuss_task_start(sofa_handle, task); /* sofa_redraw paints its own background every frame */ + /* sofa_redraw paints its own background every frame */ + delegate_desc.handle = sofa_handle; + delegate_desc.task_data = task; + delegate_desc.name = "sofa"; + rc = wuss_task_create(wuss, &delegate_desc, &delegate); + if (rc != result_OK) + return rc; - return wuss_window_create_placed(wuss, + return wuss_window_create_placed(delegate, SIZE2D(180, 160), "Sofa", wuss_WINDOW_NONE, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), - &delegate, SIZE2D(180, 160), SIZE2D(0, 0), &task->window); @@ -582,7 +589,6 @@ result_t sofa_handle(wuss_window_t *window, return sofa_idle(task_data); case wuss_EVENT_CLOSE: - wuss_window_close(window); free(sc); /* task_data was calloc'd per instance by the spawner */ return result_OK; diff --git a/libraries/wuss/test/tasks/sofa.h b/libraries/wuss/test/tasks/sofa.h index 280f2793..1551766f 100644 --- a/libraries/wuss/test/tasks/sofa.h +++ b/libraries/wuss/test/tasks/sofa.h @@ -40,7 +40,7 @@ typedef struct sofa_task } sofa_task_t; -wuss_event_fn_t sofa_handle; +wuss_window_fn_t sofa_handle; /* create the sofa window against the given wuss instance */ result_t sofa_create(wuss_t*wuss, sofa_task_t*task); diff --git a/libraries/wuss/test/tasks/swatches.c b/libraries/wuss/test/tasks/swatches.c index 22cdd12c..43db9502 100644 --- a/libraries/wuss/test/tasks/swatches.c +++ b/libraries/wuss/test/tasks/swatches.c @@ -73,21 +73,26 @@ static result_t swatches_build(swatches_task_t *task) result_t swatches_create(wuss_t *wuss, swatches_task_t *task) { - wuss_task_t delegate; + wuss_task_t *delegate; + wuss_task_desc_t delegate_desc; result_t rc; task->wuss = wuss; task->window = NULL; memset(task->icons, 0, sizeof(task->icons)); - delegate = wuss_task_start(swatches_handle, task); + delegate_desc.handle = swatches_handle; + delegate_desc.task_data = task; + delegate_desc.name = "swatches"; + rc = wuss_task_create(wuss, &delegate_desc, &delegate); + if (rc != result_OK) + return rc; - rc = wuss_window_create_placed(wuss, + rc = wuss_window_create_placed(delegate, SIZE2D(SWATCHES_DOC_W, 140), "Swatches", wuss_WINDOW_NONE, wuss_BACKDROP_COLOUR(wuss_nearest_colour(wuss, 0xDD, 0xDD, 0xDD)), - &delegate, SIZE2D(SWATCHES_DOC_W, SWATCHES_DOC_H), SIZE2D(0, 0), &task->window); @@ -119,7 +124,6 @@ result_t swatches_handle(wuss_window_t *window, return swatches_build(task); case wuss_EVENT_CLOSE: - wuss_window_close(window); free(task_data); /* calloc'd per instance by the spawner */ return result_OK; diff --git a/libraries/wuss/test/tasks/swatches.h b/libraries/wuss/test/tasks/swatches.h index 96253d6e..4dc3f698 100644 --- a/libraries/wuss/test/tasks/swatches.h +++ b/libraries/wuss/test/tasks/swatches.h @@ -22,7 +22,7 @@ typedef struct swatches_task } swatches_task_t; -wuss_event_fn_t swatches_handle; +wuss_window_fn_t swatches_handle; /* create the swatches window against the given wuss instance */ result_t swatches_create(wuss_t *wuss, swatches_task_t *task); diff --git a/libraries/wuss/test/tasks/text.c b/libraries/wuss/test/tasks/text.c index 65a2b233..3aee0b6a 100644 --- a/libraries/wuss/test/tasks/text.c +++ b/libraries/wuss/test/tasks/text.c @@ -32,7 +32,8 @@ result_t text_create(wuss_t *wuss, bmfont_t *font, text_task_t *task) { - wuss_task_t delegate; + wuss_task_t *delegate; + wuss_task_desc_t delegate_desc; size2d_t sz; result_t rc; @@ -42,18 +43,22 @@ result_t text_create(wuss_t *wuss, task->frame_count = 0; task->resizing = true; - delegate = wuss_task_start(text_handle, task); + delegate_desc.handle = text_handle; + delegate_desc.task_data = task; + delegate_desc.name = "text"; + rc = wuss_task_create(wuss, &delegate_desc, &delegate); + if (rc != result_OK) + return rc; sz = SIZE2D(220, 180); task->base_width = sz.w; task->base_height = sz.h; - rc = wuss_window_create_placed(wuss, + rc = wuss_window_create_placed(delegate, sz, "Lorem Ipsum", 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)), - &delegate, sz, SIZE2D(0, 0), &task->window); @@ -155,7 +160,6 @@ result_t text_handle(wuss_window_t *window, return text_mouse(task_data); case wuss_EVENT_CLOSE: - wuss_window_close(window); free(tcx); /* task_data was calloc'd per instance by the spawner */ return result_OK; diff --git a/libraries/wuss/test/tasks/text.h b/libraries/wuss/test/tasks/text.h index f729b786..98e0b7be 100644 --- a/libraries/wuss/test/tasks/text.h +++ b/libraries/wuss/test/tasks/text.h @@ -26,7 +26,7 @@ typedef struct text_task } text_task_t; -wuss_event_fn_t text_handle; +wuss_window_fn_t text_handle; /* create the paragraph-of-text window against the given wuss instance */ result_t text_create(wuss_t *wuss, From 240bb7e9c75fc3c1e3ce886746eca57686955a08 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Wed, 2 Sep 2026 12:59:27 +0100 Subject: [PATCH 178/287] docs(wuss): changelog the event-dispatch rework Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- CHANGELOG.md | 46 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92684e80..507c0f27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,11 +15,11 @@ _Unreleased_ until one is cut. `wuss_menu_t` (title plus an array of `wuss_menu_item_t`) as a borderless window, nudged to stay on screen and opened under the pointer; wuss owns layout, submenu chaining on hover and whole-chain dismissal. Per-item flags - cover ticks, disabled rows, dashed separators and submenus. - `wuss_menu_select_fn_t` reports a leaf pick — SELECT closes the chain, ADJUST - keeps it open. `wuss_menu_close()` / `wuss_menu_is_open()` manage a chain by - handle. An over-tall menu gets a real vertical scrollbar instead of being - cropped. + cover ticks, disabled rows, dashed separators and submenus. A leaf pick is + delivered to the opening task as `wuss_EVENT_MENU_SELECT` — SELECT closes + the chain, ADJUST keeps it open. `wuss_menu_close()` / `wuss_menu_is_open()` + manage a chain by handle. An over-tall menu gets a real vertical scrollbar + instead of being cropped. - `wuss_menu_create_from_desc()` / `wuss_menu_destroy()` — build a heap `wuss_menu_t` tree from a compact descriptor string (PrivateEye's `menu_create_from_desc` syntax: `,` between items, leading `|` for a dashed @@ -34,10 +34,12 @@ _Unreleased_ until one is cut. - `wuss_window_set_hidden()` and the `wuss_WINDOW_HIDDEN` create flag — a hidden window keeps its z-order slot but is not drawn, not hit-tested and occludes nothing. `wuss_window_move()` still works on it (translate only, no blit) so - it can be parked and re-shown in position. + it can be parked and re-shown in position. Revealing one fires a veto-able + `wuss_EVENT_PRE_SHOW` then `wuss_EVENT_SHOW`; `wuss_window_set_hidden()` + returns `result_t` accordingly. - `wuss_set_palette()` — swap the system palette mid-session. Copies the new palette in, refreshes the cached nearest-black/white indices, broadcasts a - new `wuss_EVENT_PALETTE` to every window's task so it can recache + new `wuss_EVENT_PALETTE` to every registered task so it can recache `wuss_nearest_colour()` selections, then invalidates the whole screen. Length must match `wuss_create()`'s; a now-out-of-range furniture/bevel/ backdrop index is rejected with the palette left unchanged. @@ -147,6 +149,36 @@ _Unreleased_ until one is cut. points, so one arrow can be worked both ways without moving the pointer. Toggle-size stays Select-only. - A window can no longer be resized larger than the screen. +- **Breaking:** event dispatch is reworked around a registered, opaque + `wuss_task_t` that owns its windows and is the sole delivery target. + - `wuss_task_start()` / `wuss_task_stop()` and the by-value task-delegate + struct are gone. Register a task with + `wuss_task_create(wuss, const wuss_task_desc_t *, wuss_task_t **)` — + `wuss_task_desc_t` is `{ wuss_window_fn_t *handle; void *task_data; + const char *name; }` — and tear it down with + `wuss_task_destroy(wuss_task_t *)`, which closes the task's windows, + fires one `wuss_EVENT_QUIT` and unregisters it. + - `wuss_window_create()` and `wuss_window_create_placed()` no longer take a + leading `wuss_t *` or a task-delegate pointer; their first argument is now + the owning `wuss_task_t *`. The window inherits that task's handler. + - `wuss_event_kind_t` is a single master enum + (`REDRAW, MOUSE, ICON, SCROLL, OPEN, PRE_SHOW, SHOW, PRE_CLOSE, CLOSE, + IDLE, QUIT, PALETTE, MENU_SELECT`). Handlers must handle unknown kinds + (fall through / `default:`). + - New veto-able pre-events. `wuss_window_set_hidden()` now returns + `result_t`: revealing a hidden window fires `wuss_EVENT_PRE_SHOW` first + and a non-OK return keeps it hidden and propagates. New + `wuss_window_try_close()` fires `wuss_EVENT_PRE_CLOSE` (non-OK vetoes), + then `wuss_EVENT_CLOSE`, then closes; the close icon routes through it. + `wuss_window_close()` stays the forced, unvetoable teardown and fires no + pre-events. + - `wuss_set_palette()` and `wuss_idle()` now broadcast once per *registered + task* (in registration order, `window == NULL`), not once per open + window's task. + - `wuss_menu_open()` takes a `wuss_task_t *` as its first argument. A picked + 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. ### Fixed From 017a93637d7928390fa5244930405e342e1ee07f Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Wed, 2 Sep 2026 13:06:39 +0100 Subject: [PATCH 179/287] fix(wuss): reap fire-and-forget tasks when their last window closes A task freed its task_data from wuss_EVENT_CLOSE but stayed registered, so the next wuss_idle / wuss_set_palette broadcast reached its handler with a dangling pointer (heap-use-after-free in the SDL demo: close a task window, wait one idle tick). Add wuss_task_set_autoclose(): an opted-in task self-destructs the moment its window list empties -- one wuss_EVENT_QUIT, then unlink and free the node, mirroring wuss_task_destroy. wuss_task_destroy marks itself REAPING so its own close loop does not re-trigger the path. The 14 SDL demo tasks opt in and move their task_data / asset frees from wuss_EVENT_CLOSE to wuss_EVENT_QUIT. Tasks that build a window then fail set autoclose only once fully constructed, so a create-error close does not fire QUIT into a spawner that will also free task_data. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- CHANGELOG.md | 5 +++ CMakeLists.txt | 1 + include/wuss/task.h | 20 ++++++++++ libraries/wuss/impl.h | 8 ++++ libraries/wuss/task/create.c | 1 + libraries/wuss/task/destroy.c | 4 ++ libraries/wuss/task/set-autoclose.c | 18 +++++++++ libraries/wuss/test/tasks/ball.c | 3 +- libraries/wuss/test/tasks/blank.c | 3 +- libraries/wuss/test/tasks/chars.c | 3 +- libraries/wuss/test/tasks/checker.c | 13 +++++-- libraries/wuss/test/tasks/curve.c | 3 +- libraries/wuss/test/tasks/gradient.c | 3 +- libraries/wuss/test/tasks/icons.c | 6 ++- libraries/wuss/test/tasks/image.c | 3 +- libraries/wuss/test/tasks/lissajous.c | 3 +- libraries/wuss/test/tasks/palette.c | 3 +- libraries/wuss/test/tasks/porter-duff.c | 3 +- libraries/wuss/test/tasks/sofa.c | 3 +- libraries/wuss/test/tasks/swatches.c | 6 ++- libraries/wuss/test/tasks/text.c | 3 +- libraries/wuss/test/wuss-test.c | 50 +++++++++++++++++++++++++ libraries/wuss/window/close.c | 21 ++++++++++- 23 files changed, 168 insertions(+), 18 deletions(-) create mode 100644 libraries/wuss/task/set-autoclose.c diff --git a/CHANGELOG.md b/CHANGELOG.md index 507c0f27..8914de87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -158,6 +158,11 @@ _Unreleased_ until one is cut. const char *name; }` — and tear it down with `wuss_task_destroy(wuss_task_t *)`, which closes the task's windows, fires one `wuss_EVENT_QUIT` and unregisters it. + - `wuss_task_set_autoclose(wuss_task_t *, int)` opts a task into + self-destruct: once its last window closes it fires one `wuss_EVENT_QUIT` + and unregisters, so it stops receiving `wuss_idle()` / `wuss_set_palette()` + broadcasts. Such tasks should free `task_data` from `wuss_EVENT_QUIT`, not + `wuss_EVENT_CLOSE`. - `wuss_window_create()` and `wuss_window_create_placed()` no longer take a leading `wuss_t *` or a task-delegate pointer; their first argument is now the owning `wuss_task_t *`. The window inherits that task's handler. diff --git a/CMakeLists.txt b/CMakeLists.txt index b2152729..65295d37 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -350,6 +350,7 @@ set(WUSS_CORE_SOURCES libraries/wuss/set-palette.c libraries/wuss/task/create.c libraries/wuss/task/destroy.c + libraries/wuss/task/set-autoclose.c libraries/wuss/task/deliver.c libraries/wuss/window/at.c libraries/wuss/window/create.c diff --git a/include/wuss/task.h b/include/wuss/task.h index eaa971ab..a0ab9bf8 100644 --- a/include/wuss/task.h +++ b/include/wuss/task.h @@ -265,6 +265,26 @@ result_t wuss_task_create(wuss_t *wuss, */ void wuss_task_destroy(wuss_task_t *doomed); +/** + * Opt a task into self-destruct once its last window closes. + * + * With autoclose set, the moment a task's window list becomes empty (whether + * via wuss_window_close or the wuss_window_try_close close path) the task + * behaves as if wuss_task_destroy had been called: one wuss_EVENT_QUIT is + * delivered, then the task is unlinked and freed. Free task_data from the + * wuss_EVENT_QUIT case, not wuss_EVENT_CLOSE -- a closing window would + * otherwise leave the task registered and still receiving wuss_idle / + * wuss_set_palette broadcasts with a stale task_data. + * + * Suited to fire-and-forget tasks that own exactly the windows they spawn. + * Leave it off for tasks that outlive their windows (e.g. one owning a pool + * of transient menu windows). + * + * \param[in] task Task to configure. NULL is a no-op. + * \param[in] on Non-zero to enable autoclose, zero to disable. + */ +void wuss_task_set_autoclose(wuss_task_t *task, int on); + #ifdef __cplusplus } #endif diff --git a/libraries/wuss/impl.h b/libraries/wuss/impl.h index f4d8e988..7cfc1aa3 100644 --- a/libraries/wuss/impl.h +++ b/libraries/wuss/impl.h @@ -60,6 +60,13 @@ typedef enum wuss_window_state } wuss_window_state_t; +/* Internal wuss_task::flags bits. */ +enum +{ + wuss_TASK__AUTOCLOSE = 1u << 0, /* self-destruct once the last window closes */ + wuss_TASK__REAPING = 1u << 1 /* teardown in progress; suppress autoclose */ +}; + /* A registered task. Owns a list of windows (linked through each window's * second embedded list_t, task_link) and is the single delivery target for * their events plus the app-wide IDLE/PALETTE/MENU_SELECT notifications. @@ -72,6 +79,7 @@ struct wuss_task void *task_data; const char *name; /* borrowed, may be NULL */ list_t windows; /* anchor; nodes are window->task_link */ + unsigned int flags; /* wuss_TASK__* */ }; struct wuss diff --git a/libraries/wuss/task/create.c b/libraries/wuss/task/create.c index 935fbad6..a88cbf37 100644 --- a/libraries/wuss/task/create.c +++ b/libraries/wuss/task/create.c @@ -29,6 +29,7 @@ result_t wuss_task_create(wuss_t *wuss, t->handle = desc->handle; t->task_data = desc->task_data; t->name = desc->name; + t->flags = 0; list_init(&t->windows); list_add_to_tail(&wuss->tasks, &t->link); diff --git a/libraries/wuss/task/destroy.c b/libraries/wuss/task/destroy.c index d34dec79..8d53eb84 100644 --- a/libraries/wuss/task/destroy.c +++ b/libraries/wuss/task/destroy.c @@ -21,6 +21,10 @@ void wuss_task_destroy(wuss_task_t *doomed) wuss = doomed->wuss; + /* Mark the teardown so wuss_window_close's autoclose path doesn't also + * fire QUIT and free the node from under us. */ + doomed->flags |= wuss_TASK__REAPING; + /* One QUIT while every window is still alive. */ event.kind = wuss_EVENT_QUIT; (void) wuss__deliver(doomed, NULL, &event); diff --git a/libraries/wuss/task/set-autoclose.c b/libraries/wuss/task/set-autoclose.c new file mode 100644 index 00000000..d546ac29 --- /dev/null +++ b/libraries/wuss/task/set-autoclose.c @@ -0,0 +1,18 @@ +/* wuss/task/set-autoclose.c -- opt a task into self-destruct on last close */ + +#include <stddef.h> + +#include "wuss/task.h" + +#include "../impl.h" + +void wuss_task_set_autoclose(wuss_task_t *task, int on) +{ + if (task == NULL) + return; + + if (on) + task->flags |= wuss_TASK__AUTOCLOSE; + else + task->flags &= ~wuss_TASK__AUTOCLOSE; +} diff --git a/libraries/wuss/test/tasks/ball.c b/libraries/wuss/test/tasks/ball.c index 0f700ecc..11aab9bc 100644 --- a/libraries/wuss/test/tasks/ball.c +++ b/libraries/wuss/test/tasks/ball.c @@ -56,6 +56,7 @@ result_t ball_create(wuss_t *wuss, ball_task_t *task) rc = wuss_task_create(wuss, &delegate_desc, &delegate); if (rc != result_OK) return rc; + wuss_task_set_autoclose(delegate, 1); return wuss_window_create_placed(delegate, SIZE2D(200, 160), @@ -219,7 +220,7 @@ result_t ball_handle(wuss_window_t *window, case wuss_EVENT_IDLE: return ball_idle(task_data); - case wuss_EVENT_CLOSE: + case wuss_EVENT_QUIT: free(bc); /* task_data was calloc'd per instance by the spawner */ return result_OK; diff --git a/libraries/wuss/test/tasks/blank.c b/libraries/wuss/test/tasks/blank.c index 2e1fc3cd..ce461b77 100644 --- a/libraries/wuss/test/tasks/blank.c +++ b/libraries/wuss/test/tasks/blank.c @@ -34,6 +34,7 @@ result_t blank_create(wuss_t *wuss, blank_task_t *task) rc = wuss_task_create(wuss, &delegate_desc, &delegate); if (rc != result_OK) return rc; + wuss_task_set_autoclose(delegate, 1); return wuss_window_create_placed(delegate, SIZE2D(200, 160), @@ -70,7 +71,7 @@ result_t blank_handle(wuss_window_t *window, const wuss_event_t *event, void *task_data) { - if (event->kind == wuss_EVENT_CLOSE) + if (event->kind == wuss_EVENT_QUIT) { free(task_data); /* calloc'd per instance by the spawner */ return result_OK; diff --git a/libraries/wuss/test/tasks/chars.c b/libraries/wuss/test/tasks/chars.c index 1bee8476..11cc47db 100644 --- a/libraries/wuss/test/tasks/chars.c +++ b/libraries/wuss/test/tasks/chars.c @@ -52,6 +52,7 @@ result_t chars_create(wuss_t *wuss, chars_task_t *task) rc = wuss_task_create(wuss, &delegate_desc, &delegate); if (rc != result_OK) return rc; + wuss_task_set_autoclose(delegate, 1); sz = SIZE2D(cell_w * CHARS_COLS, cell_h * CHARS_ROWS); return wuss_window_create_placed(delegate, @@ -124,7 +125,7 @@ result_t chars_handle(wuss_window_t *window, const wuss_event_t *event, void *task_data) { - if (event->kind == wuss_EVENT_CLOSE) + if (event->kind == wuss_EVENT_QUIT) { free(task_data); /* calloc'd per instance by the spawner */ return result_OK; diff --git a/libraries/wuss/test/tasks/checker.c b/libraries/wuss/test/tasks/checker.c index 6ed98497..8e3f4662 100644 --- a/libraries/wuss/test/tasks/checker.c +++ b/libraries/wuss/test/tasks/checker.c @@ -64,6 +64,10 @@ result_t checker_create(wuss_t*wuss, checker_task_t *task) return rc; } + /* both windows up: from here, closing the last one reaps the task and its + * wuss_EVENT_QUIT frees task_data */ + wuss_task_set_autoclose(delegate, 1); + return result_OK; } @@ -169,9 +173,12 @@ result_t checker_handle(wuss_window_t *window, cc->window2 = NULL; else cc->window = NULL; - /* one calloc'd block backs both windows; free it once both are gone */ - if (cc->window == NULL && cc->window2 == NULL) - free(cc); + return result_OK; + + case wuss_EVENT_QUIT: + /* one calloc'd block backs both windows; the task autocloses once the + * second window goes, so free it here */ + free(cc); return result_OK; default: diff --git a/libraries/wuss/test/tasks/curve.c b/libraries/wuss/test/tasks/curve.c index fe586d62..8574cb0f 100644 --- a/libraries/wuss/test/tasks/curve.c +++ b/libraries/wuss/test/tasks/curve.c @@ -47,6 +47,7 @@ result_t curve_create(wuss_t *wuss, curve_task_t *task) rc = wuss_task_create(wuss, &delegate_desc, &delegate); if (rc != result_OK) return rc; + wuss_task_set_autoclose(delegate, 1); return wuss_window_create_placed(delegate, SIZE2D(220, 160), @@ -181,7 +182,7 @@ result_t curve_handle(wuss_window_t *window, case wuss_EVENT_SCROLL: return curve_scroll(task, event->data.scroll.delta, window); - case wuss_EVENT_CLOSE: + 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/gradient.c b/libraries/wuss/test/tasks/gradient.c index 8df6f1a6..8f33bd99 100644 --- a/libraries/wuss/test/tasks/gradient.c +++ b/libraries/wuss/test/tasks/gradient.c @@ -46,6 +46,7 @@ result_t gradient_create(wuss_t *wuss, gradient_task_t *task) rc = wuss_task_create(wuss, &delegate_desc, &delegate); if (rc != result_OK) return rc; + wuss_task_set_autoclose(delegate, 1); return wuss_window_create_placed(delegate, SIZE2D(GRADIENT_OPEN_WIDTH, GRADIENT_OPEN_HEIGHT), @@ -101,7 +102,7 @@ result_t gradient_handle(wuss_window_t *window, case wuss_EVENT_REDRAW: return gradient_redraw(event, task_data); - case wuss_EVENT_CLOSE: + 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/icons.c b/libraries/wuss/test/tasks/icons.c index 226643d5..025793c7 100644 --- a/libraries/wuss/test/tasks/icons.c +++ b/libraries/wuss/test/tasks/icons.c @@ -246,6 +246,10 @@ result_t icons_create(wuss_t *wuss, task->hotspot = made[g + 9]; wuss_icon_set_selected(made[m + 1], 1); /* "Show grid" starts ticked */ + /* fully built: from here a last-window close reaps the task and its + * wuss_EVENT_QUIT frees task_data */ + wuss_task_set_autoclose(delegate, 1); + return result_OK; failure: @@ -391,7 +395,7 @@ result_t icons_handle(wuss_window_t *window, case wuss_EVENT_ICON: return icons_icon(event, task_data); - case wuss_EVENT_CLOSE: + case wuss_EVENT_QUIT: if (tcx->has_sprite) free(tcx->sprite.base); free(tcx); /* calloc'd per instance by the spawner */ diff --git a/libraries/wuss/test/tasks/image.c b/libraries/wuss/test/tasks/image.c index a29edede..c822806b 100644 --- a/libraries/wuss/test/tasks/image.c +++ b/libraries/wuss/test/tasks/image.c @@ -44,6 +44,7 @@ result_t image_create(wuss_t *wuss, rc = wuss_task_create(wuss, &delegate_desc, &delegate); if (rc != result_OK) return rc; + wuss_task_set_autoclose(delegate, 1); sz.w = task->bitmap.size.w + BORDER * 2; sz.h = task->bitmap.size.h + BORDER * 2; @@ -103,7 +104,7 @@ result_t image_handle(wuss_window_t *window, case wuss_EVENT_REDRAW: return image_redraw(event, task_data); - case wuss_EVENT_CLOSE: + case wuss_EVENT_QUIT: 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/lissajous.c b/libraries/wuss/test/tasks/lissajous.c index 36b6fabe..b1c1a8df 100644 --- a/libraries/wuss/test/tasks/lissajous.c +++ b/libraries/wuss/test/tasks/lissajous.c @@ -42,6 +42,7 @@ result_t lissajous_create(wuss_t *wuss, lissajous_task_t *task) rc = wuss_task_create(wuss, &delegate_desc, &delegate); if (rc != result_OK) return rc; + wuss_task_set_autoclose(delegate, 1); return wuss_window_create_placed(delegate, SIZE2D(220, 220), @@ -160,7 +161,7 @@ result_t lissajous_handle(wuss_window_t *window, case wuss_EVENT_IDLE: return lissajous_idle(task_data); - case wuss_EVENT_CLOSE: + case wuss_EVENT_QUIT: free(lc); /* task_data was calloc'd per instance by the spawner */ return result_OK; diff --git a/libraries/wuss/test/tasks/palette.c b/libraries/wuss/test/tasks/palette.c index ddc30625..0c5ccd40 100644 --- a/libraries/wuss/test/tasks/palette.c +++ b/libraries/wuss/test/tasks/palette.c @@ -33,6 +33,7 @@ result_t palette_create(wuss_t *wuss, rc = wuss_task_create(wuss, &delegate_desc, &delegate); if (rc != result_OK) return rc; + wuss_task_set_autoclose(delegate, 1); return wuss_window_create_placed(delegate, SIZE2D(100, 100), @@ -90,7 +91,7 @@ result_t palette_handle(wuss_window_t *window, const wuss_event_t *event, void *task_data) { - if (event->kind == wuss_EVENT_CLOSE) + if (event->kind == wuss_EVENT_QUIT) { free(task_data); /* calloc'd per instance by the spawner */ return result_OK; diff --git a/libraries/wuss/test/tasks/porter-duff.c b/libraries/wuss/test/tasks/porter-duff.c index b2974393..8fb79964 100644 --- a/libraries/wuss/test/tasks/porter-duff.c +++ b/libraries/wuss/test/tasks/porter-duff.c @@ -199,6 +199,7 @@ result_t porter_duff_create(wuss_t *wuss, rc = wuss_task_create(wuss, &delegate_desc, &delegate); if (rc != result_OK) return rc; + wuss_task_set_autoclose(delegate, 1); rc = wuss_window_create_placed(delegate, SIZE2D(PD_SIZE, PD_SIZE + PD_LABEL_HEIGHT), @@ -401,7 +402,7 @@ result_t porter_duff_handle(wuss_window_t *window, case wuss_EVENT_IDLE: return porter_duff_idle(task_data); - case wuss_EVENT_CLOSE: + case wuss_EVENT_QUIT: free(pd->dst.base); free(pd->src.base); free(pd->b.base); diff --git a/libraries/wuss/test/tasks/sofa.c b/libraries/wuss/test/tasks/sofa.c index 27e0b493..2df80f53 100644 --- a/libraries/wuss/test/tasks/sofa.c +++ b/libraries/wuss/test/tasks/sofa.c @@ -376,6 +376,7 @@ result_t sofa_create(wuss_t*wuss, sofa_task_t*task) rc = wuss_task_create(wuss, &delegate_desc, &delegate); if (rc != result_OK) return rc; + wuss_task_set_autoclose(delegate, 1); return wuss_window_create_placed(delegate, SIZE2D(180, 160), @@ -588,7 +589,7 @@ result_t sofa_handle(wuss_window_t *window, case wuss_EVENT_IDLE: return sofa_idle(task_data); - case wuss_EVENT_CLOSE: + case wuss_EVENT_QUIT: free(sc); /* task_data was calloc'd per instance by the spawner */ return result_OK; diff --git a/libraries/wuss/test/tasks/swatches.c b/libraries/wuss/test/tasks/swatches.c index 43db9502..f9df9ad1 100644 --- a/libraries/wuss/test/tasks/swatches.c +++ b/libraries/wuss/test/tasks/swatches.c @@ -107,6 +107,10 @@ result_t swatches_create(wuss_t *wuss, swatches_task_t *task) return rc; } + /* fully built: from here a last-window close reaps the task and its + * wuss_EVENT_QUIT frees task_data */ + wuss_task_set_autoclose(delegate, 1); + return result_OK; } @@ -123,7 +127,7 @@ result_t swatches_handle(wuss_window_t *window, case wuss_EVENT_PALETTE: return swatches_build(task); - case wuss_EVENT_CLOSE: + case wuss_EVENT_QUIT: free(task_data); /* calloc'd per instance by the spawner */ return result_OK; diff --git a/libraries/wuss/test/tasks/text.c b/libraries/wuss/test/tasks/text.c index 3aee0b6a..874fadf7 100644 --- a/libraries/wuss/test/tasks/text.c +++ b/libraries/wuss/test/tasks/text.c @@ -49,6 +49,7 @@ result_t text_create(wuss_t *wuss, rc = wuss_task_create(wuss, &delegate_desc, &delegate); if (rc != result_OK) return rc; + wuss_task_set_autoclose(delegate, 1); sz = SIZE2D(220, 180); task->base_width = sz.w; @@ -159,7 +160,7 @@ result_t text_handle(wuss_window_t *window, return result_OK; return text_mouse(task_data); - case wuss_EVENT_CLOSE: + case wuss_EVENT_QUIT: free(tcx); /* task_data was 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 deb8d54b..11c3fb37 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -3354,6 +3354,56 @@ result_t wuss_test(const char *resources) mk_task_count = 0; /* delegate_q is gone; drop the stale registry entry */ } + printf("test: an autoclose task self-destructs when its last window closes and stops receiving broadcasts\n"); + { + static test_task_t tc_ac; + wuss_task_t *delegate_ac; + box_t box_ac; + wuss_window_t *win_ac1, *win_ac2; + + memset(&tc_ac, 0, sizeof(tc_ac)); + delegate_ac = mk_task(wuss, test_handle, &tc_ac); + if (delegate_ac == NULL) goto Failure; + wuss_task_set_autoclose(delegate_ac, 1); + + 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, + 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, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + box_size(&box_ac), SIZE2D(0, 0), &win_ac2); + if (rc != result_OK) + goto Failure; + + /* first close: task still owns a window, so no reap and no QUIT */ + wuss_window_close(win_ac1); + if (tc_ac.stop_count != 0) + goto Failure; + + rc = wuss_idle(wuss); + if (rc != result_OK) + goto Failure; + if (tc_ac.idle_count != 1) /* still registered, still broadcast to */ + goto Failure; + + /* last close: reap fires one QUIT and unregisters the task */ + wuss_window_close(win_ac2); + if (tc_ac.stop_count != 1) + goto Failure; + + rc = wuss_idle(wuss); + if (rc != result_OK) + goto Failure; + if (tc_ac.idle_count != 1) /* gone: no further broadcast */ + goto Failure; + + mk_task_count = 0; /* delegate_ac reaped itself; drop the stale entry */ + } + #ifdef WUSS_MENUS printf("test: wuss_menu_create_from_desc parses a descriptor tree\n"); { diff --git a/libraries/wuss/window/close.c b/libraries/wuss/window/close.c index 15e10e4b..5563468e 100644 --- a/libraries/wuss/window/close.c +++ b/libraries/wuss/window/close.c @@ -10,12 +10,15 @@ void wuss_window_close(wuss_window_t *doomed) { - wuss_t *wuss; + wuss_task_t *task; + wuss_event_t event; + wuss_t *wuss; if (doomed == NULL) return; wuss = doomed->wuss; + task = doomed->task; #ifdef WUSS_FURNITURE if (wuss->furniture.dragging == doomed) wuss->furniture.dragging = NULL; @@ -32,11 +35,25 @@ void wuss_window_close(wuss_window_t *doomed) wuss__invalidate_clipped(doomed, &doomed->visible); list_remove(&wuss->z_order, &doomed->link); - list_remove(&doomed->task->windows, &doomed->task_link); + list_remove(&task->windows, &doomed->task_link); #ifdef WUSS_ICONS wuss__icons_free(doomed); #endif wuss__free(wuss, doomed); + + /* An autoclose task self-destructs once it loses its last window: QUIT + * (so the handler can free task_data), then unlink and free the node. + * Suppressed while wuss_task_destroy is already tearing the task down. */ + if ((task->flags & wuss_TASK__AUTOCLOSE) && + !(task->flags & wuss_TASK__REAPING) && + task->windows.next == NULL) + { + event.kind = wuss_EVENT_QUIT; + (void) wuss__deliver(task, NULL, &event); + + list_remove(&wuss->tasks, &task->link); + wuss__free(wuss, task); + } } From d92e438b7c68187c7db4ef47f2668fc9eaa39bca Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Wed, 2 Sep 2026 16:32:13 +0100 Subject: [PATCH 180/287] feat(wuss): add a native RISC OS frontend for the interactive demo The demo's platform layer is split out of main.c into a small frontend interface (frontend.h): open a surface, report normalised input events, present the framebuffer. The SDL loop moves verbatim into frontend-sdl.c; a new frontend-riscos.c drives the demo full-screen on RISC OS via OS_ScreenMode / OS_ReadVduVariables / OS_Mouse, rendering straight into screen memory (pixelfmt_p4 maps 1:1 onto RISC OS 4bpp) and programming the 16-entry hardware palette. The OS pointer is enabled for the session. main.c is now backend-agnostic; the F1-garbage stress mode (folded into redraw-all) and the compile-time 32bpp switch are dropped. The task modules' USE_SDL guard becomes WUSS_APP so they build under either frontend. CMake selects the frontend source by TARGET_RISCOS and only links SDL off RISC OS. Resource loads are logged (path plus rc, in bmfont_create and at each call site) and the RISC OS resources root now defaults to "@" so path_join_filename does not produce a leading "..". Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- CMakeLists.txt | 27 +- apps/wuss/frontend-riscos.c | 308 ++++++++++++++++++ apps/wuss/frontend-sdl.c | 309 ++++++++++++++++++ apps/wuss/frontend.h | 90 ++++++ apps/wuss/main.c | 405 +++++++++--------------- libraries/framebuf/bmfont/bmfont.c | 3 + libraries/wuss/test/tasks/ball.c | 4 +- libraries/wuss/test/tasks/ball.h | 4 +- libraries/wuss/test/tasks/blank.c | 4 +- libraries/wuss/test/tasks/blank.h | 4 +- libraries/wuss/test/tasks/chars.c | 4 +- libraries/wuss/test/tasks/chars.h | 4 +- libraries/wuss/test/tasks/checker.c | 4 +- libraries/wuss/test/tasks/checker.h | 4 +- libraries/wuss/test/tasks/curve.c | 4 +- libraries/wuss/test/tasks/curve.h | 4 +- libraries/wuss/test/tasks/gradient.c | 4 +- libraries/wuss/test/tasks/gradient.h | 4 +- libraries/wuss/test/tasks/icons.c | 4 +- libraries/wuss/test/tasks/icons.h | 4 +- libraries/wuss/test/tasks/image.c | 4 +- libraries/wuss/test/tasks/image.h | 4 +- libraries/wuss/test/tasks/lissajous.c | 4 +- libraries/wuss/test/tasks/lissajous.h | 4 +- libraries/wuss/test/tasks/palette.c | 4 +- libraries/wuss/test/tasks/palette.h | 4 +- libraries/wuss/test/tasks/porter-duff.c | 4 +- libraries/wuss/test/tasks/porter-duff.h | 4 +- libraries/wuss/test/tasks/sofa.c | 4 +- libraries/wuss/test/tasks/sofa.h | 4 +- libraries/wuss/test/tasks/swatches.c | 4 +- libraries/wuss/test/tasks/swatches.h | 4 +- libraries/wuss/test/tasks/text.c | 4 +- libraries/wuss/test/tasks/text.h | 4 +- 34 files changed, 929 insertions(+), 325 deletions(-) create mode 100644 apps/wuss/frontend-riscos.c create mode 100644 apps/wuss/frontend-sdl.c create mode 100644 apps/wuss/frontend.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 65295d37..6e5037d9 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -664,15 +664,13 @@ endif() # if(BUILD_APPS) - # The Wuss interactive demo: its own executable, built from the SDL task - # modules plus apps/wuss/main.c (which was the old in-test SDL driver). + # The Wuss interactive demo: main.c drives wuss and the task launcher; a + # per-platform backend (frontend-*.c) opens the surface and reports input. + # RISC OS uses native VDU/OS_Mouse calls; everything else uses SDL. if(NOT (WUSS_FURNITURE AND WUSS_ICONS)) message(FATAL_ERROR "BUILD_APPS requires WUSS_FURNITURE and WUSS_ICONS") endif() - find_package(SDL3 REQUIRED) - find_package(SDL3_image REQUIRED) - set(WUSS_APP_SOURCES apps/wuss/main.c libraries/wuss/test/tasks/ball.c @@ -690,6 +688,12 @@ if(BUILD_APPS) libraries/wuss/test/tasks/swatches.c libraries/wuss/test/tasks/text.c) + if(TARGET_RISCOS) + list(APPEND WUSS_APP_SOURCES apps/wuss/frontend-riscos.c) + else() + list(APPEND WUSS_APP_SOURCES apps/wuss/frontend-sdl.c) + endif() + source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" FILES ${WUSS_APP_SOURCES}) add_executable(wuss ${WUSS_APP_SOURCES}) @@ -702,9 +706,16 @@ if(BUILD_APPS) OUTPUT_NAME_RELWITHDEBINFO wuss-relwithdebinfo OUTPUT_NAME_MINSIZEREL wuss-minsizerel) - target_include_directories(wuss PRIVATE libraries/wuss/test) - target_compile_definitions(wuss PRIVATE USE_SDL) - target_link_libraries(wuss DPTLib SDL3::SDL3 SDL3_image::SDL3_image) + target_include_directories(wuss PRIVATE apps/wuss libraries/wuss/test) + target_compile_definitions(wuss PRIVATE WUSS_APP) + target_link_libraries(wuss DPTLib) + + if(NOT TARGET_RISCOS) + find_package(SDL3 REQUIRED) + target_compile_definitions(wuss PRIVATE USE_SDL) + target_link_libraries(wuss SDL3::SDL3) + endif() + if(NOT MSVC) target_link_libraries(wuss m) endif() diff --git a/apps/wuss/frontend-riscos.c b/apps/wuss/frontend-riscos.c new file mode 100644 index 00000000..7dba0644 --- /dev/null +++ b/apps/wuss/frontend-riscos.c @@ -0,0 +1,308 @@ +/* wuss/frontend-riscos.c -- native RISC OS backend for the Wuss demo */ + +#ifdef WUSS_APP +#ifdef __riscos + +#include <stdio.h> +#include <stdlib.h> + +#ifdef FORTIFY +#include "fortify/fortify.h" +#endif + +#include "swis.h" + +#include "base/result.h" +#include "base/utils.h" +#include "framebuf/bitmap.h" +#include "framebuf/colour.h" +#include "framebuf/pixelfmt.h" +#include "geom/point.h" +#include "wuss/wuss.h" + +#include "frontend.h" + +/* This backend takes over the whole screen: it selects a 16-colour linear + * mode, points wuss's framebuffer straight at screen memory (so a redraw + * lands on the display with no blit) and polls OS_Mouse / the keyboard each + * frame. It restores the entry mode on close. There is no Wimp task here -- + * wuss's own window furniture is the entire UI. */ + +/* OS_ReadVduVariables indices we ask for, in this order. */ +enum +{ + VDUVAR_XEIG = 4, /* OS-units-per-pixel shift, x */ + VDUVAR_YEIG = 5, /* OS-units-per-pixel shift, y */ + VDUVAR_LINE_LEN = 6, /* bytes per screen row */ + VDUVAR_YWIND_LIM = 11, /* max Y in OS units (screen height - 1, << YEIG) */ + VDUVAR_DISPLAY_START = 149 /* base address of screen memory */ +}; + +/* A 16-colour mode 640 x 480. Mode 27 is 640x480x16 on RISC OS; if a given + * machine lacks it, OS_ScreenMode substitutes the closest match and the code + * below reads back whatever it actually got. */ +#define WUSS_RISCOS_MODE 27 + +struct wuss_frontend +{ + int entry_mode; /* mode number to restore on close */ + int scr_width; + int scr_height; + void *screen_base; /* screen memory: wuss draws here directly */ + int rowbytes; + int xeig, yeig; /* OS_Mouse returns OS units; >> eig gives pixels */ + int last_buttons; /* OS_Mouse button state at the previous poll */ +}; + +/* ----------------------------------------------------------------------- */ + +/* Program the hardware palette from wuss's colour_t[]. colour_t.primary is + * pixelfmt_rgba8888: R in the low byte, then G, then B -- the same byte order + * OS_Word 12's palette block wants. */ +static void set_hw_palette(const colour_t *palette, int npalette) +{ + int i; + int n = (npalette < 16) ? npalette : 16; + + for (i = 0; i < n; i++) + { + unsigned int rgba = palette[i].primary; + unsigned char block[5]; + + /* block = logical colour, 16 (set both flash states), R, G, B */ + block[0] = (unsigned char) i; + block[1] = 16; + block[2] = (unsigned char) (rgba & 0xFF); + block[3] = (unsigned char) ((rgba >> 8) & 0xFF); + block[4] = (unsigned char) ((rgba >> 16) & 0xFF); + + _swix(OS_Word, _INR(0,1), 12, block); + } +} + +/* ----------------------------------------------------------------------- */ + +result_t wuss_frontend_open(int width, + int height, + const colour_t *palette, + int npalette, + void **pixels, + int *rowbytes, + pixelfmt_t *fmt, + wuss_frontend_t **frontend) +{ + wuss_frontend_t *fe; + int vars[6]; /* 5 indices + a -1 terminator */ + int vals[5]; + _kernel_oserror *err; + + fe = calloc(1, sizeof(*fe)); + if (fe == NULL) + return result_OOM; + + /* remember the mode we came in on */ + err = _swix(OS_ScreenMode, _IN(0) | _OUT(1), 1, &fe->entry_mode); + if (err != NULL) + goto failure; + + /* switch to the 16-colour demo mode */ + err = _swix(OS_ScreenMode, _INR(0,1), 0, WUSS_RISCOS_MODE); + if (err != NULL) + goto failure; + + /* read back the geometry we actually got */ + vars[0] = VDUVAR_XEIG; + vars[1] = VDUVAR_YEIG; + vars[2] = VDUVAR_LINE_LEN; + vars[3] = VDUVAR_YWIND_LIM; + vars[4] = VDUVAR_DISPLAY_START; + vars[5] = -1; /* OS_ReadVduVariables scans until it hits -1 */ + err = _swix(OS_ReadVduVariables, _INR(0,1), vars, vals); + if (err != NULL) + goto failure_restore; + + fe->xeig = vals[0]; + fe->yeig = vals[1]; + fe->rowbytes = vals[2]; + /* YWIND_LIMIT is max-Y in OS units; >> YEIG turns it into pixel rows */ + fe->scr_height = (vals[3] >> fe->yeig) + 1; + fe->screen_base = (void *) vals[4]; + fe->scr_width = fe->rowbytes * 2; /* 4bpp: 2 pixels per byte */ + fe->last_buttons = 0; + + /* the demo asks for a fixed size; if the mode came out smaller the tasks + * would draw off-screen, so bail rather than corrupt memory */ + if (fe->scr_width < width || fe->scr_height < height) + { + fprintf(stderr, "wuss: mode %d gave %dx%d, need %dx%d\n", + WUSS_RISCOS_MODE, fe->scr_width, fe->scr_height, width, height); + goto failure_restore; + } + + set_hw_palette(palette, npalette); + + /* OS_ScreenMode leaves the pointer off. Turn on pointer 1 with the default + * arrow shape (*Pointer 1 == OS_Byte 106, 1) and confine it to the screen. + * wuss draws no cursor of its own, so this is the pointer the user sees. */ + _swix(OS_Byte, _INR(0,1), 106, 1); + + *pixels = fe->screen_base; + *rowbytes = fe->rowbytes; + *fmt = pixelfmt_p4; /* RISC OS 4bpp: leftmost pixel in the low nibble */ + *frontend = fe; + return result_OK; + + +failure_restore: + + _swix(OS_ScreenMode, _INR(0,1), 0, fe->entry_mode); + +failure: + + free(fe); + return result_TEST_FAILED; +} + +/* Map an OS_Mouse button word (bit 2 = Select/left, bit 1 = Menu/middle, + * bit 0 = Adjust/right on a 3-button mouse) to wuss_button_t, whose bit + * values already match RISC OS order. */ +static wuss_button_t mouse_buttons_to_wuss(int buttons) +{ + wuss_button_t b = wuss_BUTTON_NONE; + + if (buttons & 4) b |= wuss_BUTTON_SELECT; + if (buttons & 2) b |= wuss_BUTTON_MENU; + if (buttons & 1) b |= wuss_BUTTON_ADJUST; + return b; +} + +/* Keys the demo reacts to: negative INKEY scan code -> input kind. Q and + * Escape both quit. */ +static const struct +{ + int scan; + wuss_input_kind_t kind; +} +g_keys[] = +{ + { -17, wuss_INPUT_QUIT }, /* Q */ + { -113, wuss_INPUT_QUIT }, /* Escape */ + { -114, wuss_INPUT_REDRAW_ALL }, /* F1 */ + { -116, wuss_INPUT_PIXEL_STRESS }, /* F3 */ + { -117, wuss_INPUT_PALETTE_CYCLE } /* F4 */ +}; + +static bool key_down(int scan) +{ + int r1, r2; + + /* OS_Byte 129 with R1=scan, R2=0xFF: R1 = 0xFF if that key is pressed. */ + _swix(OS_Byte, _INR(0,2) | _OUTR(1,2), 129, scan & 0xFF, 0xFF, &r1, &r2); + return (r1 & 0xFF) == 0xFF; +} + +bool wuss_frontend_poll(wuss_frontend_t *fe, wuss_input_t *event) +{ + static unsigned int key_was; /* bit i = g_keys[i] was down last poll */ + + unsigned int key_now = 0; + int mx, my, buttons, t; + int px, py; + size_t i; + + /* keyboard first: fire one event on the press edge so a held key doesn't + * repeat every frame */ + for (i = 0; i < NELEMS(g_keys); i++) + if (key_down(g_keys[i].scan)) + key_now |= 1u << i; + + for (i = 0; i < NELEMS(g_keys); i++) + { + unsigned int bit = 1u << i; + + if ((key_now & bit) && !(key_was & bit)) + { + key_was = key_now; + event->kind = g_keys[i].kind; + return true; + } + } + key_was = key_now; + + /* poll the mouse: OS_Mouse returns x,y in OS units and a button word */ + if (_swix(OS_Mouse, _OUTR(0,3), &mx, &my, &buttons, &t) != NULL) + return false; + + /* OS units -> pixels; y is bottom-up on RISC OS, wuss wants top-down */ + px = mx >> fe->xeig; + py = (fe->scr_height - 1) - (my >> fe->yeig); + + if (buttons != fe->last_buttons) + { + int pressed = buttons & ~fe->last_buttons; + int released = fe->last_buttons & ~buttons; + + fe->last_buttons = buttons; + + if (pressed) + { + event->kind = wuss_INPUT_MOUSE_DOWN; + event->pos = POINT(px, py); + event->button = mouse_buttons_to_wuss(pressed); + return true; + } + if (released) + { + event->kind = wuss_INPUT_MOUSE_UP; + event->pos = POINT(px, py); + event->button = mouse_buttons_to_wuss(released); + return true; + } + } + + /* no button change: report the move once, then say the queue is empty */ + { + static int last_px = -1, last_py = -1; + + if (px != last_px || py != last_py) + { + last_px = px; + last_py = py; + event->kind = wuss_INPUT_MOUSE_MOVE; + event->pos = POINT(px, py); + return true; + } + } + + return false; +} + +void wuss_frontend_present(wuss_frontend_t *fe, const bitmap_t *bm) +{ + NOT_USED(fe); + NOT_USED(bm); + + /* wuss drew straight into screen memory; just pace to the frame rate */ + _swix(OS_Byte, _IN(0), 19); +} + +void wuss_frontend_set_palette(wuss_frontend_t *fe, + const colour_t *palette, + int npalette) +{ + NOT_USED(fe); + set_hw_palette(palette, npalette); +} + +void wuss_frontend_close(wuss_frontend_t *fe) +{ + if (fe == NULL) + return; + + _swix(OS_Byte, _INR(0,1), 106, 0); /* *Pointer 0: turn the pointer off */ + _swix(OS_ScreenMode, _INR(0,1), 0, fe->entry_mode); + free(fe); +} + +#endif /* __riscos */ +#endif /* WUSS_APP */ diff --git a/apps/wuss/frontend-sdl.c b/apps/wuss/frontend-sdl.c new file mode 100644 index 00000000..297919a8 --- /dev/null +++ b/apps/wuss/frontend-sdl.c @@ -0,0 +1,309 @@ +/* wuss/frontend-sdl.c -- SDL backend for the Wuss interactive demo */ + +#ifdef WUSS_APP +#ifdef USE_SDL + +#include <stdio.h> +#include <stdlib.h> + +#ifdef FORTIFY +#include "fortify/fortify.h" +#endif + +#include "base/result.h" +#include "base/utils.h" +#include "framebuf/bitmap.h" +#include "framebuf/colour.h" +#include "framebuf/pixelfmt.h" +#include "geom/point.h" +#include "wuss/wuss.h" + +#include <SDL3/SDL.h> + +#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 + +/* ----------------------------------------------------------------------- */ + +struct wuss_frontend +{ + SDL_Window *window; + SDL_Renderer *renderer; + SDL_Texture *texture; + int scr_width; + int scr_height; + void *pixels; /* the private framebuffer handed to the caller */ +}; + +/* ----------------------------------------------------------------------- */ + +static wuss_button_t sdl_button_to_wuss(Uint8 button) +{ + switch (button) + { + case SDL_BUTTON_MIDDLE: return wuss_BUTTON_MENU; + case SDL_BUTTON_RIGHT: return wuss_BUTTON_ADJUST; + default: return wuss_BUTTON_SELECT; + } +} + +/* SDL delivers mouse coordinates in window space, which F2 can scale away + * from the fixed-size Wuss screen; map back down to screen space. */ +static void sdl_pos_to_scr(SDL_Window *window, + int scr_width, + int scr_height, + float in_x, + float in_y, + int *out_x, + int *out_y) +{ + int win_w, win_h; + + SDL_GetWindowSize(window, &win_w, &win_h); + + *out_x = (int) (in_x * scr_width / win_w); + *out_y = (int) (in_y * scr_height / win_h); +} + +/* ----------------------------------------------------------------------- */ + +result_t wuss_frontend_open(int width, + int height, + const colour_t *palette, + int npalette, + void **pixels, + int *rowbytes, + pixelfmt_t *fmt, + wuss_frontend_t **frontend) +{ + wuss_frontend_t *fe; + int stride; + + 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 + + fe = calloc(1, sizeof(*fe)); + if (fe == NULL) + return result_OOM; + + fe->scr_width = width; + fe->scr_height = height; + + fe->pixels = malloc((size_t) stride * height); + if (fe->pixels == NULL) + { + free(fe); + return result_OOM; + } + + if (!SDL_Init(SDL_INIT_VIDEO)) + { + fprintf(stderr, "Error: SDL_Init: %s\n", SDL_GetError()); + goto failure; + } + + fe->window = SDL_CreateWindow("Wuss", width, height, 0); + if (fe->window == NULL) + { + fprintf(stderr, "Error: SDL_CreateWindow: %s\n", SDL_GetError()); + goto failure; + } + + fe->renderer = SDL_CreateRenderer(fe->window, NULL); + if (fe->renderer == NULL) + { + fprintf(stderr, "Error: SDL_CreateRenderer: %s\n", SDL_GetError()); + goto failure; + } + + fe->texture = SDL_CreateTexture(fe->renderer, SDL_PIXELFORMAT_ARGB8888, + SDL_TEXTUREACCESS_STREAMING, width, height); + if (fe->texture == NULL) + { + fprintf(stderr, "Error: SDL_CreateTexture: %s\n", SDL_GetError()); + goto failure; + } + + SDL_SetTextureBlendMode(fe->texture, SDL_BLENDMODE_NONE); + /* 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 + + *pixels = fe->pixels; + *rowbytes = stride; + *frontend = fe; + return result_OK; + + +failure: + + if (fe->texture) SDL_DestroyTexture(fe->texture); + if (fe->renderer) SDL_DestroyRenderer(fe->renderer); + if (fe->window) SDL_DestroyWindow(fe->window); + SDL_Quit(); + free(fe->pixels); + free(fe); + return result_TEST_FAILED; +} + +bool wuss_frontend_poll(wuss_frontend_t *fe, wuss_input_t *event) +{ + SDL_Event ev; + + for (;;) + { + if (!SDL_PollEvent(&ev)) + return false; + + switch (ev.type) + { + case SDL_EVENT_QUIT: + event->kind = wuss_INPUT_QUIT; + return true; + + case SDL_EVENT_KEY_UP: + if (ev.key.key == SDLK_Q) + event->kind = wuss_INPUT_QUIT; + else if (ev.key.key == SDLK_F1) + event->kind = wuss_INPUT_REDRAW_ALL; + else if (ev.key.key == SDLK_F3) + event->kind = wuss_INPUT_PIXEL_STRESS; + else if (ev.key.key == SDLK_F4) + event->kind = wuss_INPUT_PALETTE_CYCLE; + else if (ev.key.key == SDLK_F2) + { + int w, h; + + /* F2 doubles the SDL window, Shift-F2 halves it: a backend-local + * zoom the demo loop never sees. */ + SDL_GetWindowSize(fe->window, &w, &h); + if (ev.key.mod & SDL_KMOD_SHIFT) + SDL_SetWindowSize(fe->window, w / 2, h / 2); + else + SDL_SetWindowSize(fe->window, w * 2, h * 2); + continue; + } + else + continue; + return true; + + case SDL_EVENT_MOUSE_BUTTON_DOWN: + { + int x, y; + + sdl_pos_to_scr(fe->window, fe->scr_width, fe->scr_height, + ev.button.x, ev.button.y, &x, &y); + event->kind = wuss_INPUT_MOUSE_DOWN; + event->pos = POINT(x, y); + event->button = sdl_button_to_wuss(ev.button.button); + } + return true; + + case SDL_EVENT_MOUSE_BUTTON_UP: + { + int x, y; + + sdl_pos_to_scr(fe->window, fe->scr_width, fe->scr_height, + ev.button.x, ev.button.y, &x, &y); + event->kind = wuss_INPUT_MOUSE_UP; + event->pos = POINT(x, y); + event->button = sdl_button_to_wuss(ev.button.button); + } + return true; + + case SDL_EVENT_MOUSE_MOTION: + { + int x, y; + + sdl_pos_to_scr(fe->window, fe->scr_width, fe->scr_height, + ev.motion.x, ev.motion.y, &x, &y); + event->kind = wuss_INPUT_MOUSE_MOVE; + event->pos = POINT(x, y); + } + return true; + + case SDL_EVENT_MOUSE_WHEEL: + { + int x, y; + + sdl_pos_to_scr(fe->window, fe->scr_width, fe->scr_height, + ev.wheel.mouse_x, ev.wheel.mouse_y, &x, &y); + event->kind = wuss_INPUT_WHEEL; + event->pos = POINT(x, y); + event->wheel = (int) ev.wheel.y; + } + return true; + + default: + continue; + } + } +} + +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 on F4, 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); + + SDL_Delay(1000 / 60); +} + +void wuss_frontend_set_palette(wuss_frontend_t *fe, + const colour_t *palette, + int npalette) +{ + NOT_USED(fe); + NOT_USED(palette); + NOT_USED(npalette); + /* SDL has no physical palette: the p4 -> bgrx conversion in present() reads + * the palette straight off the caller's bitmap_t, which it has already + * updated. Nothing to do here. */ +} + +void wuss_frontend_close(wuss_frontend_t *fe) +{ + if (fe == NULL) + return; + + SDL_DestroyTexture(fe->texture); + SDL_DestroyRenderer(fe->renderer); + SDL_DestroyWindow(fe->window); + SDL_Quit(); + + free(fe->pixels); + free(fe); +} + +#endif /* USE_SDL */ +#endif /* WUSS_APP */ diff --git a/apps/wuss/frontend.h b/apps/wuss/frontend.h new file mode 100644 index 00000000..46ce26e8 --- /dev/null +++ b/apps/wuss/frontend.h @@ -0,0 +1,90 @@ +/* wuss/frontend.h -- platform backend for the Wuss interactive demo */ + +#ifndef WUSS_FRONTEND_H +#define WUSS_FRONTEND_H + +#ifdef WUSS_APP + +#include <stdbool.h> + +#include "base/result.h" +#include "framebuf/bitmap.h" +#include "framebuf/colour.h" +#include "framebuf/pixelfmt.h" +#include "geom/point.h" +#include "wuss/wuss.h" + +/* One backend drives the demo: SDL on the desktop, native VDU/OS_Mouse calls + * on RISC OS. main.c owns the wuss instance and the main loop; the backend + * only opens a surface, reports input as normalised wuss_input_t records and + * gets the framebuffer onto the screen. */ + +typedef struct wuss_frontend wuss_frontend_t; + +/* Normalised input event kinds. The backend translates whatever the platform + * delivers (SDL events, polled OS_Mouse state, key scans) into these; the + * loop in main.c acts on them without knowing which backend produced them. */ +typedef enum wuss_input_kind +{ + wuss_INPUT_NONE = 0, + wuss_INPUT_QUIT, /* window closed, or the quit key */ + wuss_INPUT_MOUSE_MOVE, /* .pos */ + wuss_INPUT_MOUSE_DOWN, /* .pos, .button */ + wuss_INPUT_MOUSE_UP, /* .pos, .button */ + wuss_INPUT_WHEEL, /* .pos, .wheel */ + wuss_INPUT_REDRAW_ALL, /* force a full redraw (F1) */ + wuss_INPUT_PIXEL_STRESS, /* one-pixel-at-a-time redraw (F3) */ + wuss_INPUT_PALETTE_CYCLE /* advance to the next system palette (F4) */ +} +wuss_input_kind_t; + +typedef struct wuss_input +{ + wuss_input_kind_t kind; + point_t pos; /* screen-space pixel coordinates */ + wuss_button_t button; + int wheel; /* wheel delta, +ve = up */ +} +wuss_input_t; + +/* Open a drawing surface `width` x `height` pixels. + * + * `palette` / `npalette` are the initial system palette: a backend that owns + * the physical palette (RISC OS 16-colour mode) programmes it here. + * + * 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 + * hand back screen memory directly (no copy on present) or a private buffer + * (present() blits it). Either way the caller must not free *pixels; call + * wuss_frontend_close to release it. + */ +result_t wuss_frontend_open(int width, + int height, + const colour_t *palette, + int npalette, + void **pixels, + int *rowbytes, + pixelfmt_t *fmt, + wuss_frontend_t **frontend); + +/* Pull the next pending input event. Returns true and fills *event while + * events remain; returns false when the queue is empty for this frame. */ +bool wuss_frontend_poll(wuss_frontend_t *frontend, wuss_input_t *event); + +/* Push the current framebuffer contents to the screen and pace the frame. A + * backend rendering straight into screen memory only waits for vsync here. */ +void wuss_frontend_present(wuss_frontend_t *frontend, const bitmap_t *bm); + +/* Push a new system palette to the physical palette, if the backend owns one. + * Called after the demo cycles palettes (F4). No-op for SDL. */ +void wuss_frontend_set_palette(wuss_frontend_t *frontend, + const colour_t *palette, + int npalette); + +/* Tear down the surface and free everything wuss_frontend_open allocated. */ +void wuss_frontend_close(wuss_frontend_t *frontend); + +#endif /* WUSS_APP */ + +#endif /* WUSS_FRONTEND_H */ diff --git a/apps/wuss/main.c b/apps/wuss/main.c index 1f6df726..751e2ed0 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -8,6 +8,7 @@ #include "fortify/fortify.h" #endif +#include "base/debug.h" #include "base/result.h" #include "base/utils.h" #include "framebuf/bitmap.h" @@ -23,7 +24,7 @@ #include "wuss/window.h" #include "wuss/menu.h" -#include <SDL3/SDL.h> +#include "frontend.h" #include "tasks/ball.h" #include "tasks/blank.h" @@ -42,11 +43,6 @@ /* ----------------------------------------------------------------------- */ -/* Screen pixel format for the interactive test: 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_TEST_32BPP 0 - /* the launcher's spawn callbacks take no arguments, so the pieces they need * are stashed here instead; run_wuss runs at most once per * process, so a file-scope struct is as good as a passed-around context */ @@ -136,7 +132,10 @@ static result_t spawn_image(void) ninepatch = path_join_filename(g.resources, 3, "resources", "wuss", path_join_leafname("9tile", "png")); + logf_info("wuss: image task loading \"%s\" + \"%s\"", buf, ninepatch); rc = image_create(g.wuss, buf, ninepatch, t); + if (rc != result_OK) + logf_error("wuss: image_create(\"%s\") failed, rc=0x%X", buf, rc); if (rc != result_OK || t->window == NULL) { free(t); return rc; } return result_OK; } @@ -197,6 +196,9 @@ static result_t spawn_icons(void) result_t rc; if (t == NULL) return result_OOM; rc = icons_create(g.wuss, g.daydream_font, g.resources, t); + if (rc != result_OK) + logf_error("wuss: icons_create (resources \"%s\") failed, rc=0x%X", + g.resources, rc); if (rc != result_OK || t->window == NULL) { free(t); return rc; } return result_OK; } @@ -217,6 +219,9 @@ static result_t spawn_porter_duff(void) result_t rc; if (t == NULL) return result_OOM; rc = porter_duff_create(g.wuss, g.palette, g.daydream_font, g.resources, t); + if (rc != result_OK) + logf_error("wuss: porter_duff_create (resources \"%s\") failed, rc=0x%X", + g.resources, rc); if (rc != result_OK || t->window == NULL) { free(t); return rc; } return result_OK; } @@ -404,55 +409,88 @@ static result_t menu_handle(wuss_window_t *window, return result_OK; } -static wuss_button_t sdl_button_to_wuss(Uint8 button) -{ - switch (button) - { - case SDL_BUTTON_MIDDLE: return wuss_BUTTON_MENU; - case SDL_BUTTON_RIGHT: return wuss_BUTTON_ADJUST; - default: return wuss_BUTTON_SELECT; - } -} - -/* Palettes F4 cycles through, in order. Each fills a colour_t[16]. */ +/* Palettes the palette-cycle input steps through, in order. Each fills a + * colour_t[16]. */ static void (*const g_palettes[])(colour_t *) = { define_pico8_palette, define_wimp16_palette }; -/* SDL delivers mouse coordinates in window space, which F2 can scale away - * from the fixed-size Wuss screen; map back down to screen space */ -static void sdl_pos_to_scr(SDL_Window *window, - int scr_width, - int scr_height, - float in_x, - float in_y, - int *out_x, - int *out_y) +/* 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] = +{ + /* 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 }, + /* 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 } +}; + +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.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->backdrop.pattern = screen_PATTERN_DOTS; + config->backdrop.pattern_bg = c[14]; +} + +/* Redraw the whole screen one pixel at a time: each wuss_redraw_dirty call is + * flushed before the next pixel is invalidated, so no two pixels are ever + * coalesced into one redraw. A task whose drawing routine assumes it always + * gets a multi-pixel/aligned clip (rather than trusting scr->clip) will + * visibly misdraw here even though it looks fine under larger dirty regions. */ +static void pixel_stress(wuss_t *wuss, int scr_width, int scr_height) { - int win_w, win_h; + int x, y; + + for (y = 0; y < scr_height; y++) + { + for (x = 0; x < scr_width; x++) + { + box_t px; - SDL_GetWindowSize(window, &win_w, &win_h); + px.x0 = x; px.y0 = y; + px.x1 = x + 1; px.y1 = y + 1; - *out_x = (int) (in_x * scr_width / win_w); - *out_y = (int) (in_y * scr_height / win_h); + wuss_invalidate(wuss, &px); + wuss_redraw_dirty(wuss); + } + } } -/* click windows to bring to front, drag titlebars to move, resize the - * SDL window to see the Wuss screen scale; F2 doubles the SDL window size, - * Shift-F2 halves it; F3 redraws the whole screen one pixel at a time, to - * catch tasks whose drawing routines misbehave under a 1x1 clip; F4 cycles - * the system palette live (wuss_set_palette); Q or close to quit */ +/* click windows to bring to front, drag titlebars to move; the redraw-all + * input redraws the whole screen, the pixel-stress input does it one pixel at + * a time to catch tasks that misbehave under a 1x1 clip; the palette-cycle + * input swaps the system palette live (wuss_set_palette); the quit input or + * closing the window exits */ static result_t run_wuss(const char *resources) { const int scr_width = 640; const int scr_height = 480; -#if WUSS_TEST_32BPP - const int rowbytes = scr_width * 4; /* pixelfmt_bgrx8888: 4 bytes/pixel */ -#else - const int rowbytes = scr_width / 2; /* pixelfmt_p4: 2 pixels/byte */ -#endif result_t rc; const char *leafname; @@ -460,25 +498,18 @@ static result_t run_wuss(const char *resources) bmfont_t *font; bmfont_t *daydream_font; void *pixels; + int rowbytes; + pixelfmt_t fmt; bitmap_t bm; -#if !WUSS_TEST_32BPP - bitmap_t *disp; -#endif screen_t scr; colour_t palette[16]; wuss_t *wuss; - SDL_Window *window; - SDL_Renderer *renderer; - SDL_Texture *texture; - bool quit; - bool garbage_pending; - bool pixel_stress_pending; - int i; + wuss_frontend_t *frontend; bool use_wimp16; int palette_index; { - /* "riscos16" selects the RISC OS 16-colour palette; default is PICO-8 */ + /* "wimp16" selects the RISC OS 16-colour palette; default is PICO-8 */ const char *palette_name = getenv("WUSS_PALETTE"); use_wimp16 = (palette_name != NULL && strcmp(palette_name, "wimp16") == 0); @@ -486,27 +517,35 @@ static result_t run_wuss(const char *resources) g_palettes[palette_index](palette); } + logf_info("wuss: resources root = \"%s\"", resources); + leafname = path_join_leafname("digits", "png"); filename = path_join_filename(resources, 3, "resources", "bmfonts", leafname); + logf_info("wuss: loading font \"%s\"", filename); rc = bmfont_create(filename, &font); if (rc != result_OK) + { + logf_error("wuss: bmfont_create(\"%s\") failed, rc=0x%X", filename, rc); goto Failure; + } leafname = path_join_leafname("daydream", "png"); filename = path_join_filename(resources, 3, "resources", "bmfonts", leafname); + logf_info("wuss: loading font \"%s\"", filename); rc = bmfont_create(filename, &daydream_font); if (rc != result_OK) + { + logf_error("wuss: bmfont_create(\"%s\") failed, rc=0x%X", filename, rc); goto Failure; + } - pixels = malloc(rowbytes * scr_height); - if (pixels == NULL) + rc = wuss_frontend_open(scr_width, scr_height, palette, NELEMS(palette), + &pixels, &rowbytes, &fmt, &frontend); + if (rc != result_OK) goto Failure; -#if WUSS_TEST_32BPP - rc = bitmap_init(&bm, SIZE2D(scr_width, scr_height), pixelfmt_bgrx8888, rowbytes, palette, pixels); -#else - rc = bitmap_init(&bm, SIZE2D(scr_width, scr_height), pixelfmt_p4, rowbytes, palette, pixels); -#endif + rc = bitmap_init(&bm, SIZE2D(scr_width, scr_height), fmt, rowbytes, palette, + pixels); if (rc != result_OK) goto Failure; @@ -514,76 +553,10 @@ static result_t run_wuss(const char *resources) screen_for_bitmap(&scr, &bm); - if (!SDL_Init(SDL_INIT_VIDEO)) { - fprintf(stderr, "Error: SDL_Init: %s\n", SDL_GetError()); - goto Failure; - } + wuss_config_t config; - window = SDL_CreateWindow("Wuss", scr_width, scr_height, 0); - if (window == NULL) - { - fprintf(stderr, "Error: SDL_CreateWindow: %s\n", SDL_GetError()); - goto Failure; - } - - renderer = SDL_CreateRenderer(window, NULL); - if (renderer == NULL) - { - fprintf(stderr, "Error: SDL_CreateRenderer: %s\n", SDL_GetError()); - goto Failure; - } - - texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_ARGB8888, - SDL_TEXTUREACCESS_STREAMING, - scr_width, scr_height); - if (texture == NULL) - { - fprintf(stderr, "Error: SDL_CreateTexture: %s\n", SDL_GetError()); - goto Failure; - } - - SDL_SetTextureBlendMode(texture, SDL_BLENDMODE_NONE); - SDL_SetTextureScaleMode(texture, SDL_SCALEMODE_NEAREST); /* keep pixels crisp when F2 scales the window up */ - - { - /* Furniture/bevel/accent/backdrop colour indices, one row per palette. - * Same field order as the assignments below. */ - static const wuss_colour_t chrome[2][15] = - { - /* 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 }, - /* 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 } - }; - const wuss_colour_t *c = chrome[use_wimp16 ? 1 : 0]; - wuss_config_t config; - - 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.backdrop.pattern = screen_PATTERN_DOTS; - config.backdrop.pattern_bg = c[14]; + fill_chrome_config(&config, use_wimp16 ? 1 : 0); rc = wuss_create(&scr, font, palette, NELEMS(palette), &config, NULL, &wuss); @@ -608,97 +581,64 @@ static result_t run_wuss(const char *resources) goto Failure; } - quit = false; - g.quit = false; - garbage_pending = false; - pixel_stress_pending = false; + g.quit = false; wuss_redraw(wuss); - while (!quit && !g.quit) + while (!g.quit) { - SDL_Event event; + wuss_input_t ev; + bool pixel_stress_pending = false; - while (SDL_PollEvent(&event)) + while (wuss_frontend_poll(frontend, &ev)) { - switch (event.type) + switch (ev.kind) { - case SDL_EVENT_QUIT: - quit = true; + case wuss_INPUT_QUIT: + g.quit = true; break; - case SDL_EVENT_KEY_UP: - if (event.key.key == SDLK_Q) - quit = true; - else if (event.key.key == SDLK_F1 && (event.key.mod & SDL_KMOD_SHIFT)) - wuss_redraw(wuss); - else if (event.key.key == SDLK_F1) - garbage_pending = true; - else if (event.key.key == SDLK_F3) - pixel_stress_pending = true; - else if (event.key.key == SDLK_F4) - { - /* cycle the system palette live: rebuild the palette, push it into - * the framebuffer bitmap, then tell wuss, which refreshes its own - * copy and pokes every task to recache */ - palette_index = (palette_index + 1) % (int) NELEMS(g_palettes); - g_palettes[palette_index](palette); - bitmap_set_palette(&bm, palette); - wuss_set_palette(wuss, palette, NELEMS(palette)); - } - else if (event.key.key == SDLK_F2) - { - int w, h; + case wuss_INPUT_REDRAW_ALL: + wuss_redraw(wuss); + break; - SDL_GetWindowSize(window, &w, &h); - if (event.key.mod & SDL_KMOD_SHIFT) - SDL_SetWindowSize(window, w / 2, h / 2); - else - SDL_SetWindowSize(window, w * 2, h * 2); - } + case wuss_INPUT_PIXEL_STRESS: + pixel_stress_pending = true; + break; + + case wuss_INPUT_PALETTE_CYCLE: + /* rebuild the palette, push it into the framebuffer bitmap, tell the + * backend (which owns any physical palette), then tell wuss, which + * refreshes its own copy and pokes every task to recache */ + palette_index = (palette_index + 1) % (int) NELEMS(g_palettes); + g_palettes[palette_index](palette); + bitmap_set_palette(&bm, palette); + wuss_frontend_set_palette(frontend, palette, NELEMS(palette)); + wuss_set_palette(wuss, palette, NELEMS(palette)); break; - case SDL_EVENT_MOUSE_BUTTON_DOWN: + case wuss_INPUT_MOUSE_DOWN: { - int x, y; - wuss_button_t button; wuss_window_t *hit; - sdl_pos_to_scr(window, scr_width, scr_height, event.button.x, event.button.y, &x, &y); - button = sdl_button_to_wuss(event.button.button); - wuss_mouse_click(wuss, POINT(x, y), button, wuss_MOUSE_DOWN, &hit); + wuss_mouse_click(wuss, ev.pos, ev.button, wuss_MOUSE_DOWN, &hit); /* MENU click on bare backdrop opens the task launcher there */ - if (hit == NULL && (button & wuss_BUTTON_MENU)) - wuss_menu_open(g.menu_task, &g_task_menu, POINT(x, y), NULL); + if (hit == NULL && (ev.button & wuss_BUTTON_MENU)) + wuss_menu_open(g.menu_task, &g_task_menu, ev.pos, NULL); } break; - case SDL_EVENT_MOUSE_BUTTON_UP: - { - int x, y; - - sdl_pos_to_scr(window, scr_width, scr_height, event.button.x, event.button.y, &x, &y); - wuss_mouse_click(wuss, POINT(x, y), sdl_button_to_wuss(event.button.button), wuss_MOUSE_UP, NULL); - } + case wuss_INPUT_MOUSE_UP: + wuss_mouse_click(wuss, ev.pos, ev.button, wuss_MOUSE_UP, NULL); break; - case SDL_EVENT_MOUSE_MOTION: - { - int x, y; - - sdl_pos_to_scr(window, scr_width, scr_height, event.motion.x, event.motion.y, &x, &y); - wuss_mouse_move(wuss, POINT(x, y), NULL); - } + case wuss_INPUT_MOUSE_MOVE: + wuss_mouse_move(wuss, ev.pos, NULL); break; - case SDL_EVENT_MOUSE_WHEEL: - { - int x, y; - - sdl_pos_to_scr(window, scr_width, scr_height, event.wheel.mouse_x, event.wheel.mouse_y, &x, &y); - wuss_scroll(wuss, POINT(x, y), (int) event.wheel.y, NULL); - } + case wuss_INPUT_WHEEL: + wuss_scroll(wuss, ev.pos, ev.wheel, NULL); break; default: @@ -708,71 +648,12 @@ static result_t run_wuss(const char *resources) wuss_idle(wuss); - if (garbage_pending) - { - /* full-screen corruption, drawn over whatever's already on screen - * (windows included) and left untouched: nothing here is invalidated, - * so it stays put until something actually redraws over it, e.g. the - * ball's own small per-frame dirty rect eating a trail through it, or - * a window being dragged across it */ - unsigned char *p; - size_t n; - size_t i; - - p = pixels; - n = (size_t) rowbytes * scr_height; - for (i = 0; i < n; i++) - p[i] = (unsigned char) rand(); - - garbage_pending = false; - } - else if (pixel_stress_pending) - { - /* redraw the whole screen one pixel at a time: each wuss_redraw_dirty - * call is flushed before the next pixel is invalidated, so no two - * pixels are ever coalesced into one redraw. A task whose drawing - * routine assumes it always gets handed a multi-pixel/aligned clip - * (rather than trusting scr->clip) will visibly misdraw here even - * though it looks fine under normal, larger dirty regions. */ - int x, y; - - for (y = 0; y < scr_height; y++) - { - for (x = 0; x < scr_width; x++) - { - box_t px; - - px.x0 = x; px.y0 = y; - px.x1 = x + 1; px.y1 = y + 1; - - wuss_invalidate(wuss, &px); - wuss_redraw_dirty(wuss); - } - } - - pixel_stress_pending = false; - } + if (pixel_stress_pending) + pixel_stress(wuss, scr_width, scr_height); else - { wuss_redraw_dirty(wuss); - } - -#if WUSS_TEST_32BPP - SDL_UpdateTexture(texture, NULL, bm.base, bm.rowbytes); /* bm is already bgrx8888: no conversion needed */ -#else - rc = bitmap_convert(&bm, pixelfmt_bgrx8888, &disp); - if (rc == result_OK) - { - SDL_UpdateTexture(texture, NULL, disp->base, disp->rowbytes); - free(disp->base); - free(disp); - } -#endif - - SDL_RenderTexture(renderer, texture, NULL, NULL); - SDL_RenderPresent(renderer); - SDL_Delay(1000 / 60); + wuss_frontend_present(frontend, &bm); } /* ponytail: wuss_destroy() below force-closes every still-open window and @@ -785,12 +666,7 @@ static result_t run_wuss(const char *resources) bmfont_destroy(font); bmfont_destroy(daydream_font); - SDL_DestroyTexture(texture); - SDL_DestroyRenderer(renderer); - SDL_DestroyWindow(window); - SDL_Quit(); - - free(pixels); + wuss_frontend_close(frontend); return result_TEST_PASSED; @@ -806,7 +682,14 @@ static result_t run_wuss(const char *resources) 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 = "@"; +#else const char *resources = "."; +#endif int i; result_t rc; diff --git a/libraries/framebuf/bmfont/bmfont.c b/libraries/framebuf/bmfont/bmfont.c index 534c9996..0bee21f3 100644 --- a/libraries/framebuf/bmfont/bmfont.c +++ b/libraries/framebuf/bmfont/bmfont.c @@ -17,6 +17,7 @@ #include <png.h> +#include "base/debug.h" #include "base/utils.h" #include "framebuf/bmfont.h" #include "utils/array.h" @@ -361,12 +362,14 @@ result_t bmfont_create(const char *png, bmfont_t **pbmfont) fp = fopen(png, "rb"); if (fp == NULL) { + logf_error("bmfont_create: cannot open \"%s\"", png); rc = result_FILE_NOT_FOUND; goto cleanup; } if (!file_is_png(fp)) { + logf_error("bmfont_create: \"%s\" is not a PNG", png); rc = result_BAD_ARG; goto cleanup; } diff --git a/libraries/wuss/test/tasks/ball.c b/libraries/wuss/test/tasks/ball.c index 11aab9bc..de7130c5 100644 --- a/libraries/wuss/test/tasks/ball.c +++ b/libraries/wuss/test/tasks/ball.c @@ -1,6 +1,6 @@ /* wuss/test/tasks/ball.c -- bouncing ball task */ -#ifdef USE_SDL +#ifdef WUSS_APP #include <stdlib.h> @@ -229,4 +229,4 @@ result_t ball_handle(wuss_window_t *window, } } -#endif /* USE_SDL */ +#endif /* WUSS_APP */ diff --git a/libraries/wuss/test/tasks/ball.h b/libraries/wuss/test/tasks/ball.h index c6605d6e..73068ed6 100644 --- a/libraries/wuss/test/tasks/ball.h +++ b/libraries/wuss/test/tasks/ball.h @@ -3,7 +3,7 @@ #ifndef TASKS_BALL_H #define TASKS_BALL_H -#ifdef USE_SDL +#ifdef WUSS_APP #include "framebuf/colour.h" #include "wuss/window.h" @@ -38,6 +38,6 @@ wuss_window_fn_t ball_handle; * per-instance block owned by the window and freed when it closes */ result_t ball_create(wuss_t *wuss, ball_task_t *task); -#endif /* USE_SDL */ +#endif /* WUSS_APP */ #endif /* TASKS_BALL_H */ diff --git a/libraries/wuss/test/tasks/blank.c b/libraries/wuss/test/tasks/blank.c index ce461b77..76edcc7c 100644 --- a/libraries/wuss/test/tasks/blank.c +++ b/libraries/wuss/test/tasks/blank.c @@ -1,6 +1,6 @@ /* wuss/test/tasks/blank.c -- colour-cycling task */ -#ifdef USE_SDL +#ifdef WUSS_APP #include <stdlib.h> @@ -83,4 +83,4 @@ result_t blank_handle(wuss_window_t *window, return blank_idle(task_data); } -#endif /* USE_SDL */ +#endif /* WUSS_APP */ diff --git a/libraries/wuss/test/tasks/blank.h b/libraries/wuss/test/tasks/blank.h index c1b94ebe..34ea5459 100644 --- a/libraries/wuss/test/tasks/blank.h +++ b/libraries/wuss/test/tasks/blank.h @@ -3,7 +3,7 @@ #ifndef TASKS_BLANK_H #define TASKS_BLANK_H -#ifdef USE_SDL +#ifdef WUSS_APP #include "wuss/window.h" #include "wuss/task.h" @@ -27,6 +27,6 @@ wuss_window_fn_t blank_handle; result_t blank_create(wuss_t *wuss, blank_task_t *task); -#endif /* USE_SDL */ +#endif /* WUSS_APP */ #endif /* TASKS_BLANK_H */ diff --git a/libraries/wuss/test/tasks/chars.c b/libraries/wuss/test/tasks/chars.c index 11cc47db..af403b5e 100644 --- a/libraries/wuss/test/tasks/chars.c +++ b/libraries/wuss/test/tasks/chars.c @@ -1,6 +1,6 @@ /* wuss/test/tasks/chars.c -- system font glyph grid task */ -#ifdef USE_SDL +#ifdef WUSS_APP #include <stdlib.h> @@ -137,4 +137,4 @@ result_t chars_handle(wuss_window_t *window, return chars_redraw(event, task_data); } -#endif /* USE_SDL */ +#endif /* WUSS_APP */ diff --git a/libraries/wuss/test/tasks/chars.h b/libraries/wuss/test/tasks/chars.h index eb2dc367..5ee32c87 100644 --- a/libraries/wuss/test/tasks/chars.h +++ b/libraries/wuss/test/tasks/chars.h @@ -3,7 +3,7 @@ #ifndef TASKS_CHARS_H #define TASKS_CHARS_H -#ifdef USE_SDL +#ifdef WUSS_APP #include "framebuf/bmfont.h" #include "framebuf/colour.h" @@ -25,6 +25,6 @@ wuss_window_fn_t chars_handle; * nothing and returns result_OK if wuss has no system font */ result_t chars_create(wuss_t *wuss, chars_task_t *task); -#endif /* USE_SDL */ +#endif /* WUSS_APP */ #endif /* TASKS_CHARS_H */ diff --git a/libraries/wuss/test/tasks/checker.c b/libraries/wuss/test/tasks/checker.c index 8e3f4662..81d9f9d9 100644 --- a/libraries/wuss/test/tasks/checker.c +++ b/libraries/wuss/test/tasks/checker.c @@ -1,6 +1,6 @@ /* wuss/test/tasks/checker.c -- checkerboard task */ -#ifdef USE_SDL +#ifdef WUSS_APP #include <stdlib.h> @@ -186,4 +186,4 @@ result_t checker_handle(wuss_window_t *window, } } -#endif /* USE_SDL */ +#endif /* WUSS_APP */ diff --git a/libraries/wuss/test/tasks/checker.h b/libraries/wuss/test/tasks/checker.h index 565fe7a9..63f7d4b2 100644 --- a/libraries/wuss/test/tasks/checker.h +++ b/libraries/wuss/test/tasks/checker.h @@ -3,7 +3,7 @@ #ifndef TASKS_CHECKER_H #define TASKS_CHECKER_H -#ifdef USE_SDL +#ifdef WUSS_APP #include "framebuf/colour.h" #include "wuss/window.h" @@ -35,6 +35,6 @@ wuss_window_fn_t checker_handle; /* create the two checkerboard windows against the given wuss instance */ result_t checker_create(wuss_t *wuss, checker_task_t *task); -#endif /* USE_SDL */ +#endif /* WUSS_APP */ #endif /* TASKS_CHECKER_H */ diff --git a/libraries/wuss/test/tasks/curve.c b/libraries/wuss/test/tasks/curve.c index 8574cb0f..5fb78e8f 100644 --- a/libraries/wuss/test/tasks/curve.c +++ b/libraries/wuss/test/tasks/curve.c @@ -1,6 +1,6 @@ /* wuss/test/tasks/curve.c -- draggable Bezier curve task */ -#ifdef USE_SDL +#ifdef WUSS_APP #include <stdlib.h> @@ -191,4 +191,4 @@ result_t curve_handle(wuss_window_t *window, } } -#endif /* USE_SDL */ +#endif /* WUSS_APP */ diff --git a/libraries/wuss/test/tasks/curve.h b/libraries/wuss/test/tasks/curve.h index f8db1d99..92e842aa 100644 --- a/libraries/wuss/test/tasks/curve.h +++ b/libraries/wuss/test/tasks/curve.h @@ -3,7 +3,7 @@ #ifndef TASKS_CURVE_H #define TASKS_CURVE_H -#ifdef USE_SDL +#ifdef WUSS_APP #include "framebuf/colour.h" #include "geom/point.h" @@ -28,6 +28,6 @@ wuss_window_fn_t curve_handle; /* create the curve window against the given wuss instance */ result_t curve_create(wuss_t *wuss, curve_task_t *task); -#endif /* USE_SDL */ +#endif /* WUSS_APP */ #endif /* TASKS_CURVE_H */ diff --git a/libraries/wuss/test/tasks/gradient.c b/libraries/wuss/test/tasks/gradient.c index 8f33bd99..ec02eec3 100644 --- a/libraries/wuss/test/tasks/gradient.c +++ b/libraries/wuss/test/tasks/gradient.c @@ -1,6 +1,6 @@ /* wuss/test/tasks/gradient.c -- gradient fill task */ -#ifdef USE_SDL +#ifdef WUSS_APP #include <stdlib.h> @@ -111,4 +111,4 @@ result_t gradient_handle(wuss_window_t *window, } } -#endif /* USE_SDL */ +#endif /* WUSS_APP */ diff --git a/libraries/wuss/test/tasks/gradient.h b/libraries/wuss/test/tasks/gradient.h index db7898c0..0ad2c658 100644 --- a/libraries/wuss/test/tasks/gradient.h +++ b/libraries/wuss/test/tasks/gradient.h @@ -3,7 +3,7 @@ #ifndef TASKS_GRADIENT_H #define TASKS_GRADIENT_H -#ifdef USE_SDL +#ifdef WUSS_APP #include "wuss/window.h" @@ -22,6 +22,6 @@ wuss_window_fn_t gradient_handle; result_t gradient_create(wuss_t *wuss, gradient_task_t *task); -#endif /* USE_SDL */ +#endif /* WUSS_APP */ #endif /* TASKS_GRADIENT_H */ diff --git a/libraries/wuss/test/tasks/icons.c b/libraries/wuss/test/tasks/icons.c index 025793c7..8d92ae55 100644 --- a/libraries/wuss/test/tasks/icons.c +++ b/libraries/wuss/test/tasks/icons.c @@ -1,6 +1,6 @@ /* wuss/test/tasks/icons.c -- work-area icons task */ -#ifdef USE_SDL +#ifdef WUSS_APP #include "framebuf/palettes.h" #include <stdio.h> @@ -406,4 +406,4 @@ result_t icons_handle(wuss_window_t *window, } } -#endif /* USE_SDL */ +#endif /* WUSS_APP */ diff --git a/libraries/wuss/test/tasks/icons.h b/libraries/wuss/test/tasks/icons.h index 1780f4df..8c793a12 100644 --- a/libraries/wuss/test/tasks/icons.h +++ b/libraries/wuss/test/tasks/icons.h @@ -3,7 +3,7 @@ #ifndef TASKS_ICONS_H #define TASKS_ICONS_H -#ifdef USE_SDL +#ifdef WUSS_APP #include "framebuf/bitmap.h" #include "framebuf/bmfont.h" @@ -39,6 +39,6 @@ result_t icons_create(wuss_t *wuss, const char *resources, icons_task_t *task); -#endif /* USE_SDL */ +#endif /* WUSS_APP */ #endif /* TASKS_ICONS_H */ diff --git a/libraries/wuss/test/tasks/image.c b/libraries/wuss/test/tasks/image.c index c822806b..9e7308dd 100644 --- a/libraries/wuss/test/tasks/image.c +++ b/libraries/wuss/test/tasks/image.c @@ -1,6 +1,6 @@ /* wuss/test/tasks/image.c -- static bitmap image task */ -#ifdef USE_SDL +#ifdef WUSS_APP #include <stdlib.h> @@ -115,4 +115,4 @@ result_t image_handle(wuss_window_t *window, } } -#endif /* USE_SDL */ +#endif /* WUSS_APP */ diff --git a/libraries/wuss/test/tasks/image.h b/libraries/wuss/test/tasks/image.h index 906f3157..df56c28c 100644 --- a/libraries/wuss/test/tasks/image.h +++ b/libraries/wuss/test/tasks/image.h @@ -3,7 +3,7 @@ #ifndef TASKS_IMAGE_H #define TASKS_IMAGE_H -#ifdef USE_SDL +#ifdef WUSS_APP #include "framebuf/bitmap.h" #include "wuss/window.h" @@ -28,6 +28,6 @@ result_t image_create(wuss_t *wuss, const char *background_path, image_task_t *task); -#endif /* USE_SDL */ +#endif /* WUSS_APP */ #endif /* TASKS_IMAGE_H */ diff --git a/libraries/wuss/test/tasks/lissajous.c b/libraries/wuss/test/tasks/lissajous.c index b1c1a8df..87d1e6f5 100644 --- a/libraries/wuss/test/tasks/lissajous.c +++ b/libraries/wuss/test/tasks/lissajous.c @@ -1,6 +1,6 @@ /* wuss/test/tasks/lissajous.c -- Lissajous figure task */ -#ifdef USE_SDL +#ifdef WUSS_APP #include <math.h> #include <stdlib.h> @@ -170,4 +170,4 @@ result_t lissajous_handle(wuss_window_t *window, } } -#endif /* USE_SDL */ +#endif /* WUSS_APP */ diff --git a/libraries/wuss/test/tasks/lissajous.h b/libraries/wuss/test/tasks/lissajous.h index be6ea5ea..2fd194e7 100644 --- a/libraries/wuss/test/tasks/lissajous.h +++ b/libraries/wuss/test/tasks/lissajous.h @@ -3,7 +3,7 @@ #ifndef TASKS_LISSAJOUS_H #define TASKS_LISSAJOUS_H -#ifdef USE_SDL +#ifdef WUSS_APP #include "framebuf/colour.h" #include "wuss/window.h" @@ -30,6 +30,6 @@ wuss_window_fn_t lissajous_handle; * per-instance block owned by the window and freed when it closes */ result_t lissajous_create(wuss_t *wuss, lissajous_task_t *task); -#endif /* USE_SDL */ +#endif /* WUSS_APP */ #endif /* TASKS_LISSAJOUS_H */ diff --git a/libraries/wuss/test/tasks/palette.c b/libraries/wuss/test/tasks/palette.c index 0c5ccd40..179dc3fc 100644 --- a/libraries/wuss/test/tasks/palette.c +++ b/libraries/wuss/test/tasks/palette.c @@ -1,6 +1,6 @@ /* wuss/test/tasks/palette.c -- desktop palette swatch grid task */ -#ifdef USE_SDL +#ifdef WUSS_APP #include <stdlib.h> @@ -103,4 +103,4 @@ result_t palette_handle(wuss_window_t *window, return palette_redraw(event, task_data); } -#endif /* USE_SDL */ +#endif /* WUSS_APP */ diff --git a/libraries/wuss/test/tasks/palette.h b/libraries/wuss/test/tasks/palette.h index 6b445e62..f30a6a6d 100644 --- a/libraries/wuss/test/tasks/palette.h +++ b/libraries/wuss/test/tasks/palette.h @@ -3,7 +3,7 @@ #ifndef TASKS_PALETTE_H #define TASKS_PALETTE_H -#ifdef USE_SDL +#ifdef WUSS_APP #include "framebuf/colour.h" #include "wuss/window.h" @@ -27,6 +27,6 @@ result_t palette_create(wuss_t *wuss, palette_task_t *task); -#endif /* USE_SDL */ +#endif /* WUSS_APP */ #endif /* TASKS_PALETTE_H */ diff --git a/libraries/wuss/test/tasks/porter-duff.c b/libraries/wuss/test/tasks/porter-duff.c index 8fb79964..363d8ba9 100644 --- a/libraries/wuss/test/tasks/porter-duff.c +++ b/libraries/wuss/test/tasks/porter-duff.c @@ -1,6 +1,6 @@ /* wuss/test/tasks/porter-duff.c -- animated Porter-Duff compositing task */ -#ifdef USE_SDL +#ifdef WUSS_APP #include <stdlib.h> #include <string.h> @@ -415,4 +415,4 @@ result_t porter_duff_handle(wuss_window_t *window, } } -#endif /* USE_SDL */ +#endif /* WUSS_APP */ diff --git a/libraries/wuss/test/tasks/porter-duff.h b/libraries/wuss/test/tasks/porter-duff.h index ce58ea7e..c617aa2b 100644 --- a/libraries/wuss/test/tasks/porter-duff.h +++ b/libraries/wuss/test/tasks/porter-duff.h @@ -3,7 +3,7 @@ #ifndef TASKS_PORTER_DUFF_H #define TASKS_PORTER_DUFF_H -#ifdef USE_SDL +#ifdef WUSS_APP #include "framebuf/bitmap.h" #include "framebuf/bmfont.h" @@ -43,6 +43,6 @@ result_t porter_duff_create(wuss_t *wuss, porter_duff_task_t *task); -#endif /* USE_SDL */ +#endif /* WUSS_APP */ #endif /* TASKS_PORTER_DUFF_H */ diff --git a/libraries/wuss/test/tasks/sofa.c b/libraries/wuss/test/tasks/sofa.c index 2df80f53..a52dba89 100644 --- a/libraries/wuss/test/tasks/sofa.c +++ b/libraries/wuss/test/tasks/sofa.c @@ -1,6 +1,6 @@ /* wuss/test/tasks/sofa.c -- rotating wireframe sofa and spaceship task */ -#ifdef USE_SDL +#ifdef WUSS_APP #include <stdlib.h> @@ -598,4 +598,4 @@ result_t sofa_handle(wuss_window_t *window, } } -#endif /* USE_SDL */ +#endif /* WUSS_APP */ diff --git a/libraries/wuss/test/tasks/sofa.h b/libraries/wuss/test/tasks/sofa.h index 1551766f..732d5dc5 100644 --- a/libraries/wuss/test/tasks/sofa.h +++ b/libraries/wuss/test/tasks/sofa.h @@ -3,7 +3,7 @@ #ifndef TASKS_SOFA_H #define TASKS_SOFA_H -#ifdef USE_SDL +#ifdef WUSS_APP #include <stdbool.h> @@ -45,6 +45,6 @@ wuss_window_fn_t sofa_handle; /* create the sofa window against the given wuss instance */ result_t sofa_create(wuss_t*wuss, sofa_task_t*task); -#endif /* USE_SDL */ +#endif /* WUSS_APP */ #endif /* TASKS_SOFA_H */ diff --git a/libraries/wuss/test/tasks/swatches.c b/libraries/wuss/test/tasks/swatches.c index f9df9ad1..d2a323a3 100644 --- a/libraries/wuss/test/tasks/swatches.c +++ b/libraries/wuss/test/tasks/swatches.c @@ -1,6 +1,6 @@ /* wuss/test/tasks/swatches.c -- fill-pattern swatch grid task */ -#ifdef USE_SDL +#ifdef WUSS_APP #include <stdlib.h> #include <string.h> @@ -136,4 +136,4 @@ result_t swatches_handle(wuss_window_t *window, } } -#endif /* USE_SDL */ +#endif /* WUSS_APP */ diff --git a/libraries/wuss/test/tasks/swatches.h b/libraries/wuss/test/tasks/swatches.h index 4dc3f698..692b5ecb 100644 --- a/libraries/wuss/test/tasks/swatches.h +++ b/libraries/wuss/test/tasks/swatches.h @@ -3,7 +3,7 @@ #ifndef TASKS_SWATCHES_H #define TASKS_SWATCHES_H -#ifdef USE_SDL +#ifdef WUSS_APP #include "framebuf/screen.h" #include "wuss/icon.h" @@ -28,6 +28,6 @@ wuss_window_fn_t swatches_handle; result_t swatches_create(wuss_t *wuss, swatches_task_t *task); -#endif /* USE_SDL */ +#endif /* WUSS_APP */ #endif /* TASKS_SWATCHES_H */ diff --git a/libraries/wuss/test/tasks/text.c b/libraries/wuss/test/tasks/text.c index 874fadf7..da1604ee 100644 --- a/libraries/wuss/test/tasks/text.c +++ b/libraries/wuss/test/tasks/text.c @@ -1,6 +1,6 @@ /* wuss/test/tasks/text.c -- static paragraph task */ -#ifdef USE_SDL +#ifdef WUSS_APP #include <stdlib.h> @@ -172,4 +172,4 @@ result_t text_handle(wuss_window_t *window, } } -#endif /* USE_SDL */ +#endif /* WUSS_APP */ diff --git a/libraries/wuss/test/tasks/text.h b/libraries/wuss/test/tasks/text.h index 98e0b7be..3da481c0 100644 --- a/libraries/wuss/test/tasks/text.h +++ b/libraries/wuss/test/tasks/text.h @@ -3,7 +3,7 @@ #ifndef TASKS_TEXT_H #define TASKS_TEXT_H -#ifdef USE_SDL +#ifdef WUSS_APP #include <stdbool.h> @@ -33,6 +33,6 @@ result_t text_create(wuss_t *wuss, bmfont_t *font, text_task_t *task); -#endif /* USE_SDL */ +#endif /* WUSS_APP */ #endif /* TASKS_TEXT_H */ From 2f87040925077706e04b38ca009c029f5475ab35 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Wed, 2 Sep 2026 16:39:28 +0100 Subject: [PATCH 181/287] feat(base): map result codes to strings Add result_string(), a switch over every DPTLib result_t code returning a static human-readable description. New BASE_SOURCES group in CMakeLists for libraries/base/. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- CMakeLists.txt | 4 ++ include/base/result.h | 6 ++ libraries/base/result/result-string.c | 84 +++++++++++++++++++++++++++ 3 files changed, 94 insertions(+) create mode 100644 libraries/base/result/result-string.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 6e5037d9..5d2a9e51 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -131,6 +131,9 @@ endif() # target_sources. The quoting is essential. set_target_properties(DPTLib PROPERTIES PUBLIC_HEADER "${PUBLIC_HEADERS}") +set(BASE_SOURCES + libraries/base/result/result-string.c) + set(DATABASE_SOURCES libraries/databases/digest-db/digest-db.c libraries/databases/filename-db/filename-db.c @@ -427,6 +430,7 @@ endif() set(ALL_SOURCES ${PUBLIC_HEADERS} + ${BASE_SOURCES} ${DATABASE_SOURCES} ${DATASTRUCT_SOURCES} ${FRAMEBUF_SOURCES} diff --git a/include/base/result.h b/include/base/result.h index d8021f31..6515b495 100644 --- a/include/base/result.h +++ b/include/base/result.h @@ -67,4 +67,10 @@ typedef int result_t; /* ----------------------------------------------------------------------- */ +/* Returns a static human-readable string for the given result code, or + * "Unknown error" if unrecognised. Never returns NULL. */ +const char *result_string(result_t err); + +/* ----------------------------------------------------------------------- */ + #endif /* BASE_RESULT_H */ diff --git a/libraries/base/result/result-string.c b/libraries/base/result/result-string.c new file mode 100644 index 00000000..a24343b2 --- /dev/null +++ b/libraries/base/result/result-string.c @@ -0,0 +1,84 @@ +/* result-string.c -- map result_t codes to human-readable strings */ + +#include "base/result.h" + +#include "databases/filename-db.h" +#include "databases/pickle.h" +#include "databases/tag-db.h" +#include "datastruct/atom.h" +#include "datastruct/bitfifo.h" +#include "datastruct/hash.h" +#include "geom/layout.h" +#include "geom/packer.h" +#include "io/stream.h" +#include "wuss/wuss.h" + +const char *result_string(result_t err) +{ + switch (err) + { + case result_OK: return "OK"; + case result_OOM: return "Out of memory"; + case result_FILE_NOT_FOUND: return "File not found"; + case result_BAD_ARG: return "Bad argument"; + case result_BUFFER_OVERFLOW: return "Buffer overflow"; + case result_STOP_WALK: return "Walk cancelled"; + case result_PARSE_ERROR: return "Parse error"; + case result_TOO_BIG: return "Value too big"; + case result_NOT_IMPLEMENTED: return "Not implemented"; + case result_NOT_FOUND: return "Not found"; + case result_EXISTS: return "Already exists"; + case result_CLASHES: return "Key clashes with existing one"; + case result_NULL_ARG: return "NULL argument"; + case result_NOT_SUPPORTED: return "Not supported"; + case result_INCOMPATIBLE: return "Incompatible argument"; + case result_FOPEN_FAILED: return "fopen() failed"; + + case result_STREAM_BAD_SEEK: return "Stream: bad seek"; + case result_STREAM_CANT_SEEK: return "Stream: cannot seek"; + case result_STREAM_UNKNOWN_OP: return "Stream: unknown operation"; + + case result_ATOM_SET_EMPTY: return "Atom: set is empty"; + case result_ATOM_NAME_EXISTS: return "Atom: name already exists"; + case result_ATOM_OUT_OF_RANGE: return "Atom: index out of range"; + + case result_HASH_END: return "Hash: end of iteration"; + case result_HASH_BAD_CONT: return "Hash: invalid continuation value"; + + case result_PICKLE_END: return "Pickle: end of data"; + case result_PICKLE_SKIP: return "Pickle: skip entry"; + case result_PICKLE_INCOMPATIBLE: return "Pickle: incompatible format"; + case result_PICKLE_COULDNT_OPEN_FILE: return "Pickle: could not open file"; + case result_PICKLE_SYNTAX_ERROR: return "Pickle: syntax error"; + + case result_TAGDB_INCOMPATIBLE: return "TagDB: incompatible format"; + case result_TAGDB_COULDNT_OPEN_FILE: return "TagDB: could not open file"; + case result_TAGDB_SYNTAX_ERROR: return "TagDB: syntax error"; + case result_TAGDB_UNKNOWN_ID: return "TagDB: unknown id"; + case result_TAGDB_BUFF_OVERFLOW: return "TagDB: buffer overflow"; + case result_TAGDB_UNKNOWN_TAG: return "TagDB: unknown tag"; + + case result_FILENAMEDB_INCOMPATIBLE: return "FilenameDB: incompatible format"; + case result_FILENAMEDB_COULDNT_OPEN_FILE: return "FilenameDB: could not open file"; + case result_FILENAMEDB_SYNTAX_ERROR: return "FilenameDB: syntax error"; + case result_FILENAMEDB_BUFF_OVERFLOW: return "FilenameDB: buffer overflow"; + + case result_TEST_PASSED: return "Test passed"; + case result_TEST_FAILED: return "Test failed"; + + case result_PACKER_DIDNT_FIT: return "Packer: did not fit"; + case result_PACKER_EMPTY: return "Packer: empty"; + + case result_LAYOUT_BUFFER_FULL: return "Layout: buffer full"; + + case result_BITFIFO_EMPTY: return "BitFIFO: empty"; + case result_BITFIFO_FULL: return "BitFIFO: full"; + case result_BITFIFO_INSUFFICIENT: return "BitFIFO: insufficient bits"; + + case result_WUSS_TOO_SMALL: return "Wuss: dimensions too small"; + case result_WUSS_BAD_COLOUR: return "Wuss: palette index out of range"; + case result_WUSS_BAD_ICON: return "Wuss: malformed icon spec"; + + default: return "Unknown error"; + } +} From 6bf623537c1a3bad2fecec59a5fa02090ec368df Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Wed, 2 Sep 2026 16:43:01 +0100 Subject: [PATCH 182/287] feat(wuss): print result_string on error paths Append the human-readable result_string() to the rc=0x%X log/print sites in the wuss demo app and wuss-test. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- apps/wuss/main.c | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/apps/wuss/main.c b/apps/wuss/main.c index 751e2ed0..1015661f 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -135,7 +135,8 @@ static result_t spawn_image(void) logf_info("wuss: image task loading \"%s\" + \"%s\"", buf, ninepatch); rc = image_create(g.wuss, buf, ninepatch, t); if (rc != result_OK) - logf_error("wuss: image_create(\"%s\") failed, rc=0x%X", buf, rc); + logf_error("wuss: image_create(\"%s\") failed, rc=0x%X (%s)", buf, rc, + result_string(rc)); if (rc != result_OK || t->window == NULL) { free(t); return rc; } return result_OK; } @@ -197,8 +198,8 @@ static result_t spawn_icons(void) if (t == NULL) return result_OOM; rc = icons_create(g.wuss, g.daydream_font, g.resources, t); if (rc != result_OK) - logf_error("wuss: icons_create (resources \"%s\") failed, rc=0x%X", - g.resources, rc); + logf_error("wuss: icons_create (resources \"%s\") failed, rc=0x%X (%s)", + g.resources, rc, result_string(rc)); if (rc != result_OK || t->window == NULL) { free(t); return rc; } return result_OK; } @@ -220,8 +221,8 @@ static result_t spawn_porter_duff(void) if (t == NULL) return result_OOM; rc = porter_duff_create(g.wuss, g.palette, g.daydream_font, g.resources, t); if (rc != result_OK) - logf_error("wuss: porter_duff_create (resources \"%s\") failed, rc=0x%X", - g.resources, rc); + logf_error("wuss: porter_duff_create (resources \"%s\") failed, " + "rc=0x%X (%s)", g.resources, rc, result_string(rc)); if (rc != result_OK || t->window == NULL) { free(t); return rc; } return result_OK; } @@ -525,7 +526,8 @@ static result_t run_wuss(const char *resources) rc = bmfont_create(filename, &font); if (rc != result_OK) { - logf_error("wuss: bmfont_create(\"%s\") failed, rc=0x%X", filename, rc); + logf_error("wuss: bmfont_create(\"%s\") failed, rc=0x%X (%s)", filename, + rc, result_string(rc)); goto Failure; } @@ -535,7 +537,8 @@ static result_t run_wuss(const char *resources) rc = bmfont_create(filename, &daydream_font); if (rc != result_OK) { - logf_error("wuss: bmfont_create(\"%s\") failed, rc=0x%X", filename, rc); + logf_error("wuss: bmfont_create(\"%s\") failed, rc=0x%X (%s)", filename, + rc, result_string(rc)); goto Failure; } @@ -673,7 +676,7 @@ static result_t run_wuss(const char *resources) Failure: - printf("run_wuss: failed (rc=0x%X)\n", rc); + printf("run_wuss: failed (rc=0x%X: %s)\n", rc, result_string(rc)); return result_TEST_FAILED; } From 7152c966e1b690a2079b7ef39aea92dc25a7b8ff Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Wed, 2 Sep 2026 17:02:35 +0100 Subject: [PATCH 183/287] fix(wuss): correct RISC OS screen-height read and hide OS cursors VDU variable 11 (YWindLimit) is already the top pixel row, not OS units. The extra >> YEIG halved the height, so a 640x480 mode read as 640x320 and the size check bailed with result_TEST_FAILED. Also clamp the framebuffer to the requested size when the mode comes out larger, and report the failing SWI / geometry on the error paths. Hide the text caret (VDU 23,1,0) on open and restore it (VDU 23,1,1) on close so it does not show through wuss's full-screen redraw. Add rc logging around frontend_open / bitmap_init / wuss_create / wuss_task_create in the demo to make the next failure self-locating. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- apps/wuss/frontend-riscos.c | 40 +++++++++++++++++++++++++++++++++---- apps/wuss/main.c | 5 +++++ 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/apps/wuss/frontend-riscos.c b/apps/wuss/frontend-riscos.c index 7dba0644..f5fa332f 100644 --- a/apps/wuss/frontend-riscos.c +++ b/apps/wuss/frontend-riscos.c @@ -6,6 +6,8 @@ #include <stdio.h> #include <stdlib.h> +#include "kernel.h" + #ifdef FORTIFY #include "fortify/fortify.h" #endif @@ -95,6 +97,9 @@ result_t wuss_frontend_open(int width, int vars[6]; /* 5 indices + a -1 terminator */ int vals[5]; _kernel_oserror *err; + char msg[160]; /* deferred so it prints after the mode restore */ + + msg[0] = '\0'; fe = calloc(1, sizeof(*fe)); if (fe == NULL) @@ -103,12 +108,19 @@ result_t wuss_frontend_open(int width, /* remember the mode we came in on */ err = _swix(OS_ScreenMode, _IN(0) | _OUT(1), 1, &fe->entry_mode); if (err != NULL) + { + snprintf(msg, sizeof(msg), "OS_ScreenMode read: %s", err->errmess); goto failure; + } /* switch to the 16-colour demo mode */ err = _swix(OS_ScreenMode, _INR(0,1), 0, WUSS_RISCOS_MODE); if (err != NULL) + { + snprintf(msg, sizeof(msg), "OS_ScreenMode set mode %d: %s", + WUSS_RISCOS_MODE, err->errmess); goto failure; + } /* read back the geometry we actually got */ vars[0] = VDUVAR_XEIG; @@ -119,13 +131,16 @@ result_t wuss_frontend_open(int width, vars[5] = -1; /* OS_ReadVduVariables scans until it hits -1 */ err = _swix(OS_ReadVduVariables, _INR(0,1), vars, vals); if (err != NULL) + { + snprintf(msg, sizeof(msg), "OS_ReadVduVariables: %s", err->errmess); goto failure_restore; + } fe->xeig = vals[0]; fe->yeig = vals[1]; fe->rowbytes = vals[2]; - /* YWIND_LIMIT is max-Y in OS units; >> YEIG turns it into pixel rows */ - fe->scr_height = (vals[3] >> fe->yeig) + 1; + /* VDU var 11 (YWindLimit) is the top pixel row, in pixels not OS units */ + fe->scr_height = vals[3] + 1; fe->screen_base = (void *) vals[4]; fe->scr_width = fe->rowbytes * 2; /* 4bpp: 2 pixels per byte */ fe->last_buttons = 0; @@ -134,13 +149,25 @@ result_t wuss_frontend_open(int width, * would draw off-screen, so bail rather than corrupt memory */ if (fe->scr_width < width || fe->scr_height < height) { - fprintf(stderr, "wuss: mode %d gave %dx%d, need %dx%d\n", - WUSS_RISCOS_MODE, fe->scr_width, fe->scr_height, width, height); + snprintf(msg, sizeof(msg), + "mode %d gave %dx%d (xeig=%d yeig=%d linelen=%d ywindlim=%d)," + " need %dx%d", + WUSS_RISCOS_MODE, fe->scr_width, fe->scr_height, + vals[0], vals[1], vals[2], vals[3], width, height); goto failure_restore; } + /* the mode may be bigger than the demo; hand wuss only the area it asked + * for so a redraw can never run off the end of screen memory */ + fe->scr_width = width; + fe->scr_height = height; + set_hw_palette(palette, npalette); + /* hide the text cursor: VDU 23,1,0 -- wuss draws over the whole screen and + * a blinking caret in the corner would show through */ + _swix(OS_WriteN, _INR(0,1), "\x17\x01\x00\x00\x00\x00\x00\x00\x00\x00", 10); + /* OS_ScreenMode leaves the pointer off. Turn on pointer 1 with the default * arrow shape (*Pointer 1 == OS_Byte 106, 1) and confine it to the screen. * wuss draws no cursor of its own, so this is the pointer the user sees. */ @@ -159,6 +186,9 @@ result_t wuss_frontend_open(int width, failure: + /* mode is restored to text by now, so this reaches the screen */ + if (msg[0] != '\0') + fprintf(stderr, "wuss: frontend_open: %s\n", msg); free(fe); return result_TEST_FAILED; } @@ -300,6 +330,8 @@ void wuss_frontend_close(wuss_frontend_t *fe) return; _swix(OS_Byte, _INR(0,1), 106, 0); /* *Pointer 0: turn the pointer off */ + _swix(OS_WriteN, _INR(0,1), /* VDU 23,1,1: text cursor back on */ + "\x17\x01\x01\x00\x00\x00\x00\x00\x00\x00", 10); _swix(OS_ScreenMode, _INR(0,1), 0, fe->entry_mode); free(fe); } diff --git a/apps/wuss/main.c b/apps/wuss/main.c index 1015661f..b22ed0f7 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -544,11 +544,13 @@ static result_t run_wuss(const char *resources) rc = wuss_frontend_open(scr_width, scr_height, palette, NELEMS(palette), &pixels, &rowbytes, &fmt, &frontend); + logf_info("wuss: wuss_frontend_open -> rc=0x%X (%s)", rc, result_string(rc)); if (rc != result_OK) goto Failure; rc = bitmap_init(&bm, SIZE2D(scr_width, scr_height), fmt, rowbytes, palette, pixels); + logf_info("wuss: bitmap_init -> rc=0x%X (%s)", rc, result_string(rc)); if (rc != result_OK) goto Failure; @@ -563,6 +565,7 @@ static result_t run_wuss(const char *resources) rc = wuss_create(&scr, font, palette, NELEMS(palette), &config, NULL, &wuss); + logf_info("wuss: wuss_create -> rc=0x%X (%s)", rc, result_string(rc)); if (rc != result_OK) goto Failure; } @@ -580,6 +583,8 @@ static result_t run_wuss(const char *resources) desc.task_data = NULL; desc.name = "menu"; rc = wuss_task_create(wuss, &desc, &g.menu_task); + logf_info("wuss: wuss_task_create(menu) -> rc=0x%X (%s)", rc, + result_string(rc)); if (rc != result_OK) goto Failure; } From 2637ce797e1be90527171a9b698b4d1a66f61344 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Wed, 2 Sep 2026 17:22:03 +0100 Subject: [PATCH 184/287] refactor(wuss): drop the daydream font from the demo The demo now runs on the single digits font it already loads; the daydream.png load, its local, and its bmfont_destroy are gone. The g.daydream_font field keeps its name so the text / icons / porter-duff task signatures are untouched. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- apps/wuss/main.c | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/apps/wuss/main.c b/apps/wuss/main.c index b22ed0f7..cd6b60e9 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -497,7 +497,6 @@ static result_t run_wuss(const char *resources) const char *leafname; const char *filename; bmfont_t *font; - bmfont_t *daydream_font; void *pixels; int rowbytes; pixelfmt_t fmt; @@ -531,17 +530,6 @@ static result_t run_wuss(const char *resources) goto Failure; } - leafname = path_join_leafname("daydream", "png"); - filename = path_join_filename(resources, 3, "resources", "bmfonts", leafname); - logf_info("wuss: loading font \"%s\"", filename); - rc = bmfont_create(filename, &daydream_font); - if (rc != result_OK) - { - logf_error("wuss: bmfont_create(\"%s\") failed, rc=0x%X (%s)", filename, - rc, result_string(rc)); - goto Failure; - } - rc = wuss_frontend_open(scr_width, scr_height, palette, NELEMS(palette), &pixels, &rowbytes, &fmt, &frontend); logf_info("wuss: wuss_frontend_open -> rc=0x%X (%s)", rc, result_string(rc)); @@ -574,7 +562,7 @@ static result_t run_wuss(const char *resources) g.palette = palette; g.npalette = NELEMS(palette); g.resources = resources; - g.daydream_font = daydream_font; + g.daydream_font = font; /* the demo runs on the one font it loads */ { wuss_task_desc_t desc; @@ -672,7 +660,6 @@ static result_t run_wuss(const char *resources) wuss_destroy(wuss); /* also sweeps g.menu_task */ bmfont_destroy(font); - bmfont_destroy(daydream_font); wuss_frontend_close(frontend); From b935153714a89b8b4e255ffa5a4a234aea8d1ac4 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Wed, 2 Sep 2026 17:41:08 +0100 Subject: [PATCH 185/287] feat(wuss): add a font picker menu to the Chars task A MENU click on the glyph grid opens a pop-up listing seven bitmap fonts from resources/bmfonts; picking one loads it on demand, swaps it into the grid and resizes the window to the new cell metrics. The current font carries a tick. Fonts are freed on QUIT. chars_create now takes the resources root so the picker can find the font PNGs; the caller in main.c passes g.resources. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- apps/wuss/main.c | 2 +- libraries/wuss/test/tasks/chars.c | 170 ++++++++++++++++++++++++++---- libraries/wuss/test/tasks/chars.h | 22 +++- 3 files changed, 169 insertions(+), 25 deletions(-) diff --git a/apps/wuss/main.c b/apps/wuss/main.c index cd6b60e9..2c50c17f 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -99,7 +99,7 @@ static result_t spawn_chars(void) chars_task_t *t = calloc(1, sizeof(*t)); result_t rc; if (t == NULL) return result_OOM; - rc = chars_create(g.wuss, t); + rc = chars_create(g.wuss, g.resources, t); if (rc != result_OK || t->window == NULL) { free(t); return rc; } return result_OK; } diff --git a/libraries/wuss/test/tasks/chars.c b/libraries/wuss/test/tasks/chars.c index af403b5e..51054e05 100644 --- a/libraries/wuss/test/tasks/chars.c +++ b/libraries/wuss/test/tasks/chars.c @@ -1,8 +1,9 @@ -/* wuss/test/tasks/chars.c -- system font glyph grid task */ +/* wuss/test/tasks/chars.c -- bitmap font glyph grid task with a font picker */ #ifdef WUSS_APP #include <stdlib.h> +#include <string.h> #include <limits.h> @@ -14,6 +15,8 @@ #include "framebuf/palettes.h" #include "geom/box.h" #include "geom/point.h" +#include "io/path.h" +#include "wuss/menu.h" #include "chars.h" @@ -21,14 +24,118 @@ #define CHARS_ROWS 8 #define CHARS_PAD 2 -result_t chars_create(wuss_t *wuss, chars_task_t *task) +/* the fonts the picker offers: menu label and the leafname under + * resources/bmfonts. Index order is the menu order and the fonts[] slot. */ +static const struct +{ + const char *label; + const char *leaf; +} +chars_fonts[CHARS_NFONTS] = +{ + { "Digits", "digits" }, + { "Daydream", "daydream" }, + { "Glider Rider", "gliderrider" }, + { "Henry", "henry" }, + { "MS Sans Serif", "ms-sans-serif"}, + { "Tall", "tall" }, + { "Tiny", "tiny" } +}; + +/* rebuilt before every open so the tick tracks task->current */ +static wuss_menu_item_t chars_menu_items[CHARS_NFONTS]; +static const wuss_menu_t chars_menu = +{ + "Font", chars_menu_items, CHARS_NFONTS +}; + +/* keep the resources root the task was created with; the picker needs it to + * load a font on demand and chars_create does not stash it elsewhere. + * ponytail: one demo, one instance at a time -- a file-scope copy is fine. */ +static char chars_resources[256]; + +/* ----------------------------------------------------------------------- */ + +/* content size for the grid at the given font's cell metrics */ +static size2d_t chars_window_size(bmfont_t *font) +{ + int fw, fh; + + bmfont_get_info(font, &fw, &fh); + return SIZE2D((fw + CHARS_PAD * 2) * CHARS_COLS, + (fh + CHARS_PAD * 2) * CHARS_ROWS); +} + +/* load fonts[idx] if not already in hand; returns it or NULL on failure */ +static bmfont_t *chars_load_font(chars_task_t *task, int idx) +{ + const char *leaf; + const char *filename; + bmfont_t *font; + result_t rc; + + if (task->fonts[idx] != NULL) + return task->fonts[idx]; + + leaf = path_join_leafname(chars_fonts[idx].leaf, "png"); + filename = path_join_filename(chars_resources, 3, "resources", "bmfonts", + leaf); + + rc = bmfont_create(filename, &font); + if (rc != result_OK) + return NULL; + + task->fonts[idx] = font; + return font; +} + +/* switch the grid to fonts[idx], resizing the window to suit */ +static result_t chars_set_font(chars_task_t *task, int idx) +{ + bmfont_t *font; + + if (idx < 0 || idx >= CHARS_NFONTS || idx == task->current) + return result_OK; + + font = chars_load_font(task, idx); + if (font == NULL) + return result_OK; /* leave the current font in place */ + + task->font = font; + task->current = idx; + + wuss_window_resize(task->window, chars_window_size(font)); + wuss_window_invalidate_all(task->window); + return result_OK; +} + +static result_t chars_open_menu(chars_task_t *task) +{ + int i; + + for (i = 0; i < CHARS_NFONTS; i++) + { + chars_menu_items[i].text = chars_fonts[i].label; + chars_menu_items[i].flags = (i == task->current) ? wuss_MENU_ITEM_TICKED + : wuss_MENU_ITEM_NONE; + chars_menu_items[i].submenu = NULL; + chars_menu_items[i].window = NULL; + } + + return wuss_menu_open(task->delegate, &chars_menu, + wuss_get_pointer(task->wuss), NULL); +} + +/* ----------------------------------------------------------------------- */ + +result_t chars_create(wuss_t *wuss, + const char *resources, + chars_task_t *task) { wuss_task_t *delegate; wuss_task_desc_t delegate_desc; result_t rc; - size2d_t sz; - bmfont_t *font; - int font_width, font_height, cell_w, cell_h; + bmfont_t *font; font = wuss_get_font(wuss); if (font == NULL) @@ -37,13 +144,14 @@ result_t chars_create(wuss_t *wuss, chars_task_t *task) return result_OK; } - bmfont_get_info(font, &font_width, &font_height); - cell_w = font_width + CHARS_PAD * 2; - cell_h = font_height + CHARS_PAD * 2; + strncpy(chars_resources, resources, sizeof(chars_resources) - 1); + chars_resources[sizeof(chars_resources) - 1] = '\0'; - task->font = font; - task->fg = colour_rgb(0x00, 0x00, 0x00); - task->bg = colour_rgb(0xFF, 0xFF, 0xFF); + task->wuss = wuss; + task->font = font; + task->current = -1; /* the wuss system font is not one of chars_fonts[] */ + task->fg = colour_rgb(0x00, 0x00, 0x00); + task->bg = colour_rgb(0xFF, 0xFF, 0xFF); /* chars_redraw paints every cell itself */ delegate_desc.handle = chars_handle; @@ -53,17 +161,17 @@ result_t chars_create(wuss_t *wuss, chars_task_t *task) if (rc != result_OK) return rc; wuss_task_set_autoclose(delegate, 1); - sz = SIZE2D(cell_w * CHARS_COLS, cell_h * CHARS_ROWS); + task->delegate = delegate; return wuss_window_create_placed(delegate, - sz, + chars_window_size(font), "Chars", wuss_WINDOW_NO_RESIZE | wuss_WINDOW_NO_TOGGLE_SIZE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), - sz, + chars_window_size(font), SIZE2D(0, 0), &task->window); } @@ -125,16 +233,40 @@ result_t chars_handle(wuss_window_t *window, const wuss_event_t *event, void *task_data) { - if (event->kind == wuss_EVENT_QUIT) + chars_task_t *cc = task_data; + + NOT_USED(window); + + switch (event->kind) { - free(task_data); /* calloc'd per instance by the spawner */ + case wuss_EVENT_QUIT: + { + int i; + + for (i = 0; i < CHARS_NFONTS; i++) + if (cc->fonts[i] != NULL) + bmfont_destroy(cc->fonts[i]); + free(cc); /* calloc'd per instance by the spawner */ + } + return result_OK; + + case wuss_EVENT_MOUSE: + if (event->data.mouse.action == wuss_MOUSE_DOWN && + (event->data.mouse.button & wuss_BUTTON_MENU)) + return chars_open_menu(cc); return result_OK; - } - if (event->kind != wuss_EVENT_REDRAW) + case wuss_EVENT_MENU_SELECT: + if (event->data.menu_select.menu == &chars_menu) + return chars_set_font(cc, event->data.menu_select.index); return result_OK; - return chars_redraw(event, task_data); + case wuss_EVENT_REDRAW: + return chars_redraw(event, task_data); + + default: + return result_OK; + } } #endif /* WUSS_APP */ diff --git a/libraries/wuss/test/tasks/chars.h b/libraries/wuss/test/tasks/chars.h index 5ee32c87..9dd7b24a 100644 --- a/libraries/wuss/test/tasks/chars.h +++ b/libraries/wuss/test/tasks/chars.h @@ -7,14 +7,23 @@ #include "framebuf/bmfont.h" #include "framebuf/colour.h" +#include "wuss/task.h" #include "wuss/window.h" -/* displays every glyph (0-255) of the wuss system font as a 32x8 grid, - * so the whole font can be eyeballed at a glance */ +/* number of fonts offered by the Chars font-picker menu */ +#define CHARS_NFONTS 7 + +/* displays every glyph (0-255) of a bitmap font as a 32x8 grid, so the whole + * font can be eyeballed at a glance. A MENU click on the window opens a + * picker that swaps the font in place. */ typedef struct chars_task { wuss_window_t *window; - bmfont_t *font; + wuss_task_t *delegate; /* the wuss task backing this window */ + wuss_t *wuss; /* for wuss_get_pointer when opening the menu */ + bmfont_t *font; /* currently shown; == fonts[current] */ + bmfont_t *fonts[CHARS_NFONTS]; /* lazily loaded, NULL until first pick */ + int current; /* index into fonts[] */ colour_t fg, bg; } chars_task_t; @@ -22,8 +31,11 @@ chars_task_t; wuss_window_fn_t chars_handle; /* create the glyph-grid window against the given wuss instance; does - * nothing and returns result_OK if wuss has no system font */ -result_t chars_create(wuss_t *wuss, chars_task_t *task); + * nothing and returns result_OK if wuss has no system font. resources is the + * root the font picker loads bmfonts from (resources/bmfonts/<name>.png). */ +result_t chars_create(wuss_t *wuss, + const char *resources, + chars_task_t *task); #endif /* WUSS_APP */ From c7e2748627a523b1c4d00d744899ae31b88aab02 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Wed, 2 Sep 2026 18:09:50 +0100 Subject: [PATCH 186/287] feat(wuss): flash the picked menu row before delivering MENU_SELECT A pick now toggles the row's highlight a few times off wuss_EVENT_IDLE before the chain closes and MENU_SELECT goes out, for the RISC OS pick feel. State lives on the picked menu level (struct wuss__menu's new flash sub-struct); the internal menu task carries the wuss_t as its task_data so the IDLE handler can reach the chain. ADJUST keeps the chain open (keep_open); a fast sequence of ADJUST re-picks finishes the in-progress flash first, so no click is dropped and no row is left inverted. A flash always ends with the row un-highlit -- a later MOUSE_MOVE re-highlights it if the pointer is still there. Adds a white-box wuss-test case that drives picks through the icon layer and checks the deferred delivery, teardown, and the rapid-ADJUST path. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- libraries/wuss/menu.h | 17 ++++ libraries/wuss/menu/menu.c | 145 ++++++++++++++++++++++++---- libraries/wuss/test/wuss-test.c | 162 +++++++++++++++++++++++++++++++- 3 files changed, 302 insertions(+), 22 deletions(-) diff --git a/libraries/wuss/menu.h b/libraries/wuss/menu.h index 6d435ab8..482b392e 100644 --- a/libraries/wuss/menu.h +++ b/libraries/wuss/menu.h @@ -33,6 +33,23 @@ struct wuss__menu int borrowed; /* 1: `window` is a caller-owned window * shown in place of a submenu -- hide it * rather than close it on teardown */ + + /* SELECT-pick flash: the picked row's highlight is toggled a few times off + * wuss_EVENT_IDLE before the chain closes and MENU_SELECT is delivered. + * Set on the picked level; frames == 0 means no flash in progress. The + * owner/menu/index/button are captured because the chain (and this node) + * is freed before the deferred notification goes out. */ + struct + { + int frames; /* IDLE frames left in the flash */ + int index; /* item index being flashed */ + int keep_open; /* 1 (ADJUST pick): flash but do not tear + * the chain down before MENU_SELECT */ + wuss_task_t *owner; /* captured MENU_SELECT target */ + const wuss_menu_t *menu; /* captured menu for the notification */ + wuss_button_t button; /* captured release button */ + } + flash; }; /* Called from wuss_mouse_click on a MOUSE_DOWN before the window hit-test: if a diff --git a/libraries/wuss/menu/menu.c b/libraries/wuss/menu/menu.c index 78e326e9..f3924def 100644 --- a/libraries/wuss/menu/menu.c +++ b/libraries/wuss/menu/menu.c @@ -32,6 +32,12 @@ #define WUSS_MENU_TEXT_PAD 6 #define WUSS_MENU_SUBMENU_OVERLAP 2 /* px a submenu overlaps its parent's right edge */ +/* SELECT-pick flash: toggle the row highlight every WUSS_MENU_FLASH_PERIOD + * IDLE frames, for WUSS_MENU_FLASH_FRAMES frames total. At ~60 Hz that is + * three quick on/off blinks in about 0.2s -- the RISC OS feel. */ +#define WUSS_MENU_FLASH_FRAMES 12 +#define WUSS_MENU_FLASH_PERIOD 2 + /* ----------------------------------------------------------------------- */ static result_t wuss__menu_handle(wuss_window_t *window, @@ -45,6 +51,9 @@ static result_t wuss__menu_spawn(wuss_t *wuss, struct wuss__menu *parent, struct wuss__menu **out); +static void wuss__menu_flash_step(struct wuss__menu *self); +static void wuss__menu_flash_finish(struct wuss__menu *self); + /* The internal task that owns every borderless menu window, created on the * first wuss_menu_open of a session. */ static wuss_task_t *wuss__menu_task(wuss_t *wuss) @@ -55,7 +64,8 @@ static wuss_task_t *wuss__menu_task(wuss_t *wuss) return wuss->menu_task; desc.handle = wuss__menu_handle; - desc.task_data = NULL; /* per-window node is found by walking the chain */ + desc.task_data = wuss; /* the ICON path walks the chain by window; the IDLE + * path (window == NULL) needs the wuss_t directly */ desc.name = "wuss:menu"; if (wuss_task_create(wuss, &desc, &wuss->menu_task) != result_OK) @@ -148,6 +158,7 @@ static result_t wuss__menu_open_window(struct wuss__menu *self, int index) node->child = NULL; node->open_index = -1; node->borrowed = 1; + memset(&node->flash, 0, sizeof(node->flash)); at = wuss__submenu_anchor(self, self->icons[index]); wuss_window_move(win, at); @@ -177,7 +188,21 @@ static result_t wuss__menu_handle(wuss_window_t *window, int index; int i; - NOT_USED(task_data); + /* IDLE arrives with window == NULL; task_data is the wuss_t (see + * wuss__menu_task). Drive any running SELECT-pick flash off it. */ + if (event->kind == wuss_EVENT_IDLE) + { + struct wuss__menu *node; + + wuss = task_data; + for (node = wuss->menu_chain; node != NULL; node = node->child) + if (!node->borrowed && node->flash.frames > 0) + { + wuss__menu_flash_step(node); + break; /* node may be freed; the chain is gone if the flash ended */ + } + return result_OK; + } if (event->kind != wuss_EVENT_ICON) return result_OK; @@ -244,41 +269,120 @@ static result_t wuss__menu_handle(wuss_window_t *window, if (event->data.icon.action == wuss_MOUSE_UP) { wuss_button_t button; - struct wuss__menu *root; wuss_task_t *owner; const wuss_menu_t *menu; - wuss_event_t sel; button = event->data.icon.button; if (item->submenu != NULL || item->window != NULL) return result_OK; /* a submenu/window row opens on hover, not a pick */ - /* Capture what the notification needs, then tear the chain down *before* - * delivering it: the task's MENU_SELECT handler may itself open a new - * menu (freeing this one), so `self` must not be touched afterwards. */ - root = self; - while (root->parent != NULL) - root = root->parent; - owner = root->owner; + /* owner (the task that opened the chain) is copied onto every level */ + owner = self->owner; menu = self->menu; - if (!(button & wuss_BUTTON_ADJUST)) + /* Flash the picked row, then deliver MENU_SELECT from the IDLE handler. + * SELECT tears the whole chain down first; ADJUST keeps it open so the + * row can be re-picked. Capture everything the notification needs now -- + * a SELECT flash frees `self` when it ends. A re-pick while a flash on + * this level is still running (fast ADJUST clicks) finishes that flash + * first, so its row is cleared and its MENU_SELECT is not lost. */ { - root->wuss->menu_chain = NULL; - wuss__menu_close_from(root); + wuss_icon_t *row; + + if (self->flash.frames > 0) + wuss__menu_flash_finish(self); /* self survives: previous pick was + * ADJUST, or a SELECT would have been + * caught by the frames == 0 guard */ + + row = self->icons[index]; + + self->flash.frames = WUSS_MENU_FLASH_FRAMES; + self->flash.index = index; + self->flash.owner = owner; + self->flash.menu = menu; + self->flash.button = button; + self->flash.keep_open = (button & wuss_BUTTON_ADJUST) != 0; + + /* first blink now: the row is already highlit under the pointer, so + * drop the highlight this frame for an immediate visible change */ + wuss__icon_set_state(row, wuss_ICON_STATE_HOVERED, 0); + wuss__icon_invalidate(row); } - - sel.kind = wuss_EVENT_MENU_SELECT; - sel.data.menu_select.menu = menu; - sel.data.menu_select.index = index; - sel.data.menu_select.button = button; - (void) wuss__deliver(owner, NULL, &sel); + return result_OK; } return result_OK; } +/* End the pick flash on `self` now: leave the flashed row un-highlit (a later + * MOUSE_MOVE re-highlights it if the pointer is still over it), then deliver + * the MENU_SELECT the flash stands in for -- tearing the chain down first + * unless the pick was an ADJUST (keep_open). On a non-keep_open return `self` + * has been freed. Callers guard against calling this with no flash armed. */ +static void wuss__menu_flash_finish(struct wuss__menu *self) +{ + struct wuss__menu *root; + wuss_event_t sel; + wuss_task_t *owner = self->flash.owner; + const wuss_menu_t *menu = self->flash.menu; + int index = self->flash.index; + wuss_button_t button = self->flash.button; + int keep_open = self->flash.keep_open; + + self->flash.frames = 0; + + wuss__icon_set_state(self->icons[index], wuss_ICON_STATE_HOVERED, 0); + wuss__icon_invalidate(self->icons[index]); + + if (!keep_open) + { + root = self; + while (root->parent != NULL) + root = root->parent; + + root->wuss->menu_chain = NULL; + wuss__menu_close_from(root); /* frees `self` */ + } + + sel.kind = wuss_EVENT_MENU_SELECT; + sel.data.menu_select.menu = menu; + sel.data.menu_select.index = index; + sel.data.menu_select.button = button; + (void) wuss__deliver(owner, NULL, &sel); +} + +/* Advance a running pick flash by one IDLE frame; hand off to + * wuss__menu_flash_finish when it runs out. `self` must be a real + * (non-borrowed) level. */ +static void wuss__menu_flash_step(struct wuss__menu *self) +{ + wuss_icon_t *row; + + if (self->flash.frames <= 0) + return; + + row = self->icons[self->flash.index]; + + self->flash.frames--; + + if (self->flash.frames == 0) + { + wuss__menu_flash_finish(self); /* delivers MENU_SELECT; may free `self` */ + return; + } + + /* toggle the highlight at each period boundary; the pick frame already did + * the first (off) blink, so phase here starts the row coming back on */ + if (self->flash.frames % WUSS_MENU_FLASH_PERIOD == 0) + { + int on = ((self->flash.frames / WUSS_MENU_FLASH_PERIOD) & 1) == 0; + + wuss__icon_set_state(row, wuss_ICON_STATE_HOVERED, on); + wuss__icon_invalidate(row); + } +} + /* ----------------------------------------------------------------------- */ /* Measure the menu, create its borderless window and one MENU_ENTRY icon per @@ -425,6 +529,7 @@ static result_t wuss__menu_spawn(wuss_t *wuss, node->child = NULL; node->open_index = -1; node->borrowed = 0; + memset(&node->flash, 0, sizeof(node->flash)); /* One MENU_ENTRY icon per item, in item order, preceded by an inert * wuss_ICON_TYPE_RULE icon for each dashed item. specs[] therefore holds diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 11c3fb37..feeaf550 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -26,6 +26,13 @@ #include "wuss/menu.h" #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) +#include "../impl.h" +#include "../icon.h" +#endif + #include "test/all-tests.h" /* ----------------------------------------------------------------------- */ @@ -227,6 +234,36 @@ static void reap_test_tasks(void) /* ----------------------------------------------------------------------- */ +#if defined(WUSS_MENUS) && defined(WUSS_ICONS) +/* Click menu row `row` of the open chain level `level` with `button`: a + * MOUSE_DOWN then MOUSE_UP at the row's on-screen centre, exactly as the + * real event pump would deliver them. */ +static void flash_pick_row(wuss_t *wuss, + struct wuss__menu *level, + int row, + wuss_button_t button) +{ + 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 = (screen_box.x0 + screen_box.x1) / 2; + at.y = (screen_box.y0 + screen_box.y1) / 2; + + wuss_mouse_click(wuss, at, button, wuss_MOUSE_DOWN, NULL); + wuss_mouse_click(wuss, at, button, wuss_MOUSE_UP, NULL); +} +#endif + +/* ----------------------------------------------------------------------- */ + /* Total area covered by the dirty list, counting overlapped pixels once. * Summing each region's area instead would double-count wherever two * invalidations overlap, which they legitimately do. */ @@ -281,7 +318,9 @@ result_t wuss_test(const char *resources) int width, height; const colour_t custom_palette[2] = { 0, 0 }; - NOT_USED(resources); +#if !(defined(WUSS_MENUS) && defined(WUSS_ICONS)) + NOT_USED(resources); /* only the menu-flash test reads it */ +#endif rowbytes = 200 * 4; pixels = malloc(rowbytes * 200); @@ -3468,7 +3507,126 @@ result_t wuss_test(const char *resources) return result_TEST_FAILED; MenuOK: ; } -#endif + +#ifdef WUSS_ICONS + printf("test: menu pick flashes then delivers MENU_SELECT; fast ADJUST " + "re-picks do not lose a click or leave a row inverted\n"); + { + static const wuss_menu_item_t flash_items[] = + { + { "One", wuss_MENU_ITEM_NONE, NULL }, + { "Two", wuss_MENU_ITEM_NONE, NULL }, + { "Three", wuss_MENU_ITEM_NONE, NULL } + }; + static const wuss_menu_t flash_menu = + { + "Pick", flash_items, NELEMS(flash_items) + }; + + const char *fontfile; + bmfont_t *font; + screen_t fscr; + bitmap_t fbm; + void *fpixels; + wuss_t *fwuss; + test_task_t ftc; + wuss_task_t *fowner; + struct wuss__menu *chain; + int i; + + /* a menu needs a font for its row metrics; the core wuss above was made + * without one */ + 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: flash test could not load %s\n", fontfile); + goto Failure; + } + + fpixels = malloc((size_t) rowbytes * 200); + if (fpixels == NULL) { rc = result_OOM; goto FlashFail; } + rc = bitmap_init(&fbm, SIZE2D(200, 200), pixelfmt_bgrx8888, rowbytes, + NULL, fpixels); + if (rc != result_OK) goto FlashFailFree; + screen_for_bitmap(&fscr, &fbm); + + rc = wuss_create(&fscr, font, NULL, 0, NULL, NULL, &fwuss); + if (rc != result_OK) goto FlashFailFree; + + memset(&ftc, 0, sizeof(ftc)); + fowner = mk_task(fwuss, test_handle, &ftc); + if (fowner == NULL) { rc = result_OOM; goto FlashDestroy; } + + /* --- a plain SELECT pick: flash runs, then one MENU_SELECT --- */ + rc = wuss_menu_open(fowner, &flash_menu, POINT(40, 40), NULL); + if (rc != result_OK) goto FlashDestroy; + + chain = fwuss->menu_chain; + if (chain == NULL || chain->menu != &flash_menu) goto FlashCheckFail; + + flash_pick_row(fwuss, chain, 1, wuss_BUTTON_SELECT); + + if (ftc.menu_select_count != 0) goto FlashCheckFail; /* deferred, not yet */ + if (chain->flash.frames <= 0) goto FlashCheckFail; /* flash was armed */ + + for (i = 0; i < 64; i++) /* longer than any plausible flash */ + wuss_idle(fwuss); + + if (ftc.menu_select_count != 1) goto FlashCheckFail; + if (ftc.last_menu_index != 1) goto FlashCheckFail; + if (fwuss->menu_chain != NULL) goto FlashCheckFail; /* SELECT tore it down */ + + /* --- fast ADJUST re-picks: row 0, then row 2 mid-flash --- */ + rc = wuss_menu_open(fowner, &flash_menu, POINT(40, 40), NULL); + if (rc != result_OK) goto FlashDestroy; + chain = fwuss->menu_chain; + ftc.menu_select_count = 0; + + flash_pick_row(fwuss, chain, 0, wuss_BUTTON_ADJUST); + wuss_idle(fwuss); + wuss_idle(fwuss); /* a couple of flash frames, nowhere near done */ + flash_pick_row(fwuss, chain, 2, wuss_BUTTON_ADJUST); + + /* the pre-empted row 0 pick must have been delivered, not dropped */ + if (ftc.menu_select_count != 1) goto FlashCheckFail; + if (ftc.last_menu_index != 0) goto FlashCheckFail; + /* and row 0 must not be left highlit */ + if (wuss__icon_hovered(chain->icons[0])) goto FlashCheckFail; + + for (i = 0; i < 64; i++) /* longer than any plausible flash */ + wuss_idle(fwuss); + + if (ftc.menu_select_count != 2) goto FlashCheckFail; + if (ftc.last_menu_index != 2) goto FlashCheckFail; + /* ADJUST keeps the chain open, but no row may be left inverted */ + if (fwuss->menu_chain == NULL) goto FlashCheckFail; + for (i = 0; i < flash_menu.nitems; i++) + if (wuss__icon_hovered(fwuss->menu_chain->icons[i])) goto FlashCheckFail; + + rc = result_OK; + goto FlashDestroy; + +FlashCheckFail: + printf("wuss_test: menu flash check failed " + "(select_count=%d last_index=%d chain=%p)\n", + ftc.menu_select_count, ftc.last_menu_index, + (void *) fwuss->menu_chain); + rc = result_TEST_FAILED; + +FlashDestroy: + reap_test_tasks(); + wuss_destroy(fwuss); +FlashFailFree: + free(fpixels); +FlashFail: + bmfont_destroy(font); + if (rc != result_OK) + return result_TEST_FAILED; + } +#endif /* WUSS_ICONS */ +#endif /* WUSS_MENUS */ wuss_destroy(wuss); From c04595648bdaa0065ee24e59aa9d706c1953326e Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Wed, 2 Sep 2026 18:19:14 +0100 Subject: [PATCH 187/287] refactor(wuss): tidy up sofa proportions in demo task Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- libraries/wuss/test/tasks/sofa.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libraries/wuss/test/tasks/sofa.c b/libraries/wuss/test/tasks/sofa.c index a52dba89..bea1cad1 100644 --- a/libraries/wuss/test/tasks/sofa.c +++ b/libraries/wuss/test/tasks/sofa.c @@ -41,10 +41,10 @@ typedef struct box3 { double x0, y0, z0, x1, y1, z1; } box3_t; /* the sofa: seat, backrest and two arms, in model units (roughly -1..1) */ static const box3_t sofa_parts[] = { - { -0.8, -0.3, -0.6, 0.8, 0.1, 0.6 }, /* seat */ - { -1.0, -0.3, 0.35, 1.0, 0.9, 0.6 }, /* backrest */ - { -1.0, -0.3, -0.6, -0.8, 0.5, 0.6 }, /* left arm */ - { 0.8, -0.3, -0.6, 1.0, 0.5, 0.6 }, /* right arm */ + { -0.8, -0.3, -0.5, 0.8, 0.1, 0.5 }, /* seat */ + { -1.0, -0.3, 0.5, 1.0, 0.9, 0.8 }, /* backrest */ + { -1.0, -0.3, -0.5, -0.8, 0.5, 0.5 }, /* left arm */ + { 0.8, -0.3, -0.5, 1.0, 0.5, 0.5 }, /* right arm */ }; /* cube corner edges, indexing box3_corners' bit-numbered corners (bit0=x, From 9c239f66ddeabd353dbb7c2de49803c3e04fbd72 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Wed, 2 Sep 2026 18:22:04 +0100 Subject: [PATCH 188/287] fix(wuss): keep a flashed menu row highlit if the pointer stays on it wuss__menu_flash_finish left the row un-highlit and relied on a following MOUSE_MOVE to bring it back. On an ADJUST re-pick the pointer often does not move, so the still-hovered row was left blank. It now re-hit-tests the wuss pointer and re-highlights the row when it is still underneath. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- libraries/wuss/menu/menu.c | 34 ++++++++++++++++++++++++++------- libraries/wuss/test/wuss-test.c | 7 +++++-- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/libraries/wuss/menu/menu.c b/libraries/wuss/menu/menu.c index f3924def..4bb0bece 100644 --- a/libraries/wuss/menu/menu.c +++ b/libraries/wuss/menu/menu.c @@ -315,25 +315,45 @@ static result_t wuss__menu_handle(wuss_window_t *window, return result_OK; } -/* End the pick flash on `self` now: leave the flashed row un-highlit (a later - * MOUSE_MOVE re-highlights it if the pointer is still over it), then deliver - * the MENU_SELECT the flash stands in for -- tearing the chain down first - * unless the pick was an ADJUST (keep_open). On a non-keep_open return `self` - * has been freed. Callers guard against calling this with no flash armed. */ +/* True if the wuss pointer currently sits over icon `icon` in `window`. */ +static int wuss__pointer_over_icon(wuss_window_t *window, + const wuss_icon_t *icon) +{ + point_t p; + box_t content; + point_t doc; + + p = wuss_get_pointer(window->wuss); + wuss__content_box(window, &content); + doc.x = p.x - content.x0 + window->scroll.x; + doc.y = p.y - content.y0 + window->scroll.y; + + return wuss__icon_hit_test(window, doc) == icon; +} + +/* End the pick flash on `self` now: leave the flashed row un-highlit unless the + * pointer is still over it (on an ADJUST re-pick no MOUSE_MOVE follows to bring + * the highlight back), then deliver the MENU_SELECT the flash stands in for -- + * tearing the chain down first unless the pick was an ADJUST (keep_open). On a + * non-keep_open return `self` has been freed. Callers guard against calling + * this with no flash armed. */ static void wuss__menu_flash_finish(struct wuss__menu *self) { struct wuss__menu *root; wuss_event_t sel; + wuss_icon_t *row = self->icons[self->flash.index]; wuss_task_t *owner = self->flash.owner; const wuss_menu_t *menu = self->flash.menu; int index = self->flash.index; wuss_button_t button = self->flash.button; int keep_open = self->flash.keep_open; + int on; self->flash.frames = 0; - wuss__icon_set_state(self->icons[index], wuss_ICON_STATE_HOVERED, 0); - wuss__icon_invalidate(self->icons[index]); + on = keep_open && wuss__pointer_over_icon(self->window, row); + wuss__icon_set_state(row, wuss_ICON_STATE_HOVERED, on); + wuss__icon_invalidate(row); if (!keep_open) { diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index feeaf550..ef434af6 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -3600,10 +3600,13 @@ MenuOK: ; if (ftc.menu_select_count != 2) goto FlashCheckFail; if (ftc.last_menu_index != 2) goto FlashCheckFail; - /* ADJUST keeps the chain open, but no row may be left inverted */ + /* ADJUST keeps the chain open. The flash ends un-highlit, but the pointer + * is still parked on row 2 (flash_pick_row left it there) so that row -- + * and only that row -- comes back highlit. */ if (fwuss->menu_chain == NULL) goto FlashCheckFail; for (i = 0; i < flash_menu.nitems; i++) - if (wuss__icon_hovered(fwuss->menu_chain->icons[i])) goto FlashCheckFail; + if (wuss__icon_hovered(fwuss->menu_chain->icons[i]) != (i == 2)) + goto FlashCheckFail; rc = result_OK; goto FlashDestroy; From cd52d202a7944ed960fc128e0e5646fe09ee90f6 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Wed, 2 Sep 2026 18:44:19 +0100 Subject: [PATCH 189/287] fix(wuss): avoid use-after-free re-picking a menu row mid-flash wuss__menu_handle's MOUSE_UP path finished a still-running pick flash before starting a new one. When the previous pick was a SELECT (not keep_open) wuss__menu_flash_finish tears the whole chain down and frees `self`, but the handler carried on reading self->icons[index] and writing self->flash.*, corrupting freed heap. Spend this pick on finishing the previous SELECT flash and return early when the chain has gone; only fall through to arm a fresh flash when the finished flash was an ADJUST (keep_open) that left `self` alive. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- libraries/wuss/menu/menu.c | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/libraries/wuss/menu/menu.c b/libraries/wuss/menu/menu.c index 4bb0bece..99089985 100644 --- a/libraries/wuss/menu/menu.c +++ b/libraries/wuss/menu/menu.c @@ -285,15 +285,21 @@ static result_t wuss__menu_handle(wuss_window_t *window, * SELECT tears the whole chain down first; ADJUST keeps it open so the * row can be re-picked. Capture everything the notification needs now -- * a SELECT flash frees `self` when it ends. A re-pick while a flash on - * this level is still running (fast ADJUST clicks) finishes that flash - * first, so its row is cleared and its MENU_SELECT is not lost. */ + * this level is still running (fast clicks) finishes that flash first so + * its row is cleared and its MENU_SELECT is not lost; if that previous + * pick was a SELECT the finish frees the whole chain (including `self`), + * so this pick is spent on it and we must not touch `self` again. */ { wuss_icon_t *row; if (self->flash.frames > 0) - wuss__menu_flash_finish(self); /* self survives: previous pick was - * ADJUST, or a SELECT would have been - * caught by the frames == 0 guard */ + { + int prev_keep_open = self->flash.keep_open; + + wuss__menu_flash_finish(self); + if (!prev_keep_open) + return result_OK; /* `self` and the chain are gone */ + } row = self->icons[index]; From 4053518ff4075e3c844959034979e864acc8ccdd Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Wed, 2 Sep 2026 18:58:54 +0100 Subject: [PATCH 190/287] fix(wuss): reap the task node when a demo task fails to build its window Each X_create in the interactive demo registered its task with wuss_task_create before creating the window or icon array. If that later step failed, X_create returned the error with the task node still linked in wuss->tasks pointing at a block the spawner then free()d -- the next wuss_idle / wuss_set_palette broadcast would deliver into freed memory. Give every X_create a single ownership rule: on any error it now frees the task block itself, either directly (nothing registered yet) or via wuss_task_destroy, whose QUIT handler frees the block and any bitmaps. The spawners drop their rc-error free() accordingly and keep only the one that was always theirs: the font-less chars task that returns OK without opening a window. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- apps/wuss/main.c | 49 ++++++++++++++++--------- libraries/wuss/test/tasks/ball.c | 23 ++++++++---- libraries/wuss/test/tasks/blank.c | 23 ++++++++---- libraries/wuss/test/tasks/chars.c | 29 +++++++++------ libraries/wuss/test/tasks/checker.c | 8 +++- libraries/wuss/test/tasks/curve.c | 23 ++++++++---- libraries/wuss/test/tasks/gradient.c | 23 ++++++++---- libraries/wuss/test/tasks/icons.c | 13 ++++--- libraries/wuss/test/tasks/image.c | 31 +++++++++++----- libraries/wuss/test/tasks/lissajous.c | 23 ++++++++---- libraries/wuss/test/tasks/palette.c | 23 ++++++++---- libraries/wuss/test/tasks/porter-duff.c | 8 +++- libraries/wuss/test/tasks/sofa.c | 23 ++++++++---- libraries/wuss/test/tasks/swatches.c | 9 ++++- libraries/wuss/test/tasks/text.c | 5 +++ 15 files changed, 209 insertions(+), 104 deletions(-) diff --git a/apps/wuss/main.c b/apps/wuss/main.c index 2c50c17f..51f79b6f 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -60,9 +60,10 @@ g; /* Each spawn allocates a fresh per-instance task block so a task may run in * several windows at once; the block is owned by its window and freed by the - * task's wuss_EVENT_CLOSE handler. If create fails, or (for the font-less - * chars task) returns OK without opening a window, the block is freed here -- - * otherwise it would leak. */ + * task's wuss_EVENT_QUIT handler. On any create failure X_create has already + * torn down whatever it built and freed the block itself, so the only block + * the spawner frees is the font-less chars case: create returns OK but opens + * no window, leaving the block with no owner. */ static result_t spawn_ball(void) { @@ -70,7 +71,8 @@ static result_t spawn_ball(void) result_t rc; if (t == NULL) return result_OOM; rc = ball_create(g.wuss, t); - if (rc != result_OK || t->window == NULL) { free(t); return rc; } + if (rc != result_OK) return rc; + if (t->window == NULL) { free(t); return rc; } return result_OK; } @@ -80,7 +82,8 @@ static result_t spawn_text(void) result_t rc; if (t == NULL) return result_OOM; rc = text_create(g.wuss, g.daydream_font, t); - if (rc != result_OK || t->window == NULL) { free(t); return rc; } + if (rc != result_OK) return rc; + if (t->window == NULL) { free(t); return rc; } return result_OK; } @@ -90,7 +93,8 @@ static result_t spawn_blank(void) result_t rc; if (t == NULL) return result_OOM; rc = blank_create(g.wuss, t); - if (rc != result_OK || t->window == NULL) { free(t); return rc; } + if (rc != result_OK) return rc; + if (t->window == NULL) { free(t); return rc; } return result_OK; } @@ -100,7 +104,8 @@ static result_t spawn_chars(void) result_t rc; if (t == NULL) return result_OOM; rc = chars_create(g.wuss, g.resources, t); - if (rc != result_OK || t->window == NULL) { free(t); return rc; } + if (rc != result_OK) return rc; + if (t->window == NULL) { free(t); return rc; } return result_OK; } @@ -110,7 +115,8 @@ static result_t spawn_palette(void) result_t rc; if (t == NULL) return result_OOM; rc = palette_create(g.wuss, g.palette, g.npalette, t); - if (rc != result_OK || t->window == NULL) { free(t); return rc; } + if (rc != result_OK) return rc; + if (t->window == NULL) { free(t); return rc; } return result_OK; } @@ -137,7 +143,8 @@ static result_t spawn_image(void) if (rc != result_OK) logf_error("wuss: image_create(\"%s\") failed, rc=0x%X (%s)", buf, rc, result_string(rc)); - if (rc != result_OK || t->window == NULL) { free(t); return rc; } + if (rc != result_OK) return rc; + if (t->window == NULL) { free(t); return rc; } return result_OK; } @@ -147,7 +154,8 @@ static result_t spawn_checker(void) result_t rc; if (t == NULL) return result_OOM; rc = checker_create(g.wuss, t); - if (rc != result_OK || t->window == NULL) { free(t); return rc; } + if (rc != result_OK) return rc; + if (t->window == NULL) { free(t); return rc; } return result_OK; } @@ -157,7 +165,8 @@ static result_t spawn_curve(void) result_t rc; if (t == NULL) return result_OOM; rc = curve_create(g.wuss, t); - if (rc != result_OK || t->window == NULL) { free(t); return rc; } + if (rc != result_OK) return rc; + if (t->window == NULL) { free(t); return rc; } return result_OK; } @@ -167,7 +176,8 @@ static result_t spawn_lissajous(void) result_t rc; if (t == NULL) return result_OOM; rc = lissajous_create(g.wuss, t); - if (rc != result_OK || t->window == NULL) { free(t); return rc; } + if (rc != result_OK) return rc; + if (t->window == NULL) { free(t); return rc; } return result_OK; } @@ -177,7 +187,8 @@ static result_t spawn_sofa(void) result_t rc; if (t == NULL) return result_OOM; rc = sofa_create(g.wuss, t); - if (rc != result_OK || t->window == NULL) { free(t); return rc; } + if (rc != result_OK) return rc; + if (t->window == NULL) { free(t); return rc; } return result_OK; } @@ -187,7 +198,8 @@ static result_t spawn_gradient(void) result_t rc; if (t == NULL) return result_OOM; rc = gradient_create(g.wuss, t); - if (rc != result_OK || t->window == NULL) { free(t); return rc; } + if (rc != result_OK) return rc; + if (t->window == NULL) { free(t); return rc; } return result_OK; } @@ -200,7 +212,8 @@ static result_t spawn_icons(void) if (rc != result_OK) logf_error("wuss: icons_create (resources \"%s\") failed, rc=0x%X (%s)", g.resources, rc, result_string(rc)); - if (rc != result_OK || t->window == NULL) { free(t); return rc; } + if (rc != result_OK) return rc; + if (t->window == NULL) { free(t); return rc; } return result_OK; } @@ -210,7 +223,8 @@ static result_t spawn_swatches(void) result_t rc; if (t == NULL) return result_OOM; rc = swatches_create(g.wuss, t); - if (rc != result_OK || t->window == NULL) { free(t); return rc; } + if (rc != result_OK) return rc; + if (t->window == NULL) { free(t); return rc; } return result_OK; } @@ -223,7 +237,8 @@ static result_t spawn_porter_duff(void) if (rc != result_OK) logf_error("wuss: porter_duff_create (resources \"%s\") failed, " "rc=0x%X (%s)", g.resources, rc, result_string(rc)); - if (rc != result_OK || t->window == NULL) { free(t); return rc; } + if (rc != result_OK) return rc; + if (t->window == NULL) { free(t); return rc; } return result_OK; } diff --git a/libraries/wuss/test/tasks/ball.c b/libraries/wuss/test/tasks/ball.c index de7130c5..58f97681 100644 --- a/libraries/wuss/test/tasks/ball.c +++ b/libraries/wuss/test/tasks/ball.c @@ -55,17 +55,24 @@ result_t ball_create(wuss_t *wuss, ball_task_t *task) delegate_desc.name = "ball"; 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); - return wuss_window_create_placed(delegate, - SIZE2D(200, 160), - "Bouncing Ball", - wuss_WINDOW_NONE, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), - SIZE2D(200, 160), - SIZE2D(0, 0), - &task->window); + rc = wuss_window_create_placed(delegate, + SIZE2D(200, 160), + "Bouncing Ball", + wuss_WINDOW_NONE, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + SIZE2D(200, 160), + SIZE2D(0, 0), + &task->window); + if (rc != result_OK) + wuss_task_destroy(delegate); /* unregister; its QUIT frees the task block */ + + return rc; } static result_t ball_redraw(const wuss_event_t *event, void *task_data) diff --git a/libraries/wuss/test/tasks/blank.c b/libraries/wuss/test/tasks/blank.c index 76edcc7c..9986416a 100644 --- a/libraries/wuss/test/tasks/blank.c +++ b/libraries/wuss/test/tasks/blank.c @@ -33,17 +33,24 @@ result_t blank_create(wuss_t *wuss, blank_task_t *task) delegate_desc.name = "blank"; 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); - return wuss_window_create_placed(delegate, - SIZE2D(200, 160), - NULL, - wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE, - wuss_BACKDROP_COLOUR(task->index), - SIZE2D(200, 160), - SIZE2D(0, 0), - &task->window); + rc = wuss_window_create_placed(delegate, + SIZE2D(200, 160), + NULL, + wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE, + wuss_BACKDROP_COLOUR(task->index), + SIZE2D(200, 160), + SIZE2D(0, 0), + &task->window); + if (rc != result_OK) + wuss_task_destroy(delegate); /* unregister; its QUIT frees the task block */ + + return rc; } static result_t blank_idle(void *task_data) diff --git a/libraries/wuss/test/tasks/chars.c b/libraries/wuss/test/tasks/chars.c index 51054e05..18b933ea 100644 --- a/libraries/wuss/test/tasks/chars.c +++ b/libraries/wuss/test/tasks/chars.c @@ -159,21 +159,28 @@ result_t chars_create(wuss_t *wuss, delegate_desc.name = "chars"; 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); task->delegate = delegate; - return wuss_window_create_placed(delegate, - chars_window_size(font), - "Chars", - wuss_WINDOW_NO_RESIZE | - wuss_WINDOW_NO_TOGGLE_SIZE | - wuss_WINDOW_NO_VSCROLL | - wuss_WINDOW_NO_HSCROLL, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), - chars_window_size(font), - SIZE2D(0, 0), - &task->window); + rc = wuss_window_create_placed(delegate, + chars_window_size(font), + "Chars", + wuss_WINDOW_NO_RESIZE | + wuss_WINDOW_NO_TOGGLE_SIZE | + wuss_WINDOW_NO_VSCROLL | + wuss_WINDOW_NO_HSCROLL, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + chars_window_size(font), + SIZE2D(0, 0), + &task->window); + if (rc != result_OK) + wuss_task_destroy(delegate); /* unregister; its QUIT frees the task block */ + + return rc; } static result_t chars_redraw(const wuss_event_t *event, void *task_data) diff --git a/libraries/wuss/test/tasks/checker.c b/libraries/wuss/test/tasks/checker.c index 81d9f9d9..00735844 100644 --- a/libraries/wuss/test/tasks/checker.c +++ b/libraries/wuss/test/tasks/checker.c @@ -37,7 +37,10 @@ result_t checker_create(wuss_t*wuss, checker_task_t *task) delegate_desc.name = "checker"; 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; + } rc = wuss_window_create_placed(delegate, SIZE2D(160, 160), @@ -48,7 +51,10 @@ result_t checker_create(wuss_t*wuss, checker_task_t *task) SIZE2D(0, 0), &task->window); if (rc != result_OK) + { + wuss_task_destroy(delegate); /* unregister; its QUIT frees the task block */ return rc; + } rc = wuss_window_create_placed(delegate, SIZE2D(160, 160), @@ -60,7 +66,7 @@ result_t checker_create(wuss_t*wuss, checker_task_t *task) &task->window2); if (rc != result_OK) { - wuss_window_close(task->window); + wuss_task_destroy(delegate); /* closes "Checker 1", QUIT frees the block */ return rc; } diff --git a/libraries/wuss/test/tasks/curve.c b/libraries/wuss/test/tasks/curve.c index 5fb78e8f..a5409575 100644 --- a/libraries/wuss/test/tasks/curve.c +++ b/libraries/wuss/test/tasks/curve.c @@ -46,17 +46,24 @@ result_t curve_create(wuss_t *wuss, curve_task_t *task) delegate_desc.name = "curve"; 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); - return wuss_window_create_placed(delegate, - SIZE2D(220, 160), - "Curve", - wuss_WINDOW_NONE, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), - SIZE2D(220, 160), - SIZE2D(0, 0), - &task->window); + rc = wuss_window_create_placed(delegate, + SIZE2D(220, 160), + "Curve", + wuss_WINDOW_NONE, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + SIZE2D(220, 160), + SIZE2D(0, 0), + &task->window); + if (rc != result_OK) + wuss_task_destroy(delegate); /* unregister; its QUIT frees the task block */ + + return rc; } static int blob_hit(const point_t *p, int x, int y) diff --git a/libraries/wuss/test/tasks/gradient.c b/libraries/wuss/test/tasks/gradient.c index ec02eec3..4dd1e52b 100644 --- a/libraries/wuss/test/tasks/gradient.c +++ b/libraries/wuss/test/tasks/gradient.c @@ -45,17 +45,24 @@ result_t gradient_create(wuss_t *wuss, gradient_task_t *task) delegate_desc.name = "gradient"; 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); - return wuss_window_create_placed(delegate, - SIZE2D(GRADIENT_OPEN_WIDTH, GRADIENT_OPEN_HEIGHT), - "Gradient", - wuss_WINDOW_NONE, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), - SIZE2D(GRADIENT_DOC_WIDTH, GRADIENT_DOC_HEIGHT), - SIZE2D(0, 0), - &task->window); + rc = wuss_window_create_placed(delegate, + SIZE2D(GRADIENT_OPEN_WIDTH, GRADIENT_OPEN_HEIGHT), + "Gradient", + wuss_WINDOW_NONE, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + SIZE2D(GRADIENT_DOC_WIDTH, GRADIENT_DOC_HEIGHT), + SIZE2D(0, 0), + &task->window); + if (rc != result_OK) + wuss_task_destroy(delegate); /* unregister; its QUIT frees the task block */ + + return rc; } static result_t gradient_redraw(const wuss_event_t *event, void *task_data) diff --git a/libraries/wuss/test/tasks/icons.c b/libraries/wuss/test/tasks/icons.c index 8d92ae55..d3516aa7 100644 --- a/libraries/wuss/test/tasks/icons.c +++ b/libraries/wuss/test/tasks/icons.c @@ -74,7 +74,12 @@ result_t icons_create(wuss_t *wuss, delegate_desc.name = "icons"; rc = wuss_task_create(wuss, &delegate_desc, &delegate); if (rc != result_OK) + { + if (task->has_sprite) + free(task->sprite.base); + free(task); /* nothing registered yet; the spawner will not free it */ return rc; + } rc = wuss_window_create_placed(delegate, SIZE2D(ICONS_DOC_W, 160), @@ -88,8 +93,7 @@ result_t icons_create(wuss_t *wuss, &task->window); if (rc != result_OK) { - if (task->has_sprite) - free(task->sprite.base); + wuss_task_destroy(delegate); /* QUIT frees the task block and its sprite */ return rc; } @@ -253,10 +257,7 @@ result_t icons_create(wuss_t *wuss, return result_OK; failure: - wuss_window_close(task->window); - task->window = NULL; - if (task->has_sprite) - free(task->sprite.base); + wuss_task_destroy(delegate); /* closes the window; QUIT frees block + sprite */ return rc; } diff --git a/libraries/wuss/test/tasks/image.c b/libraries/wuss/test/tasks/image.c index 9e7308dd..d8408fa6 100644 --- a/libraries/wuss/test/tasks/image.c +++ b/libraries/wuss/test/tasks/image.c @@ -28,12 +28,16 @@ result_t image_create(wuss_t *wuss, rc = bitmap_load_png(&task->bitmap, path); if (rc != result_OK) + { + free(task); /* nothing registered yet; the spawner will not free it */ return rc; + } rc = bitmap_load_png(&task->ninepatch, background_path); if (rc != result_OK) { free(task->bitmap.base); + free(task); /* nothing registered yet; the spawner will not free it */ return rc; } @@ -43,21 +47,30 @@ result_t image_create(wuss_t *wuss, delegate_desc.name = "image"; rc = wuss_task_create(wuss, &delegate_desc, &delegate); if (rc != result_OK) + { + free(task->bitmap.base); + free(task->ninepatch.base); + free(task); /* nothing registered yet; the spawner will not free it */ return rc; + } wuss_task_set_autoclose(delegate, 1); sz.w = task->bitmap.size.w + BORDER * 2; sz.h = task->bitmap.size.h + BORDER * 2; - return wuss_window_create_placed(delegate, - /* shorter than the bitmap so there's something to scroll through */ - SIZE2D(sz.w, sz.h * 2 / 3), - "Image", - wuss_WINDOW_NONE, - wuss_BACKDROP_COLOUR(palette_PICO8_PINK), - sz, - SIZE2D(32, 32), - &task->window); + rc = wuss_window_create_placed(delegate, + /* shorter than the bitmap so there's something to scroll through */ + SIZE2D(sz.w, sz.h * 2 / 3), + "Image", + wuss_WINDOW_NONE, + wuss_BACKDROP_COLOUR(palette_PICO8_PINK), + sz, + SIZE2D(32, 32), + &task->window); + if (rc != result_OK) + wuss_task_destroy(delegate); /* QUIT frees the two bitmaps and the block */ + + return rc; } static result_t image_redraw(const wuss_event_t *event, void *task_data) diff --git a/libraries/wuss/test/tasks/lissajous.c b/libraries/wuss/test/tasks/lissajous.c index 87d1e6f5..ee5ab115 100644 --- a/libraries/wuss/test/tasks/lissajous.c +++ b/libraries/wuss/test/tasks/lissajous.c @@ -41,17 +41,24 @@ result_t lissajous_create(wuss_t *wuss, lissajous_task_t *task) delegate_desc.name = "lissajous"; 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); - return wuss_window_create_placed(delegate, - SIZE2D(220, 220), - "Lissajous", - wuss_WINDOW_NONE, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), - SIZE2D(220, 220), - SIZE2D(0, 0), - &task->window); + rc = wuss_window_create_placed(delegate, + SIZE2D(220, 220), + "Lissajous", + wuss_WINDOW_NONE, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + SIZE2D(220, 220), + SIZE2D(0, 0), + &task->window); + if (rc != result_OK) + wuss_task_destroy(delegate); /* unregister; its QUIT frees the task block */ + + return rc; } static result_t lissajous_redraw(const wuss_event_t *event, void *task_data) diff --git a/libraries/wuss/test/tasks/palette.c b/libraries/wuss/test/tasks/palette.c index 179dc3fc..57690879 100644 --- a/libraries/wuss/test/tasks/palette.c +++ b/libraries/wuss/test/tasks/palette.c @@ -32,17 +32,24 @@ result_t palette_create(wuss_t *wuss, delegate_desc.name = "palette"; 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); - return wuss_window_create_placed(delegate, - SIZE2D(100, 100), - "Palette", - wuss_WINDOW_NO_RESIZE_BLIT, /* swatch grid is laid out across the whole window, so a resize must redraw all of it, not just the newly (un)covered edge */ - wuss_BACKDROP_COLOUR(palette_PICO8_BLACK), - SIZE2D(100, 100), - SIZE2D(0, 0), - &task->window); + rc = wuss_window_create_placed(delegate, + SIZE2D(100, 100), + "Palette", + wuss_WINDOW_NO_RESIZE_BLIT, /* swatch grid is laid out across the whole window, so a resize must redraw all of it, not just the newly (un)covered edge */ + wuss_BACKDROP_COLOUR(palette_PICO8_BLACK), + SIZE2D(100, 100), + SIZE2D(0, 0), + &task->window); + if (rc != result_OK) + wuss_task_destroy(delegate); /* unregister; its QUIT frees the task block */ + + return rc; } static result_t palette_redraw(const wuss_event_t *event, void *task_data) diff --git a/libraries/wuss/test/tasks/porter-duff.c b/libraries/wuss/test/tasks/porter-duff.c index 363d8ba9..5e516645 100644 --- a/libraries/wuss/test/tasks/porter-duff.c +++ b/libraries/wuss/test/tasks/porter-duff.c @@ -198,7 +198,7 @@ result_t porter_duff_create(wuss_t *wuss, delegate_desc.name = "porter-duff"; rc = wuss_task_create(wuss, &delegate_desc, &delegate); if (rc != result_OK) - return rc; + goto free_dst; /* nothing registered yet; the spawner will not free task */ wuss_task_set_autoclose(delegate, 1); rc = wuss_window_create_placed(delegate, @@ -210,7 +210,10 @@ result_t porter_duff_create(wuss_t *wuss, SIZE2D(0, 0), &task->window); if (rc != result_OK) - goto free_dst; + { + wuss_task_destroy(delegate); /* QUIT frees the four bitmaps and task */ + return rc; + } return result_OK; @@ -222,6 +225,7 @@ result_t porter_duff_create(wuss_t *wuss, free(task->b.base); free_a: free(task->a.base); + free(task); /* no task was registered on any goto here; spawner won't free */ return rc; } diff --git a/libraries/wuss/test/tasks/sofa.c b/libraries/wuss/test/tasks/sofa.c index bea1cad1..63f2dc18 100644 --- a/libraries/wuss/test/tasks/sofa.c +++ b/libraries/wuss/test/tasks/sofa.c @@ -375,17 +375,24 @@ result_t sofa_create(wuss_t*wuss, sofa_task_t*task) delegate_desc.name = "sofa"; 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); - return wuss_window_create_placed(delegate, - SIZE2D(180, 160), - "Sofa", - wuss_WINDOW_NONE, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), - SIZE2D(180, 160), - SIZE2D(0, 0), - &task->window); + rc = wuss_window_create_placed(delegate, + SIZE2D(180, 160), + "Sofa", + wuss_WINDOW_NONE, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + SIZE2D(180, 160), + SIZE2D(0, 0), + &task->window); + if (rc != result_OK) + wuss_task_destroy(delegate); /* unregister; its QUIT frees the task block */ + + return rc; } static result_t sofa_redraw(const wuss_event_t *event, void *task_data) diff --git a/libraries/wuss/test/tasks/swatches.c b/libraries/wuss/test/tasks/swatches.c index d2a323a3..69f782d6 100644 --- a/libraries/wuss/test/tasks/swatches.c +++ b/libraries/wuss/test/tasks/swatches.c @@ -86,7 +86,10 @@ result_t swatches_create(wuss_t *wuss, swatches_task_t *task) delegate_desc.name = "swatches"; 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; + } rc = wuss_window_create_placed(delegate, SIZE2D(SWATCHES_DOC_W, 140), @@ -97,13 +100,15 @@ result_t swatches_create(wuss_t *wuss, swatches_task_t *task) SIZE2D(0, 0), &task->window); if (rc != result_OK) + { + wuss_task_destroy(delegate); /* unregister; its QUIT frees the task block */ return rc; + } rc = swatches_build(task); if (rc != result_OK) { - wuss_window_close(task->window); - task->window = NULL; + wuss_task_destroy(delegate); /* closes the window, QUIT frees the block */ return rc; } diff --git a/libraries/wuss/test/tasks/text.c b/libraries/wuss/test/tasks/text.c index da1604ee..b02e02f2 100644 --- a/libraries/wuss/test/tasks/text.c +++ b/libraries/wuss/test/tasks/text.c @@ -48,7 +48,10 @@ result_t text_create(wuss_t *wuss, delegate_desc.name = "text"; 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); sz = SIZE2D(220, 180); @@ -63,6 +66,8 @@ result_t text_create(wuss_t *wuss, sz, SIZE2D(0, 0), &task->window); + if (rc != result_OK) + wuss_task_destroy(delegate); /* unregister; its QUIT frees the task block */ return rc; } From b2494dd6b3ebd9c1f0f4c4fd5eaebf7dbed3e515 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Wed, 2 Sep 2026 19:04:44 +0100 Subject: [PATCH 191/287] fix(wuss): cache the next task before broadcasting IDLE / PALETTE wuss_idle and wuss_set_palette walked wuss->tasks with `e = e->next` evaluated after the handler ran. An autoclose task whose IDLE or PALETTE handler closed its last window got reaped mid-walk -- list_remove plus wuss__free on its node -- so the next `e = e->next` dereferenced freed memory and wuss__deliver was handed a dangling task. Read e->next into a local before the callback, matching wuss_task_destroy's own window loop. Covers a task reaping itself; a handler that reaps a different task's node is still out of scope. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- libraries/wuss/idle.c | 6 +- libraries/wuss/set-palette.c | 6 +- libraries/wuss/test/wuss-test.c | 103 ++++++++++++++++++++++++++++++++ 3 files changed, 113 insertions(+), 2 deletions(-) diff --git a/libraries/wuss/idle.c b/libraries/wuss/idle.c index c4d4165b..a27b1ef2 100644 --- a/libraries/wuss/idle.c +++ b/libraries/wuss/idle.c @@ -11,16 +11,20 @@ result_t wuss_idle(wuss_t *wuss) event.kind = wuss_EVENT_IDLE; rc = result_OK; - for (e = wuss->tasks.next; e != NULL; e = e->next) + for (e = wuss->tasks.next; e != NULL; ) { 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 */ crc = wuss__deliver(task, NULL, &event); if (crc != result_OK && rc == result_OK) rc = crc; + + e = next; } return rc; diff --git a/libraries/wuss/set-palette.c b/libraries/wuss/set-palette.c index b1a7645b..7c0c9da3 100644 --- a/libraries/wuss/set-palette.c +++ b/libraries/wuss/set-palette.c @@ -32,16 +32,20 @@ result_t wuss_set_palette(wuss_t *wuss, rc = result_OK; event.kind = wuss_EVENT_PALETTE; - for (e = wuss->tasks.next; e != NULL; e = e->next) + for (e = wuss->tasks.next; e != NULL; ) { 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 */ crc = wuss__deliver(task, NULL, &event); if (crc != result_OK && rc == result_OK) rc = crc; + + e = next; } screen.x0 = 0; diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index ef434af6..b9afbc57 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -143,6 +143,42 @@ static result_t test_handle(wuss_window_t *window, return result_OK; } +/* An IDLE handler that closes a window the first time it is broadcast to. On + * an autoclose task with that as its only window, the close reaps the task + * and frees its list node from inside the wuss_idle / wuss_set_palette walk; + * the test checks the walk survives it and still reaches the tasks behind. */ +static wuss_window_t *g_close_on_idle_win; + +static result_t close_on_idle_handle(wuss_window_t *window, + const wuss_event_t *event, + void *task_data) +{ + test_task_t *tc; + + NOT_USED(window); + + tc = task_data; + + if (event->kind == wuss_EVENT_IDLE) + { + tc->idle_count++; + if (g_close_on_idle_win != NULL) + { + wuss_window_t *doomed; + + doomed = g_close_on_idle_win; + g_close_on_idle_win = NULL; + wuss_window_close(doomed); /* reaps this task mid-walk */ + } + return result_OK; + } + + if (event->kind == wuss_EVENT_QUIT) + tc->stop_count++; + + return result_OK; +} + /* A redraw handler that paints its content as one-pixel horizontal lines * whose colour encodes the document-space Y of each row, so a test can read * the framebuffer back and tell not just whose pixels are where but whether @@ -3443,6 +3479,73 @@ result_t wuss_test(const char *resources) mk_task_count = 0; /* delegate_ac reaped itself; drop the stale entry */ } + printf("test: a task reaping itself from inside wuss_idle does not derail the task walk\n"); + { + static test_task_t tc_w1, tc_w2, tc_w3; + wuss_task_t *delegate_w1, *delegate_w2, *delegate_w3; + box_t box_w; + wuss_window_t *win_w1, *win_w3; + + memset(&tc_w1, 0, sizeof(tc_w1)); + memset(&tc_w2, 0, sizeof(tc_w2)); + memset(&tc_w3, 0, sizeof(tc_w3)); + + box_w.x0 = 0; box_w.y0 = 0; + box_w.x1 = 40; box_w.y1 = 40; + + /* w1 first, w2 (self-reaping, autoclose) in the middle, w3 last: the + * walk must deliver to w1, survive w2 freeing its own node, and still + * reach w3. */ + 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, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + box_size(&box_w), SIZE2D(0, 0), &win_w1); + if (rc != result_OK) + goto Failure; + + 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, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + box_size(&box_w), SIZE2D(0, 0), + &g_close_on_idle_win); + if (rc != result_OK) + goto Failure; + + 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, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + box_size(&box_w), SIZE2D(0, 0), &win_w3); + if (rc != result_OK) + goto Failure; + + rc = wuss_idle(wuss); + if (rc != result_OK) + goto Failure; + if (tc_w1.idle_count != 1) /* reached before the reap */ + goto Failure; + if (tc_w2.stop_count != 1) /* self-reaped mid-walk */ + goto Failure; + if (tc_w3.idle_count != 1) /* walk still reached the task behind w2 */ + goto Failure; + + /* w2 is gone; a second idle must skip it cleanly */ + rc = wuss_idle(wuss); + if (rc != result_OK) + goto Failure; + if (tc_w2.idle_count != 1 || tc_w3.idle_count != 2) + goto Failure; + + wuss_window_close(win_w1); /* autoclose: reaps w1 */ + wuss_window_close(win_w3); /* autoclose: reaps w3 */ + mk_task_count = 0; /* all three tasks reaped themselves; drop stale entries */ + } + #ifdef WUSS_MENUS printf("test: wuss_menu_create_from_desc parses a descriptor tree\n"); { From 2f9461fcb092a77de9532e11225180af0146518c Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Wed, 2 Sep 2026 19:58:16 +0100 Subject: [PATCH 192/287] feat(wuss): add wuss_set_backdrop to swap the desktop backdrop mid-session New public entry point mirrors config->backdrop handling in wuss_create: validate against the current palette, copy in, then invalidate the whole screen so the next redraw repaints behind every window. Tasks are not notified and the caller still owns the redraw. The swatches demo task now uses it: a click on a pattern swatch makes that pattern the desktop backdrop, hit-tested against per-pattern content-space boxes captured at build time. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- CMakeLists.txt | 1 + include/wuss/wuss.h | 18 +++++++++++++ libraries/wuss/set-backdrop.c | 30 +++++++++++++++++++++ libraries/wuss/test/tasks/swatches.c | 40 ++++++++++++++++++++++++++++ libraries/wuss/test/tasks/swatches.h | 4 +++ 5 files changed, 93 insertions(+) create mode 100644 libraries/wuss/set-backdrop.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 5d2a9e51..702c879e 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -350,6 +350,7 @@ set(WUSS_CORE_SOURCES libraries/wuss/redraw.c libraries/wuss/scroll.c libraries/wuss/scroll-step.c + libraries/wuss/set-backdrop.c libraries/wuss/set-palette.c libraries/wuss/task/create.c libraries/wuss/task/destroy.c diff --git a/include/wuss/wuss.h b/include/wuss/wuss.h index 144f6d23..6a613d97 100644 --- a/include/wuss/wuss.h +++ b/include/wuss/wuss.h @@ -360,6 +360,24 @@ result_t wuss_set_palette(wuss_t *wuss, const colour_t *palette, int npalette); +/** + * Replace the desktop backdrop partway through a session. + * + * Validates \p backdrop against the current palette (as wuss_create does its + * config->backdrop), copies it in over the existing one, then invalidates + * the whole screen so the next wuss_redraw / wuss_redraw_dirty repaints it + * behind every window. Tasks are not notified. The caller is still + * responsible for the next redraw. + * + * \param[in] wuss Window manager. + * \param[in] backdrop New backdrop, copied in. Set its colour to + * wuss_NO_BACKGROUND for no backdrop. + * \return \ref result_OK on success, \ref result_WUSS_BAD_COLOUR if a colour + * index in \p backdrop is out of range (the backdrop is left + * unchanged). + */ +result_t wuss_set_backdrop(wuss_t *wuss, const wuss_backdrop_t *backdrop); + /** * Destroy a window manager, and any windows still open on it. * diff --git a/libraries/wuss/set-backdrop.c b/libraries/wuss/set-backdrop.c new file mode 100644 index 00000000..905ce012 --- /dev/null +++ b/libraries/wuss/set-backdrop.c @@ -0,0 +1,30 @@ +/* wuss/set-backdrop.c -- swap the desktop backdrop mid-session */ + +#include <assert.h> + +#include "geom/box.h" + +#include "impl.h" + +result_t wuss_set_backdrop(wuss_t *wuss, const wuss_backdrop_t *backdrop) +{ + box_t screen; + result_t rc; + + assert(wuss != NULL); + assert(backdrop != NULL); + + rc = wuss__validate_backdrop(wuss, backdrop); + if (rc != result_OK) + return rc; + + wuss->backdrop = *backdrop; + + screen.x0 = 0; + screen.y0 = 0; + screen.x1 = wuss->scr->size.w; + screen.y1 = wuss->scr->size.h; + wuss_invalidate(wuss, &screen); + + return result_OK; +} diff --git a/libraries/wuss/test/tasks/swatches.c b/libraries/wuss/test/tasks/swatches.c index 69f782d6..eed7cf3e 100644 --- a/libraries/wuss/test/tasks/swatches.c +++ b/libraries/wuss/test/tasks/swatches.c @@ -12,6 +12,7 @@ #include "framebuf/palettes.h" #include "framebuf/screen.h" #include "geom/box.h" +#include "geom/point.h" #include "geom/size.h" #include "wuss/icon.h" @@ -38,6 +39,9 @@ static result_t swatches_build(swatches_task_t *task) fg = wuss_nearest_colour(task->wuss, 0x00, 0x00, 0x00); bg = wuss_nearest_colour(task->wuss, 0xFF, 0xFF, 0xFF); + task->swatch_fg = fg; + task->swatch_bg = bg; + memset(specs, 0, sizeof(specs)); /* [0] a heading label */ @@ -65,6 +69,11 @@ static result_t swatches_build(swatches_task_t *task) specs[1 + p].fg = fg; specs[1 + p].bg = bg; specs[1 + p].pattern = (screen_pattern_t) p; + + /* PATTERN icons are not interactive, so their clicks fall through to us + * as wuss_EVENT_MOUSE in this same content space -- keep each box to + * hit-test against */ + task->swatch_bbox[p] = specs[1 + p].bbox; } return wuss_icon_create_array(task->window, specs, SWATCHES_NSPECS, @@ -119,6 +128,34 @@ result_t swatches_create(wuss_t *wuss, swatches_task_t *task) return result_OK; } +/* A click landed on the window content. If it is inside a swatch, make that + * swatch's fill pattern the desktop backdrop. */ +static result_t swatches_click(swatches_task_t *task, + const wuss_event_t *event) +{ + wuss_backdrop_t backdrop; + point_t pt; + int p; + + if (event->data.mouse.action != wuss_MOUSE_DOWN) + return result_OK; + + pt = event->data.mouse.point; + + for (p = 0; p < screen_PATTERN__LIMIT; p++) + { + if (!box_contains_point(&task->swatch_bbox[p], pt.x, pt.y)) + continue; + + backdrop = (wuss_backdrop_t) wuss_BACKDROP_PATTERN(task->swatch_fg, + (screen_pattern_t) p, + task->swatch_bg); + return wuss_set_backdrop(task->wuss, &backdrop); + } + + return result_OK; +} + result_t swatches_handle(wuss_window_t *window, const wuss_event_t *event, void *task_data) @@ -129,6 +166,9 @@ result_t swatches_handle(wuss_window_t *window, switch (event->kind) { + case wuss_EVENT_MOUSE: + return swatches_click(task, event); + case wuss_EVENT_PALETTE: return swatches_build(task); diff --git a/libraries/wuss/test/tasks/swatches.h b/libraries/wuss/test/tasks/swatches.h index 692b5ecb..4a2bd7e9 100644 --- a/libraries/wuss/test/tasks/swatches.h +++ b/libraries/wuss/test/tasks/swatches.h @@ -6,6 +6,7 @@ #ifdef WUSS_APP #include "framebuf/screen.h" +#include "geom/box.h" #include "wuss/icon.h" #include "wuss/window.h" @@ -19,6 +20,9 @@ typedef struct swatches_task wuss_t *wuss; /* for re-resolving swatch colours on a palette swap */ wuss_window_t *window; wuss_icon_t *icons[SWATCHES_NSPECS]; /* current icons, deleted on rebuild */ + box_t swatch_bbox[screen_PATTERN__LIMIT]; /* content-space hit box per pattern */ + wuss_colour_t swatch_fg; /* ink to set the backdrop in when a swatch is picked */ + wuss_colour_t swatch_bg; /* paper for the backdrop pattern's clear bits */ } swatches_task_t; From 319f2c8b29a470950982a239b683ace980885fde Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Wed, 2 Sep 2026 20:02:22 +0100 Subject: [PATCH 193/287] fix(wuss): leave GARBAGE test junk on screen instead of wiping it The GARBAGE input corrupted the framebuffer, presented it, then immediately called wuss_redraw() and presented again -- so the junk never actually showed. Drop the redraw and second present: corrupt, present once, and let it sit. wuss_redraw_dirty() then only repaints regions something else invalidates, which is the behaviour the test is meant to demonstrate. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- apps/wuss/main.c | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/apps/wuss/main.c b/apps/wuss/main.c index 51f79b6f..cc968dcb 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -600,6 +600,7 @@ static result_t run_wuss(const char *resources) { wuss_input_t ev; bool pixel_stress_pending = false; + bool garbage_pending = false; while (wuss_frontend_poll(frontend, &ev)) { @@ -613,6 +614,10 @@ static result_t run_wuss(const char *resources) wuss_redraw(wuss); break; + case wuss_INPUT_GARBAGE: + garbage_pending = true; + break; + case wuss_INPUT_PIXEL_STRESS: pixel_stress_pending = true; break; @@ -659,12 +664,31 @@ static result_t run_wuss(const char *resources) wuss_idle(wuss); - if (pixel_stress_pending) - pixel_stress(wuss, scr_width, scr_height); + if (garbage_pending) + { + /* corrupt the whole framebuffer and present it, then leave it alone -- + * wuss only repaints what it knows is dirty, so the junk stays put + * until something else invalidates the screen */ + unsigned char *p; + size_t n; + size_t i; + + p = pixels; + n = (size_t) rowbytes * scr_height; + for (i = 0; i < n; i++) + p[i] = (unsigned char) rand(); + + wuss_frontend_present(frontend, &bm); + } else - wuss_redraw_dirty(wuss); + { + if (pixel_stress_pending) + pixel_stress(wuss, scr_width, scr_height); + else + wuss_redraw_dirty(wuss); - wuss_frontend_present(frontend, &bm); + wuss_frontend_present(frontend, &bm); + } } /* ponytail: wuss_destroy() below force-closes every still-open window and From 5e37664916b6c9095f907bf096bfb8f1e0876f64 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Wed, 2 Sep 2026 20:09:44 +0100 Subject: [PATCH 194/287] feat(screen): add inverse fill patterns and put the Bayer run first Each named non-Bayer tile (HSTRIPE, VSTRIPE, DIAGONAL, DOTS, GRID, CROSSHATCH) now has a matching _INV entry holding the bitwise-NOT tile. Reorder the enum and tile table so the 65 Bayer levels come first, then the named tiles and their inverses. SOLID, EMPTY and GREY50 become aliases of BAYER64/BAYER0/BAYER32 rather than duplicate tiles. GREY50 is its own inverse so needs no _INV entry. External callers and the 0..__LIMIT swatch loop are unaffected. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- include/framebuf/screen.h | 44 ++++++++++++------- .../framebuf/screen/screen-fill-pattern.c | 38 ++++++++++------ 2 files changed, 52 insertions(+), 30 deletions(-) diff --git a/include/framebuf/screen.h b/include/framebuf/screen.h index 455a786e..53dbc2e9 100644 --- a/include/framebuf/screen.h +++ b/include/framebuf/screen.h @@ -101,29 +101,41 @@ void screen_fill_square(screen_t *scr, */ typedef enum screen_pattern { - screen_PATTERN_SOLID = 0, /**< Every pixel foreground. */ - screen_PATTERN_GREY50, /**< 50% checkerboard. */ - screen_PATTERN_HSTRIPE, /**< Horizontal bars. */ - screen_PATTERN_VSTRIPE, /**< Vertical bars. */ - screen_PATTERN_DIAGONAL, /**< Diagonal lines. */ - screen_PATTERN_DOTS, /**< Sparse dots. */ - screen_PATTERN_GRID, /**< Thin grid lines. */ - screen_PATTERN_CROSSHATCH, /**< Crossed thin lines. */ - /** * 8x8 ordered (Bayer) dither, one entry per coverage level 0 (empty) to 64 * (solid). Index by level as `screen_PATTERN_BAYER0 + level`; * `screen_PATTERN_BAYER_LIMIT` is one past the last. - * `screen_PATTERN_BAYER0` matches nothing else here, - * `screen_PATTERN_BAYER32` equals `screen_PATTERN_GREY50` and - * `screen_PATTERN_BAYER64` equals `screen_PATTERN_SOLID`; the duplicates - * are kept so the level arithmetic stays simple. */ - screen_PATTERN_BAYER0, + screen_PATTERN_BAYER0 = 0, screen_PATTERN_BAYER_LIMIT = screen_PATTERN_BAYER0 + 65, - screen_PATTERN__LIMIT = screen_PATTERN_BAYER_LIMIT - /**< Count of patterns; not itself a pattern. */ + /** + * A flat fill and a 50% checkerboard are just the ends and midpoint of the + * Bayer run, so they are aliases rather than separate tiles. `EMPTY` is + * the inverse of `SOLID`; `GREY50` is its own inverse. + */ + screen_PATTERN_EMPTY = screen_PATTERN_BAYER0, + screen_PATTERN_GREY50 = screen_PATTERN_BAYER0 + 32, + screen_PATTERN_SOLID = screen_PATTERN_BAYER0 + 64, + + /* The remaining named tiles follow the Bayer run. */ + screen_PATTERN_HSTRIPE = screen_PATTERN_BAYER_LIMIT, + /**< Horizontal bars. */ + screen_PATTERN_VSTRIPE, /**< Vertical bars. */ + screen_PATTERN_DIAGONAL, /**< Diagonal lines. */ + screen_PATTERN_DOTS, /**< Sparse dots. */ + screen_PATTERN_GRID, /**< Thin grid lines. */ + screen_PATTERN_CROSSHATCH, /**< Crossed thin lines. */ + + screen_PATTERN_HSTRIPE_INV, /**< Inverse of HSTRIPE. */ + screen_PATTERN_VSTRIPE_INV, /**< Inverse of VSTRIPE. */ + screen_PATTERN_DIAGONAL_INV, /**< Inverse of DIAGONAL. */ + screen_PATTERN_DOTS_INV, /**< Inverse of DOTS. */ + screen_PATTERN_GRID_INV, /**< Inverse of GRID. */ + screen_PATTERN_CROSSHATCH_INV, /**< Inverse of CROSSHATCH. */ + + screen_PATTERN__LIMIT + /**< Count of patterns; not itself a pattern. */ } screen_pattern_t; diff --git a/libraries/framebuf/screen/screen-fill-pattern.c b/libraries/framebuf/screen/screen-fill-pattern.c index 5d5ed8a7..9d735677 100644 --- a/libraries/framebuf/screen/screen-fill-pattern.c +++ b/libraries/framebuf/screen/screen-fill-pattern.c @@ -9,22 +9,16 @@ #include "framebuf/screen.h" -/* One byte per row, MSB = leftmost pixel. */ +/* One byte per row, MSB = leftmost pixel. + * + * The 65 Bayer levels come first so a caller can index by coverage level as + * screen_PATTERN_BAYER0 + level. BAYER0/BAYER32/BAYER64 double as the EMPTY, + * GREY50 and SOLID patterns (see the enum), so those have no rows of their + * own. The named non-Bayer tiles and their inverses follow. */ static const unsigned char patterns[screen_PATTERN__LIMIT][8] = { - { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }, /* SOLID */ - { 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55 }, /* GREY50 */ - { 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00 }, /* HSTRIPE */ - { 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA }, /* VSTRIPE */ - { 0x88, 0x44, 0x22, 0x11, 0x88, 0x44, 0x22, 0x11 }, /* DIAGONAL */ - { 0x88, 0x00, 0x22, 0x00, 0x88, 0x00, 0x22, 0x00 }, /* DOTS */ - { 0xFF, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80 }, /* GRID */ - { 0x80, 0x41, 0x22, 0x14, 0x08, 0x14, 0x22, 0x41 }, /* CROSSHATCH */ - /* 8x8 ordered (Bayer) dither, one row per coverage level 0..64. Level N sets - * the pixels whose threshold in the recursively-built 8x8 matrix is < N. - * BAYER32 equals GREY50 and BAYER64 equals SOLID; kept in the run so a - * caller can index by level as screen_PATTERN_BAYER0 + level. */ + * the pixels whose threshold in the recursively-built 8x8 matrix is < N. */ { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* BAYER0 */ { 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* BAYER1 */ { 0x80, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00 }, /* BAYER2 */ @@ -89,7 +83,23 @@ static const unsigned char patterns[screen_PATTERN__LIMIT][8] = { 0xFE, 0xFF, 0xFF, 0xFF, 0xEE, 0xFF, 0xFF, 0xFF }, /* BAYER61 */ { 0xFE, 0xFF, 0xFF, 0xFF, 0xEF, 0xFF, 0xFF, 0xFF }, /* BAYER62 */ { 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }, /* BAYER63 */ - { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF } /* BAYER64 */ + { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }, /* BAYER64 */ + + /* Named non-Bayer tiles. */ + { 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00 }, /* HSTRIPE */ + { 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA }, /* VSTRIPE */ + { 0x88, 0x44, 0x22, 0x11, 0x88, 0x44, 0x22, 0x11 }, /* DIAGONAL */ + { 0x88, 0x00, 0x22, 0x00, 0x88, 0x00, 0x22, 0x00 }, /* DOTS */ + { 0xFF, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80 }, /* GRID */ + { 0x80, 0x41, 0x22, 0x14, 0x08, 0x14, 0x22, 0x41 }, /* CROSSHATCH */ + + /* Inverses of the six above (bitwise NOT of each row). */ + { 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF }, /* HSTRIPE_INV */ + { 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55 }, /* VSTRIPE_INV */ + { 0x77, 0xBB, 0xDD, 0xEE, 0x77, 0xBB, 0xDD, 0xEE }, /* DIAGONAL_INV */ + { 0x77, 0xFF, 0xDD, 0xFF, 0x77, 0xFF, 0xDD, 0xFF }, /* DOTS_INV */ + { 0x00, 0x7F, 0x7F, 0x7F, 0x7F, 0x7F, 0x7F, 0x7F }, /* GRID_INV */ + { 0x7F, 0xBE, 0xDD, 0xEB, 0xF7, 0xEB, 0xDD, 0xBE } /* CROSSHATCH_INV */ }; /* the enum leaves the 65 Bayer levels unnamed past BAYER0, so keep the table From 244cd2b1b777106c202637b9ef132c37ca8058fa Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Wed, 2 Sep 2026 20:13:12 +0100 Subject: [PATCH 195/287] fix(wuss): draw radio/option state changes and menu ticks on click Two regressions in the icon-click path: - The MOUSE_UP early-release added in 1d7d305 cleared an icon's PRESSED state before the icon-hit path in wuss_mouse_click, so wuss__icon_select never latched a radio/option toggle on a completed click. Only release early when the up lands away from the pressed icon; an up back on it stays held for the normal completion path. - wuss_icon_set_selected on a MENU_ENTRY icon was a no-op: wuss__icon_select early-returned for every type except RADIO and OPTION, so a ticked menu item never drew its tick. Accept MENU_ENTRY too. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- libraries/wuss/icon/set-selected.c | 3 ++- libraries/wuss/mouse-click.c | 32 ++++++++++++++++++++++-------- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/libraries/wuss/icon/set-selected.c b/libraries/wuss/icon/set-selected.c index 6344ac65..27430d27 100644 --- a/libraries/wuss/icon/set-selected.c +++ b/libraries/wuss/icon/set-selected.c @@ -13,7 +13,8 @@ void wuss__icon_select(wuss_icon_t *icon, int selected) assert(icon != NULL); if (icon->type != wuss_ICON_TYPE_RADIO && - icon->type != wuss_ICON_TYPE_OPTION) + icon->type != wuss_ICON_TYPE_OPTION && + icon->type != wuss_ICON_TYPE_MENU_ENTRY) return; selected = selected ? 1 : 0; diff --git a/libraries/wuss/mouse-click.c b/libraries/wuss/mouse-click.c index be676367..8e79994b 100644 --- a/libraries/wuss/mouse-click.c +++ b/libraries/wuss/mouse-click.c @@ -18,18 +18,34 @@ result_t wuss_mouse_click(wuss_t *wuss, wuss->pointer = p; #ifdef WUSS_ICONS - /* Release a held button icon on any MOUSE_UP, before the hit-test picks a - * window: the up may land on a window that opened over the icon's owner on - * MOUSE_DOWN, so wuss__window_at would never reach the pressed icon. */ + /* Release a held button icon on a MOUSE_UP that will NOT reach it through the + * normal window hit-test: the up may land on a window that opened over the + * icon's owner on MOUSE_DOWN. When the up does land back on the pressed icon, + * leave it held so the icon-hit path below completes the click (latch + * radio/option state, toggle a menu tick). */ if (action == wuss_MOUSE_UP && wuss->pressed_icon != NULL) { - wuss_icon_t *pressed = wuss->pressed_icon; + wuss_icon_t *pressed = wuss->pressed_icon; + wuss_window_t *presswin = pressed->window; + box_t content; + point_t doc_point; + int still_over; - wuss->pressed_icon = NULL; - if (wuss__icon_pressed(pressed)) + wuss__content_box(presswin, &content); + doc_point.x = x - content.x0 + presswin->scroll.x; + doc_point.y = y - content.y0 + presswin->scroll.y; + + still_over = (wuss__window_at(wuss, p) == presswin && + wuss__icon_hit_test(presswin, doc_point) == pressed); + + if (!still_over) { - wuss__icon_set_state(pressed, wuss_ICON_STATE_PRESSED, 0); - wuss__icon_invalidate(pressed); + wuss->pressed_icon = NULL; + if (wuss__icon_pressed(pressed)) + { + wuss__icon_set_state(pressed, wuss_ICON_STATE_PRESSED, 0); + wuss__icon_invalidate(pressed); + } } } #endif From 64ba979f5d92f3f5bf2f85d42f607762e416e2a1 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Wed, 2 Sep 2026 22:52:26 +0100 Subject: [PATCH 196/287] fix(wuss): only act on Select clicks in task handlers, not Menu checker, text, swatches, porter-duff, sofa and curve treated any MOUSE_DOWN as their action click, so a Menu press cycled patterns, toggled resize/spin, swapped the backdrop, advanced the demo or started a control-point drag instead of falling through to wuss's menu handling. Gate each on wuss_BUTTON_SELECT, matching ball, lissajous and chars. sofa's bare else (which caught Menu) is narrowed to else if SELECT; curve threads the button through to gate the MOUSE_DOWN drag-start only, leaving MOVE/UP untouched. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- libraries/wuss/test/tasks/checker.c | 3 ++- libraries/wuss/test/tasks/curve.c | 6 +++++- libraries/wuss/test/tasks/porter-duff.c | 3 ++- libraries/wuss/test/tasks/sofa.c | 2 +- libraries/wuss/test/tasks/swatches.c | 3 ++- libraries/wuss/test/tasks/text.c | 3 ++- 6 files changed, 14 insertions(+), 6 deletions(-) diff --git a/libraries/wuss/test/tasks/checker.c b/libraries/wuss/test/tasks/checker.c index 00735844..2d284fd2 100644 --- a/libraries/wuss/test/tasks/checker.c +++ b/libraries/wuss/test/tasks/checker.c @@ -167,7 +167,8 @@ result_t checker_handle(wuss_window_t *window, return checker_redraw(window, event, task_data); case wuss_EVENT_MOUSE: - if (event->data.mouse.action != wuss_MOUSE_DOWN) + if (event->data.mouse.action != wuss_MOUSE_DOWN || + !(event->data.mouse.button & wuss_BUTTON_SELECT)) return result_OK; return checker_mouse(window, task_data); diff --git a/libraries/wuss/test/tasks/curve.c b/libraries/wuss/test/tasks/curve.c index a5409575..38e7b34a 100644 --- a/libraries/wuss/test/tasks/curve.c +++ b/libraries/wuss/test/tasks/curve.c @@ -121,6 +121,7 @@ static result_t curve_mouse(curve_task_t *task, wuss_mouse_action_t action, int x, int y, + wuss_button_t button, wuss_window_t *window) { int i; @@ -131,6 +132,8 @@ static result_t curve_mouse(curve_task_t *task, switch (action) { case wuss_MOUSE_DOWN: + if (!(button & wuss_BUTTON_SELECT)) + break; for (i = 0; i < CURVE_NCONTROLPTS; i++) { if (blob_hit(&task->points[i], x, y)) @@ -184,7 +187,8 @@ result_t curve_handle(wuss_window_t *window, case wuss_EVENT_MOUSE: return curve_mouse(task, event->data.mouse.action, - event->data.mouse.point.x, event->data.mouse.point.y, window); + event->data.mouse.point.x, event->data.mouse.point.y, + event->data.mouse.button, window); case wuss_EVENT_SCROLL: return curve_scroll(task, event->data.scroll.delta, window); diff --git a/libraries/wuss/test/tasks/porter-duff.c b/libraries/wuss/test/tasks/porter-duff.c index 5e516645..cf27082b 100644 --- a/libraries/wuss/test/tasks/porter-duff.c +++ b/libraries/wuss/test/tasks/porter-duff.c @@ -396,7 +396,8 @@ result_t porter_duff_handle(wuss_window_t *window, return porter_duff_redraw(event, task_data); case wuss_EVENT_MOUSE: - if (event->data.mouse.action != wuss_MOUSE_DOWN) + if (event->data.mouse.action != wuss_MOUSE_DOWN || + !(event->data.mouse.button & wuss_BUTTON_SELECT)) return result_OK; return porter_duff_mouse(window, task_data); diff --git a/libraries/wuss/test/tasks/sofa.c b/libraries/wuss/test/tasks/sofa.c index 63f2dc18..b827ca2f 100644 --- a/libraries/wuss/test/tasks/sofa.c +++ b/libraries/wuss/test/tasks/sofa.c @@ -523,7 +523,7 @@ static result_t sofa_mouse(wuss_window_t *window, sc->turns = 0; wuss_window_invalidate_all(window); } - else + else if (button & wuss_BUTTON_SELECT) { sc->spinning = !sc->spinning; } diff --git a/libraries/wuss/test/tasks/swatches.c b/libraries/wuss/test/tasks/swatches.c index eed7cf3e..3f904cc3 100644 --- a/libraries/wuss/test/tasks/swatches.c +++ b/libraries/wuss/test/tasks/swatches.c @@ -137,7 +137,8 @@ static result_t swatches_click(swatches_task_t *task, point_t pt; int p; - if (event->data.mouse.action != wuss_MOUSE_DOWN) + if (event->data.mouse.action != wuss_MOUSE_DOWN || + !(event->data.mouse.button & wuss_BUTTON_SELECT)) return result_OK; pt = event->data.mouse.point; diff --git a/libraries/wuss/test/tasks/text.c b/libraries/wuss/test/tasks/text.c index b02e02f2..b66c686b 100644 --- a/libraries/wuss/test/tasks/text.c +++ b/libraries/wuss/test/tasks/text.c @@ -161,7 +161,8 @@ result_t text_handle(wuss_window_t *window, return text_redraw(event, task_data); case wuss_EVENT_MOUSE: - if (event->data.mouse.action != wuss_MOUSE_DOWN) + if (event->data.mouse.action != wuss_MOUSE_DOWN || + !(event->data.mouse.button & wuss_BUTTON_SELECT)) return result_OK; return text_mouse(task_data); From 1836fbedf11fa0bf37fe19232d3081d4f69b111d Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Wed, 2 Sep 2026 22:55:36 +0100 Subject: [PATCH 197/287] feat(wuss): add an analogue clock task A round-faced clock drawn with the Wu line primitive: a 90-sided polygon for the bezel (screen.h has no circle), 60 minute ticks with every fifth drawn longer, and hour, minute and second hands read from localtime() and advanced on every idle tick like sofa. A Select click shows or hides the second hand; Menu is left for wuss. Wired into the task launcher and CMakeLists. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- CMakeLists.txt | 1 + apps/wuss/main.c | 15 +- libraries/wuss/test/tasks/clock.c | 229 ++++++++++++++++++++++++++++++ libraries/wuss/test/tasks/clock.h | 31 ++++ 4 files changed, 275 insertions(+), 1 deletion(-) create mode 100644 libraries/wuss/test/tasks/clock.c create mode 100644 libraries/wuss/test/tasks/clock.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 702c879e..33a7a5bb 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -682,6 +682,7 @@ if(BUILD_APPS) libraries/wuss/test/tasks/blank.c libraries/wuss/test/tasks/chars.c libraries/wuss/test/tasks/checker.c + libraries/wuss/test/tasks/clock.c libraries/wuss/test/tasks/curve.c libraries/wuss/test/tasks/gradient.c libraries/wuss/test/tasks/icons.c diff --git a/apps/wuss/main.c b/apps/wuss/main.c index cc968dcb..cda2f6f6 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -30,6 +30,7 @@ #include "tasks/blank.h" #include "tasks/chars.h" #include "tasks/checker.h" +#include "tasks/clock.h" #include "tasks/curve.h" #include "tasks/gradient.h" #include "tasks/icons.h" @@ -159,6 +160,17 @@ static result_t spawn_checker(void) return result_OK; } +static result_t spawn_clock(void) +{ + clock_task_t *t = calloc(1, sizeof(*t)); + result_t rc; + if (t == NULL) return result_OOM; + rc = clock_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_curve(void) { curve_task_t *t = calloc(1, sizeof(*t)); @@ -363,6 +375,7 @@ static const wuss_menu_item_t g_task_items[] = { "Blank", wuss_MENU_ITEM_NONE, NULL }, { "Chars", wuss_MENU_ITEM_NONE, NULL }, { "Checker", wuss_MENU_ITEM_NONE, NULL }, + { "Clock", wuss_MENU_ITEM_NONE, NULL }, { "Curve", wuss_MENU_ITEM_NONE, NULL }, { "Gradient", wuss_MENU_ITEM_NONE, NULL }, { "Icons", wuss_MENU_ITEM_NONE, NULL }, @@ -386,7 +399,7 @@ static result_t spawn_quit(void) static const task_spawn_fn_t g_task_spawn[] = { - spawn_ball, spawn_blank, spawn_chars, spawn_checker, spawn_curve, + spawn_ball, spawn_blank, spawn_chars, spawn_checker, spawn_clock, spawn_curve, spawn_gradient, spawn_icons, spawn_image, spawn_lissajous, spawn_palette, spawn_porter_duff, spawn_sofa, spawn_swatches, spawn_text, spawn_menu, spawn_menu_desc, spawn_quit diff --git a/libraries/wuss/test/tasks/clock.c b/libraries/wuss/test/tasks/clock.c new file mode 100644 index 00000000..2cbe5a11 --- /dev/null +++ b/libraries/wuss/test/tasks/clock.c @@ -0,0 +1,229 @@ +/* wuss/test/tasks/clock.c -- analogue clock task */ + +#ifdef WUSS_APP + +#include <stdlib.h> +#include <time.h> + +#include <math.h> + +#ifdef FORTIFY +#include "fortify/fortify.h" +#endif + +#include "base/utils.h" +#include "framebuf/screen.h" +#include "geom/box.h" +#include "utils/fxp.h" + +#include "clock.h" + +#ifndef M_PI +#define M_PI 3.14159265358979323846 +#endif + +#define CLOCK_BEZEL_SEGMENTS 90 /* sides of the polygon standing in for the round bezel */ +#define CLOCK_FACE_FRACTION 0.92 /* bezel radius as a fraction of half the smaller side */ +#define CLOCK_HOUR_TICK 0.10 /* hour-tick length, fraction of face radius */ +#define CLOCK_MINUTE_TICK 0.05 /* minute-tick length, fraction of face radius */ +#define CLOCK_HOUR_HAND 0.55 /* hand lengths, fraction of face radius */ +#define CLOCK_MINUTE_HAND 0.80 +#define CLOCK_SECOND_HAND 0.88 + +/* a screen-space point in fix8_t, for screen_draw_line_wu_fix8 */ +typedef struct fix8_point { fix8_t x, y; } fix8_point_t; + +/* point on a circle of radius r about (cx,cy); angle 0 points straight up + * (12 o'clock) and increases clockwise, matching a clock face */ +static fix8_point_t clock_polar(double cx, double cy, double r, double angle) +{ + fix8_point_t p; + + p.x = FLOAT_TO_FIX8(cx + r * sin(angle)); + p.y = FLOAT_TO_FIX8(cy - r * cos(angle)); + + return p; +} + +static void clock_draw_hand(screen_t *scr, + double cx, + double cy, + double r, + double angle, + colour_t colour) +{ + fix8_point_t tip; + + tip = clock_polar(cx, cy, r, angle); + screen_draw_line_wu_fix8(scr, + FLOAT_TO_FIX8(cx), FLOAT_TO_FIX8(cy), + tip.x, tip.y, + colour); +} + +static result_t clock_create_window(wuss_t *wuss, + clock_task_t *task, + wuss_task_t *delegate) +{ + return wuss_window_create_placed(delegate, + SIZE2D(160, 160), + "Clock", + wuss_WINDOW_NONE, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + SIZE2D(160, 160), + SIZE2D(0, 0), + &task->window); +} + +result_t clock_create(wuss_t *wuss, clock_task_t *task) +{ + wuss_task_t *delegate; + wuss_task_desc_t delegate_desc; + result_t rc; + + task->bg = colour_rgb(0x1D, 0x2B, 0x53); + task->bezel = colour_rgb(0xFF, 0xF1, 0xE8); + task->hand = colour_rgb(0xFF, 0xF1, 0xE8); + task->second_hand = colour_rgb(0xFF, 0x00, 0x4D); + task->show_second = true; + + /* clock_redraw paints its own background every frame */ + delegate_desc.handle = clock_handle; + delegate_desc.task_data = task; + delegate_desc.name = "clock"; + 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 = clock_create_window(wuss, task, delegate); + if (rc != result_OK) + wuss_task_destroy(delegate); /* unregister; its QUIT frees the task block */ + + return rc; +} + +static result_t clock_redraw(const wuss_event_t *event, void *task_data) +{ + clock_task_t *cc; + screen_t *scr; + const box_t *content, *bounds; + time_t now; + struct tm *lt; + double cx, cy, r; + double hour_angle, minute_angle, second_angle; + int i, sx, sy; + + cc = task_data; + + scr = event->data.redraw.scr; + content = event->data.redraw.content; + bounds = event->data.redraw.bounds; + sx = event->data.redraw.scroll.x; + sy = event->data.redraw.scroll.y; + + screen_fill_rect(scr, + content->x0, + content->y0, box_size(content), + cc->bg); + + cx = bounds->x0 - sx + (bounds->x1 - bounds->x0) / 2.0; + cy = bounds->y0 - sy + (bounds->y1 - bounds->y0) / 2.0; + r = MIN(bounds->x1 - bounds->x0, bounds->y1 - bounds->y0) + / 2.0 * CLOCK_FACE_FRACTION; + + /* bezel: a many-sided polygon standing in for the circle */ + { + fix8_point_t prev, first; + + first = clock_polar(cx, cy, r, 0.0); + prev = first; + for (i = 1; i <= CLOCK_BEZEL_SEGMENTS; i++) + { + fix8_point_t p; + + p = clock_polar(cx, cy, r, + i * 2.0 * M_PI / CLOCK_BEZEL_SEGMENTS); + screen_draw_line_wu_fix8(scr, prev.x, prev.y, p.x, p.y, cc->bezel); + prev = p; + } + screen_draw_line_wu_fix8(scr, prev.x, prev.y, first.x, first.y, cc->bezel); + } + + /* 60 minute ticks, every fifth one an hour tick drawn longer */ + for (i = 0; i < 60; i++) + { + double angle, inner; + fix8_point_t a, b; + + angle = i * 2.0 * M_PI / 60.0; + inner = (i % 5 == 0) ? (1.0 - CLOCK_HOUR_TICK) : (1.0 - CLOCK_MINUTE_TICK); + a = clock_polar(cx, cy, r * inner, angle); + b = clock_polar(cx, cy, r, angle); + screen_draw_line_wu_fix8(scr, a.x, a.y, b.x, b.y, cc->bezel); + } + + now = time(NULL); + lt = localtime(&now); + + second_angle = lt->tm_sec * 2.0 * M_PI / 60.0; + minute_angle = (lt->tm_min + lt->tm_sec / 60.0) * 2.0 * M_PI / 60.0; + hour_angle = ((lt->tm_hour % 12) + lt->tm_min / 60.0) * 2.0 * M_PI / 12.0; + + clock_draw_hand(scr, cx, cy, r * CLOCK_HOUR_HAND, hour_angle, cc->hand); + clock_draw_hand(scr, cx, cy, r * CLOCK_MINUTE_HAND, minute_angle, cc->hand); + if (cc->show_second) + clock_draw_hand(scr, cx, cy, r * CLOCK_SECOND_HAND, second_angle, + cc->second_hand); + + return result_OK; +} + +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); + } + + return result_OK; +} + +result_t clock_handle(wuss_window_t *window, + const wuss_event_t *event, + void *task_data) +{ + clock_task_t *cc; + + cc = task_data; + + NOT_USED(window); + + switch (event->kind) + { + case wuss_EVENT_REDRAW: + return clock_redraw(event, task_data); + + case wuss_EVENT_MOUSE: + if (event->data.mouse.action != wuss_MOUSE_DOWN) + return result_OK; + return clock_mouse(cc, event->data.mouse.button); + + case wuss_EVENT_IDLE: + wuss_window_invalidate_all(cc->window); + return result_OK; + + case wuss_EVENT_QUIT: + free(cc); /* 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/clock.h b/libraries/wuss/test/tasks/clock.h new file mode 100644 index 00000000..2c32f4b6 --- /dev/null +++ b/libraries/wuss/test/tasks/clock.h @@ -0,0 +1,31 @@ +/* wuss/test/tasks/clock.h -- analogue clock task */ + +#ifndef TASKS_CLOCK_H +#define TASKS_CLOCK_H + +#ifdef WUSS_APP + +#include <stdbool.h> + +#include "framebuf/colour.h" +#include "wuss/window.h" + +/* a round-faced analogue clock: a tick-marked bezel with hour, minute and + * second hands read from the system clock and advanced on every idle tick. + * A Select click shows or hides the second hand. */ +typedef struct clock_task +{ + wuss_window_t *window; + colour_t bg, bezel, hand, second_hand; + bool show_second; +} +clock_task_t; + +wuss_window_fn_t clock_handle; + +/* create the clock window against the given wuss instance */ +result_t clock_create(wuss_t *wuss, clock_task_t *task); + +#endif /* WUSS_APP */ + +#endif /* TASKS_CLOCK_H */ From f1dca91376b456c1f5c54681a49c5dfd905d0bdd Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Wed, 2 Sep 2026 23:04:41 +0100 Subject: [PATCH 198/287] feat(wuss): letter the clock face with bmfont numerals clock_create now takes the demo's bmfont; clock_redraw draws the 1..12 numerals on a ring inside the hour ticks, centred with bmfont_measure and bmfont_get_info and a transparent background. Hands shortened so they clear the numerals. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- apps/wuss/main.c | 2 +- libraries/wuss/test/tasks/clock.c | 50 ++++++++++++++++++++++++++++--- libraries/wuss/test/tasks/clock.h | 13 ++++---- 3 files changed, 55 insertions(+), 10 deletions(-) diff --git a/apps/wuss/main.c b/apps/wuss/main.c index cda2f6f6..f0ead35f 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -165,7 +165,7 @@ static result_t spawn_clock(void) clock_task_t *t = calloc(1, sizeof(*t)); result_t rc; if (t == NULL) return result_OOM; - rc = clock_create(g.wuss, t); + rc = clock_create(g.wuss, g.daydream_font, t); if (rc != result_OK) return rc; if (t->window == NULL) { free(t); return rc; } return result_OK; diff --git a/libraries/wuss/test/tasks/clock.c b/libraries/wuss/test/tasks/clock.c index 2cbe5a11..a9e81add 100644 --- a/libraries/wuss/test/tasks/clock.c +++ b/libraries/wuss/test/tasks/clock.c @@ -2,7 +2,9 @@ #ifdef WUSS_APP +#include <stdio.h> #include <stdlib.h> +#include <string.h> #include <time.h> #include <math.h> @@ -12,8 +14,10 @@ #endif #include "base/utils.h" +#include "framebuf/bmfont.h" #include "framebuf/screen.h" #include "geom/box.h" +#include "geom/point.h" #include "utils/fxp.h" #include "clock.h" @@ -26,9 +30,10 @@ #define CLOCK_FACE_FRACTION 0.92 /* bezel radius as a fraction of half the smaller side */ #define CLOCK_HOUR_TICK 0.10 /* hour-tick length, fraction of face radius */ #define CLOCK_MINUTE_TICK 0.05 /* minute-tick length, fraction of face radius */ -#define CLOCK_HOUR_HAND 0.55 /* hand lengths, fraction of face radius */ -#define CLOCK_MINUTE_HAND 0.80 -#define CLOCK_SECOND_HAND 0.88 +#define CLOCK_NUMERAL_RING 0.75 /* numeral-centre radius, fraction of face radius */ +#define CLOCK_HOUR_HAND 0.50 /* hand lengths, fraction of face radius */ +#define CLOCK_MINUTE_HAND 0.68 +#define CLOCK_SECOND_HAND 0.80 /* a screen-space point in fix8_t, for screen_draw_line_wu_fix8 */ typedef struct fix8_point { fix8_t x, y; } fix8_point_t; @@ -61,6 +66,27 @@ static void clock_draw_hand(screen_t *scr, colour); } +/* draw text centred on (x,y), background transparent so it sits on the face */ +static void clock_draw_centred(bmfont_t *font, + screen_t *scr, + const char *text, + double x, + double y, + colour_t fg) +{ + bmfont_width_t width; + point_t pos; + int len, fh; + + len = (int) strlen(text); + bmfont_measure(font, text, len, INT_MAX, NULL, &width); + bmfont_get_info(font, NULL, &fh); + + pos.x = (int) (x - width / 2.0); + pos.y = (int) (y - fh / 2.0); + bmfont_draw(font, scr, text, len, fg, colour_rgba(0, 0, 0, 0), &pos, NULL); +} + static result_t clock_create_window(wuss_t *wuss, clock_task_t *task, wuss_task_t *delegate) @@ -75,12 +101,13 @@ static result_t clock_create_window(wuss_t *wuss, &task->window); } -result_t clock_create(wuss_t *wuss, clock_task_t *task) +result_t clock_create(wuss_t *wuss, bmfont_t *font, clock_task_t *task) { wuss_task_t *delegate; wuss_task_desc_t delegate_desc; result_t rc; + task->font = font; task->bg = colour_rgb(0x1D, 0x2B, 0x53); task->bezel = colour_rgb(0xFF, 0xF1, 0xE8); task->hand = colour_rgb(0xFF, 0xF1, 0xE8); @@ -166,6 +193,21 @@ static result_t clock_redraw(const wuss_event_t *event, void *task_data) screen_draw_line_wu_fix8(scr, a.x, a.y, b.x, b.y, cc->bezel); } + /* the 1..12 numerals, centred on a ring inside the hour ticks */ + for (i = 1; i <= 12; i++) + { + double angle; + fix8_point_t at; + char buf[3]; + + angle = i * 2.0 * M_PI / 12.0; + at = clock_polar(cx, cy, r * CLOCK_NUMERAL_RING, angle); + snprintf(buf, sizeof(buf), "%d", i); + clock_draw_centred(cc->font, scr, buf, + FIX8_ROUND_TO_INT(at.x), FIX8_ROUND_TO_INT(at.y), + cc->bezel); + } + now = time(NULL); lt = localtime(&now); diff --git a/libraries/wuss/test/tasks/clock.h b/libraries/wuss/test/tasks/clock.h index 2c32f4b6..8fa72414 100644 --- a/libraries/wuss/test/tasks/clock.h +++ b/libraries/wuss/test/tasks/clock.h @@ -7,15 +7,17 @@ #include <stdbool.h> +#include "framebuf/bmfont.h" #include "framebuf/colour.h" #include "wuss/window.h" -/* a round-faced analogue clock: a tick-marked bezel with hour, minute and - * second hands read from the system clock and advanced on every idle tick. - * A Select click shows or hides the second hand. */ +/* a round-faced analogue clock: a tick-marked bezel with 1..12 numerals and + * hour, minute and second hands read from the system clock and advanced on + * every idle tick. A Select click shows or hides the second hand. */ typedef struct clock_task { wuss_window_t *window; + bmfont_t *font; /* borrowed; the numerals are drawn with it */ colour_t bg, bezel, hand, second_hand; bool show_second; } @@ -23,8 +25,9 @@ clock_task_t; wuss_window_fn_t clock_handle; -/* create the clock window against the given wuss instance */ -result_t clock_create(wuss_t *wuss, clock_task_t *task); +/* create the clock window against the given wuss instance, lettering the + * face with the given (caller-owned) font */ +result_t clock_create(wuss_t *wuss, bmfont_t *font, clock_task_t *task); #endif /* WUSS_APP */ From 855179589a639532142e7420a71cd6a4b3776a4a Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Wed, 2 Sep 2026 23:32:44 +0100 Subject: [PATCH 199/287] feat(screen): add circle outline and fill primitives screen_draw_circle (integer midpoint) and screen_fill_circle (scanline disc), both clipped via the existing screen_set_pixel / screen_fill_rect paths. Tests cover axis points, interior, r=0 and the screen clip. Replaces the ad-hoc polygon and filled-square approximations the wuss clock and ball tasks were using. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- CMakeLists.txt | 1 + include/framebuf/screen.h | 34 ++++++ .../framebuf/screen/screen-draw-circle.c | 109 ++++++++++++++++++ libraries/framebuf/screen/test/screen-test.c | 99 +++++++++++++++- 4 files changed, 242 insertions(+), 1 deletion(-) create mode 100644 libraries/framebuf/screen/screen-draw-circle.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 33a7a5bb..89569a00 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -248,6 +248,7 @@ set(FRAMEBUF_SOURCES libraries/framebuf/screen/screen.c libraries/framebuf/screen/screen-copy-rect.c libraries/framebuf/screen/screen-draw.c + libraries/framebuf/screen/screen-draw-circle.c libraries/framebuf/screen/screen-draw-ninepatch.c libraries/framebuf/screen/screen-fill-pattern.c libraries/framebuf/span-registry/get.c diff --git a/include/framebuf/screen.h b/include/framebuf/screen.h index 53dbc2e9..be9e7bc9 100644 --- a/include/framebuf/screen.h +++ b/include/framebuf/screen.h @@ -304,6 +304,40 @@ void screen_draw_rect(screen_t *scr, size2d_t size, colour_t colour); +/** + * Draws a one-pixel unfilled circle outline (integer midpoint algorithm, no + * anti-aliasing). Clipped to the screen's clip region. A negative radius + * draws nothing; a zero radius draws a single pixel at the centre. + * + * \param[in] scr Screen to draw upon. + * \param[in] cx X coordinate of the centre. + * \param[in] cy Y coordinate of the centre. + * \param[in] r Radius in pixels. + * \param[in] colour Colour of the outline. + */ +void screen_draw_circle(screen_t *scr, + int cx, + int cy, + int r, + colour_t colour); + +/** + * Draws a solid filled disc of the given radius. Clipped to the screen's + * clip region. A negative radius draws nothing; a zero radius draws a single + * pixel at the centre. + * + * \param[in] scr Screen to draw upon. + * \param[in] cx X coordinate of the centre. + * \param[in] cy Y coordinate of the centre. + * \param[in] r Radius in pixels. + * \param[in] colour Colour of the disc. + */ +void screen_fill_circle(screen_t *scr, + int cx, + int cy, + int r, + colour_t colour); + /** * Draws a stippled line: `on` pixels drawn, then `off` skipped, repeating * along the line. Bresenham stepping, so the dash period is measured in diff --git a/libraries/framebuf/screen/screen-draw-circle.c b/libraries/framebuf/screen/screen-draw-circle.c new file mode 100644 index 00000000..153547d7 --- /dev/null +++ b/libraries/framebuf/screen/screen-draw-circle.c @@ -0,0 +1,109 @@ +/* framebuf/screen/screen-draw-circle.c -- circle outline and fill */ + +#include <stdlib.h> + +#include "base/utils.h" +#include "geom/box.h" +#include "geom/size.h" + +#include "framebuf/screen.h" + +/* One octant of a midpoint circle, mirrored to the other seven. screen_set_pixel + * clips each plot, so nothing here needs its own bounds check. */ +void screen_draw_circle(screen_t *scr, + int cx, + int cy, + int r, + colour_t colour) +{ + int x, y, err; + + if (r < 0) + return; + + if (r == 0) + { + screen_set_pixel(scr, cx, cy, colour); + return; + } + + x = r; + y = 0; + err = 1 - r; + + while (x >= y) + { + screen_set_pixel(scr, cx + x, cy + y, colour); + screen_set_pixel(scr, cx - x, cy + y, colour); + screen_set_pixel(scr, cx + x, cy - y, colour); + screen_set_pixel(scr, cx - x, cy - y, colour); + screen_set_pixel(scr, cx + y, cy + x, colour); + screen_set_pixel(scr, cx - y, cy + x, colour); + screen_set_pixel(scr, cx + y, cy - x, colour); + screen_set_pixel(scr, cx - y, cy - x, colour); + + y++; + if (err < 0) + { + err += 2 * y + 1; + } + else + { + x--; + err += 2 * (y - x) + 1; + } + } +} + +/* Same octant stepping as the outline, but each step emits a pair of solid + * horizontal runs (via screen_fill_rect, which clips) rather than eight + * points. Runs from the two octant families cover every scanline of the + * disc exactly once. */ +void screen_fill_circle(screen_t *scr, + int cx, + int cy, + int r, + colour_t colour) +{ + int x, y, err; + + if (r < 0) + return; + + if (r == 0) + { + screen_set_pixel(scr, cx, cy, colour); + return; + } + + x = r; + y = 0; + err = 1 - r; + + while (x >= y) + { + /* the wide pair: rows cy +/- y, spanning -x..+x */ + screen_fill_rect(scr, cx - x, cy + y, SIZE2D(2 * x + 1, 1), colour); + if (y != 0) + screen_fill_rect(scr, cx - x, cy - y, SIZE2D(2 * x + 1, 1), colour); + + /* the tall pair: rows cy +/- x, spanning -y..+y; skip while it would + * fall inside the wide pair's rows to avoid overdraw */ + if (x != y) + { + screen_fill_rect(scr, cx - y, cy + x, SIZE2D(2 * y + 1, 1), colour); + screen_fill_rect(scr, cx - y, cy - x, SIZE2D(2 * y + 1, 1), colour); + } + + y++; + if (err < 0) + { + err += 2 * y + 1; + } + else + { + x--; + err += 2 * (y - x) + 1; + } + } +} diff --git a/libraries/framebuf/screen/test/screen-test.c b/libraries/framebuf/screen/test/screen-test.c index 2ffcdb38..fb689da3 100644 --- a/libraries/framebuf/screen/test/screen-test.c +++ b/libraries/framebuf/screen/test/screen-test.c @@ -632,6 +632,101 @@ static result_t test_draw_rect(void) /* ----------------------------------------------------------------------- */ +static result_t test_draw_circle(void) +{ + static testscreen_t ts; + static testscreen_t enc; + + int fg, bg; + + fg = (int) np_encode(&enc, 0, 255, 0); + bg = (int) (pixelfmt_bgrx8888_t) BACKGROUND; + + /* r=10 about (32,32): the four axis points lie on the circle, the centre + * and a point just inside the rim do not. */ + testscreen_init(&ts); + screen_draw_circle(&ts.scr, 32, 32, 10, colour_rgb(0, 255, 0)); + + if (np_at(&ts, 42, 32) != fg || np_at(&ts, 22, 32) != fg || + np_at(&ts, 32, 42) != fg || np_at(&ts, 32, 22) != fg) + { + printf("screen: draw_circle missing an axis point\n"); + return result_TEST_FAILED; + } + if (np_at(&ts, 32, 32) != bg || np_at(&ts, 38, 32) != bg) + { + printf("screen: draw_circle filled its interior\n"); + return result_TEST_FAILED; + } + + /* r=0 draws the centre pixel only. */ + testscreen_init(&ts); + screen_draw_circle(&ts.scr, 5, 5, 0, colour_rgb(0, 255, 0)); + if (np_at(&ts, 5, 5) != fg || np_at(&ts, 6, 5) != bg) + { + printf("screen: draw_circle r=0 wrong\n"); + return result_TEST_FAILED; + } + + /* Honours the screen clip: right half of the circle is masked off. */ + testscreen_init(&ts); + ts.scr.clip = (box_t) { 0, 0, 32, 64 }; + screen_draw_circle(&ts.scr, 32, 32, 10, colour_rgb(0, 255, 0)); + if (np_at(&ts, 22, 32) != fg || np_at(&ts, 42, 32) != bg) + { + printf("screen: draw_circle ignored the screen clip\n"); + return result_TEST_FAILED; + } + + return result_TEST_PASSED; +} + +/* ----------------------------------------------------------------------- */ + +static result_t test_fill_circle(void) +{ + static testscreen_t ts; + static testscreen_t enc; + + int fg, bg; + + fg = (int) np_encode(&enc, 0, 255, 0); + bg = (int) (pixelfmt_bgrx8888_t) BACKGROUND; + + /* r=10 about (32,32): centre, rim and axis extremes lit; one pixel beyond + * the rim is background. */ + testscreen_init(&ts); + screen_fill_circle(&ts.scr, 32, 32, 10, colour_rgb(0, 255, 0)); + + if (np_at(&ts, 32, 32) != fg || /* centre */ + np_at(&ts, 38, 32) != fg || /* interior */ + np_at(&ts, 42, 32) != fg || np_at(&ts, 22, 32) != fg || + np_at(&ts, 32, 42) != fg || np_at(&ts, 32, 22) != fg) + { + printf("screen: fill_circle left a hole\n"); + return result_TEST_FAILED; + } + if (np_at(&ts, 43, 32) != bg || np_at(&ts, 32, 43) != bg) + { + printf("screen: fill_circle spilled past the rim\n"); + return result_TEST_FAILED; + } + + /* Honours the screen clip. */ + testscreen_init(&ts); + ts.scr.clip = (box_t) { 0, 0, 32, 64 }; + screen_fill_circle(&ts.scr, 32, 32, 10, colour_rgb(0, 255, 0)); + if (np_at(&ts, 28, 32) != fg || np_at(&ts, 36, 32) != bg) + { + printf("screen: fill_circle ignored the screen clip\n"); + return result_TEST_FAILED; + } + + return result_TEST_PASSED; +} + +/* ----------------------------------------------------------------------- */ + result_t screen_test(const char *resources) { typedef result_t (*screentestfn)(void); @@ -645,7 +740,9 @@ result_t screen_test(const char *resources) test_fill_pattern, test_dashed_line, test_draw_lines, - test_draw_rect + test_draw_rect, + test_draw_circle, + test_fill_circle }; result_t rc; From a1fbcabbfb33f18d7c7cefae91d0f48d84fa3a74 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Wed, 2 Sep 2026 23:32:49 +0100 Subject: [PATCH 200/287] refactor(wuss): draw ball and clock bezel with screen circle primitives ball: fill each ball with screen_fill_circle instead of a square. clock: replace the 90-segment polygon bezel with one screen_draw_circle call, dropping CLOCK_BEZEL_SEGMENTS and its Wu-line loop. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- libraries/wuss/test/tasks/ball.c | 9 +++++---- libraries/wuss/test/tasks/clock.c | 21 +++------------------ 2 files changed, 8 insertions(+), 22 deletions(-) diff --git a/libraries/wuss/test/tasks/ball.c b/libraries/wuss/test/tasks/ball.c index 58f97681..cbfb8f2f 100644 --- a/libraries/wuss/test/tasks/ball.c +++ b/libraries/wuss/test/tasks/ball.c @@ -10,6 +10,7 @@ #include "base/utils.h" #include "framebuf/palettes.h" +#include "framebuf/screen.h" #include "geom/box.h" #include "ball.h" @@ -100,10 +101,10 @@ static result_t ball_redraw(const wuss_event_t *event, void *task_data) b = &bc->balls[i]; - screen_fill_rect(scr, bounds->x0 - sx + b->x - b->radius, - bounds->y0 - sy + b->y - b->radius, - SIZE2D(b->radius * 2, b->radius * 2), - bc->ball); + screen_fill_circle(scr, bounds->x0 - sx + b->x, + bounds->y0 - sy + b->y, + b->radius, + bc->ball); } return result_OK; diff --git a/libraries/wuss/test/tasks/clock.c b/libraries/wuss/test/tasks/clock.c index a9e81add..bf8f475f 100644 --- a/libraries/wuss/test/tasks/clock.c +++ b/libraries/wuss/test/tasks/clock.c @@ -26,7 +26,6 @@ #define M_PI 3.14159265358979323846 #endif -#define CLOCK_BEZEL_SEGMENTS 90 /* sides of the polygon standing in for the round bezel */ #define CLOCK_FACE_FRACTION 0.92 /* bezel radius as a fraction of half the smaller side */ #define CLOCK_HOUR_TICK 0.10 /* hour-tick length, fraction of face radius */ #define CLOCK_MINUTE_TICK 0.05 /* minute-tick length, fraction of face radius */ @@ -162,23 +161,9 @@ static result_t clock_redraw(const wuss_event_t *event, void *task_data) r = MIN(bounds->x1 - bounds->x0, bounds->y1 - bounds->y0) / 2.0 * CLOCK_FACE_FRACTION; - /* bezel: a many-sided polygon standing in for the circle */ - { - fix8_point_t prev, first; - - first = clock_polar(cx, cy, r, 0.0); - prev = first; - for (i = 1; i <= CLOCK_BEZEL_SEGMENTS; i++) - { - fix8_point_t p; - - p = clock_polar(cx, cy, r, - i * 2.0 * M_PI / CLOCK_BEZEL_SEGMENTS); - screen_draw_line_wu_fix8(scr, prev.x, prev.y, p.x, p.y, cc->bezel); - prev = p; - } - screen_draw_line_wu_fix8(scr, prev.x, prev.y, first.x, first.y, cc->bezel); - } + /* bezel */ + screen_draw_circle(scr, (int) (cx + 0.5), (int) (cy + 0.5), (int) (r + 0.5), + cc->bezel); /* 60 minute ticks, every fifth one an hour tick drawn longer */ for (i = 0; i < 60; i++) From 806e6b9c087541f2203d0550f2223e4633f31407 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Wed, 2 Sep 2026 23:33:53 +0100 Subject: [PATCH 201/287] feat(wuss): wire up Shift-F1 screen-garbage input Adds the wuss_INPUT_GARBAGE enum and its Shift-F1 SDL mapping. main.c already handles the event (f1dca91); this completes the path so the wuss app builds and the key works. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- apps/wuss/frontend-sdl.c | 2 ++ apps/wuss/frontend.h | 1 + 2 files changed, 3 insertions(+) diff --git a/apps/wuss/frontend-sdl.c b/apps/wuss/frontend-sdl.c index 297919a8..206f53a2 100644 --- a/apps/wuss/frontend-sdl.c +++ b/apps/wuss/frontend-sdl.c @@ -179,6 +179,8 @@ bool wuss_frontend_poll(wuss_frontend_t *fe, wuss_input_t *event) case SDL_EVENT_KEY_UP: if (ev.key.key == SDLK_Q) event->kind = wuss_INPUT_QUIT; + else if (ev.key.key == SDLK_F1 && (ev.key.mod & SDL_KMOD_SHIFT)) + event->kind = wuss_INPUT_GARBAGE; else if (ev.key.key == SDLK_F1) event->kind = wuss_INPUT_REDRAW_ALL; else if (ev.key.key == SDLK_F3) diff --git a/apps/wuss/frontend.h b/apps/wuss/frontend.h index 46ce26e8..5395d5a6 100644 --- a/apps/wuss/frontend.h +++ b/apps/wuss/frontend.h @@ -33,6 +33,7 @@ typedef enum wuss_input_kind wuss_INPUT_MOUSE_UP, /* .pos, .button */ wuss_INPUT_WHEEL, /* .pos, .wheel */ wuss_INPUT_REDRAW_ALL, /* force a full redraw (F1) */ + wuss_INPUT_GARBAGE, /* corrupt the whole screen for one frame (Shift-F1) */ wuss_INPUT_PIXEL_STRESS, /* one-pixel-at-a-time redraw (F3) */ wuss_INPUT_PALETTE_CYCLE /* advance to the next system palette (F4) */ } From c093680737f2a5e0ed54351113cd5de1962c2a5d Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Wed, 2 Sep 2026 23:43:53 +0100 Subject: [PATCH 202/287] fix(wuss): stop the bouncing ball leaving a trail ball_local_box subtracted the scroll offset that wuss_window_invalidate also applies, double-counting it, and sized the dirty box at 2*radius when the drawn circle spans 2*radius+1 pixels. The uncleared right/bottom edge accumulated into a lumpy, oversized-looking smear. Pass ball positions straight through in virtual content space and widen the box by one for the half-open box_t convention. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- libraries/wuss/test/tasks/ball.c | 34 +++++++++++++++----------------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/libraries/wuss/test/tasks/ball.c b/libraries/wuss/test/tasks/ball.c index cbfb8f2f..36d6a035 100644 --- a/libraries/wuss/test/tasks/ball.c +++ b/libraries/wuss/test/tasks/ball.c @@ -15,21 +15,20 @@ #include "ball.h" -/* Window-local invalidation box covering a ball (or its swept range) whose - * centre spans [vx0,vx1] x [vy0,vy1] in virtual content space. */ -static box_t ball_local_box(int vx0, - int vy0, - int vx1, - int vy1, - int radius, - point_t scroll) +/* Invalidation box (virtual content space, as ball positions are held) + * covering a ball -- or its swept range -- whose centre spans + * [vx0,vx1] x [vy0,vy1]. wuss_window_invalidate maps this to the screen and + * applies scroll, so this must not. The circle occupies [c-radius,c+radius] + * inclusive, i.e. 2*radius+1 pixels; box_t is half-open so x1/y1 get the + * extra 1. */ +static box_t ball_local_box(int vx0, int vy0, int vx1, int vy1, int radius) { box_t local; - local.x0 = vx0 - radius - scroll.x; - local.y0 = vy0 - radius - scroll.y; - local.x1 = vx1 + radius - scroll.x; - local.y1 = vy1 + radius - scroll.y; + local.x0 = vx0 - radius; + local.y0 = vy0 - radius; + local.x1 = vx1 + radius + 1; + local.y1 = vy1 + radius + 1; return local; } @@ -119,15 +118,14 @@ static result_t ball_mouse(wuss_window_t *window, { ball_task_t *bc; box_t local; - point_t scroll; bc = task_data; + NOT_USED(window); + if (action != wuss_MOUSE_DOWN) return result_OK; - wuss_window_get_scroll(window, &scroll); - if (button & wuss_BUTTON_SELECT) { ball_t *b; @@ -145,7 +143,7 @@ static result_t ball_mouse(wuss_window_t *window, b->dy = (bc->nballs & 2) ? 2 : -2; b->radius = 8; - local = ball_local_box(b->x, b->y, b->x, b->y, b->radius, scroll); + local = ball_local_box(b->x, b->y, b->x, b->y, b->radius); wuss_window_invalidate(bc->window, &local); } else if (button & wuss_BUTTON_ADJUST) @@ -157,7 +155,7 @@ static result_t ball_mouse(wuss_window_t *window, b = &bc->balls[--bc->nballs]; - local = ball_local_box(b->x, b->y, b->x, b->y, b->radius, scroll); + local = ball_local_box(b->x, b->y, b->x, b->y, b->radius); wuss_window_invalidate(bc->window, &local); } @@ -200,7 +198,7 @@ static result_t ball_idle(void *task_data) local = ball_local_box(MIN(old_x, b->x), MIN(old_y, b->y), MAX(old_x, b->x), MAX(old_y, b->y), - b->radius, scroll); + b->radius); wuss_window_invalidate(bc->window, &local); } From d70c40c8c9ccea4cc9325b5a059600893ac79e18 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Thu, 3 Sep 2026 00:02:05 +0100 Subject: [PATCH 203/287] feat(wuss): randomise bouncing ball size and colour Each ball gets a radius of 8 +/-50% (4..12) and a random red-tinted colour at spawn, at both the initial ball and every Select-click ball. The shared ball colour moves from ball_task_t onto ball_t so balls can differ. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- libraries/wuss/test/tasks/ball.c | 77 +++++++++++++++++++------------- libraries/wuss/test/tasks/ball.h | 9 ++-- 2 files changed, 52 insertions(+), 34 deletions(-) diff --git a/libraries/wuss/test/tasks/ball.c b/libraries/wuss/test/tasks/ball.c index 36d6a035..ce73ddd2 100644 --- a/libraries/wuss/test/tasks/ball.c +++ b/libraries/wuss/test/tasks/ball.c @@ -15,6 +15,24 @@ #include "ball.h" +#define BALL_BASE_RADIUS 8 /* +/-50% at spawn -> 4..12 */ + +/* a fresh radius in [BALL_BASE_RADIUS/2, BALL_BASE_RADIUS*3/2] */ +static int ball_random_radius(void) +{ + return BALL_BASE_RADIUS / 2 + rand() % (BALL_BASE_RADIUS + 1); +} + +/* a fresh fully-opaque colour, red channel is at least 0x40 so it stays + * visible against the red background */ +static colour_t ball_random_colour(void) +{ + int i; + + i = 0x40 + rand() % 0xC0; + return colour_rgb(0xFF, i, i); +} + /* Invalidation box (virtual content space, as ball positions are held) * covering a ball -- or its swept range -- whose centre spans * [vx0,vx1] x [vy0,vy1]. wuss_window_invalidate maps this to the screen and @@ -40,14 +58,14 @@ result_t ball_create(wuss_t *wuss, ball_task_t *task) result_t rc; task->bg = colour_rgb(0xFF, 0x00, 0x00); - task->ball = colour_rgb(0xFF, 0xFF, 0xFF); task->nballs = 1; task->balls[0].x = 50; task->balls[0].y = 50; task->balls[0].dx = 3; task->balls[0].dy = 2; - task->balls[0].radius = 8; + task->balls[0].radius = ball_random_radius(); + task->balls[0].colour = ball_random_colour(); /* ball_redraw paints its own background every frame */ delegate_desc.handle = ball_handle; @@ -103,7 +121,7 @@ static result_t ball_redraw(const wuss_event_t *event, void *task_data) screen_fill_circle(scr, bounds->x0 - sx + b->x, bounds->y0 - sy + b->y, b->radius, - bc->ball); + b->colour); } return result_OK; @@ -126,35 +144,34 @@ static result_t ball_mouse(wuss_window_t *window, if (action != wuss_MOUSE_DOWN) return result_OK; - if (button & wuss_BUTTON_SELECT) - { - ball_t *b; - - if (bc->nballs >= BALL_MAX) - return result_OK; - - /* x,y already arrive in virtual content space, as ball positions are - * held; only the invalidation boxes below need the scroll offset taking - * back off to reach window-local coordinates. */ - b = &bc->balls[bc->nballs++]; - b->x = x; - b->y = y; - b->dx = (bc->nballs & 1) ? 3 : -3; - b->dy = (bc->nballs & 2) ? 2 : -2; - b->radius = 8; - - local = ball_local_box(b->x, b->y, b->x, b->y, b->radius); - wuss_window_invalidate(bc->window, &local); - } - else if (button & wuss_BUTTON_ADJUST) + if (button & (wuss_BUTTON_SELECT | wuss_BUTTON_ADJUST)) { ball_t *b; - - if (bc->nballs <= 1) - return result_OK; /* keep at least one ball on screen */ - - b = &bc->balls[--bc->nballs]; - + + if (button & wuss_BUTTON_SELECT) + { + if (bc->nballs >= BALL_MAX) + return result_OK; + + /* x,y already arrive in virtual content space, as ball positions are + * held; only the invalidation boxes below need the scroll offset taking + * back off to reach window-local coordinates. */ + b = &bc->balls[bc->nballs++]; + b->x = x; + b->y = y; + b->dx = (bc->nballs & 1) ? 3 : -3; + b->dy = (bc->nballs & 2) ? 2 : -2; + b->radius = ball_random_radius(); + b->colour = ball_random_colour(); + } + else if (button & wuss_BUTTON_ADJUST) + { + if (bc->nballs <= 1) + return result_OK; /* keep at least one ball on screen */ + + b = &bc->balls[--bc->nballs]; + } + local = ball_local_box(b->x, b->y, b->x, b->y, b->radius); wuss_window_invalidate(bc->window, &local); } diff --git a/libraries/wuss/test/tasks/ball.h b/libraries/wuss/test/tasks/ball.h index 73068ed6..a1b8d9f2 100644 --- a/libraries/wuss/test/tasks/ball.h +++ b/libraries/wuss/test/tasks/ball.h @@ -13,9 +13,10 @@ /* one bouncing ball, centre in local content coords */ typedef struct ball { - int x, y; - int dx, dy; - int radius; + int x, y; + int dx, dy; + int radius; + colour_t colour; } ball_t; @@ -26,7 +27,7 @@ ball_t; typedef struct ball_task { wuss_window_t *window; - colour_t bg, ball; + colour_t bg; ball_t balls[BALL_MAX]; int nballs; } From 64015c9d3db51b5e7a5f5ce0bdd393f99fa354dc Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Thu, 3 Sep 2026 00:53:27 +0100 Subject: [PATCH 204/287] feat(wuss): symbolic wuss_colour_t values for named and role colours wuss_colour_t values from wuss_COLOUR_SYMBOLIC (128) up are no longer raw palette indices but roles the window manager resolves to a concrete index: - wuss_COLOUR_BLACK/WHITE/RED/GREEN/BLUE/YELLOW/CYAN/MAGENTA/GREY pick the nearest system-palette entry to the named RGB. - wuss_COLOUR_TITLE_BG/TITLE_FG/BUTTON_HILIGHT/BUTTON_SHADOW/ACCENT_BG/ ACCENT_FG/BACKDROP echo the matching wuss_config_t field. Accepted anywhere a wuss_colour_t is taken (config furniture/bevel/accent/ backdrop, wuss_window_create / wuss_window_set_background backgrounds, icon specs) and resolved once at store time, so draw code and wuss_nearest_colour are unaffected. A lookup table in wuss_t::palettecache holds the resolved indices, rebuilt on create, wuss_set_palette and wuss_set_backdrop. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- CHANGELOG.md | 13 +++++ CMakeLists.txt | 1 + docs/windowing/wuss.md | 9 ++++ include/wuss/wuss.h | 37 ++++++++++++++ libraries/wuss/create.c | 42 +++++++++++----- libraries/wuss/icon/create.c | 16 +++--- libraries/wuss/impl.h | 22 +++++++++ libraries/wuss/rebuild-palettecache.c | 61 +++++++++++++++++++++++ libraries/wuss/set-backdrop.c | 16 ++++-- libraries/wuss/set-palette.c | 3 +- libraries/wuss/test/wuss-test.c | 67 +++++++++++++++++++++++++- libraries/wuss/window/create.c | 3 ++ libraries/wuss/window/set-background.c | 3 ++ 13 files changed, 266 insertions(+), 27 deletions(-) create mode 100644 libraries/wuss/rebuild-palettecache.c diff --git a/CHANGELOG.md b/CHANGELOG.md index 8914de87..177d0d45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,19 @@ _Unreleased_ until one is cut. ### Added +- Symbolic `wuss_colour_t` values (`wuss/wuss.h`). A raw `wuss_colour_t` is a + palette index, `0..127`; `wuss_COLOUR_SYMBOLIC` (128) and up name roles the + window manager resolves to a concrete index. `wuss_COLOUR_BLACK`, + `_WHITE`, `_RED`, `_GREEN`, `_BLUE`, `_YELLOW`, `_CYAN`, `_MAGENTA`, + `_GREY` pick the nearest system-palette entry to the named RGB; + `wuss_COLOUR_TITLE_BG` / `_TITLE_FG` / `_BUTTON_HILIGHT` / `_BUTTON_SHADOW` + / `_ACCENT_BG` / `_ACCENT_FG` / `_BACKDROP` echo the matching + `wuss_config_t` field. Accepted anywhere a `wuss_colour_t` is taken — + config furniture/bevel/accent/backdrop, `wuss_window_create()` / + `wuss_window_set_background()` backgrounds, icon specs — and resolved once + when the value is stored, so draw code and `wuss_nearest_colour()` are + unaffected. Resolutions are recomputed on `wuss_set_palette()` and + `wuss_set_backdrop()`. - `wuss/menu.h` (new `WUSS_MENUS` option, implies `WUSS_ICONS`) — RISC OS-style pop-up menus. `wuss_menu_open()` shows a caller-owned, immutable `wuss_menu_t` (title plus an array of `wuss_menu_item_t`) as a borderless diff --git a/CMakeLists.txt b/CMakeLists.txt index 89569a00..e782b2fa 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -348,6 +348,7 @@ set(WUSS_CORE_SOURCES libraries/wuss/mouse-click.c libraries/wuss/mouse-move.c libraries/wuss/nearest-colour.c + libraries/wuss/rebuild-palettecache.c libraries/wuss/redraw.c libraries/wuss/scroll.c libraries/wuss/scroll-step.c diff --git a/docs/windowing/wuss.md b/docs/windowing/wuss.md index b1d0ef9d..59071ff6 100644 --- a/docs/windowing/wuss.md +++ b/docs/windowing/wuss.md @@ -24,6 +24,15 @@ result_t wuss_create(screen_t *scr, A `wuss_backdrop_t` is `{ colour, pattern, pattern_bg }`; the `wuss_BACKDROP_COLOUR(c)` and `wuss_BACKDROP_PATTERN(c, p, b)` macros build one as a compound literal. +### Symbolic colours + +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` — 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. + Destroy with `wuss_destroy`, which also destroys any windows still open on it. ## Windows diff --git a/include/wuss/wuss.h b/include/wuss/wuss.h index 6a613d97..67821278 100644 --- a/include/wuss/wuss.h +++ b/include/wuss/wuss.h @@ -110,6 +110,43 @@ typedef unsigned char wuss_colour_t; */ #define wuss_NO_BACKGROUND ((wuss_colour_t) -1) +/** + * Symbolic wuss_colour_t values. A raw wuss_colour_t is a system-palette + * index, 0..wuss_COLOUR_SYMBOLIC-1 (so a palette may hold up to 128 real + * entries). Values from wuss_COLOUR_SYMBOLIC up are not palette indices but + * roles the window manager resolves to a concrete index against the live + * palette (and, for the wuss_COLOUR_*_ chrome roles, the live wuss_config): + * 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. + * wuss_NO_BACKGROUND is not symbolic and always passes through unchanged. + */ +#define wuss_COLOUR_SYMBOLIC ((wuss_colour_t) 128) + +/* Named colours: nearest system-palette entry to the named RGB. */ +#define wuss_COLOUR_BLACK (wuss_COLOUR_SYMBOLIC + 0) +#define wuss_COLOUR_WHITE (wuss_COLOUR_SYMBOLIC + 1) +#define wuss_COLOUR_RED (wuss_COLOUR_SYMBOLIC + 2) +#define wuss_COLOUR_GREEN (wuss_COLOUR_SYMBOLIC + 3) +#define wuss_COLOUR_BLUE (wuss_COLOUR_SYMBOLIC + 4) +#define wuss_COLOUR_YELLOW (wuss_COLOUR_SYMBOLIC + 5) +#define wuss_COLOUR_CYAN (wuss_COLOUR_SYMBOLIC + 6) +#define wuss_COLOUR_MAGENTA (wuss_COLOUR_SYMBOLIC + 7) +#define wuss_COLOUR_GREY (wuss_COLOUR_SYMBOLIC + 8) + +/* 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. */ +#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) + /** 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 * flat colour. Ignored when the library is built with WUSS_FURNITURE off. diff --git a/libraries/wuss/create.c b/libraries/wuss/create.c index e8457616..b9d794b0 100644 --- a/libraries/wuss/create.c +++ b/libraries/wuss/create.c @@ -98,14 +98,17 @@ result_t wuss_create(screen_t *scr, memcpy(w->palette, palette, npalette * sizeof(*w->palette)); w->npalette = npalette; - /* Cache the palette indices nearest to opaque white and black, for menu - * backdrops/text and anything else wanting "paper" or "ink". */ - w->palettecache.white = wuss_nearest_colour(w, 255, 255, 255); - w->palettecache.black = wuss_nearest_colour(w, 0, 0, 0); + /* First pass: white/black and the named symbolic colours (BLACK..GREY). + * The chrome-role slots are still the out-of-range sentinel here -- filled + * once the config's own colours below are resolved and stored. This lets + * wuss__resolve_colour handle a config that uses a named symbolic. */ + wuss__rebuild_palettecache(w); if (config != NULL) { - w->backdrop = config->backdrop; + 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); } else { @@ -118,10 +121,19 @@ result_t wuss_create(screen_t *scr, if (config != NULL) { pal = config->furniture; - blight = config->bevel.light; - bdark = config->bevel.dark; - abg = config->accent.bg; - afg = config->accent.fg; + pal.title.bg = wuss__resolve_colour(w, pal.title.bg); + pal.title.fg = wuss__resolve_colour(w, pal.title.fg); + pal.back = wuss__resolve_colour(w, pal.back); + pal.close = wuss__resolve_colour(w, pal.close); + pal.toggle = wuss__resolve_colour(w, pal.toggle); + pal.resize = wuss__resolve_colour(w, pal.resize); + 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); } else { @@ -184,10 +196,10 @@ result_t wuss_create(screen_t *scr, #ifdef WUSS_ICONS if (config != NULL) { - blight = config->bevel.light; - bdark = config->bevel.dark; - abg = config->accent.bg; - afg = config->accent.fg; + 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); } else { @@ -216,6 +228,10 @@ result_t wuss_create(screen_t *scr, #endif #endif /* WUSS_FURNITURE */ + /* Second pass: the chrome colours are stored and concrete now, so fill in + * the chrome-role symbolic slots (wuss_COLOUR_TITLE_BG etc.). */ + wuss__rebuild_palettecache(w); + w->scr = scr; w->font = font; #ifdef WUSS_FURNITURE diff --git a/libraries/wuss/icon/create.c b/libraries/wuss/icon/create.c index 9393061e..4d163d59 100644 --- a/libraries/wuss/icon/create.c +++ b/libraries/wuss/icon/create.c @@ -20,12 +20,16 @@ result_t wuss_icon_create(wuss_window_t *window, const char *src; size_t len; int newcap; + wuss_colour_t fg, bg; assert(window != NULL); assert(spec != NULL); w = window->wuss; + fg = wuss__resolve_colour(w, spec->fg); + bg = wuss__resolve_colour(w, spec->bg); + switch (spec->type) { case wuss_ICON_TYPE_LABEL: @@ -45,7 +49,7 @@ result_t wuss_icon_create(wuss_window_t *window, if ((spec->type == wuss_ICON_TYPE_BUTTON || spec->type == wuss_ICON_TYPE_PATTERN) && - spec->bg == wuss_NO_BACKGROUND) + bg == wuss_NO_BACKGROUND) return result_WUSS_BAD_ICON; if (spec->type == wuss_ICON_TYPE_BITMAP && spec->bitmap == NULL) @@ -55,11 +59,11 @@ result_t wuss_icon_create(wuss_window_t *window, (spec->pattern < 0 || spec->pattern >= screen_PATTERN__LIMIT)) return result_WUSS_BAD_ICON; - if (spec->fg < 0 || spec->fg >= w->npalette) + if (fg < 0 || fg >= w->npalette) return result_WUSS_BAD_COLOUR; - if (spec->bg != wuss_NO_BACKGROUND && - (spec->bg < 0 || spec->bg >= w->npalette)) + if (bg != wuss_NO_BACKGROUND && + (bg < 0 || bg >= w->npalette)) return result_WUSS_BAD_COLOUR; if (window->nicons == window->cap_icons) @@ -89,8 +93,8 @@ result_t wuss_icon_create(wuss_window_t *window, it->window = window; it->bbox = spec->bbox; it->type = spec->type; - it->fg = spec->fg; - it->bg = spec->bg; + it->fg = fg; + it->bg = bg; it->pattern = (spec->type == wuss_ICON_TYPE_PATTERN) ? spec->pattern : screen_PATTERN_SOLID; it->bitmap = (spec->type == wuss_ICON_TYPE_BITMAP) ? spec->bitmap : NULL; diff --git a/libraries/wuss/impl.h b/libraries/wuss/impl.h index 7cfc1aa3..f51e1690 100644 --- a/libraries/wuss/impl.h +++ b/libraries/wuss/impl.h @@ -95,6 +95,11 @@ struct wuss { wuss_colour_t white; /* palette index nearest to white */ wuss_colour_t black; /* palette index nearest to black */ + /* Concrete 0..npalette-1 index for each symbolic wuss_colour_t, indexed + * by (c - wuss_COLOUR_SYMBOLIC). Unused slots hold wuss_NO_BACKGROUND so + * a stray symbolic still fails a range check. Rebuilt by + * wuss__rebuild_palettecache on create and every palette/config change. */ + wuss_colour_t symbolic[128]; } palettecache; #ifdef WUSS_FURNITURE @@ -192,6 +197,23 @@ struct wuss_window wuss_window_t *wuss__window_at(wuss_t *wuss, point_t p); +/* 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 + * indices (resolve config through wuss__resolve_colour before storing). */ +void wuss__rebuild_palettecache(wuss_t *wuss); + +/* Concrete 0..npalette-1 palette index for any wuss_colour_t: a symbolic + * value (>= wuss_COLOUR_SYMBOLIC, bar wuss_NO_BACKGROUND) via the cache, + * everything else -- raw indices, wuss_NO_BACKGROUND -- unchanged. */ +static inline wuss_colour_t wuss__resolve_colour(const wuss_t *wuss, + wuss_colour_t c) +{ + if (c >= wuss_COLOUR_SYMBOLIC && c != wuss_NO_BACKGROUND) + return wuss->palettecache.symbolic[c - wuss_COLOUR_SYMBOLIC]; + return c; +} + /* Allocation through a wuss_t's configured hooks (see struct wuss::alloc). * Every heap block a wuss_t owns -- windows, icons, icon-pointer arrays, menu * nodes -- goes through these so a caller can supply its own allocator. */ diff --git a/libraries/wuss/rebuild-palettecache.c b/libraries/wuss/rebuild-palettecache.c new file mode 100644 index 00000000..6d7d4f3a --- /dev/null +++ b/libraries/wuss/rebuild-palettecache.c @@ -0,0 +1,61 @@ +/* wuss/rebuild-palettecache.c -- resolve symbolic colours for the current palette */ + +#include <stddef.h> + +#include "framebuf/colour.h" + +#include "impl.h" + +/* RGB each named symbolic colour resolves to, in wuss_COLOUR_BLACK.. order. */ +static const unsigned char wuss__named_rgb[][3] = +{ + { 0x00, 0x00, 0x00 }, /* wuss_COLOUR_BLACK */ + { 0xFF, 0xFF, 0xFF }, /* wuss_COLOUR_WHITE */ + { 0xFF, 0x00, 0x00 }, /* wuss_COLOUR_RED */ + { 0x00, 0xFF, 0x00 }, /* wuss_COLOUR_GREEN */ + { 0x00, 0x00, 0xFF }, /* wuss_COLOUR_BLUE */ + { 0xFF, 0xFF, 0x00 }, /* wuss_COLOUR_YELLOW */ + { 0x00, 0xFF, 0xFF }, /* wuss_COLOUR_CYAN */ + { 0xFF, 0x00, 0xFF }, /* wuss_COLOUR_MAGENTA */ + { 0x80, 0x80, 0x80 } /* wuss_COLOUR_GREY */ +}; + +void wuss__rebuild_palettecache(wuss_t *wuss) +{ + wuss_colour_t *sym; + size_t i; + + wuss->palettecache.white = wuss_nearest_colour(wuss, 255, 255, 255); + wuss->palettecache.black = wuss_nearest_colour(wuss, 0, 0, 0); + + sym = wuss->palettecache.symbolic; + + /* Default every slot to wuss_COLOUR_SYMBOLIC: always >= npalette (palettes + * hold at most 128 real entries) so an unmapped symbolic still fails every + * range check, and -- unlike wuss_NO_BACKGROUND -- it is not mistaken for + * "no background" by wuss__validate_backdrop. */ + for (i = 0; i < NELEMS(wuss->palettecache.symbolic); i++) + sym[i] = wuss_COLOUR_SYMBOLIC; + + for (i = 0; i < NELEMS(wuss__named_rgb); i++) + sym[(wuss_COLOUR_BLACK - wuss_COLOUR_SYMBOLIC) + i] = + wuss_nearest_colour(wuss, + wuss__named_rgb[i][0], + wuss__named_rgb[i][1], + wuss__named_rgb[i][2]); + +#ifdef WUSS_FURNITURE + sym[wuss_COLOUR_TITLE_BG - wuss_COLOUR_SYMBOLIC] = + wuss->furniture_colours.title.bg; + sym[wuss_COLOUR_TITLE_FG - wuss_COLOUR_SYMBOLIC] = + wuss->furniture_colours.title.fg; +#endif +#if defined(WUSS_FURNITURE) || defined(WUSS_ICONS) + sym[wuss_COLOUR_BUTTON_HILIGHT - wuss_COLOUR_SYMBOLIC] = wuss->bevel_light; + sym[wuss_COLOUR_BUTTON_SHADOW - wuss_COLOUR_SYMBOLIC] = wuss->bevel_dark; + sym[wuss_COLOUR_ACCENT_BG - wuss_COLOUR_SYMBOLIC] = wuss->accent_bg; + sym[wuss_COLOUR_ACCENT_FG - wuss_COLOUR_SYMBOLIC] = wuss->accent_fg; +#endif + if (wuss->backdrop.colour != wuss_NO_BACKGROUND) + sym[wuss_COLOUR_BACKDROP - wuss_COLOUR_SYMBOLIC] = wuss->backdrop.colour; +} diff --git a/libraries/wuss/set-backdrop.c b/libraries/wuss/set-backdrop.c index 905ce012..d098467a 100644 --- a/libraries/wuss/set-backdrop.c +++ b/libraries/wuss/set-backdrop.c @@ -8,17 +8,25 @@ result_t wuss_set_backdrop(wuss_t *wuss, const wuss_backdrop_t *backdrop) { - box_t screen; - result_t rc; + box_t screen; + result_t rc; + wuss_backdrop_t resolved; assert(wuss != NULL); assert(backdrop != NULL); - rc = wuss__validate_backdrop(wuss, backdrop); + resolved = *backdrop; + resolved.colour = wuss__resolve_colour(wuss, resolved.colour); + resolved.pattern_bg = wuss__resolve_colour(wuss, resolved.pattern_bg); + + rc = wuss__validate_backdrop(wuss, &resolved); if (rc != result_OK) return rc; - wuss->backdrop = *backdrop; + wuss->backdrop = resolved; + + /* wuss_COLOUR_BACKDROP now follows the new colour. */ + wuss__rebuild_palettecache(wuss); screen.x0 = 0; screen.y0 = 0; diff --git a/libraries/wuss/set-palette.c b/libraries/wuss/set-palette.c index 7c0c9da3..e4599909 100644 --- a/libraries/wuss/set-palette.c +++ b/libraries/wuss/set-palette.c @@ -27,8 +27,7 @@ result_t wuss_set_palette(wuss_t *wuss, memcpy(wuss->palette, palette, npalette * sizeof(*wuss->palette)); - wuss->palettecache.white = wuss_nearest_colour(wuss, 255, 255, 255); - wuss->palettecache.black = wuss_nearest_colour(wuss, 0, 0, 0); + wuss__rebuild_palettecache(wuss); rc = result_OK; event.kind = wuss_EVENT_PALETTE; diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index b9afbc57..a5a91afa 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -372,7 +372,7 @@ result_t wuss_test(const char *resources) printf("test: wuss_create with bad titlebar colour index\n"); bad_config.titlebar_height = 0; - bad_config.furniture.title.bg = 999; + 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.back = 0; bad_config.furniture.close = 0; @@ -402,6 +402,69 @@ result_t wuss_test(const char *resources) if (rc != result_OK) goto Failure; + printf("test: symbolic wuss_colour_t resolves against palette and config\n"); + + { + /* red, green, blue, white, black (colour_t primary is 0xAABBGGRR) -- + * enough for the named symbolics to land on distinct, checkable + * indices. */ + static const colour_t sympal[5] = + { + { 0xFF0000FF }, { 0xFF00FF00 }, { 0xFFFF0000 }, + { 0xFFFFFFFF }, { 0xFF000000 } + }; + wuss_config_t symcfg; + wuss_t *symw; + wuss_window_t *symwin; + wuss_task_t *symdel; + box_t symbox; + + memset(&symcfg, 0, sizeof(symcfg)); + symcfg.furniture.title.bg = wuss_COLOUR_BLUE; /* -> index 2 */ + symcfg.furniture.title.fg = wuss_COLOUR_WHITE; /* -> index 3 */ + symcfg.backdrop = (wuss_backdrop_t) wuss_BACKDROP_COLOUR(wuss_COLOUR_GREEN); /* -> 1 */ + + rc = wuss_create(&scr, NULL, sympal, 5, &symcfg, NULL, &symw); + if (rc != result_OK) + goto Failure; + + /* named colours resolve to nearest-palette-entry; raw indices and + * wuss_NO_BACKGROUND pass straight through; chrome roles echo the + * resolved config. */ + if (wuss__resolve_colour(symw, wuss_COLOUR_RED) != 0 || + wuss__resolve_colour(symw, wuss_COLOUR_GREEN) != 1 || + wuss__resolve_colour(symw, wuss_COLOUR_BLUE) != 2 || + wuss__resolve_colour(symw, wuss_COLOUR_WHITE) != 3 || + wuss__resolve_colour(symw, wuss_COLOUR_BLACK) != 4 || + wuss__resolve_colour(symw, 2) != 2 || + 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_destroy(symw); + goto Failure; + } + + /* a symbolic colour is accepted (and stored resolved) where a client + * passes a wuss_colour_t. */ + 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, + wuss_BACKDROP_COLOUR(wuss_COLOUR_RED), + SIZE2D(80, 80), SIZE2D(80, 80), &symwin); + if (rc != result_OK) { wuss_destroy(symw); goto Failure; } + if (symwin->bg.colour != 0) /* wuss_COLOUR_RED -> palette index 0 */ + { + wuss_destroy(symw); + goto Failure; + } + + wuss_destroy(symw); /* sweeps symdel too */ + mk_task_count = 0; /* drop the now-stale registry entry */ + } + printf("test: window_create too small\n"); box_a.x0 = 0; @@ -903,7 +966,7 @@ result_t wuss_test(const char *resources) printf("test: wuss_window_set_background\n"); - rc = wuss_window_set_background(win_d, wuss_BACKDROP_COLOUR(999)); + rc = wuss_window_set_background(win_d, wuss_BACKDROP_COLOUR(100)); if (rc != result_WUSS_BAD_COLOUR) goto Failure; diff --git a/libraries/wuss/window/create.c b/libraries/wuss/window/create.c index ee8c7ad7..f662c500 100644 --- a/libraries/wuss/window/create.c +++ b/libraries/wuss/window/create.c @@ -109,6 +109,9 @@ result_t wuss_window_create(wuss_task_t *task, win->task = task; + bg.colour = wuss__resolve_colour(wuss, bg.colour); + bg.pattern_bg = wuss__resolve_colour(wuss, bg.pattern_bg); + if (wuss__validate_backdrop(wuss, &bg) != result_OK) { wuss__free(wuss, win); diff --git a/libraries/wuss/window/set-background.c b/libraries/wuss/window/set-background.c index 5e846a33..1d1d5337 100644 --- a/libraries/wuss/window/set-background.c +++ b/libraries/wuss/window/set-background.c @@ -7,6 +7,9 @@ result_t wuss_window_set_background(wuss_window_t *window, { box_t content; + bg.colour = wuss__resolve_colour(window->wuss, bg.colour); + bg.pattern_bg = wuss__resolve_colour(window->wuss, bg.pattern_bg); + if (wuss__validate_backdrop(window->wuss, &bg) != result_OK) return result_WUSS_BAD_COLOUR; From 6305038de735da2db7ea20ddfd6866a80cecc229 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Thu, 3 Sep 2026 01:49:10 +0100 Subject: [PATCH 205/287] refactor(wuss): fold nearest-black/white cache into the symbolic table palettecache.white/black were duplicates of the wuss_COLOUR_WHITE/BLACK slots in the symbolic table -- same wuss_nearest_colour lookup, filled by the same rebuild. Drop the two fields, flatten palettecache to a plain wuss_colour_t[128], and have menu.c pass wuss_COLOUR_BLACK / wuss_COLOUR_WHITE directly (resolved at icon/window store time like any other symbolic). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- libraries/wuss/impl.h | 18 ++++++------------ libraries/wuss/menu/menu.c | 6 +++--- libraries/wuss/rebuild-palettecache.c | 27 ++++++++++++--------------- 3 files changed, 21 insertions(+), 30 deletions(-) diff --git a/libraries/wuss/impl.h b/libraries/wuss/impl.h index f51e1690..35151980 100644 --- a/libraries/wuss/impl.h +++ b/libraries/wuss/impl.h @@ -91,17 +91,11 @@ struct wuss * block this wuss_t owns */ colour_t *palette; /* owned */ int npalette; - struct - { - wuss_colour_t white; /* palette index nearest to white */ - wuss_colour_t black; /* palette index nearest to black */ - /* Concrete 0..npalette-1 index for each symbolic wuss_colour_t, indexed - * by (c - wuss_COLOUR_SYMBOLIC). Unused slots hold wuss_NO_BACKGROUND so - * a stray symbolic still fails a range check. Rebuilt by - * wuss__rebuild_palettecache on create and every palette/config change. */ - wuss_colour_t symbolic[128]; - } - palettecache; + /* Concrete 0..npalette-1 index for each symbolic wuss_colour_t, indexed by + * (c - wuss_COLOUR_SYMBOLIC). Unused slots hold wuss_COLOUR_SYMBOLIC so a + * stray symbolic still fails a range check. Rebuilt by + * wuss__rebuild_palettecache on create and every palette/config change. */ + wuss_colour_t palettecache[128]; #ifdef WUSS_FURNITURE wuss_furniture_palette_t furniture_colours; #endif @@ -210,7 +204,7 @@ static inline wuss_colour_t wuss__resolve_colour(const wuss_t *wuss, wuss_colour_t c) { if (c >= wuss_COLOUR_SYMBOLIC && c != wuss_NO_BACKGROUND) - return wuss->palettecache.symbolic[c - wuss_COLOUR_SYMBOLIC]; + return wuss->palettecache[c - wuss_COLOUR_SYMBOLIC]; return c; } diff --git a/libraries/wuss/menu/menu.c b/libraries/wuss/menu/menu.c index 99089985..e68f8d2e 100644 --- a/libraries/wuss/menu/menu.c +++ b/libraries/wuss/menu/menu.c @@ -580,7 +580,7 @@ static result_t wuss__menu_spawn(wuss_t *wuss, specs[s].bbox.x1 = width; specs[s].bbox.y1 = y + sep_h; specs[s].text = ""; - specs[s].fg = wuss->palettecache.black; + specs[s].fg = wuss_COLOUR_BLACK; specs[s].bg = wuss_NO_BACKGROUND; specs[s].flags = wuss_ICON_FLAGS_NONE; s++; @@ -598,7 +598,7 @@ static result_t wuss__menu_spawn(wuss_t *wuss, specs[s].bbox.x1 = width; specs[s].bbox.y1 = y + pitch; specs[s].text = item->text ? item->text : ""; - specs[s].fg = wuss->palettecache.black; + specs[s].fg = wuss_COLOUR_BLACK; specs[s].bg = wuss_NO_BACKGROUND; specs[s].flags = flags; s++; @@ -623,7 +623,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->palettecache.white), + wuss_BACKDROP_COLOUR(wuss_COLOUR_WHITE), doc, min_doc, &node->window); if (rc != result_OK) { diff --git a/libraries/wuss/rebuild-palettecache.c b/libraries/wuss/rebuild-palettecache.c index 6d7d4f3a..a2350fa1 100644 --- a/libraries/wuss/rebuild-palettecache.c +++ b/libraries/wuss/rebuild-palettecache.c @@ -22,40 +22,37 @@ static const unsigned char wuss__named_rgb[][3] = void wuss__rebuild_palettecache(wuss_t *wuss) { - wuss_colour_t *sym; + wuss_colour_t *cache; size_t i; - wuss->palettecache.white = wuss_nearest_colour(wuss, 255, 255, 255); - wuss->palettecache.black = wuss_nearest_colour(wuss, 0, 0, 0); - - sym = wuss->palettecache.symbolic; + cache = wuss->palettecache; /* Default every slot to wuss_COLOUR_SYMBOLIC: always >= npalette (palettes * hold at most 128 real entries) so an unmapped symbolic still fails every * range check, and -- unlike wuss_NO_BACKGROUND -- it is not mistaken for * "no background" by wuss__validate_backdrop. */ - for (i = 0; i < NELEMS(wuss->palettecache.symbolic); i++) - sym[i] = wuss_COLOUR_SYMBOLIC; + for (i = 0; i < NELEMS(wuss->palettecache); i++) + cache[i] = wuss_COLOUR_SYMBOLIC; for (i = 0; i < NELEMS(wuss__named_rgb); i++) - sym[(wuss_COLOUR_BLACK - wuss_COLOUR_SYMBOLIC) + i] = + cache[(wuss_COLOUR_BLACK - wuss_COLOUR_SYMBOLIC) + i] = wuss_nearest_colour(wuss, wuss__named_rgb[i][0], wuss__named_rgb[i][1], wuss__named_rgb[i][2]); #ifdef WUSS_FURNITURE - sym[wuss_COLOUR_TITLE_BG - wuss_COLOUR_SYMBOLIC] = + cache[wuss_COLOUR_TITLE_BG - wuss_COLOUR_SYMBOLIC] = wuss->furniture_colours.title.bg; - sym[wuss_COLOUR_TITLE_FG - wuss_COLOUR_SYMBOLIC] = + cache[wuss_COLOUR_TITLE_FG - wuss_COLOUR_SYMBOLIC] = wuss->furniture_colours.title.fg; #endif #if defined(WUSS_FURNITURE) || defined(WUSS_ICONS) - sym[wuss_COLOUR_BUTTON_HILIGHT - wuss_COLOUR_SYMBOLIC] = wuss->bevel_light; - sym[wuss_COLOUR_BUTTON_SHADOW - wuss_COLOUR_SYMBOLIC] = wuss->bevel_dark; - sym[wuss_COLOUR_ACCENT_BG - wuss_COLOUR_SYMBOLIC] = wuss->accent_bg; - sym[wuss_COLOUR_ACCENT_FG - wuss_COLOUR_SYMBOLIC] = wuss->accent_fg; + 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_ACCENT_BG - wuss_COLOUR_SYMBOLIC] = wuss->accent_bg; + cache[wuss_COLOUR_ACCENT_FG - wuss_COLOUR_SYMBOLIC] = wuss->accent_fg; #endif if (wuss->backdrop.colour != wuss_NO_BACKGROUND) - sym[wuss_COLOUR_BACKDROP - wuss_COLOUR_SYMBOLIC] = wuss->backdrop.colour; + cache[wuss_COLOUR_BACKDROP - wuss_COLOUR_SYMBOLIC] = wuss->backdrop.colour; } From 1d99b2560d2242671e8ed6bed3b9ca69f28b1dd0 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Thu, 3 Sep 2026 08:57:54 +0100 Subject: [PATCH 206/287] feat(framebuf): add bitmap_draw_pattern for pattern-masked fills Fills a rectangle of a bitmap with a colour only where a repeating 8x8 pattern mask has a bit set. Area is clipped to the bitmap bounds, or NULL fills the whole bitmap. Supports 8bpp and 32bpp formats; others return result_NOT_SUPPORTED. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- CMakeLists.txt | 1 + include/framebuf/bitmap.h | 27 ++++++++ libraries/framebuf/bitmap/draw-pattern.c | 82 ++++++++++++++++++++++++ 3 files changed, 110 insertions(+) create mode 100644 libraries/framebuf/bitmap/draw-pattern.c diff --git a/CMakeLists.txt b/CMakeLists.txt index e782b2fa..bf14a7bc 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -237,6 +237,7 @@ set(DATASTRUCT_SOURCES set(FRAMEBUF_SOURCES libraries/framebuf/bitmap/bitmap.c + libraries/framebuf/bitmap/draw-pattern.c libraries/framebuf/bitmap/load.c libraries/framebuf/bitmap/save.c libraries/framebuf/bmfont/bmfont.c diff --git a/include/framebuf/bitmap.h b/include/framebuf/bitmap.h index a11361ec..776847ed 100644 --- a/include/framebuf/bitmap.h +++ b/include/framebuf/bitmap.h @@ -3,10 +3,13 @@ #ifndef FRAMEBUF_BITMAP_H #define FRAMEBUF_BITMAP_H +#include <stdint.h> + #include "base/result.h" #include "framebuf/colour.h" #include "framebuf/pixelfmt.h" #include "framebuf/span.h" +#include "geom/box.h" #include "geom/size.h" /** Common bitmap structure members (used for screens too). */ @@ -81,6 +84,30 @@ void bitmap_clear(bitmap_t *bm, colour_t colour); // bm->base = base; //} +/** + * Fill a rectangle of the bitmap with a colour, but only where a repeating + * 8x8 pattern mask has a bit set. + * + * The mask is eight bytes, one per pattern row, MSB leftmost. It tiles from + * the bitmap origin, so abutting fills line up. Where a mask bit is clear + * the destination pixel is left untouched. + * + * Supported for 8bpp and 32bpp formats only; other formats return \ref + * result_NOT_SUPPORTED. + * + * \param[in] bm Bitmap to fill. + * \param[in] area Rectangle to fill, clipped to the bitmap bounds. NULL + * fills the whole bitmap. + * \param[in] colour Colour to fill with. + * \param[in] mask Eight-byte 8x8 pattern mask. + * \return \ref result_OK on success, \ref result_NOT_SUPPORTED for an + * unsupported pixel format. + */ +result_t bitmap_draw_pattern(bitmap_t *bm, + const box_t *area, + colour_t colour, + const uint8_t mask[8]); + /** * Load a PNG image into the given bitmap. * diff --git a/libraries/framebuf/bitmap/draw-pattern.c b/libraries/framebuf/bitmap/draw-pattern.c new file mode 100644 index 00000000..67431b4e --- /dev/null +++ b/libraries/framebuf/bitmap/draw-pattern.c @@ -0,0 +1,82 @@ +/* framebuf/bitmap/draw-pattern.c -- pattern-masked rectangle fill */ + +#include <assert.h> +#include <stddef.h> +#include <stdint.h> + +#include "geom/box.h" + +#include "framebuf/bitmap.h" + +result_t bitmap_draw_pattern(bitmap_t *bm, + const box_t *area, + colour_t colour, + const uint8_t mask[8]) +{ + int log2bpp; + pixelfmt_any_t px; + box_t full; + box_t clip; + int x, y; + + assert(bm); + assert(mask); + + log2bpp = pixelfmt_log2bpp(bm->format); + if (log2bpp != 3 && log2bpp != 5) + return result_NOT_SUPPORTED; + + full.x0 = 0; + full.y0 = 0; + full.x1 = bm->size.w; + full.y1 = bm->size.h; + + if (area == NULL) + { + clip = full; + } + else if (!box_intersection(area, &full, &clip)) + { + return result_OK; /* nothing to fill */ + } + + px = colour_to_pixel(bm->palette, + bm->palette ? 1 << (1 << log2bpp) : 0, + colour, + bm->format); + + if (log2bpp == 3) /* 8bpp */ + { + uint8_t *base; + uint8_t *row; + + base = bm->base; + for (y = clip.y0; y < clip.y1; y++) + { + uint8_t bits = mask[y & 7]; + + row = base + (size_t) y * bm->rowbytes; + for (x = clip.x0; x < clip.x1; x++) + if (bits & (0x80 >> (x & 7))) + row[x] = (uint8_t) px; + } + } + else /* 32bpp */ + { + uint8_t *base; + pixelfmt_any32_t *row; + + base = bm->base; + for (y = clip.y0; y < clip.y1; y++) + { + uint8_t bits = mask[y & 7]; + + row = (pixelfmt_any32_t *) (base + (size_t) y * bm->rowbytes); + for (x = clip.x0; x < clip.x1; x++) + if (bits & (0x80 >> (x & 7))) + row[x] = px; + } + } + + return result_OK; +} From f057579b47234f9c90248ce09bb4c8da9a5f612a Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Thu, 3 Sep 2026 09:48:07 +0100 Subject: [PATCH 207/287] refactor(framebuf): unify pattern fills and rename raster-transfer ops to copy Introduce a single pattern_t {bits, fg, bg, flags, origin} describing an 8x8 tile and how it paints, with pattern_from_preset() (the screen_pattern_t Bayer ramp and named tiles) and pattern_from_mask() (stencil) constructors in the new framebuf/pattern module. - bitmap_draw_pattern -> bitmap_fill_pattern, now const pattern_t *, result_t; file draw-pattern.c -> fill-pattern.c. - screen_fill_pattern takes const pattern_t *; preset table and stencil/phase handling move into pattern.c / the struct. - screen_draw_bitmap -> screen_copy_bitmap; screen_draw_ninepatch -> screen_copy_ninepatch (screen-draw-ninepatch.c -> screen-copy-ninepatch.c, screen_NINEPATCH_NO_CENTRE kept). copy is now the verb for all pixel transfer, matching screen_copy_rect. - Migrate callers: wuss backdrop, icon draw, image/porter-duff tasks, screen-test. - Art asset 9tile.png/.aseprite -> ninepatch.png/.aseprite; update the wuss loaders. - Add docs/framebuf/drawing.md recording the drawing-ops taxonomy. Hard renames, no compatibility wrappers. Aligning the three copy ops on a result_t return type is a deferred follow-up. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- CMakeLists.txt | 6 +- apps/wuss/main.c | 2 +- docs/framebuf/drawing.md | 211 ++++++++++++++++++ include/framebuf/bitmap.h | 22 +- include/framebuf/pattern.h | 108 +++++++++ include/framebuf/screen.h | 95 ++------ libraries/framebuf/bitmap/draw-pattern.c | 82 ------- libraries/framebuf/bitmap/fill-pattern.c | 101 +++++++++ libraries/framebuf/pattern/pattern.c | 139 ++++++++++++ ...aw-ninepatch.c => screen-copy-ninepatch.c} | 8 +- libraries/framebuf/screen/screen-draw.c | 4 +- .../framebuf/screen/screen-fill-pattern.c | 202 +++++------------ libraries/framebuf/screen/test/screen-test.c | 33 ++- libraries/wuss/backdrop.c | 14 +- libraries/wuss/icon/draw.c | 15 +- libraries/wuss/test/tasks/icons.c | 2 +- libraries/wuss/test/tasks/image.c | 4 +- libraries/wuss/test/tasks/porter-duff.c | 2 +- resources/wuss/ninepatch.aseprite | Bin 0 -> 374 bytes resources/wuss/{9tile.png => ninepatch.png} | Bin 20 files changed, 713 insertions(+), 337 deletions(-) create mode 100644 docs/framebuf/drawing.md create mode 100644 include/framebuf/pattern.h delete mode 100644 libraries/framebuf/bitmap/draw-pattern.c create mode 100644 libraries/framebuf/bitmap/fill-pattern.c create mode 100644 libraries/framebuf/pattern/pattern.c rename libraries/framebuf/screen/{screen-draw-ninepatch.c => screen-copy-ninepatch.c} (95%) create mode 100644 resources/wuss/ninepatch.aseprite rename resources/wuss/{9tile.png => ninepatch.png} (100%) diff --git a/CMakeLists.txt b/CMakeLists.txt index bf14a7bc..671b8d2b 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -83,6 +83,7 @@ set(PUBLIC_HEADERS include/framebuf/composite.h include/framebuf/curve.h include/framebuf/palettes.h + include/framebuf/pattern.h include/framebuf/pixelfmt.h include/framebuf/screen.h include/framebuf/span-bgrx8888.h @@ -237,7 +238,7 @@ set(DATASTRUCT_SOURCES set(FRAMEBUF_SOURCES libraries/framebuf/bitmap/bitmap.c - libraries/framebuf/bitmap/draw-pattern.c + libraries/framebuf/bitmap/fill-pattern.c libraries/framebuf/bitmap/load.c libraries/framebuf/bitmap/save.c libraries/framebuf/bmfont/bmfont.c @@ -245,12 +246,13 @@ set(FRAMEBUF_SOURCES libraries/framebuf/composite/composite.c libraries/framebuf/curve/curve.c libraries/framebuf/palettes/palettes.c + libraries/framebuf/pattern/pattern.c libraries/framebuf/pixelfmt/log2bpp.c libraries/framebuf/screen/screen.c + libraries/framebuf/screen/screen-copy-ninepatch.c libraries/framebuf/screen/screen-copy-rect.c libraries/framebuf/screen/screen-draw.c libraries/framebuf/screen/screen-draw-circle.c - libraries/framebuf/screen/screen-draw-ninepatch.c libraries/framebuf/screen/screen-fill-pattern.c libraries/framebuf/span-registry/get.c libraries/framebuf/span-registry/regdata.h diff --git a/apps/wuss/main.c b/apps/wuss/main.c index f0ead35f..7fd50b4e 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -137,7 +137,7 @@ static result_t spawn_image(void) filename = path_join_filename(g.resources, 3, "resources", "images", leafname); strcpy(buf, filename); ninepatch = path_join_filename(g.resources, 3, "resources", "wuss", - path_join_leafname("9tile", "png")); + path_join_leafname("ninepatch", "png")); logf_info("wuss: image task loading \"%s\" + \"%s\"", buf, ninepatch); rc = image_create(g.wuss, buf, ninepatch, t); diff --git a/docs/framebuf/drawing.md b/docs/framebuf/drawing.md new file mode 100644 index 00000000..696f28cc --- /dev/null +++ b/docs/framebuf/drawing.md @@ -0,0 +1,211 @@ +# [DPTLib](https://github.com/dpt/DPTLib) > framebuf > drawing + +This document defines the **taxonomy and naming scheme** for 2D drawing +operations in the `framebuf` module: primitives, fills, blits, and the paints +that feed them. + +It is a design agreement. Some of the names below are already shipped; others +are the agreed target for code not yet written. Each function's status is marked +**[shipped]** or **[planned]**. + +## Rationale + +`framebuf` grew its drawing primitives one at a time (`screen_draw_line`, +`screen_fill_rect`, `screen_fill_pattern`, `screen_draw_ninepatch`, +`bitmap_draw_pattern`, ...). Without a scheme the names had begun to contradict +each other: + +- "pattern" meant two different things (a two-colour repeating tile vs a + one-colour stencil mask), spelled with two different verbs (`fill` vs `draw`); +- `draw` was overloaded across stroking an outline, blitting a raster, and + placing text; +- "ninepatch" (code) / "9-patch" (prose) / "9tile" (art asset) were three + spellings of one thing. + +New primitives are wanted (filled triangles and polygons, rounded rectangles, +gradients, sprite-sheet tiling). A scheme fixed now keeps their names +predictable. + +## The four axes + +Every drawing operation is one point in a four-axis space. The function name is +built from the axes in order: + +``` +<surface>_<action>_<primitive|raster>[_<paint>][_<qualifier>] +``` + +### Axis 1 — SURFACE (where it draws) + +| prefix | surface | clipping | +|----------|---------|----------| +| `screen` | `screen_t` — the drawing surface | clips every op to `scr->clip` | +| `bitmap` | `bitmap_t` — a raw image | none; whole-image or caller-clipped | + +`screen_t` is `bitmap_ALL_MEMBERS` plus a `box_t clip`. It is the surface a +client or the window manager draws onto. `bitmap_*` drawing ops are for +off-screen construction and whole-image transforms. + +The per-pixel-format `span_t` layer (`copy` / `blendconst` / `blendarray`) sits +below both and is not a public drawing verb. + +### Axis 2 — ACTION (what kind of mark) + +| verb | mark | object it takes | +|---------|------|-----------------| +| `draw` | a 1D mark along a boundary: an outline, a line, a polyline | a **primitive** | +| `fill` | a 2D mark over an interior | a **primitive** | +| `copy` | pixels transferred from another raster | a **raster source** | +| `clear` | an unconditional whole-surface fill | — | + +`draw` and `fill` are the only verbs that name a geometric primitive. `copy` is +the single verb for all pixel transfer — a bitmap, a ninepatch, a sprite-sheet +cell, or a screen-to-screen move. `clear` is `bitmap_clear` only. + +Text (`bmfont_draw`, `bmtext_draw`) keeps its own verb: a string is neither a +primitive nor a raster. + +### Axis 3 — PRIMITIVE (the shape), for `draw` / `fill` + +Ordered by rough complexity: + +`pixel`, `line`, `lines` (polyline), `rect`, `square`, `box`, `roundrect`, +`circle`, `ellipse`, `arc`, `triangle`, `polygon`, `region` (a list of boxes). + +`rect` takes `x, y, size2d_t` with **inclusive** edges (historical). +`box` takes a `const box_t *` — the **half-open** rectangle used everywhere else +in DPTLib. New code should prefer `box`. + +### Axis 4 — PAINT (what goes inside), a suffix + +How a `fill` (or `clear`, or a wide `draw` stroke) obtains its pixels: + +| paint | suffix | source | +|----------|-------------|--------| +| colour | *(none)* | a single `colour_t` | +| pattern | `_pattern` | an 8x8 repeating tile — a `pattern_t` | +| gradient | `_gradient` | an interpolated ramp between stops | + +Sampling from a raster is **not** a paint suffix — that is `copy`. + +## Examples + +``` +screen_fill_rect colour fill, inclusive-edge rect [shipped] +screen_fill_box colour fill, half-open box [planned] +screen_draw_circle 1px circle outline [shipped] +screen_fill_circle solid disc [shipped] +screen_fill_pattern pattern fill on the screen [shipped] +bitmap_fill_pattern pattern fill on a raw bitmap [planned; was bitmap_draw_pattern] +screen_fill_rect_gradient gradient fill of a rect [planned] +screen_copy_bitmap blit a bitmap [planned; was screen_draw_bitmap] +screen_copy_ninepatch blit a resizable 9-patch frame [planned; was screen_draw_ninepatch] +screen_copy_rect screen-to-screen block move [shipped] +screen_draw_line_wu_float anti-aliased line, float coords [shipped] +``` + +## Patterns + +A **pattern** is one concept with one struct and one verb (`fill`): + +```C +typedef struct pattern +{ + uint8_t bits[8]; /* one byte per row, MSB = leftmost pixel */ + colour_t fg; /* painted where a bit is set */ + colour_t bg; /* painted where a bit is clear (see flags) */ + unsigned flags; /* pattern_FLAG_STENCIL: leave bg pixels as-is */ + point_t origin; /* tile phase; {0,0} locks to surface origin */ +} +pattern_t; +``` + +- A plain two-colour tile sets `fg`, `bg` and no flags — every pixel in the area + is written. +- A stencil sets `pattern_FLAG_STENCIL` — only set-bit pixels are written, the + rest of the area is untouched. (This is the old `bitmap_draw_pattern` + behaviour.) +- `origin` phases the tile so abutting fills line up regardless of the fill + rectangle. `{0,0}` phases against the surface origin. + +The built-in tiles — the 65-level Bayer coverage ramp and the named tiles +(`HSTRIPE`, `GRID`, `CROSSHATCH`, ...) — remain available as a +`screen_pattern_t` enum, resolved to a `pattern_t` by a +`pattern_from_preset(screen_pattern_t)` helper. + +Format support: 8bpp paletted and 32bpp. Other depths return +`result_NOT_SUPPORTED`. + +## Ninepatch + +The one-word lowercase spelling **`ninepatch`** is canonical in all code and +identifiers. "9-patch" is fine in prose. The art asset is `ninepatch.png` / +`ninepatch.aseprite`. + +`screen_copy_ninepatch(scr, dst, src, flags)` draws a resizable frame from a +3x3-cell source image (width and height each a positive multiple of 3): the four +corners are drawn at natural size, the four edges are tiled along their run, and +the centre is tiled to fill (unless `screen_NINEPATCH_NO_CENTRE` is set). The +clip is restored on return. + +## Error reporting + +New drawing ops return **`result_t`**. An op that clips away to nothing is not an +error — it returns `result_OK` and draws nothing. + +Existing `void`-returning `screen_draw_*` ops keep their return type. The three +`copy` ops disagree today (`screen_draw_bitmap` / `screen_draw_ninepatch` return +`void`, `screen_copy_rect` returns `int` plus a `box_t *` out-param); aligning +them on `result_t` is a signature change tracked separately from the rename. + +## Rectangle representation + +`box_t` (`{x0, y0, x1, y1}`, half-open — `x1`, `y1` exclusive) is the one +geometric rectangle type. New primitives take `const box_t *`. There is no +separate `rect_t`. `screen_fill_rect`'s inclusive `x, y, size2d_t` signature is +retained; `screen_fill_box` / `screen_draw_box` are the preferred `box_t` +spellings going forward. + +## Status summary + +**Shipped** + +- `screen_set_pixel` +- `screen_draw_line`, `screen_draw_lines`, `screen_draw_dashed_line`, + `screen_draw_line_wu_fix8`, `screen_draw_line_wu_float` +- `screen_draw_rect`, `screen_fill_rect`, `screen_fill_square` +- `screen_draw_circle`, `screen_fill_circle` +- `screen_fill_pattern` (currently takes `screen_pattern_t`; migrates to + `const pattern_t *`) +- `screen_draw_bitmap`, `screen_draw_ninepatch` (rename to `screen_copy_*` + pending) +- `screen_copy_rect` +- `bitmap_clear`, `bitmap_draw_pattern` (rename to `bitmap_fill_pattern` + pending) +- `bmfont_draw`, `bmtext_draw` +- `composite` — see [composite.md](composite.md) + +**Planned — renames** + +| from | to | +|------|----| +| `screen_draw_bitmap` | `screen_copy_bitmap` | +| `screen_draw_ninepatch` | `screen_copy_ninepatch` | +| `bitmap_draw_pattern` | `bitmap_fill_pattern` (+ `pattern_t` argument) | + +Hard renames, no compatibility wrappers, all in-tree call sites updated in the +same change. + +**Planned — new primitives** (priority order) + +1. `screen_fill_triangle` / `screen_draw_triangle` +2. `screen_fill_polygon` / `screen_draw_polygon` (convex first) +3. `screen_fill_roundrect` / `screen_draw_roundrect` +4. `screen_fill_rect_gradient` (linear, two-stop, Bayer-dithered) +5. `screen_copy_cell` (single sprite-sheet cell) +6. `screen_draw_arc` / `screen_draw_ellipse` + +## Related + +- [composite.md](composite.md) — Porter-Duff bitmap compositing +- [bmfont.md](bmfont.md) — proportional bitmap fonts diff --git a/include/framebuf/bitmap.h b/include/framebuf/bitmap.h index 776847ed..34d15095 100644 --- a/include/framebuf/bitmap.h +++ b/include/framebuf/bitmap.h @@ -7,6 +7,7 @@ #include "base/result.h" #include "framebuf/colour.h" +#include "framebuf/pattern.h" #include "framebuf/pixelfmt.h" #include "framebuf/span.h" #include "geom/box.h" @@ -85,12 +86,13 @@ void bitmap_clear(bitmap_t *bm, colour_t colour); //} /** - * Fill a rectangle of the bitmap with a colour, but only where a repeating - * 8x8 pattern mask has a bit set. + * Fill a rectangle of the bitmap with a repeating 8x8 pattern. * - * The mask is eight bytes, one per pattern row, MSB leftmost. It tiles from - * the bitmap origin, so abutting fills line up. Where a mask bit is clear - * the destination pixel is left untouched. + * A plain pattern paints every pixel in the area, set bits taking + * `pattern->fg` and clear bits `pattern->bg`. A stencil pattern (one with + * `pattern_FLAG_STENCIL`) paints only the set-bit pixels, leaving the rest + * untouched. The tile is phased against `pattern->origin`, so abutting fills + * with the same origin line up. * * Supported for 8bpp and 32bpp formats only; other formats return \ref * result_NOT_SUPPORTED. @@ -98,15 +100,13 @@ void bitmap_clear(bitmap_t *bm, colour_t colour); * \param[in] bm Bitmap to fill. * \param[in] area Rectangle to fill, clipped to the bitmap bounds. NULL * fills the whole bitmap. - * \param[in] colour Colour to fill with. - * \param[in] mask Eight-byte 8x8 pattern mask. + * \param[in] pattern Pattern to fill with. * \return \ref result_OK on success, \ref result_NOT_SUPPORTED for an * unsupported pixel format. */ -result_t bitmap_draw_pattern(bitmap_t *bm, - const box_t *area, - colour_t colour, - const uint8_t mask[8]); +result_t bitmap_fill_pattern(bitmap_t *bm, + const box_t *area, + const pattern_t *pattern); /** * Load a PNG image into the given bitmap. diff --git a/include/framebuf/pattern.h b/include/framebuf/pattern.h new file mode 100644 index 00000000..67c783ee --- /dev/null +++ b/include/framebuf/pattern.h @@ -0,0 +1,108 @@ +/* framebuf/pattern.h -- 8x8 repeating fill pattern */ + +#ifndef FRAMEBUF_PATTERN_H +#define FRAMEBUF_PATTERN_H + +#include <stdint.h> + +#include "framebuf/colour.h" +#include "geom/point.h" + +/** + * Built-in 8x8 fill patterns. Each is a 1-bit tile: set bits take the + * foreground colour, clear bits the background. Pass one to + * `pattern_from_preset` to obtain a `pattern_t`. + */ +typedef enum screen_pattern +{ + /** + * 8x8 ordered (Bayer) dither, one entry per coverage level 0 (empty) to 64 + * (solid). Index by level as `screen_PATTERN_BAYER0 + level`; + * `screen_PATTERN_BAYER_LIMIT` is one past the last. + */ + screen_PATTERN_BAYER0 = 0, + screen_PATTERN_BAYER_LIMIT = screen_PATTERN_BAYER0 + 65, + + /** + * A flat fill and a 50% checkerboard are just the ends and midpoint of the + * Bayer run, so they are aliases rather than separate tiles. `EMPTY` is + * the inverse of `SOLID`; `GREY50` is its own inverse. + */ + screen_PATTERN_EMPTY = screen_PATTERN_BAYER0, + screen_PATTERN_GREY50 = screen_PATTERN_BAYER0 + 32, + screen_PATTERN_SOLID = screen_PATTERN_BAYER0 + 64, + + /* The remaining named tiles follow the Bayer run. */ + screen_PATTERN_HSTRIPE = screen_PATTERN_BAYER_LIMIT, + /**< Horizontal bars. */ + screen_PATTERN_VSTRIPE, /**< Vertical bars. */ + screen_PATTERN_DIAGONAL, /**< Diagonal lines. */ + screen_PATTERN_DOTS, /**< Sparse dots. */ + screen_PATTERN_GRID, /**< Thin grid lines. */ + screen_PATTERN_CROSSHATCH, /**< Crossed thin lines. */ + + screen_PATTERN_HSTRIPE_INV, /**< Inverse of HSTRIPE. */ + screen_PATTERN_VSTRIPE_INV, /**< Inverse of VSTRIPE. */ + screen_PATTERN_DIAGONAL_INV, /**< Inverse of DIAGONAL. */ + screen_PATTERN_DOTS_INV, /**< Inverse of DOTS. */ + screen_PATTERN_GRID_INV, /**< Inverse of GRID. */ + screen_PATTERN_CROSSHATCH_INV, /**< Inverse of CROSSHATCH. */ + + screen_PATTERN__LIMIT + /**< Count of patterns; not itself a pattern. */ +} +screen_pattern_t; + +/** Flags for `pattern_t`. */ +enum +{ + /** + * Paint only where a pattern bit is set, leaving clear-bit pixels + * untouched (a stencil). Without this the whole area is painted, clear + * bits taking `bg`. + */ + pattern_FLAG_STENCIL = 1u << 0 +}; + +/** + * An 8x8 repeating fill pattern and how it paints. Passed to + * `screen_fill_pattern` and `bitmap_fill_pattern`. + */ +typedef struct pattern +{ + uint8_t bits[8]; /**< One byte per row, MSB = leftmost pixel. */ + colour_t fg; /**< Colour for set bits. */ + colour_t bg; /**< Colour for clear bits, unless a stencil. */ + unsigned flags; /**< Bitwise OR of `pattern_FLAG_*`, or 0. */ + point_t origin; /**< Tile phase: the coordinate mapping to the fill box's + top-left corner. Passing a scroll origin keeps the + pattern locked to content rather than crawling. */ +} +pattern_t; + +/** + * Build a `pattern_t` from a built-in preset and a pair of colours. The + * result has no flags set and a zero origin; assign `.flags` and `.origin` + * afterwards if needed. + * + * \param[in] preset One of `screen_PATTERN_*`. + * \param[in] fg Colour for set bits. + * \param[in] bg Colour for clear bits. + * \return The pattern. + */ +pattern_t pattern_from_preset(screen_pattern_t preset, + colour_t fg, + colour_t bg); + +/** + * Build a stencil `pattern_t` from a caller-supplied 8x8 mask and a single + * colour. `pattern_FLAG_STENCIL` is set, so clear-bit pixels are left + * untouched; `.bg` is unused. The origin is zero. + * + * \param[in] mask Eight bytes, one per pattern row, MSB leftmost. + * \param[in] colour Colour for set bits. + * \return The pattern. + */ +pattern_t pattern_from_mask(const uint8_t mask[8], colour_t colour); + +#endif /* FRAMEBUF_PATTERN_H */ diff --git a/include/framebuf/screen.h b/include/framebuf/screen.h index be9e7bc9..4f1cb247 100644 --- a/include/framebuf/screen.h +++ b/include/framebuf/screen.h @@ -4,6 +4,7 @@ #define FRAMEBUF_SCREEN_H #include "framebuf/bitmap.h" +#include "framebuf/pattern.h" #include "geom/box.h" #include "geom/point.h" #include "utils/fxp.h" @@ -96,80 +97,30 @@ void screen_fill_square(screen_t *scr, colour_t colour); /** - * Built-in 8x8 fill patterns for `screen_fill_pattern`. Each is a 1-bit - * tile: set bits take the foreground colour, clear bits the background. - */ -typedef enum screen_pattern -{ - /** - * 8x8 ordered (Bayer) dither, one entry per coverage level 0 (empty) to 64 - * (solid). Index by level as `screen_PATTERN_BAYER0 + level`; - * `screen_PATTERN_BAYER_LIMIT` is one past the last. - */ - screen_PATTERN_BAYER0 = 0, - screen_PATTERN_BAYER_LIMIT = screen_PATTERN_BAYER0 + 65, - - /** - * A flat fill and a 50% checkerboard are just the ends and midpoint of the - * Bayer run, so they are aliases rather than separate tiles. `EMPTY` is - * the inverse of `SOLID`; `GREY50` is its own inverse. - */ - screen_PATTERN_EMPTY = screen_PATTERN_BAYER0, - screen_PATTERN_GREY50 = screen_PATTERN_BAYER0 + 32, - screen_PATTERN_SOLID = screen_PATTERN_BAYER0 + 64, - - /* The remaining named tiles follow the Bayer run. */ - screen_PATTERN_HSTRIPE = screen_PATTERN_BAYER_LIMIT, - /**< Horizontal bars. */ - screen_PATTERN_VSTRIPE, /**< Vertical bars. */ - screen_PATTERN_DIAGONAL, /**< Diagonal lines. */ - screen_PATTERN_DOTS, /**< Sparse dots. */ - screen_PATTERN_GRID, /**< Thin grid lines. */ - screen_PATTERN_CROSSHATCH, /**< Crossed thin lines. */ - - screen_PATTERN_HSTRIPE_INV, /**< Inverse of HSTRIPE. */ - screen_PATTERN_VSTRIPE_INV, /**< Inverse of VSTRIPE. */ - screen_PATTERN_DIAGONAL_INV, /**< Inverse of DIAGONAL. */ - screen_PATTERN_DOTS_INV, /**< Inverse of DOTS. */ - screen_PATTERN_GRID_INV, /**< Inverse of GRID. */ - screen_PATTERN_CROSSHATCH_INV, /**< Inverse of CROSSHATCH. */ - - screen_PATTERN__LIMIT - /**< Count of patterns; not itself a pattern. */ -} -screen_pattern_t; - -/** - * Fills a box with a repeating 8x8 two-colour pattern. + * Fills a box with a repeating 8x8 pattern. * - * The tile is phased against (`origin_x`, `origin_y`): that coordinate is - * the one that would map to the box's top-left corner. Passing the caller's - * own scroll origin keeps the pattern locked to content as the box moves, - * rather than crawling with it. + * A plain pattern paints every pixel in the box, set bits taking + * `pattern->fg` and clear bits `pattern->bg`. A stencil pattern (one with + * `pattern_FLAG_STENCIL`) paints only the set-bit pixels. The tile is phased + * against `pattern->origin`: that coordinate is the one that maps to the + * box's top-left corner. Passing the caller's own scroll origin keeps the + * pattern locked to content as the box moves, rather than crawling with it. * * Clipped to the screen's clip region. * * \param[in] scr Screen to draw upon. * \param[in] box Box to fill, inclusive-exclusive. * \param[in] pattern Pattern to fill with. - * \param[in] origin_x X coordinate mapping to `box->x0` for tile phase. - * \param[in] origin_y Y coordinate mapping to `box->y0` for tile phase. - * \param[in] fg Colour for set pattern bits. - * \param[in] bg Colour for clear pattern bits. */ void screen_fill_pattern(screen_t *scr, const box_t *box, - screen_pattern_t pattern, - int origin_x, - int origin_y, - colour_t fg, - colour_t bg); + const pattern_t *pattern); /** - * Draws a bitmap, alpha-blending it against the screen where the bitmap has - * an alpha channel. On paletted screens, which have no linear channel bits - * to blend, this falls back to alpha-tested transparency instead (drawn at - * full strength, or not at all). + * Copies a bitmap onto the screen, alpha-blending it against the screen + * where the bitmap has an alpha channel. On paletted screens, which have no + * linear channel bits to blend, this falls back to alpha-tested transparency + * instead (drawn at full strength, or not at all). * * The bitmap is clipped to the screen's clip region. No scaling is * performed. @@ -177,29 +128,29 @@ void screen_fill_pattern(screen_t *scr, * \param[in] scr Screen to draw upon. * \param[in] x X coordinate of leftmost point to draw bitmap at. * \param[in] y Y coordinate of topmost point to draw bitmap at. - * \param[in] src Bitmap to draw. + * \param[in] src Bitmap to copy. */ -void screen_draw_bitmap(screen_t *scr, int x, int y, const bitmap_t *src); +void screen_copy_bitmap(screen_t *scr, int x, int y, const bitmap_t *src); -/** Flags for `screen_draw_ninepatch`. */ +/** Flags for `screen_copy_ninepatch`. */ enum { screen_NINEPATCH_NO_CENTRE = 1u << 0 /**< Leave the interior untouched. */ }; /** - * Draws a "9-patch": a resizable frame built from a source image that is a - * 3x3 grid of equal cells. The source width and height must each be a - * positive multiple of 3; the cell size is a third of each. Given a - * destination box, the four corner cells are drawn at their natural size in - * the destination corners, the four edge cells are tiled along the + * Copies a "9-patch" onto the screen: a resizable frame built from a source + * image that is a 3x3 grid of equal cells. The source width and height must + * each be a positive multiple of 3; the cell size is a third of each. Given + * a destination box, the four corner cells are drawn at their natural size + * in the destination corners, the four edge cells are tiled along the * destination edges, and the centre cell is tiled across the interior. * * If the destination is narrower or shorter than two cells the opposing * corners overlap and each is clipped to its own half; the edges and centre * are then omitted. Drawing is clipped to both the destination box and the * screen's clip region, which is restored on return. Cells are blended - * exactly as `screen_draw_bitmap` does. No scaling is performed. + * exactly as `screen_copy_bitmap` does. No scaling is performed. * * \param[in] scr Screen to draw upon. * \param[in] dst Destination box to fill with the frame. @@ -208,7 +159,7 @@ enum * `screen_NINEPATCH_NO_CENTRE` to draw only the border and * leave the interior untouched. */ -void screen_draw_ninepatch(screen_t *scr, +void screen_copy_ninepatch(screen_t *scr, const box_t *dst, const bitmap_t *src, unsigned int flags); diff --git a/libraries/framebuf/bitmap/draw-pattern.c b/libraries/framebuf/bitmap/draw-pattern.c deleted file mode 100644 index 67431b4e..00000000 --- a/libraries/framebuf/bitmap/draw-pattern.c +++ /dev/null @@ -1,82 +0,0 @@ -/* framebuf/bitmap/draw-pattern.c -- pattern-masked rectangle fill */ - -#include <assert.h> -#include <stddef.h> -#include <stdint.h> - -#include "geom/box.h" - -#include "framebuf/bitmap.h" - -result_t bitmap_draw_pattern(bitmap_t *bm, - const box_t *area, - colour_t colour, - const uint8_t mask[8]) -{ - int log2bpp; - pixelfmt_any_t px; - box_t full; - box_t clip; - int x, y; - - assert(bm); - assert(mask); - - log2bpp = pixelfmt_log2bpp(bm->format); - if (log2bpp != 3 && log2bpp != 5) - return result_NOT_SUPPORTED; - - full.x0 = 0; - full.y0 = 0; - full.x1 = bm->size.w; - full.y1 = bm->size.h; - - if (area == NULL) - { - clip = full; - } - else if (!box_intersection(area, &full, &clip)) - { - return result_OK; /* nothing to fill */ - } - - px = colour_to_pixel(bm->palette, - bm->palette ? 1 << (1 << log2bpp) : 0, - colour, - bm->format); - - if (log2bpp == 3) /* 8bpp */ - { - uint8_t *base; - uint8_t *row; - - base = bm->base; - for (y = clip.y0; y < clip.y1; y++) - { - uint8_t bits = mask[y & 7]; - - row = base + (size_t) y * bm->rowbytes; - for (x = clip.x0; x < clip.x1; x++) - if (bits & (0x80 >> (x & 7))) - row[x] = (uint8_t) px; - } - } - else /* 32bpp */ - { - uint8_t *base; - pixelfmt_any32_t *row; - - base = bm->base; - for (y = clip.y0; y < clip.y1; y++) - { - uint8_t bits = mask[y & 7]; - - row = (pixelfmt_any32_t *) (base + (size_t) y * bm->rowbytes); - for (x = clip.x0; x < clip.x1; x++) - if (bits & (0x80 >> (x & 7))) - row[x] = px; - } - } - - return result_OK; -} diff --git a/libraries/framebuf/bitmap/fill-pattern.c b/libraries/framebuf/bitmap/fill-pattern.c new file mode 100644 index 00000000..f30ea2ea --- /dev/null +++ b/libraries/framebuf/bitmap/fill-pattern.c @@ -0,0 +1,101 @@ +/* framebuf/bitmap/fill-pattern.c -- fill a bitmap rectangle with an 8x8 pattern */ + +#include <assert.h> +#include <stddef.h> +#include <stdint.h> + +#include "geom/box.h" + +#include "framebuf/pattern.h" + +#include "framebuf/bitmap.h" + +result_t bitmap_fill_pattern(bitmap_t *bm, + const box_t *area, + const pattern_t *pattern) +{ + int log2bpp; + int stencil; + pixelfmt_any_t fg; + pixelfmt_any_t bg; + box_t full; + box_t clip; + int xphase, yphase; + int x, y; + + assert(bm); + assert(pattern); + + log2bpp = pixelfmt_log2bpp(bm->format); + if (log2bpp != 3 && log2bpp != 5) + return result_NOT_SUPPORTED; + + full.x0 = 0; + full.y0 = 0; + full.x1 = bm->size.w; + full.y1 = bm->size.h; + + if (area == NULL) + { + clip = full; + } + else if (!box_intersection(area, &full, &clip)) + { + return result_OK; /* nothing to fill */ + } + + stencil = (pattern->flags & pattern_FLAG_STENCIL) != 0; + + fg = colour_to_pixel(bm->palette, + bm->palette ? 1 << (1 << log2bpp) : 0, + pattern->fg, + bm->format); + bg = colour_to_pixel(bm->palette, + bm->palette ? 1 << (1 << log2bpp) : 0, + pattern->bg, + bm->format); + + /* The tile phase: the coordinate in "origin" is the one that maps to the + * pattern's leftmost/topmost bit. */ + xphase = pattern->origin.x; + yphase = pattern->origin.y; + + if (log2bpp == 3) /* 8bpp */ + { + uint8_t *base; + uint8_t *row; + + base = bm->base; + for (y = clip.y0; y < clip.y1; y++) + { + uint8_t bits = pattern->bits[(y - yphase) & 7]; + + row = base + (size_t) y * bm->rowbytes; + for (x = clip.x0; x < clip.x1; x++) + if (bits & (0x80 >> ((x - xphase) & 7))) + row[x] = (uint8_t) fg; + else if (!stencil) + row[x] = (uint8_t) bg; + } + } + else /* 32bpp */ + { + uint8_t *base; + pixelfmt_any32_t *row; + + base = bm->base; + for (y = clip.y0; y < clip.y1; y++) + { + uint8_t bits = pattern->bits[(y - yphase) & 7]; + + row = (pixelfmt_any32_t *) (base + (size_t) y * bm->rowbytes); + for (x = clip.x0; x < clip.x1; x++) + if (bits & (0x80 >> ((x - xphase) & 7))) + row[x] = fg; + else if (!stencil) + row[x] = bg; + } + } + + return result_OK; +} diff --git a/libraries/framebuf/pattern/pattern.c b/libraries/framebuf/pattern/pattern.c new file mode 100644 index 00000000..42634b9d --- /dev/null +++ b/libraries/framebuf/pattern/pattern.c @@ -0,0 +1,139 @@ +/* framebuf/pattern/pattern.c -- 8x8 fill pattern constructors and preset table */ + +#include <assert.h> +#include <stddef.h> +#include <stdint.h> +#include <string.h> + +#include "framebuf/colour.h" + +#include "framebuf/pattern.h" + +/* One byte per row, MSB = leftmost pixel. + * + * The 65 Bayer levels come first so a caller can index by coverage level as + * screen_PATTERN_BAYER0 + level. BAYER0/BAYER32/BAYER64 double as the EMPTY, + * GREY50 and SOLID patterns (see the enum), so those have no rows of their + * own. The named non-Bayer tiles and their inverses follow. */ +static const uint8_t patterns[screen_PATTERN__LIMIT][8] = +{ + /* 8x8 ordered (Bayer) dither, one row per coverage level 0..64. Level N sets + * the pixels whose threshold in the recursively-built 8x8 matrix is < N. */ + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* BAYER0 */ + { 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* BAYER1 */ + { 0x80, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00 }, /* BAYER2 */ + { 0x80, 0x00, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00 }, /* BAYER3 */ + { 0x88, 0x00, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00 }, /* BAYER4 */ + { 0x88, 0x00, 0x20, 0x00, 0x88, 0x00, 0x00, 0x00 }, /* BAYER5 */ + { 0x88, 0x00, 0x20, 0x00, 0x88, 0x00, 0x02, 0x00 }, /* BAYER6 */ + { 0x88, 0x00, 0x20, 0x00, 0x88, 0x00, 0x22, 0x00 }, /* BAYER7 */ + { 0x88, 0x00, 0x22, 0x00, 0x88, 0x00, 0x22, 0x00 }, /* BAYER8 */ + { 0x88, 0x00, 0xA2, 0x00, 0x88, 0x00, 0x22, 0x00 }, /* BAYER9 */ + { 0x88, 0x00, 0xA2, 0x00, 0x88, 0x00, 0x2A, 0x00 }, /* BAYER10 */ + { 0x88, 0x00, 0xA2, 0x00, 0x88, 0x00, 0xAA, 0x00 }, /* BAYER11 */ + { 0x88, 0x00, 0xAA, 0x00, 0x88, 0x00, 0xAA, 0x00 }, /* BAYER12 */ + { 0xA8, 0x00, 0xAA, 0x00, 0x88, 0x00, 0xAA, 0x00 }, /* BAYER13 */ + { 0xA8, 0x00, 0xAA, 0x00, 0x8A, 0x00, 0xAA, 0x00 }, /* BAYER14 */ + { 0xA8, 0x00, 0xAA, 0x00, 0xAA, 0x00, 0xAA, 0x00 }, /* BAYER15 */ + { 0xAA, 0x00, 0xAA, 0x00, 0xAA, 0x00, 0xAA, 0x00 }, /* BAYER16 */ + { 0xAA, 0x40, 0xAA, 0x00, 0xAA, 0x00, 0xAA, 0x00 }, /* BAYER17 */ + { 0xAA, 0x40, 0xAA, 0x00, 0xAA, 0x04, 0xAA, 0x00 }, /* BAYER18 */ + { 0xAA, 0x40, 0xAA, 0x00, 0xAA, 0x44, 0xAA, 0x00 }, /* BAYER19 */ + { 0xAA, 0x44, 0xAA, 0x00, 0xAA, 0x44, 0xAA, 0x00 }, /* BAYER20 */ + { 0xAA, 0x44, 0xAA, 0x10, 0xAA, 0x44, 0xAA, 0x00 }, /* BAYER21 */ + { 0xAA, 0x44, 0xAA, 0x10, 0xAA, 0x44, 0xAA, 0x01 }, /* BAYER22 */ + { 0xAA, 0x44, 0xAA, 0x10, 0xAA, 0x44, 0xAA, 0x11 }, /* BAYER23 */ + { 0xAA, 0x44, 0xAA, 0x11, 0xAA, 0x44, 0xAA, 0x11 }, /* BAYER24 */ + { 0xAA, 0x44, 0xAA, 0x51, 0xAA, 0x44, 0xAA, 0x11 }, /* BAYER25 */ + { 0xAA, 0x44, 0xAA, 0x51, 0xAA, 0x44, 0xAA, 0x15 }, /* BAYER26 */ + { 0xAA, 0x44, 0xAA, 0x51, 0xAA, 0x44, 0xAA, 0x55 }, /* BAYER27 */ + { 0xAA, 0x44, 0xAA, 0x55, 0xAA, 0x44, 0xAA, 0x55 }, /* BAYER28 */ + { 0xAA, 0x54, 0xAA, 0x55, 0xAA, 0x44, 0xAA, 0x55 }, /* BAYER29 */ + { 0xAA, 0x54, 0xAA, 0x55, 0xAA, 0x45, 0xAA, 0x55 }, /* BAYER30 */ + { 0xAA, 0x54, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55 }, /* BAYER31 */ + { 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55 }, /* BAYER32 */ + { 0xAA, 0xD5, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55 }, /* BAYER33 */ + { 0xAA, 0xD5, 0xAA, 0x55, 0xAA, 0x5D, 0xAA, 0x55 }, /* BAYER34 */ + { 0xAA, 0xD5, 0xAA, 0x55, 0xAA, 0xDD, 0xAA, 0x55 }, /* BAYER35 */ + { 0xAA, 0xDD, 0xAA, 0x55, 0xAA, 0xDD, 0xAA, 0x55 }, /* BAYER36 */ + { 0xAA, 0xDD, 0xAA, 0x75, 0xAA, 0xDD, 0xAA, 0x55 }, /* BAYER37 */ + { 0xAA, 0xDD, 0xAA, 0x75, 0xAA, 0xDD, 0xAA, 0x57 }, /* BAYER38 */ + { 0xAA, 0xDD, 0xAA, 0x75, 0xAA, 0xDD, 0xAA, 0x77 }, /* BAYER39 */ + { 0xAA, 0xDD, 0xAA, 0x77, 0xAA, 0xDD, 0xAA, 0x77 }, /* BAYER40 */ + { 0xAA, 0xDD, 0xAA, 0xF7, 0xAA, 0xDD, 0xAA, 0x77 }, /* BAYER41 */ + { 0xAA, 0xDD, 0xAA, 0xF7, 0xAA, 0xDD, 0xAA, 0x7F }, /* BAYER42 */ + { 0xAA, 0xDD, 0xAA, 0xF7, 0xAA, 0xDD, 0xAA, 0xFF }, /* BAYER43 */ + { 0xAA, 0xDD, 0xAA, 0xFF, 0xAA, 0xDD, 0xAA, 0xFF }, /* BAYER44 */ + { 0xAA, 0xFD, 0xAA, 0xFF, 0xAA, 0xDD, 0xAA, 0xFF }, /* BAYER45 */ + { 0xAA, 0xFD, 0xAA, 0xFF, 0xAA, 0xDF, 0xAA, 0xFF }, /* BAYER46 */ + { 0xAA, 0xFD, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF }, /* BAYER47 */ + { 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF }, /* BAYER48 */ + { 0xEA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF }, /* BAYER49 */ + { 0xEA, 0xFF, 0xAA, 0xFF, 0xAE, 0xFF, 0xAA, 0xFF }, /* BAYER50 */ + { 0xEA, 0xFF, 0xAA, 0xFF, 0xEE, 0xFF, 0xAA, 0xFF }, /* BAYER51 */ + { 0xEE, 0xFF, 0xAA, 0xFF, 0xEE, 0xFF, 0xAA, 0xFF }, /* BAYER52 */ + { 0xEE, 0xFF, 0xBA, 0xFF, 0xEE, 0xFF, 0xAA, 0xFF }, /* BAYER53 */ + { 0xEE, 0xFF, 0xBA, 0xFF, 0xEE, 0xFF, 0xAB, 0xFF }, /* BAYER54 */ + { 0xEE, 0xFF, 0xBA, 0xFF, 0xEE, 0xFF, 0xBB, 0xFF }, /* BAYER55 */ + { 0xEE, 0xFF, 0xBB, 0xFF, 0xEE, 0xFF, 0xBB, 0xFF }, /* BAYER56 */ + { 0xEE, 0xFF, 0xFB, 0xFF, 0xEE, 0xFF, 0xBB, 0xFF }, /* BAYER57 */ + { 0xEE, 0xFF, 0xFB, 0xFF, 0xEE, 0xFF, 0xBF, 0xFF }, /* BAYER58 */ + { 0xEE, 0xFF, 0xFB, 0xFF, 0xEE, 0xFF, 0xFF, 0xFF }, /* BAYER59 */ + { 0xEE, 0xFF, 0xFF, 0xFF, 0xEE, 0xFF, 0xFF, 0xFF }, /* BAYER60 */ + { 0xFE, 0xFF, 0xFF, 0xFF, 0xEE, 0xFF, 0xFF, 0xFF }, /* BAYER61 */ + { 0xFE, 0xFF, 0xFF, 0xFF, 0xEF, 0xFF, 0xFF, 0xFF }, /* BAYER62 */ + { 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }, /* BAYER63 */ + { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }, /* BAYER64 */ + + /* Named non-Bayer tiles. */ + { 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00 }, /* HSTRIPE */ + { 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA }, /* VSTRIPE */ + { 0x88, 0x44, 0x22, 0x11, 0x88, 0x44, 0x22, 0x11 }, /* DIAGONAL */ + { 0x88, 0x00, 0x22, 0x00, 0x88, 0x00, 0x22, 0x00 }, /* DOTS */ + { 0xFF, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80 }, /* GRID */ + { 0x80, 0x41, 0x22, 0x14, 0x08, 0x14, 0x22, 0x41 }, /* CROSSHATCH */ + + /* Inverses of the six above (bitwise NOT of each row). */ + { 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF }, /* HSTRIPE_INV */ + { 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55 }, /* VSTRIPE_INV */ + { 0x77, 0xBB, 0xDD, 0xEE, 0x77, 0xBB, 0xDD, 0xEE }, /* DIAGONAL_INV */ + { 0x77, 0xFF, 0xDD, 0xFF, 0x77, 0xFF, 0xDD, 0xFF }, /* DOTS_INV */ + { 0x00, 0x7F, 0x7F, 0x7F, 0x7F, 0x7F, 0x7F, 0x7F }, /* GRID_INV */ + { 0x7F, 0xBE, 0xDD, 0xEB, 0xF7, 0xEB, 0xDD, 0xBE } /* CROSSHATCH_INV */ +}; + +/* the enum leaves the 65 Bayer levels unnamed past BAYER0, so keep the table + * length pinned to it here */ +typedef char + patterns_len_matches_enum[(sizeof patterns / sizeof patterns[0]) == + screen_PATTERN__LIMIT ? 1 : -1]; + +pattern_t pattern_from_preset(screen_pattern_t preset, + colour_t fg, + colour_t bg) +{ + pattern_t pat; + + assert(preset >= 0 && preset < screen_PATTERN__LIMIT); + + memset(&pat, 0, sizeof pat); + memcpy(pat.bits, patterns[preset], sizeof pat.bits); + pat.fg = fg; + pat.bg = bg; + + return pat; +} + +pattern_t pattern_from_mask(const uint8_t mask[8], colour_t colour) +{ + pattern_t pat; + + assert(mask); + + memset(&pat, 0, sizeof pat); + memcpy(pat.bits, mask, sizeof pat.bits); + pat.fg = colour; + pat.flags = pattern_FLAG_STENCIL; + + return pat; +} diff --git a/libraries/framebuf/screen/screen-draw-ninepatch.c b/libraries/framebuf/screen/screen-copy-ninepatch.c similarity index 95% rename from libraries/framebuf/screen/screen-draw-ninepatch.c rename to libraries/framebuf/screen/screen-copy-ninepatch.c index 6dac6608..afc92b52 100644 --- a/libraries/framebuf/screen/screen-draw-ninepatch.c +++ b/libraries/framebuf/screen/screen-copy-ninepatch.c @@ -1,4 +1,4 @@ -/* framebuf/screen/screen-draw-ninepatch.c -- 9-patch bitmap drawing */ +/* framebuf/screen/screen-copy-ninepatch.c -- 9-patch bitmap drawing */ #include <assert.h> @@ -12,7 +12,7 @@ /* ----------------------------------------------------------------------- */ /* Build a bitmap_t view onto one cell (col,row) of a 3x3 grid within "src". - * The view shares "src"'s rowbytes, so screen_draw_bitmap walks the parent + * The view shares "src"'s rowbytes, so screen_copy_bitmap walks the parent * image correctly despite the narrower size. */ static void ninepatch_cell(bitmap_t *cell, const bitmap_t *src, @@ -68,14 +68,14 @@ static void tile_area(screen_t *scr, { for (ox = area->x0; ox < area->x1; ox += (stepx > 0) ? stepx : (area->x1 - ox)) - screen_draw_bitmap(scr, ox, oy, cell); + screen_copy_bitmap(scr, ox, oy, cell); } } } /* ----------------------------------------------------------------------- */ -void screen_draw_ninepatch(screen_t *scr, +void screen_copy_ninepatch(screen_t *scr, const box_t *dst, const bitmap_t *src, unsigned int flags) diff --git a/libraries/framebuf/screen/screen-draw.c b/libraries/framebuf/screen/screen-draw.c index b3409057..88ebf85c 100644 --- a/libraries/framebuf/screen/screen-draw.c +++ b/libraries/framebuf/screen/screen-draw.c @@ -15,7 +15,7 @@ #include "framebuf/screen.h" -/* Number of pixels converted per span call in screen_draw_bitmap(). Bounds +/* 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). */ #define BITMAP_BLIT_CHUNK 256 @@ -226,7 +226,7 @@ void screen_fill_square(screen_t *scr, /* ----------------------------------------------------------------------- */ -void screen_draw_bitmap(screen_t *scr, int x, int y, const bitmap_t *src) +void screen_copy_bitmap(screen_t *scr, int x, int y, const bitmap_t *src) { box_t clip_box; box_t src_box; diff --git a/libraries/framebuf/screen/screen-fill-pattern.c b/libraries/framebuf/screen/screen-fill-pattern.c index 9d735677..fb689ea2 100644 --- a/libraries/framebuf/screen/screen-fill-pattern.c +++ b/libraries/framebuf/screen/screen-fill-pattern.c @@ -1,134 +1,29 @@ -/* framebuf/screen/screen-fill-pattern.c -- fill a box with a repeating 8x8 two-colour pattern */ +/* framebuf/screen/screen-fill-pattern.c -- fill a box with a repeating 8x8 pattern */ #include <assert.h> #include <string.h> #include "framebuf/colour.h" +#include "framebuf/pattern.h" #include "framebuf/pixelfmt.h" #include "geom/box.h" #include "framebuf/screen.h" -/* One byte per row, MSB = leftmost pixel. - * - * The 65 Bayer levels come first so a caller can index by coverage level as - * screen_PATTERN_BAYER0 + level. BAYER0/BAYER32/BAYER64 double as the EMPTY, - * GREY50 and SOLID patterns (see the enum), so those have no rows of their - * own. The named non-Bayer tiles and their inverses follow. */ -static const unsigned char patterns[screen_PATTERN__LIMIT][8] = -{ - /* 8x8 ordered (Bayer) dither, one row per coverage level 0..64. Level N sets - * the pixels whose threshold in the recursively-built 8x8 matrix is < N. */ - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* BAYER0 */ - { 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* BAYER1 */ - { 0x80, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00 }, /* BAYER2 */ - { 0x80, 0x00, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00 }, /* BAYER3 */ - { 0x88, 0x00, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00 }, /* BAYER4 */ - { 0x88, 0x00, 0x20, 0x00, 0x88, 0x00, 0x00, 0x00 }, /* BAYER5 */ - { 0x88, 0x00, 0x20, 0x00, 0x88, 0x00, 0x02, 0x00 }, /* BAYER6 */ - { 0x88, 0x00, 0x20, 0x00, 0x88, 0x00, 0x22, 0x00 }, /* BAYER7 */ - { 0x88, 0x00, 0x22, 0x00, 0x88, 0x00, 0x22, 0x00 }, /* BAYER8 */ - { 0x88, 0x00, 0xA2, 0x00, 0x88, 0x00, 0x22, 0x00 }, /* BAYER9 */ - { 0x88, 0x00, 0xA2, 0x00, 0x88, 0x00, 0x2A, 0x00 }, /* BAYER10 */ - { 0x88, 0x00, 0xA2, 0x00, 0x88, 0x00, 0xAA, 0x00 }, /* BAYER11 */ - { 0x88, 0x00, 0xAA, 0x00, 0x88, 0x00, 0xAA, 0x00 }, /* BAYER12 */ - { 0xA8, 0x00, 0xAA, 0x00, 0x88, 0x00, 0xAA, 0x00 }, /* BAYER13 */ - { 0xA8, 0x00, 0xAA, 0x00, 0x8A, 0x00, 0xAA, 0x00 }, /* BAYER14 */ - { 0xA8, 0x00, 0xAA, 0x00, 0xAA, 0x00, 0xAA, 0x00 }, /* BAYER15 */ - { 0xAA, 0x00, 0xAA, 0x00, 0xAA, 0x00, 0xAA, 0x00 }, /* BAYER16 */ - { 0xAA, 0x40, 0xAA, 0x00, 0xAA, 0x00, 0xAA, 0x00 }, /* BAYER17 */ - { 0xAA, 0x40, 0xAA, 0x00, 0xAA, 0x04, 0xAA, 0x00 }, /* BAYER18 */ - { 0xAA, 0x40, 0xAA, 0x00, 0xAA, 0x44, 0xAA, 0x00 }, /* BAYER19 */ - { 0xAA, 0x44, 0xAA, 0x00, 0xAA, 0x44, 0xAA, 0x00 }, /* BAYER20 */ - { 0xAA, 0x44, 0xAA, 0x10, 0xAA, 0x44, 0xAA, 0x00 }, /* BAYER21 */ - { 0xAA, 0x44, 0xAA, 0x10, 0xAA, 0x44, 0xAA, 0x01 }, /* BAYER22 */ - { 0xAA, 0x44, 0xAA, 0x10, 0xAA, 0x44, 0xAA, 0x11 }, /* BAYER23 */ - { 0xAA, 0x44, 0xAA, 0x11, 0xAA, 0x44, 0xAA, 0x11 }, /* BAYER24 */ - { 0xAA, 0x44, 0xAA, 0x51, 0xAA, 0x44, 0xAA, 0x11 }, /* BAYER25 */ - { 0xAA, 0x44, 0xAA, 0x51, 0xAA, 0x44, 0xAA, 0x15 }, /* BAYER26 */ - { 0xAA, 0x44, 0xAA, 0x51, 0xAA, 0x44, 0xAA, 0x55 }, /* BAYER27 */ - { 0xAA, 0x44, 0xAA, 0x55, 0xAA, 0x44, 0xAA, 0x55 }, /* BAYER28 */ - { 0xAA, 0x54, 0xAA, 0x55, 0xAA, 0x44, 0xAA, 0x55 }, /* BAYER29 */ - { 0xAA, 0x54, 0xAA, 0x55, 0xAA, 0x45, 0xAA, 0x55 }, /* BAYER30 */ - { 0xAA, 0x54, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55 }, /* BAYER31 */ - { 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55 }, /* BAYER32 */ - { 0xAA, 0xD5, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55 }, /* BAYER33 */ - { 0xAA, 0xD5, 0xAA, 0x55, 0xAA, 0x5D, 0xAA, 0x55 }, /* BAYER34 */ - { 0xAA, 0xD5, 0xAA, 0x55, 0xAA, 0xDD, 0xAA, 0x55 }, /* BAYER35 */ - { 0xAA, 0xDD, 0xAA, 0x55, 0xAA, 0xDD, 0xAA, 0x55 }, /* BAYER36 */ - { 0xAA, 0xDD, 0xAA, 0x75, 0xAA, 0xDD, 0xAA, 0x55 }, /* BAYER37 */ - { 0xAA, 0xDD, 0xAA, 0x75, 0xAA, 0xDD, 0xAA, 0x57 }, /* BAYER38 */ - { 0xAA, 0xDD, 0xAA, 0x75, 0xAA, 0xDD, 0xAA, 0x77 }, /* BAYER39 */ - { 0xAA, 0xDD, 0xAA, 0x77, 0xAA, 0xDD, 0xAA, 0x77 }, /* BAYER40 */ - { 0xAA, 0xDD, 0xAA, 0xF7, 0xAA, 0xDD, 0xAA, 0x77 }, /* BAYER41 */ - { 0xAA, 0xDD, 0xAA, 0xF7, 0xAA, 0xDD, 0xAA, 0x7F }, /* BAYER42 */ - { 0xAA, 0xDD, 0xAA, 0xF7, 0xAA, 0xDD, 0xAA, 0xFF }, /* BAYER43 */ - { 0xAA, 0xDD, 0xAA, 0xFF, 0xAA, 0xDD, 0xAA, 0xFF }, /* BAYER44 */ - { 0xAA, 0xFD, 0xAA, 0xFF, 0xAA, 0xDD, 0xAA, 0xFF }, /* BAYER45 */ - { 0xAA, 0xFD, 0xAA, 0xFF, 0xAA, 0xDF, 0xAA, 0xFF }, /* BAYER46 */ - { 0xAA, 0xFD, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF }, /* BAYER47 */ - { 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF }, /* BAYER48 */ - { 0xEA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF }, /* BAYER49 */ - { 0xEA, 0xFF, 0xAA, 0xFF, 0xAE, 0xFF, 0xAA, 0xFF }, /* BAYER50 */ - { 0xEA, 0xFF, 0xAA, 0xFF, 0xEE, 0xFF, 0xAA, 0xFF }, /* BAYER51 */ - { 0xEE, 0xFF, 0xAA, 0xFF, 0xEE, 0xFF, 0xAA, 0xFF }, /* BAYER52 */ - { 0xEE, 0xFF, 0xBA, 0xFF, 0xEE, 0xFF, 0xAA, 0xFF }, /* BAYER53 */ - { 0xEE, 0xFF, 0xBA, 0xFF, 0xEE, 0xFF, 0xAB, 0xFF }, /* BAYER54 */ - { 0xEE, 0xFF, 0xBA, 0xFF, 0xEE, 0xFF, 0xBB, 0xFF }, /* BAYER55 */ - { 0xEE, 0xFF, 0xBB, 0xFF, 0xEE, 0xFF, 0xBB, 0xFF }, /* BAYER56 */ - { 0xEE, 0xFF, 0xFB, 0xFF, 0xEE, 0xFF, 0xBB, 0xFF }, /* BAYER57 */ - { 0xEE, 0xFF, 0xFB, 0xFF, 0xEE, 0xFF, 0xBF, 0xFF }, /* BAYER58 */ - { 0xEE, 0xFF, 0xFB, 0xFF, 0xEE, 0xFF, 0xFF, 0xFF }, /* BAYER59 */ - { 0xEE, 0xFF, 0xFF, 0xFF, 0xEE, 0xFF, 0xFF, 0xFF }, /* BAYER60 */ - { 0xFE, 0xFF, 0xFF, 0xFF, 0xEE, 0xFF, 0xFF, 0xFF }, /* BAYER61 */ - { 0xFE, 0xFF, 0xFF, 0xFF, 0xEF, 0xFF, 0xFF, 0xFF }, /* BAYER62 */ - { 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }, /* BAYER63 */ - { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }, /* BAYER64 */ - - /* Named non-Bayer tiles. */ - { 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00 }, /* HSTRIPE */ - { 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA }, /* VSTRIPE */ - { 0x88, 0x44, 0x22, 0x11, 0x88, 0x44, 0x22, 0x11 }, /* DIAGONAL */ - { 0x88, 0x00, 0x22, 0x00, 0x88, 0x00, 0x22, 0x00 }, /* DOTS */ - { 0xFF, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80 }, /* GRID */ - { 0x80, 0x41, 0x22, 0x14, 0x08, 0x14, 0x22, 0x41 }, /* CROSSHATCH */ - - /* Inverses of the six above (bitwise NOT of each row). */ - { 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF }, /* HSTRIPE_INV */ - { 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55 }, /* VSTRIPE_INV */ - { 0x77, 0xBB, 0xDD, 0xEE, 0x77, 0xBB, 0xDD, 0xEE }, /* DIAGONAL_INV */ - { 0x77, 0xFF, 0xDD, 0xFF, 0x77, 0xFF, 0xDD, 0xFF }, /* DOTS_INV */ - { 0x00, 0x7F, 0x7F, 0x7F, 0x7F, 0x7F, 0x7F, 0x7F }, /* GRID_INV */ - { 0x7F, 0xBE, 0xDD, 0xEB, 0xF7, 0xEB, 0xDD, 0xBE } /* CROSSHATCH_INV */ -}; - -/* the enum leaves the 65 Bayer levels unnamed past BAYER0, so keep the table - * length pinned to it here */ -typedef char - patterns_len_matches_enum[(sizeof patterns / sizeof patterns[0]) == - screen_PATTERN__LIMIT ? 1 : -1]; - void screen_fill_pattern(screen_t *scr, const box_t *box, - screen_pattern_t pattern, - int origin_x, - int origin_y, - colour_t fg, - colour_t bg) + const pattern_t *pattern) { - const unsigned char *tile; - box_t clip_box; - box_t draw_box; - pixelfmt_any_t fg_fmt, bg_fmt; - pixelfmt_any_t runs[8][8]; /* one expanded colour run per tile row */ - int xphase; - int row, col, x, y; + box_t clip_box; + box_t draw_box; + int stencil; + pixelfmt_any_t fg_fmt, bg_fmt; + pixelfmt_any_t runs[8][8]; /* one expanded colour run per tile row */ + int xphase; + int row, col, x, y; - assert(pattern >= 0 && pattern < screen_PATTERN__LIMIT); - if (pattern < 0 || pattern >= screen_PATTERN__LIMIT) - return; - - tile = patterns[pattern]; + assert(scr); + assert(pattern); if (screen_get_clip(scr, &clip_box)) return; /* invalid clipped screen */ @@ -136,22 +31,25 @@ void screen_fill_pattern(screen_t *scr, if (box_intersection(&clip_box, box, &draw_box)) return; /* nothing visible */ + stencil = (pattern->flags & pattern_FLAG_STENCIL) != 0; + fg_fmt = colour_to_pixel(scr->palette, (scr->format == pixelfmt_p4) ? 16 : 0, - fg, scr->format); + pattern->fg, scr->format); bg_fmt = colour_to_pixel(scr->palette, (scr->format == pixelfmt_p4) ? 16 : 0, - bg, scr->format); + pattern->bg, scr->format); /* The tile is 8x8 and repeats, so there are only eight distinct pixel rows. * Expand each to a colour run once here, already phase-shifted for x, then - * the scanline loops just index runs[row][col] with no per-pixel bit test. */ - xphase = ((draw_box.x0 - origin_x) & 7); + * the scanline loops just index runs[row][col]. Stencil rows re-test the + * pattern bit per pixel rather than using the run. */ + xphase = ((draw_box.x0 - pattern->origin.x) & 7); for (row = 0; row < 8; row++) { - unsigned char bits; + uint8_t bits; - bits = tile[row]; + bits = pattern->bits[row]; for (col = 0; col < 8; col++) runs[row][col] = (bits & (0x80u >> ((xphase + col) & 7))) ? fg_fmt : bg_fmt; @@ -163,21 +61,28 @@ void screen_fill_pattern(screen_t *scr, { unsigned char *rowp; const pixelfmt_any_t *run; + uint8_t bits; unsigned char *scrp; int shift; rowp = (unsigned char *) scr->base + draw_box.y0 * scr->rowbytes; for (y = draw_box.y0; y < draw_box.y1; y++) { - run = runs[(y - origin_y) & 7]; - col = 0; + 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++) { - scrp = rowp + (x >> 1); - shift = (x & 1) * 4; - - *scrp = (unsigned char) ((*scrp & ~(0xF << shift)) | - ((run[col] & 0xF) << shift)); + if (!stencil || + (bits & (0x80u >> ((x - pattern->origin.x) & 7)))) + { + scrp = rowp + (x >> 1); + shift = (x & 1) * 4; + + *scrp = (unsigned char) ((*scrp & ~(0xF << shift)) | + ((run[col] & 0xF) << shift)); + } col = (col + 1) & 7; } rowp += scr->rowbytes; @@ -190,27 +95,42 @@ void screen_fill_pattern(screen_t *scr, unsigned char *rowp; const pixelfmt_any_t *run; pixelfmt_any32_t *scrp; + uint8_t bits; int w; int n; rowp = (unsigned char *) scr->base + draw_box.y0 * scr->rowbytes; for (y = draw_box.y0; y < draw_box.y1; y++) { - run = runs[(y - origin_y) & 7]; + row = (y - pattern->origin.y) & 7; + run = runs[row]; + bits = pattern->bits[row]; scrp = (pixelfmt_any32_t *) rowp + draw_box.x0; w = draw_box.x1 - draw_box.x0; - /* leading partial tile up to an 8-pixel boundary, then whole runs */ - col = 0; - while (w > 0) + if (!stencil) + { + /* leading partial tile up to an 8-pixel boundary, then whole runs */ + col = 0; + while (w > 0) + { + n = 8 - col; + if (n > w) + n = w; + memcpy(scrp, run + col, (size_t) n * sizeof(*scrp)); + scrp += n; + w -= n; + col = 0; + } + } + else { - n = 8 - col; - if (n > w) - n = w; - memcpy(scrp, run + col, (size_t) n * sizeof(*scrp)); - scrp += n; - w -= n; - col = 0; + for (x = draw_box.x0; x < draw_box.x1; x++) + { + if (bits & (0x80u >> ((x - pattern->origin.x) & 7))) + *scrp = fg_fmt; + scrp++; + } } rowp += scr->rowbytes; } diff --git a/libraries/framebuf/screen/test/screen-test.c b/libraries/framebuf/screen/test/screen-test.c index fb689da3..7fec322c 100644 --- a/libraries/framebuf/screen/test/screen-test.c +++ b/libraries/framebuf/screen/test/screen-test.c @@ -308,7 +308,7 @@ static result_t test_ninepatch(void) /* Normal case. */ testscreen_init(&ts); - screen_draw_ninepatch(&ts.scr, &dst, &src, 0); + screen_copy_ninepatch(&ts.scr, &dst, &src, 0); /* Corners: the 3x3 block at each destination corner is that corner colour. */ if (np_at(&ts, 5, 5) != exp[0][0] || np_at(&ts, 7, 7) != exp[0][0] || @@ -342,7 +342,7 @@ static result_t test_ninepatch(void) /* Clip composition: restrict to the left half, the right half is untouched. */ testscreen_init(&ts); ts.scr.clip = (box_t) { 0, 0, 25, 64 }; - screen_draw_ninepatch(&ts.scr, &dst, &src, 0); + screen_copy_ninepatch(&ts.scr, &dst, &src, 0); if (np_at(&ts, 6, 25) != exp[1][0] || np_at(&ts, 30, 25) != (int) (pixelfmt_bgrx8888_t) BACKGROUND) { @@ -362,7 +362,7 @@ static result_t test_ninepatch(void) { box_t small = { 10, 10, 10 + 2 * NP_CELL, 10 + 2 * NP_CELL }; - screen_draw_ninepatch(&ts.scr, &small, &src, 0); + screen_copy_ninepatch(&ts.scr, &small, &src, 0); if (np_at(&ts, 10, 10) != exp[0][0] || np_at(&ts, 15, 15) != exp[2][2] || np_at(&ts, 12, 12) == exp[1][1]) /* centre colour must NOT appear */ @@ -374,7 +374,7 @@ static result_t test_ninepatch(void) /* NO_CENTRE: border drawn, interior stays background. */ testscreen_init(&ts); - screen_draw_ninepatch(&ts.scr, &dst, &src, screen_NINEPATCH_NO_CENTRE); + screen_copy_ninepatch(&ts.scr, &dst, &src, screen_NINEPATCH_NO_CENTRE); if (np_at(&ts, 5, 5) != exp[0][0] || /* corner still drawn */ np_at(&ts, 25, 6) != exp[0][1] || /* edge still drawn */ np_at(&ts, 25, 25) != (int) (pixelfmt_bgrx8888_t) BACKGROUND) /* centre skipped */ @@ -402,8 +402,12 @@ static result_t test_fill_pattern(void) /* GREY50 is 0xAA,0x55,... : at origin (0,0) pixel (x,y) is fg when * ((x ^ y) & 1) == 0. */ testscreen_init(&ts); - screen_fill_pattern(&ts.scr, &box, screen_PATTERN_GREY50, 0, 0, - colour_rgb(255, 0, 0), colour_rgb(0, 0, 255)); + { + pattern_t pat = pattern_from_preset(screen_PATTERN_GREY50, + colour_rgb(255, 0, 0), + colour_rgb(0, 0, 255)); + screen_fill_pattern(&ts.scr, &box, &pat); + } if (np_at(&ts, 8, 8) != fg || /* (0,0) phase -> set bit */ np_at(&ts, 9, 8) != bg || @@ -425,8 +429,13 @@ static result_t test_fill_pattern(void) /* Shift the origin by one in x: every pixel's phase flips, so the same * screen coordinate takes the other colour. */ testscreen_init(&ts); - screen_fill_pattern(&ts.scr, &box, screen_PATTERN_GREY50, 1, 0, - colour_rgb(255, 0, 0), colour_rgb(0, 0, 255)); + { + pattern_t pat = pattern_from_preset(screen_PATTERN_GREY50, + colour_rgb(255, 0, 0), + colour_rgb(0, 0, 255)); + pat.origin = POINT(1, 0); + screen_fill_pattern(&ts.scr, &box, &pat); + } if (np_at(&ts, 8, 8) != bg || np_at(&ts, 9, 8) != fg) { printf("screen: fill_pattern ignored origin phase\n"); @@ -436,8 +445,12 @@ static result_t test_fill_pattern(void) /* Honours the screen clip. */ testscreen_init(&ts); ts.scr.clip = (box_t) { 0, 0, 16, 64 }; - screen_fill_pattern(&ts.scr, &box, screen_PATTERN_SOLID, 0, 0, - colour_rgb(255, 0, 0), colour_rgb(0, 0, 255)); + { + pattern_t pat = pattern_from_preset(screen_PATTERN_SOLID, + colour_rgb(255, 0, 0), + colour_rgb(0, 0, 255)); + screen_fill_pattern(&ts.scr, &box, &pat); + } if (np_at(&ts, 10, 10) != fg || np_at(&ts, 20, 10) != (int) (pixelfmt_bgrx8888_t) BACKGROUND) { diff --git a/libraries/wuss/backdrop.c b/libraries/wuss/backdrop.c index cc7071ca..a078f3c7 100644 --- a/libraries/wuss/backdrop.c +++ b/libraries/wuss/backdrop.c @@ -34,10 +34,18 @@ void wuss__fill_backdrop(screen_t *scr, return; if (backdrop->pattern == screen_PATTERN_SOLID) + { screen_fill_rect(scr, area->x0, area->y0, box_size(area), palette[backdrop->colour]); + } else - screen_fill_pattern(scr, area, backdrop->pattern, origin_x, origin_y, - palette[backdrop->colour], - palette[backdrop->pattern_bg]); + { + pattern_t pat; + + pat = pattern_from_preset(backdrop->pattern, + palette[backdrop->colour], + palette[backdrop->pattern_bg]); + pat.origin = POINT(origin_x, origin_y); + screen_fill_pattern(scr, area, &pat); + } } diff --git a/libraries/wuss/icon/draw.c b/libraries/wuss/icon/draw.c index cc48f56f..214a077c 100644 --- a/libraries/wuss/icon/draw.c +++ b/libraries/wuss/icon/draw.c @@ -81,9 +81,14 @@ static void wuss__icon_draw_pattern(const icon_draw_ctx_t *c, ? c->wuss->palette[icon->bg] : c->fg; - screen_fill_pattern(c->scr, &c->b, icon->pattern, - content->x0 - scroll.x, content->y0 - scroll.y, - pat_fg, c->wuss->palette[icon->bg]); + { + pattern_t pat; + + pat = pattern_from_preset(icon->pattern, + pat_fg, c->wuss->palette[icon->bg]); + pat.origin = POINT(content->x0 - scroll.x, content->y0 - scroll.y); + screen_fill_pattern(c->scr, &c->b, &pat); + } } /* ----------------------------------------------------------------------- */ @@ -335,14 +340,14 @@ static void wuss__icon_draw_bitmap(const icon_draw_ctx_t *c) if (c->icon->bitmap == NULL) return; - /* screen_draw_bitmap clips to scr->clip and does not scale, so narrow the + /* screen_copy_bitmap clips to scr->clip and does not scale, so narrow the * clip to the icon box (intersected with whatever redraw already set) and * blit at the box's top-left */ clipped = *c->scr; if (box_intersection(&c->scr->clip, b, &clipped.clip)) return; - screen_draw_bitmap(&clipped, b->x0, b->y0, c->icon->bitmap); + screen_copy_bitmap(&clipped, b->x0, b->y0, c->icon->bitmap); } /* ----------------------------------------------------------------------- */ diff --git a/libraries/wuss/test/tasks/icons.c b/libraries/wuss/test/tasks/icons.c index d3516aa7..939198d2 100644 --- a/libraries/wuss/test/tasks/icons.c +++ b/libraries/wuss/test/tasks/icons.c @@ -65,7 +65,7 @@ result_t icons_create(wuss_t *wuss, task->hotspot = NULL; sprite_path = path_join_filename(resources, 3, "resources", "wuss", - path_join_leafname("9tile", "png")); + path_join_leafname("ninepatch", "png")); if (bitmap_load_png(&task->sprite, sprite_path) == result_OK) task->has_sprite = 1; diff --git a/libraries/wuss/test/tasks/image.c b/libraries/wuss/test/tasks/image.c index d8408fa6..c096be84 100644 --- a/libraries/wuss/test/tasks/image.c +++ b/libraries/wuss/test/tasks/image.c @@ -97,9 +97,9 @@ static result_t image_redraw(const wuss_event_t *event, void *task_data) behind.y0 = by - NINEPATCHSZ; behind.x1 = behind.x0 + ic->bitmap.size.w + NINEPATCHSZ * 2; behind.y1 = behind.y0 + ic->bitmap.size.h + NINEPATCHSZ * 2; - screen_draw_ninepatch(scr, &behind, &ic->ninepatch, 0); + screen_copy_ninepatch(scr, &behind, &ic->ninepatch, 0); - screen_draw_bitmap(scr, bx, by, &ic->bitmap); + screen_copy_bitmap(scr, bx, by, &ic->bitmap); return result_OK; } diff --git a/libraries/wuss/test/tasks/porter-duff.c b/libraries/wuss/test/tasks/porter-duff.c index cf27082b..6d970581 100644 --- a/libraries/wuss/test/tasks/porter-duff.c +++ b/libraries/wuss/test/tasks/porter-duff.c @@ -322,7 +322,7 @@ static result_t porter_duff_redraw(const wuss_event_t *event, if (rc != result_OK) return rc; - screen_draw_bitmap(scr, bounds->x0, bounds->y0, &pd->dst); + screen_copy_bitmap(scr, bounds->x0, bounds->y0, &pd->dst); name = rule_names[pd->rule]; pos.x = bounds->x0 + 2; diff --git a/resources/wuss/ninepatch.aseprite b/resources/wuss/ninepatch.aseprite new file mode 100644 index 0000000000000000000000000000000000000000..0399756c6f1f6dc2b00912b771925fc759bc858d GIT binary patch literal 374 zcmXS8WMFu(l#xLi2ss#l9EKDiMgRc@Mvx$b01(mwd;=Q#>mv);Ru&*z42ankz*Ztz zX9Z-lfLsT1gsgUOooX;cd*JFIv-oiTLx+$5|M=oRgYW;v4FBJ#GdwZVT>HPdY|j7k z75~qyQUd8z;72m+KakDE;FOr0onDk*nwL@n<g+S36#Qp`&<x=4sF?HkQ4>?1#DhZ& zYYZBKnPP>Gv`cp=ai%HXY@N`&VZvO->CG23^jf?eHA9X&vlMz29=^j?E^=n>ox`DB jdm0W&fBNx=+h6Cgw{%QJkVu!xE|zUqxEYuaN-YNf;Jr-V literal 0 HcmV?d00001 diff --git a/resources/wuss/9tile.png b/resources/wuss/ninepatch.png similarity index 100% rename from resources/wuss/9tile.png rename to resources/wuss/ninepatch.png From 76dddacc54e6547168cab728586f013b0e86ad77 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Thu, 3 Sep 2026 09:51:49 +0100 Subject: [PATCH 208/287] refactor(framebuf): align the three copy ops on result_t screen_copy_bitmap and screen_copy_ninepatch return result_t instead of void; screen_copy_rect returns result_t instead of int. All three return result_OK on success and result_NOT_SUPPORTED when the screen's pixel format has no blit path. screen_copy_rect also returns result_NOT_SUPPORTED when clipping leaves nothing to copy (the caller must then fall back to a full redraw) and keeps its box_t *copied_dst out-param. Update the wuss window move/resize/set-scroll and toggle-action callers of screen_copy_rect to test against result_OK. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- docs/framebuf/drawing.md | 41 ++++++++----------- include/framebuf/screen.h | 35 ++++++++++------ .../framebuf/screen/screen-copy-ninepatch.c | 14 ++++--- libraries/framebuf/screen/screen-copy-rect.c | 36 ++++++++-------- libraries/framebuf/screen/screen-draw.c | 10 +++-- libraries/wuss/furniture/toggle-action.c | 2 +- libraries/wuss/window/move.c | 6 +-- libraries/wuss/window/resize.c | 2 +- libraries/wuss/window/set-scroll.c | 2 +- 9 files changed, 78 insertions(+), 70 deletions(-) diff --git a/docs/framebuf/drawing.md b/docs/framebuf/drawing.md index 696f28cc..55f1bb36 100644 --- a/docs/framebuf/drawing.md +++ b/docs/framebuf/drawing.md @@ -96,10 +96,10 @@ screen_fill_box colour fill, half-open box [planned] screen_draw_circle 1px circle outline [shipped] screen_fill_circle solid disc [shipped] screen_fill_pattern pattern fill on the screen [shipped] -bitmap_fill_pattern pattern fill on a raw bitmap [planned; was bitmap_draw_pattern] +bitmap_fill_pattern pattern fill on a raw bitmap [shipped; was bitmap_draw_pattern] screen_fill_rect_gradient gradient fill of a rect [planned] -screen_copy_bitmap blit a bitmap [planned; was screen_draw_bitmap] -screen_copy_ninepatch blit a resizable 9-patch frame [planned; was screen_draw_ninepatch] +screen_copy_bitmap blit a bitmap [shipped; was screen_draw_bitmap] +screen_copy_ninepatch blit a resizable 9-patch frame [shipped; was screen_draw_ninepatch] screen_copy_rect screen-to-screen block move [shipped] screen_draw_line_wu_float anti-aliased line, float coords [shipped] ``` @@ -154,9 +154,12 @@ New drawing ops return **`result_t`**. An op that clips away to nothing is not a error — it returns `result_OK` and draws nothing. Existing `void`-returning `screen_draw_*` ops keep their return type. The three -`copy` ops disagree today (`screen_draw_bitmap` / `screen_draw_ninepatch` return -`void`, `screen_copy_rect` returns `int` plus a `box_t *` out-param); aligning -them on `result_t` is a signature change tracked separately from the rename. +`copy` ops are aligned on `result_t`: `screen_copy_bitmap`, +`screen_copy_ninepatch` and `screen_copy_rect` all return `result_OK` on +success and `result_NOT_SUPPORTED` when the screen's pixel format has no blit +path (`screen_copy_rect` also returns `result_NOT_SUPPORTED` when clipping +leaves nothing to copy, since the caller must then fall back to a full +redraw). `screen_copy_rect` keeps its `box_t *copied_dst` out-param. ## Rectangle representation @@ -175,26 +178,18 @@ spellings going forward. `screen_draw_line_wu_fix8`, `screen_draw_line_wu_float` - `screen_draw_rect`, `screen_fill_rect`, `screen_fill_square` - `screen_draw_circle`, `screen_fill_circle` -- `screen_fill_pattern` (currently takes `screen_pattern_t`; migrates to - `const pattern_t *`) -- `screen_draw_bitmap`, `screen_draw_ninepatch` (rename to `screen_copy_*` - pending) -- `screen_copy_rect` -- `bitmap_clear`, `bitmap_draw_pattern` (rename to `bitmap_fill_pattern` - pending) +- `screen_fill_pattern` (takes `const pattern_t *`) +- `screen_copy_bitmap`, `screen_copy_ninepatch`, `screen_copy_rect` — all + return `result_t` +- `bitmap_clear`, `bitmap_fill_pattern` (`const pattern_t *`, returns + `result_t`) - `bmfont_draw`, `bmtext_draw` - `composite` — see [composite.md](composite.md) -**Planned — renames** - -| from | to | -|------|----| -| `screen_draw_bitmap` | `screen_copy_bitmap` | -| `screen_draw_ninepatch` | `screen_copy_ninepatch` | -| `bitmap_draw_pattern` | `bitmap_fill_pattern` (+ `pattern_t` argument) | - -Hard renames, no compatibility wrappers, all in-tree call sites updated in the -same change. +The rename pass was hard renames, no compatibility wrappers, all in-tree call +sites updated in the same change: `screen_draw_bitmap` → `screen_copy_bitmap`, +`screen_draw_ninepatch` → `screen_copy_ninepatch`, `bitmap_draw_pattern` → +`bitmap_fill_pattern` (now taking a `pattern_t`). **Planned — new primitives** (priority order) diff --git a/include/framebuf/screen.h b/include/framebuf/screen.h index 4f1cb247..c5f7a252 100644 --- a/include/framebuf/screen.h +++ b/include/framebuf/screen.h @@ -129,8 +129,13 @@ void screen_fill_pattern(screen_t *scr, * \param[in] x X coordinate of leftmost point to draw bitmap at. * \param[in] y Y coordinate of topmost point to draw bitmap at. * \param[in] src Bitmap to copy. + * \return \ref result_OK on success, \ref result_NOT_SUPPORTED if the + * screen's pixel format has no blit path. */ -void screen_copy_bitmap(screen_t *scr, int x, int y, const bitmap_t *src); +result_t screen_copy_bitmap(screen_t *scr, + int x, + int y, + const bitmap_t *src); /** Flags for `screen_copy_ninepatch`. */ enum @@ -158,11 +163,13 @@ enum * \param[in] flags Bitwise OR of `screen_NINEPATCH_*`, or 0. Pass * `screen_NINEPATCH_NO_CENTRE` to draw only the border and * leave the interior untouched. + * \return \ref result_OK on success, \ref result_NOT_SUPPORTED if the + * screen's pixel format has no blit path. */ -void screen_copy_ninepatch(screen_t *scr, - const box_t *dst, - const bitmap_t *src, - unsigned int flags); +result_t screen_copy_ninepatch(screen_t *scr, + const box_t *dst, + const bitmap_t *src, + unsigned int flags); /** * Copies a rectangular region of the screen to another position on the same @@ -175,8 +182,9 @@ void screen_copy_ninepatch(screen_t *scr, * destination pixel 1:1. * * Callers must check the return value and fall back to a normal - * invalidate/redraw when it's false (e.g. out of memory, or an unknown pixel - * format), since a declined copy leaves the destination untouched. + * invalidate/redraw when it is not \ref result_OK (an unknown pixel format, + * or the source/destination lying wholly off-screen), since a declined copy + * leaves the destination untouched. * * If "src" or the intended destination falls partly off-screen, the actual * copied area shrinks to what both ends have in common on-screen: callers @@ -191,13 +199,14 @@ void screen_copy_ninepatch(screen_t *scr, * smaller than intended if either end was partly * off-screen). Pass NULL if not needed. Left unset if * the copy was declined. - * \return True if the copy was performed, false if declined (unsupported - * pixel format). + * \return \ref result_OK if the copy was performed, \ref + * result_NOT_SUPPORTED if declined (unsupported pixel format, or + * nothing left to copy after clipping). */ -int screen_copy_rect(screen_t *scr, - const box_t *src, - point_t dst, - box_t *copied_dst); +result_t screen_copy_rect(screen_t *scr, + const box_t *src, + point_t dst, + box_t *copied_dst); /** * Draws a line (Bresenham version with aliasing). diff --git a/libraries/framebuf/screen/screen-copy-ninepatch.c b/libraries/framebuf/screen/screen-copy-ninepatch.c index afc92b52..9e4f24c7 100644 --- a/libraries/framebuf/screen/screen-copy-ninepatch.c +++ b/libraries/framebuf/screen/screen-copy-ninepatch.c @@ -75,10 +75,10 @@ static void tile_area(screen_t *scr, /* ----------------------------------------------------------------------- */ -void screen_copy_ninepatch(screen_t *scr, - const box_t *dst, - const bitmap_t *src, - unsigned int flags) +result_t screen_copy_ninepatch(screen_t *scr, + const box_t *dst, + const bitmap_t *src, + unsigned int flags) { box_t saved; box_t orig_clip; @@ -91,7 +91,7 @@ void screen_copy_ninepatch(screen_t *scr, assert(src->size.h > 0 && src->size.h % 3 == 0); if (box_is_empty(dst)) - return; + return result_OK; pw = src->size.w / 3; ph = src->size.h / 3; @@ -115,7 +115,7 @@ void screen_copy_ninepatch(screen_t *scr, if (box_is_empty(&orig_clip)) saved = *dst; else if (box_intersection(&orig_clip, dst, &saved)) - return; /* dst entirely outside the clip */ + return result_OK; /* dst entirely outside the clip */ /* Corners. */ { @@ -175,4 +175,6 @@ void screen_copy_ninepatch(screen_t *scr, } scr->clip = orig_clip; + + return result_OK; } diff --git a/libraries/framebuf/screen/screen-copy-rect.c b/libraries/framebuf/screen/screen-copy-rect.c index 46c5f559..b5be1b02 100644 --- a/libraries/framebuf/screen/screen-copy-rect.c +++ b/libraries/framebuf/screen/screen-copy-rect.c @@ -13,13 +13,13 @@ * columns within each row by the same rule applied to "dx" -- the standard * two-axis blit-direction trick, so every pixel is read before anything * that could overwrite it is written. */ -static int screen_copy_rect_p4(screen_t *scr, - const box_t *s, - const box_t *d, - int width, - int height, - int dx, - int dy) +static result_t screen_copy_rect_p4(screen_t *scr, + const box_t *s, + const box_t *d, + int width, + int height, + int dx, + int dy) { unsigned char *base; int rowbytes; @@ -59,13 +59,13 @@ static int screen_copy_rect_p4(screen_t *scr, } } - return 1; + return result_OK; } -int screen_copy_rect(screen_t *scr, - const box_t *src, - point_t dst, - box_t *copied_dst) +result_t screen_copy_rect(screen_t *scr, + const box_t *src, + point_t dst, + box_t *copied_dst) { box_t clip_box, s, d, d_clipped; int dx, dy, width, height, bpp; @@ -73,17 +73,17 @@ int screen_copy_rect(screen_t *scr, int rowbytes; if (screen_get_clip(scr, &clip_box)) - return 0; /* invalid clipped screen */ + return result_NOT_SUPPORTED; /* invalid clipped screen */ dx = dst.x - src->x0; dy = dst.y - src->y0; if (box_intersection(&clip_box, src, &s)) - return 0; /* source entirely off-screen */ + return result_NOT_SUPPORTED; /* source entirely off-screen */ box_translated(&s, dx, dy, &d); if (box_intersection(&clip_box, &d, &d_clipped)) - return 0; /* destination entirely off-screen */ + return result_NOT_SUPPORTED; /* destination entirely off-screen */ /* keep only the part of "s" whose translated position also survived * clipping, so source and destination stay the same size */ @@ -93,7 +93,7 @@ int screen_copy_rect(screen_t *scr, s.y1 += d_clipped.y1 - d.y1; if (box_is_empty(&s)) - return 0; + return result_NOT_SUPPORTED; width = s.x1 - s.x0; height = s.y1 - s.y0; @@ -119,7 +119,7 @@ int screen_copy_rect(screen_t *scr, default: /* ponytail: unknown/future pixel format; callers must fall back to a * normal invalidate/redraw there */ - return 0; + return result_NOT_SUPPORTED; } base = scr->base; @@ -148,5 +148,5 @@ int screen_copy_rect(screen_t *scr, (size_t) width * bpp); } - return 1; + return result_OK; } diff --git a/libraries/framebuf/screen/screen-draw.c b/libraries/framebuf/screen/screen-draw.c index 88ebf85c..4ce5494f 100644 --- a/libraries/framebuf/screen/screen-draw.c +++ b/libraries/framebuf/screen/screen-draw.c @@ -226,7 +226,7 @@ void screen_fill_square(screen_t *scr, /* ----------------------------------------------------------------------- */ -void screen_copy_bitmap(screen_t *scr, int x, int y, const bitmap_t *src) +result_t screen_copy_bitmap(screen_t *scr, int x, int y, const bitmap_t *src) { box_t clip_box; box_t src_box; @@ -235,14 +235,14 @@ void screen_copy_bitmap(screen_t *scr, int x, int y, const bitmap_t *src) int has_alpha; if (screen_get_clip(scr, &clip_box)) - return; /* invalid clipped screen */ + return result_OK; /* invalid clipped screen: nothing to draw */ src_box.x0 = x; src_box.y0 = y; src_box.x1 = x + src->size.w; src_box.y1 = y + src->size.h; if (box_intersection(&clip_box, &src_box, &draw_box)) - return; /* nothing visible */ + return result_OK; /* nothing visible */ clipped_width = draw_box.x1 - draw_box.x0; clipped_height = draw_box.y1 - draw_box.y0; @@ -352,8 +352,10 @@ void screen_copy_bitmap(screen_t *scr, int x, int y, const bitmap_t *src) default: assert(!"Unimplemented pixel format"); - break; + return result_NOT_SUPPORTED; } + + return result_OK; } /* ----------------------------------------------------------------------- */ diff --git a/libraries/wuss/furniture/toggle-action.c b/libraries/wuss/furniture/toggle-action.c index 7e2724b6..d6afc173 100644 --- a/libraries/wuss/furniture/toggle-action.c +++ b/libraries/wuss/furniture/toggle-action.c @@ -85,7 +85,7 @@ void wuss__furniture_toggle_size(wuss_window_t *window) if (!(window->flags & wuss_WINDOW_NO_RESIZE_BLIT) && window->wuss->z_order.next == &window->link && screen_copy_rect(window->wuss->scr, &before, - POINT(before.x0, before.y0), &copied)) + POINT(before.x0, before.y0), &copied) == result_OK) { /* Topmost, and the screen format supports the blit: the window's * top-left never moves for a toggle, so re-blitting "before" onto diff --git a/libraries/wuss/window/move.c b/libraries/wuss/window/move.c index 036dd655..5d0f5873 100644 --- a/libraries/wuss/window/move.c +++ b/libraries/wuss/window/move.c @@ -108,9 +108,9 @@ void wuss_window_move(wuss_window_t *window, point_t p) { idx = order[i]; - if (!screen_copy_rect(window->wuss->scr, &blit_src[idx], - POINT(blit_dest[idx].x0, blit_dest[idx].y0), - &copied)) + if (screen_copy_rect(window->wuss->scr, &blit_src[idx], + POINT(blit_dest[idx].x0, blit_dest[idx].y0), + &copied) != result_OK) { /* screen_copy_rect refused this piece: either the screen format has no * blit path (e.g. paletted -- fails on the first piece, before anything diff --git a/libraries/wuss/window/resize.c b/libraries/wuss/window/resize.c index 5c4a46af..b7edcb38 100644 --- a/libraries/wuss/window/resize.c +++ b/libraries/wuss/window/resize.c @@ -104,7 +104,7 @@ result_t wuss_window_resize(wuss_window_t *window, size2d_t size) if (screen_copy_rect(window->wuss->scr, &src[i], POINT(src[i].x0 - dx, src[i].y0 - dy), - &got) != 0 && + &got) == result_OK && ncopied < WUSS_MAX_INVALIDATE_PIECES) copied[ncopied++] = got; } diff --git a/libraries/wuss/window/set-scroll.c b/libraries/wuss/window/set-scroll.c index 721d23f5..c609c1b5 100644 --- a/libraries/wuss/window/set-scroll.c +++ b/libraries/wuss/window/set-scroll.c @@ -115,7 +115,7 @@ void wuss_window_set_scroll(wuss_window_t *window, point_t p) idx = order[i]; if (screen_copy_rect(window->wuss->scr, &blit_src[idx], POINT(blit_dest[idx].x0, blit_dest[idx].y0), - &got) == 0) + &got) != result_OK) { blit_failed = 1; break; From 438c86583647c9f5fbaf3f514728c43bee49ff4e Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Thu, 3 Sep 2026 11:00:02 +0100 Subject: [PATCH 209/287] feat(framebuf): extract screen_fill_hline from screen_fill_rect screen_fill_hline(scr, x, y, w, colour) fills one clipped horizontal run and is now the per-row primitive screen_fill_rect loops over. screen_draw_circle's scanline fill switches from stacked 1-high screen_fill_rect calls to screen_fill_hline. No behavioural change. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- CMakeLists.txt | 1 + docs/framebuf/drawing.md | 5 +- include/framebuf/screen.h | 15 ++++ .../framebuf/screen/screen-draw-circle.c | 11 ++- libraries/framebuf/screen/screen-draw.c | 66 ++-------------- libraries/framebuf/screen/screen-fill-hline.c | 77 +++++++++++++++++++ 6 files changed, 110 insertions(+), 65 deletions(-) create mode 100644 libraries/framebuf/screen/screen-fill-hline.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 671b8d2b..b83302d5 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -253,6 +253,7 @@ set(FRAMEBUF_SOURCES libraries/framebuf/screen/screen-copy-rect.c libraries/framebuf/screen/screen-draw.c libraries/framebuf/screen/screen-draw-circle.c + libraries/framebuf/screen/screen-fill-hline.c libraries/framebuf/screen/screen-fill-pattern.c libraries/framebuf/span-registry/get.c libraries/framebuf/span-registry/regdata.h diff --git a/docs/framebuf/drawing.md b/docs/framebuf/drawing.md index 55f1bb36..b782fc23 100644 --- a/docs/framebuf/drawing.md +++ b/docs/framebuf/drawing.md @@ -91,6 +91,7 @@ Sampling from a raster is **not** a paint suffix — that is `copy`. ## Examples ``` +screen_fill_hline colour fill, one horizontal run [shipped] screen_fill_rect colour fill, inclusive-edge rect [shipped] screen_fill_box colour fill, half-open box [planned] screen_draw_circle 1px circle outline [shipped] @@ -176,7 +177,9 @@ spellings going forward. - `screen_set_pixel` - `screen_draw_line`, `screen_draw_lines`, `screen_draw_dashed_line`, `screen_draw_line_wu_fix8`, `screen_draw_line_wu_float` -- `screen_draw_rect`, `screen_fill_rect`, `screen_fill_square` +- `screen_draw_rect`, `screen_fill_rect`, `screen_fill_square`, + `screen_fill_hline` (the per-row primitive `screen_fill_rect` and + `screen_draw_circle` build on) - `screen_draw_circle`, `screen_fill_circle` - `screen_fill_pattern` (takes `const pattern_t *`) - `screen_copy_bitmap`, `screen_copy_ninepatch`, `screen_copy_rect` — all diff --git a/include/framebuf/screen.h b/include/framebuf/screen.h index c5f7a252..aa8087ff 100644 --- a/include/framebuf/screen.h +++ b/include/framebuf/screen.h @@ -96,6 +96,21 @@ void screen_fill_square(screen_t *scr, int size, colour_t colour); +/** + * Fills a horizontal run of `w` pixels starting at (`x`, `y`). + * + * A non-positive `w` draws nothing. Clipped to the screen's clip region. + * This is the per-row primitive `screen_fill_rect` and `screen_draw_circle` + * build on. + * + * \param[in] scr Screen to draw upon. + * \param[in] x X coordinate of the leftmost pixel of the run. + * \param[in] y Y coordinate of the run. + * \param[in] w Length of the run in pixels. + * \param[in] colour Colour of the run. + */ +void screen_fill_hline(screen_t *scr, int x, int y, int w, colour_t colour); + /** * Fills a box with a repeating 8x8 pattern. * diff --git a/libraries/framebuf/screen/screen-draw-circle.c b/libraries/framebuf/screen/screen-draw-circle.c index 153547d7..a9737266 100644 --- a/libraries/framebuf/screen/screen-draw-circle.c +++ b/libraries/framebuf/screen/screen-draw-circle.c @@ -4,7 +4,6 @@ #include "base/utils.h" #include "geom/box.h" -#include "geom/size.h" #include "framebuf/screen.h" @@ -56,7 +55,7 @@ void screen_draw_circle(screen_t *scr, } /* Same octant stepping as the outline, but each step emits a pair of solid - * horizontal runs (via screen_fill_rect, which clips) rather than eight + * horizontal runs (via screen_fill_hline, which clips) rather than eight * points. Runs from the two octant families cover every scanline of the * disc exactly once. */ void screen_fill_circle(screen_t *scr, @@ -83,16 +82,16 @@ void screen_fill_circle(screen_t *scr, while (x >= y) { /* the wide pair: rows cy +/- y, spanning -x..+x */ - screen_fill_rect(scr, cx - x, cy + y, SIZE2D(2 * x + 1, 1), colour); + screen_fill_hline(scr, cx - x, cy + y, 2 * x + 1, colour); if (y != 0) - screen_fill_rect(scr, cx - x, cy - y, SIZE2D(2 * x + 1, 1), colour); + screen_fill_hline(scr, cx - x, cy - y, 2 * x + 1, colour); /* the tall pair: rows cy +/- x, spanning -y..+y; skip while it would * fall inside the wide pair's rows to avoid overdraw */ if (x != y) { - screen_fill_rect(scr, cx - y, cy + x, SIZE2D(2 * y + 1, 1), colour); - screen_fill_rect(scr, cx - y, cy - x, SIZE2D(2 * y + 1, 1), colour); + screen_fill_hline(scr, cx - y, cy + x, 2 * y + 1, colour); + screen_fill_hline(scr, cx - y, cy - x, 2 * y + 1, colour); } y++; diff --git a/libraries/framebuf/screen/screen-draw.c b/libraries/framebuf/screen/screen-draw.c index 4ce5494f..ce9939e1 100644 --- a/libraries/framebuf/screen/screen-draw.c +++ b/libraries/framebuf/screen/screen-draw.c @@ -144,11 +144,10 @@ void screen_fill_rect(screen_t *scr, size2d_t size, colour_t colour) { - box_t clip_box; - box_t rect_box; - box_t draw_box; - int clipped_width, clipped_height; - pixelfmt_any_t fmt; + box_t clip_box; + box_t rect_box; + box_t draw_box; + int yy; if (screen_get_clip(scr, &clip_box)) return; /* invalid clipped screen */ @@ -160,59 +159,10 @@ void screen_fill_rect(screen_t *scr, if (box_intersection(&clip_box, &rect_box, &draw_box)) return; - clipped_width = draw_box.x1 - draw_box.x0; - clipped_height = draw_box.y1 - draw_box.y0; - - fmt = colour_to_pixel(scr->palette, - (scr->format == pixelfmt_p4) ? 16 : 0, - colour, scr->format); - switch (pixelfmt_log2bpp(scr->format)) - { - case 2: - { - unsigned char *rowp; - int yy, xx; - - rowp = (unsigned char *) scr->base + draw_box.y0 * scr->rowbytes; - for (yy = 0; yy < clipped_height; yy++) - { - for (xx = 0; xx < clipped_width; xx++) - { - int x; - unsigned char *scrp; - int shift; - - x = draw_box.x0 + xx; - scrp = rowp + (x >> 1); - shift = (x & 1) * 4; - - *scrp = (unsigned char) ((*scrp & ~(0xF << shift)) | ((fmt & 0xF) << shift)); - } - rowp += scr->rowbytes; - } - } - break; - - case 5: - { - pixelfmt_any32_t *scrp; - int w; - - scrp = scr->base; - scrp += draw_box.y0 * scr->rowbytes / sizeof(*scrp) + draw_box.x0; - while (clipped_height--) - { - for (w = clipped_width; w > 0; w--) - *scrp++ = fmt; - scrp += scr->rowbytes / sizeof(*scrp) - clipped_width; - } - } - break; - - default: - assert(!"Unimplemented pixel format"); - break; - } + /* Each row is already clipped, so hand the pre-clipped span straight to + * screen_fill_hline. */ + for (yy = draw_box.y0; yy < draw_box.y1; yy++) + screen_fill_hline(scr, draw_box.x0, yy, draw_box.x1 - draw_box.x0, colour); } void screen_fill_square(screen_t *scr, diff --git a/libraries/framebuf/screen/screen-fill-hline.c b/libraries/framebuf/screen/screen-fill-hline.c new file mode 100644 index 00000000..a2b1dca4 --- /dev/null +++ b/libraries/framebuf/screen/screen-fill-hline.c @@ -0,0 +1,77 @@ +/* framebuf/screen/screen-fill-hline.c -- horizontal run fill */ + +#include <assert.h> +#include <stddef.h> + +#include "framebuf/colour.h" + +#include "framebuf/screen.h" + +/* ----------------------------------------------------------------------- */ + +void screen_fill_hline(screen_t *scr, int x, int y, int w, colour_t colour) +{ + box_t clip_box; + box_t run_box; + box_t draw_box; + int clipped_width; + pixelfmt_any_t fmt; + + if (w <= 0) + return; + + if (screen_get_clip(scr, &clip_box)) + return; /* invalid clipped screen */ + + run_box.x0 = x; + run_box.y0 = y; + run_box.x1 = x + w; + run_box.y1 = y + 1; + if (box_intersection(&clip_box, &run_box, &draw_box)) + return; + + clipped_width = draw_box.x1 - draw_box.x0; + + fmt = colour_to_pixel(scr->palette, + (scr->format == pixelfmt_p4) ? 16 : 0, + colour, scr->format); + switch (pixelfmt_log2bpp(scr->format)) + { + case 2: + { + unsigned char *rowp; + int xx; + + rowp = (unsigned char *) scr->base + draw_box.y0 * scr->rowbytes; + for (xx = 0; xx < clipped_width; xx++) + { + int px; + unsigned char *scrp; + int shift; + + px = draw_box.x0 + xx; + scrp = rowp + (px >> 1); + shift = (px & 1) * 4; + + *scrp = (unsigned char) ((*scrp & ~(0xF << shift)) | ((fmt & 0xF) << shift)); + } + } + break; + + case 5: + { + pixelfmt_any32_t *scrp; + int ww; + + scrp = scr->base; + scrp += draw_box.y0 * scr->rowbytes / sizeof(*scrp) + draw_box.x0; + for (ww = clipped_width; ww > 0; ww--) + *scrp++ = fmt; + } + break; + + default: + assert(!"Unimplemented pixel format"); + break; + } +} From f087eb3f932cc9b00cffc22dd7c4ac8b52358bdb Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Thu, 3 Sep 2026 11:07:47 +0100 Subject: [PATCH 210/287] refactor(framebuf): split per-pixel-format screen op bodies into helpers Every screen op that switch()es on pixelfmt_log2bpp now has one static helper per format, and the public function is a thin dispatcher: screen_set_pixel (_p4/_8/_16/_32), screen_blend_pixel (_p4/_32), screen_copy_bitmap (_p4/_32), screen_fill_hline (_p4/_32), screen_fill_pattern (_p4/_32), and screen_copy_rect (_p4, plus the whole-byte 8/16/32bpp path as screen_copy_rect_bytes). No behavioural change. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- libraries/framebuf/screen/screen-copy-rect.c | 72 ++-- libraries/framebuf/screen/screen-draw.c | 378 ++++++++++-------- libraries/framebuf/screen/screen-fill-hline.c | 73 ++-- .../framebuf/screen/screen-fill-pattern.c | 182 +++++---- 4 files changed, 401 insertions(+), 304 deletions(-) diff --git a/libraries/framebuf/screen/screen-copy-rect.c b/libraries/framebuf/screen/screen-copy-rect.c index b5be1b02..36eed1a7 100644 --- a/libraries/framebuf/screen/screen-copy-rect.c +++ b/libraries/framebuf/screen/screen-copy-rect.c @@ -62,15 +62,51 @@ static result_t screen_copy_rect_p4(screen_t *scr, return result_OK; } +/* Whole-byte formats (8/16/32bpp): a row of "width" pixels is "width * bpp" + * contiguous bytes, so each row moves in one memmove. Rows can alias across + * each other (never within a row) when the vertical shift is smaller than the + * copied height, so walk in the direction that always reads a row before it + * is overwritten. */ +static result_t screen_copy_rect_bytes(screen_t *scr, + const box_t *s, + const box_t *d, + int width, + int height, + int dy, + int bpp) +{ + unsigned char *base; + int rowbytes; + int row; + + base = scr->base; + rowbytes = scr->rowbytes; + + if (dy > 0) + { + for (row = height - 1; row >= 0; row--) + memmove(base + (size_t) (d->y0 + row) * rowbytes + (size_t) d->x0 * bpp, + base + (size_t) (s->y0 + row) * rowbytes + (size_t) s->x0 * bpp, + (size_t) width * bpp); + } + else + { + for (row = 0; row < height; row++) + memmove(base + (size_t) (d->y0 + row) * rowbytes + (size_t) d->x0 * bpp, + base + (size_t) (s->y0 + row) * rowbytes + (size_t) s->x0 * bpp, + (size_t) width * bpp); + } + + return result_OK; +} + result_t screen_copy_rect(screen_t *scr, const box_t *src, point_t dst, box_t *copied_dst) { - box_t clip_box, s, d, d_clipped; - int dx, dy, width, height, bpp; - unsigned char *base; - int rowbytes; + box_t clip_box, s, d, d_clipped; + int dx, dy, width, height, bpp; if (screen_get_clip(scr, &clip_box)) return result_NOT_SUPPORTED; /* invalid clipped screen */ @@ -122,31 +158,5 @@ result_t screen_copy_rect(screen_t *scr, return result_NOT_SUPPORTED; } - base = scr->base; - rowbytes = scr->rowbytes; - - /* Rows can alias across each other (never within a row, since a row's - * bytes never overlap another row's) when the vertical shift is smaller - * than the copied height, so walk in the direction that always reads a - * row before it's overwritten. */ - if (dy > 0) - { - int row; - - for (row = height - 1; row >= 0; row--) - memmove(base + (size_t) (d_clipped.y0 + row) * rowbytes + (size_t) d_clipped.x0 * bpp, - base + (size_t) (s.y0 + row) * rowbytes + (size_t) s.x0 * bpp, - (size_t) width * bpp); - } - else - { - int row; - - for (row = 0; row < height; row++) - memmove(base + (size_t) (d_clipped.y0 + row) * rowbytes + (size_t) d_clipped.x0 * bpp, - base + (size_t) (s.y0 + row) * rowbytes + (size_t) s.x0 * bpp, - (size_t) width * bpp); - } - - return result_OK; + return screen_copy_rect_bytes(scr, &s, &d_clipped, width, height, dy, bpp); } diff --git a/libraries/framebuf/screen/screen-draw.c b/libraries/framebuf/screen/screen-draw.c index ce9939e1..e45af9d4 100644 --- a/libraries/framebuf/screen/screen-draw.c +++ b/libraries/framebuf/screen/screen-draw.c @@ -20,6 +20,62 @@ * blow the stack (relevant on RISC OS). */ #define BITMAP_BLIT_CHUNK 256 +/* 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_p4(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 >> 1); + shift = (x & 1) * 4; + + *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) +{ + pixelfmt_any8_t *scrp; + + scrp = scr->base; + scrp += y * scr->rowbytes / sizeof(*scrp) + x; + + *scrp = (pixelfmt_any8_t) pxl; +} + +static void screen_set_pixel_16(screen_t *scr, + int x, + int y, + pixelfmt_any_t pxl) +{ + pixelfmt_any16_t *scrp; + + scrp = scr->base; + scrp += y * scr->rowbytes / sizeof(*scrp) + x; + + *scrp = pxl; +} + +static void screen_set_pixel_32(screen_t *scr, + int x, + int y, + pixelfmt_any_t pxl) +{ + pixelfmt_any32_t *scrp; + + scrp = scr->base; + scrp += y * scr->rowbytes / sizeof(*scrp) + x; + + *scrp = pxl; +} + void screen_set_pixel(screen_t *scr, int x, int y, colour_t colour) { box_t clip; @@ -33,55 +89,54 @@ void screen_set_pixel(screen_t *scr, int x, int y, colour_t colour) colour, scr->format); switch (pixelfmt_log2bpp(scr->format)) { - case 2: - { - unsigned char *scrp; - int shift; + 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; + case 5: screen_set_pixel_32(scr, x, y, pxl); break; - scrp = (unsigned char *) scr->base + y * scr->rowbytes + (x >> 1); - shift = (x & 1) * 4; - - *scrp = (unsigned char) ((*scrp & ~(0xF << shift)) | ((pxl & 0xF) << shift)); - } + default: + assert(!"Unimplemented pixel format"); break; + } +} - case 3: - { - pixelfmt_any8_t *scrp; - - scrp = scr->base; - scrp += y * scr->rowbytes / sizeof(*scrp) + x; +/* Each helper alpha-blends "colour" at "alpha" into the single pixel (x, y), + * already known to be inside the clip. */ - *scrp = (pixelfmt_any8_t) pxl; - } - break; +static void screen_blend_pixel_p4(screen_t *scr, + int x, + int y, + colour_t colour, + int alpha) +{ + unsigned char *scrp; + int shift; + unsigned char idx, out; - case 4: - { - pixelfmt_any16_t *scrp; + scrp = (unsigned char *) scr->base + y * scr->rowbytes + (x >> 1); + shift = (x & 1) * 4; + idx = (*scrp >> shift) & 0xF; - scrp = scr->base; - scrp += y * scr->rowbytes / sizeof(*scrp) + x; + scr->span->blendconst(&out, &idx, &colour, 1, alpha, scr->palette); - *scrp = pxl; - } - break; + *scrp = (unsigned char) ((*scrp & ~(0xF << shift)) | ((out & 0xF) << shift)); +} - case 5: - { - pixelfmt_any32_t *scrp; +static void screen_blend_pixel_32(screen_t *scr, + int x, + int y, + colour_t colour, + int alpha) +{ + pixelfmt_any32_t *scrp; + pixelfmt_any_t colpx; - scrp = scr->base; - scrp += y * scr->rowbytes / sizeof(*scrp) + x; + colpx = colour_to_pixel(NULL, 0, colour, scr->format); - *scrp = pxl; - } - break; + scrp = scr->base; + scrp += y * scr->rowbytes / sizeof(*scrp) + x; - default: - assert(!"Unimplemented pixel format"); - break; - } + scr->span->blendconst(scrp, scrp, &colpx, 1, alpha, NULL); } static void screen_blend_pixel(screen_t *scr, @@ -100,35 +155,8 @@ static void screen_blend_pixel(screen_t *scr, switch (pixelfmt_log2bpp(scr->format)) { - case 2: - { - unsigned char *scrp; - int shift; - unsigned char idx, out; - - scrp = (unsigned char *) scr->base + y * scr->rowbytes + (x >> 1); - shift = (x & 1) * 4; - idx = (*scrp >> shift) & 0xF; - - scr->span->blendconst(&out, &idx, &colour, 1, alpha, scr->palette); - - *scrp = (unsigned char) ((*scrp & ~(0xF << shift)) | ((out & 0xF) << shift)); - } - break; - - case 5: - { - pixelfmt_any32_t *scrp; - pixelfmt_any_t colpx; - - colpx = colour_to_pixel(NULL, 0, colour, scr->format); - - scrp = scr->base; - scrp += y * scr->rowbytes / sizeof(*scrp) + x; - - scr->span->blendconst(scrp, scrp, &colpx, 1, alpha, NULL); - } - 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; default: assert(!"Unimplemented pixel format"); @@ -176,12 +204,126 @@ 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) +{ + const unsigned char *srcrow; + unsigned char *dstbase; + 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; + + 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; + int dstx; + unsigned char *scrp; + int shift; + 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 >> 1); + shift = (dstx & 1) * 4; + pxl = colour_to_pixel(scr->palette, 16, c, scr->format); + + *scrp = (unsigned char) ((*scrp & ~(0xF << shift)) | ((pxl & 0xF) << shift)); + } + + srcrow += src->rowbytes; + } +} + +/* 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, + int x, + int y, + const bitmap_t *src, + const box_t *draw_box, + int has_alpha) +{ + pixelfmt_any32_t colbuf[BITMAP_BLIT_CHUNK]; + unsigned char alphabuf[BITMAP_BLIT_CHUNK]; + const unsigned char *srcrow; + pixelfmt_any32_t *dstrow; + 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; + dstrow = scr->base; + dstrow += draw_box->y0 * scr->rowbytes / (int) sizeof(*dstrow) + draw_box->x0; + + for (yy = 0; yy < clipped_height; yy++) + { + const pixelfmt_rgba8888_t *srcpx; + pixelfmt_any32_t *dstpx; + int remaining; + + srcpx = (const pixelfmt_rgba8888_t *) srcrow + (draw_box->x0 - x); + dstpx = dstrow; + remaining = clipped_width; + + while (remaining > 0) + { + int chunk, i; + + chunk = (remaining > BITMAP_BLIT_CHUNK) ? BITMAP_BLIT_CHUNK : remaining; + + for (i = 0; i < chunk; i++) + { + colour_t c; + + c.primary = srcpx[i]; + colbuf[i] = colour_to_pixel(scr->palette, 0, c, scr->format); + alphabuf[i] = has_alpha ? colour_get_alpha(&c) : PIXELFMT_OPAQUE; + } + + scr->span->blendarray(dstpx, dstpx, colbuf, chunk, alphabuf); + + srcpx += chunk; + dstpx += chunk; + remaining -= chunk; + } + + srcrow += src->rowbytes; + dstrow += scr->rowbytes / (int) sizeof(*dstrow); + } +} + result_t screen_copy_bitmap(screen_t *scr, int x, int y, const bitmap_t *src) { box_t clip_box; box_t src_box; box_t draw_box; - int clipped_width, clipped_height; int has_alpha; if (screen_get_clip(scr, &clip_box)) @@ -194,9 +336,6 @@ 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 */ - clipped_width = draw_box.x1 - draw_box.x0; - clipped_height = draw_box.y1 - draw_box.y0; - /* 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. */ @@ -204,101 +343,8 @@ result_t screen_copy_bitmap(screen_t *scr, int x, int y, const bitmap_t *src) switch (pixelfmt_log2bpp(scr->format)) { - case 2: - { - /* Paletted screen: no linear channel bits to blend, so fall back to - * alpha-tested (skip fully transparent, else nearest palette match) - * rather than true blending, matching screen_set_pixel's case 2. */ - const unsigned char *srcrow; - unsigned char *dstbase; - int yy; - - srcrow = (const unsigned char *) src->base + (draw_box.y0 - y) * src->rowbytes; - dstbase = scr->base; - - 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; - int dstx; - unsigned char *scrp; - int shift; - 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 >> 1); - shift = (dstx & 1) * 4; - pxl = colour_to_pixel(scr->palette, 16, c, scr->format); - - *scrp = (unsigned char) ((*scrp & ~(0xF << shift)) | ((pxl & 0xF) << shift)); - } - - srcrow += src->rowbytes; - } - } - break; - - case 5: - { - pixelfmt_any32_t colbuf[BITMAP_BLIT_CHUNK]; - unsigned char alphabuf[BITMAP_BLIT_CHUNK]; - const unsigned char *srcrow; - pixelfmt_any32_t *dstrow; - int yy; - - srcrow = (const unsigned char *) src->base + (draw_box.y0 - y) * src->rowbytes; - dstrow = scr->base; - dstrow += draw_box.y0 * scr->rowbytes / (int) sizeof(*dstrow) + draw_box.x0; - - for (yy = 0; yy < clipped_height; yy++) - { - const pixelfmt_rgba8888_t *srcpx; - pixelfmt_any32_t *dstpx; - int remaining; - - srcpx = (const pixelfmt_rgba8888_t *) srcrow + (draw_box.x0 - x); - dstpx = dstrow; - remaining = clipped_width; - - while (remaining > 0) - { - int chunk, i; - - chunk = (remaining > BITMAP_BLIT_CHUNK) ? BITMAP_BLIT_CHUNK : remaining; - - for (i = 0; i < chunk; i++) - { - colour_t c; - - c.primary = srcpx[i]; - colbuf[i] = colour_to_pixel(scr->palette, 0, c, scr->format); - alphabuf[i] = has_alpha ? colour_get_alpha(&c) : PIXELFMT_OPAQUE; - } - - scr->span->blendarray(dstpx, dstpx, colbuf, chunk, alphabuf); - - srcpx += chunk; - dstpx += chunk; - remaining -= chunk; - } - - srcrow += src->rowbytes; - dstrow += scr->rowbytes / (int) sizeof(*dstrow); - } - } - break; + case 2: screen_copy_bitmap_p4(scr, x, y, src, &draw_box, has_alpha); break; + case 5: screen_copy_bitmap_32(scr, x, y, src, &draw_box, has_alpha); break; default: assert(!"Unimplemented pixel format"); diff --git a/libraries/framebuf/screen/screen-fill-hline.c b/libraries/framebuf/screen/screen-fill-hline.c index a2b1dca4..1de4f563 100644 --- a/libraries/framebuf/screen/screen-fill-hline.c +++ b/libraries/framebuf/screen/screen-fill-hline.c @@ -9,6 +9,50 @@ /* ----------------------------------------------------------------------- */ +/* Each helper fills "width" pixels of one already-clipped row starting at + * (x0, y0), in the colour previously resolved to "fmt". */ + +static void screen_fill_hline_p4(screen_t *scr, + int x0, + int y0, + int width, + pixelfmt_any_t fmt) +{ + unsigned char *rowp; + int xx; + + rowp = (unsigned char *) scr->base + y0 * scr->rowbytes; + for (xx = 0; xx < width; xx++) + { + int px; + unsigned char *scrp; + int shift; + + px = x0 + xx; + scrp = rowp + (px >> 1); + shift = (px & 1) * 4; + + *scrp = (unsigned char) ((*scrp & ~(0xF << shift)) | ((fmt & 0xF) << shift)); + } +} + +static void screen_fill_hline_32(screen_t *scr, + int x0, + int y0, + int width, + pixelfmt_any_t fmt) +{ + pixelfmt_any32_t *scrp; + int ww; + + scrp = scr->base; + scrp += y0 * scr->rowbytes / sizeof(*scrp) + x0; + for (ww = width; ww > 0; ww--) + *scrp++ = fmt; +} + +/* ----------------------------------------------------------------------- */ + void screen_fill_hline(screen_t *scr, int x, int y, int w, colour_t colour) { box_t clip_box; @@ -38,36 +82,11 @@ void screen_fill_hline(screen_t *scr, int x, int y, int w, colour_t colour) switch (pixelfmt_log2bpp(scr->format)) { case 2: - { - unsigned char *rowp; - int xx; - - rowp = (unsigned char *) scr->base + draw_box.y0 * scr->rowbytes; - for (xx = 0; xx < clipped_width; xx++) - { - int px; - unsigned char *scrp; - int shift; - - px = draw_box.x0 + xx; - scrp = rowp + (px >> 1); - shift = (px & 1) * 4; - - *scrp = (unsigned char) ((*scrp & ~(0xF << shift)) | ((fmt & 0xF) << shift)); - } - } + screen_fill_hline_p4(scr, draw_box.x0, draw_box.y0, clipped_width, fmt); break; case 5: - { - pixelfmt_any32_t *scrp; - int ww; - - scrp = scr->base; - scrp += draw_box.y0 * scr->rowbytes / sizeof(*scrp) + draw_box.x0; - for (ww = clipped_width; ww > 0; ww--) - *scrp++ = fmt; - } + screen_fill_hline_32(scr, draw_box.x0, draw_box.y0, clipped_width, fmt); break; default: diff --git a/libraries/framebuf/screen/screen-fill-pattern.c b/libraries/framebuf/screen/screen-fill-pattern.c index fb689ea2..34e440f2 100644 --- a/libraries/framebuf/screen/screen-fill-pattern.c +++ b/libraries/framebuf/screen/screen-fill-pattern.c @@ -10,6 +10,104 @@ #include "framebuf/screen.h" +/* runs[row][col] is the pattern's expanded colour for tile row "row" at + * screen column (draw_box.x0 + col), i.e. already phase-shifted for x. The + * scanline helpers below just index it; stencil paths re-test the pattern + * bit per pixel rather than using the run. */ +typedef pixelfmt_any_t pattern_runs_t[8][8]; + +static void screen_fill_pattern_p4(screen_t *scr, + const pattern_t *pattern, + const box_t *draw_box, + int stencil, + const 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 >> 1); + shift = (x & 1) * 4; + + *scrp = (unsigned char) ((*scrp & ~(0xF << shift)) | + ((run[col] & 0xF) << shift)); + } + col = (col + 1) & 7; + } + rowp += scr->rowbytes; + } +} + +static void screen_fill_pattern_32(screen_t *scr, + const pattern_t *pattern, + const box_t *draw_box, + int stencil, + pixelfmt_any_t fg_fmt, + const 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; + pixelfmt_any32_t *scrp; + int w; + int n; + + row = (y - pattern->origin.y) & 7; + run = runs[row]; + bits = pattern->bits[row]; + scrp = (pixelfmt_any32_t *) rowp + draw_box->x0; + w = draw_box->x1 - draw_box->x0; + + if (!stencil) + { + /* leading partial tile up to an 8-pixel boundary, then whole runs */ + col = 0; + while (w > 0) + { + n = 8 - col; + if (n > w) + n = w; + memcpy(scrp, run + col, (size_t) n * sizeof(*scrp)); + scrp += n; + w -= n; + col = 0; + } + } + else + { + for (x = draw_box->x0; x < draw_box->x1; x++) + { + if (bits & (0x80u >> ((x - pattern->origin.x) & 7))) + *scrp = fg_fmt; + scrp++; + } + } + rowp += scr->rowbytes; + } +} + void screen_fill_pattern(screen_t *scr, const box_t *box, const pattern_t *pattern) @@ -18,9 +116,9 @@ void screen_fill_pattern(screen_t *scr, box_t draw_box; int stencil; pixelfmt_any_t fg_fmt, bg_fmt; - pixelfmt_any_t runs[8][8]; /* one expanded colour run per tile row */ + pattern_runs_t runs; /* one expanded colour run per tile row */ int xphase; - int row, col, x, y; + int row, col; assert(scr); assert(pattern); @@ -40,10 +138,6 @@ void screen_fill_pattern(screen_t *scr, (scr->format == pixelfmt_p4) ? 16 : 0, pattern->bg, scr->format); - /* The tile is 8x8 and repeats, so there are only eight distinct pixel rows. - * Expand each to a colour run once here, already phase-shifted for x, then - * the scanline loops just index runs[row][col]. Stencil rows re-test the - * pattern bit per pixel rather than using the run. */ xphase = ((draw_box.x0 - pattern->origin.x) & 7); for (row = 0; row < 8; row++) { @@ -58,83 +152,11 @@ void screen_fill_pattern(screen_t *scr, switch (pixelfmt_log2bpp(scr->format)) { case 2: - { - unsigned char *rowp; - const pixelfmt_any_t *run; - uint8_t bits; - unsigned char *scrp; - int shift; - - rowp = (unsigned char *) scr->base + draw_box.y0 * scr->rowbytes; - for (y = draw_box.y0; y < draw_box.y1; y++) - { - 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)))) - { - scrp = rowp + (x >> 1); - shift = (x & 1) * 4; - - *scrp = (unsigned char) ((*scrp & ~(0xF << shift)) | - ((run[col] & 0xF) << shift)); - } - col = (col + 1) & 7; - } - rowp += scr->rowbytes; - } - } + screen_fill_pattern_p4(scr, pattern, &draw_box, stencil, runs); break; case 5: - { - unsigned char *rowp; - const pixelfmt_any_t *run; - pixelfmt_any32_t *scrp; - uint8_t bits; - int w; - int n; - - rowp = (unsigned char *) scr->base + draw_box.y0 * scr->rowbytes; - for (y = draw_box.y0; y < draw_box.y1; y++) - { - row = (y - pattern->origin.y) & 7; - run = runs[row]; - bits = pattern->bits[row]; - scrp = (pixelfmt_any32_t *) rowp + draw_box.x0; - w = draw_box.x1 - draw_box.x0; - - if (!stencil) - { - /* leading partial tile up to an 8-pixel boundary, then whole runs */ - col = 0; - while (w > 0) - { - n = 8 - col; - if (n > w) - n = w; - memcpy(scrp, run + col, (size_t) n * sizeof(*scrp)); - scrp += n; - w -= n; - col = 0; - } - } - else - { - for (x = draw_box.x0; x < draw_box.x1; x++) - { - if (bits & (0x80u >> ((x - pattern->origin.x) & 7))) - *scrp = fg_fmt; - scrp++; - } - } - rowp += scr->rowbytes; - } - } + screen_fill_pattern_32(scr, pattern, &draw_box, stencil, fg_fmt, runs); break; default: From d271462236262c944618b4b1fe63ca77eccd918a Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Thu, 3 Sep 2026 11:19:04 +0100 Subject: [PATCH 211/287] refactor(framebuf): route solid fills through a span->fill primitive Add span_fill_t to span_t: a per-format run fill taking (row base, first pixel index, pixel, length). The first-pixel index lets the P4 handler write an odd start nibble without the caller packing, unlike span_p4_blendconst which works on unpacked indices. Implement span_all8888_fill (shared by the four 8888 spans) and span_p4_fill (packed-nibble aware). screen_fill_hline collapses its per-format _p4/_32 helpers and its pixelfmt switch into one scr->span->fill call; screen_fill_rect and screen_draw_circle inherit the join through screen_fill_hline. screen_fill_pattern is left alone: its rows are two-colour expanded runs, not a uniform pixel, so span->fill does not fit. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- docs/framebuf/drawing.md | 8 ++- include/framebuf/span.h | 20 ++++++ libraries/framebuf/screen/screen-fill-hline.c | 62 ++----------------- libraries/framebuf/span/all8888.c | 12 ++++ libraries/framebuf/span/all8888.h | 1 + libraries/framebuf/span/bgrx8888.c | 1 + libraries/framebuf/span/p4.c | 28 +++++++++ libraries/framebuf/span/rgbx8888.c | 1 + libraries/framebuf/span/xbgr8888.c | 1 + 9 files changed, 76 insertions(+), 58 deletions(-) diff --git a/docs/framebuf/drawing.md b/docs/framebuf/drawing.md index b782fc23..8bd04161 100644 --- a/docs/framebuf/drawing.md +++ b/docs/framebuf/drawing.md @@ -46,8 +46,12 @@ built from the axes in order: client or the window manager draws onto. `bitmap_*` drawing ops are for off-screen construction and whole-image transforms. -The per-pixel-format `span_t` layer (`copy` / `blendconst` / `blendarray`) sits -below both and is not a public drawing verb. +The per-pixel-format `span_t` layer (`copy` / `fill` / `blendconst` / +`blendarray`) sits below both and is not a public drawing verb. Solid fills go +through `span->fill(row_base, first_pixel, pixel, length)`; `first_pixel` lets +the P4 handler address an odd nibble so callers never pre-pack. `screen_fill_hline` +(and thus `screen_fill_rect` and the circle scanline fill) is a thin wrapper over +it. ### Axis 2 — ACTION (what kind of mark) diff --git a/include/framebuf/span.h b/include/framebuf/span.h index bda935e8..149de5a6 100644 --- a/include/framebuf/span.h +++ b/include/framebuf/span.h @@ -14,6 +14,25 @@ */ typedef void (span_copy_t)(void *dst, const void *src, int length); +/** + * Type of a "fill run of pixels" function. + * + * Writes `length` copies of a single pixel value. `dst` points at the base + * of the run's row and `first` is the index of the first pixel to write + * within that row, so sub-byte formats (e.g. P4) can address an odd nibble + * without the caller pre-packing. Whole-byte formats treat `dst` + `first` + * as an ordinary pixel pointer. + * + * \param[out] dst Base of the destination row. + * \param[in] first Index of the first pixel to write, from `dst`. + * \param[in] pixel Pixel value to write (already quantised to the format). + * \param[in] length Number of pixels to write. + */ +typedef void (span_fill_t)(void *dst, + int first, + pixelfmt_any_t pixel, + int length); + /** * Type of a "blend constant pixels" function. * @@ -65,6 +84,7 @@ typedef struct span { pixelfmt_t format; /**< Pixel format this span is for. */ span_copy_t *copy; /**< Copy pixels function. */ + span_fill_t *fill; /**< Fill run of pixels function. */ span_blendconst_t *blendconst; /**< Blend constant pixels function. */ span_blendarray_t *blendarray; /**< Blend array of pixels function. */ } diff --git a/libraries/framebuf/screen/screen-fill-hline.c b/libraries/framebuf/screen/screen-fill-hline.c index 1de4f563..0f3c0282 100644 --- a/libraries/framebuf/screen/screen-fill-hline.c +++ b/libraries/framebuf/screen/screen-fill-hline.c @@ -9,50 +9,6 @@ /* ----------------------------------------------------------------------- */ -/* Each helper fills "width" pixels of one already-clipped row starting at - * (x0, y0), in the colour previously resolved to "fmt". */ - -static void screen_fill_hline_p4(screen_t *scr, - int x0, - int y0, - int width, - pixelfmt_any_t fmt) -{ - unsigned char *rowp; - int xx; - - rowp = (unsigned char *) scr->base + y0 * scr->rowbytes; - for (xx = 0; xx < width; xx++) - { - int px; - unsigned char *scrp; - int shift; - - px = x0 + xx; - scrp = rowp + (px >> 1); - shift = (px & 1) * 4; - - *scrp = (unsigned char) ((*scrp & ~(0xF << shift)) | ((fmt & 0xF) << shift)); - } -} - -static void screen_fill_hline_32(screen_t *scr, - int x0, - int y0, - int width, - pixelfmt_any_t fmt) -{ - pixelfmt_any32_t *scrp; - int ww; - - scrp = scr->base; - scrp += y0 * scr->rowbytes / sizeof(*scrp) + x0; - for (ww = width; ww > 0; ww--) - *scrp++ = fmt; -} - -/* ----------------------------------------------------------------------- */ - void screen_fill_hline(screen_t *scr, int x, int y, int w, colour_t colour) { box_t clip_box; @@ -60,6 +16,7 @@ void screen_fill_hline(screen_t *scr, int x, int y, int w, colour_t colour) box_t draw_box; int clipped_width; pixelfmt_any_t fmt; + unsigned char *rowp; if (w <= 0) return; @@ -79,18 +36,11 @@ void screen_fill_hline(screen_t *scr, int x, int y, int w, colour_t colour) fmt = colour_to_pixel(scr->palette, (scr->format == pixelfmt_p4) ? 16 : 0, colour, scr->format); - switch (pixelfmt_log2bpp(scr->format)) - { - case 2: - screen_fill_hline_p4(scr, draw_box.x0, draw_box.y0, clipped_width, fmt); - break; - case 5: - screen_fill_hline_32(scr, draw_box.x0, draw_box.y0, clipped_width, fmt); - break; + /* the per-format run fill lives in the span table; "first" lets it address + * an odd P4 nibble so this needn't pack */ + assert(scr->span && scr->span->fill); - default: - assert(!"Unimplemented pixel format"); - break; - } + rowp = (unsigned char *) scr->base + draw_box.y0 * scr->rowbytes; + scr->span->fill(rowp, draw_box.x0, fmt, clipped_width); } diff --git a/libraries/framebuf/span/all8888.c b/libraries/framebuf/span/all8888.c index 51922e33..5d2e06be 100644 --- a/libraries/framebuf/span/all8888.c +++ b/libraries/framebuf/span/all8888.c @@ -11,3 +11,15 @@ void span_all8888_copy(void *dst, const void *src, int length) memcpy(dst, src, length * 4); } + +void span_all8888_fill(void *dst, + int first, + pixelfmt_any_t pixel, + int length) +{ + pixelfmt_any32_t *p; + + p = (pixelfmt_any32_t *) dst + first; + while (length--) + *p++ = pixel; +} diff --git a/libraries/framebuf/span/all8888.h b/libraries/framebuf/span/all8888.h index 028e41c7..6984c05c 100644 --- a/libraries/framebuf/span/all8888.h +++ b/libraries/framebuf/span/all8888.h @@ -6,5 +6,6 @@ #include "framebuf/span.h" span_copy_t span_all8888_copy; +span_fill_t span_all8888_fill; #endif /* SPAN_ALL8888_H */ diff --git a/libraries/framebuf/span/bgrx8888.c b/libraries/framebuf/span/bgrx8888.c index bafceaff..da51fc13 100644 --- a/libraries/framebuf/span/bgrx8888.c +++ b/libraries/framebuf/span/bgrx8888.c @@ -24,6 +24,7 @@ const span_t span_bgrx8888 = { pixelfmt_bgrx8888, span_all8888_copy, + span_all8888_fill, span_bgrx8888_blendconst, span_bgrx8888_blendarray, }; diff --git a/libraries/framebuf/span/p4.c b/libraries/framebuf/span/p4.c index d35e0834..e612bce2 100644 --- a/libraries/framebuf/span/p4.c +++ b/libraries/framebuf/span/p4.c @@ -67,10 +67,38 @@ static void span_p4_blendconst(void *vdst, } } +/* unlike span_p4_blendconst above, this works directly on packed screen + * bytes: "dst" is the row base, "first" the nibble (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 nibble. */ +static void span_p4_fill(void *vdst, + int first, + pixelfmt_any_t pixel, + int length) +{ + unsigned char *base; + unsigned char nib; + int x; + + base = vdst; + nib = (unsigned char) (pixel & 0xF); + + for (x = first; x < first + length; x++) + { + unsigned char *p; + int shift; + + p = base + (x >> 1); + shift = (x & 1) * 4; + *p = (unsigned char) ((*p & ~(0xF << shift)) | (nib << shift)); + } +} + const span_t span_p4 = { pixelfmt_p4, NULL, /* copy: unneeded so far (nibble packing makes a generic array copy awkward) */ + span_p4_fill, span_p4_blendconst, NULL, /* blendarray: unneeded so far */ }; diff --git a/libraries/framebuf/span/rgbx8888.c b/libraries/framebuf/span/rgbx8888.c index 6f1efe32..1b6358ad 100644 --- a/libraries/framebuf/span/rgbx8888.c +++ b/libraries/framebuf/span/rgbx8888.c @@ -24,6 +24,7 @@ const span_t span_rgbx8888 = { pixelfmt_rgbx8888, span_all8888_copy, + span_all8888_fill, span_rgbx8888_blendconst, span_rgbx8888_blendarray, }; diff --git a/libraries/framebuf/span/xbgr8888.c b/libraries/framebuf/span/xbgr8888.c index a2c8acde..b64897e0 100644 --- a/libraries/framebuf/span/xbgr8888.c +++ b/libraries/framebuf/span/xbgr8888.c @@ -24,6 +24,7 @@ const span_t span_xbgr8888 = { pixelfmt_xbgr8888, span_all8888_copy, + span_all8888_fill, span_xbgr8888_blendconst, span_xbgr8888_blendarray, }; From 9a3e85e6926e425e8b68c7f226206f553dff4af8 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Thu, 3 Sep 2026 11:32:45 +0100 Subject: [PATCH 212/287] docs(framebuf): note screen_fill_hline wraps span->fill in the status summary Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- docs/framebuf/drawing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/framebuf/drawing.md b/docs/framebuf/drawing.md index 8bd04161..a30f9357 100644 --- a/docs/framebuf/drawing.md +++ b/docs/framebuf/drawing.md @@ -183,7 +183,7 @@ spellings going forward. `screen_draw_line_wu_fix8`, `screen_draw_line_wu_float` - `screen_draw_rect`, `screen_fill_rect`, `screen_fill_square`, `screen_fill_hline` (the per-row primitive `screen_fill_rect` and - `screen_draw_circle` build on) + `screen_draw_circle` build on; itself a thin wrapper over `span->fill`) - `screen_draw_circle`, `screen_fill_circle` - `screen_fill_pattern` (takes `const pattern_t *`) - `screen_copy_bitmap`, `screen_copy_ninepatch`, `screen_copy_rect` — all From 39a618a937dc049a7a8240486f6c3f21a7277533 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Thu, 3 Sep 2026 12:09:46 +0100 Subject: [PATCH 213/287] feat(bmfont): add bmfont_enumerate to list fonts in a directory Scans a directory for *.png files and reports each as a (name, path) pair to a caller-supplied callback, name being the leaf with the .png extension stripped. Non-recursive, POSIX dirent, order unspecified; callback may return result_STOP_WALK to stop early. Groundwork for a wuss shared-component FontMenu (the RISC OS Toolbox analogue): the component needs a source of installed font names. Test covers full walk, stop-walk, missing directory and NULL args. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- CMakeLists.txt | 1 + include/framebuf/bmfont.h | 32 ++++++ libraries/framebuf/bmfont/enumerate.c | 65 +++++++++++ libraries/framebuf/bmfont/test/bmfont-test.c | 108 +++++++++++++++++++ 4 files changed, 206 insertions(+) create mode 100644 libraries/framebuf/bmfont/enumerate.c diff --git a/CMakeLists.txt b/CMakeLists.txt index b83302d5..9e399efb 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -242,6 +242,7 @@ set(FRAMEBUF_SOURCES libraries/framebuf/bitmap/load.c libraries/framebuf/bitmap/save.c libraries/framebuf/bmfont/bmfont.c + libraries/framebuf/bmfont/enumerate.c libraries/framebuf/colour/colour.c libraries/framebuf/composite/composite.c libraries/framebuf/curve/curve.c diff --git a/include/framebuf/bmfont.h b/include/framebuf/bmfont.h index 417cfa17..487ff76c 100644 --- a/include/framebuf/bmfont.h +++ b/include/framebuf/bmfont.h @@ -22,6 +22,38 @@ typedef int bmfont_width_t; /* in pixels */ */ result_t bmfont_create(const char *png, bmfont_t **bmfont); +/** + * Callback for bmfont_enumerate(), invoked once per font found. + * + * \param[in] name The font's leafname with the ".png" extension stripped, + * e.g. "Trinity.Medium" for "Trinity.Medium.png". Borrowed; + * copy it if it must outlive the call. + * \param[in] path The full path that would be passed to bmfont_create() to + * load this font. Borrowed. + * \param[in] opaque The pointer passed to bmfont_enumerate(). + * \return \ref result_OK to continue, \ref result_STOP_WALK to stop early + * (bmfont_enumerate() then also returns \ref result_OK), or any + * other code to abort with that code. + */ +typedef result_t (bmfont_enumerate_fn)(const char *name, + const char *path, + void *opaque); + +/** + * Enumerate the bitmap fonts in a directory: every ".png" file in \p dir is + * reported to \p fn (non-recursive, order unspecified). + * + * \param[in] dir Directory to scan. + * \param[in] fn Called once per font; see bmfont_enumerate_fn. + * \param[in] opaque Passed through to \p fn. + * \return \ref result_OK on success (including a stop-walk), \ref + * result_FILE_NOT_FOUND if \p dir cannot be opened, \ref + * result_NULL_ARG, or a code propagated from \p fn. + */ +result_t bmfont_enumerate(const char *dir, + bmfont_enumerate_fn *fn, + void *opaque); + /** * Destroy a bitmap font. * diff --git a/libraries/framebuf/bmfont/enumerate.c b/libraries/framebuf/bmfont/enumerate.c new file mode 100644 index 00000000..e930ee64 --- /dev/null +++ b/libraries/framebuf/bmfont/enumerate.c @@ -0,0 +1,65 @@ +/* framebuf/bmfont/enumerate.c -- list the bitmap fonts in a directory */ + +#include <dirent.h> +#include <stddef.h> +#include <stdio.h> +#include <string.h> + +#include "base/result.h" +#include "framebuf/bmfont.h" + +/* ----------------------------------------------------------------------- */ + +#define BMFONT_EXT ".png" +#define BMFONT_EXT_LEN 4 + +result_t bmfont_enumerate(const char *dir, + bmfont_enumerate_fn *fn, + void *opaque) +{ + result_t rc; + DIR *dp; + struct dirent *de; + const char *leaf; + size_t leaflen; + char name[256]; + char path[512]; + + if (dir == NULL || fn == NULL) + return result_NULL_ARG; + + dp = opendir(dir); + if (dp == NULL) + return result_FILE_NOT_FOUND; + + rc = result_OK; + + while ((de = readdir(dp)) != NULL) + { + leaf = de->d_name; + leaflen = strlen(leaf); + + if (leaflen <= BMFONT_EXT_LEN || leaflen - BMFONT_EXT_LEN >= sizeof(name)) + continue; + if (strcmp(leaf + leaflen - BMFONT_EXT_LEN, BMFONT_EXT) != 0) + continue; + + memcpy(name, leaf, leaflen - BMFONT_EXT_LEN); + name[leaflen - BMFONT_EXT_LEN] = '\0'; + + snprintf(path, sizeof(path), "%s/%s", dir, leaf); + + rc = fn(name, path, opaque); + if (rc == result_STOP_WALK) + { + rc = result_OK; + break; + } + if (rc != result_OK) + break; + } + + closedir(dp); + + return rc; +} diff --git a/libraries/framebuf/bmfont/test/bmfont-test.c b/libraries/framebuf/bmfont/test/bmfont-test.c index 86457d8f..eb936cc2 100644 --- a/libraries/framebuf/bmfont/test/bmfont-test.c +++ b/libraries/framebuf/bmfont/test/bmfont-test.c @@ -715,6 +715,110 @@ static result_t bmfont_interactive_test(bmfontteststate_t *state) return result_TEST_PASSED; } +/* ----------------------------------------------------------------------- */ + +typedef struct bmfont_enum_check +{ + int found[MAXFONTS]; /* parallel to bmfonts[]; set when that name is seen */ + int total; /* every callback, including unrecognised names */ + int stop_after; /* >0: return result_STOP_WALK once total reaches it */ +} +bmfont_enum_check_t; + +static result_t bmfont_enum_cb(const char *name, + const char *path, + void *opaque) +{ + bmfont_enum_check_t *chk = opaque; + int i; + + assert(name); + assert(path); + + chk->total++; + + for (i = 0; i < MAXFONTS; i++) + if (strcmp(name, bmfonts[i].filename) == 0) + chk->found[i] = 1; + + if (chk->stop_after > 0 && chk->total >= chk->stop_after) + return result_STOP_WALK; + + return result_OK; +} + +static result_t bmfont_enumerate_test(const char *resources) +{ + const char *dir; + bmfont_enum_check_t chk; + result_t rc; + int i; + + dir = path_join_filename(resources, 2, "resources", "bmfonts"); + + /* full walk: every fixture font is reported exactly once */ + memset(&chk, 0, sizeof(chk)); + rc = bmfont_enumerate(dir, bmfont_enum_cb, &chk); + if (rc != result_OK) + { + fprintf(stderr, "bmfont_enumerate: unexpected rc %x\n", rc); + return result_TEST_FAILED; + } + if (chk.total != MAXFONTS) + { + fprintf(stderr, "bmfont_enumerate: saw %d entries, expected %d\n", + chk.total, MAXFONTS); + return result_TEST_FAILED; + } + for (i = 0; i < MAXFONTS; i++) + { + if (!chk.found[i]) + { + fprintf(stderr, "bmfont_enumerate: missing font %s\n", + bmfonts[i].filename); + return result_TEST_FAILED; + } + } + + /* stop-walk: callback returns result_STOP_WALK, enumerate still returns OK */ + memset(&chk, 0, sizeof(chk)); + chk.stop_after = 3; + rc = bmfont_enumerate(dir, bmfont_enum_cb, &chk); + if (rc != result_OK) + { + fprintf(stderr, "bmfont_enumerate stop-walk: rc %x\n", rc); + return result_TEST_FAILED; + } + if (chk.total != 3) + { + fprintf(stderr, "bmfont_enumerate stop-walk: ran %d times, expected 3\n", + chk.total); + return result_TEST_FAILED; + } + + /* missing directory */ + memset(&chk, 0, sizeof(chk)); + rc = bmfont_enumerate("no/such/dir/here", bmfont_enum_cb, &chk); + if (rc != result_FILE_NOT_FOUND) + { + fprintf(stderr, "bmfont_enumerate bad dir: rc %x, expected %x\n", + rc, result_FILE_NOT_FOUND); + return result_TEST_FAILED; + } + + /* NULL arguments */ + if (bmfont_enumerate(NULL, bmfont_enum_cb, &chk) != result_NULL_ARG || + bmfont_enumerate(dir, NULL, &chk) != result_NULL_ARG) + { + fprintf(stderr, "bmfont_enumerate: NULL arg not rejected\n"); + return result_TEST_FAILED; + } + + return result_TEST_PASSED; +} + +/* ----------------------------------------------------------------------- */ + result_t bmfont_test_one_format(const char *resources, int scr_width, int scr_height, @@ -838,6 +942,10 @@ result_t bmfont_test(const char *resources) result_t rc; int i; + rc = bmfont_enumerate_test(resources); + if (rc != result_TEST_PASSED) + return rc; + for (i = 0; i < NELEMS(tab); i++) { rc = bmfont_test_one_format(resources, From ff4792cee26ae95c5ae8bb12f8d18b9a8baa13f8 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Thu, 3 Sep 2026 12:17:21 +0100 Subject: [PATCH 214/287] feat(wuss): add shared task components with a FontMenu Opens a space for wuss components that tasks share, the RISC OS Toolbox analogue. Gated on the new WUSS_COMPONENTS option (implies WUSS_MENUS). First component: wuss_fontmenu, a pop-up menu of the bitmap fonts in a directory (via bmfont_enumerate). It owns a plain wuss_menu_t -- a flat, name-sorted list of leaf items -- so a task opens it with wuss_menu_open and calls wuss_fontmenu_selected in its wuss_EVENT_MENU_SELECT case to recover the picked font name without indexing menu->items by hand. Test covers the menu shape, the resolver's happy path and its three decline paths, and a missing directory. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- CMakeLists.txt | 16 +++ include/wuss/component/fontmenu.h | 89 ++++++++++++ libraries/wuss/component/fontmenu.c | 209 ++++++++++++++++++++++++++++ libraries/wuss/test/wuss-test.c | 69 +++++++++ 4 files changed, 383 insertions(+) create mode 100644 include/wuss/component/fontmenu.h create mode 100644 libraries/wuss/component/fontmenu.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 9e399efb..20443200 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -33,6 +33,7 @@ option(DPTLIB_IMAGES_READ_ONLY "Remove libpng write support" OFF) option(WUSS_FURNITURE "Build the Wuss window-furniture subsystem" ON) option(WUSS_ICONS "Build the Wuss in-content icon subsystem" ON) option(WUSS_MENUS "Build the Wuss pop-up menu helper (implies WUSS_ICONS)" ON) +option(WUSS_COMPONENTS "Build the Wuss shared task components (implies WUSS_MENUS)" ON) # Referencing CMAKE_TOOLCHAIN_FILE avoids a warning on rebuilds. if(NOT ${CMAKE_TOOLCHAIN_FILE} STREQUAL "") @@ -127,6 +128,9 @@ endif() if(WUSS_MENUS) list(APPEND PUBLIC_HEADERS include/wuss/menu.h) endif() +if(WUSS_COMPONENTS) + list(APPEND PUBLIC_HEADERS include/wuss/component/fontmenu.h) +endif() # The public headers must be set as properties of the library, not as # target_sources. The quoting is essential. @@ -422,10 +426,16 @@ set(WUSS_MENU_SOURCES libraries/wuss/menu/create-from-desc.c libraries/wuss/menu.h) +set(WUSS_COMPONENT_SOURCES + libraries/wuss/component/fontmenu.c) + set(WUSS_SOURCES ${WUSS_CORE_SOURCES}) if(WUSS_FURNITURE) list(APPEND WUSS_SOURCES ${WUSS_FURNITURE_SOURCES}) endif() +if(WUSS_COMPONENTS) + set(WUSS_MENUS ON) # the shared components build on the menu helper +endif() if(WUSS_MENUS) set(WUSS_ICONS ON) # the menu helper needs the icon subsystem endif() @@ -435,6 +445,9 @@ endif() if(WUSS_MENUS) list(APPEND WUSS_SOURCES ${WUSS_MENU_SOURCES}) endif() +if(WUSS_COMPONENTS) + list(APPEND WUSS_SOURCES ${WUSS_COMPONENT_SOURCES}) +endif() set(ALL_SOURCES ${PUBLIC_HEADERS} @@ -496,6 +509,9 @@ endif() if(WUSS_MENUS) target_compile_definitions(DPTLib PUBLIC WUSS_MENUS) endif() +if(WUSS_COMPONENTS) + target_compile_definitions(DPTLib PUBLIC WUSS_COMPONENTS) +endif() if(NOT TARGET_RISCOS) set(CMAKE_FIND_FRAMEWORK NEVER) diff --git a/include/wuss/component/fontmenu.h b/include/wuss/component/fontmenu.h new file mode 100644 index 00000000..27d88461 --- /dev/null +++ b/include/wuss/component/fontmenu.h @@ -0,0 +1,89 @@ +/* wuss/component/fontmenu.h -- a menu of the available bitmap fonts */ + +/** + * \file fontmenu.h + * + * A shared wuss component: a pop-up menu listing the bitmap fonts in a + * directory, the RISC OS Toolbox FontMenu in miniature. + * + * A wuss_fontmenu owns a plain wuss_menu_t (see wuss/menu.h) built from + * bmfont_enumerate: one leaf item per font, alphabetically sorted, its label + * the font's leafname sans ".png". A task opens it with wuss_menu_open and, + * in its wuss_EVENT_MENU_SELECT case, calls wuss_fontmenu_selected to turn + * the event back into a font name -- no indexing into menu->items by hand. + * + * Built only when WUSS_COMPONENTS is defined (which implies WUSS_MENUS). + */ + +#ifndef WUSS_COMPONENT_FONTMENU_H +#define WUSS_COMPONENT_FONTMENU_H + +#ifdef __cplusplus +extern "C" +{ +#endif + +#include "base/result.h" + +#include "wuss/menu.h" +#include "wuss/task.h" + +/* ----------------------------------------------------------------------- */ + +/** Opaque handle: owns the menu tree and the font-name strings in it. */ +typedef struct wuss_fontmenu wuss_fontmenu_t; + +/** + * Build a font menu from the ".png" fonts in \p dir (see bmfont_enumerate). + * Items are sorted by name; \p title is the menu's caption (NULL -> "Font"). + * + * \param[out] out Filled with the new handle on success, untouched on + * failure. + * \param[in] dir Directory to scan for fonts. + * \param[in] title Menu caption, borrowed and copied; NULL for "Font". + * \return \ref result_OK, \ref result_OOM, \ref result_NULL_ARG, or \ref + * result_FILE_NOT_FOUND if \p dir cannot be opened. An empty + * directory still succeeds, yielding a menu with no items. + */ +result_t wuss_fontmenu_create(wuss_fontmenu_t **out, + const char *dir, + const char *title); + +/** + * Free a font menu and every string in it. Any open menu chain showing it + * must be closed first (wuss_menu_close). Safe to pass NULL. + * + * \param[in] doomed Handle to free, or NULL. + */ +void wuss_fontmenu_destroy(wuss_fontmenu_t *doomed); + +/** + * The menu to hand to wuss_menu_open. Borrowed; valid until + * wuss_fontmenu_destroy. NULL only if \p fm is NULL. + * + * \param[in] fm Handle. + * \return The menu, or NULL. + */ +const wuss_menu_t *wuss_fontmenu_menu(const wuss_fontmenu_t *fm); + +/** + * Resolve a wuss_EVENT_MENU_SELECT event to the picked font's name. + * + * Call from the task's wuss_EVENT_MENU_SELECT case. Returns NULL -- not a + * match -- unless \p ev is a MENU_SELECT whose menu is this fontmenu's own; + * so a task multiplexing several menus through one handle can call each + * fontmenu's resolver in turn. + * + * \param[in] fm Handle. + * \param[in] ev The event passed to the task's handle callback. + * \return The selected font's name (borrowed, valid until + * wuss_fontmenu_destroy), or NULL if \p ev is not this fontmenu's. + */ +const char *wuss_fontmenu_selected(const wuss_fontmenu_t *fm, + const wuss_event_t *ev); + +#ifdef __cplusplus +} +#endif + +#endif /* WUSS_COMPONENT_FONTMENU_H */ diff --git a/libraries/wuss/component/fontmenu.c b/libraries/wuss/component/fontmenu.c new file mode 100644 index 00000000..9932ae8f --- /dev/null +++ b/libraries/wuss/component/fontmenu.c @@ -0,0 +1,209 @@ +/* wuss/component/fontmenu.c -- a menu of the available bitmap fonts */ + +#include <stdlib.h> +#include <string.h> + +#ifdef FORTIFY +#include "fortify/fortify.h" +#endif + +#include "base/result.h" +#include "framebuf/bmfont.h" +#include "utils/array.h" + +#include "wuss/menu.h" +#include "wuss/task.h" + +#include "wuss/component/fontmenu.h" + +/* ----------------------------------------------------------------------- */ + +/* ponytail: flat list, one item per font, no family/weight grouping into + * submenus. wuss_fontmenu_selected then needs only a single pointer compare. + * Add a grouping pass (and widen the identity check to the submenus) if a + * font directory ever gets big enough to want it. */ +struct wuss_fontmenu +{ + wuss_menu_t *menu; /* owned; freed by wuss_menu_destroy */ +}; + +/* Growable list of strdup'd font names, filled by the enumerate callback. */ +typedef struct namelist +{ + char **names; + int n; + int cap; + int oom; /* a strdup/grow failed; stop and unwind */ +} +namelist_t; + +static result_t collect_name(const char *name, + const char *path, + void *opaque) +{ + namelist_t *nl = opaque; + char *copy; + + (void) path; + + if (array_grow((void **) &nl->names, sizeof(*nl->names), + nl->n, &nl->cap, 1, 8)) + { + nl->oom = 1; + return result_OOM; + } + + copy = strdup(name); + if (copy == NULL) + { + nl->oom = 1; + return result_OOM; + } + + nl->names[nl->n++] = copy; + return result_OK; +} + +static void namelist_free(namelist_t *nl) +{ + int i; + + for (i = 0; i < nl->n; i++) + free(nl->names[i]); + free(nl->names); + nl->names = NULL; + nl->n = nl->cap = 0; +} + +static int name_cmp(const void *a, const void *b) +{ + return strcmp(*(const char *const *) a, *(const char *const *) b); +} + +/* ----------------------------------------------------------------------- */ + +/* Build a flat wuss_menu_t whose items[i].text is names[i], transferring + * ownership of each string. On failure every string is freed and *out + * untouched. */ +static result_t build_menu(char **names, + int nnames, + const char *title, + wuss_menu_t **out) +{ + wuss_menu_t *m; + wuss_menu_item_t *items; + int i; + + m = malloc(sizeof(*m)); + if (m == NULL) + return result_OOM; + + items = (nnames > 0) ? calloc((size_t) nnames, sizeof(*items)) : NULL; + if (nnames > 0 && items == NULL) + { + free(m); + return result_OOM; + } + + m->title = strdup(title ? title : "Font"); + if (m->title == NULL) + { + free(items); + free(m); + return result_OOM; + } + + for (i = 0; i < nnames; i++) + { + items[i].text = names[i]; /* ownership transferred */ + items[i].flags = wuss_MENU_ITEM_NONE; + items[i].submenu = NULL; + items[i].window = NULL; + } + + m->items = items; + m->nitems = nnames; + + *out = m; + return result_OK; +} + +/* ----------------------------------------------------------------------- */ + +result_t wuss_fontmenu_create(wuss_fontmenu_t **out, + const char *dir, + const char *title) +{ + namelist_t nl; + wuss_fontmenu_t *fm; + result_t rc; + + if (out == NULL || dir == NULL) + return result_NULL_ARG; + + memset(&nl, 0, sizeof(nl)); + + rc = bmfont_enumerate(dir, collect_name, &nl); + if (rc != result_OK) + { + namelist_free(&nl); + return nl.oom ? result_OOM : rc; + } + + if (nl.n > 1) + qsort(nl.names, (size_t) nl.n, sizeof(nl.names[0]), name_cmp); + + fm = malloc(sizeof(*fm)); + if (fm == NULL) + { + namelist_free(&nl); + return result_OOM; + } + + rc = build_menu(nl.names, nl.n, title, &fm->menu); + if (rc != result_OK) + { + namelist_free(&nl); /* build_menu took nothing on failure */ + free(fm); + return rc; + } + + /* build_menu adopted every string; drop the array, keep the pointers. */ + free(nl.names); + + *out = fm; + return result_OK; +} + +void wuss_fontmenu_destroy(wuss_fontmenu_t *doomed) +{ + if (doomed == NULL) + return; + + wuss_menu_destroy(doomed->menu); + free(doomed); +} + +const wuss_menu_t *wuss_fontmenu_menu(const wuss_fontmenu_t *fm) +{ + return fm ? fm->menu : NULL; +} + +const char *wuss_fontmenu_selected(const wuss_fontmenu_t *fm, + const wuss_event_t *ev) +{ + int index; + + if (fm == NULL || ev == NULL) + return NULL; + if (ev->kind != wuss_EVENT_MENU_SELECT) + return NULL; + if (ev->data.menu_select.menu != fm->menu) + return NULL; + + index = ev->data.menu_select.index; + if (index < 0 || index >= fm->menu->nitems) + return NULL; + + return fm->menu->items[index].text; +} diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index a5a91afa..e02f7e91 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -25,6 +25,9 @@ #ifdef WUSS_MENUS #include "wuss/menu.h" #endif +#ifdef WUSS_COMPONENTS +#include "wuss/component/fontmenu.h" +#endif /* white-box: the menu-flash test drives picks through the icon layer and * reads back struct wuss__menu / struct wuss_icon state directly */ @@ -3674,6 +3677,72 @@ result_t wuss_test(const char *resources) MenuOK: ; } +#ifdef WUSS_COMPONENTS + printf("test: wuss_fontmenu lists the bmfonts dir and resolves a pick\n"); + { + const char *dir; + wuss_fontmenu_t *fm; + const wuss_menu_t *fmm; + wuss_event_t ev; + const char *name; + int i; + + dir = path_join_filename(resources, 2, "resources", "bmfonts"); + + rc = wuss_fontmenu_create(&fm, dir, "Font"); + if (rc != result_OK) + goto FontMenuFail; + + fmm = wuss_fontmenu_menu(fm); + if (fmm == NULL) goto FontMenuFail; + if (fmm->title == NULL || strcmp(fmm->title, "Font") != 0) + goto FontMenuFail; + if (fmm->nitems < 2) goto FontMenuFail; + + /* items sorted ascending, every row a plain leaf */ + for (i = 0; i < fmm->nitems; i++) + { + if (fmm->items[i].text == NULL) goto FontMenuFail; + if (fmm->items[i].submenu != NULL) goto FontMenuFail; + if (i > 0 && strcmp(fmm->items[i - 1].text, fmm->items[i].text) >= 0) + goto FontMenuFail; + } + + /* a MENU_SELECT for this menu resolves to the row's label */ + ev.kind = wuss_EVENT_MENU_SELECT; + ev.data.menu_select.menu = fmm; + ev.data.menu_select.index = 1; + ev.data.menu_select.button = wuss_BUTTON_SELECT; + name = wuss_fontmenu_selected(fm, &ev); + if (name == NULL || strcmp(name, fmm->items[1].text) != 0) + goto FontMenuFail; + + /* wrong event kind, foreign menu and out-of-range index all decline */ + ev.kind = wuss_EVENT_IDLE; + if (wuss_fontmenu_selected(fm, &ev) != NULL) goto FontMenuFail; + ev.kind = wuss_EVENT_MENU_SELECT; + ev.data.menu_select.menu = NULL; + if (wuss_fontmenu_selected(fm, &ev) != NULL) goto FontMenuFail; + ev.data.menu_select.menu = fmm; + ev.data.menu_select.index = fmm->nitems; + if (wuss_fontmenu_selected(fm, &ev) != NULL) goto FontMenuFail; + + wuss_fontmenu_destroy(fm); + + /* missing directory is surfaced, not swallowed */ + rc = wuss_fontmenu_create(&fm, "no/such/dir/here", NULL); + if (rc != result_FILE_NOT_FOUND) goto FontMenuFail; + + rc = result_OK; + goto FontMenuOK; + +FontMenuFail: + printf("wuss_test: fontmenu check failed\n"); + return result_TEST_FAILED; +FontMenuOK: ; + } +#endif /* WUSS_COMPONENTS */ + #ifdef WUSS_ICONS printf("test: menu pick flashes then delivers MENU_SELECT; fast ADJUST " "re-picks do not lose a click or leave a row inverted\n"); From 25139c60fb37e416c85fcd9eb5db5c0d6eb6f2aa Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Thu, 3 Sep 2026 12:40:00 +0100 Subject: [PATCH 215/287] feat(wuss): add wuss_colourmenu component backed by menu swatch rows Extend the menu/icon layer with an optional colour chip per row (wuss_MENU_ITEM_SWATCH / wuss_ICON_FLAGS_SWATCH + a wuss_colour_t swatch field), drawn in the left gutter where the tick sits; the chip wins over a selected tick. RULE specs and the desc-builder init the new field. Ship wuss_colourmenu (include/wuss/component/colourmenu.h, libraries/wuss/component/colourmenu.c): a shared component, Toolbox ColourMenu in miniature, that builds a flat wuss_menu_t with one swatch row per system-palette entry (label #RRGGBB) and resolves a wuss_EVENT_MENU_SELECT back to the picked palette index. Wired into WUSS_COMPONENT_SOURCES and PUBLIC_HEADERS; covered by a new wuss-test.c block. 24/24 tests pass. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- CMakeLists.txt | 4 +- include/wuss/component/colourmenu.h | 94 +++++++++++++++++ include/wuss/icon.h | 10 +- include/wuss/menu.h | 9 +- libraries/wuss/component/colourmenu.c | 141 +++++++++++++++++++++++++ libraries/wuss/icon.h | 2 + libraries/wuss/icon/create.c | 12 ++- libraries/wuss/icon/draw.c | 16 ++- libraries/wuss/menu/create-from-desc.c | 4 +- libraries/wuss/menu/menu.c | 5 + libraries/wuss/test/wuss-test.c | 65 ++++++++++++ 11 files changed, 355 insertions(+), 7 deletions(-) create mode 100644 include/wuss/component/colourmenu.h create mode 100644 libraries/wuss/component/colourmenu.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 20443200..d9fa660f 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -130,6 +130,7 @@ if(WUSS_MENUS) endif() if(WUSS_COMPONENTS) list(APPEND PUBLIC_HEADERS include/wuss/component/fontmenu.h) + list(APPEND PUBLIC_HEADERS include/wuss/component/colourmenu.h) endif() # The public headers must be set as properties of the library, not as @@ -427,7 +428,8 @@ set(WUSS_MENU_SOURCES libraries/wuss/menu.h) set(WUSS_COMPONENT_SOURCES - libraries/wuss/component/fontmenu.c) + libraries/wuss/component/fontmenu.c + libraries/wuss/component/colourmenu.c) set(WUSS_SOURCES ${WUSS_CORE_SOURCES}) if(WUSS_FURNITURE) diff --git a/include/wuss/component/colourmenu.h b/include/wuss/component/colourmenu.h new file mode 100644 index 00000000..2640d11d --- /dev/null +++ b/include/wuss/component/colourmenu.h @@ -0,0 +1,94 @@ +/* wuss/component/colourmenu.h -- a menu of the system palette colours */ + +/** + * \file colourmenu.h + * + * A shared wuss component: a pop-up menu with one row per system-palette + * entry, each row carrying a colour chip (see wuss_MENU_ITEM_SWATCH) -- the + * RISC OS Toolbox ColourMenu in miniature. + * + * A wuss_colourmenu owns a plain wuss_menu_t (see wuss/menu.h): item i is + * palette index i, its swatch that colour, its label a "#RRGGBB" hex string. + * A task opens it with wuss_menu_open and, in its wuss_EVENT_MENU_SELECT + * case, calls wuss_colourmenu_selected to turn the event back into a + * wuss_colour_t palette index. + * + * Built only when WUSS_COMPONENTS is defined (which implies WUSS_MENUS). + */ + +#ifndef WUSS_COMPONENT_COLOURMENU_H +#define WUSS_COMPONENT_COLOURMENU_H + +#ifdef __cplusplus +extern "C" +{ +#endif + +#include "base/result.h" + +#include "wuss/menu.h" +#include "wuss/task.h" +#include "wuss/wuss.h" + +/* ----------------------------------------------------------------------- */ + +/** Opaque handle: owns the menu tree and the hex-label strings in it. */ +typedef struct wuss_colourmenu wuss_colourmenu_t; + +/** + * Build a colour menu covering \p wuss's whole system palette: one row per + * entry, index order, each with its colour chip and a "#RRGGBB" label. + * + * \param[out] out Filled with the new handle on success, untouched on + * failure. + * \param[in] wuss Owner; its palette is read now (not retained). The menu + * outlives changes to the palette -- rebuild it after a + * wuss_set_palette if the rows should follow. + * \param[in] title Menu caption, borrowed and copied; NULL for "Colour". + * \return \ref result_OK, \ref result_OOM, or \ref result_NULL_ARG. + */ +result_t wuss_colourmenu_create(wuss_colourmenu_t **out, + const wuss_t *wuss, + const char *title); + +/** + * Free a colour menu and every string in it. Any open menu chain showing it + * must be closed first (wuss_menu_close). Safe to pass NULL. + * + * \param[in] doomed Handle to free, or NULL. + */ +void wuss_colourmenu_destroy(wuss_colourmenu_t *doomed); + +/** + * The menu to hand to wuss_menu_open. Borrowed; valid until + * wuss_colourmenu_destroy. NULL only if \p cm is NULL. + * + * \param[in] cm Handle. + * \return The menu, or NULL. + */ +const wuss_menu_t *wuss_colourmenu_menu(const wuss_colourmenu_t *cm); + +/** + * Resolve a wuss_EVENT_MENU_SELECT event to the picked palette index. + * + * Call from the task's wuss_EVENT_MENU_SELECT case. Returns 0 with \p ok + * cleared -- not a match -- unless \p ev is a MENU_SELECT whose menu is this + * colourmenu's own; so a task multiplexing several menus through one handle + * can call each resolver in turn. + * + * \param[in] cm Handle. + * \param[in] ev The event passed to the task's handle callback. + * \param[out] ok Set non-zero if \p ev was this colourmenu's, else zero. May + * be NULL. + * \return The selected palette index, or 0 if \p ev is not this colourmenu's + * (check \p ok to tell that from a real index 0). + */ +wuss_colour_t wuss_colourmenu_selected(const wuss_colourmenu_t *cm, + const wuss_event_t *ev, + int *ok); + +#ifdef __cplusplus +} +#endif + +#endif /* WUSS_COMPONENT_COLOURMENU_H */ diff --git a/include/wuss/icon.h b/include/wuss/icon.h index 39636e8b..696b74f6 100644 --- a/include/wuss/icon.h +++ b/include/wuss/icon.h @@ -135,7 +135,11 @@ typedef enum wuss_icon_flags * normal interactive row with its own label; the dashed rule above it is * laid out and drawn as a separate wuss_ICON_TYPE_RULE icon. Ignored by * other types. */ - wuss_ICON_FLAGS_SEPARATOR = 1 << 7 + wuss_ICON_FLAGS_SEPARATOR = 1 << 7, + /** wuss_ICON_TYPE_MENU_ENTRY: draw a small colour chip (spec.swatch) in the + * row's left gutter, where the tick would sit. Mutually exclusive with a + * selected tick -- the chip wins. Ignored by other types. */ + wuss_ICON_FLAGS_SWATCH = 1 << 8 } wuss_icon_flags_t; @@ -173,6 +177,10 @@ typedef struct wuss_icon_spec * (the default) means "no group": such a radio still toggles but never * clears another. Ignored by all other icon types. */ int group; + /** wuss_ICON_TYPE_MENU_ENTRY with wuss_ICON_FLAGS_SWATCH: the colour chip to + * 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; /** Appearance/behaviour flags. */ wuss_icon_flags_t flags; } diff --git a/include/wuss/menu.h b/include/wuss/menu.h index 6b8b07b4..d86b5b88 100644 --- a/include/wuss/menu.h +++ b/include/wuss/menu.h @@ -43,8 +43,10 @@ typedef enum wuss_menu_item_flags * its label and responds to the pointer. * The rule is laid out and drawn * separately and is not interactive */ - wuss_MENU_ITEM_DISABLED = 1 << 2 /**< greyed, never highlights, not + wuss_MENU_ITEM_DISABLED = 1 << 2, /**< greyed, never highlights, not * selectable */ + wuss_MENU_ITEM_SWATCH = 1 << 3 /**< draw a colour chip of \c swatch at the + * item's left edge, in place of a tick */ } wuss_menu_item_flags_t; @@ -65,6 +67,11 @@ typedef struct wuss_menu_item * must outlive the open chain -- do not * wuss_window_close it while its menu is * open. */ + wuss_colour_t swatch; /**< with wuss_MENU_ITEM_SWATCH: the colour + * chip to draw at the row's left edge, as + * an index into the system palette. + * Ignored without that flag, so a + * zero-initialised item is unaffected. */ } wuss_menu_item_t; diff --git a/libraries/wuss/component/colourmenu.c b/libraries/wuss/component/colourmenu.c new file mode 100644 index 00000000..41167f2f --- /dev/null +++ b/libraries/wuss/component/colourmenu.c @@ -0,0 +1,141 @@ +/* wuss/component/colourmenu.c -- a menu of the system palette colours */ + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#ifdef FORTIFY +#include "fortify/fortify.h" +#endif + +#include "base/result.h" +#include "framebuf/colour.h" +#include "framebuf/pixelfmt.h" + +#include "wuss/menu.h" +#include "wuss/task.h" + +#include "wuss/component/colourmenu.h" + +#include "../impl.h" + +/* ----------------------------------------------------------------------- */ + +/* ponytail: flat list, one item per palette entry, index order. No grouping + * of near colours, no "recent" section. wuss_colourmenu_selected is then a + * single pointer compare plus the item index. */ +struct wuss_colourmenu +{ + wuss_menu_t *menu; /* owned; freed by wuss_menu_destroy */ +}; + +result_t wuss_colourmenu_create(wuss_colourmenu_t **out, + const wuss_t *wuss, + const char *title) +{ + wuss_colourmenu_t *cm; + wuss_menu_t *m; + wuss_menu_item_t *items; + int n; + int i; + + if (out == NULL || wuss == NULL) + return result_NULL_ARG; + + n = wuss->npalette; + + cm = malloc(sizeof(*cm)); + if (cm == NULL) + return result_OOM; + + m = calloc(1, sizeof(*m)); + if (m == NULL) + { + free(cm); + return result_OOM; + } + + items = (n > 0) ? calloc((size_t) n, sizeof(*items)) : NULL; + if (n > 0 && items == NULL) + { + free(m); + free(cm); + return result_OOM; + } + + m->title = strdup(title ? title : "Colour"); + m->items = items; + m->nitems = n; /* items[i].text/submenu all NULL: safe for wuss_menu_destroy */ + if (m->title == NULL) + { + wuss_menu_destroy(m); + free(cm); + return result_OOM; + } + + for (i = 0; i < n; i++) + { + pixelfmt_rgba8888_t px; + char label[8]; + + px = wuss->palette[i].primary; + snprintf(label, sizeof(label), "#%02X%02X%02X", + PIXELFMT_Rxxx8888(px), + PIXELFMT_xGxx8888(px), + PIXELFMT_xxBx8888(px)); + + items[i].text = strdup(label); + if (items[i].text == NULL) + { + wuss_menu_destroy(m); + free(cm); + return result_OOM; + } + + items[i].flags = wuss_MENU_ITEM_SWATCH; + items[i].swatch = (wuss_colour_t) i; + } + + cm->menu = m; + *out = cm; + return result_OK; +} + +void wuss_colourmenu_destroy(wuss_colourmenu_t *doomed) +{ + if (doomed == NULL) + return; + + wuss_menu_destroy(doomed->menu); + free(doomed); +} + +const wuss_menu_t *wuss_colourmenu_menu(const wuss_colourmenu_t *cm) +{ + return cm ? cm->menu : NULL; +} + +wuss_colour_t wuss_colourmenu_selected(const wuss_colourmenu_t *cm, + const wuss_event_t *ev, + int *ok) +{ + int index; + + if (ok != NULL) + *ok = 0; + + if (cm == NULL || ev == NULL) + return 0; + if (ev->kind != wuss_EVENT_MENU_SELECT) + return 0; + if (ev->data.menu_select.menu != cm->menu) + return 0; + + index = ev->data.menu_select.index; + if (index < 0 || index >= cm->menu->nitems) + return 0; + + if (ok != NULL) + *ok = 1; + return cm->menu->items[index].swatch; +} diff --git a/libraries/wuss/icon.h b/libraries/wuss/icon.h index 366aa84f..70955ae6 100644 --- a/libraries/wuss/icon.h +++ b/libraries/wuss/icon.h @@ -34,6 +34,8 @@ struct wuss_icon screen_pattern_t pattern; /* wuss_ICON_TYPE_PATTERN tile; 0 otherwise */ const bitmap_t *bitmap; /* wuss_ICON_TYPE_BITMAP image; borrowed, or NULL */ 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_flags_t flags; wuss_icon_state_t state; }; diff --git a/libraries/wuss/icon/create.c b/libraries/wuss/icon/create.c index 4d163d59..b4145b2b 100644 --- a/libraries/wuss/icon/create.c +++ b/libraries/wuss/icon/create.c @@ -20,7 +20,8 @@ result_t wuss_icon_create(wuss_window_t *window, const char *src; size_t len; int newcap; - wuss_colour_t fg, bg; + wuss_colour_t fg, bg, swatch; + int has_swatch; assert(window != NULL); assert(spec != NULL); @@ -30,6 +31,11 @@ result_t wuss_icon_create(wuss_window_t *window, fg = wuss__resolve_colour(w, spec->fg); bg = wuss__resolve_colour(w, spec->bg); + has_swatch = (spec->type == wuss_ICON_TYPE_MENU_ENTRY) && + (spec->flags & wuss_ICON_FLAGS_SWATCH); + swatch = has_swatch ? wuss__resolve_colour(w, spec->swatch) + : wuss_NO_BACKGROUND; + switch (spec->type) { case wuss_ICON_TYPE_LABEL: @@ -66,6 +72,9 @@ result_t wuss_icon_create(wuss_window_t *window, (bg < 0 || bg >= w->npalette)) return result_WUSS_BAD_COLOUR; + if (has_swatch && (swatch < 0 || swatch >= w->npalette)) + return result_WUSS_BAD_COLOUR; + if (window->nicons == window->cap_icons) { newcap = (window->cap_icons == 0) ? 4 : window->cap_icons * 2; @@ -99,6 +108,7 @@ result_t wuss_icon_create(wuss_window_t *window, : screen_PATTERN_SOLID; it->bitmap = (spec->type == wuss_ICON_TYPE_BITMAP) ? spec->bitmap : NULL; it->group = (spec->type == wuss_ICON_TYPE_RADIO) ? spec->group : 0; + it->swatch = swatch; it->flags = spec->flags; it->state = wuss_ICON_STATE_NONE; diff --git a/libraries/wuss/icon/draw.c b/libraries/wuss/icon/draw.c index 214a077c..39e2937a 100644 --- a/libraries/wuss/icon/draw.c +++ b/libraries/wuss/icon/draw.c @@ -379,8 +379,20 @@ static void wuss__icon_draw_menu_entry(const icon_draw_ctx_t *c) screen_fill_rect(c->scr, b->x0, b->y0, SIZE2D(b->x1 - b->x0, b->y1 - b->y0), ground); - /* left-edge tick when selected */ - if (wuss__icon_selected(icon)) + /* left-edge colour chip (wins over the tick) or tick when selected */ + if ((icon->flags & wuss_ICON_FLAGS_SWATCH) && + icon->swatch != wuss_NO_BACKGROUND) + { + int cx, cy, h; + + h = (c->font_height >= 8) ? c->font_height : 8; + cx = b->x0 + pad; + cy = b->y0 + (b->y1 - b->y0 - h) / 2; + screen_fill_rect(c->scr, cx, cy, SIZE2D(h, h), + c->wuss->palette[icon->swatch]); + screen_draw_rect(c->scr, cx, cy, SIZE2D(h, h), ink); /* 1px border */ + } + else if (wuss__icon_selected(icon)) { int cx, cy, h; diff --git a/libraries/wuss/menu/create-from-desc.c b/libraries/wuss/menu/create-from-desc.c index 74dc441c..46787f11 100644 --- a/libraries/wuss/menu/create-from-desc.c +++ b/libraries/wuss/menu/create-from-desc.c @@ -170,6 +170,7 @@ static result_t menu_deep_copy(const wuss_menu_t *src, wuss_menu_t **out) items[i].flags = src->items[i].flags; items[i].submenu = NULL; items[i].window = src->items[i].window; /* borrowed, as in the source */ + items[i].swatch = src->items[i].swatch; if (items[i].text == NULL) { @@ -230,7 +231,8 @@ static result_t build_emit(Building *b, it->text = copy; it->flags = flags; it->submenu = submenu; - it->window = NULL; /* array_grow leaves this uninitialised otherwise */ + it->window = NULL; /* array_grow leaves these uninitialised otherwise */ + it->swatch = wuss_NO_BACKGROUND; return result_OK; } diff --git a/libraries/wuss/menu/menu.c b/libraries/wuss/menu/menu.c index e68f8d2e..a3e78538 100644 --- a/libraries/wuss/menu/menu.c +++ b/libraries/wuss/menu/menu.c @@ -582,6 +582,7 @@ static result_t wuss__menu_spawn(wuss_t *wuss, specs[s].text = ""; specs[s].fg = wuss_COLOUR_BLACK; specs[s].bg = wuss_NO_BACKGROUND; + specs[s].swatch = wuss_NO_BACKGROUND; specs[s].flags = wuss_ICON_FLAGS_NONE; s++; y += sep_h; @@ -591,6 +592,8 @@ static result_t wuss__menu_spawn(wuss_t *wuss, flags |= wuss_ICON_FLAGS_DISABLED; if (item->submenu != NULL || item->window != NULL) flags |= wuss_ICON_FLAGS_SUBMENU; + if (item->flags & wuss_MENU_ITEM_SWATCH) + flags |= wuss_ICON_FLAGS_SWATCH; specs[s].type = wuss_ICON_TYPE_MENU_ENTRY; specs[s].bbox.x0 = 0; @@ -600,6 +603,8 @@ static result_t wuss__menu_spawn(wuss_t *wuss, specs[s].text = item->text ? item->text : ""; specs[s].fg = wuss_COLOUR_BLACK; specs[s].bg = wuss_NO_BACKGROUND; + specs[s].swatch = (item->flags & wuss_MENU_ITEM_SWATCH) ? item->swatch + : wuss_NO_BACKGROUND; specs[s].flags = flags; s++; diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index e02f7e91..64aab779 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -27,6 +27,7 @@ #endif #ifdef WUSS_COMPONENTS #include "wuss/component/fontmenu.h" +#include "wuss/component/colourmenu.h" #endif /* white-box: the menu-flash test drives picks through the icon layer and @@ -3741,6 +3742,70 @@ MenuOK: ; return result_TEST_FAILED; FontMenuOK: ; } + + printf("test: wuss_colourmenu covers the palette and resolves a pick\n"); + { + wuss_colourmenu_t *cm; + const wuss_menu_t *cmm; + wuss_event_t ev; + wuss_colour_t picked; + int ok; + int i; + + rc = wuss_colourmenu_create(&cm, wuss, "Colour"); + if (rc != result_OK) + goto ColourMenuFail; + + cmm = wuss_colourmenu_menu(cm); + if (cmm == NULL) goto ColourMenuFail; + if (cmm->title == NULL || strcmp(cmm->title, "Colour") != 0) + goto ColourMenuFail; + if (cmm->nitems < 2) goto ColourMenuFail; + + /* one swatch row per palette index, in order */ + for (i = 0; i < cmm->nitems; i++) + { + if (cmm->items[i].text == NULL) goto ColourMenuFail; + if ((cmm->items[i].flags & wuss_MENU_ITEM_SWATCH) == 0) + goto ColourMenuFail; + if (cmm->items[i].swatch != (wuss_colour_t) i) goto ColourMenuFail; + } + + /* a MENU_SELECT for this menu resolves to the row's palette index */ + ev.kind = wuss_EVENT_MENU_SELECT; + ev.data.menu_select.menu = cmm; + ev.data.menu_select.index = 1; + ev.data.menu_select.button = wuss_BUTTON_SELECT; + ok = -1; + picked = wuss_colourmenu_selected(cm, &ev, &ok); + if (!ok || picked != (wuss_colour_t) 1) goto ColourMenuFail; + + /* wrong event kind, foreign menu and out-of-range index all decline */ + ev.kind = wuss_EVENT_IDLE; + if (wuss_colourmenu_selected(cm, &ev, &ok) != 0 || ok) + goto ColourMenuFail; + ev.kind = wuss_EVENT_MENU_SELECT; + ev.data.menu_select.menu = NULL; + if (wuss_colourmenu_selected(cm, &ev, &ok) != 0 || ok) + goto ColourMenuFail; + ev.data.menu_select.menu = cmm; + ev.data.menu_select.index = cmm->nitems; + if (wuss_colourmenu_selected(cm, &ev, &ok) != 0 || ok) + goto ColourMenuFail; + + wuss_colourmenu_destroy(cm); + + if (wuss_colourmenu_create(&cm, NULL, "Colour") != result_NULL_ARG) + goto ColourMenuFail; + + rc = result_OK; + goto ColourMenuOK; + +ColourMenuFail: + printf("wuss_test: colourmenu check failed\n"); + return result_TEST_FAILED; +ColourMenuOK: ; + } #endif /* WUSS_COMPONENTS */ #ifdef WUSS_ICONS From 456e2107b15ffaeb5a3d54bc515db24fe6d365e0 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Thu, 3 Sep 2026 12:45:57 +0100 Subject: [PATCH 216/287] refactor(wuss): route wuss_colourmenu allocation through wuss's allocator Every block the component owns (handle, menu node, items array, label strings) now goes through wuss__malloc/wuss__free instead of the libc malloc/calloc/strdup/free, so a caller-supplied allocator is honoured. The wuss_t is stashed in the handle for teardown, and a local menu_free walks the graph rather than wuss_menu_destroy, which uses plain free and would mismatch. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- libraries/wuss/component/colourmenu.c | 85 ++++++++++++++++++++------- 1 file changed, 65 insertions(+), 20 deletions(-) diff --git a/libraries/wuss/component/colourmenu.c b/libraries/wuss/component/colourmenu.c index 41167f2f..33fd86bd 100644 --- a/libraries/wuss/component/colourmenu.c +++ b/libraries/wuss/component/colourmenu.c @@ -1,7 +1,7 @@ /* wuss/component/colourmenu.c -- a menu of the system palette colours */ +#include <stddef.h> #include <stdio.h> -#include <stdlib.h> #include <string.h> #ifdef FORTIFY @@ -26,9 +26,40 @@ * single pointer compare plus the item index. */ struct wuss_colourmenu { - wuss_menu_t *menu; /* owned; freed by wuss_menu_destroy */ + const wuss_t *wuss; /* borrowed; supplies the allocator for teardown */ + wuss_menu_t *menu; /* owned; every block via wuss->alloc, see menu_free */ }; +/* Free a menu built here: text, items, title, node -- all through wuss's + * allocator, so wuss_menu_destroy (plain free) must not be used. Tolerates + * NULL text for partial unwinding. */ +static void menu_free(const wuss_t *wuss, wuss_menu_t *m) +{ + int i; + + if (m == NULL) + return; + + for (i = 0; i < m->nitems; i++) + wuss__free(wuss, (void *) m->items[i].text); + wuss__free(wuss, (void *) m->items); + wuss__free(wuss, (void *) m->title); + wuss__free(wuss, m); +} + +/* wuss__ has no strdup; malloc + copy. */ +static char *wuss__strdup(const wuss_t *wuss, const char *s) +{ + size_t len; + char *copy; + + len = strlen(s) + 1; + copy = wuss__malloc(wuss, len); + if (copy != NULL) + memcpy(copy, s, len); + return copy; +} + result_t wuss_colourmenu_create(wuss_colourmenu_t **out, const wuss_t *wuss, const char *title) @@ -39,37 +70,48 @@ result_t wuss_colourmenu_create(wuss_colourmenu_t **out, int n; int i; + items = NULL; + if (out == NULL || wuss == NULL) return result_NULL_ARG; n = wuss->npalette; - cm = malloc(sizeof(*cm)); + cm = wuss__malloc(wuss, sizeof(*cm)); if (cm == NULL) return result_OOM; + cm->wuss = wuss; + cm->menu = NULL; - m = calloc(1, sizeof(*m)); + m = wuss__malloc(wuss, sizeof(*m)); if (m == NULL) { - free(cm); + wuss__free(wuss, cm); return result_OOM; } + m->title = NULL; + m->items = NULL; + m->nitems = 0; - items = (n > 0) ? calloc((size_t) n, sizeof(*items)) : NULL; - if (n > 0 && items == NULL) + if (n > 0) { - free(m); - free(cm); - return result_OOM; + items = wuss__malloc(wuss, (size_t) n * sizeof(*items)); + if (items == NULL) + { + menu_free(wuss, m); + wuss__free(wuss, cm); + return result_OOM; + } + memset(items, 0, (size_t) n * sizeof(*items)); + m->items = items; + m->nitems = n; /* items zeroed: menu_free's NULL-text loop is safe now */ } - m->title = strdup(title ? title : "Colour"); - m->items = items; - m->nitems = n; /* items[i].text/submenu all NULL: safe for wuss_menu_destroy */ + m->title = wuss__strdup(wuss, title ? title : "Colour"); if (m->title == NULL) { - wuss_menu_destroy(m); - free(cm); + menu_free(wuss, m); + wuss__free(wuss, cm); return result_OOM; } @@ -84,11 +126,11 @@ result_t wuss_colourmenu_create(wuss_colourmenu_t **out, PIXELFMT_xGxx8888(px), PIXELFMT_xxBx8888(px)); - items[i].text = strdup(label); + items[i].text = wuss__strdup(wuss, label); if (items[i].text == NULL) { - wuss_menu_destroy(m); - free(cm); + menu_free(wuss, m); + wuss__free(wuss, cm); return result_OOM; } @@ -103,11 +145,14 @@ result_t wuss_colourmenu_create(wuss_colourmenu_t **out, void wuss_colourmenu_destroy(wuss_colourmenu_t *doomed) { + const wuss_t *wuss; + if (doomed == NULL) return; - wuss_menu_destroy(doomed->menu); - free(doomed); + wuss = doomed->wuss; + menu_free(wuss, doomed->menu); + wuss__free(wuss, doomed); } const wuss_menu_t *wuss_colourmenu_menu(const wuss_colourmenu_t *cm) From e542175de7a5b0fa08a7dfb288e77b4a748afc58 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Thu, 3 Sep 2026 12:49:15 +0100 Subject: [PATCH 217/287] refactor(wuss)!: give wuss_fontmenu_create an allocator param wuss_fontmenu_create takes a const wuss_alloc_t *alloc (NULL -> wuss_alloc) and routes every block the handle keeps -- handle, menu node, items array, title and label strings -- through those hooks instead of libc malloc/calloc/strdup/free, matching wuss_colourmenu and wuss_create. The hooks are copied into the handle for teardown, which now walks the graph with a local menu_free rather than wuss_menu_destroy (plain free). The transient bmfont_enumerate scratch list still uses stdlib: it never outlives the call and array_grow has no allocator seam. BREAKING CHANGE: wuss_fontmenu_create now takes a fourth argument. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- include/wuss/component/fontmenu.h | 16 +++- libraries/wuss/component/fontmenu.c | 120 ++++++++++++++++++++-------- libraries/wuss/test/wuss-test.c | 4 +- 3 files changed, 101 insertions(+), 39 deletions(-) diff --git a/include/wuss/component/fontmenu.h b/include/wuss/component/fontmenu.h index 27d88461..e0e6231b 100644 --- a/include/wuss/component/fontmenu.h +++ b/include/wuss/component/fontmenu.h @@ -27,10 +27,12 @@ extern "C" #include "wuss/menu.h" #include "wuss/task.h" +#include "wuss/wuss.h" /* ----------------------------------------------------------------------- */ -/** Opaque handle: owns the menu tree and the font-name strings in it. */ +/** Opaque handle: owns the menu tree and the font-name strings in it, + * allocated through the hooks passed to wuss_fontmenu_create. */ typedef struct wuss_fontmenu wuss_fontmenu_t; /** @@ -41,13 +43,19 @@ typedef struct wuss_fontmenu wuss_fontmenu_t; * failure. * \param[in] dir Directory to scan for fonts. * \param[in] title Menu caption, borrowed and copied; NULL for "Font". + * \param[in] alloc Allocator for every block the handle keeps, copied in; + * NULL selects \ref wuss_alloc (plain stdlib). Pass the + * same hooks given to wuss_create so the component and the + * window manager share a heap. The transient directory + * scan still uses stdlib. * \return \ref result_OK, \ref result_OOM, \ref result_NULL_ARG, or \ref * result_FILE_NOT_FOUND if \p dir cannot be opened. An empty * directory still succeeds, yielding a menu with no items. */ -result_t wuss_fontmenu_create(wuss_fontmenu_t **out, - const char *dir, - const char *title); +result_t wuss_fontmenu_create(wuss_fontmenu_t **out, + const char *dir, + const char *title, + const wuss_alloc_t *alloc); /** * Free a font menu and every string in it. Any open menu chain showing it diff --git a/libraries/wuss/component/fontmenu.c b/libraries/wuss/component/fontmenu.c index 9932ae8f..6efc351b 100644 --- a/libraries/wuss/component/fontmenu.c +++ b/libraries/wuss/component/fontmenu.c @@ -1,5 +1,6 @@ /* wuss/component/fontmenu.c -- a menu of the available bitmap fonts */ +#include <stddef.h> #include <stdlib.h> #include <string.h> @@ -13,6 +14,7 @@ #include "wuss/menu.h" #include "wuss/task.h" +#include "wuss/wuss.h" #include "wuss/component/fontmenu.h" @@ -24,10 +26,43 @@ * font directory ever gets big enough to want it. */ struct wuss_fontmenu { - wuss_menu_t *menu; /* owned; freed by wuss_menu_destroy */ + wuss_alloc_t alloc; /* copied hooks; every block below goes through these */ + wuss_menu_t *menu; /* owned; freed by menu_free, not wuss_menu_destroy */ }; -/* Growable list of strdup'd font names, filled by the enumerate callback. */ +/* wuss_alloc_t has no strdup; malloc + copy. */ +static char *alloc_strdup(const wuss_alloc_t *a, const char *s) +{ + size_t len; + char *copy; + + len = strlen(s) + 1; + copy = a->malloc(len); + if (copy != NULL) + memcpy(copy, s, len); + return copy; +} + +/* Free a menu built here -- text, items, title, node -- through the same + * hooks it was built with. Tolerates NULL text for partial unwinding. */ +static void menu_free(const wuss_alloc_t *a, wuss_menu_t *m) +{ + int i; + + if (m == NULL) + return; + + for (i = 0; i < m->nitems; i++) + a->free((void *) m->items[i].text); + a->free((void *) m->items); + a->free((void *) m->title); + a->free(m); +} + +/* ----------------------------------------------------------------------- */ + +/* Growable list of strdup'd font names, filled by the enumerate callback. + * A transient scratch buffer: plain stdlib, freed before create returns. */ typedef struct namelist { char **names; @@ -82,57 +117,71 @@ static int name_cmp(const void *a, const void *b) /* ----------------------------------------------------------------------- */ -/* Build a flat wuss_menu_t whose items[i].text is names[i], transferring - * ownership of each string. On failure every string is freed and *out - * untouched. */ -static result_t build_menu(char **names, - int nnames, - const char *title, - wuss_menu_t **out) +/* Build a flat wuss_menu_t through \p a, copying each names[i] in (the + * scratch list keeps its own copies). On failure everything taken is freed + * through \p a and *out is untouched. */ +static result_t build_menu(const wuss_alloc_t *a, + char **names, + int nnames, + const char *title, + wuss_menu_t **out) { wuss_menu_t *m; wuss_menu_item_t *items; int i; - m = malloc(sizeof(*m)); + items = NULL; + + m = a->malloc(sizeof(*m)); if (m == NULL) return result_OOM; + m->title = NULL; + m->items = NULL; + m->nitems = 0; - items = (nnames > 0) ? calloc((size_t) nnames, sizeof(*items)) : NULL; - if (nnames > 0 && items == NULL) + if (nnames > 0) { - free(m); - return result_OOM; + items = a->malloc((size_t) nnames * sizeof(*items)); + if (items == NULL) + { + menu_free(a, m); + return result_OOM; + } + memset(items, 0, (size_t) nnames * sizeof(*items)); + m->items = items; + m->nitems = nnames; /* items zeroed: menu_free's NULL-text loop is safe */ } - m->title = strdup(title ? title : "Font"); + m->title = alloc_strdup(a, title ? title : "Font"); if (m->title == NULL) { - free(items); - free(m); + menu_free(a, m); return result_OOM; } for (i = 0; i < nnames; i++) { - items[i].text = names[i]; /* ownership transferred */ + items[i].text = alloc_strdup(a, names[i]); + if (items[i].text == NULL) + { + menu_free(a, m); + return result_OOM; + } items[i].flags = wuss_MENU_ITEM_NONE; items[i].submenu = NULL; items[i].window = NULL; } - m->items = items; - m->nitems = nnames; - *out = m; return result_OK; } /* ----------------------------------------------------------------------- */ -result_t wuss_fontmenu_create(wuss_fontmenu_t **out, - const char *dir, - const char *title) +result_t wuss_fontmenu_create(wuss_fontmenu_t **out, + const char *dir, + const char *title, + const wuss_alloc_t *alloc) { namelist_t nl; wuss_fontmenu_t *fm; @@ -141,6 +190,9 @@ result_t wuss_fontmenu_create(wuss_fontmenu_t **out, if (out == NULL || dir == NULL) return result_NULL_ARG; + if (alloc == NULL) + alloc = &wuss_alloc; + memset(&nl, 0, sizeof(nl)); rc = bmfont_enumerate(dir, collect_name, &nl); @@ -153,35 +205,37 @@ result_t wuss_fontmenu_create(wuss_fontmenu_t **out, if (nl.n > 1) qsort(nl.names, (size_t) nl.n, sizeof(nl.names[0]), name_cmp); - fm = malloc(sizeof(*fm)); + fm = alloc->malloc(sizeof(*fm)); if (fm == NULL) { namelist_free(&nl); return result_OOM; } + fm->alloc = *alloc; + fm->menu = NULL; - rc = build_menu(nl.names, nl.n, title, &fm->menu); + rc = build_menu(&fm->alloc, nl.names, nl.n, title, &fm->menu); + namelist_free(&nl); /* build_menu copied what it needed */ if (rc != result_OK) { - namelist_free(&nl); /* build_menu took nothing on failure */ - free(fm); + alloc->free(fm); return rc; } - /* build_menu adopted every string; drop the array, keep the pointers. */ - free(nl.names); - *out = fm; return result_OK; } void wuss_fontmenu_destroy(wuss_fontmenu_t *doomed) { + wuss_alloc_t alloc; + if (doomed == NULL) return; - wuss_menu_destroy(doomed->menu); - free(doomed); + alloc = doomed->alloc; + menu_free(&alloc, doomed->menu); + alloc.free(doomed); } const wuss_menu_t *wuss_fontmenu_menu(const wuss_fontmenu_t *fm) diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 64aab779..b7df3cf2 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -3690,7 +3690,7 @@ MenuOK: ; dir = path_join_filename(resources, 2, "resources", "bmfonts"); - rc = wuss_fontmenu_create(&fm, dir, "Font"); + rc = wuss_fontmenu_create(&fm, dir, "Font", NULL); if (rc != result_OK) goto FontMenuFail; @@ -3731,7 +3731,7 @@ MenuOK: ; wuss_fontmenu_destroy(fm); /* missing directory is surfaced, not swallowed */ - rc = wuss_fontmenu_create(&fm, "no/such/dir/here", NULL); + rc = wuss_fontmenu_create(&fm, "no/such/dir/here", NULL, NULL); if (rc != result_FILE_NOT_FOUND) goto FontMenuFail; rc = result_OK; From 09c7cc37b0d9606c3a739c448ecf786febd425ea Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Thu, 3 Sep 2026 13:38:16 +0100 Subject: [PATCH 218/287] feat(wuss): add a Colour menu entry to the interactive demo launcher The task launcher gains a "Colour menu" row (WUSS_COMPONENTS only) that opens a wuss_colourmenu over the pointer. It is built lazily from the live system palette on first open and reported by menu_handle via wuss_colourmenu_selected. A palette cycle frees and nulls it so the next open rebuilds against the new palette; teardown frees it too. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- apps/wuss/main.c | 57 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/apps/wuss/main.c b/apps/wuss/main.c index 7fd50b4e..51ffb0ba 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -23,6 +23,9 @@ #include "wuss/wuss.h" #include "wuss/window.h" #include "wuss/menu.h" +#ifdef WUSS_COMPONENTS +#include "wuss/component/colourmenu.h" +#endif #include "frontend.h" @@ -364,6 +367,28 @@ static result_t spawn_menu_desc(void) NULL); } +#ifdef WUSS_COMPONENTS +/* A wuss_colourmenu over the current system palette, built lazily on first + * open (the palette may be cycled before then) and freed in run_wuss's + * teardown. Picks are reported by menu_handle via wuss_colourmenu_selected. */ +static wuss_colourmenu_t *g_colourmenu; + +static result_t spawn_colourmenu(void) +{ + result_t rc; + + if (g_colourmenu == NULL) + { + rc = wuss_colourmenu_create(&g_colourmenu, g.wuss, "Colour"); + if (rc != result_OK) + return rc; + } + + return wuss_menu_open(g.menu_task, wuss_colourmenu_menu(g_colourmenu), + wuss_get_pointer(g.wuss), NULL); +} +#endif + /* The task launcher is a MENU-button pop-up over the backdrop rather than a * window of buttons. g_task_items and g_task_spawn run in lock-step: picking * item i calls g_task_spawn[i]. */ @@ -388,6 +413,9 @@ static const wuss_menu_item_t g_task_items[] = { "Text", wuss_MENU_ITEM_NONE, NULL }, { "Menu", wuss_MENU_ITEM_DASHED, NULL }, { "Menu (desc)", wuss_MENU_ITEM_NONE, NULL }, +#ifdef WUSS_COMPONENTS + { "Colour menu", wuss_MENU_ITEM_NONE, NULL }, +#endif { "Quit Wuss", wuss_MENU_ITEM_DASHED, NULL } }; @@ -402,7 +430,11 @@ static const task_spawn_fn_t g_task_spawn[] = spawn_ball, spawn_blank, spawn_chars, spawn_checker, spawn_clock, spawn_curve, spawn_gradient, spawn_icons, spawn_image, spawn_lissajous, spawn_palette, spawn_porter_duff, spawn_sofa, spawn_swatches, spawn_text, spawn_menu, - spawn_menu_desc, spawn_quit + spawn_menu_desc, +#ifdef WUSS_COMPONENTS + spawn_colourmenu, +#endif + spawn_quit }; static const wuss_menu_t g_task_menu = @@ -433,6 +465,20 @@ static result_t menu_handle(wuss_window_t *window, return result_OK; } +#ifdef WUSS_COMPONENTS + { + wuss_colour_t picked; + int mine; + + picked = wuss_colourmenu_selected(g_colourmenu, event, &mine); + if (mine) + { + printf("menu: picked colour index %d\n", (int) picked); + return result_OK; + } + } +#endif + printf("menu: picked \"%s\"\n", menu->items[index].text ? menu->items[index].text : "(sep)"); return result_OK; @@ -644,6 +690,12 @@ static result_t run_wuss(const char *resources) bitmap_set_palette(&bm, palette); wuss_frontend_set_palette(frontend, palette, NELEMS(palette)); wuss_set_palette(wuss, palette, NELEMS(palette)); +#ifdef WUSS_COMPONENTS + /* the colour menu snapshots the palette at create; drop it so the + * next open rebuilds against the new one */ + wuss_colourmenu_destroy(g_colourmenu); + g_colourmenu = NULL; +#endif break; case wuss_INPUT_MOUSE_DOWN: @@ -709,6 +761,9 @@ static result_t run_wuss(const char *resources) * block a spawn_* calloc'd, so any task window left open at quit leaks that * block. Harmless at process exit. */ wuss_menu_destroy(g_menu_desc); +#ifdef WUSS_COMPONENTS + wuss_colourmenu_destroy(g_colourmenu); +#endif wuss_destroy(wuss); /* also sweeps g.menu_task */ bmfont_destroy(font); From eb3799f513bac94993e925facc3906a45ed85c59 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Thu, 3 Sep 2026 13:57:33 +0100 Subject: [PATCH 219/287] fix(wuss): stop wuss_colourmenu using the wuss_t after it is freed wuss_colourmenu_destroy dereferenced a borrowed wuss_t for its allocator, but the demo (and any caller) frees the colourmenu after wuss_destroy -- the wuss_t was already gone. Copy the wuss_alloc_t hooks into the handle at create instead and never retain the wuss_t, matching wuss_fontmenu. palette/npalette are still read from wuss at create time only. In the demo, drop the stale wuss_menu_handle_t tracking (a self-closed chain frees its node, so wuss_menu_close/is_open then deref freed memory): a palette cycle just bumps a generation counter, spawn_colourmenu rebuilds lazily once the previous chain is gone, and teardown frees the menus only after wuss_destroy has closed any chain still borrowing them. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- apps/wuss/main.c | 32 ++++++--- libraries/wuss/component/colourmenu.c | 94 ++++++++++++++------------- 2 files changed, 73 insertions(+), 53 deletions(-) diff --git a/apps/wuss/main.c b/apps/wuss/main.c index 51ffb0ba..c2f2c626 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -368,20 +368,33 @@ static result_t spawn_menu_desc(void) } #ifdef WUSS_COMPONENTS -/* A wuss_colourmenu over the current system palette, built lazily on first - * open (the palette may be cycled before then) and freed in run_wuss's - * teardown. Picks are reported by menu_handle via wuss_colourmenu_selected. */ +/* A wuss_colourmenu over the current system palette. It snapshots the + * palette at create, so a palette cycle only bumps g_palette_gen; the menu + * is rebuilt lazily in spawn_colourmenu once its previous chain has been + * torn down (wuss_menu keeps dereferencing the source menu for the whole + * life of an open chain, so it must not be freed while one is up). The menu + * itself is freed after wuss_destroy in run_wuss's teardown -- wuss_destroy + * closes any still-open chain first. */ static wuss_colourmenu_t *g_colourmenu; +static unsigned int g_colourmenu_gen; /* g_palette_gen it was built for */ +static unsigned int g_palette_gen; static result_t spawn_colourmenu(void) { result_t rc; + if (g_colourmenu != NULL && g_colourmenu_gen != g_palette_gen) + { + wuss_colourmenu_destroy(g_colourmenu); + g_colourmenu = NULL; + } + if (g_colourmenu == NULL) { rc = wuss_colourmenu_create(&g_colourmenu, g.wuss, "Colour"); if (rc != result_OK) return rc; + g_colourmenu_gen = g_palette_gen; } return wuss_menu_open(g.menu_task, wuss_colourmenu_menu(g_colourmenu), @@ -691,10 +704,10 @@ static result_t run_wuss(const char *resources) wuss_frontend_set_palette(frontend, palette, NELEMS(palette)); wuss_set_palette(wuss, palette, NELEMS(palette)); #ifdef WUSS_COMPONENTS - /* the colour menu snapshots the palette at create; drop it so the - * next open rebuilds against the new one */ - wuss_colourmenu_destroy(g_colourmenu); - g_colourmenu = NULL; + /* the colour menu snapshots the palette at create; bump the + * generation so spawn_colourmenu rebuilds it on the next open, + * once any chain it currently feeds has gone */ + g_palette_gen++; #endif break; @@ -760,12 +773,15 @@ static result_t run_wuss(const char *resources) * frees every registered task node, but not the per-instance task_data * block a spawn_* calloc'd, so any task window left open at quit leaks that * block. Harmless at process exit. */ + wuss_destroy(wuss); /* also sweeps g.menu_task and closes any open chain */ + + /* 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); #ifdef WUSS_COMPONENTS wuss_colourmenu_destroy(g_colourmenu); #endif - wuss_destroy(wuss); /* also sweeps g.menu_task */ bmfont_destroy(font); wuss_frontend_close(frontend); diff --git a/libraries/wuss/component/colourmenu.c b/libraries/wuss/component/colourmenu.c index 33fd86bd..42cf2a6b 100644 --- a/libraries/wuss/component/colourmenu.c +++ b/libraries/wuss/component/colourmenu.c @@ -14,6 +14,7 @@ #include "wuss/menu.h" #include "wuss/task.h" +#include "wuss/wuss.h" #include "wuss/component/colourmenu.h" @@ -26,67 +27,70 @@ * single pointer compare plus the item index. */ struct wuss_colourmenu { - const wuss_t *wuss; /* borrowed; supplies the allocator for teardown */ - wuss_menu_t *menu; /* owned; every block via wuss->alloc, see menu_free */ + wuss_alloc_t alloc; /* copied hooks; the wuss_t itself is not retained */ + wuss_menu_t *menu; /* owned; every block via alloc, see menu_free */ }; -/* Free a menu built here: text, items, title, node -- all through wuss's - * allocator, so wuss_menu_destroy (plain free) must not be used. Tolerates - * NULL text for partial unwinding. */ -static void menu_free(const wuss_t *wuss, wuss_menu_t *m) -{ - int i; - - if (m == NULL) - return; - - for (i = 0; i < m->nitems; i++) - wuss__free(wuss, (void *) m->items[i].text); - wuss__free(wuss, (void *) m->items); - wuss__free(wuss, (void *) m->title); - wuss__free(wuss, m); -} - -/* wuss__ has no strdup; malloc + copy. */ -static char *wuss__strdup(const wuss_t *wuss, const char *s) +/* wuss_alloc_t has no strdup; malloc + copy. */ +static char *alloc_strdup(const wuss_alloc_t *a, const char *s) { size_t len; char *copy; len = strlen(s) + 1; - copy = wuss__malloc(wuss, len); + copy = a->malloc(len); if (copy != NULL) memcpy(copy, s, len); return copy; } +/* Free a menu built here -- text, items, title, node -- through the same + * hooks it was built with, so wuss_menu_destroy (plain free) must not be + * used. Tolerates NULL text for partial unwinding. */ +static void menu_free(const wuss_alloc_t *a, wuss_menu_t *m) +{ + int i; + + if (m == NULL) + return; + + for (i = 0; i < m->nitems; i++) + a->free((void *) m->items[i].text); + a->free((void *) m->items); + a->free((void *) m->title); + a->free(m); +} + result_t wuss_colourmenu_create(wuss_colourmenu_t **out, const wuss_t *wuss, const char *title) { - wuss_colourmenu_t *cm; - wuss_menu_t *m; - wuss_menu_item_t *items; - int n; - int i; + const wuss_alloc_t *a; + wuss_colourmenu_t *cm; + wuss_menu_t *m; + wuss_menu_item_t *items; + int n; + int i; items = NULL; if (out == NULL || wuss == NULL) return result_NULL_ARG; + a = &wuss->alloc; n = wuss->npalette; - cm = wuss__malloc(wuss, sizeof(*cm)); + cm = a->malloc(sizeof(*cm)); if (cm == NULL) return result_OOM; - cm->wuss = wuss; - cm->menu = NULL; + cm->alloc = *a; + cm->menu = NULL; + a = &cm->alloc; /* use the copy from here on -- outlives the wuss_t */ - m = wuss__malloc(wuss, sizeof(*m)); + m = a->malloc(sizeof(*m)); if (m == NULL) { - wuss__free(wuss, cm); + a->free(cm); return result_OOM; } m->title = NULL; @@ -95,11 +99,11 @@ result_t wuss_colourmenu_create(wuss_colourmenu_t **out, if (n > 0) { - items = wuss__malloc(wuss, (size_t) n * sizeof(*items)); + items = a->malloc((size_t) n * sizeof(*items)); if (items == NULL) { - menu_free(wuss, m); - wuss__free(wuss, cm); + menu_free(a, m); + a->free(cm); return result_OOM; } memset(items, 0, (size_t) n * sizeof(*items)); @@ -107,11 +111,11 @@ result_t wuss_colourmenu_create(wuss_colourmenu_t **out, m->nitems = n; /* items zeroed: menu_free's NULL-text loop is safe now */ } - m->title = wuss__strdup(wuss, title ? title : "Colour"); + m->title = alloc_strdup(a, title ? title : "Colour"); if (m->title == NULL) { - menu_free(wuss, m); - wuss__free(wuss, cm); + menu_free(a, m); + a->free(cm); return result_OOM; } @@ -126,11 +130,11 @@ result_t wuss_colourmenu_create(wuss_colourmenu_t **out, PIXELFMT_xGxx8888(px), PIXELFMT_xxBx8888(px)); - items[i].text = wuss__strdup(wuss, label); + items[i].text = alloc_strdup(a, label); if (items[i].text == NULL) { - menu_free(wuss, m); - wuss__free(wuss, cm); + menu_free(a, m); + a->free(cm); return result_OOM; } @@ -145,14 +149,14 @@ result_t wuss_colourmenu_create(wuss_colourmenu_t **out, void wuss_colourmenu_destroy(wuss_colourmenu_t *doomed) { - const wuss_t *wuss; + wuss_alloc_t alloc; if (doomed == NULL) return; - wuss = doomed->wuss; - menu_free(wuss, doomed->menu); - wuss__free(wuss, doomed); + alloc = doomed->alloc; + menu_free(&alloc, doomed->menu); + alloc.free(doomed); } const wuss_menu_t *wuss_colourmenu_menu(const wuss_colourmenu_t *cm) From a76febd68c72ca49dde3159af555780085461d7a Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Thu, 3 Sep 2026 14:18:40 +0100 Subject: [PATCH 220/287] refactor(wuss): add wuss__alloc_strdup and wuss__array_grow allocator helpers Two inline helpers on const wuss_alloc_t *, in impl.h so the shared components (which keep a copy of the hooks, not the wuss_t) can use them: - wuss__alloc_strdup: strdup through a->malloc plus a copy (strdup is POSIX, not C99). - wuss__array_grow: array_grow's next-power-of-two doubling routed through a->realloc, same element/return semantics. colourmenu and fontmenu drop their local alloc_strdup copies for the shared helper. fontmenu's transient namelist scratch (the enumerate callback) now grows and dups through the resolved caller hooks via the new helpers instead of libc array_grow/strdup/free, so nothing in the component touches raw stdlib. wuss_alloc_t keeps its { malloc, realloc, free } shape -- no strdup member. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- include/wuss/wuss.h | 10 +++--- libraries/wuss/component/colourmenu.c | 17 ++------- libraries/wuss/component/fontmenu.c | 45 ++++++++++------------- libraries/wuss/create.c | 5 ++- libraries/wuss/impl.h | 52 +++++++++++++++++++++++++++ 5 files changed, 81 insertions(+), 48 deletions(-) diff --git a/include/wuss/wuss.h b/include/wuss/wuss.h index 67821278..7dcf8c8a 100644 --- a/include/wuss/wuss.h +++ b/include/wuss/wuss.h @@ -55,11 +55,11 @@ typedef struct wuss_icon wuss_icon_t; /** * Allocator hooks used by a wuss_t for every heap block it owns (the - * instance itself, windows, icons, menu nodes). The three members must - * behave like the C library malloc / realloc / free -- same argument and - * return conventions, realloc(NULL, n) == malloc(n), free(NULL) a no-op. - * Passed to wuss_create and copied in; NULL there selects wuss_alloc (plain - * stdlib). + * instance itself, windows, icons, menu nodes) and by the wuss shared + * components. \c malloc, \c realloc and \c free must behave like their C + * library namesakes -- same argument and return conventions, realloc(NULL, + * n) == malloc(n), free(NULL) a no-op. Passed to wuss_create and copied in; + * NULL there selects wuss_alloc (plain stdlib). */ typedef struct wuss_alloc { diff --git a/libraries/wuss/component/colourmenu.c b/libraries/wuss/component/colourmenu.c index 42cf2a6b..aee07b6b 100644 --- a/libraries/wuss/component/colourmenu.c +++ b/libraries/wuss/component/colourmenu.c @@ -31,19 +31,6 @@ struct wuss_colourmenu wuss_menu_t *menu; /* owned; every block via alloc, see menu_free */ }; -/* wuss_alloc_t has no strdup; malloc + copy. */ -static char *alloc_strdup(const wuss_alloc_t *a, const char *s) -{ - size_t len; - char *copy; - - len = strlen(s) + 1; - copy = a->malloc(len); - if (copy != NULL) - memcpy(copy, s, len); - return copy; -} - /* Free a menu built here -- text, items, title, node -- through the same * hooks it was built with, so wuss_menu_destroy (plain free) must not be * used. Tolerates NULL text for partial unwinding. */ @@ -111,7 +98,7 @@ result_t wuss_colourmenu_create(wuss_colourmenu_t **out, m->nitems = n; /* items zeroed: menu_free's NULL-text loop is safe now */ } - m->title = alloc_strdup(a, title ? title : "Colour"); + m->title = wuss__alloc_strdup(a, title ? title : "Colour"); if (m->title == NULL) { menu_free(a, m); @@ -130,7 +117,7 @@ result_t wuss_colourmenu_create(wuss_colourmenu_t **out, PIXELFMT_xGxx8888(px), PIXELFMT_xxBx8888(px)); - items[i].text = alloc_strdup(a, label); + items[i].text = wuss__alloc_strdup(a, label); if (items[i].text == NULL) { menu_free(a, m); diff --git a/libraries/wuss/component/fontmenu.c b/libraries/wuss/component/fontmenu.c index 6efc351b..e080d23e 100644 --- a/libraries/wuss/component/fontmenu.c +++ b/libraries/wuss/component/fontmenu.c @@ -10,7 +10,6 @@ #include "base/result.h" #include "framebuf/bmfont.h" -#include "utils/array.h" #include "wuss/menu.h" #include "wuss/task.h" @@ -18,6 +17,8 @@ #include "wuss/component/fontmenu.h" +#include "../impl.h" + /* ----------------------------------------------------------------------- */ /* ponytail: flat list, one item per font, no family/weight grouping into @@ -30,19 +31,6 @@ struct wuss_fontmenu wuss_menu_t *menu; /* owned; freed by menu_free, not wuss_menu_destroy */ }; -/* wuss_alloc_t has no strdup; malloc + copy. */ -static char *alloc_strdup(const wuss_alloc_t *a, const char *s) -{ - size_t len; - char *copy; - - len = strlen(s) + 1; - copy = a->malloc(len); - if (copy != NULL) - memcpy(copy, s, len); - return copy; -} - /* Free a menu built here -- text, items, title, node -- through the same * hooks it was built with. Tolerates NULL text for partial unwinding. */ static void menu_free(const wuss_alloc_t *a, wuss_menu_t *m) @@ -61,14 +49,16 @@ static void menu_free(const wuss_alloc_t *a, wuss_menu_t *m) /* ----------------------------------------------------------------------- */ -/* Growable list of strdup'd font names, filled by the enumerate callback. - * A transient scratch buffer: plain stdlib, freed before create returns. */ +/* Growable list of name copies, filled by the enumerate callback. Transient: + * freed before create returns. Routed through the caller's hooks so even the + * scratch never touches raw stdlib. */ typedef struct namelist { - char **names; - int n; - int cap; - int oom; /* a strdup/grow failed; stop and unwind */ + const wuss_alloc_t *alloc; /* borrowed; the resolved hooks */ + char **names; + int n; + int cap; + int oom; /* a strdup/grow failed; stop and unwind */ } namelist_t; @@ -81,14 +71,14 @@ static result_t collect_name(const char *name, (void) path; - if (array_grow((void **) &nl->names, sizeof(*nl->names), - nl->n, &nl->cap, 1, 8)) + if (wuss__array_grow(nl->alloc, (void **) &nl->names, sizeof(*nl->names), + nl->n, &nl->cap, 1, 8)) { nl->oom = 1; return result_OOM; } - copy = strdup(name); + copy = wuss__alloc_strdup(nl->alloc, name); if (copy == NULL) { nl->oom = 1; @@ -104,8 +94,8 @@ static void namelist_free(namelist_t *nl) int i; for (i = 0; i < nl->n; i++) - free(nl->names[i]); - free(nl->names); + nl->alloc->free(nl->names[i]); + nl->alloc->free(nl->names); nl->names = NULL; nl->n = nl->cap = 0; } @@ -152,7 +142,7 @@ static result_t build_menu(const wuss_alloc_t *a, m->nitems = nnames; /* items zeroed: menu_free's NULL-text loop is safe */ } - m->title = alloc_strdup(a, title ? title : "Font"); + m->title = wuss__alloc_strdup(a, title ? title : "Font"); if (m->title == NULL) { menu_free(a, m); @@ -161,7 +151,7 @@ static result_t build_menu(const wuss_alloc_t *a, for (i = 0; i < nnames; i++) { - items[i].text = alloc_strdup(a, names[i]); + items[i].text = wuss__alloc_strdup(a, names[i]); if (items[i].text == NULL) { menu_free(a, m); @@ -194,6 +184,7 @@ result_t wuss_fontmenu_create(wuss_fontmenu_t **out, alloc = &wuss_alloc; memset(&nl, 0, sizeof(nl)); + nl.alloc = alloc; rc = bmfont_enumerate(dir, collect_name, &nl); if (rc != result_OK) diff --git a/libraries/wuss/create.c b/libraries/wuss/create.c index b9d794b0..61f7d510 100644 --- a/libraries/wuss/create.c +++ b/libraries/wuss/create.c @@ -14,7 +14,10 @@ /* The default allocator: plain stdlib. wuss_create copies this in when its * alloc argument is NULL. */ -const wuss_alloc_t wuss_alloc = { malloc, realloc, free }; +const wuss_alloc_t wuss_alloc = +{ + malloc, realloc, free +}; /* Built-in fallback palette when the caller passes NULL: black and white. * wuss makes no other assumptions about palette contents or length. */ diff --git a/libraries/wuss/impl.h b/libraries/wuss/impl.h index 35151980..6e79519f 100644 --- a/libraries/wuss/impl.h +++ b/libraries/wuss/impl.h @@ -4,6 +4,7 @@ #define IMPL_H #include <stddef.h> +#include <string.h> #include "base/utils.h" #include "datastruct/list.h" @@ -13,6 +14,7 @@ #include "geom/size.h" #include "framebuf/screen.h" #include "framebuf/bmfont.h" +#include "utils/barith.h" #include "wuss/wuss.h" #include "wuss/window.h" @@ -226,6 +228,56 @@ static inline void wuss__free(const wuss_t *wuss, void *ptr) wuss->alloc.free(ptr); } +/* Duplicate a NUL-terminated string through an allocator's malloc hook + * (strdup is POSIX, not C99, so it is not assumed). Returns NULL on OOM or a + * NULL s. Takes wuss_alloc_t, not wuss_t, so the shared components (which + * keep a copy of the hooks, not the wuss_t) can use it too. */ +static inline char *wuss__alloc_strdup(const wuss_alloc_t *a, const char *s) +{ + size_t len; + char *copy; + + if (s == NULL) + return NULL; + + len = strlen(s) + 1; + copy = a->malloc(len); + if (copy != NULL) + memcpy(copy, s, len); + return copy; +} + +/* array_grow (utils/array.h) done through an allocator's realloc hook: the + * next-power-of-two doubling, same element/return semantics (0 ok, 1 OOM). + * used/need/minimum are element counts. See libraries/utils/array/grow.c. */ +static inline int wuss__array_grow(const wuss_alloc_t *a, + void **block, + size_t elemsize, + int used, + int *allocated, + int need, + int minimum) +{ + int to_allocate; + void *grown; + + need += used; + if (need < minimum) + need = minimum; + if (need <= *allocated) + return 0; + + to_allocate = power2gt(need - 1); + + grown = a->realloc(*block, elemsize * (size_t) to_allocate); + if (grown == NULL) + return 1; + + *block = grown; + *allocated = to_allocate; + return 0; +} + /* Range-check a backdrop spec against the palette: its colour, and -- when a * non-solid pattern is set -- its pattern index and background colour. * Returns result_OK or result_WUSS_BAD_COLOUR. */ From 4e9ef5c7b80b37da8bc1615ec7c68c80a2c9d59a Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Thu, 3 Sep 2026 16:22:23 +0100 Subject: [PATCH 221/287] feat(wuss): add wuss_icon_plot, an icon draw that retains nothing wuss_icon_plot validates a spec exactly as wuss_icon_create does and draws it once through the window manager's screen, allocating nothing and adding no icon to the window. For static, non-interactive content a task can redraw from its own model without a live icon per element. The spec-to-icon validation and colour resolution shared by the two entry points is factored into wuss__icon_from_spec (icon/from-spec.c); wuss_icon_create now calls it and then swaps in an owned text copy. The swatches demo task is rebuilt on wuss_icon_plot: it draws a grid of every (fill pattern, palette colour) pair -- row per pattern, column per palette index used as the pattern ink over white -- with no retained icons, so a palette cycle only needs a redraw. The task struct drops to { wuss, window }. Click-to-set-backdrop is kept, finding the cell by arithmetic on the click point rather than stored hit boxes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- CMakeLists.txt | 2 + include/wuss/icon.h | 26 +++++ libraries/wuss/icon.h | 8 ++ libraries/wuss/icon/create.c | 79 +++----------- libraries/wuss/icon/from-spec.c | 84 +++++++++++++++ libraries/wuss/icon/plot.c | 32 ++++++ libraries/wuss/test/tasks/swatches.c | 153 ++++++++++++--------------- libraries/wuss/test/tasks/swatches.h | 17 +-- 8 files changed, 238 insertions(+), 163 deletions(-) create mode 100644 libraries/wuss/icon/from-spec.c create mode 100644 libraries/wuss/icon/plot.c diff --git a/CMakeLists.txt b/CMakeLists.txt index d9fa660f..1cb4453f 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -408,6 +408,7 @@ set(WUSS_ICON_SOURCES libraries/wuss/icon/delete.c libraries/wuss/icon/draw.c libraries/wuss/icon/free.c + libraries/wuss/icon/from-spec.c libraries/wuss/icon/get-bbox.c libraries/wuss/icon/get-selected.c libraries/wuss/icon/get-text.c @@ -415,6 +416,7 @@ set(WUSS_ICON_SOURCES libraries/wuss/icon/get-window.c libraries/wuss/icon/hit-test.c libraries/wuss/icon/invalidate.c + libraries/wuss/icon/plot.c libraries/wuss/icon/screen-box.c libraries/wuss/icon/set-hidden.c libraries/wuss/icon/set-hover.c diff --git a/include/wuss/icon.h b/include/wuss/icon.h index 696b74f6..0ebb3007 100644 --- a/include/wuss/icon.h +++ b/include/wuss/icon.h @@ -31,6 +31,7 @@ extern "C" #include "base/result.h" #include "framebuf/screen.h" #include "geom/box.h" +#include "geom/point.h" #include "wuss/wuss.h" @@ -228,6 +229,31 @@ result_t wuss_icon_create_array(wuss_window_t *window, int nspecs, wuss_icon_t **icons); +/** + * Draw an icon from a spec once, retaining nothing: no allocation, no icon + * added to the window. For static, non-interactive content a task can redraw + * cheaply from its own model -- a large grid of swatches, say -- without a + * live icon per cell. + * + * Call only from a task's wuss_EVENT_REDRAW handler, passing that event's \c + * bounds and \c scroll straight through; the icon is painted through the + * window manager's screen with the redraw clip already in force. The spec is + * validated exactly as \ref wuss_icon_create validates it. The icon's \c + * bbox is in virtual document space, as for a created icon. + * + * \param[in] window Window being redrawn. + * \param[in] spec Icon description; not retained. + * \param[in] content The redraw event's \c bounds (full content box, screen + * space). + * \param[in] scroll The redraw event's \c scroll offset. + * \return \ref result_OK, or the same \ref result_WUSS_BAD_ICON / \ref + * result_WUSS_BAD_COLOUR \ref wuss_icon_create would return. + */ +result_t wuss_icon_plot(wuss_window_t *window, + const wuss_icon_spec_t *spec, + const box_t *content, + point_t scroll); + /** * Destroy an icon, unlinking it from its window and invalidating its * bounding box so the next redraw clears it. Safe to pass NULL. diff --git a/libraries/wuss/icon.h b/libraries/wuss/icon.h index 70955ae6..ff67acc7 100644 --- a/libraries/wuss/icon.h +++ b/libraries/wuss/icon.h @@ -87,6 +87,14 @@ void wuss__icon_box_to_screen(const box_t *content, * set_text / pressed-state / hide change repaints just the icon. */ void wuss__icon_invalidate(const wuss_icon_t *icon); +/* Validate a spec against the palette and fill "out" with a detached icon (no + * window, no owned text -- out->text is aliased to spec->text or ""). Shared + * by wuss_icon_create and wuss_icon_plot. Returns result_WUSS_BAD_ICON / + * result_WUSS_BAD_COLOUR as wuss_icon_create documents, else result_OK. */ +result_t wuss__icon_from_spec(const wuss_t *wuss, + const wuss_icon_spec_t *spec, + wuss_icon_t *out); + /* Draw one icon. Called from redraw_window with wuss->scr->clip already set to * the surviving content piece and the background already filled. "content" is * the window's full (unclipped) content box, screen space; "scroll" is diff --git a/libraries/wuss/icon/create.c b/libraries/wuss/icon/create.c index b4145b2b..80c3bf7d 100644 --- a/libraries/wuss/icon/create.c +++ b/libraries/wuss/icon/create.c @@ -14,66 +14,23 @@ result_t wuss_icon_create(wuss_window_t *window, const wuss_icon_spec_t *spec, wuss_icon_t **icon) { - wuss_t *w; - wuss_icon_t *it; + wuss_t *w; + wuss_icon_t *it; wuss_icon_t **grown; - const char *src; - size_t len; - int newcap; - wuss_colour_t fg, bg, swatch; - int has_swatch; + wuss_icon_t scratch; + const char *src; + size_t len; + int newcap; + result_t rc; assert(window != NULL); assert(spec != NULL); w = window->wuss; - fg = wuss__resolve_colour(w, spec->fg); - bg = wuss__resolve_colour(w, spec->bg); - - has_swatch = (spec->type == wuss_ICON_TYPE_MENU_ENTRY) && - (spec->flags & wuss_ICON_FLAGS_SWATCH); - swatch = has_swatch ? wuss__resolve_colour(w, spec->swatch) - : wuss_NO_BACKGROUND; - - switch (spec->type) - { - case wuss_ICON_TYPE_LABEL: - case wuss_ICON_TYPE_BUTTON: - case wuss_ICON_TYPE_PATTERN: - case wuss_ICON_TYPE_FRAME: - case wuss_ICON_TYPE_RADIO: - case wuss_ICON_TYPE_OPTION: - case wuss_ICON_TYPE_BITMAP: - case wuss_ICON_TYPE_MENU_ENTRY: - case wuss_ICON_TYPE_RULE: - break; - - default: - return result_WUSS_BAD_ICON; - } - - if ((spec->type == wuss_ICON_TYPE_BUTTON || - 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) - return result_WUSS_BAD_ICON; - - if (spec->type == wuss_ICON_TYPE_PATTERN && - (spec->pattern < 0 || spec->pattern >= screen_PATTERN__LIMIT)) - return result_WUSS_BAD_ICON; - - if (fg < 0 || fg >= w->npalette) - return result_WUSS_BAD_COLOUR; - - if (bg != wuss_NO_BACKGROUND && - (bg < 0 || bg >= w->npalette)) - return result_WUSS_BAD_COLOUR; - - if (has_swatch && (swatch < 0 || swatch >= w->npalette)) - return result_WUSS_BAD_COLOUR; + rc = wuss__icon_from_spec(w, spec, &scratch); + if (rc != result_OK) + return rc; if (window->nicons == window->cap_icons) { @@ -89,6 +46,9 @@ result_t wuss_icon_create(wuss_window_t *window, if (it == NULL) return result_OOM; + *it = scratch; /* scratch.text aliases spec->text; replaced with an owned copy below */ + it->window = window; + src = (spec->text != NULL) ? spec->text : ""; len = strlen(src); it->text = wuss__malloc(w, len + 1); @@ -99,19 +59,6 @@ result_t wuss_icon_create(wuss_window_t *window, } memcpy(it->text, src, len + 1); - it->window = window; - it->bbox = spec->bbox; - it->type = spec->type; - it->fg = fg; - it->bg = bg; - it->pattern = (spec->type == wuss_ICON_TYPE_PATTERN) ? spec->pattern - : screen_PATTERN_SOLID; - it->bitmap = (spec->type == wuss_ICON_TYPE_BITMAP) ? spec->bitmap : NULL; - it->group = (spec->type == wuss_ICON_TYPE_RADIO) ? spec->group : 0; - it->swatch = swatch; - it->flags = spec->flags; - it->state = wuss_ICON_STATE_NONE; - window->icons[window->nicons++] = it; wuss__icon_invalidate(it); diff --git a/libraries/wuss/icon/from-spec.c b/libraries/wuss/icon/from-spec.c new file mode 100644 index 00000000..6e01411f --- /dev/null +++ b/libraries/wuss/icon/from-spec.c @@ -0,0 +1,84 @@ +/* wuss/icon/from-spec.c -- validate an icon spec into a detached icon */ + +#include <assert.h> + +#ifdef FORTIFY +#include "fortify/fortify.h" +#endif + +#include "../impl.h" + +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; + + assert(w != NULL); + assert(spec != NULL); + assert(out != NULL); + + fg = wuss__resolve_colour(w, spec->fg); + bg = wuss__resolve_colour(w, spec->bg); + + has_swatch = (spec->type == wuss_ICON_TYPE_MENU_ENTRY) && + (spec->flags & wuss_ICON_FLAGS_SWATCH); + swatch = has_swatch ? wuss__resolve_colour(w, spec->swatch) + : wuss_NO_BACKGROUND; + + switch (spec->type) + { + case wuss_ICON_TYPE_LABEL: + case wuss_ICON_TYPE_BUTTON: + case wuss_ICON_TYPE_PATTERN: + case wuss_ICON_TYPE_FRAME: + case wuss_ICON_TYPE_RADIO: + case wuss_ICON_TYPE_OPTION: + case wuss_ICON_TYPE_BITMAP: + case wuss_ICON_TYPE_MENU_ENTRY: + case wuss_ICON_TYPE_RULE: + break; + + default: + return result_WUSS_BAD_ICON; + } + + if ((spec->type == wuss_ICON_TYPE_BUTTON || + 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) + return result_WUSS_BAD_ICON; + + if (spec->type == wuss_ICON_TYPE_PATTERN && + (spec->pattern < 0 || spec->pattern >= screen_PATTERN__LIMIT)) + return result_WUSS_BAD_ICON; + + if (fg < 0 || fg >= w->npalette) + return result_WUSS_BAD_COLOUR; + + if (bg != wuss_NO_BACKGROUND && + (bg < 0 || bg >= w->npalette)) + return result_WUSS_BAD_COLOUR; + + if (has_swatch && (swatch < 0 || swatch >= w->npalette)) + return result_WUSS_BAD_COLOUR; + + out->window = NULL; + out->bbox = spec->bbox; + out->type = spec->type; + out->text = (char *) ((spec->text != NULL) ? spec->text : ""); + out->fg = fg; + 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->group = (spec->type == wuss_ICON_TYPE_RADIO) ? spec->group : 0; + out->swatch = swatch; + out->flags = spec->flags; + out->state = wuss_ICON_STATE_NONE; + + return result_OK; +} diff --git a/libraries/wuss/icon/plot.c b/libraries/wuss/icon/plot.c new file mode 100644 index 00000000..d62dc1d3 --- /dev/null +++ b/libraries/wuss/icon/plot.c @@ -0,0 +1,32 @@ +/* wuss/icon/plot.c -- draw an icon from a spec without retaining it */ + +#include <assert.h> + +#ifdef FORTIFY +#include "fortify/fortify.h" +#endif + +#include "../impl.h" + +result_t wuss_icon_plot(wuss_window_t *window, + const wuss_icon_spec_t *spec, + const box_t *content, + point_t scroll) +{ + wuss_icon_t scratch; + result_t rc; + + assert(window != NULL); + assert(spec != NULL); + assert(content != NULL); + + rc = wuss__icon_from_spec(window->wuss, spec, &scratch); + if (rc != result_OK) + return rc; + + scratch.window = window; + + wuss__icon_draw(window->wuss, &scratch, content, scroll); + + return result_OK; +} diff --git a/libraries/wuss/test/tasks/swatches.c b/libraries/wuss/test/tasks/swatches.c index 3f904cc3..87dbee48 100644 --- a/libraries/wuss/test/tasks/swatches.c +++ b/libraries/wuss/test/tasks/swatches.c @@ -9,8 +9,8 @@ #include "fortify/fortify.h" #endif -#include "framebuf/palettes.h" -#include "framebuf/screen.h" +#include "base/utils.h" +#include "framebuf/pattern.h" #include "geom/box.h" #include "geom/point.h" #include "geom/size.h" @@ -18,66 +18,56 @@ #include "swatches.h" -#define SWATCHES_DOC_W 160 -#define SWATCHES_DOC_H 320 /* taller than the window, so scrolling is exercised */ +/* ponytail: the app builds wuss with a 16-entry palette (apps/wuss/main.c) + * and wuss exposes no count. Hardcode it; add a wuss_palette_count() only if + * another palette size ever ships. */ +#define SWATCHES_NCOLOURS 16 +#define SWATCHES_CELL 16 /* px; cells butt together, no pitch */ -/* Delete the current swatch icons and lay a fresh set out, resolving the - * heading and swatch colours through the palette as it stands now. Called at - * create time and again on wuss_EVENT_PALETTE. */ -static result_t swatches_build(swatches_task_t *task) -{ - wuss_icon_spec_t specs[SWATCHES_NSPECS]; - wuss_colour_t fg, bg; - int p; - - for (p = 0; p < SWATCHES_NSPECS; p++) - { - wuss_icon_delete(task->icons[p]); - task->icons[p] = NULL; - } - - fg = wuss_nearest_colour(task->wuss, 0x00, 0x00, 0x00); - bg = wuss_nearest_colour(task->wuss, 0xFF, 0xFF, 0xFF); - - task->swatch_fg = fg; - task->swatch_bg = bg; +#define SWATCHES_DOC_W (SWATCHES_NCOLOURS * SWATCHES_CELL) +#define SWATCHES_DOC_H (screen_PATTERN__LIMIT * SWATCHES_CELL) - memset(specs, 0, sizeof(specs)); +#define SWATCHES_PAPER_RGB 0xFF, 0xFF, 0xFF - /* [0] a heading label */ - specs[0].bbox = (box_t) BOX_POS_SIZE(12, 12, 140, 14); - specs[0].type = wuss_ICON_TYPE_LABEL; - specs[0].text = "Fill patterns:"; - specs[0].fg = fg; - specs[0].bg = wuss_NO_BACKGROUND; - - /* [1..] one 16x16 swatch per built-in fill pattern, laid out as a grid down - * the document so it scrolls through the window and stays phase-locked while - * doing so */ - for (p = 0; p < screen_PATTERN__LIMIT; p++) +/* Redraw: plot one 4x4 PATTERN swatch per (pattern, colour) pair -- row = + * pattern, column = palette index used as the pattern's ink over white. No + * icons are retained; wuss_icon_plot resolves the palette live, so a palette + * swap just redraws. */ +static result_t swatches_redraw(swatches_task_t *task, + const wuss_event_t *event) +{ + wuss_icon_spec_t spec; + wuss_colour_t paper; + const box_t *bounds; + point_t scroll; + int pat, col; + result_t rc; + + bounds = event->data.redraw.bounds; + scroll = event->data.redraw.scroll; + paper = wuss_nearest_colour(task->wuss, SWATCHES_PAPER_RGB); + + memset(&spec, 0, sizeof(spec)); + spec.type = wuss_ICON_TYPE_PATTERN; + spec.bg = paper; + + for (pat = 0; pat < screen_PATTERN__LIMIT; pat++) { - enum { SWATCHES_COLS = 6, SWATCHES_PITCH = 20 }; - int col, row; - - col = p % SWATCHES_COLS; - row = p / SWATCHES_COLS; - - specs[1 + p].bbox = (box_t) BOX_POS_SIZE(12 + col * SWATCHES_PITCH, - 34 + row * SWATCHES_PITCH, - 16, 16); - specs[1 + p].type = wuss_ICON_TYPE_PATTERN; - specs[1 + p].fg = fg; - specs[1 + p].bg = bg; - specs[1 + p].pattern = (screen_pattern_t) p; - - /* PATTERN icons are not interactive, so their clicks fall through to us - * as wuss_EVENT_MOUSE in this same content space -- keep each box to - * hit-test against */ - task->swatch_bbox[p] = specs[1 + p].bbox; + for (col = 0; col < SWATCHES_NCOLOURS; col++) + { + spec.bbox = (box_t) BOX_POS_SIZE(col * SWATCHES_CELL, + pat * SWATCHES_CELL, + SWATCHES_CELL, SWATCHES_CELL); + spec.fg = (wuss_colour_t) col; + spec.pattern = (screen_pattern_t) pat; + + rc = wuss_icon_plot(task->window, &spec, bounds, scroll); + if (rc != result_OK) + return rc; + } } - return wuss_icon_create_array(task->window, specs, SWATCHES_NSPECS, - task->icons); + return result_OK; } result_t swatches_create(wuss_t *wuss, swatches_task_t *task) @@ -88,7 +78,6 @@ result_t swatches_create(wuss_t *wuss, swatches_task_t *task) task->wuss = wuss; task->window = NULL; - memset(task->icons, 0, sizeof(task->icons)); delegate_desc.handle = swatches_handle; delegate_desc.task_data = task; @@ -103,7 +92,7 @@ result_t swatches_create(wuss_t *wuss, swatches_task_t *task) rc = wuss_window_create_placed(delegate, SIZE2D(SWATCHES_DOC_W, 140), "Swatches", - wuss_WINDOW_NONE, + 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)), SIZE2D(SWATCHES_DOC_W, SWATCHES_DOC_H), SIZE2D(0, 0), @@ -114,13 +103,6 @@ result_t swatches_create(wuss_t *wuss, swatches_task_t *task) return rc; } - rc = swatches_build(task); - if (rc != result_OK) - { - wuss_task_destroy(delegate); /* closes the window, QUIT frees the block */ - return rc; - } - /* fully built: from here a last-window close reaps the task and its * wuss_EVENT_QUIT frees task_data */ wuss_task_set_autoclose(delegate, 1); @@ -128,51 +110,52 @@ result_t swatches_create(wuss_t *wuss, swatches_task_t *task) return result_OK; } -/* A click landed on the window content. If it is inside a swatch, make that - * swatch's fill pattern the desktop backdrop. */ +/* A SELECT click on a cell makes that cell -- its fill pattern, its palette + * colour as the ink, white paper -- the desktop backdrop. The grid has no + * retained icons, so the cell is found by arithmetic on the click point + * (virtual content space). */ static result_t swatches_click(swatches_task_t *task, const wuss_event_t *event) { wuss_backdrop_t backdrop; point_t pt; - int p; + int pat, col; if (event->data.mouse.action != wuss_MOUSE_DOWN || !(event->data.mouse.button & wuss_BUTTON_SELECT)) return result_OK; - pt = event->data.mouse.point; - - for (p = 0; p < screen_PATTERN__LIMIT; p++) - { - if (!box_contains_point(&task->swatch_bbox[p], pt.x, pt.y)) - continue; + pt = event->data.mouse.point; + col = pt.x / SWATCHES_CELL; + pat = pt.y / SWATCHES_CELL; - backdrop = (wuss_backdrop_t) wuss_BACKDROP_PATTERN(task->swatch_fg, - (screen_pattern_t) p, - task->swatch_bg); - return wuss_set_backdrop(task->wuss, &backdrop); - } + if (pt.x < 0 || pt.y < 0 || + col >= SWATCHES_NCOLOURS || pat >= screen_PATTERN__LIMIT) + return result_OK; - return result_OK; + backdrop = (wuss_backdrop_t) wuss_BACKDROP_PATTERN( + (wuss_colour_t) col, + (screen_pattern_t) pat, + wuss_nearest_colour(task->wuss, SWATCHES_PAPER_RGB)); + return wuss_set_backdrop(task->wuss, &backdrop); } result_t swatches_handle(wuss_window_t *window, const wuss_event_t *event, void *task_data) { - swatches_task_t *task; + swatches_task_t *task = task_data; - task = task_data; + NOT_USED(window); switch (event->kind) { + case wuss_EVENT_REDRAW: + return swatches_redraw(task, event); + case wuss_EVENT_MOUSE: return swatches_click(task, event); - case wuss_EVENT_PALETTE: - return swatches_build(task); - case wuss_EVENT_QUIT: free(task_data); /* calloc'd per instance by the spawner */ return result_OK; diff --git a/libraries/wuss/test/tasks/swatches.h b/libraries/wuss/test/tasks/swatches.h index 4a2bd7e9..0fb723ce 100644 --- a/libraries/wuss/test/tasks/swatches.h +++ b/libraries/wuss/test/tasks/swatches.h @@ -5,24 +5,17 @@ #ifdef WUSS_APP -#include "framebuf/screen.h" -#include "geom/box.h" -#include "wuss/icon.h" #include "wuss/window.h" -/* one 16x16 PATTERN icon per built-in screen fill pattern, laid out as a - * grid down a document taller than the window so the swatches scroll - * through it and stay phase-locked while doing so */ -enum { SWATCHES_NSPECS = 1 + screen_PATTERN__LIMIT }; +/* Grid of every (fill pattern, palette colour) pair: one row per built-in + * screen fill pattern, one column per system-palette entry, each cell a 4x4 + * PATTERN icon packed edge to edge. The document is taller than the window + * so the grid scrolls through it. */ typedef struct swatches_task { - wuss_t *wuss; /* for re-resolving swatch colours on a palette swap */ + wuss_t *wuss; wuss_window_t *window; - wuss_icon_t *icons[SWATCHES_NSPECS]; /* current icons, deleted on rebuild */ - box_t swatch_bbox[screen_PATTERN__LIMIT]; /* content-space hit box per pattern */ - wuss_colour_t swatch_fg; /* ink to set the backdrop in when a swatch is picked */ - wuss_colour_t swatch_bg; /* paper for the backdrop pattern's clear bits */ } swatches_task_t; From 3d73455b5e813f3c09eb5ea8898d4ab849892c57 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Thu, 3 Sep 2026 18:32:12 +0100 Subject: [PATCH 222/287] feat(wuss): swatches colour menu sets the mixing paper The swatches task now owns a wuss_colourmenu: a MENU-button click over the grid pops it up, and the picked palette index becomes the paper the fill patterns mix over (white until then), replacing the hardcoded white in both the redraw and the SELECT-click backdrop. Add wuss_window_invalidate_extent, a central wuss call that 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 whole document repaints at every scroll position. The swatches paper change uses it. Drop the now-redundant "Colour menu" entry and its spawn_colourmenu slot from the main Tasks menu in apps/wuss/main.c, along with the file-scope g_colourmenu machinery, the menu_handle colour block and the palette-cycle generation bump. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- apps/wuss/main.c | 67 ----------------------- include/wuss/window.h | 12 +++++ libraries/wuss/test/tasks/swatches.c | 79 +++++++++++++++++++++------- libraries/wuss/test/tasks/swatches.h | 17 ++++-- libraries/wuss/window/invalidate.c | 12 +++++ 5 files changed, 98 insertions(+), 89 deletions(-) diff --git a/apps/wuss/main.c b/apps/wuss/main.c index c2f2c626..b2471076 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -23,9 +23,6 @@ #include "wuss/wuss.h" #include "wuss/window.h" #include "wuss/menu.h" -#ifdef WUSS_COMPONENTS -#include "wuss/component/colourmenu.h" -#endif #include "frontend.h" @@ -367,41 +364,6 @@ static result_t spawn_menu_desc(void) NULL); } -#ifdef WUSS_COMPONENTS -/* A wuss_colourmenu over the current system palette. It snapshots the - * palette at create, so a palette cycle only bumps g_palette_gen; the menu - * is rebuilt lazily in spawn_colourmenu once its previous chain has been - * torn down (wuss_menu keeps dereferencing the source menu for the whole - * life of an open chain, so it must not be freed while one is up). The menu - * itself is freed after wuss_destroy in run_wuss's teardown -- wuss_destroy - * closes any still-open chain first. */ -static wuss_colourmenu_t *g_colourmenu; -static unsigned int g_colourmenu_gen; /* g_palette_gen it was built for */ -static unsigned int g_palette_gen; - -static result_t spawn_colourmenu(void) -{ - result_t rc; - - if (g_colourmenu != NULL && g_colourmenu_gen != g_palette_gen) - { - wuss_colourmenu_destroy(g_colourmenu); - g_colourmenu = NULL; - } - - if (g_colourmenu == NULL) - { - rc = wuss_colourmenu_create(&g_colourmenu, g.wuss, "Colour"); - if (rc != result_OK) - return rc; - g_colourmenu_gen = g_palette_gen; - } - - return wuss_menu_open(g.menu_task, wuss_colourmenu_menu(g_colourmenu), - wuss_get_pointer(g.wuss), NULL); -} -#endif - /* The task launcher is a MENU-button pop-up over the backdrop rather than a * window of buttons. g_task_items and g_task_spawn run in lock-step: picking * item i calls g_task_spawn[i]. */ @@ -426,9 +388,6 @@ static const wuss_menu_item_t g_task_items[] = { "Text", wuss_MENU_ITEM_NONE, NULL }, { "Menu", wuss_MENU_ITEM_DASHED, NULL }, { "Menu (desc)", wuss_MENU_ITEM_NONE, NULL }, -#ifdef WUSS_COMPONENTS - { "Colour menu", wuss_MENU_ITEM_NONE, NULL }, -#endif { "Quit Wuss", wuss_MENU_ITEM_DASHED, NULL } }; @@ -444,9 +403,6 @@ static const task_spawn_fn_t g_task_spawn[] = spawn_gradient, spawn_icons, spawn_image, spawn_lissajous, spawn_palette, spawn_porter_duff, spawn_sofa, spawn_swatches, spawn_text, spawn_menu, spawn_menu_desc, -#ifdef WUSS_COMPONENTS - spawn_colourmenu, -#endif spawn_quit }; @@ -478,20 +434,6 @@ static result_t menu_handle(wuss_window_t *window, return result_OK; } -#ifdef WUSS_COMPONENTS - { - wuss_colour_t picked; - int mine; - - picked = wuss_colourmenu_selected(g_colourmenu, event, &mine); - if (mine) - { - printf("menu: picked colour index %d\n", (int) picked); - return result_OK; - } - } -#endif - printf("menu: picked \"%s\"\n", menu->items[index].text ? menu->items[index].text : "(sep)"); return result_OK; @@ -703,12 +645,6 @@ static result_t run_wuss(const char *resources) bitmap_set_palette(&bm, palette); wuss_frontend_set_palette(frontend, palette, NELEMS(palette)); wuss_set_palette(wuss, palette, NELEMS(palette)); -#ifdef WUSS_COMPONENTS - /* the colour menu snapshots the palette at create; bump the - * generation so spawn_colourmenu rebuilds it on the next open, - * once any chain it currently feeds has gone */ - g_palette_gen++; -#endif break; case wuss_INPUT_MOUSE_DOWN: @@ -778,9 +714,6 @@ static result_t run_wuss(const char *resources) /* 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); -#ifdef WUSS_COMPONENTS - wuss_colourmenu_destroy(g_colourmenu); -#endif bmfont_destroy(font); diff --git a/include/wuss/window.h b/include/wuss/window.h index b00d734c..f251fd8f 100644 --- a/include/wuss/window.h +++ b/include/wuss/window.h @@ -237,6 +237,18 @@ void wuss_window_invalidate(wuss_window_t *window, const box_t *local_box); #define wuss_window_invalidate_all(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. + * + * \param[in] window Window whose whole document changed. + */ +void wuss_window_invalidate_extent(wuss_window_t *window); + /** * Set a window's scroll offset: the point in virtual content space that * appears at the content area's top-left. Larger offsets bring later content diff --git a/libraries/wuss/test/tasks/swatches.c b/libraries/wuss/test/tasks/swatches.c index 87dbee48..cefd1b1d 100644 --- a/libraries/wuss/test/tasks/swatches.c +++ b/libraries/wuss/test/tasks/swatches.c @@ -15,6 +15,7 @@ #include "geom/point.h" #include "geom/size.h" #include "wuss/icon.h" +#include "wuss/menu.h" #include "swatches.h" @@ -29,15 +30,15 @@ #define SWATCHES_PAPER_RGB 0xFF, 0xFF, 0xFF -/* Redraw: plot one 4x4 PATTERN swatch per (pattern, colour) pair -- row = - * pattern, column = palette index used as the pattern's ink over white. No - * icons are retained; wuss_icon_plot resolves the palette live, so a palette - * swap just redraws. */ +/* Redraw: plot one PATTERN swatch per (pattern, colour) pair -- row = + * pattern, column = palette index used as the pattern's ink over task->paper + * (white until a colour is picked from the pop-up menu). No icons are + * retained; wuss_icon_plot resolves the palette live, so a palette or paper + * change just redraws. */ static result_t swatches_redraw(swatches_task_t *task, const wuss_event_t *event) { wuss_icon_spec_t spec; - wuss_colour_t paper; const box_t *bounds; point_t scroll; int pat, col; @@ -45,11 +46,10 @@ static result_t swatches_redraw(swatches_task_t *task, bounds = event->data.redraw.bounds; scroll = event->data.redraw.scroll; - paper = wuss_nearest_colour(task->wuss, SWATCHES_PAPER_RGB); memset(&spec, 0, sizeof(spec)); spec.type = wuss_ICON_TYPE_PATTERN; - spec.bg = paper; + spec.bg = task->paper; for (pat = 0; pat < screen_PATTERN__LIMIT; pat++) { @@ -76,8 +76,11 @@ result_t swatches_create(wuss_t *wuss, swatches_task_t *task) wuss_task_desc_t delegate_desc; result_t rc; - task->wuss = wuss; - task->window = NULL; + task->wuss = wuss; + task->window = NULL; + task->task = NULL; + task->colourmenu = NULL; + task->paper = wuss_nearest_colour(wuss, SWATCHES_PAPER_RGB); delegate_desc.handle = swatches_handle; delegate_desc.task_data = task; @@ -88,6 +91,14 @@ result_t swatches_create(wuss_t *wuss, swatches_task_t *task) free(task); /* nothing registered yet; the spawner will not free it */ return rc; } + task->task = delegate; + + rc = wuss_colourmenu_create(&task->colourmenu, wuss, "Colour"); + if (rc != result_OK) + { + wuss_task_destroy(delegate); + return rc; + } rc = wuss_window_create_placed(delegate, SIZE2D(SWATCHES_DOC_W, 140), @@ -99,6 +110,7 @@ result_t swatches_create(wuss_t *wuss, swatches_task_t *task) &task->window); if (rc != result_OK) { + wuss_colourmenu_destroy(task->colourmenu); wuss_task_destroy(delegate); /* unregister; its QUIT frees the task block */ return rc; } @@ -111,9 +123,10 @@ result_t swatches_create(wuss_t *wuss, swatches_task_t *task) } /* A SELECT click on a cell makes that cell -- its fill pattern, its palette - * colour as the ink, white paper -- the desktop backdrop. The grid has no - * retained icons, so the cell is found by arithmetic on the click point - * (virtual content space). */ + * colour as the ink, the current paper -- the desktop backdrop. A MENU click + * pops the colour menu; its pick becomes the paper. The grid has no retained + * icons, so the cell is found by arithmetic on the click point (virtual + * content space). */ static result_t swatches_click(swatches_task_t *task, const wuss_event_t *event) { @@ -121,8 +134,15 @@ static result_t swatches_click(swatches_task_t *task, point_t pt; int pat, col; - if (event->data.mouse.action != wuss_MOUSE_DOWN || - !(event->data.mouse.button & wuss_BUTTON_SELECT)) + if (event->data.mouse.action != wuss_MOUSE_DOWN) + return result_OK; + + if (event->data.mouse.button & wuss_BUTTON_MENU) + return wuss_menu_open(task->task, + wuss_colourmenu_menu(task->colourmenu), + wuss_get_pointer(task->wuss), NULL); + + if (!(event->data.mouse.button & wuss_BUTTON_SELECT)) return result_OK; pt = event->data.mouse.point; @@ -133,13 +153,32 @@ static result_t swatches_click(swatches_task_t *task, col >= SWATCHES_NCOLOURS || pat >= screen_PATTERN__LIMIT) return result_OK; - backdrop = (wuss_backdrop_t) wuss_BACKDROP_PATTERN( - (wuss_colour_t) col, - (screen_pattern_t) pat, - wuss_nearest_colour(task->wuss, SWATCHES_PAPER_RGB)); + backdrop = (wuss_backdrop_t) wuss_BACKDROP_PATTERN((wuss_colour_t) col, + (screen_pattern_t) pat, + task->paper); return wuss_set_backdrop(task->wuss, &backdrop); } +/* A pick from the colour menu this task opened: the chosen palette index + * becomes the paper the patterns mix over. */ +static result_t swatches_menu_select(swatches_task_t *task, + const wuss_event_t *event) +{ + wuss_colour_t picked; + int mine; + + picked = wuss_colourmenu_selected(task->colourmenu, event, &mine); + if (!mine) + return result_OK; + + task->paper = picked; + + /* every cell shares the new paper: dirty the whole document, not just the + * visible rectangle wuss_window_invalidate_all covers */ + wuss_window_invalidate_extent(task->window); + return result_OK; +} + result_t swatches_handle(wuss_window_t *window, const wuss_event_t *event, void *task_data) @@ -156,7 +195,11 @@ result_t swatches_handle(wuss_window_t *window, case wuss_EVENT_MOUSE: return swatches_click(task, event); + case wuss_EVENT_MENU_SELECT: + return swatches_menu_select(task, event); + case wuss_EVENT_QUIT: + wuss_colourmenu_destroy(task->colourmenu); free(task_data); /* calloc'd per instance by the spawner */ return result_OK; diff --git a/libraries/wuss/test/tasks/swatches.h b/libraries/wuss/test/tasks/swatches.h index 0fb723ce..71a6877c 100644 --- a/libraries/wuss/test/tasks/swatches.h +++ b/libraries/wuss/test/tasks/swatches.h @@ -5,17 +5,26 @@ #ifdef WUSS_APP +/* ponytail: no #ifdef WUSS_COMPONENTS guard -- it is PUBLIC on DPTLib and ON + * by default, so the wuss app always has the colourmenu component. */ +#include "wuss/component/colourmenu.h" +#include "wuss/task.h" #include "wuss/window.h" /* Grid of every (fill pattern, palette colour) pair: one row per built-in - * screen fill pattern, one column per system-palette entry, each cell a 4x4 + * screen fill pattern, one column per system-palette entry, each cell a * PATTERN icon packed edge to edge. The document is taller than the window - * so the grid scrolls through it. */ + * so the grid scrolls through it. A MENU-button click pops a wuss_colourmenu; + * the picked colour becomes the paper the patterns mix over (white until + * then). */ typedef struct swatches_task { - wuss_t *wuss; - wuss_window_t *window; + wuss_t *wuss; + wuss_window_t *window; + wuss_task_t *task; /* delegate; opens the colour menu */ + wuss_colourmenu_t *colourmenu; + wuss_colour_t paper; /* pattern bg; the "mixing" colour */ } swatches_task_t; diff --git a/libraries/wuss/window/invalidate.c b/libraries/wuss/window/invalidate.c index ac2c4c53..9586793c 100644 --- a/libraries/wuss/window/invalidate.c +++ b/libraries/wuss/window/invalidate.c @@ -285,3 +285,15 @@ void wuss_window_invalidate(wuss_window_t *window, const box_t *local_box) wuss__invalidate_clipped(window, &screen_box); } + +void wuss_window_invalidate_extent(wuss_window_t *window) +{ + box_t extent; + + extent.x0 = 0; + extent.y0 = 0; + extent.x1 = window->doc.w; + extent.y1 = window->doc.h; + + wuss_window_invalidate(window, &extent); +} From 941343801a2a8c88c36de2966065431094c64e5b Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Thu, 3 Sep 2026 19:57:20 +0100 Subject: [PATCH 223/287] test(wuss): use longer lorem ipsum paragraph in text task Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- libraries/wuss/test/tasks/text.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/libraries/wuss/test/tasks/text.c b/libraries/wuss/test/tasks/text.c index b66c686b..bc29a5c0 100644 --- a/libraries/wuss/test/tasks/text.c +++ b/libraries/wuss/test/tasks/text.c @@ -25,8 +25,7 @@ #endif static const char paragraph[] = - "Lorem ipsum dolor sit amet, consectetur adipiscing elit, " - "sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."; +"Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Donec mattis luctus libero. Donec imperdiet, velit quis venenatis iaculis, metus libero cursus ligula, egestas sagittis dui diam in mi."; result_t text_create(wuss_t *wuss, bmfont_t *font, From a793fc3b3e6bdba4bee2aa2c7d104b6d2b4fbfce Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Thu, 3 Sep 2026 20:40:13 +0100 Subject: [PATCH 224/287] fix(bmfont): reject malformed font grid instead of asserting extract_advance_widths asserted that the pixel cursor stayed within the decoded image. A malformed grid could walk it out of bounds; turn the two asserts into a runtime check that returns result_PARSE_ERROR before the next row is read. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- libraries/framebuf/bmfont/bmfont.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/libraries/framebuf/bmfont/bmfont.c b/libraries/framebuf/bmfont/bmfont.c index 0bee21f3..b4508972 100644 --- a/libraries/framebuf/bmfont/bmfont.c +++ b/libraries/framebuf/bmfont/bmfont.c @@ -175,8 +175,11 @@ static result_t extract_advance_widths(bmfont_t *bmfont, pixels += ((bmfont->gridheight - 1) * rowbytes) / sizeof(*pixels); - assert(pixels >= (unsigned int *) voidpixels); - assert(pixels <= (unsigned int *) voidpixels + (rowbytes * imgheight / 4)); + /* a malformed grid can walk pixels outside the decoded image; refuse it + * rather than read past the buffer on the next row */ + if (pixels < (unsigned int *) voidpixels || + pixels > (unsigned int *) voidpixels + (rowbytes * imgheight / 4)) + return result_PARSE_ERROR; } return rc; From a1dd67fd6eea7d58fa879a0d2bdc5a273e8f7b45 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Thu, 3 Sep 2026 20:40:18 +0100 Subject: [PATCH 225/287] refactor(wuss): drive the chars font picker from wuss_fontmenu Drop the hand-maintained chars_fonts[] table, the static menu-item array and CHARS_NFONTS. chars_create now builds a wuss_fontmenu over resources/bmfonts and sizes its font cache from the menu's item count; MENU_SELECT resolves via wuss_fontmenu_selected. The current row is no longer ticked -- the component owns its items as const. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- libraries/wuss/test/tasks/chars.c | 111 +++++++++++++++--------------- libraries/wuss/test/tasks/chars.h | 23 ++++--- 2 files changed, 68 insertions(+), 66 deletions(-) diff --git a/libraries/wuss/test/tasks/chars.c b/libraries/wuss/test/tasks/chars.c index 18b933ea..76209685 100644 --- a/libraries/wuss/test/tasks/chars.c +++ b/libraries/wuss/test/tasks/chars.c @@ -24,33 +24,8 @@ #define CHARS_ROWS 8 #define CHARS_PAD 2 -/* the fonts the picker offers: menu label and the leafname under - * resources/bmfonts. Index order is the menu order and the fonts[] slot. */ -static const struct -{ - const char *label; - const char *leaf; -} -chars_fonts[CHARS_NFONTS] = -{ - { "Digits", "digits" }, - { "Daydream", "daydream" }, - { "Glider Rider", "gliderrider" }, - { "Henry", "henry" }, - { "MS Sans Serif", "ms-sans-serif"}, - { "Tall", "tall" }, - { "Tiny", "tiny" } -}; - -/* rebuilt before every open so the tick tracks task->current */ -static wuss_menu_item_t chars_menu_items[CHARS_NFONTS]; -static const wuss_menu_t chars_menu = -{ - "Font", chars_menu_items, CHARS_NFONTS -}; - -/* keep the resources root the task was created with; the picker needs it to - * load a font on demand and chars_create does not stash it elsewhere. +/* keep the resources root the task was created with; the picker loads a font + * on demand by leafname and chars_create does not stash it elsewhere. * ponytail: one demo, one instance at a time -- a file-scope copy is fine. */ static char chars_resources[256]; @@ -66,8 +41,11 @@ static size2d_t chars_window_size(bmfont_t *font) (fh + CHARS_PAD * 2) * CHARS_ROWS); } -/* load fonts[idx] if not already in hand; returns it or NULL on failure */ -static bmfont_t *chars_load_font(chars_task_t *task, int idx) +/* load fonts[idx] if not already in hand; returns it or NULL on failure. + * name is the menu label for that row -- the font's leafname sans ".png". */ +static bmfont_t *chars_load_font(chars_task_t *task, + int idx, + const char *name) { const char *leaf; const char *filename; @@ -77,7 +55,7 @@ static bmfont_t *chars_load_font(chars_task_t *task, int idx) if (task->fonts[idx] != NULL) return task->fonts[idx]; - leaf = path_join_leafname(chars_fonts[idx].leaf, "png"); + leaf = path_join_leafname(name, "png"); filename = path_join_filename(chars_resources, 3, "resources", "bmfonts", leaf); @@ -89,15 +67,15 @@ static bmfont_t *chars_load_font(chars_task_t *task, int idx) return font; } -/* switch the grid to fonts[idx], resizing the window to suit */ -static result_t chars_set_font(chars_task_t *task, int idx) +/* switch the grid to the font at menu row idx (label name), resizing to suit */ +static result_t chars_set_font(chars_task_t *task, int idx, const char *name) { bmfont_t *font; - if (idx < 0 || idx >= CHARS_NFONTS || idx == task->current) + if (idx < 0 || idx >= task->nfonts || idx == task->current) return result_OK; - font = chars_load_font(task, idx); + font = chars_load_font(task, idx, name); if (font == NULL) return result_OK; /* leave the current font in place */ @@ -111,18 +89,10 @@ static result_t chars_set_font(chars_task_t *task, int idx) static result_t chars_open_menu(chars_task_t *task) { - int i; - - for (i = 0; i < CHARS_NFONTS; i++) - { - chars_menu_items[i].text = chars_fonts[i].label; - chars_menu_items[i].flags = (i == task->current) ? wuss_MENU_ITEM_TICKED - : wuss_MENU_ITEM_NONE; - chars_menu_items[i].submenu = NULL; - chars_menu_items[i].window = NULL; - } - - return wuss_menu_open(task->delegate, &chars_menu, + /* ponytail: no tick on the current row -- wuss_fontmenu owns its items as + * const and offers no way to set one. The window title tracks the font. */ + return wuss_menu_open(task->delegate, + wuss_fontmenu_menu(task->fontmenu), wuss_get_pointer(task->wuss), NULL); } @@ -132,10 +102,12 @@ result_t chars_create(wuss_t *wuss, const char *resources, chars_task_t *task) { - wuss_task_t *delegate; - wuss_task_desc_t delegate_desc; - result_t rc; - bmfont_t *font; + 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; font = wuss_get_font(wuss); if (font == NULL) @@ -149,10 +121,29 @@ result_t chars_create(wuss_t *wuss, task->wuss = wuss; task->font = font; - task->current = -1; /* the wuss system font is not one of chars_fonts[] */ + task->current = -1; /* the wuss system font is none of the picker's */ task->fg = colour_rgb(0x00, 0x00, 0x00); task->bg = colour_rgb(0xFF, 0xFF, 0xFF); + /* the picker: every ".png" font under resources/bmfonts, sorted */ + bmfonts_dir = path_join_filename(chars_resources, 2, "resources", "bmfonts"); + rc = wuss_fontmenu_create(&task->fontmenu, bmfonts_dir, "Font", NULL); + if (rc != result_OK) + { + free(task); /* nothing registered yet; the spawner will not free it */ + return rc; + } + + menu = wuss_fontmenu_menu(task->fontmenu); + task->nfonts = menu->nitems; + task->fonts = calloc((size_t) task->nfonts, sizeof(*task->fonts)); + if (task->nfonts > 0 && task->fonts == NULL) + { + wuss_fontmenu_destroy(task->fontmenu); + free(task); + return result_OOM; + } + /* chars_redraw paints every cell itself */ delegate_desc.handle = chars_handle; delegate_desc.task_data = task; @@ -160,7 +151,10 @@ result_t chars_create(wuss_t *wuss, rc = wuss_task_create(wuss, &delegate_desc, &delegate); if (rc != result_OK) { - free(task); /* nothing registered yet; the spawner will not free it */ + /* nothing registered yet; the spawner will not free it */ + wuss_fontmenu_destroy(task->fontmenu); + free(task->fonts); + free(task); return rc; } wuss_task_set_autoclose(delegate, 1); @@ -250,9 +244,11 @@ result_t chars_handle(wuss_window_t *window, { int i; - for (i = 0; i < CHARS_NFONTS; i++) + for (i = 0; i < cc->nfonts; i++) if (cc->fonts[i] != NULL) bmfont_destroy(cc->fonts[i]); + free(cc->fonts); + wuss_fontmenu_destroy(cc->fontmenu); free(cc); /* calloc'd per instance by the spawner */ } return result_OK; @@ -264,8 +260,13 @@ result_t chars_handle(wuss_window_t *window, return result_OK; case wuss_EVENT_MENU_SELECT: - if (event->data.menu_select.menu == &chars_menu) - return chars_set_font(cc, event->data.menu_select.index); + { + const char *name; + + name = wuss_fontmenu_selected(cc->fontmenu, event); + if (name != NULL) + return chars_set_font(cc, event->data.menu_select.index, name); + } return result_OK; case wuss_EVENT_REDRAW: diff --git a/libraries/wuss/test/tasks/chars.h b/libraries/wuss/test/tasks/chars.h index 9dd7b24a..6f26b534 100644 --- a/libraries/wuss/test/tasks/chars.h +++ b/libraries/wuss/test/tasks/chars.h @@ -7,24 +7,25 @@ #include "framebuf/bmfont.h" #include "framebuf/colour.h" +#include "wuss/component/fontmenu.h" #include "wuss/task.h" #include "wuss/window.h" -/* number of fonts offered by the Chars font-picker menu */ -#define CHARS_NFONTS 7 - /* displays every glyph (0-255) of a bitmap font as a 32x8 grid, so the whole * font can be eyeballed at a glance. A MENU click on the window opens a - * picker that swaps the font in place. */ + * picker -- a wuss_fontmenu over resources/bmfonts -- that swaps the font in + * place. */ typedef struct chars_task { - wuss_window_t *window; - wuss_task_t *delegate; /* the wuss task backing this window */ - wuss_t *wuss; /* for wuss_get_pointer when opening the menu */ - bmfont_t *font; /* currently shown; == fonts[current] */ - bmfont_t *fonts[CHARS_NFONTS]; /* lazily loaded, NULL until first pick */ - int current; /* index into fonts[] */ - colour_t fg, bg; + wuss_window_t *window; + wuss_task_t *delegate; /* the wuss task backing this window */ + wuss_t *wuss; /* for wuss_get_pointer when opening the menu */ + wuss_fontmenu_t *fontmenu; /* the font picker; owns the menu and names */ + bmfont_t *font; /* currently shown; == fonts[current] */ + bmfont_t **fonts; /* one slot per menu item, lazily loaded */ + int nfonts; /* length of fonts[]; == menu item count */ + int current; /* index into fonts[], or -1 for sysfont */ + colour_t fg, bg; } chars_task_t; From db649186dccf43b278149d64d5c3662dbd1eb283 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Thu, 3 Sep 2026 20:40:42 +0100 Subject: [PATCH 226/287] chore(bmfont): rename font fixtures to their display names Match the -font-suffix drop in 50f69e2: digits.png -> Digits-Regular.png (plus a new Digits-Bold.png), tall.png -> CookeTall.png, ms-sans-serif.png -> "MS Sans Serif.png", and refresh tiny.png. run_wuss now loads Digits-Regular. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- apps/wuss/main.c | 2 +- resources/bmfonts/CookeTall.png | Bin 0 -> 930 bytes resources/bmfonts/Digits-Bold.png | Bin 0 -> 771 bytes resources/bmfonts/Digits-Regular.png | Bin 0 -> 748 bytes resources/bmfonts/MS Sans Serif.png | Bin 0 -> 1802 bytes resources/bmfonts/digits.png | Bin 766 -> 0 bytes resources/bmfonts/ms-sans-serif.png | Bin 1789 -> 0 bytes resources/bmfonts/tall.png | Bin 892 -> 0 bytes resources/bmfonts/tiny.png | Bin 295 -> 378 bytes 9 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 resources/bmfonts/CookeTall.png create mode 100644 resources/bmfonts/Digits-Bold.png create mode 100755 resources/bmfonts/Digits-Regular.png create mode 100644 resources/bmfonts/MS Sans Serif.png delete mode 100644 resources/bmfonts/digits.png delete mode 100644 resources/bmfonts/ms-sans-serif.png delete mode 100644 resources/bmfonts/tall.png diff --git a/apps/wuss/main.c b/apps/wuss/main.c index b2471076..710130fb 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -548,7 +548,7 @@ static result_t run_wuss(const char *resources) logf_info("wuss: resources root = \"%s\"", resources); - leafname = path_join_leafname("digits", "png"); + leafname = path_join_leafname("Digits-Regular", "png"); filename = path_join_filename(resources, 3, "resources", "bmfonts", leafname); logf_info("wuss: loading font \"%s\"", filename); rc = bmfont_create(filename, &font); diff --git a/resources/bmfonts/CookeTall.png b/resources/bmfonts/CookeTall.png new file mode 100644 index 0000000000000000000000000000000000000000..bcb86838987ce027b376e59dfb06ea5774a2dac9 GIT binary patch literal 930 zcmV;T16}-yP)<h;3K|Lk000e1NJLTq00961001)r0{{R31%*CB0000CP)t-s00000 z0KmY&!1Dk6<||Z>00003bW%=J|Nj8j$_+sP0004EOGiWihy@);0009uNkl<ZNXM;~ zU2feV5QRtOtwNf8^r;J|#44`xn56`XHDnJ}o>+h-KX?IIOcy}s%zy#g`RPp|M!tZ* zFo)sTbbELxw+E>=T+dH>k6QMTQ<Z*<JmQ|#!i717r8tfumzCw}Rr_LyVPTScTru=< z>1lw-PNtjz=p$(XXtj^aF9V?4v4F}FmM{WjOvafrV;+9<0N@Vrg~A!Igiit1l7iRC zYe5h{U-JM!&&mmaG5}bgYyhla2~+_pW(9cDMF12?Ru_Y5ut@7VAO(m8eX|OONruyY z&cSp7Aj(=&$%2c5*#XgIy}JUC#*QnS4g!dxS(aJ>vZO5)-i{$8$&PHXx(>jI(=Y^n z1?VD5-y494)dmGrxwfeKwrxxkKp&ezFbyVvzX0%?0Ff^sj6DQ$-jcMYGTY;2PhV|K zP|E<cg9efbtjFX4z`o-M2_V?PT8B&=Lb$imOD(@&?kl85a<q%5YB8s=*7N*07)r#3 z$YrsL)CF2g{2Om=3`%I((CEBk=|*cI9_tL)`!4|y9lOwq?zaKFOmNO^eJqy_ki#u= z?fBmXSVBT7X~H!?seodW2`S|0Gk|E3!<<AculFN>G-f%v6%f@q27pYH;VXdOyhAUp z$Q=On<uEmXEou*edkCxm>su?pZ(RTX4)B)%h$!e2fE@1ud;!1{z+G))OQa*fodH}v z0{9&eLkFz9?*jKUE|$uq?<z}WY$sPMkpxx|%^x(8nc#%&Fd@GIa63SA9@(H)lFJ3) zzZuHCg>FyRh9d1`Cu$+Pq1+zz>*c0~^2HERbzpdHtWK<Ng2Ti#{mKv*Z^$qK=pO;} zC%saxo*1i5c})+ba?<UM)f07tcS|!z`sEBTB@zveG<fAD0J%VG{cO<zNgy{E3iqO~ z+0l`>TiOiZD?n)ge*i>eVywrRcs|a4a6}3XfKaN)3~BYf@G&6rH6Xra>w1g_z{+C? zaHJuGcl_W05Y62Tm|tz7`9TNt?X?f><{k~88blrv=>X6qQW3M+xeUZ3V5)529mCgf z8NW!{982X3fPKc&y8s#hvYLZE{6PC{==rXH=Q*wX1(JOYC)MUN#sB~S07*qoM6N<$ Eg2@V&SpWb4 literal 0 HcmV?d00001 diff --git a/resources/bmfonts/Digits-Bold.png b/resources/bmfonts/Digits-Bold.png new file mode 100644 index 0000000000000000000000000000000000000000..bc1590128292cfc5c46fc4d78067c6340ff492d5 GIT binary patch literal 771 zcmV+e1N{7nP)<h;3K|Lk000e1NJLTq00961001EZ0{{R3OPih200001b5ch_0Itp) z=>Px#1ZP1_K>z@;j|==^1poj5AY({UO#lFTCIA3{ga82g0001h=l}q9FaQARU;qF* zm;eA5aGbhPJOBUy3{Xr|Mf>;j000000RO<iz(hg4wg3PEX-PyuR7l6Il{=2yFbswn zAO}d3`)Gkp&w_+XAH{o=Ee)N*F4QN$5POI8Q!X5mdW<}?vzQ=}V2>n9pC#%i2U1F} zDZM~SD_Bi0T2orIwJg?m`>`yL)M|}nQv~Gu3ON*j0Ko=puYzI^ZpbA52#!{LabVUa zl_65ZuWz|eA!2=qGEy%#Za`9t18nF=xBzk(0pfrY>U3DK0diosIKT!NF90U%l@SAM zv=Sj}-5h{$(pd)C&`<5Bp>YQnfPE+1LR1KCL4Du=@?+%C0r~l!57ddlKI@*a<5F=p zYtI3tjz`!aI~FUz+{n}cXgerASONB3b}Q%Ef3z3<m-U&fKk>{_ruRv-o@Z^%)Bd)< zt1;@T;l_`Z?poHmw^CvM$5O<4EU&lXy(zY??7-y6Xpi2$3RT?|Oj-md6l^xBPAr98 zKhywy9LU=Ou01#`0rP(=8<`JIe+395Ab$XW3V;h>Sg%JL0fJWm#_|=dZ3lP+WNH8l zX$}WK->QF~9l(7D2G<~RzzHh?9`ZpSMbm!)aFO!}P(emkk@Cbbs&KV8M;nV)w7*NV z87AWM{HFHvY%}*N`A-G=Nd3Q3{=F2LdR`tSzFg<#_WhJDk*8@Q<LX)ve?JnP84m#I z6CVZq{Mv&_sQd%yz1M3j!Bj0Zxwl|yfTyQX2Q`7yZ7f()8|G~t@cd<rg--yQRtG@F zTYztjLj&+J7SvhRHz`}jknr&l;JAYkuu%bU%?^VTHSfBVMP(~4+i00Nnw(9_YSXs9 zyOec)Y%)4e*B3+AS4MU0dd?jw%XBT(*rMjnoIfok(y&f=?D7Br002ovPDHLkV1g;6 BL+bzl literal 0 HcmV?d00001 diff --git a/resources/bmfonts/Digits-Regular.png b/resources/bmfonts/Digits-Regular.png new file mode 100755 index 0000000000000000000000000000000000000000..89310f8c864d0ba6bbcc2b263fea057072b0f709 GIT binary patch literal 748 zcmV<I0u%j-P)<h;3K|Lk000e1NJLTq006)M001EZ0{{R3@~TX;00001b5ch_0Itp) z=>Px#1ZP1_K>z@;j|==^1poj5AY({UO#lFTCIA3{ga82g0001h=l}q9FaQARU;qF* zm;eA5aGbhPJOBUy3{Xr|Mf>;j000000RO<iz(hg4wg3PEQb|NXR5;6}l)sAOFc8L7 zteRlM7JeWftUDXVAmL_%o+4%N)%O%BgSm%LAbx;Agv;%(H(MFqr$2U_-MzqaVlWy> zKaWQ9Nv!LR-H~;_nNC=@Il$GM;}Ly~uiDX1!I)0->nl4a4!H8zftfTnDV+*oxIiZi z>Xew~Bwv~FKBf6fm4~^y$m=5Oi>&sBkyFWtXAY=K_A&$baRe#{Tna`>1bR-_0Kkzm zAPWj308S;A3h4n%T{-nh=5la5F6Rh50<y~c62Y^&B$EnEk=qqT;9P#np#tm^@R&am zM4<Uh<}IBO8XXS^o=F)2Ot~Bq#_ZHuYu~MF!|1+^waQvED!EtVUK#65O)VBGt73$; zQS!y0=CQ-<HyhKNI|X#Vy1RCB4|;QO5m0Rj<3d(c7w90rXA=ffA$hE{EOO0r55yr? zcAN}qNnF%X5je^S^hGSdV<7(*z&yZaATFvaS^&dWfZwP~<0DXt06^hl1&C!!(g73z z`~|X$5h_9aQ4U_7{5eFp;EyHU06PwHQgUq6q_#Vy;O>U)9$IMN4wHJH-nOlhyVj1a zFI@6H`wwaSaVq%PZSX*>bg@XoKPy-q<%tX4yF2b44tk_qx&h(d=*K|%6vbyyR3||v zo*+KlW0+XP=kKt7s+T&18nLyOi04by`w0A4wDc8_(gK7_zX1OIiNI|UC4!=sGLd}T z5?e*M11>%wElJy{fi%p%8JrgIa12lfq#>t9jmiXR>>JsJy4}4R+4>yGyX&b>(wNj* e0?H0wPW=Vs_HXm-lcOI10000<MNUMnLSTY61VlXm literal 0 HcmV?d00001 diff --git a/resources/bmfonts/MS Sans Serif.png b/resources/bmfonts/MS Sans Serif.png new file mode 100644 index 0000000000000000000000000000000000000000..969ba1e69084896336ec315b97e65e597e4d6808 GIT binary patch literal 1802 zcmV+l2le=gP)<h;3K|Lk000e1NJLTq00Dpi003eF0{{R3BvR}V0000CP)t-s^8fq* z00000z`(%3I%w4x000KLNkl<ZXx`OYL2lbR5M==$z>EI-i~wb?0w%i5DT==;)T4N5 z_!JT#?`6P3mOe$1RiMwA;gCa$a*{UgM~#6(kwZR`Lp}~iF~_~x8*}_&jtAq?$D`q# z1If|gE2q8M-e(qtD|$70n(ofBS}+;|9|hnVs$7d;24gPLG*BYb?v0rYs!b&B)dD8? zIW6?348Xlt8&fNQdI11ncJy>|Vofj|_?!#?Kmpjs)L<^#1Yj%yxDj;J=t(qtlK}M6 zY!?7J8bM82qh#_31_dyr0B6^&6o6|DV#OTbLaG{|TLExtuKQ<twZSSff|0DkY6-xJ zRBK28EWI{iX;DocL}mQ!4`PW<HQ19{e0Me&-260DZDj-jfQN6zykiOwMlk4ofHnYJ zMK=II05}n*eoT!VZQuard2VO?UE6tbe;(&2dUucO?0=kSNu18}JX=zJq1TBNIKj1E zMBiy9t1zDplg#NGB~v!@oqvu#56qy*rU&6gO6M$lNF`{xBK&c}v*|$?_l`Wu6K?w5 z2e;dh85-J#2ypoXJ?;33_N1lN?b5JS9N?ZC%$cM#kSMCTBD2Bg#@JotF<!MU1-_N7 zv}GtQ0G06o%~tZaUU@J%K)1=M-oS1<2v2dN5rY7jT$|4e0OJ8hlr23?x6NtTZQWM@ z8Ud(p0=Py7W{xF*)3DO0C3{p2Qj-R#YY)Kl&Nk2hJt_Cb1FI?e(u~{12)6kYIe<zs zxwI1-VBc#Pz|&_A;8Ihw3Q&oW9SK4snI!<%%U%ek*8mq@g4rr+A^>M<0G|*5oS9G9 zNUu)N2=HR(_Ib9B3tN0TnncRH&g~TwPctSk^BEHpmp)RO$kOFF?tFPc1(&d2g_{Ex zeK<oj>giU>{n_-ea5O{Cp8ZjFUXKeMMVVL{{86rDkZ3#e(etyEd$Bacji%or9@db` z+X2Rm?8XHEq+PfH05<`g7aFJN9sm^xz{Cd*ojDUNVV8K2rkN^s4dzU8G&Mv4jkrVX zO^tM9<x|8R2Q5^s5!$GQi<(rY68B331hyD7z@*x>5UYWluAYlhfKdR>#I+Z>!W2fJ zI}RoXs8;}n62N#1Af2KBP^|#?eD3uYUr#2EECS5JqZ1|AzeLt-y)(**@6VWUrOdlb zw2m^JBkMTzdY!+s&q|r?>ud_E3lUUlaSspa$}Y31l|;KJYKyt_do25=hrf;N{y4x1 z?hG()|8@X-ZySvF2H4$w1b@Aef36VAhk15)?LLm+zWwJB>`e@FdQX&tqsJd1M0zlC z*$<^A@nUtHHQf6D*SKj>oWK9;02g)Ea|HC%_<IBk^DgiFhJ@2PRJW7FD0h%pzoK@7 zSK+*saf7Hqfp-i?QU^JEI|=GBDwj4Jlwl(PAopACR3T)Q1ZE#<-@75bYFjmeTDrm# zV|J5-FV5m@VP`)jO!ctwiEtU_aogn-0eN|#MgS5pw&GOr2(USj;n%UtJDK8&Jx>$O z<2rZVm~&&kY*bj2r)3HOL$>Dz8#+rL18WVb<pzkatUr841K?8wfEoeUIW=Nch^y!} z0FQXA$p$rcI!+O&`R!GO^UY<x1VAh9s6h%&`MBvscl??B%a;7n@Oxg+tyMW++g#ya zZsjXJTjnB24^Ak3YFLvnpJTBUROC1O-gQ6(<;PO#L~+xl>|<DTAHPB|gBAj)2NiY= za`gE=Q17gIqP-W)J4$>$?Cr2oFWdMD!KFvFx;PVaB-6*i+&rEkZmYmHX8{BCJ*Sf3 zmTRa|ODJS^Y8`rjP7){<I5HZfQ^Vqr>I8{&95&M-LyB)+jBYmwq~S+(?HXx6=kZP2 z;ZOo_yToufG85f;4zMW$RB6sgk;z{Ks22cu7f*8rng#?2+#SVYb5@B4uq6PzDyKO; z#S0&b0MlzDs1^Xj3IOh8XZ=jZ*8p;R(g>ObfTi=%aMV1J*H+OI0D~vX5il2FKr|EE z;@w}#RW9#WzUET^RSQ$B&Uv9K-`0H7{>^Oqs+4`xpC9O(7Cy~8Hr=nY>HdB4$9>b` z!vO;CU&vbuNBr7(aAr+a3!qSim+1(K_aD*`tepn~X=b&SRf6aG?jfJwtmy%!rTmlU z=$+gfA3)VIgwUnP$T3tf{_}q-kc9~~zyxUkRcuf#0Qi|7ekc|xL?4+N0H7e2zNVJ} ziWP3KX;Hs3?*q6sMMVH-gN8heASdJhb;M-6h%)&P^ppBzAK7&F0aPtB1uCD~C>%q5 sUSreW-_PL|RA$qs-zL#V8nk=<2eu$D;f+%|IRF3v07*qoM6N<$g7XG6+W-In literal 0 HcmV?d00001 diff --git a/resources/bmfonts/digits.png b/resources/bmfonts/digits.png deleted file mode 100644 index 0a51d48b59e67408a726940814127d2dd0a72096..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 766 zcmV<a0s;MrP)<h;3K|Lk000e1NJLTq00961001EZ0{{R3OPih200001b5ch_0Itp) z=>Px#1ZP1_K>z@;j|==^1poj5AY({UO#lFTCIA3{ga82g0001h=l}q9FaQARU;qF* zm;eA5aGbhPJOBUy3{Xr|Mf>;j000000RO<iz(hg4wg3PEWJyFpR7l6Il{=0bF${(o zAO}d3`)Gkp&w_-?Im+x&wls8#nL>R6nAkg{ce$95)We$bdf_0Eps}8xB<d#(DW%tx zULd6fET<RE$y%kSjPKJ45~Pz=w7P(Rd|x1k0uUfrdrR9WR&Yfo@gms9Y*arSn6*l! zixlze+Z8~>T8J`IPu6ch(h&z(*N<=o<iOg)Q~=_DGwO6$umNn2G=Oz5UI9#dz)A}d zvgXwR2xpyTkQM#bejEDu1h!AMg{Tl(gWA9W<k!fd1M>4d8>kV3ZR?h>>ryc`Ys zk5{l)Ff3MpnUSdj&`wZnumEhEY?dyw|7a`vFKd~sJ@L#@ruRuSUuLb%)AqK#>tNJX zu-wUXcVww&D;c(bOhv5w^m;4an`G<C2~3WRR_ygfZLP{QtHT)u+n7`%mc*_fYJlDk z<aGhp9_*HY*}tWg%m(Mb0)!EeKL9`lz!fm8*Q<>H!2^J??4r4y0QZ1Q4PY+KVF&0( z_5Ila+)rR|bs`6xu_E9h8}wc@{TBciIgbDpWMma7PaLBPS6g$mzGz9?n?##oBEHOT zI)0gL=2j*DsbC+;|5wVtPerC4mq&@8j<LDzpVHOyG)!b%TnpmgN4zuR4j_Hvqkx}Z zyE6%ue*k^=`i#Yy>PQXl&6yhD>1otK4d8U^3l`Ldd20tee;Iw@6M%-*0g&+);0NQ- z0KE4FHI}ta$`&yse0&AipI`*6Q~+F~!(c>>yDnu>*^0|HT4atUXOpsyX<ffv$~r$b w8C|C1!_e``sP-M_+>x?ON2$i<HFxIx0dy|Yu2s#iegFUf07*qoM6N<$g5nuc8~^|S diff --git a/resources/bmfonts/ms-sans-serif.png b/resources/bmfonts/ms-sans-serif.png deleted file mode 100644 index 9ec9d04bd36ca51d1680e4cebb04646acaee9451..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1789 zcmV<Z1_JqsP)<h;3K|Lk000e1NJLTq00Dpi003eF0{{R3BvR}V0000CP)t-s^8fq* z00000z`(%3I%w4x000K8Nkl<Zc-rk+O{(KK5UvIvz>A*yl)-QBhKOFiQw+0e+(*eW z<P<h2dj%riGN)*=3K~?Ul1sLe%$t7cp<zN{<Z?-$em|8;f&CV?0Q>K--@$$h`#rE` zN3w_b%IRdbP0Cf_jLD3iFg#gqesBQzQ2+?g05}8-aFU{t5;N-&z(X|iBqM;qnBb>h z*!vlP2WTp26+pWHK!goF5#hj=2m>a~3;<C9m=0QmQ(XYKH2@2-#32KSJ_C$W?2rKl zYC%gXCrWOCRRFdGI0BdwfOA<H#W}!<Of|r817L(${Ug1aJSsR%5F=&<z=2F#EC39> zc6n-1PHsd4srRjn#Gncs$!vNz9asd{QMMZ`$N=1YBg5-TfZPJB*FhWujI-zt015yn za@V_Zkf$*KfDl3-;_uuCA3}HyZ}i@Wa1QCmiJruVa0$VX@s(aZ8E}GgUB&(qLNN>N zjg|baB|S5S@Z=xt_d90L$fi5tMM}?Ec2`Qa+`~TpIFZ<NCye_<9_0>)ar43Db~Fqf zEyD<K`h+8`cu#B6(CqdISSt<yz$$9Ie9a^p%_=~s;<Ey?nRtw6EjYkbN-bppr3IjY z1klx@uamKbIlxfOsUC1sTj41#biyD2CfDxs0ss<#i&E1QhI&rJ&{PiqIss_!0$7Yp zGvFG4s}t)YB@95*MQYLjZJPkN-+2r)z(~d;Bw*D=UpiMWT2RlssBNJUC4U`c3}W3| zw3z5~4&VTMcN3tIL2fF*PLyi^u1<Ris!fHaDNn)KEb1lzR%(Eh5P;Y-pJE}S+Ce9P zs|o!jm=Js)pQb0uJcs@q6K^3VqVf_Gp1;0Qn#j`kkvp%~XmAa|d=+laT=d}#(W$5D zegT{A7LI1tY}p@W=XJlZQIr|KW`C4(K_uFHuH(~v)1PE%oEDmX$7!?1QvNxBn~|Mc z0AN{#I{<hW;8d$FdIms)0^oVWp*_db6!r)&QZ!S=ri&UcTT@3A(1|<5-jt&)Yu-fz zOwM<9U=g7c+GvG~y3(c*_e%q0wy+w&t8xua*-&=Z#6=~56M!Re?MaR>D=p9!N0<Y& zHvsG!fV&4!c2NdsZU7`b*Se0c2bfysv}eaW>M6ngWn|4hIisBTc8Lie67Mq6o<nqG zFCm=I;T!v`r0gHFDc+ojsJ^bQVO!4Zbv8AU=og>*RW1Eq*L~C7??!gN9N>UY25|N7 z4q%==2JYDa8~A^a9{~L5pX}K^eI-BLHyy+QP0r<3UZX$yJ%>nlAcy_@6$&p_$652K z|38hJu8Qe?0^p?fdX9h|x^#|UIVjJyBXQ#OWS?#$2`6WeDSt!dX0M_Ja8*Q|d_r0= zt!%tD$Kr50+wj+}ief7j`5jAlh^y8#MPT-&O|`S-QQN2%v{Dt8DQ4#-d~p<KD^~VX z!Bj7m3T}!I(Ubri3I91h_=5`dZItWSNDE?d64q?nsmdGa;)Xp>6$NYojujj`_)@8` zCU=X^0Ye_oDi(B<I%d|g<;YpJ>9W2390wpiVRo327I2<JC!>nfEV>WCEp{!9K^rSQ zbrGum9aV<o-D$oAh@-fn0x39^;-;R?_<{UO_DA#YvY=a|G++C<!Czd;*ZAz2i)MEb zxOUZWCrWD70$jO2Sh||;+93M7j;W`(=~A{+SadtxLNSA`1W<P>?3!ht^eIpu?*nM< zjq0#C%7sCWlJNpO)zcNisZMj6CkQK@io~Zlm_<0^w5&4Q9M#D*(?%n~Er;o>I;aZ+ z#>*8=0D~l=805%k7JUZhkgCT*1`eC)kRio4PoT@qf@Szo9lIQ@=XrdSRyeK!G(df_ zlv$vn0I1dhno{E^GWjn7+66#7i<cTp-OzyI?nz86&M46UW(^=7l}pV?@xqr?0RN~3 z%>uyQ0EjEucXM%%0OsRKE$9{ihW1C`sCgleX3-KL22YkPU@nRQ(V3V&p8b{N!~2<! z`4m9a!W64DSIY9|E#Gu}wr~35BX6OWSM&SrGLky6>Gpl{%Y9S(j8i7l?1{Xk<D>oH z$d)P>;z|~trVbVFzmzSwwI5iroaI_($)4-Gn|yrdt!etZLLq<5HF_nFkOHV$Yz|$D zq!=p7^?xZi3lnMpk7WQ=#-LdM@N>TTMKO^=^p>ds5Ld*|ZTdREYKA*(x@teb`vC5B z(JFw2*l&%J1Ejx>z>F88_;f+<)s_9krXdATwQ!*Ft)GNrsFz!8YCqy0gcsR5o4)-t fiM~>!UqkpCwjeLzv!`9300000NkvXXu0mjfrFuSV diff --git a/resources/bmfonts/tall.png b/resources/bmfonts/tall.png deleted file mode 100644 index 3f9b68cba000d46f8b359e37a05c6178c53b4047..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 892 zcmV-?1B3jDP)<h;3K|Lk000e1NJLTq00961001)r0{{R31%*CB0000CP)t-s^8fq* z00000z`(%3I%w4x00004bW%=J|Ns90Kq{&90009UNkl<Zc-pPjy^-WL5QgE;PylBz zpq3&3Wfi()UMRXy)?_gX&_D+0BsK@t$N)J2Io$i%6XDDZSkG<)jgQp2ELxX!S=V)0 z*Y|JhvJX9HUdEzT>$1MB%ep8FLA6bZu%b*+?t`$Ru&@m$%n1wY^I=aQNI0*6Q{*@s z{v3VY*#XB%K-FfOjR3d+3J5a50MLP1Zm+;*KLun}o3$Ba6=nYVQU;(Xlc^vy0A!(j z0M4w<d<VihFt>G~;^wwlSX+aQtki*OPEl9`+CnwXDGG|Ub=oW}Kvl)uLR$wS6`^~; z$+Y@QTL)Ez+=><3fvjz+V!D=4l_)|PPaVk0O3K<Ox&`_oR`CH4$|_dWH*5+ZKew@- z0LYgT+8UgI`3B%aV8bmb_YyqKtx5M*-djx1_Qg3Cv~@rh8rHy>33~;?949Q})(%!4 zI&moBvqO*l`uFj<OY3?Q7vJ`Xd6_$Yd4C%MCCn|%_gVoJ0b*i0=E58RCZOG?)^5sj z#&HIozuy93Z5xkEw9hJ<4W*Q(K3CL%Ocnd_{}ixURaI-a1JDkr-Rwf1{AYk@l?&Y5 zX8Zy;;J6(?m{T+Wo*cJ3V4mg#Zr~PlAgrjW96<Z0o+Z#rV4BlDL*9XTV*mS3V7>)r zLrlB?o_H4U8vrLD-EHI1NLL`e0_aD;`~<=}ka_L`y^Ymc*#%B(W%fIYoG>Q#PnobR zM*$p%3AMwNo<JCi3EYapif(}aEl?hx4wP^AfwEkp=KXC9l=W><`}X@K)FYQ^PaA}a z&~<g{>02=rstP*+{)>SBQNO6{nHYOa&6brl8>+a4#h!^_W2Ci2`ZG1))JQ%g(vX$6 zKqhQ|k!qI<(hw;0OZAdS^hi4c+yOd(`2qyWiv2h{=i@vNiRg9!Vf*u1nXKBL3qJ;e zdmuiFbvOf=v4pBq3E#<s3lP@Ur32?jn;m)3fxf-%Mf*sP1E^9~Ff`Hy2y+XNTWN6) z#1%LV+Iz?FHmvcD<hZA`@(s{lWBt2;4?u7U6#wV(cs%|de);-%e7(Epxc&po*csSs S`41ET0000<MNUMnLSTZz_?IyN diff --git a/resources/bmfonts/tiny.png b/resources/bmfonts/tiny.png index b2d7770a7f284dc91522c0e30acf9d497f3adece..dc140588252dc046b1f3e527eefdf3bceb968cde 100644 GIT binary patch delta 317 zcmV-D0mA;L0{Q}wF*yWhK}|sb0I`n?{9y$E001CkNK#Dz0D2|>0Dy!50Qvv`0D$NK z0Cg|`0P0`>06Lfe02gqax=}olE*O8~Nkl<ZD9?qFv2Ddb3`14i04y{R3k{%xQ)bGL z=Kw#=hXunp*%Zwu*Uw8;ai#|ZD|G~EL`lRcDgx~p5(5i-Kk3el8(CAVX`l$Via_mg z#DR4>wpvcb0r3NmJus&-Qr7{~l?*I0vnLJ?;0yIi;Q_6ZT;se?P0A_q37>iB)c!Vx z&l|@{9djBbz_U97HiB4L;1*fL-BTRqI%ItJL|_`@SkRLUo|Rqvf_{K_T$<851+l%| z{ZKfF`#*}tW<lR6kf6_^|54o2hoZBHZxqPSu^<`j>UG!vrdZ%oI78_d$o!mHOaTvz P00000NkvXXu0mjf7KMU6 delta 254 zcmV<a00IB{0;d9yF@FhAOjJek|NH;|0002MC6bmW0002YNkl<Zc-n=~p{+y#5JFMJ z6o8n55VwF4RJm1t@xEBIc~}yXgFY{j0*m8FlBRBH+Qy?)3cVpQmSj9bG9bAgDPnEK zt}EkiTeopED@nbe7+VU?%wR@#Eh)mR94Sibc3O9oQLI;m+jm}DT0)-y(59AvkOZ4N zO$@-qgVZ+MQTtLTt)aH#k(6fvYy#wN48_Teq@}&4^e8k-U4z@H`mTs?Z+9PseZKuy ztdEs{D{MQ@(O-(A{!*;;TVY$0xV?>OC^*a626uhxAISWiS(I4HNdN!<07*qoM6N<$ Eg1K~a9smFU From c6280163b798aedbca2b808c7e1e01fb3c8aeb5c Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Thu, 3 Sep 2026 20:41:38 +0100 Subject: [PATCH 227/287] test(bmfont): update font fixture table for renamed PNGs MAXFONTS 7 -> 8 and the bmfonts[] names now match the display-name fixtures: CookeTall, MS Sans Serif, Digits-Regular, plus the new Digits-Bold. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- libraries/framebuf/bmfont/test/bmfont-test.c | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/libraries/framebuf/bmfont/test/bmfont-test.c b/libraries/framebuf/bmfont/test/bmfont-test.c index eb936cc2..52da3255 100644 --- a/libraries/framebuf/bmfont/test/bmfont-test.c +++ b/libraries/framebuf/bmfont/test/bmfont-test.c @@ -183,17 +183,18 @@ bmtestline_t; /* ----------------------------------------------------------------------- */ -#define MAXFONTS 7 +#define MAXFONTS 8 static bmtestfont_t bmfonts[MAXFONTS] = { - { "daydream", NULL }, - { "gliderrider", NULL }, - { "tiny", NULL }, - { "henry", NULL }, - { "tall", NULL }, - { "ms-sans-serif", NULL }, - { "digits", NULL } + { "Daydream", NULL }, + { "GliderRider", NULL }, + { "Tiny", NULL }, + { "Henry", NULL }, + { "CookeTall", NULL }, + { "MS Sans Serif", NULL }, + { "Digits-Regular", NULL }, + { "Digits-Bold", NULL } }; /* ----------------------------------------------------------------------- */ From 018888be0db4745cb7c27262b7e3f16b5fa94c8a Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Thu, 3 Sep 2026 23:18:49 +0100 Subject: [PATCH 228/287] refactor(bmfont): use logf_info/logf_error instead of BMFONT_DEBUG Drop the compile-time BMFONT_DEBUG switch and its fprintf(stderr) calls; route the same messages through base/debug.h's logf_info and logf_error. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- libraries/framebuf/bmfont/bmfont.c | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/libraries/framebuf/bmfont/bmfont.c b/libraries/framebuf/bmfont/bmfont.c index b4508972..557accb6 100644 --- a/libraries/framebuf/bmfont/bmfont.c +++ b/libraries/framebuf/bmfont/bmfont.c @@ -30,8 +30,6 @@ /* Input font PNGs must have 32 characters per row */ #define CHARS_PER_ROW (32) -#undef BMFONT_DEBUG - /* -------------------------------------------------------------------------- */ /* Pixel values */ @@ -403,18 +401,14 @@ result_t bmfont_create(const char *png, bmfont_t **pbmfont) png_get_IHDR(png_ptr, info_ptr, &pngwidth, &pngheight, &pngbitdepth, &pngcolourtype, NULL, NULL, NULL); -#ifdef BMFONT_DEBUG - fprintf(stderr, "bmfont load png: w=%d h=%d bit_depth=%d colour_type=%d\n", - (int) width, (int) height, bit_depth, colour_type); -#endif + logf_info("bmfont load png: w=%d h=%d bit_depth=%d colour_type=%d", + (int) pngwidth, (int) pngheight, pngbitdepth, pngcolourtype); /* we need a 2bpp paletted PNG */ if (pngbitdepth != 2 || pngcolourtype != PNG_COLOR_TYPE_PALETTE) { -#ifdef BMFONT_DEBUG - fprintf(stderr, "Incompatible PNG format: depth=%d, coltype=%d\n", - bit_depth, colour_type); -#endif + logf_error("bmfont: incompatible PNG format: depth=%d, coltype=%d", + pngbitdepth, pngcolourtype); rc = result_INCOMPATIBLE; goto cleanup; } @@ -437,9 +431,7 @@ result_t bmfont_create(const char *png, bmfont_t **pbmfont) if (gridheight == 0) { -#ifdef BMFONT_DEBUG - fprintf(stderr, "Can't determine font height\n"); -#endif + logf_error("bmfont: can't determine font height"); rc = result_BAD_ARG; goto cleanup; } @@ -459,11 +451,10 @@ result_t bmfont_create(const char *png, bmfont_t **pbmfont) bmfont->totalchars = CHARS_PER_ROW * pngheight / gridheight; bmfont->glyphrowbytes = (gridwidth + 7) / 8; /* byte width of stored glyphs (1 or 2) */ -#ifdef BMFONT_DEBUG - fprintf(stderr, "bmfont load png: charwidth=%d charheight=%d totalchars=%d glyphrowbytes=%d\n", - bmfont->charwidth, bmfont->charheight, - bmfont->totalchars, bmfont->glyphrowbytes); -#endif + logf_info("bmfont load png: charwidth=%d charheight=%d totalchars=%d " + "glyphrowbytes=%d", + bmfont->charwidth, bmfont->charheight, + bmfont->totalchars, bmfont->glyphrowbytes); { size_t pngrowbytes; From cbf1a2b2976b9370ac15468746b7d7a92f09b4ef Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Thu, 3 Sep 2026 23:26:25 +0100 Subject: [PATCH 229/287] fix(bmfont): verify grid cell height against the advance-width strip The old size detector took the first divisor of the image height at or above gridwidth. It could not see a cell height below gridwidth (missing Digits-Bold's 11) and, when gridwidth did not divide cleanly, latched onto a spurious large divisor and overflowed the pixel buffer in bmfont_draw. detect_gridheight now decodes the image first, then scans every divisor of the height from 2 up, accepting one only when the pixels confirm each row's role: strip rows (y % gh == 0) carry PIXEL_WIDTH_IDX and no glyph ink, glyph rows carry no PIXEL_WIDTH_IDX. A PNG that matches no verified height is rejected with result_BAD_ARG. CookeTall.png was a broken re-export (grid overlay on every row, ink and background swapped); restore it from the pre-rename tall.png. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- libraries/framebuf/bmfont/bmfont.c | 149 ++++++++++++++++++++--------- resources/bmfonts/CookeTall.png | Bin 930 -> 892 bytes 2 files changed, 106 insertions(+), 43 deletions(-) diff --git a/libraries/framebuf/bmfont/bmfont.c b/libraries/framebuf/bmfont/bmfont.c index 557accb6..3260910e 100644 --- a/libraries/framebuf/bmfont/bmfont.c +++ b/libraries/framebuf/bmfont/bmfont.c @@ -91,6 +91,74 @@ static int count_adw(unsigned char tab[256], pixelfmt_any_t adw_px) tab[(adw_px >> 24) & 0xFF]; } +/* -------------------------------------------------------------------------- */ + +/** True if row \p y of the 2bpp image holds at least one pixel of \p value. + * Pixels are packed four to a byte, most significant pair first. */ +static int row_has_pixel(const unsigned char *pixels, + size_t rowbytes, + png_uint_32 y, + png_uint_32 width, + int value) +{ + const unsigned char *row = pixels + rowbytes * y; + png_uint_32 x; + + for (x = 0; x < width; x++) + { + int px = (row[x >> 2] >> (6 - 2 * (x & 3))) & 3; + if (px == value) + return 1; + } + + return 0; +} + +/** + * Detect the grid cell height from the decoded pixels. + * + * The only unknown in the format is the cell height: 32 cells per row, each + * an advance-width strip row on top of the glyph rows. A strip row carries + * PIXEL_WIDTH_IDX pixels and no glyph ink (PIXEL_FG_IDX); the glyph rows + * carry no PIXEL_WIDTH_IDX. A candidate height is correct iff it divides the + * image and every row it implies matches its role. Returns 0 if nothing fits. + */ +static int detect_gridheight(const unsigned char *pixels, + size_t rowbytes, + png_uint_32 width, + png_uint_32 height) +{ + int gridwidth = (int) (width / CHARS_PER_ROW); + int gh; + + /* a cell is at least two rows (one strip, one glyph) and no taller than + * a generous multiple of its width */ + for (gh = 2; gh <= gridwidth * 3 && (png_uint_32) gh <= height; gh++) + { + png_uint_32 y; + int ok = 1; + + if ((height % (png_uint_32) gh) != 0) + continue; + + for (y = 0; y < height && ok; y++) + { + int has_adw = row_has_pixel(pixels, rowbytes, y, width, PIXEL_WIDTH_IDX); + int has_ink = row_has_pixel(pixels, rowbytes, y, width, PIXEL_FG_IDX); + + if ((y % (png_uint_32) gh) == 0) + ok = has_adw && !has_ink; /* strip row */ + else + ok = !has_adw; /* glyph row */ + } + + if (ok) + return gh; + } + + return 0; +} + /** Verify the font format and build the advance width table. */ static result_t extract_advance_widths(bmfont_t *bmfont, void *voidpixels, @@ -413,48 +481,7 @@ result_t bmfont_create(const char *png, bmfont_t **pbmfont) goto cleanup; } - /* work out the font size */ - - int gridwidth = pngwidth / CHARS_PER_ROW; - int gridheight = 0; /* to be determined */ - - { - int h; - - /* starting with gridwidth, increment until we find a height which evenly divides */ - for (h = gridwidth; h < gridwidth * 3; h++) - if ((pngheight % h) == 0) - { - gridheight = h; - break; - } - - if (gridheight == 0) - { - logf_error("bmfont: can't determine font height"); - rc = result_BAD_ARG; - goto cleanup; - } - } - - bmfont = calloc(1, sizeof(*bmfont)); - if (bmfont == NULL) - { - rc = result_OOM; - goto cleanup; - } - - bmfont->gridwidth = gridwidth; /* width of glyphs in grid */ - bmfont->gridheight = gridheight; /* height of glyphs in grid */ - bmfont->charwidth = gridwidth; /* currently same as gridwidth */ - bmfont->charheight = gridheight - 1; /* -1 to account for advance width pixel row */ - bmfont->totalchars = CHARS_PER_ROW * pngheight / gridheight; - bmfont->glyphrowbytes = (gridwidth + 7) / 8; /* byte width of stored glyphs (1 or 2) */ - - logf_info("bmfont load png: charwidth=%d charheight=%d totalchars=%d " - "glyphrowbytes=%d", - bmfont->charwidth, bmfont->charheight, - bmfont->totalchars, bmfont->glyphrowbytes); + /* decode the image; the size detector needs the pixels */ { size_t pngrowbytes; @@ -474,8 +501,44 @@ result_t bmfont_create(const char *png, bmfont_t **pbmfont) row_pointers[h] = pixels + pngrowbytes * h; png_read_image(png_ptr, row_pointers); + } + + /* work out the font size */ + + { + size_t pngrowbytes = png_get_rowbytes(png_ptr, info_ptr); + int gridwidth = pngwidth / CHARS_PER_ROW; + int gridheight = detect_gridheight(pixels, pngrowbytes, + pngwidth, pngheight); + + if (gridheight == 0) + { + logf_error("bmfont: can't determine font height"); + rc = result_BAD_ARG; + goto cleanup; + } + + bmfont = calloc(1, sizeof(*bmfont)); + if (bmfont == NULL) + { + rc = result_OOM; + goto cleanup; + } + + bmfont->gridwidth = gridwidth; /* width of glyphs in grid */ + bmfont->gridheight = gridheight; /* height of glyphs in grid */ + bmfont->charwidth = gridwidth; /* currently same as gridwidth */ + bmfont->charheight = gridheight - 1; /* -1 for the advance width row */ + bmfont->totalchars = CHARS_PER_ROW * pngheight / gridheight; + bmfont->glyphrowbytes = (gridwidth + 7) / 8; /* stored glyph row: 1 or 2 */ + + logf_info("bmfont load png: charwidth=%d charheight=%d totalchars=%d " + "glyphrowbytes=%d", + bmfont->charwidth, bmfont->charheight, + bmfont->totalchars, bmfont->glyphrowbytes); - rc = extract_advance_widths(bmfont, pixels, pngwidth, pngheight, pngrowbytes); + rc = extract_advance_widths(bmfont, pixels, pngwidth, pngheight, + pngrowbytes); if (rc) goto cleanup; diff --git a/resources/bmfonts/CookeTall.png b/resources/bmfonts/CookeTall.png index bcb86838987ce027b376e59dfb06ea5774a2dac9..3f9b68cba000d46f8b359e37a05c6178c53b4047 100644 GIT binary patch delta 864 zcmV-m1E2h&2mA(*DSz_+`~Uy|006+iz`#0a)ffN(00eYWPE-H?{{TQLsq_E<0~<+1 zK~#9!t=7Gf<Ten7;m=S2XE30aA^&9+x@2A`x=_|+F$&N?2IwR<2i3>`IRH7_``HuW z%nVr1ZUc>v)VeHMmvvd!by?T<Z|kxTJ!f9VqE+j%zOBo;D1Qq<wM~hzqD)clgRr8o zuni~72@C7<VNW1PIIn<H<TxAt9DUx|0mn%|)n=QG0Js1O2r|F`(1BTQufS$M1!Ps5 zwHag;W&Zk72B0XDsUS1}WTAWj&aBOR2f{iqw{@Z7=C)Z_TZ4_P)PZVFQCI`oLN(4Q z3W~LL+AJ(URe#0YLR$wS6`^~;$+Y@QTL)Ez+=><3fvjz+V!D=4l_)|PPaVk0O3K<O zx&`_oR`CH4$|_dWH*5+ZKew@-0LYgT+8UgI`3B%aV8bmb_YyqKtx5M*-djx1_Qg3C zv~@rh8rHy>33~;?949Q})(%!4I&moBvqO*l`uFj<OMmNn6Bpn1h<TYietCZz0wv5X z%=cOW6#-&mI_AP004AW_r`B%Da>j86p1<D$VQm|aOSI1_nhm9trao8HflL+q@&6RC zSyfeQxC77*sNL*Bp8RKkXq5}x+-Cd&IN-P)K$ufB0G=GTJ7AvX1a9CKbRev#svJQ3 zr=BIyOMhUR(>_Dqfq7#8`%hrL1!hA`ya1kf7VsMYCm`Kz<IzZ0AiV<UN5K39!a9(7 z?gG7y)mqsFPHScMJBpkzCiYL6uq;Ob9ES<D!<3#t7>Wtpio%L+fd4H}9-j`BZ})++ zT%zXvZ48w4ZBhI7`z6#PmuXKMgo@C0b?WI`F@F@Q3OfP*i-7-8zo_h)7<)|3mX$Rd zs<?&4o{3>&q_sr)Gd1AUNIoRekd?PUCTxF^YL^Sr5GeFZ^^!>RNIL`E0Xl&B0tCv6 z{Wv@4<2(<E=ym{M`}10vtlFLnKL&z(AU=w9I0KongsM{s-^qgu5Z2bE1LsGZ9eL1! zzFEEPMf*sP1E^9~Ff`Hy2y+XNTWN6)#1%LV+Iz?FHmvcD<hZA`@(s{lWBt2;4?u7U q6#wV(cs%|de);-%e7(Epxc&po*csSs`41ET0000<MNUMnLSTZkW|xNm delta 902 zcmV;119|-X2BHU$DSrR}0002Mz`(%r|NQ1FRF41v00VSVPE-H?0N2V5K>z>%0b)x> zL;#2d9Y_EG12suRK~zY`t(IMG-5?N!N9C<Tntk-C3#h~@uJV|r1c^0d4^^I6fF(b8 z0a;8JK<CVW0o(cMO(90UfWI(@;n;M0cqq3AsW)8DPkN79_J5I6m41vo;-1#Rg*k<# zIF2EgmF4PH`(lV;VUl}XG4yfiX@JO1rknxjBWVF>wU5g$1EAZnfXWh<Fal&u#+fr? z9)9xx;12MG!WpoHPXX4Fg4fAwK@dM*^8i54$_aon09c=F0IXmMQ~@ex1$fg%02D}8 z7lUcANb5Qv1%HSIeX|OONruyY&cSp7Aj(=&$%2c5*#XgIy}JUC#*QnS4g!dxS(aJ> zvZO5)-i{$8$&PHXx(>jI(=Y^n1?VD5-y494)dmGrxwfeKwrxxkKp&ezFbyVvzX0%? z0Ff^sj6DQ$-jcMYGTY;2PhV|KP|E<cg9efbtjFX4z<<8u2nitA!CHq*974Fa(n~GB zU+ycUMsl=^r)n{$vDWkaI2cOAhR9{Hiqr*KOZ*#eZ463i+0f{`V(CU}As*`t*!wR5 z5FNYFite`oyi9P;ZG9}44v@nwbM5%w1z18tDrv$sK&gOYlL;y0=re$5k;9xsEU)(? zfHY<~x_=cA)j0-$Oq1a&fZx1BFRsWP0QKcCHGnN@4}p6KtN`m<E5L7D|Njo~mjH+; z=o5e(?*V)Pz!Sh-ZDUKMBfy;jTs{K$9S}nYti106_cSh+%B1fqOJ!^)S1XYORuRn~ zG?AI$gzYdPzX5PNKyx11pjML01>nCK%DshdPk+~jBJE@+Y9YI!+#dDo<)()6#Sl_; zV0dk;PONW&!^AZG$`BWC$S?uu9|80yy;8287^_WrO%J4U((R4a6Lo`kOEX9M<qR+- z5)F<tc;zJkxj<|EY|#NpAU7Ba_oA=a(UG`Y+6>?;KxqJf07PVBtjC#nKF)q{L<$Xn zP=Bh(3~BYf@G&6rH6Xra>w1g_z{+C?aHJuGcl_W05Y62Tm|tz7`9TNt?X?f><{k~8 z8blrv=>X6qQW3M+xeUZ3V5)529mCgf8NW!{982X3fPKc&y8s#hvYLZE{6PC{==rXH c=Q*wX1(JOYC)MUN#sB~S07*qoM6N<$f|{3>)c^nh From 10453c244430a5bfa1ddf7b8d8bfc40cf8102ab3 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Thu, 3 Sep 2026 23:26:29 +0100 Subject: [PATCH 230/287] chore(bmfont): re-export Digits-Bold and Digits-Regular fixtures Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- resources/bmfonts/Digits-Bold.png | Bin 771 -> 834 bytes resources/bmfonts/Digits-Regular.png | Bin 748 -> 869 bytes 2 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 resources/bmfonts/Digits-Bold.png diff --git a/resources/bmfonts/Digits-Bold.png b/resources/bmfonts/Digits-Bold.png old mode 100644 new mode 100755 index bc1590128292cfc5c46fc4d78067c6340ff492d5..2ca5502f8084e1391181e85d7f59ec67668c3348 GIT binary patch delta 748 zcmV<I0u%j%2Eqm*iBL{Q4GJ0x0000DNk~Le0004i0000X0s{a50Ii;s(~%)$f2c`B zK~zY`#a26x+%ODfZjdJTagp>~fP~9A%Is0L9QYLL!UqU2Lhg`$%Ru2zw&c&uY&Jm* zLYDdQlSt`FFbpufz%V?&g0vv5%kXR>){Av6aB+S3?~C{hU?CppF-hc$cBXCt5G4VO zuxLV$=8q~P0+V*U71=uLi#uSBfA85whVlI~Ol=Giu(cu+MrRiHabPmH0C3y4ZVe!F z+5)5ka7Fuz=&=EiC@yXR;0E5zK>S=en;c-2f5{(F^=!Fv07=Q!0v%?=lI}<u?aW{W z;6t1Ir)V$)V`nue08|ns;SIp_^`RPQku9rK20nR`%lLfin%-_x^c8@tf1;_304o5m zGgAwIF@w1V;tx6Jl;e3j=lKz3U;1qA$@Qnq=ghr!ex0-xkwE=CtIv<D`fTB%U$5L< zp$B|s!S)mKKYq&Y-<6k|6^28%mDL$#o`KhvF<PeB_gcd)_MjG=F=6AGsv>JcrR;51 zV__@;vBhIKLQxW}Sb-rHe~(F6M~WI;{{ld_12EkJfC>Qa10>u5NO%CixQvUtNNxb& zKEOl`fRnbw?f~#!insu9I|Cnz5?ldLPBp7yz{hI<K|!w@0R7wo=$`H~+m4r3IG{H^ zP~}<s+EHcS8$GDCj}T`FZTTdcw{wK&ie}!y0e>d{gDQSrmQ5cQe+$@9#btH9SgiH% z2k~q8(oT#CLqp|mam1!~SvJk5aSf2#;Ew>%Cv3<r(Q@r!EHh}yZgH#bFE!aUX9MA8 zA5^yzx<M6wYQmZ-#unhIs{!DB0IAynnD7QbnIm@%0OLNu6#Cf7euSj%Zi?X18I05b z%!kCf$l12OQ$;c|LP&)SdcRv(XQm1^H_>RPrHW*vQa_2$2P=#Y7a~>Gqp15-kxqwO ezpt6>fvDfEsh&JdmLwto0000<MNUMnLSTZ|xmb?? delta 685 zcmV;e0#g0L27?A6iBL{Q4GJ0x0000DNk~Le000310000X0s{a5085*l(vcx#e`!fX zK~zY`t(7~D+%OD=86XEplly3aP0xaaN*~30lr0UN!Y<S&zz}<f^iwVzl6s6hv$L2W zkzkJ`N}na_CkIkWuPMDiN-J1RFIrPtw6!eOcl)s{kko38WK#s>`wBS}fB?Y;Yp;T0 z4{pdL{s@j%eQ{vcCY2#l#IJ9;e@`J|eTXtrFE(yKQi}s@=tsB!au@;PfD`I;Sg`?e zV7EBH1{f~@ChL_E18lSsA#2?nfN;`T2HDV0?Wdt}2N!^SC)+|)2yH=q-~jSt<j?{6 z`JNBdiNQYWp0MLmaW-qu0i}*d*dRL=E5O{y)B$KaC_Y#L_FZ->=h=U>e;56i^_i?c z@yt=C_er##XKl^X{<goXG3u(}#*dZmTGqO^QepqcQp9>Jueaj8DYmZcz~snikKVot zRoxX#S_CH)Y&NM*EQMV^)Bt@P$lC(0Jvb}@^M5NFnGa5X1qdS`e*l0AfD2$)uSXjJ zf>!{>@)fOZ2Y3WzY5)sqe+~yg->QF~9l(7D2G<~RzzHh?9`ZpSMbm!)aFO!}P(emk zk@Cbbs&KV8M;nV)w7*NV87AWM{HFHvY%}*N`A-G=Nd3Q3{=F2LdR`tSzFg<#_WhJD zk*8@Q<LX)ve?JnP84m#I6CVZq{Mv&_sQd%yz1M3j!Bj0Zxwl|ye}Jc_Q3o}F(`_tR zQXA%N9q{~RjD=4CnpOuu##?}Ij6(zPF&5NW);B3z#*pyw5#YFk5wKAKaLo>b6E*L; zltpDLF576CIhveJ%4*ZLzPprlerz&2PuCYi*H=b$?0U`}Da&*%)!3ru&YV9jCDO1? Tc<k~30000<MNUMn143Ye0#7YK diff --git a/resources/bmfonts/Digits-Regular.png b/resources/bmfonts/Digits-Regular.png index 89310f8c864d0ba6bbcc2b263fea057072b0f709..4e82093fa0752113be2c42430ee0049d6a516852 100755 GIT binary patch delta 783 zcmV+q1MvLp1?2`IiBL{Q4GJ0x0000DNk~Le0003X0000X0s{a500hRK0+AtPf6GZk zK~zY`wUs??<1i3~$JVMb?1c;J5~<vzXA$=dAi`CkQ>2!Gy@*Rer`Q0=0TzhhlL%WG z+&4qY6yu-7K$bC(@eKKSG&7H)c6Ya%wZDkk-HPUv!JxbCtev)O>}Ff`$ewAth0#P~ zJ8h03qTh5+i#2w?QUJY^qPfmEe-u0&?=e|PFS;-bGqLQ^5(;}twhEnF*&U7<9Q*N* znFxB+1?6h8rZ=sO7>_U{^rBak6FeTI;CdK?@cRkq-h=*Vg=1MBL2o#|&;v+Zb_~kP zNf|>7nkzY9nA`*qhz51Q9`;C$d?9p_(HLYIRAvh2S9mlSsF4R5{(1I9f6%MGQX@h8 z(g=u&p)-<^N?J1lp=+Er7jmc(7s8qmNHX#@!X9$2ce4Jvu=e<qwQ(WXxvuUv_}<rj zeW(!c+@VIO!av!wcQK<=!(0w|W1^#n+&3*C$5S7S-K_CO+oqwdH`?H7`1samgs$nF z$KMiWq=bQCfd{2U)$PKEe`Jq^A(O3@w6C0SXA2rYaq9iBOS83r)Z<zmyN-&yhs^?- zmr|qq+d<|lAU%NeMG&4b0bxW>Kz?eaH1;y6h@g%Osy~2=507R&sEi<~F)MSF5z0?D z*9e9_X0%wI2VuOczQXuh*Hu01;;Dup+~L{TdVhG{!&AAw>i35Yf0WSIu6DM1f>!zs z)WZ512J#afe9`z%68RaFL9-o<hQ<<UP9<{8XxcVn<-;|OYmh~+xY+uP`Y+xG1*IUl zqRenb+yK%#inc>X0b+@&q5VO$d_yl-v}G#VcroS6WR8rS=Ycvi*)-v+KG9kPEtiSb zu7k2N1tG^Xpmlg>WkpbV0xBY?V?$qjG`gOV2L*|UXIyN@<IfU-E-JheEF`kW%K;L> z>%vRK@*W3?bR&sW{U(lMcPf!Q4#c?Y=A`T96NzljjDIDcbRo55{R2s_!7lVKb|3%% N002ovPDHLkV1ny>b9Mj# delta 661 zcmV;G0&4x`2J8hPiBL{Q4GJ0x0000DNk~Le0002M0000X0s{a50P?C#vymZWe^N<A zK~y-)rIf#l<1i4$Rjis|!xnxZAFMkY#vtKlgPtN~@YVMeDTBF(P#}JQK!nTfuQyv6 z-KRfxoZY>^a$+zVNk5N9^GU4hj@^-UznM;0w>iMoo8u9EjIY|!Pr;Z@^Xn@+Cl0vs z*nyceHz}P8VYomi4C<7a<|JR4fAK!0`Ae0Dxw^>fBI}E+_J)yD$%tnTs7v-T1Nd<S zDhFH&Mo9#EPS*gykux9*3L^kcC6)^50Zd&v^-1P(a62yN2s{F^%KH+*v$`ac3QUpP z6-D4&e#)T&>=W>qKN3Ws`Ap_5oe>%x4+x$~839bW91_Ot)LLubt!u;Rf4+^i%33oj zxmV*}8S6|<Efy-PVuZC(^2MO$vBT^)8`GOR1$4i<yLNLAdUJ3QP;Ck0LRM54=pesm z69!Wud91W7a?NuO#35I9oD6D7T+~q!ILZk0MJ&K$ApaM@JiulkE~+b90K->+->6ID zBT$L}K;dEqh-FLC0TclIe+9CO5h_9aQ4U_7{5eFp;EyHU06PwHQgUq6q_#Vy;O>U) z9$IMN4wHJH-nOlhyVj1aFI@6H`wwaSaVq%PZSX*>bg@XoKPy-q<%tX4yF2b44tk_q zx&h(d=*K|%6vbyyR3||vo*+KlW0+XP=kKt7s+T&18nLyOi04byYx@ZNShVyNkkSH# zOTPgA{fWSB5ha46mNJoi+!9+wxC1UeAT3GTsev@iz8Rbr@Nf)J2c#jVMvck@Y3v)> vhPvIo8rk|B$-C>RPtuswS^~-rUrzl6<o0j#?31G(00000NkvXXu0mjf2?{O- From 556c8bf7acdd8699fb5aedd5dbb86f5ea82d1a56 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Thu, 3 Sep 2026 23:46:26 +0100 Subject: [PATCH 231/287] feat(wuss): let wuss_create take an array of fonts and icons pick one wuss_create now takes (bmfont_t *const *fonts, int nfonts) instead of a single font handle, storing up to wuss_MAX_FONTS (4) handles. Slot 0 is the system font used for titlebars and any icon that does not select another; unfilled slots are NULL. Icon flags gain a two-bit font-select field (wuss_ICON_FLAGS_FONT_MASK, bits 9-10) with wuss_ICON_FONT(n) / wuss_ICON_FONT_OF(f) helpers. wuss__icon_draw resolves the icon's requested weight once into the draw context, falling back to slot 0 when the slot is empty. Adds wuss_get_font_n(wuss, index); wuss_get_font stays as slot 0. The interactive wuss app now loads both Digits-Regular and Digits-Bold. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- apps/wuss/main.c | 39 ++++++++++++++++++++++----------- include/wuss/icon.h | 15 ++++++++++++- include/wuss/wuss.h | 37 +++++++++++++++++++++++++------ libraries/wuss/create.c | 19 ++++++++++++++-- libraries/wuss/furniture/draw.c | 6 ++--- libraries/wuss/get-font.c | 12 +++++++++- libraries/wuss/icon/draw.c | 35 ++++++++++++++++++----------- libraries/wuss/impl.h | 4 +++- libraries/wuss/menu/menu.c | 6 ++--- libraries/wuss/test/wuss-test.c | 12 +++++----- 10 files changed, 135 insertions(+), 50 deletions(-) diff --git a/apps/wuss/main.c b/apps/wuss/main.c index 710130fb..a60f7cc5 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -525,7 +525,9 @@ static result_t run_wuss(const char *resources) result_t rc; const char *leafname; const char *filename; - bmfont_t *font; + bmfont_t *fonts[2]; /* [0] regular (system font), [1] bold */ + int nfonts; + int i; void *pixels; int rowbytes; pixelfmt_t fmt; @@ -548,15 +550,25 @@ static result_t run_wuss(const char *resources) logf_info("wuss: resources root = \"%s\"", resources); - leafname = path_join_leafname("Digits-Regular", "png"); - filename = path_join_filename(resources, 3, "resources", "bmfonts", leafname); - logf_info("wuss: loading font \"%s\"", filename); - rc = bmfont_create(filename, &font); - if (rc != result_OK) { - logf_error("wuss: bmfont_create(\"%s\") failed, rc=0x%X (%s)", filename, - rc, result_string(rc)); - goto Failure; + static const char *const names[2] = { "Digits-Regular", "Digits-Bold" }; + + nfonts = 0; + for (i = 0; i < 2; i++) + { + leafname = path_join_leafname(names[i], "png"); + filename = path_join_filename(resources, 3, "resources", "bmfonts", + leafname); + logf_info("wuss: loading font \"%s\"", filename); + rc = bmfont_create(filename, &fonts[i]); + if (rc != result_OK) + { + logf_error("wuss: bmfont_create(\"%s\") failed, rc=0x%X (%s)", filename, + rc, result_string(rc)); + goto Failure; + } + nfonts++; + } } rc = wuss_frontend_open(scr_width, scr_height, palette, NELEMS(palette), @@ -580,8 +592,8 @@ static result_t run_wuss(const char *resources) fill_chrome_config(&config, use_wimp16 ? 1 : 0); - rc = wuss_create(&scr, font, palette, NELEMS(palette), &config, NULL, - &wuss); + rc = wuss_create(&scr, fonts, nfonts, palette, NELEMS(palette), &config, + NULL, &wuss); logf_info("wuss: wuss_create -> rc=0x%X (%s)", rc, result_string(rc)); if (rc != result_OK) goto Failure; @@ -591,7 +603,7 @@ static result_t run_wuss(const char *resources) g.palette = palette; g.npalette = NELEMS(palette); g.resources = resources; - g.daydream_font = font; /* the demo runs on the one font it loads */ + g.daydream_font = fonts[0]; /* tasks draw with the regular weight */ { wuss_task_desc_t desc; @@ -715,7 +727,8 @@ static result_t run_wuss(const char *resources) * that was still borrowing them */ wuss_menu_destroy(g_menu_desc); - bmfont_destroy(font); + for (i = 0; i < nfonts; i++) + bmfont_destroy(fonts[i]); wuss_frontend_close(frontend); diff --git a/include/wuss/icon.h b/include/wuss/icon.h index 0ebb3007..94b36f27 100644 --- a/include/wuss/icon.h +++ b/include/wuss/icon.h @@ -140,10 +140,23 @@ typedef enum wuss_icon_flags /** wuss_ICON_TYPE_MENU_ENTRY: draw a small colour chip (spec.swatch) in the * row's left gutter, where the tick would sit. Mutually exclusive with a * selected tick -- the chip wins. Ignored by other types. */ - wuss_ICON_FLAGS_SWATCH = 1 << 8 + wuss_ICON_FLAGS_SWATCH = 1 << 8, + /** Two-bit field (bits 9-10) selecting which of wuss_create's fonts draws + * this icon's text: 0 is the system font, 1-3 the further slots. Build it + * with wuss_ICON_FONT(n); read it with wuss_ICON_FONT_OF(flags). A slot + * that was not filled falls back to the system font. */ + wuss_ICON_FLAGS_FONT_MASK = 3 << 9 } wuss_icon_flags_t; +/** + * Encode font slot \p n (0..3) as icon flags; OR into wuss_icon_spec::flags. + */ +#define wuss_ICON_FONT(n) (((n) & 3) << 9) + +/** Decode the font slot (0..3) from an icon's flags. */ +#define wuss_ICON_FONT_OF(f) (((f) >> 9) & 3) + /** * Description of an icon at creation. Copied by value into the icon; the * caller keeps ownership of \c text, which is copied. diff --git a/include/wuss/wuss.h b/include/wuss/wuss.h index 7dcf8c8a..2511b43c 100644 --- a/include/wuss/wuss.h +++ b/include/wuss/wuss.h @@ -342,13 +342,23 @@ typedef struct wuss_config } wuss_config_t; +/** Most fonts wuss_create will take, and the range an icon's font-select + * flags can name (see wuss_ICON_FONT). */ +#define wuss_MAX_FONTS 4 + /** * Create a window manager. * * \param[in] scr Screen to draw windows onto. Not owned; must outlive * the wuss_t. - * \param[in] font Font used to draw titlebar labels, or NULL to draw - * titlebars unlabelled. Not owned. + * \param[in] fonts Up to \ref wuss_MAX_FONTS font handles, copied into + * the wuss_t (the handles, not the fonts -- they are + * not owned and must outlive it). Slot 0 is the system + * font, used for titlebar labels and any icon that does + * not select another. NULL, or nfonts 0, leaves + * titlebars unlabelled. + * \param[in] nfonts Number of entries in \p fonts, 0..\ref + * wuss_MAX_FONTS; more than that is an error. * \param[in] palette System palette, copied in, or NULL to use a built-in * default palette. * \param[in] npalette Number of entries in palette. Ignored if palette is @@ -358,12 +368,14 @@ wuss_config_t; * wuss_alloc (plain stdlib). Must outlive nothing -- * only the three function pointers are kept. * \param[out] wuss Newly created window manager. - * \return \ref result_OK on success, \ref result_WUSS_BAD_COLOUR if any of - * config's palette entries are out of range for the palette, or - * another appropriate result code. + * \return \ref result_OK on success, \ref result_BAD_ARG if \p nfonts is + * negative or exceeds \ref wuss_MAX_FONTS, \ref + * result_WUSS_BAD_COLOUR if any of config's palette entries are out + * of range for the palette, or another appropriate result code. */ result_t wuss_create(screen_t *scr, - bmfont_t *font, + bmfont_t *const *fonts, + int nfonts, const colour_t *palette, int npalette, const wuss_config_t *config, @@ -424,13 +436,24 @@ void wuss_destroy(wuss_t *doomed); /** * Fetch the system font (see wuss_create), for tasks to draw their own - * content in the same face as window titlebars. + * content in the same face as window titlebars. Equivalent to + * wuss_get_font_n(wuss, 0). * * \param[in] wuss Window manager. * \return System font, or NULL if none was given to wuss_create. */ bmfont_t *wuss_get_font(const wuss_t *wuss); +/** + * Fetch one of the fonts passed to wuss_create by slot. + * + * \param[in] wuss Window manager. + * \param[in] index Font slot, 0..\ref wuss_MAX_FONTS - 1. + * \return The font in that slot, or NULL if the slot is out of range or was + * not filled. + */ +bmfont_t *wuss_get_font_n(const wuss_t *wuss, int index); + /** * The last pointer position seen by wuss_mouse_click or wuss_mouse_move, * screen space. (0,0) until the first mouse event. Handy for opening a diff --git a/libraries/wuss/create.c b/libraries/wuss/create.c index 61f7d510..c9b78197 100644 --- a/libraries/wuss/create.c +++ b/libraries/wuss/create.c @@ -49,13 +49,15 @@ static result_t validate_bevel_backdrop(const wuss_t *w, #endif result_t wuss_create(screen_t *scr, - bmfont_t *font, + bmfont_t *const *fonts, + int nfonts, const colour_t *palette, int npalette, const wuss_config_t *config, const wuss_alloc_t *alloc, wuss_t **wuss) { + bmfont_t *font; wuss_alloc_t al; wuss_t *w; #ifdef WUSS_FURNITURE @@ -74,6 +76,11 @@ result_t wuss_create(screen_t *scr, assert(scr != NULL); assert(wuss != NULL); + if (nfonts < 0 || nfonts > wuss_MAX_FONTS || (nfonts > 0 && fonts == NULL)) + return result_BAD_ARG; + + font = (nfonts > 0) ? fonts[0] : NULL; + al = (alloc != NULL) ? *alloc : wuss_alloc; w = al.malloc(sizeof(*w)); @@ -236,7 +243,15 @@ result_t wuss_create(screen_t *scr, wuss__rebuild_palettecache(w); w->scr = scr; - w->font = font; + { + int i; + + for (i = 0; i < nfonts; i++) + w->fonts[i] = fonts[i]; + for (; i < wuss_MAX_FONTS; i++) + w->fonts[i] = NULL; + w->nfonts = nfonts; + } #ifdef WUSS_FURNITURE w->furniture.dragging = NULL; w->furniture.drag.x = 0; diff --git a/libraries/wuss/furniture/draw.c b/libraries/wuss/furniture/draw.c index d7433bf6..695bd2df 100644 --- a/libraries/wuss/furniture/draw.c +++ b/libraries/wuss/furniture/draw.c @@ -26,7 +26,7 @@ void wuss__furniture_draw(wuss_t *wuss, titlebar.x0, titlebar.y0, box_size(&titlebar), wuss->palette[wuss->furniture_colours.title.bg]); - if (wuss->font != NULL && window->title[0] != '\0') + if (wuss->fonts[0] != NULL && window->title[0] != '\0') { point_t pos; int text_x0, text_x1, titlelen, split_point; @@ -69,12 +69,12 @@ void wuss__furniture_draw(wuss_t *wuss, if (text_x1 > text_x0 && !box_intersection(&text_box, &clipped, &text_clip)) { titlelen = (int) strlen(window->title); - bmfont_measure(wuss->font, window->title, titlelen, text_x1 - text_x0, &split_point, &width); + bmfont_measure(wuss->fonts[0], 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(wuss->font, wuss->scr, window->title, titlelen, + bmfont_draw(wuss->fonts[0], 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; diff --git a/libraries/wuss/get-font.c b/libraries/wuss/get-font.c index f54fabd4..4e1160f0 100644 --- a/libraries/wuss/get-font.c +++ b/libraries/wuss/get-font.c @@ -8,5 +8,15 @@ bmfont_t *wuss_get_font(const wuss_t *wuss) { assert(wuss != NULL); - return wuss->font; + return wuss->fonts[0]; +} + +bmfont_t *wuss_get_font_n(const wuss_t *wuss, int index) +{ + assert(wuss != NULL); + + if (index < 0 || index >= wuss_MAX_FONTS) + return NULL; + + return wuss->fonts[index]; } diff --git a/libraries/wuss/icon/draw.c b/libraries/wuss/icon/draw.c index 39e2937a..62143b12 100644 --- a/libraries/wuss/icon/draw.c +++ b/libraries/wuss/icon/draw.c @@ -15,13 +15,15 @@ /* Shared per-draw state, resolved once by wuss__icon_draw and handed to each * type's draw helper. "b" is the icon's screen box; "fg" is its resolved - * foreground; "font_height" is 0 and "have_font" is 0 when there is no font or - * no text to draw. */ + * foreground; "font" is the icon's selected weight (falling back to slot 0); + * "font_height" is 0 and "have_font" is 0 when there is no font or no text to + * draw. */ typedef struct icon_draw_ctx { wuss_t *wuss; screen_t *scr; const wuss_icon_t *icon; + bmfont_t *font; box_t b; colour_t fg; int font_height; @@ -120,7 +122,7 @@ static void wuss__icon_draw_label(const icon_draw_ctx_t *c) if (interior_w < 1) interior_w = 1; - bmfont_measure(c->wuss->font, icon->text, (int) strlen(icon->text), + bmfont_measure(c->font, icon->text, (int) strlen(icon->text), interior_w, &split_point, &width); if (icon->flags & wuss_ICON_FLAGS_JUSTIFY_CENTRE) @@ -131,7 +133,7 @@ static void wuss__icon_draw_label(const icon_draw_ctx_t *c) pos.x = b->x0 + 1; pos.y = b->y0 + (b->y1 - b->y0 - c->font_height) / 2; - bmfont_draw(c->wuss->font, c->scr, icon->text, (int) strlen(icon->text), + bmfont_draw(c->font, c->scr, icon->text, (int) strlen(icon->text), c->fg, bg, &pos, NULL); } @@ -152,7 +154,7 @@ static void wuss__icon_draw_frame(const icon_draw_ctx_t *c) int split_point; bmfont_width_t width; - bmfont_measure(c->wuss->font, icon->text, (int) strlen(icon->text), + bmfont_measure(c->font, icon->text, (int) strlen(icon->text), (b->x1 - b->x0) - WUSS_FRAME_CAPTION_INSET * 2, &split_point, &width); cap_w = width; @@ -180,7 +182,7 @@ static void wuss__icon_draw_frame(const icon_draw_ctx_t *c) pos.x = cap_x; pos.y = b->y0; - bmfont_draw(c->wuss->font, c->scr, icon->text, (int) strlen(icon->text), + bmfont_draw(c->font, c->scr, icon->text, (int) strlen(icon->text), c->fg, bg, &pos, NULL); } } @@ -233,7 +235,7 @@ static void wuss__icon_draw_button(const icon_draw_ctx_t *c) if (interior_w < 1) interior_w = 1; - bmfont_measure(c->wuss->font, icon->text, (int) strlen(icon->text), + bmfont_measure(c->font, icon->text, (int) strlen(icon->text), interior_w, &split_point, &width); pos.x = b->x0 + ((b->x1 - b->x0) - width) / 2; @@ -244,7 +246,7 @@ static void wuss__icon_draw_button(const icon_draw_ctx_t *c) pos.y += 1; } - bmfont_draw(c->wuss->font, c->scr, icon->text, (int) strlen(icon->text), + bmfont_draw(c->font, c->scr, icon->text, (int) strlen(icon->text), label, base, &pos, NULL); } } @@ -319,13 +321,13 @@ static void wuss__icon_draw_radio_option(const icon_draw_ctx_t *c) if (interior_w < 1) interior_w = 1; - bmfont_measure(c->wuss->font, icon->text, (int) strlen(icon->text), + bmfont_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->wuss->font, c->scr, icon->text, (int) strlen(icon->text), + bmfont_draw(c->font, c->scr, icon->text, (int) strlen(icon->text), glyph, bg, &pos, NULL); } } @@ -422,7 +424,7 @@ static void wuss__icon_draw_menu_entry(const icon_draw_ctx_t *c) pos.x = b->x0 + pad + c->font_height; /* leave room for a tick */ pos.y = b->y0 + (b->y1 - b->y0 - c->font_height) / 2; - bmfont_draw(c->wuss->font, c->scr, icon->text, (int) strlen(icon->text), + bmfont_draw(c->font, c->scr, icon->text, (int) strlen(icon->text), ink, ground, &pos, NULL); } } @@ -451,6 +453,7 @@ void wuss__icon_draw(wuss_t *wuss, { icon_draw_ctx_t c; int font_width; + int fontidx; if (icon->flags & wuss_ICON_FLAGS_HIDDEN) return; @@ -465,9 +468,15 @@ void wuss__icon_draw(wuss_t *wuss, c.icon = icon; c.fg = wuss->palette[icon->fg]; - c.have_font = (wuss->font != NULL && icon->text[0] != '\0'); + /* pick the icon's requested weight; fall back to the system font */ + fontidx = wuss_ICON_FONT_OF(icon->flags); + c.font = wuss->fonts[fontidx]; + if (c.font == NULL) + c.font = wuss->fonts[0]; + + c.have_font = (c.font != NULL && icon->text[0] != '\0'); if (c.have_font) - bmfont_get_info(wuss->font, &font_width, &c.font_height); + bmfont_get_info(c.font, &font_width, &c.font_height); else font_width = c.font_height = 0; NOT_USED(font_width); diff --git a/libraries/wuss/impl.h b/libraries/wuss/impl.h index 6e79519f..2d3b9671 100644 --- a/libraries/wuss/impl.h +++ b/libraries/wuss/impl.h @@ -87,7 +87,9 @@ struct wuss_task struct wuss { screen_t *scr; - bmfont_t *font; /* nullable, not owned */ + bmfont_t *fonts[wuss_MAX_FONTS]; /* slot 0 is the system + * font; unset slots NULL; not owned */ + int nfonts; /* entries filled from wuss_create */ wuss_alloc_t alloc; /* malloc/realloc/free hooks, copied in * by wuss_create; used for every heap * block this wuss_t owns */ diff --git a/libraries/wuss/menu/menu.c b/libraries/wuss/menu/menu.c index a3e78538..917499bd 100644 --- a/libraries/wuss/menu/menu.c +++ b/libraries/wuss/menu/menu.c @@ -447,7 +447,7 @@ static result_t wuss__menu_spawn(wuss_t *wuss, assert(wuss != NULL); assert(menu != NULL); - assert(wuss->font != NULL); + assert(wuss->fonts[0] != NULL); if (menu->nitems <= 0) return result_WUSS_BAD_ICON; @@ -464,7 +464,7 @@ static result_t wuss__menu_spawn(wuss_t *wuss, outline_px = wuss__outline_px_for(menu_flags); titlebar_height = wuss__titlebar_height_for(wuss, menu_flags); - bmfont_get_info(wuss->font, NULL, &fh); + bmfont_get_info(wuss->fonts[0], NULL, &fh); pitch = fh + 2 * WUSS_MENU_ROW_PAD; sep_h = 2 * WUSS_MENU_ROW_PAD; @@ -486,7 +486,7 @@ 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->font, text, len, + if (bmfont_measure(wuss->fonts[0], text, len, INT_MAX, &split, &w) == result_OK && (int) w > widest) widest = (int) w; } diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index b7df3cf2..3df22556 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -385,7 +385,7 @@ 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; - rc = wuss_create(&scr, NULL, NULL, 0, &bad_config, NULL, &bad_wuss); + rc = wuss_create(&scr, NULL, 0, NULL, 0, &bad_config, NULL, &bad_wuss); if (rc != result_WUSS_BAD_COLOUR) goto Failure; @@ -394,7 +394,7 @@ result_t wuss_test(const char *resources) { wuss_t *custom_wuss; - rc = wuss_create(&scr, NULL, custom_palette, 2, NULL, NULL, &custom_wuss); + rc = wuss_create(&scr, NULL, 0, custom_palette, 2, NULL, NULL, &custom_wuss); if (rc != result_OK) goto Failure; wuss_destroy(custom_wuss); @@ -402,7 +402,7 @@ result_t wuss_test(const char *resources) printf("test: wuss_create with default palette\n"); - rc = wuss_create(&scr, NULL, NULL, 0, NULL, NULL, &wuss); + rc = wuss_create(&scr, NULL, 0, NULL, 0, NULL, NULL, &wuss); if (rc != result_OK) goto Failure; @@ -428,7 +428,7 @@ result_t wuss_test(const char *resources) symcfg.furniture.title.fg = wuss_COLOUR_WHITE; /* -> index 3 */ symcfg.backdrop = (wuss_backdrop_t) wuss_BACKDROP_COLOUR(wuss_COLOUR_GREEN); /* -> 1 */ - rc = wuss_create(&scr, NULL, sympal, 5, &symcfg, NULL, &symw); + rc = wuss_create(&scr, NULL, 0, sympal, 5, &symcfg, NULL, &symw); if (rc != result_OK) goto Failure; @@ -3852,7 +3852,7 @@ ColourMenuOK: ; if (rc != result_OK) goto FlashFailFree; screen_for_bitmap(&fscr, &fbm); - rc = wuss_create(&fscr, font, NULL, 0, NULL, NULL, &fwuss); + rc = wuss_create(&fscr, &font, 1, NULL, 0, NULL, NULL, &fwuss); if (rc != result_OK) goto FlashFailFree; memset(&ftc, 0, sizeof(ftc)); @@ -3987,7 +3987,7 @@ result_t wuss_test(const char *resources) printf("test: wuss_create (core)\n"); - rc = wuss_create(&scr, NULL, NULL, 0, NULL, NULL, &wuss); + rc = wuss_create(&scr, NULL, 0, NULL, 0, NULL, NULL, &wuss); if (rc != result_OK) goto Failure; From a908daff43f242fcac7fdc7f6153560361f8e15d Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Thu, 3 Sep 2026 23:47:08 +0100 Subject: [PATCH 232/287] chore(bmfonts): drop stray exec bit on Digits-Bold.png Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- resources/bmfonts/Digits-Bold.png | Bin 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 resources/bmfonts/Digits-Bold.png diff --git a/resources/bmfonts/Digits-Bold.png b/resources/bmfonts/Digits-Bold.png old mode 100755 new mode 100644 From 6088fb59b00c5cd45f062a8c14128c565aea8ada Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Thu, 3 Sep 2026 23:47:28 +0100 Subject: [PATCH 233/287] test(wuss): draw the icons demo heading in the bold font Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- libraries/wuss/test/tasks/icons.c | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/libraries/wuss/test/tasks/icons.c b/libraries/wuss/test/tasks/icons.c index 939198d2..20b7826a 100644 --- a/libraries/wuss/test/tasks/icons.c +++ b/libraries/wuss/test/tasks/icons.c @@ -102,12 +102,13 @@ result_t icons_create(wuss_t *wuss, /* icons sit past the ruler gutter (see ICONS_GUTTER in icons_redraw) so the * axis labels have the top/left strip to themselves */ - /* [0] a heading label */ - specs[0].bbox = (box_t) BOX_POS_SIZE(28, 28, 180, 14); - specs[0].type = wuss_ICON_TYPE_LABEL; - specs[0].text = "Work-area icons:"; - specs[0].fg = black; - specs[0].bg = wuss_NO_BACKGROUND; + /* [0] a heading label, drawn in the bold weight (font slot 1) */ + specs[0].bbox = (box_t) BOX_POS_SIZE(28, 28, 180, 14); + specs[0].type = wuss_ICON_TYPE_LABEL; + specs[0].text = "Work-area icons:"; + specs[0].fg = black; + specs[0].bg = wuss_NO_BACKGROUND; + specs[0].flags = wuss_ICON_FONT(1); /* [1] the button that bumps the counter -- a default action button, so it * shows the accent styling */ From 0089dbfa8397bb88bed9a98f73fe05d4e6533a49 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Thu, 3 Sep 2026 23:49:54 +0100 Subject: [PATCH 234/287] feat(wuss): draw window titles in the bold font when one is supplied wuss__furniture_draw resolves the titlebar font once: font slot 1 when a second handle was passed to wuss_create, else slot 0. wuss_create sizes the auto titlebar height to whichever weight is taller. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- libraries/wuss/create.c | 10 ++++++++++ libraries/wuss/furniture/draw.c | 19 +++++++++++++------ 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/libraries/wuss/create.c b/libraries/wuss/create.c index c9b78197..03eb4f6f 100644 --- a/libraries/wuss/create.c +++ b/libraries/wuss/create.c @@ -194,8 +194,18 @@ result_t wuss_create(screen_t *scr, } else if (font != NULL) { + /* titles draw in the bold weight when one was supplied; size the + * titlebar to whichever weight is taller */ bmfont_get_info(font, &font_width, &font_height); NOT_USED(font_width); + if (nfonts > 1 && fonts[1] != NULL) + { + int titleh; + + bmfont_get_info(fonts[1], NULL, &titleh); + if (titleh > font_height) + font_height = titleh; + } w->titlebar_height = font_height + 4; } else diff --git a/libraries/wuss/furniture/draw.c b/libraries/wuss/furniture/draw.c index 695bd2df..1e362718 100644 --- a/libraries/wuss/furniture/draw.c +++ b/libraries/wuss/furniture/draw.c @@ -11,9 +11,16 @@ void wuss__furniture_draw(wuss_t *wuss, wuss_window_t *window, const box_t *full) { - box_t visible_clipped; - box_t clipped; - box_t titlebar; + 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]; if (box_intersection(&window->visible, full, &visible_clipped)) return; /* offscreen */ @@ -26,7 +33,7 @@ void wuss__furniture_draw(wuss_t *wuss, titlebar.x0, titlebar.y0, box_size(&titlebar), wuss->palette[wuss->furniture_colours.title.bg]); - if (wuss->fonts[0] != NULL && window->title[0] != '\0') + if (titlefont != NULL && window->title[0] != '\0') { point_t pos; int text_x0, text_x1, titlelen, split_point; @@ -69,12 +76,12 @@ void wuss__furniture_draw(wuss_t *wuss, if (text_x1 > text_x0 && !box_intersection(&text_box, &clipped, &text_clip)) { titlelen = (int) strlen(window->title); - bmfont_measure(wuss->fonts[0], window->title, titlelen, text_x1 - text_x0, &split_point, &width); + 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(wuss->fonts[0], wuss->scr, window->title, titlelen, + 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; From 7bc15531caf1497f1f9b068f5554870219d6e6e9 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Fri, 4 Sep 2026 00:24:05 +0100 Subject: [PATCH 235/287] test(wuss): add a font picker to the Lorem Ipsum task text_create now takes the resources root instead of a font handle and builds a wuss_fontmenu over resources/bmfonts, mirroring the chars task. A MENU click opens the picker; the selection swaps the paragraph font (lazily loaded, freed on QUIT) and invalidates the window. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- apps/wuss/main.c | 2 +- libraries/wuss/test/tasks/text.c | 131 +++++++++++++++++++++++++-- libraries/wuss/test/tasks/text.h | 35 ++++--- resources/bmfonts/Digits-Bold.png | Bin 834 -> 852 bytes resources/bmfonts/Digits-Regular.png | Bin 869 -> 895 bytes 5 files changed, 146 insertions(+), 22 deletions(-) mode change 100644 => 100755 resources/bmfonts/Digits-Bold.png diff --git a/apps/wuss/main.c b/apps/wuss/main.c index a60f7cc5..e644d857 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -82,7 +82,7 @@ static result_t spawn_text(void) text_task_t *t = calloc(1, sizeof(*t)); result_t rc; if (t == NULL) return result_OOM; - rc = text_create(g.wuss, g.daydream_font, t); + rc = text_create(g.wuss, g.resources, t); if (rc != result_OK) return rc; if (t->window == NULL) { free(t); return rc; } return result_OK; diff --git a/libraries/wuss/test/tasks/text.c b/libraries/wuss/test/tasks/text.c index bc29a5c0..df31c6d4 100644 --- a/libraries/wuss/test/tasks/text.c +++ b/libraries/wuss/test/tasks/text.c @@ -1,4 +1,4 @@ -/* wuss/test/tasks/text.c -- static paragraph task */ +/* wuss/test/tasks/text.c -- static paragraph task with a font picker */ #ifdef WUSS_APP @@ -16,7 +16,9 @@ #include "framebuf/palettes.h" #include "geom/box.h" #include "geom/point.h" +#include "io/path.h" #include "text/bmtext.h" +#include "wuss/menu.h" #include "text.h" @@ -27,33 +29,118 @@ static const char paragraph[] = "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Donec mattis luctus libero. Donec imperdiet, velit quis venenatis iaculis, metus libero cursus ligula, egestas sagittis dui diam in mi."; -result_t text_create(wuss_t *wuss, - bmfont_t *font, - text_task_t *task) +/* ----------------------------------------------------------------------- */ + +/* load fonts[idx] if not already in hand; returns it or NULL on failure. + * name is the menu label for that row -- the font's leafname sans ".png". */ +static bmfont_t *text_load_font(text_task_t *task, + const char *resources, + int idx, + const char *name) { - wuss_task_t *delegate; - wuss_task_desc_t delegate_desc; - size2d_t sz; + const char *leaf; + const char *filename; + bmfont_t *font; result_t rc; - task->font = font; + if (task->fonts[idx] != NULL) + return task->fonts[idx]; + + leaf = path_join_leafname(name, "png"); + filename = path_join_filename(resources, 3, "resources", "bmfonts", leaf); + + rc = bmfont_create(filename, &font); + if (rc != result_OK) + return NULL; + + task->fonts[idx] = font; + return font; +} + +/* switch the paragraph to the font at menu row idx (label name) */ +static result_t text_set_font(text_task_t *task, int idx, const char *name) +{ + bmfont_t *font; + + if (idx < 0 || idx >= task->nfonts || idx == task->current) + return result_OK; + + font = text_load_font(task, task->resources, idx, name); + if (font == NULL) + return result_OK; /* leave the current font in place */ + + task->font = font; + task->current = idx; + + wuss_window_invalidate_all(task->window); + return result_OK; +} + +static result_t text_open_menu(text_task_t *task) +{ + return wuss_menu_open(task->delegate, + wuss_fontmenu_menu(task->fontmenu), + wuss_get_pointer(task->wuss), NULL); +} + +/* ----------------------------------------------------------------------- */ + +result_t text_create(wuss_t *wuss, + const char *resources, + text_task_t *task) +{ + 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); + task->current = -1; /* the wuss system font is none of the picker's */ task->bg = colour_rgb(0xFF, 0xFF, 0xFF); task->fg = colour_rgb(0x00, 0x00, 0x00); task->frame_count = 0; task->resizing = true; + strncpy(task->resources, resources, sizeof(task->resources) - 1); + task->resources[sizeof(task->resources) - 1] = '\0'; + + /* the picker: every ".png" font under resources/bmfonts, sorted */ + bmfonts_dir = path_join_filename(task->resources, 2, "resources", "bmfonts"); + rc = wuss_fontmenu_create(&task->fontmenu, bmfonts_dir, "Font", NULL); + if (rc != result_OK) + { + free(task); /* nothing registered yet; the spawner will not free it */ + return rc; + } + + menu = wuss_fontmenu_menu(task->fontmenu); + task->nfonts = menu->nitems; + task->fonts = calloc((size_t) task->nfonts, sizeof(*task->fonts)); + if (task->nfonts > 0 && task->fonts == NULL) + { + wuss_fontmenu_destroy(task->fontmenu); + free(task); + return result_OOM; + } + delegate_desc.handle = text_handle; delegate_desc.task_data = task; delegate_desc.name = "text"; rc = wuss_task_create(wuss, &delegate_desc, &delegate); if (rc != result_OK) { + wuss_fontmenu_destroy(task->fontmenu); + free(task->fonts); free(task); /* nothing registered yet; the spawner will not free it */ return rc; } wuss_task_set_autoclose(delegate, 1); - sz = SIZE2D(220, 180); + task->delegate = delegate; + sz = SIZE2D(220, 180); task->base_width = sz.w; task->base_height = sz.h; @@ -154,19 +241,43 @@ result_t text_handle(wuss_window_t *window, tcx = task_data; + NOT_USED(window); + switch (event->kind) { case wuss_EVENT_REDRAW: return text_redraw(event, task_data); case wuss_EVENT_MOUSE: + if (event->data.mouse.action == wuss_MOUSE_DOWN && + (event->data.mouse.button & wuss_BUTTON_MENU)) + return text_open_menu(tcx); if (event->data.mouse.action != wuss_MOUSE_DOWN || !(event->data.mouse.button & wuss_BUTTON_SELECT)) return result_OK; return text_mouse(task_data); + case wuss_EVENT_MENU_SELECT: + { + const char *name; + + name = wuss_fontmenu_selected(tcx->fontmenu, event); + if (name != NULL) + return text_set_font(tcx, event->data.menu_select.index, name); + } + return result_OK; + case wuss_EVENT_QUIT: - free(tcx); /* task_data was calloc'd per instance by the spawner */ + { + int i; + + for (i = 0; i < tcx->nfonts; i++) + if (tcx->fonts[i] != NULL) + bmfont_destroy(tcx->fonts[i]); + free(tcx->fonts); + wuss_fontmenu_destroy(tcx->fontmenu); + free(tcx); /* task_data was calloc'd per instance by the spawner */ + } return result_OK; case wuss_EVENT_IDLE: diff --git a/libraries/wuss/test/tasks/text.h b/libraries/wuss/test/tasks/text.h index 3da481c0..f0c6ae36 100644 --- a/libraries/wuss/test/tasks/text.h +++ b/libraries/wuss/test/tasks/text.h @@ -9,28 +9,41 @@ #include "framebuf/bmfont.h" #include "framebuf/colour.h" +#include "wuss/component/fontmenu.h" +#include "wuss/task.h" #include "wuss/window.h" /* window B's task: flows a fixed paragraph of placeholder text over its - * wuss-filled background, one line per bmfont_draw call */ + * wuss-filled background, one line per bmfont_draw call. A MENU click on the + * window opens a picker -- a wuss_fontmenu over resources/bmfonts -- that + * swaps the paragraph font in place. */ typedef struct text_task { - wuss_window_t *window; - bmfont_t *font; - colour_t bg, fg; - int base_width; /* content width when the window was created */ - int base_height; /* content height when the window was created */ - int frame_count; - bool resizing; /* toggled by a content click; text_step only - * resizes the window while this is true */ + wuss_window_t *window; + wuss_task_t *delegate; /* the wuss task backing this window */ + wuss_t *wuss; /* for wuss_get_pointer when opening the menu */ + wuss_fontmenu_t *fontmenu; /* the font picker; owns the menu and names */ + bmfont_t *font; /* currently shown; fonts[current] or sysfont */ + bmfont_t **fonts; /* one slot per menu item, lazily loaded */ + int nfonts; /* length of fonts[]; == menu item count */ + int current; /* index into fonts[], or -1 for sysfont */ + colour_t bg, fg; + char resources[256]; /* root the picker loads bmfonts from */ + int base_width; /* content width when the window was made */ + int base_height; /* content height when the window was made */ + int frame_count; + bool resizing; /* toggled by a content click; text_step only + * resizes the window while this is true */ } text_task_t; wuss_window_fn_t text_handle; -/* create the paragraph-of-text window against the given wuss instance */ +/* create the paragraph-of-text window against the given wuss instance. + * resources is the root the font picker loads bmfonts from + * (resources/bmfonts/<name>.png). */ result_t text_create(wuss_t *wuss, - bmfont_t *font, + const char *resources, text_task_t *task); #endif /* WUSS_APP */ diff --git a/resources/bmfonts/Digits-Bold.png b/resources/bmfonts/Digits-Bold.png old mode 100644 new mode 100755 index 2ca5502f8084e1391181e85d7f59ec67668c3348..bc0a318d8e407a50553ca2b459b08412b7813a2e GIT binary patch delta 731 zcmV<10wn#y2Gj<Sg@3w9L_t(Y$F){VZsRZv<=kL4y^j~|o&iL7nWLmf$<pvC;)M=S zAi~^X_FWD%{K=MLJ58rkgHULS%#%p*Sul<;o?#qcF94#O!t$!ZmO+&P1_1Xto9BiM z7CV$ci%Bd`qQp*gR)QLNob<JpgzAqX69NTMY-CwG%MW!x<bUsnX9qBhx1SK(m=J(; zWQpj?EaqdtLERj{=3Y8DU<1H=S_7m4a7FjLU<1HWRNNfECWyCLbOT^NLZ7MuCjKw} zM-)40t_*-vas|&b!%SFe;3*3lSpmpeS^!&Gv_D0K;iyh)Pyj+n6o)qe@fqSv1F%^G zDY9lYl!Zr2a(`KzZC%pat&0BE0`Mt{Wd&FPSe+@f0O&iIYry`Hb57Zx_fyWj>uFIl z>3B5zG1DniFUp^zj4T`|uP5>OmBpSlob9(KcT>UxKC@u^3i+F#(*2uqu32H+b-S50 zMwxeDwI%eCg7>}Du=5_2f)gfeJX2L<8A2(0n^iv;i+_N(xF1I-#t?2;fxr)siqB5L zYjFJ!0Nn;ayaxak0DKIPa04LW4gle@F6unF4uIPLF*E>1+8nz9KwBxI0zmB!Y$-x; z1wc4eX9WYkUITCnx~u@?bqydr-DlE{=aD&}S3VHqNpw-zD398Cw)YTY2pQ?fmiLo~ z=Za=N!G8h&Cf`pLes!qi_pmg*&%%l-hMVifGFvZyWWT1f^@|bP&vMvj(+^oT$&p_H zq&D~~0O3cu<QRpeuv~ix%MMzyTimMqOHFpow;|xF52{;9xI-28)r4EBm|B2CR|CN3 z08+OB5b+K`*&}rg0O2-3bh~%5eM07I)qaZL(NrBwp#hjb61PRp+WJNnNy#7;GU)AY z(HmhnRgk)|MU<8*l9EdOBs%S!u&{d&Lp8e>^)XeX<8JHsHIqFM^%n?uoV%(8@ge{K N002ovPDHLkV1jBvOtSz0 delta 713 zcmV;)0yh2B2EqoAg@33?L_t(Y$Hi7Vj@&Q|Wp0or_i>T*T!4hjIm+x&wjB5r>%s>J zFhcH-e#=1NPqyUG%xpG63__Oq@smjDNiYmByudI#zk;+Nt;_IiBG!v_E^u*u`0tDO z3}7K1=rKv;i*}}N0T3ktjId}zkLHgmBLb6lycO9x>x(;Jj(_jjMuzeIGfZs^5wNu) z6Gmqi_i<n{w*YY4w{8s}bJ_x=0&qq9i|DZdkSH#00pJGS%s~8HIh!0{lz+(|QT1%O zasWxm)dC%6#FFkv8STtq1>i%Q{HJIz1Y>74C;(IvCE*Rg^!1?{Xpt?eR0ckIlFRsf z>YCneQ}h*ptbd}Zi~uVDuQO8%fH8x)2I3Dn=al1lJLmZkWncPi?aB40%;(I#c7C0- z6_G&wJgd)-tom%>qF=AvU7-hjX2JFo@;`pc?%$P{niYmax0TfyWuAf8mN8nU*!Nn) zF7}`noH1eJnW`ddL#6C(R%2l-0<pzoIYLnqtyqB}7JrXPSVxK)T>k<<w*xTU0)Pqt z?gJ#;0Z4cNz_^TyyGU*T;6A`a4S<uj#O?s_UW&K?a61DZiV|D_P);?gV!+3106{^o z8vy;>0_dLZGuw`rRyd$HK2YUZ``S@u-y1!swT}>I2yOW!nzwU==Za?D!2y3J|AQ)i zUY1QC7k>-bP{n0+y;!XE@CWg0_|i^{2}48WZgIq>cUd;gr*RFC+Tf1>&?jujF41!B zVJtIf$!>A0?k_djHD?3iW*=0y61qVZerm#+D#jMzsjC6teE_N30hsUxK$#<V4FKal zz!dt}$$o^S?rw_U(HV@?0L+KPy2#nKzEedqGD1j&40^v?SZAgRHaF2|r=^Nyq*6bL v&j%}v4i_R-)}yHVRFO`HTfeWF?18A?uBo0pO_n4g00000NkvXXu0mjflA}_H diff --git a/resources/bmfonts/Digits-Regular.png b/resources/bmfonts/Digits-Regular.png index 4e82093fa0752113be2c42430ee0049d6a516852..446566ef5c80c4b828054fbc5b0a32a9f71cfe56 100755 GIT binary patch delta 774 zcmV+h1Nr>r2LA?-g@5HqL_t(Y$E}q;ZsRZzg~!&aG3<pGpe~UrTexQt_Y6S7RiIO( zmVsPErJz%6faCxRtl*OfTN&IpLrIkUvrd30&GQWTg_@a1QC;4Zi~5BKqF&Oxq?+#b zi+WzOC>Q%gQYA<B5LFcWc{QYne$j?jTdaPASkg?>A5yf`34f>V5rJrUjY%xM>ddUn z%yC3Zs#{}OWjb}TT0IEJVB5Ec#6-}e&L~y0Exl`HL_9)B>Q&!UN{9rILg;}w*Mac= zC!kFL{niTGk~D(ev3;d`kc3=+S_Y+dW@Cy$OC|d=lbQ|$qCp+7h9gp=Pze3g3?LL@ z1ab_riNgMSynh-5Y7{_*PtB1Cdea*<fKF>9=ujE~F;P<8iPWSuKOzWS<70Cn`x<c} zoEd@SM4?7lL*BPup1<DbA`bYIXXm?Q<C~)0;dfiK#W6>|@yDX&4aY*(9DF>{xiHsL zL7>G$g!~shK#r%r6Xl``qOPZ@eGqjQsd)Q7oCsahDSwZ@Bg}|}z_1{K(xU2a6;iUs zLC9olCGE2_?rcUqC{BGCc4@X2kb2yzq3gkhvRFV%D>d4D9%McO(mhCD2iYkIBYFl3 zQ!Ax$S3#)J0BX3P`UI4nyqXIj8$nWIlVsx)q5Sme8bR<?lL~0Hz6`>6=WUMhcfQG6 z_Qg{HLx1?=vv<Ye>G^<2?%TXQ9CvU+Tlm7e{0UxZcgI}((+}hgAAC@Joki-gMZR~= zphXE$(byu@xkZK(&Fe}mp7z%`u0b}v;$rIy?!S5;WJGohk}JyeSHvekn(fn>wn0Y$ zVvDN2{hev~hF-C0?bNjKVoKMOIWn@J2kOFP(|?Sw`ia*fXuY0z?FPtBK{(?D(6;+# zMUXuO;nhs^8ic<3YV_X|p*1F2L?Yv2dmewb2z-&_o8VxP13nJ02tF6VB97O1Sfm+Q zByV?d9Gi2Cq$1MOuA8&2n;RC{T^Rqo|KyDx9BuOa1E7k*?!Bd|)c^nh07*qoM6N<$ Ef>{xKG5`Po delta 748 zcmV<I0u%lJ2IU5jg@4OQL_t(Y$F-F`ZsRZzg~!&aG3<p4>Jq8kq-PQL3?RZ)pi`ul zfxU=JL8sUN$pIFK;FAbj8QeER$`s?D#6XrYkns%pc{DSRqIP$;o3+1)+TDugl)<38 z?W~=)Z0u%R_Q;-TyM@t2V>@k*A)?=OPm48nzfu6blcKrKIDZs89q%z&NiVuE3p26o z(Gm)KO127}TiG3s865lZkeLX2)CJ{gvZgn!j2MqFB=n+JloLE2q~LlOgYf$a=-z|= zXoX{09zkz7zR&|mTy_k~%SjnS44NxBUzpqk5QqkKz#jHUjeH?=lF=As8B}Hp=T~?% z7^sm48UA_pM1Rn$zEUGW`_c%AiJ>!+kxE)K0-<Z1HWzZJ5f{Rm5lAxfHNqZpu6MHj zy0G^6leKXn*txFmH~8MyeSN49@7$qAsKP(lvv)D0Q^Q;id1IoZhuk+UAjeZ5jNPp9 zM%$*LtvA}>Y54foXN0cloX6i1W~79HVSxvwMb+)Xhks;`g&~uzm9(#%aAyk|Kym8* zuuHSGfYjq!9lMT-yob#KnwL_e``bb0D<D09^hFS!F#%yjPe6WZr8M?3sEDAB3#vbW ziVu%wJ*bQzsWB^alo85LH`fS;K4!F7o(EyPtG>ecTh~=R>*A?~Al%{E*?NC?-osP5 zzUudf4S$r-*RFQ9dV*H^4b;N=83ytb9emOFPZIeVltHr{jE2S%X-*|_%xKy+V&%g% zj%$!buejLyjQTI$2L+`dxuVQ)Mce?=I*PVKM*(7qs-gWsw0uJ^ShQs-+ITVL%Vds> zoacc$GubrZt3J_M1TB||)~<uHG6f;WGoW>NXJtiDc>*dTsAEH4d^EbAkp~5dh-X}E z$K%fufi5b%6D%aM$IAf{!Rx|H#PS{oiF6~0RQ)E7V|OZ%JPyRT>*l2E<`ao*&WwL0 epL8L$Wc>q4ufZ<#FLodR0000<MNUMnLSTaEac<fG From 48edc423b03006a7d6f95d5d216fdc8b2b6ba2d9 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Fri, 4 Sep 2026 00:24:09 +0100 Subject: [PATCH 236/287] chore(bmfonts): drop stray exec bit on Digits-Bold.png (again) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- resources/bmfonts/Digits-Bold.png | Bin 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 resources/bmfonts/Digits-Bold.png diff --git a/resources/bmfonts/Digits-Bold.png b/resources/bmfonts/Digits-Bold.png old mode 100755 new mode 100644 From 5db85e59f7011fca87cee12ca409cc487636b849 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Fri, 4 Sep 2026 00:44:13 +0100 Subject: [PATCH 237/287] feat(wuss): page the content when clicking in a scroll well A Select/Adjust click landing in a scrollbar well (not on the sausage) now scrolls the content one visible extent towards the click, keeping one WUSS_SCROLL_STEP of overlap, RISC OS style. The sausage-drag state is still armed so a press that moves onto the sausage keeps working. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- libraries/wuss/mouse-click.c | 37 ++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/libraries/wuss/mouse-click.c b/libraries/wuss/mouse-click.c index 8e79994b..1b01b382 100644 --- a/libraries/wuss/mouse-click.c +++ b/libraries/wuss/mouse-click.c @@ -193,6 +193,43 @@ result_t wuss_mouse_click(wuss_t *wuss, if ((button & wuss_BUTTON_SELECT) && region == wuss_FURNITURE_RESIZE) wuss_window_restack(win, wuss_ZORDER_FRONT); + /* A click landing in the well itself (not on the sausage) pages the + * content one visible extent towards the click, RISC OS style, keeping + * one step of overlap. The drag state is still armed below so a press + * that then moves onto the sausage keeps working. */ + if (region == wuss_FURNITURE_VSCROLL_WELL || + region == wuss_FURNITURE_HSCROLL_WELL) + { + box_t sausage; + box_t content; + int page; + + wuss__content_box(win, &content); + + if (region == wuss_FURNITURE_VSCROLL_WELL) + { + wuss__vscroll_sausage_box(win, &sausage); + page = (content.y1 - content.y0) - WUSS_SCROLL_STEP; + if (page < 1) + page = 1; + if (y < sausage.y0) + wuss__scroll_step(win, POINT(0, -page)); + else if (y > sausage.y1) + wuss__scroll_step(win, POINT(0, page)); + } + else + { + wuss__hscroll_sausage_box(win, &sausage); + page = (content.x1 - content.x0) - WUSS_SCROLL_STEP; + if (page < 1) + page = 1; + if (x < sausage.x0) + wuss__scroll_step(win, POINT(-page, 0)); + else if (x > sausage.x1) + wuss__scroll_step(win, POINT(page, 0)); + } + } + wuss_window_get_scroll(win, &scroll); wuss->furniture.dragging = win; From 75788c5b0b7eb2fcbe60e3047e4c53c867db72fb Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Fri, 4 Sep 2026 00:54:59 +0100 Subject: [PATCH 238/287] feat(wuss): add an Emscripten/WebAssembly build of the interactive demo Extract the run_wuss event/redraw loop into wuss_frame(void*) driven by a wuss_frame_ctx struct, so the browser can pump it via emscripten_set_main_loop_arg instead of a blocking while (!g.quit); the desktop and RISC OS builds still spin the loop directly. CMake gains an EMSCRIPTEN branch: the DPTLib libpng dependency uses the bundled -sUSE_LIBPNG=1 port (no find_package(PNG)/FetchContent), and the wuss target links Emscripten's SDL3 port (-sUSE_SDL=3), preloads resources/ into wuss.data mounted at MEMFS /resources, and emits wuss.html. find_package(SDL3) moves into the desktop else() so it is not probed on the wasm path. Configure with: emcmake cmake -B build-emscripten -G Ninja -DBUILD_APPS=ON -DBUILD_TESTS=OFF cmake --build build-emscripten --target wuss Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- CLAUDE.md | 8 ++ CMakeLists.txt | 25 ++++- apps/wuss/main.c | 236 +++++++++++++++++++++++++++++------------------ 3 files changed, 175 insertions(+), 94 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 26c28807..7a781ae4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -20,6 +20,14 @@ Build directories are per-config and already created; there is **no** plain - `build-nosdl/` — Release, core tests only. - `build-riscos/`— GCCSDK cross build. Leave alone unless working on RISC OS. - `build.xc/` — Xcode generator. +- `build-emscripten/` — WebAssembly build of the `wuss` demo. Not pre-created; + configure with `emcmake cmake -B build-emscripten -G Ninja -DBUILD_APPS=ON + -DBUILD_TESTS=OFF`, build with `cmake --build build-emscripten --target wuss`, + then serve the dir and open `wuss.html` (e.g. + `python3 -m http.server -d build-emscripten`). Uses Emscripten's bundled SDL3 + and libpng ports (`-sUSE_SDL=3`, `-sUSE_LIBPNG=1`); the browser owns the main + loop via `emscripten_set_main_loop_arg`; `resources/` is baked into + `wuss.data` and mounted at MEMFS `/resources`. CMake options: `BUILD_TESTS`, `BUILD_SDL_TESTS`, `USE_ASAN` (ASan + UBSan), `USE_FORTIFY` (bundled Fortify), `DPTLIB_IMAGES_READ_ONLY` (libpng no write). diff --git a/CMakeLists.txt b/CMakeLists.txt index 1cb4453f..83088b1b 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -517,7 +517,12 @@ if(WUSS_COMPONENTS) target_compile_definitions(DPTLib PUBLIC WUSS_COMPONENTS) endif() -if(NOT TARGET_RISCOS) +if(EMSCRIPTEN) + # Emscripten's bundled libpng port (pulls in its zlib port); provides the + # headers at compile time and the archive at link time. + target_compile_options(DPTLib PUBLIC "-sUSE_LIBPNG=1") + target_link_options(DPTLib PUBLIC "-sUSE_LIBPNG=1") +elseif(NOT TARGET_RISCOS) set(CMAKE_FIND_FRAMEWORK NEVER) find_package(PNG REQUIRED MODULE) target_link_libraries(DPTLib PRIVATE PNG::PNG) @@ -743,7 +748,23 @@ if(BUILD_APPS) target_compile_definitions(wuss PRIVATE WUSS_APP) target_link_libraries(wuss DPTLib) - if(NOT TARGET_RISCOS) + if(TARGET_RISCOS) + # native VDU/OS_Mouse frontend, no SDL + elseif(EMSCRIPTEN) + # Emscripten ships its own SDL3 port; the browser owns the main loop + # (see emscripten_set_main_loop_arg in apps/wuss/main.c). resources/ is + # baked into wuss.data and mounted at MEMFS /resources so the runtime + # file loads and the font picker's opendir() resolve. + target_compile_definitions(wuss PRIVATE USE_SDL) + target_compile_options(wuss PRIVATE "-sUSE_SDL=3") + target_link_options(wuss PRIVATE + "-sUSE_SDL=3" + "-sALLOW_MEMORY_GROWTH=1" + "-sEXIT_RUNTIME=0" + "--preload-file" + "${CMAKE_CURRENT_SOURCE_DIR}/resources@/resources") + set_target_properties(wuss PROPERTIES SUFFIX ".html") + else() find_package(SDL3 REQUIRED) target_compile_definitions(wuss PRIVATE USE_SDL) target_link_libraries(wuss SDL3::SDL3) diff --git a/apps/wuss/main.c b/apps/wuss/main.c index e644d857..6fb56978 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -4,6 +4,10 @@ #include <stdlib.h> #include <string.h> +#ifdef __EMSCRIPTEN__ +#include <emscripten.h> +#endif + #ifdef FORTIFY #include "fortify/fortify.h" #endif @@ -512,6 +516,123 @@ static void pixel_stress(wuss_t *wuss, int scr_width, int scr_height) } } +/* one iteration of the event/redraw loop; a struct because Emscripten drives + * it as a callback (emscripten_set_main_loop_arg) rather than a plain while */ +struct wuss_frame_ctx +{ + wuss_t *wuss; + wuss_frontend_t *frontend; + bitmap_t *bm; + unsigned char *pixels; + int rowbytes; + int scr_width; + int scr_height; + colour_t *palette; + int npalette; + int palette_index; +}; + +static void wuss_frame(void *arg) +{ + struct wuss_frame_ctx *c = arg; + wuss_input_t ev; + bool pixel_stress_pending = false; + bool garbage_pending = false; + + while (wuss_frontend_poll(c->frontend, &ev)) + { + switch (ev.kind) + { + case wuss_INPUT_QUIT: + g.quit = true; + break; + + case wuss_INPUT_REDRAW_ALL: + wuss_redraw(c->wuss); + break; + + case wuss_INPUT_GARBAGE: + garbage_pending = true; + break; + + case wuss_INPUT_PIXEL_STRESS: + pixel_stress_pending = true; + break; + + case wuss_INPUT_PALETTE_CYCLE: + /* rebuild the palette, push it into the framebuffer bitmap, tell the + * backend (which owns any physical palette), then tell wuss, which + * refreshes its own copy and pokes every task to recache */ + c->palette_index = (c->palette_index + 1) % (int) NELEMS(g_palettes); + g_palettes[c->palette_index](c->palette); + bitmap_set_palette(c->bm, c->palette); + wuss_frontend_set_palette(c->frontend, c->palette, c->npalette); + wuss_set_palette(c->wuss, c->palette, c->npalette); + break; + + case wuss_INPUT_MOUSE_DOWN: + { + wuss_window_t *hit; + + wuss_mouse_click(c->wuss, ev.pos, ev.button, wuss_MOUSE_DOWN, &hit); + + /* MENU click on bare backdrop opens the task launcher there */ + if (hit == NULL && (ev.button & wuss_BUTTON_MENU)) + wuss_menu_open(g.menu_task, &g_task_menu, ev.pos, NULL); + } + break; + + case wuss_INPUT_MOUSE_UP: + wuss_mouse_click(c->wuss, ev.pos, ev.button, wuss_MOUSE_UP, NULL); + break; + + case wuss_INPUT_MOUSE_MOVE: + wuss_mouse_move(c->wuss, ev.pos, NULL); + break; + + case wuss_INPUT_WHEEL: + wuss_scroll(c->wuss, ev.pos, ev.wheel, NULL); + break; + + default: + break; + } + } + + wuss_idle(c->wuss); + + if (garbage_pending) + { + /* corrupt the whole framebuffer and present it, then leave it alone -- + * wuss only repaints what it knows is dirty, so the junk stays put + * until something else invalidates the screen */ + unsigned char *p; + size_t n; + size_t i; + + p = c->pixels; + n = (size_t) c->rowbytes * c->scr_height; + for (i = 0; i < n; i++) + p[i] = (unsigned char) rand(); + + wuss_frontend_present(c->frontend, c->bm); + } + else + { + if (pixel_stress_pending) + pixel_stress(c->wuss, c->scr_width, c->scr_height); + else + wuss_redraw_dirty(c->wuss); + + wuss_frontend_present(c->frontend, c->bm); + } + +#ifdef __EMSCRIPTEN__ + if (g.quit) + emscripten_cancel_main_loop(); +#endif +} + /* click windows to bring to front, drag titlebars to move; the redraw-all * input redraws the whole screen, the pixel-stress input does it one pixel at * a time to catch tasks that misbehave under a 1x1 clip; the palette-cycle @@ -622,99 +743,30 @@ static result_t run_wuss(const char *resources) wuss_redraw(wuss); - while (!g.quit) { - wuss_input_t ev; - bool pixel_stress_pending = false; - bool garbage_pending = false; - - while (wuss_frontend_poll(frontend, &ev)) - { - switch (ev.kind) - { - case wuss_INPUT_QUIT: - g.quit = true; - break; - - case wuss_INPUT_REDRAW_ALL: - wuss_redraw(wuss); - break; - - case wuss_INPUT_GARBAGE: - garbage_pending = true; - break; - - case wuss_INPUT_PIXEL_STRESS: - pixel_stress_pending = true; - break; - - case wuss_INPUT_PALETTE_CYCLE: - /* rebuild the palette, push it into the framebuffer bitmap, tell the - * backend (which owns any physical palette), then tell wuss, which - * refreshes its own copy and pokes every task to recache */ - palette_index = (palette_index + 1) % (int) NELEMS(g_palettes); - g_palettes[palette_index](palette); - bitmap_set_palette(&bm, palette); - wuss_frontend_set_palette(frontend, palette, NELEMS(palette)); - wuss_set_palette(wuss, palette, NELEMS(palette)); - break; - - case wuss_INPUT_MOUSE_DOWN: - { - wuss_window_t *hit; - - wuss_mouse_click(wuss, ev.pos, ev.button, wuss_MOUSE_DOWN, &hit); - - /* MENU click on bare backdrop opens the task launcher there */ - if (hit == NULL && (ev.button & wuss_BUTTON_MENU)) - wuss_menu_open(g.menu_task, &g_task_menu, ev.pos, NULL); - } - break; - - case wuss_INPUT_MOUSE_UP: - wuss_mouse_click(wuss, ev.pos, ev.button, wuss_MOUSE_UP, NULL); - break; - - case wuss_INPUT_MOUSE_MOVE: - wuss_mouse_move(wuss, ev.pos, NULL); - break; - - case wuss_INPUT_WHEEL: - wuss_scroll(wuss, ev.pos, ev.wheel, NULL); - break; - - default: - break; - } - } - - wuss_idle(wuss); - - if (garbage_pending) - { - /* corrupt the whole framebuffer and present it, then leave it alone -- - * wuss only repaints what it knows is dirty, so the junk stays put - * until something else invalidates the screen */ - unsigned char *p; - size_t n; - size_t i; - - p = pixels; - n = (size_t) rowbytes * scr_height; - for (i = 0; i < n; i++) - p[i] = (unsigned char) rand(); - - wuss_frontend_present(frontend, &bm); - } - else - { - if (pixel_stress_pending) - pixel_stress(wuss, scr_width, scr_height); - else - wuss_redraw_dirty(wuss); - - wuss_frontend_present(frontend, &bm); - } + struct wuss_frame_ctx ctx; + + ctx.wuss = wuss; + ctx.frontend = frontend; + ctx.bm = &bm; + ctx.pixels = pixels; + ctx.rowbytes = rowbytes; + ctx.scr_width = scr_width; + ctx.scr_height = scr_height; + ctx.palette = palette; + ctx.npalette = NELEMS(palette); + ctx.palette_index = palette_index; + +#ifdef __EMSCRIPTEN__ + /* the browser owns the loop; simulate_infinite_loop=1 means this call + * never returns, so &ctx (a stack local) stays live and the teardown + * below is unreachable -- fine, the page dies on navigation. fps=0 asks + * for requestAnimationFrame pacing. */ + emscripten_set_main_loop_arg(wuss_frame, &ctx, 0, 1); +#else + while (!g.quit) + wuss_frame(&ctx); +#endif } /* ponytail: wuss_destroy() below force-closes every still-open window and From 126f6d1e9e0201f8ca7e132777aa0774b4e1e94b Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Fri, 4 Sep 2026 01:12:18 +0100 Subject: [PATCH 239/287] ci: publish the Wuss wasm demo to GitHub Pages New Pages workflow: builds the Emscripten wuss target on push to master, copies wuss.html -> index.html plus the js/wasm/data assets into _site, and deploys via actions/deploy-pages. Pages source is set to GitHub Actions; the site serves at http://dpt.github.io/DPTLib/. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- .github/workflows/pages.yml | 67 +++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 .github/workflows/pages.yml diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 00000000..404fbbac --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,67 @@ +--- +# Build the Wuss WebAssembly demo and publish it to GitHub Pages. +# + +name: Pages +on: + push: + branches: [master] + workflow_dispatch: + +# Allow one concurrent deployment; let a running one finish. +concurrency: + group: pages + cancel-in-progress: false + +permissions: + contents: read + pages: write + id-token: write + +jobs: + build: + name: Build wasm + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - name: Install CMake + uses: lukka/get-cmake@latest + + - name: Set up Emscripten + uses: mymindstorm/setup-emsdk@v14 + with: + version: 4.0.6 + actions-cache-folder: emsdk-cache + + - name: Configure + run: | + emcmake cmake -B build-emscripten -G Ninja \ + -DBUILD_APPS=ON -DBUILD_TESTS=OFF + + - name: Build + run: cmake --build build-emscripten --target wuss + + - name: Assemble site + run: | + mkdir -p _site + cp build-emscripten/wuss.html _site/index.html + cp build-emscripten/wuss.js build-emscripten/wuss.wasm \ + build-emscripten/wuss.data _site/ + + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@v3 + with: + path: _site + + deploy: + name: Deploy to Pages + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy + id: deployment + uses: actions/deploy-pages@v4 From 7deda4274f59f855298af3a564d2b472f6dbb935 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Fri, 4 Sep 2026 10:36:47 +0100 Subject: [PATCH 240/287] feat(wuss): toggle-size expands windows to full screen, repositioning to fit wuss__furniture_toggle_size pinned the window's top-left and only grew the bottom-right corner, clamped to window->doc, so a window not already near the origin could never fill the screen and a doc of 0 toggled to nothing. Toggle now grows to the whole screen, capped per axis at the window's doc extent (doc == 0 on an axis means no cap), and nudges the top-left toward the origin by the minimum needed to fit the grown box on screen. Restore returns the exact pre-toggle box, position included. Add wuss__max_content_anywhere_on_screen, the screen-fit maths without the current-top-left allowance. Gate the topmost sliver-blit fast path on the top-left being unchanged; toggles that reposition fall through to the general box_union invalidate. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- libraries/wuss/furniture/toggle-action.c | 80 +++++---- libraries/wuss/impl.h | 27 +++ libraries/wuss/test/wuss-test.c | 214 +++++++++++++++++++++-- 3 files changed, 277 insertions(+), 44 deletions(-) diff --git a/libraries/wuss/furniture/toggle-action.c b/libraries/wuss/furniture/toggle-action.c index d6afc173..007f503b 100644 --- a/libraries/wuss/furniture/toggle-action.c +++ b/libraries/wuss/furniture/toggle-action.c @@ -16,43 +16,53 @@ void wuss__furniture_toggle_size(wuss_window_t *window) } else { - int outline_px, titlebar_height, available_width, available_height, width, height; + int outline_px, titlebar_height, target_w, target_h, vis_w, vis_h, x0, y0; point_t carve; - size2d_t min; + size2d_t min, screen_max; outline_px = wuss__outline_px(window); titlebar_height = wuss__titlebar_height(window); wuss__furniture_carve_for(window->flags, wuss__button_size(window), &carve); - /* bounded by what's actually left of the screen from the window's - * current top-left, not the screen's full width/height -- otherwise a - * window not already at the origin grows straight off the edge. Also - * account for scrollbar/resize-icon furniture (carve), which sits - * outside the content area same as create.c/resize.c do, or the window - * ends up carve.x/carve.y short of the doc size it's meant to reach. */ - available_width = window->wuss->scr->size.w - window->visible.x0 - 2 * outline_px - carve.x; - available_height = window->wuss->scr->size.h - window->visible.y0 - 2 * outline_px - titlebar_height - carve.y; - - /* a window dragged far enough off-screen leaves no room at all to the - * screen edge, so the above can go negative -- floor it like - * drag-resize.c does, or new_visible ends up with x1/y1 less than - * x0/y0 (an inverted box), which is never hit-testable again - * (box_contains_point can't match x1<x0), leaving the window stuck. - * Floor the available space, not the final width/height below, so a - * doc smaller than the floor still stops at its own size. */ - wuss__min_content(window, &min); - available_width = MAX(available_width, min.w); - available_height = MAX(available_height, min.h); + /* Toggle grows the window to the whole screen (not just the space left + * from its current top-left), capped per axis at the window's own doc + * extent -- a doc of 0 on an axis means "no cap", fill the screen. + * screen_max already accounts for outline/titlebar/scrollbar furniture + * sitting outside the content area. */ + wuss__max_content_anywhere_on_screen(window, &screen_max); + + target_w = window->doc.w ? MIN(window->doc.w, screen_max.w) : screen_max.w; + target_h = window->doc.h ? MIN(window->doc.h, screen_max.h) : screen_max.h; - width = MIN(window->doc.w, available_width); - height = MIN(window->doc.h, available_height); + /* a window with a tiny doc still needs to end up big enough to grab; + * floor the target like drag-resize.c does, or the toggled box could be + * smaller than WUSS_MIN_CONTENT and awkward to un-toggle. */ + wuss__min_content(window, &min); + target_w = MAX(target_w, min.w); + target_h = MAX(target_h, min.h); + + vis_w = target_w + 2 * outline_px + carve.x; + vis_h = target_h + titlebar_height + 2 * outline_px + carve.y; + + /* nudge the top-left toward the origin by the minimum needed to fit the + * grown box on screen; a window that already fits stays put. */ + x0 = window->visible.x0; + y0 = window->visible.y0; + if (x0 + vis_w > window->wuss->scr->size.w) + x0 = window->wuss->scr->size.w - vis_w; + if (y0 + vis_h > window->wuss->scr->size.h) + y0 = window->wuss->scr->size.h - vis_h; + if (x0 < 0) + x0 = 0; + if (y0 < 0) + y0 = 0; window->pre_toggle = window->visible; - new_visible.x0 = window->visible.x0; - new_visible.y0 = window->visible.y0; - new_visible.x1 = new_visible.x0 + width + 2 * outline_px + carve.x; - new_visible.y1 = new_visible.y0 + height + titlebar_height + 2 * outline_px + carve.y; + new_visible.x0 = x0; + new_visible.y0 = y0; + new_visible.x1 = x0 + vis_w; + new_visible.y1 = y0 + vis_h; } window->visible = new_visible; @@ -82,16 +92,22 @@ void wuss__furniture_toggle_size(wuss_window_t *window) } } + /* ponytail: the sliver-blit fast path below assumes the top-left didn't + * move; toggle can now reposition a window to fit it on screen, so gate + * it on x0/y0 being unchanged and let the general box_union path handle + * the moved case. Restore (pre_toggle) can move the top-left too -- same + * gate covers it. */ if (!(window->flags & wuss_WINDOW_NO_RESIZE_BLIT) && window->wuss->z_order.next == &window->link && + before.x0 == window->visible.x0 && before.y0 == window->visible.y0 && screen_copy_rect(window->wuss->scr, &before, POINT(before.x0, before.y0), &copied) == result_OK) { - /* Topmost, and the screen format supports the blit: the window's - * top-left never moves for a toggle, so re-blitting "before" onto - * itself is a no-op that just confirms which of its pixels are still - * on-screen -- only whatever's newly exposed (grown) or newly vacated - * (shrunk) relative to that needs an actual repaint. */ + /* Topmost, the screen format supports the blit, and the top-left has + * not moved: re-blitting "before" onto itself is a no-op that just + * confirms which of its pixels are still on-screen -- only whatever's + * newly exposed (grown) or newly vacated (shrunk) relative to that + * needs an actual repaint. */ wuss__invalidate_minus(window->wuss, &before, &window->visible); wuss__invalidate_minus(window->wuss, &window->visible, &copied); diff --git a/libraries/wuss/impl.h b/libraries/wuss/impl.h index 2d3b9671..74bd3e5e 100644 --- a/libraries/wuss/impl.h +++ b/libraries/wuss/impl.h @@ -566,4 +566,31 @@ static inline void wuss__max_content_on_screen(const wuss_window_t *window, max->h = WUSS_MIN_CONTENT; } +/* Largest content width/height whose visible box fits the screen when the + * window may be repositioned -- i.e. the whole screen less chrome, with no + * allowance for the window's current top-left. Toggle-size uses this and + * then nudges the top-left toward the origin by the minimum needed to fit. + * Floored at WUSS_MIN_CONTENT like wuss__max_content_on_screen. + * ponytail: a named helper (~6 lines) to stay parallel with its sibling + * above rather than open-coded into toggle-action.c. */ +static inline void wuss__max_content_anywhere_on_screen(const wuss_window_t *window, + size2d_t *max) +{ + int outline_px, titlebar_height; + point_t carve; + + outline_px = wuss__outline_px(window); + titlebar_height = wuss__titlebar_height(window); + wuss__furniture_carve_for(window->flags, wuss__button_size(window), &carve); + + max->w = window->wuss->scr->size.w - 2 * outline_px - carve.x; + max->h = window->wuss->scr->size.h + - 2 * outline_px - titlebar_height - carve.y; + + if (max->w < WUSS_MIN_CONTENT) + max->w = WUSS_MIN_CONTENT; + if (max->h < WUSS_MIN_CONTENT) + max->h = WUSS_MIN_CONTENT; +} + #endif /* IMPL_H */ diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 3df22556..3044acdc 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -1390,10 +1390,10 @@ result_t wuss_test(const char *resources) if (delegate_t == NULL) goto Failure; 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 200x200 doc */ + 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, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), - SIZE2D(200, 200), SIZE2D(0, 0), &win_t); + SIZE2D(150, 150), SIZE2D(0, 0), &win_t); if (rc != result_OK) goto Failure; @@ -1487,9 +1487,11 @@ result_t wuss_test(const char *resources) wuss_window_get_visible_bounds(win_t, &after); if (after.x1 > 200 || after.y1 > 200) - goto Failure; /* maximizing must stay on-screen: bounded by what's left - * of the screen from T's own x0/y0 (10,10), not by the - * screen's full width/height as if T were at the origin */ + goto Failure; /* toggling must leave the window fully on-screen */ + if (after.x0 != before.x0 || after.y0 != before.y0) + goto Failure; /* a 150x150 doc toggled from (10,10) fits without moving, + * so the top-left stays put and the in-place grow blit + * (asserted below) applies */ rc = wuss_redraw_dirty(wuss); if (rc != result_OK) @@ -2367,14 +2369,15 @@ result_t wuss_test(const char *resources) wuss_window_get_visible_bounds(win_v, &vb); if (vb.x1 <= vb.x0 || vb.y1 <= vb.y0) - goto Failure; /* screen-limited width/height went negative (x0 is - * further right than the screen edge plus furniture - * can make room for), producing an inverted box -- + goto Failure; /* a naive "space from x0 to the screen edge" sum goes + * negative from off-screen, producing an inverted box -- * un-hit-testable forever after, since box_contains_point - * can never match x0>x1: the window is stuck, unclickable, - * unclosable. Must floor at WUSS_MIN_CONTENT like - * drag-resize.c does, even if that leaves the maximized - * window hanging off the visible screen. */ + * can never match x0>x1: the window would be stuck, + * unclickable, unclosable */ + if (vb.x0 < 0 || vb.y0 < 0 || vb.x1 > 200 || vb.y1 > 200) + goto Failure; /* toggle repositions toward the origin by the minimum + * needed, so even a window dragged right off the screen + * comes back fully on-screen when maximized */ rc = wuss_redraw_dirty(wuss); if (rc != result_OK) @@ -2418,6 +2421,193 @@ result_t wuss_test(const char *resources) wuss_window_close(win_v); } + printf("test: toggle-size repositions a window near the far edge toward the origin, then restores it exactly\n"); + + { + wuss_task_t *delegate_p; + box_t box_p, before, after, titlebar, toggle; + wuss_window_t *win_p; + int outline_px, titlebar_height, inset, icon, cx, cy; + + delegate_p = mk_task(wuss, NULL, NULL); + if (delegate_p == NULL) goto Failure; + + box_p.x0 = 150; box_p.y0 = 150; + box_p.x1 = 190; box_p.y1 = 190; /* 40x40 content, hard against the bottom- + * right of the 200x200 screen; doc big + * enough that maximize is screen-limited */ + rc = wuss_window_create(delegate_p, &box_p, "P", wuss_WINDOW_NONE, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + SIZE2D(400, 400), SIZE2D(0, 0), &win_p); + if (rc != result_OK) + goto Failure; + + rc = wuss_redraw_dirty(wuss); + if (rc != result_OK) + goto Failure; + + outline_px = 1; + titlebar_height = 20; + inset = 3; + icon = titlebar_height - 2 * inset; + + wuss_window_get_visible_bounds(win_p, &before); + titlebar.x1 = before.x1 - outline_px; + titlebar.y0 = before.y0 + outline_px; + toggle.x1 = titlebar.x1 - inset; + toggle.x0 = toggle.x1 - icon; + toggle.y0 = titlebar.y0 + inset; + toggle.y1 = toggle.y0 + icon; + cx = (toggle.x0 + toggle.x1) / 2; + cy = (toggle.y0 + toggle.y1) / 2; + + rc = wuss_mouse_click(wuss, POINT(cx, cy), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); + if (rc != result_OK || hit != win_p) + goto Failure; + rc = wuss_mouse_click(wuss, POINT(cx, cy), wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); + if (rc != result_OK) + goto Failure; + + wuss_window_get_visible_bounds(win_p, &after); + if (after.x0 >= before.x0 || after.y0 >= before.y0) + goto Failure; /* the window had to move toward the origin to fit its + * maximized box on screen */ + if (after.x0 < 0 || after.y0 < 0 || after.x1 > 200 || after.y1 > 200) + goto Failure; /* ...but only as far as needed, and it stays on-screen */ + if (after.x1 - after.x0 < 190 || after.y1 - after.y0 < 190) + goto Failure; /* it genuinely fills (nearly) the whole screen */ + + rc = wuss_redraw_dirty(wuss); + if (rc != result_OK) + goto Failure; + + /* toggle back: the icon moved with the grown/repositioned titlebar */ + wuss_window_get_visible_bounds(win_p, &after); + titlebar.x1 = after.x1 - outline_px; + titlebar.y0 = after.y0 + outline_px; + toggle.x1 = titlebar.x1 - inset; + toggle.x0 = toggle.x1 - icon; + toggle.y0 = titlebar.y0 + inset; + toggle.y1 = toggle.y0 + icon; + cx = (toggle.x0 + toggle.x1) / 2; + cy = (toggle.y0 + toggle.y1) / 2; + + rc = wuss_mouse_click(wuss, POINT(cx, cy), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); + if (rc != result_OK || hit != win_p) + goto Failure; + rc = wuss_mouse_click(wuss, POINT(cx, cy), wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); + if (rc != result_OK) + goto Failure; + + wuss_window_get_visible_bounds(win_p, &after); + if (after.x0 != before.x0 || after.y0 != before.y0 || + after.x1 != before.x1 || after.y1 != before.y1) + goto Failure; /* restore returns the exact pre-toggle box, position too */ + + rc = wuss_redraw_dirty(wuss); + if (rc != result_OK) + goto Failure; + + wuss_window_close(win_p); + } + + printf("test: toggle-size fills the screen for a window with no document extent (doc == 0), and per-axis where only one axis has a doc\n"); + + { + wuss_task_t *delegate_z; + box_t box_z, before, after, titlebar, toggle; + wuss_window_t *win_z; + int outline_px, titlebar_height, inset, icon, cx, cy, axis; + + outline_px = 1; + titlebar_height = 20; + inset = 3; + icon = titlebar_height - 2 * inset; + + /* axis 0: doc == (0,0) -> both axes fill the screen. + * axis 1: doc == (60,0) -> width caps at ~60, height fills the screen. */ + for (axis = 0; axis < 2; axis++) + { + delegate_z = mk_task(wuss, NULL, NULL); + if (delegate_z == NULL) goto Failure; + + 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, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + axis == 0 ? SIZE2D(0, 0) : SIZE2D(60, 0), + SIZE2D(0, 0), &win_z); + if (rc != result_OK) + goto Failure; + + rc = wuss_redraw_dirty(wuss); + if (rc != result_OK) + goto Failure; + + wuss_window_get_visible_bounds(win_z, &before); + titlebar.x1 = before.x1 - outline_px; + titlebar.y0 = before.y0 + outline_px; + toggle.x1 = titlebar.x1 - inset; + toggle.x0 = toggle.x1 - icon; + toggle.y0 = titlebar.y0 + inset; + toggle.y1 = toggle.y0 + icon; + cx = (toggle.x0 + toggle.x1) / 2; + cy = (toggle.y0 + toggle.y1) / 2; + + rc = wuss_mouse_click(wuss, POINT(cx, cy), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); + if (rc != result_OK || hit != win_z) + goto Failure; + rc = wuss_mouse_click(wuss, POINT(cx, cy), wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); + if (rc != result_OK) + goto Failure; + + wuss_window_get_content_bounds(win_z, &content); + width = content.x1 - content.x0; + height = content.y1 - content.y0; + + if (height < 150) + goto Failure; /* the y axis has no doc cap in either case -> fills the + * screen, not stuck at the 40px starting height */ + if (axis == 0 && width < 150) + goto Failure; /* doc == 0 -> the x axis fills the screen too */ + if (axis == 1 && width != 60) + goto Failure; /* doc.w == 60 -> the x axis caps at the doc extent */ + + rc = wuss_redraw_dirty(wuss); + if (rc != result_OK) + goto Failure; + + /* toggle back restores the starting box */ + wuss_window_get_visible_bounds(win_z, &after); + titlebar.x1 = after.x1 - outline_px; + titlebar.y0 = after.y0 + outline_px; + toggle.x1 = titlebar.x1 - inset; + toggle.x0 = toggle.x1 - icon; + toggle.y0 = titlebar.y0 + inset; + toggle.y1 = toggle.y0 + icon; + cx = (toggle.x0 + toggle.x1) / 2; + cy = (toggle.y0 + toggle.y1) / 2; + + rc = wuss_mouse_click(wuss, POINT(cx, cy), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); + if (rc != result_OK || hit != win_z) + goto Failure; + rc = wuss_mouse_click(wuss, POINT(cx, cy), wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); + if (rc != result_OK) + goto Failure; + + wuss_window_get_visible_bounds(win_z, &after); + if (after.x0 != before.x0 || after.y0 != before.y0 || + after.x1 != before.x1 || after.y1 != before.y1) + goto Failure; + + rc = wuss_redraw_dirty(wuss); + if (rc != result_OK) + goto Failure; + + wuss_window_close(win_z); + } + } + printf("test: dragging a back-most window with nothing above it still blits\n"); { From d6412137d5fa49ba7a8b5118a361e4c9d6eebdfb Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Fri, 4 Sep 2026 10:41:44 +0100 Subject: [PATCH 241/287] perf(wuss): blit the overlapping content when a toggle-size repositions the window A toggle that had to nudge the window toward the origin to fit its maximized box on screen fell straight through to a full box_union invalidate, repainting content that had only shifted. Treat the pinned and repositioned cases as one shift by the top-left delta (dx==dy==0 for grow-in-place): intersect "before" with the new footprint pulled back by (dx,dy) to get the valid source, one screen_copy_rect to slide it, then invalidate only the newly-exposed and vacated regions plus the reflowed furniture. Still topmost-only, still falls back when the format can't blit or nothing overlaps. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- libraries/wuss/furniture/toggle-action.c | 75 ++++++++++++++---------- libraries/wuss/test/wuss-test.c | 39 ++++++++++-- 2 files changed, 79 insertions(+), 35 deletions(-) diff --git a/libraries/wuss/furniture/toggle-action.c b/libraries/wuss/furniture/toggle-action.c index 007f503b..496d46c1 100644 --- a/libraries/wuss/furniture/toggle-action.c +++ b/libraries/wuss/furniture/toggle-action.c @@ -6,7 +6,8 @@ void wuss__furniture_toggle_size(wuss_window_t *window) { - box_t before, new_visible, dirty, copied; + box_t before, new_visible, dirty, copied, blit_src, blit_dst; + int dx, dy, blitted; before = window->visible; @@ -92,40 +93,54 @@ void wuss__furniture_toggle_size(wuss_window_t *window) } } - /* ponytail: the sliver-blit fast path below assumes the top-left didn't - * move; toggle can now reposition a window to fit it on screen, so gate - * it on x0/y0 being unchanged and let the general box_union path handle - * the moved case. Restore (pre_toggle) can move the top-left too -- same - * gate covers it. */ + /* The window content is just anchored positions plus furniture, so + * wherever "before" and the new footprint overlap the pixels only need + * sliding by the top-left delta -- one screen_copy_rect -- with a real + * repaint of just the newly-exposed and vacated regions. Only for a + * topmost window (nothing above it, so no occluder pixels to preserve or + * step on) whose screen format can blit. A pure grow-in-place is the + * dx==dy==0 case of the same shift. */ + dx = window->visible.x0 - before.x0; + dy = window->visible.y0 - before.y0; + blitted = 0; + if (!(window->flags & wuss_WINDOW_NO_RESIZE_BLIT) && - window->wuss->z_order.next == &window->link && - before.x0 == window->visible.x0 && before.y0 == window->visible.y0 && - screen_copy_rect(window->wuss->scr, &before, - POINT(before.x0, before.y0), &copied) == result_OK) + window->wuss->z_order.next == &window->link) + { + box_t shifted; + + /* the part of "before" that, slid by (dx,dy), lands within the new + * footprint: its pixels are the valid source for that overlap */ + box_translated(&window->visible, -dx, -dy, &shifted); + if (box_intersection(&before, &shifted, &blit_src) == 0 && + !box_is_empty(&blit_src)) + { + box_translated(&blit_src, dx, dy, &blit_dst); + if (screen_copy_rect(window->wuss->scr, &blit_src, + POINT(blit_dst.x0, blit_dst.y0), + &copied) == result_OK) + blitted = 1; + } + } + + if (blitted) { - /* Topmost, the screen format supports the blit, and the top-left has - * not moved: re-blitting "before" onto itself is a no-op that just - * confirms which of its pixels are still on-screen -- only whatever's - * newly exposed (grown) or newly vacated (shrunk) relative to that - * needs an actual repaint. */ - wuss__invalidate_minus(window->wuss, &before, &window->visible); + /* "before" minus the pixels the blit left valid at their new home: the + * vacated L-shape (and any bit whose new position fell off-screen). */ + wuss__invalidate_minus(window->wuss, &before, &copied); + /* the new footprint minus what the blit filled: the newly-exposed area. */ wuss__invalidate_minus(window->wuss, &window->visible, &copied); - /* Unlike a move, a toggle changes the window's size, so furniture that - * lays itself out relative to that size (titlebar icons anchored to its - * right edge, scrollbar well/sausage proportions, the resize corner) - * reflows even where the blit reused valid content pixels -- e.g. the - * old toggle icon location is now mid-titlebar, not redrawn by either - * invalidate_minus above since it falls inside both "before" and the - * new "visible". Force it dirty regardless of the blit. */ - if (window->visible.x1 - window->visible.x0 > before.x1 - before.x0 || + /* Furniture lays itself out relative to the window's size/position + * (titlebar icons on the right edge, scrollbar proportions, the resize + * corner), so it reflows even where the blit reused valid content + * pixels. Force the new furniture dirty always, and the old furniture + * positions too when the window grew or moved -- either can strand a + * stale scrollbar/icon glyph inside what is now interior content, a + * region neither invalidate_minus above repaints. */ + if (dx != 0 || dy != 0 || + window->visible.x1 - window->visible.x0 > before.x1 - before.x0 || window->visible.y1 - window->visible.y0 > before.y1 - before.y0) - /* Growing strands old furniture (e.g. the old vscroll column) inside - * what's now interior content, a region the blit above treats as - * already-valid and so never repaints -- force its old position dirty - * too. Shrinking needs no such help: old furniture positions only - * ever land outside the new, smaller box, already covered by the - * invalidate_minus vacated-region call above. */ wuss__furniture_invalidate_for(window, &before); wuss__furniture_invalidate(window); } diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 3044acdc..8426fb97 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -2421,21 +2421,24 @@ result_t wuss_test(const char *resources) wuss_window_close(win_v); } - printf("test: toggle-size repositions a window near the far edge toward the origin, then restores it exactly\n"); + printf("test: a toggle-size that repositions the window blits the overlapping content rather than repainting it all, and restores the exact pre-toggle box\n"); { wuss_task_t *delegate_p; box_t box_p, before, after, titlebar, toggle; wuss_window_t *win_p; int outline_px, titlebar_height, inset, icon, cx, cy; + int i, dx, dy, kept_x, kept_y, kept_dirty; delegate_p = mk_task(wuss, NULL, NULL); if (delegate_p == NULL) goto Failure; - box_p.x0 = 150; box_p.y0 = 150; - box_p.x1 = 190; box_p.y1 = 190; /* 40x40 content, hard against the bottom- - * right of the 200x200 screen; doc big - * enough that maximize is screen-limited */ + box_p.x0 = 110; box_p.y0 = 110; + box_p.x1 = 190; box_p.y1 = 190; /* 80x80 content, well down the bottom-right + * of the 200x200 screen -- big enough to + * 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, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), SIZE2D(400, 400), SIZE2D(0, 0), &win_p); @@ -2477,6 +2480,32 @@ result_t wuss_test(const char *resources) if (after.x1 - after.x0 < 190 || after.y1 - after.y0 < 190) goto Failure; /* it genuinely fills (nearly) the whole screen */ + dx = after.x0 - before.x0; + dy = after.y0 - before.y0; + + /* a point in the old content interior, clear of outline/titlebar/ + * scrollbar furniture on every side: after the toggle's shift by + * (dx,dy) it lands in the new content interior, and the blit must have + * carried its pixels there -- so it must NOT be in the dirty list. + * (This window starts screen-clamped hard against the bottom-right, so + * the maximized footprint is a superset of the old one -- there is no + * vacated backdrop strip to check, only the preserved interior.) */ + kept_x = (before.x0 + outline_px + before.x1 - outline_px - icon) / 2 + dx; + kept_y = (before.y0 + outline_px + titlebar_height + + before.y1 - outline_px - icon) / 2 + dy; + + kept_dirty = 0; + for (i = 0; i < wuss_get_dirty_count(wuss); i++) + { + box_t region; + + wuss_get_dirty(wuss, i, ®ion); + if (box_contains_point(®ion, kept_x, kept_y)) + kept_dirty = 1; + } + if (kept_dirty) + goto Failure; /* the shifted interior content was blitted, not repainted */ + rc = wuss_redraw_dirty(wuss); if (rc != result_OK) goto Failure; From d169b1aac2155c28fb2da2eeaabd0ff5232e41ca Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Fri, 4 Sep 2026 10:55:10 +0100 Subject: [PATCH 242/287] fix(wuss): stop the toggle-size blit carrying furniture The repositioning-toggle blit optimisation copied the whole "before" footprint, dragging stale titlebar, outline and scrollbar pixels into what became interior content once the window grew or moved. The furniture-strand special-case only invalidated the old furniture at its old position, not the blitted-in copy at before + (dx,dy). Blit the content box only: capture "before_content" before mutating "visible", intersect old and new content boxes for the slide source, and skip the blit entirely when the toggle re-clamped scroll (the whole new content box is already invalidated at the corrected offset). Invalidate "before \ copied" (covers every old furniture strip), "new_content \ copied" (newly-exposed content) and the reflowed furniture outright. Adds a regression assertion: the old vertical scrollbar column, shifted by the toggle delta, must land in the dirty list -- a whole-footprint blit leaves it as a furniture ghost. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- libraries/wuss/furniture/toggle-action.c | 65 +++++++++++++----------- libraries/wuss/test/wuss-test.c | 17 ++++++- 2 files changed, 50 insertions(+), 32 deletions(-) diff --git a/libraries/wuss/furniture/toggle-action.c b/libraries/wuss/furniture/toggle-action.c index 496d46c1..9ee85310 100644 --- a/libraries/wuss/furniture/toggle-action.c +++ b/libraries/wuss/furniture/toggle-action.c @@ -6,10 +6,12 @@ void wuss__furniture_toggle_size(wuss_window_t *window) { - box_t before, new_visible, dirty, copied, blit_src, blit_dst; - int dx, dy, blitted; + box_t before, before_content, new_visible, dirty, copied, blit_src, blit_dst; + int dx, dy, blitted, scroll_reclamped; before = window->visible; + wuss__content_box(window, &before_content); + scroll_reclamped = 0; if (wuss__window_toggled(window)) { @@ -90,29 +92,37 @@ void wuss__furniture_toggle_size(wuss_window_t *window) window->scroll = new_scroll; wuss__content_box(window, &content); wuss__invalidate_clipped(window, &content); + scroll_reclamped = 1; } } - /* The window content is just anchored positions plus furniture, so - * wherever "before" and the new footprint overlap the pixels only need - * sliding by the top-left delta -- one screen_copy_rect -- with a real - * repaint of just the newly-exposed and vacated regions. Only for a - * topmost window (nothing above it, so no occluder pixels to preserve or - * step on) whose screen format can blit. A pure grow-in-place is the - * dx==dy==0 case of the same shift. */ + /* Only the *content* pixels are anchored-position rendering that can just + * be slid; the furniture (titlebar, outline, scrollbars) reflows to the + * window's new size and position, so it must never be part of the blit -- + * blitting it would drag a stale titlebar/scrollbar strip into what is + * now interior content. Blit the old content box to where it now sits + * (shifted by the top-left delta; dx==dy==0 for a pure grow-in-place), + * then repaint the newly-exposed content, the vacated region and all the + * furniture. Topmost only (nothing above it, so no occluder pixels to + * preserve or step on) and only when the screen format can blit. Skipped + * when the toggle re-clamped scroll: the whole new content box is already + * invalidated above at the corrected offset. */ dx = window->visible.x0 - before.x0; dy = window->visible.y0 - before.y0; blitted = 0; - if (!(window->flags & wuss_WINDOW_NO_RESIZE_BLIT) && + if (!scroll_reclamped && + !(window->flags & wuss_WINDOW_NO_RESIZE_BLIT) && window->wuss->z_order.next == &window->link) { - box_t shifted; + box_t new_content, shifted; - /* the part of "before" that, slid by (dx,dy), lands within the new - * footprint: its pixels are the valid source for that overlap */ - box_translated(&window->visible, -dx, -dy, &shifted); - if (box_intersection(&before, &shifted, &blit_src) == 0 && + wuss__content_box(window, &new_content); + + /* the part of the old content box that, slid by (dx,dy), lands within + * the new content box: its pixels are the valid source for that overlap */ + box_translated(&new_content, -dx, -dy, &shifted); + if (box_intersection(&before_content, &shifted, &blit_src) == 0 && !box_is_empty(&blit_src)) { box_translated(&blit_src, dx, dy, &blit_dst); @@ -125,23 +135,16 @@ void wuss__furniture_toggle_size(wuss_window_t *window) if (blitted) { - /* "before" minus the pixels the blit left valid at their new home: the - * vacated L-shape (and any bit whose new position fell off-screen). */ + box_t new_content; + + wuss__content_box(window, &new_content); + + /* the old footprint minus the content pixels the blit left valid at + * their new home: the vacated region plus every furniture strip. */ wuss__invalidate_minus(window->wuss, &before, &copied); - /* the new footprint minus what the blit filled: the newly-exposed area. */ - wuss__invalidate_minus(window->wuss, &window->visible, &copied); - - /* Furniture lays itself out relative to the window's size/position - * (titlebar icons on the right edge, scrollbar proportions, the resize - * corner), so it reflows even where the blit reused valid content - * pixels. Force the new furniture dirty always, and the old furniture - * positions too when the window grew or moved -- either can strand a - * stale scrollbar/icon glyph inside what is now interior content, a - * region neither invalidate_minus above repaints. */ - if (dx != 0 || dy != 0 || - window->visible.x1 - window->visible.x0 > before.x1 - before.x0 || - window->visible.y1 - window->visible.y0 > before.y1 - before.y0) - wuss__furniture_invalidate_for(window, &before); + /* the new content box minus what the blit filled: newly-exposed content. */ + wuss__invalidate_minus(window->wuss, &new_content, &copied); + /* furniture always reflows -- repaint it at the new position outright. */ wuss__furniture_invalidate(window); } else diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 8426fb97..2a3ddf60 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -2429,6 +2429,7 @@ result_t wuss_test(const char *resources) wuss_window_t *win_p; int outline_px, titlebar_height, inset, icon, cx, cy; int i, dx, dy, kept_x, kept_y, kept_dirty; + int ghost_x, ghost_y, ghost_dirty; delegate_p = mk_task(wuss, NULL, NULL); if (delegate_p == NULL) goto Failure; @@ -2494,7 +2495,16 @@ result_t wuss_test(const char *resources) kept_y = (before.y0 + outline_px + titlebar_height + before.y1 - outline_px - icon) / 2 + dy; - kept_dirty = 0; + /* where the OLD vertical scrollbar column lands once shifted by (dx,dy): + * a whole-footprint blit would drag that stale scrollbar strip here, + * into what is now interior content. The blit must be content-only, so + * this point must be in the dirty list to be repainted as content. */ + ghost_x = before.x1 - outline_px - icon / 2 + dx; + ghost_y = (before.y0 + outline_px + titlebar_height + + before.y1 - outline_px) / 2 + dy; + + kept_dirty = 0; + ghost_dirty = 0; for (i = 0; i < wuss_get_dirty_count(wuss); i++) { box_t region; @@ -2502,9 +2512,14 @@ result_t wuss_test(const char *resources) wuss_get_dirty(wuss, i, ®ion); if (box_contains_point(®ion, kept_x, kept_y)) kept_dirty = 1; + if (box_contains_point(®ion, ghost_x, ghost_y)) + ghost_dirty = 1; } if (kept_dirty) goto Failure; /* the shifted interior content was blitted, not repainted */ + if (!ghost_dirty) + goto Failure; /* the old scrollbar's shifted position must repaint as + * content -- the blit must not have carried furniture */ rc = wuss_redraw_dirty(wuss); if (rc != result_OK) From 82b2e47e17c41b5543dd62155e2692a7e3487fb1 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Fri, 4 Sep 2026 11:22:46 +0100 Subject: [PATCH 243/287] fix(wuss): wuss_window_resize honours the requested size verbatim The on-screen clamp truncated a window to the visible strip when its top-left was off-screen: switching to a larger font in the chars task shrank the window to whatever screen space remained rather than sizing it to the new font's glyph grid. Drop the clamp from wuss_window_resize. The requested content size is now applied as asked and the window may overhang the screen edge -- the same latitude toggle-size and drag already allow. wuss_window_create keeps its own screen cap. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- libraries/wuss/test/wuss-test.c | 26 ++++++++++++++------------ libraries/wuss/window/resize.c | 14 ++++---------- 2 files changed, 18 insertions(+), 22 deletions(-) diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 2a3ddf60..1fc3c76e 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -800,23 +800,24 @@ result_t wuss_test(const char *resources) if (width != 52 || height != 72) goto Failure; - printf("test: window_resize can never grow a window past the screen\n"); + printf("test: window_resize honours the requested size even past the " + "screen edge\n"); /* screen is 200x200; win_a's visible box sits at (0,25) with a 1px * outline all round and a 20px titlebar. asking for a 500x500 content - * area must clamp so the visible box still fits: width 200-0-2, - * height 200-25-2-20. */ + * area is honoured verbatim; the visible box simply overhangs the + * screen (width 500+2, height 500+2+20). */ rc = wuss_window_resize(win_a, SIZE2D(500, 500)); if (rc != result_OK) goto Failure; wuss_window_get_content_bounds(win_a, &content); - if (content.x1 - content.x0 != 198 || content.y1 - content.y0 != 153) + if (content.x1 - content.x0 != 500 || content.y1 - content.y0 != 500) goto Failure; wuss_window_get_visible_bounds(win_a, &visible); - if (visible.x1 != 200 || visible.y1 != 200) + if (visible.x1 != 502 || visible.y1 != 547) goto Failure; - /* a request that already fits is honoured verbatim */ + /* a smaller request is likewise honoured verbatim */ rc = wuss_window_resize(win_a, SIZE2D(60, 40)); if (rc != result_OK) goto Failure; @@ -4263,18 +4264,19 @@ result_t wuss_test(const char *resources) content.x1 != 100 || content.y1 != 100) goto Failure; - printf("test: window_resize can never grow a window past the screen\n"); + printf("test: window_resize honours the requested size even past the " + "screen edge\n"); /* screen is 200x200; win_a sits at (0,0), chromeless. asking for a - * 500x500 content area must clamp to the 200x200 screen. */ + * 500x500 content area is honoured verbatim. */ rc = wuss_window_resize(win_a, SIZE2D(500, 500)); if (rc != result_OK) goto Failure; wuss_window_get_content_bounds(win_a, &content); - if (content.x1 - content.x0 != 200 || content.y1 - content.y0 != 200) + if (content.x1 - content.x0 != 500 || content.y1 - content.y0 != 500) goto Failure; - /* a request that already fits is left exactly as asked */ + /* a smaller request is likewise honoured verbatim */ rc = wuss_window_resize(win_a, SIZE2D(120, 90)); if (rc != result_OK) goto Failure; @@ -4282,13 +4284,13 @@ result_t wuss_test(const char *resources) if (content.x1 - content.x0 != 120 || content.y1 - content.y0 != 90) goto Failure; - /* a window whose top-left is offset only gets the space that is left */ + /* an offset top-left does not clip the size either */ wuss_window_move(win_a, POINT(60, 40)); rc = wuss_window_resize(win_a, SIZE2D(500, 500)); if (rc != result_OK) goto Failure; wuss_window_get_content_bounds(win_a, &content); - if (content.x1 - content.x0 != 140 || content.y1 - content.y0 != 160) + if (content.x1 - content.x0 != 500 || content.y1 - content.y0 != 500) goto Failure; wuss_window_move(win_a, POINT(0, 0)); rc = wuss_window_resize(win_a, SIZE2D(100, 100)); diff --git a/libraries/wuss/window/resize.c b/libraries/wuss/window/resize.c index b7edcb38..014e1358 100644 --- a/libraries/wuss/window/resize.c +++ b/libraries/wuss/window/resize.c @@ -45,16 +45,10 @@ result_t wuss_window_resize(wuss_window_t *window, size2d_t size) /* a manual resize desyncs the window from its layout-packer slot */ wuss__release_packed(window); - /* a window can never grow larger than the screen */ - { - size2d_t max; - - wuss__max_content_on_screen(window, &max); - if (size.w > max.w) - size.w = max.w; - if (size.h > max.h) - size.h = max.h; - } + /* The requested content size is honoured verbatim: a window may end up + * overhanging the screen edge (same as one dragged there, or one whose + * far corner is off-screen when toggle-size floors it at WUSS_MIN_CONTENT). + * Only wuss_window_create still caps at the screen. */ outline_px = wuss__outline_px(window); titlebar_height = wuss__titlebar_height(window); From fc11bcbc99bd69c2bb8ebd641ce8b488d7021282 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Fri, 4 Sep 2026 11:28:27 +0100 Subject: [PATCH 244/287] refactor(wuss): split menu-desc helper out of the core menu header wuss_menu_create_from_desc() and wuss_menu_destroy() build and free a heap wuss_menu_t tree from a descriptor string -- a convenience layer, not part of the core menu plumbing. Move their declarations into a new include/wuss/menu-desc.h and the implementation from libraries/wuss/menu/create-from-desc.c to libraries/wuss/helpers/. menu.h now only carries the static-menu open/close/is-open API and points at the new header. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- CMakeLists.txt | 3 +- apps/wuss/main.c | 1 + include/wuss/menu-desc.h | 73 +++++++++++++++++++ include/wuss/menu.h | 43 +---------- .../wuss/{menu => helpers}/create-from-desc.c | 4 +- libraries/wuss/test/wuss-test.c | 1 + 6 files changed, 82 insertions(+), 43 deletions(-) create mode 100644 include/wuss/menu-desc.h rename libraries/wuss/{menu => helpers}/create-from-desc.c (99%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 83088b1b..28d947ba 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -127,6 +127,7 @@ if(WUSS_ICONS) endif() if(WUSS_MENUS) list(APPEND PUBLIC_HEADERS include/wuss/menu.h) + list(APPEND PUBLIC_HEADERS include/wuss/menu-desc.h) endif() if(WUSS_COMPONENTS) list(APPEND PUBLIC_HEADERS include/wuss/component/fontmenu.h) @@ -426,7 +427,7 @@ set(WUSS_ICON_SOURCES set(WUSS_MENU_SOURCES libraries/wuss/menu/menu.c - libraries/wuss/menu/create-from-desc.c + libraries/wuss/helpers/create-from-desc.c libraries/wuss/menu.h) set(WUSS_COMPONENT_SOURCES diff --git a/apps/wuss/main.c b/apps/wuss/main.c index 6fb56978..e78d0ad4 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -27,6 +27,7 @@ #include "wuss/wuss.h" #include "wuss/window.h" #include "wuss/menu.h" +#include "wuss/menu-desc.h" #include "frontend.h" diff --git a/include/wuss/menu-desc.h b/include/wuss/menu-desc.h new file mode 100644 index 00000000..d15c9a19 --- /dev/null +++ b/include/wuss/menu-desc.h @@ -0,0 +1,73 @@ +/* wuss/menu-desc.h -- build a wuss_menu_t tree from a descriptor string */ + +/** + * \file menu-desc.h + * + * A convenience helper on top of wuss/menu.h: parse a compact + * PrivateEye-style descriptor string into a heap wuss_menu_t tree the caller + * owns, and free it again. Nothing in the core menu helper depends on this; + * a task that hand-assembles its wuss_menu_t arrays never needs to include it. + * + * Built only when WUSS_MENUS is defined. + */ + +#ifndef WUSS_MENU_DESC_H +#define WUSS_MENU_DESC_H + +#ifdef __cplusplus +extern "C" +{ +#endif + +#include "base/result.h" + +#include "wuss/menu.h" + +/* ----------------------------------------------------------------------- */ + +/** + * Build a wuss_menu_t tree from a compact descriptor string. The syntax is + * lifted from PrivateEye's menu_create_from_desc. A comma separates items. + * The very first token is the root menu's titlebar caption, not an item -- + * exactly as the first token inside a '{ }' is that submenu's title -- so + * the same descriptor strings port across unchanged. Submenus keep the Wimp + * behaviour of discarding their title token; only the root's is kept. A + * leading '|' on an item draws a dashed rule above it, marking a group + * boundary; the item itself stays an ordinary interactive row. A '{ ... }' + * group after an item is that item's submenu. A per-token prefix '!' ticks + * the item, '~' shades (disables) it, and '>' attaches a submenu pulled as a + * <tt>const wuss_menu_t *</tt> from the varargs rather than from a following + * '{ }' block. A "%s" in a token substitutes the next <tt>const char *</tt> + * vararg. The '>' and "%s" varargs are consumed in the order they are + * encountered scanning left to right. + * + * Example: <tt>wuss_menu_create_from_desc(&m, "Display, Open, !Grid, + * ~Export, |Quit")</tt> -- "Display" is the caption; the menu has four + * items, with a dashed rule above "Quit". + * + * The whole tree, including copied label text, is one heap allocation graph + * owned by the caller; free it with wuss_menu_destroy. wuss_menu_open treats + * a desc-built tree exactly like a static literal. + * + * \param[out] out Filled with the root menu on success, untouched on + * failure. + * \param[in] desc Descriptor string; its first token is the root caption. + * \return \ref result_OK, \ref result_OOM, or \ref result_BAD_ARG for a + * malformed descriptor (unbalanced braces, empty token, too deep). + */ +result_t wuss_menu_create_from_desc(wuss_menu_t **out, const char *desc, ...); + +/** + * Free a tree built by wuss_menu_create_from_desc, including every submenu + * and copied label. Safe to pass NULL. Never call this on a static or + * caller-assembled wuss_menu_t. + * + * \param[in] menu Root menu returned by wuss_menu_create_from_desc, or NULL. + */ +void wuss_menu_destroy(wuss_menu_t *menu); + +#ifdef __cplusplus +} +#endif + +#endif /* WUSS_MENU_DESC_H */ diff --git a/include/wuss/menu.h b/include/wuss/menu.h index d86b5b88..d99d50e2 100644 --- a/include/wuss/menu.h +++ b/include/wuss/menu.h @@ -120,46 +120,9 @@ int wuss_menu_is_open(wuss_menu_handle_t handle); /* ----------------------------------------------------------------------- */ -/** - * Build a wuss_menu_t tree from a compact descriptor string. The syntax is - * lifted from PrivateEye's menu_create_from_desc. A comma separates items. - * The very first token is the root menu's titlebar caption, not an item -- - * exactly as the first token inside a '{ }' is that submenu's title -- so - * the same descriptor strings port across unchanged. Submenus keep the Wimp - * behaviour of discarding their title token; only the root's is kept. A - * leading '|' on an item draws a dashed rule above it, marking a group - * boundary; the item itself stays an ordinary interactive row. A '{ ... }' - * group after an item is that item's submenu. A per-token prefix '!' ticks - * the item, '~' shades (disables) it, and '>' attaches a submenu pulled as a - * <tt>const wuss_menu_t *</tt> from the varargs rather than from a following - * '{ }' block. A "%s" in a token substitutes the next <tt>const char *</tt> - * vararg. The '>' and "%s" varargs are consumed in the order they are - * encountered scanning left to right. - * - * Example: <tt>wuss_menu_create_from_desc(&m, "Display, Open, !Grid, - * ~Export, |Quit")</tt> -- "Display" is the caption; the menu has four - * items, with a dashed rule above "Quit". - * - * The whole tree, including copied label text, is one heap allocation graph - * owned by the caller; free it with wuss_menu_destroy. wuss_menu_open treats - * a desc-built tree exactly like a static literal. - * - * \param[out] out Filled with the root menu on success, untouched on - * failure. - * \param[in] desc Descriptor string; its first token is the root caption. - * \return \ref result_OK, \ref result_OOM, or \ref result_BAD_ARG for a - * malformed descriptor (unbalanced braces, empty token, too deep). - */ -result_t wuss_menu_create_from_desc(wuss_menu_t **out, const char *desc, ...); - -/** - * Free a tree built by wuss_menu_create_from_desc, including every submenu - * and copied label. Safe to pass NULL. Never call this on a static or - * caller-assembled wuss_menu_t. - * - * \param[in] menu Root menu returned by wuss_menu_create_from_desc, or NULL. - */ -void wuss_menu_destroy(wuss_menu_t *menu); +/* Building a wuss_menu_t tree from a compact descriptor string, and freeing + * it again, lives in wuss/menu-desc.h -- a convenience layer on top of this + * core helper. */ #ifdef __cplusplus } diff --git a/libraries/wuss/menu/create-from-desc.c b/libraries/wuss/helpers/create-from-desc.c similarity index 99% rename from libraries/wuss/menu/create-from-desc.c rename to libraries/wuss/helpers/create-from-desc.c index 46787f11..e6c92bb2 100644 --- a/libraries/wuss/menu/create-from-desc.c +++ b/libraries/wuss/helpers/create-from-desc.c @@ -1,4 +1,4 @@ -/* wuss/menu/create-from-desc.c -- build a menu tree from a descriptor string */ +/* wuss/helpers/create-from-desc.c -- build a menu tree from a descriptor string */ /* The token machine (Token / Parser / getopt / getname / getnext / isdelim) * and the explicit menu stack are ported from PrivateEye's @@ -22,7 +22,7 @@ #include "base/result.h" #include "utils/array.h" -#include "wuss/menu.h" +#include "wuss/menu-desc.h" /* ----------------------------------------------------------------------- */ diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 1fc3c76e..7ec90b2e 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -24,6 +24,7 @@ #include "wuss/window.h" #ifdef WUSS_MENUS #include "wuss/menu.h" +#include "wuss/menu-desc.h" #endif #ifdef WUSS_COMPONENTS #include "wuss/component/fontmenu.h" From f7932a568a8da5737dad062fe21a66b11de0b2eb Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Fri, 4 Sep 2026 11:32:06 +0100 Subject: [PATCH 245/287] docs(wuss): annotate free() const-discard casts in menu teardown The menu struct holds items, title and item text as const pointers; the teardown paths cast them through (void *) to free them. Add a /* discard const */ comment at each site so the cast reads as intent. --- libraries/wuss/component/colourmenu.c | 6 +++--- libraries/wuss/component/fontmenu.c | 6 +++--- libraries/wuss/helpers/create-from-desc.c | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/libraries/wuss/component/colourmenu.c b/libraries/wuss/component/colourmenu.c index aee07b6b..9e89efaa 100644 --- a/libraries/wuss/component/colourmenu.c +++ b/libraries/wuss/component/colourmenu.c @@ -42,9 +42,9 @@ static void menu_free(const wuss_alloc_t *a, wuss_menu_t *m) return; for (i = 0; i < m->nitems; i++) - a->free((void *) m->items[i].text); - a->free((void *) m->items); - a->free((void *) m->title); + a->free((void *) m->items[i].text); /* discard const */ + a->free((void *) m->items); /* discard const */ + a->free((void *) m->title); /* discard const */ a->free(m); } diff --git a/libraries/wuss/component/fontmenu.c b/libraries/wuss/component/fontmenu.c index e080d23e..76df5cfd 100644 --- a/libraries/wuss/component/fontmenu.c +++ b/libraries/wuss/component/fontmenu.c @@ -41,9 +41,9 @@ static void menu_free(const wuss_alloc_t *a, wuss_menu_t *m) return; for (i = 0; i < m->nitems; i++) - a->free((void *) m->items[i].text); - a->free((void *) m->items); - a->free((void *) m->title); + a->free((void *) m->items[i].text); /* discard const */ + a->free((void *) m->items); /* discard const */ + a->free((void *) m->title); /* discard const */ a->free(m); } diff --git a/libraries/wuss/helpers/create-from-desc.c b/libraries/wuss/helpers/create-from-desc.c index e6c92bb2..85ac41d7 100644 --- a/libraries/wuss/helpers/create-from-desc.c +++ b/libraries/wuss/helpers/create-from-desc.c @@ -123,11 +123,11 @@ void wuss_menu_destroy(wuss_menu_t *menu) for (i = 0; i < menu->nitems; i++) { - free((void *) menu->items[i].text); + free((void *) menu->items[i].text); /* discard const */ wuss_menu_destroy((wuss_menu_t *) menu->items[i].submenu); } - free((void *) menu->title); - free((void *) menu->items); + free((void *) menu->title); /* discard const */ + free((void *) menu->items); /* discard const */ free(menu); } @@ -290,7 +290,7 @@ static void build_discard(Building *b) for (i = 0; i < b->n; i++) { - free((void *) b->items[i].text); + free((void *) b->items[i].text); /* discard const */ wuss_menu_destroy((wuss_menu_t *) b->items[i].submenu); } free(b->items); From d1dd1edd64130d5ab20e1f9eb7faf9f50d412989 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Fri, 4 Sep 2026 11:43:38 +0100 Subject: [PATCH 246/287] refactor(wuss): move core sources into a core/ subdir The optional subsystems (furniture, icon, menu, component) already live in their own subdirectories; the core window-manager sources sat loose at the wuss root. Group them under libraries/wuss/core/, taking the window/ and task/ subdirs (both part of WUSS_CORE_SOURCES) with them. impl.h moves too, so its includes of the still-root furniture.h/icon.h/menu.h become ../ and the staying subsystem files reference ../core/impl.h. CMake WUSS_CORE_SOURCES paths reprefixed accordingly; no build options change. --- CMakeLists.txt | 72 +++++++++---------- libraries/wuss/component/colourmenu.c | 2 +- libraries/wuss/component/fontmenu.c | 2 +- libraries/wuss/{ => core}/backdrop.c | 0 libraries/wuss/{ => core}/create.c | 0 libraries/wuss/{ => core}/destroy.c | 0 libraries/wuss/{ => core}/get-font.c | 0 libraries/wuss/{ => core}/get-pointer.c | 0 libraries/wuss/{ => core}/idle.c | 0 libraries/wuss/{ => core}/impl.h | 6 +- libraries/wuss/{ => core}/invalidate.c | 0 libraries/wuss/{ => core}/mouse-click.c | 0 libraries/wuss/{ => core}/mouse-move.c | 0 libraries/wuss/{ => core}/nearest-colour.c | 0 .../wuss/{ => core}/rebuild-palettecache.c | 0 libraries/wuss/{ => core}/redraw.c | 0 libraries/wuss/{ => core}/scroll-step.c | 0 libraries/wuss/{ => core}/scroll.c | 0 libraries/wuss/{ => core}/set-backdrop.c | 0 libraries/wuss/{ => core}/set-palette.c | 0 libraries/wuss/{ => core}/task/create.c | 0 libraries/wuss/{ => core}/task/deliver.c | 0 libraries/wuss/{ => core}/task/destroy.c | 0 .../wuss/{ => core}/task/set-autoclose.c | 0 libraries/wuss/{ => core}/window/at.c | 0 libraries/wuss/{ => core}/window/close.c | 0 .../wuss/{ => core}/window/create-placed.c | 0 libraries/wuss/{ => core}/window/create.c | 0 .../{ => core}/window/get-content-bounds.c | 0 libraries/wuss/{ => core}/window/get-scroll.c | 0 .../{ => core}/window/get-visible-bounds.c | 0 libraries/wuss/{ => core}/window/invalidate.c | 0 libraries/wuss/{ => core}/window/move.c | 0 libraries/wuss/{ => core}/window/resize.c | 0 libraries/wuss/{ => core}/window/restack.c | 0 .../wuss/{ => core}/window/set-background.c | 0 libraries/wuss/{ => core}/window/set-hidden.c | 0 libraries/wuss/{ => core}/window/set-scroll.c | 0 libraries/wuss/{ => core}/window/try-close.c | 0 libraries/wuss/furniture/back-box.c | 2 +- libraries/wuss/furniture/close-box.c | 2 +- libraries/wuss/furniture/content-box.c | 2 +- libraries/wuss/furniture/drag-resize.c | 2 +- libraries/wuss/furniture/draw.c | 2 +- libraries/wuss/furniture/hit-test.c | 2 +- libraries/wuss/furniture/hscroll-box.c | 2 +- libraries/wuss/furniture/invalidate.c | 2 +- libraries/wuss/furniture/ops.c | 2 +- libraries/wuss/furniture/resize-box.c | 2 +- libraries/wuss/furniture/scroll-action.c | 2 +- libraries/wuss/furniture/titlebar-box.c | 2 +- libraries/wuss/furniture/toggle-action.c | 2 +- libraries/wuss/furniture/toggle-box.c | 2 +- libraries/wuss/furniture/vscroll-box.c | 2 +- libraries/wuss/icon/create-array.c | 2 +- libraries/wuss/icon/create.c | 2 +- libraries/wuss/icon/delete.c | 2 +- libraries/wuss/icon/draw.c | 2 +- libraries/wuss/icon/free.c | 2 +- libraries/wuss/icon/from-spec.c | 2 +- libraries/wuss/icon/get-bbox.c | 2 +- libraries/wuss/icon/get-selected.c | 2 +- libraries/wuss/icon/get-text.c | 2 +- libraries/wuss/icon/get-type.c | 2 +- libraries/wuss/icon/get-window.c | 2 +- libraries/wuss/icon/hit-test.c | 2 +- libraries/wuss/icon/invalidate.c | 2 +- libraries/wuss/icon/plot.c | 2 +- libraries/wuss/icon/screen-box.c | 2 +- libraries/wuss/icon/set-hidden.c | 2 +- libraries/wuss/icon/set-hover.c | 2 +- libraries/wuss/icon/set-selected.c | 2 +- libraries/wuss/icon/set-text.c | 2 +- libraries/wuss/menu/menu.c | 2 +- libraries/wuss/test/wuss-test.c | 2 +- 75 files changed, 77 insertions(+), 77 deletions(-) rename libraries/wuss/{ => core}/backdrop.c (100%) rename libraries/wuss/{ => core}/create.c (100%) rename libraries/wuss/{ => core}/destroy.c (100%) rename libraries/wuss/{ => core}/get-font.c (100%) rename libraries/wuss/{ => core}/get-pointer.c (100%) rename libraries/wuss/{ => core}/idle.c (100%) rename libraries/wuss/{ => core}/impl.h (99%) rename libraries/wuss/{ => core}/invalidate.c (100%) rename libraries/wuss/{ => core}/mouse-click.c (100%) rename libraries/wuss/{ => core}/mouse-move.c (100%) rename libraries/wuss/{ => core}/nearest-colour.c (100%) rename libraries/wuss/{ => core}/rebuild-palettecache.c (100%) rename libraries/wuss/{ => core}/redraw.c (100%) rename libraries/wuss/{ => core}/scroll-step.c (100%) rename libraries/wuss/{ => core}/scroll.c (100%) rename libraries/wuss/{ => core}/set-backdrop.c (100%) rename libraries/wuss/{ => core}/set-palette.c (100%) rename libraries/wuss/{ => core}/task/create.c (100%) rename libraries/wuss/{ => core}/task/deliver.c (100%) rename libraries/wuss/{ => core}/task/destroy.c (100%) rename libraries/wuss/{ => core}/task/set-autoclose.c (100%) rename libraries/wuss/{ => core}/window/at.c (100%) rename libraries/wuss/{ => core}/window/close.c (100%) rename libraries/wuss/{ => core}/window/create-placed.c (100%) rename libraries/wuss/{ => core}/window/create.c (100%) rename libraries/wuss/{ => core}/window/get-content-bounds.c (100%) rename libraries/wuss/{ => core}/window/get-scroll.c (100%) rename libraries/wuss/{ => core}/window/get-visible-bounds.c (100%) rename libraries/wuss/{ => core}/window/invalidate.c (100%) rename libraries/wuss/{ => core}/window/move.c (100%) rename libraries/wuss/{ => core}/window/resize.c (100%) rename libraries/wuss/{ => core}/window/restack.c (100%) rename libraries/wuss/{ => core}/window/set-background.c (100%) rename libraries/wuss/{ => core}/window/set-hidden.c (100%) rename libraries/wuss/{ => core}/window/set-scroll.c (100%) rename libraries/wuss/{ => core}/window/try-close.c (100%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 28d947ba..5493dd2d 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -348,42 +348,42 @@ set(UTILS_SOURCES libraries/utils/primes/primes.c) set(WUSS_CORE_SOURCES - libraries/wuss/backdrop.c - libraries/wuss/create.c - libraries/wuss/destroy.c - libraries/wuss/get-font.c - libraries/wuss/get-pointer.c - libraries/wuss/idle.c - libraries/wuss/impl.h - libraries/wuss/invalidate.c - libraries/wuss/mouse-click.c - libraries/wuss/mouse-move.c - libraries/wuss/nearest-colour.c - libraries/wuss/rebuild-palettecache.c - libraries/wuss/redraw.c - libraries/wuss/scroll.c - libraries/wuss/scroll-step.c - libraries/wuss/set-backdrop.c - libraries/wuss/set-palette.c - libraries/wuss/task/create.c - libraries/wuss/task/destroy.c - libraries/wuss/task/set-autoclose.c - libraries/wuss/task/deliver.c - libraries/wuss/window/at.c - libraries/wuss/window/create.c - libraries/wuss/window/create-placed.c - libraries/wuss/window/close.c - libraries/wuss/window/get-content-bounds.c - libraries/wuss/window/get-scroll.c - libraries/wuss/window/get-visible-bounds.c - libraries/wuss/window/invalidate.c - libraries/wuss/window/move.c - libraries/wuss/window/resize.c - libraries/wuss/window/restack.c - libraries/wuss/window/set-background.c - libraries/wuss/window/set-hidden.c - libraries/wuss/window/set-scroll.c - libraries/wuss/window/try-close.c) + libraries/wuss/core/backdrop.c + libraries/wuss/core/create.c + libraries/wuss/core/destroy.c + libraries/wuss/core/get-font.c + libraries/wuss/core/get-pointer.c + libraries/wuss/core/idle.c + libraries/wuss/core/impl.h + libraries/wuss/core/invalidate.c + libraries/wuss/core/mouse-click.c + libraries/wuss/core/mouse-move.c + libraries/wuss/core/nearest-colour.c + libraries/wuss/core/rebuild-palettecache.c + libraries/wuss/core/redraw.c + libraries/wuss/core/scroll.c + libraries/wuss/core/scroll-step.c + libraries/wuss/core/set-backdrop.c + libraries/wuss/core/set-palette.c + libraries/wuss/core/task/create.c + libraries/wuss/core/task/destroy.c + libraries/wuss/core/task/set-autoclose.c + libraries/wuss/core/task/deliver.c + libraries/wuss/core/window/at.c + libraries/wuss/core/window/create.c + libraries/wuss/core/window/create-placed.c + libraries/wuss/core/window/close.c + libraries/wuss/core/window/get-content-bounds.c + libraries/wuss/core/window/get-scroll.c + libraries/wuss/core/window/get-visible-bounds.c + libraries/wuss/core/window/invalidate.c + libraries/wuss/core/window/move.c + libraries/wuss/core/window/resize.c + libraries/wuss/core/window/restack.c + libraries/wuss/core/window/set-background.c + libraries/wuss/core/window/set-hidden.c + libraries/wuss/core/window/set-scroll.c + libraries/wuss/core/window/try-close.c) set(WUSS_FURNITURE_SOURCES libraries/wuss/furniture/back-box.c diff --git a/libraries/wuss/component/colourmenu.c b/libraries/wuss/component/colourmenu.c index 9e89efaa..faa77554 100644 --- a/libraries/wuss/component/colourmenu.c +++ b/libraries/wuss/component/colourmenu.c @@ -18,7 +18,7 @@ #include "wuss/component/colourmenu.h" -#include "../impl.h" +#include "../core/impl.h" /* ----------------------------------------------------------------------- */ diff --git a/libraries/wuss/component/fontmenu.c b/libraries/wuss/component/fontmenu.c index 76df5cfd..812b4326 100644 --- a/libraries/wuss/component/fontmenu.c +++ b/libraries/wuss/component/fontmenu.c @@ -17,7 +17,7 @@ #include "wuss/component/fontmenu.h" -#include "../impl.h" +#include "../core/impl.h" /* ----------------------------------------------------------------------- */ diff --git a/libraries/wuss/backdrop.c b/libraries/wuss/core/backdrop.c similarity index 100% rename from libraries/wuss/backdrop.c rename to libraries/wuss/core/backdrop.c diff --git a/libraries/wuss/create.c b/libraries/wuss/core/create.c similarity index 100% rename from libraries/wuss/create.c rename to libraries/wuss/core/create.c diff --git a/libraries/wuss/destroy.c b/libraries/wuss/core/destroy.c similarity index 100% rename from libraries/wuss/destroy.c rename to libraries/wuss/core/destroy.c diff --git a/libraries/wuss/get-font.c b/libraries/wuss/core/get-font.c similarity index 100% rename from libraries/wuss/get-font.c rename to libraries/wuss/core/get-font.c diff --git a/libraries/wuss/get-pointer.c b/libraries/wuss/core/get-pointer.c similarity index 100% rename from libraries/wuss/get-pointer.c rename to libraries/wuss/core/get-pointer.c diff --git a/libraries/wuss/idle.c b/libraries/wuss/core/idle.c similarity index 100% rename from libraries/wuss/idle.c rename to libraries/wuss/core/idle.c diff --git a/libraries/wuss/impl.h b/libraries/wuss/core/impl.h similarity index 99% rename from libraries/wuss/impl.h rename to libraries/wuss/core/impl.h index 74bd3e5e..67ae740d 100644 --- a/libraries/wuss/impl.h +++ b/libraries/wuss/core/impl.h @@ -21,13 +21,13 @@ #include "wuss/task.h" #ifdef WUSS_FURNITURE -#include "furniture.h" +#include "../furniture.h" #endif #ifdef WUSS_ICONS -#include "icon.h" +#include "../icon.h" #endif #ifdef WUSS_MENUS -#include "menu.h" +#include "../menu.h" #endif #define WUSS_TITLE_MAX 63 diff --git a/libraries/wuss/invalidate.c b/libraries/wuss/core/invalidate.c similarity index 100% rename from libraries/wuss/invalidate.c rename to libraries/wuss/core/invalidate.c diff --git a/libraries/wuss/mouse-click.c b/libraries/wuss/core/mouse-click.c similarity index 100% rename from libraries/wuss/mouse-click.c rename to libraries/wuss/core/mouse-click.c diff --git a/libraries/wuss/mouse-move.c b/libraries/wuss/core/mouse-move.c similarity index 100% rename from libraries/wuss/mouse-move.c rename to libraries/wuss/core/mouse-move.c diff --git a/libraries/wuss/nearest-colour.c b/libraries/wuss/core/nearest-colour.c similarity index 100% rename from libraries/wuss/nearest-colour.c rename to libraries/wuss/core/nearest-colour.c diff --git a/libraries/wuss/rebuild-palettecache.c b/libraries/wuss/core/rebuild-palettecache.c similarity index 100% rename from libraries/wuss/rebuild-palettecache.c rename to libraries/wuss/core/rebuild-palettecache.c diff --git a/libraries/wuss/redraw.c b/libraries/wuss/core/redraw.c similarity index 100% rename from libraries/wuss/redraw.c rename to libraries/wuss/core/redraw.c diff --git a/libraries/wuss/scroll-step.c b/libraries/wuss/core/scroll-step.c similarity index 100% rename from libraries/wuss/scroll-step.c rename to libraries/wuss/core/scroll-step.c diff --git a/libraries/wuss/scroll.c b/libraries/wuss/core/scroll.c similarity index 100% rename from libraries/wuss/scroll.c rename to libraries/wuss/core/scroll.c diff --git a/libraries/wuss/set-backdrop.c b/libraries/wuss/core/set-backdrop.c similarity index 100% rename from libraries/wuss/set-backdrop.c rename to libraries/wuss/core/set-backdrop.c diff --git a/libraries/wuss/set-palette.c b/libraries/wuss/core/set-palette.c similarity index 100% rename from libraries/wuss/set-palette.c rename to libraries/wuss/core/set-palette.c diff --git a/libraries/wuss/task/create.c b/libraries/wuss/core/task/create.c similarity index 100% rename from libraries/wuss/task/create.c rename to libraries/wuss/core/task/create.c diff --git a/libraries/wuss/task/deliver.c b/libraries/wuss/core/task/deliver.c similarity index 100% rename from libraries/wuss/task/deliver.c rename to libraries/wuss/core/task/deliver.c diff --git a/libraries/wuss/task/destroy.c b/libraries/wuss/core/task/destroy.c similarity index 100% rename from libraries/wuss/task/destroy.c rename to libraries/wuss/core/task/destroy.c diff --git a/libraries/wuss/task/set-autoclose.c b/libraries/wuss/core/task/set-autoclose.c similarity index 100% rename from libraries/wuss/task/set-autoclose.c rename to libraries/wuss/core/task/set-autoclose.c diff --git a/libraries/wuss/window/at.c b/libraries/wuss/core/window/at.c similarity index 100% rename from libraries/wuss/window/at.c rename to libraries/wuss/core/window/at.c diff --git a/libraries/wuss/window/close.c b/libraries/wuss/core/window/close.c similarity index 100% rename from libraries/wuss/window/close.c rename to libraries/wuss/core/window/close.c diff --git a/libraries/wuss/window/create-placed.c b/libraries/wuss/core/window/create-placed.c similarity index 100% rename from libraries/wuss/window/create-placed.c rename to libraries/wuss/core/window/create-placed.c diff --git a/libraries/wuss/window/create.c b/libraries/wuss/core/window/create.c similarity index 100% rename from libraries/wuss/window/create.c rename to libraries/wuss/core/window/create.c diff --git a/libraries/wuss/window/get-content-bounds.c b/libraries/wuss/core/window/get-content-bounds.c similarity index 100% rename from libraries/wuss/window/get-content-bounds.c rename to libraries/wuss/core/window/get-content-bounds.c diff --git a/libraries/wuss/window/get-scroll.c b/libraries/wuss/core/window/get-scroll.c similarity index 100% rename from libraries/wuss/window/get-scroll.c rename to libraries/wuss/core/window/get-scroll.c diff --git a/libraries/wuss/window/get-visible-bounds.c b/libraries/wuss/core/window/get-visible-bounds.c similarity index 100% rename from libraries/wuss/window/get-visible-bounds.c rename to libraries/wuss/core/window/get-visible-bounds.c diff --git a/libraries/wuss/window/invalidate.c b/libraries/wuss/core/window/invalidate.c similarity index 100% rename from libraries/wuss/window/invalidate.c rename to libraries/wuss/core/window/invalidate.c diff --git a/libraries/wuss/window/move.c b/libraries/wuss/core/window/move.c similarity index 100% rename from libraries/wuss/window/move.c rename to libraries/wuss/core/window/move.c diff --git a/libraries/wuss/window/resize.c b/libraries/wuss/core/window/resize.c similarity index 100% rename from libraries/wuss/window/resize.c rename to libraries/wuss/core/window/resize.c diff --git a/libraries/wuss/window/restack.c b/libraries/wuss/core/window/restack.c similarity index 100% rename from libraries/wuss/window/restack.c rename to libraries/wuss/core/window/restack.c diff --git a/libraries/wuss/window/set-background.c b/libraries/wuss/core/window/set-background.c similarity index 100% rename from libraries/wuss/window/set-background.c rename to libraries/wuss/core/window/set-background.c diff --git a/libraries/wuss/window/set-hidden.c b/libraries/wuss/core/window/set-hidden.c similarity index 100% rename from libraries/wuss/window/set-hidden.c rename to libraries/wuss/core/window/set-hidden.c diff --git a/libraries/wuss/window/set-scroll.c b/libraries/wuss/core/window/set-scroll.c similarity index 100% rename from libraries/wuss/window/set-scroll.c rename to libraries/wuss/core/window/set-scroll.c diff --git a/libraries/wuss/window/try-close.c b/libraries/wuss/core/window/try-close.c similarity index 100% rename from libraries/wuss/window/try-close.c rename to libraries/wuss/core/window/try-close.c diff --git a/libraries/wuss/furniture/back-box.c b/libraries/wuss/furniture/back-box.c index c2d92f25..3998a785 100644 --- a/libraries/wuss/furniture/back-box.c +++ b/libraries/wuss/furniture/back-box.c @@ -1,6 +1,6 @@ /* wuss/furniture/back-box.c -- wuss - minimal window manager */ -#include "../impl.h" +#include "../core/impl.h" void wuss__back_box(const wuss_window_t *window, box_t *out) { diff --git a/libraries/wuss/furniture/close-box.c b/libraries/wuss/furniture/close-box.c index 3972fd02..e7c19c8e 100644 --- a/libraries/wuss/furniture/close-box.c +++ b/libraries/wuss/furniture/close-box.c @@ -1,6 +1,6 @@ /* wuss/furniture/close-box.c -- wuss - minimal window manager */ -#include "../impl.h" +#include "../core/impl.h" void wuss__close_box(const wuss_window_t *window, box_t *out) { diff --git a/libraries/wuss/furniture/content-box.c b/libraries/wuss/furniture/content-box.c index 14249a4f..45675d84 100644 --- a/libraries/wuss/furniture/content-box.c +++ b/libraries/wuss/furniture/content-box.c @@ -1,6 +1,6 @@ /* wuss/furniture/content-box.c -- wuss - minimal window manager */ -#include "../impl.h" +#include "../core/impl.h" void wuss__content_box(const wuss_window_t *window, box_t *out) { diff --git a/libraries/wuss/furniture/drag-resize.c b/libraries/wuss/furniture/drag-resize.c index 3e0d02c6..00acdc0a 100644 --- a/libraries/wuss/furniture/drag-resize.c +++ b/libraries/wuss/furniture/drag-resize.c @@ -2,7 +2,7 @@ #include "base/utils.h" -#include "../impl.h" +#include "../core/impl.h" void wuss__furniture_drag_resize(wuss_window_t *window, point_t p) { diff --git a/libraries/wuss/furniture/draw.c b/libraries/wuss/furniture/draw.c index 1e362718..944ee9f5 100644 --- a/libraries/wuss/furniture/draw.c +++ b/libraries/wuss/furniture/draw.c @@ -5,7 +5,7 @@ #include "base/utils.h" #include "geom/point.h" -#include "../impl.h" +#include "../core/impl.h" void wuss__furniture_draw(wuss_t *wuss, wuss_window_t *window, diff --git a/libraries/wuss/furniture/hit-test.c b/libraries/wuss/furniture/hit-test.c index 15eb1972..b2439005 100644 --- a/libraries/wuss/furniture/hit-test.c +++ b/libraries/wuss/furniture/hit-test.c @@ -1,6 +1,6 @@ /* wuss/furniture/hit-test.c -- wuss - minimal window manager */ -#include "../impl.h" +#include "../core/impl.h" wuss_furniture_region_t wuss__furniture_hit_test(const wuss_window_t *window, point_t p) diff --git a/libraries/wuss/furniture/hscroll-box.c b/libraries/wuss/furniture/hscroll-box.c index b3b2cf81..e5265156 100644 --- a/libraries/wuss/furniture/hscroll-box.c +++ b/libraries/wuss/furniture/hscroll-box.c @@ -1,6 +1,6 @@ /* wuss/furniture/hscroll-box.c -- wuss - minimal window manager */ -#include "../impl.h" +#include "../core/impl.h" static void hscroll_row(const wuss_window_t *window, box_t *out) { diff --git a/libraries/wuss/furniture/invalidate.c b/libraries/wuss/furniture/invalidate.c index ad058751..51c03d53 100644 --- a/libraries/wuss/furniture/invalidate.c +++ b/libraries/wuss/furniture/invalidate.c @@ -1,6 +1,6 @@ /* wuss/furniture/invalidate.c -- wuss - minimal window manager */ -#include "../impl.h" +#include "../core/impl.h" /* Same as wuss__furniture_invalidate, but against an arbitrary box rather * than window->visible -- lets a caller that's just resized the window diff --git a/libraries/wuss/furniture/ops.c b/libraries/wuss/furniture/ops.c index 15789161..081d788b 100644 --- a/libraries/wuss/furniture/ops.c +++ b/libraries/wuss/furniture/ops.c @@ -1,6 +1,6 @@ /* wuss/furniture/ops.c -- wuss - core-to-furniture dispatch table */ -#include "../impl.h" +#include "../core/impl.h" /* The built-in furniture: wuss_create points every window's furniture_ops * here unless a caller overrides it. */ diff --git a/libraries/wuss/furniture/resize-box.c b/libraries/wuss/furniture/resize-box.c index 789a2e67..a55aba86 100644 --- a/libraries/wuss/furniture/resize-box.c +++ b/libraries/wuss/furniture/resize-box.c @@ -1,6 +1,6 @@ /* wuss/furniture/resize-box.c -- wuss - minimal window manager */ -#include "../impl.h" +#include "../core/impl.h" void wuss__resize_box(const wuss_window_t *window, box_t *out) { diff --git a/libraries/wuss/furniture/scroll-action.c b/libraries/wuss/furniture/scroll-action.c index 8b84d759..1a53254d 100644 --- a/libraries/wuss/furniture/scroll-action.c +++ b/libraries/wuss/furniture/scroll-action.c @@ -1,6 +1,6 @@ /* wuss/furniture/scroll-action.c -- wuss - minimal window manager */ -#include "../impl.h" +#include "../core/impl.h" void wuss__furniture_drag_sausage(wuss_window_t *window, int delta_px, diff --git a/libraries/wuss/furniture/titlebar-box.c b/libraries/wuss/furniture/titlebar-box.c index 0d1faa49..6a620b6c 100644 --- a/libraries/wuss/furniture/titlebar-box.c +++ b/libraries/wuss/furniture/titlebar-box.c @@ -1,6 +1,6 @@ /* wuss/furniture/titlebar-box.c -- wuss - minimal window manager */ -#include "../impl.h" +#include "../core/impl.h" void wuss__titlebar_box(const wuss_window_t *window, box_t *out) { diff --git a/libraries/wuss/furniture/toggle-action.c b/libraries/wuss/furniture/toggle-action.c index 9ee85310..83669eed 100644 --- a/libraries/wuss/furniture/toggle-action.c +++ b/libraries/wuss/furniture/toggle-action.c @@ -2,7 +2,7 @@ #include "base/utils.h" -#include "../impl.h" +#include "../core/impl.h" void wuss__furniture_toggle_size(wuss_window_t *window) { diff --git a/libraries/wuss/furniture/toggle-box.c b/libraries/wuss/furniture/toggle-box.c index 2a77a588..326ecf67 100644 --- a/libraries/wuss/furniture/toggle-box.c +++ b/libraries/wuss/furniture/toggle-box.c @@ -1,6 +1,6 @@ /* wuss/furniture/toggle-box.c -- wuss - minimal window manager */ -#include "../impl.h" +#include "../core/impl.h" void wuss__toggle_box(const wuss_window_t *window, box_t *out) { diff --git a/libraries/wuss/furniture/vscroll-box.c b/libraries/wuss/furniture/vscroll-box.c index 1d936b71..2c965688 100644 --- a/libraries/wuss/furniture/vscroll-box.c +++ b/libraries/wuss/furniture/vscroll-box.c @@ -1,6 +1,6 @@ /* wuss/furniture/vscroll-box.c -- wuss - minimal window manager */ -#include "../impl.h" +#include "../core/impl.h" static void vscroll_column(const wuss_window_t *window, box_t *out) { diff --git a/libraries/wuss/icon/create-array.c b/libraries/wuss/icon/create-array.c index 07bdef4d..879b4104 100644 --- a/libraries/wuss/icon/create-array.c +++ b/libraries/wuss/icon/create-array.c @@ -7,7 +7,7 @@ #include "fortify/fortify.h" #endif -#include "../impl.h" +#include "../core/impl.h" result_t wuss_icon_create_array(wuss_window_t *window, const wuss_icon_spec_t *specs, diff --git a/libraries/wuss/icon/create.c b/libraries/wuss/icon/create.c index 80c3bf7d..aeac0f57 100644 --- a/libraries/wuss/icon/create.c +++ b/libraries/wuss/icon/create.c @@ -8,7 +8,7 @@ #include "fortify/fortify.h" #endif -#include "../impl.h" +#include "../core/impl.h" result_t wuss_icon_create(wuss_window_t *window, const wuss_icon_spec_t *spec, diff --git a/libraries/wuss/icon/delete.c b/libraries/wuss/icon/delete.c index 097690cd..cc3f1be1 100644 --- a/libraries/wuss/icon/delete.c +++ b/libraries/wuss/icon/delete.c @@ -6,7 +6,7 @@ #include "fortify/fortify.h" #endif -#include "../impl.h" +#include "../core/impl.h" void wuss_icon_delete(wuss_icon_t *icon) { diff --git a/libraries/wuss/icon/draw.c b/libraries/wuss/icon/draw.c index 62143b12..a0d09585 100644 --- a/libraries/wuss/icon/draw.c +++ b/libraries/wuss/icon/draw.c @@ -9,7 +9,7 @@ #include "framebuf/bmfont.h" #include "framebuf/screen.h" -#include "../impl.h" +#include "../core/impl.h" /* ----------------------------------------------------------------------- */ diff --git a/libraries/wuss/icon/free.c b/libraries/wuss/icon/free.c index d8a08432..cb4c5e08 100644 --- a/libraries/wuss/icon/free.c +++ b/libraries/wuss/icon/free.c @@ -6,7 +6,7 @@ #include "fortify/fortify.h" #endif -#include "../impl.h" +#include "../core/impl.h" void wuss__icons_free(wuss_window_t *window) { diff --git a/libraries/wuss/icon/from-spec.c b/libraries/wuss/icon/from-spec.c index 6e01411f..46d817ef 100644 --- a/libraries/wuss/icon/from-spec.c +++ b/libraries/wuss/icon/from-spec.c @@ -6,7 +6,7 @@ #include "fortify/fortify.h" #endif -#include "../impl.h" +#include "../core/impl.h" result_t wuss__icon_from_spec(const wuss_t *w, const wuss_icon_spec_t *spec, diff --git a/libraries/wuss/icon/get-bbox.c b/libraries/wuss/icon/get-bbox.c index 700f96d9..99b42c1d 100644 --- a/libraries/wuss/icon/get-bbox.c +++ b/libraries/wuss/icon/get-bbox.c @@ -1,6 +1,6 @@ /* wuss/icon/get-bbox.c -- read a work-area icon's bounding box */ -#include "../impl.h" +#include "../core/impl.h" void wuss_icon_get_bbox(const wuss_icon_t *icon, box_t *bbox) { diff --git a/libraries/wuss/icon/get-selected.c b/libraries/wuss/icon/get-selected.c index 3029e5d9..dffcb792 100644 --- a/libraries/wuss/icon/get-selected.c +++ b/libraries/wuss/icon/get-selected.c @@ -2,7 +2,7 @@ #include <assert.h> -#include "../impl.h" +#include "../core/impl.h" int wuss_icon_get_selected(const wuss_icon_t *icon) { diff --git a/libraries/wuss/icon/get-text.c b/libraries/wuss/icon/get-text.c index 783f685f..b3f8de33 100644 --- a/libraries/wuss/icon/get-text.c +++ b/libraries/wuss/icon/get-text.c @@ -1,6 +1,6 @@ /* wuss/icon/get-text.c -- read a work-area icon's label */ -#include "../impl.h" +#include "../core/impl.h" const char *wuss_icon_get_text(const wuss_icon_t *icon) { diff --git a/libraries/wuss/icon/get-type.c b/libraries/wuss/icon/get-type.c index 2335286b..f988044e 100644 --- a/libraries/wuss/icon/get-type.c +++ b/libraries/wuss/icon/get-type.c @@ -1,6 +1,6 @@ /* wuss/icon/get-type.c -- read a work-area icon's type */ -#include "../impl.h" +#include "../core/impl.h" wuss_icon_type_t wuss_icon_get_type(const wuss_icon_t *icon) { diff --git a/libraries/wuss/icon/get-window.c b/libraries/wuss/icon/get-window.c index 43fa0127..a5e41e6d 100644 --- a/libraries/wuss/icon/get-window.c +++ b/libraries/wuss/icon/get-window.c @@ -1,6 +1,6 @@ /* wuss/icon/get-window.c -- read a work-area icon's owning window */ -#include "../impl.h" +#include "../core/impl.h" wuss_window_t *wuss_icon_get_window(const wuss_icon_t *icon) { diff --git a/libraries/wuss/icon/hit-test.c b/libraries/wuss/icon/hit-test.c index 2e9ab7c8..d734b9c1 100644 --- a/libraries/wuss/icon/hit-test.c +++ b/libraries/wuss/icon/hit-test.c @@ -2,7 +2,7 @@ #include "geom/box.h" -#include "../impl.h" +#include "../core/impl.h" wuss_icon_t *wuss__icon_hit_test(wuss_window_t *window, point_t doc_point) { diff --git a/libraries/wuss/icon/invalidate.c b/libraries/wuss/icon/invalidate.c index d702fb36..156c5747 100644 --- a/libraries/wuss/icon/invalidate.c +++ b/libraries/wuss/icon/invalidate.c @@ -1,6 +1,6 @@ /* wuss/icon/invalidate.c -- mark a work-area icon's bbox dirty */ -#include "../impl.h" +#include "../core/impl.h" void wuss__icon_invalidate(const wuss_icon_t *icon) { diff --git a/libraries/wuss/icon/plot.c b/libraries/wuss/icon/plot.c index d62dc1d3..a305e968 100644 --- a/libraries/wuss/icon/plot.c +++ b/libraries/wuss/icon/plot.c @@ -6,7 +6,7 @@ #include "fortify/fortify.h" #endif -#include "../impl.h" +#include "../core/impl.h" result_t wuss_icon_plot(wuss_window_t *window, const wuss_icon_spec_t *spec, diff --git a/libraries/wuss/icon/screen-box.c b/libraries/wuss/icon/screen-box.c index 3f630519..74a9035e 100644 --- a/libraries/wuss/icon/screen-box.c +++ b/libraries/wuss/icon/screen-box.c @@ -3,7 +3,7 @@ #include "geom/box.h" #include "geom/point.h" -#include "../impl.h" +#include "../core/impl.h" /* Map an icon bbox (virtual document space) into screen space, given the * owning window's content box and scroll offset. wuss__icon_draw uses this diff --git a/libraries/wuss/icon/set-hidden.c b/libraries/wuss/icon/set-hidden.c index 7adfee0d..996c2fdc 100644 --- a/libraries/wuss/icon/set-hidden.c +++ b/libraries/wuss/icon/set-hidden.c @@ -1,6 +1,6 @@ /* wuss/icon/set-hidden.c -- show or hide a work-area icon */ -#include "../impl.h" +#include "../core/impl.h" void wuss_icon_set_hidden(wuss_icon_t *icon, int hidden) { diff --git a/libraries/wuss/icon/set-hover.c b/libraries/wuss/icon/set-hover.c index 08ef518a..1c55d1fd 100644 --- a/libraries/wuss/icon/set-hover.c +++ b/libraries/wuss/icon/set-hover.c @@ -2,7 +2,7 @@ #include <stddef.h> -#include "../impl.h" +#include "../core/impl.h" /* Only wuss_ICON_TYPE_MENU_ENTRY changes appearance on hover (see * wuss__icon_draw_menu_entry). Buttons and every other type ignore the hover state, diff --git a/libraries/wuss/icon/set-selected.c b/libraries/wuss/icon/set-selected.c index 27430d27..712aee57 100644 --- a/libraries/wuss/icon/set-selected.c +++ b/libraries/wuss/icon/set-selected.c @@ -2,7 +2,7 @@ #include <assert.h> -#include "../impl.h" +#include "../core/impl.h" void wuss__icon_select(wuss_icon_t *icon, int selected) { diff --git a/libraries/wuss/icon/set-text.c b/libraries/wuss/icon/set-text.c index 2c5d517a..35c8fcce 100644 --- a/libraries/wuss/icon/set-text.c +++ b/libraries/wuss/icon/set-text.c @@ -7,7 +7,7 @@ #include "fortify/fortify.h" #endif -#include "../impl.h" +#include "../core/impl.h" result_t wuss_icon_set_text(wuss_icon_t *icon, const char *text) { diff --git a/libraries/wuss/menu/menu.c b/libraries/wuss/menu/menu.c index 917499bd..4c7e869b 100644 --- a/libraries/wuss/menu/menu.c +++ b/libraries/wuss/menu/menu.c @@ -22,7 +22,7 @@ #include "wuss/window.h" #include "wuss/wuss.h" -#include "../impl.h" +#include "../core/impl.h" /* Row padding above/below the glyph, and the gutters left for the tick (left) * and the submenu arrow (right). All in pixels. */ diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 7ec90b2e..6a289fca 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -34,7 +34,7 @@ /* 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) -#include "../impl.h" +#include "../core/impl.h" #include "../icon.h" #endif From 127d7597eec7d82a7206f13ce4b53e92af287436 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Fri, 4 Sep 2026 11:55:50 +0100 Subject: [PATCH 247/287] refactor(wuss): merge vscroll/hscroll box geometry into one axis-generic file The vertical and horizontal scrollbar geometry helpers were the same layout transposed: a strip helper, per-end arrow slices and an axis-mirrored sausage calculation, duplicated across vscroll-box.c and hscroll-box.c with comments noting the two must stay in lockstep. Fold both into scroll-box.c, written once against a "horizontal" flag (the same pattern scroll-action.c already uses). The ten public wuss__vscroll_* / wuss__hscroll_* entry points become thin wrappers that pin the flag. No behaviour change; SDL and ASan builds clean, full test suite passes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- CMakeLists.txt | 3 +- libraries/wuss/furniture/hscroll-box.c | 109 ----------- libraries/wuss/furniture/scroll-box.c | 242 +++++++++++++++++++++++++ libraries/wuss/furniture/vscroll-box.c | 111 ------------ 4 files changed, 243 insertions(+), 222 deletions(-) delete mode 100644 libraries/wuss/furniture/hscroll-box.c create mode 100644 libraries/wuss/furniture/scroll-box.c delete mode 100644 libraries/wuss/furniture/vscroll-box.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 5493dd2d..1917c3e2 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -392,15 +392,14 @@ set(WUSS_FURNITURE_SOURCES libraries/wuss/furniture/drag-resize.c libraries/wuss/furniture/draw.c libraries/wuss/furniture/hit-test.c - libraries/wuss/furniture/hscroll-box.c libraries/wuss/furniture/invalidate.c libraries/wuss/furniture/ops.c libraries/wuss/furniture/resize-box.c libraries/wuss/furniture/scroll-action.c + libraries/wuss/furniture/scroll-box.c libraries/wuss/furniture/titlebar-box.c libraries/wuss/furniture/toggle-action.c libraries/wuss/furniture/toggle-box.c - libraries/wuss/furniture/vscroll-box.c libraries/wuss/furniture.h) set(WUSS_ICON_SOURCES diff --git a/libraries/wuss/furniture/hscroll-box.c b/libraries/wuss/furniture/hscroll-box.c deleted file mode 100644 index e5265156..00000000 --- a/libraries/wuss/furniture/hscroll-box.c +++ /dev/null @@ -1,109 +0,0 @@ -/* wuss/furniture/hscroll-box.c -- wuss - minimal window manager */ - -#include "../core/impl.h" - -static void hscroll_row(const wuss_window_t *window, box_t *out) -{ - int outline_px, size; - - outline_px = wuss__outline_px(window); - size = wuss__button_size(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; /* stop left of the resize corner */ -} - -void wuss__hscroll_left_box(const wuss_window_t *window, box_t *out) -{ - box_t row; - int size; - - hscroll_row(window, &row); - size = wuss__button_size(window); - - out->y0 = row.y0; - out->y1 = row.y1; - out->x0 = row.x0; - out->x1 = row.x0 + size; -} - -void wuss__hscroll_right_box(const wuss_window_t *window, box_t *out) -{ - box_t row; - int size; - - hscroll_row(window, &row); - size = wuss__button_size(window); - - out->y0 = row.y0; - out->y1 = row.y1; - out->x1 = row.x1; - out->x0 = row.x1 - size; -} - -void wuss__hscroll_well_box(const wuss_window_t *window, box_t *out) -{ - box_t row; - int size; - - hscroll_row(window, &row); - size = wuss__button_size(window); - - out->y0 = row.y0; - out->y1 = row.y1; - out->x0 = row.x0 + size; - out->x1 = row.x1 - size; -} - -int wuss__hscroll_well_px(const wuss_window_t *window) -{ - box_t well; - - wuss__hscroll_well_box(window, &well); - - return well.x1 - well.x0; -} - -void wuss__hscroll_sausage_box(const wuss_window_t *window, box_t *out) -{ - box_t well, content; - int well_px, track_px, end_gap, content_size, doc_size, sausage_px, sausage_x0, inset; - - wuss__hscroll_well_box(window, &well); - wuss__content_box(window, &content); - - well_px = well.x1 - well.x0; - - /* Leave a cosmetic gap at each end of the well; drop it if the well is - * too short to spare two gaps plus a minimum sausage. */ - end_gap = (well_px > 2 * WUSS_SCROLL_END_GAP + WUSS_MIN_SAUSAGE) ? WUSS_SCROLL_END_GAP : 0; - track_px = well_px - 2 * end_gap; - - content_size = content.x1 - content.x0; - doc_size = window->doc.w; - if (doc_size < content_size) - doc_size = content_size; - - sausage_px = (doc_size > 0) ? track_px * content_size / doc_size : track_px; - if (sausage_px < WUSS_MIN_SAUSAGE) - sausage_px = WUSS_MIN_SAUSAGE; - if (sausage_px > track_px) - sausage_px = track_px; - - if (doc_size > content_size && track_px > sausage_px) - sausage_x0 = well.x0 + end_gap + (track_px - sausage_px) * window->scroll.x / (doc_size - content_size); - else - sausage_x0 = well.x0 + end_gap; - - /* Inset the sausage from the well's long edges, but only while that - * leaves something to draw: a tiny titlebar font can make the well - * narrower than two insets, which would invert the box. */ - inset = (well.y1 - well.y0 > 2 * WUSS_SCROLL_INSET) ? WUSS_SCROLL_INSET : 0; - out->y0 = well.y0 + inset; - out->y1 = well.y1 - inset; - out->x0 = sausage_x0; - out->x1 = sausage_x0 + sausage_px; -} diff --git a/libraries/wuss/furniture/scroll-box.c b/libraries/wuss/furniture/scroll-box.c new file mode 100644 index 00000000..57c51dd5 --- /dev/null +++ b/libraries/wuss/furniture/scroll-box.c @@ -0,0 +1,242 @@ +/* wuss/furniture/scroll-box.c -- wuss - minimal window manager */ + +/* The vertical and horizontal scrollbars are the same layout transposed: a + * fixed-breadth strip down the right / along the bottom of the visible box, + * an arrow at each end and the well between them, with a proportional sausage + * in the well. Everything here is written once against a "horizontal" flag; + * the ten public wuss__vscroll_ / wuss__hscroll_ entry points are thin + * wrappers that pin the flag. */ + +#include "../core/impl.h" + +/* 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. */ +static void scroll_strip(const wuss_window_t *window, + int horizontal, + box_t *out) +{ + box_t titlebar; + int outline_px, size; + + outline_px = wuss__outline_px(window); + size = wuss__button_size(window); + + 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; /* clear of resize corner */ + } + else + { + out->x1 = window->visible.x1 - outline_px; + 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 */ + } +} + +/* 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. */ +static void scroll_end_slice(const box_t *strip, + int horizontal, + int far_end, + int size, + box_t *out) +{ + *out = *strip; + + if (horizontal) + { + if (far_end) + out->x0 = strip->x1 - size; + else + out->x1 = strip->x0 + size; + } + else + { + if (far_end) + out->y0 = strip->y1 - size; + else + out->y1 = strip->y0 + size; + } +} + +/* The well: "strip" with a "size"-long arrow slice removed from each end. */ +static void scroll_middle_slice(const box_t *strip, + int horizontal, + int size, + box_t *out) +{ + *out = *strip; + + if (horizontal) + { + out->x0 = strip->x0 + size; + out->x1 = strip->x1 - size; + } + else + { + out->y0 = strip->y0 + size; + out->y1 = strip->y1 - size; + } +} + +/* The well as a standalone call: strip, then middle slice, without the + * caller threading "size" through. */ +static void scroll_well(const wuss_window_t *window, + int horizontal, + box_t *out) +{ + box_t strip; + + scroll_strip(window, horizontal, &strip); + scroll_middle_slice(&strip, horizontal, wuss__button_size(window), out); +} + +/* The sausage: a proportional handle in the well, offset by the scroll + * position. Must track wuss__furniture_drag_sausage's maths so a drag follows + * the pointer 1:1. */ +static void scroll_sausage(const wuss_window_t *window, + int horizontal, + box_t *out) +{ + box_t well, content; + int well_px, track_px, end_gap, content_size, doc_size; + int sausage_px, sausage_lo, inset, scroll; + + scroll_well(window, horizontal, &well); + wuss__content_box(window, &content); + + if (horizontal) + { + well_px = well.x1 - well.x0; + content_size = content.x1 - content.x0; + doc_size = window->doc.w; + } + else + { + well_px = well.y1 - well.y0; + content_size = content.y1 - content.y0; + doc_size = window->doc.h; + } + + /* Leave a cosmetic gap at each end of the well; drop it if the well is + * too short to spare two gaps plus a minimum sausage. */ + end_gap = (well_px > 2 * WUSS_SCROLL_END_GAP + WUSS_MIN_SAUSAGE) ? WUSS_SCROLL_END_GAP : 0; + track_px = well_px - 2 * end_gap; + + if (doc_size < content_size) + doc_size = content_size; + + sausage_px = (doc_size > 0) ? track_px * content_size / doc_size : track_px; + if (sausage_px < WUSS_MIN_SAUSAGE) + sausage_px = WUSS_MIN_SAUSAGE; + if (sausage_px > track_px) + sausage_px = track_px; + + scroll = horizontal ? window->scroll.x : window->scroll.y; + + if (doc_size > content_size && track_px > sausage_px) + sausage_lo = end_gap + (track_px - sausage_px) * scroll / (doc_size - content_size); + else + sausage_lo = end_gap; + + if (horizontal) + { + /* Inset the sausage from the well's long edges, but only while that + * leaves something to draw: a tiny titlebar font can make the well + * narrower than two insets, which would invert the box. */ + inset = (well.y1 - well.y0 > 2 * WUSS_SCROLL_INSET) ? WUSS_SCROLL_INSET : 0; + out->y0 = well.y0 + inset; + out->y1 = well.y1 - inset; + out->x0 = well.x0 + sausage_lo; + out->x1 = out->x0 + sausage_px; + } + else + { + inset = (well.x1 - well.x0 > 2 * WUSS_SCROLL_INSET) ? WUSS_SCROLL_INSET : 0; + out->x0 = well.x0 + inset; + out->x1 = well.x1 - inset; + out->y0 = well.y0 + sausage_lo; + out->y1 = out->y0 + sausage_px; + } +} + +/* ----- vertical scrollbar ------------------------------------------------ */ + +void wuss__vscroll_up_box(const wuss_window_t *window, box_t *out) +{ + box_t strip; + + scroll_strip(window, 0, &strip); + scroll_end_slice(&strip, 0, 0, wuss__button_size(window), out); +} + +void wuss__vscroll_down_box(const wuss_window_t *window, box_t *out) +{ + box_t strip; + + scroll_strip(window, 0, &strip); + scroll_end_slice(&strip, 0, 1, wuss__button_size(window), out); +} + +void wuss__vscroll_well_box(const wuss_window_t *window, box_t *out) +{ + scroll_well(window, 0, out); +} + +int wuss__vscroll_well_px(const wuss_window_t *window) +{ + box_t well; + + wuss__vscroll_well_box(window, &well); + + return well.y1 - well.y0; +} + +void wuss__vscroll_sausage_box(const wuss_window_t *window, box_t *out) +{ + scroll_sausage(window, 0, out); +} + +/* ----- horizontal scrollbar -------------------------------------------- */ + +void wuss__hscroll_left_box(const wuss_window_t *window, box_t *out) +{ + box_t strip; + + scroll_strip(window, 1, &strip); + scroll_end_slice(&strip, 1, 0, wuss__button_size(window), out); +} + +void wuss__hscroll_right_box(const wuss_window_t *window, box_t *out) +{ + box_t strip; + + scroll_strip(window, 1, &strip); + scroll_end_slice(&strip, 1, 1, wuss__button_size(window), out); +} + +void wuss__hscroll_well_box(const wuss_window_t *window, box_t *out) +{ + scroll_well(window, 1, out); +} + +int wuss__hscroll_well_px(const wuss_window_t *window) +{ + box_t well; + + wuss__hscroll_well_box(window, &well); + + return well.x1 - well.x0; +} + +void wuss__hscroll_sausage_box(const wuss_window_t *window, box_t *out) +{ + scroll_sausage(window, 1, out); +} diff --git a/libraries/wuss/furniture/vscroll-box.c b/libraries/wuss/furniture/vscroll-box.c deleted file mode 100644 index 2c965688..00000000 --- a/libraries/wuss/furniture/vscroll-box.c +++ /dev/null @@ -1,111 +0,0 @@ -/* wuss/furniture/vscroll-box.c -- wuss - minimal window manager */ - -#include "../core/impl.h" - -static void vscroll_column(const wuss_window_t *window, box_t *out) -{ - box_t titlebar; - int outline_px, size; - - outline_px = wuss__outline_px(window); - size = wuss__button_size(window); - - out->x1 = window->visible.x1 - outline_px; - out->x0 = out->x1 - size; - - wuss__titlebar_box(window, &titlebar); - out->y0 = titlebar.y1; - out->y1 = window->visible.y1 - outline_px - size; /* stop above the resize corner */ -} - -void wuss__vscroll_up_box(const wuss_window_t *window, box_t *out) -{ - box_t column; - int size; - - vscroll_column(window, &column); - size = wuss__button_size(window); - - out->x0 = column.x0; - out->x1 = column.x1; - out->y0 = column.y0; - out->y1 = column.y0 + size; -} - -void wuss__vscroll_down_box(const wuss_window_t *window, box_t *out) -{ - box_t column; - int size; - - vscroll_column(window, &column); - size = wuss__button_size(window); - - out->x0 = column.x0; - out->x1 = column.x1; - out->y1 = column.y1; - out->y0 = column.y1 - size; -} - -void wuss__vscroll_well_box(const wuss_window_t *window, box_t *out) -{ - box_t column; - int size; - - vscroll_column(window, &column); - size = wuss__button_size(window); - - out->x0 = column.x0; - out->x1 = column.x1; - out->y0 = column.y0 + size; - out->y1 = column.y1 - size; -} - -int wuss__vscroll_well_px(const wuss_window_t *window) -{ - box_t well; - - wuss__vscroll_well_box(window, &well); - - return well.y1 - well.y0; -} - -void wuss__vscroll_sausage_box(const wuss_window_t *window, box_t *out) -{ - box_t well, content; - int well_px, track_px, end_gap, content_size, doc_size, sausage_px, sausage_y0, inset; - - wuss__vscroll_well_box(window, &well); - wuss__content_box(window, &content); - - well_px = well.y1 - well.y0; - - /* Leave a cosmetic gap at each end of the well; drop it if the well is - * too short to spare two gaps plus a minimum sausage. */ - end_gap = (well_px > 2 * WUSS_SCROLL_END_GAP + WUSS_MIN_SAUSAGE) ? WUSS_SCROLL_END_GAP : 0; - track_px = well_px - 2 * end_gap; - - content_size = content.y1 - content.y0; - doc_size = window->doc.h; - if (doc_size < content_size) - doc_size = content_size; - - sausage_px = (doc_size > 0) ? track_px * content_size / doc_size : track_px; - if (sausage_px < WUSS_MIN_SAUSAGE) - sausage_px = WUSS_MIN_SAUSAGE; - if (sausage_px > track_px) - sausage_px = track_px; - - if (doc_size > content_size && track_px > sausage_px) - sausage_y0 = well.y0 + end_gap + (track_px - sausage_px) * window->scroll.y / (doc_size - content_size); - else - sausage_y0 = well.y0 + end_gap; - - /* Inset the sausage from the well's long edges, but only while that - * leaves something to draw: a tiny titlebar font can make the well - * narrower than two insets, which would invert the box. */ - inset = (well.x1 - well.x0 > 2 * WUSS_SCROLL_INSET) ? WUSS_SCROLL_INSET : 0; - out->x0 = well.x0 + inset; - out->x1 = well.x1 - inset; - out->y0 = sausage_y0; - out->y1 = sausage_y0 + sausage_px; -} From 513c890dcda60d75042acb0abceb2b96b33bec4b Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Fri, 4 Sep 2026 12:01:56 +0100 Subject: [PATCH 248/287] refactor(wuss): extract shared piece-array blit engine move.c and set-scroll.c each carried their own copy of the same occluder-clipped, clobber-ordered, partial-copy-repairing blit loop over an array of clean pieces. Factor that into wuss__blit_pieces() in invalidate.c. The two callers keep their genuinely different before/after work: set-scroll collects stale dirty rects and pins scr->clip to the content box; move keeps its vacated-sliver and hidden-destination invalidation and leaves the clip alone (clip == NULL). The helper saves and restores scr->clip when it is given one, which set-scroll's old inline copy did not bother to do. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- libraries/wuss/core/impl.h | 19 +++++ libraries/wuss/core/window/invalidate.c | 101 ++++++++++++++++++++++++ libraries/wuss/core/window/move.c | 97 ++++------------------- libraries/wuss/core/window/set-scroll.c | 79 +++--------------- 4 files changed, 147 insertions(+), 149 deletions(-) diff --git a/libraries/wuss/core/impl.h b/libraries/wuss/core/impl.h index 67ae740d..b8b55e26 100644 --- a/libraries/wuss/core/impl.h +++ b/libraries/wuss/core/impl.h @@ -346,6 +346,25 @@ int wuss__order_pieces(const box_t *clean, int n, int *order); +/* Slide the pixels of "src" (nsrc pieces the caller has already carved clear + * of occluders and stale regions) by (dx, dy), clipping each destination + * against the screen and the windows above "window", clobber-ordered so no + * blit steps on another's source; if "clip" is non-NULL scr->clip is pinned + * to it for the copy (and restored after), else the clip is left alone. On + * success + * fills "copied" (capacity WUSS_MAX_INVALIDATE_PIECES) with the pieces + * actually blitted, sets "*ncopied" and returns 1 -- the caller repaints + * whatever "copied" misses. Returns 0 (with "*ncopied" zeroed) when there is + * no safe fast path, and the caller must fall back to a full invalidate. */ +int wuss__blit_pieces(wuss_window_t *window, + const box_t *src, + int nsrc, + int dx, + int dy, + const box_t *clip, + box_t *copied, + int *ncopied); + /* Recover the owning wuss_window_t from a node in a task's window list * (window->task_link). task_link is not the first member, so a plain cast * won't do. */ diff --git a/libraries/wuss/core/window/invalidate.c b/libraries/wuss/core/window/invalidate.c index 9586793c..54682a9f 100644 --- a/libraries/wuss/core/window/invalidate.c +++ b/libraries/wuss/core/window/invalidate.c @@ -206,6 +206,107 @@ int wuss__order_pieces(const box_t *clean, return nout == n; } +/* Slide the pixels of "src" (nsrc clean pieces -- the caller has already + * carved them clear of occluders and any stale regions) by (dx, dy), + * clipping every destination against the screen and against the windows + * above "window" so the blit never paints over an occluder. Blits are + * clobber-ordered (wuss__order_pieces) so no piece overwrites another's + * still-unread source. If "clip" is non-NULL scr->clip is pinned to it for + * the duration (and restored after) -- pass it when the destinations must + * not spill past some box (set-scroll pins the content box); pass NULL to + * leave the clip alone and let screen_copy_rect self-clip to the screen. + * + * On success the pieces actually copied are written to "copied" (capacity + * WUSS_MAX_INVALIDATE_PIECES), "*ncopied" is set, and 1 is returned -- the + * caller then repaints whatever "copied" did not cover. Returns 0, with + * "*ncopied" zeroed, when there is no safe fast path (nothing clean, the + * piece budget overflowed, no clobber-free order exists, or screen_copy_rect + * declined -- e.g. a paletted screen with no blit path): the caller must + * fall back to invalidating its whole dirty region. A piece copied only + * partially (its far edge slid off-screen) still has the uncovered remainder + * invalidated here, since that ground was already clipped clear of every + * occluder. A caller that already invalidates its whole affected region + * against "copied" afterwards just sees those strips folded into the dirty + * list twice, which is harmless. */ +int wuss__blit_pieces(wuss_window_t *window, + const box_t *src, + int nsrc, + int dx, + int dy, + const box_t *clip, + box_t *copied, + int *ncopied) +{ + box_t blit_src[WUSS_MAX_INVALIDATE_PIECES]; + box_t blit_dest[WUSS_MAX_INVALIDATE_PIECES]; + int order[WUSS_MAX_INVALIDATE_PIECES]; + int nblit, overflow, i, j, idx; + box_t saved_clip; + + *ncopied = 0; + + nblit = 0; + overflow = 0; + for (i = 0; i < nsrc && !overflow; i++) + { + box_t want, vis[WUSS_MAX_INVALIDATE_PIECES]; + int nvis; + + box_translated(&src[i], dx, dy, &want); + nvis = wuss__clip_to_visible(window, &want, vis); + for (j = 0; j < nvis; j++) + { + if (nblit == WUSS_MAX_INVALIDATE_PIECES) + { + overflow = 1; + break; + } + blit_dest[nblit] = vis[j]; + box_translated(&vis[j], -dx, -dy, &blit_src[nblit]); + nblit++; + } + } + + if (nsrc == 0 || overflow || + !wuss__order_pieces(blit_src, blit_dest, nblit, order)) + return 0; + + if (clip != NULL) + { + saved_clip = window->wuss->scr->clip; + window->wuss->scr->clip = *clip; + } + + for (i = 0; i < nblit; i++) + { + box_t got; + + idx = order[i]; + if (screen_copy_rect(window->wuss->scr, &blit_src[idx], + POINT(blit_dest[idx].x0, blit_dest[idx].y0), + &got) != result_OK) + { + if (clip != NULL) + window->wuss->scr->clip = saved_clip; + *ncopied = 0; + return 0; + } + + /* "got" can be smaller than blit_dest[idx] if part slid off-screen: the + * uncovered remainder has no source pixels and needs a real repaint. + * Safe raw -- blit_dest[idx] was already clipped clear of occluders. */ + wuss__invalidate_minus(window->wuss, &blit_dest[idx], &got); + + if (*ncopied < WUSS_MAX_INVALIDATE_PIECES) + copied[(*ncopied)++] = got; + } + + if (clip != NULL) + window->wuss->scr->clip = saved_clip; + + return 1; +} + /* Invalidate the parts of "window"'s footprint that are hidden behind other * windows at the current z-order -- the rest of its footprint is already * showing its own correct pixels, so redrawing that too would just be diff --git a/libraries/wuss/core/window/move.c b/libraries/wuss/core/window/move.c index 5d0f5873..a25c9414 100644 --- a/libraries/wuss/core/window/move.c +++ b/libraries/wuss/core/window/move.c @@ -2,15 +2,6 @@ #include "../impl.h" -/* Translate "box" by (dx, dy) into "out". */ -static void translate_box(const box_t *box, int dx, int dy, box_t *out) -{ - out->x0 = box->x0 + dx; - out->y0 = box->y0 + dy; - out->x1 = box->x1 + dx; - out->y1 = box->y1 + dy; -} - /* p is the window's content top-left; the furniture offset (outline plus * any titlebar) is constant for a given window, so the footprint just * follows it */ @@ -18,13 +9,10 @@ void wuss_window_move(wuss_window_t *window, point_t p) { box_t clean[WUSS_MAX_INVALIDATE_PIECES]; box_t full_dest[WUSS_MAX_INVALIDATE_PIECES]; - box_t blit_src[WUSS_MAX_INVALIDATE_PIECES]; - box_t blit_dest[WUSS_MAX_INVALIDATE_PIECES]; - int order[WUSS_MAX_INVALIDATE_PIECES]; + box_t copied[WUSS_MAX_INVALIDATE_PIECES]; int width, height, outline_px, titlebar_height; - int dx, dy, nclean, nblit, overflow, i, idx; - box_t before, dirty, copied; - int blit_failed; + int dx, dy, nclean, ncopied, i; + box_t before, dirty; /* a manual move desyncs the window from its layout-packer slot; hand the * slot back and stop tracking this window's position */ @@ -65,72 +53,15 @@ void wuss_window_move(wuss_window_t *window, point_t p) dy = window->visible.y0 - before.y0; for (i = 0; i < nclean; i++) - translate_box(&clean[i], dx, dy, &full_dest[i]); - - /* Split each clean piece's full (untrimmed) destination down to the parts - * not already sitting under an occluder above this window there: that - * occluder hasn't moved, so its pixels are already correct, and blitting - * this window's stale pixels over them would just have to be repainted - * straight back -- cheaper to never touch them at all. Only the surviving, - * genuinely-blittable sub-pieces go on to the clobber-ordering/blit below; - * the occluded remainder needs no repair because nothing was ever pasted - * over it. */ - nblit = 0; - overflow = 0; - for (i = 0; i < nclean && !overflow; i++) - { - box_t visible_dest[WUSS_MAX_INVALIDATE_PIECES]; - int nvisible, v; - - nvisible = wuss__clip_to_visible(window, &full_dest[i], visible_dest); - - for (v = 0; v < nvisible; v++) - { - if (nblit == WUSS_MAX_INVALIDATE_PIECES) - { - overflow = 1; - break; - } - - blit_dest[nblit] = visible_dest[v]; - translate_box(&visible_dest[v], -dx, -dy, &blit_src[nblit]); - nblit++; - } - } - - /* If no ordering of these single-rect blits avoids one clobbering - * another's still-unread source, fall back rather than risk corrupting - * this window's own pixels. */ - blit_failed = nclean == 0 || overflow || - !wuss__order_pieces(blit_src, blit_dest, nblit, order); - - for (i = 0; i < nblit && !blit_failed; i++) - { - idx = order[i]; - - if (screen_copy_rect(window->wuss->scr, &blit_src[idx], - POINT(blit_dest[idx].x0, blit_dest[idx].y0), - &copied) != result_OK) - { - /* screen_copy_rect refused this piece: either the screen format has no - * blit path (e.g. paletted -- fails on the first piece, before anything - * has moved) or this piece's source/dest lies off-screen, which can - * happen part-way through after earlier pieces have already blitted. - * Either way, bail: the pieces done so far are self-consistent and the - * caller's fallback full invalidate repaints the whole union. */ - blit_failed = 1; - break; - } - - /* "copied" can be smaller than "blit_dest[idx]" if the move was partly - * off-screen: the leftover part has no valid source pixels behind it, - * so it needs a real repaint too. Safe to invalidate raw, without - * re-checking occlusion, since "blit_dest[idx]" (and so its "copied" - * subset) was already clipped clear of every occluder above. */ - wuss__invalidate_minus(window->wuss, &blit_dest[idx], &copied); - } - - if (nclean > 0 && !blit_failed) + box_translated(&clean[i], dx, dy, &full_dest[i]); + + /* Slide the clean pieces by (dx, dy); wuss__blit_pieces clips each + * destination clear of the windows above this one -- an occluder there + * hasn't moved, so its pixels are already correct and pasting stale ones + * over them would just have to be repainted straight back. It also + * repairs any piece that slid partly off-screen. */ + if (nclean > 0 && + wuss__blit_pieces(window, clean, nclean, dx, dy, NULL, copied, &ncopied)) { box_t hidden[WUSS_MAX_INVALIDATE_PIECES]; int nhidden; @@ -172,7 +103,7 @@ void wuss_window_move(wuss_window_t *window, point_t p) { box_t hidden_dest; - translate_box(&hidden[i], dx, dy, &hidden_dest); + box_translated(&hidden[i], dx, dy, &hidden_dest); wuss__invalidate_clipped(window, &hidden_dest); } } @@ -184,4 +115,6 @@ void wuss_window_move(wuss_window_t *window, point_t p) box_union(&before, &window->visible, &dirty); wuss__invalidate_clipped(window, &dirty); } + + (void) ncopied; /* move.c repairs via the sliver logic above, not "copied" */ } diff --git a/libraries/wuss/core/window/set-scroll.c b/libraries/wuss/core/window/set-scroll.c index c609c1b5..47080e75 100644 --- a/libraries/wuss/core/window/set-scroll.c +++ b/libraries/wuss/core/window/set-scroll.c @@ -8,14 +8,9 @@ void wuss_window_set_scroll(wuss_window_t *window, point_t p) box_t stale[WUSS_MAX_DIRTY]; box_t clean[WUSS_MAX_INVALIDATE_PIECES]; box_t src[WUSS_MAX_INVALIDATE_PIECES]; - box_t blit_src[WUSS_MAX_INVALIDATE_PIECES]; - box_t blit_dest[WUSS_MAX_INVALIDATE_PIECES]; box_t copied[WUSS_MAX_INVALIDATE_PIECES]; box_t dirty[WUSS_MAX_INVALIDATE_PIECES]; - box_t vis[WUSS_MAX_INVALIDATE_PIECES]; - int order[WUSS_MAX_INVALIDATE_PIECES]; - int dx, dy, nstale, nclean, nsrc, nblit, ncopied, ndirty, nvis, i, j, idx; - int overflow, blit_failed; + int dx, dy, nstale, nclean, nsrc, ncopied, ndirty, i, overflow; dx = p.x - window->scroll.x; dy = p.y - window->scroll.y; @@ -45,21 +40,14 @@ void wuss_window_set_scroll(wuss_window_t *window, point_t p) #endif /* The valid blit source is the content box minus whatever windows above it - * were covering (those areas hold occluder pixels, not this window's). - * Sliding a source piece by the scroll delta can still land its - * destination on screen a higher window owns -- screen_copy_rect only - * clips to the content box, so it would paint over the occluder there. - * Clip each destination against the occluders too and keep only the - * surviving sub-pieces, each with its own matching source offset. */ + * were covering (those areas hold occluder pixels, not this window's), + * minus the stale regions collected above. wuss__blit_pieces then clips + * each destination against the occluders too. Each subtract can split a + * piece into up to four bands, so this can overflow the piece budget on a + * badly fragmented window -- treat that as "no safe fast path". */ nclean = wuss__clip_to_visible(window, &content, clean); - nblit = 0; + nsrc = 0; overflow = 0; - - /* Carve the stale regions out of each clean source piece. Each subtract - * can split a piece into up to four bands, so this can overflow the piece - * budget on a badly fragmented window -- treat that as "no safe fast - * path" and let the blit_failed branch below invalidate what's left. */ - nsrc = 0; for (i = 0; i < nclean; i++) { box_t kept[WUSS_MAX_INVALIDATE_PIECES]; @@ -79,55 +67,12 @@ void wuss_window_set_scroll(wuss_window_t *window, point_t p) break; } - for (i = 0; i < nsrc && !overflow; i++) - { - box_t want; - - box_translated(&src[i], -dx, -dy, &want); - nvis = wuss__clip_to_visible(window, &want, vis); - for (j = 0; j < nvis; j++) - { - if (nblit == WUSS_MAX_INVALIDATE_PIECES) - { - overflow = 1; - break; - } - blit_dest[nblit] = vis[j]; - box_translated(&vis[j], dx, dy, &blit_src[nblit]); - nblit++; - } - } - - /* Each sub-piece blit is a self-consistent memmove, but one piece's - * destination can land on another piece's still-unread source (e.g. the - * bands carved around a floating occluder overlap once shifted). Order - * them so that never happens; fall back to a full content invalidate if - * no safe order exists or the piece budget overflowed. */ - window->wuss->scr->clip = content; - ncopied = 0; - blit_failed = nsrc == 0 || overflow || - !wuss__order_pieces(blit_src, blit_dest, nblit, order); - - for (i = 0; i < nblit && !blit_failed; i++) - { - box_t got; - - idx = order[i]; - if (screen_copy_rect(window->wuss->scr, &blit_src[idx], - POINT(blit_dest[idx].x0, blit_dest[idx].y0), - &got) != result_OK) - { - blit_failed = 1; - break; - } - if (ncopied < WUSS_MAX_INVALIDATE_PIECES) - copied[ncopied++] = got; - } - - if (!blit_failed && ncopied > 0) + if (!overflow && + wuss__blit_pieces(window, src, nsrc, -dx, -dy, &content, + copied, &ncopied)) { - /* Repaint the content box minus what the blit reused, then clip each - * survivor to this window's visible area: the parts that were behind an + /* Repaint the content box minus what the blit reused, each survivor + * clipped to this window's visible area: parts that were behind an * occluder still show the occluder's own correct pixels, so leaving * them out keeps the scroll from redrawing the occluding window. */ ndirty = wuss__subtract_boxes(&content, copied, ncopied, dirty); From 59deb8c93882be66edd7028e4304713a0c63d479 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Fri, 4 Sep 2026 12:08:13 +0100 Subject: [PATCH 249/287] refactor(wuss): use box_translated for the on-screen nudge in window_create The birth-position clamp shifted win->visible with four inline field adds on two statement-per-line rows. box_translated does exactly that and is used for every other box shift in the module; it is alias-safe for in == out. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- libraries/wuss/core/window/create.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/libraries/wuss/core/window/create.c b/libraries/wuss/core/window/create.c index f662c500..a48ed7ba 100644 --- a/libraries/wuss/core/window/create.c +++ b/libraries/wuss/core/window/create.c @@ -72,8 +72,7 @@ result_t wuss_window_create(wuss_task_t *task, if (win->visible.y0 + dy < 0) dy = -win->visible.y0; - win->visible.x0 += dx; win->visible.x1 += dx; - win->visible.y0 += dy; win->visible.y1 += dy; + box_translated(&win->visible, dx, dy, &win->visible); win->flags = flags; From 5c85166b233a020b2d11b5d03620c77afd5cedf0 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Fri, 4 Sep 2026 12:11:21 +0100 Subject: [PATCH 250/287] refactor(wuss): wrap the redraw-only furniture dispatch calls redraw.c, resize.c (x2) and set-scroll.c each guarded a lone furniture_ops->{draw,invalidate,invalidate_for} call with its own #ifdef WUSS_FURNITURE. Add wuss__chrome_{draw,repaint,repaint_for} inlines to impl.h -- forwarding to the dispatch when furniture is built, empty when it is not -- so those call sites read straight. The routing hooks (hit_test, toggle_size, drag_*) keep their guards: those sit inside furniture-only branches that are whole-block conditional anyway. Named chrome_* rather than furniture_* to avoid colliding with the wuss__furniture_* functions they forward to. Verified against build-wuss-min (WUSS_FURNITURE=OFF): the DPTLib static lib builds clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- libraries/wuss/core/impl.h | 48 +++++++++++++++++++++++++ libraries/wuss/core/redraw.c | 4 +-- libraries/wuss/core/window/resize.c | 10 ++---- libraries/wuss/core/window/set-scroll.c | 4 +-- 4 files changed, 53 insertions(+), 13 deletions(-) diff --git a/libraries/wuss/core/impl.h b/libraries/wuss/core/impl.h index b8b55e26..06fa3fc7 100644 --- a/libraries/wuss/core/impl.h +++ b/libraries/wuss/core/impl.h @@ -193,6 +193,54 @@ struct wuss_window #endif }; +/* Thin wrappers over the furniture_ops dispatch so core call sites that only + * poke furniture for a redraw don't each need their own #ifdef WUSS_FURNITURE + * guard -- they compile to nothing in a no-furniture build. The routing hooks + * (hit_test, toggle_size, drag_*) stay unwrapped: their call sites sit inside + * furniture-only logic that is already whole-block guarded. Named "chrome" + * rather than "furniture" to steer clear of the wuss__furniture_* dispatch + * targets these forward to. */ +#ifdef WUSS_FURNITURE +static inline void wuss__chrome_draw(wuss_t *wuss, + wuss_window_t *window, + const box_t *full) +{ + wuss->furniture_ops->draw(wuss, window, full); +} + +static inline void wuss__chrome_repaint(wuss_window_t *window) +{ + window->wuss->furniture_ops->invalidate(window); +} + +static inline void wuss__chrome_repaint_for(wuss_window_t *window, + const box_t *visible) +{ + window->wuss->furniture_ops->invalidate_for(window, visible); +} +#else +static inline void wuss__chrome_draw(wuss_t *wuss, + wuss_window_t *window, + const box_t *full) +{ + (void) wuss; + (void) window; + (void) full; +} + +static inline void wuss__chrome_repaint(wuss_window_t *window) +{ + (void) window; +} + +static inline void wuss__chrome_repaint_for(wuss_window_t *window, + const box_t *visible) +{ + (void) window; + (void) visible; +} +#endif + wuss_window_t *wuss__window_at(wuss_t *wuss, point_t p); /* Rebuild wuss->palettecache (white, black and the symbolic[] table) from diff --git a/libraries/wuss/core/redraw.c b/libraries/wuss/core/redraw.c index 2b8ce1fb..9399fb77 100644 --- a/libraries/wuss/core/redraw.c +++ b/libraries/wuss/core/redraw.c @@ -19,9 +19,7 @@ static void redraw_window(wuss_t *wuss, if (box_intersection(&win->visible, full, &visible_clipped)) return; /* offscreen */ -#ifdef WUSS_FURNITURE - wuss->furniture_ops->draw(wuss, win, full); -#endif + wuss__chrome_draw(wuss, win, full); wuss__content_box(win, &content); if (box_intersection(&content, full, &clipped)) diff --git a/libraries/wuss/core/window/resize.c b/libraries/wuss/core/window/resize.c index 014e1358..d05745e5 100644 --- a/libraries/wuss/core/window/resize.c +++ b/libraries/wuss/core/window/resize.c @@ -117,9 +117,7 @@ result_t wuss_window_resize(wuss_window_t *window, size2d_t size) { wuss__invalidate_clipped(window, &content); } -#ifdef WUSS_FURNITURE - window->wuss->furniture_ops->invalidate(window); -#endif + wuss__chrome_repaint(window); } if (window->flags & wuss_WINDOW_NO_RESIZE_BLIT) @@ -148,12 +146,10 @@ result_t wuss_window_resize(wuss_window_t *window, size2d_t size) * as already-valid and so never repaint. Force its old position dirty * too. Shrinking needs no such help: old furniture positions only ever * land outside the new, smaller box, already covered above. */ -#ifdef WUSS_FURNITURE if (window->visible.x1 - window->visible.x0 > before.x1 - before.x0 || window->visible.y1 - window->visible.y0 > before.y1 - before.y0) - window->wuss->furniture_ops->invalidate_for(window, &before); - window->wuss->furniture_ops->invalidate(window); -#endif + wuss__chrome_repaint_for(window, &before); + wuss__chrome_repaint(window); } return result_OK; diff --git a/libraries/wuss/core/window/set-scroll.c b/libraries/wuss/core/window/set-scroll.c index 47080e75..0a2921b2 100644 --- a/libraries/wuss/core/window/set-scroll.c +++ b/libraries/wuss/core/window/set-scroll.c @@ -33,11 +33,9 @@ void wuss_window_set_scroll(wuss_window_t *window, point_t p) if (box_intersects(&window->wuss->dirty[i], &content)) stale[nstale++] = window->wuss->dirty[i]; -#ifdef WUSS_FURNITURE /* the scrollbar sausage position depends on scroll, so its well needs * redrawing too -- content invalidation alone never touches it */ - window->wuss->furniture_ops->invalidate(window); -#endif + wuss__chrome_repaint(window); /* The valid blit source is the content box minus whatever windows above it * were covering (those areas hold occluder pixels, not this window's), From d551bb5a3f3c37e2ced5a8e38e3e739de1795757 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Fri, 4 Sep 2026 12:24:12 +0100 Subject: [PATCH 251/287] refactor(wuss): name the z-order node-to-window cast Four z-order walks (at.c, invalidate.c, redraw.c, destroy.c) each did a bare (wuss_window_t *) cast off a list_t * chain node, silently relying on link being wuss_window's first member. Add wuss__window_from_link alongside the existing wuss__window_from_task_link and route all four through it, so that dependence is stated at every use rather than assumed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- libraries/wuss/core/destroy.c | 2 +- libraries/wuss/core/impl.h | 6 ++++++ libraries/wuss/core/redraw.c | 2 +- libraries/wuss/core/window/at.c | 2 +- libraries/wuss/core/window/invalidate.c | 2 +- 5 files changed, 10 insertions(+), 4 deletions(-) diff --git a/libraries/wuss/core/destroy.c b/libraries/wuss/core/destroy.c index 769534a9..140676c7 100644 --- a/libraries/wuss/core/destroy.c +++ b/libraries/wuss/core/destroy.c @@ -28,7 +28,7 @@ void wuss_destroy(wuss_t *doomed) next = e->next; #ifdef WUSS_ICONS - wuss__icons_free((wuss_window_t *) e); + wuss__icons_free(wuss__window_from_link(e)); #endif wuss__free(doomed, e); e = next; diff --git a/libraries/wuss/core/impl.h b/libraries/wuss/core/impl.h index 06fa3fc7..e0049122 100644 --- a/libraries/wuss/core/impl.h +++ b/libraries/wuss/core/impl.h @@ -419,6 +419,12 @@ int wuss__blit_pieces(wuss_window_t *window, #define wuss__window_from_task_link(node) \ ((wuss_window_t *) ((char *) (node) - offsetof(struct wuss_window, task_link))) +/* Recover the owning wuss_window_t from a node in the z-order chain + * (window->link). link is the first member, so this is just a cast -- but + * naming it keeps every z-order walk honest about depending on that. */ +#define wuss__window_from_link(node) \ + ((wuss_window_t *) (void *) (node)) + /* The single dispatch chokepoint. Delivers "ev" to "task"'s handle, passing * "win_or_null" as the window (NULL for task-view events). No-op (returns * result_OK) when task is NULL or has no handle. A debug build also asserts diff --git a/libraries/wuss/core/redraw.c b/libraries/wuss/core/redraw.c index 9399fb77..1a395016 100644 --- a/libraries/wuss/core/redraw.c +++ b/libraries/wuss/core/redraw.c @@ -91,7 +91,7 @@ static void redraw_from(wuss_t *wuss, if (last == NULL) break; - redraw_window(wuss, (wuss_window_t *) last, full, rc); + redraw_window(wuss, wuss__window_from_link(last), full, rc); stop = last; } } diff --git a/libraries/wuss/core/window/at.c b/libraries/wuss/core/window/at.c index c450a8fb..8574538a 100644 --- a/libraries/wuss/core/window/at.c +++ b/libraries/wuss/core/window/at.c @@ -10,7 +10,7 @@ wuss_window_t *wuss__window_at(wuss_t *wuss, point_t p) { wuss_window_t *win; - win = (wuss_window_t *) e; + win = wuss__window_from_link(e); if (win->flags & wuss_WINDOW_HIDDEN) continue; if (box_contains_point(&win->visible, p.x, p.y)) diff --git a/libraries/wuss/core/window/invalidate.c b/libraries/wuss/core/window/invalidate.c index 54682a9f..d6b7032a 100644 --- a/libraries/wuss/core/window/invalidate.c +++ b/libraries/wuss/core/window/invalidate.c @@ -64,7 +64,7 @@ int wuss__clip_to_visible(wuss_window_t *window, wuss_window_t *occluder; int nnext, p; - occluder = (wuss_window_t *) e; + occluder = wuss__window_from_link(e); if (occluder->flags & wuss_WINDOW_HIDDEN) continue; /* a hidden window occludes nothing */ nnext = 0; From 77ef4afc9569d3ca72cdab9b1af3eb24563c2704 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Fri, 4 Sep 2026 12:32:45 +0100 Subject: [PATCH 252/287] refactor(wuss): fold the furniture-rect fills behind one helper wuss__furniture_draw repeated the same six-line clip-then-fill for every titlebar icon, the resize grip, all eight scrollbar parts and both interior rules: intersect the part box with the redraw region, pin scr->clip, one screen_fill_rect. Extract fill_furniture_rect(wuss, box, full, colour) and call it at each site (~13), dropping ~30 lines net. Each fill now clips to its own part box rather than the enclosing titlebar or whole redraw region; every part box already sits inside that area, so the painted pixels are unchanged -- only the clip is tighter. The titlebar background fill stays inline: its intersection test also gates the icon and title-text sub-block and shares the clipped rect with the text draw. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- libraries/wuss/furniture/draw.c | 122 ++++++++++++-------------------- 1 file changed, 46 insertions(+), 76 deletions(-) diff --git a/libraries/wuss/furniture/draw.c b/libraries/wuss/furniture/draw.c index 944ee9f5..38d73fe4 100644 --- a/libraries/wuss/furniture/draw.c +++ b/libraries/wuss/furniture/draw.c @@ -7,6 +7,24 @@ #include "../core/impl.h" +/* Paint one furniture rectangle "b" in "colour", clipped to the part of it + * that falls inside "full" (the redraw region). A no-op when "b" is wholly + * outside "full". Pins scr->clip to the clipped rect -- the caller's next + * draw is expected to set its own clip. */ +static void fill_furniture_rect(wuss_t *wuss, + const box_t *b, + const box_t *full, + colour_t colour) +{ + box_t clipped; + + if (box_intersection(b, full, &clipped)) + return; /* wholly outside the redraw region */ + + wuss->scr->clip = clipped; + 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) @@ -93,9 +111,8 @@ void wuss__furniture_draw(wuss_t *wuss, box_t close; wuss__close_box(window, &close); - screen_fill_rect(wuss->scr, - close.x0, close.y0, box_size(&close), - wuss->palette[wuss->furniture_colours.close]); + fill_furniture_rect(wuss, &close, full, + wuss->palette[wuss->furniture_colours.close]); } if (!(window->flags & wuss_WINDOW_NO_BACK)) @@ -103,9 +120,8 @@ void wuss__furniture_draw(wuss_t *wuss, box_t back; wuss__back_box(window, &back); - screen_fill_rect(wuss->scr, - back.x0, back.y0, box_size(&back), - wuss->palette[wuss->furniture_colours.back]); + fill_furniture_rect(wuss, &back, full, + wuss->palette[wuss->furniture_colours.back]); } if (!(window->flags & wuss_WINDOW_NO_TOGGLE_SIZE)) @@ -113,9 +129,8 @@ void wuss__furniture_draw(wuss_t *wuss, box_t toggle; wuss__toggle_box(window, &toggle); - screen_fill_rect(wuss->scr, - toggle.x0, toggle.y0, box_size(&toggle), - wuss->palette[wuss->furniture_colours.toggle]); + fill_furniture_rect(wuss, &toggle, full, + wuss->palette[wuss->furniture_colours.toggle]); } } @@ -124,13 +139,8 @@ void wuss__furniture_draw(wuss_t *wuss, box_t resize; wuss__resize_box(window, &resize); - if (!box_intersection(&resize, full, &clipped)) - { - wuss->scr->clip = clipped; - screen_fill_rect(wuss->scr, - resize.x0, resize.y0, box_size(&resize), - wuss->palette[wuss->furniture_colours.resize]); - } + fill_furniture_rect(wuss, &resize, full, + wuss->palette[wuss->furniture_colours.resize]); } if (!(window->flags & wuss_WINDOW_NO_VSCROLL)) @@ -138,36 +148,20 @@ void wuss__furniture_draw(wuss_t *wuss, box_t up, down, well, sausage; wuss__vscroll_up_box(window, &up); - if (!box_intersection(&up, full, &clipped)) - { - wuss->scr->clip = clipped; - screen_fill_rect(wuss->scr, up.x0, up.y0, box_size(&up), - wuss->palette[wuss->furniture_colours.scroll.arrows]); - } + fill_furniture_rect(wuss, &up, full, + wuss->palette[wuss->furniture_colours.scroll.arrows]); wuss__vscroll_down_box(window, &down); - if (!box_intersection(&down, full, &clipped)) - { - wuss->scr->clip = clipped; - screen_fill_rect(wuss->scr, down.x0, down.y0, box_size(&down), - wuss->palette[wuss->furniture_colours.scroll.arrows]); - } + fill_furniture_rect(wuss, &down, full, + wuss->palette[wuss->furniture_colours.scroll.arrows]); wuss__vscroll_well_box(window, &well); - if (!box_intersection(&well, full, &clipped)) - { - wuss->scr->clip = clipped; - screen_fill_rect(wuss->scr, well.x0, well.y0, box_size(&well), - wuss->palette[wuss->furniture_colours.scroll.wells]); - } + fill_furniture_rect(wuss, &well, full, + wuss->palette[wuss->furniture_colours.scroll.wells]); wuss__vscroll_sausage_box(window, &sausage); - if (!box_intersection(&sausage, full, &clipped)) - { - wuss->scr->clip = clipped; - screen_fill_rect(wuss->scr, sausage.x0, sausage.y0, box_size(&sausage), - wuss->palette[wuss->furniture_colours.scroll.sausages]); - } + fill_furniture_rect(wuss, &sausage, full, + wuss->palette[wuss->furniture_colours.scroll.sausages]); } if (!(window->flags & wuss_WINDOW_NO_HSCROLL)) @@ -175,36 +169,20 @@ void wuss__furniture_draw(wuss_t *wuss, box_t left, right, well, sausage; wuss__hscroll_left_box(window, &left); - if (!box_intersection(&left, full, &clipped)) - { - wuss->scr->clip = clipped; - screen_fill_rect(wuss->scr, left.x0, left.y0, box_size(&left), - wuss->palette[wuss->furniture_colours.scroll.arrows]); - } + fill_furniture_rect(wuss, &left, full, + wuss->palette[wuss->furniture_colours.scroll.arrows]); wuss__hscroll_right_box(window, &right); - if (!box_intersection(&right, full, &clipped)) - { - wuss->scr->clip = clipped; - screen_fill_rect(wuss->scr, right.x0, right.y0, box_size(&right), - wuss->palette[wuss->furniture_colours.scroll.arrows]); - } + fill_furniture_rect(wuss, &right, full, + wuss->palette[wuss->furniture_colours.scroll.arrows]); wuss__hscroll_well_box(window, &well); - if (!box_intersection(&well, full, &clipped)) - { - wuss->scr->clip = clipped; - screen_fill_rect(wuss->scr, well.x0, well.y0, box_size(&well), - wuss->palette[wuss->furniture_colours.scroll.wells]); - } + fill_furniture_rect(wuss, &well, full, + wuss->palette[wuss->furniture_colours.scroll.wells]); wuss__hscroll_sausage_box(window, &sausage); - if (!box_intersection(&sausage, full, &clipped)) - { - wuss->scr->clip = clipped; - screen_fill_rect(wuss->scr, sausage.x0, sausage.y0, box_size(&sausage), - wuss->palette[wuss->furniture_colours.scroll.sausages]); - } + fill_furniture_rect(wuss, &sausage, full, + wuss->palette[wuss->furniture_colours.scroll.sausages]); } { @@ -222,12 +200,8 @@ void wuss__furniture_draw(wuss_t *wuss, rule.x1 = content.x1 + WUSS_DIVIDER_PX; rule.y0 = content.y0; rule.y1 = content.y1; - if (!box_intersection(&rule, full, &clipped)) - { - wuss->scr->clip = clipped; - screen_fill_rect(wuss->scr, rule.x0, rule.y0, box_size(&rule), - wuss->palette[wuss->furniture_colours.title.bg]); - } + fill_furniture_rect(wuss, &rule, full, + wuss->palette[wuss->furniture_colours.title.bg]); } if (carve.y > 0) @@ -236,12 +210,8 @@ void wuss__furniture_draw(wuss_t *wuss, 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; - if (!box_intersection(&rule, full, &clipped)) - { - wuss->scr->clip = clipped; - screen_fill_rect(wuss->scr, rule.x0, rule.y0, box_size(&rule), - wuss->palette[wuss->furniture_colours.title.bg]); - } + fill_furniture_rect(wuss, &rule, full, + wuss->palette[wuss->furniture_colours.title.bg]); } } From 2e5b4beccb1c3773c1a312e0fcc2a987a6efd4aa Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Fri, 4 Sep 2026 12:35:38 +0100 Subject: [PATCH 253/287] feat(wuss): log when a window op drops to a whole-region repaint move, set_scroll, resize (scroll-reclamp) and the furniture toggle each have a blit fast path with a fallback that invalidates the entire footprint or content box. That fallback means the frame's redraw cost jumps from a sliver to the whole window, so it is worth knowing when it fires. Emit logf_warning at the three blit-declined fallbacks (paletted screen, piece-budget overflow, no clobber-free order) and logf_info at the toggle one, which also covers by-design cases (non-topmost, NO_RESIZE_BLIT). Pull base/debug.h into core/impl.h so every core .c has the macros. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- libraries/wuss/core/impl.h | 1 + libraries/wuss/core/window/move.c | 2 ++ libraries/wuss/core/window/resize.c | 4 ++++ libraries/wuss/core/window/set-scroll.c | 4 ++++ libraries/wuss/furniture/toggle-action.c | 6 ++++++ 5 files changed, 17 insertions(+) diff --git a/libraries/wuss/core/impl.h b/libraries/wuss/core/impl.h index e0049122..b538a451 100644 --- a/libraries/wuss/core/impl.h +++ b/libraries/wuss/core/impl.h @@ -6,6 +6,7 @@ #include <stddef.h> #include <string.h> +#include "base/debug.h" #include "base/utils.h" #include "datastruct/list.h" #include "geom/box.h" diff --git a/libraries/wuss/core/window/move.c b/libraries/wuss/core/window/move.c index a25c9414..9f6bf379 100644 --- a/libraries/wuss/core/window/move.c +++ b/libraries/wuss/core/window/move.c @@ -112,6 +112,8 @@ void wuss_window_move(wuss_window_t *window, point_t p) /* Nothing of "before" was clean, splitting overflowed the piece budget, * the pieces would have clobbered each other, or the blit was declined: * fall back to a normal clipped redraw of the whole moved footprint. */ + logf_warning("wuss_window_move: blit fast path failed, repainting the " + "whole footprint (nclean=%d)", nclean); box_union(&before, &window->visible, &dirty); wuss__invalidate_clipped(window, &dirty); } diff --git a/libraries/wuss/core/window/resize.c b/libraries/wuss/core/window/resize.c index d05745e5..a3d26e10 100644 --- a/libraries/wuss/core/window/resize.c +++ b/libraries/wuss/core/window/resize.c @@ -115,6 +115,10 @@ result_t wuss_window_resize(wuss_window_t *window, size2d_t size) } else { + /* the scroll-reclamp blit copied nothing (paletted screen, or every + * piece off-screen): repaint the whole content box */ + logf_warning("wuss_window_resize: scroll-reclamp blit copied nothing, " + "repainting the whole content box (nsrc=%d)", nsrc); wuss__invalidate_clipped(window, &content); } wuss__chrome_repaint(window); diff --git a/libraries/wuss/core/window/set-scroll.c b/libraries/wuss/core/window/set-scroll.c index 0a2921b2..4717e7cf 100644 --- a/libraries/wuss/core/window/set-scroll.c +++ b/libraries/wuss/core/window/set-scroll.c @@ -79,5 +79,9 @@ void wuss_window_set_scroll(wuss_window_t *window, point_t p) return; } + /* fast path unavailable (piece budget overflowed, or wuss__blit_pieces + * found no safe blit): repaint the whole content box */ + logf_warning("wuss_window_set_scroll: blit fast path failed, repainting the " + "whole content box (overflow=%d)", overflow); wuss__invalidate_clipped(window, &content); } diff --git a/libraries/wuss/furniture/toggle-action.c b/libraries/wuss/furniture/toggle-action.c index 83669eed..cc0e1100 100644 --- a/libraries/wuss/furniture/toggle-action.c +++ b/libraries/wuss/furniture/toggle-action.c @@ -149,6 +149,12 @@ 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. */ + logf_info("wuss furniture toggle: no blit fast path, repainting the whole " + "old+new footprint"); box_union(&before, &window->visible, &dirty); wuss__invalidate_clipped(window, &dirty); } From a2d11cea20d9848ecc94d1718a706477767e9bfd Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Fri, 4 Sep 2026 12:45:09 +0100 Subject: [PATCH 254/287] docs(wuss): move menu-item field docs before each member Convert the trailing /**< comments in wuss_menu_item_flags and wuss_menu_item to leading /** comments, and capitalise them. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- include/wuss/menu.h | 70 +++++++++++++++++++++++++-------------------- include/wuss/task.h | 63 ++++++++++++++++++++++------------------ 2 files changed, 74 insertions(+), 59 deletions(-) diff --git a/include/wuss/menu.h b/include/wuss/menu.h index d99d50e2..f1386b20 100644 --- a/include/wuss/menu.h +++ b/include/wuss/menu.h @@ -35,43 +35,51 @@ extern "C" /** Per-item flags for a wuss_menu_item_t. OR'd together. */ typedef enum wuss_menu_item_flags { - wuss_MENU_ITEM_NONE = 0, - wuss_MENU_ITEM_TICKED = 1 << 0, /**< draw a tick at the item's left edge */ - wuss_MENU_ITEM_DASHED = 1 << 1, /**< draw a dashed rule above this item, - * marking a group boundary. The item is - * otherwise an ordinary row: it keeps - * its label and responds to the pointer. - * The rule is laid out and drawn - * separately and is not interactive */ - wuss_MENU_ITEM_DISABLED = 1 << 2, /**< greyed, never highlights, not - * selectable */ - wuss_MENU_ITEM_SWATCH = 1 << 3 /**< draw a colour chip of \c swatch at the - * item's left edge, in place of a tick */ + wuss_MENU_ITEM_NONE = 0, + + /** Draw a tick at the item's left edge */ + wuss_MENU_ITEM_TICKED = 1 << 0, + + /** Draw a dashed rule above this item, marking a group boundary. The + * item is otherwise an ordinary row: it keeps its label and responds + * to the pointer. The rule is laid out and drawn separately and is not + * interactive */ + wuss_MENU_ITEM_DASHED = 1 << 1, + + /** Greyed, never highlights, not selectable */ + wuss_MENU_ITEM_DISABLED = 1 << 2, + + /** Draw a colour chip of \c swatch at the item's left edge, in place of + * a tick */ + wuss_MENU_ITEM_SWATCH = 1 << 3 } wuss_menu_item_flags_t; /** One row of a menu. */ typedef struct wuss_menu_item { - const char *text; /**< row label; NULL is treated as "" */ - wuss_menu_item_flags_t flags; /**< see wuss_menu_item_flags_t */ - const struct wuss_menu *submenu; /**< non-NULL: draw a right arrow and open - * this menu to the right on hover */ - wuss_window_t *window; /**< non-NULL: draw a right arrow and, on - * hover, show this caller-owned window - * where a submenu would open, hiding it - * again when the pointer leaves the row - * or the chain is dismissed. Create it - * with wuss_WINDOW_HIDDEN. Mutually - * exclusive with \c submenu. The window - * must outlive the open chain -- do not - * wuss_window_close it while its menu is - * open. */ - wuss_colour_t swatch; /**< with wuss_MENU_ITEM_SWATCH: the colour - * chip to draw at the row's left edge, as - * an index into the system palette. - * Ignored without that flag, so a - * zero-initialised item is unaffected. */ + /** Row label; NULL is treated as "" */ + const char *text; + + /** See wuss_menu_item_flags_t */ + wuss_menu_item_flags_t flags; + + /** Non-NULL: draw a right arrow and open this menu to the right on + * hover */ + const struct wuss_menu *submenu; + + /** Non-NULL: draw a right arrow and, on hover, show this caller-owned + * window where a submenu would open, hiding it again when the pointer + * leaves the row or the chain is dismissed. Create it with + * wuss_WINDOW_HIDDEN. Mutually exclusive with \c submenu. The window + * must outlive the open chain -- do not wuss_window_close it while its + * menu is open. */ + wuss_window_t *window; + + /** With wuss_MENU_ITEM_SWATCH: the colour chip to draw at the row's + * left edge, as an index into the system palette. Ignored without that + * flag, so a zero-initialised item is unaffected. */ + wuss_colour_t swatch; } wuss_menu_item_t; diff --git a/include/wuss/task.h b/include/wuss/task.h index a0ab9bf8..7bed6dc9 100644 --- a/include/wuss/task.h +++ b/include/wuss/task.h @@ -38,34 +38,41 @@ extern "C" */ typedef enum wuss_event_kind { - wuss_EVENT_REDRAW, /**< Part of a window's content needs repaint. */ - wuss_EVENT_MOUSE, /**< Button down/up/move over window content. */ - wuss_EVENT_ICON, /**< A work-area button icon was clicked or hovered - (window view); or, in the task view, a future - shared/dock element -- reserved, nothing emits - it yet. */ - wuss_EVENT_SCROLL, /**< Mouse wheel used over a window's content. */ - wuss_EVENT_OPEN, /**< A visible window moved or resized. */ - wuss_EVENT_PRE_SHOW, /**< A hidden window is about to become visible; a - non-OK return vetoes the reveal. */ - wuss_EVENT_SHOW, /**< A window has become visible (hidden->visible - transition only, not at create). */ - wuss_EVENT_PRE_CLOSE, /**< A window is about to close via - wuss_window_try_close; a non-OK return vetoes - it. Never fired by wuss_window_close. */ - wuss_EVENT_CLOSE, /**< A window has closed after a successful - wuss_window_try_close. Never fired by - wuss_window_close. */ - wuss_EVENT_IDLE, /**< Wuss has finished its pending work; once per - task per wuss_idle. */ - wuss_EVENT_QUIT, /**< The task is shutting down, via - wuss_task_destroy; its windows are still - alive. */ - wuss_EVENT_PALETTE, /**< System palette changed, via wuss_set_palette; - once per task. Recache any wuss_nearest_colour - selections. */ - wuss_EVENT_MENU_SELECT /**< A leaf menu item was picked; delivered to the - task that opened the menu. */ + /** Part of a window's content needs repaint. */ + wuss_EVENT_REDRAW, + /** Button down/up/move over window content. */ + wuss_EVENT_MOUSE, + /** A work-area button icon was clicked or hovered (window view); or, in + * the task view, a future shared/dock element -- reserved, nothing + * emits it yet. */ + wuss_EVENT_ICON, + /** Mouse wheel used over a window's content. */ + wuss_EVENT_SCROLL, + /** A visible window moved or resized. */ + wuss_EVENT_OPEN, + /** A hidden window is about to become visible; a non-OK return vetoes + * the reveal. */ + wuss_EVENT_PRE_SHOW, + /** A window has become visible (hidden->visible transition only, not at + * create). */ + wuss_EVENT_SHOW, + /** A window is about to close via wuss_window_try_close; a non-OK return + * 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. */ + wuss_EVENT_CLOSE, + /** Wuss has finished its pending work; once per task per wuss_idle. */ + wuss_EVENT_IDLE, + /** The task is shutting down, via wuss_task_destroy; its windows are + * still alive. */ + wuss_EVENT_QUIT, + /** System palette changed, via wuss_set_palette; once per task. Recache + * any wuss_nearest_colour selections. */ + wuss_EVENT_PALETTE, + /** A leaf menu item was picked; delivered to the task that opened the + * menu. */ + wuss_EVENT_MENU_SELECT } wuss_event_kind_t; From 6eeb29615b39ac2017e85624f5333dc6b6ac5a39 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Fri, 4 Sep 2026 13:13:44 +0100 Subject: [PATCH 255/287] feat(wuss): group the task menu into Launch and Test submenus The demo-task launchers move onto a "Launch" submenu and the two menu-system exercisers onto a "Test" submenu, leaving Launch / Test / Quit Wuss at the top level. menu_handle now routes MENU_SELECT by data.menu_select.menu: g_launch_menu picks index g_launch_spawn, g_test_menu picks index g_test_spawn, g_task_menu handles Quit Wuss. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- apps/wuss/main.c | 106 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 76 insertions(+), 30 deletions(-) diff --git a/apps/wuss/main.c b/apps/wuss/main.c index e78d0ad4..2e04dc01 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -294,10 +294,10 @@ static const wuss_menu_t g_menu = "Display", g_menu_items, NELEMS(g_menu_items) }; -/* g_task_menu picks are dispatched by index; g_menu / g_menu_desc picks just - * print. All three menus are opened by g.menu_task, so one handler sees every - * wuss_EVENT_MENU_SELECT and tells them apart by data.menu_select.menu. - * Defined after g_task_menu / g_task_spawn, which it needs. */ +/* 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. Defined after the menus / spawn tables it needs. */ static result_t menu_handle(wuss_window_t *window, const wuss_event_t *event, void *task_data); @@ -370,30 +370,40 @@ static result_t spawn_menu_desc(void) } /* The task launcher is a MENU-button pop-up over the backdrop rather than a - * window of buttons. g_task_items and g_task_spawn run in lock-step: picking - * item i calls g_task_spawn[i]. */ + * 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]. */ typedef result_t (*task_spawn_fn_t)(void); -static const wuss_menu_item_t g_task_items[] = +/* "Launch" submenu: the demo tasks. */ +static const wuss_menu_item_t g_launch_items[] = +{ + { "Ball", wuss_MENU_ITEM_NONE, NULL }, + { "Blank", wuss_MENU_ITEM_NONE, NULL }, + { "Chars", wuss_MENU_ITEM_NONE, NULL }, + { "Checker", wuss_MENU_ITEM_NONE, NULL }, + { "Clock", wuss_MENU_ITEM_NONE, NULL }, + { "Curve", wuss_MENU_ITEM_NONE, NULL }, + { "Gradient", wuss_MENU_ITEM_NONE, NULL }, + { "Icons", wuss_MENU_ITEM_NONE, NULL }, + { "Image", wuss_MENU_ITEM_NONE, NULL }, + { "Lissajous", wuss_MENU_ITEM_NONE, NULL }, + { "Palette", wuss_MENU_ITEM_NONE, NULL }, + { "Porter-Duff", wuss_MENU_ITEM_NONE, NULL }, + { "Sofa", wuss_MENU_ITEM_NONE, NULL }, + { "Swatches", wuss_MENU_ITEM_NONE, NULL }, + { "Text", wuss_MENU_ITEM_NONE, NULL } +}; + +static const task_spawn_fn_t g_launch_spawn[] = +{ + spawn_ball, spawn_blank, spawn_chars, spawn_checker, spawn_clock, spawn_curve, + spawn_gradient, spawn_icons, spawn_image, spawn_lissajous, spawn_palette, + spawn_porter_duff, spawn_sofa, spawn_swatches, spawn_text +}; + +static const wuss_menu_t g_launch_menu = { - { "Ball", wuss_MENU_ITEM_NONE, NULL }, - { "Blank", wuss_MENU_ITEM_NONE, NULL }, - { "Chars", wuss_MENU_ITEM_NONE, NULL }, - { "Checker", wuss_MENU_ITEM_NONE, NULL }, - { "Clock", wuss_MENU_ITEM_NONE, NULL }, - { "Curve", wuss_MENU_ITEM_NONE, NULL }, - { "Gradient", wuss_MENU_ITEM_NONE, NULL }, - { "Icons", wuss_MENU_ITEM_NONE, NULL }, - { "Image", wuss_MENU_ITEM_NONE, NULL }, - { "Lissajous", wuss_MENU_ITEM_NONE, NULL }, - { "Palette", wuss_MENU_ITEM_NONE, NULL }, - { "Porter-Duff", wuss_MENU_ITEM_NONE, NULL }, - { "Sofa", wuss_MENU_ITEM_NONE, NULL }, - { "Swatches", wuss_MENU_ITEM_NONE, NULL }, - { "Text", wuss_MENU_ITEM_NONE, NULL }, - { "Menu", wuss_MENU_ITEM_DASHED, NULL }, - { "Menu (desc)", wuss_MENU_ITEM_NONE, NULL }, - { "Quit Wuss", wuss_MENU_ITEM_DASHED, NULL } + "Launch", g_launch_items, NELEMS(g_launch_items) }; static result_t spawn_quit(void) @@ -402,12 +412,34 @@ 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[] = { - spawn_ball, spawn_blank, spawn_chars, spawn_checker, spawn_clock, spawn_curve, - spawn_gradient, spawn_icons, spawn_image, spawn_lissajous, spawn_palette, - spawn_porter_duff, spawn_sofa, spawn_swatches, spawn_text, spawn_menu, - spawn_menu_desc, + NULL, /* "Launch" -> submenu g_launch_menu */ + NULL, /* "Test" -> submenu g_test_menu */ spawn_quit }; @@ -432,9 +464,23 @@ static result_t menu_handle(wuss_window_t *window, menu = event->data.menu_select.menu; index = event->data.menu_select.index; + if (menu == &g_launch_menu) + { + if (index >= 0 && index < (int) NELEMS(g_launch_spawn)) + (void) g_launch_spawn[index](); + 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)) + if (index >= 0 && index < (int) NELEMS(g_task_spawn) && g_task_spawn[index]) (void) g_task_spawn[index](); return result_OK; } From 1489263fdb2e30ceb4f78cab04eed9a91e5a11f4 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Fri, 4 Sep 2026 13:13:49 +0100 Subject: [PATCH 256/287] fix(wuss): open a submenu only from its arrow, close it on any re-entry A submenu row used to open its child on any hover anywhere on the row, and kept it open for every MOUSE_MOVE that stayed on that row. Now the child opens only while the pointer sits in the row's arrow gutter (new wuss__pointer_over_row_arrow helper: the rightmost WUSS_MENU_ARROW_W px), and any move that re-enters the parent level -- a different row, or this row's text off the arrow -- tears the child down. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- libraries/wuss/menu/menu.c | 44 +++++++++++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/libraries/wuss/menu/menu.c b/libraries/wuss/menu/menu.c index 4c7e869b..11a56423 100644 --- a/libraries/wuss/menu/menu.c +++ b/libraries/wuss/menu/menu.c @@ -54,6 +54,9 @@ static result_t wuss__menu_spawn(wuss_t *wuss, static void wuss__menu_flash_step(struct wuss__menu *self); static void wuss__menu_flash_finish(struct wuss__menu *self); +static int wuss__pointer_over_row_arrow(wuss_window_t *window, + const wuss_icon_t *icon); + /* The internal task that owns every borderless menu window, created on the * first wuss_menu_open of a session. */ static wuss_task_t *wuss__menu_task(wuss_t *wuss) @@ -233,9 +236,17 @@ static result_t wuss__menu_handle(wuss_window_t *window, if (event->data.icon.action == wuss_MOUSE_MOVE) { point_t at; + int has_child_row; + int on_arrow; + + has_child_row = (item->submenu != NULL || item->window != NULL); + on_arrow = has_child_row + && wuss__pointer_over_row_arrow(self->window, icon); - /* Hovering a different row closes any submenu the previous row opened. */ - if (self->child != NULL && self->open_index != index) + /* A submenu opens only while the pointer is over its row's arrow. Any + * other move that re-enters this level -- onto a different row, or onto + * this row's text off the arrow -- closes the child it opened. */ + if (self->child != NULL && !(self->open_index == index && on_arrow)) { wuss__menu_close_from(self->child); self->child = NULL; @@ -243,6 +254,9 @@ static result_t wuss__menu_handle(wuss_window_t *window, } if (self->child != NULL) + return result_OK; /* still on the open row's arrow: leave it up */ + + if (!on_arrow) return result_OK; /* A borrowed window opens where a submenu would; a submenu spawns as a @@ -255,9 +269,6 @@ static result_t wuss__menu_handle(wuss_window_t *window, return result_OK; } - if (item->submenu == NULL) - return result_OK; - at = wuss__submenu_anchor(self, icon); if (wuss__menu_spawn(self->wuss, self->owner, item->submenu, at, self, &self->child) == result_OK) @@ -337,6 +348,29 @@ static int wuss__pointer_over_icon(wuss_window_t *window, return wuss__icon_hit_test(window, doc) == icon; } +/* True if the wuss pointer sits over the submenu-arrow gutter of row `icon` + * in `window`: the pointer is within the row vertically and inside the + * rightmost WUSS_MENU_ARROW_W pixels of it. A submenu opens only from here, + * so re-entering the parent anywhere else closes the child. */ +static int wuss__pointer_over_row_arrow(wuss_window_t *window, + const wuss_icon_t *icon) +{ + point_t p; + box_t content; + box_t bbox; + point_t doc; + + p = wuss_get_pointer(window->wuss); + wuss__content_box(window, &content); + doc.x = p.x - content.x0 + window->scroll.x; + doc.y = p.y - content.y0 + window->scroll.y; + + wuss_icon_get_bbox(icon, &bbox); + + return doc.y >= bbox.y0 && doc.y < bbox.y1 + && doc.x >= bbox.x1 - WUSS_MENU_ARROW_W && doc.x < bbox.x1; +} + /* End the pick flash on `self` now: leave the flashed row un-highlit unless the * pointer is still over it (on an ADJUST re-pick no MOUSE_MOVE follows to bring * the highlight back), then deliver the MENU_SELECT the flash stands in for -- From c218de9cbaa6bf3286213f68d01dc95ced078a28 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Fri, 4 Sep 2026 13:28:22 +0100 Subject: [PATCH 257/287] feat(wuss): draw menu tick and submenu arrow from a symbol font Font slot 2 (WUSS_SYMBOL_FONT) is now consulted for the two menu decoration glyphs. When a font is present in that slot the selection tick and submenu arrow are drawn from its WUSS_GLYPH_TICK ('*') and WUSS_GLYPH_SUBMENU ('>') cells; when the slot is empty, or the font lacks the cell, they fall back to the existing vector strokes. The glyph codepoints default to printable ASCII so an ordinary bmfont can stand in, and are overridable with -D. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- libraries/wuss/core/impl.h | 13 +++++++++ libraries/wuss/icon/draw.c | 58 +++++++++++++++++++++++++++++++++----- 2 files changed, 64 insertions(+), 7 deletions(-) diff --git a/libraries/wuss/core/impl.h b/libraries/wuss/core/impl.h index b538a451..01696b33 100644 --- a/libraries/wuss/core/impl.h +++ b/libraries/wuss/core/impl.h @@ -48,6 +48,19 @@ #ifdef WUSS_ICONS #define WUSS_FRAME_CAPTION_INSET 8 /* x offset of a wuss_ICON_TYPE_FRAME caption from the frame's left edge */ #define WUSS_FRAME_CAPTION_PAD 2 /* gap left in the frame's top edge either side of the caption */ + +/* Font slot (see wuss_create's fonts[]) consulted for menu decoration glyphs: + * the selection tick and the submenu arrow. When the slot is empty those are + * drawn as vector strokes instead. A font in this slot is expected to carry a + * checkmark bitmap in its WUSS_GLYPH_TICK cell and an arrowhead in its + * WUSS_GLYPH_SUBMENU cell; the ASCII defaults let an ordinary bmfont stand in. */ +#define WUSS_SYMBOL_FONT 2 +#ifndef WUSS_GLYPH_TICK +#define WUSS_GLYPH_TICK '*' +#endif +#ifndef WUSS_GLYPH_SUBMENU +#define WUSS_GLYPH_SUBMENU '>' +#endif #endif #define WUSS_MIN_CONTENT 20 /* resize-drag floor: content can never be squeezed smaller than this */ #define WUSS_SCROLL_INSET 2 /* sausage cross-axis margin from its well's edges, purely cosmetic */ diff --git a/libraries/wuss/icon/draw.c b/libraries/wuss/icon/draw.c index a0d09585..2ba24992 100644 --- a/libraries/wuss/icon/draw.c +++ b/libraries/wuss/icon/draw.c @@ -68,6 +68,36 @@ static colour_t icon_blend_ground(const icon_draw_ctx_t *c, return fallback; } +/* Draw one menu-decoration glyph (a tick or a submenu arrow) from the symbol + * font slot, centred on "centre", blending "ink" over "ground". Returns 1 when + * the glyph was drawn, 0 when the slot is empty or the font lacks that cell -- + * the caller then falls back to its vector rendering. */ +static int wuss__draw_symbol_glyph(const wuss_t *wuss, + screen_t *scr, + point_t centre, + char glyph, + colour_t ink, + colour_t ground) +{ + bmfont_t *font; + point_t pos; + int font_width, font_height; + + font = wuss->fonts[WUSS_SYMBOL_FONT]; + if (font == NULL) + return 0; + + if ((unsigned char) glyph < ' ' || + (unsigned char) glyph - ' ' >= bmfont_get_count(font)) + return 0; + + bmfont_get_info(font, &font_width, &font_height); + pos.x = centre.x - font_width / 2; + pos.y = centre.y - font_height / 2; + bmfont_draw(font, scr, &glyph, 1, ink, ground, &pos, NULL); + return 1; +} + /* ----------------------------------------------------------------------- */ static void wuss__icon_draw_pattern(const icon_draw_ctx_t *c, @@ -396,26 +426,40 @@ static void wuss__icon_draw_menu_entry(const icon_draw_ctx_t *c) } else if (wuss__icon_selected(icon)) { - int cx, cy, h; + int cx, cy, h; + point_t centre; h = (c->font_height >= 8) ? c->font_height : 8; cx = b->x0 + pad; cy = b->y0 + (b->y1 - b->y0 - h) / 2; - screen_draw_line(c->scr, cx, cy + h / 2, cx + h / 2 - 1, cy + h - 2, ink); - screen_draw_line(c->scr, cx + h / 2 - 1, cy + h - 2, cx + h - 2, cy, ink); + centre.x = cx + h / 2; + centre.y = cy + h / 2; + if (!wuss__draw_symbol_glyph(c->wuss, c->scr, centre, + WUSS_GLYPH_TICK, ink, ground)) + { + screen_draw_line(c->scr, cx, cy + h / 2, cx + h / 2 - 1, cy + h - 2, ink); + screen_draw_line(c->scr, cx + h / 2 - 1, cy + h - 2, cx + h - 2, cy, ink); + } } /* right-edge arrow for a submenu entry */ if (icon->flags & wuss_ICON_FLAGS_SUBMENU) { - int ax, ay, r, dy; + int ax, ay, r, dy; + point_t centre; r = (c->font_height >= 8) ? c->font_height / 3 : 3; ax = b->x1 - 1 - pad - r; ay = b->y0 + (b->y1 - b->y0) / 2; - for (dy = -r; dy <= r; dy++) - screen_draw_line(c->scr, ax, ay + dy, - ax + (r - (dy < 0 ? -dy : dy)), ay + dy, ink); + centre.x = ax + r / 2; + centre.y = ay; + if (!wuss__draw_symbol_glyph(c->wuss, c->scr, centre, + WUSS_GLYPH_SUBMENU, ink, ground)) + { + for (dy = -r; dy <= r; dy++) + screen_draw_line(c->scr, ax, ay + dy, + ax + (r - (dy < 0 ? -dy : dy)), ay + dy, ink); + } } if (c->have_font && icon->text != NULL && icon->text[0] != '\0') From 1846d2cda7636346599fb3d2ef2df143decb0ac8 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Fri, 4 Sep 2026 13:31:08 +0100 Subject: [PATCH 258/287] test(wuss): load a symbol font in the interactive driver Adds resources/bmfonts/Symbols.png -- a 2bpp bmfont with a checkmark in the '*' cell and an arrowhead in '>' -- and loads it into font slot 2 of the interactive wuss app, so the menu selection tick and submenu arrow render as glyphs rather than vector strokes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- apps/wuss/main.c | 8 +++++--- resources/bmfonts/Symbols.png | Bin 0 -> 152 bytes 2 files changed, 5 insertions(+), 3 deletions(-) create mode 100644 resources/bmfonts/Symbols.png diff --git a/apps/wuss/main.c b/apps/wuss/main.c index 2e04dc01..eb4fb1b3 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -693,7 +693,8 @@ static result_t run_wuss(const char *resources) result_t rc; const char *leafname; const char *filename; - bmfont_t *fonts[2]; /* [0] regular (system font), [1] bold */ + bmfont_t *fonts[3]; /* [0] regular (system font), [1] bold, + * [2] symbol (menu tick / submenu arrow) */ int nfonts; int i; void *pixels; @@ -719,10 +720,11 @@ static result_t run_wuss(const char *resources) logf_info("wuss: resources root = \"%s\"", resources); { - static const char *const names[2] = { "Digits-Regular", "Digits-Bold" }; + static const char *const names[3] = + { "Digits-Regular", "Digits-Bold", "Symbols" }; nfonts = 0; - for (i = 0; i < 2; i++) + for (i = 0; i < 3; i++) { leafname = path_join_leafname(names[i], "png"); filename = path_join_filename(resources, 3, "resources", "bmfonts", diff --git a/resources/bmfonts/Symbols.png b/resources/bmfonts/Symbols.png new file mode 100644 index 0000000000000000000000000000000000000000..fdcbc9843f9698eedab07e53a836ef1fe5910781 GIT binary patch literal 152 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K5xS5!N<l8m7n}8HgfKQ0)|NsAiObG7J3Hc1< zxO=)dhE&W+jyY;Dg^`<?nOQhY((MsX!jVh{?umW{2ND~&?I*HvJTP#0z(0}g!KXuv v>`JBe2NchFEn!@<L#3mKWzk_NeFg@OeJtx*n^IYUMlyK1`njxgN@xNAHUTY@ literal 0 HcmV?d00001 From ba6e27ae007154c17ce362e65ae8602b3502947c Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Fri, 4 Sep 2026 14:12:37 +0100 Subject: [PATCH 259/287] feat(wuss): log when invalidations overflow and get coalesced wuss_invalidate tracks at most WUSS_MAX_DIRTY (16) dirty regions. Once full, a further invalidation is folded into the last entry, silently over-approximating its area. Emit a logf_info at that point so the coalescing is visible during debugging; it compiles out under NDEBUG. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- libraries/wuss/core/invalidate.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/libraries/wuss/core/invalidate.c b/libraries/wuss/core/invalidate.c index 1b027431..d1a8535d 100644 --- a/libraries/wuss/core/invalidate.c +++ b/libraries/wuss/core/invalidate.c @@ -68,11 +68,18 @@ result_t wuss_invalidate(wuss_t *wuss, const box_t *box) while (changed); if (wuss->ndirty < WUSS_MAX_DIRTY) + { wuss->dirty[wuss->ndirty++] = cur; + } else + { /* ponytail: array full, fold into the last entry rather than growing * storage; over-approximates that entry's area but stays correct */ + logf_info("wuss_invalidate: %d dirty regions, coalescing " + "(%d,%d)-(%d,%d) into the last entry", + WUSS_MAX_DIRTY, cur.x0, cur.y0, cur.x1, cur.y1); box_union(&wuss->dirty[WUSS_MAX_DIRTY - 1], &cur, &wuss->dirty[WUSS_MAX_DIRTY - 1]); + } return result_OK; } From 1268a3c6a3bce91eb61bfda3ead57eb9cf9a14bb Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Fri, 4 Sep 2026 14:34:00 +0100 Subject: [PATCH 260/287] feat(wuss): tick the selected font in chars' font menu wuss_fontmenu owned its menu items as const, so chars.c could not mark the current font's row. Add wuss_fontmenu_set_ticked(fm, index), which mutates the component's own array (ticking index, unticking every other row); index -1 clears every tick, for when the wuss system font -- outside the picker -- is in use. chars_open_menu calls it with task->current right before wuss_menu_open. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- include/wuss/component/fontmenu.h | 14 ++++++++++++++ libraries/wuss/component/fontmenu.c | 18 ++++++++++++++++++ libraries/wuss/test/tasks/chars.c | 5 +++-- 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/include/wuss/component/fontmenu.h b/include/wuss/component/fontmenu.h index e0e6231b..aad6d054 100644 --- a/include/wuss/component/fontmenu.h +++ b/include/wuss/component/fontmenu.h @@ -90,6 +90,20 @@ const wuss_menu_t *wuss_fontmenu_menu(const wuss_fontmenu_t *fm); const char *wuss_fontmenu_selected(const wuss_fontmenu_t *fm, const wuss_event_t *ev); +/** + * Tick the item at \p index and untick every other item, so the menu shows + * which font is currently in use. The menu is caller-displayed, not redrawn + * here -- call before wuss_menu_open (or reopen the menu) for the tick to be + * seen. + * + * \param[in] fm Handle; a NULL \p fm is a no-op. + * \param[in] index Row to tick, or -1 to untick every row (e.g. when a font + * outside the menu, such as the wuss system font, is in + * use). Out-of-range values other than -1 still untick + * every row. + */ +void wuss_fontmenu_set_ticked(wuss_fontmenu_t *fm, int index); + #ifdef __cplusplus } #endif diff --git a/libraries/wuss/component/fontmenu.c b/libraries/wuss/component/fontmenu.c index 812b4326..bc1d5de8 100644 --- a/libraries/wuss/component/fontmenu.c +++ b/libraries/wuss/component/fontmenu.c @@ -252,3 +252,21 @@ const char *wuss_fontmenu_selected(const wuss_fontmenu_t *fm, return fm->menu->items[index].text; } + +void wuss_fontmenu_set_ticked(wuss_fontmenu_t *fm, int index) +{ + wuss_menu_item_t *items; + int i; + + if (fm == NULL) + return; + + /* fm->menu is ours to mutate; only wuss_fontmenu_menu hands it out const */ + items = (wuss_menu_item_t *) fm->menu->items; + + for (i = 0; i < fm->menu->nitems; i++) + if (i == index) + items[i].flags |= wuss_MENU_ITEM_TICKED; + else + items[i].flags &= ~(wuss_menu_item_flags_t) wuss_MENU_ITEM_TICKED; +} diff --git a/libraries/wuss/test/tasks/chars.c b/libraries/wuss/test/tasks/chars.c index 76209685..9030a4af 100644 --- a/libraries/wuss/test/tasks/chars.c +++ b/libraries/wuss/test/tasks/chars.c @@ -89,8 +89,9 @@ static result_t chars_set_font(chars_task_t *task, int idx, const char *name) static result_t chars_open_menu(chars_task_t *task) { - /* ponytail: no tick on the current row -- wuss_fontmenu owns its items as - * const and offers no way to set one. The window title tracks the font. */ + /* the current font's row is ticked; task->current is -1 for the wuss + * system font, which ticks nothing */ + wuss_fontmenu_set_ticked(task->fontmenu, task->current); return wuss_menu_open(task->delegate, wuss_fontmenu_menu(task->fontmenu), wuss_get_pointer(task->wuss), NULL); From 76dbc7ba16102073d0063e4eaea13e1aeb973f10 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Fri, 4 Sep 2026 14:42:04 +0100 Subject: [PATCH 261/287] fix(wuss): retick an ADJUST-kept-open menu in place An ADJUST pick on a menu row delivers MENU_SELECT but, RISC OS style, leaves the chain open rather than tearing it down. A task's tick, however, is baked into the row's icon selected-state once at wuss_menu_open time (wuss_menu_item_t.flags is read only when the chain is (re)built) -- so a task that only edits its own item array on MENU_SELECT never sees the change while that same chain stays open. Add wuss_menu_set_ticked(handle, menu, index): finds the open level whose menu matches, ticks index and unticks every other row's icon directly, invalidating the ones that changed. chars.c now keeps the handle from wuss_menu_open and calls this after an ADJUST re-pick switches font, so the picker's tick tracks the current font even with the menu held open. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- include/wuss/menu.h | 24 ++++++++++++++++++++++ libraries/wuss/menu/menu.c | 28 +++++++++++++++++++++++++ libraries/wuss/test/tasks/chars.c | 34 +++++++++++++++++++++++-------- libraries/wuss/test/tasks/chars.h | 22 ++++++++++++-------- 4 files changed, 90 insertions(+), 18 deletions(-) diff --git a/include/wuss/menu.h b/include/wuss/menu.h index f1386b20..b73eddcb 100644 --- a/include/wuss/menu.h +++ b/include/wuss/menu.h @@ -126,6 +126,30 @@ void wuss_menu_close(wuss_menu_handle_t handle); /** Non-zero while \p handle refers to a currently open chain. */ int wuss_menu_is_open(wuss_menu_handle_t handle); +/** + * Re-tick a currently open menu level in place, for a task that keeps an + * ADJUST-picked menu open (see wuss_menu_open) and wants its own tick to + * change without rebuilding the chain: an ADJUST pick delivers + * wuss_EVENT_MENU_SELECT but does not close or redraw the menu, so a task + * that only edits its wuss_menu_item_t.flags array never sees it take effect + * on screen. + * + * Ticks item \p index and unticks every other item of the open level whose + * \c menu is \p menu (searched from \p handle's chain), then invalidates the + * changed rows. A no-op if \p handle is stale/closed, \p menu is not an open + * level of its chain, or \p index is out of range (still unticking every row + * in that case). + * + * \param[in] handle Chain handle from wuss_menu_open. + * \param[in] menu The (sub)menu level to update; matched by pointer + * against the description passed to wuss_menu_open or + * reached via a wuss_menu_item_t.submenu. + * \param[in] index Row to tick, or -1 to untick every row. + */ +void wuss_menu_set_ticked(wuss_menu_handle_t handle, + const wuss_menu_t *menu, + int index); + /* ----------------------------------------------------------------------- */ /* Building a wuss_menu_t tree from a compact descriptor string, and freeing diff --git a/libraries/wuss/menu/menu.c b/libraries/wuss/menu/menu.c index 11a56423..e935e704 100644 --- a/libraries/wuss/menu/menu.c +++ b/libraries/wuss/menu/menu.c @@ -781,6 +781,34 @@ int wuss_menu_is_open(wuss_menu_handle_t handle) return root->wuss->menu_chain == root; } +void wuss_menu_set_ticked(wuss_menu_handle_t handle, + const wuss_menu_t *menu, + int index) +{ + struct wuss__menu *root; + struct wuss__menu *node; + int i; + + if (handle == NULL || menu == NULL) + return; + + root = handle; + while (root->parent != NULL) + root = root->parent; + + if (root->wuss->menu_chain != root) + return; /* stale handle */ + + for (node = root; node != NULL; node = node->child) + if (node->menu == menu) + break; + if (node == NULL) + return; /* menu is not a level of this chain */ + + for (i = 0; i < menu->nitems; i++) + wuss_icon_set_selected(node->icons[i], i == index); +} + /* ----------------------------------------------------------------------- */ int wuss__menu_click_outside(wuss_t *wuss, const wuss_window_t *hit) diff --git a/libraries/wuss/test/tasks/chars.c b/libraries/wuss/test/tasks/chars.c index 9030a4af..8592c0b0 100644 --- a/libraries/wuss/test/tasks/chars.c +++ b/libraries/wuss/test/tasks/chars.c @@ -94,7 +94,7 @@ static result_t chars_open_menu(chars_task_t *task) wuss_fontmenu_set_ticked(task->fontmenu, task->current); return wuss_menu_open(task->delegate, wuss_fontmenu_menu(task->fontmenu), - wuss_get_pointer(task->wuss), NULL); + wuss_get_pointer(task->wuss), &task->menu_handle); } /* ----------------------------------------------------------------------- */ @@ -120,11 +120,12 @@ result_t chars_create(wuss_t *wuss, strncpy(chars_resources, resources, sizeof(chars_resources) - 1); chars_resources[sizeof(chars_resources) - 1] = '\0'; - task->wuss = wuss; - task->font = font; - task->current = -1; /* the wuss system font is none of the picker's */ - task->fg = colour_rgb(0x00, 0x00, 0x00); - task->bg = colour_rgb(0xFF, 0xFF, 0xFF); + task->wuss = wuss; + task->font = font; + task->current = -1; /* the wuss system font is none of the picker's */ + task->menu_handle = NULL; + task->fg = colour_rgb(0x00, 0x00, 0x00); + task->bg = colour_rgb(0xFF, 0xFF, 0xFF); /* the picker: every ".png" font under resources/bmfonts, sorted */ bmfonts_dir = path_join_filename(chars_resources, 2, "resources", "bmfonts"); @@ -263,12 +264,27 @@ result_t chars_handle(wuss_window_t *window, case wuss_EVENT_MENU_SELECT: { const char *name; + result_t rc; name = wuss_fontmenu_selected(cc->fontmenu, event); - if (name != NULL) - return chars_set_font(cc, event->data.menu_select.index, name); + if (name == NULL) + return result_OK; + + rc = chars_set_font(cc, event->data.menu_select.index, name); + + if (event->data.menu_select.button & wuss_BUTTON_ADJUST) + /* ADJUST keeps the chain open without rebuilding it, so the + * fresh-open tick set in chars_open_menu is now stale on screen; + * retick the still-open chain in place to match cc->current */ + wuss_menu_set_ticked(cc->menu_handle, + wuss_fontmenu_menu(cc->fontmenu), cc->current); + else + /* SELECT has already closed and freed the chain by the time this + * event arrives; the handle is stale, don't touch it */ + cc->menu_handle = NULL; + + return rc; } - return result_OK; case wuss_EVENT_REDRAW: return chars_redraw(event, task_data); diff --git a/libraries/wuss/test/tasks/chars.h b/libraries/wuss/test/tasks/chars.h index 6f26b534..014d5cff 100644 --- a/libraries/wuss/test/tasks/chars.h +++ b/libraries/wuss/test/tasks/chars.h @@ -17,15 +17,19 @@ * place. */ typedef struct chars_task { - wuss_window_t *window; - wuss_task_t *delegate; /* the wuss task backing this window */ - wuss_t *wuss; /* for wuss_get_pointer when opening the menu */ - wuss_fontmenu_t *fontmenu; /* the font picker; owns the menu and names */ - bmfont_t *font; /* currently shown; == fonts[current] */ - bmfont_t **fonts; /* one slot per menu item, lazily loaded */ - int nfonts; /* length of fonts[]; == menu item count */ - int current; /* index into fonts[], or -1 for sysfont */ - colour_t fg, bg; + wuss_window_t *window; + wuss_task_t *delegate; /* the wuss task backing this window */ + wuss_t *wuss; /* for wuss_get_pointer when opening the + * menu */ + wuss_fontmenu_t *fontmenu; /* the font picker; owns the menu and + * names */ + wuss_menu_handle_t menu_handle; /* the open chain, to re-tick it live on + * an ADJUST pick that keeps it open */ + bmfont_t *font; /* currently shown; == fonts[current] */ + bmfont_t **fonts; /* one slot per menu item, lazily loaded */ + int nfonts; /* length of fonts[]; == menu item count */ + int current; /* index into fonts[], or -1 for sysfont */ + colour_t fg, bg; } chars_task_t; From e885c54333e6e6656efdb99f016146da8fd7b2d5 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Fri, 4 Sep 2026 15:19:08 +0100 Subject: [PATCH 262/287] feat(wuss): tag font slots with a class and name for pickers wuss_create's fonts[] is now an array of wuss_font_desc_t (font handle, wuss_font_class_t, and a borrowed leafname), replacing the old bare bmfont_t* array. wuss_FONT_CLASS_SYSTEM marks a slot as chrome/decoration only (e.g. the symbol font menu ticks and submenu arrows are drawn from), as opposed to a normal user-selectable text font. wuss_fontmenu_create takes the owning wuss_t and skips any SYSTEM-class slot whose name matches a scanned leafname, so a font picker no longer offers chrome fonts as text choices. Matching is by name, not by loading the bmfont_t, preserving the menu's lazy load-on-select design. Adds wuss_get_font_class_n/wuss_get_font_name_n getters and updates all wuss_create/wuss_fontmenu_create call sites (apps/wuss demo, chars/text test tasks, wuss-test.c) accordingly, plus a test covering SYSTEM-class exclusion. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- apps/wuss/main.c | 54 ++++++++++-------- include/wuss/component/fontmenu.h | 8 +++ include/wuss/wuss.h | 88 ++++++++++++++++++++++++----- libraries/wuss/component/fontmenu.c | 28 +++++++++ libraries/wuss/core/create.c | 34 ++++++----- libraries/wuss/core/get-font.c | 20 +++++++ libraries/wuss/core/impl.h | 5 ++ libraries/wuss/test/tasks/chars.c | 5 +- libraries/wuss/test/tasks/text.c | 5 +- libraries/wuss/test/wuss-test.c | 59 ++++++++++++++++++- 10 files changed, 250 insertions(+), 56 deletions(-) diff --git a/apps/wuss/main.c b/apps/wuss/main.c index eb4fb1b3..3adf39e0 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -690,23 +690,25 @@ static result_t run_wuss(const char *resources) const int scr_width = 640; const int scr_height = 480; - result_t rc; - const char *leafname; - const char *filename; - bmfont_t *fonts[3]; /* [0] regular (system font), [1] bold, - * [2] symbol (menu tick / submenu arrow) */ - int nfonts; - int i; - void *pixels; - int rowbytes; - pixelfmt_t fmt; - bitmap_t bm; - screen_t scr; - colour_t palette[16]; - wuss_t *wuss; - wuss_frontend_t *frontend; - bool use_wimp16; - int palette_index; + result_t rc; + const char *leafname; + const char *filename; + bmfont_t *fonts[3]; /* [0] regular (system font), [1] bold, + * [2] symbol (menu tick / submenu arrow) */ + static const char *const names[3] = + { "Digits-Regular", "Digits-Bold", "Symbols" }; + int nfonts; + int i; + void *pixels; + int rowbytes; + pixelfmt_t fmt; + bitmap_t bm; + screen_t scr; + colour_t palette[16]; + wuss_t *wuss; + wuss_frontend_t *frontend; + bool use_wimp16; + int palette_index; { /* "wimp16" selects the RISC OS 16-colour palette; default is PICO-8 */ @@ -720,9 +722,6 @@ static result_t run_wuss(const char *resources) logf_info("wuss: resources root = \"%s\"", resources); { - static const char *const names[3] = - { "Digits-Regular", "Digits-Bold", "Symbols" }; - nfonts = 0; for (i = 0; i < 3; i++) { @@ -758,11 +757,22 @@ static result_t run_wuss(const char *resources) screen_for_bitmap(&scr, &bm); { - wuss_config_t config; + wuss_config_t config; + wuss_font_desc_t descs[3]; /* slot classes/names for the picker menus -- + * [2] Symbols is chrome-only, never a text + * font choice */ fill_chrome_config(&config, use_wimp16 ? 1 : 0); - rc = wuss_create(&scr, fonts, nfonts, palette, NELEMS(palette), &config, + for (i = 0; i < nfonts; i++) + { + descs[i].font = fonts[i]; + descs[i].font_class = (i == 2) ? wuss_FONT_CLASS_SYSTEM + : wuss_FONT_CLASS_NONE; + descs[i].name = names[i]; + } + + rc = wuss_create(&scr, descs, nfonts, palette, NELEMS(palette), &config, NULL, &wuss); logf_info("wuss: wuss_create -> rc=0x%X (%s)", rc, result_string(rc)); if (rc != result_OK) diff --git a/include/wuss/component/fontmenu.h b/include/wuss/component/fontmenu.h index aad6d054..95613dfc 100644 --- a/include/wuss/component/fontmenu.h +++ b/include/wuss/component/fontmenu.h @@ -43,6 +43,13 @@ typedef struct wuss_fontmenu wuss_fontmenu_t; * failure. * \param[in] dir Directory to scan for fonts. * \param[in] title Menu caption, borrowed and copied; NULL for "Font". + * \param[in] wuss Window manager to consult for fonts to leave out, or + * NULL to include every font found. Any wuss_create font + * slot (see \ref wuss_font_desc_t) whose class is \ref + * wuss_FONT_CLASS_SYSTEM and whose name matches a scanned + * leafname is skipped -- e.g. the symbol font wuss itself + * draws menu ticks and submenu arrows from, which is not + * meant to be picked as a text font. * \param[in] alloc Allocator for every block the handle keeps, copied in; * NULL selects \ref wuss_alloc (plain stdlib). Pass the * same hooks given to wuss_create so the component and the @@ -55,6 +62,7 @@ typedef struct wuss_fontmenu wuss_fontmenu_t; result_t wuss_fontmenu_create(wuss_fontmenu_t **out, const char *dir, const char *title, + const wuss_t *wuss, const wuss_alloc_t *alloc); /** diff --git a/include/wuss/wuss.h b/include/wuss/wuss.h index 2511b43c..b7728d65 100644 --- a/include/wuss/wuss.h +++ b/include/wuss/wuss.h @@ -346,17 +346,55 @@ wuss_config_t; * flags can name (see wuss_ICON_FONT). */ #define wuss_MAX_FONTS 4 +/** + * What a font passed to wuss_create is for. Lets a shared component such as + * a font-picker menu tell a decorative/chrome font apart from one meant to + * be offered as a user-selectable text face. + */ +typedef enum wuss_font_class +{ + wuss_FONT_CLASS_NONE, /**< An ordinary text font. */ + + /** + * Chrome/decoration only, e.g. the symbol font menu ticks and submenu + * arrows are drawn from (see \ref WUSS_SYMBOL_FONT). Not meant to be + * offered as a text face. + */ + wuss_FONT_CLASS_SYSTEM +} +wuss_font_class_t; + +/** + * One font slot passed to wuss_create: the font itself, what it is for, and + * the name a caller-side picker (e.g. wuss_fontmenu) should know it by. + */ +typedef struct wuss_font_desc +{ + /** Font handle. Not owned; must outlive the wuss_t. */ + bmfont_t *font; + + wuss_font_class_t font_class; /**< What the font is for. */ + + /** + * Borrowed; the font's leafname sans ".png", for a picker to match + * against. NULL if the slot has no name (e.g. a NONE-class font that no + * picker needs to skip). + */ + const char *name; +} +wuss_font_desc_t; + /** * Create a window manager. * * \param[in] scr Screen to draw windows onto. Not owned; must outlive * the wuss_t. - * \param[in] fonts Up to \ref wuss_MAX_FONTS font handles, copied into - * the wuss_t (the handles, not the fonts -- they are - * not owned and must outlive it). Slot 0 is the system - * font, used for titlebar labels and any icon that does - * not select another. NULL, or nfonts 0, leaves - * titlebars unlabelled. + * \param[in] fonts Up to \ref wuss_MAX_FONTS font slots, copied into the + * wuss_t (the descriptors, not the fonts -- the fonts + * are not owned and must outlive it). Slot 0 is the + * system font, used for titlebar labels and any icon + * that does not select another. NULL, or nfonts 0, + * leaves titlebars unlabelled. * \param[in] nfonts Number of entries in \p fonts, 0..\ref * wuss_MAX_FONTS; more than that is an error. * \param[in] palette System palette, copied in, or NULL to use a built-in @@ -373,14 +411,14 @@ wuss_config_t; * result_WUSS_BAD_COLOUR if any of config's palette entries are out * of range for the palette, or another appropriate result code. */ -result_t wuss_create(screen_t *scr, - bmfont_t *const *fonts, - int nfonts, - const colour_t *palette, - int npalette, - const wuss_config_t *config, - const wuss_alloc_t *alloc, - wuss_t **wuss); +result_t wuss_create(screen_t *scr, + const wuss_font_desc_t *fonts, + int nfonts, + const colour_t *palette, + int npalette, + const wuss_config_t *config, + const wuss_alloc_t *alloc, + wuss_t **wuss); /** * Replace the system palette partway through a session. @@ -454,6 +492,28 @@ bmfont_t *wuss_get_font(const wuss_t *wuss); */ bmfont_t *wuss_get_font_n(const wuss_t *wuss, int index); +/** + * Fetch the class of one of the fonts passed to wuss_create by slot (see + * \ref wuss_font_desc_t). + * + * \param[in] wuss Window manager. + * \param[in] index Font slot, 0..\ref wuss_MAX_FONTS - 1. + * \return The slot's class, or \ref wuss_FONT_CLASS_NONE if the slot is out + * of range or was not filled. + */ +wuss_font_class_t wuss_get_font_class_n(const wuss_t *wuss, int index); + +/** + * Fetch the name of one of the fonts passed to wuss_create by slot (see \ref + * wuss_font_desc_t). + * + * \param[in] wuss Window manager. + * \param[in] index Font slot, 0..\ref wuss_MAX_FONTS - 1. + * \return The slot's name (borrowed, valid until wuss_destroy), or NULL if + * the slot is out of range, was not filled, or was given no name. + */ +const char *wuss_get_font_name_n(const wuss_t *wuss, int index); + /** * The last pointer position seen by wuss_mouse_click or wuss_mouse_move, * screen space. (0,0) until the first mouse event. Handy for opening a diff --git a/libraries/wuss/component/fontmenu.c b/libraries/wuss/component/fontmenu.c index bc1d5de8..8bf3f14e 100644 --- a/libraries/wuss/component/fontmenu.c +++ b/libraries/wuss/component/fontmenu.c @@ -55,6 +55,8 @@ static void menu_free(const wuss_alloc_t *a, wuss_menu_t *m) typedef struct namelist { const wuss_alloc_t *alloc; /* borrowed; the resolved hooks */ + const wuss_t *wuss; /* borrowed; consulted to skip SYSTEM fonts, or + * NULL to skip nothing */ char **names; int n; int cap; @@ -62,6 +64,27 @@ typedef struct namelist } namelist_t; +/* True if any wuss_create font slot names \p name and is classed SYSTEM -- + * chrome-only, not meant to be offered as a text font. */ +static int is_system_font_name(const wuss_t *wuss, const char *name) +{ + int i; + + if (wuss == NULL) + return 0; + + for (i = 0; i < wuss_MAX_FONTS; i++) + { + const char *slot_name = wuss_get_font_name_n(wuss, i); + + if (slot_name != NULL && strcmp(slot_name, name) == 0 && + wuss_get_font_class_n(wuss, i) == wuss_FONT_CLASS_SYSTEM) + return 1; + } + + return 0; +} + static result_t collect_name(const char *name, const char *path, void *opaque) @@ -71,6 +94,9 @@ static result_t collect_name(const char *name, (void) path; + if (is_system_font_name(nl->wuss, name)) + return result_OK; + if (wuss__array_grow(nl->alloc, (void **) &nl->names, sizeof(*nl->names), nl->n, &nl->cap, 1, 8)) { @@ -171,6 +197,7 @@ static result_t build_menu(const wuss_alloc_t *a, result_t wuss_fontmenu_create(wuss_fontmenu_t **out, const char *dir, const char *title, + const wuss_t *wuss, const wuss_alloc_t *alloc) { namelist_t nl; @@ -185,6 +212,7 @@ result_t wuss_fontmenu_create(wuss_fontmenu_t **out, memset(&nl, 0, sizeof(nl)); nl.alloc = alloc; + nl.wuss = wuss; rc = bmfont_enumerate(dir, collect_name, &nl); if (rc != result_OK) diff --git a/libraries/wuss/core/create.c b/libraries/wuss/core/create.c index 03eb4f6f..36800256 100644 --- a/libraries/wuss/core/create.c +++ b/libraries/wuss/core/create.c @@ -48,14 +48,14 @@ static result_t validate_bevel_backdrop(const wuss_t *w, } #endif -result_t wuss_create(screen_t *scr, - bmfont_t *const *fonts, - int nfonts, - const colour_t *palette, - int npalette, - const wuss_config_t *config, - const wuss_alloc_t *alloc, - wuss_t **wuss) +result_t wuss_create(screen_t *scr, + const wuss_font_desc_t *fonts, + int nfonts, + const colour_t *palette, + int npalette, + const wuss_config_t *config, + const wuss_alloc_t *alloc, + wuss_t **wuss) { bmfont_t *font; wuss_alloc_t al; @@ -79,7 +79,7 @@ result_t wuss_create(screen_t *scr, if (nfonts < 0 || nfonts > wuss_MAX_FONTS || (nfonts > 0 && fonts == NULL)) return result_BAD_ARG; - font = (nfonts > 0) ? fonts[0] : NULL; + font = (nfonts > 0) ? fonts[0].font : NULL; al = (alloc != NULL) ? *alloc : wuss_alloc; @@ -198,11 +198,11 @@ result_t wuss_create(screen_t *scr, * titlebar to whichever weight is taller */ bmfont_get_info(font, &font_width, &font_height); NOT_USED(font_width); - if (nfonts > 1 && fonts[1] != NULL) + if (nfonts > 1 && fonts[1].font != NULL) { int titleh; - bmfont_get_info(fonts[1], NULL, &titleh); + bmfont_get_info(fonts[1].font, NULL, &titleh); if (titleh > font_height) font_height = titleh; } @@ -257,9 +257,17 @@ result_t wuss_create(screen_t *scr, int i; for (i = 0; i < nfonts; i++) - w->fonts[i] = fonts[i]; + { + w->fonts[i] = fonts[i].font; + w->font_classes[i] = fonts[i].font_class; + w->font_names[i] = fonts[i].name; + } for (; i < wuss_MAX_FONTS; i++) - w->fonts[i] = NULL; + { + w->fonts[i] = NULL; + w->font_classes[i] = wuss_FONT_CLASS_NONE; + w->font_names[i] = NULL; + } w->nfonts = nfonts; } #ifdef WUSS_FURNITURE diff --git a/libraries/wuss/core/get-font.c b/libraries/wuss/core/get-font.c index 4e1160f0..2cb0d874 100644 --- a/libraries/wuss/core/get-font.c +++ b/libraries/wuss/core/get-font.c @@ -20,3 +20,23 @@ bmfont_t *wuss_get_font_n(const wuss_t *wuss, int index) return wuss->fonts[index]; } + +wuss_font_class_t wuss_get_font_class_n(const wuss_t *wuss, int index) +{ + assert(wuss != NULL); + + if (index < 0 || index >= wuss_MAX_FONTS) + return wuss_FONT_CLASS_NONE; + + return wuss->font_classes[index]; +} + +const char *wuss_get_font_name_n(const wuss_t *wuss, int index) +{ + assert(wuss != NULL); + + if (index < 0 || index >= wuss_MAX_FONTS) + return NULL; + + return wuss->font_names[index]; +} diff --git a/libraries/wuss/core/impl.h b/libraries/wuss/core/impl.h index 01696b33..a82d3196 100644 --- a/libraries/wuss/core/impl.h +++ b/libraries/wuss/core/impl.h @@ -103,6 +103,11 @@ struct wuss screen_t *scr; bmfont_t *fonts[wuss_MAX_FONTS]; /* slot 0 is the system * font; unset slots NULL; not owned */ + wuss_font_class_t font_classes[wuss_MAX_FONTS]; /* parallel to + * fonts[]; NONE for an unset slot */ + const char *font_names[wuss_MAX_FONTS]; /* parallel to + * fonts[]; borrowed; NULL if unset + * or given no name */ int nfonts; /* entries filled from wuss_create */ wuss_alloc_t alloc; /* malloc/realloc/free hooks, copied in * by wuss_create; used for every heap diff --git a/libraries/wuss/test/tasks/chars.c b/libraries/wuss/test/tasks/chars.c index 8592c0b0..0594a350 100644 --- a/libraries/wuss/test/tasks/chars.c +++ b/libraries/wuss/test/tasks/chars.c @@ -127,9 +127,10 @@ result_t chars_create(wuss_t *wuss, task->fg = colour_rgb(0x00, 0x00, 0x00); task->bg = colour_rgb(0xFF, 0xFF, 0xFF); - /* the picker: every ".png" font under resources/bmfonts, sorted */ + /* the picker: every ".png" font under resources/bmfonts, sorted, less any + * SYSTEM-class font (e.g. the one wuss draws menu ticks/arrows from) */ bmfonts_dir = path_join_filename(chars_resources, 2, "resources", "bmfonts"); - rc = wuss_fontmenu_create(&task->fontmenu, bmfonts_dir, "Font", NULL); + rc = wuss_fontmenu_create(&task->fontmenu, bmfonts_dir, "Font", wuss, NULL); if (rc != result_OK) { free(task); /* nothing registered yet; the spawner will not free it */ diff --git a/libraries/wuss/test/tasks/text.c b/libraries/wuss/test/tasks/text.c index df31c6d4..c296a052 100644 --- a/libraries/wuss/test/tasks/text.c +++ b/libraries/wuss/test/tasks/text.c @@ -107,9 +107,10 @@ result_t text_create(wuss_t *wuss, strncpy(task->resources, resources, sizeof(task->resources) - 1); task->resources[sizeof(task->resources) - 1] = '\0'; - /* the picker: every ".png" font under resources/bmfonts, sorted */ + /* the picker: every ".png" font under resources/bmfonts, sorted, less any + * SYSTEM-class font (e.g. the one wuss draws menu ticks/arrows from) */ bmfonts_dir = path_join_filename(task->resources, 2, "resources", "bmfonts"); - rc = wuss_fontmenu_create(&task->fontmenu, bmfonts_dir, "Font", NULL); + rc = wuss_fontmenu_create(&task->fontmenu, bmfonts_dir, "Font", wuss, NULL); if (rc != result_OK) { free(task); /* nothing registered yet; the spawner will not free it */ diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 6a289fca..26f005c5 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -3926,7 +3926,7 @@ MenuOK: ; dir = path_join_filename(resources, 2, "resources", "bmfonts"); - rc = wuss_fontmenu_create(&fm, dir, "Font", NULL); + rc = wuss_fontmenu_create(&fm, dir, "Font", NULL, NULL); if (rc != result_OK) goto FontMenuFail; @@ -3966,8 +3966,57 @@ MenuOK: ; wuss_fontmenu_destroy(fm); + /* a SYSTEM-class wuss font is dropped from the menu; NONE-class and + * unnamed slots are not */ + { + const char *sysfontfile; + bmfont_t *sysfont; + wuss_font_desc_t sysdescs[2]; + wuss_t *syswuss; + + sysfontfile = path_join_filename(resources, 3, "resources", "bmfonts", + path_join_leafname("Symbols", "png")); + rc = bmfont_create(sysfontfile, &sysfont); + if (rc != result_OK) goto FontMenuFail; + + sysdescs[0].font = sysfont; + sysdescs[0].font_class = wuss_FONT_CLASS_SYSTEM; + sysdescs[0].name = "Symbols"; + sysdescs[1].font = sysfont; + sysdescs[1].font_class = wuss_FONT_CLASS_NONE; + sysdescs[1].name = "Tiny"; /* NONE class: not skipped though named */ + + rc = wuss_create(&scr, sysdescs, 2, NULL, 0, NULL, NULL, &syswuss); + if (rc != result_OK) + { + bmfont_destroy(sysfont); + goto FontMenuFail; + } + + /* dir points into path_join_filename's shared scratch buffer, which + * sysfontfile's path_join_filename call above just overwrote -- + * re-derive it rather than reuse the now-stale pointer */ + dir = path_join_filename(resources, 2, "resources", "bmfonts"); + + rc = wuss_fontmenu_create(&fm, dir, "Font", syswuss, NULL); + wuss_destroy(syswuss); + bmfont_destroy(sysfont); + if (rc != result_OK) goto FontMenuFail; + + fmm = wuss_fontmenu_menu(fm); + if (fmm == NULL) goto FontMenuFail; + for (i = 0; i < fmm->nitems; i++) + if (strcmp(fmm->items[i].text, "Symbols") == 0) goto FontMenuFail; + for (i = 0; i < fmm->nitems; i++) + if (strcmp(fmm->items[i].text, "Tiny") == 0) + break; + if (i == fmm->nitems) goto FontMenuFail; + + wuss_fontmenu_destroy(fm); + } + /* missing directory is surfaced, not swallowed */ - rc = wuss_fontmenu_create(&fm, "no/such/dir/here", NULL, NULL); + rc = wuss_fontmenu_create(&fm, "no/such/dir/here", NULL, NULL, NULL); if (rc != result_FILE_NOT_FOUND) goto FontMenuFail; rc = result_OK; @@ -4061,6 +4110,7 @@ ColourMenuOK: ; const char *fontfile; bmfont_t *font; + wuss_font_desc_t fdesc; screen_t fscr; bitmap_t fbm; void *fpixels; @@ -4088,7 +4138,10 @@ ColourMenuOK: ; if (rc != result_OK) goto FlashFailFree; screen_for_bitmap(&fscr, &fbm); - rc = wuss_create(&fscr, &font, 1, NULL, 0, NULL, NULL, &fwuss); + fdesc.font = font; + fdesc.font_class = wuss_FONT_CLASS_NONE; + fdesc.name = NULL; + rc = wuss_create(&fscr, &fdesc, 1, NULL, 0, NULL, NULL, &fwuss); if (rc != result_OK) goto FlashFailFree; memset(&ftc, 0, sizeof(ftc)); From 2068de49ffb4a27ed31e61350993720876b67f29 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Fri, 4 Sep 2026 15:44:49 +0100 Subject: [PATCH 263/287] chore(bmfonts): regenerate Digits-Regular and Symbols PNGs Byproduct of recent font-slot tagging/picker work on wuss. --- resources/bmfonts/Digits-Regular.png | Bin 895 -> 894 bytes resources/bmfonts/Symbols.png | Bin 152 -> 265 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/resources/bmfonts/Digits-Regular.png b/resources/bmfonts/Digits-Regular.png index 446566ef5c80c4b828054fbc5b0a32a9f71cfe56..3daeda586b4c56fba059dd8128b41b95393f0cdd 100755 GIT binary patch delta 222 zcmV<403rYX2L1++g#qNThA0Amf)*u2MPrLp=N1`GG_NbMc-mj%xCYtuii@o;xc};X zkP+E2NUkW;UlE@GX|_*i+6Em3h%Ku6_IIY`8+yg2wNul^iz!`C=E%r?9;geGO*6jg zCti!7^?Ks98z4Ic;fxnR+wPkcLG~1cS2NLT5c=w?(SJ{b)|hA!iHwU;?Ros!BJf3y zZ-RqG4){30BKTYci#T56VUcEJk-Xi-acs^ll8Q)AyKc_9Zf;m)cVYbV{*yO)aJ0$u Y52mug@KgOBIsgCw07*qoM6N<$f-_5P9RL6T delta 223 zcmV<503iSV2LA?-g#qQUhA0Am&Y(pJQPJ2U)wxB66V2;NES~n)IIcl9z2ai)3+}&q zA7n&!43aC#^jE|uK$`8-nYKYk0b+})zWtqP`G#JxY3<as@nTBXlQ}Z7p9ku~WYdhV z`ia*fXuY0z?FPtBK{(?D(6;+#MUXuO;nhs^8ic<3YV_X|p*1F2L?Yu-VtXEcwg`NY z<D1}Mkpn&sun0aE!6J^=cvz$vStM_FaU7d-i=-mb)2^GduA3Vc*<Bd_y#M5l9vp4* Z`~#qh!S21Ks?`7h002ovPDHLkV1g+vZ}|WK diff --git a/resources/bmfonts/Symbols.png b/resources/bmfonts/Symbols.png index fdcbc9843f9698eedab07e53a836ef1fe5910781..f4c8a6368e6b4f455d88725f09847bed01f8fc79 100644 GIT binary patch literal 265 zcmeAS@N?(olHy`uVBq!ia0y~yU{nCIxtW-O<f4x|fC3DR#X;^)4C~IxyaaMs(j9#r z85lP9bN@+X1@aY=J%W507^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+10 zf+xTy#P#?0&p;*v!~X*Z4!Ah(-3Dald%8G=Xq->BIFNAQz~KXD4m2Ek5VcjxmxaGM z`)U%~ovxNn^N!<<lKqb7jz}MoYB;Xo7R1AKFkL{<u1Q*?>$pPLB%Y#W(;Roi9#L}Y za(rO$t6%ib-hcBeKA6gRJlN1;-oPU6cu9~i`jKaWpacWMB>vl%zZm>H2Xcm|tDnm{ Hr-UW|6QEiI literal 152 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K5xS5!N<l8m7n}8HgfKQ0)|NsAiObG7J3Hc1< zxO=)dhE&W+jyY;Dg^`<?nOQhY((MsX!jVh{?umW{2ND~&?I*HvJTP#0z(0}g!KXuv v>`JBe2NchFEn!@<L#3mKWzk_NeFg@OeJtx*n^IYUMlyK1`njxgN@xNAHUTY@ From f6b062488307dc6f89c68adb4c6aa3ac80ce280d Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Fri, 4 Sep 2026 16:43:33 +0100 Subject: [PATCH 264/287] fix(wuss): clip furniture redraws to unoccluded pieces redraw_window drew a window's chrome (titlebar, scrollbars, outline) against the raw dirty/full rect with no occlusion clipping at all -- only its content went through wuss__clip_to_visible. A window redrawing its own dirty region while partly covered by a higher window would repaint its furniture straight back over that higher window's pixels. Also exclude wuss_NO_BACKGROUND windows' content boxes from the desktop backdrop's pre-fill, so a task that skips or only partially repaints its content sees whatever was already on screen instead of a flash of backdrop colour. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- libraries/wuss/core/redraw.c | 56 +++++++++++++++++++++++++++++++++--- 1 file changed, 52 insertions(+), 4 deletions(-) diff --git a/libraries/wuss/core/redraw.c b/libraries/wuss/core/redraw.c index 1a395016..fe6d31aa 100644 --- a/libraries/wuss/core/redraw.c +++ b/libraries/wuss/core/redraw.c @@ -2,6 +2,46 @@ #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. */ +static void fill_backdrop_excluding_content(wuss_t *wuss, const box_t *area) +{ + box_t cuts[WUSS_MAX_INVALIDATE_PIECES]; + box_t pieces[WUSS_MAX_INVALIDATE_PIECES]; + int ncuts, npieces, i; + list_t *e; + + ncuts = 0; + for (e = wuss->z_order.next; + e != NULL && ncuts < WUSS_MAX_INVALIDATE_PIECES; + e = e->next) + { + wuss_window_t *win; + box_t content, 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)) + continue; /* no overlap with the area being filled */ + + cuts[ncuts++] = clipped; + } + + npieces = wuss__subtract_boxes(area, cuts, ncuts, pieces); + + for (i = 0; i < npieces; i++) + wuss__fill_backdrop(wuss->scr, wuss->palette, &wuss->backdrop, + &pieces[i], 0, 0); +} + static void redraw_window(wuss_t *wuss, wuss_window_t *win, const box_t *full, @@ -19,7 +59,16 @@ static void redraw_window(wuss_t *wuss, if (box_intersection(&win->visible, full, &visible_clipped)) return; /* offscreen */ - wuss__chrome_draw(wuss, win, full); + /* clip chrome to whatever part of the window isn't hidden behind a + * higher window, same reasoning as the content clip below -- otherwise + * a window redrawing under an overlap would paint its furniture back + * over whatever is covering it */ + npieces = wuss__clip_to_visible(win, &visible_clipped, pieces); + for (i = 0; i < npieces; i++) + { + wuss->scr->clip = pieces[i]; + wuss__chrome_draw(wuss, win, &pieces[i]); + } wuss__content_box(win, &content); if (box_intersection(&content, full, &clipped)) @@ -107,7 +156,7 @@ result_t wuss_redraw(wuss_t *wuss) full.y1 = wuss->scr->size.h; wuss->scr->clip = full; - wuss__fill_backdrop(wuss->scr, wuss->palette, &wuss->backdrop, &full, 0, 0); + fill_backdrop_excluding_content(wuss, &full); rc = result_OK; redraw_from(wuss, wuss->z_order.next, &full, &rc); @@ -135,8 +184,7 @@ result_t wuss_redraw_dirty(wuss_t *wuss) for (i = 0; i < wuss->ndirty; i++) { wuss->scr->clip = wuss->dirty[i]; - wuss__fill_backdrop(wuss->scr, wuss->palette, &wuss->backdrop, - &wuss->dirty[i], 0, 0); + fill_backdrop_excluding_content(wuss, &wuss->dirty[i]); redraw_from(wuss, wuss->z_order.next, &wuss->dirty[i], &rc); } From 74496d7f1a7b51bd7bf259ad4182e97ef93e57c4 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Fri, 4 Sep 2026 16:51:25 +0100 Subject: [PATCH 265/287] refactor(wuss): rebuild icons demo task with grouped layout Replace hand-picked spec index arithmetic (g, m, g+N offsets into one flat array) with a layout cursor threaded through per-group helper functions (icons_add_intro/buttons/radios/bitmaps/pattern/menu), each appending its own specs and advancing a document y. A named enum reserves the exact spec count per group so the total can't silently drift from the array bound. Expand the demo to cover every wuss_icon_type_t: bordered FRAME groups for buttons (normal/default/disabled), radios+option, bitmaps, and a new PATTERN swatch group, plus a SWATCH and a DISABLED entry added to the menu strip. Window and document grown to fit; the scrolled-away proof button is kept. Fix two -Wcomment warnings from *ptr inside doc comments. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- libraries/wuss/test/tasks/icons.c | 548 +++++++++++++++++++++--------- libraries/wuss/test/tasks/icons.h | 7 +- 2 files changed, 382 insertions(+), 173 deletions(-) diff --git a/libraries/wuss/test/tasks/icons.c b/libraries/wuss/test/tasks/icons.c index 20b7826a..149eb00e 100644 --- a/libraries/wuss/test/tasks/icons.c +++ b/libraries/wuss/test/tasks/icons.c @@ -13,6 +13,7 @@ #include "framebuf/bitmap.h" #include "framebuf/palettes.h" +#include "framebuf/pattern.h" #include "framebuf/screen.h" #include "geom/box.h" #include "geom/point.h" @@ -21,37 +22,364 @@ #include "icons.h" -#define ICONS_DOC_W 220 -#define ICONS_DOC_H 640 /* taller than the window, so scrolling is exercised */ +#define ICONS_DOC_W 260 +#define ICONS_DOC_H 900 /* 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 */ + +/* Maximum spec count across every group icons_create can lay out; the sprite + * pair is the only optional part (skipped when the PNG fails to load). Kept + * as one enum so a miscount between this and the groups below fails loudly + * (an array bound, not a silent overrun) rather than corrupting the heap. */ +enum +{ + ICONS_N_INTRO = 4, /* heading, counter button, counter label, scrolled-away button */ + 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_PATTERN = 2, /* frame + one PATTERN swatch */ + 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 +}; + +/* Running state threaded through the icons_add_* helpers: where to write the + * next spec, and how far down the document the next group should start. */ +typedef struct icons_layout +{ + wuss_icon_spec_t *specs; + 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 */ +} +icons_layout_t; + +/* ----------------------------------------------------------------------- */ + +/* [0] a heading, [1] the button that bumps the counter (shown as a default + * action button, so it carries the accent styling), [2] the counter label + * beside it, [3] a button far down the document, to prove icons scroll and + * stay clickable. Returns the indices of [1] and [2] via button/counter. */ +static void icons_add_intro(icons_layout_t *lay, int *button, int *counter) +{ + wuss_icon_spec_t *s; + + s = &lay->specs[lay->n]; + s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN, lay->y, 220, 14); + s->type = wuss_ICON_TYPE_LABEL; + s->text = "Work-area icons:"; + s->fg = lay->black; + s->bg = wuss_NO_BACKGROUND; + s->flags = wuss_ICON_FONT(1); + lay->n++; + lay->y += 30; + + *button = lay->n; + s = &lay->specs[lay->n]; + s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN, lay->y, 80, 22); + s->type = wuss_ICON_TYPE_BUTTON; + s->text = "Press me"; + s->fg = lay->black; + s->bg = lay->grey6; + s->flags = wuss_ICON_FLAGS_DEFAULT; + lay->n++; + + *counter = lay->n; + s = &lay->specs[lay->n]; + s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN + 92, lay->y, 120, 22); + s->type = wuss_ICON_TYPE_LABEL; + s->text = "0"; + s->fg = lay->black; + s->bg = wuss_NO_BACKGROUND; + lay->n++; + lay->y += 46; + + s = &lay->specs[lay->n]; + s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN, 780, 90, 52); + s->type = wuss_ICON_TYPE_BUTTON; + s->text = "Scrolled"; + s->fg = lay->black; + s->bg = lay->grey6; + lay->n++; +} + +/* A grouping frame captioned "Buttons", with a normal, a default (accent) and + * a disabled button side by side inside it. */ +static void icons_add_buttons(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, 56); + 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->grey6; + lay->n++; + + s = &lay->specs[lay->n]; + s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN + 74, top + 22, 56, 22); + s->type = wuss_ICON_TYPE_BUTTON; + s->text = "Default"; + s->fg = lay->black; + s->bg = lay->grey6; + 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->grey6; + s->flags = wuss_ICON_FLAGS_DISABLED; + lay->n++; + + lay->y = top + 70; +} + +/* A grouping frame captioned "Radios & options", with two justified labels, + * three radios sharing group 1, a standalone option and a label echoing + * whichever control last changed. Returns the indices of the option and the + * echo label via opt/state. */ +static void icons_add_radios(icons_layout_t *lay, int *opt, int *state) +{ + wuss_icon_spec_t *s; + int top; + int r; + + top = lay->y; + s = &lay->specs[lay->n]; + s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN, top, 200, 170); + s->type = wuss_ICON_TYPE_FRAME; + s->text = "Radios & options"; + 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, 14); + s->type = wuss_ICON_TYPE_LABEL; + s->text = "right"; + s->fg = lay->black; + s->bg = wuss_NO_BACKGROUND; + s->flags = wuss_ICON_FLAGS_JUSTIFY_RIGHT; + lay->n++; + + s = &lay->specs[lay->n]; + s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN + 10, top + 40, 180, 14); + s->type = wuss_ICON_TYPE_LABEL; + s->text = "centre"; + s->fg = lay->black; + s->bg = wuss_NO_BACKGROUND; + s->flags = wuss_ICON_FLAGS_JUSTIFY_CENTRE; + lay->n++; + + for (r = 0; r < 3; r++) + { + s = &lay->specs[lay->n]; + s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN + 10, top + 66 + r * 20, 180, 16); + s->type = wuss_ICON_TYPE_RADIO; + s->text = (r == 0) ? "Red" : (r == 1) ? "Green" : "Blue"; + s->fg = lay->black; + s->bg = wuss_NO_BACKGROUND; + s->group = 1; + lay->n++; + } + + s = &lay->specs[lay->n]; + s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN + 10, top + 130, 180, 16); + s->type = wuss_ICON_TYPE_OPTION; + s->text = "Wireframe"; + s->fg = lay->black; + s->bg = wuss_NO_BACKGROUND; + *opt = lay->n; + lay->n++; + + s = &lay->specs[lay->n]; + s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN + 10, top + 150, 180, 14); + s->type = wuss_ICON_TYPE_LABEL; + s->text = "(no selection)"; + s->fg = lay->black; + s->bg = wuss_NO_BACKGROUND; + *state = lay->n; + lay->n++; + + lay->y = top + 190; +} + +/* A grouping frame captioned "Bitmaps", holding a decorative sprite and (to + * its right) an interactive one that bumps the counter -- only laid out if + * the sprite loaded. Returns the interactive icon's index via *hotspot, or + * leaves it unset if sprite is NULL. */ +static void icons_add_bitmaps(icons_layout_t *lay, + const bitmap_t *sprite, + int *hotspot) +{ + wuss_icon_spec_t *s; + int top; + + if (sprite == NULL) + return; + + top = lay->y; + s = &lay->specs[lay->n]; + s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN, top, 200, + sprite->size.h + 30); + s->type = wuss_ICON_TYPE_FRAME; + s->text = "Bitmaps"; + 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, + sprite->size.w, sprite->size.h); + s->type = wuss_ICON_TYPE_BITMAP; + s->bitmap = sprite; + lay->n++; + + s = &lay->specs[lay->n]; + s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN + 20 + sprite->size.w, top + 20, + sprite->size.w, sprite->size.h); + s->type = wuss_ICON_TYPE_BITMAP; + s->bitmap = sprite; + s->flags = wuss_ICON_FLAGS_INTERACTIVE; + *hotspot = lay->n; + lay->n++; + + lay->y = top + sprite->size.h + 50; +} + +/* A grouping frame captioned "Pattern", holding one PATTERN-filled swatch. */ +static void icons_add_pattern(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, 60); + s->type = wuss_ICON_TYPE_FRAME; + s->text = "Pattern"; + 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, 26); + s->type = wuss_ICON_TYPE_PATTERN; + s->fg = lay->black; + s->bg = lay->grey6; + s->pattern = screen_PATTERN_DIAGONAL; + lay->n++; + + lay->y = top + 76; +} + +/* 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; + + top = lay->y; + s = &lay->specs[lay->n]; + s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN, top, 180, 16); + s->type = wuss_ICON_TYPE_MENU_ENTRY; + s->text = "Open"; + 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, 180, 16); + s->type = wuss_ICON_TYPE_MENU_ENTRY; + s->text = "Show grid"; + s->fg = lay->black; + s->bg = wuss_NO_BACKGROUND; + *ticked = lay->n; + lay->n++; + + s = &lay->specs[lay->n]; + s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN, top + ICONS_ROW * 2, 180, 16); + s->type = wuss_ICON_TYPE_MENU_ENTRY; + s->text = "Layer colour"; + s->fg = lay->black; + s->bg = wuss_NO_BACKGROUND; + s->swatch = lay->red; + s->flags = wuss_ICON_FLAGS_SWATCH; + lay->n++; + + s = &lay->specs[lay->n]; + s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN, top + ICONS_ROW * 3, 180, 16); + s->type = wuss_ICON_TYPE_MENU_ENTRY; + s->text = "Export"; + s->fg = lay->black; + s->bg = wuss_NO_BACKGROUND; + s->flags = wuss_ICON_FLAGS_SUBMENU; + lay->n++; + + s = &lay->specs[lay->n]; + s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN, top + ICONS_ROW * 4, 180, 16); + s->type = wuss_ICON_TYPE_MENU_ENTRY; + s->text = "Disabled"; + s->fg = lay->black; + s->bg = wuss_NO_BACKGROUND; + s->flags = wuss_ICON_FLAGS_DISABLED; + lay->n++; + + s = &lay->specs[lay->n]; + s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN, top + 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->type = wuss_ICON_TYPE_MENU_ENTRY; + s->text = "Quit"; + s->fg = lay->black; + s->bg = wuss_NO_BACKGROUND; + s->flags = wuss_ICON_FLAGS_SEPARATOR; + lay->n++; + + lay->y = top + ICONS_ROW * 7 + 10; +} + +/* ----------------------------------------------------------------------- */ result_t icons_create(wuss_t *wuss, bmfont_t *font, const char *resources, icons_task_t *task) { - /* [0..3] heading, counter button, counter label and a scrolled-away button, - * then [.. +3] a grouping frame with two differently-justified labels inside - * it, then [.. +5] three grouped radios, a standalone option and a label - * echoing the selection, then [.. +2] a decorative bitmap icon and an - * interactive one that bumps the counter, then [.. +4] a strip of menu-entry - * icons that hover-highlight */ - enum { ICONS_NSPECS = 4 + 3 + 5 + 2 + 5 }; wuss_task_t *delegate; wuss_task_desc_t delegate_desc; - wuss_colour_t black, grey5, grey6; wuss_icon_spec_t specs[ICONS_NSPECS]; wuss_icon_t *made[ICONS_NSPECS]; + icons_layout_t lay; const char *sprite_path; - int g; /* index of the first frame spec */ - int r; /* radio index */ - int m; /* index of the first menu-entry spec */ - int nspecs; /* live spec count (sprite icons are optional) */ + int i_button, i_counter, i_opt, i_state, i_hotspot, i_ticked; result_t rc; - black = wuss_nearest_colour(wuss, 0x00, 0x00, 0x00); - grey5 = wuss_nearest_colour(wuss, 0xBB, 0xBB, 0xBB); - grey6 = wuss_nearest_colour(wuss, 0xDD, 0xDD, 0xDD); - task->font = font; task->label = colour_rgb(0x00, 0x00, 0x00); task->paper = colour_rgb(0xDD, 0xDD, 0xDD); /* the window bg, below */ @@ -81,13 +409,19 @@ result_t icons_create(wuss_t *wuss, return rc; } + 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); + rc = wuss_window_create_placed(delegate, - SIZE2D(ICONS_DOC_W, 160), + SIZE2D(ICONS_DOC_W, 200), "Icons", wuss_WINDOW_NONE, - wuss_BACKDROP_PATTERN(grey5, + wuss_BACKDROP_PATTERN(lay.grey5, screen_PATTERN_CROSSHATCH, - grey6), + lay.grey6), SIZE2D(ICONS_DOC_W, ICONS_DOC_H), SIZE2D(0, 0), &task->window); @@ -98,158 +432,32 @@ result_t icons_create(wuss_t *wuss, } memset(specs, 0, sizeof(specs)); - - /* icons sit past the ruler gutter (see ICONS_GUTTER in icons_redraw) so the - * axis labels have the top/left strip to themselves */ - - /* [0] a heading label, drawn in the bold weight (font slot 1) */ - specs[0].bbox = (box_t) BOX_POS_SIZE(28, 28, 180, 14); - specs[0].type = wuss_ICON_TYPE_LABEL; - specs[0].text = "Work-area icons:"; - specs[0].fg = black; - specs[0].bg = wuss_NO_BACKGROUND; - specs[0].flags = wuss_ICON_FONT(1); - - /* [1] the button that bumps the counter -- a default action button, so it - * shows the accent styling */ - specs[1].bbox = (box_t) BOX_POS_SIZE(28, 50, 80, 22); - specs[1].type = wuss_ICON_TYPE_BUTTON; - specs[1].text = "Press me"; - specs[1].fg = black; - specs[1].bg = grey6; - specs[1].flags = wuss_ICON_FLAGS_DEFAULT; - - /* [2] the counter label beside it */ - specs[2].bbox = (box_t) BOX_POS_SIZE(120, 50, 120, 22); - specs[2].type = wuss_ICON_TYPE_LABEL; - specs[2].text = "0"; - specs[2].fg = black; - specs[2].bg = wuss_NO_BACKGROUND; - - /* [3] a button far down the document, to prove icons scroll and stay clickable */ - specs[3].bbox = (box_t) BOX_POS_SIZE(28, 460, 90, 52); - specs[3].type = wuss_ICON_TYPE_BUTTON; - specs[3].text = "Scrolled"; - specs[3].fg = black; - specs[3].bg = grey6; - - /* [g] a grouping frame down the document, with [g+1] a right-justified and - * [g+2] a centred label sat inside it */ - g = 4; - - specs[g].bbox = (box_t) BOX_POS_SIZE(28, 300, 170, 70); - specs[g].type = wuss_ICON_TYPE_FRAME; - specs[g].text = "Grouping frame"; - specs[g].fg = black; - specs[g].bg = wuss_NO_BACKGROUND; - - specs[g + 1].bbox = (box_t) BOX_POS_SIZE(38, 320, 150, 14); - specs[g + 1].type = wuss_ICON_TYPE_LABEL; - specs[g + 1].text = "right"; - specs[g + 1].fg = black; - specs[g + 1].bg = wuss_NO_BACKGROUND; - specs[g + 1].flags = wuss_ICON_FLAGS_JUSTIFY_RIGHT; - - specs[g + 2].bbox = (box_t) BOX_POS_SIZE(38, 342, 150, 14); - specs[g + 2].type = wuss_ICON_TYPE_LABEL; - specs[g + 2].text = "centre"; - specs[g + 2].fg = black; - specs[g + 2].bg = wuss_NO_BACKGROUND; - specs[g + 2].flags = wuss_ICON_FLAGS_JUSTIFY_CENTRE; - - /* [g+3..g+5] three radios sharing group 1, [g+6] a standalone option, - * [g+7] a label echoing whichever control last changed */ - for (r = 0; r < 3; r++) - { - specs[g + 3 + r].bbox = (box_t) BOX_POS_SIZE(28, 380 + r * 20, 150, 16); - specs[g + 3 + r].type = wuss_ICON_TYPE_RADIO; - specs[g + 3 + r].text = (r == 0) ? "Red" : (r == 1) ? "Green" : "Blue"; - specs[g + 3 + r].fg = black; - specs[g + 3 + r].bg = wuss_NO_BACKGROUND; - specs[g + 3 + r].group = 1; - } - - specs[g + 6].bbox = (box_t) BOX_POS_SIZE(28, 444, 150, 16); - specs[g + 6].type = wuss_ICON_TYPE_OPTION; - specs[g + 6].text = "Wireframe"; - specs[g + 6].fg = black; - specs[g + 6].bg = wuss_NO_BACKGROUND; - - specs[g + 7].bbox = (box_t) BOX_POS_SIZE(28, 464, 180, 14); - specs[g + 7].type = wuss_ICON_TYPE_LABEL; - specs[g + 7].text = "(no selection)"; - specs[g + 7].fg = black; - specs[g + 7].bg = wuss_NO_BACKGROUND; - - nspecs = g + 8; - - /* [g+8] a decorative bitmap, [g+9] an interactive one that bumps the - * counter -- only if the sprite loaded */ - if (task->has_sprite) - { - specs[g + 8].bbox = (box_t) BOX_POS_SIZE(28, 484, task->sprite.size.w, - task->sprite.size.h); - specs[g + 8].type = wuss_ICON_TYPE_BITMAP; - specs[g + 8].bitmap = &task->sprite; - - specs[g + 9].bbox = (box_t) BOX_POS_SIZE(120, 484, task->sprite.size.w, - task->sprite.size.h); - specs[g + 9].type = wuss_ICON_TYPE_BITMAP; - specs[g + 9].bitmap = &task->sprite; - specs[g + 9].flags = wuss_ICON_FLAGS_INTERACTIVE; - - nspecs = g + 10; - } - - /* [nspecs..nspecs+4] a menu-entry strip: plain, ticked, submenu, then a - * standalone dashed rule and a SEPARATOR-flagged entry below it. Hover the - * pointer over them to see the highlight track; the rule stays inert. */ - m = nspecs; - - specs[m].bbox = (box_t) BOX_POS_SIZE(28, 524, 160, 16); - specs[m].type = wuss_ICON_TYPE_MENU_ENTRY; - specs[m].text = "Open"; - specs[m].fg = black; - specs[m].bg = wuss_NO_BACKGROUND; - - specs[m + 1].bbox = (box_t) BOX_POS_SIZE(28, 542, 160, 16); - specs[m + 1].type = wuss_ICON_TYPE_MENU_ENTRY; - specs[m + 1].text = "Show grid"; - specs[m + 1].fg = black; - specs[m + 1].bg = wuss_NO_BACKGROUND; - - specs[m + 2].bbox = (box_t) BOX_POS_SIZE(28, 560, 160, 16); - specs[m + 2].type = wuss_ICON_TYPE_MENU_ENTRY; - specs[m + 2].text = "Export"; - specs[m + 2].fg = black; - specs[m + 2].bg = wuss_NO_BACKGROUND; - specs[m + 2].flags = wuss_ICON_FLAGS_SUBMENU; - - specs[m + 3].bbox = (box_t) BOX_POS_SIZE(28, 578, 160, 10); - specs[m + 3].type = wuss_ICON_TYPE_RULE; - specs[m + 3].fg = black; - specs[m + 3].bg = wuss_NO_BACKGROUND; - - specs[m + 4].bbox = (box_t) BOX_POS_SIZE(28, 588, 160, 16); - specs[m + 4].type = wuss_ICON_TYPE_MENU_ENTRY; - specs[m + 4].text = "Quit"; - specs[m + 4].fg = black; - specs[m + 4].bg = wuss_NO_BACKGROUND; - specs[m + 4].flags = wuss_ICON_FLAGS_SEPARATOR; - - nspecs = m + 5; - - rc = wuss_icon_create_array(task->window, specs, nspecs, made); + lay.specs = specs; + lay.n = 0; + lay.y = 28; /* icons sit past the ruler gutter -- see ICONS_GUTTER-ish + * axis labels drawn by icons_redraw, which keep the top/ + * left strip to themselves */ + + i_hotspot = -1; + + icons_add_intro(&lay, &i_button, &i_counter); + 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_pattern(&lay); + icons_add_menu(&lay, &i_ticked); + + rc = wuss_icon_create_array(task->window, specs, lay.n, made); if (rc != result_OK) goto failure; - task->button = made[1]; - task->counter = made[2]; - task->opt = made[g + 6]; - task->state = made[g + 7]; - if (task->has_sprite) - task->hotspot = made[g + 9]; - wuss_icon_set_selected(made[m + 1], 1); /* "Show grid" starts ticked */ + task->button = made[i_button]; + task->counter = made[i_counter]; + task->opt = made[i_opt]; + task->state = made[i_state]; + if (i_hotspot >= 0) + task->hotspot = made[i_hotspot]; + wuss_icon_set_selected(made[i_ticked], 1); /* "Show grid" starts ticked */ /* fully built: from here a last-window close reaps the task and its * wuss_EVENT_QUIT frees task_data */ diff --git a/libraries/wuss/test/tasks/icons.h b/libraries/wuss/test/tasks/icons.h index 8c793a12..775a2d2d 100644 --- a/libraries/wuss/test/tasks/icons.h +++ b/libraries/wuss/test/tasks/icons.h @@ -11,9 +11,10 @@ #include "wuss/icon.h" #include "wuss/window.h" -/* demonstrates wuss-managed work-area icons: a couple of labels, a button that - * bumps a counter, and a second button placed far down the document to show - * icons scroll with the content and stay clickable */ +/* demonstrates wuss-managed work-area icons: labelled groups covering every + * icon type (buttons, radios/options, bitmaps, a pattern swatch, a menu-entry + * strip) plus a button placed far down the document to show icons scroll with + * the content and stay clickable */ typedef struct icons_task { wuss_window_t *window; From a941be85415742121aadac8971e618bced1398ae Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Fri, 4 Sep 2026 17:54:44 +0100 Subject: [PATCH 266/287] fix(wuss): drop unused bmfont_get_info out-params, pass NULL create.c, icon/draw.c and bmtext/draw.c queried font_width via bmfont_get_info but never used it, needing a NOT_USED() escape hatch. Pass NULL for outputs callers don't need instead. --- include/framebuf/bmfont.h | 4 ++-- libraries/text/bmtext/draw.c | 4 ++-- libraries/wuss/core/create.c | 4 +--- libraries/wuss/icon/draw.c | 6 ++---- 4 files changed, 7 insertions(+), 11 deletions(-) diff --git a/include/framebuf/bmfont.h b/include/framebuf/bmfont.h index 487ff76c..1153533c 100644 --- a/include/framebuf/bmfont.h +++ b/include/framebuf/bmfont.h @@ -65,8 +65,8 @@ void bmfont_destroy(bmfont_t *bmfont); * Read the width and height of the specified bitmap font. * * \param[in] bmfont Bitmap font to query. - * \param[out] width Width of the font in pixels. - * \param[out] height Height of the font in pixels. + * \param[out] width Width of the font in pixels, or NULL if not wanted. + * \param[out] height Height of the font in pixels, or NULL if not wanted. */ void bmfont_get_info(bmfont_t *bmfont, int *width, int *height); diff --git a/libraries/text/bmtext/draw.c b/libraries/text/bmtext/draw.c index 1f508354..d64a8de7 100644 --- a/libraries/text/bmtext/draw.c +++ b/libraries/text/bmtext/draw.c @@ -17,11 +17,11 @@ void bmtext_draw(bmfont_t *font, int leading, point_t origin) { - int font_width, font_height; + int font_height; point_t pos; int i; - bmfont_get_info(font, &font_width, &font_height); + bmfont_get_info(font, NULL, &font_height); pos = origin; for (i = 0; i < nlines; i++) diff --git a/libraries/wuss/core/create.c b/libraries/wuss/core/create.c index 36800256..65f9f7eb 100644 --- a/libraries/wuss/core/create.c +++ b/libraries/wuss/core/create.c @@ -70,7 +70,6 @@ result_t wuss_create(screen_t *scr, #endif #ifdef WUSS_FURNITURE int font_height; - int font_width; #endif assert(scr != NULL); @@ -196,8 +195,7 @@ result_t wuss_create(screen_t *scr, { /* titles draw in the bold weight when one was supplied; size the * titlebar to whichever weight is taller */ - bmfont_get_info(font, &font_width, &font_height); - NOT_USED(font_width); + bmfont_get_info(font, NULL, &font_height); if (nfonts > 1 && fonts[1].font != NULL) { int titleh; diff --git a/libraries/wuss/icon/draw.c b/libraries/wuss/icon/draw.c index 2ba24992..24ece368 100644 --- a/libraries/wuss/icon/draw.c +++ b/libraries/wuss/icon/draw.c @@ -496,7 +496,6 @@ void wuss__icon_draw(wuss_t *wuss, point_t scroll) { icon_draw_ctx_t c; - int font_width; int fontidx; if (icon->flags & wuss_ICON_FLAGS_HIDDEN) @@ -520,10 +519,9 @@ void wuss__icon_draw(wuss_t *wuss, c.have_font = (c.font != NULL && icon->text[0] != '\0'); if (c.have_font) - bmfont_get_info(c.font, &font_width, &c.font_height); + bmfont_get_info(c.font, NULL, &c.font_height); else - font_width = c.font_height = 0; - NOT_USED(font_width); + c.font_height = 0; switch (icon->type) { From 3d65dca9e662e887df1a9087d20720aae48725e5 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Fri, 4 Sep 2026 18:07:22 +0100 Subject: [PATCH 267/287] refactor: use MIN/MAX/CLAMP macros for hand-rolled clamps Replace open-coded floor/ceiling ifs and min/max ternaries across wuss, framebuf, geom, datastruct, text and utils with the MIN/MAX/CLAMP macros from base/utils.h. Adds the include where a file didn't already pull it in. Left the vendored libraries/fortify/fortify.c untouched. --- apps/wuss/frontend-riscos.c | 2 +- libraries/databases/tag-db/tag-db.c | 3 +-- libraries/datastruct/atom/new.c | 7 +++---- libraries/datastruct/cache/cache.c | 4 ++-- libraries/datastruct/ntree/max-height.c | 6 +++--- .../framebuf/screen/screen-copy-ninepatch.c | 9 +++++---- libraries/framebuf/screen/screen-draw.c | 5 ++--- .../framebuf/screen/screen-fill-pattern.c | 5 ++--- libraries/geom/line/line.c | 2 +- libraries/geom/packer/packer.c | 3 ++- libraries/text/txtfmt/wrap.c | 8 +++----- libraries/utils/array/grow.c | 5 ++--- libraries/wuss/core/create.c | 3 +-- libraries/wuss/core/impl.h | 15 +++++--------- libraries/wuss/core/mouse-click.c | 6 ++---- libraries/wuss/core/scroll-step.c | 18 +++++------------ libraries/wuss/furniture/scroll-action.c | 9 ++------- libraries/wuss/furniture/scroll-box.c | 8 ++------ libraries/wuss/furniture/toggle-action.c | 6 ++---- libraries/wuss/icon/draw.c | 20 ++++++------------- 20 files changed, 52 insertions(+), 92 deletions(-) diff --git a/apps/wuss/frontend-riscos.c b/apps/wuss/frontend-riscos.c index f5fa332f..3e88712e 100644 --- a/apps/wuss/frontend-riscos.c +++ b/apps/wuss/frontend-riscos.c @@ -64,7 +64,7 @@ struct wuss_frontend static void set_hw_palette(const colour_t *palette, int npalette) { int i; - int n = (npalette < 16) ? npalette : 16; + int n = MIN(npalette, 16); for (i = 0; i < n; i++) { diff --git a/libraries/databases/tag-db/tag-db.c b/libraries/databases/tag-db/tag-db.c index b6ce93ab..95865d17 100644 --- a/libraries/databases/tag-db/tag-db.c +++ b/libraries/databases/tag-db/tag-db.c @@ -461,8 +461,7 @@ result_t tagdb_add(tagdb_t *db, const unsigned char *name, tagdb_tag_t *ptag) void *newarr; n = power2gt(db->c_allocated); - if (n < 8) - n = 8; + n = MAX(n, 8); newarr = realloc(db->counts, n * sizeof(*db->counts)); if (newarr == NULL) diff --git a/libraries/datastruct/atom/new.c b/libraries/datastruct/atom/new.c index 026ebebe..f8a3b23a 100644 --- a/libraries/datastruct/atom/new.c +++ b/libraries/datastruct/atom/new.c @@ -8,6 +8,7 @@ #include "fortify/fortify.h" #endif +#include "base/utils.h" #include "utils/array.h" #include "utils/barith.h" @@ -53,8 +54,7 @@ result_t atom_ensure_loc_space(atom_set_t *s) newallocated = s->l_used + 1; /* start with at least this many entries */ - if (newallocated < LOCPTRMINSZ) - newallocated = LOCPTRMINSZ; + newallocated = MAX(newallocated, LOCPTRMINSZ); /* subtract 1 to make it greater than or equal */ newallocated = power2gt(newallocated - 1); @@ -120,8 +120,7 @@ result_t atom_ensure_blk_space(atom_set_t *s, size_t length) newallocated = s->b_used + 1; /* start with at least this many entries */ - if (newallocated < BLKPTRMINSZ) - newallocated = BLKPTRMINSZ; + newallocated = MAX(newallocated, BLKPTRMINSZ); /* subtract 1 to make it greater than or equal */ newallocated = power2gt(newallocated - 1); diff --git a/libraries/datastruct/cache/cache.c b/libraries/datastruct/cache/cache.c index 0b25c784..d911b8be 100644 --- a/libraries/datastruct/cache/cache.c +++ b/libraries/datastruct/cache/cache.c @@ -7,6 +7,7 @@ #include <stdlib.h> #include <string.h> +#include "base/utils.h" #include "utils/barith.h" #include "datastruct/cache.h" @@ -521,8 +522,7 @@ static void purge(cache_t *c, entry_t *purgeent, int entbin) } /* firstfree points to the lowest block */ - if (free < c->firstfree) - c->firstfree = free; + c->firstfree = MIN(c->firstfree, free); } cache_check(c, 0); diff --git a/libraries/datastruct/ntree/max-height.c b/libraries/datastruct/ntree/max-height.c index 30df91fe..ea5343d1 100644 --- a/libraries/datastruct/ntree/max-height.c +++ b/libraries/datastruct/ntree/max-height.c @@ -1,5 +1,6 @@ /* datastruct/ntree/max-height.c -- n-ary tree */ +#include "base/utils.h" #include "datastruct/ntree.h" #include "impl.h" @@ -18,9 +19,8 @@ int ntree_max_height(ntree_t *t) { int h; - h = ntree_max_height(child); - if (h > max) - max = h; + h = ntree_max_height(child); + max = MAX(max, h); } return max + 1; diff --git a/libraries/framebuf/screen/screen-copy-ninepatch.c b/libraries/framebuf/screen/screen-copy-ninepatch.c index 9e4f24c7..be1c6b76 100644 --- a/libraries/framebuf/screen/screen-copy-ninepatch.c +++ b/libraries/framebuf/screen/screen-copy-ninepatch.c @@ -2,6 +2,7 @@ #include <assert.h> +#include "base/utils.h" #include "geom/box.h" #include "geom/size.h" @@ -103,10 +104,10 @@ result_t screen_copy_ninepatch(screen_t *scr, * them collapse to nothing. */ midx = (dst->x0 + dst->x1) / 2; midy = (dst->y0 + dst->y1) / 2; - lx = dst->x0 + pw; if (lx > midx) lx = midx; - rx = dst->x1 - pw; if (rx < midx) rx = midx; - ty = dst->y0 + ph; if (ty > midy) ty = midy; - by = dst->y1 - ph; if (by < midy) by = midy; + lx = MIN(dst->x0 + pw, midx); + rx = MAX(dst->x1 - pw, midx); + ty = MIN(dst->y0 + ph, midy); + by = MAX(dst->y1 - ph, midy); /* Fold "dst" into the saved clip once, so every tile_area call is bounded by * the destination rectangle as well as the caller's clip. An empty caller diff --git a/libraries/framebuf/screen/screen-draw.c b/libraries/framebuf/screen/screen-draw.c index e45af9d4..4c5e30ac 100644 --- a/libraries/framebuf/screen/screen-draw.c +++ b/libraries/framebuf/screen/screen-draw.c @@ -296,7 +296,7 @@ static void screen_copy_bitmap_32(screen_t *scr, { int chunk, i; - chunk = (remaining > BITMAP_BLIT_CHUNK) ? BITMAP_BLIT_CHUNK : remaining; + chunk = MIN(remaining, BITMAP_BLIT_CHUNK); for (i = 0; i < chunk; i++) { @@ -501,8 +501,7 @@ void screen_draw_dashed_line(screen_t *scr, if (on <= 0) return; - if (off < 0) - off = 0; + off = MAX(off, 0); period = on + off; if (screen_get_clip(scr, &clip_box)) diff --git a/libraries/framebuf/screen/screen-fill-pattern.c b/libraries/framebuf/screen/screen-fill-pattern.c index 34e440f2..6d83bd89 100644 --- a/libraries/framebuf/screen/screen-fill-pattern.c +++ b/libraries/framebuf/screen/screen-fill-pattern.c @@ -3,6 +3,7 @@ #include <assert.h> #include <string.h> +#include "base/utils.h" #include "framebuf/colour.h" #include "framebuf/pattern.h" #include "framebuf/pixelfmt.h" @@ -86,9 +87,7 @@ static void screen_fill_pattern_32(screen_t *scr, col = 0; while (w > 0) { - n = 8 - col; - if (n > w) - n = w; + n = MIN(8 - col, w); memcpy(scrp, run + col, (size_t) n * sizeof(*scrp)); scrp += n; w -= n; diff --git a/libraries/geom/line/line.c b/libraries/geom/line/line.c index 3d925bb7..96370dd3 100644 --- a/libraries/geom/line/line.c +++ b/libraries/geom/line/line.c @@ -128,7 +128,7 @@ int line_clip(const box_t *clip, } else { - oc = oc1 > oc0 ? oc1 : oc0; + oc = MAX(oc1, oc0); w = x1 - x0; h = y1 - y0; diff --git a/libraries/geom/packer/packer.c b/libraries/geom/packer/packer.c index 324919fc..18cf8928 100644 --- a/libraries/geom/packer/packer.c +++ b/libraries/geom/packer/packer.c @@ -11,6 +11,7 @@ #include "base/result.h" #include "base/debug.h" +#include "base/utils.h" #include "geom/box.h" #include "utils/array.h" @@ -496,7 +497,7 @@ result_t packer_release(packer_t *packer, const box_t *area) void packer_set_gutter(packer_t *packer, int gutter) { - packer->gutter = (gutter > 0) ? gutter : 0; + packer->gutter = MAX(gutter, 0); } result_t packer_place_by(packer_t *packer, diff --git a/libraries/text/txtfmt/wrap.c b/libraries/text/txtfmt/wrap.c index 8d64842d..37316673 100644 --- a/libraries/text/txtfmt/wrap.c +++ b/libraries/text/txtfmt/wrap.c @@ -7,6 +7,7 @@ #include "fortify/fortify.h" #endif +#include "base/utils.h" #include "text/txtfmt.h" #include "impl.h" @@ -24,9 +25,7 @@ static result_t emit_line(txtfmt_t *tx, int start, int length) /* doubling strategy */ - n = tx->allocated * 2; - if (n < StartAt) - n = StartAt; + n = MAX(tx->allocated * 2, StartAt); newspans = realloc(tx->spans, sizeof(*tx->spans) * n); if (newspans == NULL) @@ -39,8 +38,7 @@ static result_t emit_line(txtfmt_t *tx, int start, int length) tx->spans[i].start = start; tx->spans[i].length = length; - if (length > tx->wrapped_width) - tx->wrapped_width = length; + tx->wrapped_width = MAX(tx->wrapped_width, length); tx->nspans = ++i; diff --git a/libraries/utils/array/grow.c b/libraries/utils/array/grow.c index 5dbb51a6..f49d6771 100644 --- a/libraries/utils/array/grow.c +++ b/libraries/utils/array/grow.c @@ -7,6 +7,7 @@ #include "fortify/fortify.h" #endif +#include "base/utils.h" #include "utils/array.h" #include "utils/barith.h" @@ -29,9 +30,7 @@ int array_grow(void **pblock, assert(minimum > 0); need += used; - - if (need < minimum) - need = minimum; + need = MAX(need, minimum); if (need <= *pallocated) return 0; /* block has enough spare space */ diff --git a/libraries/wuss/core/create.c b/libraries/wuss/core/create.c index 65f9f7eb..b5ed44b5 100644 --- a/libraries/wuss/core/create.c +++ b/libraries/wuss/core/create.c @@ -201,8 +201,7 @@ result_t wuss_create(screen_t *scr, int titleh; bmfont_get_info(fonts[1].font, NULL, &titleh); - if (titleh > font_height) - font_height = titleh; + font_height = MAX(font_height, titleh); } w->titlebar_height = font_height + 4; } diff --git a/libraries/wuss/core/impl.h b/libraries/wuss/core/impl.h index a82d3196..25c09495 100644 --- a/libraries/wuss/core/impl.h +++ b/libraries/wuss/core/impl.h @@ -331,8 +331,7 @@ static inline int wuss__array_grow(const wuss_alloc_t *a, void *grown; need += used; - if (need < minimum) - need = minimum; + need = MAX(need, minimum); if (need <= *allocated) return 0; @@ -652,10 +651,8 @@ static inline void wuss__max_content_on_screen(const wuss_window_t *window, max->h = window->wuss->scr->size.h - window->visible.y0 - 2 * outline_px - titlebar_height - carve.y; - if (max->w < WUSS_MIN_CONTENT) - max->w = WUSS_MIN_CONTENT; - if (max->h < WUSS_MIN_CONTENT) - max->h = WUSS_MIN_CONTENT; + max->w = MAX(max->w, WUSS_MIN_CONTENT); + max->h = MAX(max->h, WUSS_MIN_CONTENT); } /* Largest content width/height whose visible box fits the screen when the @@ -679,10 +676,8 @@ static inline void wuss__max_content_anywhere_on_screen(const wuss_window_t *win max->h = window->wuss->scr->size.h - 2 * outline_px - titlebar_height - carve.y; - if (max->w < WUSS_MIN_CONTENT) - max->w = WUSS_MIN_CONTENT; - if (max->h < WUSS_MIN_CONTENT) - max->h = WUSS_MIN_CONTENT; + max->w = MAX(max->w, WUSS_MIN_CONTENT); + max->h = MAX(max->h, WUSS_MIN_CONTENT); } #endif /* IMPL_H */ diff --git a/libraries/wuss/core/mouse-click.c b/libraries/wuss/core/mouse-click.c index 1b01b382..108c1307 100644 --- a/libraries/wuss/core/mouse-click.c +++ b/libraries/wuss/core/mouse-click.c @@ -210,8 +210,7 @@ result_t wuss_mouse_click(wuss_t *wuss, { wuss__vscroll_sausage_box(win, &sausage); page = (content.y1 - content.y0) - WUSS_SCROLL_STEP; - if (page < 1) - page = 1; + page = MAX(page, 1); if (y < sausage.y0) wuss__scroll_step(win, POINT(0, -page)); else if (y > sausage.y1) @@ -221,8 +220,7 @@ result_t wuss_mouse_click(wuss_t *wuss, { wuss__hscroll_sausage_box(win, &sausage); page = (content.x1 - content.x0) - WUSS_SCROLL_STEP; - if (page < 1) - page = 1; + page = MAX(page, 1); if (x < sausage.x0) wuss__scroll_step(win, POINT(-page, 0)); else if (x > sausage.x1) diff --git a/libraries/wuss/core/scroll-step.c b/libraries/wuss/core/scroll-step.c index 5267b006..23b653b4 100644 --- a/libraries/wuss/core/scroll-step.c +++ b/libraries/wuss/core/scroll-step.c @@ -11,19 +11,11 @@ point_t wuss__scroll_clamp(const wuss_window_t *window, point_t desired) max_x = window->doc.w - (content.x1 - content.x0); max_y = window->doc.h - (content.y1 - content.y0); - if (max_x < 0) - max_x = 0; - if (max_y < 0) - max_y = 0; - - if (desired.x < 0) - desired.x = 0; - else if (desired.x > max_x) - desired.x = max_x; - if (desired.y < 0) - desired.y = 0; - else if (desired.y > max_y) - desired.y = max_y; + max_x = MAX(max_x, 0); + max_y = MAX(max_y, 0); + + desired.x = CLAMP(desired.x, 0, max_x); + desired.y = CLAMP(desired.y, 0, max_y); return desired; } diff --git a/libraries/wuss/furniture/scroll-action.c b/libraries/wuss/furniture/scroll-action.c index 1a53254d..be92019a 100644 --- a/libraries/wuss/furniture/scroll-action.c +++ b/libraries/wuss/furniture/scroll-action.c @@ -30,19 +30,14 @@ void wuss__furniture_drag_sausage(wuss_window_t *window, end_gap = (well_px > 2 * WUSS_SCROLL_END_GAP + WUSS_MIN_SAUSAGE) ? WUSS_SCROLL_END_GAP : 0; track_px = well_px - 2 * end_gap; - max_scroll = doc_size - content_size; - if (max_scroll < 0) - max_scroll = 0; + max_scroll = MAX(doc_size - content_size, 0); if (track_px <= 0 || doc_size <= content_size) new_scroll = 0; else new_scroll = scroll_start + delta_px * doc_size / track_px; - if (new_scroll < 0) - new_scroll = 0; - else if (new_scroll > max_scroll) - new_scroll = max_scroll; + new_scroll = CLAMP(new_scroll, 0, max_scroll); if (horizontal) wuss_window_set_scroll(window, POINT(new_scroll, window->scroll.y)); diff --git a/libraries/wuss/furniture/scroll-box.c b/libraries/wuss/furniture/scroll-box.c index 57c51dd5..42aaf39b 100644 --- a/libraries/wuss/furniture/scroll-box.c +++ b/libraries/wuss/furniture/scroll-box.c @@ -130,14 +130,10 @@ static void scroll_sausage(const wuss_window_t *window, end_gap = (well_px > 2 * WUSS_SCROLL_END_GAP + WUSS_MIN_SAUSAGE) ? WUSS_SCROLL_END_GAP : 0; track_px = well_px - 2 * end_gap; - if (doc_size < content_size) - doc_size = content_size; + doc_size = MAX(doc_size, content_size); sausage_px = (doc_size > 0) ? track_px * content_size / doc_size : track_px; - if (sausage_px < WUSS_MIN_SAUSAGE) - sausage_px = WUSS_MIN_SAUSAGE; - if (sausage_px > track_px) - sausage_px = track_px; + sausage_px = CLAMP(sausage_px, WUSS_MIN_SAUSAGE, track_px); scroll = horizontal ? window->scroll.x : window->scroll.y; diff --git a/libraries/wuss/furniture/toggle-action.c b/libraries/wuss/furniture/toggle-action.c index cc0e1100..d7ee6572 100644 --- a/libraries/wuss/furniture/toggle-action.c +++ b/libraries/wuss/furniture/toggle-action.c @@ -55,10 +55,8 @@ void wuss__furniture_toggle_size(wuss_window_t *window) x0 = window->wuss->scr->size.w - vis_w; if (y0 + vis_h > window->wuss->scr->size.h) y0 = window->wuss->scr->size.h - vis_h; - if (x0 < 0) - x0 = 0; - if (y0 < 0) - y0 = 0; + x0 = MAX(x0, 0); + y0 = MAX(y0, 0); window->pre_toggle = window->visible; diff --git a/libraries/wuss/icon/draw.c b/libraries/wuss/icon/draw.c index 24ece368..c37850ff 100644 --- a/libraries/wuss/icon/draw.c +++ b/libraries/wuss/icon/draw.c @@ -148,9 +148,7 @@ static void wuss__icon_draw_label(const icon_draw_ctx_t *c) if (!c->have_font) return; - interior_w = (b->x1 - b->x0) - 2; - if (interior_w < 1) - interior_w = 1; + interior_w = MAX((b->x1 - b->x0) - 2, 1); bmfont_measure(c->font, icon->text, (int) strlen(icon->text), interior_w, &split_point, &width); @@ -261,9 +259,7 @@ static void wuss__icon_draw_button(const icon_draw_ctx_t *c) int interior_w, split_point; bmfont_width_t width; - interior_w = (b->x1 - b->x0) - 2; - if (interior_w < 1) - interior_w = 1; + interior_w = MAX((b->x1 - b->x0) - 2, 1); bmfont_measure(c->font, icon->text, (int) strlen(icon->text), interior_w, &split_point, &width); @@ -295,9 +291,7 @@ static void wuss__icon_draw_radio_option(const icon_draw_ctx_t *c) box_t g; int gsz, gy, tx; - gsz = (c->font_height >= 8) ? c->font_height : 8; - if (gsz > b->y1 - b->y0) - gsz = b->y1 - b->y0; + gsz = CLAMP(c->font_height, 8, b->y1 - b->y0); gy = b->y0 + (b->y1 - b->y0 - gsz) / 2; g.x0 = b->x0; @@ -347,9 +341,7 @@ static void wuss__icon_draw_radio_option(const icon_draw_ctx_t *c) bmfont_width_t width; tx = g.x1 + 4; - interior_w = (b->x1 - tx) - 1; - if (interior_w < 1) - interior_w = 1; + interior_w = MAX((b->x1 - tx) - 1, 1); bmfont_measure(c->font, icon->text, (int) strlen(icon->text), interior_w, &split_point, &width); @@ -417,7 +409,7 @@ static void wuss__icon_draw_menu_entry(const icon_draw_ctx_t *c) { int cx, cy, h; - h = (c->font_height >= 8) ? c->font_height : 8; + h = MAX(c->font_height, 8); cx = b->x0 + pad; cy = b->y0 + (b->y1 - b->y0 - h) / 2; screen_fill_rect(c->scr, cx, cy, SIZE2D(h, h), @@ -429,7 +421,7 @@ static void wuss__icon_draw_menu_entry(const icon_draw_ctx_t *c) int cx, cy, h; point_t centre; - h = (c->font_height >= 8) ? c->font_height : 8; + h = MAX(c->font_height, 8); cx = b->x0 + pad; cy = b->y0 + (b->y1 - b->y0 - h) / 2; centre.x = cx + h / 2; From 457011cc4e8d6b1daeac474c228e9d13323a79b1 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Fri, 4 Sep 2026 18:26:34 +0100 Subject: [PATCH 268/287] chore(bmfont): Improve Digits-Regular 's' --- resources/bmfonts/Digits-Regular.png | Bin 894 -> 890 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/resources/bmfonts/Digits-Regular.png b/resources/bmfonts/Digits-Regular.png index 3daeda586b4c56fba059dd8128b41b95393f0cdd..ecad722f5afcd4f7ae6c3196f2ac49898a034f47 100755 GIT binary patch delta 769 zcmV+c1OEK}2Kok&g@52lL_t(Y$E}q;ZsRZzg~!&aG3<pGpe~UrTexRo_Y6S7RiIO( zmVsPErJz%6faCxRtl*OfTN&IpL&=o<vyOo%&GQWTWi*^eQC&Wiv-*_?qMp;Vq?#Uf zvwB*yC}+DwQYA-r5LFbrX?08y{h}2uHdy@*F{g>9Kcr}`6Ms%UA_CFy8k1Oh)0tVA ziQ|ZtRJX>m%5>^vwOR|vVB5EKVj}2CXOybRhCZ}1BAy^5^`;*wB}4*9A+#pWbs+rz z5oi@azqP`)B#odCY+vXeBq7&V%b?UwY)mm|u4I2^QqzGzH0T4?a720(8liuhBM6Nc zfE<HtqOkuFuYU%C9tDu$syPxt?|P+<pwk`+I<!VWOq5htks2M%j|jr4aoOC+zDL{$ zX9ggtDD((x$otmI^Vj=a#2$b0Y<-t(d{eYr{BDc3IONDT{!p~M;ZVq$y^j^03v)dc z1djL^A^$}Wki)6(L^-R1sOxcR7ew7fD&D>e72(u$%75eU2s2_KFf53mwdlHAgp{mt z5Hi_X$?@3<pKL}wC{BGCb~$V<Aoa9S$5RhBl*Iy?Tj|m2evtVLNcSLp6@+&jfiR+H zpfI)48h0I(MNq>H)hD3r<kegN*$9#zvoi-3q5X7sk0AJ}382ODG6>_Hw>ie&`6h4K z7taL@;eQV=-WB`jmpvl6Z}WD4*un{I;S2BbXLzOE9&+(dKae+k@JaD?7O968`Q8bd zl@JvTEmECZ<XF+PuEgSLe~sfBWYa5dw!Yy0tM@@hWQQQRqD+59+yT;TpU$)mP81-v zsOpcuGcDiHBJ9|-@nTAsqd79Np9ku~WQQ6@UV4k5<#Ob;8z3_Vp~efKP4~@;AbSc* zYy>qd^wn3R|5k*Lhlv)E$gtU-$Db_%U*z~EI9Ozlj{_`%&qc6^<24?JXa*L^+ie`j z=G-Ewi1hT-&Dp7&8y4AK82`Ng<P9E_HhTU69NfW^Ps9<#00000NkvXXu0mjfwiI)A delta 773 zcmV+g1N!{>2L1++g@5EpL_t(Y$E}q;ZsRZzg~!&aG3<pGpe~UrTexQt_Y6S7RiIO( zmVsPErJz%6faCxRtl*OfTN&IpLrIkUvrd30&GQWTg_@a1QC;4Zi~5BKqF&Oxq?+#b zi+WzOC>Q%gQYA<B5LFcWc{QYne$j?jTdaPASkg?>A5yf`34f>V5rJrUjY%xM>ddUn z%yC3Zs#{}OWjb}TT0IEJVB5Ec#6-}e&L~y0Exl`HL_9)B>Q&!UN{9rILg;}w*Mac= zC!kFL{niTGk~D(ev3;d`kc3=+S_Y+dW@Cy$OC|d=lbQ|$qCp+7h9gp=Pze3g3?LL@ z1ab_riNgMSynh-5Y7{_*PtB1Cdea*<fKF>9=ujE~F;P<8iPWSuKOzWS<70Cn`x<c} zoEd@SM4?7lL*BPup1<DbA`bYIXXm?Q<C~)0;dfiK#W6>|@yDX&4aY*(9DF>{xiHsL zL7>G$g!~shK#r%r6Xl``qOPZ@eGqjQsd)Q7oCsahDSwZ@Bg}|}z_1{K(xU2a6;iUs zLC9olCGE2_?rcUqC{BGCc4@X2kb2yzq3gkhvRFV%D>d4D9%McO(mhCD2iYkIBYFl3 zQ!Ax$S3#)J0BX3P`UI4nyqXIj8$nWIlVsx)q5Sme8bR<?lL~0Hz6`>6=WUMhcfQG6 z_Qg{HLx1?=vv<Ye>G^<2?%TXQ9CvU+Tlm7e{0UxZcgI}((+}hgAAC@Joki-gMZR}} z79~VQV~bSh78y=7uPd>5+F#?i2HEtAi>)uX|LT2^5!o?Ft|-%A5uX5Qwohl;1|0>6 zEvowVcc$eVdc~%-Q`5$aDP2$I$jE*ks0))#Gk(76Cti!7^?Ks98z4Ic;fxnR+wPkc zLG~1cS2NLT5c=w?(SJ{b)|hA!iHwWwdHmTT@I{Vqf`dg4_&C5K_*?{wI9}snk!EC( zyxqldY|br`ibzkpZqB-HZdhb@Vf^#{lQ(*Bw8`@irn15CQ~e$~00000NkvXXu0mjf Dk*9fT From 045ea2a968255590eb35158a3483fb9d0ac1f3d6 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Fri, 4 Sep 2026 18:43:00 +0100 Subject: [PATCH 269/287] feat(wuss): show glyph index above glyph in chars task grid 16x16 grid instead of 32x8, cell shows index in system font stacked above the glyph. Window now resizable/scrollable and placed with a 64,64 offset instead of pinned at origin. --- libraries/wuss/test/tasks/chars.c | 54 ++++++++++++++++++++----------- libraries/wuss/test/tasks/chars.h | 2 +- 2 files changed, 36 insertions(+), 20 deletions(-) diff --git a/libraries/wuss/test/tasks/chars.c b/libraries/wuss/test/tasks/chars.c index 0594a350..1db82b14 100644 --- a/libraries/wuss/test/tasks/chars.c +++ b/libraries/wuss/test/tasks/chars.c @@ -2,6 +2,7 @@ #ifdef WUSS_APP +#include <stdio.h> #include <stdlib.h> #include <string.h> @@ -20,9 +21,9 @@ #include "chars.h" -#define CHARS_COLS 32 -#define CHARS_ROWS 8 -#define CHARS_PAD 2 +#define CHARS_COLS 16 +#define CHARS_ROWS 16 +#define CHARS_PAD 1 /* keep the resources root the task was created with; the picker loads a font * on demand by leafname and chars_create does not stash it elsewhere. @@ -31,14 +32,17 @@ static char chars_resources[256]; /* ----------------------------------------------------------------------- */ -/* content size for the grid at the given font's cell metrics */ -static size2d_t chars_window_size(bmfont_t *font) +/* content size for the grid at the given font's cell metrics. Each cell + * stacks the index (drawn in the wuss system font) above the glyph. */ +static size2d_t chars_window_size(chars_task_t *task, bmfont_t *font) { - int fw, fh; + int fw, fh, ifw, ifh, cell_w, cell_h; bmfont_get_info(font, &fw, &fh); - return SIZE2D((fw + CHARS_PAD * 2) * CHARS_COLS, - (fh + CHARS_PAD * 2) * CHARS_ROWS); + bmfont_get_info(wuss_get_font(task->wuss), &ifw, &ifh); + cell_w = MAX(fw, ifw * 3) + CHARS_PAD * 2; + cell_h = ifh + fh + CHARS_PAD * 3; + return SIZE2D(cell_w * CHARS_COLS, cell_h * CHARS_ROWS); } /* load fonts[idx] if not already in hand; returns it or NULL on failure. @@ -82,7 +86,7 @@ static result_t chars_set_font(chars_task_t *task, int idx, const char *name) task->font = font; task->current = idx; - wuss_window_resize(task->window, chars_window_size(font)); + wuss_window_resize(task->window, chars_window_size(task, font)); wuss_window_invalidate_all(task->window); return result_OK; } @@ -125,6 +129,7 @@ result_t chars_create(wuss_t *wuss, task->current = -1; /* the wuss system font is none of the picker's */ task->menu_handle = NULL; task->fg = colour_rgb(0x00, 0x00, 0x00); + task->mg = colour_rgb(0xBB, 0xBB, 0xBB); task->bg = colour_rgb(0xFF, 0xFF, 0xFF); /* the picker: every ".png" font under resources/bmfonts, sorted, less any @@ -164,15 +169,12 @@ result_t chars_create(wuss_t *wuss, task->delegate = delegate; rc = wuss_window_create_placed(delegate, - chars_window_size(font), + chars_window_size(task, font), "Chars", - wuss_WINDOW_NO_RESIZE | - wuss_WINDOW_NO_TOGGLE_SIZE | - wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), - chars_window_size(font), - SIZE2D(0, 0), + chars_window_size(task, font), + SIZE2D(64, 64), &task->window); if (rc != result_OK) wuss_task_destroy(delegate); /* unregister; its QUIT frees the task block */ @@ -184,8 +186,10 @@ static result_t chars_redraw(const wuss_event_t *event, void *task_data) { chars_task_t *cc; screen_t *scr; + bmfont_t *sysfont; const box_t *bounds; - int font_width, font_height, cell_w, cell_h; + int font_width, font_height, sysfont_width, sysfont_height; + int cell_w, cell_h; int first, count; int i, sx, sy; @@ -196,9 +200,12 @@ static result_t chars_redraw(const wuss_event_t *event, void *task_data) sx = event->data.redraw.scroll.x; sy = event->data.redraw.scroll.y; + sysfont = wuss_get_font(cc->wuss); + bmfont_get_info(cc->font, &font_width, &font_height); - cell_w = font_width + CHARS_PAD * 2; - cell_h = font_height + CHARS_PAD * 2; + bmfont_get_info(sysfont, &sysfont_width, &sysfont_height); + cell_w = MAX(font_width, sysfont_width * 3) + CHARS_PAD * 2; + cell_h = sysfont_height + font_height + CHARS_PAD * 3; first = ' '; /* bmfont glyphs are laid out contiguously starting here */ count = bmfont_get_count(cc->font); @@ -212,6 +219,7 @@ static result_t chars_redraw(const wuss_event_t *event, void *task_data) { int col, row, x, y; char ch; + char label[4]; point_t pos; col = i % CHARS_COLS; @@ -220,13 +228,21 @@ static result_t chars_redraw(const wuss_event_t *event, void *task_data) y = bounds->y0 - sy + row * cell_h; screen_fill_rect(scr, x, y, SIZE2D(cell_w, cell_h), cc->bg); + screen_draw_line(scr, x, y, x + cell_w - 1, y, cc->mg); + screen_draw_line(scr, x, y, x, y + cell_h - 1, cc->mg); + + snprintf(label, 4, "%d", i); + pos.x = x + CHARS_PAD; + pos.y = y + CHARS_PAD; + bmfont_draw(sysfont, scr, label, (int) strlen(label), cc->mg, cc->bg, + &pos, NULL); if (i < first || i >= first + count) continue; /* no glyph for this byte value: leave the cell blank */ ch = (char) i; pos.x = x + CHARS_PAD; - pos.y = y + CHARS_PAD; + pos.y = y + CHARS_PAD * 2 + sysfont_height; bmfont_draw(cc->font, scr, &ch, 1, cc->fg, cc->bg, &pos, NULL); } diff --git a/libraries/wuss/test/tasks/chars.h b/libraries/wuss/test/tasks/chars.h index 014d5cff..9fa940fa 100644 --- a/libraries/wuss/test/tasks/chars.h +++ b/libraries/wuss/test/tasks/chars.h @@ -29,7 +29,7 @@ typedef struct chars_task bmfont_t **fonts; /* one slot per menu item, lazily loaded */ int nfonts; /* length of fonts[]; == menu item count */ int current; /* index into fonts[], or -1 for sysfont */ - colour_t fg, bg; + colour_t fg, mg, bg; } chars_task_t; From 09af33c27e8055d22b869d9b4dca831da8452907 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Fri, 4 Sep 2026 19:06:49 +0100 Subject: [PATCH 270/287] fix(wuss): fix UAF when menu-pick flash finish re-enters and closes chain wuss__menu_flash_finish's MENU_SELECT delivery can re-enter and close the whole chain even for a keep_open (ADJUST) pick, e.g. when the owner's handler calls wuss_menu_close(). The old guard only bailed out for the non-keep_open case, which frees the chain itself; the re-entrant client-triggered free was not covered, so a fast ADJUST re-pick could dereference the freed self->icons[index] afterwards. Replace the prev_keep_open check with a liveness check against the live chain (wuss__menu_node_for), matching the existing pattern used by the IDLE flash-step handler. --- libraries/wuss/menu/menu.c | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/libraries/wuss/menu/menu.c b/libraries/wuss/menu/menu.c index e935e704..cd76cc5e 100644 --- a/libraries/wuss/menu/menu.c +++ b/libraries/wuss/menu/menu.c @@ -305,10 +305,14 @@ static result_t wuss__menu_handle(wuss_window_t *window, if (self->flash.frames > 0) { - int prev_keep_open = self->flash.keep_open; - wuss__menu_flash_finish(self); - if (!prev_keep_open) + + /* the finish's own SELECT tears the chain down when it wasn't a + * keep_open pick, and even a keep_open pick's MENU_SELECT delivery + * may re-enter and close the chain from the client side (e.g. the + * owner calling wuss_menu_close()) -- either way `self` is gone the + * moment the chain no longer starts at it */ + if (wuss__menu_node_for(wuss, window) != self) return result_OK; /* `self` and the chain are gone */ } From 69b2c179eb78067d2cbaefc77ed1916598481c1c Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Fri, 4 Sep 2026 19:07:34 +0100 Subject: [PATCH 271/287] fix(wuss): close owned menu chain in wuss_task_destroy wuss_task_destroy freed the task without checking whether the live menu chain (or an in-flight pick flash) still recorded it as owner. A later row pick or flash completion would then call wuss__deliver on the freed task through that dangling owner pointer. wuss_destroy already closes menu_chain before freeing tasks during whole-manager teardown; add the equivalent check to the single-task path, closing the chain first when this task is its owner. --- libraries/wuss/core/task/destroy.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/libraries/wuss/core/task/destroy.c b/libraries/wuss/core/task/destroy.c index 8d53eb84..74c7760f 100644 --- a/libraries/wuss/core/task/destroy.c +++ b/libraries/wuss/core/task/destroy.c @@ -25,6 +25,15 @@ void wuss_task_destroy(wuss_task_t *doomed) * fire QUIT and free the node from under us. */ 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 (wuss->menu_chain != NULL && wuss->menu_chain->owner == doomed) + wuss_menu_close(wuss->menu_chain); +#endif + /* One QUIT while every window is still alive. */ event.kind = wuss_EVENT_QUIT; (void) wuss__deliver(doomed, NULL, &event); From 75cc9d91069ced37fceccc82125ba355e276b3f8 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Fri, 4 Sep 2026 19:08:19 +0100 Subject: [PATCH 272/287] fix(wuss): deliver pending MENU_SELECT when a flashing menu is torn down early wuss__menu_close_from froze every chain node unconditionally without finishing an in-flight pick flash first, so a click outside the menu (or another wuss_menu_open/close) landing during the highlight flash silently dropped the MENU_SELECT the flash was standing in for. Deliver the flash's captured MENU_SELECT directly here instead of going through wuss__menu_flash_finish, whose own chain teardown would race the caller's (the chain is already unlinked from wuss->menu_chain by the time close_from runs). --- libraries/wuss/menu/menu.c | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/libraries/wuss/menu/menu.c b/libraries/wuss/menu/menu.c index cd76cc5e..dfe375c2 100644 --- a/libraries/wuss/menu/menu.c +++ b/libraries/wuss/menu/menu.c @@ -103,6 +103,23 @@ static void wuss__menu_close_from(struct wuss__menu *node) wuss__menu_close_from(node->child); node->child = NULL; + /* A pending pick flash still owes its owner a MENU_SELECT; the chain is + * about to be freed out from under it (the caller unlinks it from + * wuss->menu_chain first), so delivering the notification here -- rather + * than via wuss__menu_flash_finish, whose own teardown would race this + * one -- is the only chance the pick has of reaching its owner. */ + if (node->flash.frames > 0) + { + wuss_event_t sel; + + node->flash.frames = 0; + sel.kind = wuss_EVENT_MENU_SELECT; + sel.data.menu_select.menu = node->flash.menu; + sel.data.menu_select.index = node->flash.index; + sel.data.menu_select.button = node->flash.button; + (void) wuss__deliver(node->flash.owner, NULL, &sel); + } + { wuss_t *w; From 43a3b3b923bfc4740e6e171ddab25f92e7e3e47a Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Fri, 4 Sep 2026 19:08:58 +0100 Subject: [PATCH 273/287] fix(wuss): restore borrowed-window position when its PRE_SHOW vetoes a menu row wuss__menu_open_window moved the window to the submenu anchor before checking wuss_window_set_hidden's PRE_SHOW veto result. On veto only the menu node was freed; the window itself was left permanently repositioned at the anchor even though it never became visible, so its next legitimate show rendered in the wrong place. Capture the window's prior position first and restore it on veto. --- libraries/wuss/menu/menu.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/libraries/wuss/menu/menu.c b/libraries/wuss/menu/menu.c index dfe375c2..f5256697 100644 --- a/libraries/wuss/menu/menu.c +++ b/libraries/wuss/menu/menu.c @@ -162,8 +162,10 @@ static result_t wuss__menu_open_window(struct wuss__menu *self, int index) struct wuss__menu *node; wuss_window_t *win; point_t at; + point_t prev; - win = self->menu->items[index].window; + win = self->menu->items[index].window; + prev = POINT(win->visible.x0, win->visible.y0); node = wuss__malloc(self->wuss, sizeof(*node)); if (node == NULL) @@ -184,7 +186,8 @@ static result_t wuss__menu_open_window(struct wuss__menu *self, int index) wuss_window_move(win, at); if (wuss_window_set_hidden(win, 0) != result_OK) { - wuss__free(self->wuss, node); /* vetoed: row stays unopened */ + wuss_window_move(win, prev); /* vetoed: undo the anchor move */ + wuss__free(self->wuss, node); /* row stays unopened */ return result_OK; } wuss_window_restack(win, wuss_ZORDER_FRONT); From b431d656f96b4bb569502d72f82c955aa90a4927 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Fri, 4 Sep 2026 19:10:23 +0100 Subject: [PATCH 274/287] fix(wuss): deliver QUIT to registered tasks in wuss_destroy wuss_destroy freed each leftover task's block directly, skipping the QUIT event. Per the task_data-ownership contract in include/wuss/task.h a task's client-owned allocations are freed only from its QUIT handler, so a still-registered task (e.g. a menu-spawned demo window not yet closed) leaked its task_data on whole-manager teardown. Deliver QUIT before freeing each task block; windows are already gone by this point in the sweep, so QUIT handlers see the same view they would from wuss_task_destroy. Add wuss__task_from_link alongside the existing wuss__window_from_link, naming the cast rather than doing it inline. --- libraries/wuss/core/destroy.c | 11 +++++++---- libraries/wuss/core/impl.h | 6 ++++++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/libraries/wuss/core/destroy.c b/libraries/wuss/core/destroy.c index 140676c7..b8582de6 100644 --- a/libraries/wuss/core/destroy.c +++ b/libraries/wuss/core/destroy.c @@ -34,15 +34,18 @@ void wuss_destroy(wuss_t *doomed) e = next; } - /* Windows are gone; free any tasks the caller left registered. No QUIT -- - * wuss_destroy is the whole-manager teardown, past the point of - * notification. */ + /* 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; + 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; } diff --git a/libraries/wuss/core/impl.h b/libraries/wuss/core/impl.h index 25c09495..33ea2100 100644 --- a/libraries/wuss/core/impl.h +++ b/libraries/wuss/core/impl.h @@ -443,6 +443,12 @@ int wuss__blit_pieces(wuss_window_t *window, #define wuss__window_from_link(node) \ ((wuss_window_t *) (void *) (node)) +/* Recover the owning wuss_task_t from a node in the task chain (task->link). + * link is the first member, so this is just a cast -- but naming it keeps + * every task walk honest about depending on that. */ +#define wuss__task_from_link(node) \ + ((wuss_task_t *) (void *) (node)) + /* The single dispatch chokepoint. Delivers "ev" to "task"'s handle, passing * "win_or_null" as the window (NULL for task-view events). No-op (returns * result_OK) when task is NULL or has no handle. A debug build also asserts From d89736d51966a69923e5d7beadde80a6f12477eb Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Fri, 4 Sep 2026 19:11:14 +0100 Subject: [PATCH 275/287] fix(wuss): bounds-check bmfont advance-width pixel reads before they happen extract_advance_widths's end-of-row bounds check ran after the row's pixel reads, including the per-character refill inside the inner loop still marked with a TODO. A font PNG whose width isn't an exact multiple of CHARS_PER_ROW produces a truncating gridwidth, so the inner loop can iterate past the row's -- or the buffer's -- last valid word before any check catches it. Add an explicit end-of-buffer pointer and check it at both read sites, before the read rather than after. --- libraries/framebuf/bmfont/bmfont.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/libraries/framebuf/bmfont/bmfont.c b/libraries/framebuf/bmfont/bmfont.c index 3260910e..7a16ce51 100644 --- a/libraries/framebuf/bmfont/bmfont.c +++ b/libraries/framebuf/bmfont/bmfont.c @@ -169,6 +169,7 @@ static result_t extract_advance_widths(bmfont_t *bmfont, result_t rc = result_OK; unsigned char adwtab[256]; unsigned int *pixels; + unsigned int *pixels_end; int bitsperchar; unsigned int mask; png_uint_32 y; @@ -182,6 +183,7 @@ static result_t extract_advance_widths(bmfont_t *bmfont, build_adw_tab(adwtab, PIXEL_WIDTH_IDX); pixels = (unsigned int *) voidpixels; + pixels_end = pixels + (rowbytes * imgheight) / sizeof(*pixels); bitsperchar = bmfont->charwidth * 2; /* 2 because 2bpp */ mask = 0xFFFFFFFFu << (32 - bitsperchar); @@ -198,6 +200,8 @@ static result_t extract_advance_widths(bmfont_t *bmfont, /* maintain two words, a current and a pending so we've always got enough * bits ready */ + if (pixels >= pixels_end) + return result_PARSE_ERROR; /* malformed grid: row starts past the buffer */ currbits = rev_l(*pixels++); ncurrbits = 32; /* bits available in currbits */ nextbits = 0; @@ -211,7 +215,9 @@ static result_t extract_advance_widths(bmfont_t *bmfont, if (nnextbits == 0) /* refill if needed */ { - nextbits = rev_l(*pixels++); // TODO: Check for end of buffer + if (pixels >= pixels_end) + return result_PARSE_ERROR; /* malformed grid: row overruns the buffer */ + nextbits = rev_l(*pixels++); nnextbits = 32; } From 8335f3e0eb61c671a446b021fe19d56171c43d90 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Fri, 4 Sep 2026 19:12:03 +0100 Subject: [PATCH 276/287] fix(screen): no-op screen_fill_hline for an unregistered pixel format in release scr->span is NULL when screen_init's pixelfmt_t has no span-registry entry; the assert guarding the fill dispatch is compiled out under NDEBUG, so a release build crashed on the NULL dereference instead of getting the old hand-rolled switch's safe do-nothing default. screen_fill_rect calls screen_fill_hline per row, so this covers it too. --- libraries/framebuf/screen/screen-fill-hline.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/libraries/framebuf/screen/screen-fill-hline.c b/libraries/framebuf/screen/screen-fill-hline.c index 0f3c0282..45403dac 100644 --- a/libraries/framebuf/screen/screen-fill-hline.c +++ b/libraries/framebuf/screen/screen-fill-hline.c @@ -38,8 +38,12 @@ void screen_fill_hline(screen_t *scr, int x, int y, int w, colour_t colour) colour, scr->format); /* the per-format run fill lives in the span table; "first" lets it address - * an odd P4 nibble so this needn't pack */ + * an odd P4 nibble so this needn't pack. scr->span is NULL for a pixelfmt_t + * 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) + return; rowp = (unsigned char *) scr->base + draw_box.y0 * scr->rowbytes; scr->span->fill(rowp, draw_box.x0, fmt, clipped_width); From 118293c3444db607dab3388edce0d6a6efb65ed5 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Fri, 4 Sep 2026 19:13:20 +0100 Subject: [PATCH 277/287] fix(wuss): tie font-slot array sizes to a named, bounds-checked constant fonts[3]/descs[3] and the font-loading loop bound were three separate hardcoded literal 3s, unrelated to wuss_MAX_FONTS (4) and to each other; wuss_create's nfonts-vs-wuss_MAX_FONTS validation runs only after both arrays are already fully populated in the caller, so it can't protect against them. Introduce WUSS_MAIN_NFONTS, used for both array sizes and the loop bound, with a compile-time check against wuss_MAX_FONTS. --- apps/wuss/main.c | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/apps/wuss/main.c b/apps/wuss/main.c index 3adf39e0..14f54546 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -680,6 +680,14 @@ static void wuss_frame(void *arg) #endif } +/* fonts[]/descs[] below hold [0] regular (system font), [1] bold, [2] symbol + * (menu tick / submenu arrow); kept <= wuss_MAX_FONTS so wuss_create's own + * nfonts check is never reached with arrays it can no longer fit in */ +#define WUSS_MAIN_NFONTS 3 +#if WUSS_MAIN_NFONTS > wuss_MAX_FONTS +#error WUSS_MAIN_NFONTS exceeds wuss_MAX_FONTS +#endif + /* click windows to bring to front, drag titlebars to move; the redraw-all * input redraws the whole screen, the pixel-stress input does it one pixel at * a time to catch tasks that misbehave under a 1x1 clip; the palette-cycle @@ -693,9 +701,8 @@ static result_t run_wuss(const char *resources) result_t rc; const char *leafname; const char *filename; - bmfont_t *fonts[3]; /* [0] regular (system font), [1] bold, - * [2] symbol (menu tick / submenu arrow) */ - static const char *const names[3] = + bmfont_t *fonts[WUSS_MAIN_NFONTS]; + static const char *const names[WUSS_MAIN_NFONTS] = { "Digits-Regular", "Digits-Bold", "Symbols" }; int nfonts; int i; @@ -723,7 +730,7 @@ static result_t run_wuss(const char *resources) { nfonts = 0; - for (i = 0; i < 3; i++) + for (i = 0; i < WUSS_MAIN_NFONTS; i++) { leafname = path_join_leafname(names[i], "png"); filename = path_join_filename(resources, 3, "resources", "bmfonts", @@ -758,9 +765,9 @@ static result_t run_wuss(const char *resources) { wuss_config_t config; - wuss_font_desc_t descs[3]; /* slot classes/names for the picker menus -- - * [2] Symbols is chrome-only, never a text - * font choice */ + wuss_font_desc_t descs[WUSS_MAIN_NFONTS]; /* slot classes/names for the + * picker menus -- [2] Symbols is chrome-only, + * never a text font choice */ fill_chrome_config(&config, use_wimp16 ? 1 : 0); From de873bd303fa4e2959cf5741e3e376be6de73955 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Fri, 4 Sep 2026 19:13:35 +0100 Subject: [PATCH 278/287] docs(bmfont): fix broken Digits Font image link after PNG rename resources/bmfonts/digits.png was renamed/split into Digits-Regular.png and Digits-Bold.png; the doc's example image still pointed at the old deleted path. --- docs/framebuf/bmfont.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/framebuf/bmfont.md b/docs/framebuf/bmfont.md index 2a9b94b3..025686f7 100644 --- a/docs/framebuf/bmfont.md +++ b/docs/framebuf/bmfont.md @@ -6,7 +6,7 @@ or this: -![Digits Font](../resources/bmfonts/digits.png) +![Digits Font](../resources/bmfonts/Digits-Regular.png) or even this: From ac237eb9706064f8e40beb02a9095ed985a1eeda Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Fri, 4 Sep 2026 19:13:51 +0100 Subject: [PATCH 279/287] chore(wuss): fix stale file header after get-font.c moved into core/ The header comment still named the old libraries/wuss/get-font.c path with a generic blurb; other files moved into core/ in the same diff got their headers updated, this one didn't. --- libraries/wuss/core/get-font.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/wuss/core/get-font.c b/libraries/wuss/core/get-font.c index 2cb0d874..8e2f75ac 100644 --- a/libraries/wuss/core/get-font.c +++ b/libraries/wuss/core/get-font.c @@ -1,4 +1,4 @@ -/* wuss/get-font.c -- wuss - minimal window manager */ +/* wuss/core/get-font.c -- look up a registered font slot by index */ #include <assert.h> From 5827b2f814fb3168c2e4bad09707f25cbd5c35da Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Fri, 4 Sep 2026 19:17:12 +0100 Subject: [PATCH 280/287] ci(pages): deploy wasm build on develop pushes too Pages workflow only triggered on master; add develop so the emscripten demo deploys on develop branch pushes as well. --- .github/workflows/pages.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 404fbbac..47ed7cd2 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -5,7 +5,7 @@ name: Pages on: push: - branches: [master] + branches: [master, develop] workflow_dispatch: # Allow one concurrent deployment; let a running one finish. From dc1fabe7f7f7361c3711bb5425ecfbfb42864447 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Fri, 4 Sep 2026 19:21:34 +0100 Subject: [PATCH 281/287] docs: changelog work since 64015c9 Covers the Emscripten/wasm build and Pages deploy, shared fontmenu/colourmenu components, menu/icon polish, and the wuss_create font-array and fontmenu allocator breaking changes, plus the QUIT-delivery, menu UAF, bmfont bounds and screen_fill_hline fixes landed since. --- CHANGELOG.md | 148 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 148 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 177d0d45..0f14481d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,64 @@ _Unreleased_ until one is cut. ### Added +- An Emscripten/WebAssembly build of the interactive `wuss` demo. The + run-loop is extracted into `wuss_frame(void *)` over a `wuss_frame_ctx`, + driven by `emscripten_set_main_loop_arg` in the browser while desktop and + RISC OS still spin it directly. CMake's `EMSCRIPTEN` branch uses the + bundled `-sUSE_LIBPNG=1` port instead of `find_package(PNG)`, links + Emscripten's SDL3 port (`-sUSE_SDL=3`), and preloads `resources/` into + `wuss.data` mounted at MEMFS `/resources`; the target emits `wuss.html`. + A GitHub Actions Pages workflow builds and deploys the demo on pushes to + `master` and `develop`. +- `bmfont_enumerate()` — non-recursive scan of a directory's `*.png` files, + reporting each as a (name, path) pair (name is the leaf with `.png` + stripped) to a caller-supplied callback; the callback may return + `result_STOP_WALK` to stop early. +- `wuss/component/fontmenu.h` and `wuss/component/colourmenu.h` — shared + task components (the RISC OS Toolbox analogue), gated on a new + `WUSS_COMPONENTS` CMake option (implies `WUSS_MENUS`). `wuss_fontmenu_create()` + builds a flat, name-sorted `wuss_menu_t` from the bitmap fonts in a + directory (via `bmfont_enumerate()`); `wuss_fontmenu_selected()` recovers + the picked font name from a `wuss_EVENT_MENU_SELECT`, and + `wuss_fontmenu_set_ticked(fm, index)` mutates the component's own items to + tick one row (index -1 clears every tick). `wuss_colourmenu_create()` + builds a swatch-row menu, one row per system-palette entry labelled + `#RRGGBB`, and resolves a pick back to the palette index. Both components + route every allocation through the caller's `wuss_alloc_t` hooks (copied + into the handle, not a borrowed `wuss_t *`, so they can safely outlive + `wuss_destroy()`). +- `wuss_menu_item_t` / icon rows gain an optional colour chip + (`wuss_MENU_ITEM_SWATCH` / `wuss_ICON_FLAGS_SWATCH` plus a `wuss_colour_t + swatch` field), drawn in the left gutter where the tick sits; a chip wins + over a selected tick. +- Font slot 2 (`WUSS_SYMBOL_FONT`) is now consulted to draw the menu + selection tick and submenu arrow as glyphs (`WUSS_GLYPH_TICK` `'*'` / + `WUSS_GLYPH_SUBMENU` `'>'`) instead of vector strokes, when a font is + present in that slot. +- `wuss_create()`'s `fonts` array elements are now tagged with a + `wuss_font_class_t` and a borrowed leafname (`wuss_font_desc_t`), letting + a picker such as `wuss_fontmenu` and the system-decoration font (menu + ticks/arrows) be identified by class rather than array position. + `wuss_FONT_CLASS_SYSTEM` marks a chrome/decoration-only slot. +- `wuss_icon_plot()` — validates an icon spec exactly as `wuss_icon_create()` + does and draws it once through the window manager's screen, retaining + nothing; for static content a task can redraw from its own model without + 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 + 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 + overlap, RISC OS style. +- `bitmap_fill_pattern()` (see _Changed_ for the rename from + `bitmap_draw_pattern`) and `screen_fill_pattern()` now share a single + `pattern_t { bits, fg, bg, flags, origin }` describing an 8x8 tile, + built via `pattern_from_preset()` (the Bayer ramp and named tiles) or + `pattern_from_mask()` (stencil) in the new `framebuf/pattern` module. +- `screen_fill_hline()` — fills one clipped horizontal run; the per-row + primitive `screen_fill_rect()` and `screen_draw_circle()`'s scanline fill + now loop over. - Symbolic `wuss_colour_t` values (`wuss/wuss.h`). A raw `wuss_colour_t` is a palette index, `0..127`; `wuss_COLOUR_SYMBOLIC` (128) and up name roles the window manager resolves to a concrete index. `wuss_COLOUR_BLACK`, @@ -132,6 +190,45 @@ _Unreleased_ until one is cut. ### Changed +- **Breaking:** `wuss_create()`'s `fonts` argument is now + `const wuss_font_desc_t *` (handle, `wuss_font_class_t`, borrowed + leafname) instead of a bare `bmfont_t *const *` array; up to + `wuss_MAX_FONTS` (4) slots are stored, slot 0 remaining the system font + used for titlebars and any icon that does not select another. + `wuss_get_font_n(wuss, index)` reads a given slot; `wuss_get_font` stays + slot 0. An icon's flags gain a two-bit font-select field + (`wuss_ICON_FLAGS_FONT_MASK`, bits 9-10) with `wuss_ICON_FONT(n)` / + `wuss_ICON_FONT_OF(f)` helpers, and titlebars are drawn in font slot 1 + (the bold weight) when one is supplied, else slot 0. +- **Breaking:** `wuss_fontmenu_create()` takes a fourth argument, + `const wuss_alloc_t *alloc` (NULL selects `wuss_alloc`), routing every + block the handle keeps through those hooks instead of libc + malloc/calloc/strdup/free, matching `wuss_colourmenu` and `wuss_create`. +- **Breaking:** `bitmap_draw_pattern()` is renamed `bitmap_fill_pattern()` + and now takes `const pattern_t *` and returns `result_t`. + `screen_draw_bitmap()` is renamed `screen_copy_bitmap()` and + `screen_draw_ninepatch()` renamed `screen_copy_ninepatch()` (its + `screen_NINEPATCH_NO_CENTRE` flag is unchanged) — `copy` is now the verb + for all pixel transfer, matching `screen_copy_rect()`. `screen_fill_pattern()` + now takes `const pattern_t *` instead of a preset enum. Hard renames, no + compatibility wrappers. +- `screen_copy_bitmap()`, `screen_copy_ninepatch()` and `screen_copy_rect()` + now all return `result_t` (`result_OK` / `result_NOT_SUPPORTED`) instead + of `void` / `int`; `screen_copy_rect()` also returns + `result_NOT_SUPPORTED` when clipping leaves nothing to copy, so a caller + must fall back to a full redraw in that case. +- Resizing a window (`wuss_window_resize()`) no longer clamps the result to + the visible on-screen strip when the window's top-left is off-screen — + the requested content size is applied verbatim and the window may + overhang the screen edge, the same latitude toggle-size and drag already + allow. `wuss_window_create()` keeps its own screen cap. +- Toggle-size now grows a window to fill the whole screen (capped per axis + at the window's document extent, or uncapped when that extent is 0), + nudging the top-left toward the origin by the minimum needed to fit; it + previously pinned the top-left and only grew the bottom-right corner, so + a window not already near the origin could never fill the screen and a + zero document extent toggled to nothing. Restore returns the exact + pre-toggle box, position included. - **Breaking:** `wuss_create()` takes a `const wuss_alloc_t *alloc` argument after `config`; pass NULL for the stdlib allocator. - **Breaking:** `wuss_config_t::palette` is renamed `furniture`, and its type @@ -200,6 +297,57 @@ _Unreleased_ until one is cut. ### Fixed +- `wuss_destroy()` now delivers `wuss_EVENT_QUIT` to each still-registered + task before freeing it, instead of freeing the task block directly. Per + the `task_data`-ownership contract a task's client-owned allocations are + freed only from its `QUIT` handler, so a still-registered task (e.g. a + menu-spawned demo window not yet closed) previously leaked `task_data` + on whole-manager teardown. +- `wuss_task_destroy()` now closes a live menu chain it owns before freeing + the task, matching `wuss_destroy()`'s whole-manager teardown; previously + a later row pick or in-flight pick-flash completion could call into the + freed task through a dangling chain-owner pointer. +- A click outside a menu (or another `wuss_menu_open()`/`wuss_menu_close()`) + landing during a pick's highlight flash no longer silently drops the + `wuss_EVENT_MENU_SELECT` the flash was standing in for. +- A borrowed window opened by hovering a menu row (`wuss_menu_item_t::window`) + no longer sticks at the submenu anchor position when its + `wuss_EVENT_PRE_SHOW` vetoes the reveal — its prior position is restored. +- A submenu now opens only while the pointer is in the row's arrow gutter, + not anywhere on the row, and closes on any re-entry rather than staying + open for every subsequent mouse move over that row. +- An ADJUST pick that keeps a menu chain open now re-ticks the picked row + in place; previously the tick was only applied at `wuss_menu_open()` time + so an ADJUST-kept-open menu's tick state went stale. +- A window's furniture (titlebar, scrollbars, outline) is now clipped to + its unoccluded pieces when redrawing its own dirty region; previously + only the content redraw was occlusion-clipped, so a partly-covered + window repainted furniture pixels straight over the window on top. +- Bounds-checked `extract_advance_widths()` pixel reads in `bmfont` before + they happen, rather than after: a font PNG whose width is not an exact + multiple of the glyphs-per-row count could previously run the inner loop + past the row's — or the buffer's — last valid word before any check + caught it. +- `bmfont`'s grid-size detector now decodes the image and verifies each + candidate cell height against the advance-width strip's pixel markers, + rather than taking the first divisor of the image height at or above the + grid width. The old heuristic could miss a valid cell height below the + grid width and, when the grid width did not divide the image height + cleanly, could latch onto a spurious large divisor and overflow the + pixel buffer in `bmfont_draw()`. +- `bmfont` rejects a malformed font grid with `result_PARSE_ERROR` instead + of asserting, when `extract_advance_widths()`'s pixel cursor would walk + out of the decoded image (previously two `assert`s, compiled out under + `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. +- `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 + independent hardcoded literals that could silently drift apart. - Opening a menu from a task's mouse-down handler no longer picks the menu's first row on the matching mouse-up: that release is now swallowed. - A menu chain is now closed before its `on_select` callback runs, so a From 94b221e336ca4cc94d65a7fb6416be783c556ded Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Fri, 4 Sep 2026 19:42:36 +0100 Subject: [PATCH 282/287] fix(bmfont): fix RISC OS build failing on undefined dirent references SharedCLibrary (-mlibscl) has no opendir/readdir/closedir, so bmfont_enumerate() failed to link on RISC OS. Add a TARGET_RISCOS branch using OSLib's xosgbpb_dir_entries instead, and actually define TARGET_RISCOS as a compiler macro (it was only a CMake-scope variable before, so the #ifdef was dead code). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- CMakeLists.txt | 1 + libraries/framebuf/bmfont/enumerate.c | 83 ++++++++++++++++++++++++++- 2 files changed, 83 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 1917c3e2..9deba578 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -488,6 +488,7 @@ if(TARGET_RISCOS) # OSLib (& anything else in GCCSDK) target_include_directories(DPTLib PUBLIC $ENV{GCCSDK_INSTALL_ENV}/include) target_link_libraries(DPTLib PUBLIC $ENV{GCCSDK_INSTALL_ENV}/lib/libOSLib32.a) + target_compile_definitions(DPTLib PUBLIC TARGET_RISCOS) endif() if(USE_FORTIFY) diff --git a/libraries/framebuf/bmfont/enumerate.c b/libraries/framebuf/bmfont/enumerate.c index e930ee64..068d3915 100644 --- a/libraries/framebuf/bmfont/enumerate.c +++ b/libraries/framebuf/bmfont/enumerate.c @@ -1,6 +1,5 @@ /* framebuf/bmfont/enumerate.c -- list the bitmap fonts in a directory */ -#include <dirent.h> #include <stddef.h> #include <stdio.h> #include <string.h> @@ -13,6 +12,86 @@ #define BMFONT_EXT ".png" #define BMFONT_EXT_LEN 4 +#ifdef TARGET_RISCOS + +/* SharedCLibrary (-mlibscl) has no <dirent.h> support, so on RISC OS we + * enumerate via OSLib's OS_GBPB 9 wrapper instead of opendir/readdir. */ + +#include "oslib/osgbpb.h" +#include "oslib/os.h" + +result_t bmfont_enumerate(const char *dir, + bmfont_enumerate_fn *fn, + void *opaque) +{ + result_t rc; + os_error *err; + int context; + int read; + char buffer[256]; + const char *leaf; + size_t leaflen; + char name[256]; + char path[512]; + + if (dir == NULL || fn == NULL) + return result_NULL_ARG; + + rc = result_OK; + context = 0; + + for (;;) + { + err = xosgbpb_dir_entries(dir, + (osgbpb_string_list *) buffer, + 1, + context, + sizeof(buffer), + "*", + &read, + &context); + if (err != NULL) + { + rc = result_FILE_NOT_FOUND; + break; + } + + if (read > 0) + { + leaf = buffer; + leaflen = strlen(leaf); + + if (leaflen > BMFONT_EXT_LEN && + leaflen - BMFONT_EXT_LEN < sizeof(name) && + strcmp(leaf + leaflen - BMFONT_EXT_LEN, BMFONT_EXT) == 0) + { + memcpy(name, leaf, leaflen - BMFONT_EXT_LEN); + name[leaflen - BMFONT_EXT_LEN] = '\0'; + + snprintf(path, sizeof(path), "%s.%s", dir, leaf); + + rc = fn(name, path, opaque); + if (rc == result_STOP_WALK) + { + rc = result_OK; + break; + } + if (rc != result_OK) + break; + } + } + + if (context == -1) + break; + } + + return rc; +} + +#else /* !TARGET_RISCOS */ + +#include <dirent.h> + result_t bmfont_enumerate(const char *dir, bmfont_enumerate_fn *fn, void *opaque) @@ -63,3 +142,5 @@ result_t bmfont_enumerate(const char *dir, return rc; } + +#endif /* TARGET_RISCOS */ From c4c2762045d526a41272220e3e32f21225e3fd76 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Fri, 4 Sep 2026 19:45:23 +0100 Subject: [PATCH 283/287] test(bmfont): fix bmfont_enumerate_test missing Symbols.png fixture The fixture dir gained Symbols.png but the test's hardcoded MAXFONTS stayed at 8, so bmfont_enumerate_test failed. Symbols can't just join bmfonts[] though: the clipping/layout tests draw Latin lorem_ipsum through every font in that list, and Symbols' small glyph table overflows on ASCII input. Split into bmfonts[] (8, Latin-drawing tests) and a separate bmfonts_enum_extra[] checked only by the enumerate test. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- libraries/framebuf/bmfont/test/bmfont-test.c | 28 ++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/libraries/framebuf/bmfont/test/bmfont-test.c b/libraries/framebuf/bmfont/test/bmfont-test.c index 52da3255..e937c354 100644 --- a/libraries/framebuf/bmfont/test/bmfont-test.c +++ b/libraries/framebuf/bmfont/test/bmfont-test.c @@ -197,6 +197,16 @@ 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 + +static const char *bmfonts_enum_extra[MAXFONTS_ENUM - MAXFONTS] = +{ + "Symbols" +}; + /* ----------------------------------------------------------------------- */ static const char lorem_ipsum[] = @@ -721,6 +731,7 @@ static result_t bmfont_interactive_test(bmfontteststate_t *state) typedef struct bmfont_enum_check { int found[MAXFONTS]; /* parallel to bmfonts[]; set when that name is seen */ + int found_extra[MAXFONTS_ENUM - MAXFONTS]; /* parallel to bmfonts_enum_extra[] */ int total; /* every callback, including unrecognised names */ int stop_after; /* >0: return result_STOP_WALK once total reaches it */ } @@ -742,6 +753,10 @@ static result_t bmfont_enum_cb(const char *name, if (strcmp(name, bmfonts[i].filename) == 0) chk->found[i] = 1; + for (i = 0; i < MAXFONTS_ENUM - MAXFONTS; i++) + if (strcmp(name, bmfonts_enum_extra[i]) == 0) + chk->found_extra[i] = 1; + if (chk->stop_after > 0 && chk->total >= chk->stop_after) return result_STOP_WALK; @@ -765,12 +780,21 @@ static result_t bmfont_enumerate_test(const char *resources) fprintf(stderr, "bmfont_enumerate: unexpected rc %x\n", rc); return result_TEST_FAILED; } - if (chk.total != MAXFONTS) + if (chk.total != MAXFONTS_ENUM) { fprintf(stderr, "bmfont_enumerate: saw %d entries, expected %d\n", - chk.total, MAXFONTS); + chk.total, MAXFONTS_ENUM); return result_TEST_FAILED; } + for (i = 0; i < MAXFONTS_ENUM - MAXFONTS; i++) + { + if (!chk.found_extra[i]) + { + fprintf(stderr, "bmfont_enumerate: missing font %s\n", + bmfonts_enum_extra[i]); + return result_TEST_FAILED; + } + } for (i = 0; i < MAXFONTS; i++) { if (!chk.found[i]) From 5487df82c9301606018930d5d2cf0bd1904ca1be Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Fri, 4 Sep 2026 19:49:34 +0100 Subject: [PATCH 284/287] fix(bmfonts): correct case of tracked fixture filenames git's index had daydream/gliderrider/henry/tiny.png in lowercase while the working files on disk are Daydream/GliderRider/Henry/Tiny.png. macOS's case-insensitive filesystem hid the mismatch locally, but a case-sensitive Linux checkout (e.g. the Ubuntu release build) sees the lowercase names, bmfont_enumerate's names don't match wuss-test's expected font names, and the fontmenu check fails. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- resources/bmfonts/{daydream.png => Daydream.png} | Bin .../bmfonts/{gliderrider.png => GliderRider.png} | Bin resources/bmfonts/{henry.png => Henry.png} | Bin resources/bmfonts/{tiny.png => Tiny.png} | Bin 4 files changed, 0 insertions(+), 0 deletions(-) rename resources/bmfonts/{daydream.png => Daydream.png} (100%) rename resources/bmfonts/{gliderrider.png => GliderRider.png} (100%) rename resources/bmfonts/{henry.png => Henry.png} (100%) rename resources/bmfonts/{tiny.png => Tiny.png} (100%) diff --git a/resources/bmfonts/daydream.png b/resources/bmfonts/Daydream.png similarity index 100% rename from resources/bmfonts/daydream.png rename to resources/bmfonts/Daydream.png diff --git a/resources/bmfonts/gliderrider.png b/resources/bmfonts/GliderRider.png similarity index 100% rename from resources/bmfonts/gliderrider.png rename to resources/bmfonts/GliderRider.png diff --git a/resources/bmfonts/henry.png b/resources/bmfonts/Henry.png similarity index 100% rename from resources/bmfonts/henry.png rename to resources/bmfonts/Henry.png diff --git a/resources/bmfonts/tiny.png b/resources/bmfonts/Tiny.png similarity index 100% rename from resources/bmfonts/tiny.png rename to resources/bmfonts/Tiny.png From 3053939840fffd4e5ba26e27853eb949fc13c67a Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Fri, 4 Sep 2026 19:52:34 +0100 Subject: [PATCH 285/287] fix(wuss): fix stale lowercase font filename causing test segfault wuss_test still referenced the fixture as "tiny.png"; after the bmfonts case-fix commit the file is Tiny.png, so on case-sensitive filesystems bmfont_create fails and left 'font' uninitialised. The FlashFail unwind path then called bmfont_destroy on that garbage pointer -- segfault on Linux CI, masked on macOS's case-insensitive filesystem. Also init font = NULL at declaration so any future load failure hits a safe no-op free instead of relying on every call site remembering to set it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- libraries/wuss/test/wuss-test.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 26f005c5..a2921dfb 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -4109,7 +4109,7 @@ ColourMenuOK: ; }; const char *fontfile; - bmfont_t *font; + bmfont_t *font = NULL; wuss_font_desc_t fdesc; screen_t fscr; bitmap_t fbm; @@ -4123,7 +4123,7 @@ ColourMenuOK: ; /* a menu needs a font for its row metrics; the core wuss above was made * without one */ fontfile = path_join_filename(resources, 3, "resources", "bmfonts", - path_join_leafname("tiny", "png")); + path_join_leafname("Tiny", "png")); rc = bmfont_create(fontfile, &font); if (rc != result_OK) { From 754bc5c266836aa3b7b226f9244b450b420b9d88 Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Fri, 4 Sep 2026 19:58:43 +0100 Subject: [PATCH 286/287] fix(bmfont): add MSVC Win32 enumerate path MSVC has no dirent.h. Enumerate via FindFirstFileA/FindNextFileA instead of opendir/readdir on that toolchain; POSIX and RISC OS paths unchanged. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- libraries/framebuf/bmfont/enumerate.c | 64 ++++++++++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/libraries/framebuf/bmfont/enumerate.c b/libraries/framebuf/bmfont/enumerate.c index 068d3915..dd34f3a2 100644 --- a/libraries/framebuf/bmfont/enumerate.c +++ b/libraries/framebuf/bmfont/enumerate.c @@ -88,7 +88,69 @@ result_t bmfont_enumerate(const char *dir, return rc; } -#else /* !TARGET_RISCOS */ +#elif defined(_MSC_VER) /* !TARGET_RISCOS */ + +/* MSVC has no <dirent.h>; enumerate via the Win32 FindFirstFile family + * instead of opendir/readdir. */ + +#include <windows.h> + +result_t bmfont_enumerate(const char *dir, + bmfont_enumerate_fn *fn, + void *opaque) +{ + result_t rc; + WIN32_FIND_DATAA fd; + HANDLE h; + char pattern[512]; + const char *leaf; + size_t leaflen; + char name[256]; + char path[512]; + + if (dir == NULL || fn == NULL) + return result_NULL_ARG; + + snprintf(pattern, sizeof(pattern), "%s\\*", dir); + + h = FindFirstFileA(pattern, &fd); + if (h == INVALID_HANDLE_VALUE) + return result_FILE_NOT_FOUND; + + rc = result_OK; + + do + { + leaf = fd.cFileName; + leaflen = strlen(leaf); + + if (leaflen <= BMFONT_EXT_LEN || leaflen - BMFONT_EXT_LEN >= sizeof(name)) + continue; + if (strcmp(leaf + leaflen - BMFONT_EXT_LEN, BMFONT_EXT) != 0) + continue; + + memcpy(name, leaf, leaflen - BMFONT_EXT_LEN); + name[leaflen - BMFONT_EXT_LEN] = '\0'; + + snprintf(path, sizeof(path), "%s/%s", dir, leaf); + + rc = fn(name, path, opaque); + if (rc == result_STOP_WALK) + { + rc = result_OK; + break; + } + if (rc != result_OK) + break; + } + while (FindNextFileA(h, &fd)); + + FindClose(h); + + return rc; +} + +#else /* !TARGET_RISCOS, !_MSC_VER */ #include <dirent.h> From 24f8bd22e2267e9fd03192db2da615ed04f2836b Mon Sep 17 00:00:00 2001 From: David Thomas <dave@davespace.co.uk> Date: Fri, 4 Sep 2026 20:04:24 +0100 Subject: [PATCH 287/287] fix(wuss): drop redundant compound-literal cast in test wuss_BACKDROP_COLOUR already casts its compound literal to wuss_backdrop_t; casting the macro result again gave MSVC C2440 (cannot convert type to itself). GCC/Clang tolerated it, MSVC did not. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- libraries/wuss/test/wuss-test.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index a2921dfb..c19152b1 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -427,7 +427,7 @@ result_t wuss_test(const char *resources) memset(&symcfg, 0, sizeof(symcfg)); symcfg.furniture.title.bg = wuss_COLOUR_BLUE; /* -> index 2 */ symcfg.furniture.title.fg = wuss_COLOUR_WHITE; /* -> index 3 */ - symcfg.backdrop = (wuss_backdrop_t) wuss_BACKDROP_COLOUR(wuss_COLOUR_GREEN); /* -> 1 */ + symcfg.backdrop = wuss_BACKDROP_COLOUR(wuss_COLOUR_GREEN); /* -> 1 */ rc = wuss_create(&scr, NULL, 0, sympal, 5, &symcfg, NULL, &symw); if (rc != result_OK)