From 229da8de924e721f8b06770c0ba78a93c7d7b69e Mon Sep 17 00:00:00 2001 From: David Thomas Date: Fri, 28 Aug 2026 16:22:47 +0100 Subject: [PATCH 01/44] feat(wuss): add an animated Porter-Duff demo task to the wuss test Adds an eleventh launcher task to the interactive wuss test: the two bundled composite demo images (resources/composite/A.png and B.png) composited under a Porter-Duff rule that cycles through all twelve operators, over an alpha checkerboard so transparency reads as transparent rather than white. composite() takes no offset or alpha parameter, so the animation is a per-frame edit on scratch copies: the destination is restored from a pristine B and the source's alpha channel is scaled by a triangle ramp (0 to 255 and back across each rule's turn). Alpha is non-premultiplied, so only the alpha byte is touched. Pacing is a per-idle frame counter, as in ball.c; clicking advances the rule and the wheel adjusts the cycle speed. The bitmap clone/convert helpers are copied from composite-test.c, where they are statics rather than library functions. Widens the launcher window to fit the longer entry name. Co-Authored-By: Claude Opus 5 --- CMakeLists.txt | 1 + libraries/wuss/test/tasks/launcher.c | 2 +- libraries/wuss/test/tasks/porter-duff.c | 415 ++++++++++++++++++++++++ libraries/wuss/test/tasks/porter-duff.h | 50 +++ libraries/wuss/test/wuss-test.c | 7 +- 5 files changed, 473 insertions(+), 2 deletions(-) create mode 100644 libraries/wuss/test/tasks/porter-duff.c create mode 100644 libraries/wuss/test/tasks/porter-duff.h diff --git a/CMakeLists.txt b/CMakeLists.txt index f9b0db89..e50ac931 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -552,6 +552,7 @@ if(BUILD_TESTS) libraries/wuss/test/tasks/image.c libraries/wuss/test/tasks/launcher.c libraries/wuss/test/tasks/palette.c + libraries/wuss/test/tasks/porter-duff.c libraries/wuss/test/tasks/sofa.c libraries/wuss/test/tasks/text.c libraries/wuss/test/wuss-test.c) diff --git a/libraries/wuss/test/tasks/launcher.c b/libraries/wuss/test/tasks/launcher.c index 32188120..1039b726 100644 --- a/libraries/wuss/test/tasks/launcher.c +++ b/libraries/wuss/test/tasks/launcher.c @@ -17,7 +17,7 @@ #define LAUNCHER_ROW_HEIGHT 20 #define LAUNCHER_PAD 4 -#define LAUNCHER_WIDTH 140 +#define LAUNCHER_WIDTH 160 result_t launcher_create(wuss_t *wuss, launcher_entry_t *entries, diff --git a/libraries/wuss/test/tasks/porter-duff.c b/libraries/wuss/test/tasks/porter-duff.c new file mode 100644 index 00000000..1a482abc --- /dev/null +++ b/libraries/wuss/test/tasks/porter-duff.c @@ -0,0 +1,415 @@ +/* porter-duff.c -- wuss test - animated Porter-Duff compositing task */ + +#ifdef USE_SDL + +#include +#include + +#ifdef FORTIFY +#include "fortify/fortify.h" +#endif + +#include "base/utils.h" +#include "framebuf/palettes.h" +#include "framebuf/pixelfmt.h" +#include "geom/box.h" +#include "geom/point.h" +#include "io/path.h" + +#include "porter-duff.h" + +#define PD_SIZE (256) /* the demo images are 256x256 */ +#define PD_LABEL_HEIGHT (20) /* strip below the pane, for the rule name */ +#define PD_CHECKER_BAND (8) /* checkerboard square size, in pixels */ +#define PD_FORMAT (pixelfmt_bgra8888) + +#define PD_FRAMES_DEFAULT (120) /* ~2s per rule at 60 main-loop passes/sec */ +#define PD_FRAMES_MIN (10) +#define PD_FRAMES_MAX (600) + +/* ----------------------------------------------------------------------- */ + +/* These three are lifted from libraries/framebuf/composite/test/composite-test.c, + * where they're static helpers rather than library functions: their memory + * management is raw malloc and the original carries a FIXME saying as much, so + * they're copied rather than promoted to framebuf. */ + +static result_t bitmap_clone_by_size(bitmap_t *cloned, const bitmap_t *src) +{ + size_t pixelbytes; + void *pixels; + + pixelbytes = src->size.h * src->rowbytes; + pixels = malloc(pixelbytes); + if (pixels == NULL) + return result_OOM; + + *cloned = *src; + cloned->base = pixels; + + return result_OK; +} + +static result_t bitmap_clone_pixels(bitmap_t *dst, const bitmap_t *src) +{ + if (dst->size.w != src->size.w || + dst->size.h != src->size.h || + dst->format != src->format || + dst->rowbytes != src->rowbytes) + return result_INCOMPATIBLE; + + memcpy(dst->base, src->base, src->size.h * src->rowbytes); + + return result_OK; +} + +/* Only the arms reachable from bitmap_load_png's output are implemented. */ +static result_t bitmap_convert_inplace(bitmap_t *bm, pixelfmt_t new_fmt) +{ + pixelfmt_any_t *p; + int x, y; + + if (new_fmt != pixelfmt_bgra8888) + return result_NOT_IMPLEMENTED; + + switch (bm->format) + { + case pixelfmt_bgra8888: + return result_OK; + + case pixelfmt_rgbx8888: + p = bm->base; + for (y = 0; y < bm->size.h; y++) + for (x = 0; x < bm->size.w; x++) + { + pixelfmt_rgbx8888_t px = *p; + /* the x byte isn't a real alpha channel, so force it opaque */ + *p++ = PIXELFMT_MAKE_BGRA8888(PIXELFMT_Bxxx8888(px), + PIXELFMT_xGxx8888(px), + PIXELFMT_xxRx8888(px), + 0xFF); + } + bm->format = pixelfmt_bgra8888; + return result_OK; + + case pixelfmt_rgba8888: + p = bm->base; + for (y = 0; y < bm->size.h; y++) + for (x = 0; x < bm->size.w; x++) + { + pixelfmt_rgba8888_t px = *p; + *p++ = PIXELFMT_MAKE_BGRA8888(PIXELFMT_Bxxx8888(px), + PIXELFMT_xGxx8888(px), + PIXELFMT_xxRx8888(px), + PIXELFMT_xxxA8888(px)); + } + bm->format = pixelfmt_bgra8888; + return result_OK; + + default: + return result_NOT_IMPLEMENTED; + } +} + +/* ----------------------------------------------------------------------- */ + +static const char *const rule_names[composite_RULE__LIMIT] = +{ + "CLEAR", + "SRC", + "DST", + "SRC OVER", + "DST OVER", + "SRC IN", + "DST IN", + "SRC OUT", + "DST OUT", + "SRC ATOP", + "DST ATOP", + "XOR" +}; + +static result_t load_demo_png(bitmap_t *bm, const char *resources, + const char *leafname) +{ + const char *leafname_ext; + const char *filename; + result_t rc; + + leafname_ext = path_join_leafname(leafname, "png"); + filename = path_join_filename(resources, 3, + "resources", "composite", leafname_ext); + + rc = bitmap_load_png(bm, filename); + if (rc != result_OK) + return rc; + + rc = bitmap_convert_inplace(bm, PD_FORMAT); + if (rc != result_OK) + { + free(bm->base); + return rc; + } + + return result_OK; +} + +/* ----------------------------------------------------------------------- */ + +result_t porter_duff_create(wuss_t *wuss, + const colour_t *palette, + bmfont_t *font, + const char *resources, + porter_duff_task_t *task) +{ + wuss_task_t delegate; + box_t box; + result_t rc; + + task->font = font; + task->rule = composite_RULE_CLEAR; + task->frame = 0; + task->frames_per_rule = PD_FRAMES_DEFAULT; + task->light = palette[palette_PICO8_LIGHT_GREY]; + task->dark = palette[palette_PICO8_DARK_GREY]; + task->fg = palette[palette_PICO8_WHITE]; + task->bg = palette[palette_PICO8_BLACK]; + + rc = load_demo_png(&task->a, resources, "A"); + if (rc != result_OK) + return rc; + + rc = load_demo_png(&task->b, resources, "B"); + if (rc != result_OK) + goto free_a; + + rc = bitmap_clone_by_size(&task->src, &task->a); + if (rc != result_OK) + goto free_b; + + rc = bitmap_clone_by_size(&task->dst, &task->b); + if (rc != result_OK) + goto free_src; + + delegate = wuss_task_start(porter_duff_handle, task); /* porter_duff_redraw paints every pixel itself */ + box = (box_t) BOX_POS_SIZE(60, 180, PD_SIZE, PD_SIZE + PD_LABEL_HEIGHT); + + rc = wuss_window_create(wuss, + &box, + "Porter-Duff", + wuss_WINDOW_NONE, + wuss_NO_BACKGROUND, + &delegate, + box_size(&box), + &task->window); + if (rc != result_OK) + goto free_dst; + + return result_OK; + +free_dst: + free(task->dst.base); +free_src: + free(task->src.base); +free_b: + free(task->b.base); +free_a: + free(task->a.base); + + return rc; +} + +void porter_duff_destroy(porter_duff_task_t *task) +{ + wuss_window_close(task->window); + free(task->dst.base); + free(task->src.base); + free(task->b.base); + free(task->a.base); +} + +/* ----------------------------------------------------------------------- */ + +/* Triangle ramp: 0 at the start of the rule's turn, 255 at its midpoint, back + * to 0 at its end. */ +static int porter_duff_ramp(const porter_duff_task_t *pd) +{ + int half; + + half = pd->frames_per_rule / 2; + if (half <= 0) + return 255; + + if (pd->frame < half) + return pd->frame * 255 / half; + else + return MAX(0, (pd->frames_per_rule - pd->frame) * 255 / half); +} + +/* Copy "a" into "src", scaling its alpha channel by "ramp" (0..255). The + * library's compositing works on non-premultiplied values, so the colour + * components are left alone. */ +static void porter_duff_ramp_src(porter_duff_task_t *pd, int ramp) +{ + pixelfmt_any_t *sp; + pixelfmt_any_t *dp; + int x, y; + + sp = pd->a.base; + dp = pd->src.base; + + for (y = 0; y < pd->a.size.h; y++) + for (x = 0; x < pd->a.size.w; x++) + { + pixelfmt_any_t px = *sp++; + unsigned int alpha; + + alpha = PIXELFMT_xxxA8888(px) * ramp / 255; + *dp++ = (px & ~PIXELFMT_xxxA8888_MASK) | + ((pixelfmt_any_t) alpha << PIXELFMT_xxxA8888_SHIFT); + } +} + +static void porter_duff_draw_checkerboard(const porter_duff_task_t *pd, + screen_t *scr, + const box_t *content, + const box_t *bounds) +{ + int x, y, lx, ly, band; + + for (y = content->y0; y < content->y1; y++) + for (x = content->x0; x < content->x1; x++) + { + lx = x - bounds->x0; + ly = y - bounds->y0; + band = lx / PD_CHECKER_BAND + ly / PD_CHECKER_BAND; + + screen_draw_pixel(scr, x, y, (band & 1) ? pd->dark : pd->light); + } +} + +static result_t porter_duff_redraw(const wuss_event_t *event, void *task_data) +{ + porter_duff_task_t *pd; + screen_t *scr; + const box_t *content, *bounds; + const char *name; + point_t pos; + result_t rc; + + pd = task_data; + + scr = event->data.redraw.scr; + content = event->data.redraw.content; + bounds = event->data.redraw.bounds; + + porter_duff_draw_checkerboard(pd, scr, content, bounds); + + /* ponytail: the whole 256x256 pane is recomposited on every redraw -- two + * full-image memcpys plus two full-image passes. Fine for one window in a + * test harness; if the main loop ever gets tight, cache the composited + * bitmap and rebuild it only when the ramp value or rule actually changes. */ + rc = bitmap_clone_pixels(&pd->dst, &pd->b); + if (rc != result_OK) + return rc; + + porter_duff_ramp_src(pd, porter_duff_ramp(pd)); + + rc = composite(pd->rule, &pd->src, &pd->dst); + if (rc != result_OK) + return rc; + + screen_draw_bitmap(scr, bounds->x0, bounds->y0, &pd->dst); + + name = rule_names[pd->rule]; + pos.x = bounds->x0 + 2; + pos.y = bounds->y0 + PD_SIZE + 2; + + return bmfont_draw(pd->font, scr, name, (int) strlen(name), + pd->fg, pd->bg, &pos, NULL); +} + +static result_t porter_duff_idle(void *task_data) +{ + porter_duff_task_t *pd; + + pd = task_data; + + if (++pd->frame >= pd->frames_per_rule) + { + pd->frame = 0; + pd->rule = (pd->rule + 1) % composite_RULE__LIMIT; + } + + /* the ramp changes every frame, so the whole pane is stale every frame */ + wuss_window_invalidate_all(pd->window); + + return result_OK; +} + +static result_t porter_duff_mouse(wuss_window_t *window, void *task_data) +{ + porter_duff_task_t *pd; + + pd = task_data; + + pd->rule = (pd->rule + 1) % composite_RULE__LIMIT; + pd->frame = 0; + + wuss_window_invalidate_all(window); + + return result_OK; +} + +static result_t porter_duff_scroll(wuss_window_t *window, int delta, + void *task_data) +{ + porter_duff_task_t *pd; + + pd = task_data; + + pd->frames_per_rule += delta * 10; + pd->frames_per_rule = CLAMP(pd->frames_per_rule, + PD_FRAMES_MIN, PD_FRAMES_MAX); + pd->frame = MIN(pd->frame, pd->frames_per_rule); + + wuss_window_invalidate_all(window); + + return result_OK; +} + +result_t porter_duff_handle(wuss_window_t *window, + const wuss_event_t *event, + void *task_data) +{ + porter_duff_task_t *pd; + + pd = task_data; + + switch (event->kind) + { + case wuss_EVENT_REDRAW: + return porter_duff_redraw(event, task_data); + + case wuss_EVENT_MOUSE: + if (event->data.mouse.action != wuss_MOUSE_DOWN) + return result_OK; + return porter_duff_mouse(window, task_data); + + case wuss_EVENT_SCROLL: + return porter_duff_scroll(window, event->data.scroll.delta, task_data); + + case wuss_EVENT_IDLE: + return porter_duff_idle(task_data); + + case wuss_EVENT_CLOSE: + wuss_window_close(window); + pd->window = NULL; + return result_OK; + + default: + return result_OK; + } +} + +#endif /* USE_SDL */ diff --git a/libraries/wuss/test/tasks/porter-duff.h b/libraries/wuss/test/tasks/porter-duff.h new file mode 100644 index 00000000..d8dbd366 --- /dev/null +++ b/libraries/wuss/test/tasks/porter-duff.h @@ -0,0 +1,50 @@ +/* porter-duff.h -- wuss test - animated Porter-Duff compositing task */ + +#ifndef TASKS_PORTER_DUFF_H +#define TASKS_PORTER_DUFF_H + +#ifdef USE_SDL + +#include "framebuf/bitmap.h" +#include "framebuf/bmfont.h" +#include "framebuf/composite.h" +#include "wuss/window.h" + +/* window's task: the two composite demo images blended under a cycling + * Porter-Duff rule, over an alpha checkerboard. The source image's alpha is + * ramped up and back down across each rule's turn, so every operator is seen + * across its full range */ +typedef struct porter_duff_task +{ + wuss_window_t *window; + bmfont_t *font; + bitmap_t a; /* owned: pristine source, BGRA */ + bitmap_t b; /* owned: pristine destination, BGRA */ + bitmap_t src; /* owned: scratch, rebuilt each frame */ + bitmap_t dst; /* owned: scratch, rebuilt each frame */ + composite_rule_t rule; + int frame; /* frames elapsed in the current rule */ + int frames_per_rule; + colour_t light; /* checkerboard */ + colour_t dark; /* checkerboard */ + colour_t fg; /* rule name label */ + colour_t bg; /* rule name label */ +} +porter_duff_task_t; + +wuss_event_fn_t porter_duff_handle; + +/* load the two demo images and create the window against the given wuss + * instance; resources is the DPTLib repo root, for locating the bundled PNGs */ +result_t porter_duff_create(wuss_t *wuss, + const colour_t *palette, + bmfont_t *font, + const char *resources, + porter_duff_task_t *task); + +/* destroy the window and free the bitmaps allocated by porter_duff_create */ +void porter_duff_destroy(porter_duff_task_t *task); + +#endif /* USE_SDL */ + +#endif /* TASKS_PORTER_DUFF_H */ diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 0a744361..27f54847 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -38,6 +38,7 @@ #include "tasks/image.h" #include "tasks/launcher.h" #include "tasks/palette.h" +#include "tasks/porter-duff.h" #include "tasks/sofa.h" #include "tasks/text.h" @@ -65,6 +66,7 @@ static checker_task_t g_checker_task; static curve_task_t g_curve_task; static sofa_task_t g_sofa_task; static gradient_task_t g_gradient_task; +static porter_duff_task_t g_porter_duff_task; static result_t spawn_ball(void) { return ball_create(g_wuss, g_palette, &g_ball_task); } static result_t spawn_text(void) { return text_create(g_wuss, g_palette, g_daydream_font, &g_text_task); } @@ -76,6 +78,7 @@ static result_t spawn_checker(void) { return checker_create(g_wuss, g_palette, static result_t spawn_curve(void) { return curve_create(g_wuss, g_palette, &g_curve_task); } static result_t spawn_sofa(void) { return sofa_create(g_wuss, g_palette, &g_sofa_task); } static result_t spawn_gradient(void) { return gradient_create(g_wuss, &g_gradient_task); } +static result_t spawn_porter_duff(void) { return porter_duff_create(g_wuss, g_palette, g_daydream_font, g_resources, &g_porter_duff_task); } static void destroy_ball(void) { ball_destroy(&g_ball_task); } static void destroy_text(void) { text_destroy(&g_text_task); } @@ -87,6 +90,7 @@ static void destroy_checker(void) { checker_destroy(&g_checker_task); } static void destroy_curve(void) { curve_destroy(&g_curve_task); } static void destroy_sofa(void) { sofa_destroy(&g_sofa_task); } static void destroy_gradient(void) { gradient_destroy(&g_gradient_task); } +static void destroy_porter_duff(void) { porter_duff_destroy(&g_porter_duff_task); } static launcher_entry_t g_launcher_entries[] = { @@ -99,7 +103,8 @@ static launcher_entry_t g_launcher_entries[] = { "Checker", spawn_checker, destroy_checker, false }, { "Curve", spawn_curve, destroy_curve, false }, { "Sofa", spawn_sofa, destroy_sofa, false }, - { "Gradient", spawn_gradient, destroy_gradient, false } + { "Gradient", spawn_gradient, destroy_gradient, false }, + { "Porter-Duff", spawn_porter_duff, destroy_porter_duff, false } }; static wuss_button_t sdl_button_to_wuss(Uint8 button) From 2e67c4b1c8a666c1f1d432de50a7d8f28ea3efa0 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Fri, 28 Aug 2026 16:52:00 +0100 Subject: [PATCH 02/44] feat(wuss): scroll the opposite way on an Adjust-clicked scroll arrow Adjust-clicking a scroll arrow now steps against the direction the arrow points, so a single arrow can be worked both ways without moving the pointer. Toggle-size remains Select-only. Co-Authored-By: Claude Opus 5 --- libraries/wuss/mouse-click.c | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/libraries/wuss/mouse-click.c b/libraries/wuss/mouse-click.c index 88ff9c58..138640a3 100644 --- a/libraries/wuss/mouse-click.c +++ b/libraries/wuss/mouse-click.c @@ -62,24 +62,34 @@ result_t wuss_mouse_click(wuss_t *wuss, region == wuss_FURNITURE_HSCROLL_LEFT || region == wuss_FURNITURE_HSCROLL_RIGHT) { - if (action == wuss_MOUSE_DOWN && button == wuss_BUTTON_SELECT) + if (action == wuss_MOUSE_DOWN && + (button == wuss_BUTTON_SELECT || button == wuss_BUTTON_ADJUST)) { + /* Adjust-clicking a scroll arrow steps the opposite way to the arrow it + * points, so one arrow can be worked in both directions without moving + * the pointer. Toggle-size stays Select-only. */ + int step; + + step = (button == wuss_BUTTON_ADJUST) ? -WUSS_SCROLL_STEP + : WUSS_SCROLL_STEP; + switch (region) { case wuss_FURNITURE_TOGGLE_SIZE: - wuss__furniture_toggle_size(win); + if (button == wuss_BUTTON_SELECT) + wuss__furniture_toggle_size(win); break; case wuss_FURNITURE_VSCROLL_UP: - wuss__furniture_scroll_step(win, (point_t) { 0, -WUSS_SCROLL_STEP }); + wuss__furniture_scroll_step(win, (point_t) { 0, -step }); break; case wuss_FURNITURE_VSCROLL_DOWN: - wuss__furniture_scroll_step(win, (point_t) { 0, WUSS_SCROLL_STEP }); + wuss__furniture_scroll_step(win, (point_t) { 0, step }); break; case wuss_FURNITURE_HSCROLL_LEFT: - wuss__furniture_scroll_step(win, (point_t) { -WUSS_SCROLL_STEP, 0 }); + wuss__furniture_scroll_step(win, (point_t) { -step, 0 }); break; case wuss_FURNITURE_HSCROLL_RIGHT: - wuss__furniture_scroll_step(win, (point_t) { WUSS_SCROLL_STEP, 0 }); + wuss__furniture_scroll_step(win, (point_t) { step, 0 }); break; default: break; From fa65ea39d19308652785e681f9322d74a1a15f11 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Fri, 28 Aug 2026 17:15:04 +0100 Subject: [PATCH 03/44] docs(wuss): rewrap the window and wuss headers, retitle the README section Reflows the Doxygen parameter blocks in window.h and wuss.h, and renames the README's "Wuss" section to "Windowing". --- README.md | 2 +- include/wuss/window.h | 46 +++++++++++++++++++++---------------------- include/wuss/wuss.h | 22 ++++++++++----------- 3 files changed, 34 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 187fac83..2f17af0d 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ DPTLib is my platform independent C library. It contains a wide variety of funct - [`utils/pack.h`](https://github.com/dpt/DPTLib/blob/master/include/utils/pack.h) — structure packing and unpacking helpers - [`utils/primes.h`](https://github.com/dpt/DPTLib/blob/master/include/utils/primes.h) — cache of prime numbers -### Wuss +### Windowing - [`wuss/wuss.h`](https://github.com/dpt/DPTLib/blob/master/include/wuss/wuss.h) — minimal window manager {[docs](https://github.com/dpt/DPTLib/blob/master/docs/wuss.md)} - [`wuss/window.h`](https://github.com/dpt/DPTLib/blob/master/include/wuss/window.h) — window creation, positioning, sizing and client delegation diff --git a/include/wuss/window.h b/include/wuss/window.h index 60762c05..4444636b 100644 --- a/include/wuss/window.h +++ b/include/wuss/window.h @@ -39,26 +39,24 @@ extern "C" * screen keeps its top-left corner on-screen instead. The bottom/right edges * are not clamped. * - * \param[in] wuss Window manager to create the window on. - * \param[in] content Requested content-area bounds, screen space. Copied - * in. - * \param[in] title Titlebar label, or NULL for none. Copied in, truncated - * if too long. Ignored if flags includes - * wuss_WINDOW_NO_TITLEBAR. - * \param[in] flags Appearance flags, e.g. wuss_WINDOW_NO_TITLEBAR / - * wuss_WINDOW_NO_OUTLINE, OR'd together, or - * wuss_WINDOW_NONE for the default furniture. - * \param[in] bg Content background, filled by Wuss before each redraw, - * or wuss_NO_BACKGROUND for the task to draw its own - * background (avoids a redundant fill behind an opaque - * task). Changeable later via - * wuss_window_set_background. - * \param[in] task Content delegate. Copied in. May be NULL for a window - * with no content handling. - * \param[in] doc Virtual document extent, for the scrollbars' sausage - * proportions; pass content's own width and height for a - * window with nothing to scroll. - * \param[out] window Newly created window. Becomes the topmost window. + * \param[in] wuss Window manager to create the window on. + * \param[in] content Requested content-area bounds, screen space. Copied in. + * \param[in] title Titlebar label, or NULL for none. Copied in, truncated if + * too long. Ignored if flags includes + * wuss_WINDOW_NO_TITLEBAR. + * \param[in] flags Appearance flags, e.g. wuss_WINDOW_NO_TITLEBAR / + * wuss_WINDOW_NO_OUTLINE, OR'd together, or + * wuss_WINDOW_NONE for the default furniture. + * \param[in] bg Content background, filled by Wuss before each redraw, or + * wuss_NO_BACKGROUND for the task to draw its own + * background (avoids a redundant fill behind an opaque + * task). Changeable later via wuss_window_set_background. + * \param[in] task Content delegate. Copied in. May be NULL for a window + * with no content handling. + * \param[in] doc Virtual document extent, for the scrollbars' sausage + * proportions; pass content's own width and height for a + * window with nothing to scroll. + * \param[out] window Newly created window. Becomes the topmost window. * \return \ref result_OK on success, \ref result_WUSS_TOO_SMALL if content's * width or height is not positive, \ref result_WUSS_BAD_COLOUR if bg is * out of range for the palette, or another appropriate result code. @@ -132,10 +130,10 @@ void wuss_window_get_content_bounds(const wuss_window_t *window, * call. Content changes are opaque to Wuss, so tasks must call this themselves * (e.g. the union of an animated element's old and new positions). * - * \param[in] window Window whose content changed. - * \param[in] local_box Region, in window-local content coordinates (as passed - * to the task's mouse callback), or NULL to mark the - * whole content area dirty. + * \param[in] window Window whose content changed. + * \param[in] local_box Region, in window-local content coordinates (as passed + * to the task's mouse callback), or NULL to mark the whole + * content area dirty. */ void wuss_window_invalidate(wuss_window_t *window, const box_t *local_box); diff --git a/include/wuss/wuss.h b/include/wuss/wuss.h index 9c0f1f7c..60e04023 100644 --- a/include/wuss/wuss.h +++ b/include/wuss/wuss.h @@ -76,14 +76,14 @@ typedef struct wuss_palette { struct { - wuss_colour_t bg; /**< Titlebar fill. */ - wuss_colour_t fg; /**< Titlebar text. */ + wuss_colour_t bg; /**< Titlebar fill. */ + wuss_colour_t fg; /**< Titlebar text. */ } title; - wuss_colour_t back; /**< Send-to-back icon. */ - wuss_colour_t close; /**< Close icon. */ - wuss_colour_t toggle; /**< Toggle-size icon. */ - wuss_colour_t resize; /**< Resize icon. */ + wuss_colour_t back; /**< Send-to-back icon. */ + wuss_colour_t close; /**< Close icon. */ + wuss_colour_t toggle; /**< Toggle-size icon. */ + wuss_colour_t resize; /**< Resize icon. */ struct { wuss_colour_t arrows; /**< Scrollbar arrows. */ @@ -297,11 +297,11 @@ void wuss_get_dirty(const wuss_t *wuss, int index, box_t *out); * \return \ref result_OK, or a result code returned by the task's mouse * callback. */ -result_t wuss_mouse_click(wuss_t *wuss, - point_t p, - wuss_button_t button, - wuss_mouse_action_t action, - wuss_window_t **hit); +result_t wuss_mouse_click(wuss_t *wuss, + point_t p, + wuss_button_t button, + wuss_mouse_action_t action, + wuss_window_t **hit); /** * Deliver a mouse-move event. Updates the dragged window's position if a drag From bdb82889a48b5607c05071fb4ffd384a3a57ec46 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Fri, 28 Aug 2026 17:15:04 +0100 Subject: [PATCH 04/44] fix(wuss)!: make mouse buttons flags so chords can be reported Select/Menu/Adjust were consecutive enumerated values, so a chord such as Select+Adjust could not be expressed at all. They become flags -- Select 4, Menu 2, Adjust 1, matching the RISC OS button order -- with a wuss_BUTTON_NONE of zero, and every internal comparison switches from equality to a bit test. Where a chord is ambiguous Select wins: a Select+Adjust click on a scroll arrow scrolls the way the arrow points rather than backwards, and on the back icon sends the window to the back. BREAKING CHANGE: wuss_button_t's values have changed, and client code comparing the reported button for equality must now test with '&' or it will fail to match a chord. Co-Authored-By: Claude Opus 5 --- include/wuss/task.h | 7 ++++--- include/wuss/wuss.h | 12 +++++++++--- libraries/wuss/mouse-click.c | 24 +++++++++++++----------- libraries/wuss/test/tasks/ball.c | 4 ++-- libraries/wuss/test/tasks/sofa.c | 2 +- 5 files changed, 29 insertions(+), 20 deletions(-) diff --git a/include/wuss/task.h b/include/wuss/task.h index b3cd3942..78a11cb8 100644 --- a/include/wuss/task.h +++ b/include/wuss/task.h @@ -3,8 +3,8 @@ /** * \file task.h * - * A Wuss task: the content delegate a window hands its drawing and input - * events to, and the events themselves. + * A Wuss task: the content delegate a window hands its drawing and input events + * to, and the events themselves. */ #ifndef WUSS_TASK_H @@ -77,7 +77,8 @@ typedef struct wuss_event * scroll offset has already been added, so the task must not add it * again. With a scroll offset of (0,0) this is the same as window-local * content coordinates, where the content area's top-left is (0,0). - * button is meaningful for DOWN/UP. */ + * button is meaningful for DOWN/UP, and is a set of wuss_button_t + * flags, so test it with '&' rather than comparing for equality. */ struct { wuss_mouse_action_t action; diff --git a/include/wuss/wuss.h b/include/wuss/wuss.h index 60e04023..99d6e1cc 100644 --- a/include/wuss/wuss.h +++ b/include/wuss/wuss.h @@ -40,12 +40,18 @@ typedef struct wuss_window wuss_window_t; /** * Mouse buttons, RISC OS-style: Select is the primary action, Adjust the * secondary action, Menu pops up a menu. + * + * These are flags, OR'd together, so that chords (e.g. Select and Adjust + * pressed together) can be reported. The bit values match the RISC OS button + * order. Test them with '&' rather than comparing for equality, or a chord will + * match no button at all. */ typedef enum wuss_button { - wuss_BUTTON_SELECT, - wuss_BUTTON_MENU, - wuss_BUTTON_ADJUST + wuss_BUTTON_NONE = 0, + wuss_BUTTON_ADJUST = 1 << 0, + wuss_BUTTON_MENU = 1 << 1, + wuss_BUTTON_SELECT = 1 << 2 } wuss_button_t; diff --git a/libraries/wuss/mouse-click.c b/libraries/wuss/mouse-click.c index 138640a3..b7bee4be 100644 --- a/libraries/wuss/mouse-click.c +++ b/libraries/wuss/mouse-click.c @@ -36,9 +36,9 @@ result_t wuss_mouse_click(wuss_t *wuss, region = wuss__furniture_hit_test(win, (point_t) { x, y }); - if (region == wuss_FURNITURE_CLOSE && - action == wuss_MOUSE_DOWN && - button == wuss_BUTTON_SELECT) + if (region == wuss_FURNITURE_CLOSE && + action == wuss_MOUSE_DOWN && + (button & wuss_BUTTON_SELECT)) { if (win->task.handle == NULL) return result_OK; @@ -49,9 +49,9 @@ result_t wuss_mouse_click(wuss_t *wuss, if (region == wuss_FURNITURE_BACK && action == wuss_MOUSE_DOWN) { - if (button == wuss_BUTTON_SELECT) + if (button & wuss_BUTTON_SELECT) wuss_window_restack(win, wuss_ZORDER_BACK); - else if (button == wuss_BUTTON_ADJUST) + else if (button & wuss_BUTTON_ADJUST) wuss_window_restack(win, wuss_ZORDER_FRONT); return result_OK; } @@ -63,20 +63,22 @@ result_t wuss_mouse_click(wuss_t *wuss, region == wuss_FURNITURE_HSCROLL_RIGHT) { if (action == wuss_MOUSE_DOWN && - (button == wuss_BUTTON_SELECT || button == wuss_BUTTON_ADJUST)) + (button & (wuss_BUTTON_SELECT | wuss_BUTTON_ADJUST))) { /* Adjust-clicking a scroll arrow steps the opposite way to the arrow it * points, so one arrow can be worked in both directions without moving * the pointer. Toggle-size stays Select-only. */ int step; - step = (button == wuss_BUTTON_ADJUST) ? -WUSS_SCROLL_STEP - : WUSS_SCROLL_STEP; + /* Select wins a Select+Adjust chord, so a chord never scrolls backwards + * unexpectedly. */ + step = (button & wuss_BUTTON_SELECT) ? WUSS_SCROLL_STEP + : -WUSS_SCROLL_STEP; switch (region) { case wuss_FURNITURE_TOGGLE_SIZE: - if (button == wuss_BUTTON_SELECT) + if (button & wuss_BUTTON_SELECT) wuss__furniture_toggle_size(win); break; case wuss_FURNITURE_VSCROLL_UP: @@ -104,7 +106,7 @@ result_t wuss_mouse_click(wuss_t *wuss, { box_t content; - if (button == wuss_BUTTON_SELECT) + if (button & wuss_BUTTON_SELECT) wuss_window_restack(win, wuss_ZORDER_FRONT); wuss__content_box(win, &content); @@ -124,7 +126,7 @@ result_t wuss_mouse_click(wuss_t *wuss, { point_t scroll; - if (button == wuss_BUTTON_SELECT) + if (button & wuss_BUTTON_SELECT) wuss_window_restack(win, wuss_ZORDER_FRONT); wuss_window_get_scroll(win, &scroll); diff --git a/libraries/wuss/test/tasks/ball.c b/libraries/wuss/test/tasks/ball.c index d0af7601..c188b4cb 100644 --- a/libraries/wuss/test/tasks/ball.c +++ b/libraries/wuss/test/tasks/ball.c @@ -93,7 +93,7 @@ static result_t ball_mouse(wuss_window_t *window, wuss_window_get_scroll(window, &scroll); - if (button == wuss_BUTTON_SELECT) + if (button & wuss_BUTTON_SELECT) { ball_t *b; @@ -116,7 +116,7 @@ static result_t ball_mouse(wuss_window_t *window, local.y1 = b->y + b->radius - scroll.y; wuss_window_invalidate(bc->window, &local); } - else if (button == wuss_BUTTON_ADJUST) + else if (button & wuss_BUTTON_ADJUST) { ball_t *b; diff --git a/libraries/wuss/test/tasks/sofa.c b/libraries/wuss/test/tasks/sofa.c index 300c89da..92b00302 100644 --- a/libraries/wuss/test/tasks/sofa.c +++ b/libraries/wuss/test/tasks/sofa.c @@ -381,7 +381,7 @@ static result_t sofa_mouse(wuss_window_t *window, wuss_button_t button, void *ta sc = task_data; - if (button == wuss_BUTTON_ADJUST) + if (button & wuss_BUTTON_ADJUST) { sc->shape = (sc->shape + 1) % sofa_SHAPE__LIMIT; sc->turns = 0; From 6e2c57f035bf2693f7faa6935edceb55a5aab9f2 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Fri, 28 Aug 2026 17:27:38 +0100 Subject: [PATCH 05/44] docs(wuss): move wuss.md into a windowing directory Matches the per-area layout of the other docs (databases, datastruct, framebuf, geom, io). --- docs/{ => windowing}/wuss.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/{ => windowing}/wuss.md (100%) diff --git a/docs/wuss.md b/docs/windowing/wuss.md similarity index 100% rename from docs/wuss.md rename to docs/windowing/wuss.md From 8c0509dad75d54f1f7b9eabe2e82fd1e123b5997 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Fri, 28 Aug 2026 17:27:56 +0100 Subject: [PATCH 06/44] docs(wuss): add a glossary Names the terms the document already leans on: the three coordinate spaces (screen, window-local, virtual content), content area versus visible bounds, furniture and its parts, and the RISC OS button conventions. Co-Authored-By: Claude Opus 5 --- docs/windowing/wuss.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/docs/windowing/wuss.md b/docs/windowing/wuss.md index 1d91fc58..c5368c5e 100644 --- a/docs/windowing/wuss.md +++ b/docs/windowing/wuss.md @@ -127,6 +127,35 @@ Each window carries a scroll offset, `(0, 0)` by default: the point in the task' - `wuss_get_dirty_count`/`wuss_get_dirty(wuss, index, out)` fetch the currently accumulated dirty regions (coalesced as they accumulate, up to a fixed cap after which further regions are merged into the last one) without redrawing. - A window move or resize is clipped, piece by piece, against whatever's above it in the z-order, and any pixels a move can preserve are blitted directly rather than queued dirty; only the genuinely-changed pieces end up in the dirty region. This is an internal optimisation with no effect on a task's own redraw handling. +## Glossary + +Terms as this document and the API use them. Several are RISC OS conventions, which Wuss follows. + +- **Adjust** — the secondary mouse button, `wuss_BUTTON_ADJUST`. Conventionally the variant of an action: Adjust on the back icon brings a window to front rather than sending it back, and Adjust on a scroll arrow steps against the direction the arrow points. +- **Backdrop** — the desktop background colour painted behind all windows, set by `config->backdrop` at `wuss_create` time. `wuss_NO_BACKGROUND` leaves the area behind windows untouched, making it the caller's to repaint. +- **Button flags** — `wuss_button_t` values are flags (Select 4, Menu 2, Adjust 1), OR'd together so a chord can be reported. Test a reported button with `&`, never for equality. +- **Chord** — two or more mouse buttons held together, e.g. Select+Adjust. Wuss's own furniture handling resolves an ambiguous chord in Select's favour. +- **Content area** — the part of a window belonging to its task. Its bounds are exactly what was passed to `wuss_window_create`, furniture being added outside it; read back with `wuss_window_get_content_bounds`. +- **Dirty region** — the accumulated set of screen-space boxes needing repaint, coalesced as they accumulate. `wuss_redraw_dirty` repaints and clears it. +- **Document extent** — `doc`, the size of a task's virtual content space, fixed at window creation. Sets how far a window can scroll and the scrollbar sausages' proportions. +- **Furniture** — everything Wuss draws around a window's content: outline, titlebar and its icons, scrollbars, resize icon. Drawn outside the content area, never carved out of it. Furniture clicks are handled entirely within Wuss and never reach the task. +- **Handle callback** — a task's single `wuss_event_fn_t`, receiving every event kind and dispatching on `event->kind`. A window whose task has no handle receives no events at all. +- **Icon** — a clickable furniture region in the titlebar or window corner: close, back, toggle-size, resize. +- **Invalidate** — mark a region dirty for the next `wuss_redraw_dirty`. Window management does this for its own changes; a task must do it for its own content changes. +- **Menu** — the middle mouse button, `wuss_BUTTON_MENU`. Routed like any other button; Wuss provides no menu widget of its own. +- **Outline** — the 1px border drawn around a window, suppressed by `wuss_WINDOW_NO_OUTLINE`. +- **Sausage** — the draggable thumb within a scrollbar well, sized in proportion to how much of the document extent the content area shows. +- **Screen space** — coordinates in the underlying `screen_t`, origin at its top-left. Visible and content bounds are in screen space. +- **Select** — the primary mouse button, `wuss_BUTTON_SELECT`. Performs the plain action, and raises a window when used on its titlebar. +- **Task** — the client of a window: an event callback plus an opaque `task_data` pointer, held in a `wuss_task_t`. Wuss owns the window; the task owns what's drawn inside it. +- **Titlebar** — the strip above the content area carrying the window's label and its icons, and the drag handle for moving the window. Suppressed by `wuss_WINDOW_NO_TITLEBAR`. +- **Toggle size** — the titlebar icon that switches a window between its normal size and a maximised size, and back. +- **Virtual content space** — the task's own full coordinate space, of size `doc`. Mouse and scroll events arrive in it, i.e. with the scroll offset already added. +- **Visible bounds** — a window's whole on-screen footprint, content plus furniture; `wuss_window_get_visible_bounds`. +- **Well** — the track a scrollbar's sausage slides along, between the two arrow icons. +- **Window-local coordinates** — coordinates relative to the content area's top-left, before the scroll offset is added. `wuss_window_invalidate` takes its box in these. +- **Z-order** — the back-to-front stacking order of windows. Changed with `wuss_window_restack`, or by a Select click on a titlebar; content clicks never change it. + ## Limitations - No menus: `wuss_BUTTON_MENU` is defined and routed like any other button, but Wuss has no built-in menu widget. From 5fa615d04e5cb8c0a3988b549b74e9b35adae857 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Fri, 28 Aug 2026 17:28:04 +0100 Subject: [PATCH 07/44] feat(wuss)!: let a window set a minimum content extent doc already caps how large a resize-drag or toggle-size can make a window; the floor was the fixed WUSS_MIN_CONTENT, so a task whose content stops making sense below some size had no way to say so. wuss_window_create gains a min_doc alongside doc, set once at creation. wuss__min_content clamps it up to WUSS_MIN_CONTENT, so a window can never be squeezed too small to grab, and down to doc, so a minimum can never demand a window larger than the document it shows. Pass (0,0) for the built-in floor. BREAKING CHANGE: wuss_window_create takes a min_doc argument between doc and window. Co-Authored-By: Claude Opus 5 --- docs/windowing/wuss.md | 9 ++- include/wuss/window.h | 8 ++- libraries/wuss/furniture/drag-resize.c | 10 +-- libraries/wuss/furniture/toggle-action.c | 12 ++-- libraries/wuss/impl.h | 15 ++++ libraries/wuss/test/tasks/ball.c | 1 + libraries/wuss/test/tasks/blank.c | 1 + libraries/wuss/test/tasks/chars.c | 1 + libraries/wuss/test/tasks/checker.c | 2 + libraries/wuss/test/tasks/curve.c | 1 + libraries/wuss/test/tasks/gradient.c | 1 + libraries/wuss/test/tasks/image.c | 1 + libraries/wuss/test/tasks/launcher.c | 1 + libraries/wuss/test/tasks/palette.c | 1 + libraries/wuss/test/tasks/porter-duff.c | 1 + libraries/wuss/test/tasks/sofa.c | 1 + libraries/wuss/test/tasks/text.c | 1 + libraries/wuss/test/wuss-test.c | 88 ++++++++++++++++++++++-- libraries/wuss/window/create.c | 2 + 19 files changed, 138 insertions(+), 19 deletions(-) diff --git a/docs/windowing/wuss.md b/docs/windowing/wuss.md index c5368c5e..a648f97a 100644 --- a/docs/windowing/wuss.md +++ b/docs/windowing/wuss.md @@ -32,13 +32,15 @@ Create a window with a content bounding box, optional title, appearance flags, a result_t wuss_window_create(wuss_t *wuss, const box_t *content, const char *title, wuss_window_flags_t flags, wuss_colour_t bg, const wuss_task_t *task, - size2d_t doc, + size2d_t doc, size2d_t min_doc, wuss_window_t **window); ``` `bg` is filled in by wuss before each redraw event, or `wuss_NO_BACKGROUND` for the task to draw its own background (avoids a redundant fill behind an opaque task); changeable later via `wuss_window_set_background`. -`doc` is the virtual document extent behind the horizontal/vertical scrollbars' sausage proportion; pass `content`'s own width/height for a window with nothing to scroll. Set once at creation, immutable thereafter. +`doc` is the virtual document extent behind the horizontal/vertical scrollbars' sausage proportion; pass `content`'s own width/height for a window with nothing to scroll. It is also the ceiling a resize-drag or toggle-size grows the content area to. Set once at creation, immutable thereafter. + +`min_doc` is the opposite end: the smallest content size a resize-drag or toggle-size will shrink to. Pass `(0, 0)` for the built-in floor. It is clamped both to that built-in floor, so a window can never be squeezed too small to grab, and to `doc`, so it can never demand a window larger than the document it shows. Also set once at creation. Furniture is additional to `content`, not carved out of it: the window's content area always ends up exactly the box requested, and its on-screen footprint (`wuss_window_get_visible_bounds`) is `content` expanded outward by whatever furniture flags request — a titlebar above, and/or a 1px outline around all four sides. @@ -137,7 +139,8 @@ Terms as this document and the API use them. Several are RISC OS conventions, wh - **Chord** — two or more mouse buttons held together, e.g. Select+Adjust. Wuss's own furniture handling resolves an ambiguous chord in Select's favour. - **Content area** — the part of a window belonging to its task. Its bounds are exactly what was passed to `wuss_window_create`, furniture being added outside it; read back with `wuss_window_get_content_bounds`. - **Dirty region** — the accumulated set of screen-space boxes needing repaint, coalesced as they accumulate. `wuss_redraw_dirty` repaints and clears it. -- **Document extent** — `doc`, the size of a task's virtual content space, fixed at window creation. Sets how far a window can scroll and the scrollbar sausages' proportions. +- **Document extent** — `doc`, the size of a task's virtual content space, fixed at window creation. Sets how far a window can scroll, the scrollbar sausages' proportions, and the size a resize-drag or toggle-size can grow the content area to. +- **Minimum extent** — `min_doc`, the smallest content size a resize-drag or toggle-size will leave a window at, fixed at window creation. `(0, 0)` means the built-in floor. - **Furniture** — everything Wuss draws around a window's content: outline, titlebar and its icons, scrollbars, resize icon. Drawn outside the content area, never carved out of it. Furniture clicks are handled entirely within Wuss and never reach the task. - **Handle callback** — a task's single `wuss_event_fn_t`, receiving every event kind and dispatching on `event->kind`. A window whose task has no handle receives no events at all. - **Icon** — a clickable furniture region in the titlebar or window corner: close, back, toggle-size, resize. diff --git a/include/wuss/window.h b/include/wuss/window.h index 4444636b..fa53be11 100644 --- a/include/wuss/window.h +++ b/include/wuss/window.h @@ -55,7 +55,12 @@ extern "C" * with no content handling. * \param[in] doc Virtual document extent, for the scrollbars' sausage * proportions; pass content's own width and height for a - * window with nothing to scroll. + * window with nothing to scroll. Also the ceiling a + * resize-drag or toggle-size will grow the content area to. + * \param[in] min_doc Minimum content size a resize-drag or toggle-size will + * shrink to. Pass (0,0) for the built-in floor. Clamped to + * doc, and to the built-in floor, so it can never make a + * window unusably small or larger than its document. * \param[out] window Newly created window. Becomes the topmost window. * \return \ref result_OK on success, \ref result_WUSS_TOO_SMALL if content's * width or height is not positive, \ref result_WUSS_BAD_COLOUR if bg is @@ -68,6 +73,7 @@ result_t wuss_window_create(wuss_t *wuss, wuss_colour_t bg, const wuss_task_t *task, size2d_t doc, + size2d_t min_doc, wuss_window_t **window); /** diff --git a/libraries/wuss/furniture/drag-resize.c b/libraries/wuss/furniture/drag-resize.c index 19995ec4..61c2f93d 100644 --- a/libraries/wuss/furniture/drag-resize.c +++ b/libraries/wuss/furniture/drag-resize.c @@ -6,15 +6,17 @@ void wuss__furniture_drag_resize(wuss_window_t *window, point_t p) { - box_t content; - int width, height; + box_t content; + size2d_t min; + int width, height; wuss__content_box(window, &content); + wuss__min_content(window, &min); width = p.x - content.x0; height = p.y - content.y0; - width = CLAMP(width, WUSS_MIN_CONTENT, window->doc.w); - height = CLAMP(height, WUSS_MIN_CONTENT, window->doc.h); + width = CLAMP(width, min.w, MAX(window->doc.w, min.w)); + height = CLAMP(height, min.h, MAX(window->doc.h, min.h)); wuss_window_resize(window, (size2d_t) { width, height }); } diff --git a/libraries/wuss/furniture/toggle-action.c b/libraries/wuss/furniture/toggle-action.c index 4203f778..4d6a6fcd 100644 --- a/libraries/wuss/furniture/toggle-action.c +++ b/libraries/wuss/furniture/toggle-action.c @@ -16,8 +16,9 @@ void wuss__furniture_toggle_size(wuss_window_t *window) } else { - int outline_px, titlebar_height, available_width, available_height, width, height; - point_t carve; + int outline_px, titlebar_height, available_width, available_height, width, height; + point_t carve; + size2d_t min; outline_px = wuss__outline_px(window); titlebar_height = wuss__titlebar_height(window); @@ -38,9 +39,10 @@ void wuss__furniture_toggle_size(wuss_window_t *window) * x0/y0 (an inverted box), which is never hit-testable again * (box_contains_point can't match x1doc.w, available_width); height = MIN(window->doc.h, available_height); diff --git a/libraries/wuss/impl.h b/libraries/wuss/impl.h index 18425751..7ab0ed0f 100644 --- a/libraries/wuss/impl.h +++ b/libraries/wuss/impl.h @@ -3,6 +3,7 @@ #ifndef IMPL_H #define IMPL_H +#include "base/utils.h" #include "datastruct/list.h" #include "geom/box.h" #include "geom/size.h" @@ -66,6 +67,8 @@ struct wuss_window point_t scroll; /* offset into virtual content space of the * content box's top-left; see wuss_window_set_scroll */ size2d_t doc; /* virtual document extent, set at creation */ + size2d_t min_doc; /* resize floor, set at creation; see + * wuss__min_content */ wuss_window_state_t state; /* see wuss_window_state_t */ box_t pre_toggle; /* visible bounds to restore on the next toggle */ char title[WUSS_TITLE_MAX + 1]; @@ -116,6 +119,18 @@ static inline int wuss__size_ok(int width, int height) return width > 0 && height > 0; } +/* The floor a resize-drag or toggle-size will shrink a window's content to: + * the client's min_doc where it set one, but never below WUSS_MIN_CONTENT (a + * window must stay big enough to grab) nor above the window's own doc extent + * (a window can't be forced larger than the document it shows). */ +static inline void wuss__min_content(const wuss_window_t *window, size2d_t *min) +{ + min->w = CLAMP(window->min_doc.w, WUSS_MIN_CONTENT, MAX(window->doc.w, + WUSS_MIN_CONTENT)); + min->h = CLAMP(window->min_doc.h, WUSS_MIN_CONTENT, MAX(window->doc.h, + WUSS_MIN_CONTENT)); +} + static inline int wuss__titlebar_height_for(const wuss_t *wuss, wuss_window_flags_t flags) { diff --git a/libraries/wuss/test/tasks/ball.c b/libraries/wuss/test/tasks/ball.c index c188b4cb..9e41f4ac 100644 --- a/libraries/wuss/test/tasks/ball.c +++ b/libraries/wuss/test/tasks/ball.c @@ -37,6 +37,7 @@ result_t ball_create(wuss_t *wuss, const colour_t *palette, ball_task_t *task) wuss_NO_BACKGROUND, &delegate, box_size(&box), + (size2d_t) { 0, 0 }, &task->window); } diff --git a/libraries/wuss/test/tasks/blank.c b/libraries/wuss/test/tasks/blank.c index 39f3b709..470a8817 100644 --- a/libraries/wuss/test/tasks/blank.c +++ b/libraries/wuss/test/tasks/blank.c @@ -34,6 +34,7 @@ result_t blank_create(wuss_t *wuss, int npalette, blank_task_t *task) palette_PICO8_GREEN, &delegate, box_size(&box), + (size2d_t) { 0, 0 }, &task->window); } diff --git a/libraries/wuss/test/tasks/chars.c b/libraries/wuss/test/tasks/chars.c index 16c25168..3cc75d2a 100644 --- a/libraries/wuss/test/tasks/chars.c +++ b/libraries/wuss/test/tasks/chars.c @@ -56,6 +56,7 @@ result_t chars_create(wuss_t *wuss, wuss_NO_BACKGROUND, &delegate, box_size(&box), + (size2d_t) { 0, 0 }, &task->window); } diff --git a/libraries/wuss/test/tasks/checker.c b/libraries/wuss/test/tasks/checker.c index 7639a535..71a22ae6 100644 --- a/libraries/wuss/test/tasks/checker.c +++ b/libraries/wuss/test/tasks/checker.c @@ -41,6 +41,7 @@ result_t checker_create(wuss_t *wuss, wuss_NO_BACKGROUND, &delegate, box_size(&box), + (size2d_t) { 0, 0 }, &task->window); if (rc != result_OK) return rc; @@ -54,6 +55,7 @@ result_t checker_create(wuss_t *wuss, wuss_NO_BACKGROUND, &delegate, box_size(&box), + (size2d_t) { 0, 0 }, &task->window2); if (rc != result_OK) { diff --git a/libraries/wuss/test/tasks/curve.c b/libraries/wuss/test/tasks/curve.c index b19b09e3..9fc73ef6 100644 --- a/libraries/wuss/test/tasks/curve.c +++ b/libraries/wuss/test/tasks/curve.c @@ -49,6 +49,7 @@ result_t curve_create(wuss_t *wuss, wuss_NO_BACKGROUND, &delegate, box_size(&box), + (size2d_t) { 0, 0 }, &task->window); } diff --git a/libraries/wuss/test/tasks/gradient.c b/libraries/wuss/test/tasks/gradient.c index 78f0c479..6e171fa6 100644 --- a/libraries/wuss/test/tasks/gradient.c +++ b/libraries/wuss/test/tasks/gradient.c @@ -46,6 +46,7 @@ result_t gradient_create(wuss_t *wuss, gradient_task_t *task) wuss_NO_BACKGROUND, &delegate, (size2d_t) { GRADIENT_DOC_WIDTH, GRADIENT_DOC_HEIGHT }, + (size2d_t) { 0, 0 }, &task->window); } diff --git a/libraries/wuss/test/tasks/image.c b/libraries/wuss/test/tasks/image.c index d9e55207..a804b49b 100644 --- a/libraries/wuss/test/tasks/image.c +++ b/libraries/wuss/test/tasks/image.c @@ -43,6 +43,7 @@ result_t image_create(wuss_t *wuss, palette_PICO8_BLACK, &delegate, (size2d_t) { task->bitmap.size.w, task->bitmap.size.h }, + (size2d_t) { 0, 0 }, &task->window); } diff --git a/libraries/wuss/test/tasks/launcher.c b/libraries/wuss/test/tasks/launcher.c index 1039b726..07c6829f 100644 --- a/libraries/wuss/test/tasks/launcher.c +++ b/libraries/wuss/test/tasks/launcher.c @@ -46,6 +46,7 @@ result_t launcher_create(wuss_t *wuss, wuss_NO_BACKGROUND, &delegate, box_size(&box), + (size2d_t) { 0, 0 }, &task->window); } diff --git a/libraries/wuss/test/tasks/palette.c b/libraries/wuss/test/tasks/palette.c index 4e44965a..0c781b41 100644 --- a/libraries/wuss/test/tasks/palette.c +++ b/libraries/wuss/test/tasks/palette.c @@ -33,6 +33,7 @@ result_t palette_create(wuss_t *wuss, palette_PICO8_BLACK, &delegate, box_size(&box), + (size2d_t) { 0, 0 }, &task->window); } diff --git a/libraries/wuss/test/tasks/porter-duff.c b/libraries/wuss/test/tasks/porter-duff.c index 1a482abc..82cc8e95 100644 --- a/libraries/wuss/test/tasks/porter-duff.c +++ b/libraries/wuss/test/tasks/porter-duff.c @@ -201,6 +201,7 @@ result_t porter_duff_create(wuss_t *wuss, wuss_NO_BACKGROUND, &delegate, box_size(&box), + (size2d_t) { 0, 0 }, &task->window); if (rc != result_OK) goto free_dst; diff --git a/libraries/wuss/test/tasks/sofa.c b/libraries/wuss/test/tasks/sofa.c index 92b00302..8d25a6da 100644 --- a/libraries/wuss/test/tasks/sofa.c +++ b/libraries/wuss/test/tasks/sofa.c @@ -278,6 +278,7 @@ result_t sofa_create(wuss_t *wuss, const colour_t *palette, sofa_task_t *task) wuss_NO_BACKGROUND, &delegate, box_size(&box), + (size2d_t) { 0, 0 }, &task->window); } diff --git a/libraries/wuss/test/tasks/text.c b/libraries/wuss/test/tasks/text.c index 629bea22..41fc2e4f 100644 --- a/libraries/wuss/test/tasks/text.c +++ b/libraries/wuss/test/tasks/text.c @@ -54,6 +54,7 @@ result_t text_create(wuss_t *wuss, palette_PICO8_BLUE, &delegate, box_size(&box), + (size2d_t) { 0, 0 }, &task->window); return rc; diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 27f54847..861f71a7 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -618,6 +618,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, NULL, box_size(&box_a), + (size2d_t) { 0, 0 }, &win_a); if (rc != result_WUSS_TOO_SMALL) goto Failure; @@ -643,6 +644,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_a, box_size(&box_a), + (size2d_t) { 0, 0 }, &win_a); if (rc != result_OK) goto Failure; @@ -665,6 +667,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_b, box_size(&box_b), + (size2d_t) { 0, 0 }, &win_b); if (rc != result_OK) goto Failure; @@ -950,6 +953,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_d, box_size(&box_d), + (size2d_t) { 0, 0 }, &win_d); if (rc != result_OK) goto Failure; @@ -997,6 +1001,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_e, box_size(&box_e), + (size2d_t) { 0, 0 }, &win_e); if (rc != result_OK) goto Failure; @@ -1017,6 +1022,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_f, box_size(&box_f), + (size2d_t) { 0, 0 }, &win_f); if (rc != result_OK) goto Failure; @@ -1084,6 +1090,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_h, box_size(&box_h), + (size2d_t) { 0, 0 }, &win_h); if (rc != result_OK) goto Failure; @@ -1104,6 +1111,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_g, box_size(&box_g), + (size2d_t) { 0, 0 }, &win_g); if (rc != result_OK) goto Failure; @@ -1165,6 +1173,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_i, box_size(&box_i), + (size2d_t) { 0, 0 }, &win_i); if (rc != result_OK) goto Failure; @@ -1185,6 +1194,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_j, box_size(&box_j), + (size2d_t) { 0, 0 }, &win_j); if (rc != result_OK) goto Failure; @@ -1235,6 +1245,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_m, box_size(&box_m), + (size2d_t) { 0, 0 }, &win_m); if (rc != result_OK) goto Failure; @@ -1287,6 +1298,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_h, box_size(&box_h), + (size2d_t) { 0, 0 }, &win_h); if (rc != result_OK) goto Failure; @@ -1305,6 +1317,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_g, box_size(&box_g), + (size2d_t) { 0, 0 }, &win_g); if (rc != result_OK) goto Failure; @@ -1405,6 +1418,53 @@ result_t wuss_test(const char *resources) wuss_window_close(win_h); } + printf("test: drag-resize stops at min_doc, not just WUSS_MIN_CONTENT\n"); + + { + test_task_t tc_m; + wuss_task_t delegate_m; + box_t box_m, content, visible; + wuss_window_t *win_m; + + tc_m.redraw_count = 0; + tc_m.mouse_count = 0; + delegate_m.handle = test_handle; + delegate_m.task_data = &tc_m; + + box_m.x0 = 10; box_m.y0 = 10; + box_m.x1 = 210; box_m.y1 = 210; /* 200x200 content, floored at 80x60 */ + rc = wuss_window_create(wuss, &box_m, "M", wuss_WINDOW_NONE, + wuss_NO_BACKGROUND, + &delegate_m, + (size2d_t) { 200, 200 }, (size2d_t) { 80, 60 }, + &win_m); + if (rc != result_OK) + goto Failure; + + wuss_window_get_visible_bounds(win_m, &visible); + wuss_window_get_content_bounds(win_m, &content); + + rc = wuss_mouse_click(wuss, (point_t) { visible.x1 - 3, visible.y1 - 3 }, wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* M's resize icon */ + if (rc != result_OK) + goto Failure; + if (hit != win_m) + goto Failure; + + rc = wuss_mouse_move(wuss, (point_t) { content.x0 + 5, content.y0 + 5 }, &hit); /* drag far inside min_doc */ + if (rc != result_OK) + goto Failure; + + rc = wuss_mouse_click(wuss, (point_t) { content.x0 + 5, content.y0 + 5 }, wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); + if (rc != result_OK) + goto Failure; + + wuss_window_get_content_bounds(win_m, &content); + if (content.x1 - content.x0 != 80 || content.y1 - content.y0 != 60) + goto Failure; /* floored at min_doc, not squeezed down to WUSS_MIN_CONTENT */ + + wuss_window_close(win_m); + } + printf("test: toggle-size blits rather than redrawing the whole window\n"); { @@ -1427,7 +1487,7 @@ result_t wuss_test(const char *resources) rc = wuss_window_create(wuss, &box_t_win, "T", wuss_WINDOW_NONE, wuss_NO_BACKGROUND, &delegate_t, - (size2d_t) { 200, 200 }, &win_t); + (size2d_t) { 200, 200 }, (size2d_t) { 0, 0 }, &win_t); if (rc != result_OK) goto Failure; @@ -1593,7 +1653,7 @@ result_t wuss_test(const char *resources) rc = wuss_window_create(wuss, &box_r, "R", wuss_WINDOW_NONE, wuss_NO_BACKGROUND, &delegate_r, - (size2d_t) { 70, 70 }, &win_r); + (size2d_t) { 70, 70 }, (size2d_t) { 0, 0 }, &win_r); if (rc != result_OK) goto Failure; @@ -1691,7 +1751,7 @@ result_t wuss_test(const char *resources) rc = wuss_window_create(wuss, &box_nb, "NB", wuss_WINDOW_NO_RESIZE_BLIT, wuss_NO_BACKGROUND, &delegate_nb, - (size2d_t) { 200, 200 }, &win_nb); + (size2d_t) { 200, 200 }, (size2d_t) { 0, 0 }, &win_nb); if (rc != result_OK) goto Failure; @@ -1770,7 +1830,7 @@ result_t wuss_test(const char *resources) box_u.x1 = 120; box_u.y1 = 120; /* 40x40 content */ rc = wuss_window_create(wuss, &box_u, "U", wuss_WINDOW_NONE, wuss_NO_BACKGROUND, /* scrollbars on: carve.x/y = icon size */ &delegate_u, - (size2d_t) { 70, 70 }, &win_u); /* doc size well within the 200x200 screen: growth is doc-limited, not screen-limited */ + (size2d_t) { 70, 70 }, (size2d_t) { 0, 0 }, &win_u); /* doc size well within the 200x200 screen: growth is doc-limited, not screen-limited */ if (rc != result_OK) goto Failure; @@ -1878,7 +1938,7 @@ result_t wuss_test(const char *resources) rc = wuss_window_create(wuss, &box_v, "V", wuss_WINDOW_NONE, wuss_NO_BACKGROUND, &delegate_v, - (size2d_t) { 200, 200 }, &win_v); + (size2d_t) { 200, 200 }, (size2d_t) { 0, 0 }, &win_v); if (rc != result_OK) goto Failure; @@ -1992,6 +2052,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_k, box_size(&box_k), + (size2d_t) { 0, 0 }, &win_k); if (rc != result_OK) goto Failure; @@ -2012,6 +2073,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_l, box_size(&box_l), + (size2d_t) { 0, 0 }, &win_l); if (rc != result_OK) goto Failure; @@ -2078,6 +2140,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_m2, box_size(&box_m2), + (size2d_t) { 0, 0 }, &win_m2); if (rc != result_OK) goto Failure; @@ -2168,6 +2231,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_nb2, box_size(&box_nb2), + (size2d_t) { 0, 0 }, &win_nb2); if (rc != result_OK) goto Failure; @@ -2219,6 +2283,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_n, box_size(&box_n), + (size2d_t) { 0, 0 }, &win_n); if (rc != result_OK) goto Failure; @@ -2237,6 +2302,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_o, box_size(&box_o), + (size2d_t) { 0, 0 }, &win_o); if (rc != result_OK) goto Failure; @@ -2321,6 +2387,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_b, box_size(&box_b), + (size2d_t) { 0, 0 }, &win_b); if (rc != result_OK) goto Failure; @@ -2339,6 +2406,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_a, box_size(&box_a), + (size2d_t) { 0, 0 }, &win_a); if (rc != result_OK) goto Failure; @@ -2422,6 +2490,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_b, box_size(&box_b), + (size2d_t) { 0, 0 }, &win_b); if (rc != result_OK) goto Failure; @@ -2440,6 +2509,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_a, box_size(&box_a), + (size2d_t) { 0, 0 }, &win_a); if (rc != result_OK) goto Failure; @@ -2500,6 +2570,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_b, box_size(&box_b), + (size2d_t) { 0, 0 }, &win_b); if (rc != result_OK) goto Failure; @@ -2518,6 +2589,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_a, box_size(&box_a), + (size2d_t) { 0, 0 }, &win_a); if (rc != result_OK) goto Failure; @@ -2611,6 +2683,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_b, box_size(&box_b), + (size2d_t) { 0, 0 }, &win_b); if (rc != result_OK) goto Failure; @@ -2629,6 +2702,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_a, box_size(&box_a), + (size2d_t) { 0, 0 }, &win_a); if (rc != result_OK) goto Failure; @@ -2705,6 +2779,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_c, box_size(&box_c), + (size2d_t) { 0, 0 }, &win_c); if (rc != result_OK) goto Failure; @@ -2738,7 +2813,7 @@ result_t wuss_test(const char *resources) rc = wuss_window_create(wuss, &box_s, "S", wuss_WINDOW_NONE, wuss_NO_BACKGROUND, &delegate_s, - (size2d_t) { 200, 200 }, &win_s); + (size2d_t) { 200, 200 }, (size2d_t) { 0, 0 }, &win_s); if (rc != result_OK) goto Failure; @@ -2815,6 +2890,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_r, (size2d_t) { 400, 400 }, + (size2d_t) { 0, 0 }, &win_r); if (rc != result_OK) goto Failure; diff --git a/libraries/wuss/window/create.c b/libraries/wuss/window/create.c index 591da41e..421ab08e 100644 --- a/libraries/wuss/window/create.c +++ b/libraries/wuss/window/create.c @@ -17,6 +17,7 @@ result_t wuss_window_create(wuss_t *wuss, wuss_colour_t bg, const wuss_task_t *task, size2d_t doc, + size2d_t min_doc, wuss_window_t **window) { wuss_window_t *win; @@ -76,6 +77,7 @@ result_t wuss_window_create(wuss_t *wuss, win->scroll.x = 0; win->scroll.y = 0; win->doc = doc; + win->min_doc = min_doc; win->state = wuss_WINDOW_STATE_NONE; if (task != NULL) From 61e1f7a578d9a9ba963a9718970d08c6b95af4c8 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Fri, 28 Aug 2026 18:48:27 +0100 Subject: [PATCH 08/44] fix(wuss): track launcher running state in a separate array The launcher entry table is now const, so the per-row "running" flag can no longer live in it. Move it into launcher_task as a fixed-size bool array indexed by entry, capped at LAUNCHER_MAX_ENTRIES. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/test/tasks/launcher.c | 50 +++++++------- libraries/wuss/test/tasks/launcher.h | 37 ++++++----- libraries/wuss/test/wuss-test.c | 98 ++++++++++++++-------------- 3 files changed, 95 insertions(+), 90 deletions(-) diff --git a/libraries/wuss/test/tasks/launcher.c b/libraries/wuss/test/tasks/launcher.c index 07c6829f..ec779f4b 100644 --- a/libraries/wuss/test/tasks/launcher.c +++ b/libraries/wuss/test/tasks/launcher.c @@ -2,6 +2,7 @@ #ifdef USE_SDL +#include #include #ifdef FORTIFY @@ -19,18 +20,21 @@ #define LAUNCHER_PAD 4 #define LAUNCHER_WIDTH 160 -result_t launcher_create(wuss_t *wuss, - launcher_entry_t *entries, - int nentries, - bmfont_t *font, - const colour_t *palette, - launcher_task_t *task) +result_t launcher_create(wuss_t *wuss, + const launcher_entry_t *entries, + int nentries, + bmfont_t *font, + const colour_t *palette, + launcher_task_t *task) { wuss_task_t delegate; box_t box; + assert(nentries <= LAUNCHER_MAX_ENTRIES); + task->entries = entries; task->nentries = nentries; + memset(task->running, 0, sizeof(task->running)); task->font = font; task->fg = palette[palette_PICO8_BLACK]; task->bg = palette[palette_PICO8_WHITE]; @@ -58,11 +62,11 @@ void launcher_destroy(launcher_task_t *task) static result_t launcher_redraw(const wuss_event_t *event, void *task_data) { launcher_task_t *lc; - screen_t *scr; - const box_t *content, *bounds; - int i, font_width, font_height, sx, sy; - point_t pos; - const launcher_entry_t *entry; + screen_t *scr; + const box_t *content, *bounds; + int i, font_width, font_height, sx, sy; + point_t pos; + const launcher_entry_t *entry; lc = task_data; @@ -72,11 +76,9 @@ static result_t launcher_redraw(const wuss_event_t *event, void *task_data) sx = event->data.redraw.scroll.x; sy = event->data.redraw.scroll.y; - screen_draw_rect(scr, content->x0, content->y0, box_size(content), - lc->bg); + screen_draw_rect(scr, content->x0, content->y0, box_size(content), lc->bg); - bmfont_get_info(lc->font, &font_width, &font_height); - NOT_USED(font_width); + bmfont_get_info(lc->font, NULL, &font_height); for (i = 0; i < lc->nentries; i++) { @@ -86,7 +88,7 @@ static result_t launcher_redraw(const wuss_event_t *event, void *task_data) pos.y = bounds->y0 - sy + LAUNCHER_PAD + i * LAUNCHER_ROW_HEIGHT + (LAUNCHER_ROW_HEIGHT - font_height) / 2; bmfont_draw(lc->font, scr, entry->name, (int) strlen(entry->name), - entry->running ? lc->running_fg : lc->fg, lc->bg, &pos, NULL); + lc->running[i] ? lc->running_fg : lc->fg, lc->bg, &pos, NULL); } return result_OK; @@ -94,10 +96,10 @@ static result_t launcher_redraw(const wuss_event_t *event, void *task_data) static result_t launcher_mouse(wuss_window_t *window, int y, void *task_data) { - launcher_task_t *lc; - int i; - launcher_entry_t *entry; - result_t rc; + launcher_task_t *lc; + int i; + const launcher_entry_t *entry; + result_t rc; lc = task_data; @@ -107,14 +109,14 @@ static result_t launcher_mouse(wuss_window_t *window, int y, void *task_data) if (i < 0 || i >= lc->nentries) return result_OK; - entry = &lc->entries[i]; - if (entry->running) + if (lc->running[i]) return result_OK; - rc = entry->spawn(); + entry = &lc->entries[i]; + rc = entry->spawn(); if (rc == result_OK) { - entry->running = true; + lc->running[i] = true; wuss_window_invalidate_all(window); } diff --git a/libraries/wuss/test/tasks/launcher.h b/libraries/wuss/test/tasks/launcher.h index 05ebd89c..4147975e 100644 --- a/libraries/wuss/test/tasks/launcher.h +++ b/libraries/wuss/test/tasks/launcher.h @@ -14,25 +14,28 @@ typedef result_t (*launcher_spawn_fn_t)(void); typedef void (*launcher_destroy_fn_t)(void); -/* one clickable row; "running" is set once spawned and never cleared, so a - * second click is a no-op -- relaunching a task after its window closes - * needs the test restarted */ +/* one clickable row */ typedef struct launcher_entry { const char *name; - launcher_spawn_fn_t spawn; - launcher_destroy_fn_t destroy; - bool running; + launcher_spawn_fn_t spawn; + launcher_destroy_fn_t destroy; } launcher_entry_t; +/* a row's task is spawned at most once: launcher_task's "running" array is + * set on the first click and never cleared, so a second click is a no-op -- + * relaunching a task after its window closes needs the test restarted */ +#define LAUNCHER_MAX_ENTRIES 32 + typedef struct launcher_task { - launcher_entry_t *entries; /* owned by the caller, must outlive the window */ - int nentries; - bmfont_t *font; - colour_t fg, bg, running_fg; - wuss_window_t *window; + const launcher_entry_t *entries; /* owned by the caller, must outlive the window */ + int nentries; + bool running[LAUNCHER_MAX_ENTRIES]; + bmfont_t *font; + colour_t fg, bg, running_fg; + wuss_window_t *window; } launcher_task_t; @@ -40,12 +43,12 @@ wuss_event_fn_t launcher_handle; /* create a window listing "entries"; clicking a row calls its spawn * function once */ -result_t launcher_create(wuss_t *wuss, - launcher_entry_t *entries, - int nentries, - bmfont_t *font, - const colour_t *palette, - launcher_task_t *task); +result_t launcher_create(wuss_t *wuss, + const launcher_entry_t *entries, + int nentries, + bmfont_t *font, + const colour_t *palette, + launcher_task_t *task); void launcher_destroy(launcher_task_t *task); diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 861f71a7..c88a7e0c 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -50,61 +50,61 @@ /* the launcher's spawn callbacks take no arguments, so the pieces they need * are stashed here instead; wuss_interactive_test runs at most once per * process, so file-scope statics are as good as a context struct */ -static wuss_t *g_wuss; -static const colour_t *g_palette; -static int g_npalette; -static const char *g_resources; -static bmfont_t *g_daydream_font; - -static ball_task_t g_ball_task; -static text_task_t g_text_task; -static blank_task_t g_blank_task; -static chars_task_t g_chars_task; -static palette_task_t g_palette_task; -static image_task_t g_image_task; -static checker_task_t g_checker_task; -static curve_task_t g_curve_task; -static sofa_task_t g_sofa_task; -static gradient_task_t g_gradient_task; +static wuss_t *g_wuss; +static const colour_t *g_palette; +static int g_npalette; +static const char *g_resources; +static bmfont_t *g_daydream_font; + +static ball_task_t g_ball_task; +static text_task_t g_text_task; +static blank_task_t g_blank_task; +static chars_task_t g_chars_task; +static palette_task_t g_palette_task; +static image_task_t g_image_task; +static checker_task_t g_checker_task; +static curve_task_t g_curve_task; +static sofa_task_t g_sofa_task; +static gradient_task_t g_gradient_task; static porter_duff_task_t g_porter_duff_task; -static result_t spawn_ball(void) { return ball_create(g_wuss, g_palette, &g_ball_task); } -static result_t spawn_text(void) { return text_create(g_wuss, g_palette, g_daydream_font, &g_text_task); } -static result_t spawn_blank(void) { return blank_create(g_wuss, g_npalette, &g_blank_task); } -static result_t spawn_chars(void) { return chars_create(g_wuss, g_palette, &g_chars_task); } -static result_t spawn_palette(void) { return palette_create(g_wuss, g_palette, g_npalette, &g_palette_task); } -static result_t spawn_image(void) { return image_create(g_wuss, g_palette, g_resources, &g_image_task); } -static result_t spawn_checker(void) { return checker_create(g_wuss, g_palette, &g_checker_task); } -static result_t spawn_curve(void) { return curve_create(g_wuss, g_palette, &g_curve_task); } -static result_t spawn_sofa(void) { return sofa_create(g_wuss, g_palette, &g_sofa_task); } -static result_t spawn_gradient(void) { return gradient_create(g_wuss, &g_gradient_task); } +static result_t spawn_ball(void) { return ball_create(g_wuss, g_palette, &g_ball_task); } +static result_t spawn_text(void) { return text_create(g_wuss, g_palette, g_daydream_font, &g_text_task); } +static result_t spawn_blank(void) { return blank_create(g_wuss, g_npalette, &g_blank_task); } +static result_t spawn_chars(void) { return chars_create(g_wuss, g_palette, &g_chars_task); } +static result_t spawn_palette(void) { return palette_create(g_wuss, g_palette, g_npalette, &g_palette_task); } +static result_t spawn_image(void) { return image_create(g_wuss, g_palette, g_resources, &g_image_task); } +static result_t spawn_checker(void) { return checker_create(g_wuss, g_palette, &g_checker_task); } +static result_t spawn_curve(void) { return curve_create(g_wuss, g_palette, &g_curve_task); } +static result_t spawn_sofa(void) { return sofa_create(g_wuss, g_palette, &g_sofa_task); } +static result_t spawn_gradient(void) { return gradient_create(g_wuss, &g_gradient_task); } static result_t spawn_porter_duff(void) { return porter_duff_create(g_wuss, g_palette, g_daydream_font, g_resources, &g_porter_duff_task); } -static void destroy_ball(void) { ball_destroy(&g_ball_task); } -static void destroy_text(void) { text_destroy(&g_text_task); } -static void destroy_blank(void) { blank_destroy(&g_blank_task); } -static void destroy_chars(void) { chars_destroy(&g_chars_task); } -static void destroy_palette(void) { palette_destroy(&g_palette_task); } -static void destroy_image(void) { image_destroy(&g_image_task); } -static void destroy_checker(void) { checker_destroy(&g_checker_task); } -static void destroy_curve(void) { curve_destroy(&g_curve_task); } -static void destroy_sofa(void) { sofa_destroy(&g_sofa_task); } -static void destroy_gradient(void) { gradient_destroy(&g_gradient_task); } +static void destroy_ball(void) { ball_destroy(&g_ball_task); } +static void destroy_text(void) { text_destroy(&g_text_task); } +static void destroy_blank(void) { blank_destroy(&g_blank_task); } +static void destroy_chars(void) { chars_destroy(&g_chars_task); } +static void destroy_palette(void) { palette_destroy(&g_palette_task); } +static void destroy_image(void) { image_destroy(&g_image_task); } +static void destroy_checker(void) { checker_destroy(&g_checker_task); } +static void destroy_curve(void) { curve_destroy(&g_curve_task); } +static void destroy_sofa(void) { sofa_destroy(&g_sofa_task); } +static void destroy_gradient(void) { gradient_destroy(&g_gradient_task); } static void destroy_porter_duff(void) { porter_duff_destroy(&g_porter_duff_task); } -static launcher_entry_t g_launcher_entries[] = +static const launcher_entry_t g_launcher_entries[] = { - { "Ball", spawn_ball, destroy_ball, false }, - { "Text", spawn_text, destroy_text, false }, - { "Blank", spawn_blank, destroy_blank, false }, - { "Chars", spawn_chars, destroy_chars, false }, - { "Palette", spawn_palette, destroy_palette, false }, - { "Image", spawn_image, destroy_image, false }, - { "Checker", spawn_checker, destroy_checker, false }, - { "Curve", spawn_curve, destroy_curve, false }, - { "Sofa", spawn_sofa, destroy_sofa, false }, - { "Gradient", spawn_gradient, destroy_gradient, false }, - { "Porter-Duff", spawn_porter_duff, destroy_porter_duff, false } + { "Ball", spawn_ball, destroy_ball }, + { "Text", spawn_text, destroy_text }, + { "Blank", spawn_blank, destroy_blank }, + { "Chars", spawn_chars, destroy_chars }, + { "Palette", spawn_palette, destroy_palette }, + { "Image", spawn_image, destroy_image }, + { "Checker", spawn_checker, destroy_checker }, + { "Curve", spawn_curve, destroy_curve }, + { "Sofa", spawn_sofa, destroy_sofa }, + { "Gradient", spawn_gradient, destroy_gradient }, + { "Porter-Duff", spawn_porter_duff, destroy_porter_duff } }; static wuss_button_t sdl_button_to_wuss(Uint8 button) @@ -414,7 +414,7 @@ static result_t wuss_interactive_test(const char *resources) } for (i = 0; i < NELEMS(g_launcher_entries); i++) - if (g_launcher_entries[i].running) + if (launcher_task.running[i]) g_launcher_entries[i].destroy(); launcher_destroy(&launcher_task); From 9c468a4d7722d1992a509c5440d661dffdff40fb Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sat, 29 Aug 2026 00:28:45 +0100 Subject: [PATCH 09/44] refactor(geom): add POINT and SIZE2D compound-literal macros Replace inline (point_t) { x, y } and (size2d_t) { w, h } compound literals across the tree with POINT(x, y) / SIZE2D(w, h) helpers defined alongside their types. Also folds in a pre-existing null-pointer guard in bmfont_get_info and minor alignment tidy-ups that were already in the working tree. Co-Authored-By: Claude Sonnet 5 --- include/geom/point.h | 3 + include/geom/size.h | 3 + libraries/framebuf/bitmap/load.c | 2 +- libraries/framebuf/bmfont/bmfont.c | 8 +- libraries/framebuf/bmfont/test/bmfont-test.c | 2 +- .../framebuf/composite/test/composite-test.c | 2 +- libraries/framebuf/curve/test/curve-test.c | 2 +- libraries/framebuf/screen/screen-draw.c | 2 +- libraries/framebuf/screen/test/screen-test.c | 2 +- libraries/wuss/furniture/drag-resize.c | 2 +- libraries/wuss/furniture/draw.c | 8 +- libraries/wuss/furniture/scroll-action.c | 4 +- libraries/wuss/furniture/toggle-action.c | 2 +- libraries/wuss/mouse-click.c | 10 +- libraries/wuss/mouse-move.c | 6 +- libraries/wuss/redraw.c | 2 +- libraries/wuss/scroll.c | 4 +- libraries/wuss/test/tasks/ball.c | 4 +- libraries/wuss/test/tasks/blank.c | 2 +- libraries/wuss/test/tasks/chars.c | 8 +- libraries/wuss/test/tasks/checker.c | 4 +- libraries/wuss/test/tasks/curve.c | 10 +- libraries/wuss/test/tasks/gradient.c | 4 +- libraries/wuss/test/tasks/image.c | 4 +- libraries/wuss/test/tasks/launcher.c | 2 +- libraries/wuss/test/tasks/palette.c | 4 +- libraries/wuss/test/tasks/porter-duff.c | 2 +- libraries/wuss/test/tasks/sofa.c | 2 +- libraries/wuss/test/tasks/text.c | 4 +- libraries/wuss/test/wuss-test.c | 308 +++++++++--------- libraries/wuss/window/move.c | 2 +- libraries/wuss/window/set-scroll.c | 2 +- 32 files changed, 217 insertions(+), 209 deletions(-) diff --git a/include/geom/point.h b/include/geom/point.h index be19615c..a1360842 100644 --- a/include/geom/point.h +++ b/include/geom/point.h @@ -10,4 +10,7 @@ typedef struct point } point_t; +/** Construct a point_t compound literal. */ +#define POINT(x, y) ((point_t) { (x), (y) }) + #endif /* GEOM_POINT_H */ diff --git a/include/geom/size.h b/include/geom/size.h index 202f474b..5d66d4c0 100644 --- a/include/geom/size.h +++ b/include/geom/size.h @@ -10,4 +10,7 @@ typedef struct size2d } size2d_t; +/** Construct a size2d_t compound literal. */ +#define SIZE2D(w, h) ((size2d_t) { (w), (h) }) + #endif /* GEOM_SIZE_H */ diff --git a/libraries/framebuf/bitmap/load.c b/libraries/framebuf/bitmap/load.c index 609a349e..b4b3fbaa 100644 --- a/libraries/framebuf/bitmap/load.c +++ b/libraries/framebuf/bitmap/load.c @@ -124,7 +124,7 @@ result_t bitmap_load_png(bitmap_t *bm, const char *filename) png_read_image(png_ptr, row_pointers); - bitmap_init(bm, (size2d_t) { pngwidth, pngheight }, + bitmap_init(bm, SIZE2D(pngwidth, pngheight), bm_fmt, bm_rowbytes, NULL, /* no palette */ diff --git a/libraries/framebuf/bmfont/bmfont.c b/libraries/framebuf/bmfont/bmfont.c index 5dd01fa4..b01d6508 100644 --- a/libraries/framebuf/bmfont/bmfont.c +++ b/libraries/framebuf/bmfont/bmfont.c @@ -515,8 +515,10 @@ void bmfont_destroy(bmfont_t *bmfont) void bmfont_get_info(bmfont_t *bmfont, int *width, int *height) { - *width = bmfont->charwidth; - *height = bmfont->charheight; + if (width) + *width = bmfont->charwidth; + if (height) + *height = bmfont->charheight; } int bmfont_get_count(bmfont_t *bmfont) @@ -1246,7 +1248,7 @@ result_t bmfont_draw(bmfont_t *bmfont, } } - *end_pos = (point_t) { x, pos->y }; + *end_pos = POINT(x, pos->y); } return result_OK; diff --git a/libraries/framebuf/bmfont/test/bmfont-test.c b/libraries/framebuf/bmfont/test/bmfont-test.c index f46302dd..c9caed05 100644 --- a/libraries/framebuf/bmfont/test/bmfont-test.c +++ b/libraries/framebuf/bmfont/test/bmfont-test.c @@ -753,7 +753,7 @@ result_t bmfont_test_one_format(const char *resources, goto Failure; } - bitmap_init(&state.bm, (size2d_t) { state.scr_width, state.scr_height }, + bitmap_init(&state.bm, SIZE2D(state.scr_width, state.scr_height), scr_fmt, scr_rowbytes, state.palette, diff --git a/libraries/framebuf/composite/test/composite-test.c b/libraries/framebuf/composite/test/composite-test.c index 58d9bb45..fdb0fe95 100644 --- a/libraries/framebuf/composite/test/composite-test.c +++ b/libraries/framebuf/composite/test/composite-test.c @@ -210,7 +210,7 @@ result_t composite_test(const char *resources) goto Failure; } - bitmap_init(&bigbitmap, (size2d_t) { WIDTH, HEIGHT }, FORMAT, scr_rowbytes, NULL, bigpixels); + bitmap_init(&bigbitmap, SIZE2D(WIDTH, HEIGHT), FORMAT, scr_rowbytes, NULL, bigpixels); rc = load_test_png(&bm[0], resources, "A"); /* source */ if (rc) diff --git a/libraries/framebuf/curve/test/curve-test.c b/libraries/framebuf/curve/test/curve-test.c index 31e83507..5cca0837 100644 --- a/libraries/framebuf/curve/test/curve-test.c +++ b/libraries/framebuf/curve/test/curve-test.c @@ -804,7 +804,7 @@ result_t curve_test_one_format(const char *resources, goto Failure; } - bitmap_init(&state.bm, (size2d_t) { state.scr_width, state.scr_height }, + bitmap_init(&state.bm, SIZE2D(state.scr_width, state.scr_height), scr_fmt, scr_rowbytes, state.palette, diff --git a/libraries/framebuf/screen/screen-draw.c b/libraries/framebuf/screen/screen-draw.c index afff1d28..50c9b453 100644 --- a/libraries/framebuf/screen/screen-draw.c +++ b/libraries/framebuf/screen/screen-draw.c @@ -214,7 +214,7 @@ void screen_draw_rect(screen_t *scr, void screen_draw_square(screen_t *scr, int x, int y, int size, colour_t colour) { - screen_draw_rect(scr, x, y, (size2d_t) { size, size }, colour); + screen_draw_rect(scr, x, y, SIZE2D(size, size), colour); } /* ----------------------------------------------------------------------- */ diff --git a/libraries/framebuf/screen/test/screen-test.c b/libraries/framebuf/screen/test/screen-test.c index 5b59876f..fb8cd919 100644 --- a/libraries/framebuf/screen/test/screen-test.c +++ b/libraries/framebuf/screen/test/screen-test.c @@ -85,7 +85,7 @@ static void testscreen_init(testscreen_t *ts) for (i = 0; i < WIDTH * HEIGHT; i++) ts->pixels[i] = BACKGROUND; - screen_init(&ts->scr, (size2d_t) { WIDTH, HEIGHT }, + screen_init(&ts->scr, SIZE2D(WIDTH, HEIGHT), pixelfmt_bgrx8888, WIDTH * (int) sizeof(ts->pixels[0]), NULL, diff --git a/libraries/wuss/furniture/drag-resize.c b/libraries/wuss/furniture/drag-resize.c index 61c2f93d..21773b94 100644 --- a/libraries/wuss/furniture/drag-resize.c +++ b/libraries/wuss/furniture/drag-resize.c @@ -18,5 +18,5 @@ void wuss__furniture_drag_resize(wuss_window_t *window, point_t p) width = CLAMP(width, min.w, MAX(window->doc.w, min.w)); height = CLAMP(height, min.h, MAX(window->doc.h, min.h)); - wuss_window_resize(window, (size2d_t) { width, height }); + wuss_window_resize(window, SIZE2D(width, height)); } diff --git a/libraries/wuss/furniture/draw.c b/libraries/wuss/furniture/draw.c index bc4dc31b..abd8968d 100644 --- a/libraries/wuss/furniture/draw.c +++ b/libraries/wuss/furniture/draw.c @@ -248,9 +248,9 @@ void wuss__furniture_draw(wuss_t *wuss, border = wuss->palette[wuss->furniture_colours.title.bg]; /* no dedicated outline class; matches titlebar chrome */ wuss->scr->clip = visible_clipped; - screen_draw_rect(wuss->scr, window->visible.x0, window->visible.y0, (size2d_t) { width, 1 }, border); - screen_draw_rect(wuss->scr, window->visible.x0, window->visible.y1 - 1, (size2d_t) { width, 1 }, border); - screen_draw_rect(wuss->scr, window->visible.x0, window->visible.y0, (size2d_t) { 1, height }, border); - screen_draw_rect(wuss->scr, window->visible.x1 - 1, window->visible.y0, (size2d_t) { 1, height }, border); + screen_draw_rect(wuss->scr, window->visible.x0, window->visible.y0, SIZE2D(width, 1), border); + screen_draw_rect(wuss->scr, window->visible.x0, window->visible.y1 - 1, SIZE2D(width, 1), border); + screen_draw_rect(wuss->scr, window->visible.x0, window->visible.y0, SIZE2D(1, height), border); + screen_draw_rect(wuss->scr, window->visible.x1 - 1, window->visible.y0, SIZE2D(1, height), border); } } diff --git a/libraries/wuss/furniture/scroll-action.c b/libraries/wuss/furniture/scroll-action.c index 9b9edf54..903b7648 100644 --- a/libraries/wuss/furniture/scroll-action.c +++ b/libraries/wuss/furniture/scroll-action.c @@ -78,7 +78,7 @@ void wuss__furniture_drag_sausage(wuss_window_t *window, new_scroll = max_scroll; if (horizontal) - wuss_window_set_scroll(window, (point_t) { new_scroll, window->scroll.y }); + wuss_window_set_scroll(window, POINT(new_scroll, window->scroll.y)); else - wuss_window_set_scroll(window, (point_t) { window->scroll.x, new_scroll }); + wuss_window_set_scroll(window, POINT(window->scroll.x, new_scroll)); } diff --git a/libraries/wuss/furniture/toggle-action.c b/libraries/wuss/furniture/toggle-action.c index 4d6a6fcd..0fda6c67 100644 --- a/libraries/wuss/furniture/toggle-action.c +++ b/libraries/wuss/furniture/toggle-action.c @@ -85,7 +85,7 @@ void wuss__furniture_toggle_size(wuss_window_t *window) if (!(window->flags & wuss_WINDOW_NO_RESIZE_BLIT) && window->wuss->z_order.next == &window->link && screen_copy_rect(window->wuss->scr, &before, - (point_t) { before.x0, before.y0 }, &copied)) + POINT(before.x0, before.y0), &copied)) { /* Topmost, and the screen format supports the blit: the window's * top-left never moves for a toggle, so re-blitting "before" onto diff --git a/libraries/wuss/mouse-click.c b/libraries/wuss/mouse-click.c index b7bee4be..a68c9bb0 100644 --- a/libraries/wuss/mouse-click.c +++ b/libraries/wuss/mouse-click.c @@ -34,7 +34,7 @@ result_t wuss_mouse_click(wuss_t *wuss, if (win == NULL) return result_OK; - region = wuss__furniture_hit_test(win, (point_t) { x, y }); + region = wuss__furniture_hit_test(win, POINT(x, y)); if (region == wuss_FURNITURE_CLOSE && action == wuss_MOUSE_DOWN && @@ -82,16 +82,16 @@ result_t wuss_mouse_click(wuss_t *wuss, wuss__furniture_toggle_size(win); break; case wuss_FURNITURE_VSCROLL_UP: - wuss__furniture_scroll_step(win, (point_t) { 0, -step }); + wuss__furniture_scroll_step(win, POINT(0, -step)); break; case wuss_FURNITURE_VSCROLL_DOWN: - wuss__furniture_scroll_step(win, (point_t) { 0, step }); + wuss__furniture_scroll_step(win, POINT(0, step)); break; case wuss_FURNITURE_HSCROLL_LEFT: - wuss__furniture_scroll_step(win, (point_t) { -step, 0 }); + wuss__furniture_scroll_step(win, POINT(-step, 0)); break; case wuss_FURNITURE_HSCROLL_RIGHT: - wuss__furniture_scroll_step(win, (point_t) { step, 0 }); + wuss__furniture_scroll_step(win, POINT(step, 0)); break; default: break; diff --git a/libraries/wuss/mouse-move.c b/libraries/wuss/mouse-move.c index 859bd5c5..9b73076d 100644 --- a/libraries/wuss/mouse-move.c +++ b/libraries/wuss/mouse-move.c @@ -19,7 +19,7 @@ result_t wuss_mouse_move(wuss_t *wuss, point_t p, wuss_window_t **hit) switch (wuss->furniture.drag_kind) { case wuss_FURNITURE_DRAG_RESIZE: - wuss__furniture_drag_resize(win, (point_t) { x, y }); + wuss__furniture_drag_resize(win, POINT(x, y)); break; case wuss_FURNITURE_DRAG_VSCROLL_SAUSAGE: @@ -32,7 +32,7 @@ result_t wuss_mouse_move(wuss_t *wuss, point_t p, wuss_window_t **hit) case wuss_FURNITURE_DRAG_MOVE: default: - wuss_window_move(win, (point_t) { x - wuss->furniture.drag.x, y - wuss->furniture.drag.y }); + wuss_window_move(win, POINT(x - wuss->furniture.drag.x, y - wuss->furniture.drag.y)); break; } @@ -46,7 +46,7 @@ result_t wuss_mouse_move(wuss_t *wuss, point_t p, wuss_window_t **hit) if (win == NULL) return result_OK; - if (wuss__furniture_hit_test(win, (point_t) { x, y }) != wuss_FURNITURE_CONTENT) + if (wuss__furniture_hit_test(win, POINT(x, y)) != wuss_FURNITURE_CONTENT) return result_OK; if (win->task.handle != NULL) diff --git a/libraries/wuss/redraw.c b/libraries/wuss/redraw.c index e3f403a3..769257a9 100644 --- a/libraries/wuss/redraw.c +++ b/libraries/wuss/redraw.c @@ -128,7 +128,7 @@ result_t wuss_redraw_dirty(wuss_t *wuss) { wuss->scr->clip = wuss->dirty[i]; screen_draw_rect(wuss->scr, - wuss->dirty[i].x0, wuss->dirty[i].y0, (size2d_t) { wuss->dirty[i].x1 - wuss->dirty[i].x0, wuss->dirty[i].y1 - wuss->dirty[i].y0 }, + wuss->dirty[i].x0, wuss->dirty[i].y0, SIZE2D(wuss->dirty[i].x1 - wuss->dirty[i].x0, wuss->dirty[i].y1 - wuss->dirty[i].y0), wuss->palette[wuss->backdrop]); } diff --git a/libraries/wuss/scroll.c b/libraries/wuss/scroll.c index f4d0979a..7be356e7 100644 --- a/libraries/wuss/scroll.c +++ b/libraries/wuss/scroll.c @@ -36,12 +36,12 @@ result_t wuss_scroll(wuss_t *wuss, point_t p, int delta, wuss_window_t **hit) event.data.scroll.point.y = y - content.y0 + win->scroll.y; event.data.scroll.delta = delta; - wuss__furniture_scroll_step(win, (point_t) { 0, delta }); + wuss__furniture_scroll_step(win, POINT(0, delta)); return win->task.handle(win, &event, win->task.task_data); } - wuss__furniture_scroll_step(win, (point_t) { 0, delta }); + wuss__furniture_scroll_step(win, POINT(0, delta)); return result_OK; } diff --git a/libraries/wuss/test/tasks/ball.c b/libraries/wuss/test/tasks/ball.c index 9e41f4ac..8d5fa59b 100644 --- a/libraries/wuss/test/tasks/ball.c +++ b/libraries/wuss/test/tasks/ball.c @@ -37,7 +37,7 @@ result_t ball_create(wuss_t *wuss, const colour_t *palette, ball_task_t *task) wuss_NO_BACKGROUND, &delegate, box_size(&box), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &task->window); } @@ -70,7 +70,7 @@ static result_t ball_redraw(const wuss_event_t *event, void *task_data) b = &bc->balls[i]; - screen_draw_rect(scr, bounds->x0 - sx + b->x - b->radius, bounds->y0 - sy + b->y - b->radius, (size2d_t) { b->radius * 2, b->radius * 2 }, bc->ball); + screen_draw_rect(scr, bounds->x0 - sx + b->x - b->radius, bounds->y0 - sy + b->y - b->radius, SIZE2D(b->radius * 2, b->radius * 2), bc->ball); } return result_OK; diff --git a/libraries/wuss/test/tasks/blank.c b/libraries/wuss/test/tasks/blank.c index 470a8817..344a59d3 100644 --- a/libraries/wuss/test/tasks/blank.c +++ b/libraries/wuss/test/tasks/blank.c @@ -34,7 +34,7 @@ result_t blank_create(wuss_t *wuss, int npalette, blank_task_t *task) palette_PICO8_GREEN, &delegate, box_size(&box), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &task->window); } diff --git a/libraries/wuss/test/tasks/chars.c b/libraries/wuss/test/tasks/chars.c index 3cc75d2a..6f2d6603 100644 --- a/libraries/wuss/test/tasks/chars.c +++ b/libraries/wuss/test/tasks/chars.c @@ -49,14 +49,14 @@ result_t chars_create(wuss_t *wuss, return wuss_window_create(wuss, &box, "Chars", - wuss_WINDOW_NO_RESIZE | + wuss_WINDOW_NO_RESIZE | wuss_WINDOW_NO_TOGGLE_SIZE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL, wuss_NO_BACKGROUND, &delegate, box_size(&box), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &task->window); } @@ -105,7 +105,7 @@ static result_t chars_redraw(const wuss_event_t *event, void *task_data) x = bounds->x0 - sx + col * cell_w; y = bounds->y0 - sy + row * cell_h; - screen_draw_rect(scr, x, y, (size2d_t) { cell_w, cell_h }, cc->bg); + screen_draw_rect(scr, x, y, SIZE2D(cell_w, cell_h), cc->bg); if (i < first || i >= first + count) continue; /* no glyph for this byte value: leave the cell blank */ @@ -121,7 +121,7 @@ static result_t chars_redraw(const wuss_event_t *event, void *task_data) result_t chars_handle(wuss_window_t *window, const wuss_event_t *event, - void *task_data) + void *task_data) { if (event->kind == wuss_EVENT_CLOSE) { diff --git a/libraries/wuss/test/tasks/checker.c b/libraries/wuss/test/tasks/checker.c index 71a22ae6..d61baa56 100644 --- a/libraries/wuss/test/tasks/checker.c +++ b/libraries/wuss/test/tasks/checker.c @@ -41,7 +41,7 @@ result_t checker_create(wuss_t *wuss, wuss_NO_BACKGROUND, &delegate, box_size(&box), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &task->window); if (rc != result_OK) return rc; @@ -55,7 +55,7 @@ result_t checker_create(wuss_t *wuss, wuss_NO_BACKGROUND, &delegate, box_size(&box), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &task->window2); if (rc != result_OK) { diff --git a/libraries/wuss/test/tasks/curve.c b/libraries/wuss/test/tasks/curve.c index 9fc73ef6..24572802 100644 --- a/libraries/wuss/test/tasks/curve.c +++ b/libraries/wuss/test/tasks/curve.c @@ -34,10 +34,10 @@ result_t curve_create(wuss_t *wuss, task->nsegments = CURVE_SEGMENTS_DEFAULT; task->dragging = -1; - task->points[0] = (point_t) { 10, 10 }; - task->points[1] = (point_t) { 10, 140 }; - task->points[2] = (point_t) { 210, 10 }; - task->points[3] = (point_t) { 210, 140 }; + task->points[0] = POINT(10, 10); + task->points[1] = POINT(10, 140); + task->points[2] = POINT(210, 10); + task->points[3] = POINT(210, 140); delegate = wuss_task_start(curve_handle, task); /* curve_redraw paints its own background */ box = (box_t) BOX_POS_SIZE(20, 260, 220, 160); @@ -49,7 +49,7 @@ result_t curve_create(wuss_t *wuss, wuss_NO_BACKGROUND, &delegate, box_size(&box), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &task->window); } diff --git a/libraries/wuss/test/tasks/gradient.c b/libraries/wuss/test/tasks/gradient.c index 6e171fa6..491e8460 100644 --- a/libraries/wuss/test/tasks/gradient.c +++ b/libraries/wuss/test/tasks/gradient.c @@ -45,8 +45,8 @@ result_t gradient_create(wuss_t *wuss, gradient_task_t *task) wuss_WINDOW_NONE, wuss_NO_BACKGROUND, &delegate, - (size2d_t) { GRADIENT_DOC_WIDTH, GRADIENT_DOC_HEIGHT }, - (size2d_t) { 0, 0 }, + SIZE2D(GRADIENT_DOC_WIDTH, GRADIENT_DOC_HEIGHT), + SIZE2D(0, 0), &task->window); } diff --git a/libraries/wuss/test/tasks/image.c b/libraries/wuss/test/tasks/image.c index a804b49b..22d5d801 100644 --- a/libraries/wuss/test/tasks/image.c +++ b/libraries/wuss/test/tasks/image.c @@ -42,8 +42,8 @@ result_t image_create(wuss_t *wuss, wuss_WINDOW_NONE, palette_PICO8_BLACK, &delegate, - (size2d_t) { task->bitmap.size.w, task->bitmap.size.h }, - (size2d_t) { 0, 0 }, + SIZE2D(task->bitmap.size.w, task->bitmap.size.h), + SIZE2D(0, 0), &task->window); } diff --git a/libraries/wuss/test/tasks/launcher.c b/libraries/wuss/test/tasks/launcher.c index ec779f4b..3e3e93aa 100644 --- a/libraries/wuss/test/tasks/launcher.c +++ b/libraries/wuss/test/tasks/launcher.c @@ -50,7 +50,7 @@ result_t launcher_create(wuss_t *wuss, wuss_NO_BACKGROUND, &delegate, box_size(&box), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &task->window); } diff --git a/libraries/wuss/test/tasks/palette.c b/libraries/wuss/test/tasks/palette.c index 0c781b41..0ba14831 100644 --- a/libraries/wuss/test/tasks/palette.c +++ b/libraries/wuss/test/tasks/palette.c @@ -33,7 +33,7 @@ result_t palette_create(wuss_t *wuss, palette_PICO8_BLACK, &delegate, box_size(&box), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &task->window); } @@ -78,7 +78,7 @@ static result_t palette_redraw(const wuss_event_t *event, void *task_data) x = bounds->x0 - sx + col * cell_w; y = bounds->y0 - sy + row * cell_h; - screen_draw_rect(scr, x, y, (size2d_t) { cell_w, cell_h }, pc->palette[i]); + screen_draw_rect(scr, x, y, SIZE2D(cell_w, cell_h), pc->palette[i]); } return result_OK; diff --git a/libraries/wuss/test/tasks/porter-duff.c b/libraries/wuss/test/tasks/porter-duff.c index 82cc8e95..8ff10b92 100644 --- a/libraries/wuss/test/tasks/porter-duff.c +++ b/libraries/wuss/test/tasks/porter-duff.c @@ -201,7 +201,7 @@ result_t porter_duff_create(wuss_t *wuss, wuss_NO_BACKGROUND, &delegate, box_size(&box), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &task->window); if (rc != result_OK) goto free_dst; diff --git a/libraries/wuss/test/tasks/sofa.c b/libraries/wuss/test/tasks/sofa.c index 8d25a6da..c006691e 100644 --- a/libraries/wuss/test/tasks/sofa.c +++ b/libraries/wuss/test/tasks/sofa.c @@ -278,7 +278,7 @@ result_t sofa_create(wuss_t *wuss, const colour_t *palette, sofa_task_t *task) wuss_NO_BACKGROUND, &delegate, box_size(&box), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &task->window); } diff --git a/libraries/wuss/test/tasks/text.c b/libraries/wuss/test/tasks/text.c index 41fc2e4f..c96d9274 100644 --- a/libraries/wuss/test/tasks/text.c +++ b/libraries/wuss/test/tasks/text.c @@ -54,7 +54,7 @@ result_t text_create(wuss_t *wuss, palette_PICO8_BLUE, &delegate, box_size(&box), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &task->window); return rc; @@ -156,7 +156,7 @@ static result_t text_idle(void *task_data) angle = tcx->frame_count * (2.0 * M_PI / TEXT_RESIZE_PERIOD_FRAMES); width = tcx->base_width + (int) (TEXT_RESIZE_AMPLITUDE * sin(angle)); - rc = wuss_window_resize(tcx->window, (size2d_t) { width, height }); + rc = wuss_window_resize(tcx->window, SIZE2D(width, height)); if (rc != result_OK) logf_warning("text_idle: wuss_window_resize(%d, %d) failed", width, height); diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index c88a7e0c..686cd217 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -191,9 +191,9 @@ static result_t wuss_interactive_test(const char *resources) goto Failure; #if WUSS_TEST_32BPP - rc = bitmap_init(&bm, (size2d_t) { scr_width, scr_height }, pixelfmt_bgrx8888, rowbytes, palette, pixels); + rc = bitmap_init(&bm, SIZE2D(scr_width, scr_height), pixelfmt_bgrx8888, rowbytes, palette, pixels); #else - rc = bitmap_init(&bm, (size2d_t) { scr_width, scr_height }, pixelfmt_p4, rowbytes, palette, pixels); + rc = bitmap_init(&bm, SIZE2D(scr_width, scr_height), pixelfmt_p4, rowbytes, palette, pixels); #endif if (rc != result_OK) goto Failure; @@ -308,7 +308,7 @@ static result_t wuss_interactive_test(const char *resources) int x, y; sdl_pos_to_scr(window, scr_width, scr_height, event.button.x, event.button.y, &x, &y); - wuss_mouse_click(wuss, (point_t) { x, y }, sdl_button_to_wuss(event.button.button), wuss_MOUSE_DOWN, NULL); + wuss_mouse_click(wuss, POINT(x, y), sdl_button_to_wuss(event.button.button), wuss_MOUSE_DOWN, NULL); } break; @@ -317,7 +317,7 @@ static result_t wuss_interactive_test(const char *resources) int x, y; sdl_pos_to_scr(window, scr_width, scr_height, event.button.x, event.button.y, &x, &y); - wuss_mouse_click(wuss, (point_t) { x, y }, sdl_button_to_wuss(event.button.button), wuss_MOUSE_UP, NULL); + wuss_mouse_click(wuss, POINT(x, y), sdl_button_to_wuss(event.button.button), wuss_MOUSE_UP, NULL); } break; @@ -326,7 +326,7 @@ static result_t wuss_interactive_test(const char *resources) int x, y; sdl_pos_to_scr(window, scr_width, scr_height, event.motion.x, event.motion.y, &x, &y); - wuss_mouse_move(wuss, (point_t) { x, y }, NULL); + wuss_mouse_move(wuss, POINT(x, y), NULL); } break; @@ -335,7 +335,7 @@ static result_t wuss_interactive_test(const char *resources) int x, y; sdl_pos_to_scr(window, scr_width, scr_height, event.wheel.mouse_x, event.wheel.mouse_y, &x, &y); - wuss_scroll(wuss, (point_t) { x, y }, (int) event.wheel.y, NULL); + wuss_scroll(wuss, POINT(x, y), (int) event.wheel.y, NULL); } break; @@ -549,8 +549,8 @@ result_t wuss_test(const char *resources) wuss_t *wuss; wuss_config_t bad_config; wuss_t *bad_wuss; - test_task_t tc_a, tc_b, tc_c, tc_d; - wuss_task_t delegate_a, delegate_b, delegate_c, delegate_d; + test_task_t tc_a, tc_b, tc_c, tc_d; + wuss_task_t delegate_a, delegate_b, delegate_c, delegate_d; box_t box_a, box_b, box_c, box_d; wuss_window_t *win_a, *win_b, *win_c, *win_d; wuss_window_t *hit; @@ -566,7 +566,7 @@ result_t wuss_test(const char *resources) if (pixels == NULL) goto Failure; - rc = bitmap_init(&bm, (size2d_t) { 200, 200 }, pixelfmt_bgrx8888, rowbytes, NULL, pixels); + rc = bitmap_init(&bm, SIZE2D(200, 200), pixelfmt_bgrx8888, rowbytes, NULL, pixels); if (rc != result_OK) goto Failure; @@ -618,7 +618,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, NULL, box_size(&box_a), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &win_a); if (rc != result_WUSS_TOO_SMALL) goto Failure; @@ -628,7 +628,7 @@ result_t wuss_test(const char *resources) tc_a.redraw_count = 0; tc_a.mouse_count = 0; tc_a.open_count = 0; - delegate_a.handle = test_handle; + delegate_a.handle = test_handle; delegate_a.task_data = &tc_a; box_a.x0 = 0; @@ -644,14 +644,14 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_a, box_size(&box_a), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &win_a); if (rc != result_OK) goto Failure; tc_b.redraw_count = 0; tc_b.mouse_count = 0; - delegate_b.handle = test_handle; + delegate_b.handle = test_handle; delegate_b.task_data = &tc_b; box_b.x0 = 50; @@ -667,7 +667,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_b, box_size(&box_b), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &win_b); if (rc != result_OK) goto Failure; @@ -736,7 +736,7 @@ result_t wuss_test(const char *resources) printf("test: z-order hit test and local coordinate translation (B on top)\n"); tc_b.mouse_count = 0; - rc = wuss_mouse_click(wuss, (point_t) { 75, 75 }, wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); + rc = wuss_mouse_click(wuss, POINT(75, 75), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); if (rc != result_OK) goto Failure; if (hit != win_b) @@ -746,14 +746,14 @@ result_t wuss_test(const char *resources) if (tc_b.last_action != wuss_MOUSE_DOWN || tc_b.last_x != 25 || tc_b.last_y != 25) goto Failure; - rc = wuss_mouse_click(wuss, (point_t) { 75, 75 }, wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); + rc = wuss_mouse_click(wuss, POINT(75, 75), wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); if (rc != result_OK) goto Failure; printf("test: clicking a window's close icon sends wuss_EVENT_CLOSE, not a drag\n"); tc_a.close_count = 0; - rc = wuss_mouse_click(wuss, (point_t) { 6, 11 }, wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* A's close icon */ + rc = wuss_mouse_click(wuss, POINT(6, 11), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* A's close icon */ if (rc != result_OK) goto Failure; if (hit != win_a) @@ -761,7 +761,7 @@ result_t wuss_test(const char *resources) if (tc_a.close_count != 1) goto Failure; - rc = wuss_mouse_click(wuss, (point_t) { 31, 36 }, wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); /* if the close click had started a drag, this would move A */ + rc = wuss_mouse_click(wuss, POINT(31, 36), wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); /* if the close click had started a drag, this would move A */ if (rc != result_OK) goto Failure; @@ -772,20 +772,20 @@ result_t wuss_test(const char *resources) printf("test: click-to-front changes subsequent overlap hits\n"); tc_a.mouse_count = 0; - rc = wuss_mouse_click(wuss, (point_t) { 31, 11 }, wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* A's titlebar, above its content, clear of the close icon */ + rc = wuss_mouse_click(wuss, POINT(31, 11), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* A's titlebar, above its content, clear of the close icon */ if (rc != result_OK) goto Failure; if (hit != win_a) goto Failure; - rc = wuss_mouse_click(wuss, (point_t) { 31, 11 }, wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); + rc = wuss_mouse_click(wuss, POINT(31, 11), wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); if (rc != result_OK) goto Failure; printf("test: content click does not change z-order\n"); tc_b.mouse_count = 0; - rc = wuss_mouse_click(wuss, (point_t) { 120, 120 }, wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* B's content, only within B */ + rc = wuss_mouse_click(wuss, POINT(120, 120), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* B's content, only within B */ if (rc != result_OK) goto Failure; if (hit != win_b) @@ -793,12 +793,12 @@ result_t wuss_test(const char *resources) if (tc_b.mouse_count != 1) goto Failure; - rc = wuss_mouse_click(wuss, (point_t) { 120, 120 }, wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); + rc = wuss_mouse_click(wuss, POINT(120, 120), wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); if (rc != result_OK) goto Failure; tc_a.mouse_count = 0; - rc = wuss_mouse_click(wuss, (point_t) { 75, 75 }, wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* A still topmost: B's content click above didn't bring it to front */ + rc = wuss_mouse_click(wuss, POINT(75, 75), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* A still topmost: B's content click above didn't bring it to front */ if (rc != result_OK) goto Failure; if (hit != win_a) @@ -806,14 +806,14 @@ result_t wuss_test(const char *resources) if (tc_a.mouse_count != 1 || tc_a.last_x != 74 || tc_a.last_y != 54) goto Failure; - rc = wuss_mouse_click(wuss, (point_t) { 75, 75 }, wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); + rc = wuss_mouse_click(wuss, POINT(75, 75), wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); if (rc != result_OK) goto Failure; printf("test: titlebar click starts a drag, not delivered as content\n"); tc_a.mouse_count = 0; - rc = wuss_mouse_click(wuss, (point_t) { 31, 11 }, wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* A's titlebar, A already topmost, clear of the close icon */ + rc = wuss_mouse_click(wuss, POINT(31, 11), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* A's titlebar, A already topmost, clear of the close icon */ if (rc != result_OK) goto Failure; if (hit != win_a) @@ -829,7 +829,7 @@ result_t wuss_test(const char *resources) before_a = tc_a.redraw_count; before_b = tc_b.redraw_count; - rc = wuss_mouse_move(wuss, (point_t) { 31, 36 }, &hit); + rc = wuss_mouse_move(wuss, POINT(31, 36), &hit); if (rc != result_OK) goto Failure; if (hit != win_a) @@ -854,7 +854,7 @@ result_t wuss_test(const char *resources) printf("test: mouse-up ends the drag\n"); - rc = wuss_mouse_click(wuss, (point_t) { 31, 36 }, wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); + rc = wuss_mouse_click(wuss, POINT(31, 36), wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); if (rc != result_OK) goto Failure; if (hit != win_a) @@ -862,13 +862,13 @@ result_t wuss_test(const char *resources) printf("test: Adjust-drag moves a window without bringing it to front\n"); - rc = wuss_mouse_click(wuss, (point_t) { 140, 35 }, wuss_BUTTON_ADJUST, wuss_MOUSE_DOWN, &hit); /* B's titlebar, clear of A */ + rc = wuss_mouse_click(wuss, POINT(140, 35), wuss_BUTTON_ADJUST, wuss_MOUSE_DOWN, &hit); /* B's titlebar, clear of A */ if (rc != result_OK) goto Failure; if (hit != win_b) goto Failure; - rc = wuss_mouse_move(wuss, (point_t) { 145, 60 }, &hit); + rc = wuss_mouse_move(wuss, POINT(145, 60), &hit); if (rc != result_OK) goto Failure; if (hit != win_b) @@ -878,23 +878,23 @@ result_t wuss_test(const char *resources) if (visible.x0 != 54 || visible.y0 != 54) goto Failure; - rc = wuss_mouse_click(wuss, (point_t) { 145, 60 }, wuss_BUTTON_ADJUST, wuss_MOUSE_UP, &hit); + rc = wuss_mouse_click(wuss, POINT(145, 60), wuss_BUTTON_ADJUST, wuss_MOUSE_UP, &hit); if (rc != result_OK) goto Failure; if (hit != win_b) goto Failure; - rc = wuss_mouse_click(wuss, (point_t) { 75, 75 }, wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* within both A and B; A still topmost */ + rc = wuss_mouse_click(wuss, POINT(75, 75), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* within both A and B; A still topmost */ if (rc != result_OK) goto Failure; if (hit != win_a) goto Failure; - rc = wuss_mouse_click(wuss, (point_t) { 75, 75 }, wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); + rc = wuss_mouse_click(wuss, POINT(75, 75), wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); if (rc != result_OK) goto Failure; - rc = wuss_mouse_move(wuss, (point_t) { 200, 200 }, &hit); /* off all windows, drag must have ended */ + rc = wuss_mouse_move(wuss, POINT(200, 200), &hit); /* off all windows, drag must have ended */ if (rc != result_OK) goto Failure; if (hit != NULL) @@ -909,13 +909,13 @@ result_t wuss_test(const char *resources) printf("test: window_resize valid and too-small cases\n"); - rc = wuss_window_resize(win_a, (size2d_t) { 50, 0 }); /* zero-height content is invalid */ + rc = wuss_window_resize(win_a, SIZE2D(50, 0)); /* zero-height content is invalid */ if (rc != result_WUSS_TOO_SMALL) goto Failure; if (tc_a.open_count != 1) goto Failure; /* rejected resize: no wuss_EVENT_OPEN */ - rc = wuss_window_resize(win_a, (size2d_t) { 50, 50 }); + rc = wuss_window_resize(win_a, SIZE2D(50, 50)); if (rc != result_OK) goto Failure; if (tc_a.open_count != 2) @@ -953,7 +953,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_d, box_size(&box_d), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &win_d); if (rc != result_OK) goto Failure; @@ -965,7 +965,7 @@ result_t wuss_test(const char *resources) printf("test: click within a title-less window's top edge is delivered as content, not a drag\n"); - rc = wuss_mouse_click(wuss, (point_t) { 5, 165 }, wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); + rc = wuss_mouse_click(wuss, POINT(5, 165), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); if (rc != result_OK) goto Failure; if (hit != win_d) @@ -973,21 +973,21 @@ result_t wuss_test(const char *resources) if (tc_d.mouse_count != 1 || tc_d.last_action != wuss_MOUSE_DOWN || tc_d.last_x != 5 || tc_d.last_y != 5) goto Failure; - rc = wuss_mouse_click(wuss, (point_t) { 5, 165 }, wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); + rc = wuss_mouse_click(wuss, POINT(5, 165), wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); if (rc != result_OK) goto Failure; printf("test: content click on a title-less window does not change z-order\n"); { - test_task_t tc_e, tc_f; - wuss_task_t delegate_e, delegate_f; + test_task_t tc_e, tc_f; + wuss_task_t delegate_e, delegate_f; box_t box_e, box_f; wuss_window_t *win_e, *win_f; tc_e.redraw_count = 0; tc_e.mouse_count = 0; - delegate_e.handle = test_handle; + delegate_e.handle = test_handle; delegate_e.task_data = &tc_e; box_e.x0 = 100; box_e.y0 = 0; @@ -1001,14 +1001,14 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_e, box_size(&box_e), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &win_e); if (rc != result_OK) goto Failure; tc_f.redraw_count = 0; tc_f.mouse_count = 0; - delegate_f.handle = test_handle; + delegate_f.handle = test_handle; delegate_f.task_data = &tc_f; box_f.x0 = 130; box_f.y0 = 20; @@ -1022,30 +1022,30 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_f, box_size(&box_f), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &win_f); if (rc != result_OK) goto Failure; /* F was created after E, so F is topmost; clicking E's exposed content * (outside the overlap) is delivered to E but must not raise it */ - rc = wuss_mouse_click(wuss, (point_t) { 110, 10 }, wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* within E only */ + rc = wuss_mouse_click(wuss, POINT(110, 10), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* within E only */ if (rc != result_OK) goto Failure; if (hit != win_e) goto Failure; - rc = wuss_mouse_click(wuss, (point_t) { 110, 10 }, wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); + rc = wuss_mouse_click(wuss, POINT(110, 10), wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); if (rc != result_OK) goto Failure; - rc = wuss_mouse_click(wuss, (point_t) { 135, 25 }, wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* overlap: F still on top */ + rc = wuss_mouse_click(wuss, POINT(135, 25), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* overlap: F still on top */ if (rc != result_OK) goto Failure; if (hit != win_f) goto Failure; - rc = wuss_mouse_click(wuss, (point_t) { 135, 25 }, wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); + rc = wuss_mouse_click(wuss, POINT(135, 25), wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); if (rc != result_OK) goto Failure; @@ -1068,15 +1068,15 @@ result_t wuss_test(const char *resources) printf("test: moving/resizing a window entirely behind an occluder has no visible effect\n"); { - test_task_t tc_h, tc_g; - wuss_task_t delegate_h, delegate_g; + test_task_t tc_h, tc_g; + wuss_task_t delegate_h, delegate_g; box_t box_h, box_g; wuss_window_t *win_h, *win_g; - int before_h, before_g; + int before_h, before_g; tc_h.redraw_count = 0; tc_h.mouse_count = 0; - delegate_h.handle = test_handle; + delegate_h.handle = test_handle; delegate_h.task_data = &tc_h; box_h.x0 = 10; box_h.y0 = 10; @@ -1090,14 +1090,14 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_h, box_size(&box_h), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &win_h); if (rc != result_OK) goto Failure; tc_g.redraw_count = 0; tc_g.mouse_count = 0; - delegate_g.handle = test_handle; + delegate_g.handle = test_handle; delegate_g.task_data = &tc_g; box_g.x0 = 0; box_g.y0 = 0; @@ -1111,7 +1111,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_g, box_size(&box_g), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &win_g); if (rc != result_OK) goto Failure; @@ -1123,7 +1123,7 @@ result_t wuss_test(const char *resources) before_h = tc_h.redraw_count; before_g = tc_g.redraw_count; - wuss_window_move(win_h, (point_t) { 60, 60 }); /* still entirely within G's footprint */ + wuss_window_move(win_h, POINT(60, 60)); /* still entirely within G's footprint */ if (wuss_get_dirty_count(wuss) != 0) goto Failure; @@ -1133,7 +1133,7 @@ result_t wuss_test(const char *resources) if (tc_h.redraw_count != before_h || tc_g.redraw_count != before_g) goto Failure; /* nothing visible changed: no redraw of either window */ - rc = wuss_window_resize(win_h, (size2d_t) { 25, 25 }); /* still entirely within G's footprint */ + rc = wuss_window_resize(win_h, SIZE2D(25, 25)); /* still entirely within G's footprint */ if (rc != result_OK) goto Failure; if (wuss_get_dirty_count(wuss) != 0) @@ -1152,14 +1152,14 @@ result_t wuss_test(const char *resources) printf("test: bring-to-front only invalidates the newly-uncovered part\n"); { - test_task_t tc_i, tc_j; - wuss_task_t delegate_i, delegate_j; + test_task_t tc_i, tc_j; + wuss_task_t delegate_i, delegate_j; box_t box_i, box_j, dirty; wuss_window_t *win_i, *win_j; tc_i.redraw_count = 0; tc_i.mouse_count = 0; - delegate_i.handle = test_handle; + delegate_i.handle = test_handle; delegate_i.task_data = &tc_i; box_i.x0 = 0; box_i.y0 = 0; @@ -1173,14 +1173,14 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_i, box_size(&box_i), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &win_i); if (rc != result_OK) goto Failure; tc_j.redraw_count = 0; tc_j.mouse_count = 0; - delegate_j.handle = test_handle; + delegate_j.handle = test_handle; delegate_j.task_data = &tc_j; box_j.x0 = 50; box_j.y0 = 0; @@ -1194,7 +1194,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_j, box_size(&box_j), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &win_j); if (rc != result_OK) goto Failure; @@ -1223,15 +1223,15 @@ result_t wuss_test(const char *resources) printf("test: dragging off-screen and back on repaints the reappearing edge\n"); { - test_task_t tc_m; - wuss_task_t delegate_m; + test_task_t tc_m; + wuss_task_t delegate_m; box_t box_m; wuss_window_t *win_m; int before_m; tc_m.redraw_count = 0; tc_m.mouse_count = 0; - delegate_m.handle = test_handle; + delegate_m.handle = test_handle; delegate_m.task_data = &tc_m; box_m.x0 = 10; box_m.y0 = 10; @@ -1245,7 +1245,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_m, box_size(&box_m), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &win_m); if (rc != result_OK) goto Failure; @@ -1254,14 +1254,14 @@ result_t wuss_test(const char *resources) if (rc != result_OK) goto Failure; - wuss_window_move(win_m, (point_t) { -40, 10 }); /* slide left until half of M is off the left edge */ + wuss_window_move(win_m, POINT(-40, 10)); /* slide left until half of M is off the left edge */ rc = wuss_redraw_dirty(wuss); /* flush the vacated-sliver repaint from this move */ if (rc != result_OK) goto Failure; before_m = tc_m.redraw_count; - wuss_window_move(win_m, (point_t) { 10, 10 }); /* slide back: the part that re-enters the screen was + wuss_window_move(win_m, POINT(10, 10)); /* slide back: the part that re-enters the screen was * never blitted (its source pixels were off-screen), * so it must be a real task redraw, not a blit */ if (wuss_get_dirty_count(wuss) == 0) @@ -1298,7 +1298,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_h, box_size(&box_h), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &win_h); if (rc != result_OK) goto Failure; @@ -1317,7 +1317,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_g, box_size(&box_g), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &win_g); if (rc != result_OK) goto Failure; @@ -1326,53 +1326,53 @@ result_t wuss_test(const char *resources) * corner) never falls under H, so it stays clickable either way */ wuss_window_get_visible_bounds(win_g, &visible); - rc = wuss_mouse_click(wuss, (point_t) { 145, 65 }, wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* overlap of G and H */ + rc = wuss_mouse_click(wuss, POINT(145, 65), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* overlap of G and H */ if (rc != result_OK) goto Failure; if (hit != win_g) goto Failure; - rc = wuss_mouse_click(wuss, (point_t) { 145, 65 }, wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); + rc = wuss_mouse_click(wuss, POINT(145, 65), wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); if (rc != result_OK) goto Failure; - rc = wuss_mouse_click(wuss, (point_t) { visible.x0 + 5, visible.y0 + 5 }, wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* G's back icon */ + rc = wuss_mouse_click(wuss, POINT(visible.x0 + 5, visible.y0 + 5), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* G's back icon */ if (rc != result_OK) goto Failure; if (hit != win_g) goto Failure; - rc = wuss_mouse_click(wuss, (point_t) { visible.x0 + 5, visible.y0 + 5 }, wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); + rc = wuss_mouse_click(wuss, POINT(visible.x0 + 5, visible.y0 + 5), wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); if (rc != result_OK) goto Failure; - rc = wuss_mouse_click(wuss, (point_t) { 145, 65 }, wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* overlap: H now on top */ + rc = wuss_mouse_click(wuss, POINT(145, 65), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* overlap: H now on top */ if (rc != result_OK) goto Failure; if (hit != win_h) /* G was sent to back */ goto Failure; - rc = wuss_mouse_click(wuss, (point_t) { 145, 65 }, wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); + rc = wuss_mouse_click(wuss, POINT(145, 65), wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); if (rc != result_OK) goto Failure; - rc = wuss_mouse_click(wuss, (point_t) { visible.x0 + 5, visible.y0 + 5 }, wuss_BUTTON_ADJUST, wuss_MOUSE_DOWN, &hit); /* Adjust-click G's back icon */ + rc = wuss_mouse_click(wuss, POINT(visible.x0 + 5, visible.y0 + 5), wuss_BUTTON_ADJUST, wuss_MOUSE_DOWN, &hit); /* Adjust-click G's back icon */ if (rc != result_OK) goto Failure; if (hit != win_g) goto Failure; - rc = wuss_mouse_click(wuss, (point_t) { visible.x0 + 5, visible.y0 + 5 }, wuss_BUTTON_ADJUST, wuss_MOUSE_UP, &hit); + rc = wuss_mouse_click(wuss, POINT(visible.x0 + 5, visible.y0 + 5), wuss_BUTTON_ADJUST, wuss_MOUSE_UP, &hit); if (rc != result_OK) goto Failure; - rc = wuss_mouse_click(wuss, (point_t) { 145, 65 }, wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* overlap: G back on top */ + rc = wuss_mouse_click(wuss, POINT(145, 65), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* overlap: G back on top */ if (rc != result_OK) goto Failure; if (hit != win_g) /* Adjust-click on the back icon brought G back to front */ goto Failure; - rc = wuss_mouse_click(wuss, (point_t) { 145, 65 }, wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); + rc = wuss_mouse_click(wuss, POINT(145, 65), wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); if (rc != result_OK) goto Failure; @@ -1380,17 +1380,17 @@ result_t wuss_test(const char *resources) wuss_window_get_content_bounds(win_g, &content); /* G's doc_width/doc_height are 50x50, same as its initial content size */ - rc = wuss_mouse_click(wuss, (point_t) { visible.x1 - 3, visible.y1 - 3 }, wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* G's resize icon */ + rc = wuss_mouse_click(wuss, POINT(visible.x1 - 3, visible.y1 - 3), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* G's resize icon */ if (rc != result_OK) goto Failure; if (hit != win_g) goto Failure; - rc = wuss_mouse_move(wuss, (point_t) { content.x0 + 50, content.y0 + 50 }, &hit); /* drag to exactly the doc extent */ + rc = wuss_mouse_move(wuss, POINT(content.x0 + 50, content.y0 + 50), &hit); /* drag to exactly the doc extent */ if (rc != result_OK) goto Failure; - rc = wuss_mouse_click(wuss, (point_t) { content.x0 + 50, content.y0 + 50 }, wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); + rc = wuss_mouse_click(wuss, POINT(content.x0 + 50, content.y0 + 50), wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); if (rc != result_OK) goto Failure; @@ -1398,15 +1398,15 @@ result_t wuss_test(const char *resources) width = content.x1 - content.x0; height = content.y1 - content.y0; - rc = wuss_mouse_click(wuss, (point_t) { visible.x1 - 3, visible.y1 - 3 }, wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* G's resize icon again */ + rc = wuss_mouse_click(wuss, POINT(visible.x1 - 3, visible.y1 - 3), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* G's resize icon again */ if (rc != result_OK) goto Failure; - rc = wuss_mouse_move(wuss, (point_t) { content.x0 + 500, content.y0 + 500 }, &hit); /* drag far past the doc extent */ + rc = wuss_mouse_move(wuss, POINT(content.x0 + 500, content.y0 + 500), &hit); /* drag far past the doc extent */ if (rc != result_OK) goto Failure; - rc = wuss_mouse_click(wuss, (point_t) { content.x0 + 500, content.y0 + 500 }, wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); + rc = wuss_mouse_click(wuss, POINT(content.x0 + 500, content.y0 + 500), wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); if (rc != result_OK) goto Failure; @@ -1436,7 +1436,7 @@ result_t wuss_test(const char *resources) rc = wuss_window_create(wuss, &box_m, "M", wuss_WINDOW_NONE, wuss_NO_BACKGROUND, &delegate_m, - (size2d_t) { 200, 200 }, (size2d_t) { 80, 60 }, + SIZE2D(200, 200), SIZE2D(80, 60), &win_m); if (rc != result_OK) goto Failure; @@ -1444,17 +1444,17 @@ result_t wuss_test(const char *resources) wuss_window_get_visible_bounds(win_m, &visible); wuss_window_get_content_bounds(win_m, &content); - rc = wuss_mouse_click(wuss, (point_t) { visible.x1 - 3, visible.y1 - 3 }, wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* M's resize icon */ + rc = wuss_mouse_click(wuss, POINT(visible.x1 - 3, visible.y1 - 3), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* M's resize icon */ if (rc != result_OK) goto Failure; if (hit != win_m) goto Failure; - rc = wuss_mouse_move(wuss, (point_t) { content.x0 + 5, content.y0 + 5 }, &hit); /* drag far inside min_doc */ + rc = wuss_mouse_move(wuss, POINT(content.x0 + 5, content.y0 + 5), &hit); /* drag far inside min_doc */ if (rc != result_OK) goto Failure; - rc = wuss_mouse_click(wuss, (point_t) { content.x0 + 5, content.y0 + 5 }, wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); + rc = wuss_mouse_click(wuss, POINT(content.x0 + 5, content.y0 + 5), wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); if (rc != result_OK) goto Failure; @@ -1487,7 +1487,7 @@ result_t wuss_test(const char *resources) rc = wuss_window_create(wuss, &box_t_win, "T", wuss_WINDOW_NONE, wuss_NO_BACKGROUND, &delegate_t, - (size2d_t) { 200, 200 }, (size2d_t) { 0, 0 }, &win_t); + SIZE2D(200, 200), SIZE2D(0, 0), &win_t); if (rc != result_OK) goto Failure; @@ -1534,12 +1534,12 @@ result_t wuss_test(const char *resources) old_vscroll_x = before.x1 - outline_px - icon / 2; old_vscroll_y = interior_y; - rc = wuss_mouse_click(wuss, (point_t) { cx, cy }, wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* T's toggle-size icon: grow */ + rc = wuss_mouse_click(wuss, POINT(cx, cy), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* T's toggle-size icon: grow */ if (rc != result_OK) goto Failure; if (hit != win_t) goto Failure; - rc = wuss_mouse_click(wuss, (point_t) { cx, cy }, wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); + rc = wuss_mouse_click(wuss, POINT(cx, cy), wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); if (rc != result_OK) goto Failure; @@ -1601,12 +1601,12 @@ result_t wuss_test(const char *resources) cx = (toggle.x0 + toggle.x1) / 2; cy = (toggle.y0 + toggle.y1) / 2; - rc = wuss_mouse_click(wuss, (point_t) { cx, cy }, wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* T's toggle-size icon: shrink */ + rc = wuss_mouse_click(wuss, POINT(cx, cy), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* T's toggle-size icon: shrink */ if (rc != result_OK) goto Failure; if (hit != win_t) goto Failure; - rc = wuss_mouse_click(wuss, (point_t) { cx, cy }, wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); + rc = wuss_mouse_click(wuss, POINT(cx, cy), wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); if (rc != result_OK) goto Failure; @@ -1653,7 +1653,7 @@ result_t wuss_test(const char *resources) rc = wuss_window_create(wuss, &box_r, "R", wuss_WINDOW_NONE, wuss_NO_BACKGROUND, &delegate_r, - (size2d_t) { 70, 70 }, (size2d_t) { 0, 0 }, &win_r); + SIZE2D(70, 70), SIZE2D(0, 0), &win_r); if (rc != result_OK) goto Failure; @@ -1661,7 +1661,7 @@ result_t wuss_test(const char *resources) if (rc != result_OK) goto Failure; - wuss_window_set_scroll(win_r, (point_t) { 0, 15 }); /* within range: max_y = 70 - 40 = 30 */ + wuss_window_set_scroll(win_r, POINT(0, 15)); /* within range: max_y = 70 - 40 = 30 */ rc = wuss_redraw_dirty(wuss); /* flush the scroll's own invalidate */ if (rc != result_OK) @@ -1693,12 +1693,12 @@ result_t wuss_test(const char *resources) interior_x = (before.x0 + outline_px + before.x1 - outline_px - icon) / 2; interior_y = (before.y0 + outline_px + titlebar_height + before.y1 - outline_px - icon) / 2; - rc = wuss_mouse_click(wuss, (point_t) { cx, cy }, wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* R's toggle-size icon: grow past doc_height, forcing a scroll re-clamp */ + rc = wuss_mouse_click(wuss, POINT(cx, cy), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* R's toggle-size icon: grow past doc_height, forcing a scroll re-clamp */ if (rc != result_OK) goto Failure; if (hit != win_r) goto Failure; - rc = wuss_mouse_click(wuss, (point_t) { cx, cy }, wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); + rc = wuss_mouse_click(wuss, POINT(cx, cy), wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); if (rc != result_OK) goto Failure; @@ -1751,7 +1751,7 @@ result_t wuss_test(const char *resources) rc = wuss_window_create(wuss, &box_nb, "NB", wuss_WINDOW_NO_RESIZE_BLIT, wuss_NO_BACKGROUND, &delegate_nb, - (size2d_t) { 200, 200 }, (size2d_t) { 0, 0 }, &win_nb); + SIZE2D(200, 200), SIZE2D(0, 0), &win_nb); if (rc != result_OK) goto Failure; @@ -1781,12 +1781,12 @@ result_t wuss_test(const char *resources) interior_x = (before.x0 + outline_px + before.x1 - outline_px - icon) / 2; interior_y = (before.y0 + outline_px + titlebar_height + before.y1 - outline_px - icon) / 2; - rc = wuss_mouse_click(wuss, (point_t) { cx, cy }, wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* NB's toggle-size icon: grow */ + rc = wuss_mouse_click(wuss, POINT(cx, cy), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* NB's toggle-size icon: grow */ if (rc != result_OK) goto Failure; if (hit != win_nb) goto Failure; - rc = wuss_mouse_click(wuss, (point_t) { cx, cy }, wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); + rc = wuss_mouse_click(wuss, POINT(cx, cy), wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); if (rc != result_OK) goto Failure; @@ -1830,7 +1830,7 @@ result_t wuss_test(const char *resources) box_u.x1 = 120; box_u.y1 = 120; /* 40x40 content */ rc = wuss_window_create(wuss, &box_u, "U", wuss_WINDOW_NONE, wuss_NO_BACKGROUND, /* scrollbars on: carve.x/y = icon size */ &delegate_u, - (size2d_t) { 70, 70 }, (size2d_t) { 0, 0 }, &win_u); /* doc size well within the 200x200 screen: growth is doc-limited, not screen-limited */ + SIZE2D(70, 70), SIZE2D(0, 0), &win_u); /* doc size well within the 200x200 screen: growth is doc-limited, not screen-limited */ if (rc != result_OK) goto Failure; @@ -1854,12 +1854,12 @@ result_t wuss_test(const char *resources) cx = (toggle.x0 + toggle.x1) / 2; cy = (toggle.y0 + toggle.y1) / 2; - rc = wuss_mouse_click(wuss, (point_t) { cx, cy }, wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* U's toggle-size icon: grow to doc size */ + rc = wuss_mouse_click(wuss, POINT(cx, cy), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* U's toggle-size icon: grow to doc size */ if (rc != result_OK) goto Failure; if (hit != win_u) goto Failure; - rc = wuss_mouse_click(wuss, (point_t) { cx, cy }, wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); + rc = wuss_mouse_click(wuss, POINT(cx, cy), wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); if (rc != result_OK) goto Failure; @@ -1887,12 +1887,12 @@ result_t wuss_test(const char *resources) cx = (toggle.x0 + toggle.x1) / 2; cy = (toggle.y0 + toggle.y1) / 2; - rc = wuss_mouse_click(wuss, (point_t) { cx, cy }, wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* U's toggle-size icon: shrink back */ + rc = wuss_mouse_click(wuss, POINT(cx, cy), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* U's toggle-size icon: shrink back */ if (rc != result_OK) goto Failure; if (hit != win_u) goto Failure; - rc = wuss_mouse_click(wuss, (point_t) { cx, cy }, wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); + rc = wuss_mouse_click(wuss, POINT(cx, cy), wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); if (rc != result_OK) goto Failure; @@ -1938,7 +1938,7 @@ result_t wuss_test(const char *resources) rc = wuss_window_create(wuss, &box_v, "V", wuss_WINDOW_NONE, wuss_NO_BACKGROUND, &delegate_v, - (size2d_t) { 200, 200 }, (size2d_t) { 0, 0 }, &win_v); + SIZE2D(200, 200), SIZE2D(0, 0), &win_v); if (rc != result_OK) goto Failure; @@ -1951,7 +1951,7 @@ result_t wuss_test(const char *resources) * above): titlebar drag calls wuss_window_move with no clamping. From * here, "available space to the screen edge" (scr_width - visible.x0 - * furniture) goes negative, further than furniture alone can absorb. */ - wuss_window_move(win_v, (point_t) { 310, 310 }); + wuss_window_move(win_v, POINT(310, 310)); outline_px = 1; titlebar_height = 20; @@ -1969,12 +1969,12 @@ result_t wuss_test(const char *resources) cx = (toggle.x0 + toggle.x1) / 2; cy = (toggle.y0 + toggle.y1) / 2; - rc = wuss_mouse_click(wuss, (point_t) { cx, cy }, wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* V's toggle-size icon: maximize while off-screen */ + rc = wuss_mouse_click(wuss, POINT(cx, cy), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* V's toggle-size icon: maximize while off-screen */ if (rc != result_OK) goto Failure; if (hit != win_v) goto Failure; - rc = wuss_mouse_click(wuss, (point_t) { cx, cy }, wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); + rc = wuss_mouse_click(wuss, POINT(cx, cy), wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); if (rc != result_OK) goto Failure; @@ -2007,12 +2007,12 @@ result_t wuss_test(const char *resources) cx = (toggle.x0 + toggle.x1) / 2; cy = (toggle.y0 + toggle.y1) / 2; - rc = wuss_mouse_click(wuss, (point_t) { cx, cy }, wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* V's toggle-size icon: shrink back */ + rc = wuss_mouse_click(wuss, POINT(cx, cy), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* V's toggle-size icon: shrink back */ if (rc != result_OK) goto Failure; if (hit != win_v) goto Failure; - rc = wuss_mouse_click(wuss, (point_t) { cx, cy }, wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); + rc = wuss_mouse_click(wuss, POINT(cx, cy), wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); if (rc != result_OK) goto Failure; @@ -2052,7 +2052,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_k, box_size(&box_k), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &win_k); if (rc != result_OK) goto Failure; @@ -2073,7 +2073,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_l, box_size(&box_l), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &win_l); if (rc != result_OK) goto Failure; @@ -2086,7 +2086,7 @@ result_t wuss_test(const char *resources) wuss_window_get_visible_bounds(win_k, &visible); - rc = wuss_mouse_click(wuss, (point_t) { visible.x0 + 31, visible.y0 + 11 }, wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* K's titlebar, clear of the close icon */ + rc = wuss_mouse_click(wuss, POINT(visible.x0 + 31, visible.y0 + 11), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* K's titlebar, clear of the close icon */ if (rc != result_OK) goto Failure; if (hit != win_k) @@ -2094,7 +2094,7 @@ result_t wuss_test(const char *resources) before_k = tc_k.redraw_count; before_l = tc_l.redraw_count; - rc = wuss_mouse_move(wuss, (point_t) { visible.x0 + 45, visible.y0 + 21 }, &hit); + rc = wuss_mouse_move(wuss, POINT(visible.x0 + 45, visible.y0 + 21), &hit); if (rc != result_OK) goto Failure; if (hit != win_k) @@ -2108,7 +2108,7 @@ result_t wuss_test(const char *resources) * footprint, so the move fast path must still apply even * though K isn't topmost */ - rc = wuss_mouse_click(wuss, (point_t) { visible.x0 + 45, visible.y0 + 21 }, wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); + rc = wuss_mouse_click(wuss, POINT(visible.x0 + 45, visible.y0 + 21), wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); if (rc != result_OK) goto Failure; @@ -2140,7 +2140,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_m2, box_size(&box_m2), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &win_m2); if (rc != result_OK) goto Failure; @@ -2153,7 +2153,7 @@ result_t wuss_test(const char *resources) interior_x = before2.x0 + 2; /* inside the untouched left edge */ interior_y = before2.y0 + 25; /* below the titlebar, in plain content */ - rc = wuss_window_resize(win_m2, (size2d_t) { 80, 80 }); /* grow */ + rc = wuss_window_resize(win_m2, SIZE2D(80, 80)); /* grow */ if (rc != result_OK) goto Failure; @@ -2182,7 +2182,7 @@ result_t wuss_test(const char *resources) before2 = after2; - rc = wuss_window_resize(win_m2, (size2d_t) { 40, 40 }); /* shrink back */ + rc = wuss_window_resize(win_m2, SIZE2D(40, 40)); /* shrink back */ if (rc != result_OK) goto Failure; @@ -2231,7 +2231,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_nb2, box_size(&box_nb2), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &win_nb2); if (rc != result_OK) goto Failure; @@ -2242,7 +2242,7 @@ result_t wuss_test(const char *resources) wuss_window_get_visible_bounds(win_nb2, &before3); - rc = wuss_window_resize(win_nb2, (size2d_t) { 80, 80 }); /* grow */ + rc = wuss_window_resize(win_nb2, SIZE2D(80, 80)); /* grow */ if (rc != result_OK) goto Failure; @@ -2283,7 +2283,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_n, box_size(&box_n), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &win_n); if (rc != result_OK) goto Failure; @@ -2302,7 +2302,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_o, box_size(&box_o), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &win_o); if (rc != result_OK) goto Failure; @@ -2319,7 +2319,7 @@ result_t wuss_test(const char *resources) /* Move N far enough right that its new footprint lands partly under O * (which stays wholly untouched), while its old footprint started * entirely clear of O. */ - wuss_window_move(win_n, (point_t) { visible.x0 + 80, visible.y0 + 10 }); + wuss_window_move(win_n, POINT(visible.x0 + 80, visible.y0 + 10)); wuss_window_get_visible_bounds(win_n, &visible); wuss_window_get_visible_bounds(win_o, &visible_o); @@ -2387,7 +2387,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_b, box_size(&box_b), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &win_b); if (rc != result_OK) goto Failure; @@ -2406,7 +2406,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_a, box_size(&box_a), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &win_a); if (rc != result_OK) goto Failure; @@ -2423,8 +2423,8 @@ result_t wuss_test(const char *resources) /* Move B far enough right that its whole new footprint clears A. */ dx = 60; - wuss_window_move(win_b, (point_t) { visible_b_before.x0 + dx, - visible_b_before.y0 }); + wuss_window_move(win_b, POINT(visible_b_before.x0 + dx, + visible_b_before.y0)); /* The clean strip (previously exposed, genuinely B's own pixels) lands * at its translated destination and must have been blitted there, not @@ -2490,7 +2490,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_b, box_size(&box_b), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &win_b); if (rc != result_OK) goto Failure; @@ -2509,7 +2509,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_a, box_size(&box_a), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &win_a); if (rc != result_OK) goto Failure; @@ -2525,8 +2525,8 @@ result_t wuss_test(const char *resources) * (x:40-60, under A) both stay clear of / under A exactly as before -- * nothing about A's own pixels is ever touched by the blit, so A must * not be forced to redraw. */ - wuss_window_move(win_b, (point_t) { visible_b_before.x0, - visible_b_before.y0 + 5 }); + wuss_window_move(win_b, POINT(visible_b_before.x0, + visible_b_before.y0 + 5)); occluder_dirty = 0; for (i = 0; i < wuss_get_dirty_count(wuss); i++) @@ -2570,7 +2570,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_b, box_size(&box_b), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &win_b); if (rc != result_OK) goto Failure; @@ -2589,7 +2589,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_a, box_size(&box_a), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &win_a); if (rc != result_OK) goto Failure; @@ -2616,8 +2616,8 @@ result_t wuss_test(const char *resources) * moved, its pixels there are already correct), so it must stay clean; * the translated hidden band (y:45-65, never had valid pixels) still * needs forcing dirty for a real repaint. */ - wuss_window_move(win_b, (point_t) { visible_b_before.x0, - visible_b_before.y0 + 25 }); + wuss_window_move(win_b, POINT(visible_b_before.x0, + visible_b_before.y0 + 25)); occluded_overlap.x0 = 0; occluded_overlap.y0 = 25; occluded_overlap.x1 = 60; occluded_overlap.y1 = 40; @@ -2683,7 +2683,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_b, box_size(&box_b), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &win_b); if (rc != result_OK) goto Failure; @@ -2702,7 +2702,7 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_a, box_size(&box_a), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &win_a); if (rc != result_OK) goto Failure; @@ -2725,8 +2725,8 @@ result_t wuss_test(const char *resources) wuss_window_get_visible_bounds(win_b, &visible_b_before); wuss_window_get_visible_bounds(win_a, &visible_a); - wuss_window_move(win_b, (point_t) { visible_b_before.x0 - 15, - visible_b_before.y0 - 15 }); + wuss_window_move(win_b, POINT(visible_b_before.x0 - 15, + visible_b_before.y0 - 15)); /* The blit must have actually happened, not fallen back: B's own * footprint (outside A) must not be dirtied wholesale. */ @@ -2779,12 +2779,12 @@ result_t wuss_test(const char *resources) wuss_NO_BACKGROUND, &delegate_c, box_size(&box_c), - (size2d_t) { 0, 0 }, + SIZE2D(0, 0), &win_c); if (rc != result_OK) goto Failure; - rc = wuss_mouse_click(wuss, (point_t) { 31, 11 }, wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* C's titlebar, above its content, clear of the close icon */ + rc = wuss_mouse_click(wuss, POINT(31, 11), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* C's titlebar, above its content, clear of the close icon */ if (rc != result_OK) goto Failure; if (hit != win_c) @@ -2792,7 +2792,7 @@ result_t wuss_test(const char *resources) wuss_window_close(win_c); - rc = wuss_mouse_move(wuss, (point_t) { 20, 20 }, &hit); + rc = wuss_mouse_move(wuss, POINT(20, 20), &hit); if (rc != result_OK) goto Failure; @@ -2813,7 +2813,7 @@ result_t wuss_test(const char *resources) rc = wuss_window_create(wuss, &box_s, "S", wuss_WINDOW_NONE, wuss_NO_BACKGROUND, &delegate_s, - (size2d_t) { 200, 200 }, (size2d_t) { 0, 0 }, &win_s); + SIZE2D(200, 200), SIZE2D(0, 0), &win_s); if (rc != result_OK) goto Failure; @@ -2825,7 +2825,7 @@ result_t wuss_test(const char *resources) wuss_window_get_content_bounds(win_s, &content_s); rc = wuss_mouse_click(wuss, - (point_t) { content_s.x0 + 5, content_s.y0 + 7 }, + POINT(content_s.x0 + 5, content_s.y0 + 7), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); if (rc != result_OK) goto Failure; @@ -2835,13 +2835,13 @@ result_t wuss_test(const char *resources) goto Failure; /* a task adding the scroll offset itself would double-count it */ rc = wuss_mouse_click(wuss, - (point_t) { content_s.x0 + 5, content_s.y0 + 7 }, + POINT(content_s.x0 + 5, content_s.y0 + 7), wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); if (rc != result_OK) goto Failure; rc = wuss_mouse_move(wuss, - (point_t) { content_s.x0 + 11, content_s.y0 + 13 }, + POINT(content_s.x0 + 11, content_s.y0 + 13), &hit); if (rc != result_OK) goto Failure; @@ -2851,7 +2851,7 @@ result_t wuss_test(const char *resources) tc_s.last_scroll_x = -1; tc_s.last_scroll_y = -1; rc = wuss_scroll(wuss, - (point_t) { content_s.x0 + 3, content_s.y0 + 4 }, + POINT(content_s.x0 + 3, content_s.y0 + 4), 1, &hit); if (rc != result_OK) goto Failure; @@ -2889,8 +2889,8 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NONE, /* all furniture present */ wuss_NO_BACKGROUND, &delegate_r, - (size2d_t) { 400, 400 }, - (size2d_t) { 0, 0 }, + SIZE2D(400, 400), + SIZE2D(0, 0), &win_r); if (rc != result_OK) goto Failure; @@ -2900,7 +2900,7 @@ result_t wuss_test(const char *resources) content_r.y1 - content_r.y0 != box_r.y1 - box_r.y0) goto Failure; - rc = wuss_window_resize(win_r, (size2d_t) { 61, 47 }); + rc = wuss_window_resize(win_r, SIZE2D(61, 47)); if (rc != result_OK) goto Failure; diff --git a/libraries/wuss/window/move.c b/libraries/wuss/window/move.c index ef0ee7d0..19c05a54 100644 --- a/libraries/wuss/window/move.c +++ b/libraries/wuss/window/move.c @@ -145,7 +145,7 @@ void wuss_window_move(wuss_window_t *window, point_t p) idx = order[i]; if (!screen_copy_rect(window->wuss->scr, &blit_src[idx], - (point_t) { blit_dest[idx].x0, blit_dest[idx].y0 }, + POINT(blit_dest[idx].x0, blit_dest[idx].y0), &copied)) { /* The screen format doesn't support the blit at all (e.g. paletted): diff --git a/libraries/wuss/window/set-scroll.c b/libraries/wuss/window/set-scroll.c index 1543289e..531665f4 100644 --- a/libraries/wuss/window/set-scroll.c +++ b/libraries/wuss/window/set-scroll.c @@ -28,7 +28,7 @@ void wuss_window_set_scroll(wuss_window_t *window, point_t p) * newly-exposed edge strip(s) need an actual repaint. */ window->wuss->scr->clip = content; if (screen_copy_rect(window->wuss->scr, &content, - (point_t) { content.x0 - dx, content.y0 - dy }, &copied)) + POINT(content.x0 - dx, content.y0 - dy), &copied)) { wuss__invalidate_minus(window->wuss, &content, &copied); return; From fa735d7a5cec3922e7c2ea603be5531696d4c206 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sat, 29 Aug 2026 13:48:22 +0100 Subject: [PATCH 10/44] fix(wuss): preserve pointer offset in resize-corner drag Resize-drag recomputed width/height as pointer minus content origin on every move, discarding where within the resize icon the mouse-down click landed. The window's corner snapped to the raw pointer position on the first move, jumping if the click wasn't at the icon's exact corner pixel. Store the pointer's offset from the content box's bottom-right corner at mouse-down (mirroring how a titlebar drag already stores its content-relative offset), and subtract it on each move, so the grabbed point stays under the pointer through the drag. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01TWwV3bFEi9z5MZc5wqiJzZ --- libraries/wuss/furniture.h | 11 ++++++++--- libraries/wuss/furniture/drag-resize.c | 7 +++++-- libraries/wuss/mouse-click.c | 12 ++++++++++++ 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/libraries/wuss/furniture.h b/libraries/wuss/furniture.h index ddc707b3..1bac5c97 100644 --- a/libraries/wuss/furniture.h +++ b/libraries/wuss/furniture.h @@ -46,9 +46,14 @@ struct wuss__furniture { wuss_window_t *dragging; /* NULL when idle */ wuss_furniture_drag_kind_t drag_kind; - point_t drag; /* MOVE: pointer offset within content; - * RESIZE: unused, recomputed each move; - * *_SAUSAGE: pointer position at drag start */ + point_t drag; /* *_SAUSAGE: pointer position at drag start; + * MOVE: pointer offset within content */ + point_t drag_offset; /* RESIZE: pointer offset from the + * content box's bottom-right corner + * at drag start, so the grab point + * stays under the pointer instead + * of the window's edge snapping to + * it on the first move */ int drag_scroll_start; /* *_SAUSAGE: scroll.x/scroll.y at drag start */ }; diff --git a/libraries/wuss/furniture/drag-resize.c b/libraries/wuss/furniture/drag-resize.c index 21773b94..9a92a3a3 100644 --- a/libraries/wuss/furniture/drag-resize.c +++ b/libraries/wuss/furniture/drag-resize.c @@ -13,8 +13,11 @@ void wuss__furniture_drag_resize(wuss_window_t *window, point_t p) wuss__content_box(window, &content); wuss__min_content(window, &min); - width = p.x - content.x0; - height = p.y - content.y0; + /* Subtract the offset recorded at drag start so the point originally + * grabbed on the resize icon stays under the pointer, rather than the + * window's edge snapping to meet the pointer on the first move. */ + width = p.x - window->wuss->furniture.drag_offset.x - content.x0; + height = p.y - window->wuss->furniture.drag_offset.y - content.y0; width = CLAMP(width, min.w, MAX(window->doc.w, min.w)); height = CLAMP(height, min.h, MAX(window->doc.h, min.h)); diff --git a/libraries/wuss/mouse-click.c b/libraries/wuss/mouse-click.c index a68c9bb0..745f5e7b 100644 --- a/libraries/wuss/mouse-click.c +++ b/libraries/wuss/mouse-click.c @@ -136,6 +136,18 @@ result_t wuss_mouse_click(wuss_t *wuss, wuss->furniture.drag.x = x; wuss->furniture.drag.y = y; wuss->furniture.drag_scroll_start = (region == wuss_FURNITURE_VSCROLL_WELL) ? scroll.y : scroll.x; + + /* Resize needs the pointer's offset from the content box's current + * bottom-right corner, so the point grabbed on the resize icon stays + * under the pointer as it moves, rather than that corner jumping to + * meet the pointer on the very first move. */ + if (region == wuss_FURNITURE_RESIZE) + { + box_t content; + wuss__content_box(win, &content); + wuss->furniture.drag_offset.x = x - content.x1; + wuss->furniture.drag_offset.y = y - content.y1; + } } return result_OK; } From d99aa4ed66d62d969ed2cb3735adf17d22f980c6 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sat, 29 Aug 2026 17:54:14 +0100 Subject: [PATCH 11/44] docs: trim derivable build and module-layout notes from CLAUDE.md Co-Authored-By: Claude Sonnet 5 --- CLAUDE.md | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index ad68b665..340b704f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,12 +8,6 @@ DPTLib is a platform-independent C99 library (base, databases, datastruct, frame ## Build -``` -mkdir build && cd build -cmake -DBUILD_TESTS=YES .. -make -j4 -``` - Useful CMake options: - `BUILD_TESTS=YES` — build the `DPTLibTest` self-test executable. - `BUILD_SDL_TESTS=YES` — additionally build tests needing SDL2/SDL2_image. @@ -45,9 +39,7 @@ Success prints `++ Tests completed in Ns: N of N tests passed.` ## Architecture -**Module layout.** Each module lives in two places that must be kept in sync: -- `include//.h` — the public API, always wrapped in `extern "C"`, documented with Doxygen `\file`/`\param`/`\return` comments. -- `libraries///` — implementation `.c` files (often one function per file, e.g. `libraries/datastruct/vector/{create,destroy,insert,...}.c`), plus a private `impl.h` defining the opaque struct behind the public typedef and any internal-only declarations. +**Module layout.** Each module is split between `include//.h` (public API, `extern "C"`, Doxygen-documented) and `libraries///` (implementation `.c` files, often one function per file, plus a private `impl.h` for the opaque struct and internal-only declarations). New source/header files must be added by hand to the relevant `set(..._SOURCES ...)` list and, for public headers, to `PUBLIC_HEADERS`, in `CMakeLists.txt` — there is no globbing. From 131d0e5273ba8513e26b8a15f8e43873c0bf850e Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sat, 29 Aug 2026 19:37:33 +0100 Subject: [PATCH 12/44] feat(wuss): add work-area icons Add a work-area icon subsystem: static labels and clickable bevelled buttons that Wuss draws inside a window's content area and hit-tests before the content task sees a click. Icon boxes are in virtual document space so they scroll with the content; button clicks and hovers reach the task as wuss_EVENT_ICON, while labels and hidden or disabled icons fall through as wuss_EVENT_MOUSE. Includes the public wuss/icon.h API, per-window icon storage, drawing, mouse routing integration, and an icons test task. Co-Authored-By: Claude Sonnet 5 --- CMakeLists.txt | 16 ++ docs/windowing/wuss.md | 56 +++++-- include/wuss/icon.h | 177 +++++++++++++++++++++ include/wuss/task.h | 18 ++- include/wuss/wuss.h | 17 ++ libraries/wuss/create.c | 10 ++ libraries/wuss/destroy.c | 1 + libraries/wuss/furniture/back-box.c | 4 +- libraries/wuss/furniture/close-box.c | 4 +- libraries/wuss/furniture/content-box.c | 2 +- libraries/wuss/furniture/draw.c | 2 +- libraries/wuss/furniture/hscroll-box.c | 8 +- libraries/wuss/furniture/invalidate.c | 2 +- libraries/wuss/furniture/resize-box.c | 2 +- libraries/wuss/furniture/toggle-action.c | 2 +- libraries/wuss/furniture/toggle-box.c | 4 +- libraries/wuss/furniture/vscroll-box.c | 8 +- libraries/wuss/icon.h | 53 +++++++ libraries/wuss/icon/create.c | 82 ++++++++++ libraries/wuss/icon/delete.c | 34 ++++ libraries/wuss/icon/draw.c | 144 +++++++++++++++++ libraries/wuss/icon/free.c | 25 +++ libraries/wuss/icon/get-bbox.c | 8 + libraries/wuss/icon/get-text.c | 8 + libraries/wuss/icon/get-type.c | 8 + libraries/wuss/icon/get-window.c | 8 + libraries/wuss/icon/hit-test.c | 28 ++++ libraries/wuss/icon/invalidate.c | 10 ++ libraries/wuss/icon/screen-box.c | 17 ++ libraries/wuss/icon/set-hidden.c | 13 ++ libraries/wuss/icon/set-text.c | 32 ++++ libraries/wuss/impl.h | 30 ++-- libraries/wuss/mouse-click.c | 38 ++++- libraries/wuss/mouse-move.c | 41 ++++- libraries/wuss/redraw.c | 9 ++ libraries/wuss/test/tasks/icons.c | 189 +++++++++++++++++++++++ libraries/wuss/test/tasks/icons.h | 40 +++++ libraries/wuss/test/wuss-test.c | 9 +- libraries/wuss/window/close.c | 2 + libraries/wuss/window/create.c | 5 +- libraries/wuss/window/resize.c | 2 +- 41 files changed, 1109 insertions(+), 59 deletions(-) create mode 100644 include/wuss/icon.h create mode 100644 libraries/wuss/icon.h create mode 100644 libraries/wuss/icon/create.c create mode 100644 libraries/wuss/icon/delete.c create mode 100644 libraries/wuss/icon/draw.c create mode 100644 libraries/wuss/icon/free.c create mode 100644 libraries/wuss/icon/get-bbox.c create mode 100644 libraries/wuss/icon/get-text.c create mode 100644 libraries/wuss/icon/get-type.c create mode 100644 libraries/wuss/icon/get-window.c create mode 100644 libraries/wuss/icon/hit-test.c create mode 100644 libraries/wuss/icon/invalidate.c create mode 100644 libraries/wuss/icon/screen-box.c create mode 100644 libraries/wuss/icon/set-hidden.c create mode 100644 libraries/wuss/icon/set-text.c create mode 100644 libraries/wuss/test/tasks/icons.c create mode 100644 libraries/wuss/test/tasks/icons.h diff --git a/CMakeLists.txt b/CMakeLists.txt index e50ac931..8861523b 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -112,6 +112,7 @@ set(PUBLIC_HEADERS include/utils/maths.h include/utils/pack.h include/utils/primes.h + include/wuss/icon.h include/wuss/task.h include/wuss/window.h include/wuss/wuss.h) @@ -336,6 +337,20 @@ set(WUSS_SOURCES libraries/wuss/furniture/vscroll-box.c libraries/wuss/furniture.h libraries/wuss/get-font.c + libraries/wuss/icon/create.c + libraries/wuss/icon/delete.c + libraries/wuss/icon/draw.c + libraries/wuss/icon/free.c + libraries/wuss/icon/get-bbox.c + libraries/wuss/icon/get-text.c + libraries/wuss/icon/get-type.c + libraries/wuss/icon/get-window.c + libraries/wuss/icon/hit-test.c + libraries/wuss/icon/invalidate.c + libraries/wuss/icon/screen-box.c + libraries/wuss/icon/set-hidden.c + libraries/wuss/icon/set-text.c + libraries/wuss/icon.h libraries/wuss/idle.c libraries/wuss/impl.h libraries/wuss/invalidate.c @@ -549,6 +564,7 @@ if(BUILD_TESTS) libraries/wuss/test/tasks/checker.c libraries/wuss/test/tasks/curve.c libraries/wuss/test/tasks/gradient.c + libraries/wuss/test/tasks/icons.c libraries/wuss/test/tasks/image.c libraries/wuss/test/tasks/launcher.c libraries/wuss/test/tasks/palette.c diff --git a/docs/windowing/wuss.md b/docs/windowing/wuss.md index a648f97a..0871dd5c 100644 --- a/docs/windowing/wuss.md +++ b/docs/windowing/wuss.md @@ -62,12 +62,12 @@ wuss_task_t; - `wuss_WINDOW_NONE` — default: every furniture region drawn. - `wuss_WINDOW_NO_TITLEBAR` — no titlebar, and no drag handle. - `wuss_WINDOW_NO_OUTLINE` — no 1px border around the window. -- `wuss_WINDOW_NO_CLOSE` — no close icon in the titlebar. -- `wuss_WINDOW_NO_BACK` — no back icon in the titlebar (`wuss_BUTTON_SELECT` sends the window to back, `wuss_BUTTON_ADJUST` brings it to front). -- `wuss_WINDOW_NO_TOGGLE_SIZE` — no toggle-size icon in the titlebar. +- `wuss_WINDOW_NO_CLOSE` — no close button in the titlebar. +- `wuss_WINDOW_NO_BACK` — no back button in the titlebar (`wuss_BUTTON_SELECT` sends the window to back, `wuss_BUTTON_ADJUST` brings it to front). +- `wuss_WINDOW_NO_TOGGLE_SIZE` — no toggle-size button in the titlebar. - `wuss_WINDOW_NO_VSCROLL` — no vertical scrollbar on the right edge. - `wuss_WINDOW_NO_HSCROLL` — no horizontal scrollbar on the bottom edge. -- `wuss_WINDOW_NO_RESIZE` — no resize icon in the bottom-right corner. +- `wuss_WINDOW_NO_RESIZE` — no resize button in the bottom-right corner. - `wuss_WINDOW_NO_RESIZE_BLIT` — a resize (drag or toggle-size) always fully redraws the window's content instead of blitting the preserved region; for a task whose rendering depends on window size in ways a partial redraw can't patch (e.g. a layout that spans the whole window). `wuss_WINDOW_NO_CLOSE`/`NO_BACK`/`NO_TOGGLE_SIZE` are ignored if `flags` includes `wuss_WINDOW_NO_TITLEBAR`; `NO_VSCROLL`/`NO_HSCROLL`/`NO_RESIZE`/`NO_RESIZE_BLIT` apply regardless. @@ -83,6 +83,7 @@ typedef enum wuss_event_kind { wuss_EVENT_REDRAW, wuss_EVENT_MOUSE, + wuss_EVENT_ICON, wuss_EVENT_SCROLL } wuss_event_kind_t; @@ -93,8 +94,9 @@ typedef struct wuss_event union { struct { screen_t *scr; const box_t *content; } redraw; - struct { wuss_mouse_action_t action; int x, y; wuss_button_t button; } mouse; - struct { int x, y, delta; } scroll; + struct { wuss_mouse_action_t action; point_t point; wuss_button_t button; } mouse; + struct { wuss_icon_t *icon; wuss_mouse_action_t action; wuss_button_t button; } icon; + struct { point_t point; int delta; } scroll; } data; } @@ -110,6 +112,8 @@ For `wuss_EVENT_REDRAW`, `event->data.redraw.scr` is called with `scr->clip` alr For `wuss_EVENT_MOUSE` and `wuss_EVENT_SCROLL`, `event->data.mouse.point` and `event->data.scroll.point` are window-local content coordinates: the content area's top-left is `(0,0)` plus the window's current scroll offset (see "Scrolling" below). `event->data.mouse.action` is `wuss_MOUSE_DOWN`/`wuss_MOUSE_UP`/`wuss_MOUSE_MOVE`. A titlebar click never reaches a task's handle callback: it starts a drag (and, for `wuss_BUTTON_SELECT`, brings the window to front) instead. A content click, even on a `wuss_WINDOW_NO_TITLEBAR` window with no drag handle, never changes z-order — only a titlebar click raises a window — so tasks are free to use content clicks for their own purposes without Wuss reordering windows underneath them. +`wuss_EVENT_ICON` is delivered instead of `wuss_EVENT_MOUSE` whenever the pointer is inside a `wuss_ICON_TYPE_BUTTON` icon's bounding box (see "Icons" below): `event->data.icon.icon` names the icon, `action` and `button` carry the same values a `wuss_EVENT_MOUSE` would. `wuss_ICON_TYPE_LABEL` icons, and hidden or disabled icons, never raise it — clicks over them fall through as `wuss_EVENT_MOUSE`. + ## Mouse and scroll routing Feed mouse events in with `wuss_mouse_click` (action `wuss_MOUSE_DOWN` or `wuss_MOUSE_UP`) and `wuss_mouse_move`, and scroll events with `wuss_scroll`, each hit-testing the topmost window at `(x, y)` and delivering to its task in window-local coordinates. All three take an optional `wuss_window_t **hit` out-parameter naming the window under the pointer (or being dragged). Events are dropped if the hit window has no handle callback, or (for scroll) the pointer is over its titlebar. @@ -124,16 +128,41 @@ Each window carries a scroll offset, `(0, 0)` by default: the point in the task' ## Redrawing - `wuss_redraw` repaints every window, back-to-front, unconditionally, having first painted the configured backdrop colour (see Setup) behind them, if any. +- Within a window, Wuss paints in a fixed order: the window background colour, then its icons (see "Icons" below), then the task's `wuss_EVENT_REDRAW` handler — so a task always draws over the background and any icons, never under them. - `wuss_invalidate` / `wuss_window_invalidate` mark a screen-space or window-local region dirty; window management calls these automatically for its own changes, but a task must call one of them itself whenever its content changes on its own (e.g. an animation), passing the union of the old and new areas that need repainting. - `wuss_redraw_dirty` repaints only the accumulated dirty region, then clears it, painting the backdrop colour into each dirty region first if one was configured. Without a configured backdrop, Wuss only repaints windows, not the background between/behind them, so a caller whose invalidation can expose background (e.g. after a window move) should clear that region itself first. - `wuss_get_dirty_count`/`wuss_get_dirty(wuss, index, out)` fetch the currently accumulated dirty regions (coalesced as they accumulate, up to a fixed cap after which further regions are merged into the last one) without redrawing. - A window move or resize is clipped, piece by piece, against whatever's above it in the z-order, and any pixels a move can preserve are blitted directly rather than queued dirty; only the genuinely-changed pieces end up in the dirty region. This is an internal optimisation with no effect on a task's own redraw handling. +## Icons + +Taking inspiration from RISC OS, a window can carry **icons**: rectangular UI elements Wuss draws and hit-tests inside the content area. v1 ships two types: + +- `wuss_ICON_TYPE_LABEL` — static text. Clicks fall through to the task as `wuss_EVENT_MOUSE`. +- `wuss_ICON_TYPE_BUTTON` — a bevelled rectangle with a centred label and pressed-state feedback (the bevel inverts and the label shifts one pixel down-right while held). Clicks and hovers arrive as `wuss_EVENT_ICON`. + +The enum is left open for sprite and editable-text types later. + +Icons are dynamic and owned by their window: + +- `wuss_icon_create(window, spec, &icon)` — returns an opaque `wuss_icon_t *`. The spec gives the bounding box, type, text (copied; `NULL` treated as `""`), foreground and background palette indices, and flags. A `wuss_ICON_TYPE_BUTTON` must pass a real `bg`; passing `wuss_NO_BACKGROUND` is rejected with `result_WUSS_BAD_ICON`. An unknown type is also `result_WUSS_BAD_ICON`; an out-of-range `fg`/`bg` is `result_WUSS_BAD_COLOUR`. +- `wuss_icon_delete(icon)` — NULL-safe. +- `wuss_icon_set_text(icon, text)`, `wuss_icon_set_hidden(icon, hidden)`. +- Getters: `wuss_icon_get_bbox`, `wuss_icon_get_type`, `wuss_icon_get_text` (never `NULL`), `wuss_icon_get_window`. + +Flags: `wuss_ICON_FLAGS_HIDDEN` (not drawn, not hit-tested) and `wuss_ICON_FLAGS_DISABLED` (drawn greyed; clicks fall through as `wuss_EVENT_MOUSE`). + +An icon's bounding box is in **virtual content space** — the same space as `wuss_EVENT_MOUSE`'s `point` and `wuss_window_invalidate`'s box — so icons scroll with the content. The on-screen box is `content-top-left - scroll + bbox`. + +Wuss draws icons in creation order (later icons paint on top); hit-testing scans in reverse, so the topmost icon at a point wins. When the pointer leaves a pressed button its pressed state clears; v1 does not re-press on drag-back-in and does not track which mouse button is held. + +The bevel's light (top/left) and dark (bottom/right) edge shades come from `config->bevel.light` / `config->bevel.dark` at `wuss_create` time, validated like the other furniture colours; both default to the titlebar fill colour when `config` is `NULL`. + ## Glossary Terms as this document and the API use them. Several are RISC OS conventions, which Wuss follows. -- **Adjust** — the secondary mouse button, `wuss_BUTTON_ADJUST`. Conventionally the variant of an action: Adjust on the back icon brings a window to front rather than sending it back, and Adjust on a scroll arrow steps against the direction the arrow points. +- **Adjust** — the secondary mouse button, `wuss_BUTTON_ADJUST`. Conventionally the variant of an action: Adjust on the back button brings a window to front rather than sending it back, and Adjust on a scroll arrow steps against the direction the arrow points. - **Backdrop** — the desktop background colour painted behind all windows, set by `config->backdrop` at `wuss_create` time. `wuss_NO_BACKGROUND` leaves the area behind windows untouched, making it the caller's to repaint. - **Button flags** — `wuss_button_t` values are flags (Select 4, Menu 2, Adjust 1), OR'd together so a chord can be reported. Test a reported button with `&`, never for equality. - **Chord** — two or more mouse buttons held together, e.g. Select+Adjust. Wuss's own furniture handling resolves an ambiguous chord in Select's favour. @@ -141,9 +170,10 @@ Terms as this document and the API use them. Several are RISC OS conventions, wh - **Dirty region** — the accumulated set of screen-space boxes needing repaint, coalesced as they accumulate. `wuss_redraw_dirty` repaints and clears it. - **Document extent** — `doc`, the size of a task's virtual content space, fixed at window creation. Sets how far a window can scroll, the scrollbar sausages' proportions, and the size a resize-drag or toggle-size can grow the content area to. - **Minimum extent** — `min_doc`, the smallest content size a resize-drag or toggle-size will leave a window at, fixed at window creation. `(0, 0)` means the built-in floor. -- **Furniture** — everything Wuss draws around a window's content: outline, titlebar and its icons, scrollbars, resize icon. Drawn outside the content area, never carved out of it. Furniture clicks are handled entirely within Wuss and never reach the task. +- **Furniture** — everything Wuss draws around a window's content: outline, titlebar and its buttons, scrollbars, resize button. Drawn outside the content area, never carved out of it. Furniture clicks are handled entirely within Wuss and never reach the task. +- **Furniture button** — a clickable furniture region in the titlebar or window corner: close, back, toggle-size, resize. (Called an "icon" in earlier revisions; that name now means the work-area element below.) - **Handle callback** — a task's single `wuss_event_fn_t`, receiving every event kind and dispatching on `event->kind`. A window whose task has no handle receives no events at all. -- **Icon** — a clickable furniture region in the titlebar or window corner: close, back, toggle-size, resize. +- **Icon** — a rectangular UI element Wuss draws and hit-tests inside a window's content area: a static `wuss_ICON_TYPE_LABEL`, or a clickable bevelled `wuss_ICON_TYPE_BUTTON`. Created with `wuss_icon_create` and owned by its window. Its bounding box is in virtual content space, so it scrolls with the content; its screen position is `content-top-left - scroll + bbox`. See "Icons". - **Invalidate** — mark a region dirty for the next `wuss_redraw_dirty`. Window management does this for its own changes; a task must do it for its own content changes. - **Menu** — the middle mouse button, `wuss_BUTTON_MENU`. Routed like any other button; Wuss provides no menu widget of its own. - **Outline** — the 1px border drawn around a window, suppressed by `wuss_WINDOW_NO_OUTLINE`. @@ -151,12 +181,12 @@ Terms as this document and the API use them. Several are RISC OS conventions, wh - **Screen space** — coordinates in the underlying `screen_t`, origin at its top-left. Visible and content bounds are in screen space. - **Select** — the primary mouse button, `wuss_BUTTON_SELECT`. Performs the plain action, and raises a window when used on its titlebar. - **Task** — the client of a window: an event callback plus an opaque `task_data` pointer, held in a `wuss_task_t`. Wuss owns the window; the task owns what's drawn inside it. -- **Titlebar** — the strip above the content area carrying the window's label and its icons, and the drag handle for moving the window. Suppressed by `wuss_WINDOW_NO_TITLEBAR`. -- **Toggle size** — the titlebar icon that switches a window between its normal size and a maximised size, and back. +- **Titlebar** — the strip above the content area carrying the window's label and its buttons, and the drag handle for moving the window. Suppressed by `wuss_WINDOW_NO_TITLEBAR`. +- **Toggle size** — the titlebar button that switches a window between its normal size and a maximised size, and back. - **Virtual content space** — the task's own full coordinate space, of size `doc`. Mouse and scroll events arrive in it, i.e. with the scroll offset already added. - **Visible bounds** — a window's whole on-screen footprint, content plus furniture; `wuss_window_get_visible_bounds`. -- **Well** — the track a scrollbar's sausage slides along, between the two arrow icons. -- **Window-local coordinates** — coordinates relative to the content area's top-left, before the scroll offset is added. `wuss_window_invalidate` takes its box in these. +- **Well** — the track a scrollbar's sausage slides along, between the two arrow buttons. +- **Window-local coordinates** — coordinates relative to the content area's top-left. `wuss_window_invalidate` takes its box in virtual content space, i.e. with the scroll offset already added. - **Z-order** — the back-to-front stacking order of windows. Changed with `wuss_window_restack`, or by a Select click on a titlebar; content clicks never change it. ## Limitations diff --git a/include/wuss/icon.h b/include/wuss/icon.h new file mode 100644 index 00000000..ce8c3ea7 --- /dev/null +++ b/include/wuss/icon.h @@ -0,0 +1,177 @@ +/* icon.h -- wuss work-area icons */ + +/** + * \file icon.h + * + * Work-area icons: static labels and clickable bevelled buttons that Wuss draws + * inside a window's content area and hit-tests before the content task sees a + * click. + * + * An icon's bounding box is given in virtual document space -- the same + * coordinate space as wuss_EVENT_MOUSE's point and wuss_window_invalidate's + * local_box -- so an icon scrolls with the content it sits on. Its on-screen + * position is (content.x0 - scroll.x + bbox), using the window's current + * content bounds and scroll offset. + * + * Wuss fills a window's background, draws its icons, then delivers + * wuss_EVENT_REDRAW, so a task is free to paint over or around icon pixels. A + * click on a wuss_ICON_TYPE_BUTTON reaches the task as wuss_EVENT_ICON; clicks + * on a label, or on a hidden or disabled icon, fall through as + * wuss_EVENT_MOUSE. + */ + +#ifndef WUSS_ICON_H +#define WUSS_ICON_H + +#ifdef __cplusplus +extern "C" +{ +#endif + +#include "base/result.h" +#include "geom/box.h" + +#include "wuss/wuss.h" + +/* ----------------------------------------------------------------------- */ + +/** An opaque work-area icon handle, owned by the window it is created on. */ +typedef struct wuss_icon wuss_icon_t; + +/** + * What an icon looks like and how it behaves. The enum is left open so sprite + * and editable-text icons can be added later without breaking existing specs. + */ +typedef enum wuss_icon_type +{ + wuss_ICON_TYPE_LABEL = 0, /**< Static text drawn with the window manager's + * font. Not interactive: clicks fall through to + * the task as wuss_EVENT_MOUSE. */ + wuss_ICON_TYPE_BUTTON /**< Bevelled rectangle with a centred text label + * and pressed-state visual feedback; clicks and + * hovers are delivered to the task as + * wuss_EVENT_ICON. */ +} +wuss_icon_type_t; + +/** Icon appearance and behaviour flags, combinable with bitwise OR. */ +typedef enum wuss_icon_flags +{ + wuss_ICON_FLAGS_NONE = 0, + wuss_ICON_FLAGS_HIDDEN = 1 << 0, /**< Not drawn, not hit-tested. */ + wuss_ICON_FLAGS_DISABLED = 1 << 1 /**< Drawn greyed; clicks fall through to + * the task as wuss_EVENT_MOUSE rather + * than raising wuss_EVENT_ICON. */ +} +wuss_icon_flags_t; + +/** + * Description of an icon at creation. Copied by value into the icon; the caller + * keeps ownership of \c text, which is copied. + * + * A RISC OS-style validation string is deliberately omitted for now; a later \c + * validation field would stay source-compatible for callers that + * zero-initialise the spec. + */ +typedef struct wuss_icon_spec +{ + box_t bbox; /**< Bounding box, virtual document space, + * inclusive-exclusive. */ + wuss_icon_type_t type; /**< Icon type. */ + const char *text; /**< NUL-terminated label; copied. NULL means "". */ + wuss_colour_t fg; /**< Text colour, as an index into the system + * palette. */ + wuss_colour_t bg; /**< Fill/bevel base colour, as an index into the + * system palette. A label may pass + * wuss_NO_BACKGROUND for text with no fill; a + * button must pass a real index. */ + wuss_icon_flags_t flags; /**< Appearance/behaviour flags. */ +} +wuss_icon_spec_t; + +/* ----------------------------------------------------------------------- */ + +/** + * Create an icon on a window. The icon is owned by the window and freed when + * the window is closed (or the window manager destroyed). Its bounding box is + * invalidated so the next redraw paints it. + * + * \param[in] window Window to attach the icon to. + * \param[in] spec Icon description; copied. + * \param[out] icon Filled in with the new icon handle, or NULL if the caller + * does not need it. + * \return \ref result_OK on success, \ref result_OOM on allocation failure, + * \ref result_WUSS_BAD_COLOUR if fg or bg is out of range for the + * palette, or \ref result_WUSS_BAD_ICON if type is unknown or a button + * spec has no fill colour. + */ +result_t wuss_icon_create(wuss_window_t *window, + const wuss_icon_spec_t *spec, + wuss_icon_t **icon); + +/** + * Destroy an icon, unlinking it from its window and invalidating its bounding + * box so the next redraw clears it. Safe to pass NULL. + * + * \param[in] icon Icon to destroy, or NULL. + */ +void wuss_icon_delete(wuss_icon_t *icon); + +/** + * Replace an icon's label text. The new text is copied. Invalidates the icon's + * bounding box. + * + * \param[in] icon Icon to change. + * \param[in] text New NUL-terminated label; copied. NULL means "". + * \return \ref result_OK on success, \ref result_OOM on allocation failure (the + * icon keeps its old text). + */ +result_t wuss_icon_set_text(wuss_icon_t *icon, const char *text); + +/** + * Show or hide an icon, toggling wuss_ICON_FLAGS_HIDDEN. Invalidates the icon's + * bounding box. + * + * \param[in] icon Icon to change. + * \param[in] hidden Non-zero to hide the icon, zero to show it. + */ +void wuss_icon_set_hidden(wuss_icon_t *icon, int hidden); + +/** + * Fetch an icon's bounding box, in virtual document space. + * + * \param[in] icon Icon to query. + * \param[out] bbox Filled in with the bounding box. + */ +void wuss_icon_get_bbox(const wuss_icon_t *icon, box_t *bbox); + +/** + * Fetch an icon's type. + * + * \param[in] icon Icon to query. + * \return The icon's type. + */ +wuss_icon_type_t wuss_icon_get_type(const wuss_icon_t *icon); + +/** + * Fetch an icon's current label text. + * + * \param[in] icon Icon to query. + * \return The label, never NULL (may be ""). Owned by the icon; valid until the + * next wuss_icon_set_text or wuss_icon_delete on it. + */ +const char *wuss_icon_get_text(const wuss_icon_t *icon); + +/** + * Fetch the window an icon belongs to. + * + * \param[in] icon Icon to query. + * \return The owning window. + */ +wuss_window_t *wuss_icon_get_window(const wuss_icon_t *icon); + +#ifdef __cplusplus +} +#endif + +#endif /* WUSS_ICON_H */ diff --git a/include/wuss/task.h b/include/wuss/task.h index 78a11cb8..02b65a4a 100644 --- a/include/wuss/task.h +++ b/include/wuss/task.h @@ -21,6 +21,7 @@ extern "C" #include "framebuf/screen.h" #include "wuss/wuss.h" +#include "wuss/icon.h" /* ----------------------------------------------------------------------- */ @@ -30,8 +31,9 @@ typedef enum wuss_event_kind wuss_EVENT_IDLE, /**< Wuss has finished its pending tasks. */ wuss_EVENT_REDRAW, /**< Part of the window's content needs repainting. */ wuss_EVENT_OPEN, /**< Window moved or resized. */ - wuss_EVENT_CLOSE, /**< Close icon clicked; Wuss takes no action itself. */ + wuss_EVENT_CLOSE, /**< Close button clicked; Wuss takes no action itself. */ wuss_EVENT_MOUSE, /**< Button down/up over the window's content. */ + wuss_EVENT_ICON, /**< A work-area button icon was clicked or hovered. */ wuss_EVENT_SCROLL, /**< Mouse wheel used over the window's content. */ wuss_EVENT_QUIT /**< Task shutting down, via wuss_task_stop. */ } @@ -87,6 +89,20 @@ typedef struct wuss_event } mouse; + /** wuss_EVENT_ICON: delivered instead of wuss_EVENT_MOUSE while the + * pointer is inside a wuss_ICON_TYPE_BUTTON icon's bounding box. Label, + * hidden and disabled icons never raise this -- those clicks fall through + * as wuss_EVENT_MOUSE. action is DOWN/UP/MOVE; button is a set of + * wuss_button_t flags, so test it with '&' rather than comparing for + * equality. */ + struct + { + wuss_icon_t *icon; + wuss_mouse_action_t action; + wuss_button_t button; + } + icon; + /** wuss_EVENT_SCROLL: point is window-local content coordinates, as * per mouse. delta's sign and units are as passed to wuss_scroll. */ struct diff --git a/include/wuss/wuss.h b/include/wuss/wuss.h index 99d6e1cc..9752036e 100644 --- a/include/wuss/wuss.h +++ b/include/wuss/wuss.h @@ -28,6 +28,10 @@ extern "C" #define result_WUSS_TOO_SMALL (result_BASE_WUSS + 0) /** A palette index was out of range. */ #define result_WUSS_BAD_COLOUR (result_BASE_WUSS + 1) +/** + * An icon spec was malformed (unknown type, or BUTTON without a fill colour). + */ +#define result_WUSS_BAD_ICON (result_BASE_WUSS + 2) /* ----------------------------------------------------------------------- */ @@ -173,6 +177,19 @@ typedef struct wuss_config /** Furniture chrome colours. */ wuss_palette_t palette; + /** + * Bevelled work-area button edge shades, as indices into the system palette: + * light on the top/left edges, dark on the bottom/right (swapped when the + * button is pressed). Both default to the titlebar fill colour when config is + * NULL. + */ + struct + { + wuss_colour_t light; /**< Top/left bevel edge. */ + wuss_colour_t dark; /**< Bottom/right bevel edge. */ + } + bevel; + /** * Desktop background colour, painted behind windows on every redraw, or * wuss_NO_BACKGROUND to leave the background untouched (the caller must then diff --git a/libraries/wuss/create.c b/libraries/wuss/create.c index 82eaca5c..7e00e5fe 100644 --- a/libraries/wuss/create.c +++ b/libraries/wuss/create.c @@ -23,6 +23,7 @@ result_t wuss_create(screen_t *scr, wuss_t *w; wuss_palette_t pal; wuss_colour_t bg, fg; + wuss_colour_t blight, bdark; int font_height; int font_width; @@ -65,6 +66,8 @@ result_t wuss_create(screen_t *scr, if (config != NULL) { pal = config->palette; + blight = config->bevel.light; + bdark = config->bevel.dark; w->backdrop = config->backdrop; } else @@ -91,6 +94,9 @@ result_t wuss_create(screen_t *scr, pal.scroll.arrows = bg; pal.scroll.wells = bg; pal.scroll.sausages = fg; + + blight = pal.title.bg; + bdark = pal.title.bg; } if (pal.title.bg < 0 || pal.title.bg >= w->npalette || @@ -102,6 +108,8 @@ result_t wuss_create(screen_t *scr, pal.scroll.arrows < 0 || pal.scroll.arrows >= w->npalette || pal.scroll.wells < 0 || pal.scroll.wells >= w->npalette || pal.scroll.sausages < 0 || pal.scroll.sausages >= w->npalette || + blight < 0 || blight >= w->npalette || + bdark < 0 || bdark >= w->npalette || (w->backdrop != wuss_NO_BACKGROUND && (w->backdrop < 0 || w->backdrop >= w->npalette))) { @@ -111,6 +119,8 @@ result_t wuss_create(screen_t *scr, } w->furniture_colours = pal; + w->bevel_light = blight; + w->bevel_dark = bdark; if (config != NULL && config->titlebar_height > 0) { diff --git a/libraries/wuss/destroy.c b/libraries/wuss/destroy.c index c546af82..256b8791 100644 --- a/libraries/wuss/destroy.c +++ b/libraries/wuss/destroy.c @@ -21,6 +21,7 @@ void wuss_destroy(wuss_t *doomed) list_t *next; next = e->next; + wuss__icons_free((wuss_window_t *) e); free(e); e = next; } diff --git a/libraries/wuss/furniture/back-box.c b/libraries/wuss/furniture/back-box.c index f1ada608..deab1238 100644 --- a/libraries/wuss/furniture/back-box.c +++ b/libraries/wuss/furniture/back-box.c @@ -9,8 +9,8 @@ void wuss__back_box(const wuss_window_t *window, box_t *out) wuss__titlebar_box(window, &titlebar); - inset = WUSS_ICON_INSET; - size = wuss__icon_size(window); + inset = WUSS_BUTTON_INSET; + size = wuss__button_size(window); out->x0 = titlebar.x0 + inset; out->y0 = titlebar.y0 + inset; diff --git a/libraries/wuss/furniture/close-box.c b/libraries/wuss/furniture/close-box.c index c927f8c5..424af7c4 100644 --- a/libraries/wuss/furniture/close-box.c +++ b/libraries/wuss/furniture/close-box.c @@ -9,8 +9,8 @@ void wuss__close_box(const wuss_window_t *window, box_t *out) wuss__titlebar_box(window, &titlebar); - inset = WUSS_ICON_INSET; - size = wuss__icon_size(window); + inset = WUSS_BUTTON_INSET; + size = wuss__button_size(window); out->x0 = titlebar.x0 + inset; if (!(window->flags & wuss_WINDOW_NO_BACK)) diff --git a/libraries/wuss/furniture/content-box.c b/libraries/wuss/furniture/content-box.c index 0f8081e6..72f379eb 100644 --- a/libraries/wuss/furniture/content-box.c +++ b/libraries/wuss/furniture/content-box.c @@ -14,7 +14,7 @@ void wuss__content_box(const wuss_window_t *window, box_t *out) out->x1 = window->visible.x1 - outline_px; out->y1 = window->visible.y1 - outline_px; - wuss__furniture_carve_for(window->flags, wuss__icon_size(window), &carve); + wuss__furniture_carve_for(window->flags, wuss__button_size(window), &carve); out->x1 -= carve.x; out->y1 -= carve.y; } diff --git a/libraries/wuss/furniture/draw.c b/libraries/wuss/furniture/draw.c index abd8968d..a30cb44d 100644 --- a/libraries/wuss/furniture/draw.c +++ b/libraries/wuss/furniture/draw.c @@ -207,7 +207,7 @@ void wuss__furniture_draw(wuss_t *wuss, /* Interior rules: where furniture is carved off the content area's right * or bottom edge, the last pixel of the carve is a dividing line. */ wuss__content_box(window, &content); - wuss__furniture_carve_for(window->flags, wuss__icon_size(window), &carve); + wuss__furniture_carve_for(window->flags, wuss__button_size(window), &carve); if (carve.x > 0) { diff --git a/libraries/wuss/furniture/hscroll-box.c b/libraries/wuss/furniture/hscroll-box.c index 763071e7..31bbdc9b 100644 --- a/libraries/wuss/furniture/hscroll-box.c +++ b/libraries/wuss/furniture/hscroll-box.c @@ -7,7 +7,7 @@ static void hscroll_row(const wuss_window_t *window, box_t *out) int outline_px, size; outline_px = wuss__outline_px(window); - size = wuss__icon_size(window); + size = wuss__button_size(window); out->y1 = window->visible.y1 - outline_px; out->y0 = out->y1 - size; @@ -22,7 +22,7 @@ void wuss__hscroll_left_box(const wuss_window_t *window, box_t *out) int size; hscroll_row(window, &row); - size = wuss__icon_size(window); + size = wuss__button_size(window); out->y0 = row.y0; out->y1 = row.y1; @@ -36,7 +36,7 @@ void wuss__hscroll_right_box(const wuss_window_t *window, box_t *out) int size; hscroll_row(window, &row); - size = wuss__icon_size(window); + size = wuss__button_size(window); out->y0 = row.y0; out->y1 = row.y1; @@ -50,7 +50,7 @@ void wuss__hscroll_well_box(const wuss_window_t *window, box_t *out) int size; hscroll_row(window, &row); - size = wuss__icon_size(window); + size = wuss__button_size(window); out->y0 = row.y0; out->y1 = row.y1; diff --git a/libraries/wuss/furniture/invalidate.c b/libraries/wuss/furniture/invalidate.c index bd044e3f..dc2e3ba7 100644 --- a/libraries/wuss/furniture/invalidate.c +++ b/libraries/wuss/furniture/invalidate.c @@ -16,7 +16,7 @@ void wuss__furniture_invalidate_for(wuss_window_t *window, const box_t *visible) /* Ask for the same carve the layout uses rather than reading the * scrollbar flags directly: a window with both scrollbars off but resize * on still reserves both strips, for the resize icon and the rules. */ - wuss__furniture_carve_for(window->flags, wuss__icon_size(window), &carve); + wuss__furniture_carve_for(window->flags, wuss__button_size(window), &carve); if (!(window->flags & wuss_WINDOW_NO_TITLEBAR)) { diff --git a/libraries/wuss/furniture/resize-box.c b/libraries/wuss/furniture/resize-box.c index 0a9707b9..143ab98c 100644 --- a/libraries/wuss/furniture/resize-box.c +++ b/libraries/wuss/furniture/resize-box.c @@ -7,7 +7,7 @@ void wuss__resize_box(const wuss_window_t *window, box_t *out) int outline_px, size; outline_px = wuss__outline_px(window); - size = wuss__icon_size(window); + size = wuss__button_size(window); out->x1 = window->visible.x1 - outline_px; out->x0 = out->x1 - size; diff --git a/libraries/wuss/furniture/toggle-action.c b/libraries/wuss/furniture/toggle-action.c index 0fda6c67..6a330d6f 100644 --- a/libraries/wuss/furniture/toggle-action.c +++ b/libraries/wuss/furniture/toggle-action.c @@ -22,7 +22,7 @@ void wuss__furniture_toggle_size(wuss_window_t *window) outline_px = wuss__outline_px(window); titlebar_height = wuss__titlebar_height(window); - wuss__furniture_carve_for(window->flags, wuss__icon_size(window), &carve); + wuss__furniture_carve_for(window->flags, wuss__button_size(window), &carve); /* bounded by what's actually left of the screen from the window's * current top-left, not the screen's full width/height -- otherwise a diff --git a/libraries/wuss/furniture/toggle-box.c b/libraries/wuss/furniture/toggle-box.c index 8f7ee7d3..4d9cf3f4 100644 --- a/libraries/wuss/furniture/toggle-box.c +++ b/libraries/wuss/furniture/toggle-box.c @@ -9,8 +9,8 @@ void wuss__toggle_box(const wuss_window_t *window, box_t *out) wuss__titlebar_box(window, &titlebar); - inset = WUSS_ICON_INSET; - size = wuss__icon_size(window); + inset = WUSS_BUTTON_INSET; + size = wuss__button_size(window); out->x1 = titlebar.x1 - inset; out->x0 = out->x1 - size; diff --git a/libraries/wuss/furniture/vscroll-box.c b/libraries/wuss/furniture/vscroll-box.c index 58e84f12..ba873db6 100644 --- a/libraries/wuss/furniture/vscroll-box.c +++ b/libraries/wuss/furniture/vscroll-box.c @@ -8,7 +8,7 @@ static void vscroll_column(const wuss_window_t *window, box_t *out) int outline_px, size; outline_px = wuss__outline_px(window); - size = wuss__icon_size(window); + size = wuss__button_size(window); out->x1 = window->visible.x1 - outline_px; out->x0 = out->x1 - size; @@ -24,7 +24,7 @@ void wuss__vscroll_up_box(const wuss_window_t *window, box_t *out) int size; vscroll_column(window, &column); - size = wuss__icon_size(window); + size = wuss__button_size(window); out->x0 = column.x0; out->x1 = column.x1; @@ -38,7 +38,7 @@ void wuss__vscroll_down_box(const wuss_window_t *window, box_t *out) int size; vscroll_column(window, &column); - size = wuss__icon_size(window); + size = wuss__button_size(window); out->x0 = column.x0; out->x1 = column.x1; @@ -52,7 +52,7 @@ void wuss__vscroll_well_box(const wuss_window_t *window, box_t *out) int size; vscroll_column(window, &column); - size = wuss__icon_size(window); + size = wuss__button_size(window); out->x0 = column.x0; out->x1 = column.x1; diff --git a/libraries/wuss/icon.h b/libraries/wuss/icon.h new file mode 100644 index 00000000..a2c415c9 --- /dev/null +++ b/libraries/wuss/icon.h @@ -0,0 +1,53 @@ +/* icon.h -- wuss - work-area icons, internal */ + +#ifndef WUSS_ICON_IMPL_H +#define WUSS_ICON_IMPL_H + +#include "geom/box.h" +#include "geom/point.h" + +#include "framebuf/screen.h" + +#include "wuss/wuss.h" +#include "wuss/icon.h" + +struct wuss_icon +{ + wuss_window_t *window; /* owner; back-pointer for invalidate/get_window */ + box_t bbox; /* virtual document space */ + wuss_icon_type_t type; + char *text; /* owned; never NULL ("" instead) */ + wuss_colour_t fg; + wuss_colour_t bg; + wuss_icon_flags_t flags; + int pressed; /* button: 1 while held with the pointer inside */ +}; + +/* Convert an icon's bbox (virtual document space) to a screen-space box, using + * the owning window's current content box and scroll offset: + * screen = content.x0 - scroll.x + bbox. Mirrors wuss_window_invalidate. */ +void wuss__icon_screen_box(const wuss_icon_t *icon, box_t *out); + +/* Invalidate exactly this icon's bbox, via wuss_window_invalidate, so a + * set_text / pressed-state / hide change repaints just the icon. */ +void wuss__icon_invalidate(const wuss_icon_t *icon); + +/* Draw one icon. Called from redraw_window with wuss->scr->clip already set to + * the surviving content piece and the background already filled. "content" is + * the window's full (unclipped) content box, screen space; "scroll" is + * window->scroll. */ +void wuss__icon_draw(wuss_t *wuss, + const wuss_icon_t *icon, + const box_t *content, + point_t scroll); + +/* Hit-test every visible, enabled button icon of "window" against a point given + * in virtual document space. Returns the topmost (last-created wins) match, or + * NULL. Label, hidden and disabled icons are skipped. */ +wuss_icon_t *wuss__icon_hit_test(wuss_window_t *window, point_t doc_point); + +/* Free a window's whole icon store (text + nodes + array). Teardown only: does + * not invalidate or swap-remove. */ +void wuss__icons_free(wuss_window_t *window); + +#endif /* WUSS_ICON_IMPL_H */ diff --git a/libraries/wuss/icon/create.c b/libraries/wuss/icon/create.c new file mode 100644 index 00000000..823f1860 --- /dev/null +++ b/libraries/wuss/icon/create.c @@ -0,0 +1,82 @@ +/* create.c -- wuss - create a work-area icon */ + +#include +#include +#include + +#ifdef FORTIFY +#include "fortify/fortify.h" +#endif + +#include "../impl.h" + +result_t wuss_icon_create(wuss_window_t *window, + const wuss_icon_spec_t *spec, + wuss_icon_t **icon) +{ + wuss_t *w; + wuss_icon_t *it; + wuss_icon_t **grown; + const char *src; + size_t len; + int newcap; + + assert(window != NULL); + assert(spec != NULL); + + w = window->wuss; + + if (spec->type != wuss_ICON_TYPE_LABEL && spec->type != wuss_ICON_TYPE_BUTTON) + return result_WUSS_BAD_ICON; + + if (spec->type == wuss_ICON_TYPE_BUTTON && spec->bg == wuss_NO_BACKGROUND) + return result_WUSS_BAD_ICON; + + if (spec->fg < 0 || spec->fg >= w->npalette) + return result_WUSS_BAD_COLOUR; + + if (spec->bg != wuss_NO_BACKGROUND && + (spec->bg < 0 || spec->bg >= w->npalette)) + return result_WUSS_BAD_COLOUR; + + if (window->nicons == window->cap_icons) + { + newcap = (window->cap_icons == 0) ? 4 : window->cap_icons * 2; + grown = realloc(window->icons, newcap * sizeof(*window->icons)); + if (grown == NULL) + return result_OOM; + window->icons = grown; + window->cap_icons = newcap; + } + + it = malloc(sizeof(*it)); + if (it == NULL) + return result_OOM; + + src = (spec->text != NULL) ? spec->text : ""; + len = strlen(src); + it->text = malloc(len + 1); + if (it->text == NULL) + { + free(it); + return result_OOM; + } + memcpy(it->text, src, len + 1); + + it->window = window; + it->bbox = spec->bbox; + it->type = spec->type; + it->fg = spec->fg; + it->bg = spec->bg; + it->flags = spec->flags; + it->pressed = 0; + + window->icons[window->nicons++] = it; + + wuss__icon_invalidate(it); + + if (icon != NULL) + *icon = it; + + return result_OK; +} diff --git a/libraries/wuss/icon/delete.c b/libraries/wuss/icon/delete.c new file mode 100644 index 00000000..d8cc7ee5 --- /dev/null +++ b/libraries/wuss/icon/delete.c @@ -0,0 +1,34 @@ +/* delete.c -- wuss - destroy a work-area icon */ + +#include + +#ifdef FORTIFY +#include "fortify/fortify.h" +#endif + +#include "../impl.h" + +void wuss_icon_delete(wuss_icon_t *icon) +{ + wuss_window_t *window; + int i; + + if (icon == NULL) + return; + + window = icon->window; + + wuss__icon_invalidate(icon); + + for (i = 0; i < window->nicons; i++) + { + if (window->icons[i] == icon) + { + window->icons[i] = window->icons[--window->nicons]; + break; + } + } + + free(icon->text); + free(icon); +} diff --git a/libraries/wuss/icon/draw.c b/libraries/wuss/icon/draw.c new file mode 100644 index 00000000..98606923 --- /dev/null +++ b/libraries/wuss/icon/draw.c @@ -0,0 +1,144 @@ +/* draw.c -- wuss - draw a work-area icon */ + +#include + +#include "base/utils.h" +#include "geom/box.h" +#include "geom/point.h" +#include "geom/size.h" +#include "framebuf/bmfont.h" +#include "framebuf/screen.h" + +#include "../impl.h" + +/* ----------------------------------------------------------------------- */ + +static void icon_bevel(screen_t *scr, + const box_t *b, + colour_t fill, + colour_t light, + colour_t dark) +{ + screen_draw_rect(scr, b->x0, b->y0, + SIZE2D(b->x1 - b->x0, b->y1 - b->y0), fill); + + screen_draw_line(scr, b->x0, b->y0, b->x1 - 1, b->y0, light); + screen_draw_line(scr, b->x0, b->y0, b->x0, b->y1 - 1, light); + screen_draw_line(scr, b->x0, b->y1 - 1, b->x1 - 1, b->y1 - 1, dark); + screen_draw_line(scr, b->x1 - 1, b->y0, b->x1 - 1, b->y1 - 1, dark); +} + +/* ----------------------------------------------------------------------- */ + +void wuss__icon_draw(wuss_t *wuss, + const wuss_icon_t *icon, + const box_t *content, + point_t scroll) +{ + screen_t *scr; + box_t b; + colour_t fg; + int font_width, font_height; + int have_font; + + if (icon->flags & wuss_ICON_FLAGS_HIDDEN) + return; + + scr = wuss->scr; + + b.x0 = content->x0 - scroll.x + icon->bbox.x0; + b.y0 = content->y0 - scroll.y + icon->bbox.y0; + b.x1 = content->x0 - scroll.x + icon->bbox.x1; + b.y1 = content->y0 - scroll.y + icon->bbox.y1; + + if (b.x1 <= b.x0 || b.y1 <= b.y0) + return; + + fg = wuss->palette[icon->fg]; + + have_font = (wuss->font != NULL && icon->text[0] != '\0'); + if (have_font) + bmfont_get_info(wuss->font, &font_width, &font_height); + else + font_width = font_height = 0; + NOT_USED(font_width); + + switch (icon->type) + { + case wuss_ICON_TYPE_LABEL: + { + colour_t bg; + + if (icon->bg != wuss_NO_BACKGROUND) + { + bg = wuss->palette[icon->bg]; + screen_draw_rect(scr, b.x0, b.y0, + SIZE2D(b.x1 - b.x0, b.y1 - b.y0), bg); + } + else if (icon->window->bg != wuss_NO_BACKGROUND) + { + bg = wuss->palette[icon->window->bg]; + } + else + { + bg = fg; /* bmfont needs a blend colour; nothing better to offer */ + } + + if (have_font) + { + point_t pos; + + pos.x = b.x0 + 1; + pos.y = b.y0 + (b.y1 - b.y0 - font_height) / 2; + bmfont_draw(wuss->font, scr, icon->text, (int) strlen(icon->text), + fg, bg, &pos, NULL); + } + } + break; + + case wuss_ICON_TYPE_BUTTON: + { + colour_t light, dark, base; + int pressed; + + base = wuss->palette[icon->bg]; + light = wuss->palette[wuss->bevel_light]; + dark = wuss->palette[wuss->bevel_dark]; + pressed = icon->pressed; + + if (icon->flags & wuss_ICON_FLAGS_DISABLED) + fg = dark; /* greyed: label sinks toward the dark bevel shade */ + + if (pressed) + icon_bevel(scr, &b, base, dark, light); + else + icon_bevel(scr, &b, base, light, dark); + + if (have_font) + { + point_t pos; + int interior_w, split_point; + bmfont_width_t width; + + interior_w = (b.x1 - b.x0) - 2; + if (interior_w < 1) + interior_w = 1; + + bmfont_measure(wuss->font, icon->text, (int) strlen(icon->text), + interior_w, &split_point, &width); + + pos.x = b.x0 + ((b.x1 - b.x0) - width) / 2; + pos.y = b.y0 + (b.y1 - b.y0 - font_height) / 2; + if (pressed) + { + pos.x += 1; + pos.y += 1; + } + + bmfont_draw(wuss->font, scr, icon->text, (int) strlen(icon->text), + fg, base, &pos, NULL); + } + } + break; + } +} diff --git a/libraries/wuss/icon/free.c b/libraries/wuss/icon/free.c new file mode 100644 index 00000000..d7cbac7e --- /dev/null +++ b/libraries/wuss/icon/free.c @@ -0,0 +1,25 @@ +/* free.c -- wuss - free a window's whole icon store */ + +#include + +#ifdef FORTIFY +#include "fortify/fortify.h" +#endif + +#include "../impl.h" + +void wuss__icons_free(wuss_window_t *window) +{ + int i; + + for (i = 0; i < window->nicons; i++) + { + free(window->icons[i]->text); + free(window->icons[i]); + } + + free(window->icons); + window->icons = NULL; + window->nicons = 0; + window->cap_icons = 0; +} diff --git a/libraries/wuss/icon/get-bbox.c b/libraries/wuss/icon/get-bbox.c new file mode 100644 index 00000000..55bdbd8d --- /dev/null +++ b/libraries/wuss/icon/get-bbox.c @@ -0,0 +1,8 @@ +/* get-bbox.c -- wuss - read a work-area icon's bounding box */ + +#include "../impl.h" + +void wuss_icon_get_bbox(const wuss_icon_t *icon, box_t *bbox) +{ + *bbox = icon->bbox; +} diff --git a/libraries/wuss/icon/get-text.c b/libraries/wuss/icon/get-text.c new file mode 100644 index 00000000..2978cdb7 --- /dev/null +++ b/libraries/wuss/icon/get-text.c @@ -0,0 +1,8 @@ +/* get-text.c -- wuss - read a work-area icon's label */ + +#include "../impl.h" + +const char *wuss_icon_get_text(const wuss_icon_t *icon) +{ + return icon->text; +} diff --git a/libraries/wuss/icon/get-type.c b/libraries/wuss/icon/get-type.c new file mode 100644 index 00000000..84b34091 --- /dev/null +++ b/libraries/wuss/icon/get-type.c @@ -0,0 +1,8 @@ +/* get-type.c -- wuss - read a work-area icon's type */ + +#include "../impl.h" + +wuss_icon_type_t wuss_icon_get_type(const wuss_icon_t *icon) +{ + return icon->type; +} diff --git a/libraries/wuss/icon/get-window.c b/libraries/wuss/icon/get-window.c new file mode 100644 index 00000000..9921af6d --- /dev/null +++ b/libraries/wuss/icon/get-window.c @@ -0,0 +1,8 @@ +/* get-window.c -- wuss - read a work-area icon's owning window */ + +#include "../impl.h" + +wuss_window_t *wuss_icon_get_window(const wuss_icon_t *icon) +{ + return icon->window; +} diff --git a/libraries/wuss/icon/hit-test.c b/libraries/wuss/icon/hit-test.c new file mode 100644 index 00000000..d7579e21 --- /dev/null +++ b/libraries/wuss/icon/hit-test.c @@ -0,0 +1,28 @@ +/* hit-test.c -- wuss - work-area icon hit testing */ + +#include "geom/box.h" + +#include "../impl.h" + +wuss_icon_t *wuss__icon_hit_test(wuss_window_t *window, point_t doc_point) +{ + wuss_icon_t *it; + int i; + + /* last created wins, matching the draw order in redraw_window */ + for (i = window->nicons - 1; i >= 0; i--) + { + it = window->icons[i]; + + if (it->type != wuss_ICON_TYPE_BUTTON) + continue; + + if (it->flags & (wuss_ICON_FLAGS_HIDDEN | wuss_ICON_FLAGS_DISABLED)) + continue; + + if (box_contains_point(&it->bbox, doc_point.x, doc_point.y)) + return it; + } + + return NULL; +} diff --git a/libraries/wuss/icon/invalidate.c b/libraries/wuss/icon/invalidate.c new file mode 100644 index 00000000..91c9e47f --- /dev/null +++ b/libraries/wuss/icon/invalidate.c @@ -0,0 +1,10 @@ +/* invalidate.c -- wuss - mark a work-area icon's bbox dirty */ + +#include "../impl.h" + +void wuss__icon_invalidate(const wuss_icon_t *icon) +{ + /* the icon's bbox is already in the window-local (pre-scroll) coordinates + * wuss_window_invalidate expects */ + wuss_window_invalidate(icon->window, &icon->bbox); +} diff --git a/libraries/wuss/icon/screen-box.c b/libraries/wuss/icon/screen-box.c new file mode 100644 index 00000000..25440c7e --- /dev/null +++ b/libraries/wuss/icon/screen-box.c @@ -0,0 +1,17 @@ +/* screen-box.c -- wuss - work-area icon bbox to screen space */ + +#include "../impl.h" + +void wuss__icon_screen_box(const wuss_icon_t *icon, box_t *out) +{ + box_t content; + point_t scroll; + + wuss__content_box(icon->window, &content); + scroll = icon->window->scroll; + + out->x0 = content.x0 - scroll.x + icon->bbox.x0; + out->y0 = content.y0 - scroll.y + icon->bbox.y0; + out->x1 = content.x0 - scroll.x + icon->bbox.x1; + out->y1 = content.y0 - scroll.y + icon->bbox.y1; +} diff --git a/libraries/wuss/icon/set-hidden.c b/libraries/wuss/icon/set-hidden.c new file mode 100644 index 00000000..c76e35a8 --- /dev/null +++ b/libraries/wuss/icon/set-hidden.c @@ -0,0 +1,13 @@ +/* set-hidden.c -- wuss - show or hide a work-area icon */ + +#include "../impl.h" + +void wuss_icon_set_hidden(wuss_icon_t *icon, int hidden) +{ + if (hidden) + icon->flags |= wuss_ICON_FLAGS_HIDDEN; + else + icon->flags &= (wuss_icon_flags_t) ~wuss_ICON_FLAGS_HIDDEN; + + wuss__icon_invalidate(icon); +} diff --git a/libraries/wuss/icon/set-text.c b/libraries/wuss/icon/set-text.c new file mode 100644 index 00000000..3a5eca25 --- /dev/null +++ b/libraries/wuss/icon/set-text.c @@ -0,0 +1,32 @@ +/* set-text.c -- wuss - replace a work-area icon's label */ + +#include +#include + +#ifdef FORTIFY +#include "fortify/fortify.h" +#endif + +#include "../impl.h" + +result_t wuss_icon_set_text(wuss_icon_t *icon, const char *text) +{ + const char *src; + char *dup; + size_t len; + + src = (text != NULL) ? text : ""; + len = strlen(src); + + dup = malloc(len + 1); + if (dup == NULL) + return result_OOM; + memcpy(dup, src, len + 1); + + free(icon->text); + icon->text = dup; + + wuss__icon_invalidate(icon); + + return result_OK; +} diff --git a/libraries/wuss/impl.h b/libraries/wuss/impl.h index 7ab0ed0f..9a83a09d 100644 --- a/libraries/wuss/impl.h +++ b/libraries/wuss/impl.h @@ -14,6 +14,7 @@ #include "wuss/window.h" #include "furniture.h" +#include "icon.h" #define WUSS_TITLE_MAX 63 #define WUSS_DEFAULT_TITLEBAR_HEIGHT 20 @@ -25,7 +26,7 @@ * just some avoidable redraw work, never wrong */ #define WUSS_MAX_INVALIDATE_PIECES 32 -#define WUSS_ICON_INSET 3 /* shared by close/back/toggle/resize icons and scrollbar breadth */ +#define WUSS_BUTTON_INSET 3 /* shared by close/back/toggle/resize furniture buttons and scrollbar breadth */ #define WUSS_MIN_CONTENT 20 /* resize-drag floor: content can never be squeezed smaller than this */ #define WUSS_SCROLL_INSET 2 /* sausage cross-axis margin from its well's edges, purely cosmetic */ #define WUSS_DIVIDER_PX 1 /* interior rule between the content area and the furniture on its right/bottom */ @@ -47,6 +48,8 @@ struct wuss colour_t *palette; /* owned */ int npalette; wuss_palette_t furniture_colours; + wuss_colour_t bevel_light; /* work-area button top/left edge */ + wuss_colour_t bevel_dark; /* work-area button bottom/right edge */ wuss_colour_t backdrop; /* wuss_NO_BACKGROUND for none */ int titlebar_height; list_t z_order; /* anchor; head = topmost window */ @@ -72,6 +75,9 @@ struct wuss_window wuss_window_state_t state; /* see wuss_window_state_t */ box_t pre_toggle; /* visible bounds to restore on the next toggle */ char title[WUSS_TITLE_MAX + 1]; + wuss_icon_t **icons; /* owned; array of owned icon pointers */ + int nicons; + int cap_icons; }; wuss_window_t *wuss__window_at(wuss_t *wuss, point_t p); @@ -169,22 +175,22 @@ static inline int wuss__outline_px(const wuss_window_t *window) * none, so NO_TITLEBAR windows that still opt into scrollbars/resize match * their titled siblings instead of a hardcoded size; the hardcoded default * is only a last-resort floor if even that isn't positive */ -static inline int wuss__icon_size_for(const wuss_t *wuss, wuss_window_flags_t flags) +static inline int wuss__button_size_for(const wuss_t *wuss, wuss_window_flags_t flags) { int size; - size = wuss__titlebar_height_for(wuss, flags) - 2 * WUSS_ICON_INSET; + size = wuss__titlebar_height_for(wuss, flags) - 2 * WUSS_BUTTON_INSET; if (size > 0) return size; - size = wuss->titlebar_height - 2 * WUSS_ICON_INSET; + size = wuss->titlebar_height - 2 * WUSS_BUTTON_INSET; - return (size > 0) ? size : WUSS_DEFAULT_TITLEBAR_HEIGHT - 2 * WUSS_ICON_INSET; + return (size > 0) ? size : WUSS_DEFAULT_TITLEBAR_HEIGHT - 2 * WUSS_BUTTON_INSET; } -static inline int wuss__icon_size(const wuss_window_t *window) +static inline int wuss__button_size(const wuss_window_t *window) { - return wuss__icon_size_for(window->wuss, window->flags); + return wuss__button_size_for(window->wuss, window->flags); } /* how much of a content box's width/height is furniture (scrollbars, the @@ -193,18 +199,18 @@ static inline int wuss__icon_size(const wuss_window_t *window) * visible) and window creation/resize (add it to visible up front) so the * two stay consistent with each other */ static inline void wuss__furniture_carve_for(wuss_window_flags_t flags, - int icon_size, + int button_size, point_t *carve) { - carve->x = (flags & wuss_WINDOW_NO_VSCROLL) ? 0 : icon_size; - carve->y = (flags & wuss_WINDOW_NO_HSCROLL) ? 0 : icon_size; + carve->x = (flags & wuss_WINDOW_NO_VSCROLL) ? 0 : button_size; + carve->y = (flags & wuss_WINDOW_NO_HSCROLL) ? 0 : button_size; if (!(flags & wuss_WINDOW_NO_RESIZE) && (flags & wuss_WINDOW_NO_VSCROLL) && (flags & wuss_WINDOW_NO_HSCROLL)) { - carve->x = icon_size; - carve->y = icon_size; + carve->x = button_size; + carve->y = button_size; } /* where furniture abuts the content area, a rule divides the two */ diff --git a/libraries/wuss/mouse-click.c b/libraries/wuss/mouse-click.c index 745f5e7b..634f907d 100644 --- a/libraries/wuss/mouse-click.c +++ b/libraries/wuss/mouse-click.c @@ -154,14 +154,40 @@ result_t wuss_mouse_click(wuss_t *wuss, if (win->task.handle != NULL) { - box_t content; + box_t content; + point_t doc_point; + wuss_icon_t *icon; wuss__content_box(win, &content); - event.kind = wuss_EVENT_MOUSE; - event.data.mouse.action = action; - event.data.mouse.point.x = x - content.x0 + win->scroll.x; - event.data.mouse.point.y = y - content.y0 + win->scroll.y; - event.data.mouse.button = button; + doc_point.x = x - content.x0 + win->scroll.x; + doc_point.y = y - content.y0 + win->scroll.y; + + icon = wuss__icon_hit_test(win, doc_point); + if (icon != NULL) + { + if (action == wuss_MOUSE_DOWN && + (button & (wuss_BUTTON_SELECT | wuss_BUTTON_ADJUST))) + { + icon->pressed = 1; + wuss__icon_invalidate(icon); + } + else if (action == wuss_MOUSE_UP && icon->pressed) + { + icon->pressed = 0; + wuss__icon_invalidate(icon); + } + + event.kind = wuss_EVENT_ICON; + event.data.icon.icon = icon; + event.data.icon.action = action; + event.data.icon.button = button; + return win->task.handle(win, &event, win->task.task_data); + } + + event.kind = wuss_EVENT_MOUSE; + event.data.mouse.action = action; + event.data.mouse.point = doc_point; + event.data.mouse.button = button; return win->task.handle(win, &event, win->task.task_data); } diff --git a/libraries/wuss/mouse-move.c b/libraries/wuss/mouse-move.c index 9b73076d..b5e644e3 100644 --- a/libraries/wuss/mouse-move.c +++ b/libraries/wuss/mouse-move.c @@ -52,14 +52,45 @@ result_t wuss_mouse_move(wuss_t *wuss, point_t p, wuss_window_t **hit) if (win->task.handle != NULL) { box_t content; + point_t doc_point; + wuss_icon_t *icon; wuss_event_t event; + int k; wuss__content_box(win, &content); - event.kind = wuss_EVENT_MOUSE; - event.data.mouse.action = wuss_MOUSE_MOVE; - event.data.mouse.point.x = x - content.x0 + win->scroll.x; - event.data.mouse.point.y = y - content.y0 + win->scroll.y; - event.data.mouse.button = wuss_BUTTON_SELECT; + doc_point.x = x - content.x0 + win->scroll.x; + doc_point.y = y - content.y0 + win->scroll.y; + + icon = wuss__icon_hit_test(win, doc_point); + + /* Clear the pressed state of any button the pointer has left. This does + * not re-press a button on drag-back-in, and does not track which mouse + * button is held -- wuss keeps no persistent "button down over content" + * state. */ + for (k = 0; k < win->nicons; k++) + { + wuss_icon_t *it = win->icons[k]; + + if (it->pressed && it != icon) + { + it->pressed = 0; + wuss__icon_invalidate(it); + } + } + + if (icon != NULL) + { + event.kind = wuss_EVENT_ICON; + event.data.icon.icon = icon; + event.data.icon.action = wuss_MOUSE_MOVE; + event.data.icon.button = wuss_BUTTON_SELECT; + return win->task.handle(win, &event, win->task.task_data); + } + + event.kind = wuss_EVENT_MOUSE; + event.data.mouse.action = wuss_MOUSE_MOVE; + event.data.mouse.point = doc_point; + event.data.mouse.button = wuss_BUTTON_SELECT; return win->task.handle(win, &event, win->task.task_data); } diff --git a/libraries/wuss/redraw.c b/libraries/wuss/redraw.c index 769257a9..e444b126 100644 --- a/libraries/wuss/redraw.c +++ b/libraries/wuss/redraw.c @@ -51,6 +51,15 @@ static void redraw_window(wuss_t *wuss, if (crc != result_OK) *rc = crc; } + + { + int k; + + /* draw in array order so later-created icons paint on top, matching + * wuss__icon_hit_test's reverse scan */ + for (k = 0; k < win->nicons; k++) + wuss__icon_draw(wuss, win->icons[k], &content, win->scroll); + } } } diff --git a/libraries/wuss/test/tasks/icons.c b/libraries/wuss/test/tasks/icons.c new file mode 100644 index 00000000..a16f7b47 --- /dev/null +++ b/libraries/wuss/test/tasks/icons.c @@ -0,0 +1,189 @@ +/* icons.c -- wuss test - work-area icons task */ + +#ifdef USE_SDL +#include "framebuf/palettes.h" + +#include +#include + +#ifdef FORTIFY +#include "fortify/fortify.h" +#endif + +#include "framebuf/palettes.h" +#include "framebuf/screen.h" +#include "geom/box.h" +#include "geom/point.h" +#include "geom/size.h" + +#include "icons.h" + +#define ICONS_DOC_W 220 +#define ICONS_DOC_H 520 /* taller than the window, so scrolling is exercised */ + +result_t icons_create(wuss_t *wuss, + const colour_t *palette, + bmfont_t *font, + icons_task_t *task) +{ + wuss_task_t delegate; + box_t box; + wuss_icon_spec_t spec; + result_t rc; + + task->font = font; + task->ink = palette[palette_PICO8_LAVENDER]; + task->window = NULL; + task->button = NULL; + task->counter = NULL; + task->count = 0; + + delegate = wuss_task_start(icons_handle, task); + box = (box_t) BOX_POS_SIZE(160, 120, ICONS_DOC_W, 160); + + rc = wuss_window_create(wuss, + &box, + "Icons", + wuss_WINDOW_NONE, + palette_PICO8_LIGHT_GREY, + &delegate, + SIZE2D(ICONS_DOC_W, ICONS_DOC_H), + SIZE2D(0, 0), + &task->window); + if (rc != result_OK) + return rc; + + memset(&spec, 0, sizeof(spec)); + + /* a heading label */ + spec.bbox = (box_t) BOX_POS_SIZE(8, 8, 180, 14); + spec.type = wuss_ICON_TYPE_LABEL; + spec.text = "Work-area icons:"; + spec.fg = palette_PICO8_DARK_BLUE; + spec.bg = wuss_NO_BACKGROUND; + spec.flags = wuss_ICON_FLAGS_NONE; + rc = wuss_icon_create(task->window, &spec, NULL); + if (rc != result_OK) + goto failure; + + /* the button that bumps the counter */ + spec.bbox = (box_t) BOX_POS_SIZE(8, 30, 80, 22); + spec.type = wuss_ICON_TYPE_BUTTON; + spec.text = "Press me"; + spec.fg = palette_PICO8_BLACK; + spec.bg = palette_PICO8_LIGHT_GREY; + rc = wuss_icon_create(task->window, &spec, &task->button); + if (rc != result_OK) + goto failure; + + /* the counter label beside it */ + spec.bbox = (box_t) BOX_POS_SIZE(100, 30, 140, 52); + spec.type = wuss_ICON_TYPE_LABEL; + spec.text = "0"; + spec.fg = palette_PICO8_DARK_BLUE; + spec.bg = wuss_NO_BACKGROUND; + rc = wuss_icon_create(task->window, &spec, &task->counter); + if (rc != result_OK) + goto failure; + + /* a button far down the document, to prove icons scroll and stay clickable */ + spec.bbox = (box_t) BOX_POS_SIZE(8, 460, 90, 52); + spec.type = wuss_ICON_TYPE_BUTTON; + spec.text = "Scrolled"; + spec.fg = palette_PICO8_BLACK; + spec.bg = palette_PICO8_LIGHT_GREY; + rc = wuss_icon_create(task->window, &spec, NULL); + if (rc != result_OK) + goto failure; + + return result_OK; + +failure: + wuss_window_close(task->window); + task->window = NULL; + return rc; +} + +void icons_destroy(icons_task_t *task) +{ + wuss_window_close(task->window); +} + +static result_t icons_redraw(const wuss_event_t *event, void *task_data) +{ + icons_task_t *tcx; + screen_t *scr; + const box_t *content; + const box_t *bounds; + point_t scroll; + int phase; + int first; + int x; + + tcx = task_data; + + scr = event->data.redraw.scr; + content = event->data.redraw.content; + bounds = event->data.redraw.bounds; + scroll = event->data.redraw.scroll; + + /* faint vertical rules every 16 document units, to show the task still + * paints under and around the wuss-managed icons -- anchored to the + * document so they track the scroll offset. The screen x of document + * x=d is bounds->x0 - scroll.x + d, so the rules land on screen + * columns congruent to (bounds->x0 - scroll.x) modulo 16. */ + phase = (bounds->x0 - scroll.x) % 16; + if (phase < 0) + phase += 16; + first = content->x0 - ((content->x0 - phase) % 16 + 16) % 16; + for (x = first; x < content->x1; x += 16) + screen_draw_line(scr, x, content->y0, x, content->y1 - 1, tcx->ink); + + return result_OK; +} + +static result_t icons_icon(const wuss_event_t *event, void *task_data) +{ + icons_task_t *tcx; + char buf[16]; + + tcx = task_data; + + if (event->data.icon.action != wuss_MOUSE_DOWN) + return result_OK; + if (event->data.icon.icon != tcx->button) + return result_OK; + + tcx->count++; + snprintf(buf, sizeof(buf), "%d", tcx->count); + + return wuss_icon_set_text(tcx->counter, buf); +} + +result_t icons_handle(wuss_window_t *window, + const wuss_event_t *event, + void *task_data) +{ + icons_task_t *tcx; + + tcx = task_data; + + switch (event->kind) + { + case wuss_EVENT_REDRAW: + return icons_redraw(event, task_data); + + case wuss_EVENT_ICON: + return icons_icon(event, task_data); + + case wuss_EVENT_CLOSE: + wuss_window_close(window); + tcx->window = NULL; + return result_OK; + + default: + return result_OK; + } +} + +#endif /* USE_SDL */ diff --git a/libraries/wuss/test/tasks/icons.h b/libraries/wuss/test/tasks/icons.h new file mode 100644 index 00000000..24da9a89 --- /dev/null +++ b/libraries/wuss/test/tasks/icons.h @@ -0,0 +1,40 @@ +/* icons.h -- wuss test - work-area icons task */ + +#ifndef TASKS_ICONS_H +#define TASKS_ICONS_H + +#ifdef USE_SDL + +#include "framebuf/bmfont.h" +#include "framebuf/colour.h" +#include "wuss/icon.h" +#include "wuss/window.h" + +/* demonstrates wuss-managed work-area icons: a couple of labels, a button that + * bumps a counter, and a second button placed far down the document to show + * icons scroll with the content and stay clickable */ +typedef struct icons_task +{ + wuss_window_t *window; + bmfont_t *font; + colour_t ink; + wuss_icon_t *button; /* "Press me" */ + wuss_icon_t *counter; /* label showing hit count */ + int count; +} +icons_task_t; + +wuss_event_fn_t icons_handle; + +/* create the icons window against the given wuss instance */ +result_t icons_create(wuss_t *wuss, + const colour_t *palette, + bmfont_t *font, + icons_task_t *task); + +/* destroy the icons window created by icons_create */ +void icons_destroy(icons_task_t *task); + +#endif /* USE_SDL */ + +#endif /* TASKS_ICONS_H */ diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 686cd217..05f44151 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -35,6 +35,7 @@ #include "tasks/checker.h" #include "tasks/curve.h" #include "tasks/gradient.h" +#include "tasks/icons.h" #include "tasks/image.h" #include "tasks/launcher.h" #include "tasks/palette.h" @@ -66,6 +67,7 @@ static checker_task_t g_checker_task; static curve_task_t g_curve_task; static sofa_task_t g_sofa_task; static gradient_task_t g_gradient_task; +static icons_task_t g_icons_task; static porter_duff_task_t g_porter_duff_task; static result_t spawn_ball(void) { return ball_create(g_wuss, g_palette, &g_ball_task); } @@ -78,6 +80,7 @@ static result_t spawn_checker(void) { return checker_create(g_wuss, g_palett static result_t spawn_curve(void) { return curve_create(g_wuss, g_palette, &g_curve_task); } static result_t spawn_sofa(void) { return sofa_create(g_wuss, g_palette, &g_sofa_task); } static result_t spawn_gradient(void) { return gradient_create(g_wuss, &g_gradient_task); } +static result_t spawn_icons(void) { return icons_create(g_wuss, g_palette, g_daydream_font, &g_icons_task); } static result_t spawn_porter_duff(void) { return porter_duff_create(g_wuss, g_palette, g_daydream_font, g_resources, &g_porter_duff_task); } static void destroy_ball(void) { ball_destroy(&g_ball_task); } @@ -90,6 +93,7 @@ static void destroy_checker(void) { checker_destroy(&g_checker_task); } static void destroy_curve(void) { curve_destroy(&g_curve_task); } static void destroy_sofa(void) { sofa_destroy(&g_sofa_task); } static void destroy_gradient(void) { gradient_destroy(&g_gradient_task); } +static void destroy_icons(void) { icons_destroy(&g_icons_task); } static void destroy_porter_duff(void) { porter_duff_destroy(&g_porter_duff_task); } static const launcher_entry_t g_launcher_entries[] = @@ -104,6 +108,7 @@ static const launcher_entry_t g_launcher_entries[] = { "Curve", spawn_curve, destroy_curve }, { "Sofa", spawn_sofa, destroy_sofa }, { "Gradient", spawn_gradient, destroy_gradient }, + { "Icons", spawn_icons, destroy_icons }, { "Porter-Duff", spawn_porter_duff, destroy_porter_duff } }; @@ -247,6 +252,8 @@ static result_t wuss_interactive_test(const char *resources) config.palette.scroll.arrows = palette_PICO8_BLUE; config.palette.scroll.wells = palette_PICO8_DARK_BLUE; config.palette.scroll.sausages = palette_PICO8_LIGHT_GREY; + config.bevel.light = palette_PICO8_WHITE; + config.bevel.dark = palette_PICO8_DARK_GREY; config.backdrop = palette_PICO8_LIGHT_GREY; rc = wuss_create(&scr, font, palette, NELEMS(palette), &config, &wuss); @@ -1496,7 +1503,7 @@ result_t wuss_test(const char *resources) goto Failure; /* toggle icon: top-right of the titlebar, inset by 3px, sized 20 - 2*3 - * (default titlebar height 20, WUSS_ICON_INSET 3), matching + * (default titlebar height 20, WUSS_BUTTON_INSET 3), matching * wuss__toggle_box's formula -- mirrored here since the test only sees * the public API */ outline_px = 1; diff --git a/libraries/wuss/window/close.c b/libraries/wuss/window/close.c index 252158cd..411b9e00 100644 --- a/libraries/wuss/window/close.c +++ b/libraries/wuss/window/close.c @@ -23,5 +23,7 @@ void wuss_window_close(wuss_window_t *doomed) list_remove(&wuss->z_order, &doomed->link); + wuss__icons_free(doomed); + free(doomed); } diff --git a/libraries/wuss/window/create.c b/libraries/wuss/window/create.c index 421ab08e..c29e87f2 100644 --- a/libraries/wuss/window/create.c +++ b/libraries/wuss/window/create.c @@ -40,7 +40,7 @@ result_t wuss_window_create(wuss_t *wuss, outline_px = wuss__outline_px_for(flags); titlebar_height = wuss__titlebar_height_for(wuss, flags); - wuss__furniture_carve_for(flags, wuss__icon_size_for(wuss, flags), &carve); + wuss__furniture_carve_for(flags, wuss__button_size_for(wuss, flags), &carve); win->wuss = wuss; win->visible.x0 = content->x0 - outline_px; @@ -79,6 +79,9 @@ result_t wuss_window_create(wuss_t *wuss, win->doc = doc; win->min_doc = min_doc; win->state = wuss_WINDOW_STATE_NONE; + win->icons = NULL; + win->nicons = 0; + win->cap_icons = 0; if (task != NULL) win->task = *task; diff --git a/libraries/wuss/window/resize.c b/libraries/wuss/window/resize.c index 46e6e7ff..4e0fd995 100644 --- a/libraries/wuss/window/resize.c +++ b/libraries/wuss/window/resize.c @@ -42,7 +42,7 @@ result_t wuss_window_resize(wuss_window_t *window, size2d_t size) outline_px = wuss__outline_px(window); titlebar_height = wuss__titlebar_height(window); before = window->visible; - wuss__furniture_carve_for(window->flags, wuss__icon_size(window), &carve); + wuss__furniture_carve_for(window->flags, wuss__button_size(window), &carve); window->visible.x1 = window->visible.x0 + size.w + 2 * outline_px + carve.x; window->visible.y1 = window->visible.y0 + size.h + titlebar_height + 2 * outline_px + carve.y; From 43af586a5d47545b27e6e0570b5be3d3c7631a2e Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sat, 29 Aug 2026 22:08:42 +0100 Subject: [PATCH 13/44] fix(wuss): scroll work-area grid and axis rulers with the document The icons task drew its backdrop grid and x/y axis rulers pinned to the window edge, so Wuss's scroll blit displaced the painted pixels without repainting them, smearing the labels. Anchor all task drawing to document space so it scrolls rigidly with the content, as the blit assumes; add a bmfont-drawn coordinate ruler along the document x=0 and y=0 lines. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/test/tasks/icons.c | 98 ++++++++++++++++++++++++++----- libraries/wuss/test/tasks/icons.h | 4 +- 2 files changed, 85 insertions(+), 17 deletions(-) diff --git a/libraries/wuss/test/tasks/icons.c b/libraries/wuss/test/tasks/icons.c index a16f7b47..bb10155a 100644 --- a/libraries/wuss/test/tasks/icons.c +++ b/libraries/wuss/test/tasks/icons.c @@ -33,6 +33,8 @@ result_t icons_create(wuss_t *wuss, task->font = font; task->ink = palette[palette_PICO8_LAVENDER]; + task->label = palette[palette_PICO8_DARK_BLUE]; + task->paper = palette[palette_PICO8_LIGHT_GREY]; /* the window bg, below */ task->window = NULL; task->button = NULL; task->counter = NULL; @@ -55,8 +57,11 @@ result_t icons_create(wuss_t *wuss, memset(&spec, 0, sizeof(spec)); + /* icons sit past the ruler gutter (see ICONS_GUTTER in icons_redraw) so the + * axis labels have the top/left strip to themselves */ + /* a heading label */ - spec.bbox = (box_t) BOX_POS_SIZE(8, 8, 180, 14); + spec.bbox = (box_t) BOX_POS_SIZE(28, 28, 180, 14); spec.type = wuss_ICON_TYPE_LABEL; spec.text = "Work-area icons:"; spec.fg = palette_PICO8_DARK_BLUE; @@ -67,7 +72,7 @@ result_t icons_create(wuss_t *wuss, goto failure; /* the button that bumps the counter */ - spec.bbox = (box_t) BOX_POS_SIZE(8, 30, 80, 22); + spec.bbox = (box_t) BOX_POS_SIZE(28, 50, 80, 22); spec.type = wuss_ICON_TYPE_BUTTON; spec.text = "Press me"; spec.fg = palette_PICO8_BLACK; @@ -77,7 +82,7 @@ result_t icons_create(wuss_t *wuss, goto failure; /* the counter label beside it */ - spec.bbox = (box_t) BOX_POS_SIZE(100, 30, 140, 52); + spec.bbox = (box_t) BOX_POS_SIZE(120, 50, 120, 22); spec.type = wuss_ICON_TYPE_LABEL; spec.text = "0"; spec.fg = palette_PICO8_DARK_BLUE; @@ -87,7 +92,7 @@ result_t icons_create(wuss_t *wuss, goto failure; /* a button far down the document, to prove icons scroll and stay clickable */ - spec.bbox = (box_t) BOX_POS_SIZE(8, 460, 90, 52); + spec.bbox = (box_t) BOX_POS_SIZE(28, 460, 90, 52); spec.type = wuss_ICON_TYPE_BUTTON; spec.text = "Scrolled"; spec.fg = palette_PICO8_BLACK; @@ -109,6 +114,24 @@ void icons_destroy(icons_task_t *task) wuss_window_close(task->window); } +#define ICONS_GRID 16 /* document-space pitch of the backdrop grid */ +#define ICONS_AXIS_LABEL 64 /* label every Nth grid line along each axis */ + +/* Screen coordinate of the first grid line at or after lo. Grid lines sit at + * document multiples of ICONS_GRID; the screen coordinate of document d is + * origin - scroll + d, so lines fall on screen coordinates congruent to + * (origin - scroll) modulo ICONS_GRID. */ +static int icons_grid_first(int origin, int scroll, int lo) +{ + int phase; + + phase = (origin - scroll) % ICONS_GRID; + if (phase < 0) + phase += ICONS_GRID; + + return lo - ((lo - phase) % ICONS_GRID + ICONS_GRID) % ICONS_GRID; +} + static result_t icons_redraw(const wuss_event_t *event, void *task_data) { icons_task_t *tcx; @@ -116,9 +139,13 @@ static result_t icons_redraw(const wuss_event_t *event, void *task_data) const box_t *content; const box_t *bounds; point_t scroll; - int phase; - int first; + point_t pos; + char buf[16]; + int ox; /* screen x of document x=0 */ + int oy; /* screen y of document y=0 */ + int doc; int x; + int y; tcx = task_data; @@ -127,18 +154,57 @@ static result_t icons_redraw(const wuss_event_t *event, void *task_data) bounds = event->data.redraw.bounds; scroll = event->data.redraw.scroll; - /* faint vertical rules every 16 document units, to show the task still - * paints under and around the wuss-managed icons -- anchored to the - * document so they track the scroll offset. The screen x of document - * x=d is bounds->x0 - scroll.x + d, so the rules land on screen - * columns congruent to (bounds->x0 - scroll.x) modulo 16. */ - phase = (bounds->x0 - scroll.x) % 16; - if (phase < 0) - phase += 16; - first = content->x0 - ((content->x0 - phase) % 16 + 16) % 16; - for (x = first; x < content->x1; x += 16) + ox = bounds->x0 - scroll.x; + oy = bounds->y0 - scroll.y; + + /* Everything this task paints is anchored to the document, not the window, + * so it scrolls rigidly with the content -- which is what Wuss's scroll + * blit assumes. Nothing here is pinned to a window edge. Every draw is + * clipped to the dirty rectangle (content) so partial redraws stay cheap. */ + + /* a faint grid across the whole work area */ + for (x = icons_grid_first(bounds->x0, scroll.x, content->x0); + x < content->x1; + x += ICONS_GRID) screen_draw_line(scr, x, content->y0, x, content->y1 - 1, tcx->ink); + for (y = icons_grid_first(bounds->y0, scroll.y, content->y0); + y < content->y1; + y += ICONS_GRID) + screen_draw_line(scr, content->x0, y, content->x1 - 1, y, tcx->ink); + + /* x-axis ruler: document x printed just below the y=0 line, at each + * labelled grid column. Scrolls with the document like the grid. */ + for (x = icons_grid_first(bounds->x0, scroll.x, content->x0); + x < content->x1; + x += ICONS_GRID) + { + doc = x - ox; + if (doc <= 0 || doc % ICONS_AXIS_LABEL != 0) + continue; + + snprintf(buf, sizeof(buf), "%d", doc); + pos = POINT(x + 2, oy + 2); + bmfont_draw(tcx->font, scr, buf, (int) strlen(buf), + tcx->label, tcx->paper, &pos, NULL); + } + + /* y-axis ruler: document y printed just right of the x=0 line, at each + * labelled grid row. */ + for (y = icons_grid_first(bounds->y0, scroll.y, content->y0); + y < content->y1; + y += ICONS_GRID) + { + doc = y - oy; + if (doc <= 0 || doc % ICONS_AXIS_LABEL != 0) + continue; + + snprintf(buf, sizeof(buf), "%d", doc); + pos = POINT(ox + 2, y + 2); + bmfont_draw(tcx->font, scr, buf, (int) strlen(buf), + tcx->label, tcx->paper, &pos, NULL); + } + return result_OK; } diff --git a/libraries/wuss/test/tasks/icons.h b/libraries/wuss/test/tasks/icons.h index 24da9a89..e683142f 100644 --- a/libraries/wuss/test/tasks/icons.h +++ b/libraries/wuss/test/tasks/icons.h @@ -17,7 +17,9 @@ typedef struct icons_task { wuss_window_t *window; bmfont_t *font; - colour_t ink; + colour_t ink; /* grid lines */ + colour_t label; /* axis coordinate text */ + colour_t paper; /* window bg, for bmfont_draw glyph blending */ wuss_icon_t *button; /* "Press me" */ wuss_icon_t *counter; /* label showing hit count */ int count; From 63a6fdea0318fec93c316367e052d18143ca0299 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sat, 29 Aug 2026 23:01:52 +0100 Subject: [PATCH 14/44] feat(wuss): add wuss_icon_create_array for bulk icon creation Loops wuss_icon_create over a spec array with all-or-nothing rollback: on the first failure any icons already created by the call are destroyed and no handles are written. Converts the icons test task to use it. Co-Authored-By: Claude Sonnet 5 --- CMakeLists.txt | 1 + include/wuss/icon.h | 20 ++++++++ libraries/wuss/icon/create-array.c | 43 ++++++++++++++++++ libraries/wuss/test/tasks/icons.c | 73 ++++++++++++++---------------- 4 files changed, 98 insertions(+), 39 deletions(-) create mode 100644 libraries/wuss/icon/create-array.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 8861523b..f71da178 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -338,6 +338,7 @@ set(WUSS_SOURCES libraries/wuss/furniture.h libraries/wuss/get-font.c libraries/wuss/icon/create.c + libraries/wuss/icon/create-array.c libraries/wuss/icon/delete.c libraries/wuss/icon/draw.c libraries/wuss/icon/free.c diff --git a/include/wuss/icon.h b/include/wuss/icon.h index ce8c3ea7..ee931169 100644 --- a/include/wuss/icon.h +++ b/include/wuss/icon.h @@ -109,6 +109,26 @@ result_t wuss_icon_create(wuss_window_t *window, const wuss_icon_spec_t *spec, wuss_icon_t **icon); +/** + * Create several icons on a window in one call, as if by \ref wuss_icon_create + * for each. Either all \c nspecs icons are created, or none are: on the first + * failure any icons already created by this call are destroyed and no handles + * are written. + * + * \param[in] window Window to attach the icons to. + * \param[in] specs Array of \c nspecs icon descriptions; each copied. + * \param[in] nspecs Number of entries in \c specs. Zero is a no-op. + * \param[out] icons Array of \c nspecs handles, filled in on success, or NULL + * if the caller does not need them. Untouched on failure. + * \return \ref result_OK on success, or the first failing \ref wuss_icon_create + * code (\ref result_OOM, \ref result_WUSS_BAD_COLOUR, \ref + * result_WUSS_BAD_ICON). + */ +result_t wuss_icon_create_array(wuss_window_t *window, + const wuss_icon_spec_t *specs, + int nspecs, + wuss_icon_t **icons); + /** * Destroy an icon, unlinking it from its window and invalidating its bounding * box so the next redraw clears it. Safe to pass NULL. diff --git a/libraries/wuss/icon/create-array.c b/libraries/wuss/icon/create-array.c new file mode 100644 index 00000000..5da88935 --- /dev/null +++ b/libraries/wuss/icon/create-array.c @@ -0,0 +1,43 @@ +/* create-array.c -- wuss - create several work-area icons at once */ + +#include +#include + +#ifdef FORTIFY +#include "fortify/fortify.h" +#endif + +#include "../impl.h" + +result_t wuss_icon_create_array(wuss_window_t *window, + const wuss_icon_spec_t *specs, + int nspecs, + wuss_icon_t **icons) +{ + result_t rc; + int i; + int j; + + assert(window != NULL); + assert(specs != NULL || nspecs == 0); + + for (i = 0; i < nspecs; i++) + { + wuss_icon_t *it; + + rc = wuss_icon_create(window, &specs[i], &it); + if (rc != result_OK) + { + /* all-or-nothing: unwind the icons this call already created. They are + * the last (i) entries on the window's icon list, newest last. */ + for (j = 0; j < i; j++) + wuss_icon_delete(window->icons[window->nicons - 1]); + return rc; + } + + if (icons != NULL) + icons[i] = it; + } + + return result_OK; +} diff --git a/libraries/wuss/test/tasks/icons.c b/libraries/wuss/test/tasks/icons.c index bb10155a..a183a0cb 100644 --- a/libraries/wuss/test/tasks/icons.c +++ b/libraries/wuss/test/tasks/icons.c @@ -28,7 +28,8 @@ result_t icons_create(wuss_t *wuss, { wuss_task_t delegate; box_t box; - wuss_icon_spec_t spec; + wuss_icon_spec_t specs[4]; + wuss_icon_t *made[4]; result_t rc; task->font = font; @@ -55,51 +56,45 @@ result_t icons_create(wuss_t *wuss, if (rc != result_OK) return rc; - memset(&spec, 0, sizeof(spec)); + memset(specs, 0, sizeof(specs)); /* icons sit past the ruler gutter (see ICONS_GUTTER in icons_redraw) so the * axis labels have the top/left strip to themselves */ - /* a heading label */ - spec.bbox = (box_t) BOX_POS_SIZE(28, 28, 180, 14); - spec.type = wuss_ICON_TYPE_LABEL; - spec.text = "Work-area icons:"; - spec.fg = palette_PICO8_DARK_BLUE; - spec.bg = wuss_NO_BACKGROUND; - spec.flags = wuss_ICON_FLAGS_NONE; - rc = wuss_icon_create(task->window, &spec, NULL); + /* [0] a heading label */ + specs[0].bbox = (box_t) BOX_POS_SIZE(28, 28, 180, 14); + specs[0].type = wuss_ICON_TYPE_LABEL; + specs[0].text = "Work-area icons:"; + specs[0].fg = palette_PICO8_DARK_BLUE; + specs[0].bg = wuss_NO_BACKGROUND; + + /* [1] the button that bumps the counter */ + specs[1].bbox = (box_t) BOX_POS_SIZE(28, 50, 80, 22); + specs[1].type = wuss_ICON_TYPE_BUTTON; + specs[1].text = "Press me"; + specs[1].fg = palette_PICO8_BLACK; + specs[1].bg = palette_PICO8_LIGHT_GREY; + + /* [2] the counter label beside it */ + specs[2].bbox = (box_t) BOX_POS_SIZE(120, 50, 120, 22); + specs[2].type = wuss_ICON_TYPE_LABEL; + specs[2].text = "0"; + specs[2].fg = palette_PICO8_DARK_BLUE; + specs[2].bg = wuss_NO_BACKGROUND; + + /* [3] a button far down the document, to prove icons scroll and stay clickable */ + specs[3].bbox = (box_t) BOX_POS_SIZE(28, 460, 90, 52); + specs[3].type = wuss_ICON_TYPE_BUTTON; + specs[3].text = "Scrolled"; + specs[3].fg = palette_PICO8_BLACK; + specs[3].bg = palette_PICO8_LIGHT_GREY; + + rc = wuss_icon_create_array(task->window, specs, 4, made); if (rc != result_OK) goto failure; - /* the button that bumps the counter */ - spec.bbox = (box_t) BOX_POS_SIZE(28, 50, 80, 22); - spec.type = wuss_ICON_TYPE_BUTTON; - spec.text = "Press me"; - spec.fg = palette_PICO8_BLACK; - spec.bg = palette_PICO8_LIGHT_GREY; - rc = wuss_icon_create(task->window, &spec, &task->button); - if (rc != result_OK) - goto failure; - - /* the counter label beside it */ - spec.bbox = (box_t) BOX_POS_SIZE(120, 50, 120, 22); - spec.type = wuss_ICON_TYPE_LABEL; - spec.text = "0"; - spec.fg = palette_PICO8_DARK_BLUE; - spec.bg = wuss_NO_BACKGROUND; - rc = wuss_icon_create(task->window, &spec, &task->counter); - if (rc != result_OK) - goto failure; - - /* a button far down the document, to prove icons scroll and stay clickable */ - spec.bbox = (box_t) BOX_POS_SIZE(28, 460, 90, 52); - spec.type = wuss_ICON_TYPE_BUTTON; - spec.text = "Scrolled"; - spec.fg = palette_PICO8_BLACK; - spec.bg = palette_PICO8_LIGHT_GREY; - rc = wuss_icon_create(task->window, &spec, NULL); - if (rc != result_OK) - goto failure; + task->button = made[1]; + task->counter = made[2]; return result_OK; From 85a004d525d8bce8cab806447fae1d3c2943d136 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sat, 29 Aug 2026 23:32:31 +0100 Subject: [PATCH 15/44] test(wuss): pad the image task with a border and min-doc floor Adds a BORDER margin around the bitmap, a min-doc resize floor, and a pink background so the border and transparent pixels are visible. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/test/tasks/image.c | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/libraries/wuss/test/tasks/image.c b/libraries/wuss/test/tasks/image.c index 22d5d801..a7a07b9a 100644 --- a/libraries/wuss/test/tasks/image.c +++ b/libraries/wuss/test/tasks/image.c @@ -15,6 +15,8 @@ #include "image.h" +#define BORDER 16 + result_t image_create(wuss_t *wuss, const colour_t *palette, const char *resources, @@ -25,6 +27,7 @@ result_t image_create(wuss_t *wuss, wuss_task_t delegate; box_t box; result_t rc; + size2d_t sz; leafname = path_join_leafname("jessica", "png"); filename = path_join_filename(resources, 3, "resources", "images", leafname); @@ -33,17 +36,21 @@ result_t image_create(wuss_t *wuss, return rc; delegate = wuss_task_start(image_handle, task); /* shows through the image's transparent pixels */ + + sz.w = task->bitmap.size.w + BORDER * 2; + sz.h = task->bitmap.size.h + BORDER * 2; + /* shorter than the bitmap so there's something to scroll through */ - box = (box_t) BOX_POS_SIZE(370, 10, task->bitmap.size.w, task->bitmap.size.h * 2 / 3); + box = (box_t) BOX_POS_SIZE(370, 10, sz.w, sz.h * 2 / 3); return wuss_window_create(wuss, &box, "Image", wuss_WINDOW_NONE, - palette_PICO8_BLACK, + palette_PICO8_PINK, &delegate, - SIZE2D(task->bitmap.size.w, task->bitmap.size.h), - SIZE2D(0, 0), + sz, + SIZE2D(32, 32), &task->window); } @@ -67,7 +74,7 @@ static result_t image_redraw(const wuss_event_t *event, void *task_data) sx = event->data.redraw.scroll.x; sy = event->data.redraw.scroll.y; - screen_draw_bitmap(scr, bounds->x0 - sx, bounds->y0 - sy, &ic->bitmap); + screen_draw_bitmap(scr, bounds->x0 - sx + BORDER, bounds->y0 - sy + BORDER, &ic->bitmap); return result_OK; } From ac5e5cc913a611bf33959c327c56f48e382438ef Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sat, 29 Aug 2026 23:35:23 +0100 Subject: [PATCH 16/44] refactor(wuss): pass the image path into image_create image_create took the repo root and built the PNG path itself; it now takes the full filename, with path assembly moved to the spawn_image caller in wuss-test.c. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/test/tasks/image.c | 9 ++------- libraries/wuss/test/tasks/image.h | 5 ++--- libraries/wuss/test/wuss-test.c | 11 ++++++++++- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/libraries/wuss/test/tasks/image.c b/libraries/wuss/test/tasks/image.c index a7a07b9a..7a6501e4 100644 --- a/libraries/wuss/test/tasks/image.c +++ b/libraries/wuss/test/tasks/image.c @@ -11,7 +11,6 @@ #include "base/utils.h" #include "framebuf/palettes.h" #include "geom/box.h" -#include "io/path.h" #include "image.h" @@ -19,19 +18,15 @@ result_t image_create(wuss_t *wuss, const colour_t *palette, - const char *resources, + const char *path, image_task_t *task) { - const char *leafname; - const char *filename; wuss_task_t delegate; box_t box; result_t rc; size2d_t sz; - leafname = path_join_leafname("jessica", "png"); - filename = path_join_filename(resources, 3, "resources", "images", leafname); - rc = bitmap_load_png(&task->bitmap, filename); + rc = bitmap_load_png(&task->bitmap, path); if (rc != result_OK) return rc; diff --git a/libraries/wuss/test/tasks/image.h b/libraries/wuss/test/tasks/image.h index c17aa0f5..87e1c53b 100644 --- a/libraries/wuss/test/tasks/image.h +++ b/libraries/wuss/test/tasks/image.h @@ -20,11 +20,10 @@ image_task_t; wuss_event_fn_t image_handle; -/* load the image and create its window against the given wuss instance; - * resources is the DPTLib repo root, for locating the bundled PNG */ +/* load the PNG at path and create its window against the given wuss instance */ result_t image_create(wuss_t *wuss, const colour_t *palette, - const char *resources, + const char *path, image_task_t *task); /* destroy the window and free the bitmap loaded by image_create */ diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 05f44151..682b3444 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -75,7 +75,16 @@ static result_t spawn_text(void) { return text_create(g_wuss, g_palette, static result_t spawn_blank(void) { return blank_create(g_wuss, g_npalette, &g_blank_task); } static result_t spawn_chars(void) { return chars_create(g_wuss, g_palette, &g_chars_task); } static result_t spawn_palette(void) { return palette_create(g_wuss, g_palette, g_npalette, &g_palette_task); } -static result_t spawn_image(void) { return image_create(g_wuss, g_palette, g_resources, &g_image_task); } +static result_t spawn_image(void) +{ + const char *leafname; + const char *filename; + + leafname = path_join_leafname("jessica", "png"); + filename = path_join_filename(g_resources, 3, "resources", "images", leafname); + + return image_create(g_wuss, g_palette, filename, &g_image_task); +} static result_t spawn_checker(void) { return checker_create(g_wuss, g_palette, &g_checker_task); } static result_t spawn_curve(void) { return curve_create(g_wuss, g_palette, &g_curve_task); } static result_t spawn_sofa(void) { return sofa_create(g_wuss, g_palette, &g_sofa_task); } From 0b3bff90df51932761e205211661fc6564c47f03 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 30 Aug 2026 00:04:46 +0100 Subject: [PATCH 17/44] feat(wuss): add wuss_window_create_placed for wuss-chosen positions New entry point taking a content size instead of a box: wuss packs the window (furniture included) into the first free screen region via a packer_t it owns, top-left order, tracking occupied area across calls so successive auto-placed windows tile. Cascades from the previous placement when no region fits. The slot is released back to the pool on close, and on the first wuss_window_move / wuss_window_resize (a titlebar drag counts as a move), after which wuss stops tracking the window's position. Adds packer_release() to geom/packer as the inverse of packer_place_*; released areas are not coalesced, which is sufficient for whole-window placement. Co-Authored-By: Claude Sonnet 5 --- CMakeLists.txt | 1 + include/geom/packer.h | 17 +++ include/wuss/window.h | 39 ++++++ libraries/geom/packer/packer.c | 15 +++ libraries/geom/packer/test/packer-test.c | 73 +++++++++++ libraries/wuss/create.c | 4 + libraries/wuss/destroy.c | 1 + libraries/wuss/impl.h | 22 ++++ libraries/wuss/test/wuss-test.c | 65 ++++++++++ libraries/wuss/window/close.c | 2 + libraries/wuss/window/create-placed.c | 149 +++++++++++++++++++++++ libraries/wuss/window/create.c | 2 + libraries/wuss/window/move.c | 4 + libraries/wuss/window/resize.c | 3 + 14 files changed, 397 insertions(+) create mode 100644 libraries/wuss/window/create-placed.c diff --git a/CMakeLists.txt b/CMakeLists.txt index f71da178..84556e01 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -363,6 +363,7 @@ set(WUSS_SOURCES libraries/wuss/task/stop.c libraries/wuss/window/at.c libraries/wuss/window/create.c + libraries/wuss/window/create-placed.c libraries/wuss/window/close.c libraries/wuss/window/get-content-bounds.c libraries/wuss/window/get-scroll.c diff --git a/include/geom/packer.h b/include/geom/packer.h index ea966075..16fb3d53 100644 --- a/include/geom/packer.h +++ b/include/geom/packer.h @@ -77,6 +77,23 @@ int packer_next_width(T *packer, packer_loc_t loc); result_t packer_place_at(T *packer, const box_t *area); +/** + * Returns a previously placed area to the free pool: the inverse of + * packer_place_at / packer_place_by. + * + * Adjacent released areas are not coalesced, so a single placement that would + * span two separately-released areas will not fit until the gap between them is + * also freed; placing a box that fits wholly within one free area is + * unaffected. packer_get_consumed_area is not narrowed by a release. + * + * \param[in] packer Packer to release into. + * \param[in] area Area to release. Clipped to the packer's margins. Copied. + * \return \ref result_OK, or \ref result_PACKER_EMPTY if 'area' lies entirely + * outside the margins. + */ +result_t packer_release(T *packer, + const box_t *area); + /** * Places a box of dimensions (w,h) in the next free area determined by location * 'loc'. diff --git a/include/wuss/window.h b/include/wuss/window.h index fa53be11..efdc7464 100644 --- a/include/wuss/window.h +++ b/include/wuss/window.h @@ -76,6 +76,45 @@ result_t wuss_window_create(wuss_t *wuss, size2d_t min_doc, wuss_window_t **window); +/** + * Create a window, letting Wuss choose its position. + * + * As wuss_window_create, but instead of a content box you pass just the content + * size; Wuss places the window (furniture included) in the first free screen + * region, packed towards the top-left, tracking occupied area across calls so + * successive auto-placed windows tile rather than stack. When no region is + * large enough the window is cascaded from the previous placement, stepping by + * a titlebar height and wrapping at the screen edge. + * + * The chosen slot is returned to the pool when the window is closed, or when it + * is first moved or resized via wuss_window_move / wuss_window_resize (after + * which Wuss no longer tracks its position). A window dragged by its titlebar + * counts as moved. + * + * \param[in] wuss Window manager to create the window on. + * \param[in] size Requested content-area size. Width and height must both + * be positive. + * \param[in] title Titlebar label, as wuss_window_create. + * \param[in] flags Appearance flags, as wuss_window_create. + * \param[in] bg Content background, as wuss_window_create. + * \param[in] task Content delegate, as wuss_window_create. + * \param[in] doc Virtual document extent, as wuss_window_create. + * \param[in] min_doc Minimum content size, as wuss_window_create. + * \param[out] window Newly created window. Becomes the topmost window. + * \return \ref result_OK on success, \ref result_WUSS_TOO_SMALL if size's width + * or height is not positive, \ref result_OOM if the layout tracker + * could not be created, or another result code from wuss_window_create. + */ +result_t wuss_window_create_placed(wuss_t *wuss, + size2d_t size, + const char *title, + wuss_window_flags_t flags, + wuss_colour_t bg, + const wuss_task_t *task, + size2d_t doc, + size2d_t min_doc, + wuss_window_t **window); + /** * Destroy a window. * diff --git a/libraries/geom/packer/packer.c b/libraries/geom/packer/packer.c index 59c19fe7..fc47e182 100644 --- a/libraries/geom/packer/packer.c +++ b/libraries/geom/packer/packer.c @@ -408,6 +408,21 @@ result_t packer_place_at(packer_t *packer, const box_t *area) return remove_area(packer, &b); } +result_t packer_release(packer_t *packer, const box_t *area) +{ + box_t b; + + (void) box_intersection(&packer->margins, area, &b); + + if (box_is_empty(&b)) + return result_PACKER_EMPTY; + + /* ponytail: no coalescing with neighbouring free areas -- a released + * fragment is usable on its own, which is all whole-box placement needs; + * tile-style reuse spanning two released areas would need a merge pass */ + return add_area(packer, &b); +} + result_t packer_place_by(packer_t *packer, packer_loc_t loc, int w, diff --git a/libraries/geom/packer/test/packer-test.c b/libraries/geom/packer/test/packer-test.c index ba744b0b..d93ba541 100644 --- a/libraries/geom/packer/test/packer-test.c +++ b/libraries/geom/packer/test/packer-test.c @@ -399,6 +399,75 @@ static int test2(void) return 1; } +/* packer_release: a slot handed back becomes available again. */ +static int test3(void) +{ + static const box_t pagedims = { 0, 0, 100, 100 }; + + packer_t *packer; + const box_t *a, *b, *c; + box_t freed; + result_t err; + + printf("test3: packer_release\n"); + + packer = packer_create(&pagedims); + if (packer == NULL) + return 1; + + /* fill the page with four 50x50 quads, top-left order */ + err = packer_place_by(packer, packer_LOC_TOP_LEFT, 50, 50, &a); + err |= packer_place_by(packer, packer_LOC_TOP_LEFT, 50, 50, &b); + err |= packer_place_by(packer, packer_LOC_TOP_LEFT, 50, 50, &c); + err |= packer_place_by(packer, packer_LOC_TOP_LEFT, 50, 50, NULL); + if (err) + goto failure; + + /* page is now full: a fifth 50x50 must not fit */ + if (packer_place_by(packer, packer_LOC_TOP_LEFT, 50, 50, NULL) + != result_PACKER_DIDNT_FIT) + { + printf("test3: expected DIDNT_FIT while full\n"); + goto failure; + } + + /* release the top-left quad, then a 50x50 must fit again, in that slot */ + freed.x0 = 0; freed.y0 = 0; freed.x1 = 50; freed.y1 = 50; + err = packer_release(packer, &freed); + if (err) + goto failure; + + err = packer_place_by(packer, packer_LOC_TOP_LEFT, 50, 50, &a); + if (err) + { + printf("test3: placement after release failed (%d)\n", err); + goto failure; + } + if (a->x0 != 0 || a->y0 != 0 || a->x1 != 50 || a->y1 != 50) + { + printf("test3: reused slot <%d,%d-%d,%d>, wanted <0,0-50,50>\n", + a->x0, a->y0, a->x1, a->y1); + goto failure; + } + + /* a box wholly outside the margins is rejected */ + freed.x0 = 200; freed.y0 = 200; freed.x1 = 250; freed.y1 = 250; + if (packer_release(packer, &freed) != result_PACKER_EMPTY) + { + printf("test3: out-of-bounds release not rejected\n"); + goto failure; + } + + packer_destroy(packer); + return 0; + + +failure: + + packer_destroy(packer); + return 1; +} + result_t packer_test(const char *resources) { result_t err; @@ -413,6 +482,10 @@ result_t packer_test(const char *resources) if (err) goto failure; + err = test3(); + if (err) + goto failure; + return result_TEST_PASSED; diff --git a/libraries/wuss/create.c b/libraries/wuss/create.c index 7e00e5fe..6b56de61 100644 --- a/libraries/wuss/create.c +++ b/libraries/wuss/create.c @@ -145,6 +145,10 @@ result_t wuss_create(screen_t *scr, w->ndirty = 0; + w->layout = NULL; + w->cascade.x = 0; + w->cascade.y = 0; + list_init(&w->z_order); *wuss = w; diff --git a/libraries/wuss/destroy.c b/libraries/wuss/destroy.c index 256b8791..ede75c1b 100644 --- a/libraries/wuss/destroy.c +++ b/libraries/wuss/destroy.c @@ -26,6 +26,7 @@ void wuss_destroy(wuss_t *doomed) e = next; } + packer_destroy(doomed->layout); free(doomed->palette); free(doomed); } diff --git a/libraries/wuss/impl.h b/libraries/wuss/impl.h index 9a83a09d..73692802 100644 --- a/libraries/wuss/impl.h +++ b/libraries/wuss/impl.h @@ -6,6 +6,8 @@ #include "base/utils.h" #include "datastruct/list.h" #include "geom/box.h" +#include "geom/packer.h" +#include "geom/point.h" #include "geom/size.h" #include "framebuf/screen.h" #include "framebuf/bmfont.h" @@ -56,6 +58,11 @@ struct wuss struct wuss__furniture furniture; box_t dirty[WUSS_MAX_DIRTY]; /* accumulated by wuss_invalidate; reset by a redraw */ int ndirty; + packer_t *layout; /* owned; occupied screen area for + * wuss_window_create_placed, lazily + * created on first auto-placement */ + point_t cascade; /* next cascade offset, used once the + * layout packer has no room left */ }; struct wuss_window @@ -73,6 +80,9 @@ struct wuss_window size2d_t min_doc; /* resize floor, set at creation; see * wuss__min_content */ wuss_window_state_t state; /* see wuss_window_state_t */ + box_t packed; /* footprint handed to wuss->layout by + * wuss_window_create_placed, or empty if + * not auto-placed or already released */ box_t pre_toggle; /* visible bounds to restore on the next toggle */ char title[WUSS_TITLE_MAX + 1]; wuss_icon_t **icons; /* owned; array of owned icon pointers */ @@ -125,6 +135,18 @@ static inline int wuss__size_ok(int width, int height) return width > 0 && height > 0; } +/* Give an auto-placed window's slot back to the layout packer and stop + * tracking it, so a later close/move/resize doesn't release it twice. A + * no-op for windows that were never auto-placed (empty "packed"). */ +static inline void wuss__release_packed(wuss_window_t *window) +{ + if (box_is_empty(&window->packed)) + return; + + (void) packer_release(window->wuss->layout, &window->packed); + box_reset(&window->packed); +} + /* The floor a resize-drag or toggle-size will shrink a window's content to: * the client's min_doc where it set one, but never below WUSS_MIN_CONTENT (a * window must stay big enough to grab) nor above the window's own doc extent diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 682b3444..c0de3242 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -2927,6 +2927,71 @@ result_t wuss_test(const char *resources) wuss_window_close(win_r); } + printf("test: wuss_window_create_placed tiles windows and reclaims a closed slot\n"); + + { + /* Windows created without a position are packed towards the top-left and + * must not overlap; closing one frees its slot for the next create. */ + test_task_t tc_p[4]; + wuss_task_t delegate_p; + wuss_window_t *win_p[4]; + box_t vis[4], probe; + int k, m; + + memset(tc_p, 0, sizeof(tc_p)); + delegate_p.handle = test_handle; + delegate_p.task_data = &tc_p[0]; + + for (k = 0; k < 4; k++) + { + rc = wuss_window_create_placed(wuss, + SIZE2D(40, 30), + "P", + wuss_WINDOW_NONE, + wuss_NO_BACKGROUND, + &delegate_p, + SIZE2D(40, 30), + SIZE2D(0, 0), + &win_p[k]); + if (rc != result_OK) + goto Failure; + wuss_window_get_visible_bounds(win_p[k], &vis[k]); + } + + for (k = 0; k < 4; k++) + for (m = k + 1; m < 4; m++) + if (box_intersects(&vis[k], &vis[m])) + goto Failure; /* auto-placed windows overlapped */ + + /* free the second window's slot, then a new placed window should land + * back in it rather than being pushed past the others */ + probe = vis[1]; + wuss_window_close(win_p[1]); + + rc = wuss_window_create_placed(wuss, + SIZE2D(40, 30), + "P", + wuss_WINDOW_NONE, + wuss_NO_BACKGROUND, + &delegate_p, + SIZE2D(40, 30), + SIZE2D(0, 0), + &win_p[1]); + if (rc != result_OK) + goto Failure; + wuss_window_get_visible_bounds(win_p[1], &vis[1]); + if (vis[1].x0 != probe.x0 || vis[1].y0 != probe.y0 || + vis[1].x1 != probe.x1 || vis[1].y1 != probe.y1) + goto Failure; /* freed slot not reused */ + + /* a manual move releases the slot: closing afterwards must not + * double-release (would corrupt the packer's free list) */ + wuss_window_move(win_p[0], POINT(200, 200)); + + for (k = 0; k < 4; k++) + wuss_window_close(win_p[k]); + } + printf("test: wuss_task_stop sends wuss_EVENT_QUIT to each window's task\n"); tc_a.stop_count = 0; diff --git a/libraries/wuss/window/close.c b/libraries/wuss/window/close.c index 411b9e00..058743fa 100644 --- a/libraries/wuss/window/close.c +++ b/libraries/wuss/window/close.c @@ -19,6 +19,8 @@ void wuss_window_close(wuss_window_t *doomed) if (wuss->furniture.dragging == doomed) wuss->furniture.dragging = NULL; + wuss__release_packed(doomed); + wuss__invalidate_clipped(doomed, &doomed->visible); list_remove(&wuss->z_order, &doomed->link); diff --git a/libraries/wuss/window/create-placed.c b/libraries/wuss/window/create-placed.c new file mode 100644 index 00000000..0550d2ba --- /dev/null +++ b/libraries/wuss/window/create-placed.c @@ -0,0 +1,149 @@ +/* create-placed.c -- wuss - window creation with wuss-chosen position */ + +#include + +#ifdef FORTIFY +#include "fortify/fortify.h" +#endif + +#include "geom/box.h" +#include "geom/packer.h" + +#include "../impl.h" + +/* Footprint padding around a content area of the given flags: the outline on + * every edge, the titlebar on top, and the scrollbar/resize carve on the + * right and bottom. Matches wuss_window_create's own visible-box maths so an + * auto-placed slot ends up exactly the size the window will occupy. */ +static void footprint_pad(const wuss_t *wuss, + wuss_window_flags_t flags, + int *left, + int *top, + int *right, + int *bottom) +{ + int outline_px, titlebar_height; + point_t carve; + + outline_px = wuss__outline_px_for(flags); + titlebar_height = wuss__titlebar_height_for(wuss, flags); + wuss__furniture_carve_for(flags, wuss__button_size_for(wuss, flags), &carve); + + *left = outline_px; + *top = outline_px + titlebar_height; + *right = outline_px + carve.x; + *bottom = outline_px + carve.y; +} + +/* Pick the next cascade position for a window of the given footprint size, + * once the layout packer has no room. Steps down/right by a titlebar each + * call, wrapping back to the top-left when the step would push the footprint + * off the screen. */ +static void next_cascade(wuss_t *wuss, int fw, int fh, point_t *pos) +{ + int scr_w, scr_h, step; + + scr_w = wuss->scr->size.w; + scr_h = wuss->scr->size.h; + step = wuss->titlebar_height; + if (step <= 0) + step = WUSS_DEFAULT_TITLEBAR_HEIGHT; + + if (wuss->cascade.x + fw > scr_w || wuss->cascade.y + fh > scr_h) + { + wuss->cascade.x = 0; + wuss->cascade.y = 0; + } + + *pos = wuss->cascade; + + wuss->cascade.x += step; + wuss->cascade.y += step; +} + +result_t wuss_window_create_placed(wuss_t *wuss, + size2d_t size, + const char *title, + wuss_window_flags_t flags, + wuss_colour_t bg, + const wuss_task_t *task, + size2d_t doc, + size2d_t min_doc, + wuss_window_t **window) +{ + result_t rc; + int left, top, right, bottom; + int fw, fh; + box_t screen, content; + point_t origin; + const box_t *slot; + int tracked; + + assert(wuss != NULL); + assert(window != NULL); + + if (!wuss__size_ok(size.w, size.h)) + return result_WUSS_TOO_SMALL; + + if (wuss->layout == NULL) + { + screen.x0 = 0; + screen.y0 = 0; + screen.x1 = wuss->scr->size.w; + screen.y1 = wuss->scr->size.h; + + wuss->layout = packer_create(&screen); + if (wuss->layout == NULL) + return result_OOM; + } + + footprint_pad(wuss, flags, &left, &top, &right, &bottom); + fw = left + size.w + right; + fh = top + size.h + bottom; + + /* packer's Y axis is reversed. bottom left here gives top left packing. */ + rc = packer_place_by(wuss->layout, packer_LOC_BOTTOM_LEFT, fw, fh, &slot); + if (rc == result_OK) + { + origin.x = slot->x0; + origin.y = slot->y0; + tracked = 1; + } + else if (rc == result_PACKER_DIDNT_FIT) + { + next_cascade(wuss, fw, fh, &origin); + tracked = 0; + } + else + { + return rc; + } + + /* content box = footprint origin plus the top/left furniture padding */ + content.x0 = origin.x + left; + content.y0 = origin.y + top; + content.x1 = content.x0 + size.w; + content.y1 = content.y0 + size.h; + + rc = wuss_window_create(wuss, &content, title, flags, bg, task, + doc, min_doc, window); + if (rc != result_OK) + { + if (tracked) + { + box_t placed; + + placed.x0 = origin.x; + placed.y0 = origin.y; + placed.x1 = origin.x + fw; + placed.y1 = origin.y + fh; + (void) packer_release(wuss->layout, &placed); + } + return rc; + } + + if (tracked) + (*window)->packed = *slot; + + return result_OK; +} diff --git a/libraries/wuss/window/create.c b/libraries/wuss/window/create.c index c29e87f2..7beb1ed5 100644 --- a/libraries/wuss/window/create.c +++ b/libraries/wuss/window/create.c @@ -83,6 +83,8 @@ result_t wuss_window_create(wuss_t *wuss, win->nicons = 0; win->cap_icons = 0; + box_reset(&win->packed); /* wuss_window_create_placed fills this in after */ + if (task != NULL) win->task = *task; else diff --git a/libraries/wuss/window/move.c b/libraries/wuss/window/move.c index 19c05a54..9acab623 100644 --- a/libraries/wuss/window/move.c +++ b/libraries/wuss/window/move.c @@ -78,6 +78,10 @@ void wuss_window_move(wuss_window_t *window, point_t p) box_t before, dirty, copied; int blit_failed; + /* a manual move desyncs the window from its layout-packer slot; hand the + * slot back and stop tracking this window's position */ + wuss__release_packed(window); + width = window->visible.x1 - window->visible.x0; height = window->visible.y1 - window->visible.y0; outline_px = wuss__outline_px(window); diff --git a/libraries/wuss/window/resize.c b/libraries/wuss/window/resize.c index 4e0fd995..c43057a1 100644 --- a/libraries/wuss/window/resize.c +++ b/libraries/wuss/window/resize.c @@ -39,6 +39,9 @@ result_t wuss_window_resize(wuss_window_t *window, size2d_t size) if (!wuss__size_ok(size.w, size.h)) return result_WUSS_TOO_SMALL; + /* a manual resize desyncs the window from its layout-packer slot */ + wuss__release_packed(window); + outline_px = wuss__outline_px(window); titlebar_height = wuss__titlebar_height(window); before = window->visible; From 3232d2d92b8eb50db77bcf9f9f628cc2f56fc5c9 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 30 Aug 2026 00:04:51 +0100 Subject: [PATCH 18/44] refactor(wuss): let test tasks self-place via wuss_window_create_placed Switch every test task from a hardcoded BOX_POS_SIZE origin to wuss_window_create_placed, passing just the content size. Windows now tile from the top-left instead of landing at fixed coordinates. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/test/tasks/ball.c | 22 ++++++------- libraries/wuss/test/tasks/blank.c | 22 ++++++------- libraries/wuss/test/tasks/chars.c | 30 +++++++++--------- libraries/wuss/test/tasks/checker.c | 42 +++++++++++-------------- libraries/wuss/test/tasks/curve.c | 22 ++++++------- libraries/wuss/test/tasks/gradient.c | 22 ++++++------- libraries/wuss/test/tasks/icons.c | 22 ++++++------- libraries/wuss/test/tasks/image.c | 25 +++++++-------- libraries/wuss/test/tasks/launcher.c | 24 +++++++------- libraries/wuss/test/tasks/palette.c | 22 ++++++------- libraries/wuss/test/tasks/porter-duff.c | 22 ++++++------- libraries/wuss/test/tasks/sofa.c | 22 ++++++------- libraries/wuss/test/tasks/text.c | 30 +++++++++--------- 13 files changed, 152 insertions(+), 175 deletions(-) diff --git a/libraries/wuss/test/tasks/ball.c b/libraries/wuss/test/tasks/ball.c index 8d5fa59b..388f37b4 100644 --- a/libraries/wuss/test/tasks/ball.c +++ b/libraries/wuss/test/tasks/ball.c @@ -15,7 +15,6 @@ result_t ball_create(wuss_t *wuss, const colour_t *palette, ball_task_t *task) { wuss_task_t delegate; - box_t box; task->bg = palette[palette_PICO8_RED]; task->ball = palette[palette_PICO8_WHITE]; @@ -28,17 +27,16 @@ result_t ball_create(wuss_t *wuss, const colour_t *palette, ball_task_t *task) task->balls[0].radius = 8; delegate = wuss_task_start(ball_handle, task); /* ball_redraw paints its own background every frame */ - box = (box_t) BOX_POS_SIZE(20, 20, 200, 160); - - return wuss_window_create(wuss, - &box, - "Bouncing Ball", - wuss_WINDOW_NONE, - wuss_NO_BACKGROUND, - &delegate, - box_size(&box), - SIZE2D(0, 0), - &task->window); + + return wuss_window_create_placed(wuss, + SIZE2D(200, 160), + "Bouncing Ball", + wuss_WINDOW_NONE, + wuss_NO_BACKGROUND, + &delegate, + SIZE2D(200, 160), + SIZE2D(0, 0), + &task->window); } void ball_destroy(ball_task_t *task) diff --git a/libraries/wuss/test/tasks/blank.c b/libraries/wuss/test/tasks/blank.c index 344a59d3..7c95c9c8 100644 --- a/libraries/wuss/test/tasks/blank.c +++ b/libraries/wuss/test/tasks/blank.c @@ -18,24 +18,22 @@ result_t blank_create(wuss_t *wuss, int npalette, blank_task_t *task) { wuss_task_t delegate; - box_t box; task->npalette = npalette; task->index = palette_PICO8_GREEN; task->frame_count = 0; delegate = wuss_task_start(blank_handle, task); /* wuss fills the content area itself */ - box = (box_t) BOX_POS_SIZE(260, 60, 200, 160); - - return wuss_window_create(wuss, - &box, - NULL, - wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE, - palette_PICO8_GREEN, - &delegate, - box_size(&box), - SIZE2D(0, 0), - &task->window); + + return wuss_window_create_placed(wuss, + SIZE2D(200, 160), + NULL, + wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE, + palette_PICO8_GREEN, + &delegate, + SIZE2D(200, 160), + SIZE2D(0, 0), + &task->window); } void blank_destroy(blank_task_t *task) diff --git a/libraries/wuss/test/tasks/chars.c b/libraries/wuss/test/tasks/chars.c index 6f2d6603..572461ca 100644 --- a/libraries/wuss/test/tasks/chars.c +++ b/libraries/wuss/test/tasks/chars.c @@ -24,7 +24,7 @@ result_t chars_create(wuss_t *wuss, chars_task_t *task) { wuss_task_t delegate; - box_t box; + size2d_t sz; bmfont_t *font; int font_width, font_height, cell_w, cell_h; @@ -44,20 +44,20 @@ result_t chars_create(wuss_t *wuss, task->bg = palette[palette_PICO8_WHITE]; delegate = wuss_task_start(chars_handle, task); /* chars_redraw paints every cell itself */ - box = (box_t) BOX_POS_SIZE(500, 100, cell_w * CHARS_COLS, cell_h * CHARS_ROWS); - - return wuss_window_create(wuss, - &box, - "Chars", - wuss_WINDOW_NO_RESIZE | - wuss_WINDOW_NO_TOGGLE_SIZE | - wuss_WINDOW_NO_VSCROLL | - wuss_WINDOW_NO_HSCROLL, - wuss_NO_BACKGROUND, - &delegate, - box_size(&box), - SIZE2D(0, 0), - &task->window); + sz = SIZE2D(cell_w * CHARS_COLS, cell_h * CHARS_ROWS); + + return wuss_window_create_placed(wuss, + sz, + "Chars", + wuss_WINDOW_NO_RESIZE | + wuss_WINDOW_NO_TOGGLE_SIZE | + wuss_WINDOW_NO_VSCROLL | + wuss_WINDOW_NO_HSCROLL, + wuss_NO_BACKGROUND, + &delegate, + sz, + SIZE2D(0, 0), + &task->window); } void chars_destroy(chars_task_t *task) diff --git a/libraries/wuss/test/tasks/checker.c b/libraries/wuss/test/tasks/checker.c index d61baa56..dbd5e4e8 100644 --- a/libraries/wuss/test/tasks/checker.c +++ b/libraries/wuss/test/tasks/checker.c @@ -21,7 +21,6 @@ result_t checker_create(wuss_t *wuss, checker_task_t *task) { wuss_task_t delegate; - box_t box; result_t rc; task->black = palette[palette_PICO8_BLACK]; @@ -32,31 +31,28 @@ result_t checker_create(wuss_t *wuss, task->band2 = CHECKER_BAND_DEFAULT; delegate = wuss_task_start(checker_handle, task); /* checker_redraw paints every pixel itself */ - box = (box_t) BOX_POS_SIZE(440, 300, 160, 160); - - rc = wuss_window_create(wuss, - &box, - "Checker 1", - wuss_WINDOW_NONE, - wuss_NO_BACKGROUND, - &delegate, - box_size(&box), - SIZE2D(0, 0), - &task->window); + + rc = wuss_window_create_placed(wuss, + SIZE2D(160, 160), + "Checker 1", + wuss_WINDOW_NONE, + wuss_NO_BACKGROUND, + &delegate, + SIZE2D(160, 160), + SIZE2D(0, 0), + &task->window); if (rc != result_OK) return rc; - box = (box_t) BOX_POS_SIZE(440, 10, 160, 160); - - rc = wuss_window_create(wuss, - &box, - "Checker 2", - wuss_WINDOW_NONE, - wuss_NO_BACKGROUND, - &delegate, - box_size(&box), - SIZE2D(0, 0), - &task->window2); + rc = wuss_window_create_placed(wuss, + SIZE2D(160, 160), + "Checker 2", + wuss_WINDOW_NONE, + wuss_NO_BACKGROUND, + &delegate, + SIZE2D(160, 160), + SIZE2D(0, 0), + &task->window2); if (rc != result_OK) { wuss_window_close(task->window); diff --git a/libraries/wuss/test/tasks/curve.c b/libraries/wuss/test/tasks/curve.c index 24572802..2b9f44ef 100644 --- a/libraries/wuss/test/tasks/curve.c +++ b/libraries/wuss/test/tasks/curve.c @@ -26,7 +26,6 @@ result_t curve_create(wuss_t *wuss, curve_task_t *task) { wuss_task_t delegate; - box_t box; task->bg = palette[palette_PICO8_WHITE]; task->line = palette[palette_PICO8_BLACK]; @@ -40,17 +39,16 @@ result_t curve_create(wuss_t *wuss, task->points[3] = POINT(210, 140); delegate = wuss_task_start(curve_handle, task); /* curve_redraw paints its own background */ - box = (box_t) BOX_POS_SIZE(20, 260, 220, 160); - - return wuss_window_create(wuss, - &box, - "Curve", - wuss_WINDOW_NONE, - wuss_NO_BACKGROUND, - &delegate, - box_size(&box), - SIZE2D(0, 0), - &task->window); + + return wuss_window_create_placed(wuss, + SIZE2D(220, 160), + "Curve", + wuss_WINDOW_NONE, + wuss_NO_BACKGROUND, + &delegate, + SIZE2D(220, 160), + SIZE2D(0, 0), + &task->window); } void curve_destroy(curve_task_t *task) diff --git a/libraries/wuss/test/tasks/gradient.c b/libraries/wuss/test/tasks/gradient.c index 491e8460..0e3cc849 100644 --- a/libraries/wuss/test/tasks/gradient.c +++ b/libraries/wuss/test/tasks/gradient.c @@ -34,20 +34,18 @@ static int dither(int v, int x, int y) result_t gradient_create(wuss_t *wuss, gradient_task_t *task) { wuss_task_t delegate; - box_t box; delegate = wuss_task_start(gradient_handle, task); /* gradient_redraw paints every pixel itself */ - box = (box_t) BOX_POS_SIZE(620, 300, GRADIENT_OPEN_WIDTH, GRADIENT_OPEN_HEIGHT); - - return wuss_window_create(wuss, - &box, - "Gradient", - wuss_WINDOW_NONE, - wuss_NO_BACKGROUND, - &delegate, - SIZE2D(GRADIENT_DOC_WIDTH, GRADIENT_DOC_HEIGHT), - SIZE2D(0, 0), - &task->window); + + return wuss_window_create_placed(wuss, + SIZE2D(GRADIENT_OPEN_WIDTH, GRADIENT_OPEN_HEIGHT), + "Gradient", + wuss_WINDOW_NONE, + wuss_NO_BACKGROUND, + &delegate, + SIZE2D(GRADIENT_DOC_WIDTH, GRADIENT_DOC_HEIGHT), + SIZE2D(0, 0), + &task->window); } void gradient_destroy(gradient_task_t *task) diff --git a/libraries/wuss/test/tasks/icons.c b/libraries/wuss/test/tasks/icons.c index a183a0cb..70a247c7 100644 --- a/libraries/wuss/test/tasks/icons.c +++ b/libraries/wuss/test/tasks/icons.c @@ -27,7 +27,6 @@ result_t icons_create(wuss_t *wuss, icons_task_t *task) { wuss_task_t delegate; - box_t box; wuss_icon_spec_t specs[4]; wuss_icon_t *made[4]; result_t rc; @@ -42,17 +41,16 @@ result_t icons_create(wuss_t *wuss, task->count = 0; delegate = wuss_task_start(icons_handle, task); - box = (box_t) BOX_POS_SIZE(160, 120, ICONS_DOC_W, 160); - - rc = wuss_window_create(wuss, - &box, - "Icons", - wuss_WINDOW_NONE, - palette_PICO8_LIGHT_GREY, - &delegate, - SIZE2D(ICONS_DOC_W, ICONS_DOC_H), - SIZE2D(0, 0), - &task->window); + + rc = wuss_window_create_placed(wuss, + SIZE2D(ICONS_DOC_W, 160), + "Icons", + wuss_WINDOW_NONE, + palette_PICO8_LIGHT_GREY, + &delegate, + SIZE2D(ICONS_DOC_W, ICONS_DOC_H), + SIZE2D(0, 0), + &task->window); if (rc != result_OK) return rc; diff --git a/libraries/wuss/test/tasks/image.c b/libraries/wuss/test/tasks/image.c index 7a6501e4..0c7c9ee5 100644 --- a/libraries/wuss/test/tasks/image.c +++ b/libraries/wuss/test/tasks/image.c @@ -22,7 +22,6 @@ result_t image_create(wuss_t *wuss, image_task_t *task) { wuss_task_t delegate; - box_t box; result_t rc; size2d_t sz; @@ -34,19 +33,17 @@ result_t image_create(wuss_t *wuss, sz.w = task->bitmap.size.w + BORDER * 2; sz.h = task->bitmap.size.h + BORDER * 2; - - /* shorter than the bitmap so there's something to scroll through */ - box = (box_t) BOX_POS_SIZE(370, 10, sz.w, sz.h * 2 / 3); - - return wuss_window_create(wuss, - &box, - "Image", - wuss_WINDOW_NONE, - palette_PICO8_PINK, - &delegate, - sz, - SIZE2D(32, 32), - &task->window); + + return wuss_window_create_placed(wuss, + /* shorter than the bitmap so there's something to scroll through */ + SIZE2D(sz.w, sz.h * 2 / 3), + "Image", + wuss_WINDOW_NONE, + palette_PICO8_PINK, + &delegate, + sz, + SIZE2D(32, 32), + &task->window); } void image_destroy(image_task_t *task) diff --git a/libraries/wuss/test/tasks/launcher.c b/libraries/wuss/test/tasks/launcher.c index 3e3e93aa..69831975 100644 --- a/libraries/wuss/test/tasks/launcher.c +++ b/libraries/wuss/test/tasks/launcher.c @@ -28,7 +28,7 @@ result_t launcher_create(wuss_t *wuss, launcher_task_t *task) { wuss_task_t delegate; - box_t box; + size2d_t sz; assert(nentries <= LAUNCHER_MAX_ENTRIES); @@ -41,17 +41,17 @@ result_t launcher_create(wuss_t *wuss, task->running_fg = palette[palette_PICO8_LIGHT_GREY]; delegate = wuss_task_start(launcher_handle, task); /* launcher_redraw paints its own background */ - box = (box_t) BOX_POS_SIZE(10, 10, LAUNCHER_WIDTH, LAUNCHER_PAD * 2 + nentries * LAUNCHER_ROW_HEIGHT); - - return wuss_window_create(wuss, - &box, - "Launcher", - wuss_WINDOW_NO_CLOSE, - wuss_NO_BACKGROUND, - &delegate, - box_size(&box), - SIZE2D(0, 0), - &task->window); + sz = SIZE2D(LAUNCHER_WIDTH, LAUNCHER_PAD * 2 + nentries * LAUNCHER_ROW_HEIGHT); + + return wuss_window_create_placed(wuss, + sz, + "Launcher", + wuss_WINDOW_NO_CLOSE, + wuss_NO_BACKGROUND, + &delegate, + sz, + SIZE2D(0, 0), + &task->window); } void launcher_destroy(launcher_task_t *task) diff --git a/libraries/wuss/test/tasks/palette.c b/libraries/wuss/test/tasks/palette.c index 0ba14831..f98e18bd 100644 --- a/libraries/wuss/test/tasks/palette.c +++ b/libraries/wuss/test/tasks/palette.c @@ -18,23 +18,21 @@ result_t palette_create(wuss_t *wuss, palette_task_t *task) { wuss_task_t delegate; - box_t box; task->palette = palette; task->npalette = npalette; delegate = wuss_task_start(palette_handle, task); /* backdrop for any rounding gap around the grid */ - box = (box_t) BOX_POS_SIZE(380, 260, 100, 100); - - return wuss_window_create(wuss, - &box, - "Palette", - wuss_WINDOW_NO_RESIZE_BLIT, /* swatch grid is laid out across the whole window, so a resize must redraw all of it, not just the newly (un)covered edge */ - palette_PICO8_BLACK, - &delegate, - box_size(&box), - SIZE2D(0, 0), - &task->window); + + return wuss_window_create_placed(wuss, + SIZE2D(100, 100), + "Palette", + wuss_WINDOW_NO_RESIZE_BLIT, /* swatch grid is laid out across the whole window, so a resize must redraw all of it, not just the newly (un)covered edge */ + palette_PICO8_BLACK, + &delegate, + SIZE2D(100, 100), + SIZE2D(0, 0), + &task->window); } void palette_destroy(palette_task_t *task) diff --git a/libraries/wuss/test/tasks/porter-duff.c b/libraries/wuss/test/tasks/porter-duff.c index 8ff10b92..5d586693 100644 --- a/libraries/wuss/test/tasks/porter-duff.c +++ b/libraries/wuss/test/tasks/porter-duff.c @@ -163,7 +163,6 @@ result_t porter_duff_create(wuss_t *wuss, porter_duff_task_t *task) { wuss_task_t delegate; - box_t box; result_t rc; task->font = font; @@ -192,17 +191,16 @@ result_t porter_duff_create(wuss_t *wuss, goto free_src; delegate = wuss_task_start(porter_duff_handle, task); /* porter_duff_redraw paints every pixel itself */ - box = (box_t) BOX_POS_SIZE(60, 180, PD_SIZE, PD_SIZE + PD_LABEL_HEIGHT); - - rc = wuss_window_create(wuss, - &box, - "Porter-Duff", - wuss_WINDOW_NONE, - wuss_NO_BACKGROUND, - &delegate, - box_size(&box), - SIZE2D(0, 0), - &task->window); + + rc = wuss_window_create_placed(wuss, + SIZE2D(PD_SIZE, PD_SIZE + PD_LABEL_HEIGHT), + "Porter-Duff", + wuss_WINDOW_NONE, + wuss_NO_BACKGROUND, + &delegate, + SIZE2D(PD_SIZE, PD_SIZE + PD_LABEL_HEIGHT), + SIZE2D(0, 0), + &task->window); if (rc != result_OK) goto free_dst; diff --git a/libraries/wuss/test/tasks/sofa.c b/libraries/wuss/test/tasks/sofa.c index c006691e..0e143148 100644 --- a/libraries/wuss/test/tasks/sofa.c +++ b/libraries/wuss/test/tasks/sofa.c @@ -258,7 +258,6 @@ static fix8_point_t project(vec3_t v, int cx, int cy, double unit) result_t sofa_create(wuss_t *wuss, const colour_t *palette, sofa_task_t *task) { wuss_task_t delegate; - box_t box; task->bg = palette[palette_PICO8_DARK_PURPLE]; task->line = palette[palette_PICO8_ORANGE]; @@ -269,17 +268,16 @@ result_t sofa_create(wuss_t *wuss, const colour_t *palette, sofa_task_t *task) task->turns = 0; delegate = wuss_task_start(sofa_handle, task); /* sofa_redraw paints its own background every frame */ - box = (box_t) BOX_POS_SIZE(250, 260, 180, 160); - - return wuss_window_create(wuss, - &box, - "Sofa", - wuss_WINDOW_NONE, - wuss_NO_BACKGROUND, - &delegate, - box_size(&box), - SIZE2D(0, 0), - &task->window); + + return wuss_window_create_placed(wuss, + SIZE2D(180, 160), + "Sofa", + wuss_WINDOW_NONE, + wuss_NO_BACKGROUND, + &delegate, + SIZE2D(180, 160), + SIZE2D(0, 0), + &task->window); } void sofa_destroy(sofa_task_t *task) diff --git a/libraries/wuss/test/tasks/text.c b/libraries/wuss/test/tasks/text.c index c96d9274..2c2e9eb9 100644 --- a/libraries/wuss/test/tasks/text.c +++ b/libraries/wuss/test/tasks/text.c @@ -32,7 +32,7 @@ result_t text_create(wuss_t *wuss, text_task_t *task) { wuss_task_t delegate; - box_t box; + size2d_t sz; result_t rc; task->font = font; @@ -42,20 +42,20 @@ result_t text_create(wuss_t *wuss, task->resizing = true; delegate = wuss_task_start(text_handle, task); - box = (box_t) BOX_POS_SIZE(120, 100, 220, 180); - - task->base_width = box.x1 - box.x0; - task->base_height = box.y1 - box.y0; - - rc = wuss_window_create(wuss, - &box, - "Lorem Ipsum", - wuss_WINDOW_NO_RESIZE_BLIT, /* paragraph reflows across the whole window, so a resize must redraw all of it, not just the newly (un)covered edge */ - palette_PICO8_BLUE, - &delegate, - box_size(&box), - SIZE2D(0, 0), - &task->window); + sz = SIZE2D(220, 180); + + task->base_width = sz.w; + task->base_height = sz.h; + + rc = wuss_window_create_placed(wuss, + sz, + "Lorem Ipsum", + wuss_WINDOW_NO_RESIZE_BLIT, /* paragraph reflows across the whole window, so a resize must redraw all of it, not just the newly (un)covered edge */ + palette_PICO8_BLUE, + &delegate, + sz, + SIZE2D(0, 0), + &task->window); return rc; } From 34962d73418fb84deaa27270db24685f4662cd6b Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 30 Aug 2026 00:30:34 +0100 Subject: [PATCH 19/44] feat(packer): add packer_set_gutter for spacing between located boxes packer_place_by now searches for the box plus a configurable gutter strip along its two inner edges, so boxes placed by location never end up flush. The gutter defaults to 0, leaving every existing caller unchanged. The returned position is still the un-inflated box. wuss_window_create_placed sets a fixed WUSS_PLACE_GUTTER and stores the gutter-inflated region in wuss_window::packed so the whole reservation, not just the footprint, is handed back on close/move. Co-Authored-By: Claude Sonnet 5 --- include/geom/packer.h | 12 +++++++ libraries/geom/packer/impl.h | 3 ++ libraries/geom/packer/packer.c | 29 +++++++++++++-- libraries/geom/packer/test/packer-test.c | 45 ++++++++++++++++++++++++ libraries/wuss/impl.h | 9 +++-- libraries/wuss/window/create-placed.c | 28 +++++++++------ 6 files changed, 110 insertions(+), 16 deletions(-) diff --git a/include/geom/packer.h b/include/geom/packer.h index 16fb3d53..184f7d4f 100644 --- a/include/geom/packer.h +++ b/include/geom/packer.h @@ -77,6 +77,18 @@ int packer_next_width(T *packer, packer_loc_t loc); result_t packer_place_at(T *packer, const box_t *area); +/** + * Sets a gutter for packer_place_by: the width in pixels of a strip it + * additionally reserves along each placed box's two edges facing away from the + * search corner, so boxes placed by location never end up flush against each + * other. Default 0 (no gutter). Negative values are treated as 0. Does not + * affect packer_place_at. + * + * \param[in] packer Packer to configure. + * \param[in] gutter Gutter width in pixels. + */ +void packer_set_gutter(T *packer, int gutter); + /** * Returns a previously placed area to the free pool: the inverse of * packer_place_at / packer_place_by. diff --git a/libraries/geom/packer/impl.h b/libraries/geom/packer/impl.h index 2b3312a5..f8beff00 100644 --- a/libraries/geom/packer/impl.h +++ b/libraries/geom/packer/impl.h @@ -34,6 +34,9 @@ struct packer packer_sortdir_t order; /* order to which we have sorted */ int sorted; /* a bool */ + int gutter; /* px reserved past each placed box's + inner edges by packer_place_by; 0 = none */ + box_t consumed_area; /* total consumed area */ }; diff --git a/libraries/geom/packer/packer.c b/libraries/geom/packer/packer.c index fc47e182..a542c5e2 100644 --- a/libraries/geom/packer/packer.c +++ b/libraries/geom/packer/packer.c @@ -46,6 +46,8 @@ packer_t *packer_create(const box_t *dims) p->order = packer_SORT_TOP_LEFT; /* any will do */ p->sorted = 1; + p->gutter = 0; + p->consumed_area.x0 = INT_MAX; p->consumed_area.y0 = INT_MAX; p->consumed_area.x1 = INT_MIN; @@ -423,6 +425,11 @@ result_t packer_release(packer_t *packer, const box_t *area) return add_area(packer, &b); } +void packer_set_gutter(packer_t *packer, int gutter) +{ + packer->gutter = (gutter > 0) ? gutter : 0; +} + result_t packer_place_by(packer_t *packer, packer_loc_t loc, int w, @@ -431,6 +438,8 @@ result_t packer_place_by(packer_t *packer, { result_t err; const box_t *b; + int g, fw, fh; + box_t consume; if (pos) *pos = NULL; @@ -438,11 +447,17 @@ result_t packer_place_by(packer_t *packer, if (w == 0 || h == 0) return result_PACKER_EMPTY; + /* look for a free area big enough for the box plus the gutter strip it + * reserves along its two inner edges */ + g = packer->gutter; + fw = w + g; + fh = h + g; + for (b = packer_start(packer, (packer_sortdir_t) loc); b; b = packer_next(packer)) { - if (box_could_hold(b, w, h)) + if (box_could_hold(b, fw, fh)) { logf_debug("packer_place_by: %s", "fits"); break; @@ -455,18 +470,24 @@ result_t packer_place_by(packer_t *packer, return result_PACKER_DIDNT_FIT; } + /* the box sits flush in the chosen corner of the free area; the gutter is + * reserved on its two edges that face away from that corner */ switch (loc) { case packer_LOC_TOP_LEFT: case packer_LOC_BOTTOM_LEFT: packer->placed_area.x0 = b->x0; packer->placed_area.x1 = b->x0 + w; + consume.x0 = b->x0; + consume.x1 = b->x0 + fw; /* box + gutter to the right */ break; case packer_LOC_TOP_RIGHT: case packer_LOC_BOTTOM_RIGHT: packer->placed_area.x0 = b->x1 - w; packer->placed_area.x1 = b->x1; + consume.x0 = b->x1 - fw; /* box + gutter to the left */ + consume.x1 = b->x1; break; default: @@ -479,19 +500,23 @@ result_t packer_place_by(packer_t *packer, case packer_LOC_TOP_RIGHT: packer->placed_area.y0 = b->y1 - h; packer->placed_area.y1 = b->y1; + consume.y0 = b->y1 - fh; /* box + gutter below */ + consume.y1 = b->y1; break; case packer_LOC_BOTTOM_LEFT: case packer_LOC_BOTTOM_RIGHT: packer->placed_area.y0 = b->y0; packer->placed_area.y1 = b->y0 + h; + consume.y0 = b->y0; + consume.y1 = b->y0 + fh; /* box + gutter above */ break; default: break; } - err = remove_area(packer, &packer->placed_area); + err = remove_area(packer, &consume); if (err) return err; diff --git a/libraries/geom/packer/test/packer-test.c b/libraries/geom/packer/test/packer-test.c index d93ba541..523c4f50 100644 --- a/libraries/geom/packer/test/packer-test.c +++ b/libraries/geom/packer/test/packer-test.c @@ -458,6 +458,51 @@ static int test3(void) goto failure; } + packer_destroy(packer); + + + /* gutter: a column just wide enough for one box plus its gutter forces the + * second placement to stack above the first, gutter between them */ + { + static const box_t coldims = { 0, 0, 30, 200 }; + + box_t first; + + printf("test3: packer_set_gutter\n"); + + packer = packer_create(&coldims); + if (packer == NULL) + return 1; + + packer_set_gutter(packer, 10); + + /* pos points at the packer's single result buffer, so copy the first + * placement out before the second overwrites it */ + err = packer_place_by(packer, packer_LOC_BOTTOM_LEFT, 20, 20, &a); + if (err) + goto failure; + first = *a; + + err = packer_place_by(packer, packer_LOC_BOTTOM_LEFT, 20, 20, &b); + if (err) + goto failure; + + /* boxes are still 20x20 (the gutter is not added to the result)... */ + if (first.x1 - first.x0 != 20 || first.y1 - first.y0 != 20 || + b->x1 - b->x0 != 20 || b->y1 - b->y0 != 20) + { + printf("test3: gutter inflated the placed box\n"); + goto failure; + } + /* ...but the second sits a full gutter above the first, not flush */ + if (b->y0 - first.y1 != 10) + { + printf("test3: gap between placements was %d, wanted 10\n", + b->y0 - first.y1); + goto failure; + } + } + packer_destroy(packer); return 0; diff --git a/libraries/wuss/impl.h b/libraries/wuss/impl.h index 73692802..d5e5f139 100644 --- a/libraries/wuss/impl.h +++ b/libraries/wuss/impl.h @@ -28,6 +28,8 @@ * just some avoidable redraw work, never wrong */ #define WUSS_MAX_INVALIDATE_PIECES 32 +#define WUSS_PLACE_GUTTER 6 /* px left between windows auto-placed by wuss_window_create_placed */ + #define WUSS_BUTTON_INSET 3 /* shared by close/back/toggle/resize furniture buttons and scrollbar breadth */ #define WUSS_MIN_CONTENT 20 /* resize-drag floor: content can never be squeezed smaller than this */ #define WUSS_SCROLL_INSET 2 /* sausage cross-axis margin from its well's edges, purely cosmetic */ @@ -80,9 +82,10 @@ struct wuss_window size2d_t min_doc; /* resize floor, set at creation; see * wuss__min_content */ wuss_window_state_t state; /* see wuss_window_state_t */ - box_t packed; /* footprint handed to wuss->layout by - * wuss_window_create_placed, or empty if - * not auto-placed or already released */ + box_t packed; /* region wuss_window_create_placed took + * out of wuss->layout (footprint + gutter), + * to give back on close/move; empty if not + * auto-placed or already released */ box_t pre_toggle; /* visible bounds to restore on the next toggle */ char title[WUSS_TITLE_MAX + 1]; wuss_icon_t **icons; /* owned; array of owned icon pointers */ diff --git a/libraries/wuss/window/create-placed.c b/libraries/wuss/window/create-placed.c index 0550d2ba..7f884ace 100644 --- a/libraries/wuss/window/create-placed.c +++ b/libraries/wuss/window/create-placed.c @@ -74,7 +74,7 @@ result_t wuss_window_create_placed(wuss_t *wuss, result_t rc; int left, top, right, bottom; int fw, fh; - box_t screen, content; + box_t screen, content, consumed; point_t origin; const box_t *slot; int tracked; @@ -95,6 +95,8 @@ result_t wuss_window_create_placed(wuss_t *wuss, wuss->layout = packer_create(&screen); if (wuss->layout == NULL) return result_OOM; + + packer_set_gutter(wuss->layout, WUSS_PLACE_GUTTER); } footprint_pad(wuss, flags, &left, &top, &right, &bottom); @@ -125,25 +127,29 @@ result_t wuss_window_create_placed(wuss_t *wuss, content.x1 = content.x0 + size.w; content.y1 = content.y0 + size.h; + /* what packer_place_by actually took out of the free list: the footprint + * plus the gutter strip on its inner edges (right and, in packer space, + * top -- see packer_LOC_BOTTOM_LEFT). Releasing exactly this on close / + * move keeps the gutter from leaking away over a session. */ + if (tracked) + { + consumed.x0 = slot->x0; + consumed.y0 = slot->y0; + consumed.x1 = slot->x1 + WUSS_PLACE_GUTTER; + consumed.y1 = slot->y1 + WUSS_PLACE_GUTTER; + } + rc = wuss_window_create(wuss, &content, title, flags, bg, task, doc, min_doc, window); if (rc != result_OK) { if (tracked) - { - box_t placed; - - placed.x0 = origin.x; - placed.y0 = origin.y; - placed.x1 = origin.x + fw; - placed.y1 = origin.y + fh; - (void) packer_release(wuss->layout, &placed); - } + (void) packer_release(wuss->layout, &consumed); return rc; } if (tracked) - (*window)->packed = *slot; + (*window)->packed = consumed; return result_OK; } From 316fd6d8338451cc09a91cec872000bfe7bd8d40 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 30 Aug 2026 01:38:53 +0100 Subject: [PATCH 20/44] feat(wuss): mark sofa-task vertices with white dots Each wireframe model in the sofa test task now draws a small white square at every projected vertex, on top of the edges. Also adds a Cobra Mk III model alongside the existing ship. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/test/tasks/sofa.c | 123 +++++++++++++++++++++++++++++++ libraries/wuss/test/tasks/sofa.h | 3 +- 2 files changed, 125 insertions(+), 1 deletion(-) diff --git a/libraries/wuss/test/tasks/sofa.c b/libraries/wuss/test/tasks/sofa.c index 0e143148..7d0d0aff 100644 --- a/libraries/wuss/test/tasks/sofa.c +++ b/libraries/wuss/test/tasks/sofa.c @@ -15,6 +15,8 @@ #include "sofa.h" +#define SOFA_VERTEX_DOT 2 /* side, px, of the white marker square drawn at each vertex */ + #ifndef M_PI #define M_PI 3.14159265358979323846 #endif @@ -77,6 +79,81 @@ static const int ship_edges[18][2] = { 5, 8 }, { 8, 9 }, { 9, 5 }, /* right wing */ }; +/* a Cobra Mk. 3 from Elite */ +static const vec3_t cobra_vertices[28] = +{ + { 32/100.0, 0/100.0, 76/100.0 }, + { -32/100.0, 0/100.0, 76/100.0 }, + { 0/100.0, 26/100.0, 24/100.0 }, + {-120/100.0, -3/100.0, -8/100.0 }, + { 120/100.0, -3/100.0, -8/100.0 }, + { -88/100.0, 16/100.0, -40/100.0 }, + { 88/100.0, 16/100.0, -40/100.0 }, + { 128/100.0, -8/100.0, -40/100.0 }, + {-128/100.0, -8/100.0, -40/100.0 }, + { 0/100.0, 26/100.0, -40/100.0 }, + { -32/100.0, -24/100.0, -40/100.0 }, + { 32/100.0, -24/100.0, -40/100.0 }, + { -36/100.0, 8/100.0, -40/100.0 }, + { -8/100.0, 12/100.0, -40/100.0 }, + { 8/100.0, 12/100.0, -40/100.0 }, + { 36/100.0, 8/100.0, -40/100.0 }, + { 36/100.0, -12/100.0, -40/100.0 }, + { 8/100.0, -16/100.0, -40/100.0 }, + { -8/100.0, -16/100.0, -40/100.0 }, + { -36/100.0, -12/100.0, -40/100.0 }, + { 0/100.0, 0/100.0, 76/100.0 }, + { 0/100.0, 0/100.0, 90/100.0 }, + { -80/100.0, -6/100.0, -40/100.0 }, + { -80/100.0, 6/100.0, -40/100.0 }, + { -88/100.0, 0/100.0, -40/100.0 }, + { 80/100.0, 6/100.0, -40/100.0 }, + { 88/100.0, 0/100.0, -40/100.0 }, + { 80/100.0, -6/100.0, -40/100.0 }, +}; + +static const int cobra_edges[38][2] = +{ + { 0, 1 }, + { 0, 4 }, + { 1, 3 }, + { 3, 8 }, + { 4, 7 }, + { 6, 7 }, + { 6, 9 }, + { 5, 9 }, + { 5, 8 }, + { 2, 5 }, + { 2, 6 }, + { 3, 5 }, + { 4, 6 }, + { 1, 2 }, + { 0, 2 }, + { 8, 10 }, + { 10, 11 }, + { 7, 11 }, + { 1, 10 }, + { 0, 11 }, + { 1, 5 }, + { 0, 6 }, + { 20, 21 }, + { 12, 13 }, + { 18, 19 }, + { 14, 15 }, + { 16, 17 }, + { 15, 16 }, + { 14, 17 }, + { 13, 18 }, + { 12, 19 }, + { 2, 9 }, + { 22, 24 }, + { 23, 24 }, + { 22, 23 }, + { 25, 26 }, + { 26, 27 }, + { 25, 27 }, +}; + /* the five Platonic solids, vertices normalised to unit circumradius */ static const vec3_t tetra_vertices[4] = @@ -255,12 +332,33 @@ static fix8_point_t project(vec3_t v, int cx, int cy, double unit) return p; } +/* a marker square, SOFA_VERTEX_DOT on a side, centred on each projected + * vertex; drawn after the wireframe so the dots sit on top of the edges */ +static void draw_vertex_dots(screen_t *scr, + const fix8_point_t *screen, + int nvertices, + colour_t colour) +{ + int half; + int i; + + half = SOFA_VERTEX_DOT / 2; + + for (i = 0; i < nvertices; i++) + screen_draw_square(scr, + FIX8_ROUND_TO_INT(screen[i].x) - half, + FIX8_ROUND_TO_INT(screen[i].y) - half, + SOFA_VERTEX_DOT, + colour); +} + result_t sofa_create(wuss_t *wuss, const colour_t *palette, sofa_task_t *task) { wuss_task_t delegate; task->bg = palette[palette_PICO8_DARK_PURPLE]; task->line = palette[palette_PICO8_ORANGE]; + task->dot = palette[palette_PICO8_WHITE]; task->angle = 0.0; task->zoom = 1.0; task->spinning = true; @@ -331,6 +429,8 @@ static result_t sofa_redraw(const wuss_event_t *event, void *task_data) b = &screen[box_cube_edges[i][1]]; screen_draw_line_wu_fix8(scr, a->x, a->y, b->x, b->y, sc->line); } + + draw_vertex_dots(scr, screen, 8, sc->dot); } } else if (sc->shape == sofa_SHAPE_SHIP) @@ -349,6 +449,27 @@ static result_t sofa_redraw(const wuss_event_t *event, void *task_data) b = &screen[ship_edges[i][1]]; screen_draw_line_wu_fix8(scr, a->x, a->y, b->x, b->y, sc->line); } + + draw_vertex_dots(scr, screen, (int) NELEMS(ship_vertices), sc->dot); + } + else if (sc->shape == sofa_SHAPE_COBRA) + { + fix8_point_t screen[NELEMS(cobra_vertices)]; + int i; + + for (i = 0; i < (int) NELEMS(cobra_vertices); i++) + screen[i] = project(rotate_xy(cobra_vertices[i], SOFA_TILT, sc->angle), cx, cy, unit); + + for (i = 0; i < (int) NELEMS(cobra_edges); i++) + { + const fix8_point_t *a, *b; + + a = &screen[cobra_edges[i][0]]; + b = &screen[cobra_edges[i][1]]; + screen_draw_line_wu_fix8(scr, a->x, a->y, b->x, b->y, sc->line); + } + + draw_vertex_dots(scr, screen, (int) NELEMS(cobra_vertices), sc->dot); } else { @@ -369,6 +490,8 @@ static result_t sofa_redraw(const wuss_event_t *event, void *task_data) b = &screen[wf->edges[i][1]]; screen_draw_line_wu_fix8(scr, a->x, a->y, b->x, b->y, sc->line); } + + draw_vertex_dots(scr, screen, wf->nvertices, sc->dot); } return result_OK; diff --git a/libraries/wuss/test/tasks/sofa.h b/libraries/wuss/test/tasks/sofa.h index a092ecaf..7c4894e9 100644 --- a/libraries/wuss/test/tasks/sofa.h +++ b/libraries/wuss/test/tasks/sofa.h @@ -15,6 +15,7 @@ typedef enum sofa_shape { sofa_SHAPE_SOFA, sofa_SHAPE_SHIP, + sofa_SHAPE_COBRA, sofa_SHAPE_TETRAHEDRON, sofa_SHAPE_CUBE, sofa_SHAPE_OCTAHEDRON, @@ -30,7 +31,7 @@ sofa_shape_t; typedef struct sofa_task { wuss_window_t *window; - colour_t bg, line; + colour_t bg, line, dot; double angle; double zoom; /* scroll-adjustable */ bool spinning; From cbd439706f025256e180ef1fa987a7cccabb1f77 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 30 Aug 2026 10:28:35 +0100 Subject: [PATCH 21/44] fix(screen): remove signed-overflow and negative-shift UB in wu fix8 lines The anti-aliased fixed-point line rasteriser had three undefined behaviours, all reachable with long or off-screen endpoints: - FIX16_ONE * dy_f8 (and grad_f16 * dx) overflowed 32-bit int once a line exceeded ~32k fixed-point units; compute those in long long. - INT_TO_FIX8(iy) left-shifted pixel Y coordinates that can be negative; use a multiply instead. - the yf recurrence left-shifted a possibly-negative fix8 value before shifting back; form the fix16 sum by multiply, then arithmetic-shift down. Adds test_wu_fix8_extreme_coords covering large and off-screen endpoints, and fixes the test draw() helper's own negative left shift. Co-Authored-By: Claude Sonnet 5 --- libraries/framebuf/screen/screen-draw.c | 26 +++++++----- libraries/framebuf/screen/test/screen-test.c | 43 ++++++++++++++++++-- 2 files changed, 55 insertions(+), 14 deletions(-) diff --git a/libraries/framebuf/screen/screen-draw.c b/libraries/framebuf/screen/screen-draw.c index 50c9b453..2fc46547 100644 --- a/libraries/framebuf/screen/screen-draw.c +++ b/libraries/framebuf/screen/screen-draw.c @@ -483,18 +483,20 @@ void screen_draw_line_wu_fix8(screen_t *scr, SWAP(y0_f8, y1_f8); } - grad_f16 = (dx_f8 == 0) ? FIX16_ONE : FIX16_ONE * dy_f8 / dx_f8; + /* 64-bit intermediates: FIX16_ONE * dy_f8 and grad_f16 * dx overflow int. */ + grad_f16 = (dx_f8 == 0) ? FIX16_ONE : (fix16_t) ((long long) FIX16_ONE * dy_f8 / dx_f8); /* start point */ xend_i = FIX8_ROUND_TO_INT(x0_f8); - yend_f8 = y0_f8 + grad_f16 * (INT_TO_FIX8(xend_i) - x0_f8) / FIX16_ONE; + yend_f8 = y0_f8 + (fix8_t) ((long long) grad_f16 * (INT_TO_FIX8(xend_i) - x0_f8) / FIX16_ONE); xgap_f8 = INT_TO_FIX8(xend_i) + FIX8_ONE / 2 - x0_f8; assert(xgap_f8 >= 0 && xgap_f8 <= FIX8_ONE); ix0_i = xend_i; iy0_i = FIX8_FLOOR_TO_INT(yend_f8); - alpha1_i = (255 * (INT_TO_FIX8(iy0_i) + FIX8_ONE - yend_f8) * xgap_f8 / FIX8_ONE) / FIX8_ONE; - alpha2_i = (255 * -(INT_TO_FIX8(iy0_i) - yend_f8) * xgap_f8 / FIX8_ONE) / FIX8_ONE; + /* iy0_i may be negative; use multiply not INT_TO_FIX8's left shift. */ + alpha1_i = (255 * (iy0_i * FIX8_ONE + FIX8_ONE - yend_f8) * xgap_f8 / FIX8_ONE) / FIX8_ONE; + alpha2_i = (255 * -(iy0_i * FIX8_ONE - yend_f8) * xgap_f8 / FIX8_ONE) / FIX8_ONE; if (steep_b) { screen_blend_pixel(scr, iy0_i, ix0_i, colour, alpha1_i); @@ -506,18 +508,20 @@ void screen_draw_line_wu_fix8(screen_t *scr, screen_blend_pixel(scr, ix0_i, iy0_i + 1, colour, alpha2_i); } - yf_f8 = ((yend_f8 << (FIX16_SHIFT - FIX8_SHIFT)) + grad_f16) >> (FIX16_SHIFT - FIX8_SHIFT); + /* yend_f8 may be negative; form the fix16 sum by multiply (left-shifting a + * negative is UB) then arithmetic-shift back down. */ + yf_f8 = (yend_f8 * (FIX16_ONE / FIX8_ONE) + grad_f16) >> (FIX16_SHIFT - FIX8_SHIFT); /* end point */ xend_i = FIX8_ROUND_TO_INT(x1_f8); - yend_f8 = y1_f8 + grad_f16 * (INT_TO_FIX8(xend_i) - x1_f8) / FIX16_ONE; + yend_f8 = y1_f8 + (fix8_t) ((long long) grad_f16 * (INT_TO_FIX8(xend_i) - x1_f8) / FIX16_ONE); xgap_f8 = x1_f8 + FIX8_ONE / 2 - INT_TO_FIX8(xend_i); assert(xgap_f8 >= 0 && xgap_f8 < FIX8_ONE); ix1_i = xend_i; iy1_i = FIX8_FLOOR_TO_INT(yend_f8); - alpha1_i = (255 * (INT_TO_FIX8(iy1_i) + FIX8_ONE - yend_f8) * xgap_f8 / FIX8_ONE) / FIX8_ONE; - alpha2_i = (255 * -(INT_TO_FIX8(iy1_i) - yend_f8) * xgap_f8 / FIX8_ONE) / FIX8_ONE; + alpha1_i = (255 * (iy1_i * FIX8_ONE + FIX8_ONE - yend_f8) * xgap_f8 / FIX8_ONE) / FIX8_ONE; + alpha2_i = (255 * -(iy1_i * FIX8_ONE - yend_f8) * xgap_f8 / FIX8_ONE) / FIX8_ONE; if (steep_b) { screen_blend_pixel(scr, iy1_i, ix1_i, colour, alpha1_i); @@ -534,8 +538,8 @@ void screen_draw_line_wu_fix8(screen_t *scr, for (x_i = ix0_i + 1; x_i < ix1_i; x_i++) { y_i = FIX8_FLOOR_TO_INT(yf_f8); - alpha1_i = (255 * (INT_TO_FIX8(y_i) + FIX8_ONE - yf_f8)) / FIX8_ONE; - alpha2_i = (255 * -(INT_TO_FIX8(y_i) - yf_f8)) / FIX8_ONE; + alpha1_i = (255 * (y_i * FIX8_ONE + FIX8_ONE - yf_f8)) / FIX8_ONE; + alpha2_i = (255 * -(y_i * FIX8_ONE - yf_f8)) / FIX8_ONE; if (steep_b) { screen_blend_pixel(scr, y_i, x_i, colour, alpha1_i); @@ -546,7 +550,7 @@ void screen_draw_line_wu_fix8(screen_t *scr, screen_blend_pixel(scr, x_i, y_i, colour, alpha1_i); screen_blend_pixel(scr, x_i, y_i + 1, colour, alpha2_i); } - yf_f8 = ((yf_f8 << (FIX16_SHIFT - FIX8_SHIFT)) + grad_f16) >> (FIX16_SHIFT - FIX8_SHIFT); + yf_f8 = (yf_f8 * (FIX16_ONE / FIX8_ONE) + grad_f16) >> (FIX16_SHIFT - FIX8_SHIFT); } } diff --git a/libraries/framebuf/screen/test/screen-test.c b/libraries/framebuf/screen/test/screen-test.c index fb8cd919..a3ce2903 100644 --- a/libraries/framebuf/screen/test/screen-test.c +++ b/libraries/framebuf/screen/test/screen-test.c @@ -102,9 +102,10 @@ static void draw(screen_t *scr, linekind_t kind, const linetest_t *line, break; case linekind_WU_FIX8: + /* multiply, not INT_TO_FIX8: coords may be negative (left shift is UB). */ screen_draw_line_wu_fix8(scr, - INT_TO_FIX8(line->x0), INT_TO_FIX8(line->y0), - INT_TO_FIX8(line->x1), INT_TO_FIX8(line->y1), + line->x0 * FIX8_ONE, line->y0 * FIX8_ONE, + line->x1 * FIX8_ONE, line->y1 * FIX8_ONE, colour); break; @@ -202,6 +203,41 @@ static result_t test_clipping_still_happens(void) /* ----------------------------------------------------------------------- */ +/* Wu fix8 lines with large and off-screen endpoints must not trip + * UndefinedBehaviorSanitizer: the gradient maths once overflowed 32-bit int + * (FIX16_ONE * dy_f8) and left-shifted negative pixel coordinates. */ +static result_t test_wu_fix8_extreme_coords(void) +{ + /* fix8: value * 256, written out to avoid left-shifting negatives here too. */ + static const fix8_t endpoints[][4] = + { + { -1000 * 256, 32 * 256, 2000 * 256, 33 * 256 }, + { 32 * 256, -1000 * 256, 31 * 256, 2000 * 256 }, + { -5000 * 256, -5000 * 256, 5000 * 256, 5000 * 256 }, + { -32000 * 256, 10 * 256, 32000 * 256, 50 * 256 } + }; + + static testscreen_t ts; + + colour_t colour; + size_t i; + + colour = colour_rgb(255, 255, 255); + + for (i = 0; i < NELEMS(endpoints); i++) + { + testscreen_init(&ts); + screen_draw_line_wu_fix8(&ts.scr, + endpoints[i][0], endpoints[i][1], + endpoints[i][2], endpoints[i][3], + colour); + } + + return result_TEST_PASSED; +} + +/* ----------------------------------------------------------------------- */ + result_t screen_test(const char *resources) { typedef result_t (*screentestfn)(void); @@ -209,7 +245,8 @@ result_t screen_test(const char *resources) static const screentestfn tests[] = { test_clip_invariance, - test_clipping_still_happens + test_clipping_still_happens, + test_wu_fix8_extreme_coords }; result_t rc; From d6cf9d15e3afc15cfb1a698581cc972de2953fbe Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 30 Aug 2026 11:01:29 +0100 Subject: [PATCH 22/44] refactor(wuss): make launcher rows work-area button icons Replace the launcher's hand-drawn text rows and bespoke mouse hit-testing with wuss_ICON_TYPE_BUTTON icons created via wuss_icon_create_array; wuss now paints and hit-tests them and delivers clicks as wuss_EVENT_ICON. Each click spawns a fresh instance of the row's task, so a row can be clicked any number of times. Drops launcher_redraw/launcher_mouse, the running[] tint array, and the now-unused font and palette parameters. Shutdown calls every entry's destroy() unconditionally (NULL-safe). Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/test/tasks/launcher.c | 150 +++++++++++---------------- libraries/wuss/test/tasks/launcher.h | 18 ++-- libraries/wuss/test/wuss-test.c | 8 +- 3 files changed, 72 insertions(+), 104 deletions(-) diff --git a/libraries/wuss/test/tasks/launcher.c b/libraries/wuss/test/tasks/launcher.c index 69831975..d01b565e 100644 --- a/libraries/wuss/test/tasks/launcher.c +++ b/libraries/wuss/test/tasks/launcher.c @@ -12,46 +12,72 @@ #include "base/utils.h" #include "framebuf/palettes.h" #include "geom/box.h" -#include "geom/point.h" + +#include "wuss/icon.h" #include "launcher.h" -#define LAUNCHER_ROW_HEIGHT 20 +#define LAUNCHER_ROW_HEIGHT 22 #define LAUNCHER_PAD 4 #define LAUNCHER_WIDTH 160 result_t launcher_create(wuss_t *wuss, const launcher_entry_t *entries, int nentries, - bmfont_t *font, - const colour_t *palette, launcher_task_t *task) { - wuss_task_t delegate; - size2d_t sz; + wuss_task_t delegate; + wuss_icon_spec_t specs[LAUNCHER_MAX_ENTRIES]; + size2d_t sz; + int i; + result_t rc; assert(nentries <= LAUNCHER_MAX_ENTRIES); - task->entries = entries; - task->nentries = nentries; - memset(task->running, 0, sizeof(task->running)); - task->font = font; - task->fg = palette[palette_PICO8_BLACK]; - task->bg = palette[palette_PICO8_WHITE]; - task->running_fg = palette[palette_PICO8_LIGHT_GREY]; - - delegate = wuss_task_start(launcher_handle, task); /* launcher_redraw paints its own background */ - sz = SIZE2D(LAUNCHER_WIDTH, LAUNCHER_PAD * 2 + nentries * LAUNCHER_ROW_HEIGHT); - - return wuss_window_create_placed(wuss, - sz, - "Launcher", - wuss_WINDOW_NO_CLOSE, - wuss_NO_BACKGROUND, - &delegate, - sz, - SIZE2D(0, 0), - &task->window); + task->entries = entries; + task->nentries = nentries; + task->window = NULL; + memset(task->icons, 0, sizeof(task->icons)); + + delegate = wuss_task_start(launcher_handle, task); + sz = SIZE2D(LAUNCHER_WIDTH, + LAUNCHER_PAD * 2 + nentries * LAUNCHER_ROW_HEIGHT); + + rc = wuss_window_create_placed(wuss, + sz, + "Launcher", + wuss_WINDOW_NO_CLOSE, + palette_PICO8_LIGHT_GREY, + &delegate, + sz, + SIZE2D(0, 0), + &task->window); + if (rc != result_OK) + return rc; + + memset(specs, 0, sizeof(specs)); + + for (i = 0; i < nentries; i++) + { + specs[i].bbox = (box_t) BOX_POS_SIZE(LAUNCHER_PAD, + LAUNCHER_PAD + i * LAUNCHER_ROW_HEIGHT, + LAUNCHER_WIDTH - LAUNCHER_PAD * 2, + LAUNCHER_ROW_HEIGHT - 2); + specs[i].type = wuss_ICON_TYPE_BUTTON; + specs[i].text = entries[i].name; + specs[i].fg = palette_PICO8_BLACK; + specs[i].bg = palette_PICO8_LIGHT_GREY; + } + + rc = wuss_icon_create_array(task->window, specs, nentries, task->icons); + if (rc != result_OK) + { + wuss_window_close(task->window); + task->window = NULL; + return rc; + } + + return result_OK; } void launcher_destroy(launcher_task_t *task) @@ -59,83 +85,29 @@ void launcher_destroy(launcher_task_t *task) wuss_window_close(task->window); } -static result_t launcher_redraw(const wuss_event_t *event, void *task_data) +static result_t launcher_icon(launcher_task_t *lc, const wuss_icon_t *icon) { - launcher_task_t *lc; - screen_t *scr; - const box_t *content, *bounds; - int i, font_width, font_height, sx, sy; - point_t pos; - const launcher_entry_t *entry; - - lc = task_data; - - scr = event->data.redraw.scr; - content = event->data.redraw.content; - bounds = event->data.redraw.bounds; - sx = event->data.redraw.scroll.x; - sy = event->data.redraw.scroll.y; - - screen_draw_rect(scr, content->x0, content->y0, box_size(content), lc->bg); - - bmfont_get_info(lc->font, NULL, &font_height); + int i; for (i = 0; i < lc->nentries; i++) - { - entry = &lc->entries[i]; - - pos.x = bounds->x0 - sx + LAUNCHER_PAD; - pos.y = bounds->y0 - sy + LAUNCHER_PAD + i * LAUNCHER_ROW_HEIGHT + (LAUNCHER_ROW_HEIGHT - font_height) / 2; - - bmfont_draw(lc->font, scr, entry->name, (int) strlen(entry->name), - lc->running[i] ? lc->running_fg : lc->fg, lc->bg, &pos, NULL); - } + if (lc->icons[i] == icon) + return lc->entries[i].spawn(); return result_OK; } -static result_t launcher_mouse(wuss_window_t *window, int y, void *task_data) -{ - launcher_task_t *lc; - int i; - const launcher_entry_t *entry; - result_t rc; - - lc = task_data; - - /* y already arrives in virtual content space, which is how the rows are - * laid out, so the scroll offset must not be added again here. */ - i = (y - LAUNCHER_PAD) / LAUNCHER_ROW_HEIGHT; - if (i < 0 || i >= lc->nentries) - return result_OK; - - if (lc->running[i]) - return result_OK; - - entry = &lc->entries[i]; - rc = entry->spawn(); - if (rc == result_OK) - { - lc->running[i] = true; - wuss_window_invalidate_all(window); - } - - return rc; -} - result_t launcher_handle(wuss_window_t *window, const wuss_event_t *event, void *task_data) { + NOT_USED(window); + switch (event->kind) { - case wuss_EVENT_REDRAW: - return launcher_redraw(event, task_data); - - case wuss_EVENT_MOUSE: - if (event->data.mouse.action != wuss_MOUSE_DOWN) + case wuss_EVENT_ICON: + if (event->data.icon.action != wuss_MOUSE_DOWN) return result_OK; - return launcher_mouse(window, event->data.mouse.point.y, task_data); + return launcher_icon(task_data, event->data.icon.icon); default: return result_OK; diff --git a/libraries/wuss/test/tasks/launcher.h b/libraries/wuss/test/tasks/launcher.h index 4147975e..a72976b9 100644 --- a/libraries/wuss/test/tasks/launcher.h +++ b/libraries/wuss/test/tasks/launcher.h @@ -7,8 +7,7 @@ #include -#include "framebuf/bmfont.h" -#include "framebuf/colour.h" +#include "wuss/icon.h" #include "wuss/window.h" typedef result_t (*launcher_spawn_fn_t)(void); @@ -23,31 +22,26 @@ typedef struct launcher_entry } launcher_entry_t; -/* a row's task is spawned at most once: launcher_task's "running" array is - * set on the first click and never cleared, so a second click is a no-op -- - * relaunching a task after its window closes needs the test restarted */ +/* each row is a work-area button icon; clicking one spawns a fresh instance + * of its task, so a row may be clicked any number of times */ #define LAUNCHER_MAX_ENTRIES 32 typedef struct launcher_task { const launcher_entry_t *entries; /* owned by the caller, must outlive the window */ int nentries; - bool running[LAUNCHER_MAX_ENTRIES]; - bmfont_t *font; - colour_t fg, bg, running_fg; + wuss_icon_t *icons[LAUNCHER_MAX_ENTRIES]; wuss_window_t *window; } launcher_task_t; wuss_event_fn_t launcher_handle; -/* create a window listing "entries"; clicking a row calls its spawn - * function once */ +/* create a window of button icons, one per entry; each click on a button + * calls its spawn function */ result_t launcher_create(wuss_t *wuss, const launcher_entry_t *entries, int nentries, - bmfont_t *font, - const colour_t *palette, launcher_task_t *task); void launcher_destroy(launcher_task_t *task); diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index c0de3242..d9fe6da5 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -276,7 +276,7 @@ static result_t wuss_interactive_test(const char *resources) g_resources = resources; g_daydream_font = daydream_font; - rc = launcher_create(wuss, g_launcher_entries, NELEMS(g_launcher_entries), font, palette, &launcher_task); + rc = launcher_create(wuss, g_launcher_entries, NELEMS(g_launcher_entries), &launcher_task); if (rc != result_OK) goto Failure; @@ -429,9 +429,11 @@ static result_t wuss_interactive_test(const char *resources) SDL_Delay(1000 / 60); } + /* a task may have been spawned any number of times but each keeps only its + * latest window in a shared static; destroy() closes that window if still + * open and is a no-op otherwise */ for (i = 0; i < NELEMS(g_launcher_entries); i++) - if (launcher_task.running[i]) - g_launcher_entries[i].destroy(); + g_launcher_entries[i].destroy(); launcher_destroy(&launcher_task); wuss_destroy(wuss); From bac930dc581118b674db4df48a4bf9af27e71770 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 30 Aug 2026 11:39:45 +0100 Subject: [PATCH 23/44] feat(wuss): set an overall screen margin in window packer Reuses WUSS_PLACE_GUTTER to pad the whole screen. --- libraries/wuss/window/create-placed.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/libraries/wuss/window/create-placed.c b/libraries/wuss/window/create-placed.c index 7f884ace..59475c98 100644 --- a/libraries/wuss/window/create-placed.c +++ b/libraries/wuss/window/create-placed.c @@ -87,6 +87,10 @@ result_t wuss_window_create_placed(wuss_t *wuss, if (wuss->layout == NULL) { + static const box_t margins = { + WUSS_PLACE_GUTTER, WUSS_PLACE_GUTTER, WUSS_PLACE_GUTTER, WUSS_PLACE_GUTTER + }; + screen.x0 = 0; screen.y0 = 0; screen.x1 = wuss->scr->size.w; @@ -96,6 +100,7 @@ result_t wuss_window_create_placed(wuss_t *wuss, if (wuss->layout == NULL) return result_OOM; + packer_set_margins(wuss->layout, &margins); packer_set_gutter(wuss->layout, WUSS_PLACE_GUTTER); } From 1afeb0d14db34cc041355abefe9aa818a54b93bd Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 30 Aug 2026 11:51:56 +0100 Subject: [PATCH 24/44] refactor(wuss): give each launcher task per-instance heap state Every launcher task backed its state with a single file-scope static in wuss-test.c, so spawning a task twice made both windows share one state block and only the latest window's animation ran. Each spawn_* now calloc's a fresh per-instance block, passes it to the task's *_create, and frees it again if create fails or opens no window. Each task's wuss_EVENT_CLOSE handler frees its own block (and any owned bitmaps); the checker task, whose two windows share one block, frees it once both have closed. The now-redundant *_destroy functions, the twelve g_*_task statics, the destroy_* wrappers and the launcher_entry_t.destroy column are removed. wuss_idle already walks every window, so tasks now animate independently in as many windows as are open. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/test/tasks/ball.c | 8 +- libraries/wuss/test/tasks/ball.h | 6 +- libraries/wuss/test/tasks/blank.c | 14 +- libraries/wuss/test/tasks/blank.h | 2 - libraries/wuss/test/tasks/chars.c | 10 +- libraries/wuss/test/tasks/chars.h | 2 - libraries/wuss/test/tasks/checker.c | 11 +- libraries/wuss/test/tasks/checker.h | 2 - libraries/wuss/test/tasks/curve.c | 9 +- libraries/wuss/test/tasks/curve.h | 2 - libraries/wuss/test/tasks/gradient.c | 9 +- libraries/wuss/test/tasks/gradient.h | 2 - libraries/wuss/test/tasks/icons.c | 8 +- libraries/wuss/test/tasks/icons.h | 2 - libraries/wuss/test/tasks/image.c | 9 +- libraries/wuss/test/tasks/image.h | 2 - libraries/wuss/test/tasks/launcher.h | 6 +- libraries/wuss/test/tasks/palette.c | 9 +- libraries/wuss/test/tasks/palette.h | 2 - libraries/wuss/test/tasks/porter-duff.c | 15 +- libraries/wuss/test/tasks/porter-duff.h | 2 - libraries/wuss/test/tasks/sofa.c | 9 +- libraries/wuss/test/tasks/sofa.h | 2 - libraries/wuss/test/tasks/text.c | 9 +- libraries/wuss/test/tasks/text.h | 2 - libraries/wuss/test/wuss-test.c | 199 +++++++++++++++++------- 26 files changed, 189 insertions(+), 164 deletions(-) diff --git a/libraries/wuss/test/tasks/ball.c b/libraries/wuss/test/tasks/ball.c index 388f37b4..4e0127ba 100644 --- a/libraries/wuss/test/tasks/ball.c +++ b/libraries/wuss/test/tasks/ball.c @@ -2,6 +2,8 @@ #ifdef USE_SDL +#include + #ifdef FORTIFY #include "fortify/fortify.h" #endif @@ -39,10 +41,6 @@ result_t ball_create(wuss_t *wuss, const colour_t *palette, ball_task_t *task) &task->window); } -void ball_destroy(ball_task_t *task) -{ - wuss_window_close(task->window); -} static result_t ball_redraw(const wuss_event_t *event, void *task_data) { @@ -201,7 +199,7 @@ result_t ball_handle(wuss_window_t *window, case wuss_EVENT_CLOSE: wuss_window_close(window); - bc->window = NULL; + free(bc); /* task_data was calloc'd per instance by the spawner */ return result_OK; default: diff --git a/libraries/wuss/test/tasks/ball.h b/libraries/wuss/test/tasks/ball.h index 3d17ab1e..f42fb579 100644 --- a/libraries/wuss/test/tasks/ball.h +++ b/libraries/wuss/test/tasks/ball.h @@ -34,12 +34,10 @@ ball_task_t; wuss_event_fn_t ball_handle; -/* create the bouncing-ball window against the given wuss instance */ +/* create the bouncing-ball window against the given wuss instance; "task" is a + * per-instance block owned by the window and freed when it closes */ result_t ball_create(wuss_t *wuss, const colour_t *palette, ball_task_t *task); -/* destroy the bouncing-ball window created by ball_create */ -void ball_destroy(ball_task_t *task); - #endif /* USE_SDL */ #endif /* TASKS_BALL_H */ diff --git a/libraries/wuss/test/tasks/blank.c b/libraries/wuss/test/tasks/blank.c index 7c95c9c8..86d3365a 100644 --- a/libraries/wuss/test/tasks/blank.c +++ b/libraries/wuss/test/tasks/blank.c @@ -2,6 +2,8 @@ #ifdef USE_SDL +#include + #ifdef FORTIFY #include "fortify/fortify.h" #endif @@ -36,11 +38,6 @@ result_t blank_create(wuss_t *wuss, int npalette, blank_task_t *task) &task->window); } -void blank_destroy(blank_task_t *task) -{ - wuss_window_close(task->window); -} - static result_t blank_idle(void *task_data) { blank_task_t *bc; @@ -65,7 +62,12 @@ result_t blank_handle(wuss_window_t *window, const wuss_event_t *event, void *task_data) { - NOT_USED(window); + if (event->kind == wuss_EVENT_CLOSE) + { + wuss_window_close(window); + free(task_data); /* calloc'd per instance by the spawner */ + return result_OK; + } if (event->kind != wuss_EVENT_IDLE) return result_OK; diff --git a/libraries/wuss/test/tasks/blank.h b/libraries/wuss/test/tasks/blank.h index a2586304..b1ffd74f 100644 --- a/libraries/wuss/test/tasks/blank.h +++ b/libraries/wuss/test/tasks/blank.h @@ -25,8 +25,6 @@ wuss_event_fn_t blank_handle; /* create the colour-cycling blank window against the given wuss instance */ result_t blank_create(wuss_t *wuss, int npalette, blank_task_t *task); -/* destroy the colour-cycling window created by blank_create */ -void blank_destroy(blank_task_t *task); #endif /* USE_SDL */ diff --git a/libraries/wuss/test/tasks/chars.c b/libraries/wuss/test/tasks/chars.c index 572461ca..1680d005 100644 --- a/libraries/wuss/test/tasks/chars.c +++ b/libraries/wuss/test/tasks/chars.c @@ -2,6 +2,8 @@ #ifdef USE_SDL +#include + #include #ifdef FORTIFY @@ -60,12 +62,6 @@ result_t chars_create(wuss_t *wuss, &task->window); } -void chars_destroy(chars_task_t *task) -{ - if (task->window != NULL) - wuss_window_close(task->window); -} - static result_t chars_redraw(const wuss_event_t *event, void *task_data) { chars_task_t *cc; @@ -126,7 +122,7 @@ result_t chars_handle(wuss_window_t *window, if (event->kind == wuss_EVENT_CLOSE) { wuss_window_close(window); - ((chars_task_t *) task_data)->window = NULL; + free(task_data); /* calloc'd per instance by the spawner */ return result_OK; } diff --git a/libraries/wuss/test/tasks/chars.h b/libraries/wuss/test/tasks/chars.h index 80495809..5df4199c 100644 --- a/libraries/wuss/test/tasks/chars.h +++ b/libraries/wuss/test/tasks/chars.h @@ -27,8 +27,6 @@ result_t chars_create(wuss_t *wuss, const colour_t *palette, chars_task_t *task); -/* destroy the glyph-grid window created by chars_create, if any */ -void chars_destroy(chars_task_t *task); #endif /* USE_SDL */ diff --git a/libraries/wuss/test/tasks/checker.c b/libraries/wuss/test/tasks/checker.c index dbd5e4e8..71a77c9d 100644 --- a/libraries/wuss/test/tasks/checker.c +++ b/libraries/wuss/test/tasks/checker.c @@ -2,6 +2,8 @@ #ifdef USE_SDL +#include + #ifdef FORTIFY #include "fortify/fortify.h" #endif @@ -62,12 +64,6 @@ result_t checker_create(wuss_t *wuss, return result_OK; } -void checker_destroy(checker_task_t *task) -{ - wuss_window_close(task->window); - wuss_window_close(task->window2); -} - static result_t checker_redraw(wuss_window_t *window, const wuss_event_t *event, void *task_data) @@ -169,6 +165,9 @@ result_t checker_handle(wuss_window_t *window, cc->window2 = NULL; else cc->window = NULL; + /* one calloc'd block backs both windows; free it once both are gone */ + if (cc->window == NULL && cc->window2 == NULL) + free(cc); return result_OK; default: diff --git a/libraries/wuss/test/tasks/checker.h b/libraries/wuss/test/tasks/checker.h index 4df752a8..ca3983ce 100644 --- a/libraries/wuss/test/tasks/checker.h +++ b/libraries/wuss/test/tasks/checker.h @@ -37,8 +37,6 @@ result_t checker_create(wuss_t *wuss, const colour_t *palette, checker_task_t *task); -/* destroy the checkerboard windows created by checker_create */ -void checker_destroy(checker_task_t *task); #endif /* USE_SDL */ diff --git a/libraries/wuss/test/tasks/curve.c b/libraries/wuss/test/tasks/curve.c index 2b9f44ef..d7ae2971 100644 --- a/libraries/wuss/test/tasks/curve.c +++ b/libraries/wuss/test/tasks/curve.c @@ -2,6 +2,8 @@ #ifdef USE_SDL +#include + #include #ifdef FORTIFY @@ -51,11 +53,6 @@ result_t curve_create(wuss_t *wuss, &task->window); } -void curve_destroy(curve_task_t *task) -{ - wuss_window_close(task->window); -} - static int blob_hit(const point_t *p, int x, int y) { int half = CURVE_BLOBSZ / 2; @@ -181,7 +178,7 @@ result_t curve_handle(wuss_window_t *window, case wuss_EVENT_CLOSE: wuss_window_close(window); - task->window = NULL; + free(task); /* task_data was calloc'd per instance by the spawner */ return result_OK; default: diff --git a/libraries/wuss/test/tasks/curve.h b/libraries/wuss/test/tasks/curve.h index 2a411ffe..de7a2e80 100644 --- a/libraries/wuss/test/tasks/curve.h +++ b/libraries/wuss/test/tasks/curve.h @@ -30,8 +30,6 @@ result_t curve_create(wuss_t *wuss, const colour_t *palette, curve_task_t *task); -/* destroy the curve window created by curve_create */ -void curve_destroy(curve_task_t *task); #endif /* USE_SDL */ diff --git a/libraries/wuss/test/tasks/gradient.c b/libraries/wuss/test/tasks/gradient.c index 0e3cc849..0602325c 100644 --- a/libraries/wuss/test/tasks/gradient.c +++ b/libraries/wuss/test/tasks/gradient.c @@ -2,6 +2,8 @@ #ifdef USE_SDL +#include + #ifdef FORTIFY #include "fortify/fortify.h" #endif @@ -48,11 +50,6 @@ result_t gradient_create(wuss_t *wuss, gradient_task_t *task) &task->window); } -void gradient_destroy(gradient_task_t *task) -{ - wuss_window_close(task->window); -} - static result_t gradient_redraw(const wuss_event_t *event, void *task_data) { screen_t *scr; @@ -99,7 +96,7 @@ result_t gradient_handle(wuss_window_t *window, case wuss_EVENT_CLOSE: wuss_window_close(window); - gc->window = NULL; + free(gc); /* task_data was calloc'd per instance by the spawner */ return result_OK; default: diff --git a/libraries/wuss/test/tasks/gradient.h b/libraries/wuss/test/tasks/gradient.h index a37ff22a..8f720073 100644 --- a/libraries/wuss/test/tasks/gradient.h +++ b/libraries/wuss/test/tasks/gradient.h @@ -21,8 +21,6 @@ wuss_event_fn_t gradient_handle; /* create the gradient window against the given wuss instance */ result_t gradient_create(wuss_t *wuss, gradient_task_t *task); -/* destroy the gradient window created by gradient_create */ -void gradient_destroy(gradient_task_t *task); #endif /* USE_SDL */ diff --git a/libraries/wuss/test/tasks/icons.c b/libraries/wuss/test/tasks/icons.c index 70a247c7..5f184f38 100644 --- a/libraries/wuss/test/tasks/icons.c +++ b/libraries/wuss/test/tasks/icons.c @@ -4,6 +4,7 @@ #include "framebuf/palettes.h" #include +#include #include #ifdef FORTIFY @@ -102,11 +103,6 @@ result_t icons_create(wuss_t *wuss, return rc; } -void icons_destroy(icons_task_t *task) -{ - wuss_window_close(task->window); -} - #define ICONS_GRID 16 /* document-space pitch of the backdrop grid */ #define ICONS_AXIS_LABEL 64 /* label every Nth grid line along each axis */ @@ -237,7 +233,7 @@ result_t icons_handle(wuss_window_t *window, case wuss_EVENT_CLOSE: wuss_window_close(window); - tcx->window = NULL; + free(tcx); /* calloc'd per instance by the spawner */ return result_OK; default: diff --git a/libraries/wuss/test/tasks/icons.h b/libraries/wuss/test/tasks/icons.h index e683142f..aebf0ea2 100644 --- a/libraries/wuss/test/tasks/icons.h +++ b/libraries/wuss/test/tasks/icons.h @@ -34,8 +34,6 @@ result_t icons_create(wuss_t *wuss, bmfont_t *font, icons_task_t *task); -/* destroy the icons window created by icons_create */ -void icons_destroy(icons_task_t *task); #endif /* USE_SDL */ diff --git a/libraries/wuss/test/tasks/image.c b/libraries/wuss/test/tasks/image.c index 0c7c9ee5..70bd56da 100644 --- a/libraries/wuss/test/tasks/image.c +++ b/libraries/wuss/test/tasks/image.c @@ -46,12 +46,6 @@ result_t image_create(wuss_t *wuss, &task->window); } -void image_destroy(image_task_t *task) -{ - wuss_window_close(task->window); - free(task->bitmap.base); -} - static result_t image_redraw(const wuss_event_t *event, void *task_data) { image_task_t *ic; @@ -86,7 +80,8 @@ result_t image_handle(wuss_window_t *window, case wuss_EVENT_CLOSE: wuss_window_close(window); - ic->window = NULL; + free(ic->bitmap.base); + free(ic); /* task_data was calloc'd per instance by the spawner */ return result_OK; default: diff --git a/libraries/wuss/test/tasks/image.h b/libraries/wuss/test/tasks/image.h index 87e1c53b..3f767b4c 100644 --- a/libraries/wuss/test/tasks/image.h +++ b/libraries/wuss/test/tasks/image.h @@ -26,8 +26,6 @@ result_t image_create(wuss_t *wuss, const char *path, image_task_t *task); -/* destroy the window and free the bitmap loaded by image_create */ -void image_destroy(image_task_t *task); #endif /* USE_SDL */ diff --git a/libraries/wuss/test/tasks/launcher.h b/libraries/wuss/test/tasks/launcher.h index a72976b9..573f3e07 100644 --- a/libraries/wuss/test/tasks/launcher.h +++ b/libraries/wuss/test/tasks/launcher.h @@ -11,14 +11,12 @@ #include "wuss/window.h" typedef result_t (*launcher_spawn_fn_t)(void); -typedef void (*launcher_destroy_fn_t)(void); /* one clickable row */ typedef struct launcher_entry { - const char *name; - launcher_spawn_fn_t spawn; - launcher_destroy_fn_t destroy; + const char *name; + launcher_spawn_fn_t spawn; } launcher_entry_t; diff --git a/libraries/wuss/test/tasks/palette.c b/libraries/wuss/test/tasks/palette.c index f98e18bd..6aa16e7c 100644 --- a/libraries/wuss/test/tasks/palette.c +++ b/libraries/wuss/test/tasks/palette.c @@ -2,6 +2,8 @@ #ifdef USE_SDL +#include + #ifdef FORTIFY #include "fortify/fortify.h" #endif @@ -35,11 +37,6 @@ result_t palette_create(wuss_t *wuss, &task->window); } -void palette_destroy(palette_task_t *task) -{ - wuss_window_close(task->window); -} - static result_t palette_redraw(const wuss_event_t *event, void *task_data) { palette_task_t *pc; @@ -89,7 +86,7 @@ result_t palette_handle(wuss_window_t *window, if (event->kind == wuss_EVENT_CLOSE) { wuss_window_close(window); - ((palette_task_t *) task_data)->window = NULL; + free(task_data); /* calloc'd per instance by the spawner */ return result_OK; } diff --git a/libraries/wuss/test/tasks/palette.h b/libraries/wuss/test/tasks/palette.h index 8f7445b1..8463df9a 100644 --- a/libraries/wuss/test/tasks/palette.h +++ b/libraries/wuss/test/tasks/palette.h @@ -26,8 +26,6 @@ result_t palette_create(wuss_t *wuss, int npalette, palette_task_t *task); -/* destroy the palette-swatch-grid window created by palette_create */ -void palette_destroy(palette_task_t *task); #endif /* USE_SDL */ diff --git a/libraries/wuss/test/tasks/porter-duff.c b/libraries/wuss/test/tasks/porter-duff.c index 5d586693..8c6d1c91 100644 --- a/libraries/wuss/test/tasks/porter-duff.c +++ b/libraries/wuss/test/tasks/porter-duff.c @@ -218,15 +218,6 @@ result_t porter_duff_create(wuss_t *wuss, return rc; } -void porter_duff_destroy(porter_duff_task_t *task) -{ - wuss_window_close(task->window); - free(task->dst.base); - free(task->src.base); - free(task->b.base); - free(task->a.base); -} - /* ----------------------------------------------------------------------- */ /* Triangle ramp: 0 at the start of the rule's turn, 255 at its midpoint, back @@ -403,7 +394,11 @@ result_t porter_duff_handle(wuss_window_t *window, case wuss_EVENT_CLOSE: wuss_window_close(window); - pd->window = NULL; + free(pd->dst.base); + free(pd->src.base); + free(pd->b.base); + free(pd->a.base); + free(pd); /* calloc'd per instance by the spawner */ return result_OK; default: diff --git a/libraries/wuss/test/tasks/porter-duff.h b/libraries/wuss/test/tasks/porter-duff.h index d8dbd366..3a7f210e 100644 --- a/libraries/wuss/test/tasks/porter-duff.h +++ b/libraries/wuss/test/tasks/porter-duff.h @@ -42,8 +42,6 @@ result_t porter_duff_create(wuss_t *wuss, const char *resources, porter_duff_task_t *task); -/* destroy the window and free the bitmaps allocated by porter_duff_create */ -void porter_duff_destroy(porter_duff_task_t *task); #endif /* USE_SDL */ diff --git a/libraries/wuss/test/tasks/sofa.c b/libraries/wuss/test/tasks/sofa.c index 7d0d0aff..e0294768 100644 --- a/libraries/wuss/test/tasks/sofa.c +++ b/libraries/wuss/test/tasks/sofa.c @@ -2,6 +2,8 @@ #ifdef USE_SDL +#include + #include #ifdef FORTIFY @@ -378,11 +380,6 @@ result_t sofa_create(wuss_t *wuss, const colour_t *palette, sofa_task_t *task) &task->window); } -void sofa_destroy(sofa_task_t *task) -{ - wuss_window_close(task->window); -} - static result_t sofa_redraw(const wuss_event_t *event, void *task_data) { sofa_task_t *sc; @@ -582,7 +579,7 @@ result_t sofa_handle(wuss_window_t *window, case wuss_EVENT_CLOSE: wuss_window_close(window); - sc->window = NULL; + free(sc); /* task_data was calloc'd per instance by the spawner */ return result_OK; default: diff --git a/libraries/wuss/test/tasks/sofa.h b/libraries/wuss/test/tasks/sofa.h index 7c4894e9..1a8c9ffd 100644 --- a/libraries/wuss/test/tasks/sofa.h +++ b/libraries/wuss/test/tasks/sofa.h @@ -45,8 +45,6 @@ wuss_event_fn_t sofa_handle; /* create the sofa window against the given wuss instance */ result_t sofa_create(wuss_t *wuss, const colour_t *palette, sofa_task_t *task); -/* destroy the sofa window created by sofa_create */ -void sofa_destroy(sofa_task_t *task); #endif /* USE_SDL */ diff --git a/libraries/wuss/test/tasks/text.c b/libraries/wuss/test/tasks/text.c index 2c2e9eb9..1c5e8d0a 100644 --- a/libraries/wuss/test/tasks/text.c +++ b/libraries/wuss/test/tasks/text.c @@ -2,6 +2,8 @@ #ifdef USE_SDL +#include + #include #include #include @@ -60,11 +62,6 @@ result_t text_create(wuss_t *wuss, return rc; } -void text_destroy(text_task_t *task) -{ - wuss_window_close(task->window); -} - static result_t text_redraw(const wuss_event_t *event, void *task_data) { text_task_t *tcx; @@ -183,7 +180,7 @@ result_t text_handle(wuss_window_t *window, case wuss_EVENT_CLOSE: wuss_window_close(window); - tcx->window = NULL; + free(tcx); /* task_data was calloc'd per instance by the spawner */ return result_OK; case wuss_EVENT_IDLE: diff --git a/libraries/wuss/test/tasks/text.h b/libraries/wuss/test/tasks/text.h index cf1dd7fe..d7a985af 100644 --- a/libraries/wuss/test/tasks/text.h +++ b/libraries/wuss/test/tasks/text.h @@ -34,8 +34,6 @@ result_t text_create(wuss_t *wuss, bmfont_t *font, text_task_t *task); -/* destroy the paragraph-of-text window created by text_create */ -void text_destroy(text_task_t *task); #endif /* USE_SDL */ diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index d9fe6da5..e8dcc6ec 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -57,68 +57,154 @@ static int g_npalette; static const char *g_resources; static bmfont_t *g_daydream_font; -static ball_task_t g_ball_task; -static text_task_t g_text_task; -static blank_task_t g_blank_task; -static chars_task_t g_chars_task; -static palette_task_t g_palette_task; -static image_task_t g_image_task; -static checker_task_t g_checker_task; -static curve_task_t g_curve_task; -static sofa_task_t g_sofa_task; -static gradient_task_t g_gradient_task; -static icons_task_t g_icons_task; -static porter_duff_task_t g_porter_duff_task; - -static result_t spawn_ball(void) { return ball_create(g_wuss, g_palette, &g_ball_task); } -static result_t spawn_text(void) { return text_create(g_wuss, g_palette, g_daydream_font, &g_text_task); } -static result_t spawn_blank(void) { return blank_create(g_wuss, g_npalette, &g_blank_task); } -static result_t spawn_chars(void) { return chars_create(g_wuss, g_palette, &g_chars_task); } -static result_t spawn_palette(void) { return palette_create(g_wuss, g_palette, g_npalette, &g_palette_task); } +/* Each spawn allocates a fresh per-instance task block so a task may run in + * several windows at once; the block is owned by its window and freed by the + * task's wuss_EVENT_CLOSE handler. If create fails, or (for the font-less + * chars task) returns OK without opening a window, the block is freed here -- + * otherwise it would leak. */ + +static result_t spawn_ball(void) +{ + ball_task_t *t = calloc(1, sizeof(*t)); + result_t rc; + if (t == NULL) return result_OOM; + rc = ball_create(g_wuss, g_palette, t); + if (rc != result_OK || t->window == NULL) { free(t); return rc; } + return result_OK; +} + +static result_t spawn_text(void) +{ + text_task_t *t = calloc(1, sizeof(*t)); + result_t rc; + if (t == NULL) return result_OOM; + rc = text_create(g_wuss, g_palette, g_daydream_font, t); + if (rc != result_OK || t->window == NULL) { free(t); return rc; } + return result_OK; +} + +static result_t spawn_blank(void) +{ + blank_task_t *t = calloc(1, sizeof(*t)); + result_t rc; + if (t == NULL) return result_OOM; + rc = blank_create(g_wuss, g_npalette, t); + if (rc != result_OK || t->window == NULL) { free(t); return rc; } + return result_OK; +} + +static result_t spawn_chars(void) +{ + chars_task_t *t = calloc(1, sizeof(*t)); + result_t rc; + if (t == NULL) return result_OOM; + rc = chars_create(g_wuss, g_palette, t); + if (rc != result_OK || t->window == NULL) { free(t); return rc; } + return result_OK; +} + +static result_t spawn_palette(void) +{ + palette_task_t *t = calloc(1, sizeof(*t)); + result_t rc; + if (t == NULL) return result_OOM; + rc = palette_create(g_wuss, g_palette, g_npalette, t); + if (rc != result_OK || t->window == NULL) { free(t); return rc; } + return result_OK; +} + static result_t spawn_image(void) { - const char *leafname; - const char *filename; + image_task_t *t; + const char *leafname; + const char *filename; + result_t rc; + + t = calloc(1, sizeof(*t)); + if (t == NULL) return result_OOM; leafname = path_join_leafname("jessica", "png"); filename = path_join_filename(g_resources, 3, "resources", "images", leafname); - return image_create(g_wuss, g_palette, filename, &g_image_task); + rc = image_create(g_wuss, g_palette, filename, t); + if (rc != result_OK || t->window == NULL) { free(t); return rc; } + return result_OK; +} + +static result_t spawn_checker(void) +{ + checker_task_t *t = calloc(1, sizeof(*t)); + result_t rc; + if (t == NULL) return result_OOM; + rc = checker_create(g_wuss, g_palette, t); + if (rc != result_OK || t->window == NULL) { free(t); return rc; } + return result_OK; +} + +static result_t spawn_curve(void) +{ + curve_task_t *t = calloc(1, sizeof(*t)); + result_t rc; + if (t == NULL) return result_OOM; + rc = curve_create(g_wuss, g_palette, t); + if (rc != result_OK || t->window == NULL) { free(t); return rc; } + return result_OK; +} + +static result_t spawn_sofa(void) +{ + sofa_task_t *t = calloc(1, sizeof(*t)); + result_t rc; + if (t == NULL) return result_OOM; + rc = sofa_create(g_wuss, g_palette, t); + if (rc != result_OK || t->window == NULL) { free(t); return rc; } + return result_OK; +} + +static result_t spawn_gradient(void) +{ + gradient_task_t *t = calloc(1, sizeof(*t)); + result_t rc; + if (t == NULL) return result_OOM; + rc = gradient_create(g_wuss, t); + if (rc != result_OK || t->window == NULL) { free(t); return rc; } + return result_OK; +} + +static result_t spawn_icons(void) +{ + icons_task_t *t = calloc(1, sizeof(*t)); + result_t rc; + if (t == NULL) return result_OOM; + rc = icons_create(g_wuss, g_palette, g_daydream_font, t); + if (rc != result_OK || t->window == NULL) { free(t); return rc; } + return result_OK; +} + +static result_t spawn_porter_duff(void) +{ + porter_duff_task_t *t = calloc(1, sizeof(*t)); + result_t rc; + if (t == NULL) return result_OOM; + rc = porter_duff_create(g_wuss, g_palette, g_daydream_font, g_resources, t); + if (rc != result_OK || t->window == NULL) { free(t); return rc; } + return result_OK; } -static result_t spawn_checker(void) { return checker_create(g_wuss, g_palette, &g_checker_task); } -static result_t spawn_curve(void) { return curve_create(g_wuss, g_palette, &g_curve_task); } -static result_t spawn_sofa(void) { return sofa_create(g_wuss, g_palette, &g_sofa_task); } -static result_t spawn_gradient(void) { return gradient_create(g_wuss, &g_gradient_task); } -static result_t spawn_icons(void) { return icons_create(g_wuss, g_palette, g_daydream_font, &g_icons_task); } -static result_t spawn_porter_duff(void) { return porter_duff_create(g_wuss, g_palette, g_daydream_font, g_resources, &g_porter_duff_task); } - -static void destroy_ball(void) { ball_destroy(&g_ball_task); } -static void destroy_text(void) { text_destroy(&g_text_task); } -static void destroy_blank(void) { blank_destroy(&g_blank_task); } -static void destroy_chars(void) { chars_destroy(&g_chars_task); } -static void destroy_palette(void) { palette_destroy(&g_palette_task); } -static void destroy_image(void) { image_destroy(&g_image_task); } -static void destroy_checker(void) { checker_destroy(&g_checker_task); } -static void destroy_curve(void) { curve_destroy(&g_curve_task); } -static void destroy_sofa(void) { sofa_destroy(&g_sofa_task); } -static void destroy_gradient(void) { gradient_destroy(&g_gradient_task); } -static void destroy_icons(void) { icons_destroy(&g_icons_task); } -static void destroy_porter_duff(void) { porter_duff_destroy(&g_porter_duff_task); } static const launcher_entry_t g_launcher_entries[] = { - { "Ball", spawn_ball, destroy_ball }, - { "Text", spawn_text, destroy_text }, - { "Blank", spawn_blank, destroy_blank }, - { "Chars", spawn_chars, destroy_chars }, - { "Palette", spawn_palette, destroy_palette }, - { "Image", spawn_image, destroy_image }, - { "Checker", spawn_checker, destroy_checker }, - { "Curve", spawn_curve, destroy_curve }, - { "Sofa", spawn_sofa, destroy_sofa }, - { "Gradient", spawn_gradient, destroy_gradient }, - { "Icons", spawn_icons, destroy_icons }, - { "Porter-Duff", spawn_porter_duff, destroy_porter_duff } + { "Ball", spawn_ball }, + { "Text", spawn_text }, + { "Blank", spawn_blank }, + { "Chars", spawn_chars }, + { "Palette", spawn_palette }, + { "Image", spawn_image }, + { "Checker", spawn_checker }, + { "Curve", spawn_curve }, + { "Sofa", spawn_sofa }, + { "Gradient", spawn_gradient }, + { "Icons", spawn_icons }, + { "Porter-Duff", spawn_porter_duff } }; static wuss_button_t sdl_button_to_wuss(Uint8 button) @@ -429,11 +515,10 @@ static result_t wuss_interactive_test(const char *resources) SDL_Delay(1000 / 60); } - /* a task may have been spawned any number of times but each keeps only its - * latest window in a shared static; destroy() closes that window if still - * open and is a no-op otherwise */ - for (i = 0; i < NELEMS(g_launcher_entries); i++) - g_launcher_entries[i].destroy(); + /* ponytail: wuss_destroy() below frees every still-open window but not the + * per-instance task block hung off it, so any task window left open at quit + * leaks its block. Harmless at process exit; add a wuss close callback if a + * task ever needs deterministic teardown. */ launcher_destroy(&launcher_task); wuss_destroy(wuss); From d6a610367e1940577779b7e5f6a310f77d96477e Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 30 Aug 2026 13:06:48 +0100 Subject: [PATCH 25/44] build(tools): add wrap_protos.py and rewrap overlong prototypes New tools/wrap_protos.py rewraps C function prototypes and definitions onto one parameter per line, declarators aligned in a column (the style in screen-copy-rect.c), but only when the one-line form exceeds 80 columns. Conservative guards keep it off call statements, control-flow, macro bodies and comment text. Handles function-pointer and array parameters. Self-check in tools/test_wrap_protos.py. Applied across the tree: 35 files rewrapped. Builds clean (nosdl, sdl, asan); DPTLibTest 24/24. Co-Authored-By: Claude Sonnet 5 --- include/datastruct/bitfifo.h | 6 +- include/datastruct/cache.h | 10 +- include/datastruct/hash.h | 14 +- include/datastruct/ntree.h | 10 +- include/framebuf/screen.h | 28 +- include/text/txtfmt.h | 6 +- include/utils/array.h | 12 +- include/wuss/wuss.h | 12 +- libraries/databases/pickle/test/pickle-test.c | 20 +- libraries/datastruct/cache/cache.c | 10 +- libraries/datastruct/hash/create.c | 14 +- libraries/datastruct/ntree/test/ntree-test.c | 14 +- libraries/framebuf/bmfont/bmfont.c | 20 +- libraries/framebuf/screen/screen-copy-rect.c | 14 +- libraries/framebuf/screen/screen-draw.c | 34 +- libraries/framebuf/screen/test/screen-test.c | 6 +- libraries/io/stream/stream-packbitscomp.c | 4 +- libraries/text/txtfmt/get-line.c | 6 +- libraries/utils/array/grow.c | 12 +- libraries/wuss/create.c | 12 +- libraries/wuss/furniture.h | 3 +- libraries/wuss/impl.h | 5 +- libraries/wuss/mouse-click.c | 10 +- libraries/wuss/test/tasks/ball.c | 2 +- libraries/wuss/test/tasks/blank.c | 2 +- libraries/wuss/test/tasks/checker.c | 6 +- libraries/wuss/test/tasks/curve.c | 10 +- libraries/wuss/test/tasks/image.c | 2 +- libraries/wuss/test/tasks/palette.c | 2 +- libraries/wuss/test/tasks/porter-duff.c | 8 +- libraries/wuss/test/tasks/sofa.c | 6 +- libraries/wuss/test/tasks/text.c | 2 +- libraries/wuss/test/wuss-test.c | 2 +- libraries/wuss/window/create-placed.c | 12 +- libraries/wuss/window/move.c | 6 +- tools/test_wrap_protos.py | 191 ++++++++++ tools/wrap_protos.py | 328 ++++++++++++++++++ 37 files changed, 718 insertions(+), 143 deletions(-) create mode 100644 tools/test_wrap_protos.py create mode 100644 tools/wrap_protos.py diff --git a/include/datastruct/bitfifo.h b/include/datastruct/bitfifo.h index 509b6560..f7ec8e50 100644 --- a/include/datastruct/bitfifo.h +++ b/include/datastruct/bitfifo.h @@ -55,10 +55,10 @@ void bitfifo_destroy(T *doomed); /* writes bits to 'head' onwards, wrapping around if required */ /* fifo will reject attempts to store more bits than there is space for */ -result_t bitfifo_enqueue(T *fifo, +result_t bitfifo_enqueue(T *fifo, const unsigned int *newbits, - unsigned int newbitsoffset, - size_t nnewbits); + unsigned int newbitsoffset, + size_t nnewbits); /* reads bits from 'tail' onwards, wrapping around if required */ result_t bitfifo_dequeue(T *fifo, diff --git a/include/datastruct/cache.h b/include/datastruct/cache.h index a7983c82..164a18e6 100644 --- a/include/datastruct/cache.h +++ b/include/datastruct/cache.h @@ -109,11 +109,11 @@ void *cache_get(cache_t *cache, cachekey_t key); * * \return Error indication. */ -result_t cache_put(cache_t *cache, - cachekey_t key, - void *data, - size_t length, - void **inserted); +result_t cache_put(cache_t *cache, + cachekey_t key, + void *data, + size_t length, + void **inserted); /** * Print cache statistics to stdout. diff --git a/include/datastruct/hash.h b/include/datastruct/hash.h index 4d5b881f..75067575 100644 --- a/include/datastruct/hash.h +++ b/include/datastruct/hash.h @@ -75,13 +75,13 @@ hash_destroy_value_t hash_no_destroy_value; * * \return Error indication. */ -result_t hash_create(const void *default_value, - int nbins, - hash_fn_t *fn, - hash_compare_t *compare, - hash_destroy_key_t *destroy_key, - hash_destroy_value_t *destroy_value, - T **hash); +result_t hash_create(const void *default_value, + int nbins, + hash_fn_t *fn, + hash_compare_t *compare, + hash_destroy_key_t *destroy_key, + hash_destroy_value_t *destroy_value, + T **hash); /** * Destroy a hash. diff --git a/include/datastruct/ntree.h b/include/datastruct/ntree.h index 609833a2..9e7d54af 100644 --- a/include/datastruct/ntree.h +++ b/include/datastruct/ntree.h @@ -77,11 +77,11 @@ typedef unsigned int ntree_walk_flags_t; typedef result_t (ntree_walk_fn_t)(T *t, void *opaque); /* max_depth of 0 means 'walk all', 1..N just walk level 1..N */ -result_t ntree_walk(T *t, - ntree_walk_flags_t flags, - int max_depth, - ntree_walk_fn_t *fn, - void *opaque); +result_t ntree_walk(T *t, + ntree_walk_flags_t flags, + int max_depth, + ntree_walk_fn_t *fn, + void *opaque); /* ----------------------------------------------------------------------- */ diff --git a/include/framebuf/screen.h b/include/framebuf/screen.h index 41988ede..65af70f0 100644 --- a/include/framebuf/screen.h +++ b/include/framebuf/screen.h @@ -75,9 +75,10 @@ void screen_draw_pixel(screen_t *scr, int x, int y, colour_t colour); * \param[in] colour Colour of rectangle. */ void screen_draw_rect(screen_t *scr, - int x, int y, - size2d_t size, - colour_t colour); + int x, + int y, + size2d_t size, + colour_t colour); /** * Special case of `screen_draw_rect`. @@ -156,8 +157,11 @@ int screen_copy_rect(screen_t *scr, * \param[in] colour Colour of line. */ void screen_draw_line(screen_t *scr, - int x0, int y0, int x1, int y1, - colour_t colour); + int x0, + int y0, + int x1, + int y1, + colour_t colour); /** * Draws a line (fixed-point Wu version with anti-aliasing). @@ -173,8 +177,11 @@ void screen_draw_line(screen_t *scr, * \param[in] colour Colour of line. */ void screen_draw_line_wu_fix8(screen_t *scr, - fix8_t x0, fix8_t y0, fix8_t x1, fix8_t y1, - colour_t colour); + fix8_t x0, + fix8_t y0, + fix8_t x1, + fix8_t y1, + colour_t colour); /** * Draws a line (floating point Wu version with anti-aliasing). @@ -190,7 +197,10 @@ void screen_draw_line_wu_fix8(screen_t *scr, * \param[in] colour Colour of rectangle. */ void screen_draw_line_wu_float(screen_t *scr, - float x0, float y0, float x1, float y1, - colour_t colour); + float x0, + float y0, + float x1, + float y1, + colour_t colour); #endif /* FRAMEBUF_SCREEN_H */ diff --git a/include/text/txtfmt.h b/include/text/txtfmt.h index ab520886..dc087c06 100644 --- a/include/text/txtfmt.h +++ b/include/text/txtfmt.h @@ -104,9 +104,9 @@ int txtfmt_get_wrapped_width(const txtfmt_t *tx); * \return result_OK or result_BAD_ARG if index is out of range. */ result_t txtfmt_get_line(const txtfmt_t *tx, - int index, - const char **line, - int *length); + int index, + const char **line, + int *length); /* ----------------------------------------------------------------------- */ diff --git a/include/utils/array.h b/include/utils/array.h index eb5072b7..26ae20df 100644 --- a/include/utils/array.h +++ b/include/utils/array.h @@ -92,12 +92,12 @@ void array_squeeze2(unsigned char *base, * * \return 0 - ok, 1 - out of memory */ -int array_grow(void **block, - size_t elemsize, - int used, - int *allocated, - int need, - int minimum); +int array_grow(void **block, + size_t elemsize, + int used, + int *allocated, + int need, + int minimum); /** * Shrink a dynamically allocated array to have no free entries. diff --git a/include/wuss/wuss.h b/include/wuss/wuss.h index 9752036e..1195de5d 100644 --- a/include/wuss/wuss.h +++ b/include/wuss/wuss.h @@ -216,12 +216,12 @@ wuss_config_t; * config's palette entries are out of range for the palette, or another * appropriate result code. */ -result_t wuss_create(screen_t *scr, - bmfont_t *font, - const colour_t *palette, - int npalette, - const wuss_config_t *config, - wuss_t **wuss); +result_t wuss_create(screen_t *scr, + bmfont_t *font, + const colour_t *palette, + int npalette, + const wuss_config_t *config, + wuss_t **wuss); /** * Destroy a window manager, and any windows still open on it. diff --git a/libraries/databases/pickle/test/pickle-test.c b/libraries/databases/pickle/test/pickle-test.c index a72a268b..59ab3981 100644 --- a/libraries/databases/pickle/test/pickle-test.c +++ b/libraries/databases/pickle/test/pickle-test.c @@ -40,7 +40,10 @@ static char *my_strdup(const char *s) /* ----------------------------------------------------------------------- */ -static result_t test1_format_key(const void *key, char *buf, size_t len, void *opaque) +static result_t test1_format_key(const void *key, + char *buf, + size_t len, + void *opaque) { NOT_USED(len); NOT_USED(opaque); @@ -50,7 +53,10 @@ static result_t test1_format_key(const void *key, char *buf, size_t len, void *o return result_OK; } -static result_t test1_format_value(const void *key, char *buf, size_t len, void *opaque) +static result_t test1_format_value(const void *key, + char *buf, + size_t len, + void *opaque) { NOT_USED(len); NOT_USED(opaque); @@ -320,7 +326,10 @@ cheese_value_t; /* ----------------------------------------------------------------------- */ -static result_t cheese_format_key(const void *vkey, char *buf, size_t len, void *opaque) +static result_t cheese_format_key(const void *vkey, + char *buf, + size_t len, + void *opaque) { const cheese_key_t *key = vkey; @@ -332,7 +341,10 @@ static result_t cheese_format_key(const void *vkey, char *buf, size_t len, void return result_OK; } -static result_t cheese_format_value(const void *vvalue, char *buf, size_t len, void *opaque) +static result_t cheese_format_value(const void *vvalue, + char *buf, + size_t len, + void *opaque) { const cheese_value_t *value = vvalue; diff --git a/libraries/datastruct/cache/cache.c b/libraries/datastruct/cache/cache.c index 77a08af3..1a8c1cc1 100644 --- a/libraries/datastruct/cache/cache.c +++ b/libraries/datastruct/cache/cache.c @@ -573,11 +573,11 @@ static entry_t *evict(cache_t *c) return evictee; } -result_t cache_put(cache_t *c, - cachekey_t key, - void *data, - size_t length, - void **inserted) +result_t cache_put(cache_t *c, + cachekey_t key, + void *data, + size_t length, + void **inserted) { const size_t quantum = sizeof(free_t); diff --git a/libraries/datastruct/hash/create.c b/libraries/datastruct/hash/create.c index f5db0315..70f23800 100644 --- a/libraries/datastruct/hash/create.c +++ b/libraries/datastruct/hash/create.c @@ -74,13 +74,13 @@ void hash_no_destroy_value(void *string) /* ----------------------------------------------------------------------- */ -result_t hash_create(const void *default_value, - int nbins, - hash_fn_t *fn, - hash_compare_t *compare, - hash_destroy_key_t *destroy_key, - hash_destroy_value_t *destroy_value, - hash_t **ph) +result_t hash_create(const void *default_value, + int nbins, + hash_fn_t *fn, + hash_compare_t *compare, + hash_destroy_key_t *destroy_key, + hash_destroy_value_t *destroy_value, + hash_t **ph) { hash_t *h; hash_node_t **bins; diff --git a/libraries/datastruct/ntree/test/ntree-test.c b/libraries/datastruct/ntree/test/ntree-test.c index 63279894..8a9c8950 100644 --- a/libraries/datastruct/ntree/test/ntree-test.c +++ b/libraries/datastruct/ntree/test/ntree-test.c @@ -119,10 +119,10 @@ static result_t concat(ntree_t *t, void *opaque) return result_OK; } -static result_t tree_to_string(ntree_t *t, - ntree_walk_flags_t flags, - int depth, - char *buf) +static result_t tree_to_string(ntree_t *t, + ntree_walk_flags_t flags, + int depth, + char *buf) { result_t err; concat_data_t concat_data; @@ -137,9 +137,9 @@ static result_t tree_to_string(ntree_t *t, return err; } -static result_t walk_test(ntree_t *t, - ntree_walk_flags_t flags, - const char *expected[]) +static result_t walk_test(ntree_t *t, + ntree_walk_flags_t flags, + const char *expected[]) { result_t err = result_OK; int i; diff --git a/libraries/framebuf/bmfont/bmfont.c b/libraries/framebuf/bmfont/bmfont.c index b01d6508..00d3b87c 100644 --- a/libraries/framebuf/bmfont/bmfont.c +++ b/libraries/framebuf/bmfont/bmfont.c @@ -93,11 +93,11 @@ static int count_adw(unsigned char tab[256], pixelfmt_any_t adw_px) } /** Verify the font format and build the advance width table. */ -static result_t extract_advance_widths(bmfont_t *bmfont, - void *voidpixels, - png_uint_32 imgwidth, - png_uint_32 imgheight, - size_t rowbytes) +static result_t extract_advance_widths(bmfont_t *bmfont, + void *voidpixels, + png_uint_32 imgwidth, + png_uint_32 imgheight, + size_t rowbytes) { result_t rc = result_OK; unsigned char adwtab[256]; @@ -198,11 +198,11 @@ static void build_repack_tab(unsigned char tab[256], int idx) ((((i >> 6) & 3) == idx) << 3); } -static result_t extract_glyphs(bmfont_t *bmfont, - void *voidpixels, - png_uint_32 imgwidth, - png_uint_32 imgheight, - size_t rowbytes) +static result_t extract_glyphs(bmfont_t *bmfont, + void *voidpixels, + png_uint_32 imgwidth, + png_uint_32 imgheight, + size_t rowbytes) { result_t rc = result_OK; unsigned char repacktab[256]; diff --git a/libraries/framebuf/screen/screen-copy-rect.c b/libraries/framebuf/screen/screen-copy-rect.c index 87bd4284..5ae5231e 100644 --- a/libraries/framebuf/screen/screen-copy-rect.c +++ b/libraries/framebuf/screen/screen-copy-rect.c @@ -13,8 +13,13 @@ * columns within each row by the same rule applied to "dx" -- the standard * two-axis blit-direction trick, so every pixel is read before anything * that could overwrite it is written. */ -static int screen_copy_rect_p4(screen_t *scr, const box_t *s, const box_t *d, - int width, int height, int dx, int dy) +static int screen_copy_rect_p4(screen_t *scr, + const box_t *s, + const box_t *d, + int width, + int height, + int dx, + int dy) { unsigned char *base; int rowbytes; @@ -57,7 +62,10 @@ static int screen_copy_rect_p4(screen_t *scr, const box_t *s, const box_t *d, return 1; } -int screen_copy_rect(screen_t *scr, const box_t *src, point_t dst, box_t *copied_dst) +int screen_copy_rect(screen_t *scr, + const box_t *src, + point_t dst, + box_t *copied_dst) { box_t clip_box, s, d, d_clipped; int dx, dy, width, height, bpp; diff --git a/libraries/framebuf/screen/screen-draw.c b/libraries/framebuf/screen/screen-draw.c index 2fc46547..26523abe 100644 --- a/libraries/framebuf/screen/screen-draw.c +++ b/libraries/framebuf/screen/screen-draw.c @@ -85,8 +85,10 @@ void screen_draw_pixel(screen_t *scr, int x, int y, colour_t colour) } static void screen_blend_pixel(screen_t *scr, - int x, int y, - colour_t colour, int alpha) + int x, + int y, + colour_t colour, + int alpha) { box_t clip; @@ -137,9 +139,10 @@ static void screen_blend_pixel(screen_t *scr, /* ----------------------------------------------------------------------- */ void screen_draw_rect(screen_t *scr, - int x, int y, - size2d_t size, - colour_t colour) + int x, + int y, + size2d_t size, + colour_t colour) { box_t clip_box; box_t rect_box; @@ -364,8 +367,11 @@ static void screen_get_bounds(const screen_t *scr, box_t *bounds) } void screen_draw_line(screen_t *scr, - int x0, int y0, int x1, int y1, - colour_t colour) + int x0, + int y0, + int x1, + int y1, + colour_t colour) { box_t clip_box; box_t bounds; @@ -428,8 +434,11 @@ void screen_draw_line(screen_t *scr, } void screen_draw_line_wu_fix8(screen_t *scr, - fix8_t x0_f8, fix8_t y0_f8, fix8_t x1_f8, fix8_t y1_f8, - colour_t colour) + fix8_t x0_f8, + fix8_t y0_f8, + fix8_t x1_f8, + fix8_t y1_f8, + colour_t colour) { box_t clip_box_f8; box_t bounds_f8; @@ -561,8 +570,11 @@ static int my_lroundf(float x) } void screen_draw_line_wu_float(screen_t *scr, - float fx0, float fy0, float fx1, float fy1, - colour_t colour) + float fx0, + float fy0, + float fx1, + float fy1, + colour_t colour) { box_t clip_box; box_t bounds; diff --git a/libraries/framebuf/screen/test/screen-test.c b/libraries/framebuf/screen/test/screen-test.c index a3ce2903..e8c0b4d7 100644 --- a/libraries/framebuf/screen/test/screen-test.c +++ b/libraries/framebuf/screen/test/screen-test.c @@ -92,8 +92,10 @@ static void testscreen_init(testscreen_t *ts) ts->pixels); } -static void draw(screen_t *scr, linekind_t kind, const linetest_t *line, - colour_t colour) +static void draw(screen_t *scr, + linekind_t kind, + const linetest_t *line, + colour_t colour) { switch (kind) { diff --git a/libraries/io/stream/stream-packbitscomp.c b/libraries/io/stream/stream-packbitscomp.c index e8dab061..a9630c43 100644 --- a/libraries/io/stream/stream-packbitscomp.c +++ b/libraries/io/stream/stream-packbitscomp.c @@ -44,7 +44,9 @@ typedef struct stream_packbitscomp } stream_packbitscomp_t; -static result_t stream_packbitscomp_op(stream_t *s, stream_opcode_t op, void *arg) +static result_t stream_packbitscomp_op(stream_t *s, + stream_opcode_t op, + void *arg) { NOT_USED(s); NOT_USED(op); diff --git a/libraries/text/txtfmt/get-line.c b/libraries/text/txtfmt/get-line.c index 90023946..4be628fb 100644 --- a/libraries/text/txtfmt/get-line.c +++ b/libraries/text/txtfmt/get-line.c @@ -5,9 +5,9 @@ #include "impl.h" result_t txtfmt_get_line(const txtfmt_t *tx, - int index, - const char **line, - int *length) + int index, + const char **line, + int *length) { if (index < 0 || index >= tx->nspans) return result_BAD_ARG; diff --git a/libraries/utils/array/grow.c b/libraries/utils/array/grow.c index 267f81d3..42952902 100644 --- a/libraries/utils/array/grow.c +++ b/libraries/utils/array/grow.c @@ -11,12 +11,12 @@ #include "utils/barith.h" /* used, need, minimum - specified as number of elements (not bytes) */ -int array_grow(void **pblock, - size_t elemsize, - int used, - int *pallocated, - int need, - int minimum) +int array_grow(void **pblock, + size_t elemsize, + int used, + int *pallocated, + int need, + int minimum) { int to_allocate; void *block; diff --git a/libraries/wuss/create.c b/libraries/wuss/create.c index 6b56de61..aa5226c0 100644 --- a/libraries/wuss/create.c +++ b/libraries/wuss/create.c @@ -13,12 +13,12 @@ #include "impl.h" -result_t wuss_create(screen_t *scr, - bmfont_t *font, - const colour_t *palette, - int npalette, - const wuss_config_t *config, - wuss_t **wuss) +result_t wuss_create(screen_t *scr, + bmfont_t *font, + const colour_t *palette, + int npalette, + const wuss_config_t *config, + wuss_t **wuss) { wuss_t *w; wuss_palette_t pal; diff --git a/libraries/wuss/furniture.h b/libraries/wuss/furniture.h index 1bac5c97..a8216208 100644 --- a/libraries/wuss/furniture.h +++ b/libraries/wuss/furniture.h @@ -81,7 +81,8 @@ void wuss__furniture_draw(wuss_t *wuss, wuss_window_t *window, const box_t *full); void wuss__furniture_invalidate(wuss_window_t *window); -void wuss__furniture_invalidate_for(wuss_window_t *window, const box_t *visible); +void wuss__furniture_invalidate_for(wuss_window_t *window, + const box_t *visible); /* geometry: titlebar icons */ void wuss__back_box(const wuss_window_t *window, box_t *out); diff --git a/libraries/wuss/impl.h b/libraries/wuss/impl.h index d5e5f139..373af5e9 100644 --- a/libraries/wuss/impl.h +++ b/libraries/wuss/impl.h @@ -162,7 +162,7 @@ static inline void wuss__min_content(const wuss_window_t *window, size2d_t *min) WUSS_MIN_CONTENT)); } -static inline int wuss__titlebar_height_for(const wuss_t *wuss, +static inline int wuss__titlebar_height_for(const wuss_t *wuss, wuss_window_flags_t flags) { return (flags & wuss_WINDOW_NO_TITLEBAR) ? 0 : wuss->titlebar_height; @@ -200,7 +200,8 @@ static inline int wuss__outline_px(const wuss_window_t *window) * none, so NO_TITLEBAR windows that still opt into scrollbars/resize match * their titled siblings instead of a hardcoded size; the hardcoded default * is only a last-resort floor if even that isn't positive */ -static inline int wuss__button_size_for(const wuss_t *wuss, wuss_window_flags_t flags) +static inline int wuss__button_size_for(const wuss_t *wuss, + wuss_window_flags_t flags) { int size; diff --git a/libraries/wuss/mouse-click.c b/libraries/wuss/mouse-click.c index 634f907d..66eb1048 100644 --- a/libraries/wuss/mouse-click.c +++ b/libraries/wuss/mouse-click.c @@ -2,11 +2,11 @@ #include "impl.h" -result_t wuss_mouse_click(wuss_t *wuss, - point_t p, - wuss_button_t button, - wuss_mouse_action_t action, - wuss_window_t **hit) +result_t wuss_mouse_click(wuss_t *wuss, + point_t p, + wuss_button_t button, + wuss_mouse_action_t action, + wuss_window_t **hit) { wuss_window_t *win; wuss_furniture_region_t region; diff --git a/libraries/wuss/test/tasks/ball.c b/libraries/wuss/test/tasks/ball.c index 4e0127ba..2ee7a424 100644 --- a/libraries/wuss/test/tasks/ball.c +++ b/libraries/wuss/test/tasks/ball.c @@ -177,7 +177,7 @@ static result_t ball_idle(void *task_data) return result_OK; } -result_t ball_handle(wuss_window_t *window, +result_t ball_handle(wuss_window_t *window, const wuss_event_t *event, void *task_data) { diff --git a/libraries/wuss/test/tasks/blank.c b/libraries/wuss/test/tasks/blank.c index 86d3365a..603bec2d 100644 --- a/libraries/wuss/test/tasks/blank.c +++ b/libraries/wuss/test/tasks/blank.c @@ -58,7 +58,7 @@ static result_t blank_idle(void *task_data) return rc; } -result_t blank_handle(wuss_window_t *window, +result_t blank_handle(wuss_window_t *window, const wuss_event_t *event, void *task_data) { diff --git a/libraries/wuss/test/tasks/checker.c b/libraries/wuss/test/tasks/checker.c index 71a77c9d..cecbbc48 100644 --- a/libraries/wuss/test/tasks/checker.c +++ b/libraries/wuss/test/tasks/checker.c @@ -122,7 +122,9 @@ static result_t checker_mouse(wuss_window_t *window, void *task_data) return result_OK; } -static result_t checker_scroll(wuss_window_t *window, int delta, void *task_data) +static result_t checker_scroll(wuss_window_t *window, + int delta, + void *task_data) { checker_task_t *cc; int *band; @@ -138,7 +140,7 @@ static result_t checker_scroll(wuss_window_t *window, int delta, void *task_data return result_OK; } -result_t checker_handle(wuss_window_t *window, +result_t checker_handle(wuss_window_t *window, const wuss_event_t *event, void *task_data) { diff --git a/libraries/wuss/test/tasks/curve.c b/libraries/wuss/test/tasks/curve.c index d7ae2971..09456f0c 100644 --- a/libraries/wuss/test/tasks/curve.c +++ b/libraries/wuss/test/tasks/curve.c @@ -104,11 +104,11 @@ static result_t curve_redraw(const wuss_event_t *event, curve_task_t *task) return result_OK; } -static result_t curve_mouse(curve_task_t *task, - wuss_mouse_action_t action, - int x, - int y, - wuss_window_t *window) +static result_t curve_mouse(curve_task_t *task, + wuss_mouse_action_t action, + int x, + int y, + wuss_window_t *window) { int i; diff --git a/libraries/wuss/test/tasks/image.c b/libraries/wuss/test/tasks/image.c index 70bd56da..c5546cbc 100644 --- a/libraries/wuss/test/tasks/image.c +++ b/libraries/wuss/test/tasks/image.c @@ -65,7 +65,7 @@ static result_t image_redraw(const wuss_event_t *event, void *task_data) return result_OK; } -result_t image_handle(wuss_window_t *window, +result_t image_handle(wuss_window_t *window, const wuss_event_t *event, void *task_data) { diff --git a/libraries/wuss/test/tasks/palette.c b/libraries/wuss/test/tasks/palette.c index 6aa16e7c..dfcc2eee 100644 --- a/libraries/wuss/test/tasks/palette.c +++ b/libraries/wuss/test/tasks/palette.c @@ -79,7 +79,7 @@ static result_t palette_redraw(const wuss_event_t *event, void *task_data) return result_OK; } -result_t palette_handle(wuss_window_t *window, +result_t palette_handle(wuss_window_t *window, const wuss_event_t *event, void *task_data) { diff --git a/libraries/wuss/test/tasks/porter-duff.c b/libraries/wuss/test/tasks/porter-duff.c index 8c6d1c91..14fc1dc4 100644 --- a/libraries/wuss/test/tasks/porter-duff.c +++ b/libraries/wuss/test/tasks/porter-duff.c @@ -129,7 +129,8 @@ static const char *const rule_names[composite_RULE__LIMIT] = "XOR" }; -static result_t load_demo_png(bitmap_t *bm, const char *resources, +static result_t load_demo_png(bitmap_t *bm, + const char *resources, const char *leafname) { const char *leafname_ext; @@ -351,8 +352,9 @@ static result_t porter_duff_mouse(wuss_window_t *window, void *task_data) return result_OK; } -static result_t porter_duff_scroll(wuss_window_t *window, int delta, - void *task_data) +static result_t porter_duff_scroll(wuss_window_t *window, + int delta, + void *task_data) { porter_duff_task_t *pd; diff --git a/libraries/wuss/test/tasks/sofa.c b/libraries/wuss/test/tasks/sofa.c index e0294768..28cef250 100644 --- a/libraries/wuss/test/tasks/sofa.c +++ b/libraries/wuss/test/tasks/sofa.c @@ -494,7 +494,9 @@ static result_t sofa_redraw(const wuss_event_t *event, void *task_data) return result_OK; } -static result_t sofa_mouse(wuss_window_t *window, wuss_button_t button, void *task_data) +static result_t sofa_mouse(wuss_window_t *window, + wuss_button_t button, + void *task_data) { sofa_task_t *sc; @@ -553,7 +555,7 @@ static result_t sofa_idle(void *task_data) return result_OK; } -result_t sofa_handle(wuss_window_t *window, +result_t sofa_handle(wuss_window_t *window, const wuss_event_t *event, void *task_data) { diff --git a/libraries/wuss/test/tasks/text.c b/libraries/wuss/test/tasks/text.c index 1c5e8d0a..aa094435 100644 --- a/libraries/wuss/test/tasks/text.c +++ b/libraries/wuss/test/tasks/text.c @@ -160,7 +160,7 @@ static result_t text_idle(void *task_data) return rc; } -result_t text_handle(wuss_window_t *window, +result_t text_handle(wuss_window_t *window, const wuss_event_t *event, void *task_data) { diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index e8dcc6ec..52ea8d49 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -560,7 +560,7 @@ typedef struct test_task } test_task_t; -static result_t test_handle(wuss_window_t *window, +static result_t test_handle(wuss_window_t *window, const wuss_event_t *event, void *task_data) { diff --git a/libraries/wuss/window/create-placed.c b/libraries/wuss/window/create-placed.c index 59475c98..44f95ecb 100644 --- a/libraries/wuss/window/create-placed.c +++ b/libraries/wuss/window/create-placed.c @@ -15,12 +15,12 @@ * every edge, the titlebar on top, and the scrollbar/resize carve on the * right and bottom. Matches wuss_window_create's own visible-box maths so an * auto-placed slot ends up exactly the size the window will occupy. */ -static void footprint_pad(const wuss_t *wuss, - wuss_window_flags_t flags, - int *left, - int *top, - int *right, - int *bottom) +static void footprint_pad(const wuss_t *wuss, + wuss_window_flags_t flags, + int *left, + int *top, + int *right, + int *bottom) { int outline_px, titlebar_height; point_t carve; diff --git a/libraries/wuss/window/move.c b/libraries/wuss/window/move.c index 9acab623..ef2f8ace 100644 --- a/libraries/wuss/window/move.c +++ b/libraries/wuss/window/move.c @@ -25,8 +25,10 @@ static void translate_box(const box_t *box, int dx, int dy, box_t *out) * cycle, and rejecting those outright regressed plain corner-occlusion * drags into full fallback redraws. Only an actual cycle (i must precede j * and j must precede i) has no safe order and needs the fallback. */ -static int wuss__order_pieces(const box_t *clean, const box_t *dest, int n, - int *order) +static int wuss__order_pieces(const box_t *clean, + const box_t *dest, + int n, + int *order) { int adj[WUSS_MAX_INVALIDATE_PIECES][WUSS_MAX_INVALIDATE_PIECES]; int indeg[WUSS_MAX_INVALIDATE_PIECES]; diff --git a/tools/test_wrap_protos.py b/tools/test_wrap_protos.py new file mode 100644 index 00000000..abef8405 --- /dev/null +++ b/tools/test_wrap_protos.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python3 +"""assert-based self-check for wrap_protos.py""" +from wrap_protos import process, split_top_level + +W = 80 + + +def one(src): + out, changed = process(src, W) + return out if changed else None + + +def test_short_untouched(): + assert one("int f(int a, int b);\n") is None + + +def test_void_untouched(): + long = "static int " + "x" * 70 + "(void);\n" + assert one(long) is None + + +def test_basic_alignment(): + src = ("static int screen_copy_rect_p4(screen_t *scr, const box_t *s, " + "const box_t *d, int width, int height, int dx, int dy)\n") + assert one(src) == ( + "static int screen_copy_rect_p4(screen_t *scr,\n" + " const box_t *s,\n" + " const box_t *d,\n" + " int width,\n" + " int height,\n" + " int dx,\n" + " int dy)\n" + ) + + +def test_idempotent(): + src = ("static int screen_copy_rect_p4(screen_t *scr, const box_t *s, " + "const box_t *d, int width, int height, int dx, int dy)\n") + first = one(src) + assert process(first, W)[1] is False, "second pass should be a no-op" + + +def test_rewrap_bad_alignment(): + # joined form is >80, and it arrives already (badly) wrapped + src = ( + "static int screen_copy_rectangle_p4(screen_t *scr,\n" + " const box_t *s, const box_t *d,\n" + " int width, int height, int dx, int dy)\n" + ) + assert one(src) == ( + "static int screen_copy_rectangle_p4(screen_t *scr,\n" + " const box_t *s,\n" + " const box_t *d,\n" + " int width,\n" + " int height,\n" + " int dx,\n" + " int dy)\n" + ) + + +def test_short_multiline_untouched(): + # already multi-line but joins to <80 -- leave the hand grouping alone + src = ( + "void draw_rect(screen_t *scr,\n" + " int x, int y,\n" + " colour_t colour);\n" + ) + assert one(src) is None + + +def test_semicolon_tail_kept(): + src = ("extern void some_really_long_function_name_here(int first_argument, " + "int second_argument, int third_arg);\n") + got = one(src) + assert got.rstrip().endswith(");") + assert got.count("\n") == 3 + + +def test_brace_tail_spaced(): + src = ("static long another_long_one_that_is_over_the_limit(char *buffer, " + "unsigned long length, int flags) {\n") + assert one(src).rstrip().endswith(") {") + + +def test_func_pointer_param_aligned(): + src = ("int register_a_callback_with_a_longish_name(void *ctx, " + "int (*cb)(void *, int), unsigned long some_flags)\n") + got = one(src) + assert got == ( + "int register_a_callback_with_a_longish_name(void *ctx,\n" + " int (*cb)(void *, int),\n" + " unsigned long some_flags)\n" + ), got + + +def test_unnamed_params_aligned(): + src = ("int a_function_with_unnamed_parameters_that_is_long(const char *, " + "unsigned long, int);\n") + got = one(src) + assert got == ( + "int a_function_with_unnamed_parameters_that_is_long(const char *,\n" + " unsigned long,\n" + " int);\n" + ), got + + +def test_split_top_level_respects_nesting(): + assert split_top_level("int a, void (*f)(int, int), char *b") == \ + ["int a", "void (*f)(int, int)", "char *b"] + + +def test_variadic_bails(): + src = ("int a_printf_like_function_with_a_long_name(const char *fmt, " + "int count, ...)\n") + assert one(src) is None + + +def test_call_statement_not_touched(): + src = (" array_squeeze(v->base, v->used, v->width, width_argument_here); " + "// a call, way over 80 columns of course yes indeed\n") + assert one(src) is None + + +def test_bare_call_not_touched(): + src = (" some_function_call_that_is_quite_long(argument_one, argument_two, " + "argument_three, argument_four);\n") + assert one(src) is None + + +def test_if_statement_not_touched(): + src = (" if (some_long_condition_function(a, b) && another_condition(c, d) " + "&& yet_another_one(e))\n") + assert one(src) is None + + +def test_method_call_not_touched(): + src = (" obj->do_something_with_a_really_long_method_name(first_arg, " + "second_arg, third_arg, fourth);\n") + assert one(src) is None + + +def test_array_param_aligned(): + src = ("static result_t walk_test(ntree_t *t, ntree_walk_flags_t flags, " + "const char *expected[])\n{\n") + got = one(src) + assert got == ( + "static result_t walk_test(ntree_t *t,\n" + " ntree_walk_flags_t flags,\n" + " const char *expected[])\n" + "{\n" + ), got + # declarators all begin at the same column + lines = got.splitlines() + assert lines[0].index('*t') + 1 == lines[1].rindex('flags') \ + == lines[2].index('*expected') + 1 + + +def test_return_statement_not_touched(): + src = (" return atom_set(db->tags, db->counts[tag].index, " + "(const unsigned char *) name, strlen((char *) name) + 1);\n") + assert one(src) is None + + +def test_sizeof_call_not_touched(): + src = (" return some_allocator_function_with_a_long_name(sizeof(*p), " + "count_of_things, alignment_bytes);\n") + assert one(src) is None + + +def test_comment_line_not_touched(): + src = ( + "/** Mark it dirty. Shorthand for\n" + " * some_function_with_a_long_name(window, NULL, 0, extra_argument). */\n" + "#define M(w) some_function_with_a_long_name((w), NULL, 0, 0)\n" + ) + assert one(src) is None + + +def test_definition_still_wraps(): + src = ("result_t vector_set_width_with_a_longer_name(vector_t *v, " + "size_t width, int flags, int more)\n{\n") + got = one(src) + assert got is not None and got.splitlines()[0].endswith("(vector_t *v,") + + +if __name__ == '__main__': + for name, fn in sorted(globals().items()): + if name.startswith('test_'): + fn() + print(f'ok {name}') + print('all passed') diff --git a/tools/wrap_protos.py b/tools/wrap_protos.py new file mode 100644 index 00000000..03191596 --- /dev/null +++ b/tools/wrap_protos.py @@ -0,0 +1,328 @@ +#!/usr/bin/env python3 +"""wrap_protos.py -- rewrap C function prototypes/definitions that exceed a +column limit onto one parameter per line, with the parameter names aligned +in a column (the style already used across DPTLib, e.g. screen-copy-rect.c): + + static int screen_copy_rect_p4(screen_t *scr, + const box_t *s, + int width) + +Scope: + - Rewraps a header/definition whose parameter list ends in ")" then an + optional ";" or "{". Handles both a single overlong physical line and an + already-wrapped multi-line prototype (the list is joined, then re-split + and realigned) -- so re-running is idempotent and fixes bad alignment. + - Splits the parameter list on top-level commas only, so function-pointer + params and (T)(args) casts survive the split. + - Aligns the declarator (name, or the "(*name)" of a function pointer) + into one column; "*"s are pulled against the name. Unnamed params align + on their trailing token. void / empty / variadic-only lists are left + as-is (nothing to align). + - Only rewrites when the original spans >1 line OR exceeds the width. +""" +import argparse +import re +import sys + +# Match " (" ... ")" , across newlines. Non-greedy head stops +# at the first "(", the args group is everything up to the final ")". +# +# The head is deliberately strict -- " (" with +# nothing else -- so a bare call statement like "foo(a, b);" or "p->fn(x)" +# never matches. It still needs the is_prototype_head() checks below (>=2 +# space-separated words, a plausible return type, real param syntax). +DECL_RE = re.compile( + r'^(?P[ \t]*)' + r'(?P[A-Za-z_]\w*(?:[ \t]+(?:[A-Za-z_]\w*|\*+))*[ \t]*' + r'\*?[ \t]*[A-Za-z_]\w*\()' + r'(?P.*?)' + r'\)[ \t]*(?P[;{]?)[ \t]*$', + re.DOTALL) +# statement-start guard: the previous non-blank line must end like this for the +# candidate to be at the start of a statement (not mid-expression / mid-call). +STMT_END_RE = re.compile(r'(?:[;{}]|\*/|\)|,|^\s*#.*|^\s*/[/*].*)\s*$') +QUALIFIERS = {'static', 'extern', 'inline', '_Noreturn', 'auto', 'register', + '__inline', '__inline__', '__forceinline'} +# C keywords that can't appear in a function decl/def head -- their presence +# means the line is a statement (return/if/while/...), not a prototype. +STMT_KW = {'return', 'if', 'else', 'while', 'for', 'do', 'switch', 'case', + 'default', 'goto', 'break', 'continue', 'sizeof', 'typedef', + 'typeof', '__typeof__', 'defined'} +IDENT_TAIL_RE = re.compile(r'([A-Za-z_]\w*)\s*$') +# C type keywords -- a param made only of these (+ * and whitespace) is unnamed +TYPE_KW = {'void', 'char', 'short', 'int', 'long', 'float', 'double', + 'signed', 'unsigned', 'const', 'volatile', 'struct', 'union', + 'enum', '_Bool', 'size_t', 'ssize_t', 'ptrdiff_t', 'wchar_t'} +# function pointer / array-of-fn-pointer param: "TYPE (*name)(...)" / "(*name[])" +FNPTR_RE = re.compile(r'^(?P.*?)\(\s*\*\s*(?P\w*)\s*(?P(?:\[\s*\w*\s*\])*)\)\s*(?P\(.*\))\s*$', re.DOTALL) + + +def is_prototype_head(indent, head, args): + """Reject anything that isn't a real top-level function decl/def head. + + Guards against call statements ("foo(a,b);", "obj->method(x)"), macro + invocations, and control-flow ("if (...)"). Conservative: a false + negative just means a line is left unwrapped.""" + h = head[:-1].strip() # drop trailing "(" + if any(c in h for c in '->.=[]"\'+-/%<>&|!~?:'): + return False + words = h.split() + if len(words) < 2: # need at least " " + return False + name = words[-1].lstrip('*') + if not name.isidentifier() or name in QUALIFIERS: + return False + # every leading word is a type-ish token (keyword, identifier, or stars) + # and none is a statement keyword + for w in words: + bare = w.strip('*') + if bare in STMT_KW: + return False + for w in words[:-1]: + if w.strip('*') and not (w.strip('*').isidentifier()): + return False + # args must look like a parameter list: empty, "void", or comma-separated + # items that each contain a type token (an identifier or a "*"). + a = args.strip() + if a in ('', 'void'): + return True + for part in split_top_level(a): + p = part.strip() + if not p: + return False + if p == '...': + continue + if not re.search(r'[A-Za-z_]', p): # no type at all -> not a param + return False + if p[0] in '"\'' or '=' in p: # string arg / default -> a call + return False + return True + + +def split_top_level(args): + """Split on commas not nested in () or [].""" + parts, depth, start = [], 0, 0 + for i, c in enumerate(args): + if c in '([': + depth += 1 + elif c in ')]': + depth -= 1 + elif c == ',' and depth == 0: + parts.append(args[start:i]) + start = i + 1 + parts.append(args[start:]) + return [' '.join(p.split()) for p in parts] + + +def parse_param(param): + """Return (lhs, stars, decl) so that "lhs" + pad + stars + decl reflows the + param with "decl" (the name, or "(*name)(...)") in an aligned column. + + Returns None for void / "..." (nothing meaningful to align).""" + p = param.strip() + if p in ('', 'void', '...'): + return None + + m = FNPTR_RE.match(p) + if m: + ret = m.group('ret').rstrip() + stars = '' + while ret.endswith('*'): + stars += '*' + ret = ret[:-1].rstrip() + decl = f"(*{m.group('name')}{m.group('arr')}){m.group('rest')}" + return ret, stars, decl + + if '(' in p: # some other parenthesised form -- leave whole + return p, '', '' + + # peel a trailing array suffix ("[]", "[N]", "[N][M]") off the declarator + arr = '' + mt = re.search(r'((?:\[[^\]]*\])+)\s*$', p) + if mt: + arr = mt.group(1) + p = p[:mt.start()].rstrip() + + bare = {w for w in p.replace('*', ' ').split()} + m = IDENT_TAIL_RE.search(p) + if m and p[:m.start()].strip() and not bare <= TYPE_KW: # "TYPE name" + head = p[:m.start()].rstrip() + name = m.group(1) + else: # unnamed: "TYPE" / "TYPE *" + head, name = p, '' + stars = '' + while head.endswith('*'): + stars += '*' + head = head[:-1].rstrip() + return head, stars, name + arr + + +def reflow_parts(indent, head, args, tail): + params = split_top_level(args.strip()) + if len(params) == 1 and params[0].strip() in ('', 'void'): + return None + + parsed = [parse_param(p) for p in params] + if any(p is None for p in parsed): # void mixed in, or "..." -- bail + return None + + # DPTLib style (see screen-copy-rect.c, cache.c): the "*" sits immediately + # before the declarator, declarators line up in one column, and a "**" + # param's extra star hangs one place to the left. So the name column is + # one past the longest "type + space + stars". + name_col = max(len(l) + 1 + len(s) for l, s, _ in parsed) + cont = ' ' * len(indent + head) + close = ') ' + tail if tail == '{' else ')' + tail + + out = [] + for i, (lhs, stars, decl) in enumerate(parsed): + # right-align "stars + decl" so decl starts at name_col + tail_txt = stars + decl + piece = (lhs + ' ' * (name_col - len(stars) - len(lhs)) + tail_txt).rstrip() + prefix = (indent + head) if i == 0 else cont + sep = ',' if i < len(parsed) - 1 else close + out.append(prefix + piece + sep) + return '\n'.join(out) + '\n' + + +def _balanced(s): + """True if () and [] are balanced and never go negative in s.""" + depth = 0 + for c in s: + if c in '([': + depth += 1 + elif c in ')]': + depth -= 1 + if depth < 0: + return False + return depth == 0 + + +def _strip_comments(s): + """Blank out /* ... */ and // ... content so scanning ignores it. + Assumes s starts outside a comment.""" + out = [] + k = 0 + while k < len(s): + if s[k:k+2] == '//': + break + if s[k:k+2] == '/*': + end = s.find('*/', k + 2) + if end < 0: + return ''.join(out), True # unterminated -> rest is comment + out.append(' ') + out.append(' ' * (end + 2 - k - 2)) + k = end + 2 + continue + out.append(s[k]) + k += 1 + return ''.join(out), False + + +def process(text, width): + out = [] + changed = False + lines = text.splitlines(keepends=True) + prev_nonblank = '' # last source line already emitted + in_comment = False + i = 0 + while i < len(lines): + if in_comment: + out.append(lines[i]) + if '*/' in lines[i]: + in_comment = False + rest, cont = _strip_comments(lines[i].split('*/', 1)[1]) + in_comment = cont + i += 1 + continue + _, opened = _strip_comments(lines[i]) + if opened: # this line opens a block comment + out.append(lines[i]) + in_comment = True + i += 1 + continue + # a comment-continuation line (" * ...") never starts a prototype + if lines[i].lstrip().startswith('*'): + out.append(lines[i]) + i += 1 + continue + # Grow a candidate span up to the first line that closes the parens and + # ends in ) / ); / {. Stop early if a line ends in ; or { without + # balancing -- that means this was never a prototype. + j = i + span = '' + m = None + while j < len(lines) and j - i < 40: + span += lines[j] + body = span.rstrip() + if _balanced(body) and body.endswith((')', ');', ') {', '){')): + m = DECL_RE.match(body) + break + if body.endswith((';', '{', '}')) and _balanced(body): + break # closed, but not as a prototype + j += 1 + + ok = False + if m: + at_stmt_start = (prev_nonblank == '' + or STMT_END_RE.search(prev_nonblank)) + head_first = m.group('head').split()[0] + # a definition head may sit on its own line after "static\n" etc. + ok = (at_stmt_start + and is_prototype_head(m.group('indent'), m.group('head'), + m.group('args'))) + # reject macro-style ALL-CAPS "return type" + if head_first.isupper() and head_first not in TYPE_KW: + ok = False + + if ok: + one_line = (m.group('indent') + m.group('head') + + ', '.join(split_top_level(m.group('args').strip())) + + ')' + (' ' + m.group('tail') if m.group('tail') == '{' + else m.group('tail'))) + if len(one_line) > width: + new = reflow_parts(m.group('indent'), m.group('head'), + m.group('args'), m.group('tail')) + if new is not None and new != span: + out.append(new) + changed = True + prev_nonblank = new.rstrip('\n').rsplit('\n', 1)[-1] + i = j + 1 + continue + + out.append(lines[i]) + if lines[i].strip(): + prev_nonblank = lines[i].rstrip('\n') + i += 1 + return ''.join(out), changed + + +def main(): + ap = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument('files', nargs='+') + ap.add_argument('-w', '--width', type=int, default=80) + ap.add_argument('-n', '--dry-run', action='store_true', + help='report files that would change, write nothing') + args = ap.parse_args() + + rc = 0 + for path in args.files: + with open(path) as f: + text = f.read() + new, changed = process(text, args.width) + if not changed: + continue + if args.dry_run: + print(f'would rewrap: {path}') + rc = 1 + else: + with open(path, 'w') as f: + f.write(new) + print(f'rewrapped: {path}') + return rc + + +if __name__ == '__main__': + sys.exit(main()) From f679894c16191aed5048da80334a81c467c9122a Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 30 Aug 2026 15:34:54 +0100 Subject: [PATCH 26/44] feat(screen): add screen_draw_ninepatch for resizable 9-patch frames Draw a 3x3-grid source image into a destination box: corners at natural size, edges and centre tiled. Clipped to the destination and the screen clip, which is restored on return. Inherits pixel-format support and blending from screen_draw_bitmap. Exercised by a new screen test and by the wuss SDL image task, which now tiles resources/wuss/9tile.png behind the loaded PNG. Co-Authored-By: Claude Sonnet 5 --- CMakeLists.txt | 1 + include/framebuf/screen.h | 22 +++ .../framebuf/screen/screen-draw-ninepatch.c | 164 ++++++++++++++++++ libraries/framebuf/screen/test/screen-test.c | 139 ++++++++++++++- libraries/wuss/test/tasks/image.c | 19 ++ libraries/wuss/test/tasks/image.h | 7 +- libraries/wuss/test/wuss-test.c | 11 +- resources/wuss/9tile.png | Bin 0 -> 222 bytes 8 files changed, 357 insertions(+), 6 deletions(-) create mode 100644 libraries/framebuf/screen/screen-draw-ninepatch.c create mode 100644 resources/wuss/9tile.png diff --git a/CMakeLists.txt b/CMakeLists.txt index 84556e01..d81e15a5 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -235,6 +235,7 @@ set(FRAMEBUF_SOURCES libraries/framebuf/screen/screen.c libraries/framebuf/screen/screen-copy-rect.c libraries/framebuf/screen/screen-draw.c + libraries/framebuf/screen/screen-draw-ninepatch.c libraries/framebuf/span-registry/get.c libraries/framebuf/span-registry/regdata.h libraries/framebuf/span/all8888.c diff --git a/include/framebuf/screen.h b/include/framebuf/screen.h index 65af70f0..39a4f358 100644 --- a/include/framebuf/screen.h +++ b/include/framebuf/screen.h @@ -109,6 +109,28 @@ void screen_draw_square(screen_t *scr, */ void screen_draw_bitmap(screen_t *scr, int x, int y, const bitmap_t *src); +/** + * Draws a "9-patch": a resizable frame built from a source image that is a 3x3 + * grid of equal cells. The source width and height must each be a positive + * multiple of 3; the cell size is a third of each. Given a destination box, the + * four corner cells are drawn at their natural size in the destination corners, + * the four edge cells are tiled along the destination edges, and the centre + * cell is tiled across the interior. + * + * If the destination is narrower or shorter than two cells the opposing corners + * overlap and each is clipped to its own half; the edges and centre are then + * omitted. Drawing is clipped to both the destination box and the screen's clip + * region, which is restored on return. Cells are blended exactly as + * `screen_draw_bitmap` does. No scaling is performed. + * + * \param[in] scr Screen to draw upon. + * \param[in] dst Destination box to fill with the frame. + * \param[in] src Source image, a 3x3 grid of cells. + */ +void screen_draw_ninepatch(screen_t *scr, + const box_t *dst, + const bitmap_t *src); + /** * Copies a rectangular region of the screen to another position on the same * screen (e.g. sliding an already-rendered window's pixels to a new position diff --git a/libraries/framebuf/screen/screen-draw-ninepatch.c b/libraries/framebuf/screen/screen-draw-ninepatch.c new file mode 100644 index 00000000..fcefabfa --- /dev/null +++ b/libraries/framebuf/screen/screen-draw-ninepatch.c @@ -0,0 +1,164 @@ +/* screen-draw-ninepatch.c -- 9-patch bitmap drawing */ + +#include + +#include "geom/box.h" +#include "geom/size.h" + +#include "framebuf/bitmap.h" +#include "framebuf/pixelfmt.h" +#include "framebuf/screen.h" + +/* ----------------------------------------------------------------------- */ + +/* Build a bitmap_t view onto one cell (col,row) of a 3x3 grid within "src". + * The view shares "src"'s rowbytes, so screen_draw_bitmap walks the parent + * image correctly despite the narrower size. */ +static void ninepatch_cell(bitmap_t *cell, + const bitmap_t *src, + int col, + int row, + int pw, + int ph) +{ + int bpp; + + bpp = 1 << (pixelfmt_log2bpp(src->format) - 3); + + *cell = *src; + cell->size = SIZE2D(pw, ph); + cell->base = (unsigned char *) src->base + + row * ph * src->rowbytes + + col * pw * bpp; +} + +/* Tile "cell" across "area" (a screen-space box), with the screen clip set to + * the intersection of "area" and "saved". The draw origin starts at + * (area->x0, area->y0) and steps by (stepx, stepy); a zero step means a single + * row or column. Overhang past "area" is removed by the clip. */ +static void tile_area(screen_t *scr, + const box_t *saved, + const box_t *area, + const bitmap_t *cell, + int stepx, + int stepy) +{ + box_t clip; + int oy; + + if (box_is_empty(saved)) + clip = *area; + else if (box_intersection(saved, area, &clip)) + return; /* nothing visible */ + + if (box_is_empty(&clip)) + return; + + scr->clip = clip; + + for (oy = area->y0; oy < area->y1; oy += (stepy > 0) ? stepy : (area->y1 - oy)) + { + int ox; + + for (ox = area->x0; ox < area->x1; ox += (stepx > 0) ? stepx : (area->x1 - ox)) + screen_draw_bitmap(scr, ox, oy, cell); + } +} + +/* ----------------------------------------------------------------------- */ + +void screen_draw_ninepatch(screen_t *scr, + const box_t *dst, + const bitmap_t *src) +{ + box_t saved; + box_t orig_clip; + int pw, ph; + int lx, rx, ty, by; + bitmap_t cell; + + assert(src->size.w > 0 && src->size.w % 3 == 0); + assert(src->size.h > 0 && src->size.h % 3 == 0); + + if (box_is_empty(dst)) + return; + + pw = src->size.w / 3; + ph = src->size.h / 3; + + /* Corner column/row boundaries in the destination. When "dst" is narrower + * or shorter than two patches the near and far corners overlap; the clip in + * tile_area trims each to its own half. */ + lx = dst->x0 + pw; + rx = dst->x1 - pw; + ty = dst->y0 + ph; + by = dst->y1 - ph; + + /* Fold "dst" into the saved clip once, so every tile_area call is bounded by + * the destination rectangle as well as the caller's clip. An empty caller + * clip means "no clipping", so in that case the bound is "dst" alone. */ + orig_clip = scr->clip; + if (box_is_empty(&orig_clip)) + saved = *dst; + else if (box_intersection(&orig_clip, dst, &saved)) + return; /* dst entirely outside the clip */ + + /* Corners. */ + { + box_t b; + + ninepatch_cell(&cell, src, 0, 0, pw, ph); + b.x0 = dst->x0; b.y0 = dst->y0; b.x1 = lx; b.y1 = ty; + tile_area(scr, &saved, &b, &cell, 0, 0); + + ninepatch_cell(&cell, src, 2, 0, pw, ph); + b.x0 = rx; b.y0 = dst->y0; b.x1 = dst->x1; b.y1 = ty; + tile_area(scr, &saved, &b, &cell, 0, 0); + + ninepatch_cell(&cell, src, 0, 2, pw, ph); + b.x0 = dst->x0; b.y0 = by; b.x1 = lx; b.y1 = dst->y1; + tile_area(scr, &saved, &b, &cell, 0, 0); + + ninepatch_cell(&cell, src, 2, 2, pw, ph); + b.x0 = rx; b.y0 = by; b.x1 = dst->x1; b.y1 = dst->y1; + tile_area(scr, &saved, &b, &cell, 0, 0); + } + + /* Edges. */ + if (rx > lx) + { + box_t b; + + ninepatch_cell(&cell, src, 1, 0, pw, ph); + b.x0 = lx; b.y0 = dst->y0; b.x1 = rx; b.y1 = ty; + tile_area(scr, &saved, &b, &cell, pw, 0); + + ninepatch_cell(&cell, src, 1, 2, pw, ph); + b.x0 = lx; b.y0 = by; b.x1 = rx; b.y1 = dst->y1; + tile_area(scr, &saved, &b, &cell, pw, 0); + } + if (by > ty) + { + box_t b; + + ninepatch_cell(&cell, src, 0, 1, pw, ph); + b.x0 = dst->x0; b.y0 = ty; b.x1 = lx; b.y1 = by; + tile_area(scr, &saved, &b, &cell, 0, ph); + + ninepatch_cell(&cell, src, 2, 1, pw, ph); + b.x0 = rx; b.y0 = ty; b.x1 = dst->x1; b.y1 = by; + tile_area(scr, &saved, &b, &cell, 0, ph); + } + + /* Centre. */ + if (rx > lx && by > ty) + { + box_t b; + + ninepatch_cell(&cell, src, 1, 1, pw, ph); + b.x0 = lx; b.y0 = ty; b.x1 = rx; b.y1 = by; + tile_area(scr, &saved, &b, &cell, pw, ph); + } + + scr->clip = orig_clip; +} diff --git a/libraries/framebuf/screen/test/screen-test.c b/libraries/framebuf/screen/test/screen-test.c index e8c0b4d7..92e459e7 100644 --- a/libraries/framebuf/screen/test/screen-test.c +++ b/libraries/framebuf/screen/test/screen-test.c @@ -5,6 +5,7 @@ #include "base/result.h" #include "base/utils.h" +#include "framebuf/bitmap.h" #include "framebuf/colour.h" #include "framebuf/pixelfmt.h" #include "framebuf/screen.h" @@ -240,6 +241,141 @@ static result_t test_wu_fix8_extreme_coords(void) /* ----------------------------------------------------------------------- */ +/* 9x9 source: each 3x3 cell a distinct solid colour, indexed [row][col]. */ +#define NP_SRC 9 +#define NP_CELL 3 + +static const int np_rgb[3][3][3] = +{ + { { 255, 0, 0 }, { 255, 255, 0 }, { 0, 255, 0 } }, + { { 0, 255, 255 }, { 128, 128, 128 }, { 0, 0, 255 } }, + { { 255, 0, 255 }, { 255, 255, 255 }, { 64, 64, 64 } } +}; + +/* Encode an rgb colour to a screen pixel the same way the draw path does. */ +static pixelfmt_bgrx8888_t np_encode(testscreen_t *ts, int r, int g, int b) +{ + testscreen_init(ts); + screen_draw_pixel(&ts->scr, 0, 0, colour_rgb(r, g, b)); + return ts->pixels[0]; +} + +static void np_make_src(bitmap_t *src, pixelfmt_rgba8888_t *buf) +{ + int cx, cy, x, y; + + for (cy = 0; cy < 3; cy++) + for (cx = 0; cx < 3; cx++) + { + colour_t c; + + c = colour_rgb(np_rgb[cy][cx][0], np_rgb[cy][cx][1], np_rgb[cy][cx][2]); + + for (y = 0; y < NP_CELL; y++) + for (x = 0; x < NP_CELL; x++) + buf[(cy * NP_CELL + y) * NP_SRC + (cx * NP_CELL + x)] = c.primary; + } + + bitmap_init(src, SIZE2D(NP_SRC, NP_SRC), pixelfmt_rgba8888, + NP_SRC * (int) sizeof(buf[0]), NULL, buf); +} + +static int np_at(testscreen_t *ts, int x, int y) +{ + return (int) ts->pixels[y * WIDTH + x]; +} + +static result_t test_ninepatch(void) +{ + static testscreen_t ts; + static testscreen_t enc; + static pixelfmt_rgba8888_t srcbuf[NP_SRC * NP_SRC]; + + bitmap_t src; + box_t dst = { 5, 5, 45, 45 }; + int exp[3][3]; + int cx, cy; + + np_make_src(&src, srcbuf); + + for (cy = 0; cy < 3; cy++) + for (cx = 0; cx < 3; cx++) + exp[cy][cx] = (int) np_encode(&enc, + np_rgb[cy][cx][0], + np_rgb[cy][cx][1], + np_rgb[cy][cx][2]); + + /* Normal case. */ + testscreen_init(&ts); + screen_draw_ninepatch(&ts.scr, &dst, &src); + + /* Corners: the 3x3 block at each destination corner is that corner colour. */ + if (np_at(&ts, 5, 5) != exp[0][0] || np_at(&ts, 7, 7) != exp[0][0] || + np_at(&ts, 44, 5) != exp[0][2] || np_at(&ts, 42, 7) != exp[0][2] || + np_at(&ts, 5, 44) != exp[2][0] || np_at(&ts, 7, 42) != exp[2][0] || + np_at(&ts, 44, 44) != exp[2][2] || np_at(&ts, 42, 42) != exp[2][2]) + { + printf("screen: ninepatch corner mismatch\n"); + return result_TEST_FAILED; + } + + /* Mid-edge and interior. */ + if (np_at(&ts, 25, 6) != exp[0][1] || /* top edge */ + np_at(&ts, 25, 43) != exp[2][1] || /* bottom edge */ + np_at(&ts, 6, 25) != exp[1][0] || /* left edge */ + np_at(&ts, 43, 25) != exp[1][2] || /* right edge */ + np_at(&ts, 25, 25) != exp[1][1]) /* centre */ + { + printf("screen: ninepatch edge/centre mismatch\n"); + return result_TEST_FAILED; + } + + /* Clipping: a pixel just outside dst stays background. */ + if (np_at(&ts, 4, 4) != (int) (pixelfmt_bgrx8888_t) BACKGROUND || + np_at(&ts, 45, 45) != (int) (pixelfmt_bgrx8888_t) BACKGROUND) + { + printf("screen: ninepatch drew outside dst\n"); + return result_TEST_FAILED; + } + + /* Clip composition: restrict to the left half, the right half is untouched. */ + testscreen_init(&ts); + ts.scr.clip = (box_t) { 0, 0, 25, 64 }; + screen_draw_ninepatch(&ts.scr, &dst, &src); + if (np_at(&ts, 6, 25) != exp[1][0] || + np_at(&ts, 30, 25) != (int) (pixelfmt_bgrx8888_t) BACKGROUND) + { + printf("screen: ninepatch ignored the screen clip\n"); + return result_TEST_FAILED; + } + /* The clip is restored on return. */ + if (!box_is_empty(&ts.scr.clip) && + (ts.scr.clip.x0 != 0 || ts.scr.clip.x1 != 25)) + { + printf("screen: ninepatch did not restore the clip\n"); + return result_TEST_FAILED; + } + + /* Degenerate: dst exactly two cells each way -> only corners, no centre. */ + testscreen_init(&ts); + { + box_t small = { 10, 10, 10 + 2 * NP_CELL, 10 + 2 * NP_CELL }; + + screen_draw_ninepatch(&ts.scr, &small, &src); + if (np_at(&ts, 10, 10) != exp[0][0] || + np_at(&ts, 15, 15) != exp[2][2] || + np_at(&ts, 12, 12) == exp[1][1]) /* centre colour must NOT appear */ + { + printf("screen: ninepatch degenerate case wrong\n"); + return result_TEST_FAILED; + } + } + + return result_TEST_PASSED; +} + +/* ----------------------------------------------------------------------- */ + result_t screen_test(const char *resources) { typedef result_t (*screentestfn)(void); @@ -248,7 +384,8 @@ result_t screen_test(const char *resources) { test_clip_invariance, test_clipping_still_happens, - test_wu_fix8_extreme_coords + test_wu_fix8_extreme_coords, + test_ninepatch }; result_t rc; diff --git a/libraries/wuss/test/tasks/image.c b/libraries/wuss/test/tasks/image.c index c5546cbc..0df46c5d 100644 --- a/libraries/wuss/test/tasks/image.c +++ b/libraries/wuss/test/tasks/image.c @@ -19,6 +19,7 @@ result_t image_create(wuss_t *wuss, const colour_t *palette, const char *path, + const char *background_path, image_task_t *task) { wuss_task_t delegate; @@ -29,6 +30,13 @@ result_t image_create(wuss_t *wuss, if (rc != result_OK) return rc; + rc = bitmap_load_png(&task->ninepatch, background_path); + if (rc != result_OK) + { + free(task->bitmap.base); + return rc; + } + delegate = wuss_task_start(image_handle, task); /* shows through the image's transparent pixels */ sz.w = task->bitmap.size.w + BORDER * 2; @@ -60,6 +68,16 @@ static result_t image_redraw(const wuss_event_t *event, void *task_data) sx = event->data.redraw.scroll.x; sy = event->data.redraw.scroll.y; + { + box_t behind; + + behind.x0 = bounds->x0 - sx + BORDER - 9; + behind.y0 = bounds->y0 - sy + BORDER - 9; + behind.x1 = behind.x0 + ic->bitmap.size.w + 9 * 2; + behind.y1 = behind.y0 + ic->bitmap.size.h + 9 * 2; + screen_draw_ninepatch(scr, &behind, &ic->ninepatch); + } + screen_draw_bitmap(scr, bounds->x0 - sx + BORDER, bounds->y0 - sy + BORDER, &ic->bitmap); return result_OK; @@ -81,6 +99,7 @@ result_t image_handle(wuss_window_t *window, case wuss_EVENT_CLOSE: wuss_window_close(window); free(ic->bitmap.base); + free(ic->ninepatch.base); free(ic); /* task_data was calloc'd per instance by the spawner */ return result_OK; diff --git a/libraries/wuss/test/tasks/image.h b/libraries/wuss/test/tasks/image.h index 3f767b4c..f62eb928 100644 --- a/libraries/wuss/test/tasks/image.h +++ b/libraries/wuss/test/tasks/image.h @@ -14,16 +14,19 @@ typedef struct image_task { wuss_window_t *window; - bitmap_t bitmap; /* owned: base freed by the caller when done */ + bitmap_t bitmap; /* owned: base freed by the caller when done */ + bitmap_t ninepatch; /* owned: 9-patch tiled behind the main image */ } image_task_t; wuss_event_fn_t image_handle; -/* load the PNG at path and create its window against the given wuss instance */ +/* load the PNG at path (and the 9-patch PNG at background_path, drawn tiled + * behind it) and create its window against the given wuss instance */ result_t image_create(wuss_t *wuss, const colour_t *palette, const char *path, + const char *background_path, image_task_t *task); diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 52ea8d49..307d9929 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -118,15 +118,20 @@ static result_t spawn_image(void) image_task_t *t; const char *leafname; const char *filename; + char buf[DPTLIB_MAXPATH]; + const char *ninepatch; result_t rc; t = calloc(1, sizeof(*t)); if (t == NULL) return result_OOM; - leafname = path_join_leafname("jessica", "png"); - filename = path_join_filename(g_resources, 3, "resources", "images", leafname); + leafname = path_join_leafname("jessica", "png"); + filename = path_join_filename(g_resources, 3, "resources", "images", leafname); + strcpy(buf, filename); + ninepatch = path_join_filename(g_resources, 3, "resources", "wuss", + path_join_leafname("9tile", "png")); - rc = image_create(g_wuss, g_palette, filename, t); + rc = image_create(g_wuss, g_palette, buf, ninepatch, t); if (rc != result_OK || t->window == NULL) { free(t); return rc; } return result_OK; } diff --git a/resources/wuss/9tile.png b/resources/wuss/9tile.png new file mode 100644 index 0000000000000000000000000000000000000000..304e2af151478de591561302e36be15078355983 GIT binary patch literal 222 zcmeAS@N?(olHy`uVBq!ia0vp^(jd&i3?z4Pv7`ej#^NA%Cx&(BWL^R}1_3@Hu0UE= zJGf3Yn4vvzb&y$nxc{NU$NztP@t?u>|6+#!Z`2u{m}##4-&{85fBB04XI8yiY*ho) z6XWUP7@{%p?j&nI1_ci1$cQKZzMHyux%jZVThH9b@-u`jCBSNueyh~w*w&pBO(%OT z_!RHJsI^xoed9Ith>-QhJ3MDDJeYHu#UCI5-d*ydqpXEL)dH%WXb>X;3spSWQ R>_HA@@O1TaS?83{1OQgiQB?o{ literal 0 HcmV?d00001 From 52bc62f45adadcebdecb136f521f6c16070b58a6 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 30 Aug 2026 15:36:18 +0100 Subject: [PATCH 27/44] docs: update CLAUDE.md build/test/style guidance Co-Authored-By: Claude Sonnet 5 --- CLAUDE.md | 48 +++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 39 insertions(+), 9 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 340b704f..26c28807 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,26 +8,49 @@ DPTLib is a platform-independent C99 library (base, databases, datastruct, frame ## Build -Useful CMake options: -- `BUILD_TESTS=YES` — build the `DPTLibTest` self-test executable. -- `BUILD_SDL_TESTS=YES` — additionally build tests needing SDL2/SDL2_image. -- `USE_FORTIFY=YES` — link the bundled Fortify memory-debugging library. -- `DPTLIB_IMAGES_READ_ONLY=YES` — build libpng without write support. +Build directories are per-config and already created; there is **no** plain +`build/` — never invoke `./build/DPTLibTest` or `cmake --build build`. + +- `build-asan/` — **default**. Debug, core tests (`BUILD_TESTS=YES`). Use this + unless told otherwise. Despite the name its cache currently has + `USE_ASAN=OFF`; re-run cmake with `-DUSE_ASAN=YES` if you actually need the + sanitisers. +- `build-sdl/` — Release, SDL tests on (`BUILD_SDL_TESTS=ON`). Needed for the + interactive `wuss` driver and anything under `libraries/wuss/test/tasks/`. +- `build-nosdl/` — Release, core tests only. +- `build-riscos/`— GCCSDK cross build. Leave alone unless working on RISC OS. +- `build.xc/` — Xcode generator. + +CMake options: `BUILD_TESTS`, `BUILD_SDL_TESTS`, `USE_ASAN` (ASan + UBSan), +`USE_FORTIFY` (bundled Fortify), `DPTLIB_IMAGES_READ_ONLY` (libpng no write). + +(Re)configure a dir only if its `CMakeCache.txt` is missing or you are changing +options: +``` +cmake -B build-asan -G Ninja -DBUILD_TESTS=YES +``` + +Build: +``` +cmake --build build-asan --target DPTLibTest +``` Requires libpng (`find_package(PNG)` on non-RISC OS). On RISC OS the build fetches and patches zlib/libpng itself via `FetchContent` (see `cmake/*.patch`). ## Testing -Run all tests (needs `-resources` pointing at the repo root, for test fixture files): +Run from the **repo root** so `-resources .` resolves the fixture files: ``` -./build/DPTLibTest -resources /path/to/DPTLib +./build-asan/DPTLibTest -resources . ``` -Run a subset by naming tests (names come from the `tests[]` table in `apps/test/main.c`, e.g. `atom`, `bitvec`, `curve`, `pickle`, `stream`, `packer`): +Run a subset by naming tests (names come from the `tests[]` table in `apps/test/main.c`, e.g. `atom`, `bitvec`, `curve`, `pickle`, `stream`, `packer`, `wuss`): ``` -./build/DPTLibTest -resources /path/to/DPTLib atom bitvec +./build-asan/DPTLibTest -resources . atom bitvec ``` +SDL / interactive tests use the `build-sdl` binary instead. + Success prints `++ Tests completed in Ns: N of N tests passed.` ### Adding a new test @@ -62,7 +85,14 @@ New source/header files must be added by hand to the relevant `set(..._SOURCES . - File header comment format: `/* filename.c -- one-line description */`. - Section breaks within files use `/* ----- ... ----- */` rule comments. - Public API docs use Doxygen (`\file`, `\param`, `\return`); a `Doxyfile` exists for generating them. +- Edit `.c`/`.h` files with the Edit tool, never `sed -i` line-range splices — they corrupt the Allman/2-space layout and can't be verified without re-reading. To inspect exact bytes or indentation, Read the file; don't shell out to `cat -A`/`cat -v`. +- After editing any `.c`/`.h` function prototype or definition, run `python3 tools/wrap_protos.py `; after editing a header's Doxygen, run `python3 tools/wrap_doxygen.py ` (skip vendored headers). ## Commit messages Use [Conventional Commits](https://www.conventionalcommits.org/): `[optional scope]: `, e.g. `fix(pickle): handle zero-length blobs`. Common types: `feat`, `fix`, `docs`, `refactor`, `test`, `chore`, `ci`, `build`. Add a `!` before the colon (or a `BREAKING CHANGE:` footer) for breaking changes. + +When the user says "commit": stage the relevant files and `git commit` with the +message passed via repeated `-m` flags (subject, then body). Do **not** write a +`COMMIT_MSG` / `COMMIT_MSG_TMP` file. Never `git push` unless explicitly asked — +pushing prompts for an SSH key passphrase and will hang. From 7a0d2f6cf415038973d67b447694df047f0cdb24 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 30 Aug 2026 15:49:30 +0100 Subject: [PATCH 28/44] feat(screen): add screen_NINEPATCH_NO_CENTRE flag screen_draw_ninepatch gains a flags argument; passing screen_NINEPATCH_NO_CENTRE draws only the border and leaves the interior untouched. Existing callers pass 0. Co-Authored-By: Claude Sonnet 5 --- include/framebuf/screen.h | 18 +++++++++++---- .../framebuf/screen/screen-draw-ninepatch.c | 5 +++-- libraries/framebuf/screen/test/screen-test.c | 17 +++++++++++--- libraries/wuss/test/tasks/image.c | 22 ++++++++++--------- 4 files changed, 43 insertions(+), 19 deletions(-) diff --git a/include/framebuf/screen.h b/include/framebuf/screen.h index 39a4f358..da0b5325 100644 --- a/include/framebuf/screen.h +++ b/include/framebuf/screen.h @@ -109,6 +109,12 @@ void screen_draw_square(screen_t *scr, */ void screen_draw_bitmap(screen_t *scr, int x, int y, const bitmap_t *src); +/** Flags for `screen_draw_ninepatch`. */ +enum +{ + screen_NINEPATCH_NO_CENTRE = 1u << 0 /**< Leave the interior untouched. */ +}; + /** * Draws a "9-patch": a resizable frame built from a source image that is a 3x3 * grid of equal cells. The source width and height must each be a positive @@ -123,13 +129,17 @@ void screen_draw_bitmap(screen_t *scr, int x, int y, const bitmap_t *src); * region, which is restored on return. Cells are blended exactly as * `screen_draw_bitmap` does. No scaling is performed. * - * \param[in] scr Screen to draw upon. - * \param[in] dst Destination box to fill with the frame. - * \param[in] src Source image, a 3x3 grid of cells. + * \param[in] scr Screen to draw upon. + * \param[in] dst Destination box to fill with the frame. + * \param[in] src Source image, a 3x3 grid of cells. + * \param[in] flags Bitwise OR of `screen_NINEPATCH_*`, or 0. Pass + * `screen_NINEPATCH_NO_CENTRE` to draw only the border and + * leave the interior untouched. */ void screen_draw_ninepatch(screen_t *scr, const box_t *dst, - const bitmap_t *src); + const bitmap_t *src, + unsigned int flags); /** * Copies a rectangular region of the screen to another position on the same diff --git a/libraries/framebuf/screen/screen-draw-ninepatch.c b/libraries/framebuf/screen/screen-draw-ninepatch.c index fcefabfa..96ba082f 100644 --- a/libraries/framebuf/screen/screen-draw-ninepatch.c +++ b/libraries/framebuf/screen/screen-draw-ninepatch.c @@ -69,7 +69,8 @@ static void tile_area(screen_t *scr, void screen_draw_ninepatch(screen_t *scr, const box_t *dst, - const bitmap_t *src) + const bitmap_t *src, + unsigned int flags) { box_t saved; box_t orig_clip; @@ -151,7 +152,7 @@ void screen_draw_ninepatch(screen_t *scr, } /* Centre. */ - if (rx > lx && by > ty) + if (rx > lx && by > ty && !(flags & screen_NINEPATCH_NO_CENTRE)) { box_t b; diff --git a/libraries/framebuf/screen/test/screen-test.c b/libraries/framebuf/screen/test/screen-test.c index 92e459e7..dc8c3fd4 100644 --- a/libraries/framebuf/screen/test/screen-test.c +++ b/libraries/framebuf/screen/test/screen-test.c @@ -307,7 +307,7 @@ static result_t test_ninepatch(void) /* Normal case. */ testscreen_init(&ts); - screen_draw_ninepatch(&ts.scr, &dst, &src); + screen_draw_ninepatch(&ts.scr, &dst, &src, 0); /* Corners: the 3x3 block at each destination corner is that corner colour. */ if (np_at(&ts, 5, 5) != exp[0][0] || np_at(&ts, 7, 7) != exp[0][0] || @@ -341,7 +341,7 @@ static result_t test_ninepatch(void) /* Clip composition: restrict to the left half, the right half is untouched. */ testscreen_init(&ts); ts.scr.clip = (box_t) { 0, 0, 25, 64 }; - screen_draw_ninepatch(&ts.scr, &dst, &src); + screen_draw_ninepatch(&ts.scr, &dst, &src, 0); if (np_at(&ts, 6, 25) != exp[1][0] || np_at(&ts, 30, 25) != (int) (pixelfmt_bgrx8888_t) BACKGROUND) { @@ -361,7 +361,7 @@ static result_t test_ninepatch(void) { box_t small = { 10, 10, 10 + 2 * NP_CELL, 10 + 2 * NP_CELL }; - screen_draw_ninepatch(&ts.scr, &small, &src); + screen_draw_ninepatch(&ts.scr, &small, &src, 0); if (np_at(&ts, 10, 10) != exp[0][0] || np_at(&ts, 15, 15) != exp[2][2] || np_at(&ts, 12, 12) == exp[1][1]) /* centre colour must NOT appear */ @@ -371,6 +371,17 @@ static result_t test_ninepatch(void) } } + /* NO_CENTRE: border drawn, interior stays background. */ + testscreen_init(&ts); + screen_draw_ninepatch(&ts.scr, &dst, &src, screen_NINEPATCH_NO_CENTRE); + if (np_at(&ts, 5, 5) != exp[0][0] || /* corner still drawn */ + np_at(&ts, 25, 6) != exp[0][1] || /* edge still drawn */ + np_at(&ts, 25, 25) != (int) (pixelfmt_bgrx8888_t) BACKGROUND) /* centre skipped */ + { + printf("screen: ninepatch NO_CENTRE wrong\n"); + return result_TEST_FAILED; + } + return result_TEST_PASSED; } diff --git a/libraries/wuss/test/tasks/image.c b/libraries/wuss/test/tasks/image.c index 0df46c5d..943836e1 100644 --- a/libraries/wuss/test/tasks/image.c +++ b/libraries/wuss/test/tasks/image.c @@ -60,6 +60,8 @@ static result_t image_redraw(const wuss_event_t *event, void *task_data) screen_t *scr; const box_t *bounds; int sx, sy; + int bx, by; + box_t behind; ic = task_data; @@ -67,18 +69,18 @@ static result_t image_redraw(const wuss_event_t *event, void *task_data) bounds = event->data.redraw.bounds; sx = event->data.redraw.scroll.x; sy = event->data.redraw.scroll.y; + bx = bounds->x0 - sx + BORDER; + by = bounds->y0 - sy + BORDER; - { - box_t behind; - - behind.x0 = bounds->x0 - sx + BORDER - 9; - behind.y0 = bounds->y0 - sy + BORDER - 9; - behind.x1 = behind.x0 + ic->bitmap.size.w + 9 * 2; - behind.y1 = behind.y0 + ic->bitmap.size.h + 9 * 2; - screen_draw_ninepatch(scr, &behind, &ic->ninepatch); - } +#define NINEPATCHSZ 9 + + behind.x0 = bx - NINEPATCHSZ; + behind.y0 = by - NINEPATCHSZ; + behind.x1 = behind.x0 + ic->bitmap.size.w + NINEPATCHSZ * 2; + behind.y1 = behind.y0 + ic->bitmap.size.h + NINEPATCHSZ * 2; + screen_draw_ninepatch(scr, &behind, &ic->ninepatch, 0); - screen_draw_bitmap(scr, bounds->x0 - sx + BORDER, bounds->y0 - sy + BORDER, &ic->bitmap); + screen_draw_bitmap(scr, bx, by, &ic->bitmap); return result_OK; } From 8a741b080bd078d344c1eec3427bc115fb1dce6c Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 30 Aug 2026 15:50:31 +0100 Subject: [PATCH 29/44] docs: start CHANGELOG.md Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..ce2e44f9 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,17 @@ +# Changelog + +All notable changes to DPTLib are recorded here. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). +This project does not yet publish versioned releases; entries are grouped under +_Unreleased_ until one is cut. + +## [Unreleased] + +### Added + +- `screen_draw_ninepatch()` — draws a resizable "9-patch" frame from a source + image that is a 3x3 grid of equal cells: corners at natural size, edges and + centre tiled, clipped to the destination box and the screen clip. +- `screen_NINEPATCH_NO_CENTRE` flag for `screen_draw_ninepatch()` to draw only + the border and leave the interior untouched. From 1d7d305dc9245e6bb6c9296b93a83e2d6a655e58 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 30 Aug 2026 17:46:29 +0100 Subject: [PATCH 30/44] fix(wuss): release held icon when a window opens over it A button icon pressed on MOUSE_DOWN stayed pressed if the client's ICON handler opened a window covering the icon's owner: the later MOUSE_UP resolved via wuss__window_at to the new window and never reached the pressed icon. Track the held icon on struct wuss as pressed_icon (mirroring furniture.dragging) and release it on any MOUSE_UP before the window hit-test. Also clear it on pointer-leave in mouse-move, and in wuss_window_close and wuss_icon_delete so it can't dangle. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/create.c | 1 + libraries/wuss/icon/delete.c | 3 +++ libraries/wuss/impl.h | 4 ++++ libraries/wuss/mouse-click.c | 21 +++++++++++++++++++-- libraries/wuss/mouse-move.c | 2 ++ libraries/wuss/window/close.c | 2 ++ 6 files changed, 31 insertions(+), 2 deletions(-) diff --git a/libraries/wuss/create.c b/libraries/wuss/create.c index aa5226c0..6eb6533f 100644 --- a/libraries/wuss/create.c +++ b/libraries/wuss/create.c @@ -140,6 +140,7 @@ result_t wuss_create(screen_t *scr, w->scr = scr; w->font = font; w->furniture.dragging = NULL; + w->pressed_icon = NULL; w->furniture.drag.x = 0; w->furniture.drag.y = 0; diff --git a/libraries/wuss/icon/delete.c b/libraries/wuss/icon/delete.c index d8cc7ee5..5f5932b7 100644 --- a/libraries/wuss/icon/delete.c +++ b/libraries/wuss/icon/delete.c @@ -18,6 +18,9 @@ void wuss_icon_delete(wuss_icon_t *icon) window = icon->window; + if (window->wuss->pressed_icon == icon) + window->wuss->pressed_icon = NULL; + wuss__icon_invalidate(icon); for (i = 0; i < window->nicons; i++) diff --git a/libraries/wuss/impl.h b/libraries/wuss/impl.h index 373af5e9..9d66b46a 100644 --- a/libraries/wuss/impl.h +++ b/libraries/wuss/impl.h @@ -65,6 +65,10 @@ struct wuss * created on first auto-placement */ point_t cascade; /* next cascade offset, used once the * layout packer has no room left */ + wuss_icon_t *pressed_icon; /* button icon held down, NULL when + * idle; released on any MOUSE_UP + * even if a new window now covers + * its owner */ }; struct wuss_window diff --git a/libraries/wuss/mouse-click.c b/libraries/wuss/mouse-click.c index 66eb1048..85e44374 100644 --- a/libraries/wuss/mouse-click.c +++ b/libraries/wuss/mouse-click.c @@ -16,6 +16,21 @@ result_t wuss_mouse_click(wuss_t *wuss, x = p.x; y = p.y; + /* Release a held button icon on any MOUSE_UP, before the hit-test picks a + * window: the up may land on a window that opened over the icon's owner on + * MOUSE_DOWN, so wuss__window_at would never reach the pressed icon. */ + if (action == wuss_MOUSE_UP && wuss->pressed_icon != NULL) + { + wuss_icon_t *pressed = wuss->pressed_icon; + + wuss->pressed_icon = NULL; + if (pressed->pressed) + { + pressed->pressed = 0; + wuss__icon_invalidate(pressed); + } + } + if (action == wuss_MOUSE_UP && wuss->furniture.dragging != NULL) { win = wuss->furniture.dragging; @@ -168,12 +183,14 @@ result_t wuss_mouse_click(wuss_t *wuss, if (action == wuss_MOUSE_DOWN && (button & (wuss_BUTTON_SELECT | wuss_BUTTON_ADJUST))) { - icon->pressed = 1; + icon->pressed = 1; + wuss->pressed_icon = icon; wuss__icon_invalidate(icon); } else if (action == wuss_MOUSE_UP && icon->pressed) { - icon->pressed = 0; + icon->pressed = 0; + wuss->pressed_icon = NULL; wuss__icon_invalidate(icon); } diff --git a/libraries/wuss/mouse-move.c b/libraries/wuss/mouse-move.c index b5e644e3..c56da51f 100644 --- a/libraries/wuss/mouse-move.c +++ b/libraries/wuss/mouse-move.c @@ -74,6 +74,8 @@ result_t wuss_mouse_move(wuss_t *wuss, point_t p, wuss_window_t **hit) if (it->pressed && it != icon) { it->pressed = 0; + if (wuss->pressed_icon == it) + wuss->pressed_icon = NULL; wuss__icon_invalidate(it); } } diff --git a/libraries/wuss/window/close.c b/libraries/wuss/window/close.c index 058743fa..8ff5fb9e 100644 --- a/libraries/wuss/window/close.c +++ b/libraries/wuss/window/close.c @@ -18,6 +18,8 @@ void wuss_window_close(wuss_window_t *doomed) wuss = doomed->wuss; if (wuss->furniture.dragging == doomed) wuss->furniture.dragging = NULL; + if (wuss->pressed_icon != NULL && wuss->pressed_icon->window == doomed) + wuss->pressed_icon = NULL; wuss__release_packed(doomed); From 74faa6572ca2526f31fdf7a4f4eac061902d55d1 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 30 Aug 2026 18:01:47 +0100 Subject: [PATCH 31/44] fix(wuss): don't repaint blitted pixels when a drag slides one clean piece onto another's vacated ground With an occluder biting a corner out of a window's pre-move footprint, the clean (non-occluded) pieces can overlap in destination: on a downward drag a full-width bottom band slides straight into the destination of the right-side band. The vacated-sliver invalidate only subtracted each piece's own destination, so that shared ground -- which the other piece's blit had already filled with valid pixels -- was invalidated and repainted across the full window width for nothing. Compute each sliver as clean[i] minus every clean piece's destination via wuss__subtract_boxes, not just full_dest[i]. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/window/move.c | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/libraries/wuss/window/move.c b/libraries/wuss/window/move.c index ef2f8ace..f9eeb611 100644 --- a/libraries/wuss/window/move.c +++ b/libraries/wuss/window/move.c @@ -181,9 +181,28 @@ void wuss_window_move(wuss_window_t *window, point_t p) * re-checking occlusion -- regardless of whether every pixel of that * new position actually got a blit above: the part that landed under an * occluder was skipped there, but the old position is vacated either - * way. */ + * way. + * + * The sliver is clean[i] minus *every* clean piece's destination, not + * just its own: with an occluder biting a corner out of "before", one + * clean piece can slide onto ground another clean piece just vacated + * (e.g. a full-width bottom band vacated straight into the destination + * of the right-side band on a downward drag). That overlap already got + * valid pixels from the other piece's blit, so invalidating it would + * just repaint good pixels. */ for (i = 0; i < nclean; i++) - wuss__invalidate_minus(window->wuss, &clean[i], &full_dest[i]); + { + box_t sliver[WUSS_MAX_INVALIDATE_PIECES]; + int nsliver, s; + + /* ponytail: nclean is a handful, so this can't approach the + * WUSS_MAX_INVALIDATE_PIECES cap; if that ever changes, a dropped + * piece here means a missed repaint (visible corruption), not just + * wasted work -- revisit then. */ + nsliver = wuss__subtract_boxes(&clean[i], full_dest, nclean, sliver); + for (s = 0; s < nsliver; s++) + wuss_invalidate(window->wuss, &sliver[s]); + } /* Whatever of "before" wasn't clean has no valid source pixels: its * translated destination needs a genuine repaint, clipped against From 277b4e878afef1e2662a708b3756c9ac63722cbb Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 30 Aug 2026 18:06:17 +0100 Subject: [PATCH 32/44] docs: fill in CHANGELOG for changes since 62a9193 Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce2e44f9..ff6845ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,8 +10,58 @@ _Unreleased_ until one is cut. ### Added +- `wuss/icon.h` — work-area icons: static labels and clickable bevelled + buttons drawn inside a window's content area. Icon boxes are in virtual + document space, so they scroll with the content. Wuss hit-tests buttons + before the content task sees a click and delivers them as `wuss_EVENT_ICON`; + labels and hidden or disabled icons fall through as `wuss_EVENT_MOUSE`. +- `wuss_icon_create_array()` — creates a batch of icons from a spec array with + all-or-nothing rollback: on the first failure any icons already created by + the call are destroyed and no handles are written. +- `wuss_window_create_placed()` — creates a window from a content size instead + of a box, letting Wuss pack it (furniture included) into the first free + screen region. Successive auto-placed windows tile; placement cascades when + no region fits. The slot is released on close and on the first + `wuss_window_move()` / `wuss_window_resize()`. An overall screen margin is + kept around all auto-placed windows. - `screen_draw_ninepatch()` — draws a resizable "9-patch" frame from a source image that is a 3x3 grid of equal cells: corners at natural size, edges and centre tiled, clipped to the destination box and the screen clip. - `screen_NINEPATCH_NO_CENTRE` flag for `screen_draw_ninepatch()` to draw only the border and leave the interior untouched. +- `packer_set_gutter()` — `packer_place_by()` now reserves a configurable + gutter strip along the box's two inner edges so located boxes are never + flush. Defaults to 0, leaving existing callers unchanged; the returned + position is still the un-inflated box. +- `packer_release()` — inverse of `packer_place_*`, returns an area to the + pool. Released areas are not coalesced. +- `POINT(x, y)` and `SIZE2D(w, h)` compound-literal macros in `geom/point.h` + and `geom/size.h`. + +### Changed + +- **Breaking:** `wuss_button_t` values are now flags (`wuss_BUTTON_SELECT` 4, + `wuss_BUTTON_MENU` 2, `wuss_BUTTON_ADJUST` 1, `wuss_BUTTON_NONE` 0) so + chords such as Select+Adjust can be reported. Client code comparing a + reported button for equality must now test with `&`. +- **Breaking:** `wuss_window_create()` takes a `min_doc` argument between + `doc` and `window`, the minimum content extent a resize-drag or toggle-size + will shrink to. It is clamped up to the built-in grab floor and down to + `doc`. Pass `(0, 0)` for the built-in floor. +- Adjust-clicking a scroll arrow now steps against the direction the arrow + points, so one arrow can be worked both ways without moving the pointer. + Toggle-size stays Select-only. + +### Fixed + +- `wuss_window_move()` no longer repaints already-blitted pixels when a drag + past an occluded corner slides one clean piece of the window onto ground + another clean piece just vacated. +- A work-area button held on mouse-down is now released if the click opens a + window that covers the button's owner, instead of staying stuck pressed. +- Resize-corner drag preserves where within the resize icon the mouse-down + landed, so the window's corner no longer jumps to the raw pointer position + on the first move. +- Removed signed-overflow and negative-shift undefined behaviour in the + anti-aliased fixed-point line rasteriser, reachable with long or off-screen + endpoints. From b860d831259411832f859159e8a5aaf73d8a1ac9 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 30 Aug 2026 18:11:39 +0100 Subject: [PATCH 33/44] fix(wuss): don't raise window when dragging a scrollbar well The resize icon and scrollbar wells shared a restack block, so grabbing a scroll well on Select popped the window to the front. Restrict the raise to FURNITURE_RESIZE. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/mouse-click.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libraries/wuss/mouse-click.c b/libraries/wuss/mouse-click.c index 85e44374..fadd05e3 100644 --- a/libraries/wuss/mouse-click.c +++ b/libraries/wuss/mouse-click.c @@ -141,7 +141,9 @@ result_t wuss_mouse_click(wuss_t *wuss, { point_t scroll; - if (button & wuss_BUTTON_SELECT) + /* Only resize raises the window; dragging a scrollbar well must not + * reorder the stack. */ + if ((button & wuss_BUTTON_SELECT) && region == wuss_FURNITURE_RESIZE) wuss_window_restack(win, wuss_ZORDER_FRONT); wuss_window_get_scroll(win, &scroll); From 834fa55ce93f06135142036d4fea99c086fe597b Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 30 Aug 2026 18:57:37 +0100 Subject: [PATCH 34/44] feat(wuss): make furniture and icon subsystems compile-time optional Two CMake options, WUSS_FURNITURE and WUSS_ICONS (both default ON, so current behaviour is unchanged), drop the respective furniture/*.c and icon/*.c files from the build and #ifdef-guard every core call site, struct field and inline helper that references them. With WUSS_FURNITURE off every window is chromeless: content box == visible box, no furniture drawn, hit-tested or dragged. wuss__scroll_clamp and wuss__scroll_step move to a new core file scroll-step.c so programmatic and wheel scrolling survive. With WUSS_ICONS off the wuss_icon_* API is not compiled and struct wuss/wuss_window lose their icon fields. The icon typedef moves to wuss.h (forward declaration) so wuss_event_t can name it regardless of the option; icon.h's body is wrapped in #ifdef WUSS_ICONS and dropped from PUBLIC_HEADERS when off. Tests stay ON-only: tasks/icons.c, tasks/launcher.c and the SDL interactive driver build only when both options are on. The core wuss_test is split - the existing body is gated on both options, with a compact core-only replacement exercising the chromeless window path (create, move, z-order, doc coordinates, scroll, invalidate, close) for the off configs. Co-Authored-By: Claude Sonnet 5 --- CMakeLists.txt | 93 ++++++--- include/wuss/icon.h | 11 +- include/wuss/wuss.h | 29 ++- libraries/wuss/create.c | 45 +++- libraries/wuss/destroy.c | 2 + libraries/wuss/furniture.h | 2 - libraries/wuss/furniture/scroll-action.c | 38 ---- libraries/wuss/impl.h | 87 ++++++++ libraries/wuss/mouse-click.c | 252 ++++++++++++----------- libraries/wuss/mouse-move.c | 55 ++--- libraries/wuss/redraw.c | 4 + libraries/wuss/scroll-step.c | 41 ++++ libraries/wuss/scroll.c | 17 +- libraries/wuss/test/wuss-test.c | 174 ++++++++++++++++ libraries/wuss/window/close.c | 6 + libraries/wuss/window/create-placed.c | 4 + libraries/wuss/window/create.c | 8 + libraries/wuss/window/resize.c | 2 + libraries/wuss/window/set-scroll.c | 2 + 19 files changed, 646 insertions(+), 226 deletions(-) create mode 100644 libraries/wuss/scroll-step.c diff --git a/CMakeLists.txt b/CMakeLists.txt index d81e15a5..0b62d49d 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -30,6 +30,8 @@ endif(CCACHE_FOUND) option(USE_FORTIFY "Use Fortify" OFF) option(USE_ASAN "Build with AddressSanitizer and UndefinedBehaviorSanitizer" OFF) option(DPTLIB_IMAGES_READ_ONLY "Remove libpng write support" OFF) +option(WUSS_FURNITURE "Build the wuss window-furniture subsystem" ON) +option(WUSS_ICONS "Build the wuss in-content icon subsystem" ON) # Referencing CMAKE_TOOLCHAIN_FILE avoids a warning on rebuilds. if(NOT ${CMAKE_TOOLCHAIN_FILE} STREQUAL "") @@ -112,11 +114,14 @@ set(PUBLIC_HEADERS include/utils/maths.h include/utils/pack.h include/utils/primes.h - include/wuss/icon.h include/wuss/task.h include/wuss/window.h include/wuss/wuss.h) +if(WUSS_ICONS) + list(APPEND PUBLIC_HEADERS include/wuss/icon.h) +endif() + # The public headers must be set as properties of the library, not as # target_sources. The quoting is essential. set_target_properties(DPTLib PROPERTIES PUBLIC_HEADER "${PUBLIC_HEADERS}") @@ -319,9 +324,35 @@ set(UTILS_SOURCES libraries/utils/pack/unpack.c libraries/utils/primes/primes.c) -set(WUSS_SOURCES +set(WUSS_CORE_SOURCES libraries/wuss/create.c libraries/wuss/destroy.c + libraries/wuss/get-font.c + libraries/wuss/idle.c + libraries/wuss/impl.h + libraries/wuss/invalidate.c + libraries/wuss/mouse-click.c + libraries/wuss/mouse-move.c + libraries/wuss/redraw.c + libraries/wuss/scroll.c + libraries/wuss/scroll-step.c + libraries/wuss/task/start.c + libraries/wuss/task/stop.c + libraries/wuss/window/at.c + libraries/wuss/window/create.c + libraries/wuss/window/create-placed.c + libraries/wuss/window/close.c + libraries/wuss/window/get-content-bounds.c + libraries/wuss/window/get-scroll.c + libraries/wuss/window/get-visible-bounds.c + libraries/wuss/window/invalidate.c + libraries/wuss/window/move.c + libraries/wuss/window/resize.c + libraries/wuss/window/restack.c + libraries/wuss/window/set-background.c + libraries/wuss/window/set-scroll.c) + +set(WUSS_FURNITURE_SOURCES libraries/wuss/furniture/back-box.c libraries/wuss/furniture/close-box.c libraries/wuss/furniture/content-box.c @@ -336,8 +367,9 @@ set(WUSS_SOURCES libraries/wuss/furniture/toggle-action.c libraries/wuss/furniture/toggle-box.c libraries/wuss/furniture/vscroll-box.c - libraries/wuss/furniture.h - libraries/wuss/get-font.c + libraries/wuss/furniture.h) + +set(WUSS_ICON_SOURCES libraries/wuss/icon/create.c libraries/wuss/icon/create-array.c libraries/wuss/icon/delete.c @@ -352,29 +384,15 @@ set(WUSS_SOURCES libraries/wuss/icon/screen-box.c libraries/wuss/icon/set-hidden.c libraries/wuss/icon/set-text.c - libraries/wuss/icon.h - libraries/wuss/idle.c - libraries/wuss/impl.h - libraries/wuss/invalidate.c - libraries/wuss/mouse-click.c - libraries/wuss/mouse-move.c - libraries/wuss/redraw.c - libraries/wuss/scroll.c - libraries/wuss/task/start.c - libraries/wuss/task/stop.c - libraries/wuss/window/at.c - libraries/wuss/window/create.c - libraries/wuss/window/create-placed.c - libraries/wuss/window/close.c - libraries/wuss/window/get-content-bounds.c - libraries/wuss/window/get-scroll.c - libraries/wuss/window/get-visible-bounds.c - libraries/wuss/window/invalidate.c - libraries/wuss/window/move.c - libraries/wuss/window/resize.c - libraries/wuss/window/restack.c - libraries/wuss/window/set-background.c - libraries/wuss/window/set-scroll.c) + libraries/wuss/icon.h) + +set(WUSS_SOURCES ${WUSS_CORE_SOURCES}) +if(WUSS_FURNITURE) + list(APPEND WUSS_SOURCES ${WUSS_FURNITURE_SOURCES}) +endif() +if(WUSS_ICONS) + list(APPEND WUSS_SOURCES ${WUSS_ICON_SOURCES}) +endif() set(ALL_SOURCES ${PUBLIC_HEADERS} @@ -426,6 +444,13 @@ if(DPTLIB_IMAGES_READ_ONLY) target_compile_definitions(DPTLib PRIVATE DPTLIB_IMAGES_READ_ONLY) endif() +if(WUSS_FURNITURE) + target_compile_definitions(DPTLib PUBLIC WUSS_FURNITURE) +endif() +if(WUSS_ICONS) + target_compile_definitions(DPTLib PUBLIC WUSS_ICONS) +endif() + if(NOT TARGET_RISCOS) set(CMAKE_FIND_FRAMEWORK NEVER) find_package(PNG REQUIRED MODULE) @@ -567,15 +592,21 @@ if(BUILD_TESTS) libraries/wuss/test/tasks/checker.c libraries/wuss/test/tasks/curve.c libraries/wuss/test/tasks/gradient.c - libraries/wuss/test/tasks/icons.c libraries/wuss/test/tasks/image.c - libraries/wuss/test/tasks/launcher.c libraries/wuss/test/tasks/palette.c libraries/wuss/test/tasks/porter-duff.c libraries/wuss/test/tasks/sofa.c libraries/wuss/test/tasks/text.c libraries/wuss/test/wuss-test.c) + # The icon-driven launcher and the icons task only build when both wuss + # subsystems are present; the SDL interactive driver needs them too. + if(WUSS_FURNITURE AND WUSS_ICONS) + list(APPEND TEST_SOURCES + libraries/wuss/test/tasks/icons.c + libraries/wuss/test/tasks/launcher.c) + endif() + source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" FILES ${TEST_SOURCES}) # Avoid a warning from CMake @@ -599,6 +630,10 @@ if(BUILD_TESTS) endif() if(BUILD_SDL_TESTS) + if(NOT (WUSS_FURNITURE AND WUSS_ICONS)) + message(FATAL_ERROR "BUILD_SDL_TESTS requires WUSS_FURNITURE and WUSS_ICONS") + endif() + find_package(SDL3 REQUIRED) find_package(SDL3_image REQUIRED) diff --git a/include/wuss/icon.h b/include/wuss/icon.h index ee931169..f09b5a0b 100644 --- a/include/wuss/icon.h +++ b/include/wuss/icon.h @@ -33,10 +33,15 @@ extern "C" #include "wuss/wuss.h" +/* The in-content icon subsystem is a compile-time option (CMake WUSS_ICONS). + * With it off the library has no wuss_icon_* symbols, so this header body is + * skipped. */ +#ifdef WUSS_ICONS + /* ----------------------------------------------------------------------- */ -/** An opaque work-area icon handle, owned by the window it is created on. */ -typedef struct wuss_icon wuss_icon_t; +/* wuss_icon_t (opaque, owned by the window it is created on) is forward-declared + * in wuss.h so wuss_event_t can name it regardless of this option. */ /** * What an icon looks like and how it behaves. The enum is left open so sprite @@ -190,6 +195,8 @@ const char *wuss_icon_get_text(const wuss_icon_t *icon); */ wuss_window_t *wuss_icon_get_window(const wuss_icon_t *icon); +#endif /* WUSS_ICONS */ + #ifdef __cplusplus } #endif diff --git a/include/wuss/wuss.h b/include/wuss/wuss.h index 1195de5d..de8d74bc 100644 --- a/include/wuss/wuss.h +++ b/include/wuss/wuss.h @@ -41,6 +41,10 @@ typedef struct wuss wuss_t; /** A window. Full API is in window.h. */ typedef struct wuss_window wuss_window_t; +/** A work-area icon. Full API is in icon.h, and is compiled only when the + * library is built with the WUSS_ICONS option on. */ +typedef struct wuss_icon wuss_icon_t; + /** * Mouse buttons, RISC OS-style: Select is the primary action, Adjust the * secondary action, Menu pops up a menu. @@ -80,7 +84,8 @@ typedef int wuss_colour_t; /** Furniture chrome colours, one entry per class of furniture. Title is * the only two-tone class (fill + text); the rest are drawn as a single - * flat colour. Each value is an index into the system palette (see + * flat colour. Ignored when the library is built with WUSS_FURNITURE off. + * Each value is an index into the system palette (see * wuss_create). */ typedef struct wuss_palette { @@ -104,7 +109,13 @@ typedef struct wuss_palette } wuss_palette_t; -/** Per-window appearance flags, combinable with bitwise OR. */ +/** + * Per-window appearance flags, combinable with bitwise OR. + * + * \note When the library is built with the WUSS_FURNITURE CMake option off, + * every window is chromeless regardless of these flags and the + * wuss_WINDOW_NO_* bits are ignored. + */ typedef enum wuss_window_flags { /** Default: every furniture region drawn. */ @@ -165,23 +176,29 @@ typedef enum wuss_zorder } wuss_zorder_t; -/** Optional creation-time configuration. */ +/** + * Optional creation-time configuration. + * + * \note titlebar_height and palette are ignored when the library is built with + * WUSS_FURNITURE off; bevel is ignored when built with both + * WUSS_FURNITURE and WUSS_ICONS off. backdrop is always honoured. + */ typedef struct wuss_config { /** * Titlebar height in pixels, or 0 to derive from font metrics (or a built-in - * fallback if no font). + * fallback if no font). Ignored when WUSS_FURNITURE is off. */ int titlebar_height; - /** Furniture chrome colours. */ + /** Furniture chrome colours. Ignored when WUSS_FURNITURE is off. */ wuss_palette_t palette; /** * Bevelled work-area button edge shades, as indices into the system palette: * light on the top/left edges, dark on the bottom/right (swapped when the * button is pressed). Both default to the titlebar fill colour when config is - * NULL. + * NULL. Ignored when both WUSS_FURNITURE and WUSS_ICONS are off. */ struct { diff --git a/libraries/wuss/create.c b/libraries/wuss/create.c index 6eb6533f..81a653fa 100644 --- a/libraries/wuss/create.c +++ b/libraries/wuss/create.c @@ -21,11 +21,17 @@ result_t wuss_create(screen_t *scr, wuss_t **wuss) { wuss_t *w; +#ifdef WUSS_FURNITURE wuss_palette_t pal; wuss_colour_t bg, fg; +#endif +#if defined(WUSS_FURNITURE) || defined(WUSS_ICONS) wuss_colour_t blight, bdark; +#endif +#ifdef WUSS_FURNITURE int font_height; int font_width; +#endif assert(scr != NULL); assert(wuss != NULL); @@ -63,6 +69,7 @@ result_t wuss_create(screen_t *scr, w->npalette = palette_PICO8__LENGTH; } +#ifdef WUSS_FURNITURE if (config != NULL) { pal = config->palette; @@ -136,13 +143,49 @@ result_t wuss_create(screen_t *scr, { w->titlebar_height = WUSS_DEFAULT_TITLEBAR_HEIGHT; } +#else /* !WUSS_FURNITURE */ + w->backdrop = (config != NULL) ? config->backdrop : wuss_NO_BACKGROUND; + if (w->backdrop != wuss_NO_BACKGROUND && + (w->backdrop < 0 || w->backdrop >= w->npalette)) + { + free(w->palette); + free(w); + return result_WUSS_BAD_COLOUR; + } + +#ifdef WUSS_ICONS + if (config != NULL) + { + blight = config->bevel.light; + bdark = config->bevel.dark; + } + else + { + blight = 0; + bdark = 0; + } + if (blight < 0 || blight >= w->npalette || + bdark < 0 || bdark >= w->npalette) + { + free(w->palette); + free(w); + return result_WUSS_BAD_COLOUR; + } + w->bevel_light = blight; + w->bevel_dark = bdark; +#endif +#endif /* WUSS_FURNITURE */ w->scr = scr; w->font = font; +#ifdef WUSS_FURNITURE w->furniture.dragging = NULL; - w->pressed_icon = NULL; w->furniture.drag.x = 0; w->furniture.drag.y = 0; +#endif +#ifdef WUSS_ICONS + w->pressed_icon = NULL; +#endif w->ndirty = 0; diff --git a/libraries/wuss/destroy.c b/libraries/wuss/destroy.c index ede75c1b..372fd517 100644 --- a/libraries/wuss/destroy.c +++ b/libraries/wuss/destroy.c @@ -21,7 +21,9 @@ void wuss_destroy(wuss_t *doomed) list_t *next; next = e->next; +#ifdef WUSS_ICONS wuss__icons_free((wuss_window_t *) e); +#endif free(e); e = next; } diff --git a/libraries/wuss/furniture.h b/libraries/wuss/furniture.h index a8216208..4b5b7bd8 100644 --- a/libraries/wuss/furniture.h +++ b/libraries/wuss/furniture.h @@ -105,8 +105,6 @@ int wuss__hscroll_well_px(const wuss_window_t *window); /* actions */ void wuss__furniture_toggle_size(wuss_window_t *window); -void wuss__furniture_scroll_step(wuss_window_t *window, point_t delta); -point_t wuss__scroll_clamp(const wuss_window_t *window, point_t desired); void wuss__furniture_drag_resize(wuss_window_t *window, point_t p); void wuss__furniture_drag_sausage(wuss_window_t *window, int delta_px, diff --git a/libraries/wuss/furniture/scroll-action.c b/libraries/wuss/furniture/scroll-action.c index 903b7648..6ac7a498 100644 --- a/libraries/wuss/furniture/scroll-action.c +++ b/libraries/wuss/furniture/scroll-action.c @@ -2,44 +2,6 @@ #include "../impl.h" -point_t wuss__scroll_clamp(const wuss_window_t *window, point_t desired) -{ - box_t content; - int max_x, max_y; - - wuss__content_box(window, &content); - - max_x = window->doc.w - (content.x1 - content.x0); - max_y = window->doc.h - (content.y1 - content.y0); - if (max_x < 0) - max_x = 0; - if (max_y < 0) - max_y = 0; - - if (desired.x < 0) - desired.x = 0; - else if (desired.x > max_x) - desired.x = max_x; - if (desired.y < 0) - desired.y = 0; - else if (desired.y > max_y) - desired.y = max_y; - - return desired; -} - -void wuss__furniture_scroll_step(wuss_window_t *window, point_t delta) -{ - point_t scroll; - - wuss_window_get_scroll(window, &scroll); - scroll.x += delta.x; - scroll.y += delta.y; - scroll = wuss__scroll_clamp(window, scroll); - - wuss_window_set_scroll(window, scroll); -} - void wuss__furniture_drag_sausage(wuss_window_t *window, int delta_px, int scroll_start, diff --git a/libraries/wuss/impl.h b/libraries/wuss/impl.h index 9d66b46a..fa8f52da 100644 --- a/libraries/wuss/impl.h +++ b/libraries/wuss/impl.h @@ -15,8 +15,12 @@ #include "wuss/wuss.h" #include "wuss/window.h" +#ifdef WUSS_FURNITURE #include "furniture.h" +#endif +#ifdef WUSS_ICONS #include "icon.h" +#endif #define WUSS_TITLE_MAX 63 #define WUSS_DEFAULT_TITLEBAR_HEIGHT 20 @@ -51,13 +55,21 @@ struct wuss bmfont_t *font; /* nullable, not owned */ colour_t *palette; /* owned */ int npalette; +#ifdef WUSS_FURNITURE wuss_palette_t furniture_colours; +#endif +#if defined(WUSS_FURNITURE) || defined(WUSS_ICONS) wuss_colour_t bevel_light; /* work-area button top/left edge */ wuss_colour_t bevel_dark; /* work-area button bottom/right edge */ +#endif wuss_colour_t backdrop; /* wuss_NO_BACKGROUND for none */ +#ifdef WUSS_FURNITURE int titlebar_height; +#endif list_t z_order; /* anchor; head = topmost window */ +#ifdef WUSS_FURNITURE struct wuss__furniture furniture; +#endif box_t dirty[WUSS_MAX_DIRTY]; /* accumulated by wuss_invalidate; reset by a redraw */ int ndirty; packer_t *layout; /* owned; occupied screen area for @@ -65,10 +77,12 @@ struct wuss * created on first auto-placement */ point_t cascade; /* next cascade offset, used once the * layout packer has no room left */ +#ifdef WUSS_ICONS wuss_icon_t *pressed_icon; /* button icon held down, NULL when * idle; released on any MOUSE_UP * even if a new window now covers * its owner */ +#endif }; struct wuss_window @@ -85,22 +99,43 @@ struct wuss_window size2d_t doc; /* virtual document extent, set at creation */ size2d_t min_doc; /* resize floor, set at creation; see * wuss__min_content */ +#ifdef WUSS_FURNITURE wuss_window_state_t state; /* see wuss_window_state_t */ +#endif box_t packed; /* region wuss_window_create_placed took * out of wuss->layout (footprint + gutter), * to give back on close/move; empty if not * auto-placed or already released */ +#ifdef WUSS_FURNITURE box_t pre_toggle; /* visible bounds to restore on the next toggle */ char title[WUSS_TITLE_MAX + 1]; +#endif +#ifdef WUSS_ICONS wuss_icon_t **icons; /* owned; array of owned icon pointers */ int nicons; int cap_icons; +#endif }; wuss_window_t *wuss__window_at(wuss_t *wuss, point_t p); + +/* clamp "desired" to the window's scrollable range; step the current offset + * by "delta" and apply it. Core (furniture-independent) -- used by the wheel + * and, when built, the scrollbar furniture. */ +point_t wuss__scroll_clamp(const wuss_window_t *window, point_t desired); +void wuss__scroll_step(wuss_window_t *window, point_t delta); + +#ifdef WUSS_FURNITURE void wuss__titlebar_box(const wuss_window_t *window, box_t *out); void wuss__close_box(const wuss_window_t *window, box_t *out); void wuss__content_box(const wuss_window_t *window, box_t *out); +#else +/* No furniture: the content area is the whole visible footprint. */ +static inline void wuss__content_box(const wuss_window_t *window, box_t *out) +{ + *out = window->visible; +} +#endif void wuss__invalidate_clipped(wuss_window_t *window, const box_t *box); void wuss__invalidate_minus(wuss_t *wuss, @@ -166,6 +201,7 @@ static inline void wuss__min_content(const wuss_window_t *window, size2d_t *min) WUSS_MIN_CONTENT)); } +#ifdef WUSS_FURNITURE static inline int wuss__titlebar_height_for(const wuss_t *wuss, wuss_window_flags_t flags) { @@ -249,5 +285,56 @@ static inline void wuss__furniture_carve_for(wuss_window_flags_t flags, if (carve->y > 0) carve->y += WUSS_DIVIDER_PX; } +#else /* !WUSS_FURNITURE */ +/* No furniture: every geometry helper collapses to "no chrome", so the core + * window create/move/resize maths still compiles and yields visible == + * content. */ +static inline int wuss__titlebar_height_for(const wuss_t *wuss, + wuss_window_flags_t flags) +{ + (void) wuss; (void) flags; + return 0; +} + +static inline int wuss__titlebar_height(const wuss_window_t *window) +{ + (void) window; + return 0; +} + +static inline int wuss__outline_px_for(wuss_window_flags_t flags) +{ + (void) flags; + return 0; +} + +static inline int wuss__outline_px(const wuss_window_t *window) +{ + (void) window; + return 0; +} + +static inline int wuss__button_size_for(const wuss_t *wuss, + wuss_window_flags_t flags) +{ + (void) wuss; (void) flags; + return 0; +} + +static inline int wuss__button_size(const wuss_window_t *window) +{ + (void) window; + return 0; +} + +static inline void wuss__furniture_carve_for(wuss_window_flags_t flags, + int button_size, + point_t *carve) +{ + (void) flags; (void) button_size; + carve->x = 0; + carve->y = 0; +} +#endif /* WUSS_FURNITURE */ #endif /* IMPL_H */ diff --git a/libraries/wuss/mouse-click.c b/libraries/wuss/mouse-click.c index fadd05e3..8028deb7 100644 --- a/libraries/wuss/mouse-click.c +++ b/libraries/wuss/mouse-click.c @@ -9,13 +9,13 @@ result_t wuss_mouse_click(wuss_t *wuss, wuss_window_t **hit) { wuss_window_t *win; - wuss_furniture_region_t region; wuss_event_t event; int x, y; x = p.x; y = p.y; +#ifdef WUSS_ICONS /* Release a held button icon on any MOUSE_UP, before the hit-test picks a * window: the up may land on a window that opened over the icon's owner on * MOUSE_DOWN, so wuss__window_at would never reach the pressed icon. */ @@ -30,7 +30,9 @@ result_t wuss_mouse_click(wuss_t *wuss, wuss__icon_invalidate(pressed); } } +#endif +#ifdef WUSS_FURNITURE if (action == wuss_MOUSE_UP && wuss->furniture.dragging != NULL) { win = wuss->furniture.dragging; @@ -41,6 +43,7 @@ result_t wuss_mouse_click(wuss_t *wuss, return result_OK; } +#endif win = wuss__window_at(wuss, p); if (hit != NULL) @@ -49,159 +52,170 @@ result_t wuss_mouse_click(wuss_t *wuss, if (win == NULL) return result_OK; - region = wuss__furniture_hit_test(win, POINT(x, y)); - - if (region == wuss_FURNITURE_CLOSE && - action == wuss_MOUSE_DOWN && - (button & wuss_BUTTON_SELECT)) +#ifdef WUSS_FURNITURE { - if (win->task.handle == NULL) - return result_OK; + wuss_furniture_region_t region; - event.kind = wuss_EVENT_CLOSE; - return win->task.handle(win, &event, win->task.task_data); - } + region = wuss__furniture_hit_test(win, POINT(x, y)); - if (region == wuss_FURNITURE_BACK && action == wuss_MOUSE_DOWN) - { - if (button & wuss_BUTTON_SELECT) - wuss_window_restack(win, wuss_ZORDER_BACK); - else if (button & wuss_BUTTON_ADJUST) - wuss_window_restack(win, wuss_ZORDER_FRONT); - return result_OK; - } - - if (region == wuss_FURNITURE_TOGGLE_SIZE || - region == wuss_FURNITURE_VSCROLL_UP || - region == wuss_FURNITURE_VSCROLL_DOWN || - region == wuss_FURNITURE_HSCROLL_LEFT || - region == wuss_FURNITURE_HSCROLL_RIGHT) - { - if (action == wuss_MOUSE_DOWN && - (button & (wuss_BUTTON_SELECT | wuss_BUTTON_ADJUST))) + if (region == wuss_FURNITURE_CLOSE && + action == wuss_MOUSE_DOWN && + (button & wuss_BUTTON_SELECT)) { - /* Adjust-clicking a scroll arrow steps the opposite way to the arrow it - * points, so one arrow can be worked in both directions without moving - * the pointer. Toggle-size stays Select-only. */ - int step; - - /* Select wins a Select+Adjust chord, so a chord never scrolls backwards - * unexpectedly. */ - step = (button & wuss_BUTTON_SELECT) ? WUSS_SCROLL_STEP - : -WUSS_SCROLL_STEP; + if (win->task.handle == NULL) + return result_OK; - switch (region) - { - case wuss_FURNITURE_TOGGLE_SIZE: - if (button & wuss_BUTTON_SELECT) - wuss__furniture_toggle_size(win); - break; - case wuss_FURNITURE_VSCROLL_UP: - wuss__furniture_scroll_step(win, POINT(0, -step)); - break; - case wuss_FURNITURE_VSCROLL_DOWN: - wuss__furniture_scroll_step(win, POINT(0, step)); - break; - case wuss_FURNITURE_HSCROLL_LEFT: - wuss__furniture_scroll_step(win, POINT(-step, 0)); - break; - case wuss_FURNITURE_HSCROLL_RIGHT: - wuss__furniture_scroll_step(win, POINT(step, 0)); - break; - default: - break; - } + event.kind = wuss_EVENT_CLOSE; + return win->task.handle(win, &event, win->task.task_data); } - return result_OK; - } - if (region == wuss_FURNITURE_CLOSE || region == wuss_FURNITURE_TITLE) - { - if (action == wuss_MOUSE_DOWN) + if (region == wuss_FURNITURE_BACK && action == wuss_MOUSE_DOWN) { - box_t content; - if (button & wuss_BUTTON_SELECT) + wuss_window_restack(win, wuss_ZORDER_BACK); + else if (button & wuss_BUTTON_ADJUST) wuss_window_restack(win, wuss_ZORDER_FRONT); - - wuss__content_box(win, &content); - wuss->furniture.dragging = win; - wuss->furniture.drag_kind = wuss_FURNITURE_DRAG_MOVE; - wuss->furniture.drag.x = x - content.x0; - wuss->furniture.drag.y = y - content.y0; + return result_OK; } - return result_OK; - } - if (region == wuss_FURNITURE_RESIZE || - region == wuss_FURNITURE_VSCROLL_WELL || - region == wuss_FURNITURE_HSCROLL_WELL) - { - if (action == wuss_MOUSE_DOWN) + if (region == wuss_FURNITURE_TOGGLE_SIZE || + region == wuss_FURNITURE_VSCROLL_UP || + region == wuss_FURNITURE_VSCROLL_DOWN || + region == wuss_FURNITURE_HSCROLL_LEFT || + region == wuss_FURNITURE_HSCROLL_RIGHT) { - point_t scroll; + if (action == wuss_MOUSE_DOWN && + (button & (wuss_BUTTON_SELECT | wuss_BUTTON_ADJUST))) + { + /* Adjust-clicking a scroll arrow steps the opposite way to the arrow + * it points, so one arrow can be worked in both directions without + * moving the pointer. Toggle-size stays Select-only. */ + int step; + + /* Select wins a Select+Adjust chord, so a chord never scrolls + * backwards unexpectedly. */ + step = (button & wuss_BUTTON_SELECT) ? WUSS_SCROLL_STEP + : -WUSS_SCROLL_STEP; + + switch (region) + { + case wuss_FURNITURE_TOGGLE_SIZE: + if (button & wuss_BUTTON_SELECT) + wuss__furniture_toggle_size(win); + break; + case wuss_FURNITURE_VSCROLL_UP: + wuss__scroll_step(win, POINT(0, -step)); + break; + case wuss_FURNITURE_VSCROLL_DOWN: + wuss__scroll_step(win, POINT(0, step)); + break; + case wuss_FURNITURE_HSCROLL_LEFT: + wuss__scroll_step(win, POINT(-step, 0)); + break; + case wuss_FURNITURE_HSCROLL_RIGHT: + wuss__scroll_step(win, POINT(step, 0)); + break; + default: + break; + } + } + return result_OK; + } - /* Only resize raises the window; dragging a scrollbar well must not - * reorder the stack. */ - if ((button & wuss_BUTTON_SELECT) && region == wuss_FURNITURE_RESIZE) - wuss_window_restack(win, wuss_ZORDER_FRONT); + if (region == wuss_FURNITURE_CLOSE || region == wuss_FURNITURE_TITLE) + { + if (action == wuss_MOUSE_DOWN) + { + box_t content; - wuss_window_get_scroll(win, &scroll); + if (button & wuss_BUTTON_SELECT) + wuss_window_restack(win, wuss_ZORDER_FRONT); - wuss->furniture.dragging = win; - wuss->furniture.drag_kind = wuss__furniture_drag_kind(region); - wuss->furniture.drag.x = x; - wuss->furniture.drag.y = y; - wuss->furniture.drag_scroll_start = (region == wuss_FURNITURE_VSCROLL_WELL) ? scroll.y : scroll.x; + wuss__content_box(win, &content); + wuss->furniture.dragging = win; + wuss->furniture.drag_kind = wuss_FURNITURE_DRAG_MOVE; + wuss->furniture.drag.x = x - content.x0; + wuss->furniture.drag.y = y - content.y0; + } + return result_OK; + } - /* Resize needs the pointer's offset from the content box's current - * bottom-right corner, so the point grabbed on the resize icon stays - * under the pointer as it moves, rather than that corner jumping to - * meet the pointer on the very first move. */ - if (region == wuss_FURNITURE_RESIZE) + if (region == wuss_FURNITURE_RESIZE || + region == wuss_FURNITURE_VSCROLL_WELL || + region == wuss_FURNITURE_HSCROLL_WELL) + { + if (action == wuss_MOUSE_DOWN) { - box_t content; - wuss__content_box(win, &content); - wuss->furniture.drag_offset.x = x - content.x1; - wuss->furniture.drag_offset.y = y - content.y1; + point_t scroll; + + /* Only resize raises the window; dragging a scrollbar well must not + * reorder the stack. */ + if ((button & wuss_BUTTON_SELECT) && region == wuss_FURNITURE_RESIZE) + wuss_window_restack(win, wuss_ZORDER_FRONT); + + wuss_window_get_scroll(win, &scroll); + + wuss->furniture.dragging = win; + wuss->furniture.drag_kind = wuss__furniture_drag_kind(region); + wuss->furniture.drag.x = x; + wuss->furniture.drag.y = y; + wuss->furniture.drag_scroll_start = (region == wuss_FURNITURE_VSCROLL_WELL) ? scroll.y : scroll.x; + + /* Resize needs the pointer's offset from the content box's current + * bottom-right corner, so the point grabbed on the resize icon stays + * under the pointer as it moves, rather than that corner jumping to + * meet the pointer on the very first move. */ + if (region == wuss_FURNITURE_RESIZE) + { + box_t content; + wuss__content_box(win, &content); + wuss->furniture.drag_offset.x = x - content.x1; + wuss->furniture.drag_offset.y = y - content.y1; + } } + return result_OK; } - return result_OK; } +#endif /* WUSS_FURNITURE */ if (win->task.handle != NULL) { box_t content; point_t doc_point; - wuss_icon_t *icon; wuss__content_box(win, &content); doc_point.x = x - content.x0 + win->scroll.x; doc_point.y = y - content.y0 + win->scroll.y; - icon = wuss__icon_hit_test(win, doc_point); - if (icon != NULL) +#ifdef WUSS_ICONS { - if (action == wuss_MOUSE_DOWN && - (button & (wuss_BUTTON_SELECT | wuss_BUTTON_ADJUST))) - { - icon->pressed = 1; - wuss->pressed_icon = icon; - wuss__icon_invalidate(icon); - } - else if (action == wuss_MOUSE_UP && icon->pressed) + wuss_icon_t *icon; + + icon = wuss__icon_hit_test(win, doc_point); + if (icon != NULL) { - icon->pressed = 0; - wuss->pressed_icon = NULL; - wuss__icon_invalidate(icon); + if (action == wuss_MOUSE_DOWN && + (button & (wuss_BUTTON_SELECT | wuss_BUTTON_ADJUST))) + { + icon->pressed = 1; + wuss->pressed_icon = icon; + wuss__icon_invalidate(icon); + } + else if (action == wuss_MOUSE_UP && icon->pressed) + { + icon->pressed = 0; + wuss->pressed_icon = NULL; + wuss__icon_invalidate(icon); + } + + event.kind = wuss_EVENT_ICON; + event.data.icon.icon = icon; + event.data.icon.action = action; + event.data.icon.button = button; + return win->task.handle(win, &event, win->task.task_data); } - - event.kind = wuss_EVENT_ICON; - event.data.icon.icon = icon; - event.data.icon.action = action; - event.data.icon.button = button; - return win->task.handle(win, &event, win->task.task_data); } +#endif event.kind = wuss_EVENT_MOUSE; event.data.mouse.action = action; diff --git a/libraries/wuss/mouse-move.c b/libraries/wuss/mouse-move.c index c56da51f..e07144ee 100644 --- a/libraries/wuss/mouse-move.c +++ b/libraries/wuss/mouse-move.c @@ -10,6 +10,7 @@ result_t wuss_mouse_move(wuss_t *wuss, point_t p, wuss_window_t **hit) x = p.x; y = p.y; +#ifdef WUSS_FURNITURE if (wuss->furniture.dragging != NULL) { win = wuss->furniture.dragging; @@ -38,6 +39,7 @@ result_t wuss_mouse_move(wuss_t *wuss, point_t p, wuss_window_t **hit) return result_OK; } +#endif win = wuss__window_at(wuss, p); if (hit != NULL) @@ -46,48 +48,55 @@ result_t wuss_mouse_move(wuss_t *wuss, point_t p, wuss_window_t **hit) if (win == NULL) return result_OK; +#ifdef WUSS_FURNITURE if (wuss__furniture_hit_test(win, POINT(x, y)) != wuss_FURNITURE_CONTENT) return result_OK; +#endif if (win->task.handle != NULL) { box_t content; point_t doc_point; - wuss_icon_t *icon; wuss_event_t event; - int k; wuss__content_box(win, &content); doc_point.x = x - content.x0 + win->scroll.x; doc_point.y = y - content.y0 + win->scroll.y; - icon = wuss__icon_hit_test(win, doc_point); - - /* Clear the pressed state of any button the pointer has left. This does - * not re-press a button on drag-back-in, and does not track which mouse - * button is held -- wuss keeps no persistent "button down over content" - * state. */ - for (k = 0; k < win->nicons; k++) +#ifdef WUSS_ICONS { - wuss_icon_t *it = win->icons[k]; + wuss_icon_t *icon; + int k; + + icon = wuss__icon_hit_test(win, doc_point); - if (it->pressed && it != icon) + /* Clear the pressed state of any button the pointer has left. This does + * not re-press a button on drag-back-in, and does not track which mouse + * button is held -- wuss keeps no persistent "button down over content" + * state. */ + for (k = 0; k < win->nicons; k++) { - it->pressed = 0; - if (wuss->pressed_icon == it) - wuss->pressed_icon = NULL; - wuss__icon_invalidate(it); + wuss_icon_t *it = win->icons[k]; + + if (it->pressed && it != icon) + { + it->pressed = 0; + if (wuss->pressed_icon == it) + wuss->pressed_icon = NULL; + wuss__icon_invalidate(it); + } } - } - if (icon != NULL) - { - event.kind = wuss_EVENT_ICON; - event.data.icon.icon = icon; - event.data.icon.action = wuss_MOUSE_MOVE; - event.data.icon.button = wuss_BUTTON_SELECT; - return win->task.handle(win, &event, win->task.task_data); + if (icon != NULL) + { + event.kind = wuss_EVENT_ICON; + event.data.icon.icon = icon; + event.data.icon.action = wuss_MOUSE_MOVE; + event.data.icon.button = wuss_BUTTON_SELECT; + return win->task.handle(win, &event, win->task.task_data); + } } +#endif event.kind = wuss_EVENT_MOUSE; event.data.mouse.action = wuss_MOUSE_MOVE; diff --git a/libraries/wuss/redraw.c b/libraries/wuss/redraw.c index e444b126..d86e3567 100644 --- a/libraries/wuss/redraw.c +++ b/libraries/wuss/redraw.c @@ -16,7 +16,9 @@ static void redraw_window(wuss_t *wuss, if (box_intersection(&win->visible, full, &visible_clipped)) return; /* offscreen */ +#ifdef WUSS_FURNITURE wuss__furniture_draw(wuss, win, full); +#endif wuss__content_box(win, &content); if (box_intersection(&content, full, &clipped)) @@ -52,6 +54,7 @@ static void redraw_window(wuss_t *wuss, *rc = crc; } +#ifdef WUSS_ICONS { int k; @@ -60,6 +63,7 @@ static void redraw_window(wuss_t *wuss, for (k = 0; k < win->nicons; k++) wuss__icon_draw(wuss, win->icons[k], &content, win->scroll); } +#endif } } diff --git a/libraries/wuss/scroll-step.c b/libraries/wuss/scroll-step.c new file mode 100644 index 00000000..c69766e8 --- /dev/null +++ b/libraries/wuss/scroll-step.c @@ -0,0 +1,41 @@ +/* scroll-step.c -- wuss - clamp and apply a scroll offset */ + +#include "impl.h" + +point_t wuss__scroll_clamp(const wuss_window_t *window, point_t desired) +{ + box_t content; + int max_x, max_y; + + wuss__content_box(window, &content); + + max_x = window->doc.w - (content.x1 - content.x0); + max_y = window->doc.h - (content.y1 - content.y0); + if (max_x < 0) + max_x = 0; + if (max_y < 0) + max_y = 0; + + if (desired.x < 0) + desired.x = 0; + else if (desired.x > max_x) + desired.x = max_x; + if (desired.y < 0) + desired.y = 0; + else if (desired.y > max_y) + desired.y = max_y; + + return desired; +} + +void wuss__scroll_step(wuss_window_t *window, point_t delta) +{ + point_t scroll; + + wuss_window_get_scroll(window, &scroll); + scroll.x += delta.x; + scroll.y += delta.y; + scroll = wuss__scroll_clamp(window, scroll); + + wuss_window_set_scroll(window, scroll); +} diff --git a/libraries/wuss/scroll.c b/libraries/wuss/scroll.c index 7be356e7..bcabbb43 100644 --- a/libraries/wuss/scroll.c +++ b/libraries/wuss/scroll.c @@ -5,7 +5,6 @@ result_t wuss_scroll(wuss_t *wuss, point_t p, int delta, wuss_window_t **hit) { wuss_window_t *win; - box_t titlebar; int x, y; x = p.x; @@ -18,9 +17,15 @@ result_t wuss_scroll(wuss_t *wuss, point_t p, int delta, wuss_window_t **hit) if (win == NULL) return result_OK; - wuss__titlebar_box(win, &titlebar); - if (box_contains_point(&titlebar, x, y)) - return result_OK; +#ifdef WUSS_FURNITURE + { + box_t titlebar; + + wuss__titlebar_box(win, &titlebar); + if (box_contains_point(&titlebar, x, y)) + return result_OK; + } +#endif if (win->task.handle != NULL) { @@ -36,12 +41,12 @@ result_t wuss_scroll(wuss_t *wuss, point_t p, int delta, wuss_window_t **hit) event.data.scroll.point.y = y - content.y0 + win->scroll.y; event.data.scroll.delta = delta; - wuss__furniture_scroll_step(win, POINT(0, delta)); + wuss__scroll_step(win, POINT(0, delta)); return win->task.handle(win, &event, win->task.task_data); } - wuss__furniture_scroll_step(win, POINT(0, delta)); + wuss__scroll_step(win, POINT(0, delta)); return result_OK; } diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 307d9929..6017f6f4 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -647,6 +647,8 @@ static int dirty_union_area(wuss_t *wuss, const box_t *bounds) return area; } +#if defined(WUSS_FURNITURE) && defined(WUSS_ICONS) + result_t wuss_test(const char *resources) { result_t rc; @@ -3114,3 +3116,175 @@ result_t wuss_test(const char *resources) return result_TEST_FAILED; } + +#else /* !(WUSS_FURNITURE && WUSS_ICONS) */ + +/* Compact core test for builds with furniture and/or icons compiled out. + * Exercises the chromeless window path: create, move, z-order, invalidate, + * redraw and programmatic scroll. No titlebar, no scrollbars, no icons. */ +result_t wuss_test(const char *resources) +{ + result_t rc; + int rowbytes; + void *pixels; + bitmap_t bm; + screen_t scr; + wuss_t *wuss; + test_task_t tc_a, tc_b; + wuss_task_t delegate_a, delegate_b; + box_t box_a, box_b, content; + wuss_window_t *win_a, *win_b, *hit; + point_t scroll; + + /* Force a chromeless window even in a build that still has furniture, so the + * assertions below (content box == visible box) hold in every config. */ + const wuss_window_flags_t chromeless = + wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | wuss_WINDOW_NO_CLOSE | + wuss_WINDOW_NO_BACK | wuss_WINDOW_NO_TOGGLE_SIZE | wuss_WINDOW_NO_VSCROLL | + wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE; + + NOT_USED(resources); + + rowbytes = 200 * 4; + pixels = malloc((size_t) rowbytes * 200); + if (pixels == NULL) + goto Failure; + + rc = bitmap_init(&bm, SIZE2D(200, 200), pixelfmt_bgrx8888, rowbytes, NULL, + pixels); + if (rc != result_OK) + goto Failure; + + screen_for_bitmap(&scr, &bm); + + printf("test: wuss_create (core)\n"); + + rc = wuss_create(&scr, NULL, NULL, 0, NULL, &wuss); + if (rc != result_OK) + goto Failure; + + printf("test: window_create too small\n"); + + box_a.x0 = 0; box_a.y0 = 0; box_a.x1 = 100; box_a.y1 = 0; + rc = wuss_window_create(wuss, &box_a, "toosmall", wuss_WINDOW_NONE, + wuss_NO_BACKGROUND, NULL, box_size(&box_a), + SIZE2D(0, 0), &win_a); + if (rc != result_WUSS_TOO_SMALL) + goto Failure; + + printf("test: create overlapping windows A and B\n"); + + memset(&tc_a, 0, sizeof(tc_a)); + memset(&tc_b, 0, sizeof(tc_b)); + delegate_a.handle = test_handle; delegate_a.task_data = &tc_a; + delegate_b.handle = test_handle; delegate_b.task_data = &tc_b; + + box_a.x0 = 0; box_a.y0 = 0; box_a.x1 = 100; box_a.y1 = 100; + rc = wuss_window_create(wuss, &box_a, "A", chromeless, + wuss_NO_BACKGROUND, &delegate_a, SIZE2D(400, 400), + SIZE2D(0, 0), &win_a); + if (rc != result_OK) + goto Failure; + + box_b.x0 = 50; box_b.y0 = 50; box_b.x1 = 150; box_b.y1 = 150; + rc = wuss_window_create(wuss, &box_b, "B", chromeless, + wuss_NO_BACKGROUND, &delegate_b, SIZE2D(400, 400), + SIZE2D(0, 0), &win_b); + if (rc != result_OK) + goto Failure; + + /* With furniture off the content box is the visible box verbatim. */ + wuss_window_get_content_bounds(win_a, &content); + if (content.x0 != 0 || content.y0 != 0 || + content.x1 != 100 || content.y1 != 100) + goto Failure; + + printf("test: redraw delivers REDRAW events\n"); + + wuss_redraw(wuss); + if (tc_a.redraw_count == 0 || tc_b.redraw_count == 0) + goto Failure; + + printf("test: z-order - click routes to topmost window\n"); + + /* B was created last so it is on top over the overlap at (75,75). */ + rc = wuss_mouse_click(wuss, POINT(75, 75), wuss_BUTTON_SELECT, + wuss_MOUSE_DOWN, &hit); + if (rc != result_OK || hit != win_b) + goto Failure; + if (tc_b.mouse_count != 1 || tc_a.mouse_count != 0) + goto Failure; + + wuss_window_restack(win_a, wuss_ZORDER_FRONT); + rc = wuss_mouse_click(wuss, POINT(75, 75), wuss_BUTTON_SELECT, + wuss_MOUSE_DOWN, &hit); + if (rc != result_OK || hit != win_a) + goto Failure; + if (tc_a.mouse_count != 1) + goto Failure; + + printf("test: mouse point is in document coordinates\n"); + + if (tc_a.last_x != 75 || tc_a.last_y != 75) + goto Failure; + + printf("test: window_move\n"); + + wuss_window_move(win_a, POINT(20, 20)); + wuss_window_get_content_bounds(win_a, &content); + if (content.x0 != 20 || content.y0 != 20) + goto Failure; + + printf("test: programmatic scroll offsets document coordinates\n"); + + wuss_window_set_scroll(win_a, POINT(10, 5)); + wuss_window_get_scroll(win_a, &scroll); + if (scroll.x != 10 || scroll.y != 5) + goto Failure; + + rc = wuss_mouse_click(wuss, POINT(20, 20), wuss_BUTTON_SELECT, + wuss_MOUSE_DOWN, &hit); + if (rc != result_OK || hit != win_a) + goto Failure; + if (tc_a.last_x != 10 || tc_a.last_y != 5) + goto Failure; + + printf("test: wheel scroll delivers SCROLL event and moves offset\n"); + + wuss_scroll(wuss, POINT(20, 20), 8, &hit); + if (hit != win_a) + goto Failure; + wuss_window_get_scroll(win_a, &scroll); + if (scroll.y != 13) + goto Failure; + + printf("test: invalidate marks dirty region\n"); + + wuss_window_invalidate(win_a, NULL); + if (wuss_get_dirty_count(wuss) == 0) + goto Failure; + wuss_redraw(wuss); + if (wuss_get_dirty_count(wuss) != 0) + goto Failure; + + printf("test: window_close delivers CLOSE and drops the window\n"); + + wuss_window_close(win_b); + rc = wuss_mouse_click(wuss, POINT(140, 140), wuss_BUTTON_SELECT, + wuss_MOUSE_DOWN, &hit); + if (rc != result_OK || hit != NULL) + goto Failure; + + wuss_destroy(wuss); + free(pixels); + + return result_TEST_PASSED; + +Failure: + + printf("wuss_test: failed\n"); + + return result_TEST_FAILED; +} + +#endif /* WUSS_FURNITURE && WUSS_ICONS */ diff --git a/libraries/wuss/window/close.c b/libraries/wuss/window/close.c index 8ff5fb9e..570425d0 100644 --- a/libraries/wuss/window/close.c +++ b/libraries/wuss/window/close.c @@ -16,10 +16,14 @@ void wuss_window_close(wuss_window_t *doomed) return; wuss = doomed->wuss; +#ifdef WUSS_FURNITURE if (wuss->furniture.dragging == doomed) wuss->furniture.dragging = NULL; +#endif +#ifdef WUSS_ICONS if (wuss->pressed_icon != NULL && wuss->pressed_icon->window == doomed) wuss->pressed_icon = NULL; +#endif wuss__release_packed(doomed); @@ -27,7 +31,9 @@ void wuss_window_close(wuss_window_t *doomed) list_remove(&wuss->z_order, &doomed->link); +#ifdef WUSS_ICONS wuss__icons_free(doomed); +#endif free(doomed); } diff --git a/libraries/wuss/window/create-placed.c b/libraries/wuss/window/create-placed.c index 44f95ecb..d3314f79 100644 --- a/libraries/wuss/window/create-placed.c +++ b/libraries/wuss/window/create-placed.c @@ -45,7 +45,11 @@ static void next_cascade(wuss_t *wuss, int fw, int fh, point_t *pos) scr_w = wuss->scr->size.w; scr_h = wuss->scr->size.h; +#ifdef WUSS_FURNITURE step = wuss->titlebar_height; +#else + step = 0; +#endif if (step <= 0) step = WUSS_DEFAULT_TITLEBAR_HEIGHT; diff --git a/libraries/wuss/window/create.c b/libraries/wuss/window/create.c index 7beb1ed5..11bb445b 100644 --- a/libraries/wuss/window/create.c +++ b/libraries/wuss/window/create.c @@ -78,10 +78,14 @@ result_t wuss_window_create(wuss_t *wuss, win->scroll.y = 0; win->doc = doc; win->min_doc = min_doc; +#ifdef WUSS_FURNITURE win->state = wuss_WINDOW_STATE_NONE; +#endif +#ifdef WUSS_ICONS win->icons = NULL; win->nicons = 0; win->cap_icons = 0; +#endif box_reset(&win->packed); /* wuss_window_create_placed fills this in after */ @@ -97,6 +101,7 @@ result_t wuss_window_create(wuss_t *wuss, } win->bg = bg; +#ifdef WUSS_FURNITURE if (title != NULL) { strncpy(win->title, title, WUSS_TITLE_MAX); @@ -106,6 +111,9 @@ result_t wuss_window_create(wuss_t *wuss, { win->title[0] = '\0'; } +#else + (void) title; +#endif list_add_to_head(&wuss->z_order, &win->link); diff --git a/libraries/wuss/window/resize.c b/libraries/wuss/window/resize.c index c43057a1..bcaca693 100644 --- a/libraries/wuss/window/resize.c +++ b/libraries/wuss/window/resize.c @@ -78,10 +78,12 @@ result_t wuss_window_resize(wuss_window_t *window, size2d_t size) * as already-valid and so never repaint. Force its old position dirty * too. Shrinking needs no such help: old furniture positions only ever * land outside the new, smaller box, already covered above. */ +#ifdef WUSS_FURNITURE if (window->visible.x1 - window->visible.x0 > before.x1 - before.x0 || window->visible.y1 - window->visible.y0 > before.y1 - before.y0) wuss__furniture_invalidate_for(window, &before); wuss__furniture_invalidate(window); +#endif } return result_OK; diff --git a/libraries/wuss/window/set-scroll.c b/libraries/wuss/window/set-scroll.c index 531665f4..e1371013 100644 --- a/libraries/wuss/window/set-scroll.c +++ b/libraries/wuss/window/set-scroll.c @@ -16,9 +16,11 @@ void wuss_window_set_scroll(wuss_window_t *window, point_t p) wuss__content_box(window, &content); +#ifdef WUSS_FURNITURE /* the scrollbar sausage position depends on scroll, so its well needs * redrawing too -- content invalidation alone never touches it */ wuss__furniture_invalidate(window); +#endif if (window->wuss->z_order.next == &window->link) { From 587bcc440e896b1a2d65fcc1b745d0a1a95aeb04 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 30 Aug 2026 21:06:29 +0100 Subject: [PATCH 35/44] feat(wuss): pattern-fill work-area icons Add screen_fill_pattern(), an 8x8 two-colour tile fill primitive in framebuf/screen with eight built-in patterns (solid, grey50, stripes, diagonal, dots, grid, crosshatch), phase-locked to a caller-supplied origin so a scrolling fill stays put. Add wuss_ICON_TYPE_PATTERN: a non-interactive icon whose bbox is filled with one of those patterns in fg/bg, aligned to document space. Clicks fall through as wuss_EVENT_MOUSE; disabled swatches fold fg into bg. Cover the primitive in screen-test.c (phase, clipping) and show every pattern in the interactive icons task. Co-Authored-By: Claude Sonnet 5 --- CMakeLists.txt | 1 + include/framebuf/screen.h | 44 +++++++ include/wuss/icon.h | 15 ++- .../framebuf/screen/screen-fill-pattern.c | 113 ++++++++++++++++++ libraries/framebuf/screen/test/screen-test.c | 65 +++++++++- libraries/wuss/icon.h | 1 + libraries/wuss/icon/create.c | 14 ++- libraries/wuss/icon/draw.c | 16 +++ libraries/wuss/test/tasks/icons.c | 20 +++- 9 files changed, 281 insertions(+), 8 deletions(-) create mode 100644 libraries/framebuf/screen/screen-fill-pattern.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 0b62d49d..a322d526 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -241,6 +241,7 @@ set(FRAMEBUF_SOURCES libraries/framebuf/screen/screen-copy-rect.c libraries/framebuf/screen/screen-draw.c libraries/framebuf/screen/screen-draw-ninepatch.c + libraries/framebuf/screen/screen-fill-pattern.c libraries/framebuf/span-registry/get.c libraries/framebuf/span-registry/regdata.h libraries/framebuf/span/all8888.c diff --git a/include/framebuf/screen.h b/include/framebuf/screen.h index da0b5325..309f205b 100644 --- a/include/framebuf/screen.h +++ b/include/framebuf/screen.h @@ -94,6 +94,50 @@ void screen_draw_square(screen_t *scr, int size, colour_t colour); +/** + * Built-in 8x8 fill patterns for `screen_fill_pattern`. Each is a 1-bit tile: + * set bits take the foreground colour, clear bits the background. + */ +typedef enum screen_pattern +{ + screen_PATTERN_SOLID = 0, /**< Every pixel foreground. */ + screen_PATTERN_GREY50, /**< 50% checkerboard. */ + screen_PATTERN_HSTRIPE, /**< Horizontal bars. */ + screen_PATTERN_VSTRIPE, /**< Vertical bars. */ + screen_PATTERN_DIAGONAL, /**< Diagonal lines. */ + screen_PATTERN_DOTS, /**< Sparse dots. */ + screen_PATTERN_GRID, /**< Thin grid lines. */ + screen_PATTERN_CROSSHATCH, /**< Crossed thin lines. */ + screen_PATTERN__LIMIT /**< Count of patterns; not itself a pattern. */ +} +screen_pattern_t; + +/** + * Fills a box with a repeating 8x8 two-colour pattern. + * + * The tile is phased against (`origin_x`, `origin_y`): that coordinate is the + * one that would map to the box's top-left corner. Passing the caller's own + * scroll origin keeps the pattern locked to content as the box moves, rather + * than crawling with it. + * + * Clipped to the screen's clip region. + * + * \param[in] scr Screen to draw upon. + * \param[in] box Box to fill, inclusive-exclusive. + * \param[in] pattern Pattern to fill with. + * \param[in] origin_x X coordinate mapping to `box->x0` for tile phase. + * \param[in] origin_y Y coordinate mapping to `box->y0` for tile phase. + * \param[in] fg Colour for set pattern bits. + * \param[in] bg Colour for clear pattern bits. + */ +void screen_fill_pattern(screen_t *scr, + const box_t *box, + screen_pattern_t pattern, + int origin_x, + int origin_y, + colour_t fg, + colour_t bg); + /** * Draws a bitmap, alpha-blending it against the screen where the bitmap has an * alpha channel. On paletted screens, which have no linear channel bits to diff --git a/include/wuss/icon.h b/include/wuss/icon.h index f09b5a0b..be9eaa18 100644 --- a/include/wuss/icon.h +++ b/include/wuss/icon.h @@ -29,6 +29,7 @@ extern "C" #endif #include "base/result.h" +#include "framebuf/screen.h" #include "geom/box.h" #include "wuss/wuss.h" @@ -52,10 +53,16 @@ typedef enum wuss_icon_type wuss_ICON_TYPE_LABEL = 0, /**< Static text drawn with the window manager's * font. Not interactive: clicks fall through to * the task as wuss_EVENT_MOUSE. */ - wuss_ICON_TYPE_BUTTON /**< Bevelled rectangle with a centred text label + wuss_ICON_TYPE_BUTTON, /**< Bevelled rectangle with a centred text label * and pressed-state visual feedback; clicks and * hovers are delivered to the task as * wuss_EVENT_ICON. */ + wuss_ICON_TYPE_PATTERN /**< Bounding box filled with a repeating two-colour + * 8x8 tile (spec.pattern) in fg/bg, phased to + * document space so it scrolls rigidly with + * content. Not interactive: clicks fall through + * to the task as wuss_EVENT_MOUSE. text is + * ignored. */ } wuss_icon_type_t; @@ -89,7 +96,11 @@ typedef struct wuss_icon_spec wuss_colour_t bg; /**< Fill/bevel base colour, as an index into the * system palette. A label may pass * wuss_NO_BACKGROUND for text with no fill; a - * button must pass a real index. */ + * button or pattern icon must pass a real + * index. */ + screen_pattern_t pattern; /**< Tile for wuss_ICON_TYPE_PATTERN; ignored by + * other types. Zero (screen_PATTERN_SOLID) is a + * safe default for zero-initialised specs. */ wuss_icon_flags_t flags; /**< Appearance/behaviour flags. */ } wuss_icon_spec_t; diff --git a/libraries/framebuf/screen/screen-fill-pattern.c b/libraries/framebuf/screen/screen-fill-pattern.c new file mode 100644 index 00000000..dd514224 --- /dev/null +++ b/libraries/framebuf/screen/screen-fill-pattern.c @@ -0,0 +1,113 @@ +/* screen-fill-pattern.c -- fill a box with a repeating 8x8 two-colour pattern */ + +#include + +#include "framebuf/colour.h" +#include "framebuf/pixelfmt.h" +#include "geom/box.h" + +#include "framebuf/screen.h" + +/* One byte per row, MSB = leftmost pixel. */ +static const unsigned char patterns[screen_PATTERN__LIMIT][8] = +{ + { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }, /* SOLID */ + { 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55 }, /* GREY50 */ + { 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00 }, /* HSTRIPE */ + { 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA }, /* VSTRIPE */ + { 0x88, 0x44, 0x22, 0x11, 0x88, 0x44, 0x22, 0x11 }, /* DIAGONAL */ + { 0x88, 0x00, 0x22, 0x00, 0x88, 0x00, 0x22, 0x00 }, /* DOTS */ + { 0xFF, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80 }, /* GRID */ + { 0x80, 0x41, 0x22, 0x14, 0x08, 0x14, 0x22, 0x41 } /* CROSSHATCH */ +}; + +/* ponytail: per-pixel loop. Fine for icon-sized fills; if a large pattern + * fill ever profiles hot, expand each pattern row to a colour run once per + * scanline instead. */ +void screen_fill_pattern(screen_t *scr, + const box_t *box, + screen_pattern_t pattern, + int origin_x, + int origin_y, + colour_t fg, + colour_t bg) +{ + const unsigned char *tile; + box_t clip_box; + box_t draw_box; + pixelfmt_any_t fg_fmt, bg_fmt; + int x, y; + + assert(pattern >= 0 && pattern < screen_PATTERN__LIMIT); + if (pattern < 0 || pattern >= screen_PATTERN__LIMIT) + return; + + tile = patterns[pattern]; + + if (screen_get_clip(scr, &clip_box)) + return; /* invalid clipped screen */ + + if (box_intersection(&clip_box, box, &draw_box)) + return; /* nothing visible */ + + fg_fmt = colour_to_pixel(scr->palette, + (scr->format == pixelfmt_p4) ? 16 : 0, + fg, scr->format); + bg_fmt = colour_to_pixel(scr->palette, + (scr->format == pixelfmt_p4) ? 16 : 0, + bg, scr->format); + + switch (pixelfmt_log2bpp(scr->format)) + { + case 2: + { + unsigned char *rowp; + + rowp = (unsigned char *) scr->base + draw_box.y0 * scr->rowbytes; + for (y = draw_box.y0; y < draw_box.y1; y++) + { + unsigned char bits; + + bits = tile[(y - origin_y) & 7]; + for (x = draw_box.x0; x < draw_box.x1; x++) + { + unsigned char *scrp; + int shift; + pixelfmt_any_t px; + + px = (bits & (0x80u >> ((x - origin_x) & 7))) ? fg_fmt : bg_fmt; + scrp = rowp + (x >> 1); + shift = (x & 1) * 4; + + *scrp = (unsigned char) ((*scrp & ~(0xF << shift)) | + ((px & 0xF) << shift)); + } + rowp += scr->rowbytes; + } + } + break; + + case 5: + { + unsigned char *rowp; + + rowp = (unsigned char *) scr->base + draw_box.y0 * scr->rowbytes; + for (y = draw_box.y0; y < draw_box.y1; y++) + { + pixelfmt_any32_t *scrp; + unsigned char bits; + + bits = tile[(y - origin_y) & 7]; + scrp = (pixelfmt_any32_t *) rowp + draw_box.x0; + for (x = draw_box.x0; x < draw_box.x1; x++) + *scrp++ = (bits & (0x80u >> ((x - origin_x) & 7))) ? fg_fmt : bg_fmt; + rowp += scr->rowbytes; + } + } + break; + + default: + assert(!"Unimplemented pixel format"); + break; + } +} diff --git a/libraries/framebuf/screen/test/screen-test.c b/libraries/framebuf/screen/test/screen-test.c index dc8c3fd4..2d14a006 100644 --- a/libraries/framebuf/screen/test/screen-test.c +++ b/libraries/framebuf/screen/test/screen-test.c @@ -387,6 +387,68 @@ static result_t test_ninepatch(void) /* ----------------------------------------------------------------------- */ +static result_t test_fill_pattern(void) +{ + static testscreen_t ts; + static testscreen_t enc; + + box_t box = { 8, 8, 24, 24 }; + int fg, bg; + + fg = (int) np_encode(&enc, 255, 0, 0); + bg = (int) np_encode(&enc, 0, 0, 255); + + /* GREY50 is 0xAA,0x55,... : at origin (0,0) pixel (x,y) is fg when + * ((x ^ y) & 1) == 0. */ + testscreen_init(&ts); + screen_fill_pattern(&ts.scr, &box, screen_PATTERN_GREY50, 0, 0, + colour_rgb(255, 0, 0), colour_rgb(0, 0, 255)); + + if (np_at(&ts, 8, 8) != fg || /* (0,0) phase -> set bit */ + np_at(&ts, 9, 8) != bg || + np_at(&ts, 8, 9) != bg || + np_at(&ts, 9, 9) != fg) + { + printf("screen: fill_pattern GREY50 wrong at origin 0\n"); + return result_TEST_FAILED; + } + + /* Outside the box stays background. */ + if (np_at(&ts, 7, 7) != (int) (pixelfmt_bgrx8888_t) BACKGROUND || + np_at(&ts, 24, 24) != (int) (pixelfmt_bgrx8888_t) BACKGROUND) + { + printf("screen: fill_pattern drew outside the box\n"); + return result_TEST_FAILED; + } + + /* Shift the origin by one in x: every pixel's phase flips, so the same + * screen coordinate takes the other colour. */ + testscreen_init(&ts); + screen_fill_pattern(&ts.scr, &box, screen_PATTERN_GREY50, 1, 0, + colour_rgb(255, 0, 0), colour_rgb(0, 0, 255)); + if (np_at(&ts, 8, 8) != bg || np_at(&ts, 9, 8) != fg) + { + printf("screen: fill_pattern ignored origin phase\n"); + return result_TEST_FAILED; + } + + /* Honours the screen clip. */ + testscreen_init(&ts); + ts.scr.clip = (box_t) { 0, 0, 16, 64 }; + screen_fill_pattern(&ts.scr, &box, screen_PATTERN_SOLID, 0, 0, + colour_rgb(255, 0, 0), colour_rgb(0, 0, 255)); + if (np_at(&ts, 10, 10) != fg || + np_at(&ts, 20, 10) != (int) (pixelfmt_bgrx8888_t) BACKGROUND) + { + printf("screen: fill_pattern ignored the screen clip\n"); + return result_TEST_FAILED; + } + + return result_TEST_PASSED; +} + +/* ----------------------------------------------------------------------- */ + result_t screen_test(const char *resources) { typedef result_t (*screentestfn)(void); @@ -396,7 +458,8 @@ result_t screen_test(const char *resources) test_clip_invariance, test_clipping_still_happens, test_wu_fix8_extreme_coords, - test_ninepatch + test_ninepatch, + test_fill_pattern }; result_t rc; diff --git a/libraries/wuss/icon.h b/libraries/wuss/icon.h index a2c415c9..84e3aead 100644 --- a/libraries/wuss/icon.h +++ b/libraries/wuss/icon.h @@ -19,6 +19,7 @@ struct wuss_icon char *text; /* owned; never NULL ("" instead) */ wuss_colour_t fg; wuss_colour_t bg; + screen_pattern_t pattern; /* wuss_ICON_TYPE_PATTERN tile; 0 otherwise */ wuss_icon_flags_t flags; int pressed; /* button: 1 while held with the pointer inside */ }; diff --git a/libraries/wuss/icon/create.c b/libraries/wuss/icon/create.c index 823f1860..86686b56 100644 --- a/libraries/wuss/icon/create.c +++ b/libraries/wuss/icon/create.c @@ -26,10 +26,18 @@ result_t wuss_icon_create(wuss_window_t *window, w = window->wuss; - if (spec->type != wuss_ICON_TYPE_LABEL && spec->type != wuss_ICON_TYPE_BUTTON) + if (spec->type != wuss_ICON_TYPE_LABEL && + spec->type != wuss_ICON_TYPE_BUTTON && + spec->type != wuss_ICON_TYPE_PATTERN) return result_WUSS_BAD_ICON; - if (spec->type == wuss_ICON_TYPE_BUTTON && spec->bg == wuss_NO_BACKGROUND) + if ((spec->type == wuss_ICON_TYPE_BUTTON || + spec->type == wuss_ICON_TYPE_PATTERN) && + spec->bg == wuss_NO_BACKGROUND) + return result_WUSS_BAD_ICON; + + if (spec->type == wuss_ICON_TYPE_PATTERN && + (spec->pattern < 0 || spec->pattern >= screen_PATTERN__LIMIT)) return result_WUSS_BAD_ICON; if (spec->fg < 0 || spec->fg >= w->npalette) @@ -68,6 +76,8 @@ result_t wuss_icon_create(wuss_window_t *window, it->type = spec->type; it->fg = spec->fg; it->bg = spec->bg; + it->pattern = (spec->type == wuss_ICON_TYPE_PATTERN) ? spec->pattern + : screen_PATTERN_SOLID; it->flags = spec->flags; it->pressed = 0; diff --git a/libraries/wuss/icon/draw.c b/libraries/wuss/icon/draw.c index 98606923..9fb2b950 100644 --- a/libraries/wuss/icon/draw.c +++ b/libraries/wuss/icon/draw.c @@ -65,6 +65,22 @@ void wuss__icon_draw(wuss_t *wuss, switch (icon->type) { + case wuss_ICON_TYPE_PATTERN: + { + colour_t pat_fg; + + /* disabled: fold the pattern into its own ground so it reads as greyed, + * mirroring the button's fg-swap */ + pat_fg = (icon->flags & wuss_ICON_FLAGS_DISABLED) + ? wuss->palette[icon->bg] + : fg; + + screen_fill_pattern(scr, &b, icon->pattern, + content->x0 - scroll.x, content->y0 - scroll.y, + pat_fg, wuss->palette[icon->bg]); + } + break; + case wuss_ICON_TYPE_LABEL: { colour_t bg; diff --git a/libraries/wuss/test/tasks/icons.c b/libraries/wuss/test/tasks/icons.c index 5f184f38..a9953354 100644 --- a/libraries/wuss/test/tasks/icons.c +++ b/libraries/wuss/test/tasks/icons.c @@ -27,9 +27,11 @@ result_t icons_create(wuss_t *wuss, bmfont_t *font, icons_task_t *task) { + /* [0..3] plus one swatch per built-in pattern */ wuss_task_t delegate; - wuss_icon_spec_t specs[4]; - wuss_icon_t *made[4]; + wuss_icon_spec_t specs[4 + screen_PATTERN__LIMIT]; + wuss_icon_t *made[4 + screen_PATTERN__LIMIT]; + int p; result_t rc; task->font = font; @@ -88,7 +90,19 @@ result_t icons_create(wuss_t *wuss, specs[3].fg = palette_PICO8_BLACK; specs[3].bg = palette_PICO8_LIGHT_GREY; - rc = wuss_icon_create_array(task->window, specs, 4, made); + /* [4..] one swatch per built-in pattern, in a column down the document so + * they scroll through the window and stay phase-locked while doing so */ + for (p = 0; p < screen_PATTERN__LIMIT; p++) + { + specs[4 + p].bbox = (box_t) BOX_POS_SIZE(28, 90 + p * 44, 90, 36); + specs[4 + p].type = wuss_ICON_TYPE_PATTERN; + specs[4 + p].fg = palette_PICO8_DARK_BLUE; + specs[4 + p].bg = palette_PICO8_LIGHT_GREY; + specs[4 + p].pattern = (screen_pattern_t) p; + } + + rc = wuss_icon_create_array(task->window, specs, + 4 + screen_PATTERN__LIMIT, made); if (rc != result_OK) goto failure; From 3a9a4bfebeebe3bb730521925d97f2d7512562b2 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 30 Aug 2026 21:11:09 +0100 Subject: [PATCH 36/44] perf(framebuf): expand screen_fill_pattern tile rows to colour runs The tile is 8x8 and repeats, so a fill has only eight distinct pixel rows. Expand each to a phase-shifted colour run once up front; the scanline loops then index the run with no per-pixel bit test. The 32bpp path memcpy's whole 8-pixel runs instead of storing pixel by pixel. Co-Authored-By: Claude Sonnet 5 --- .../framebuf/screen/screen-fill-pattern.c | 58 ++++++++++++------- 1 file changed, 38 insertions(+), 20 deletions(-) diff --git a/libraries/framebuf/screen/screen-fill-pattern.c b/libraries/framebuf/screen/screen-fill-pattern.c index dd514224..0d9b5696 100644 --- a/libraries/framebuf/screen/screen-fill-pattern.c +++ b/libraries/framebuf/screen/screen-fill-pattern.c @@ -1,6 +1,7 @@ /* screen-fill-pattern.c -- fill a box with a repeating 8x8 two-colour pattern */ #include +#include #include "framebuf/colour.h" #include "framebuf/pixelfmt.h" @@ -21,9 +22,6 @@ static const unsigned char patterns[screen_PATTERN__LIMIT][8] = { 0x80, 0x41, 0x22, 0x14, 0x08, 0x14, 0x22, 0x41 } /* CROSSHATCH */ }; -/* ponytail: per-pixel loop. Fine for icon-sized fills; if a large pattern - * fill ever profiles hot, expand each pattern row to a colour run once per - * scanline instead. */ void screen_fill_pattern(screen_t *scr, const box_t *box, screen_pattern_t pattern, @@ -36,7 +34,9 @@ void screen_fill_pattern(screen_t *scr, box_t clip_box; box_t draw_box; pixelfmt_any_t fg_fmt, bg_fmt; - int x, y; + pixelfmt_any_t runs[8][8]; /* one expanded colour run per tile row */ + int xphase; + int row, col, x, y; assert(pattern >= 0 && pattern < screen_PATTERN__LIMIT); if (pattern < 0 || pattern >= screen_PATTERN__LIMIT) @@ -57,6 +57,19 @@ void screen_fill_pattern(screen_t *scr, (scr->format == pixelfmt_p4) ? 16 : 0, bg, scr->format); + /* The tile is 8x8 and repeats, so there are only eight distinct pixel rows. + * Expand each to a colour run once here, already phase-shifted for x, then + * the scanline loops just index runs[row][col] with no per-pixel bit test. */ + xphase = ((draw_box.x0 - origin_x) & 7); + for (row = 0; row < 8; row++) + { + unsigned char bits = tile[row]; + + for (col = 0; col < 8; col++) + runs[row][col] = + (bits & (0x80u >> ((xphase + col) & 7))) ? fg_fmt : bg_fmt; + } + switch (pixelfmt_log2bpp(scr->format)) { case 2: @@ -66,21 +79,17 @@ void screen_fill_pattern(screen_t *scr, rowp = (unsigned char *) scr->base + draw_box.y0 * scr->rowbytes; for (y = draw_box.y0; y < draw_box.y1; y++) { - unsigned char bits; + const pixelfmt_any_t *run = runs[(y - origin_y) & 7]; - bits = tile[(y - origin_y) & 7]; + col = 0; for (x = draw_box.x0; x < draw_box.x1; x++) { - unsigned char *scrp; - int shift; - pixelfmt_any_t px; - - px = (bits & (0x80u >> ((x - origin_x) & 7))) ? fg_fmt : bg_fmt; - scrp = rowp + (x >> 1); - shift = (x & 1) * 4; + unsigned char *scrp = rowp + (x >> 1); + int shift = (x & 1) * 4; *scrp = (unsigned char) ((*scrp & ~(0xF << shift)) | - ((px & 0xF) << shift)); + ((run[col] & 0xF) << shift)); + col = (col + 1) & 7; } rowp += scr->rowbytes; } @@ -94,13 +103,22 @@ void screen_fill_pattern(screen_t *scr, rowp = (unsigned char *) scr->base + draw_box.y0 * scr->rowbytes; for (y = draw_box.y0; y < draw_box.y1; y++) { - pixelfmt_any32_t *scrp; - unsigned char bits; + const pixelfmt_any_t *run = runs[(y - origin_y) & 7]; + pixelfmt_any32_t *scrp = (pixelfmt_any32_t *) rowp + draw_box.x0; + int w = draw_box.x1 - draw_box.x0; - bits = tile[(y - origin_y) & 7]; - scrp = (pixelfmt_any32_t *) rowp + draw_box.x0; - for (x = draw_box.x0; x < draw_box.x1; x++) - *scrp++ = (bits & (0x80u >> ((x - origin_x) & 7))) ? fg_fmt : bg_fmt; + /* leading partial tile up to an 8-pixel boundary, then whole runs */ + col = 0; + while (w > 0) + { + int n = 8 - col; + if (n > w) + n = w; + memcpy(scrp, run + col, (size_t) n * sizeof(*scrp)); + scrp += n; + w -= n; + col = 0; + } rowp += scr->rowbytes; } } From e00cabf35545c1ccd2305a5bb5530cd0f552a655 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 30 Aug 2026 21:15:11 +0100 Subject: [PATCH 37/44] docs: fill in CHANGELOG for changes since 277b4e8 Covers screen_fill_pattern(), wuss_ICON_TYPE_PATTERN, the WUSS_FURNITURE/WUSS_ICONS compile-time options and the scrollbar-well raise fix. Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff6845ab..b50d2567 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,23 @@ _Unreleased_ until one is cut. no region fits. The slot is released on close and on the first `wuss_window_move()` / `wuss_window_resize()`. An overall screen margin is kept around all auto-placed windows. +- `screen_fill_pattern()` — 8x8 two-colour tile fill primitive in + `framebuf/screen` with eight built-in patterns (solid, grey50, stripes, + diagonal, dots, grid, crosshatch), phase-locked to a caller-supplied origin + so a scrolling fill stays put. The tile has only eight distinct rows, so each + is expanded to a phase-shifted colour run once up front and the scanline + loops index it with no per-pixel bit test; the 32bpp path memcpy's whole + 8-pixel runs. +- `wuss_ICON_TYPE_PATTERN` — a non-interactive work-area icon whose bbox is + filled with a `screen_fill_pattern()` pattern in fg/bg, aligned to document + space. Clicks fall through as `wuss_EVENT_MOUSE`; disabled swatches fold fg + into bg. +- `WUSS_FURNITURE` and `WUSS_ICONS` CMake options (both default ON) drop the + furniture/*.c and icon/*.c files and `#ifdef`-guard every core call site, + struct field and helper that references them. With `WUSS_FURNITURE` off every + window is chromeless (content box == visible box); with `WUSS_ICONS` off the + `wuss_icon_*` API is not compiled. Programmatic and wheel scrolling survive + either off via the new core `scroll-step.c`. - `screen_draw_ninepatch()` — draws a resizable "9-patch" frame from a source image that is a 3x3 grid of equal cells: corners at natural size, edges and centre tiled, clipped to the destination box and the screen clip. @@ -54,6 +71,8 @@ _Unreleased_ until one is cut. ### Fixed +- Dragging a scrollbar well with Select no longer raises the window; only a + resize-icon grab restacks it. - `wuss_window_move()` no longer repaints already-blitted pixels when a drag past an occluded corner slides one clean piece of the window onto ground another clean piece just vacated. From fcdeb889e0621555046f3ee74c01c5b916cbf4b2 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 30 Aug 2026 21:47:04 +0100 Subject: [PATCH 38/44] fix(screen): clamp ninepatch corners so they don't overlap on tiny boxes When dst was narrower or shorter than two patch cells the near and far corner boxes overlapped and each drew its cell full-size across the shared band, so the last one won. Clamp the corner column/row boundaries to the destination midpoint: the near corner keeps the near half, the far corner the far half, and the edge/centre runs between collapse. Also assert log2bpp >= 3 in ninepatch_cell so a sub-8bpp source aborts cleanly instead of hitting a negative shift, and hoist a mid-scope loop var. Co-Authored-By: Claude Sonnet 5 --- .../framebuf/screen/screen-draw-ninepatch.c | 37 +++++++++++++------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/libraries/framebuf/screen/screen-draw-ninepatch.c b/libraries/framebuf/screen/screen-draw-ninepatch.c index 96ba082f..b0b7a795 100644 --- a/libraries/framebuf/screen/screen-draw-ninepatch.c +++ b/libraries/framebuf/screen/screen-draw-ninepatch.c @@ -21,9 +21,13 @@ static void ninepatch_cell(bitmap_t *cell, int pw, int ph) { + int log2bpp; int bpp; - bpp = 1 << (pixelfmt_log2bpp(src->format) - 3); + /* Byte stride per pixel. Sub-byte formats have no meaningful cell offset. */ + log2bpp = pixelfmt_log2bpp(src->format); + assert(log2bpp >= 3); + bpp = 1 << (log2bpp - 3); *cell = *src; cell->size = SIZE2D(pw, ph); @@ -44,7 +48,6 @@ static void tile_area(screen_t *scr, int stepy) { box_t clip; - int oy; if (box_is_empty(saved)) clip = *area; @@ -56,12 +59,17 @@ static void tile_area(screen_t *scr, scr->clip = clip; - for (oy = area->y0; oy < area->y1; oy += (stepy > 0) ? stepy : (area->y1 - oy)) { + int oy; int ox; - for (ox = area->x0; ox < area->x1; ox += (stepx > 0) ? stepx : (area->x1 - ox)) - screen_draw_bitmap(scr, ox, oy, cell); + for (oy = area->y0; oy < area->y1; + oy += (stepy > 0) ? stepy : (area->y1 - oy)) + { + for (ox = area->x0; ox < area->x1; + ox += (stepx > 0) ? stepx : (area->x1 - ox)) + screen_draw_bitmap(scr, ox, oy, cell); + } } } @@ -75,6 +83,7 @@ void screen_draw_ninepatch(screen_t *scr, box_t saved; box_t orig_clip; int pw, ph; + int midx, midy; int lx, rx, ty, by; bitmap_t cell; @@ -87,13 +96,17 @@ void screen_draw_ninepatch(screen_t *scr, pw = src->size.w / 3; ph = src->size.h / 3; - /* Corner column/row boundaries in the destination. When "dst" is narrower - * or shorter than two patches the near and far corners overlap; the clip in - * tile_area trims each to its own half. */ - lx = dst->x0 + pw; - rx = dst->x1 - pw; - ty = dst->y0 + ph; - by = dst->y1 - ph; + /* Corner column/row boundaries in the destination. When "dst" is narrower or + * shorter than two patches the near and far corners would overlap, so each + * boundary is clamped to the destination midpoint: the near corner gets the + * near half, the far corner the far half, and the edge/centre runs between + * them collapse to nothing. */ + midx = (dst->x0 + dst->x1) / 2; + midy = (dst->y0 + dst->y1) / 2; + lx = dst->x0 + pw; if (lx > midx) lx = midx; + rx = dst->x1 - pw; if (rx < midx) rx = midx; + ty = dst->y0 + ph; if (ty > midy) ty = midy; + by = dst->y1 - ph; if (by < midy) by = midy; /* Fold "dst" into the saved clip once, so every tile_area call is bounded by * the destination rectangle as well as the caller's clip. An empty caller From 4d561b2a4ec9a78440cec172e685f23df772f893 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 30 Aug 2026 21:47:04 +0100 Subject: [PATCH 39/44] style(screen): group screen_fill_pattern declarations at top of scope Co-Authored-By: Claude Sonnet 5 --- .../framebuf/screen/screen-fill-pattern.c | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/libraries/framebuf/screen/screen-fill-pattern.c b/libraries/framebuf/screen/screen-fill-pattern.c index 0d9b5696..d0613b6b 100644 --- a/libraries/framebuf/screen/screen-fill-pattern.c +++ b/libraries/framebuf/screen/screen-fill-pattern.c @@ -63,8 +63,9 @@ void screen_fill_pattern(screen_t *scr, xphase = ((draw_box.x0 - origin_x) & 7); for (row = 0; row < 8; row++) { - unsigned char bits = tile[row]; + unsigned char bits; + bits = tile[row]; for (col = 0; col < 8; col++) runs[row][col] = (bits & (0x80u >> ((xphase + col) & 7))) ? fg_fmt : bg_fmt; @@ -74,18 +75,20 @@ void screen_fill_pattern(screen_t *scr, { case 2: { - unsigned char *rowp; + unsigned char *rowp; + const pixelfmt_any_t *run; + unsigned char *scrp; + int shift; rowp = (unsigned char *) scr->base + draw_box.y0 * scr->rowbytes; for (y = draw_box.y0; y < draw_box.y1; y++) { - const pixelfmt_any_t *run = runs[(y - origin_y) & 7]; - + run = runs[(y - origin_y) & 7]; col = 0; for (x = draw_box.x0; x < draw_box.x1; x++) { - unsigned char *scrp = rowp + (x >> 1); - int shift = (x & 1) * 4; + scrp = rowp + (x >> 1); + shift = (x & 1) * 4; *scrp = (unsigned char) ((*scrp & ~(0xF << shift)) | ((run[col] & 0xF) << shift)); @@ -98,20 +101,24 @@ void screen_fill_pattern(screen_t *scr, case 5: { - unsigned char *rowp; + unsigned char *rowp; + const pixelfmt_any_t *run; + pixelfmt_any32_t *scrp; + int w; + int n; rowp = (unsigned char *) scr->base + draw_box.y0 * scr->rowbytes; for (y = draw_box.y0; y < draw_box.y1; y++) { - const pixelfmt_any_t *run = runs[(y - origin_y) & 7]; - pixelfmt_any32_t *scrp = (pixelfmt_any32_t *) rowp + draw_box.x0; - int w = draw_box.x1 - draw_box.x0; + run = runs[(y - origin_y) & 7]; + scrp = (pixelfmt_any32_t *) rowp + draw_box.x0; + w = draw_box.x1 - draw_box.x0; /* leading partial tile up to an 8-pixel boundary, then whole runs */ col = 0; while (w > 0) { - int n = 8 - col; + n = 8 - col; if (n > w) n = w; memcpy(scrp, run + col, (size_t) n * sizeof(*scrp)); From 57e3c715485249eeeb5791b6726936b7a6b75110 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 30 Aug 2026 21:47:04 +0100 Subject: [PATCH 40/44] fix(wuss): don't wedge oversized auto-placed windows at the origin next_cascade had a single wrap check and no loop, so a footprint wider or taller than the screen made the wrap condition permanently true and every oversized auto-placed window landed at (0,0). Pin an over-screen footprint at the top-left and return without advancing the cascade counter, so later normal-sized windows still cascade. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/window/create-placed.c | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/libraries/wuss/window/create-placed.c b/libraries/wuss/window/create-placed.c index d3314f79..6088a289 100644 --- a/libraries/wuss/window/create-placed.c +++ b/libraries/wuss/window/create-placed.c @@ -38,7 +38,9 @@ static void footprint_pad(const wuss_t *wuss, /* Pick the next cascade position for a window of the given footprint size, * once the layout packer has no room. Steps down/right by a titlebar each * call, wrapping back to the top-left when the step would push the footprint - * off the screen. */ + * off the screen. A footprint that is itself larger than the screen can never + * fit; it is pinned at the top-left and the cascade counter is not advanced, + * so it does not wedge every later window at the origin too. */ static void next_cascade(wuss_t *wuss, int fw, int fh, point_t *pos) { int scr_w, scr_h, step; @@ -53,6 +55,13 @@ static void next_cascade(wuss_t *wuss, int fw, int fh, point_t *pos) if (step <= 0) step = WUSS_DEFAULT_TITLEBAR_HEIGHT; + if (fw > scr_w || fh > scr_h) + { + pos->x = 0; + pos->y = 0; + return; + } + if (wuss->cascade.x + fw > scr_w || wuss->cascade.y + fh > scr_h) { wuss->cascade.x = 0; From 55dc77d3346ba0dc38cf17c40f9d525584d33ed2 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 30 Aug 2026 21:47:04 +0100 Subject: [PATCH 41/44] refactor(wuss): share bevel/backdrop colour validation in wuss_create The backdrop and bevel-colour range check plus its free/free/return cleanup were duplicated across the WUSS_FURNITURE and WUSS_ICONS-only branches and had drifted: the two paths defaulted the bevel colours differently for a NULL config. Extract validate_bevel_backdrop() and default both bevels to 0. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/create.c | 45 +++++++++++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/libraries/wuss/create.c b/libraries/wuss/create.c index 81a653fa..086dd587 100644 --- a/libraries/wuss/create.c +++ b/libraries/wuss/create.c @@ -13,6 +13,24 @@ #include "impl.h" +#if defined(WUSS_FURNITURE) || defined(WUSS_ICONS) +/* Range-check the bevel colours and (when set) the backdrop against the + * palette. Shared by the furniture and icons-only paths so the accepted + * range, the error code and the freed-pointer set stay in one place. */ +static result_t validate_bevel_backdrop(const wuss_t *w, + wuss_colour_t blight, + wuss_colour_t bdark) +{ + if (blight < 0 || blight >= w->npalette || + bdark < 0 || bdark >= w->npalette || + (w->backdrop != wuss_NO_BACKGROUND && + (w->backdrop < 0 || w->backdrop >= w->npalette))) + return result_WUSS_BAD_COLOUR; + + return result_OK; +} +#endif + result_t wuss_create(screen_t *scr, bmfont_t *font, const colour_t *palette, @@ -102,8 +120,8 @@ result_t wuss_create(screen_t *scr, pal.scroll.wells = bg; pal.scroll.sausages = fg; - blight = pal.title.bg; - bdark = pal.title.bg; + blight = 0; + bdark = 0; } if (pal.title.bg < 0 || pal.title.bg >= w->npalette || @@ -115,10 +133,7 @@ result_t wuss_create(screen_t *scr, pal.scroll.arrows < 0 || pal.scroll.arrows >= w->npalette || pal.scroll.wells < 0 || pal.scroll.wells >= w->npalette || pal.scroll.sausages < 0 || pal.scroll.sausages >= w->npalette || - blight < 0 || blight >= w->npalette || - bdark < 0 || bdark >= w->npalette || - (w->backdrop != wuss_NO_BACKGROUND && - (w->backdrop < 0 || w->backdrop >= w->npalette))) + validate_bevel_backdrop(w, blight, bdark) != result_OK) { free(w->palette); free(w); @@ -145,13 +160,6 @@ result_t wuss_create(screen_t *scr, } #else /* !WUSS_FURNITURE */ w->backdrop = (config != NULL) ? config->backdrop : wuss_NO_BACKGROUND; - if (w->backdrop != wuss_NO_BACKGROUND && - (w->backdrop < 0 || w->backdrop >= w->npalette)) - { - free(w->palette); - free(w); - return result_WUSS_BAD_COLOUR; - } #ifdef WUSS_ICONS if (config != NULL) @@ -164,8 +172,7 @@ result_t wuss_create(screen_t *scr, blight = 0; bdark = 0; } - if (blight < 0 || blight >= w->npalette || - bdark < 0 || bdark >= w->npalette) + if (validate_bevel_backdrop(w, blight, bdark) != result_OK) { free(w->palette); free(w); @@ -173,6 +180,14 @@ result_t wuss_create(screen_t *scr, } w->bevel_light = blight; w->bevel_dark = bdark; +#else + if (w->backdrop != wuss_NO_BACKGROUND && + (w->backdrop < 0 || w->backdrop >= w->npalette)) + { + free(w->palette); + free(w); + return result_WUSS_BAD_COLOUR; + } #endif #endif /* WUSS_FURNITURE */ From e82cb39d3b72c05b731a606908fa961dfea7d14a Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 30 Aug 2026 21:47:04 +0100 Subject: [PATCH 42/44] refactor(wuss): replace dead wuss__icon_screen_box with a shared transform wuss__icon_screen_box was declared, compiled and linked with zero callers, re-deriving the content-to-screen bbox transform that wuss__icon_draw open-codes. Drop it for wuss__icon_box_to_screen(content, scroll, bbox, out), which wuss__icon_draw now routes through, so the transform exists once. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/icon.h | 11 +++++++---- libraries/wuss/icon/draw.c | 5 +---- libraries/wuss/icon/screen-box.c | 26 +++++++++++++++----------- 3 files changed, 23 insertions(+), 19 deletions(-) diff --git a/libraries/wuss/icon.h b/libraries/wuss/icon.h index 84e3aead..13c2c67f 100644 --- a/libraries/wuss/icon.h +++ b/libraries/wuss/icon.h @@ -24,10 +24,13 @@ struct wuss_icon int pressed; /* button: 1 while held with the pointer inside */ }; -/* Convert an icon's bbox (virtual document space) to a screen-space box, using - * the owning window's current content box and scroll offset: - * screen = content.x0 - scroll.x + bbox. Mirrors wuss_window_invalidate. */ -void wuss__icon_screen_box(const wuss_icon_t *icon, box_t *out); +/* Map an icon bbox (virtual document space) into screen space: + * screen = content.x0 - scroll.x + bbox. wuss__icon_draw paints through this + * so hit-testing and invalidation cannot drift from what is drawn. */ +void wuss__icon_box_to_screen(const box_t *content, + point_t scroll, + const box_t *bbox, + box_t *out); /* Invalidate exactly this icon's bbox, via wuss_window_invalidate, so a * set_text / pressed-state / hide change repaints just the icon. */ diff --git a/libraries/wuss/icon/draw.c b/libraries/wuss/icon/draw.c index 9fb2b950..2a06be2b 100644 --- a/libraries/wuss/icon/draw.c +++ b/libraries/wuss/icon/draw.c @@ -46,10 +46,7 @@ void wuss__icon_draw(wuss_t *wuss, scr = wuss->scr; - b.x0 = content->x0 - scroll.x + icon->bbox.x0; - b.y0 = content->y0 - scroll.y + icon->bbox.y0; - b.x1 = content->x0 - scroll.x + icon->bbox.x1; - b.y1 = content->y0 - scroll.y + icon->bbox.y1; + wuss__icon_box_to_screen(content, scroll, &icon->bbox, &b); if (b.x1 <= b.x0 || b.y1 <= b.y0) return; diff --git a/libraries/wuss/icon/screen-box.c b/libraries/wuss/icon/screen-box.c index 25440c7e..59e4e4e0 100644 --- a/libraries/wuss/icon/screen-box.c +++ b/libraries/wuss/icon/screen-box.c @@ -1,17 +1,21 @@ /* screen-box.c -- wuss - work-area icon bbox to screen space */ +#include "geom/box.h" +#include "geom/point.h" + #include "../impl.h" -void wuss__icon_screen_box(const wuss_icon_t *icon, box_t *out) +/* Map an icon bbox (virtual document space) into screen space, given the + * owning window's content box and scroll offset. wuss__icon_draw uses this + * for the box it paints into; keeping the transform here means hit-testing + * and invalidation cannot drift from what is drawn. */ +void wuss__icon_box_to_screen(const box_t *content, + point_t scroll, + const box_t *bbox, + box_t *out) { - box_t content; - point_t scroll; - - wuss__content_box(icon->window, &content); - scroll = icon->window->scroll; - - out->x0 = content.x0 - scroll.x + icon->bbox.x0; - out->y0 = content.y0 - scroll.y + icon->bbox.y0; - out->x1 = content.x0 - scroll.x + icon->bbox.x1; - out->y1 = content.y0 - scroll.y + icon->bbox.y1; + out->x0 = content->x0 - scroll.x + bbox->x0; + out->y0 = content->y0 - scroll.y + bbox->y0; + out->x1 = content->x0 - scroll.x + bbox->x1; + out->y1 = content->y0 - scroll.y + bbox->y1; } From 1ca951ab4fd79371954a971abe7ef0d23b8e3c36 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 30 Aug 2026 21:47:04 +0100 Subject: [PATCH 43/44] docs(wuss): correct the screen_copy_rect failure comment in window_move screen_copy_rect also returns 0 for an off-screen source or destination, and the blit pieces are only occlusion-clipped, so a later piece can fail after earlier pieces have already moved pixels. The frame still self-heals via the union fallback; the comment now says so instead of claiming the blit fails identically on the first piece. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/window/move.c | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/libraries/wuss/window/move.c b/libraries/wuss/window/move.c index f9eeb611..a23eaa17 100644 --- a/libraries/wuss/window/move.c +++ b/libraries/wuss/window/move.c @@ -154,10 +154,12 @@ void wuss_window_move(wuss_window_t *window, point_t p) POINT(blit_dest[idx].x0, blit_dest[idx].y0), &copied)) { - /* The screen format doesn't support the blit at all (e.g. paletted): - * this fails identically for every piece, so it fails on the first - * one, before any blit has happened. Bail out; the caller's fallback - * full invalidate repairs the window either way. */ + /* screen_copy_rect refused this piece: either the screen format has no + * blit path (e.g. paletted -- fails on the first piece, before anything + * has moved) or this piece's source/dest lies off-screen, which can + * happen part-way through after earlier pieces have already blitted. + * Either way, bail: the pieces done so far are self-consistent and the + * caller's fallback full invalidate repaints the whole union. */ blit_failed = 1; break; } From fea701586baec88ace718b436be74dc2e12d4972 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 30 Aug 2026 21:50:10 +0100 Subject: [PATCH 44/44] docs: note the ninepatch corner-clamp fix in CHANGELOG Covers fcdeb88; the other commits since e00cabf are docs/refactor/style with no user-facing change. Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b50d2567..cc9b7fb8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -84,3 +84,5 @@ _Unreleased_ until one is cut. - Removed signed-overflow and negative-shift undefined behaviour in the anti-aliased fixed-point line rasteriser, reachable with long or off-screen endpoints. +- `screen_draw_ninepatch()` clamps its corner cells so they no longer overlap + and double-draw when the destination box is smaller than the source corners.