From 076c25a6419b2f8a70aa9f0f7a8132835178681d Mon Sep 17 00:00:00 2001 From: David Thomas Date: Thu, 27 Aug 2026 00:48:48 +0100 Subject: [PATCH 01/48] 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 02/48] 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 03/48] 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 04/48] 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 05/48] 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 06/48] 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 07/48] 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 08/48] 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 09/48] 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 10/48] 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 11/48] 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 12/48] 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 13/48] 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 14/48] 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 15/48] 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 16/48] 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 17/48] 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 18/48] 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 19/48] 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 20/48] 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 21/48] 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 22/48] 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 23/48] 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 24/48] 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 25/48] 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 26/48] 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 27/48] 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 28/48] 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 29/48] 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 30/48] 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 31/48] 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 32/48] 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 33/48] 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 34/48] 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 35/48] 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 36/48] 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 37/48] 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 38/48] 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 39/48] 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 40/48] 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 41/48] 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 42/48] 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 43/48] 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 44/48] 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 45/48] 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 46/48] 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 47/48] 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 48/48] 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; }