From f1e37104330c8ba5ce94c2635867595ac3932ede Mon Sep 17 00:00:00 2001 From: David Thomas Date: Thu, 10 Sep 2026 11:29:33 +0100 Subject: [PATCH 001/128] chore(bmfonts): namespace fixture fonts with source prefixes Rename the bundled bitmap fonts to carry a provenance prefix: DPT- for the in-house fonts (Digits-Regular, Digits-Bold, Henry, Daydream, CookeTall) and ZX- for GliderRider. Update the bmfont test table, the wuss demo font list and the bmfont doc image links to match. Co-Authored-By: Claude Sonnet 5 --- apps/wuss/main.c | 2 +- docs/framebuf/bmfont.md | 4 ++-- libraries/framebuf/bmfont/test/bmfont-test.c | 12 ++++++------ .../bmfonts/{CookeTall.png => DPT-CookeTall.png} | Bin .../bmfonts/{Daydream.png => DPT-Daydream.png} | Bin .../{Digits-Bold.png => DPT-Digits-Bold.png} | Bin .../{Digits-Regular.png => DPT-Digits-Regular.png} | Bin resources/bmfonts/{Henry.png => DPT-Henry.png} | Bin .../bmfonts/{GliderRider.png => ZX-GliderRider.png} | Bin 9 files changed, 9 insertions(+), 9 deletions(-) rename resources/bmfonts/{CookeTall.png => DPT-CookeTall.png} (100%) rename resources/bmfonts/{Daydream.png => DPT-Daydream.png} (100%) rename resources/bmfonts/{Digits-Bold.png => DPT-Digits-Bold.png} (100%) rename resources/bmfonts/{Digits-Regular.png => DPT-Digits-Regular.png} (100%) rename resources/bmfonts/{Henry.png => DPT-Henry.png} (100%) rename resources/bmfonts/{GliderRider.png => ZX-GliderRider.png} (100%) diff --git a/apps/wuss/main.c b/apps/wuss/main.c index 5a82d8c4..e16c88dc 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -252,7 +252,7 @@ static result_t run_wuss(const char *resources, const char *filename; bmfont_t *fonts[WUSS_MAIN_NFONTS]; static const char *const names[WUSS_MAIN_NFONTS] = - { "Digits-Regular", "Digits-Bold", "Symbols" }; + { "DPT-Digits-Regular", "DPT-Digits-Bold", "Symbols" }; int nfonts; int i; void *pixels; diff --git a/docs/framebuf/bmfont.md b/docs/framebuf/bmfont.md index 025686f7..0cbfcafa 100644 --- a/docs/framebuf/bmfont.md +++ b/docs/framebuf/bmfont.md @@ -2,11 +2,11 @@ "bmfont" is a sub-library of DPTLib for drawing proportionally spaced bitmap fonts. It reads font definitions from PNG files like this: -![Henry Font](../resources/bmfonts/henry.png) +![DPT-Henry Font](../resources/bmfonts/DPT-Henry.png) or this: -![Digits Font](../resources/bmfonts/Digits-Regular.png) +![DPT-Digits Font](../resources/bmfonts/DPT-Digits-Regular.png) or even this: diff --git a/libraries/framebuf/bmfont/test/bmfont-test.c b/libraries/framebuf/bmfont/test/bmfont-test.c index 11c7f25e..849d1228 100644 --- a/libraries/framebuf/bmfont/test/bmfont-test.c +++ b/libraries/framebuf/bmfont/test/bmfont-test.c @@ -187,14 +187,14 @@ bmtestline_t; static bmtestfont_t bmfonts[MAXFONTS] = { - { "Daydream", NULL }, - { "GliderRider", NULL }, + { "DPT-Daydream", NULL }, + { "ZX-GliderRider", NULL }, { "Tiny", NULL }, - { "Henry", NULL }, - { "CookeTall", NULL }, + { "DPT-Henry", NULL }, + { "DPT-CookeTall", NULL }, { "MS Sans Serif", NULL }, - { "Digits-Regular", NULL }, - { "Digits-Bold", NULL } + { "DPT-Digits-Regular", NULL }, + { "DPT-Digits-Bold", NULL } }; /* Fixture PNGs not in bmfonts[] above (which is the Latin-text set the diff --git a/resources/bmfonts/CookeTall.png b/resources/bmfonts/DPT-CookeTall.png similarity index 100% rename from resources/bmfonts/CookeTall.png rename to resources/bmfonts/DPT-CookeTall.png diff --git a/resources/bmfonts/Daydream.png b/resources/bmfonts/DPT-Daydream.png similarity index 100% rename from resources/bmfonts/Daydream.png rename to resources/bmfonts/DPT-Daydream.png diff --git a/resources/bmfonts/Digits-Bold.png b/resources/bmfonts/DPT-Digits-Bold.png similarity index 100% rename from resources/bmfonts/Digits-Bold.png rename to resources/bmfonts/DPT-Digits-Bold.png diff --git a/resources/bmfonts/Digits-Regular.png b/resources/bmfonts/DPT-Digits-Regular.png similarity index 100% rename from resources/bmfonts/Digits-Regular.png rename to resources/bmfonts/DPT-Digits-Regular.png diff --git a/resources/bmfonts/Henry.png b/resources/bmfonts/DPT-Henry.png similarity index 100% rename from resources/bmfonts/Henry.png rename to resources/bmfonts/DPT-Henry.png diff --git a/resources/bmfonts/GliderRider.png b/resources/bmfonts/ZX-GliderRider.png similarity index 100% rename from resources/bmfonts/GliderRider.png rename to resources/bmfonts/ZX-GliderRider.png From e66c62476cfd8a1262a4ea378d6738ebad53fc83 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Thu, 10 Sep 2026 14:31:53 +0100 Subject: [PATCH 002/128] refactor(wuss): rename ICON_TYPE_BUTTON to ACTION, reserve new icon types Rename wuss_ICON_TYPE_BUTTON to wuss_ICON_TYPE_ACTION throughout the icon API, implementation, tests and docs. Add six reserved icon type constants after wuss_ICON_TYPE_RULE: DISPLAY, WRITABLE, NUMBER, STRING_SET, SLIDER and DRAGGABLE. The enum values and from-spec validation exist but no rendering, hit-testing or event routing is wired up yet; a spec using one is accepted and currently draws as a plain wuss_ICON_TYPE_LABEL. Co-Authored-By: Claude Sonnet 5 --- docs/windowing/wuss.md | 8 ++++---- include/wuss/icon.h | 32 ++++++++++++++++++++++++++----- include/wuss/task.h | 2 +- include/wuss/wuss.h | 4 ++-- libraries/wuss/icon/draw.c | 12 +++++++++++- libraries/wuss/icon/from-spec.c | 10 ++++++++-- libraries/wuss/icon/hit-test.c | 2 +- libraries/wuss/test/tasks/icons.c | 6 +++--- 8 files changed, 57 insertions(+), 19 deletions(-) diff --git a/docs/windowing/wuss.md b/docs/windowing/wuss.md index 6261f54f..03ae6052 100644 --- a/docs/windowing/wuss.md +++ b/docs/windowing/wuss.md @@ -124,7 +124,7 @@ 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`. +`wuss_EVENT_ICON` is delivered instead of `wuss_EVENT_MOUSE` whenever the pointer is inside a `wuss_ICON_TYPE_ACTION` 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 @@ -199,13 +199,13 @@ In a redraw callback: start drawing at `bounds.x0 - scroll.x`, `bounds.y0 - scro Taking inspiration from RISC OS, a window can carry **icons**: rectangular UI elements Wuss draws and hit-tests inside the content area. v1 ships two types: - `wuss_ICON_TYPE_LABEL` — static text, optionally in a 1px raised or sunken border (`spec.border` — `wuss_ICON_BORDER_NONE` / `_RIDGE` / `_GROOVE`, the last a RISC OS-style read-only display field). Clicks fall through to the task as `wuss_EVENT_MOUSE`. -- `wuss_ICON_TYPE_BUTTON` — a bevelled rectangle with a centred label and pressed-state feedback (the bevel inverts and the label shifts one pixel down-right while held). Clicks and hovers arrive as `wuss_EVENT_ICON`. +- `wuss_ICON_TYPE_ACTION` — 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, a `border` (label only), and flags. A `wuss_ICON_TYPE_BUTTON` must pass a real `bg`; passing `wuss_NO_BACKGROUND` is rejected with `result_WUSS_BAD_ICON`. An unknown type is also `result_WUSS_BAD_ICON`; an out-of-range `fg`/`bg` is `result_WUSS_BAD_COLOUR`. +- `wuss_icon_create(window, spec, &icon)` — returns an opaque `wuss_icon_t *`. The spec gives the bounding box, type, text (copied; `NULL` treated as `""`), foreground and background palette indices, a `border` (label only), and flags. A `wuss_ICON_TYPE_ACTION` 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`. @@ -273,7 +273,7 @@ Terms as this document and the API use them. Several are RISC OS conventions, wh - **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 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". +- **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_ACTION`. 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`. diff --git a/include/wuss/icon.h b/include/wuss/icon.h index 57ade2b7..09691654 100644 --- a/include/wuss/icon.h +++ b/include/wuss/icon.h @@ -15,7 +15,7 @@ * * 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; + * A click on a wuss_ICON_TYPE_ACTION reaches the task as wuss_EVENT_ICON; * clicks on a label, or on a hidden or disabled icon, fall through as * wuss_EVENT_MOUSE. */ @@ -58,13 +58,13 @@ typedef enum wuss_icon_type /** Bevelled rectangle with a centred text label and pressed-state visual * feedback; clicks and hovers are delivered to the task as * wuss_EVENT_ICON. */ - wuss_ICON_TYPE_BUTTON, + wuss_ICON_TYPE_ACTION, /** Bounding box filled with a repeating two-colour 8x8 tile (spec.pattern) in * fg/bg, phased to document space so it scrolls rigidly with content. Not * interactive: clicks fall through to the task as wuss_EVENT_MOUSE. text is * ignored. */ wuss_ICON_TYPE_PATTERN, - /** A grouping box: a one-pixel rectangle in fg around the bounding box, + /** A grouping box: a bevelled rectangle around the bounding box, * broken at the top-left for an optional caption (text) drawn over the * window background. Not interactive: clicks fall through to the task as * wuss_EVENT_MOUSE. */ @@ -101,7 +101,29 @@ typedef enum wuss_icon_type * window background. Purely decorative: never hit-tested, never highlights. * text, bg and pattern are ignored. Used between menu rows to render the * line a wuss_ICON_FLAGS_SEPARATOR entry sits below. */ - wuss_ICON_TYPE_RULE + wuss_ICON_TYPE_RULE, + + /* The following types are reserved: the enum values and validation exist but + * no rendering, hit-testing or event routing is wired up yet. A spec using + * one is accepted and currently draws as a plain wuss_ICON_TYPE_LABEL. */ + + /** A read-only value field: a bevelled well showing text the task updates but + * the user cannot edit. Not yet implemented. */ + wuss_ICON_TYPE_DISPLAY, + /** An editable single-line text field. Not yet implemented. */ + wuss_ICON_TYPE_WRITABLE, + /** An editable numeric field, optionally with up/down adjusters. Not yet + * implemented. */ + wuss_ICON_TYPE_NUMBER, + /** A field cycling through a fixed set of string values. Not yet + * implemented. */ + wuss_ICON_TYPE_STRING_SET, + /** A slider: a track with a draggable thumb selecting a value in a range. + * Not yet implemented. */ + wuss_ICON_TYPE_SLIDER, + /** A free-drag handle: reports pointer motion to the task while dragged. Not + * yet implemented. */ + wuss_ICON_TYPE_DRAGGABLE } wuss_icon_type_t; @@ -148,7 +170,7 @@ typedef enum wuss_icon_flags /** wuss_ICON_TYPE_LABEL: centre the text in the bounding box. Takes * precedence over wuss_ICON_FLAGS_JUSTIFY_RIGHT. */ wuss_ICON_FLAGS_JUSTIFY_CENTRE = 1 << 3, - /** wuss_ICON_TYPE_BUTTON: draw as a default action button -- an accent fill + /** wuss_ICON_TYPE_ACTION: draw as a default action button -- an accent fill * (see wuss_config_t::accent) inside the same 6px "action" surround as * wuss_ICON_BORDER_ACTION, instead of the ordinary 1px bevel. Ignored by * other icon types. */ diff --git a/include/wuss/task.h b/include/wuss/task.h index e3ea2af2..6c29dde2 100644 --- a/include/wuss/task.h +++ b/include/wuss/task.h @@ -161,7 +161,7 @@ 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. + * pointer is inside a wuss_ICON_TYPE_ACTION 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 diff --git a/include/wuss/wuss.h b/include/wuss/wuss.h index 836a0f88..0f91ac6d 100644 --- a/include/wuss/wuss.h +++ b/include/wuss/wuss.h @@ -353,7 +353,7 @@ typedef struct wuss_config /** * Work-area button colours, as indices into the system palette. bg is the - * button face, used when a wuss_ICON_TYPE_BUTTON spec passes + * button face, used when a wuss_ICON_TYPE_ACTION spec passes * wuss_NO_BACKGROUND rather than its own fill. fg is the button label. * pressed replaces bg on the face while the button is held down. bg and * pressed default to bevel.light and bevel.dark when config is NULL; fg @@ -371,7 +371,7 @@ typedef struct wuss_config button; /** - * Accent colour for a default action button -- a wuss_ICON_TYPE_BUTTON + * Accent colour for a default action button -- a wuss_ICON_TYPE_ACTION * created with wuss_ICON_FLAGS_DEFAULT, drawn to stand out from the * ordinary bevelled buttons around it (RISC OS's "default action button"): * its face fill at rest, and the moat of the wuss_ICON_BORDER_ACTION diff --git a/libraries/wuss/icon/draw.c b/libraries/wuss/icon/draw.c index 954e77a5..be424e69 100644 --- a/libraries/wuss/icon/draw.c +++ b/libraries/wuss/icon/draw.c @@ -672,7 +672,7 @@ void wuss__icon_draw(wuss_t *wuss, wuss__icon_draw_frame(&c); break; - case wuss_ICON_TYPE_BUTTON: + case wuss_ICON_TYPE_ACTION: wuss__icon_draw_button(&c); break; @@ -692,5 +692,15 @@ void wuss__icon_draw(wuss_t *wuss, case wuss_ICON_TYPE_RULE: wuss__icon_draw_rule(&c); break; + + /* reserved types with no renderer yet: fall back to a plain label */ + case wuss_ICON_TYPE_DISPLAY: + case wuss_ICON_TYPE_WRITABLE: + case wuss_ICON_TYPE_NUMBER: + case wuss_ICON_TYPE_STRING_SET: + case wuss_ICON_TYPE_SLIDER: + case wuss_ICON_TYPE_DRAGGABLE: + wuss__icon_draw_label(&c); + break; } } diff --git a/libraries/wuss/icon/from-spec.c b/libraries/wuss/icon/from-spec.c index e1e0852e..3eb66435 100644 --- a/libraries/wuss/icon/from-spec.c +++ b/libraries/wuss/icon/from-spec.c @@ -31,7 +31,7 @@ result_t wuss__icon_from_spec(const wuss_t *w, switch (spec->type) { case wuss_ICON_TYPE_LABEL: - case wuss_ICON_TYPE_BUTTON: + case wuss_ICON_TYPE_ACTION: case wuss_ICON_TYPE_PATTERN: case wuss_ICON_TYPE_FRAME: case wuss_ICON_TYPE_RADIO: @@ -39,13 +39,19 @@ result_t wuss__icon_from_spec(const wuss_t *w, case wuss_ICON_TYPE_BITMAP: case wuss_ICON_TYPE_MENU_ENTRY: case wuss_ICON_TYPE_RULE: + case wuss_ICON_TYPE_DISPLAY: + case wuss_ICON_TYPE_WRITABLE: + case wuss_ICON_TYPE_NUMBER: + case wuss_ICON_TYPE_STRING_SET: + case wuss_ICON_TYPE_SLIDER: + case wuss_ICON_TYPE_DRAGGABLE: break; default: return result_WUSS_BAD_ICON; } - /* a BUTTON may leave bg unset -- it then draws on the config button face + /* an ACTION may leave bg unset -- it then draws on the config button face * (wuss->button_bg); a PATTERN needs a concrete clear-bit colour */ if (spec->type == wuss_ICON_TYPE_PATTERN && bg == wuss_NO_BACKGROUND) return result_WUSS_BAD_ICON; diff --git a/libraries/wuss/icon/hit-test.c b/libraries/wuss/icon/hit-test.c index d734b9c1..023d15e2 100644 --- a/libraries/wuss/icon/hit-test.c +++ b/libraries/wuss/icon/hit-test.c @@ -21,7 +21,7 @@ wuss_icon_t *wuss__icon_hit_test(wuss_window_t *window, point_t doc_point) if (!(it->flags & wuss_ICON_FLAGS_INTERACTIVE)) continue; } - else if (it->type != wuss_ICON_TYPE_BUTTON && + else if (it->type != wuss_ICON_TYPE_ACTION && it->type != wuss_ICON_TYPE_RADIO && it->type != wuss_ICON_TYPE_OPTION && it->type != wuss_ICON_TYPE_MENU_ENTRY) diff --git a/libraries/wuss/test/tasks/icons.c b/libraries/wuss/test/tasks/icons.c index 44cb800e..f9ee3294 100644 --- a/libraries/wuss/test/tasks/icons.c +++ b/libraries/wuss/test/tasks/icons.c @@ -84,7 +84,7 @@ static void icons_add_intro(icons_layout_t *lay, int *button, int *counter) /* 4px larger on every side than a plain button to seat the DEFAULT icon's * 6px action surround */ s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN - 4, lay->y - 4, 88, 30); - s->type = wuss_ICON_TYPE_BUTTON; + s->type = wuss_ICON_TYPE_ACTION; s->text = "Press me"; s->fg = lay->black; s->bg = lay->window; @@ -103,7 +103,7 @@ static void icons_add_intro(icons_layout_t *lay, int *button, int *counter) s = &lay->specs[lay->n]; s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN, 980, 90, 52); - s->type = wuss_ICON_TYPE_BUTTON; + s->type = wuss_ICON_TYPE_ACTION; s->text = "Scrolled"; s->fg = lay->black; s->bg = lay->window; @@ -142,7 +142,7 @@ static void icons_add_buttons(icons_layout_t *lay) s = &lay->specs[lay->n]; s->fg = lay->black; s->bg = lay->window; - s->type = wuss_ICON_TYPE_BUTTON; + s->type = wuss_ICON_TYPE_ACTION; if (col == 0) { From ab8d3b7b042fbb3ca51a2e98ccc6a18cf7a8b0d1 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Thu, 10 Sep 2026 15:46:45 +0100 Subject: [PATCH 003/128] refactor(wuss): drop wuss_icon.window; add per-type data union The icon struct no longer stores a back-pointer to its window. The four public mutators that need the window (delete, set_text, set_hidden, set_selected) now take it as an explicit first argument; getters are unchanged. wuss_icon_get_window is removed. wuss_t gains pressed_window and hover_window companions to pressed_icon / hover_icon so window close/hide ownership checks and the hover prev-invalidate path have a window handle without icon->window. The internal helpers wuss__icon_invalidate / _select / _draw / _set_hover take the window explicitly. struct wuss_icon's mutually-exclusive per-type fields (label border, pattern tile, bitmap image, radio group, menu-entry swatch) move into a named union u, one sub-struct per arm, zeroed then filled by type in wuss__icon_from_spec. TODO: note the gadget taxonomy. Co-Authored-By: Claude Sonnet 5 --- CMakeLists.txt | 1 - docs/windowing/wuss.md | 6 +- include/wuss/icon.h | 34 +++++---- libraries/wuss/TODO.txt | 21 ++++++ libraries/wuss/core/create.c | 2 + libraries/wuss/core/impl.h | 4 + libraries/wuss/core/mouse-click.c | 21 +++--- libraries/wuss/core/mouse-move.c | 15 ++-- libraries/wuss/core/redraw.c | 2 +- libraries/wuss/core/scroll.c | 2 +- libraries/wuss/core/window/close.c | 14 +++- libraries/wuss/core/window/set-hidden.c | 14 +++- libraries/wuss/icon.h | 97 ++++++++++++++++++------- libraries/wuss/icon/create-array.c | 2 +- libraries/wuss/icon/create.c | 3 +- libraries/wuss/icon/delete.c | 19 +++-- libraries/wuss/icon/draw.c | 61 ++++++++-------- libraries/wuss/icon/from-spec.c | 32 ++++++-- libraries/wuss/icon/get-window.c | 8 -- libraries/wuss/icon/invalidate.c | 4 +- libraries/wuss/icon/plot.c | 4 +- libraries/wuss/icon/set-hidden.c | 6 +- libraries/wuss/icon/set-hover.c | 17 +++-- libraries/wuss/icon/set-selected.c | 28 ++++--- libraries/wuss/icon/set-text.c | 8 +- libraries/wuss/menu/menu.c | 14 ++-- libraries/wuss/test/tasks/icons.c | 6 +- 27 files changed, 281 insertions(+), 164 deletions(-) delete mode 100644 libraries/wuss/icon/get-window.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 2c54a5bb..255e14c5 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -437,7 +437,6 @@ set(WUSS_ICON_SOURCES libraries/wuss/icon/get-selected.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/plot.c diff --git a/docs/windowing/wuss.md b/docs/windowing/wuss.md index 03ae6052..66cebc89 100644 --- a/docs/windowing/wuss.md +++ b/docs/windowing/wuss.md @@ -206,9 +206,9 @@ 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, a `border` (label only), and flags. A `wuss_ICON_TYPE_ACTION` 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`. +- `wuss_icon_delete(window, icon)` — NULL-safe. +- `wuss_icon_set_text(window, icon, text)`, `wuss_icon_set_hidden(window, icon, hidden)`, `wuss_icon_set_selected(window, icon, selected)`. +- Getters: `wuss_icon_get_bbox`, `wuss_icon_get_type`, `wuss_icon_get_text` (never `NULL`), `wuss_icon_get_selected`. Flags: `wuss_ICON_FLAGS_HIDDEN` (not drawn, not hit-tested) and `wuss_ICON_FLAGS_DISABLED` (drawn greyed; clicks fall through as `wuss_EVENT_MOUSE`). diff --git a/include/wuss/icon.h b/include/wuss/icon.h index 09691654..2960c84f 100644 --- a/include/wuss/icon.h +++ b/include/wuss/icon.h @@ -338,31 +338,38 @@ result_t wuss_icon_plot(wuss_window_t *window, /** * Destroy an icon, unlinking it from its window and invalidating its - * bounding box so the next redraw clears it. Safe to pass NULL. + * bounding box so the next redraw clears it. Safe to pass NULL for \p icon. * - * \param[in] icon Icon to destroy, or NULL. + * \param[in] window Window the icon belongs to. + * \param[in] icon Icon to destroy, or NULL. */ -void wuss_icon_delete(wuss_icon_t *icon); +void wuss_icon_delete(wuss_window_t *window, 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 "". + * \param[in] window Window the icon belongs to. + * \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); +result_t wuss_icon_set_text(wuss_window_t *window, + 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] window Window the icon belongs to. * \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); +void wuss_icon_set_hidden(wuss_window_t *window, + wuss_icon_t *icon, + int hidden); /** * Fetch an icon's bounding box, in virtual document space. @@ -389,14 +396,6 @@ wuss_icon_type_t wuss_icon_get_type(const wuss_icon_t *icon); */ 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); - /** * Fetch a radio or option icon's selected (latched) state. * @@ -414,10 +413,13 @@ int wuss_icon_get_selected(const wuss_icon_t *icon); * programmatic path, distinct from a user click. A no-op for icon types with * no latched state. * + * \param[in] window Window the icon belongs to. * \param[in] icon Icon to change. * \param[in] selected Non-zero to select, zero to deselect. */ -void wuss_icon_set_selected(wuss_icon_t *icon, int selected); +void wuss_icon_set_selected(wuss_window_t *window, + wuss_icon_t *icon, + int selected); /* ----------------------------------------------------------------------- */ diff --git a/libraries/wuss/TODO.txt b/libraries/wuss/TODO.txt index d39e91c5..b4415871 100644 --- a/libraries/wuss/TODO.txt +++ b/libraries/wuss/TODO.txt @@ -21,6 +21,27 @@ LAYER 1 - CORE PLUMBING (no wuss deps; unblocks everything else) blocks: text abstraction, window/menu templates - Is having font handling in wuss-core a bad thing? Decide arch now. blocks: Wuss text abstraction +- Icon type taxonomy +- Window layout / constraint system / stacking primitives? + + +Toolbox (Window) Gadgets: +Label +Action +Display +Writable +Number +String Set +Labelled Box +Option +Radio -- should be a set +GRight -- need? +Slider (H/V) +Draggable +Button + +Menu entries are an extra thing + LAYER 2 - TEXT & FONTS (needs UTF-8, font-arch decision) diff --git a/libraries/wuss/core/create.c b/libraries/wuss/core/create.c index b75fd87f..0b8547c4 100644 --- a/libraries/wuss/core/create.c +++ b/libraries/wuss/core/create.c @@ -329,7 +329,9 @@ result_t wuss_create(screen_t *scr, #endif #ifdef WUSS_ICONS w->pressed_icon = NULL; + w->pressed_window = NULL; w->hover_icon = NULL; + w->hover_window = NULL; w->icon.names = NULL; w->icon.atoms = NULL; w->icon.bitmaps = NULL; diff --git a/libraries/wuss/core/impl.h b/libraries/wuss/core/impl.h index 30ebc4eb..d8c83c53 100644 --- a/libraries/wuss/core/impl.h +++ b/libraries/wuss/core/impl.h @@ -160,10 +160,14 @@ struct wuss * idle; released on any MOUSE_UP * even if a new window now covers * its owner */ + wuss_window_t *pressed_window; /* the window pressed_icon is on; + * NULL iff pressed_icon is NULL */ wuss_icon_t *hover_icon; /* icon the pointer is currently * over, NULL when none; drives * hover-highlight repaint of * menu-entry icons */ + wuss_window_t *hover_window; /* the window hover_icon is on; + * NULL iff hover_icon is NULL */ /* Icon set loaded by wuss_icons_load. index i is the i-th ".png" the * directory scan yielded (order unspecified -- address by name). * names interns the leafnames-sans-".png"; atoms[i] is entry i's atom diff --git a/libraries/wuss/core/mouse-click.c b/libraries/wuss/core/mouse-click.c index a02ebdbd..cb20206c 100644 --- a/libraries/wuss/core/mouse-click.c +++ b/libraries/wuss/core/mouse-click.c @@ -26,7 +26,7 @@ result_t wuss_mouse_click(wuss_t *wuss, if (action == wuss_MOUSE_UP && wuss->pressed_icon != NULL) { wuss_icon_t *pressed = wuss->pressed_icon; - wuss_window_t *presswin = pressed->window; + wuss_window_t *presswin = wuss->pressed_window; box_t content; point_t doc_point; int still_over; @@ -40,11 +40,12 @@ result_t wuss_mouse_click(wuss_t *wuss, if (!still_over) { - wuss->pressed_icon = NULL; + wuss->pressed_icon = NULL; + wuss->pressed_window = NULL; if (wuss__icon_pressed(pressed)) { wuss__icon_set_state(pressed, wuss_ICON_STATE_PRESSED, 0); - wuss__icon_invalidate(pressed); + wuss__icon_invalidate(presswin, pressed); } } } @@ -277,21 +278,23 @@ result_t wuss_mouse_click(wuss_t *wuss, (button & (wuss_BUTTON_SELECT | wuss_BUTTON_ADJUST))) { wuss__icon_set_state(icon, wuss_ICON_STATE_PRESSED, 1); - wuss->pressed_icon = icon; - wuss__icon_invalidate(icon); + wuss->pressed_icon = icon; + wuss->pressed_window = win; + wuss__icon_invalidate(win, icon); } else if (action == wuss_MOUSE_UP && wuss__icon_pressed(icon)) { wuss__icon_set_state(icon, wuss_ICON_STATE_PRESSED, 0); - wuss->pressed_icon = NULL; - wuss__icon_invalidate(icon); + wuss->pressed_icon = NULL; + wuss->pressed_window = NULL; + wuss__icon_invalidate(win, icon); /* a completed click latches radio/option state before the task is * told, so the wuss_EVENT_ICON handler sees the new value */ if (icon->type == wuss_ICON_TYPE_OPTION) - wuss__icon_select(icon, !wuss__icon_selected(icon)); + wuss__icon_select(win, icon, !wuss__icon_selected(icon)); else if (icon->type == wuss_ICON_TYPE_RADIO) - wuss__icon_select(icon, + wuss__icon_select(win, icon, (button & wuss_BUTTON_ADJUST) ? !wuss__icon_selected(icon) : 1); } diff --git a/libraries/wuss/core/mouse-move.c b/libraries/wuss/core/mouse-move.c index b0e5b24f..702fe21e 100644 --- a/libraries/wuss/core/mouse-move.c +++ b/libraries/wuss/core/mouse-move.c @@ -55,7 +55,7 @@ result_t wuss_mouse_move(wuss_t *wuss, point_t p, wuss_window_t **hit) if (win == NULL) { #ifdef WUSS_ICONS - wuss__icon_set_hover(wuss, NULL); + wuss__icon_set_hover(wuss, NULL, NULL); #endif return result_OK; } @@ -64,7 +64,7 @@ result_t wuss_mouse_move(wuss_t *wuss, point_t p, wuss_window_t **hit) if (wuss->furniture_ops->hit_test(win, POINT(x, y)) != wuss_FURNITURE_CONTENT) { #ifdef WUSS_ICONS - wuss__icon_set_hover(wuss, NULL); + wuss__icon_set_hover(wuss, NULL, NULL); #endif return result_OK; } @@ -73,7 +73,7 @@ result_t wuss_mouse_move(wuss_t *wuss, point_t p, wuss_window_t **hit) if (win->task->handle == NULL) { #ifdef WUSS_ICONS - wuss__icon_set_hover(wuss, NULL); + wuss__icon_set_hover(wuss, NULL, NULL); #endif return result_OK; } @@ -94,7 +94,7 @@ result_t wuss_mouse_move(wuss_t *wuss, point_t p, wuss_window_t **hit) icon = wuss__icon_hit_test(win, doc_point); - wuss__icon_set_hover(wuss, icon); + wuss__icon_set_hover(wuss, win, 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 @@ -108,8 +108,11 @@ result_t wuss_mouse_move(wuss_t *wuss, point_t p, wuss_window_t **hit) { wuss__icon_set_state(it, wuss_ICON_STATE_PRESSED, 0); if (wuss->pressed_icon == it) - wuss->pressed_icon = NULL; - wuss__icon_invalidate(it); + { + wuss->pressed_icon = NULL; + wuss->pressed_window = NULL; + } + wuss__icon_invalidate(win, it); } } diff --git a/libraries/wuss/core/redraw.c b/libraries/wuss/core/redraw.c index d06481a6..71dd7843 100644 --- a/libraries/wuss/core/redraw.c +++ b/libraries/wuss/core/redraw.c @@ -112,7 +112,7 @@ static void redraw_window(wuss_t *wuss, /* 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); + wuss__icon_draw(wuss, win, win->icons[k], &content, win->scroll); } #endif } diff --git a/libraries/wuss/core/scroll.c b/libraries/wuss/core/scroll.c index 45dff36d..b4f3c111 100644 --- a/libraries/wuss/core/scroll.c +++ b/libraries/wuss/core/scroll.c @@ -18,7 +18,7 @@ static void wuss__scroll_rehover(wuss_t *wuss, doc_point.x = screen_point.x - content.x0 + win->scroll.x; doc_point.y = screen_point.y - content.y0 + win->scroll.y; - wuss__icon_set_hover(wuss, wuss__icon_hit_test(win, doc_point)); + wuss__icon_set_hover(wuss, win, wuss__icon_hit_test(win, doc_point)); #else (void) wuss; (void) win; diff --git a/libraries/wuss/core/window/close.c b/libraries/wuss/core/window/close.c index 5563468e..3263fcc6 100644 --- a/libraries/wuss/core/window/close.c +++ b/libraries/wuss/core/window/close.c @@ -24,10 +24,16 @@ void wuss_window_close(wuss_window_t *doomed) wuss->furniture.dragging = NULL; #endif #ifdef WUSS_ICONS - if (wuss->pressed_icon != NULL && wuss->pressed_icon->window == doomed) - wuss->pressed_icon = NULL; - if (wuss->hover_icon != NULL && wuss->hover_icon->window == doomed) - wuss->hover_icon = NULL; + if (wuss->pressed_window == doomed) + { + wuss->pressed_icon = NULL; + wuss->pressed_window = NULL; + } + if (wuss->hover_window == doomed) + { + wuss->hover_icon = NULL; + wuss->hover_window = NULL; + } #endif wuss__release_packed(doomed); diff --git a/libraries/wuss/core/window/set-hidden.c b/libraries/wuss/core/window/set-hidden.c index f1b9467f..0bc14fff 100644 --- a/libraries/wuss/core/window/set-hidden.c +++ b/libraries/wuss/core/window/set-hidden.c @@ -31,10 +31,16 @@ result_t wuss_window_set_hidden(wuss_window_t *window, int hidden) } #endif #ifdef WUSS_ICONS - if (wuss->pressed_icon != NULL && wuss->pressed_icon->window == window) - wuss->pressed_icon = NULL; - if (wuss->hover_icon != NULL && wuss->hover_icon->window == window) - wuss->hover_icon = NULL; + if (wuss->pressed_window == window) + { + wuss->pressed_icon = NULL; + wuss->pressed_window = NULL; + } + if (wuss->hover_window == window) + { + wuss->hover_icon = NULL; + wuss->hover_window = NULL; + } #endif /* still on screen: repaint its footprint now, then mark it gone */ diff --git a/libraries/wuss/icon.h b/libraries/wuss/icon.h index b553eb35..8f56897d 100644 --- a/libraries/wuss/icon.h +++ b/libraries/wuss/icon.h @@ -24,21 +24,60 @@ typedef enum wuss_icon_state } wuss_icon_state_t; +/* Per-type payload. Exactly one arm is live, selected by wuss_icon::type; + * wuss__icon_from_spec zeroes the whole union then fills the arm for the + * icon's type. Types with no type-specific data (ACTION, FRAME, OPTION, + * RULE, and the reserved types) touch no arm. Each arm is its own struct so + * a type can gain fields without disturbing the others. */ +typedef union wuss_icon_data +{ + /* wuss_ICON_TYPE_LABEL */ + struct + { + wuss_icon_border_t border; /* inside-bbox border; NONE otherwise */ + } + label; + + /* wuss_ICON_TYPE_PATTERN */ + struct + { + screen_pattern_t tile; /* repeating preset tile */ + } + pattern; + + /* wuss_ICON_TYPE_BITMAP */ + struct + { + const bitmap_t *image; /* borrowed, never owned */ + } + bitmap; + + /* wuss_ICON_TYPE_RADIO */ + struct + { + int group; /* exclusive-selection group; 0 = none */ + } + radio; + + /* wuss_ICON_TYPE_MENU_ENTRY */ + struct + { + wuss_colour_t swatch; /* FLAGS_SWATCH left-gutter chip colour; + * wuss_NO_BACKGROUND otherwise */ + } + menu_entry; +} +wuss_icon_data_t; + struct wuss_icon { - wuss_window_t *window; /* owner; back-pointer for invalidate/get_window */ - box_t bbox; /* virtual document space */ + box_t bbox; /* in virtual document space */ wuss_icon_type_t type; char *text; /* owned; never NULL ("" instead) */ wuss_colour_t fg, bg; - screen_pattern_t pattern; /* wuss_ICON_TYPE_PATTERN tile; 0 otherwise */ - const bitmap_t *bitmap; /* wuss_ICON_TYPE_BITMAP image; borrowed, or NULL */ - int group; /* radio: exclusive-selection group; 0 = none */ - wuss_colour_t swatch; /* menu entry + FLAGS_SWATCH: left-gutter chip - * colour; wuss_NO_BACKGROUND otherwise */ - wuss_icon_border_t border; /* label: inside-bbox border; NONE otherwise */ wuss_icon_flags_t flags; wuss_icon_state_t state; + wuss_icon_data_t u; /* per-type payload, selected by type */ }; static inline int wuss__icon_pressed(const wuss_icon_t *icon) @@ -67,14 +106,20 @@ static inline void wuss__icon_set_state(wuss_icon_t *icon, } /* Set icon->selected, invalidating it. For a radio with a non-zero group, - * selecting it also clears every other selected radio on the same window with - * that group. Ignored for types with no latched state. */ -void wuss__icon_select(wuss_icon_t *icon, int selected); - -/* Make "icon" (may be NULL) the hovered icon: clears the hovered flag on the - * previous wuss->hover_icon and sets it on the new one, invalidating whichever - * of the two changed. A no-op if nothing changed. */ -void wuss__icon_set_hover(wuss_t *wuss, wuss_icon_t *icon); + * selecting it also clears every other selected radio on "window" with that + * group. Ignored for types with no latched state. "icon" must be an icon of + * "window". */ +void wuss__icon_select(wuss_window_t *window, + wuss_icon_t *icon, + int selected); + +/* Make "icon" (an icon of "window", or NULL) the hovered icon: clears the + * hovered flag on the previous wuss->hover_icon and sets it on the new one, + * invalidating whichever of the two changed. "window" is ignored when "icon" + * is NULL. A no-op if nothing changed. */ +void wuss__icon_set_hover(wuss_t *wuss, + wuss_window_t *window, + wuss_icon_t *icon); /* Map an icon bbox (virtual document space) into screen space: * screen = content.x0 - scroll.x + bbox. wuss__icon_draw paints through this @@ -84,13 +129,14 @@ void wuss__icon_box_to_screen(const box_t *content, 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. */ -void wuss__icon_invalidate(const wuss_icon_t *icon); +/* Invalidate exactly this icon's bbox on "window", via wuss_window_invalidate, + * so a set_text / pressed-state / hide change repaints just the icon. "icon" + * must be an icon of "window". */ +void wuss__icon_invalidate(wuss_window_t *window, const wuss_icon_t *icon); /* Validate a spec against the palette and fill "out" with a detached icon (no - * window, no owned text -- out->text is aliased to spec->text or ""). Shared - * by wuss_icon_create and wuss_icon_plot. Returns result_WUSS_BAD_ICON / + * owned text -- out->text is aliased to spec->text or ""). Shared by + * wuss_icon_create and wuss_icon_plot. Returns result_WUSS_BAD_ICON / * result_WUSS_BAD_COLOUR as wuss_icon_create documents, else result_OK. */ result_t wuss__icon_from_spec(const wuss_t *wuss, const wuss_icon_spec_t *spec, @@ -100,10 +146,11 @@ result_t wuss__icon_from_spec(const wuss_t *wuss, * 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); +void wuss__icon_draw(wuss_t *wuss, + const wuss_window_t *window, + 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 diff --git a/libraries/wuss/icon/create-array.c b/libraries/wuss/icon/create-array.c index 879b4104..4fcd3438 100644 --- a/libraries/wuss/icon/create-array.c +++ b/libraries/wuss/icon/create-array.c @@ -31,7 +31,7 @@ result_t wuss_icon_create_array(wuss_window_t *window, /* 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]); + wuss_icon_delete(window, window->icons[window->nicons - 1]); return rc; } diff --git a/libraries/wuss/icon/create.c b/libraries/wuss/icon/create.c index be9ef715..ca4782f3 100644 --- a/libraries/wuss/icon/create.c +++ b/libraries/wuss/icon/create.c @@ -47,7 +47,6 @@ result_t wuss_icon_create(wuss_window_t *window, return result_OOM; *it = scratch; /* scratch.text aliases spec->text; replaced with an owned copy below */ - it->window = window; src = (spec->text != NULL) ? spec->text : ""; len = strlen(src); @@ -61,7 +60,7 @@ result_t wuss_icon_create(wuss_window_t *window, window->icons[window->nicons++] = it; - wuss__icon_invalidate(it); + wuss__icon_invalidate(window, it); if (icon != NULL) *icon = it; diff --git a/libraries/wuss/icon/delete.c b/libraries/wuss/icon/delete.c index cc3f1be1..c796a743 100644 --- a/libraries/wuss/icon/delete.c +++ b/libraries/wuss/icon/delete.c @@ -8,22 +8,25 @@ #include "../core/impl.h" -void wuss_icon_delete(wuss_icon_t *icon) +void wuss_icon_delete(wuss_window_t *window, wuss_icon_t *icon) { - wuss_window_t *window; - int i; + int i; if (icon == NULL) return; - window = icon->window; - if (window->wuss->pressed_icon == icon) - window->wuss->pressed_icon = NULL; + { + window->wuss->pressed_icon = NULL; + window->wuss->pressed_window = NULL; + } if (window->wuss->hover_icon == icon) - window->wuss->hover_icon = NULL; + { + window->wuss->hover_icon = NULL; + window->wuss->hover_window = NULL; + } - wuss__icon_invalidate(icon); + wuss__icon_invalidate(window, icon); for (i = 0; i < window->nicons; i++) { diff --git a/libraries/wuss/icon/draw.c b/libraries/wuss/icon/draw.c index be424e69..0c3b731a 100644 --- a/libraries/wuss/icon/draw.c +++ b/libraries/wuss/icon/draw.c @@ -21,14 +21,15 @@ * draw. */ typedef struct icon_draw_ctx { - wuss_t *wuss; - screen_t *scr; - const wuss_icon_t *icon; - bmfont_t *font; - box_t b; - colour_t fg; - int font_height; - int have_font; + wuss_t *wuss; + const wuss_window_t *window; + screen_t *scr; + const wuss_icon_t *icon; + bmfont_t *font; + box_t b; + colour_t fg; + int font_height; + int have_font; } icon_draw_ctx_t; @@ -103,10 +104,10 @@ static colour_t icon_blend_ground(const icon_draw_ctx_t *c, if (icon->bg != wuss_NO_BACKGROUND) return c->wuss->palette[icon->bg]; - if (icon->window->bg.colour != wuss_NO_BACKGROUND) - return (icon->window->bg.pattern != screen_PATTERN_SOLID) - ? c->wuss->palette[icon->window->bg.pattern_bg] - : c->wuss->palette[icon->window->bg.colour]; + if (c->window->bg.colour != wuss_NO_BACKGROUND) + return (c->window->bg.pattern != screen_PATTERN_SOLID) + ? c->wuss->palette[c->window->bg.pattern_bg] + : c->wuss->palette[c->window->bg.colour]; return fallback; } @@ -159,7 +160,7 @@ static void wuss__icon_draw_pattern(const icon_draw_ctx_t *c, { pattern_t pat; - pat = pattern_from_preset(icon->pattern, + pat = pattern_from_preset(icon->u.pattern.tile, pat_fg, c->wuss->palette[icon->bg]); pat.origin = POINT(content->x0 - scroll.x, content->y0 - scroll.y); screen_fill_pattern(c->scr, &c->b, &pat); @@ -187,7 +188,7 @@ static void wuss__icon_draw_label(const icon_draw_ctx_t *c) bg = icon_blend_ground(c, c->fg); } - if (icon->border != wuss_ICON_BORDER_NONE) + if (icon->u.label.border != wuss_ICON_BORDER_NONE) { colour_t light, dark, accent, divider; @@ -196,11 +197,11 @@ static void wuss__icon_draw_label(const icon_draw_ctx_t *c) accent = c->wuss->palette[c->wuss->accent]; /* the action-button fill */ divider = c->wuss->palette[c->wuss->bevel_divider]; - if (icon->border == wuss_ICON_BORDER_ACTION) + if (icon->u.label.border == wuss_ICON_BORDER_ACTION) { icon_draw_action_border(c->scr, b, light, dark, accent, 0); } - else if (icon->border == wuss_ICON_BORDER_DIVIDER) + else if (icon->u.label.border == wuss_ICON_BORDER_DIVIDER) { icon_draw_divider_border(c->scr, b, light, divider); } @@ -210,7 +211,7 @@ static void wuss__icon_draw_label(const icon_draw_ctx_t *c) * bevel ring. */ box_t ring = *b; - if (icon->border == wuss_ICON_BORDER_RIDGE) + if (icon->u.label.border == wuss_ICON_BORDER_RIDGE) screen_draw_bevel_edge(c->scr, &ring, light, dark); else screen_draw_bevel_edge(c->scr, &ring, dark, light); @@ -485,7 +486,7 @@ static void wuss__icon_draw_bitmap(const icon_draw_ctx_t *c) const box_t *b = &c->b; screen_t clipped; - if (c->icon->bitmap == NULL) + if (c->icon->u.bitmap.image == NULL) return; /* screen_copy_bitmap clips to scr->clip and does not scale, so narrow the @@ -495,7 +496,7 @@ static void wuss__icon_draw_bitmap(const icon_draw_ctx_t *c) if (box_intersection(&c->scr->clip, b, &clipped.clip)) return; - screen_copy_bitmap(&clipped, b->x0, b->y0, c->icon->bitmap); + screen_copy_bitmap(&clipped, b->x0, b->y0, c->icon->u.bitmap.image); } /* ----------------------------------------------------------------------- */ @@ -540,7 +541,7 @@ static void wuss__icon_draw_menu_entry(const icon_draw_ctx_t *c) /* left-edge colour chip (wins over the tick) or tick when selected */ if ((icon->flags & wuss_ICON_FLAGS_SWATCH) && - icon->swatch != wuss_NO_BACKGROUND) + icon->u.menu_entry.swatch != wuss_NO_BACKGROUND) { int cx, cy, h; @@ -548,7 +549,7 @@ static void wuss__icon_draw_menu_entry(const icon_draw_ctx_t *c) cx = b->x0 + pad; cy = b->y0 + (b->y1 - b->y0 - h) / 2; screen_fill_rect(c->scr, cx, cy, SIZE2D(h, h), - c->wuss->palette[icon->swatch]); + c->wuss->palette[icon->u.menu_entry.swatch]); screen_draw_rect(c->scr, cx, cy, SIZE2D(h, h), ink); /* 1px border */ } else if (wuss__icon_selected(icon)) @@ -625,10 +626,11 @@ static void wuss__icon_draw_rule(const icon_draw_ctx_t *c) /* ----------------------------------------------------------------------- */ -void wuss__icon_draw(wuss_t *wuss, - const wuss_icon_t *icon, - const box_t *content, - point_t scroll) +void wuss__icon_draw(wuss_t *wuss, + const wuss_window_t *window, + const wuss_icon_t *icon, + const box_t *content, + point_t scroll) { icon_draw_ctx_t c; int fontidx; @@ -641,10 +643,11 @@ void wuss__icon_draw(wuss_t *wuss, if (box_is_empty(&c.b)) return; - c.wuss = wuss; - c.scr = wuss->scr; - c.icon = icon; - c.fg = wuss->palette[icon->fg]; + c.wuss = wuss; + c.window = window; + c.scr = wuss->scr; + c.icon = icon; + c.fg = wuss->palette[icon->fg]; /* pick the icon's requested weight; fall back to the system font */ fontidx = wuss_ICON_FONT_OF(icon->flags); diff --git a/libraries/wuss/icon/from-spec.c b/libraries/wuss/icon/from-spec.c index 3eb66435..c2d2199c 100644 --- a/libraries/wuss/icon/from-spec.c +++ b/libraries/wuss/icon/from-spec.c @@ -1,6 +1,7 @@ /* wuss/icon/from-spec.c -- validate an icon spec into a detached icon */ #include +#include #ifdef FORTIFY #include "fortify/fortify.h" @@ -87,19 +88,34 @@ result_t wuss__icon_from_spec(const wuss_t *w, if (has_swatch && swatch >= w->npalette) return result_WUSS_BAD_COLOUR; - out->window = NULL; out->bbox = spec->bbox; out->type = spec->type; out->text = (char *) ((spec->text != NULL) ? spec->text : ""); out->fg = fg; out->bg = bg; - out->pattern = (spec->type == wuss_ICON_TYPE_PATTERN) ? spec->pattern - : screen_PATTERN_SOLID; - out->bitmap = (spec->type == wuss_ICON_TYPE_BITMAP) ? bitmap : NULL; - out->group = (spec->type == wuss_ICON_TYPE_RADIO) ? spec->group : 0; - out->swatch = swatch; - out->border = (spec->type == wuss_ICON_TYPE_LABEL) ? spec->border - : wuss_ICON_BORDER_NONE; + + memset(&out->u, 0, sizeof(out->u)); + switch (spec->type) + { + case wuss_ICON_TYPE_LABEL: + out->u.label.border = spec->border; + break; + case wuss_ICON_TYPE_PATTERN: + out->u.pattern.tile = spec->pattern; + break; + case wuss_ICON_TYPE_BITMAP: + out->u.bitmap.image = bitmap; + break; + case wuss_ICON_TYPE_RADIO: + out->u.radio.group = spec->group; + break; + case wuss_ICON_TYPE_MENU_ENTRY: + out->u.menu_entry.swatch = swatch; + break; + default: + break; + } + out->flags = spec->flags; out->state = wuss_ICON_STATE_NONE; diff --git a/libraries/wuss/icon/get-window.c b/libraries/wuss/icon/get-window.c deleted file mode 100644 index a5e41e6d..00000000 --- a/libraries/wuss/icon/get-window.c +++ /dev/null @@ -1,8 +0,0 @@ -/* wuss/icon/get-window.c -- read a work-area icon's owning window */ - -#include "../core/impl.h" - -wuss_window_t *wuss_icon_get_window(const wuss_icon_t *icon) -{ - return icon->window; -} diff --git a/libraries/wuss/icon/invalidate.c b/libraries/wuss/icon/invalidate.c index 156c5747..1ea11534 100644 --- a/libraries/wuss/icon/invalidate.c +++ b/libraries/wuss/icon/invalidate.c @@ -2,9 +2,9 @@ #include "../core/impl.h" -void wuss__icon_invalidate(const wuss_icon_t *icon) +void wuss__icon_invalidate(wuss_window_t *window, 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); + wuss_window_invalidate(window, &icon->bbox); } diff --git a/libraries/wuss/icon/plot.c b/libraries/wuss/icon/plot.c index caa1c4b8..5575db6d 100644 --- a/libraries/wuss/icon/plot.c +++ b/libraries/wuss/icon/plot.c @@ -24,9 +24,7 @@ result_t wuss_icon_plot(wuss_window_t *window, if (rc != result_OK) return rc; - scratch.window = window; - - wuss__icon_draw(window->wuss, &scratch, content, scroll); + wuss__icon_draw(window->wuss, window, &scratch, content, scroll); return result_OK; } diff --git a/libraries/wuss/icon/set-hidden.c b/libraries/wuss/icon/set-hidden.c index 996c2fdc..08be7a87 100644 --- a/libraries/wuss/icon/set-hidden.c +++ b/libraries/wuss/icon/set-hidden.c @@ -2,12 +2,14 @@ #include "../core/impl.h" -void wuss_icon_set_hidden(wuss_icon_t *icon, int hidden) +void wuss_icon_set_hidden(wuss_window_t *window, + 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); + wuss__icon_invalidate(window, icon); } diff --git a/libraries/wuss/icon/set-hover.c b/libraries/wuss/icon/set-hover.c index 090c91d0..cec40d17 100644 --- a/libraries/wuss/icon/set-hover.c +++ b/libraries/wuss/icon/set-hover.c @@ -12,11 +12,15 @@ static int wuss__icon_hover_visible(const wuss_icon_t *icon) return icon->type == wuss_ICON_TYPE_MENU_ENTRY; } -void wuss__icon_set_hover(wuss_t *wuss, wuss_icon_t *icon) +void wuss__icon_set_hover(wuss_t *wuss, + wuss_window_t *window, + wuss_icon_t *icon) { - wuss_icon_t *prev; + wuss_icon_t *prev; + wuss_window_t *prevwin; - prev = wuss->hover_icon; + prev = wuss->hover_icon; + prevwin = wuss->hover_window; if (prev == icon) return; @@ -31,15 +35,16 @@ void wuss__icon_set_hover(wuss_t *wuss, wuss_icon_t *icon) { wuss__icon_set_state(prev, wuss_ICON_STATE_HOVERED, 0); if (wuss__icon_hover_visible(prev)) - wuss__icon_invalidate(prev); + wuss__icon_invalidate(prevwin, prev); } - wuss->hover_icon = icon; + wuss->hover_icon = icon; + wuss->hover_window = (icon != NULL) ? window : NULL; if (icon != NULL) { wuss__icon_set_state(icon, wuss_ICON_STATE_HOVERED, 1); if (wuss__icon_hover_visible(icon)) - wuss__icon_invalidate(icon); + wuss__icon_invalidate(window, icon); } } diff --git a/libraries/wuss/icon/set-selected.c b/libraries/wuss/icon/set-selected.c index 712aee57..db42b2a2 100644 --- a/libraries/wuss/icon/set-selected.c +++ b/libraries/wuss/icon/set-selected.c @@ -4,13 +4,15 @@ #include "../core/impl.h" -void wuss__icon_select(wuss_icon_t *icon, int selected) +void wuss__icon_select(wuss_window_t *window, + wuss_icon_t *icon, + int selected) { - wuss_window_t *window; - wuss_icon_t *other; - int i; + wuss_icon_t *other; + int i; - assert(icon != NULL); + assert(window != NULL); + assert(icon != NULL); if (icon->type != wuss_ICON_TYPE_RADIO && icon->type != wuss_ICON_TYPE_OPTION && @@ -22,20 +24,20 @@ void wuss__icon_select(wuss_icon_t *icon, int selected) /* selecting a grouped radio clears its siblings first */ if (selected && icon->type == wuss_ICON_TYPE_RADIO && - icon->group != 0) + icon->u.radio.group != 0) { - window = icon->window; for (i = 0; i < window->nicons; i++) { other = window->icons[i]; if (other == icon) continue; - if (other->type != wuss_ICON_TYPE_RADIO || other->group != icon->group) + if (other->type != wuss_ICON_TYPE_RADIO || + other->u.radio.group != icon->u.radio.group) continue; if (!wuss__icon_selected(other)) continue; wuss__icon_set_state(other, wuss_ICON_STATE_SELECTED, 0); - wuss__icon_invalidate(other); + wuss__icon_invalidate(window, other); } } @@ -43,10 +45,12 @@ void wuss__icon_select(wuss_icon_t *icon, int selected) return; wuss__icon_set_state(icon, wuss_ICON_STATE_SELECTED, selected); - wuss__icon_invalidate(icon); + wuss__icon_invalidate(window, icon); } -void wuss_icon_set_selected(wuss_icon_t *icon, int selected) +void wuss_icon_set_selected(wuss_window_t *window, + wuss_icon_t *icon, + int selected) { - wuss__icon_select(icon, selected); + wuss__icon_select(window, icon, selected); } diff --git a/libraries/wuss/icon/set-text.c b/libraries/wuss/icon/set-text.c index 35c8fcce..1a565ef9 100644 --- a/libraries/wuss/icon/set-text.c +++ b/libraries/wuss/icon/set-text.c @@ -9,14 +9,16 @@ #include "../core/impl.h" -result_t wuss_icon_set_text(wuss_icon_t *icon, const char *text) +result_t wuss_icon_set_text(wuss_window_t *window, + wuss_icon_t *icon, + const char *text) { wuss_t *w; const char *src; char *dup; size_t len; - w = icon->window->wuss; + w = window->wuss; src = (text != NULL) ? text : ""; len = strlen(src); @@ -28,7 +30,7 @@ result_t wuss_icon_set_text(wuss_icon_t *icon, const char *text) wuss__free(w, icon->text); icon->text = dup; - wuss__icon_invalidate(icon); + wuss__icon_invalidate(window, icon); return result_OK; } diff --git a/libraries/wuss/menu/menu.c b/libraries/wuss/menu/menu.c index 6124b6c0..edb6e0f1 100644 --- a/libraries/wuss/menu/menu.c +++ b/libraries/wuss/menu/menu.c @@ -314,7 +314,7 @@ static result_t wuss__menu_handle(wuss_window_t *window, if (was_parent != NULL && was_parent != icon) { wuss__icon_set_state(was_parent, wuss_ICON_STATE_HOVERED, 0); - wuss__icon_invalidate(was_parent); + wuss__icon_invalidate(self->window, was_parent); } } @@ -393,7 +393,7 @@ static result_t wuss__menu_handle(wuss_window_t *window, /* first blink now: the row is already highlit under the pointer, so * drop the highlight this frame for an immediate visible change */ wuss__icon_set_state(row, wuss_ICON_STATE_HOVERED, 0); - wuss__icon_invalidate(row); + wuss__icon_invalidate(self->window, row); } return result_OK; } @@ -462,7 +462,7 @@ static void wuss__menu_flash_finish(struct wuss__menu *self) on = keep_open && wuss__pointer_over_icon(self->window, row); wuss__icon_set_state(row, wuss_ICON_STATE_HOVERED, on); - wuss__icon_invalidate(row); + wuss__icon_invalidate(self->window, row); if (!keep_open) { @@ -508,7 +508,7 @@ static void wuss__menu_flash_step(struct wuss__menu *self) int on = ((self->flash.frames / WUSS_MENU_FLASH_PERIOD) & 1) == 0; wuss__icon_set_state(row, wuss_ICON_STATE_HOVERED, on); - wuss__icon_invalidate(row); + wuss__icon_invalidate(self->window, row); } } @@ -795,7 +795,7 @@ static result_t wuss__menu_spawn(wuss_t *wuss, s++; if (menu->items[i].flags & wuss_MENU_ITEM_TICKED) - wuss_icon_set_selected(node->icons[i], 1); + wuss_icon_set_selected(node->window, node->icons[i], 1); } wuss__free(wuss, made); @@ -946,7 +946,7 @@ void wuss_menu_set_ticked(wuss_menu_handle_t handle, return; for (i = 0; i < menu->nitems; i++) - wuss_icon_set_selected(node->icons[i], i == index); + wuss_icon_set_selected(node->window, node->icons[i], i == index); } void wuss_menu_set_item_ticked(wuss_menu_handle_t handle, @@ -963,7 +963,7 @@ void wuss_menu_set_item_ticked(wuss_menu_handle_t handle, if (index < 0 || index >= menu->nitems) return; - wuss_icon_set_selected(node->icons[index], ticked); + wuss_icon_set_selected(node->window, node->icons[index], ticked); } /* ----------------------------------------------------------------------- */ diff --git a/libraries/wuss/test/tasks/icons.c b/libraries/wuss/test/tasks/icons.c index f9ee3294..5fd100da 100644 --- a/libraries/wuss/test/tasks/icons.c +++ b/libraries/wuss/test/tasks/icons.c @@ -612,7 +612,7 @@ result_t icons_create(wuss_t *wuss, task->state = made[i_state]; if (i_hotspot >= 0) task->hotspot = made[i_hotspot]; - wuss_icon_set_selected(made[i_ticked], 1); /* "Show grid" starts ticked */ + wuss_icon_set_selected(task->window, made[i_ticked], 1); /* "Show grid" starts ticked */ /* fully built: from here a last-window close reaps the task and its * wuss_EVENT_QUIT frees task_data */ @@ -730,7 +730,7 @@ static result_t icons_icon(const wuss_event_t *event, void *task_data) wuss_icon_get_text(icon), wuss_icon_get_selected(icon) ? "on" : "off", (icon == tcx->opt) ? "" : " (radio)"); - return wuss_icon_set_text(tcx->state, buf); + return wuss_icon_set_text(tcx->window, tcx->state, buf); } if (event->data.icon.action != wuss_MOUSE_DOWN) @@ -741,7 +741,7 @@ static result_t icons_icon(const wuss_event_t *event, void *task_data) tcx->count++; snprintf(buf, sizeof(buf), "%d", tcx->count); - return wuss_icon_set_text(tcx->counter, buf); + return wuss_icon_set_text(tcx->window, tcx->counter, buf); } result_t icons_handle(wuss_window_t *window, From 490daa8d2208ed421ebe96523e25912f1f6a7579 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Thu, 10 Sep 2026 16:01:55 +0100 Subject: [PATCH 004/128] refactor(wuss): give wuss_icon_spec the same per-type union as the icon The spec's mutually-exclusive per-type fields (border, pattern, bitmap + icon_set, group, swatch) move into a named union u mirroring wuss_icon_data: u.label.border, u.pattern.tile, u.bitmap.image / u.bitmap.set, u.radio.group, u.menu_entry.swatch. The flat fields are gone; bbox, type, text, fg, bg and flags stay common. icon_set is renamed to bitmap.set. wuss__icon_from_spec reads the arms; the five in-tree call sites and the docs are updated. info.c's label specs never set a u arm, so it now zeroes the whole specs array before filling it. Co-Authored-By: Claude Sonnet 5 --- docs/windowing/wuss.md | 6 +- include/wuss/icon.h | 127 ++++++++++++++++++--------- libraries/wuss/component/info.c | 4 +- libraries/wuss/icon/from-spec.c | 25 +++--- libraries/wuss/menu/menu.c | 6 +- libraries/wuss/test/tasks/icons.c | 20 ++--- libraries/wuss/test/tasks/swatches.c | 2 +- libraries/wuss/test/wuss-test.c | 4 +- 8 files changed, 119 insertions(+), 75 deletions(-) diff --git a/docs/windowing/wuss.md b/docs/windowing/wuss.md index 66cebc89..9b96951c 100644 --- a/docs/windowing/wuss.md +++ b/docs/windowing/wuss.md @@ -198,14 +198,14 @@ In a redraw callback: start drawing at `bounds.x0 - scroll.x`, `bounds.y0 - scro Taking inspiration from RISC OS, a window can carry **icons**: rectangular UI elements Wuss draws and hit-tests inside the content area. v1 ships two types: -- `wuss_ICON_TYPE_LABEL` — static text, optionally in a 1px raised or sunken border (`spec.border` — `wuss_ICON_BORDER_NONE` / `_RIDGE` / `_GROOVE`, the last a RISC OS-style read-only display field). Clicks fall through to the task as `wuss_EVENT_MOUSE`. +- `wuss_ICON_TYPE_LABEL` — static text, optionally in a 1px raised or sunken border (`spec.u.label.border` — `wuss_ICON_BORDER_NONE` / `_RIDGE` / `_GROOVE`, the last a RISC OS-style read-only display field). Clicks fall through to the task as `wuss_EVENT_MOUSE`. - `wuss_ICON_TYPE_ACTION` — 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, a `border` (label only), and flags. A `wuss_ICON_TYPE_ACTION` must pass a real `bg`; passing `wuss_NO_BACKGROUND` is rejected with `result_WUSS_BAD_ICON`. An unknown type is also `result_WUSS_BAD_ICON`; an out-of-range `fg`/`bg` is `result_WUSS_BAD_COLOUR`. +- `wuss_icon_create(window, spec, &icon)` — returns an opaque `wuss_icon_t *`. The spec gives the bounding box, type, text (copied; `NULL` treated as `""`), foreground and background palette indices, a per-type payload in the `u` union (`u.label.border`, `u.pattern.tile`, `u.bitmap.image`/`u.bitmap.set`, `u.radio.group`, `u.menu_entry.swatch`), and flags. A `wuss_ICON_TYPE_ACTION` 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(window, icon)` — NULL-safe. - `wuss_icon_set_text(window, icon, text)`, `wuss_icon_set_hidden(window, icon, hidden)`, `wuss_icon_set_selected(window, icon, selected)`. - Getters: `wuss_icon_get_bbox`, `wuss_icon_get_type`, `wuss_icon_get_text` (never `NULL`), `wuss_icon_get_selected`. @@ -226,7 +226,7 @@ The bevel's light (top/left) and dark (bottom/right) edge shades come from `conf - `wuss_icons_bitmap(wuss, index)` → the compressed `bitmap_t *` (window-manager-owned), or `NULL` out of range. - `wuss_icons_count(wuss)`. -A `wuss_ICON_TYPE_BITMAP` spec with `bitmap == NULL` and `icon_set = wuss_ICON_SET(idx)` draws the loaded entry at `idx` (the `wuss_ICON_SET` macro offsets by one so a zero-initialised spec means "no entry"). An out-of-range index is `result_WUSS_BAD_INDEX`. Calling `wuss_icons_load` again replaces the set; `wuss_destroy` frees it. A missing directory is not an error — it yields a zero-length set. `dir` is copied internally, so a `path_join_filename` result is safe to pass. +A `wuss_ICON_TYPE_BITMAP` spec with `u.bitmap.image == NULL` and `u.bitmap.set = wuss_ICON_SET(idx)` draws the loaded entry at `idx` (the `wuss_ICON_SET` macro offsets by one so a zero-initialised spec means "no entry"). An out-of-range index is `result_WUSS_BAD_INDEX`. Calling `wuss_icons_load` again replaces the set; `wuss_destroy` frees it. A missing directory is not an error — it yields a zero-length set. `dir` is copied internally, so a `path_join_filename` result is safe to pass. ## Components diff --git a/include/wuss/icon.h b/include/wuss/icon.h index 2960c84f..f399cef5 100644 --- a/include/wuss/icon.h +++ b/include/wuss/icon.h @@ -59,10 +59,10 @@ typedef enum wuss_icon_type * feedback; clicks and hovers are delivered to the task as * wuss_EVENT_ICON. */ wuss_ICON_TYPE_ACTION, - /** 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. */ + /** Bounding box filled with a repeating two-colour 8x8 tile + * (spec.u.pattern.tile) in fg/bg, phased to document space so it scrolls + * rigidly with content. Not interactive: clicks fall through to the task as + * wuss_EVENT_MOUSE. text is ignored. */ wuss_ICON_TYPE_PATTERN, /** A grouping box: a bevelled rectangle around the bounding box, * broken at the top-left for an optional caption (text) drawn over the @@ -79,10 +79,10 @@ typedef enum wuss_icon_type * its own selected state (group is ignored), then the task is told via * wuss_EVENT_ICON. */ wuss_ICON_TYPE_OPTION, - /** A caller-owned bitmap (spec.bitmap) drawn at the top-left of the bounding - * box, alpha-blended against what is already there, clipped to the box; no - * scaling. The bitmap is borrowed, not copied, and must outlive the icon - * (unlike text). fg, bg, text and pattern are ignored. Not interactive + /** A caller-owned bitmap (spec.u.bitmap.image) drawn at the top-left of the + * bounding box, alpha-blended against what is already there, clipped to the + * box; no scaling. The bitmap is borrowed, not copied, and must outlive the + * icon (unlike text). fg, bg, text and pattern are ignored. Not interactive * unless wuss_ICON_FLAGS_INTERACTIVE is set, in which case clicks raise * wuss_EVENT_ICON like a button. */ wuss_ICON_TYPE_BITMAP, @@ -188,7 +188,7 @@ typedef enum wuss_icon_flags * laid out and drawn as a separate wuss_ICON_TYPE_RULE icon. Ignored by * other types. */ wuss_ICON_FLAGS_SEPARATOR = 1 << 7, - /** wuss_ICON_TYPE_MENU_ENTRY: draw a small colour chip (spec.swatch) in the + /** wuss_ICON_TYPE_MENU_ENTRY: draw a small colour chip (spec.u.menu_entry.swatch) in the * row's left gutter, where the tick would sit. Mutually exclusive with a * selected tick -- the chip wins. Ignored by other types. */ wuss_ICON_FLAGS_SWATCH = 1 << 8, @@ -210,11 +210,77 @@ wuss_icon_flags_t; /** * Encode a 0-based icon-set index (from \ref wuss_icons_lookup) for - * wuss_icon_spec::icon_set. The stored value is offset by one so a + * wuss_icon_spec_data::bitmap::set. The stored value is offset by one so a * zero-initialised spec reads as "no icon-set entry". */ #define wuss_ICON_SET(i) ((i) + 1) +/** + * Per-type payload in a wuss_icon_spec. Exactly one arm applies, selected by + * wuss_icon_spec::type; the arms mirror the icon's internal storage. Types + * with no type-specific data (ACTION, FRAME, OPTION, RULE, and the reserved + * types) touch no arm, so a zero-initialised spec is valid for them. Each + * arm is its own struct so a type can gain fields without disturbing the + * others. + */ +typedef union wuss_icon_spec_data +{ + /** wuss_ICON_TYPE_LABEL */ + struct + { + /** Border drawn inside the bounding box. Zero (wuss_ICON_BORDER_NONE) is + * the default for a zero-initialised spec. */ + wuss_icon_border_t border; + } + label; + + /** wuss_ICON_TYPE_PATTERN */ + struct + { + /** Repeating two-colour 8x8 tile. Zero (screen_PATTERN_SOLID) is a safe + * default for a zero-initialised spec. */ + screen_pattern_t tile; + } + pattern; + + /** wuss_ICON_TYPE_BITMAP */ + struct + { + /** The image to draw. Borrowed, not copied; must outlive the icon. NULL + * (the default) falls back to \c set. */ + const bitmap_t *image; + /** When \c image is NULL, draw an entry from the window manager's loaded + * icon set (see \ref wuss_icons_load). Zero (the default for a + * zero-initialised spec) means "no icon-set entry"; encode a 0-based + * index from \ref wuss_icons_lookup with \ref wuss_ICON_SET. Ignored + * when \c image is set. */ + int set; + } + bitmap; + + /** wuss_ICON_TYPE_RADIO */ + struct + { + /** Exclusive-selection group. Selecting a radio clears every other + * selected radio on the same window with the same group. Zero (the + * default) means "no group": such a radio still toggles but never clears + * another. */ + int group; + } + radio; + + /** wuss_ICON_TYPE_MENU_ENTRY */ + struct + { + /** With wuss_ICON_FLAGS_SWATCH: the colour chip to draw in the left + * gutter, as an index into the system palette. Ignored unless that flag + * is set. */ + wuss_colour_t swatch; + } + menu_entry; +} +wuss_icon_spec_data_t; + /** * Description of an icon at creation. Copied by value into the icon; the * caller keeps ownership of \c text, which is copied. @@ -226,46 +292,21 @@ wuss_icon_flags_t; typedef struct wuss_icon_spec { /** Bounding box, virtual document space, inclusive-exclusive. */ - box_t bbox; + box_t bbox; /** Icon type. */ - wuss_icon_type_t type; + wuss_icon_type_t type; /** NUL-terminated label; copied. NULL means "". */ - const char *text; + const char *text; /** Text colour, as an index into the system palette. */ - wuss_colour_t fg; + wuss_colour_t fg; /** Fill/bevel base colour, as an index into the system palette. A label, * frame, radio or option icon may pass wuss_NO_BACKGROUND for no fill behind * its text/glyph; a button or pattern icon must pass a real index. */ - wuss_colour_t bg; - /** Tile for wuss_ICON_TYPE_PATTERN; ignored by other types. Zero - * (screen_PATTERN_SOLID) is a safe default for zero-initialised specs. */ - screen_pattern_t pattern; - /** wuss_ICON_TYPE_BITMAP: the image to draw. Borrowed, not copied; must - * outlive the icon. Ignored by other types; NULL (the default) is only valid - * when type is not wuss_ICON_TYPE_BITMAP, or when \c icon_set selects a - * bitmap from the window manager's loaded icon set instead. */ - const bitmap_t *bitmap; - /** wuss_ICON_TYPE_BITMAP: when \c bitmap is NULL, draw an entry from the - * window manager's loaded icon set (see \ref wuss_icons_load). Zero (the - * default for a zero-initialised spec) means "no icon-set entry"; encode a - * 0-based index from \ref wuss_icons_lookup with \ref wuss_ICON_SET. - * Ignored by other types and when \c bitmap is set. */ - int icon_set; - /** wuss_ICON_TYPE_RADIO: exclusive-selection group. Selecting a radio clears - * every other selected radio on the same window with the same group. Zero - * (the default) means "no group": such a radio still toggles but never - * clears another. Ignored by all other icon types. */ - int group; - /** wuss_ICON_TYPE_MENU_ENTRY with wuss_ICON_FLAGS_SWATCH: the colour chip to - * draw in the left gutter, as an index into the system palette. Ignored - * unless that flag is set; ignored by all other icon types. */ - wuss_colour_t swatch; - /** wuss_ICON_TYPE_LABEL: border drawn inside the bounding box. Zero - * (wuss_ICON_BORDER_NONE) is the default for zero-initialised specs. - * Ignored by all other icon types. */ - wuss_icon_border_t border; + wuss_colour_t bg; /** Appearance/behaviour flags. */ - wuss_icon_flags_t flags; + wuss_icon_flags_t flags; + /** Per-type payload, selected by \c type. */ + wuss_icon_spec_data_t u; } wuss_icon_spec_t; diff --git a/libraries/wuss/component/info.c b/libraries/wuss/component/info.c index 7bd7aee2..d901a3bc 100644 --- a/libraries/wuss/component/info.c +++ b/libraries/wuss/component/info.c @@ -90,6 +90,8 @@ result_t wuss_info_create(wuss_info_t **out, if (nrows < 1 || nrows > INFO_MAX_ROWS) return result_BAD_ARG; + memset(specs, 0, sizeof(specs)); /* leave every unset spec.u arm zeroed */ + wuss = task->wuss; font = wuss_get_font(wuss); @@ -169,7 +171,7 @@ result_t wuss_info_create(wuss_info_t **out, value->text = rows[i].value; value->fg = wuss_COLOUR_BLACK; value->bg = wuss_NO_BACKGROUND; - value->border = wuss_ICON_BORDER_GROOVE; /* RISC OS display field */ + value->u.label.border = wuss_ICON_BORDER_GROOVE; /* RISC OS display field */ value->flags = wuss_ICON_FLAGS_JUSTIFY_CENTRE; } diff --git a/libraries/wuss/icon/from-spec.c b/libraries/wuss/icon/from-spec.c index c2d2199c..034200ea 100644 --- a/libraries/wuss/icon/from-spec.c +++ b/libraries/wuss/icon/from-spec.c @@ -26,7 +26,7 @@ result_t wuss__icon_from_spec(const wuss_t *w, has_swatch = (spec->type == wuss_ICON_TYPE_MENU_ENTRY) && (spec->flags & wuss_ICON_FLAGS_SWATCH); - swatch = has_swatch ? wuss__resolve_colour(w, spec->swatch) + swatch = has_swatch ? wuss__resolve_colour(w, spec->u.menu_entry.swatch) : wuss_NO_BACKGROUND; switch (spec->type) @@ -57,13 +57,14 @@ result_t wuss__icon_from_spec(const wuss_t *w, if (spec->type == wuss_ICON_TYPE_PATTERN && bg == wuss_NO_BACKGROUND) return result_WUSS_BAD_ICON; - /* A BITMAP icon draws spec->bitmap, or -- when that is NULL -- the loaded - * icon-set entry spec->icon_set names (wuss_ICON_SET encodes the 0-based - * index +1, so 0 means "no entry"). */ - bitmap = spec->bitmap; - if (spec->type == wuss_ICON_TYPE_BITMAP && bitmap == NULL && spec->icon_set > 0) + /* A BITMAP icon draws spec->u.bitmap.image, or -- when that is NULL -- the + * loaded icon-set entry spec->u.bitmap.set names (wuss_ICON_SET encodes the + * 0-based index +1, so 0 means "no entry"). */ + bitmap = spec->u.bitmap.image; + if (spec->type == wuss_ICON_TYPE_BITMAP && bitmap == NULL && + spec->u.bitmap.set > 0) { - bitmap = wuss_icons_bitmap(w, spec->icon_set - 1); + bitmap = wuss_icons_bitmap(w, spec->u.bitmap.set - 1); if (bitmap == NULL) return result_WUSS_BAD_INDEX; } @@ -72,11 +73,11 @@ result_t wuss__icon_from_spec(const wuss_t *w, return result_WUSS_BAD_ICON; if (spec->type == wuss_ICON_TYPE_PATTERN && - spec->pattern >= screen_PATTERN__LIMIT) + spec->u.pattern.tile >= screen_PATTERN__LIMIT) return result_WUSS_BAD_ICON; if (spec->type == wuss_ICON_TYPE_LABEL && - spec->border > wuss_ICON_BORDER_DIVIDER) + spec->u.label.border > wuss_ICON_BORDER_DIVIDER) return result_WUSS_BAD_ICON; if (fg >= w->npalette) @@ -98,16 +99,16 @@ result_t wuss__icon_from_spec(const wuss_t *w, switch (spec->type) { case wuss_ICON_TYPE_LABEL: - out->u.label.border = spec->border; + out->u.label.border = spec->u.label.border; break; case wuss_ICON_TYPE_PATTERN: - out->u.pattern.tile = spec->pattern; + out->u.pattern.tile = spec->u.pattern.tile; break; case wuss_ICON_TYPE_BITMAP: out->u.bitmap.image = bitmap; break; case wuss_ICON_TYPE_RADIO: - out->u.radio.group = spec->group; + out->u.radio.group = spec->u.radio.group; break; case wuss_ICON_TYPE_MENU_ENTRY: out->u.menu_entry.swatch = swatch; diff --git a/libraries/wuss/menu/menu.c b/libraries/wuss/menu/menu.c index edb6e0f1..f456bb21 100644 --- a/libraries/wuss/menu/menu.c +++ b/libraries/wuss/menu/menu.c @@ -715,7 +715,7 @@ static result_t wuss__menu_spawn(wuss_t *wuss, specs[s].text = ""; specs[s].fg = wuss_COLOUR_BLACK; specs[s].bg = wuss_NO_BACKGROUND; - specs[s].swatch = wuss_NO_BACKGROUND; + specs[s].u.menu_entry.swatch = wuss_NO_BACKGROUND; specs[s].flags = wuss_ICON_FLAGS_NONE; s++; y += sep_h; @@ -736,8 +736,8 @@ static result_t wuss__menu_spawn(wuss_t *wuss, specs[s].text = item->text ? item->text : ""; specs[s].fg = wuss_COLOUR_BLACK; specs[s].bg = wuss_NO_BACKGROUND; - specs[s].swatch = (item->flags & wuss_MENU_ITEM_SWATCH) ? item->swatch - : wuss_NO_BACKGROUND; + specs[s].u.menu_entry.swatch = + (item->flags & wuss_MENU_ITEM_SWATCH) ? item->swatch : wuss_NO_BACKGROUND; specs[s].flags = flags; s++; diff --git a/libraries/wuss/test/tasks/icons.c b/libraries/wuss/test/tasks/icons.c index 5fd100da..13f0bf89 100644 --- a/libraries/wuss/test/tasks/icons.c +++ b/libraries/wuss/test/tasks/icons.c @@ -212,7 +212,7 @@ static void icons_add_radios(icons_layout_t *lay, int *opt, int *state) s->text = (r == 0) ? "Red" : (r == 1) ? "Green" : "Blue"; s->fg = lay->black; s->bg = wuss_NO_BACKGROUND; - s->group = 1; + s->u.radio.group = 1; lay->n++; } @@ -265,14 +265,14 @@ static void icons_add_bitmaps(icons_layout_t *lay, s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN + 10, top + 20, sprite->size.w, sprite->size.h); s->type = wuss_ICON_TYPE_BITMAP; - s->bitmap = sprite; + s->u.bitmap.image = sprite; lay->n++; s = &lay->specs[lay->n]; s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN + 20 + sprite->size.w, top + 20, sprite->size.w, sprite->size.h); s->type = wuss_ICON_TYPE_BITMAP; - s->bitmap = sprite; + s->u.bitmap.image = sprite; s->flags = wuss_ICON_FLAGS_INTERACTIVE; *hotspot = lay->n; lay->n++; @@ -321,7 +321,7 @@ static void icons_add_iconset(icons_layout_t *lay, const wuss_t *wuss) s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN + 10 + i * (w + 6), top + 20, w, h); s->type = wuss_ICON_TYPE_BITMAP; - s->icon_set = wuss_ICON_SET(idx[i]); + s->u.bitmap.set = wuss_ICON_SET(idx[i]); lay->n++; } @@ -348,7 +348,7 @@ static void icons_add_pattern(icons_layout_t *lay) s->type = wuss_ICON_TYPE_PATTERN; s->fg = lay->black; s->bg = lay->window; - s->pattern = screen_PATTERN_DIAGONAL; + s->u.pattern.tile = screen_PATTERN_DIAGONAL; lay->n++; lay->y = top + 76; @@ -379,7 +379,7 @@ static void icons_add_borders(icons_layout_t *lay) s->text = "Groove"; s->fg = lay->black; s->bg = lay->window; - s->border = wuss_ICON_BORDER_GROOVE; + s->u.label.border = wuss_ICON_BORDER_GROOVE; s->flags = wuss_ICON_FLAGS_JUSTIFY_CENTRE; lay->n++; @@ -389,7 +389,7 @@ static void icons_add_borders(icons_layout_t *lay) s->text = "Ridge"; s->fg = lay->black; s->bg = lay->window; - s->border = wuss_ICON_BORDER_RIDGE; + s->u.label.border = wuss_ICON_BORDER_RIDGE; s->flags = wuss_ICON_FLAGS_JUSTIFY_CENTRE; lay->n++; @@ -399,7 +399,7 @@ static void icons_add_borders(icons_layout_t *lay) s->text = "Action"; s->fg = lay->black; s->bg = lay->window; - s->border = wuss_ICON_BORDER_ACTION; + s->u.label.border = wuss_ICON_BORDER_ACTION; s->flags = wuss_ICON_FLAGS_JUSTIFY_CENTRE; lay->n++; @@ -409,7 +409,7 @@ static void icons_add_borders(icons_layout_t *lay) s->text = "Divider"; s->fg = lay->black; s->bg = lay->window; - s->border = wuss_ICON_BORDER_DIVIDER; + s->u.label.border = wuss_ICON_BORDER_DIVIDER; s->flags = wuss_ICON_FLAGS_JUSTIFY_CENTRE; lay->n++; @@ -460,7 +460,7 @@ static void icons_add_menu(icons_layout_t *lay, int *ticked) s->text = "Layer colour"; s->fg = lay->black; s->bg = wuss_NO_BACKGROUND; - s->swatch = lay->red; + s->u.menu_entry.swatch = lay->red; s->flags = wuss_ICON_FLAGS_SWATCH; lay->n++; diff --git a/libraries/wuss/test/tasks/swatches.c b/libraries/wuss/test/tasks/swatches.c index deb23d9a..a35f5e08 100644 --- a/libraries/wuss/test/tasks/swatches.c +++ b/libraries/wuss/test/tasks/swatches.c @@ -59,7 +59,7 @@ static result_t swatches_redraw(swatches_task_t *task, pat * SWATCHES_CELL, SWATCHES_CELL, SWATCHES_CELL); spec.fg = (wuss_colour_t) col; - spec.pattern = (screen_pattern_t) pat; + spec.u.pattern.tile = (screen_pattern_t) pat; rc = wuss_icon_plot(task->window, &spec, bounds, scroll); if (rc != result_OK) diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 4667779b..ef4dece8 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -5161,13 +5161,13 @@ ColourMenuOK: ; memset(&spec, 0, sizeof(spec)); spec.bbox = (box_t) BOX_POS_SIZE(0, 0, 16, 16); spec.type = wuss_ICON_TYPE_BITMAP; - spec.icon_set = wuss_ICON_SET(opton); + spec.u.bitmap.set = wuss_ICON_SET(opton); rc = wuss_icon_create(win_ic, &spec, &icon); if (rc != result_OK) goto Failure; /* a bogus index is rejected */ - spec.icon_set = wuss_ICON_SET(99); + spec.u.bitmap.set = wuss_ICON_SET(99); if (wuss_icon_create(win_ic, &spec, &icon) != result_WUSS_BAD_INDEX) goto Failure; rc = result_OK; From 2fcda0590e99c670831b9b34fe924f38abfeca6b Mon Sep 17 00:00:00 2001 From: David Thomas Date: Thu, 10 Sep 2026 16:18:02 +0100 Subject: [PATCH 005/128] fix(wuss): scroll the gradient task's matrix-size label with the content The label was pinned to the window's content corner, so it stayed put while the gradient scrolled under it. Anchor it at document (2, 2) instead: screen pos = content top-left - scroll + offset. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/test/tasks/gradient.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libraries/wuss/test/tasks/gradient.c b/libraries/wuss/test/tasks/gradient.c index 1a21c878..01f24673 100644 --- a/libraries/wuss/test/tasks/gradient.c +++ b/libraries/wuss/test/tasks/gradient.c @@ -143,14 +143,14 @@ static result_t gradient_redraw(const wuss_event_t *event, void *task_data) } } - /* matrix-size label, pinned to the content area's top-left corner (bounds, - * not the per-redraw dirty piece, and not the scrolled document) so it - * stays put on a partial redraw */ + /* matrix-size label, anchored at document (2, 2) so it scrolls with the + * content: screen pos = content top-left - scroll + doc offset. Uses bounds, + * not the per-redraw dirty piece, so it is drawn whole on a partial redraw */ { bmfont_t *font = wuss_get_font(gc->wuss); int dim = gradient_dithers[di].dim; char label[8]; - point_t pos = POINT(bounds->x0 + 2, bounds->y0 + 2); + point_t pos = POINT(bounds->x0 - sx + 2, bounds->y0 - sy + 2); colour_t ink = colour_rgb(0xFF, 0xFF, 0xFF); colour_t bg = colour_rgba(0, 0, 0, 0); /* transparent */ From 4b3c922d31e6ed2847af6c6ab47b2ad194c20fec Mon Sep 17 00:00:00 2001 From: David Thomas Date: Thu, 10 Sep 2026 16:25:06 +0100 Subject: [PATCH 006/128] fix(wuss): scroll curve and porter-duff task content with the document curve's type label was pinned to the window corner like gradient's; anchor it at document (2, 2) instead. porter-duff ignored scroll entirely: the checkerboard was phased to the window corner and the composited bitmap and rule label were drawn at raw bounds->x0/y0. Phase the checkerboard with the scroll offset and draw the bitmap and label at bounds->x0 - scroll, so the pane tracks the document when the window is shrunk below the 256x256 content size. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/test/tasks/curve.c | 9 +++++---- libraries/wuss/test/tasks/porter-duff.c | 19 ++++++++++++------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/libraries/wuss/test/tasks/curve.c b/libraries/wuss/test/tasks/curve.c index 9e39148f..8c8b8368 100644 --- a/libraries/wuss/test/tasks/curve.c +++ b/libraries/wuss/test/tasks/curve.c @@ -239,13 +239,14 @@ static result_t curve_redraw(const wuss_event_t *event, curve_task_t *task) blob_colour(task, i)); } - /* curve-type label, pinned to the content area's top-left corner (bounds, - * not the per-redraw dirty piece "content", and not the scrolled document) - * so it stays put and readable on a partial redraw */ + /* curve-type label, anchored at document (2, 2) so it scrolls with the + * content: screen pos = content top-left - scroll + doc offset. Uses bounds, + * not the per-redraw dirty piece "content", so it is drawn whole on a + * partial redraw */ { bmfont_t *font = wuss_get_font(task->wuss); const char *name = curve_kind_name(task->npoints); - point_t pos = POINT(bounds->x0 + 2, bounds->y0 + 2); + point_t pos = POINT(bounds->x0 - sx + 2, bounds->y0 - sy + 2); if (font != NULL) bmfont_draw(font, scr, name, (int) strlen(name), diff --git a/libraries/wuss/test/tasks/porter-duff.c b/libraries/wuss/test/tasks/porter-duff.c index 24fadbaf..91fc78ea 100644 --- a/libraries/wuss/test/tasks/porter-duff.c +++ b/libraries/wuss/test/tasks/porter-duff.c @@ -275,15 +275,17 @@ static void porter_duff_ramp_src(porter_duff_task_t *pd, int ramp) static void porter_duff_draw_checkerboard(const porter_duff_task_t *pd, screen_t *scr, const box_t *content, - const box_t *bounds) + const box_t *bounds, + int sx, + int sy) { 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; + lx = x - bounds->x0 + sx; + ly = y - bounds->y0 + sy; band = lx / PD_CHECKER_BAND + ly / PD_CHECKER_BAND; screen_set_pixel(scr, x, y, (band & 1) ? pd->dark : pd->light); @@ -299,14 +301,17 @@ static result_t porter_duff_redraw(const wuss_event_t *event, const box_t *content, *bounds; const char *name; point_t pos; + int sx, sy; pd = 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; - porter_duff_draw_checkerboard(pd, scr, content, bounds); + porter_duff_draw_checkerboard(pd, scr, content, bounds, sx, sy); /* 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 @@ -322,11 +327,11 @@ static result_t porter_duff_redraw(const wuss_event_t *event, if (rc != result_OK) return rc; - screen_copy_bitmap(scr, bounds->x0, bounds->y0, &pd->dst); + screen_copy_bitmap(scr, bounds->x0 - sx, bounds->y0 - sy, &pd->dst); name = rule_names[pd->rule]; - pos.x = bounds->x0 + 2; - pos.y = bounds->y0 + PD_SIZE + 2; + pos.x = bounds->x0 - sx + 2; + pos.y = bounds->y0 - sy + PD_SIZE + 2; return bmfont_draw(pd->font, scr, name, (int) strlen(name), pd->fg, pd->bg, &pos, NULL); From 143990d476d090da069fbdb766a30bf2675a83ce Mon Sep 17 00:00:00 2001 From: David Thomas Date: Thu, 10 Sep 2026 17:52:15 +0100 Subject: [PATCH 007/128] refactor(wuss): nest wuss_icon_spec inside struct wuss_icon struct wuss_icon becomes { wuss_icon_spec_t spec; wuss_icon_state_t state; } and the internal wuss_icon_data union is removed. wuss__icon_from_spec now copies the whole spec into out->spec, resolving fg/bg/swatch to palette indices and a BITMAP's u.bitmap.image from u.bitmap.set in place. The label text lives in spec.text (const char *), owned as before by wuss_icon_create and freed with a const cast. All icon->FIELD accesses become icon->spec.FIELD; runtime state stays on icon->state via the existing wuss__icon_* accessors. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/core/mouse-click.c | 4 +- libraries/wuss/icon.h | 68 +++++------------------- libraries/wuss/icon/create.c | 8 +-- libraries/wuss/icon/delete.c | 2 +- libraries/wuss/icon/draw.c | 83 +++++++++++++++--------------- libraries/wuss/icon/free.c | 2 +- libraries/wuss/icon/from-spec.c | 30 ++++------- libraries/wuss/icon/get-bbox.c | 2 +- libraries/wuss/icon/get-text.c | 2 +- libraries/wuss/icon/get-type.c | 2 +- libraries/wuss/icon/hit-test.c | 16 +++--- libraries/wuss/icon/invalidate.c | 2 +- libraries/wuss/icon/set-hidden.c | 4 +- libraries/wuss/icon/set-hover.c | 2 +- libraries/wuss/icon/set-selected.c | 14 ++--- libraries/wuss/icon/set-text.c | 4 +- 16 files changed, 97 insertions(+), 148 deletions(-) diff --git a/libraries/wuss/core/mouse-click.c b/libraries/wuss/core/mouse-click.c index cb20206c..147754c2 100644 --- a/libraries/wuss/core/mouse-click.c +++ b/libraries/wuss/core/mouse-click.c @@ -291,9 +291,9 @@ result_t wuss_mouse_click(wuss_t *wuss, /* a completed click latches radio/option state before the task is * told, so the wuss_EVENT_ICON handler sees the new value */ - if (icon->type == wuss_ICON_TYPE_OPTION) + if (icon->spec.type == wuss_ICON_TYPE_OPTION) wuss__icon_select(win, icon, !wuss__icon_selected(icon)); - else if (icon->type == wuss_ICON_TYPE_RADIO) + else if (icon->spec.type == wuss_ICON_TYPE_RADIO) wuss__icon_select(win, icon, (button & wuss_BUTTON_ADJUST) ? !wuss__icon_selected(icon) : 1); diff --git a/libraries/wuss/icon.h b/libraries/wuss/icon.h index 8f56897d..cae584e7 100644 --- a/libraries/wuss/icon.h +++ b/libraries/wuss/icon.h @@ -24,60 +24,15 @@ typedef enum wuss_icon_state } wuss_icon_state_t; -/* Per-type payload. Exactly one arm is live, selected by wuss_icon::type; - * wuss__icon_from_spec zeroes the whole union then fills the arm for the - * icon's type. Types with no type-specific data (ACTION, FRAME, OPTION, - * RULE, and the reserved types) touch no arm. Each arm is its own struct so - * a type can gain fields without disturbing the others. */ -typedef union wuss_icon_data -{ - /* wuss_ICON_TYPE_LABEL */ - struct - { - wuss_icon_border_t border; /* inside-bbox border; NONE otherwise */ - } - label; - - /* wuss_ICON_TYPE_PATTERN */ - struct - { - screen_pattern_t tile; /* repeating preset tile */ - } - pattern; - - /* wuss_ICON_TYPE_BITMAP */ - struct - { - const bitmap_t *image; /* borrowed, never owned */ - } - bitmap; - - /* wuss_ICON_TYPE_RADIO */ - struct - { - int group; /* exclusive-selection group; 0 = none */ - } - radio; - - /* wuss_ICON_TYPE_MENU_ENTRY */ - struct - { - wuss_colour_t swatch; /* FLAGS_SWATCH left-gutter chip colour; - * wuss_NO_BACKGROUND otherwise */ - } - menu_entry; -} -wuss_icon_data_t; - +/* A live icon is its creation spec plus transient runtime state. + * wuss__icon_from_spec validates a wuss_icon_spec_t and stores it in "spec" + * with fg/bg/swatch resolved to concrete palette indices and, for a BITMAP, + * u.bitmap.image resolved from u.bitmap.set. spec.text is owned (strdup'd by + * wuss_icon_create, aliased by wuss_icon_plot) -- never NULL, "" instead. */ struct wuss_icon { - box_t bbox; /* in virtual document space */ - wuss_icon_type_t type; - char *text; /* owned; never NULL ("" instead) */ - wuss_colour_t fg, bg; - wuss_icon_flags_t flags; + wuss_icon_spec_t spec; /* bbox in virtual document space; text owned */ wuss_icon_state_t state; - wuss_icon_data_t u; /* per-type payload, selected by type */ }; static inline int wuss__icon_pressed(const wuss_icon_t *icon) @@ -134,10 +89,13 @@ void wuss__icon_box_to_screen(const box_t *content, * must be an icon of "window". */ void wuss__icon_invalidate(wuss_window_t *window, const wuss_icon_t *icon); -/* Validate a spec against the palette and fill "out" with a detached icon (no - * owned text -- out->text is aliased to spec->text or ""). Shared by - * wuss_icon_create and wuss_icon_plot. Returns result_WUSS_BAD_ICON / - * result_WUSS_BAD_COLOUR as wuss_icon_create documents, else result_OK. */ +/* Validate a spec against the palette and fill "out" with a detached icon: + * "out->spec" is a copy of "spec" with fg/bg/swatch resolved to palette + * indices and, for a BITMAP, u.bitmap.image resolved from u.bitmap.set. + * out->spec.text is not owned -- it aliases spec->text, or "" when that is + * NULL. Shared by wuss_icon_create and wuss_icon_plot. Returns + * result_WUSS_BAD_ICON / result_WUSS_BAD_COLOUR as wuss_icon_create + * documents, else result_OK. */ result_t wuss__icon_from_spec(const wuss_t *wuss, const wuss_icon_spec_t *spec, wuss_icon_t *out); diff --git a/libraries/wuss/icon/create.c b/libraries/wuss/icon/create.c index ca4782f3..6d9d4d53 100644 --- a/libraries/wuss/icon/create.c +++ b/libraries/wuss/icon/create.c @@ -46,17 +46,17 @@ result_t wuss_icon_create(wuss_window_t *window, if (it == NULL) return result_OOM; - *it = scratch; /* scratch.text aliases spec->text; replaced with an owned copy below */ + *it = scratch; /* scratch.spec.text aliases spec->text; replaced with an owned copy below */ src = (spec->text != NULL) ? spec->text : ""; len = strlen(src); - it->text = wuss__malloc(w, len + 1); - if (it->text == NULL) + it->spec.text = wuss__malloc(w, len + 1); + if (it->spec.text == NULL) { wuss__free(w, it); return result_OOM; } - memcpy(it->text, src, len + 1); + memcpy((char *) it->spec.text, src, len + 1); window->icons[window->nicons++] = it; diff --git a/libraries/wuss/icon/delete.c b/libraries/wuss/icon/delete.c index c796a743..116c774d 100644 --- a/libraries/wuss/icon/delete.c +++ b/libraries/wuss/icon/delete.c @@ -37,6 +37,6 @@ void wuss_icon_delete(wuss_window_t *window, wuss_icon_t *icon) } } - wuss__free(window->wuss, icon->text); + wuss__free(window->wuss, (char *) icon->spec.text); wuss__free(window->wuss, icon); } diff --git a/libraries/wuss/icon/draw.c b/libraries/wuss/icon/draw.c index 0c3b731a..37ad99d1 100644 --- a/libraries/wuss/icon/draw.c +++ b/libraries/wuss/icon/draw.c @@ -99,7 +99,7 @@ static void icon_draw_divider_border(screen_t *scr, static colour_t icon_blend_ground(const icon_draw_ctx_t *c, colour_t fallback) { - const wuss_icon_t *icon = c->icon; + const wuss_icon_spec_t *icon = &c->icon->spec; if (icon->bg != wuss_NO_BACKGROUND) return c->wuss->palette[icon->bg]; @@ -148,8 +148,8 @@ static void wuss__icon_draw_pattern(const icon_draw_ctx_t *c, const box_t *content, point_t scroll) { - const wuss_icon_t *icon = c->icon; - colour_t pat_fg; + const wuss_icon_spec_t *icon = &c->icon->spec; + colour_t pat_fg; /* disabled: fold the pattern into its own ground so it reads as greyed, * mirroring the button's fg-swap */ @@ -171,11 +171,11 @@ static void wuss__icon_draw_pattern(const icon_draw_ctx_t *c, static void wuss__icon_draw_label(const icon_draw_ctx_t *c) { - const wuss_icon_t *icon = c->icon; - const box_t *b = &c->b; - colour_t bg; - point_t pos; - bmfont_width_t width; + const wuss_icon_spec_t *icon = &c->icon->spec; + const box_t *b = &c->b; + colour_t bg; + point_t pos; + bmfont_width_t width; if (icon->bg != wuss_NO_BACKGROUND) { @@ -243,10 +243,10 @@ static void wuss__icon_draw_label(const icon_draw_ctx_t *c) static void wuss__icon_draw_frame(const icon_draw_ctx_t *c) { - const wuss_icon_t *icon = c->icon; - const box_t *b = &c->b; - colour_t bg, light, divider; - int cap_w, cap_x, gap_x0, gap_x1; + const wuss_icon_spec_t *icon = &c->icon->spec; + const box_t *b = &c->b; + colour_t bg, light, divider; + int cap_w, cap_x, gap_x0, gap_x1; bg = icon_blend_ground(c, c->fg); light = c->wuss->palette[c->wuss->bevel_light]; @@ -294,12 +294,12 @@ static void wuss__icon_draw_frame(const icon_draw_ctx_t *c) static void wuss__icon_draw_button(const icon_draw_ctx_t *c) { - const wuss_icon_t *icon = c->icon; - const box_t *b = &c->b; - colour_t light, dark, base, label; - int pressed, is_default; + const wuss_icon_spec_t *icon = &c->icon->spec; + const box_t *b = &c->b; + colour_t light, dark, base, label; + int pressed, is_default; - pressed = wuss__icon_pressed(icon); + pressed = wuss__icon_pressed(c->icon); is_default = (icon->flags & wuss_ICON_FLAGS_DEFAULT) != 0; /* a plain button whose spec left bg as wuss_NO_BACKGROUND takes the config @@ -375,7 +375,7 @@ static int wuss__icon_blit_radio_option(const icon_draw_ctx_t *c, screen_t clipped; int idx, bx, by; - if (c->icon->type == wuss_ICON_TYPE_RADIO) + if (c->icon->spec.type == wuss_ICON_TYPE_RADIO) name = wuss__icon_selected(c->icon) ? "radon" : "radoff"; else name = wuss__icon_selected(c->icon) ? "opton" : "optoff"; @@ -406,11 +406,11 @@ static int wuss__icon_blit_radio_option(const icon_draw_ctx_t *c, * centre when selected and OPTION draws a box with a tick when selected. */ static void wuss__icon_draw_radio_option(const icon_draw_ctx_t *c) { - const wuss_icon_t *icon = c->icon; - const box_t *b = &c->b; - colour_t glyph, bg; - box_t g; - int gsz, gy, tx; + const wuss_icon_spec_t *icon = &c->icon->spec; + const box_t *b = &c->b; + colour_t glyph, bg; + box_t g; + int gsz, gy, tx; gsz = CLAMP(c->font_height, 8, b->y1 - b->y0); gy = b->y0 + (b->y1 - b->y0 - gsz) / 2; @@ -441,7 +441,7 @@ static void wuss__icon_draw_radio_option(const icon_draw_ctx_t *c) screen_draw_line(c->scr, g.x0 + 2, g.y1 - 1, g.x1 - 3, g.y1 - 1, glyph); screen_draw_line(c->scr, g.x0, g.y0 + 2, g.x0, g.y1 - 3, glyph); screen_draw_line(c->scr, g.x1 - 1, g.y0 + 2, g.x1 - 1, g.y1 - 3, glyph); - if (wuss__icon_selected(icon)) + if (wuss__icon_selected(c->icon)) screen_fill_rect(c->scr, g.x0 + 3, g.y0 + 3, SIZE2D(gsz - 6, gsz - 6), glyph); } @@ -450,7 +450,7 @@ static void wuss__icon_draw_radio_option(const icon_draw_ctx_t *c) /* a box; a tick (two strokes) when selected */ screen_draw_rect(c->scr, g.x0, g.y0, SIZE2D(g.x1 - g.x0, g.y1 - g.y0), glyph); - if (wuss__icon_selected(icon)) + if (wuss__icon_selected(c->icon)) { screen_draw_line(c->scr, g.x0 + 2, g.y0 + gsz / 2, g.x0 + gsz / 2 - 1, g.y1 - 3, glyph); @@ -486,7 +486,7 @@ static void wuss__icon_draw_bitmap(const icon_draw_ctx_t *c) const box_t *b = &c->b; screen_t clipped; - if (c->icon->u.bitmap.image == NULL) + if (c->icon->spec.u.bitmap.image == NULL) return; /* screen_copy_bitmap clips to scr->clip and does not scale, so narrow the @@ -496,20 +496,20 @@ static void wuss__icon_draw_bitmap(const icon_draw_ctx_t *c) if (box_intersection(&c->scr->clip, b, &clipped.clip)) return; - screen_copy_bitmap(&clipped, b->x0, b->y0, c->icon->u.bitmap.image); + screen_copy_bitmap(&clipped, b->x0, b->y0, c->icon->spec.u.bitmap.image); } /* ----------------------------------------------------------------------- */ static void wuss__icon_draw_menu_entry(const icon_draw_ctx_t *c) { - const wuss_icon_t *icon = c->icon; - const box_t *b = &c->b; - colour_t ink, ground, text_ink, text_ground, tmp; - int disabled, highlit, pad, text_x0, text_x1; + const wuss_icon_spec_t *icon = &c->icon->spec; + const box_t *b = &c->b; + colour_t ink, ground, text_ink, text_ground, tmp; + int disabled, highlit, pad, text_x0, text_x1; disabled = (icon->flags & wuss_ICON_FLAGS_DISABLED) != 0; - highlit = wuss__icon_hovered(icon) && !disabled; + highlit = wuss__icon_hovered(c->icon) && !disabled; pad = 4; /* resolve the row's own ink over its own/inherited ground */ @@ -552,7 +552,7 @@ static void wuss__icon_draw_menu_entry(const icon_draw_ctx_t *c) c->wuss->palette[icon->u.menu_entry.swatch]); screen_draw_rect(c->scr, cx, cy, SIZE2D(h, h), ink); /* 1px border */ } - else if (wuss__icon_selected(icon)) + else if (wuss__icon_selected(c->icon)) { int cx, cy, h; point_t centre; @@ -632,13 +632,14 @@ void wuss__icon_draw(wuss_t *wuss, const box_t *content, point_t scroll) { - icon_draw_ctx_t c; - int fontidx; + const wuss_icon_spec_t *spec = &icon->spec; + icon_draw_ctx_t c; + int fontidx; - if (icon->flags & wuss_ICON_FLAGS_HIDDEN) + if (spec->flags & wuss_ICON_FLAGS_HIDDEN) return; - wuss__icon_box_to_screen(content, scroll, &icon->bbox, &c.b); + wuss__icon_box_to_screen(content, scroll, &spec->bbox, &c.b); if (box_is_empty(&c.b)) return; @@ -647,21 +648,21 @@ void wuss__icon_draw(wuss_t *wuss, c.window = window; c.scr = wuss->scr; c.icon = icon; - c.fg = wuss->palette[icon->fg]; + c.fg = wuss->palette[spec->fg]; /* pick the icon's requested weight; fall back to the system font */ - fontidx = wuss_ICON_FONT_OF(icon->flags); + fontidx = wuss_ICON_FONT_OF(spec->flags); c.font = wuss->fonts[fontidx]; if (c.font == NULL) c.font = wuss->fonts[0]; - c.have_font = (c.font != NULL && icon->text[0] != '\0'); + c.have_font = (c.font != NULL && spec->text[0] != '\0'); if (c.have_font) bmfont_get_info(c.font, NULL, &c.font_height); else c.font_height = 0; - switch (icon->type) + switch (spec->type) { case wuss_ICON_TYPE_PATTERN: wuss__icon_draw_pattern(&c, content, scroll); diff --git a/libraries/wuss/icon/free.c b/libraries/wuss/icon/free.c index cb4c5e08..b2ce4fc3 100644 --- a/libraries/wuss/icon/free.c +++ b/libraries/wuss/icon/free.c @@ -17,7 +17,7 @@ void wuss__icons_free(wuss_window_t *window) for (i = 0; i < window->nicons; i++) { - wuss__free(w, window->icons[i]->text); + wuss__free(w, (char *) window->icons[i]->spec.text); wuss__free(w, window->icons[i]); } diff --git a/libraries/wuss/icon/from-spec.c b/libraries/wuss/icon/from-spec.c index 034200ea..b909108a 100644 --- a/libraries/wuss/icon/from-spec.c +++ b/libraries/wuss/icon/from-spec.c @@ -1,7 +1,6 @@ /* wuss/icon/from-spec.c -- validate an icon spec into a detached icon */ #include -#include #ifdef FORTIFY #include "fortify/fortify.h" @@ -89,36 +88,27 @@ result_t wuss__icon_from_spec(const wuss_t *w, if (has_swatch && swatch >= w->npalette) return result_WUSS_BAD_COLOUR; - out->bbox = spec->bbox; - out->type = spec->type; - out->text = (char *) ((spec->text != NULL) ? spec->text : ""); - out->fg = fg; - out->bg = bg; + out->spec = *spec; + out->spec.text = (spec->text != NULL) ? spec->text : ""; + out->spec.fg = fg; + out->spec.bg = bg; - memset(&out->u, 0, sizeof(out->u)); + /* out->spec.u is already the caller's copy; overwrite only the arms whose + * value we resolved (bitmap image from the icon set, swatch to a palette + * index) */ switch (spec->type) { - case wuss_ICON_TYPE_LABEL: - out->u.label.border = spec->u.label.border; - break; - case wuss_ICON_TYPE_PATTERN: - out->u.pattern.tile = spec->u.pattern.tile; - break; case wuss_ICON_TYPE_BITMAP: - out->u.bitmap.image = bitmap; - break; - case wuss_ICON_TYPE_RADIO: - out->u.radio.group = spec->u.radio.group; + out->spec.u.bitmap.image = bitmap; break; case wuss_ICON_TYPE_MENU_ENTRY: - out->u.menu_entry.swatch = swatch; + out->spec.u.menu_entry.swatch = swatch; break; default: break; } - out->flags = spec->flags; - out->state = wuss_ICON_STATE_NONE; + out->state = wuss_ICON_STATE_NONE; return result_OK; } diff --git a/libraries/wuss/icon/get-bbox.c b/libraries/wuss/icon/get-bbox.c index 99b42c1d..bdc867df 100644 --- a/libraries/wuss/icon/get-bbox.c +++ b/libraries/wuss/icon/get-bbox.c @@ -4,5 +4,5 @@ void wuss_icon_get_bbox(const wuss_icon_t *icon, box_t *bbox) { - *bbox = icon->bbox; + *bbox = icon->spec.bbox; } diff --git a/libraries/wuss/icon/get-text.c b/libraries/wuss/icon/get-text.c index b3f8de33..3cf01510 100644 --- a/libraries/wuss/icon/get-text.c +++ b/libraries/wuss/icon/get-text.c @@ -4,5 +4,5 @@ const char *wuss_icon_get_text(const wuss_icon_t *icon) { - return icon->text; + return icon->spec.text; } diff --git a/libraries/wuss/icon/get-type.c b/libraries/wuss/icon/get-type.c index f988044e..ef5271dd 100644 --- a/libraries/wuss/icon/get-type.c +++ b/libraries/wuss/icon/get-type.c @@ -4,5 +4,5 @@ wuss_icon_type_t wuss_icon_get_type(const wuss_icon_t *icon) { - return icon->type; + return icon->spec.type; } diff --git a/libraries/wuss/icon/hit-test.c b/libraries/wuss/icon/hit-test.c index 023d15e2..6626d089 100644 --- a/libraries/wuss/icon/hit-test.c +++ b/libraries/wuss/icon/hit-test.c @@ -16,23 +16,23 @@ wuss_icon_t *wuss__icon_hit_test(wuss_window_t *window, point_t doc_point) /* a bitmap icon is interactive only when it asks to be; RULE and the * static types are always inert; the other types always click */ - if (it->type == wuss_ICON_TYPE_BITMAP) + if (it->spec.type == wuss_ICON_TYPE_BITMAP) { - if (!(it->flags & wuss_ICON_FLAGS_INTERACTIVE)) + if (!(it->spec.flags & wuss_ICON_FLAGS_INTERACTIVE)) continue; } - else if (it->type != wuss_ICON_TYPE_ACTION && - it->type != wuss_ICON_TYPE_RADIO && - it->type != wuss_ICON_TYPE_OPTION && - it->type != wuss_ICON_TYPE_MENU_ENTRY) + else if (it->spec.type != wuss_ICON_TYPE_ACTION && + it->spec.type != wuss_ICON_TYPE_RADIO && + it->spec.type != wuss_ICON_TYPE_OPTION && + it->spec.type != wuss_ICON_TYPE_MENU_ENTRY) { continue; } - if (it->flags & (wuss_ICON_FLAGS_HIDDEN | wuss_ICON_FLAGS_DISABLED)) + if (it->spec.flags & (wuss_ICON_FLAGS_HIDDEN | wuss_ICON_FLAGS_DISABLED)) continue; - if (box_contains_point(&it->bbox, doc_point.x, doc_point.y)) + if (box_contains_point(&it->spec.bbox, doc_point.x, doc_point.y)) return it; } diff --git a/libraries/wuss/icon/invalidate.c b/libraries/wuss/icon/invalidate.c index 1ea11534..12d2a7a6 100644 --- a/libraries/wuss/icon/invalidate.c +++ b/libraries/wuss/icon/invalidate.c @@ -6,5 +6,5 @@ void wuss__icon_invalidate(wuss_window_t *window, 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(window, &icon->bbox); + wuss_window_invalidate(window, &icon->spec.bbox); } diff --git a/libraries/wuss/icon/set-hidden.c b/libraries/wuss/icon/set-hidden.c index 08be7a87..52bbf9a8 100644 --- a/libraries/wuss/icon/set-hidden.c +++ b/libraries/wuss/icon/set-hidden.c @@ -7,9 +7,9 @@ void wuss_icon_set_hidden(wuss_window_t *window, int hidden) { if (hidden) - icon->flags |= wuss_ICON_FLAGS_HIDDEN; + icon->spec.flags |= wuss_ICON_FLAGS_HIDDEN; else - icon->flags &= (wuss_icon_flags_t) ~wuss_ICON_FLAGS_HIDDEN; + icon->spec.flags &= (wuss_icon_flags_t) ~wuss_ICON_FLAGS_HIDDEN; wuss__icon_invalidate(window, icon); } diff --git a/libraries/wuss/icon/set-hover.c b/libraries/wuss/icon/set-hover.c index cec40d17..61cab05a 100644 --- a/libraries/wuss/icon/set-hover.c +++ b/libraries/wuss/icon/set-hover.c @@ -9,7 +9,7 @@ * so redrawing them on pointer enter/leave is wasted work. */ static int wuss__icon_hover_visible(const wuss_icon_t *icon) { - return icon->type == wuss_ICON_TYPE_MENU_ENTRY; + return icon->spec.type == wuss_ICON_TYPE_MENU_ENTRY; } void wuss__icon_set_hover(wuss_t *wuss, diff --git a/libraries/wuss/icon/set-selected.c b/libraries/wuss/icon/set-selected.c index db42b2a2..58640f65 100644 --- a/libraries/wuss/icon/set-selected.c +++ b/libraries/wuss/icon/set-selected.c @@ -14,25 +14,25 @@ void wuss__icon_select(wuss_window_t *window, assert(window != NULL); assert(icon != NULL); - if (icon->type != wuss_ICON_TYPE_RADIO && - icon->type != wuss_ICON_TYPE_OPTION && - icon->type != wuss_ICON_TYPE_MENU_ENTRY) + if (icon->spec.type != wuss_ICON_TYPE_RADIO && + icon->spec.type != wuss_ICON_TYPE_OPTION && + icon->spec.type != wuss_ICON_TYPE_MENU_ENTRY) return; selected = selected ? 1 : 0; /* selecting a grouped radio clears its siblings first */ if (selected && - icon->type == wuss_ICON_TYPE_RADIO && - icon->u.radio.group != 0) + icon->spec.type == wuss_ICON_TYPE_RADIO && + icon->spec.u.radio.group != 0) { for (i = 0; i < window->nicons; i++) { other = window->icons[i]; if (other == icon) continue; - if (other->type != wuss_ICON_TYPE_RADIO || - other->u.radio.group != icon->u.radio.group) + if (other->spec.type != wuss_ICON_TYPE_RADIO || + other->spec.u.radio.group != icon->spec.u.radio.group) continue; if (!wuss__icon_selected(other)) continue; diff --git a/libraries/wuss/icon/set-text.c b/libraries/wuss/icon/set-text.c index 1a565ef9..395e006f 100644 --- a/libraries/wuss/icon/set-text.c +++ b/libraries/wuss/icon/set-text.c @@ -27,8 +27,8 @@ result_t wuss_icon_set_text(wuss_window_t *window, return result_OOM; memcpy(dup, src, len + 1); - wuss__free(w, icon->text); - icon->text = dup; + wuss__free(w, (char *) icon->spec.text); + icon->spec.text = dup; wuss__icon_invalidate(window, icon); From 5071d2618638a3da130adaf9401ec4ea9648b0ea Mon Sep 17 00:00:00 2001 From: David Thomas Date: Thu, 10 Sep 2026 18:46:27 +0100 Subject: [PATCH 008/128] docs: rewrap Doxygen comments to column limit Ran tools/wrap_doxygen.py across the headers; two blocks were over 77 cols and reflowed. Co-Authored-By: Claude Sonnet 5 --- include/wuss/menu-desc.h | 3 ++- libraries/framebuf/pixelmap/impl.h | 11 ++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/include/wuss/menu-desc.h b/include/wuss/menu-desc.h index d15c9a19..99e87f5f 100644 --- a/include/wuss/menu-desc.h +++ b/include/wuss/menu-desc.h @@ -6,7 +6,8 @@ * A convenience helper on top of wuss/menu.h: parse a compact * PrivateEye-style descriptor string into a heap wuss_menu_t tree the caller * owns, and free it again. Nothing in the core menu helper depends on this; - * a task that hand-assembles its wuss_menu_t arrays never needs to include it. + * a task that hand-assembles its wuss_menu_t arrays never needs to include + * it. * * Built only when WUSS_MENUS is defined. */ diff --git a/libraries/framebuf/pixelmap/impl.h b/libraries/framebuf/pixelmap/impl.h index 4ed94fb3..90048769 100644 --- a/libraries/framebuf/pixelmap/impl.h +++ b/libraries/framebuf/pixelmap/impl.h @@ -11,12 +11,13 @@ /* ----------------------------------------------------------------------- */ /** - * Fill in the channel layout of `out` (rbits/gbits/bbits, rshift/gshift/bshift, - * destfmt, dest_log2bpp, nentries) for the given format pair, from the built-in - * policy table. + * Fill in the channel layout of `out` (rbits/gbits/bbits, + * rshift/gshift/bshift, destfmt, dest_log2bpp, nentries) for the given + * format pair, from the built-in policy table. * - * The source format must be a 32bpp `*8888` format; its channels are extracted - * with an 8-bit right shift (\ref pixelmap_t rshift/gshift/bshift). + * The source format must be a 32bpp `*8888` format; its channels are + * extracted with an 8-bit right shift (\ref pixelmap_t + * rshift/gshift/bshift). * * \param[in] srcfmt Source pixel format. * \param[in] destfmt Destination format. From 57c42878978da47f3c2f7ec49851704b455efc02 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Thu, 10 Sep 2026 18:56:52 +0100 Subject: [PATCH 009/128] refactor(wuss): move fonts into a core-owned wuss/font module The region core delegated furniture, icons and menus but kept font storage and text rendering as loose fields and files. Collect them into libraries/wuss/font/: struct wuss_fontset (the slot arrays plus count), wuss__fontset_init/_height, the wuss_get_font* accessors (was core/get-font.c) and the centralised wuss__text_* renderer (was core/text.c). struct wuss's four loose font fields collapse to one embedded fontset. create.c drops its inline slot-fill loop and sizes the titlebar from wuss__fontset_height. Furniture, menu and icon draw sites move from wuss->fonts[n] / wuss->nfonts to the fontset members. Fonts stay a core service consumed by furniture/menu/icons, not an icon concern. Co-Authored-By: Claude Sonnet 5 --- CMakeLists.txt | 5 +- libraries/wuss/core/create.c | 44 +++----------- libraries/wuss/core/get-font.c | 42 -------------- libraries/wuss/core/impl.h | 31 ++-------- libraries/wuss/font/font.h | 61 ++++++++++++++++++++ libraries/wuss/font/registry.c | 86 ++++++++++++++++++++++++++++ libraries/wuss/{core => font}/text.c | 4 +- libraries/wuss/furniture/draw.c | 6 +- libraries/wuss/icon/draw.c | 6 +- libraries/wuss/menu/menu.c | 13 +++-- 10 files changed, 178 insertions(+), 120 deletions(-) delete mode 100644 libraries/wuss/core/get-font.c create mode 100644 libraries/wuss/font/font.h create mode 100644 libraries/wuss/font/registry.c rename libraries/wuss/{core => font}/text.c (93%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 255e14c5..db553d62 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -371,7 +371,6 @@ set(WUSS_CORE_SOURCES libraries/wuss/core/backdrop.c libraries/wuss/core/create.c libraries/wuss/core/destroy.c - libraries/wuss/core/get-font.c libraries/wuss/core/get-palette.c libraries/wuss/core/get-pointer.c libraries/wuss/core/idle.c @@ -386,7 +385,9 @@ set(WUSS_CORE_SOURCES libraries/wuss/core/scroll-step.c libraries/wuss/core/set-backdrop.c libraries/wuss/core/set-palette.c - libraries/wuss/core/text.c + libraries/wuss/font/font.h + libraries/wuss/font/registry.c + libraries/wuss/font/text.c libraries/wuss/core/task/create.c libraries/wuss/core/task/destroy.c libraries/wuss/core/task/set-autoclose.c diff --git a/libraries/wuss/core/create.c b/libraries/wuss/core/create.c index 0b8547c4..d1a0487d 100644 --- a/libraries/wuss/core/create.c +++ b/libraries/wuss/core/create.c @@ -63,7 +63,6 @@ result_t wuss_create(screen_t *scr, const wuss_alloc_t *alloc, wuss_t **wuss) { - bmfont_t *font; wuss_alloc_t al; wuss_t *w; #ifdef WUSS_FURNITURE @@ -84,8 +83,6 @@ result_t wuss_create(screen_t *scr, if (nfonts < 0 || nfonts > wuss_MAX_FONTS || (nfonts > 0 && fonts == NULL)) return result_BAD_ARG; - font = (nfonts > 0) ? fonts[0].font : NULL; - al = (alloc != NULL) ? *alloc : wuss_alloc; w = al.malloc(sizeof(*w)); @@ -93,6 +90,8 @@ result_t wuss_create(screen_t *scr, return result_OOM; w->alloc = al; + wuss__fontset_init(&w->fonts, fonts, nfonts); + if (palette == NULL) { palette = wuss__default_palette; @@ -229,23 +228,15 @@ result_t wuss_create(screen_t *scr, { w->titlebar_height = config->titlebar_height; } - else if (font != NULL) - { - /* titles draw in the bold weight when one was supplied; size the - * titlebar to whichever weight is taller */ - bmfont_get_info(font, NULL, &font_height); - if (nfonts > 1 && fonts[1].font != NULL) - { - int titleh; - - bmfont_get_info(fonts[1].font, NULL, &titleh); - font_height = MAX(font_height, titleh); - } - w->titlebar_height = font_height + 4; - } else { - w->titlebar_height = WUSS_DEFAULT_TITLEBAR_HEIGHT; + /* titles draw in the bold weight when one was supplied; size the + * titlebar to whichever weight is taller. Both slots empty -> fall back + * to the default. */ + font_height = MAX(wuss__fontset_height(&w->fonts, 0), + wuss__fontset_height(&w->fonts, 1)); + w->titlebar_height = (font_height > 0) ? font_height + 4 + : WUSS_DEFAULT_TITLEBAR_HEIGHT; } #else /* !WUSS_FURNITURE */ #ifdef WUSS_ICONS @@ -304,23 +295,6 @@ result_t wuss_create(screen_t *scr, wuss__rebuild_palettecache(w); w->scr = scr; - { - int i; - - for (i = 0; i < nfonts; i++) - { - w->fonts[i] = fonts[i].font; - w->font_classes[i] = fonts[i].font_class; - w->font_names[i] = fonts[i].name; - } - for (; i < wuss_MAX_FONTS; i++) - { - w->fonts[i] = NULL; - w->font_classes[i] = wuss_FONT_CLASS_NONE; - w->font_names[i] = NULL; - } - w->nfonts = nfonts; - } #ifdef WUSS_FURNITURE w->furniture.dragging = NULL; w->furniture.drag.x = 0; diff --git a/libraries/wuss/core/get-font.c b/libraries/wuss/core/get-font.c deleted file mode 100644 index 8e2f75ac..00000000 --- a/libraries/wuss/core/get-font.c +++ /dev/null @@ -1,42 +0,0 @@ -/* wuss/core/get-font.c -- look up a registered font slot by index */ - -#include - -#include "impl.h" - -bmfont_t *wuss_get_font(const wuss_t *wuss) -{ - assert(wuss != NULL); - - return wuss->fonts[0]; -} - -bmfont_t *wuss_get_font_n(const wuss_t *wuss, int index) -{ - assert(wuss != NULL); - - if (index < 0 || index >= wuss_MAX_FONTS) - return NULL; - - return wuss->fonts[index]; -} - -wuss_font_class_t wuss_get_font_class_n(const wuss_t *wuss, int index) -{ - assert(wuss != NULL); - - if (index < 0 || index >= wuss_MAX_FONTS) - return wuss_FONT_CLASS_NONE; - - return wuss->font_classes[index]; -} - -const char *wuss_get_font_name_n(const wuss_t *wuss, int index) -{ - assert(wuss != NULL); - - if (index < 0 || index >= wuss_MAX_FONTS) - return NULL; - - return wuss->font_names[index]; -} diff --git a/libraries/wuss/core/impl.h b/libraries/wuss/core/impl.h index d8c83c53..87438b42 100644 --- a/libraries/wuss/core/impl.h +++ b/libraries/wuss/core/impl.h @@ -21,6 +21,8 @@ #include "wuss/window.h" #include "wuss/task.h" +#include "../font/font.h" + #ifdef WUSS_FURNITURE #include "../furniture.h" #endif @@ -101,14 +103,8 @@ struct wuss_task struct wuss { screen_t *scr; - bmfont_t *fonts[wuss_MAX_FONTS]; /* slot 0 is the system - * font; unset slots NULL; not owned */ - wuss_font_class_t font_classes[wuss_MAX_FONTS]; /* parallel to - * fonts[]; NONE for an unset slot */ - const char *font_names[wuss_MAX_FONTS]; /* parallel to - * fonts[]; borrowed; NULL if unset - * or given no name */ - int nfonts; /* entries filled from wuss_create */ + struct wuss_fontset fonts; /* font slots from wuss_create; slot 0 + * is the system font. See font/font.h */ wuss_alloc_t alloc; /* malloc/realloc/free hooks, copied in * by wuss_create; used for every heap * block this wuss_t owns */ @@ -299,25 +295,6 @@ static inline void wuss__chrome_invalidate_layout(wuss_window_t *window) wuss_window_t *wuss__window_at(wuss_t *wuss, point_t p); -/* Centralised text rendering. Every wuss text draw goes through - * wuss__text_draw so an optical vertical bias (WUSS_TEXT_BASELINE_ADJUST, - * default 1px down) is applied uniformly; wuss__text_measure is a plain - * pass-through kept alongside for a single point of policy. */ -result_t wuss__text_measure(bmfont_t *font, - const char *text, - int len, - bmfont_width_t target_width, - int *split_point, - bmfont_width_t *actual_width); -result_t wuss__text_draw(bmfont_t *font, - screen_t *scr, - const char *text, - int len, - colour_t fg, - colour_t bg, - const point_t *pos, - point_t *end_pos); - /* Rebuild wuss->palettecache (white, black and the symbolic[] table) from * the current palette and the stored chrome colours. Call after the palette * or any chrome colour changes; the chrome fields must already be concrete diff --git a/libraries/wuss/font/font.h b/libraries/wuss/font/font.h new file mode 100644 index 00000000..4343c9f7 --- /dev/null +++ b/libraries/wuss/font/font.h @@ -0,0 +1,61 @@ +/* wuss/font/font.h -- wuss font registry and centralised text rendering */ + +#ifndef WUSS_FONT_H +#define WUSS_FONT_H + +#include "geom/point.h" +#include "framebuf/bmfont.h" +#include "framebuf/screen.h" + +#include "base/result.h" + +#include "wuss/wuss.h" + +/* The set of fonts a wuss_t was created with: slot 0 is the system font used + * for window titlebars and offered to tasks via wuss_get_font; further slots + * hold optional bold/symbol faces. Populated once by wuss__fontset_init from + * wuss_create's argument and never changed after. Fonts are borrowed, not + * owned. */ +struct wuss_fontset +{ + /* unset slots NULL */ + bmfont_t *fonts[wuss_MAX_FONTS]; + /* parallel to fonts[]; wuss_FONT_CLASS_NONE for an unset slot */ + wuss_font_class_t font_classes[wuss_MAX_FONTS]; + /* parallel to fonts[]; borrowed; NULL if unset or given no name */ + const char *font_names[wuss_MAX_FONTS]; + /* slots filled from the wuss_create argument */ + int nfonts; +}; + +/* Copy the first "nfonts" descriptors into "set" and NULL the rest. "nfonts" + * has already been range-checked by the caller (0..wuss_MAX_FONTS). */ +void wuss__fontset_init(struct wuss_fontset *set, + const wuss_font_desc_t *fonts, + int nfonts); + +/* Pixel height of the font in "slot", or 0 if that slot is empty or out of + * range. Used by wuss_create to size the titlebar to the tallest of the + * regular and bold faces. */ +int wuss__fontset_height(const struct wuss_fontset *set, int slot); + +/* Centralised text rendering. Every wuss text draw goes through + * wuss__text_draw so an optical vertical bias (WUSS_TEXT_BASELINE_ADJUST, + * default 1px down) is applied uniformly; wuss__text_measure is a plain + * pass-through kept alongside for a single point of policy. */ +result_t wuss__text_measure(bmfont_t *font, + const char *text, + int len, + bmfont_width_t target_width, + int *split_point, + bmfont_width_t *actual_width); +result_t wuss__text_draw(bmfont_t *font, + screen_t *scr, + const char *text, + int len, + colour_t fg, + colour_t bg, + const point_t *pos, + point_t *end_pos); + +#endif /* WUSS_FONT_H */ diff --git a/libraries/wuss/font/registry.c b/libraries/wuss/font/registry.c new file mode 100644 index 00000000..5787fc0f --- /dev/null +++ b/libraries/wuss/font/registry.c @@ -0,0 +1,86 @@ +/* wuss/font/registry.c -- wuss font slot registry and lookups */ + +#include + +#include "framebuf/bmfont.h" + +#include "wuss/wuss.h" + +#include "../core/impl.h" +#include "font.h" + +void wuss__fontset_init(struct wuss_fontset *set, + const wuss_font_desc_t *fonts, + int nfonts) +{ + int i; + + assert(set != NULL); + assert(nfonts >= 0 && nfonts <= wuss_MAX_FONTS); + + for (i = 0; i < nfonts; i++) + { + set->fonts[i] = fonts[i].font; + set->font_classes[i] = fonts[i].font_class; + set->font_names[i] = fonts[i].name; + } + for (; i < wuss_MAX_FONTS; i++) + { + set->fonts[i] = NULL; + set->font_classes[i] = wuss_FONT_CLASS_NONE; + set->font_names[i] = NULL; + } + set->nfonts = nfonts; +} + +int wuss__fontset_height(const struct wuss_fontset *set, int slot) +{ + int height; + + assert(set != NULL); + + if (slot < 0 || slot >= wuss_MAX_FONTS || set->fonts[slot] == NULL) + return 0; + + bmfont_get_info(set->fonts[slot], NULL, &height); + return height; +} + +/* ----------------------------------------------------------------------- */ + +bmfont_t *wuss_get_font(const wuss_t *wuss) +{ + assert(wuss != NULL); + + return wuss->fonts.fonts[0]; +} + +bmfont_t *wuss_get_font_n(const wuss_t *wuss, int index) +{ + assert(wuss != NULL); + + if (index < 0 || index >= wuss_MAX_FONTS) + return NULL; + + return wuss->fonts.fonts[index]; +} + +wuss_font_class_t wuss_get_font_class_n(const wuss_t *wuss, int index) +{ + assert(wuss != NULL); + + if (index < 0 || index >= wuss_MAX_FONTS) + return wuss_FONT_CLASS_NONE; + + return wuss->fonts.font_classes[index]; +} + +const char *wuss_get_font_name_n(const wuss_t *wuss, int index) +{ + assert(wuss != NULL); + + if (index < 0 || index >= wuss_MAX_FONTS) + return NULL; + + return wuss->fonts.font_names[index]; +} diff --git a/libraries/wuss/core/text.c b/libraries/wuss/font/text.c similarity index 93% rename from libraries/wuss/core/text.c rename to libraries/wuss/font/text.c index 6d04abe6..920fa729 100644 --- a/libraries/wuss/core/text.c +++ b/libraries/wuss/font/text.c @@ -1,10 +1,10 @@ -/* text.c -- wuss centralised bitmap-font text rendering */ +/* wuss/font/text.c -- wuss centralised bitmap-font text rendering */ #include "geom/point.h" #include "framebuf/bmfont.h" #include "framebuf/screen.h" -#include "impl.h" +#include "font.h" /* Optical vertical bias applied to every text draw so glyphs sit visually * centred rather than mathematically centred. Positive shifts text down. */ diff --git a/libraries/wuss/furniture/draw.c b/libraries/wuss/furniture/draw.c index a9b94050..52db2d24 100644 --- a/libraries/wuss/furniture/draw.c +++ b/libraries/wuss/furniture/draw.c @@ -76,9 +76,9 @@ static void draw_title(wuss_t *wuss, /* window titles are drawn in the bold weight (font slot 1) when one was * supplied, falling back to the system font */ - titlefont = (wuss->nfonts > 1 && wuss->fonts[1] != NULL) - ? wuss->fonts[1] - : wuss->fonts[0]; + titlefont = (wuss->fonts.nfonts > 1 && wuss->fonts.fonts[1] != NULL) + ? wuss->fonts.fonts[1] + : wuss->fonts.fonts[0]; if (titlefont == NULL) return; diff --git a/libraries/wuss/icon/draw.c b/libraries/wuss/icon/draw.c index 37ad99d1..2910457e 100644 --- a/libraries/wuss/icon/draw.c +++ b/libraries/wuss/icon/draw.c @@ -127,7 +127,7 @@ static int wuss__draw_symbol_glyph(const wuss_t *wuss, point_t pos; int font_width, font_height; - font = wuss->fonts[WUSS_SYMBOL_FONT]; + font = wuss->fonts.fonts[WUSS_SYMBOL_FONT]; if (font == NULL) return 0; @@ -652,9 +652,9 @@ void wuss__icon_draw(wuss_t *wuss, /* pick the icon's requested weight; fall back to the system font */ fontidx = wuss_ICON_FONT_OF(spec->flags); - c.font = wuss->fonts[fontidx]; + c.font = wuss->fonts.fonts[fontidx]; if (c.font == NULL) - c.font = wuss->fonts[0]; + c.font = wuss->fonts.fonts[0]; c.have_font = (c.font != NULL && spec->text[0] != '\0'); if (c.have_font) diff --git a/libraries/wuss/menu/menu.c b/libraries/wuss/menu/menu.c index f456bb21..c56857e1 100644 --- a/libraries/wuss/menu/menu.c +++ b/libraries/wuss/menu/menu.c @@ -550,7 +550,7 @@ static result_t wuss__menu_spawn(wuss_t *wuss, assert(wuss != NULL); assert(menu != NULL); - assert(wuss->fonts[0] != NULL); + assert(wuss->fonts.fonts[0] != NULL); if (menu->nitems <= 0) return result_WUSS_BAD_ICON; @@ -567,7 +567,7 @@ static result_t wuss__menu_spawn(wuss_t *wuss, outline_px = wuss__outline_px_for(menu_flags); titlebar_height = wuss__titlebar_height_for(wuss, menu_flags); - bmfont_get_info(wuss->fonts[0], NULL, &fh); + bmfont_get_info(wuss->fonts.fonts[0], NULL, &fh); pitch = fh + 2 * WUSS_MENU_ROW_PAD; sep_h = 2 * WUSS_MENU_ROW_PAD; @@ -589,7 +589,7 @@ static result_t wuss__menu_spawn(wuss_t *wuss, len = (int) strlen(text); if (len == 0) continue; /* empty label (bare rule row); nothing to measure */ - if (wuss__text_measure(wuss->fonts[0], text, len, + if (wuss__text_measure(wuss->fonts.fonts[0], text, len, INT_MAX, &split, &w) == result_OK && (int) w > widest) widest = (int) w; } @@ -600,7 +600,7 @@ static result_t wuss__menu_spawn(wuss_t *wuss, { bmfont_width_t space_w = 0; - wuss__text_measure(wuss->fonts[0], " ", 1, INT_MAX, NULL, &space_w); + wuss__text_measure(wuss->fonts.fonts[0], " ", 1, INT_MAX, NULL, &space_w); widest += 2 * (int) space_w; } @@ -617,8 +617,9 @@ static result_t wuss__menu_spawn(wuss_t *wuss, int split; bmfont_width_t title_w; - titlefont = (wuss->nfonts > 1 && wuss->fonts[1] != NULL) ? wuss->fonts[1] - : wuss->fonts[0]; + titlefont = (wuss->fonts.nfonts > 1 && wuss->fonts.fonts[1] != NULL) + ? wuss->fonts.fonts[1] + : wuss->fonts.fonts[0]; titlelen = (int) strlen(menu->title); if (wuss__text_measure(titlefont, menu->title, titlelen, INT_MAX, &split, &title_w) == result_OK && From aa1c52b791a17d257116c3c7bda8e19df49a79dd Mon Sep 17 00:00:00 2001 From: David Thomas Date: Thu, 10 Sep 2026 18:57:54 +0100 Subject: [PATCH 010/128] docs(wuss): describe the font module Add a Fonts subsection under Setup covering libraries/wuss/font/: the slot array, wuss_font_desc_t, per-slot roles (system / bold / symbol), the empty-slot fallbacks and titlebar sizing, and the readback accessors. Update the stale wuss_create signature to the current fonts-array / alloc form and add a Font slot glossary entry. Co-Authored-By: Claude Sonnet 5 --- docs/windowing/wuss.md | 47 +++++++++++++++++++++++++++++++++++------- 1 file changed, 39 insertions(+), 8 deletions(-) diff --git a/docs/windowing/wuss.md b/docs/windowing/wuss.md index 9b96951c..fd4f73a4 100644 --- a/docs/windowing/wuss.md +++ b/docs/windowing/wuss.md @@ -9,18 +9,48 @@ ## Setup -Create a window manager onto a `screen_t`, with an optional font for titlebar labels and an optional system palette: +Create a window manager onto a `screen_t`, with an array of font slots, an optional system palette, optional creation config and optional allocator hooks: ```C -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, + const wuss_font_desc_t *fonts, + int nfonts, + const colour_t *palette, + int npalette, + const wuss_config_t *config, + const wuss_alloc_t *alloc, + wuss_t **wuss); ``` -`font` and `palette` may both be NULL, for unlabelled titlebars and a built-in default palette respectively. `config` may be NULL for default titlebar height/colours. `config->backdrop` is a `wuss_backdrop_t` — a flat colour, or an 8x8 fill pattern — painted behind windows on every redraw; set its `colour` to `wuss_NO_BACKGROUND` (the default when `config` is NULL) to leave the background untouched and require the caller to repaint it itself. A non-`screen_PATTERN_SOLID` `pattern` tiles that pattern in `colour` over `pattern_bg`, phased to a fixed screen origin so it does not crawl between full and dirty-region redraws. `wuss_get_font` reads back the font passed in (or NULL), for a task that wants to draw its own content in the same face as titlebars. +`fonts` (or `nfonts` 0) may be NULL, for unlabelled titlebars; `palette` may be NULL for a built-in default palette; `alloc` may be NULL for plain stdlib `malloc`/`realloc`/`free`. `config` may be NULL for default titlebar height/colours. `config->backdrop` is a `wuss_backdrop_t` — a flat colour, or an 8x8 fill pattern — painted behind windows on every redraw; set its `colour` to `wuss_NO_BACKGROUND` (the default when `config` is NULL) to leave the background untouched and require the caller to repaint it itself. A non-`screen_PATTERN_SOLID` `pattern` tiles that pattern in `colour` over `pattern_bg`, phased to a fixed screen origin so it does not crawl between full and dirty-region redraws. + +### Fonts + +Text drawing lives in its own module, `libraries/wuss/font/`: the region core delegates it the same way it delegates furniture, icons and menus. It owns the font slots a `wuss_t` was created with and the one text-rendering primitive furniture, menus and icons all call, so region handling can be used without pulling any font code in beyond the slot storage. + +`wuss_create` takes up to `wuss_MAX_FONTS` (4) slots as an array of `wuss_font_desc_t`: + +```C +typedef struct wuss_font_desc +{ + bmfont_t *font; /* not owned; must outlive the wuss_t */ + wuss_font_class_t font_class; /* wuss_FONT_CLASS_NONE (a text face) or + * wuss_FONT_CLASS_SYSTEM (chrome only) */ + const char *name; /* borrowed; the font's leafname sans ".png", + * for a picker to match against; may be NULL */ +} +wuss_font_desc_t; +``` + +The descriptors are copied in; the fonts are not owned. Slot use: + +- **slot 0** — the system font: titlebar labels, menu rows, and any icon that does not select another weight. Also what `wuss_get_font` reads back, for a task that wants to draw its own content in the same face. +- **slot 1** — the bold weight, if supplied: window and menu titles draw from it, falling back to slot 0. +- **slot `WUSS_SYMBOL_FONT` (2)** — an optional symbol font the menu tick and submenu arrow are drawn from as glyphs; without it they are drawn as vector strokes. + +An empty slot is NULL and callers degrade to slot 0 or a stroke fallback. When no `config->titlebar_height` is set, the titlebar is sized to the taller of the slot-0 and slot-1 faces, or `WUSS_DEFAULT_TITLEBAR_HEIGHT` if both are empty. + +Read the slots back with `wuss_get_font` (slot 0), `wuss_get_font_n(wuss, i)`, `wuss_get_font_class_n(wuss, i)` and `wuss_get_font_name_n(wuss, i)` — the last two let a picker such as `wuss_fontmenu` tell a chrome font apart from a selectable text face. All text wuss itself draws goes through one internal primitive so a uniform optical baseline nudge is applied everywhere. A `wuss_backdrop_t` is `{ colour, pattern, pattern_bg }`; the `wuss_BACKDROP_COLOUR(c)` and `wuss_BACKDROP_PATTERN(c, p, b)` macros build one as a compound literal. @@ -268,6 +298,7 @@ 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. +- **Font slot** — one of up to `wuss_MAX_FONTS` `wuss_font_desc_t` entries passed to `wuss_create` and owned by the `libraries/wuss/font/` module. Slot 0 is the system font, slot 1 the bold weight, slot 2 (`WUSS_SYMBOL_FONT`) an optional glyph font for menu decoration. Read back with `wuss_get_font` / `wuss_get_font_n`. See "Fonts". - **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 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. From af13876a6433695edbc328177d643f6faa674f8d Mon Sep 17 00:00:00 2001 From: David Thomas Date: Thu, 10 Sep 2026 20:43:01 +0100 Subject: [PATCH 011/128] feat(framebuf): add ordered-dither variant of screen_copy_bitmap screen_copy_bitmap_dithered() blits a bitmap onto a paletted screen with per-pixel 8x8 Bayer ordered dithering before the nearest-palette-entry lookup, so a smooth gradient stipples between the available entries instead of banding at each quantisation step. The dither is phased to screen coordinates so it stays put across redraws. On 32bpp screens and for RLE sources it is identical to screen_copy_bitmap. pattern_bayer_threshold() exposes the recursive 8x8 Bayer matrix (0..63) that the screen_PATTERN_BAYER* tiles are derived from, for callers that dither a continuous value rather than fill with a fixed-coverage tile. The dither amplitude is half the mean palette-level gap, a heuristic that assumes a roughly even greyish palette. The wuss image task gains a 'Dithering' menu toggle: it seeds the row tick from the task state on open, flips the state and redraws on select, and re-ticks the row in place when the menu is kept open after the pick. Co-Authored-By: Claude Sonnet 5 --- include/framebuf/pattern.h | 13 +++ include/framebuf/screen.h | 25 +++++ libraries/framebuf/pattern/pattern.c | 22 ++++ libraries/framebuf/screen/screen-draw.c | 107 +++++++++++++++--- libraries/framebuf/screen/test/screen-test.c | 111 ++++++++++++++++++- libraries/wuss/test/tasks/image.c | 30 ++++- libraries/wuss/test/tasks/image.h | 1 + 7 files changed, 286 insertions(+), 23 deletions(-) diff --git a/include/framebuf/pattern.h b/include/framebuf/pattern.h index 67c783ee..641be866 100644 --- a/include/framebuf/pattern.h +++ b/include/framebuf/pattern.h @@ -105,4 +105,17 @@ pattern_t pattern_from_preset(screen_pattern_t preset, */ pattern_t pattern_from_mask(const uint8_t mask[8], colour_t colour); +/** + * The 8x8 ordered (Bayer) dither threshold for screen pixel (`x`, `y`), a + * value in the range 0 to 63. This is the recursively-built matrix the + * `screen_PATTERN_BAYER*` tiles are derived from, exposed for callers that + * dither a continuous value per pixel rather than fill with a fixed-coverage + * tile (e.g. `screen_copy_bitmap_dithered`). `x` and `y` may be negative. + * + * \param[in] x Screen X coordinate. + * \param[in] y Screen Y coordinate. + * \return The threshold, 0 to 63. + */ +int pattern_bayer_threshold(int x, int y); + #endif /* FRAMEBUF_PATTERN_H */ diff --git a/include/framebuf/screen.h b/include/framebuf/screen.h index 6c9b668f..58b0b634 100644 --- a/include/framebuf/screen.h +++ b/include/framebuf/screen.h @@ -169,6 +169,31 @@ result_t screen_copy_bitmap(screen_t *scr, int y, const bitmap_t *src); +/** + * As `screen_copy_bitmap`, but on a paletted screen the source colour is + * ordered-dithered (8x8 Bayer) per pixel before the nearest-palette-entry + * lookup, so a smooth gradient stipples between the available entries + * instead of banding at each quantisation step. The dither is phased to + * screen coordinates, so it stays put when the same content is redrawn at + * the same place. + * + * On a 32bpp screen, and for an RLE-compressed source, this is identical to + * `screen_copy_bitmap` (a deep screen has no banding to break up; an RLE + * source is already quantised UI art). Alpha handling, clipping and the lack + * of scaling are all as `screen_copy_bitmap`. + * + * \param[in] scr Screen to draw upon. + * \param[in] x X coordinate of leftmost point to draw bitmap at. + * \param[in] y Y coordinate of topmost point to draw bitmap at. + * \param[in] src Bitmap to copy. + * \return \ref result_OK on success, \ref result_NOT_SUPPORTED if the + * screen's pixel format has no blit path. + */ +result_t screen_copy_bitmap_dithered(screen_t *scr, + int x, + int y, + const bitmap_t *src); + /** Flags for `screen_copy_ninepatch`. */ enum { diff --git a/libraries/framebuf/pattern/pattern.c b/libraries/framebuf/pattern/pattern.c index 42634b9d..6e3c5b27 100644 --- a/libraries/framebuf/pattern/pattern.c +++ b/libraries/framebuf/pattern/pattern.c @@ -137,3 +137,25 @@ pattern_t pattern_from_mask(const uint8_t mask[8], colour_t colour) return pat; } + +/* ----------------------------------------------------------------------- */ + +/* The recursively-built 8x8 ordered dither matrix, cell values 0..63. The + * BAYER* tiles above are this same matrix thresholded at each coverage + * level; here it is kept whole for per-pixel continuous dithering. */ +static const uint8_t bayer8x8[8][8] = +{ + { 0, 32, 8, 40, 2, 34, 10, 42 }, + { 48, 16, 56, 24, 50, 18, 58, 26 }, + { 12, 44, 4, 36, 14, 46, 6, 38 }, + { 60, 28, 52, 20, 62, 30, 54, 22 }, + { 3, 35, 11, 43, 1, 33, 9, 41 }, + { 51, 19, 59, 27, 49, 17, 57, 25 }, + { 15, 47, 7, 39, 13, 45, 5, 37 }, + { 63, 31, 55, 23, 61, 29, 53, 21 } +}; + +int pattern_bayer_threshold(int x, int y) +{ + return bayer8x8[y & 7][x & 7]; +} diff --git a/libraries/framebuf/screen/screen-draw.c b/libraries/framebuf/screen/screen-draw.c index 877d0105..4f9fbd94 100644 --- a/libraries/framebuf/screen/screen-draw.c +++ b/libraries/framebuf/screen/screen-draw.c @@ -9,6 +9,7 @@ #include "base/utils.h" #include "framebuf/colour.h" +#include "framebuf/pattern.h" #include "framebuf/pixelmap.h" #include "framebuf/span-registry.h" #include "geom/line.h" @@ -292,27 +293,62 @@ void screen_fill_square(screen_t *scr, /* ----------------------------------------------------------------------- */ +/* Ordered-dither one channel value "v" (0..255) for screen pixel (sx, sy) + * before it is matched to the nearest palette entry. "nlevels" is the palette + * entry count; the mean gap between adjacent levels of a linear ramp is then + * 255 / (nlevels - 1), and the Bayer cell (-32..31 about zero) nudges "v" by + * up to half that gap either way, so a value sitting between two entries lands + * on one or the other in a fixed 8x8 pattern instead of always snapping to + * the nearer. Result clamped to 0..255. + * + * ponytail: this assumes a roughly even, roughly greyscale palette -- the one + * case (shallow paletted screen showing a gradient) the dithered blit is for. + * A wildly non-uniform palette dithers weakly, not wrongly; swap in a + * per-entry nearest-two search keyed on the real palette if that matters. */ +static unsigned int dither_channel(unsigned int v, + int nlevels, + int dither, + int sx, + int sy) +{ + int gap; + int adj; + + if (!dither || nlevels < 2) + return v; + + gap = 255 / (nlevels - 1); + adj = ((pattern_bayer_threshold(sx, sy) - 32) * gap) / 64; + + return (unsigned int) CLAMP((int) v + adj, 0, 255); +} + /* Blit "src" onto the paletted screen, its top-left at (x, y), clipped to * "draw_box". No linear channel bits to blend, so this does an alpha-tested * transfer (skip fully transparent, else nearest palette match) rather than - * true blending, matching screen_set_pixel's case 2. Returns - * result_NOT_SUPPORTED if there is no deep->paletted conversion table for the - * screen's format. */ + * true blending, matching screen_set_pixel's case 2. With "dither" set the + * source RGB is ordered-dithered per pixel before the nearest-match lookup, + * breaking up the banding a shallow palette otherwise shows on a gradient. + * Returns result_NOT_SUPPORTED if there is no deep->paletted conversion table + * for the screen's format. */ static result_t screen_copy_bitmap_p4(screen_t *scr, int x, int y, const bitmap_t *src, const box_t *draw_box, - int has_alpha) + int has_alpha, + int dither) { const unsigned char *srcrow; unsigned char *dstbase; const pixelmap_t *pm; + int nlevels; int clipped_width, clipped_height; int yy; clipped_width = draw_box->x1 - draw_box->x0; clipped_height = draw_box->y1 - draw_box->y0; + nlevels = pixelfmt_paletted_nentries(scr->format); srcrow = (const unsigned char *) src->base + (draw_box->y0 - y) * src->rowbytes; dstbase = scr->base; @@ -352,6 +388,9 @@ static result_t screen_copy_bitmap_p4(screen_t *scr, r = (c.primary >> pm->rshift) & 0xFF; g = (c.primary >> pm->gshift) & 0xFF; b = (c.primary >> pm->bshift) & 0xFF; + r = dither_channel(r, nlevels, dither, dstx, draw_box->y0 + yy); + g = dither_channel(g, nlevels, dither, dstx, draw_box->y0 + yy); + b = dither_channel(b, nlevels, dither, dstx, draw_box->y0 + yy); idx = ((r >> (8 - pm->rbits)) << (pm->gbits + pm->bbits)) | ((g >> (8 - pm->gbits)) << pm->bbits) | ( b >> (8 - pm->bbits)); @@ -369,25 +408,29 @@ static result_t screen_copy_bitmap_p4(screen_t *scr, /* Blit "src" onto the 1bpp screen, its top-left at (x, y), clipped to * "draw_box". Like screen_copy_bitmap_p4 this is an alpha-tested transfer * (skip fully transparent, else nearest of the two palette entries) rather - * than a blend -- a 1bpp screen has no channel bits to blend. The cached - * RGB->index table turns the inner loop into a mask-and-lookup; bit 7 of a - * byte is the leftmost pixel. Returns result_NOT_SUPPORTED if there is no - * deep->paletted conversion table for the screen's format. */ + * than a blend -- a 1bpp screen has no channel bits to blend. "dither" + * ordered-dithers the source RGB per pixel first. The cached RGB->index table + * turns the inner loop into a mask-and-lookup; bit 7 of a byte is the + * leftmost pixel. Returns result_NOT_SUPPORTED if there is no deep->paletted + * conversion table for the screen's format. */ static result_t screen_copy_bitmap_p1(screen_t *scr, int x, int y, const bitmap_t *src, const box_t *draw_box, - int has_alpha) + int has_alpha, + int dither) { const unsigned char *srcrow; unsigned char *dstbase; const pixelmap_t *pm; + int nlevels; int clipped_width, clipped_height; int yy; clipped_width = draw_box->x1 - draw_box->x0; clipped_height = draw_box->y1 - draw_box->y0; + nlevels = pixelfmt_paletted_nentries(scr->format); srcrow = (const unsigned char *) src->base + (draw_box->y0 - y) * src->rowbytes; dstbase = scr->base; @@ -424,6 +467,9 @@ static result_t screen_copy_bitmap_p1(screen_t *scr, r = (c.primary >> pm->rshift) & 0xFF; g = (c.primary >> pm->gshift) & 0xFF; b = (c.primary >> pm->bshift) & 0xFF; + r = dither_channel(r, nlevels, dither, dstx, draw_box->y0 + yy); + g = dither_channel(g, nlevels, dither, dstx, draw_box->y0 + yy); + b = dither_channel(b, nlevels, dither, dstx, draw_box->y0 + yy); idx = ((r >> (8 - pm->rbits)) << (pm->gbits + pm->bbits)) | ((g >> (8 - pm->gbits)) << pm->bbits) | ( b >> (8 - pm->bbits)); @@ -442,23 +488,27 @@ static result_t screen_copy_bitmap_p1(screen_t *scr, * "draw_box". Alpha-tested transfer to the nearest of the four palette * entries, exactly as screen_copy_bitmap_p1 but two bits per pixel: bits * 7..6 of a byte are the leftmost pixel and the cached RGB->index table is - * packed four entries to the byte. Returns result_NOT_SUPPORTED if there is - * no deep->paletted conversion table for the screen's format. */ + * packed four entries to the byte. "dither" ordered-dithers the source RGB + * per pixel first. Returns result_NOT_SUPPORTED if there is no deep->paletted + * conversion table for the screen's format. */ static result_t screen_copy_bitmap_p2(screen_t *scr, int x, int y, const bitmap_t *src, const box_t *draw_box, - int has_alpha) + int has_alpha, + int dither) { const unsigned char *srcrow; unsigned char *dstbase; const pixelmap_t *pm; + int nlevels; int clipped_width, clipped_height; int yy; clipped_width = draw_box->x1 - draw_box->x0; clipped_height = draw_box->y1 - draw_box->y0; + nlevels = pixelfmt_paletted_nentries(scr->format); srcrow = (const unsigned char *) src->base + (draw_box->y0 - y) * src->rowbytes; dstbase = scr->base; @@ -495,6 +545,9 @@ static result_t screen_copy_bitmap_p2(screen_t *scr, r = (c.primary >> pm->rshift) & 0xFF; g = (c.primary >> pm->gshift) & 0xFF; b = (c.primary >> pm->bshift) & 0xFF; + r = dither_channel(r, nlevels, dither, dstx, draw_box->y0 + yy); + g = dither_channel(g, nlevels, dither, dstx, draw_box->y0 + yy); + b = dither_channel(b, nlevels, dither, dstx, draw_box->y0 + yy); idx = ((r >> (8 - pm->rbits)) << (pm->gbits + pm->bbits)) | ((g >> (8 - pm->gbits)) << pm->bbits) | ( b >> (8 - pm->bbits)); @@ -569,7 +622,16 @@ static void screen_copy_bitmap_32(screen_t *scr, } } -result_t screen_copy_bitmap(screen_t *scr, int x, int y, const bitmap_t *src) +/* Shared body for screen_copy_bitmap and screen_copy_bitmap_dithered. With + * "dither" set the paletted (p1/p2/p4) paths ordered-dither the source RGB + * before the nearest-match lookup; the 32bpp path and the RLE path ignore it + * (a 32bpp screen has the channel depth not to band, and an RLE source is + * pre-quantised UI art). */ +static result_t screen_copy_bitmap_i(screen_t *scr, + int x, + int y, + const bitmap_t *src, + int dither) { box_t clip_box; box_t src_box; @@ -596,9 +658,9 @@ result_t screen_copy_bitmap(screen_t *scr, int x, int y, const bitmap_t *src) switch (pixelfmt_log2bpp(scr->format)) { - case 0: return screen_copy_bitmap_p1(scr, x, y, src, &draw_box, has_alpha); - case 1: return screen_copy_bitmap_p2(scr, x, y, src, &draw_box, has_alpha); - case 2: return screen_copy_bitmap_p4(scr, x, y, src, &draw_box, has_alpha); + case 0: return screen_copy_bitmap_p1(scr, x, y, src, &draw_box, has_alpha, dither); + case 1: return screen_copy_bitmap_p2(scr, x, y, src, &draw_box, has_alpha, dither); + case 2: return screen_copy_bitmap_p4(scr, x, y, src, &draw_box, has_alpha, dither); case 5: screen_copy_bitmap_32(scr, x, y, src, &draw_box, has_alpha); break; default: @@ -609,6 +671,19 @@ result_t screen_copy_bitmap(screen_t *scr, int x, int y, const bitmap_t *src) return result_OK; } +result_t screen_copy_bitmap(screen_t *scr, int x, int y, const bitmap_t *src) +{ + return screen_copy_bitmap_i(scr, x, y, src, 0); +} + +result_t screen_copy_bitmap_dithered(screen_t *scr, + int x, + int y, + const bitmap_t *src) +{ + return screen_copy_bitmap_i(scr, x, y, src, 1); +} + /* ----------------------------------------------------------------------- */ /* Build the box covering the whole screen, ignoring the current clip diff --git a/libraries/framebuf/screen/test/screen-test.c b/libraries/framebuf/screen/test/screen-test.c index 8bacb423..de53904d 100644 --- a/libraries/framebuf/screen/test/screen-test.c +++ b/libraries/framebuf/screen/test/screen-test.c @@ -958,6 +958,114 @@ static result_t test_copy_bitmap_p2(void) return result_TEST_PASSED; } +/* Unpack the p2 pixel at column "px" of a row's byte buffer (bits 7..6 are + * column 0, MSB-first, four to the byte). */ +static int p2_pixel_at(const unsigned char *rowbuf, int px) +{ + return (rowbuf[px >> 2] >> (6 - ((px & 3) << 1))) & 3; +} + +/* screen_copy_bitmap_dithered on a p2 screen: a flat mid-grey source, which + * screen_copy_bitmap would quantise to one uniform index across the row, must + * come out as a stipple of at least two indices; the flat black and white + * ends must still be uniform (the Bayer nudge can't push them off their + * clamp). */ +static result_t test_copy_bitmap_dithered(void) +{ +#define PD_ROWBYTES (WIDTH / 4) + static unsigned char pdpixels[PD_ROWBYTES * HEIGHT]; + static pixelfmt_rgba8888_t srcbuf[16 * 8]; + + screen_t scr; + bitmap_t src; + colour_t pal[4]; + int first; + int varied; + int px; + int x, y; + + pal[0] = colour_rgb(0x00, 0x00, 0x00); + pal[1] = colour_rgb(0x55, 0x55, 0x55); + pal[2] = colour_rgb(0xAA, 0xAA, 0xAA); + pal[3] = colour_rgb(0xFF, 0xFF, 0xFF); + + /* 16x8 flat mid-grey (0x80): sits between pal[1] and pal[2]. */ + for (y = 0; y < 8; y++) + for (x = 0; x < 16; x++) + srcbuf[y * 16 + x] = colour_rgb(0x80, 0x80, 0x80).primary; + + bitmap_init(&src, SIZE2D(16, 8), pixelfmt_rgba8888, + 16 * (int) sizeof(srcbuf[0]), NULL, srcbuf); + + memset(pdpixels, 0, sizeof(pdpixels)); + screen_init(&scr, SIZE2D(WIDTH, HEIGHT), pixelfmt_p2, PD_ROWBYTES, pal, + pdpixels); + + if (screen_copy_bitmap_dithered(&scr, 0, 0, &src) != result_OK) + { + printf("screen: copy_bitmap_dithered to p2 screen failed\n"); + return result_TEST_FAILED; + } + + /* plain blit of the same source: every one of the first 16 pixels + * quantises to the same index. */ + { + unsigned char plain[PD_ROWBYTES * HEIGHT]; + screen_t pscr; + + memset(plain, 0, sizeof(plain)); + screen_init(&pscr, SIZE2D(WIDTH, HEIGHT), pixelfmt_p2, PD_ROWBYTES, pal, + plain); + screen_copy_bitmap(&pscr, 0, 0, &src); + for (px = 1; px < 16; px++) + if (p2_pixel_at(plain, px) != p2_pixel_at(plain, 0)) + { + printf("screen: plain p2 blit of a flat source was not uniform\n"); + return result_TEST_FAILED; + } + } + + /* dithered: at least one of the first 16 pixels differs from pixel 0 (the + * 8x8 Bayer cell varies across the row). */ + first = p2_pixel_at(pdpixels, 0); + varied = 0; + for (px = 1; px < 16; px++) + if (p2_pixel_at(pdpixels, px) != first) + varied = 1; + if (!varied) + { + printf("screen: dithered p2 blit of a flat mid-grey did not stipple\n"); + return result_TEST_FAILED; + } + + /* flat black stays index 0 across the row, flat white stays index 3. */ + for (y = 0; y < 8; y++) + for (x = 0; x < 16; x++) + srcbuf[y * 16 + x] = colour_rgb(0x00, 0x00, 0x00).primary; + memset(pdpixels, 0xAA, sizeof(pdpixels)); + screen_copy_bitmap_dithered(&scr, 0, 0, &src); + if (pdpixels[0] != 0x00 || pdpixels[1] != 0x00 || + pdpixels[2] != 0x00 || pdpixels[3] != 0x00) + { + printf("screen: dithered p2 blit pushed flat black off index 0\n"); + return result_TEST_FAILED; + } + + for (y = 0; y < 8; y++) + for (x = 0; x < 16; x++) + srcbuf[y * 16 + x] = colour_rgb(0xFF, 0xFF, 0xFF).primary; + memset(pdpixels, 0x00, sizeof(pdpixels)); + screen_copy_bitmap_dithered(&scr, 0, 0, &src); + if (pdpixels[0] != 0xFF || pdpixels[1] != 0xFF || + pdpixels[2] != 0xFF || pdpixels[3] != 0xFF) + { + printf("screen: dithered p2 blit pushed flat white off index 3\n"); + return result_TEST_FAILED; + } + + return result_TEST_PASSED; +} + /* ----------------------------------------------------------------------- */ result_t screen_test(const char *resources) @@ -977,7 +1085,8 @@ result_t screen_test(const char *resources) test_draw_circle, test_fill_circle, test_copy_bitmap_p1, - test_copy_bitmap_p2 + test_copy_bitmap_p2, + test_copy_bitmap_dithered }; result_t rc; diff --git a/libraries/wuss/test/tasks/image.c b/libraries/wuss/test/tasks/image.c index f4f56c37..c86e90ff 100644 --- a/libraries/wuss/test/tasks/image.c +++ b/libraries/wuss/test/tasks/image.c @@ -46,6 +46,7 @@ result_t image_create(wuss_t *wuss, task->menu = NULL; task->proginfo = NULL; task->menu_handle = NULL; + task->dithering = 1; images_dir = path_join_filename(resources, 2, "resources", "images"); rc = namelist_scan(images_dir, IMAGE_EXT, task->names[0], @@ -114,7 +115,7 @@ result_t image_create(wuss_t *wuss, static const wuss_proginfo_desc_t desc = { "Image", - "Cycle the PNGs under resources/images", + "View the PNGs under resources/images", "(c) DPTLib contributors", "1.0 (" __DATE__ ")" }; @@ -170,7 +171,10 @@ static result_t image_redraw(const wuss_event_t *event, void *task_data) band[3].y1 = by + ic->bitmap.size.h; /* right */ screen_fill_rects(scr, band, 4, colour_rgb(0xFF, 0x77, 0xA8)); /* PICO-8 pink */ - screen_copy_bitmap(scr, bx, by, &ic->bitmap); + if (ic->dithering) + screen_copy_bitmap_dithered(scr, bx, by, &ic->bitmap); + else + screen_copy_bitmap(scr, bx, by, &ic->bitmap); return result_OK; } @@ -236,9 +240,10 @@ static result_t image_open_menu(image_task_t *ic) result_t rc; wuss_menu_t *m; int i; + int ticked[7]; rc = wuss_menu_create_from_desc(&m, - "Image, Info, New..., Open, !Show grid, !Wireframe, >Export, |Quit", + "Image, Info, New..., Open, !Dithering, !Wireframe, >Export, |Quit", &image_menu_export); if (rc != result_OK) return rc; @@ -258,9 +263,16 @@ static result_t image_open_menu(image_task_t *ic) wuss_menu_destroy(ic->menu); ic->menu = m; - - return wuss_menu_open(ic->delegate, ic->menu, wuss_get_pointer(ic->wuss), - &ic->menu_handle); + + // TODO: Use a bitfield? + memset(ticked, 0, sizeof(ticked)); + ticked[3] = ic->dithering; + + return wuss_menu_open_ticked(ic->delegate, + ic->menu, + ticked, + wuss_get_pointer(ic->wuss), + &ic->menu_handle); } result_t image_handle(wuss_window_t *window, @@ -296,8 +308,14 @@ result_t image_handle(wuss_window_t *window, index = event->data.menu_select.index; printf("image menu: picked \"%s\"\n", menu->items[index].text ? menu->items[index].text : "(sep)"); + if (index == 3) { + ic->dithering = !ic->dithering; + wuss_window_invalidate_visible(ic->window); + } if (!wuss_menu_should_keep_open(event)) ic->menu_handle = NULL; /* SELECT pick already freed the chain */ + else if (index == 3) + wuss_menu_set_item_ticked(ic->menu_handle, ic->menu, 3, ic->dithering); return result_OK; case wuss_EVENT_MENU_CLOSED: diff --git a/libraries/wuss/test/tasks/image.h b/libraries/wuss/test/tasks/image.h index 6c4f3ab9..cb2c7547 100644 --- a/libraries/wuss/test/tasks/image.h +++ b/libraries/wuss/test/tasks/image.h @@ -37,6 +37,7 @@ typedef struct image_task * freed at QUIT */ wuss_menu_handle_t menu_handle; /* the open chain, if any -- closed before the * proginfo it borrows is destroyed at QUIT */ + int dithering; /* enable Bayer ordered dithering */ } image_task_t; From 3ab387d59df15183af9683c2cb7de0cf0a479b25 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Thu, 10 Sep 2026 21:02:41 +0100 Subject: [PATCH 012/128] fix(wuss): close submenus when the parent menu's title bar is hovered wuss_mouse_move early-returns without delivering any event once the pointer is over window furniture, so a menu chain never heard about the pointer leaving a row for the parent menu's title bar and the submenu stayed open. Add a second chain walk in the wuss__menu_handle IDLE branch: for any menu that has a child open and is not mid-flash, if the pointer is now over that menu's title bar (inside window->visible but above the content box), close the child chain and clear the parent row's hovered state. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/menu/menu.c | 45 +++++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/libraries/wuss/menu/menu.c b/libraries/wuss/menu/menu.c index c56857e1..8af500fd 100644 --- a/libraries/wuss/menu/menu.c +++ b/libraries/wuss/menu/menu.c @@ -227,6 +227,24 @@ static result_t wuss__menu_open_window(struct wuss__menu *self, int index) return result_OK; } +/* True if the wuss pointer sits over `window`'s chrome above its content area + * -- the titlebar strip. Core routes furniture hovers straight to the window + * manager and delivers no event to the menu delegate (see wuss_mouse_move), + * so a titlebar hover on a parent level cannot be caught in the ICON handler; + * the IDLE tick polls this instead to close a submenu the pointer has slid up + * onto the parent's titlebar to reach (e.g. to drag the parent). */ +static int wuss__pointer_over_titlebar(const wuss_window_t *window) +{ + point_t p; + box_t content; + + p = wuss_get_pointer(window->wuss); + wuss__content_box(window, &content); + + return box_contains_point(&window->visible, p.x, p.y) + && p.y < content.y0; +} + /* ----------------------------------------------------------------------- */ /* The menu window's task delegate. Shared across every borderless menu @@ -243,18 +261,43 @@ static result_t wuss__menu_handle(wuss_window_t *window, int i; /* IDLE arrives with window == NULL; task_data is the wuss_t (see - * wuss__menu_task). Drive any running SELECT-pick flash off it. */ + * wuss__menu_task). Drive any running SELECT-pick flash off it, and close a + * submenu the pointer has slid up onto its parent's titlebar. */ if (event->kind == wuss_EVENT_IDLE) { struct wuss__menu *node; wuss = task_data; + for (node = wuss->menu_chain; node != NULL; node = node->child) if (!node->borrowed && node->flash.frames > 0) { wuss__menu_flash_step(node); break; /* node may be freed; the chain is gone if the flash ended */ } + + for (node = wuss->menu_chain; node != NULL; node = node->child) + if (node->child != NULL + && node->flash.frames == 0 + && wuss__pointer_over_titlebar(node->window)) + { + wuss_icon_t *was_parent; + + was_parent = (node->open_index >= 0) ? node->icons[node->open_index] + : NULL; + + wuss__menu_close_from(node->child); + node->child = NULL; + node->open_index = -1; + + if (was_parent != NULL) + { + wuss__icon_set_state(was_parent, wuss_ICON_STATE_HOVERED, 0); + wuss__icon_invalidate(node->window, was_parent); + } + break; /* closing the child re-shaped the chain past this node */ + } + return result_OK; } From 1a59508501dbbdde1a421e248354f856b830054c Mon Sep 17 00:00:00 2001 From: David Thomas Date: Thu, 10 Sep 2026 21:02:49 +0100 Subject: [PATCH 013/128] feat(wuss): add wuss_EVENT_POINTER_ENTER / wuss_EVENT_POINTER_EXIT Window-scoped events fired when the pointer crosses onto or off a window's whole on-screen footprint -- content or furniture alike. Exactly one ENTER is outstanding per window, always balanced by a later EXIT; moving between content and furniture within one window does not re-fire them. Carry no data. Reusable infrastructure for tooltips, per-window cursors, and hover affordances. struct wuss gains a pointer_window tracker. wuss__pointer_set_window moves it and emits EXIT-then-ENTER on the crossing edge; it is called from wuss_mouse_move and wuss_mouse_click right after the window hit-test, ahead of the furniture / no-handler early returns. wuss__pointer_forget_window drops a window from the tracker without an EXIT for the close and hide paths, where wuss_EVENT_CLOSE (or the window simply going off the mouse) already covers it. Co-Authored-By: Claude Sonnet 5 --- include/wuss/task.h | 17 ++++++++-- libraries/wuss/core/create.c | 1 + libraries/wuss/core/impl.h | 44 +++++++++++++++++++++++++ libraries/wuss/core/mouse-click.c | 4 +++ libraries/wuss/core/mouse-move.c | 4 +++ libraries/wuss/core/task/deliver.c | 2 ++ libraries/wuss/core/window/close.c | 3 ++ libraries/wuss/core/window/set-hidden.c | 3 ++ 8 files changed, 76 insertions(+), 2 deletions(-) diff --git a/include/wuss/task.h b/include/wuss/task.h index 6c29dde2..c8c693c3 100644 --- a/include/wuss/task.h +++ b/include/wuss/task.h @@ -85,7 +85,19 @@ typedef enum wuss_event_kind * stored the wuss_menu_open handle must drop it here: the chain is * already freed. Not fired for a SELECT pick (a wuss_EVENT_MENU_SELECT * with a chain-closing button) or for a wuss_menu_close the task made. */ - wuss_EVENT_MENU_CLOSED + wuss_EVENT_MENU_CLOSED, + /** The pointer has moved onto this window's on-screen footprint (content + * or furniture) from elsewhere. Exactly one is outstanding per window at + * a time, always balanced by a later wuss_EVENT_POINTER_EXIT. Carries no + * data. Fires only on the window-crossing edge: moving between content + * and furniture within the same window does not re-fire it. */ + wuss_EVENT_POINTER_ENTER, + /** The pointer has left this window's footprint -- onto another window, + * onto no window, or because the window is closing under it. Balances an + * earlier wuss_EVENT_POINTER_ENTER. Carries no data. A task must not + * assume the window is still usable if this arrives during teardown; it + * is safe to read but a wuss_EVENT_CLOSE may follow immediately. */ + wuss_EVENT_POINTER_EXIT } wuss_event_kind_t; @@ -96,7 +108,8 @@ wuss_event_kind_t; * * Members: wuss_EVENT_REDRAW, wuss_EVENT_MOUSE, wuss_EVENT_ICON, * wuss_EVENT_SCROLL, wuss_EVENT_OPEN, wuss_EVENT_PRE_SHOW, wuss_EVENT_SHOW, - * wuss_EVENT_PRE_CLOSE, wuss_EVENT_CLOSE. + * wuss_EVENT_PRE_CLOSE, wuss_EVENT_CLOSE, wuss_EVENT_POINTER_ENTER, + * wuss_EVENT_POINTER_EXIT. */ typedef wuss_event_kind_t wuss_window_event_kind_t; diff --git a/libraries/wuss/core/create.c b/libraries/wuss/core/create.c index d1a0487d..9531ccb5 100644 --- a/libraries/wuss/core/create.c +++ b/libraries/wuss/core/create.c @@ -295,6 +295,7 @@ result_t wuss_create(screen_t *scr, wuss__rebuild_palettecache(w); w->scr = scr; + w->pointer_window = NULL; #ifdef WUSS_FURNITURE w->furniture.dragging = NULL; w->furniture.drag.x = 0; diff --git a/libraries/wuss/core/impl.h b/libraries/wuss/core/impl.h index 87438b42..63c953f8 100644 --- a/libraries/wuss/core/impl.h +++ b/libraries/wuss/core/impl.h @@ -151,6 +151,11 @@ struct wuss * layout packer has no room left */ point_t pointer; /* last pointer position, screen * space, from any mouse click/move */ + wuss_window_t *pointer_window; /* window whose on-screen + * footprint the pointer was last + * inside (content or furniture), + * NULL if none; drives + * wuss_EVENT_POINTER_ENTER/EXIT */ #ifdef WUSS_ICONS wuss_icon_t *pressed_icon; /* button icon held down, NULL when * idle; released on any MOUSE_UP @@ -512,6 +517,45 @@ static inline void wuss__notify_open(wuss_window_t *window) (void) wuss__deliver(window->task, window, &event); } +/* Move the "pointer is inside this window's footprint" tracker to "now" + * (which may be NULL). If it changed, deliver wuss_EVENT_POINTER_EXIT to the + * window it left and wuss_EVENT_POINTER_ENTER to the one it reached, in that + * order. Called from every pointer-position update. Fires only on the + * window-crossing edge; content<->furniture moves within one window keep the + * same tracker and emit nothing. */ +static inline void wuss__pointer_set_window(wuss_t *wuss, wuss_window_t *now) +{ + wuss_window_t *was; + wuss_event_t event; + + was = wuss->pointer_window; + if (now == was) + return; + + wuss->pointer_window = now; + + if (was != NULL) + { + event.kind = wuss_EVENT_POINTER_EXIT; + (void) wuss__deliver(was->task, was, &event); + } + if (now != NULL) + { + event.kind = wuss_EVENT_POINTER_ENTER; + (void) wuss__deliver(now->task, now, &event); + } +} + +/* Drop "window" from the pointer tracker without delivering EXIT -- for the + * window teardown path, where wuss_EVENT_CLOSE already tells the task and the + * window is about to be freed. No-op unless the pointer was inside it. */ +static inline void wuss__pointer_forget_window(wuss_t *wuss, + wuss_window_t *window) +{ + if (wuss->pointer_window == window) + wuss->pointer_window = NULL; +} + static inline int wuss__size_ok(int width, int height) { return width > 0 && height > 0; diff --git a/libraries/wuss/core/mouse-click.c b/libraries/wuss/core/mouse-click.c index 147754c2..5a5f7ecc 100644 --- a/libraries/wuss/core/mouse-click.c +++ b/libraries/wuss/core/mouse-click.c @@ -68,6 +68,10 @@ result_t wuss_mouse_click(wuss_t *wuss, if (hit != NULL) *hit = win; + /* A click can land the pointer on a window without a preceding move + * (window opened under it, or a synthetic tap); keep enter/exit balanced. */ + wuss__pointer_set_window(wuss, win); + #ifdef WUSS_MENUS /* The MOUSE_UP that follows the MENU press which opened the chain is spent: * without this it would land on the new menu's row 0 and pick it. */ diff --git a/libraries/wuss/core/mouse-move.c b/libraries/wuss/core/mouse-move.c index 702fe21e..5d4bacc4 100644 --- a/libraries/wuss/core/mouse-move.c +++ b/libraries/wuss/core/mouse-move.c @@ -52,6 +52,10 @@ result_t wuss_mouse_move(wuss_t *wuss, point_t p, wuss_window_t **hit) if (hit != NULL) *hit = win; + /* Whole-footprint enter/exit tracking, before any furniture/no-handler + * early return -- a furniture hover still counts as "inside the window". */ + wuss__pointer_set_window(wuss, win); + if (win == NULL) { #ifdef WUSS_ICONS diff --git a/libraries/wuss/core/task/deliver.c b/libraries/wuss/core/task/deliver.c index 02d80622..b610a1c3 100644 --- a/libraries/wuss/core/task/deliver.c +++ b/libraries/wuss/core/task/deliver.c @@ -23,6 +23,8 @@ static int wuss__kind_ok_for(wuss_event_kind_t kind, int have_window) case wuss_EVENT_SHOW: case wuss_EVENT_PRE_CLOSE: case wuss_EVENT_CLOSE: + case wuss_EVENT_POINTER_ENTER: + case wuss_EVENT_POINTER_EXIT: return have_window; case wuss_EVENT_IDLE: diff --git a/libraries/wuss/core/window/close.c b/libraries/wuss/core/window/close.c index 3263fcc6..2f2e6dcf 100644 --- a/libraries/wuss/core/window/close.c +++ b/libraries/wuss/core/window/close.c @@ -35,6 +35,9 @@ void wuss_window_close(wuss_window_t *doomed) wuss->hover_window = NULL; } #endif + /* No wuss_EVENT_POINTER_EXIT: the task hears about this window going away + * through PRE_CLOSE/CLOSE, and the struct is freed below. */ + wuss__pointer_forget_window(wuss, doomed); wuss__release_packed(doomed); diff --git a/libraries/wuss/core/window/set-hidden.c b/libraries/wuss/core/window/set-hidden.c index 0bc14fff..e78232fb 100644 --- a/libraries/wuss/core/window/set-hidden.c +++ b/libraries/wuss/core/window/set-hidden.c @@ -42,6 +42,9 @@ result_t wuss_window_set_hidden(wuss_window_t *window, int hidden) wuss->hover_window = NULL; } #endif + /* A hidden window is off the mouse; drop it from enter/exit tracking. No + * EXIT event -- it stops receiving pointer events regardless. */ + wuss__pointer_forget_window(wuss, window); /* still on screen: repaint its footprint now, then mark it gone */ wuss_invalidate(wuss, &window->visible); From 8c6431d0e2154137d64bb1ea4c4f526f378e9597 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Thu, 10 Sep 2026 22:47:54 +0100 Subject: [PATCH 014/128] fix(wuss): size radio/option glyph box from the state sprite The RADIO and OPTION icon glyph square was always font-height and the blit was clipped to it, so an icon-set sprite (radon/radoff/opton/optoff) larger than the font was centred in that square and truncated. The label offset ignored the real sprite width too. Resolve the state bitmap first and, when present, take the glyph box and label offset from its own size, vertically centred in the icon box. Drop the clip-to-glyph-square intersection; screen_copy_bitmap already honours the screen clip. Vector fallback keeps the font-height square. Also refresh libraries/wuss/TODO.txt. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/TODO.txt | 28 +++------------ libraries/wuss/icon/draw.c | 70 ++++++++++++++++---------------------- 2 files changed, 34 insertions(+), 64 deletions(-) diff --git a/libraries/wuss/TODO.txt b/libraries/wuss/TODO.txt index b4415871..274bff1e 100644 --- a/libraries/wuss/TODO.txt +++ b/libraries/wuss/TODO.txt @@ -6,10 +6,9 @@ Items marked (blocks: X) are prerequisites for X. LAYER 0 - BUGS (fix before building on top) -- Various window size toggle ops need checking out +- Various window size toggle ops need checking out - or at least understood - Auto window placement packer still not working as expected blocks: reliable multi-window tasks -- Image task not shrinking window when new image loads LAYER 1 - CORE PLUMBING (no wuss deps; unblocks everything else) @@ -19,31 +18,10 @@ LAYER 1 - CORE PLUMBING (no wuss deps; unblocks everything else) blocks: Draw bitmap through mask, Masked sprites/bitmaps, furniture tinting - UTF-8 everywhere blocks: text abstraction, window/menu templates -- Is having font handling in wuss-core a bad thing? Decide arch now. - blocks: Wuss text abstraction - Icon type taxonomy - Window layout / constraint system / stacking primitives? -Toolbox (Window) Gadgets: -Label -Action -Display -Writable -Number -String Set -Labelled Box -Option -Radio -- should be a set -GRight -- need? -Slider (H/V) -Draggable -Button - -Menu entries are an extra thing - - - LAYER 2 - TEXT & FONTS (needs UTF-8, font-arch decision) - Wuss text abstraction (wraps bmfont, supplies the Wuss font(s)) @@ -57,8 +35,8 @@ LAYER 3 - SPRITES (needs masks) - Sprites (with names, with masks, with multiple sprites) - Masked sprites/bitmaps. Covered by RLE work? - Sprite scaling -- Draw bitmap through (bayer) mask - Import Kare style icons +- Sprites noticably brighter / colour matching needs work LAYER 4 - WINDOW FURNITURE & INTERACTION (needs bug layer, text layer) @@ -73,6 +51,7 @@ LAYER 4 - WINDOW FURNITURE & INTERACTION (needs bug layer, text layer) - Snap to edges (outline-click bug fixed; unblocked) - Dragging against window edge shuffles window in opposite direction (outline-click bug fixed; unblocked) +- 2D Adjust window scroll drag - Input focus, text entry, furniture tinting - Icon layout - Furniture colour selection needs improvements (e.g. losing menu readability when palette changed) @@ -98,6 +77,7 @@ LAYER 6 - MULTI-TASK (needs stable windows, templates) LAYER 7 - APPS & POLISH (needs most of the above) +- Post-Its app with explanatory text for newbies - Have a Paint app as a target - Desktop cohort: Filer, Task Manager, Palette Util - Colour Picker component diff --git a/libraries/wuss/icon/draw.c b/libraries/wuss/icon/draw.c index 2910457e..28963f20 100644 --- a/libraries/wuss/icon/draw.c +++ b/libraries/wuss/icon/draw.c @@ -363,17 +363,13 @@ static void wuss__icon_draw_button(const icon_draw_ctx_t *c) /* ----------------------------------------------------------------------- */ -/* Blit the icon-set bitmap for a radio/option's current state, centred in the - * glyph square "g", clipped to it. Names are radon/radoff for RADIO and - * opton/optoff for OPTION. Returns 1 when a bitmap was drawn, 0 when the set is - * absent or lacks that entry -- the caller then draws the vector glyph. */ -static int wuss__icon_blit_radio_option(const icon_draw_ctx_t *c, - const box_t *g) +/* The icon-set bitmap for a radio/option's current state, or NULL when the set + * is absent or lacks that entry. Names are radon/radoff for RADIO and + * opton/optoff for OPTION. */ +static const bitmap_t *wuss__icon_radio_option_bitmap(const icon_draw_ctx_t *c) { - const char *name; - const bitmap_t *bm; - screen_t clipped; - int idx, bx, by; + const char *name; + int idx; if (c->icon->spec.type == wuss_ICON_TYPE_RADIO) name = wuss__icon_selected(c->icon) ? "radon" : "radoff"; @@ -382,43 +378,37 @@ static int wuss__icon_blit_radio_option(const icon_draw_ctx_t *c, idx = wuss_icons_lookup(c->wuss, name); if (idx < 0) - return 0; - - bm = wuss_icons_bitmap(c->wuss, idx); - if (bm == NULL) - return 0; - - bx = g->x0 + ((g->x1 - g->x0) - bm->size.w) / 2; - by = g->y0 + ((g->y1 - g->y0) - bm->size.h) / 2; + return NULL; - clipped = *c->scr; - if (box_intersection(&c->scr->clip, g, &clipped.clip)) - return 1; /* fully clipped away, but still "handled" -- no vector fallback */ - - screen_copy_bitmap(&clipped, bx, by, bm); - return 1; + return wuss_icons_bitmap(c->wuss, idx); } -/* wuss_ICON_TYPE_RADIO and wuss_ICON_TYPE_OPTION: a font-height square glyph at - * the left, vertically centred, with the label to its right. If the icon set - * carries radon/radoff (RADIO) or opton/optoff (OPTION) those bitmaps are - * blitted for the state; otherwise RADIO draws a square ring with a solid - * centre when selected and OPTION draws a box with a tick when selected. */ +/* wuss_ICON_TYPE_RADIO and wuss_ICON_TYPE_OPTION: a glyph at the left, + * vertically centred, with the label to its right. If the icon set carries + * radon/radoff (RADIO) or opton/optoff (OPTION) those bitmaps are blitted for + * the state, and the glyph box and label offset follow the bitmap's own size; + * otherwise a font-height square is used -- RADIO draws a square ring with a + * solid centre when selected and OPTION draws a box with a tick when + * selected. */ static void wuss__icon_draw_radio_option(const icon_draw_ctx_t *c) { const wuss_icon_spec_t *icon = &c->icon->spec; const box_t *b = &c->b; + const bitmap_t *bm; colour_t glyph, bg; box_t g; - int gsz, gy, tx; + int gw, gh, gy, tx; + + bm = wuss__icon_radio_option_bitmap(c); - gsz = CLAMP(c->font_height, 8, b->y1 - b->y0); - gy = b->y0 + (b->y1 - b->y0 - gsz) / 2; + gw = bm ? bm->size.w : CLAMP(c->font_height, 8, b->x1 - b->x0); + gh = bm ? bm->size.h : CLAMP(c->font_height, 8, b->y1 - b->y0); + gy = b->y0 + (b->y1 - b->y0 - gh) / 2; g.x0 = b->x0; g.y0 = gy; - g.x1 = b->x0 + gsz; - g.y1 = gy + gsz; + g.x1 = b->x0 + gw; + g.y1 = gy + gh; glyph = (icon->flags & wuss_ICON_FLAGS_DISABLED) ? c->wuss->palette[c->wuss->bevel_dark] @@ -430,9 +420,9 @@ static void wuss__icon_draw_radio_option(const icon_draw_ctx_t *c) screen_fill_rect(c->scr, b->x0, b->y0, SIZE2D(b->x1 - b->x0, b->y1 - b->y0), bg); - if (wuss__icon_blit_radio_option(c, &g)) + if (bm != NULL) { - /* bitmap drawn -- fall through to the label */ + screen_copy_bitmap(c->scr, g.x0, g.y0, bm); } else if (icon->type == wuss_ICON_TYPE_RADIO) { @@ -443,7 +433,7 @@ static void wuss__icon_draw_radio_option(const icon_draw_ctx_t *c) screen_draw_line(c->scr, g.x1 - 1, g.y0 + 2, g.x1 - 1, g.y1 - 3, glyph); if (wuss__icon_selected(c->icon)) screen_fill_rect(c->scr, g.x0 + 3, g.y0 + 3, - SIZE2D(gsz - 6, gsz - 6), glyph); + SIZE2D(gw - 6, gh - 6), glyph); } else { @@ -452,9 +442,9 @@ static void wuss__icon_draw_radio_option(const icon_draw_ctx_t *c) SIZE2D(g.x1 - g.x0, g.y1 - g.y0), glyph); if (wuss__icon_selected(c->icon)) { - screen_draw_line(c->scr, g.x0 + 2, g.y0 + gsz / 2, - g.x0 + gsz / 2 - 1, g.y1 - 3, glyph); - screen_draw_line(c->scr, g.x0 + gsz / 2 - 1, g.y1 - 3, + screen_draw_line(c->scr, g.x0 + 2, g.y0 + gh / 2, + g.x0 + gw / 2 - 1, g.y1 - 3, glyph); + screen_draw_line(c->scr, g.x0 + gw / 2 - 1, g.y1 - 3, g.x1 - 3, g.y0 + 2, glyph); } } From 89b32b29bd7586896a75bc0a03dc52bf73a1e3c6 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Thu, 10 Sep 2026 22:54:57 +0100 Subject: [PATCH 015/128] perf(framebuf): hoist dither divide out of the paletted blit inner loop dither_channel recomputed 255 / (nlevels - 1) and a divide-by-64 for every channel of every pixel -- nine integer divides per dithered pixel, all by values constant for the whole blit. Precompute a 64-entry signed bias table, one per Bayer cell, once per blit (dither_bias_build: a single divide plus a 64-iteration fill). The inner loop drops to a table lookup and an add per channel, gated behind one do_dither branch. dither_channel now just applies the pre-built bias for a pixel position. No behavioural change; all 26 tests pass. Co-Authored-By: Claude Sonnet 5 --- libraries/framebuf/screen/screen-draw.c | 94 +++++++++++++++++-------- 1 file changed, 64 insertions(+), 30 deletions(-) diff --git a/libraries/framebuf/screen/screen-draw.c b/libraries/framebuf/screen/screen-draw.c index 4f9fbd94..2fcbef0a 100644 --- a/libraries/framebuf/screen/screen-draw.c +++ b/libraries/framebuf/screen/screen-draw.c @@ -293,32 +293,48 @@ void screen_fill_square(screen_t *scr, /* ----------------------------------------------------------------------- */ -/* Ordered-dither one channel value "v" (0..255) for screen pixel (sx, sy) - * before it is matched to the nearest palette entry. "nlevels" is the palette - * entry count; the mean gap between adjacent levels of a linear ramp is then - * 255 / (nlevels - 1), and the Bayer cell (-32..31 about zero) nudges "v" by - * up to half that gap either way, so a value sitting between two entries lands - * on one or the other in a fixed 8x8 pattern instead of always snapping to - * the nearer. Result clamped to 0..255. +/* Per-blit ordered-dither bias table: one signed offset for each of the 64 + * cells of the 8x8 Bayer matrix, indexed by pattern_bayer_threshold(sx, sy). + * + * The mean gap between adjacent levels of a linear ramp of "nlevels" entries + * is 255 / (nlevels - 1); the Bayer cell (-32..31 about zero) nudges a channel + * value by up to half that gap either way, so a value sitting between two + * entries lands on one or the other in a fixed 8x8 pattern instead of always + * snapping to the nearer. Building the table hoists the one divide out of the + * inner loop -- the per-pixel cost drops to a lookup and an add. + * + * dither_bias_build fills "tab" and returns 1 when dithering is on and useful + * (>= 2 levels), 0 when the caller should skip the bias entirely. * * ponytail: this assumes a roughly even, roughly greyscale palette -- the one * case (shallow paletted screen showing a gradient) the dithered blit is for. * A wildly non-uniform palette dithers weakly, not wrongly; swap in a * per-entry nearest-two search keyed on the real palette if that matters. */ -static unsigned int dither_channel(unsigned int v, - int nlevels, - int dither, - int sx, - int sy) +static int dither_bias_build(int tab[64], int nlevels, int dither) { int gap; - int adj; + int i; if (!dither || nlevels < 2) - return v; + return 0; gap = 255 / (nlevels - 1); - adj = ((pattern_bayer_threshold(sx, sy) - 32) * gap) / 64; + for (i = 0; i < 64; i++) + tab[i] = ((i - 32) * gap) / 64; + + return 1; +} + +/* Apply the pre-built bias for screen pixel (sx, sy) to channel value "v" + * (0..255), result clamped to 0..255. */ +static unsigned int dither_channel(unsigned int v, + const int tab[64], + int sx, + int sy) +{ + int adj; + + adj = tab[pattern_bayer_threshold(sx, sy)]; return (unsigned int) CLAMP((int) v + adj, 0, 255); } @@ -342,13 +358,16 @@ static result_t screen_copy_bitmap_p4(screen_t *scr, const unsigned char *srcrow; unsigned char *dstbase; const pixelmap_t *pm; - int nlevels; + int bias[64]; + int do_dither; int clipped_width, clipped_height; int yy; clipped_width = draw_box->x1 - draw_box->x0; clipped_height = draw_box->y1 - draw_box->y0; - nlevels = pixelfmt_paletted_nentries(scr->format); + do_dither = dither_bias_build(bias, + pixelfmt_paletted_nentries(scr->format), + dither); srcrow = (const unsigned char *) src->base + (draw_box->y0 - y) * src->rowbytes; dstbase = scr->base; @@ -388,9 +407,12 @@ static result_t screen_copy_bitmap_p4(screen_t *scr, r = (c.primary >> pm->rshift) & 0xFF; g = (c.primary >> pm->gshift) & 0xFF; b = (c.primary >> pm->bshift) & 0xFF; - r = dither_channel(r, nlevels, dither, dstx, draw_box->y0 + yy); - g = dither_channel(g, nlevels, dither, dstx, draw_box->y0 + yy); - b = dither_channel(b, nlevels, dither, dstx, draw_box->y0 + yy); + if (do_dither) + { + r = dither_channel(r, bias, dstx, draw_box->y0 + yy); + g = dither_channel(g, bias, dstx, draw_box->y0 + yy); + b = dither_channel(b, bias, dstx, draw_box->y0 + yy); + } idx = ((r >> (8 - pm->rbits)) << (pm->gbits + pm->bbits)) | ((g >> (8 - pm->gbits)) << pm->bbits) | ( b >> (8 - pm->bbits)); @@ -424,13 +446,16 @@ static result_t screen_copy_bitmap_p1(screen_t *scr, const unsigned char *srcrow; unsigned char *dstbase; const pixelmap_t *pm; - int nlevels; + int bias[64]; + int do_dither; int clipped_width, clipped_height; int yy; clipped_width = draw_box->x1 - draw_box->x0; clipped_height = draw_box->y1 - draw_box->y0; - nlevels = pixelfmt_paletted_nentries(scr->format); + do_dither = dither_bias_build(bias, + pixelfmt_paletted_nentries(scr->format), + dither); srcrow = (const unsigned char *) src->base + (draw_box->y0 - y) * src->rowbytes; dstbase = scr->base; @@ -467,9 +492,12 @@ static result_t screen_copy_bitmap_p1(screen_t *scr, r = (c.primary >> pm->rshift) & 0xFF; g = (c.primary >> pm->gshift) & 0xFF; b = (c.primary >> pm->bshift) & 0xFF; - r = dither_channel(r, nlevels, dither, dstx, draw_box->y0 + yy); - g = dither_channel(g, nlevels, dither, dstx, draw_box->y0 + yy); - b = dither_channel(b, nlevels, dither, dstx, draw_box->y0 + yy); + if (do_dither) + { + r = dither_channel(r, bias, dstx, draw_box->y0 + yy); + g = dither_channel(g, bias, dstx, draw_box->y0 + yy); + b = dither_channel(b, bias, dstx, draw_box->y0 + yy); + } idx = ((r >> (8 - pm->rbits)) << (pm->gbits + pm->bbits)) | ((g >> (8 - pm->gbits)) << pm->bbits) | ( b >> (8 - pm->bbits)); @@ -502,13 +530,16 @@ static result_t screen_copy_bitmap_p2(screen_t *scr, const unsigned char *srcrow; unsigned char *dstbase; const pixelmap_t *pm; - int nlevels; + int bias[64]; + int do_dither; int clipped_width, clipped_height; int yy; clipped_width = draw_box->x1 - draw_box->x0; clipped_height = draw_box->y1 - draw_box->y0; - nlevels = pixelfmt_paletted_nentries(scr->format); + do_dither = dither_bias_build(bias, + pixelfmt_paletted_nentries(scr->format), + dither); srcrow = (const unsigned char *) src->base + (draw_box->y0 - y) * src->rowbytes; dstbase = scr->base; @@ -545,9 +576,12 @@ static result_t screen_copy_bitmap_p2(screen_t *scr, r = (c.primary >> pm->rshift) & 0xFF; g = (c.primary >> pm->gshift) & 0xFF; b = (c.primary >> pm->bshift) & 0xFF; - r = dither_channel(r, nlevels, dither, dstx, draw_box->y0 + yy); - g = dither_channel(g, nlevels, dither, dstx, draw_box->y0 + yy); - b = dither_channel(b, nlevels, dither, dstx, draw_box->y0 + yy); + if (do_dither) + { + r = dither_channel(r, bias, dstx, draw_box->y0 + yy); + g = dither_channel(g, bias, dstx, draw_box->y0 + yy); + b = dither_channel(b, bias, dstx, draw_box->y0 + yy); + } idx = ((r >> (8 - pm->rbits)) << (pm->gbits + pm->bbits)) | ((g >> (8 - pm->gbits)) << pm->bbits) | ( b >> (8 - pm->bbits)); From eb722446ff3b0b085efe1809e9afdf3118eacfb2 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Thu, 10 Sep 2026 23:29:17 +0100 Subject: [PATCH 016/128] fix(framebuf): key blit dither on sprite-local coordinates The ordered-dither Bayer cell was selected from the destination screen coordinate, so moving a sprite's window shifted every pixel onto a different cell and the dither pattern crawled across the sprite as it moved. Key it on the sprite-local pixel (screen coord minus the blit origin) instead, so the pattern is fixed to the sprite. Co-Authored-By: Claude Sonnet 5 --- libraries/framebuf/screen/screen-draw.c | 31 ++++++++++++++----------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/libraries/framebuf/screen/screen-draw.c b/libraries/framebuf/screen/screen-draw.c index 2fcbef0a..2895c7b6 100644 --- a/libraries/framebuf/screen/screen-draw.c +++ b/libraries/framebuf/screen/screen-draw.c @@ -325,8 +325,10 @@ static int dither_bias_build(int tab[64], int nlevels, int dither) return 1; } -/* Apply the pre-built bias for screen pixel (sx, sy) to channel value "v" - * (0..255), result clamped to 0..255. */ +/* Apply the pre-built bias for sprite-local pixel (sx, sy) to channel value + * "v" (0..255), result clamped to 0..255. Coordinates are relative to the + * source bitmap's top-left, not the screen, so the dither pattern stays fixed + * to the sprite as its window moves rather than crawling across it. */ static unsigned int dither_channel(unsigned int v, const int tab[64], int sx, @@ -344,9 +346,10 @@ static unsigned int dither_channel(unsigned int v, * transfer (skip fully transparent, else nearest palette match) rather than * true blending, matching screen_set_pixel's case 2. With "dither" set the * source RGB is ordered-dithered per pixel before the nearest-match lookup, - * breaking up the banding a shallow palette otherwise shows on a gradient. - * Returns result_NOT_SUPPORTED if there is no deep->paletted conversion table - * for the screen's format. */ + * breaking up the banding a shallow palette otherwise shows on a gradient; the + * Bayer cell is keyed on the sprite-local pixel so the pattern travels with + * the sprite. Returns result_NOT_SUPPORTED if there is no deep->paletted + * conversion table for the screen's format. */ static result_t screen_copy_bitmap_p4(screen_t *scr, int x, int y, @@ -409,9 +412,9 @@ static result_t screen_copy_bitmap_p4(screen_t *scr, b = (c.primary >> pm->bshift) & 0xFF; if (do_dither) { - r = dither_channel(r, bias, dstx, draw_box->y0 + yy); - g = dither_channel(g, bias, dstx, draw_box->y0 + yy); - b = dither_channel(b, bias, dstx, draw_box->y0 + yy); + r = dither_channel(r, bias, dstx - x, draw_box->y0 + yy - y); + g = dither_channel(g, bias, dstx - x, draw_box->y0 + yy - y); + b = dither_channel(b, bias, dstx - x, draw_box->y0 + yy - y); } idx = ((r >> (8 - pm->rbits)) << (pm->gbits + pm->bbits)) | ((g >> (8 - pm->gbits)) << pm->bbits) @@ -494,9 +497,9 @@ static result_t screen_copy_bitmap_p1(screen_t *scr, b = (c.primary >> pm->bshift) & 0xFF; if (do_dither) { - r = dither_channel(r, bias, dstx, draw_box->y0 + yy); - g = dither_channel(g, bias, dstx, draw_box->y0 + yy); - b = dither_channel(b, bias, dstx, draw_box->y0 + yy); + r = dither_channel(r, bias, dstx - x, draw_box->y0 + yy - y); + g = dither_channel(g, bias, dstx - x, draw_box->y0 + yy - y); + b = dither_channel(b, bias, dstx - x, draw_box->y0 + yy - y); } idx = ((r >> (8 - pm->rbits)) << (pm->gbits + pm->bbits)) | ((g >> (8 - pm->gbits)) << pm->bbits) @@ -578,9 +581,9 @@ static result_t screen_copy_bitmap_p2(screen_t *scr, b = (c.primary >> pm->bshift) & 0xFF; if (do_dither) { - r = dither_channel(r, bias, dstx, draw_box->y0 + yy); - g = dither_channel(g, bias, dstx, draw_box->y0 + yy); - b = dither_channel(b, bias, dstx, draw_box->y0 + yy); + r = dither_channel(r, bias, dstx - x, draw_box->y0 + yy - y); + g = dither_channel(g, bias, dstx - x, draw_box->y0 + yy - y); + b = dither_channel(b, bias, dstx - x, draw_box->y0 + yy - y); } idx = ((r >> (8 - pm->rbits)) << (pm->gbits + pm->bbits)) | ((g >> (8 - pm->gbits)) << pm->bbits) From af99859537ca212b6a99b560ae741e7ffa8522b5 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Thu, 10 Sep 2026 23:34:56 +0100 Subject: [PATCH 017/128] fix(framebuf): support 1bpp and 2bpp screens in screen_copy_rect screen_copy_rect only handled 4bpp and the whole-byte formats; 1bpp and 2bpp screens fell through to result_NOT_SUPPORTED, so every window move or scroll on a low-bpp screen failed the wuss blit fast path and repainted the entire footprint. Generalise the 4bpp packed-pixel copy (screen_copy_rect_p4) to any sub-byte depth (screen_copy_rect_packed, bpp_bits 1/2/4), keeping the two-axis blit-direction trick that makes the in-place copy safe under self-overlap. Dispatch log2bpp 0 and 1 to it alongside 2. Add test_copy_rect_packed covering p1/p2/p4: a non-byte-aligned copy lands at the destination, does not smear past it, and leaves rows outside the source untouched. Co-Authored-By: Claude Sonnet 5 --- libraries/framebuf/screen/screen-copy-rect.c | 62 ++++++---- libraries/framebuf/screen/test/screen-test.c | 114 ++++++++++++++++++- 2 files changed, 151 insertions(+), 25 deletions(-) diff --git a/libraries/framebuf/screen/screen-copy-rect.c b/libraries/framebuf/screen/screen-copy-rect.c index 36eed1a7..968998eb 100644 --- a/libraries/framebuf/screen/screen-copy-rect.c +++ b/libraries/framebuf/screen/screen-copy-rect.c @@ -4,25 +4,28 @@ #include "framebuf/screen.h" -/* 4bpp paletted screens pack two pixels per byte, so a moving window's - * source and destination nibbles don't line up on byte boundaries in - * general. Copied in place, pixel by pixel, masking each nibble out and - * rolling it into place: safe under self-overlap by walking rows in the - * direction "dy" dictates (as the byte path already does) and, since a - * nibble's own byte-mate is its immediate horizontal neighbour, walking - * 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 result_t screen_copy_rect_p4(screen_t *scr, - const box_t *s, - const box_t *d, - int width, - int height, - int dx, - int dy) +/* Sub-byte paletted screens (1/2/4bpp) pack several pixels per byte, so a + * moving window's source and destination pixels don't line up on byte + * boundaries in general. Copied in place, pixel by pixel, masking each one + * out and rolling it into place: safe under self-overlap by walking rows in + * the direction "dy" dictates (as the byte path already does) and, since a + * pixel's byte-mates are its immediate horizontal neighbours, walking 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. "bpp_bits" is 1, 2 or 4. */ +static result_t screen_copy_rect_packed(screen_t *scr, + const box_t *s, + const box_t *d, + int width, + int height, + int dx, + int dy, + int bpp_bits) { unsigned char *base; int rowbytes; + int ppb, xshift, xmask; + unsigned char pmask; int row_first, row_last, row_step; int col_first, col_last, col_step; int row, col; @@ -30,6 +33,11 @@ static result_t screen_copy_rect_p4(screen_t *scr, base = scr->base; rowbytes = scr->rowbytes; + ppb = 8 / bpp_bits; /* pixels per byte: 8, 4 or 2 */ + xshift = (bpp_bits == 1) ? 3 : (bpp_bits == 2) ? 2 : 1; + xmask = ppb - 1; + pmask = (unsigned char) ((1u << bpp_bits) - 1); + if (dy > 0) { row_first = height - 1; row_last = -1; row_step = -1; } else { row_first = 0; row_last = height; row_step = 1; } @@ -44,18 +52,19 @@ static result_t screen_copy_rect_p4(screen_t *scr, const unsigned char *scrp_s; unsigned char *scrp_d; int shift_s, shift_d; - unsigned char nib; + unsigned char pix; sx = s->x0 + col; sy = s->y0 + row; dxp = d->x0 + col; dyp = d->y0 + row; - scrp_s = base + (size_t) sy * rowbytes + (sx >> 1); - shift_s = (sx & 1) * 4; - nib = (unsigned char) ((*scrp_s >> shift_s) & 0xF); + scrp_s = base + (size_t) sy * rowbytes + (sx >> xshift); + shift_s = (sx & xmask) * bpp_bits; + pix = (unsigned char) ((*scrp_s >> shift_s) & pmask); - scrp_d = base + (size_t) dyp * rowbytes + (dxp >> 1); - shift_d = (dxp & 1) * 4; - *scrp_d = (unsigned char) ((*scrp_d & ~(0xF << shift_d)) | (nib << shift_d)); + scrp_d = base + (size_t) dyp * rowbytes + (dxp >> xshift); + shift_d = (dxp & xmask) * bpp_bits; + *scrp_d = (unsigned char) ((*scrp_d & ~(pmask << shift_d)) + | (pix << shift_d)); } } @@ -146,7 +155,12 @@ result_t screen_copy_rect(screen_t *scr, switch (pixelfmt_log2bpp(scr->format)) { - case 2: return screen_copy_rect_p4(scr, &s, &d_clipped, width, height, dx, dy); + case 0: return screen_copy_rect_packed(scr, &s, &d_clipped, + width, height, dx, dy, 1); + case 1: return screen_copy_rect_packed(scr, &s, &d_clipped, + width, height, dx, dy, 2); + case 2: return screen_copy_rect_packed(scr, &s, &d_clipped, + width, height, dx, dy, 4); case 3: bpp = 1; break; case 4: bpp = 2; break; diff --git a/libraries/framebuf/screen/test/screen-test.c b/libraries/framebuf/screen/test/screen-test.c index de53904d..19a55d27 100644 --- a/libraries/framebuf/screen/test/screen-test.c +++ b/libraries/framebuf/screen/test/screen-test.c @@ -1068,6 +1068,117 @@ static result_t test_copy_bitmap_dithered(void) /* ----------------------------------------------------------------------- */ +/* Read/write the "bpp_bits"-deep pixel at column "px" of a byte buffer, + * LSB-first within the byte -- the same packing screen_copy_rect_packed uses + * internally. (screen_set_pixel is MSB-first for p1/p2; this test does its own + * packing so it stays independent of that convention -- a rect copy only has + * to be self-consistent to be correct.) */ +static int packed_pixel_at(const unsigned char *buf, int px, int bpp_bits) +{ + int ppb = 8 / bpp_bits; + int shift = (px % ppb) * bpp_bits; + int mask = (1 << bpp_bits) - 1; + + return (buf[px / ppb] >> shift) & mask; +} + +static void packed_pixel_set(unsigned char *buf, + int px, + int bpp_bits, + int val) +{ + int ppb = 8 / bpp_bits; + int shift = (px % ppb) * bpp_bits; + int mask = (1 << bpp_bits) - 1; + + buf[px / ppb] = (unsigned char) ((buf[px / ppb] & ~(mask << shift)) + | ((val & mask) << shift)); +} + +/* screen_copy_rect on the sub-byte paletted formats (p1/p2/p4): fill a + * left-hand column block with a non-zero index, copy it right by an odd + * (non-byte-aligned) offset, and check the pixels landed at the destination + * and the untouched column to the right of the source stayed clear (the copy + * reads before it writes -- no smear). Regression: these formats used to fall + * through to result_NOT_SUPPORTED, forcing a full repaint on every low-bpp + * window move. */ +static result_t test_copy_rect_packed(void) +{ + static const struct { pixelfmt_t fmt; int bits; } cases[] = + { + { pixelfmt_p1, 1 }, + { pixelfmt_p2, 2 }, + { pixelfmt_p4, 4 }, + }; + + static unsigned char buf[WIDTH / 2 * HEIGHT]; /* widest sub-byte case */ + + colour_t pal[16]; + size_t ci; + int i; + + for (i = 0; i < 16; i++) + pal[i] = colour_rgb((unsigned char) (i * 17), + (unsigned char) (i * 17), + (unsigned char) (i * 17)); + + for (ci = 0; ci < NELEMS(cases); ci++) + { + screen_t scr; + box_t src; + box_t got; + int rowbytes, fill, x, y; + + rowbytes = WIDTH * cases[ci].bits / 8; + fill = (1 << cases[ci].bits) - 1; /* max index for this depth */ + + memset(buf, 0, sizeof(buf)); + screen_init(&scr, SIZE2D(WIDTH, HEIGHT), cases[ci].fmt, rowbytes, pal, buf); + + /* an 8-wide, 8-tall block at the top-left */ + for (y = 0; y < 8; y++) + for (x = 0; x < 8; x++) + packed_pixel_set(buf + (size_t) y * rowbytes, x, cases[ci].bits, fill); + + src = (box_t) { 0, 0, 8, 8 }; + if (screen_copy_rect(&scr, &src, POINT(5, 0), &got) != result_OK) + { + printf("screen: copy_rect declined a p%d screen\n", cases[ci].bits); + return result_TEST_FAILED; + } + + /* moved: columns 5..12 of row 0 now carry the fill index */ + for (x = 5; x < 13; x++) + if (packed_pixel_at(buf, x, cases[ci].bits) != fill) + { + printf("screen: copy_rect p%d did not move pixel at x=%d\n", + cases[ci].bits, x); + return result_TEST_FAILED; + } + + /* read-before-write: column 13, just past the destination, is untouched */ + if (packed_pixel_at(buf, 13, cases[ci].bits) != 0) + { + printf("screen: copy_rect p%d smeared past its destination\n", + cases[ci].bits); + return result_TEST_FAILED; + } + + /* row 8, the first row below the 8-tall source, stayed clear */ + for (x = 0; x < 16; x++) + if (packed_pixel_at(buf + (size_t) 8 * rowbytes, x, cases[ci].bits) != 0) + { + printf("screen: copy_rect p%d touched a row below the source\n", + cases[ci].bits); + return result_TEST_FAILED; + } + } + + return result_TEST_PASSED; +} + +/* ----------------------------------------------------------------------- */ + result_t screen_test(const char *resources) { typedef result_t (*screentestfn)(void); @@ -1086,7 +1197,8 @@ result_t screen_test(const char *resources) test_fill_circle, test_copy_bitmap_p1, test_copy_bitmap_p2, - test_copy_bitmap_dithered + test_copy_bitmap_dithered, + test_copy_rect_packed }; result_t rc; From 24f2094ce77fcea817ca7b324a02caf7ceab8f92 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Thu, 10 Sep 2026 23:39:55 +0100 Subject: [PATCH 018/128] fix(framebuf): match house within-byte pixel order in screen_copy_rect_packed The generalised packed copy used LSB-first indexing for every sub-byte depth, but the rest of the screen code packs p1 and p2 MSB-first (bit 7 / bits 7..6 leftmost, per screen_set_pixel_p1/p2 and screen_copy_bitmap_p1/ p2) -- only p4 is LSB-first. On a 2bpp screen a non-byte-aligned window move therefore picked the wrong pixel within each byte and shuffled the copied row. Carry an msb_first flag (set for bpp_bits 1 and 2) and flip the within-byte shift accordingly. Strengthen test_copy_rect_packed: lay the source down with screen_set_pixel, give each column a distinct index, and read back with a house-order reader -- it now fails if the copy uses the wrong endianness. Co-Authored-By: Claude Sonnet 5 --- libraries/framebuf/screen/screen-copy-rect.c | 22 +++++-- libraries/framebuf/screen/test/screen-test.c | 66 +++++++++----------- 2 files changed, 46 insertions(+), 42 deletions(-) diff --git a/libraries/framebuf/screen/screen-copy-rect.c b/libraries/framebuf/screen/screen-copy-rect.c index 968998eb..00ec31c2 100644 --- a/libraries/framebuf/screen/screen-copy-rect.c +++ b/libraries/framebuf/screen/screen-copy-rect.c @@ -12,7 +12,12 @@ * pixel's byte-mates are its immediate horizontal neighbours, walking 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. "bpp_bits" is 1, 2 or 4. */ + * overwrite it is written. "bpp_bits" is 1, 2 or 4. + * + * The within-byte pixel order must match the rest of the screen code, which + * is not uniform: p1 and p2 are MSB-first (bit 7 / bits 7..6 are the leftmost + * pixel -- see screen_set_pixel_p1/p2 and screen_copy_bitmap_p1/p2) while p4 + * is LSB-first (see screen_set_pixel_p4). "msb_first" carries that. */ static result_t screen_copy_rect_packed(screen_t *scr, const box_t *s, const box_t *d, @@ -24,7 +29,7 @@ static result_t screen_copy_rect_packed(screen_t *scr, { unsigned char *base; int rowbytes; - int ppb, xshift, xmask; + int ppb, xshift, xmask, msb_first; unsigned char pmask; int row_first, row_last, row_step; int col_first, col_last, col_step; @@ -33,10 +38,11 @@ static result_t screen_copy_rect_packed(screen_t *scr, base = scr->base; rowbytes = scr->rowbytes; - ppb = 8 / bpp_bits; /* pixels per byte: 8, 4 or 2 */ - xshift = (bpp_bits == 1) ? 3 : (bpp_bits == 2) ? 2 : 1; - xmask = ppb - 1; - pmask = (unsigned char) ((1u << bpp_bits) - 1); + ppb = 8 / bpp_bits; /* pixels per byte: 8, 4 or 2 */ + xshift = (bpp_bits == 1) ? 3 : (bpp_bits == 2) ? 2 : 1; + xmask = ppb - 1; + pmask = (unsigned char) ((1u << bpp_bits) - 1); + msb_first = (bpp_bits != 4); if (dy > 0) { row_first = height - 1; row_last = -1; row_step = -1; } else { row_first = 0; row_last = height; row_step = 1; } @@ -59,10 +65,14 @@ static result_t screen_copy_rect_packed(screen_t *scr, scrp_s = base + (size_t) sy * rowbytes + (sx >> xshift); shift_s = (sx & xmask) * bpp_bits; + if (msb_first) + shift_s = 8 - bpp_bits - shift_s; pix = (unsigned char) ((*scrp_s >> shift_s) & pmask); scrp_d = base + (size_t) dyp * rowbytes + (dxp >> xshift); shift_d = (dxp & xmask) * bpp_bits; + if (msb_first) + shift_d = 8 - bpp_bits - shift_d; *scrp_d = (unsigned char) ((*scrp_d & ~(pmask << shift_d)) | (pix << shift_d)); } diff --git a/libraries/framebuf/screen/test/screen-test.c b/libraries/framebuf/screen/test/screen-test.c index 19a55d27..7ad0e29d 100644 --- a/libraries/framebuf/screen/test/screen-test.c +++ b/libraries/framebuf/screen/test/screen-test.c @@ -1068,40 +1068,29 @@ static result_t test_copy_bitmap_dithered(void) /* ----------------------------------------------------------------------- */ -/* Read/write the "bpp_bits"-deep pixel at column "px" of a byte buffer, - * LSB-first within the byte -- the same packing screen_copy_rect_packed uses - * internally. (screen_set_pixel is MSB-first for p1/p2; this test does its own - * packing so it stays independent of that convention -- a rect copy only has - * to be self-consistent to be correct.) */ -static int packed_pixel_at(const unsigned char *buf, int px, int bpp_bits) +/* Read the "bpp_bits"-deep pixel at column "px" of a byte buffer using the + * house within-byte order: p1/p2 MSB-first (bit 7 leftmost), p4 LSB-first -- + * matching screen_set_pixel_p1/p2/p4. The source block is laid down with + * screen_set_pixel, so a wrong-endian copy shows up here as a shuffled row. */ +static int house_pixel_at(const unsigned char *buf, int px, int bpp_bits) { int ppb = 8 / bpp_bits; - int shift = (px % ppb) * bpp_bits; int mask = (1 << bpp_bits) - 1; - - return (buf[px / ppb] >> shift) & mask; -} - -static void packed_pixel_set(unsigned char *buf, - int px, - int bpp_bits, - int val) -{ - int ppb = 8 / bpp_bits; int shift = (px % ppb) * bpp_bits; - int mask = (1 << bpp_bits) - 1; - buf[px / ppb] = (unsigned char) ((buf[px / ppb] & ~(mask << shift)) - | ((val & mask) << shift)); + if (bpp_bits != 4) + shift = 8 - bpp_bits - shift; /* MSB-first for p1/p2 */ + + return (buf[px / ppb] >> shift) & mask; } -/* screen_copy_rect on the sub-byte paletted formats (p1/p2/p4): fill a - * left-hand column block with a non-zero index, copy it right by an odd - * (non-byte-aligned) offset, and check the pixels landed at the destination - * and the untouched column to the right of the source stayed clear (the copy - * reads before it writes -- no smear). Regression: these formats used to fall - * through to result_NOT_SUPPORTED, forcing a full repaint on every low-bpp - * window move. */ +/* screen_copy_rect on the sub-byte paletted formats (p1/p2/p4): lay down an + * 8x8 block with screen_set_pixel, copy it right by an odd (non-byte-aligned) + * offset, and check the pixels landed at the destination in the right order, + * did not smear past it, and left the row below the source clear. Regression: + * these formats used to fall through to result_NOT_SUPPORTED (full repaint on + * every low-bpp window move); the first packed implementation then copied + * p1/p2 with the wrong within-byte order and shuffled the row. */ static result_t test_copy_rect_packed(void) { static const struct { pixelfmt_t fmt; int bits; } cases[] = @@ -1135,10 +1124,14 @@ static result_t test_copy_rect_packed(void) memset(buf, 0, sizeof(buf)); screen_init(&scr, SIZE2D(WIDTH, HEIGHT), cases[ci].fmt, rowbytes, pal, buf); - /* an 8-wide, 8-tall block at the top-left */ + /* an 8-wide, 8-tall block at the top-left, laid down the house way. Each + * column carries a distinct non-zero index (1 + x % fill; adjacent + * columns -- including byte-mates -- differ) so a within-byte reordering + * can't hide behind a uniform fill. p1 has only index 1 available, so its + * shuffle is caught by the no-smear check below instead. */ for (y = 0; y < 8; y++) for (x = 0; x < 8; x++) - packed_pixel_set(buf + (size_t) y * rowbytes, x, cases[ci].bits, fill); + screen_set_pixel(&scr, x, y, pal[1 + x % fill]); src = (box_t) { 0, 0, 8, 8 }; if (screen_copy_rect(&scr, &src, POINT(5, 0), &got) != result_OK) @@ -1147,17 +1140,18 @@ static result_t test_copy_rect_packed(void) return result_TEST_FAILED; } - /* moved: columns 5..12 of row 0 now carry the fill index */ - for (x = 5; x < 13; x++) - if (packed_pixel_at(buf, x, cases[ci].bits) != fill) + /* moved: column c of the source now sits at c+5, same index */ + for (x = 0; x < 8; x++) + if (house_pixel_at(buf, x + 5, cases[ci].bits) != 1 + x % fill) { - printf("screen: copy_rect p%d did not move pixel at x=%d\n", - cases[ci].bits, x); + printf("screen: copy_rect p%d wrong pixel at x=%d " + "(got %d want %d)\n", cases[ci].bits, x + 5, + house_pixel_at(buf, x + 5, cases[ci].bits), 1 + x % fill); return result_TEST_FAILED; } /* read-before-write: column 13, just past the destination, is untouched */ - if (packed_pixel_at(buf, 13, cases[ci].bits) != 0) + if (house_pixel_at(buf, 13, cases[ci].bits) != 0) { printf("screen: copy_rect p%d smeared past its destination\n", cases[ci].bits); @@ -1166,7 +1160,7 @@ static result_t test_copy_rect_packed(void) /* row 8, the first row below the 8-tall source, stayed clear */ for (x = 0; x < 16; x++) - if (packed_pixel_at(buf + (size_t) 8 * rowbytes, x, cases[ci].bits) != 0) + if (house_pixel_at(buf + (size_t) 8 * rowbytes, x, cases[ci].bits) != 0) { printf("screen: copy_rect p%d touched a row below the source\n", cases[ci].bits); From 78ed9e4e12b09d29204e56baa8cc75c31e3d849e Mon Sep 17 00:00:00 2001 From: David Thomas Date: Fri, 11 Sep 2026 00:00:21 +0100 Subject: [PATCH 019/128] fix(wuss): keep a menu-opened window on screen A borrowed-window menu item was positioned off the parent menu's right edge by wuss__submenu_anchor with no on-screen clamp; wuss_window_move deliberately does not clamp (drags rely on it), so a parent menu near the right screen edge left the window opening off-screen. Extract wuss_window_create's inline on-screen nudge into a shared wuss__nudge_visible_onscreen helper in core/impl.h and call it from wuss__menu_open_window after the anchor move. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/core/impl.h | 34 +++++++++++++++++++++++++++++ libraries/wuss/core/window/create.c | 29 +++++------------------- libraries/wuss/menu/menu.c | 4 ++++ 3 files changed, 43 insertions(+), 24 deletions(-) diff --git a/libraries/wuss/core/impl.h b/libraries/wuss/core/impl.h index 63c953f8..e641af08 100644 --- a/libraries/wuss/core/impl.h +++ b/libraries/wuss/core/impl.h @@ -773,4 +773,38 @@ static inline void wuss__max_content_anywhere_on_screen(const wuss_window_t *win max->h = MAX(max->h, WUSS_MIN_CONTENT); } +/* Nudge window->visible back onto the screen by the minimum needed to bring + * its top-left (titlebar/close-icon) edge into view: shift right/down if it + * is off the top or left, shift left/up if it hangs off the right or bottom, + * but never so far that the top-left goes off the opposite edge -- a window + * bigger than the screen keeps its top-left on and overhangs bottom-right. + * Only the position moves, not the size. Shared by wuss_window_create and the + * menu code's borrowed-window opener so no menu can place a window off-screen. + */ +static inline void wuss__nudge_visible_onscreen(wuss_window_t *window) +{ + int scr_width, scr_height, dx, dy; + + scr_width = window->wuss->scr->size.w; + scr_height = window->wuss->scr->size.h; + + dx = 0; + if (window->visible.x0 < 0) + dx = -window->visible.x0; + else if (window->visible.x1 > scr_width) + dx = scr_width - window->visible.x1; + if (window->visible.x0 + dx < 0) + dx = -window->visible.x0; + + dy = 0; + if (window->visible.y0 < 0) + dy = -window->visible.y0; + else if (window->visible.y1 > scr_height) + dy = scr_height - window->visible.y1; + if (window->visible.y0 + dy < 0) + dy = -window->visible.y0; + + box_translated(&window->visible, dx, dy, &window->visible); +} + #endif /* IMPL_H */ diff --git a/libraries/wuss/core/window/create.c b/libraries/wuss/core/window/create.c index 941807fe..730afc28 100644 --- a/libraries/wuss/core/window/create.c +++ b/libraries/wuss/core/window/create.c @@ -22,7 +22,6 @@ result_t wuss_window_create(wuss_task_t *task, wuss_t *wuss; wuss_window_t *win; int width, height, outline_px, titlebar_height; - int scr_width, scr_height, dx, dy; point_t carve; assert(task != NULL); @@ -50,31 +49,13 @@ result_t wuss_window_create(wuss_task_t *task, win->visible.x1 = content->x1 + outline_px + carve.x; win->visible.y1 = content->y1 + outline_px + carve.y; + win->flags = flags; + /* nudge back on-screen so the titlebar/close icon stay reachable; a * window bigger than the screen keeps its top-left (titlebar) edge - * on-screen rather than being centred or left alone */ - scr_width = wuss->scr->size.w; - scr_height = wuss->scr->size.h; - - dx = 0; - if (win->visible.x0 < 0) - dx = -win->visible.x0; - else if (win->visible.x1 > scr_width) - dx = scr_width - win->visible.x1; - if (win->visible.x0 + dx < 0) - dx = -win->visible.x0; - - dy = 0; - if (win->visible.y0 < 0) - dy = -win->visible.y0; - else if (win->visible.y1 > scr_height) - dy = scr_height - win->visible.y1; - if (win->visible.y0 + dy < 0) - dy = -win->visible.y0; - - box_translated(&win->visible, dx, dy, &win->visible); - - win->flags = flags; + * on-screen rather than being centred or left alone. Needs win->flags + * set (the helper reads the carve off it). */ + wuss__nudge_visible_onscreen(win); /* clamp so the window can never be born larger than the screen; done * after the on-screen nudge above so it measures from the final diff --git a/libraries/wuss/menu/menu.c b/libraries/wuss/menu/menu.c index 8af500fd..0ee41e54 100644 --- a/libraries/wuss/menu/menu.c +++ b/libraries/wuss/menu/menu.c @@ -215,6 +215,10 @@ static result_t wuss__menu_open_window(struct wuss__menu *self, int index) at = wuss__submenu_anchor(self, self->icons[index]); wuss_window_move(win, at); + /* the anchor sits off the parent's right edge; if the parent menu is near + * the screen edge that puts the window (partly) off-screen. wuss_window_move + * does not clamp -- drags rely on it not clamping -- so pull it back here. */ + wuss__nudge_visible_onscreen(win); if (wuss_window_set_hidden(win, 0) != result_OK) { wuss_window_move(win, prev); /* vetoed: undo the anchor move */ From 2126ef07da02be3dd5aa4a4b0955b0d4c1f7e3df Mon Sep 17 00:00:00 2001 From: David Thomas Date: Fri, 11 Sep 2026 00:08:14 +0100 Subject: [PATCH 020/128] fix(wuss): refresh the whole radio/option icon on select wuss__icon_draw_radio_option only repainted its box when the icon carried an explicit bg. With bg unset (the icon blending against the window backdrop) a state change redrew the new glyph over the old one with no clear, so a shrinking radon->radoff swap or a removed tick/centre left stale pixels. Clear the glyph box to the blend ground in that case. Separately, wuss__icon_from_spec copied the caller's bbox verbatim. The state bitmap (radon/radoff, opton/optoff) is drawn centred in the bbox with a top-left offset, so a bitmap larger than the bbox overhangs it; a select-time invalidate covers only the bbox and leaves the overhanging edges stale. Grow a RADIO/OPTION bbox to the largest of its two state bitmaps so the drawn glyph always fits the box invalidate, hit-test and layout use. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/icon/draw.c | 7 ++++++ libraries/wuss/icon/from-spec.c | 38 +++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/libraries/wuss/icon/draw.c b/libraries/wuss/icon/draw.c index 28963f20..e081d647 100644 --- a/libraries/wuss/icon/draw.c +++ b/libraries/wuss/icon/draw.c @@ -419,6 +419,13 @@ static void wuss__icon_draw_radio_option(const icon_draw_ctx_t *c) if (icon->bg != wuss_NO_BACKGROUND) screen_fill_rect(c->scr, b->x0, b->y0, SIZE2D(b->x1 - b->x0, b->y1 - b->y0), bg); + else + /* no explicit bg to repaint the whole box, but the state we are about to + * draw (a smaller radon/radoff swap, or a cleared tick/centre) can leave + * fewer pixels than last time -- clear just the glyph box so no stale + * glyph shows through. The label to its right never changes on select. */ + screen_fill_rect(c->scr, g.x0, g.y0, + SIZE2D(g.x1 - g.x0, g.y1 - g.y0), bg); if (bm != NULL) { diff --git a/libraries/wuss/icon/from-spec.c b/libraries/wuss/icon/from-spec.c index b909108a..47ab0cc8 100644 --- a/libraries/wuss/icon/from-spec.c +++ b/libraries/wuss/icon/from-spec.c @@ -93,6 +93,44 @@ result_t wuss__icon_from_spec(const wuss_t *w, out->spec.fg = fg; out->spec.bg = bg; + /* A RADIO/OPTION glyph is drawn from the icon-set bitmap (radon/radoff, + * opton/optoff) when the set carries one, vertically centred in the bbox + * and offset from the bbox top-left. If a state bitmap is larger than the + * caller's bbox the glyph overhangs it, and a select-time invalidate -- + * which only covers the bbox -- then leaves the overhanging edges stale. + * Grow the bbox to the largest of the two state bitmaps so the drawn glyph + * always sits inside the box that hit-test, layout and invalidate use. */ + if (spec->type == wuss_ICON_TYPE_RADIO || spec->type == wuss_ICON_TYPE_OPTION) + { + const char *names[2]; + const bitmap_t *state_bm; + int idx; + int gw, gh; + int i; + + names[0] = (spec->type == wuss_ICON_TYPE_RADIO) ? "radon" : "opton"; + names[1] = (spec->type == wuss_ICON_TYPE_RADIO) ? "radoff" : "optoff"; + + gw = 0; + gh = 0; + for (i = 0; i < 2; i++) + { + idx = wuss_icons_lookup(w, names[i]); + if (idx < 0) + continue; + state_bm = wuss_icons_bitmap(w, idx); + if (state_bm == NULL) + continue; + gw = MAX(gw, state_bm->size.w); + gh = MAX(gh, state_bm->size.h); + } + + if (gw > 0 && out->spec.bbox.x1 - out->spec.bbox.x0 < gw) + out->spec.bbox.x1 = out->spec.bbox.x0 + gw; + if (gh > 0 && out->spec.bbox.y1 - out->spec.bbox.y0 < gh) + out->spec.bbox.y1 = out->spec.bbox.y0 + gh; + } + /* out->spec.u is already the caller's copy; overwrite only the arms whose * value we resolved (bitmap image from the icon set, swatch to a palette * index) */ From 96a40293c5cb0ddda7447f3b63ae5b4747733b5e Mon Sep 17 00:00:00 2001 From: David Thomas Date: Fri, 11 Sep 2026 00:32:13 +0100 Subject: [PATCH 021/128] fix(wuss): don't close a submenu dragged over its parent's titlebar The IDLE tick closes a submenu when the pointer is over its parent's titlebar, so the parent can be grabbed and dragged. wuss__pointer_over_ titlebar only checked the parent's visible box and y, not whether the parent was the frontmost window at the pointer, so dragging the child menu back over the parent's titlebar -- child window then on top there -- was read as a parent-titlebar hover and closed the child mid-drag. Require wuss__window_at at the pointer to be the parent itself. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/menu/menu.c | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/libraries/wuss/menu/menu.c b/libraries/wuss/menu/menu.c index 0ee41e54..dd8c346d 100644 --- a/libraries/wuss/menu/menu.c +++ b/libraries/wuss/menu/menu.c @@ -232,11 +232,14 @@ static result_t wuss__menu_open_window(struct wuss__menu *self, int index) } /* True if the wuss pointer sits over `window`'s chrome above its content area - * -- the titlebar strip. Core routes furniture hovers straight to the window - * manager and delivers no event to the menu delegate (see wuss_mouse_move), - * so a titlebar hover on a parent level cannot be caught in the ICON handler; - * the IDLE tick polls this instead to close a submenu the pointer has slid up - * onto the parent's titlebar to reach (e.g. to drag the parent). */ + * -- the titlebar strip -- and `window` is the frontmost window there. Core + * routes furniture hovers straight to the window manager and delivers no event + * to the menu delegate (see wuss_mouse_move), so a titlebar hover on a parent + * level cannot be caught in the ICON handler; the IDLE tick polls this instead + * to close a submenu the pointer has slid up onto the parent's titlebar to + * reach (e.g. to drag the parent). The frontmost check stops a child menu + * dragged back over its parent's titlebar -- the child window is then on top + * at that point -- from being read as a parent-titlebar hover and closed. */ static int wuss__pointer_over_titlebar(const wuss_window_t *window) { point_t p; @@ -246,7 +249,8 @@ static int wuss__pointer_over_titlebar(const wuss_window_t *window) wuss__content_box(window, &content); return box_contains_point(&window->visible, p.x, p.y) - && p.y < content.y0; + && p.y < content.y0 + && wuss__window_at(window->wuss, p) == window; } /* ----------------------------------------------------------------------- */ From 68cc2a0f7e6b7299799875485dc8bf27a0265d0e Mon Sep 17 00:00:00 2001 From: David Thomas Date: Fri, 11 Sep 2026 00:45:12 +0100 Subject: [PATCH 022/128] fix(wuss): update the chars grid's doc extent on font switch chars_set_font resized the window but not its document extent, so the scroll range stayed sized to the font the window was created with; switching to a larger font put the far grid cells past the scroll range and out of reach, and a smaller font left dead scroll space. Call wuss_window_set_doc with the new grid size alongside the resize. Also hoist the duplicated chars_window_size call in chars_create into a local. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/test/tasks/chars.c | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/libraries/wuss/test/tasks/chars.c b/libraries/wuss/test/tasks/chars.c index 4156fcdc..2ea57b92 100644 --- a/libraries/wuss/test/tasks/chars.c +++ b/libraries/wuss/test/tasks/chars.c @@ -86,7 +86,17 @@ static result_t chars_set_font(chars_task_t *task, int idx, const char *name) task->font = font; task->current = idx; - wuss_window_resize(task->window, chars_window_size(task, font)); + /* the grid's cell metrics scale with the font, so both the window and its + * scrollable extent have to follow the new size -- resize alone would leave + * the doc (and so the scroll range) sized to the font the window was + * created with, clipping the far cells of a larger font unreachably */ + { + size2d_t grid; + + grid = chars_window_size(task, font); + wuss_window_resize(task->window, grid); + wuss_window_set_doc(task->window, grid); + } wuss_window_invalidate_visible(task->window); return result_OK; } @@ -113,6 +123,7 @@ result_t chars_create(wuss_t *wuss, bmfont_t *font; const char *bmfonts_dir; const wuss_menu_t *menu; + size2d_t grid; font = wuss_get_font(wuss); if (font == NULL) @@ -168,12 +179,13 @@ result_t chars_create(wuss_t *wuss, wuss_task_set_autoclose(delegate, 1); task->delegate = delegate; + grid = chars_window_size(task, font); rc = wuss_window_create_placed(delegate, - chars_window_size(task, font), + grid, "Chars", wuss_WINDOW_NO_HSCROLL, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), - chars_window_size(task, font), + grid, SIZE2D(64, 64), &task->window); if (rc != result_OK) From def8aa89eb950ca4d9c447dba0e20ffd7383c6e5 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Fri, 11 Sep 2026 09:39:30 +0100 Subject: [PATCH 023/128] feat(framebuf): add 8bpp paletted (p8) screen support Adds pixelfmt_p8 across the framebuf/wuss/image-io stack, mirroring the existing p4 paletted path throughout: - span_p8 (copy/fill/blendconst), registered in the span registry - screen: blend_pixel, copy_bitmap (+ ordered dither), fill_pattern, and the RLE-blit destination path all gain p8 branches - bmfont: p8 glyph drawing (byte-per-pixel, no nibble packing) - bitmap_convert: p8 -> bgrx8888 - bitmap_load_png: a palette-type PNG is now kept as pixelfmt_p8 (PLTE and any tRNS copied into bm->palette) instead of being expanded to RGB/RGBA - bitmap_save_png: pixelfmt_p8 is written back out as a palette-type PNG - wuss SDL frontend: --depth 8 Fixes found along the way: - apps/wuss/main.c: the UI palette and the physical screen palette were the same fixed-size array; growing it for p8 leaked screen_copy_ninepatch its own gain (256 entries needed for a paletted screen) into wuss_create's npalette, corrupting the 16-colour UI palette lookups. Split into a 16-entry UI palette and a separately-sized screen palette. - bitmap_load_png: an OOM from the new p8 palette allocation inside bitmap_init was silently dropped; now checked and propagated. - libraries/wuss/test/tasks/image.c: screen_copy_bitmap/ninepatch assume a deep 32bpp source, an assumption bitmap_load_png's new p8-preserving behaviour breaks for palette-type PNG fixtures (ninepatch.png, jessica.png). Added load_png_deep() to convert a p8 load result back to bgrx8888 before it reaches the blitters. 26/26 DPTLibTest tests pass; wuss (build-sdl) builds clean. Co-Authored-By: Claude Sonnet 5 --- CMakeLists.txt | 2 + apps/wuss/frontend-sdl.c | 20 +- apps/wuss/main.c | 20 +- include/framebuf/bitmap.h | 10 + include/framebuf/span-p8.h | 10 + libraries/framebuf/bitmap/bitmap.c | 64 ++++++ libraries/framebuf/bitmap/load.c | 63 ++++-- libraries/framebuf/bitmap/save.c | 60 ++++++ libraries/framebuf/bitmap/test/rle-test.c | 115 ++++++++++ libraries/framebuf/bmfont/bmfont.c | 199 ++++++++++++++++++ .../framebuf/screen/screen-copy-bitmap-rle.c | 91 ++++++++ libraries/framebuf/screen/screen-draw.c | 97 +++++++++ .../framebuf/screen/screen-fill-pattern.c | 34 +++ libraries/framebuf/screen/test/screen-test.c | 168 +++++++++++++++ libraries/framebuf/span-registry/regdata.h | 2 + libraries/framebuf/span/p8.c | 97 +++++++++ libraries/wuss/test/tasks/image.c | 37 +++- 17 files changed, 1055 insertions(+), 34 deletions(-) create mode 100644 include/framebuf/span-p8.h create mode 100644 libraries/framebuf/span/p8.c diff --git a/CMakeLists.txt b/CMakeLists.txt index db553d62..93b41cb5 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -92,6 +92,7 @@ set(PUBLIC_HEADERS include/framebuf/span-p1.h include/framebuf/span-p2.h include/framebuf/span-p4.h + include/framebuf/span-p8.h include/framebuf/span-registry.h include/framebuf/span-rgba8888.h include/framebuf/span-rgbx8888.h @@ -286,6 +287,7 @@ set(FRAMEBUF_SOURCES libraries/framebuf/span/p1.c libraries/framebuf/span/p2.c libraries/framebuf/span/p4.c + libraries/framebuf/span/p8.c libraries/framebuf/span/rgbx8888.c libraries/framebuf/span/xbgr8888.c) diff --git a/apps/wuss/frontend-sdl.c b/apps/wuss/frontend-sdl.c index 77f1d193..15c8287b 100644 --- a/apps/wuss/frontend-sdl.c +++ b/apps/wuss/frontend-sdl.c @@ -23,11 +23,12 @@ #include "frontend.h" /* Screen pixel format for the demo, chosen at run time via --depth: 32 = - * pixelfmt_bgrx8888 (feeds SDL directly, no per-frame conversion); 4 (the - * default) = pixelfmt_p4 paletted (exercises screen_copy_rect's nibble-packed - * blit path instead); 2 = pixelfmt_p2; 1 = pixelfmt_p1 monochrome. Paletted - * depths are converted to bgrx8888 per frame via bitmap_convert. Stashed in - * struct wuss_frontend so present() can branch on it. */ + * pixelfmt_bgrx8888 (feeds SDL directly, no per-frame conversion); 8 = + * pixelfmt_p8; 4 (the default) = pixelfmt_p4 paletted (exercises + * screen_copy_rect's nibble-packed blit path instead); 2 = pixelfmt_p2; 1 = + * pixelfmt_p1 monochrome. Paletted depths are converted to bgrx8888 per + * frame via bitmap_convert. Stashed in struct wuss_frontend so present() can + * branch on it. */ /* Integer window zoom: the fixed Wuss screen is drawn at this many device * pixels per screen pixel. The initial value comes from -scale (0 => use the @@ -47,7 +48,7 @@ struct wuss_frontend int scr_width; int scr_height; int scale; /* device pixels per screen pixel; see WUSS_SDL_*_SCALE */ - int depth; /* framebuffer bits per pixel: 32 (bgrx8888), 4 (p4), 2 (p2) or 1 (p1) */ + int depth; /* framebuffer bits per pixel: 32 (bgrx8888), 8 (p8), 4 (p4), 2 (p2) or 1 (p1) */ void *pixels; /* the private framebuffer handed to the caller */ }; @@ -100,9 +101,10 @@ result_t wuss_frontend_open(int width, NOT_USED(palette); NOT_USED(npalette); - if (depth != 1 && depth != 2 && depth != 4 && depth != 32) + if (depth != 1 && depth != 2 && depth != 4 && depth != 8 && depth != 32) { - fprintf(stderr, "Error: unsupported depth %d (want 1, 2, 4 or 32)\n", depth); + fprintf(stderr, + "Error: unsupported depth %d (want 1, 2, 4, 8 or 32)\n", depth); return result_BAD_ARG; } @@ -111,6 +113,7 @@ result_t wuss_frontend_open(int width, scale = CLAMP(scale, WUSS_SDL_MIN_SCALE, WUSS_SDL_MAX_SCALE); stride = (depth == 32) ? width * 4 /* pixelfmt_bgrx8888: 4 bytes/pixel */ + : (depth == 8) ? width /* pixelfmt_p8: 1 byte/pixel */ : (depth == 4) ? width / 2 /* pixelfmt_p4: 2 pixels/byte */ : (depth == 2) ? (width + 3) / 4 /* pixelfmt_p2: 4 pixels/byte */ : (width + 7) / 8; /* pixelfmt_p1: 8 pixels/byte */ @@ -165,6 +168,7 @@ result_t wuss_frontend_open(int width, SDL_SetTextureScaleMode(fe->texture, SDL_SCALEMODE_NEAREST); *fmt = (depth == 32) ? pixelfmt_bgrx8888 + : (depth == 8) ? pixelfmt_p8 : (depth == 4) ? pixelfmt_p4 : (depth == 2) ? pixelfmt_p2 : pixelfmt_p1; diff --git a/apps/wuss/main.c b/apps/wuss/main.c index e16c88dc..95b765ad 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -260,7 +260,10 @@ static result_t run_wuss(const char *resources, pixelfmt_t fmt; bitmap_t bm; screen_t scr; - colour_t palette[16]; + colour_t palette[16]; /* the fixed-size UI palette */ + colour_t scr_palette[256]; /* palette[] padded out to whatever + * count the chosen depth's bitmap + * needs (p8 reads all 256) */ wuss_t *wuss; wuss_frontend_t *frontend; bool use_wimp16; @@ -313,8 +316,15 @@ static result_t run_wuss(const char *resources, if (rc != result_OK) goto Failure; - rc = bitmap_init(&bm, SIZE2D(scr_width, scr_height), fmt, rowbytes, palette, - pixels); + /* bitmap_set_palette reads every entry a paletted format's bitmap_init + * needs (up to 256 for p8); pad scr_palette with palette's 16 UI colours + * so closest_palette_entry always has exact matches to find, then black + * for the unused tail. */ + memset(scr_palette, 0, sizeof(scr_palette)); + memcpy(scr_palette, palette, sizeof(palette)); + + rc = bitmap_init(&bm, SIZE2D(scr_width, scr_height), fmt, rowbytes, + scr_palette, pixels); logf_info("wuss: bitmap_init -> rc=0x%X (%s)", rc, result_string(rc)); if (rc != result_OK) goto Failure; @@ -426,14 +436,14 @@ typedef struct wuss_options { const char *resources; /* -r/--resources: fixture root */ const char *palette_name; /* -p/--palette: startup *.hex leafname */ - int depth; /* -d/--depth: framebuffer bpp (1, 2, 4 or 32) */ + int depth; /* -d/--depth: framebuffer bpp (1, 2, 4, 8 or 32) */ int scale; /* -s/--scale: initial window zoom, 0 = default */ } wuss_options_t; static const char wuss_usage[] = "usage: wuss [-r|--resources DIR] [-p|--palette NAME] " - "[-d|--depth 1|2|4|32] [-s|--scale N]\n"; + "[-d|--depth 1|2|4|8|32] [-s|--scale N]\n"; #ifndef __riscos diff --git a/include/framebuf/bitmap.h b/include/framebuf/bitmap.h index a219b42a..4fad5cf4 100644 --- a/include/framebuf/bitmap.h +++ b/include/framebuf/bitmap.h @@ -111,6 +111,10 @@ result_t bitmap_fill_pattern(bitmap_t *bm, /** * Load a PNG image into the given bitmap. * + * A palette-type PNG is kept as \ref pixelfmt_p8 with its PLTE (and any + * tRNS) copied into `bm->palette`; RGB and greyscale PNGs are expanded to a + * 32bpp `rgbx8888` / `rgba8888` bitmap with no palette. + * * \param[in] bm Bitmap to load the image into. * \param[in] filename Filename of the image to load. * \return \ref result_OK on success, or appropriate result code otherwise. @@ -120,6 +124,12 @@ result_t bitmap_load_png(bitmap_t *bm, const char *filename); /** * Save the given bitmap as a PNG image. * + * Supported formats: `bgrx8888` (written as RGB), `bgra8888` (RGBA), and + * \ref pixelfmt_p8 (written as a palette-type PNG, its `bm->palette` + * becoming the PLTE, plus a tRNS chunk if any palette entry is non-opaque). + * Other formats return \ref result_NOT_SUPPORTED, as does a \ref pixelfmt_p8 + * bitmap with no palette. + * * \param[in] bm Bitmap to save. * \param[in] filename Filename to save the image to. * \return \ref result_OK on success, or appropriate result code otherwise. diff --git a/include/framebuf/span-p8.h b/include/framebuf/span-p8.h new file mode 100644 index 00000000..ffea4789 --- /dev/null +++ b/include/framebuf/span-p8.h @@ -0,0 +1,10 @@ +/* framebuf/span-p8.h -- P8 (8bpp paletted) format plot methods */ + +#ifndef SPAN_P8_H +#define SPAN_P8_H + +#include "framebuf/span.h" + +extern const span_t span_p8; + +#endif /* SPAN_P8_H */ diff --git a/libraries/framebuf/bitmap/bitmap.c b/libraries/framebuf/bitmap/bitmap.c index 04c07a3c..7aac8829 100644 --- a/libraries/framebuf/bitmap/bitmap.c +++ b/libraries/framebuf/bitmap/bitmap.c @@ -186,6 +186,59 @@ static result_t bmconv_p4_to_bgrx8888(const bitmap_t *src, bitmap_t **pdst) return rc; } +/* As bmconv_p4_to_bgrx8888 but for 8bpp paletted: one index per byte, so the + * inner loop is a plain byte read and table lookup. */ +static result_t bmconv_p8_to_bgrx8888(const bitmap_t *src, bitmap_t **pdst) +{ + result_t rc; + const pixelmap_t *pm; + const pixelfmt_bgrx8888_t *map; + bitmap_t *dst; + pixelfmt_bgrx8888_t *outpixels; + const unsigned char *inrow; + int x, y; + + assert(src); + assert(src->palette); + + pm = pixelmap_get(pixelfmt_p8, pixelfmt_bgrx8888, src->palette, 256); + if (pm == NULL) + return result_NOT_SUPPORTED; + map = (const pixelfmt_bgrx8888_t *) pm->entries; + + outpixels = malloc(src->size.w * sizeof(pixelfmt_bgrx8888_t) * src->size.h); + if (outpixels == NULL) + return result_OOM; + + dst = malloc(sizeof(*dst)); + if (dst == NULL) + { + free(outpixels); + return result_OOM; + } + + rc = bitmap_init(dst, + src->size, + pixelfmt_bgrx8888, + src->size.w * sizeof(pixelfmt_bgrx8888_t), + NULL, + outpixels); + if (rc) + return rc; + + inrow = src->base; + for (y = 0; y < src->size.h; y++) + { + for (x = 0; x < src->size.w; x++) + *outpixels++ = map[inrow[x]]; + inrow += src->rowbytes; + } + + *pdst = dst; + + return rc; +} + /* As bmconv_p4_to_bgrx8888 but for 1bpp paletted, packed MSB-first (bit 7 is * the leftmost pixel) to match the screen p1 plot helpers. */ static result_t bmconv_p1_to_bgrx8888(const bitmap_t *src, bitmap_t **pdst) @@ -336,6 +389,17 @@ result_t bitmap_convert(const bitmap_t *src, } break; + case pixelfmt_p8: + switch (newfmt) + { + case pixelfmt_bgrx8888: + return bmconv_p8_to_bgrx8888(src, dst); + + default: + return result_NOT_SUPPORTED; + } + break; + default: return result_NOT_SUPPORTED; } diff --git a/libraries/framebuf/bitmap/load.c b/libraries/framebuf/bitmap/load.c index 9bc22300..fe0992d6 100644 --- a/libraries/framebuf/bitmap/load.c +++ b/libraries/framebuf/bitmap/load.c @@ -34,6 +34,8 @@ result_t bitmap_load_png(bitmap_t *bm, const char *filename) png_bytep *row_pointers = NULL; pixelfmt_t bm_fmt; int bm_rowbytes; + colour_t plte[256]; + colour_t *bm_palette = NULL; png_uint_32 h; fp = fopen(filename, "rb"); @@ -77,8 +79,8 @@ result_t bitmap_load_png(bitmap_t *bm, const char *filename) NULL, NULL, NULL); /* paletted PNGs may be 1/2/4/8 bpp; png_set_packing() unpacks the - * sub-byte cases to one index per byte before the palette expansion - * below. every other colour type must already be 8-bit. */ + * sub-byte cases to one index per byte. every other colour type must + * already be 8-bit. */ if (pngbitdepth != 8 && pngcolourtype != PNG_COLOR_TYPE_PALETTE) { rc = result_INCOMPATIBLE; @@ -88,17 +90,41 @@ result_t bitmap_load_png(bitmap_t *bm, const char *filename) switch (pngcolourtype) { case PNG_COLOR_TYPE_PALETTE: - png_set_packing(png_ptr); - png_set_palette_to_rgb(png_ptr); - if (png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS)) + /* keep a native paletted PNG as pixelfmt_p8 (one index per byte), + * carrying its PLTE across as the bitmap's palette, rather than + * expanding to RGB */ { - png_set_tRNS_to_alpha(png_ptr); - bm_fmt = pixelfmt_rgba8888; - } - else - { - bm_fmt = pixelfmt_rgbx8888; - png_set_filler(png_ptr, 0xFF, PNG_FILLER_AFTER); + png_colorp png_palette; + int nplte; + png_bytep trns = NULL; + int ntrns = 0; + int i; + + png_set_packing(png_ptr); + + if (png_get_PLTE(png_ptr, info_ptr, &png_palette, &nplte) == 0) + { + rc = result_INCOMPATIBLE; + goto cleanup; + } + if (png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS)) + png_get_tRNS(png_ptr, info_ptr, &trns, &ntrns, NULL); + + for (i = 0; i < nplte; i++) + { + int a; + + a = (i < ntrns) ? trns[i] : 0xFF; + plte[i] = colour_rgba(png_palette[i].red, + png_palette[i].green, + png_palette[i].blue, + a); + } + for (; i < 256; i++) + plte[i] = colour_rgba(0, 0, 0, 0xFF); + + bm_fmt = pixelfmt_p8; + bm_palette = plte; } break; case PNG_COLOR_TYPE_RGB: @@ -128,15 +154,16 @@ result_t bitmap_load_png(bitmap_t *bm, const char *filename) png_read_image(png_ptr, row_pointers); - bitmap_init(bm, SIZE2D(pngwidth, pngheight), - bm_fmt, - bm_rowbytes, - NULL, /* no palette */ - pixels); + rc = bitmap_init(bm, SIZE2D(pngwidth, pngheight), + bm_fmt, + bm_rowbytes, + bm_palette, /* p8: PLTE copied in; NULL for deep formats */ + pixels); + if (rc != result_OK) + goto cleanup; pixels = NULL; /* belongs to bitmap now */ - rc = result_OK; goto cleanup; oom: diff --git a/libraries/framebuf/bitmap/save.c b/libraries/framebuf/bitmap/save.c index a8ce58a1..c5eea7be 100755 --- a/libraries/framebuf/bitmap/save.c +++ b/libraries/framebuf/bitmap/save.c @@ -16,13 +16,17 @@ result_t bitmap_save_png(const bitmap_t *bm, const char *filename) result_t rc; volatile pixelfmt_t fmt; volatile size_t bytespp; + volatile int paletted; FILE *fp; png_structp png_ptr = NULL; png_infop volatile info_ptr = NULL; png_bytep volatile outrow = NULL; + png_colorp volatile png_plte = NULL; pixelfmt_xxxa8888_t *inrow; // more like xxxx8888 int x,y; + paletted = 0; + switch (bm->format) { case pixelfmt_bgrx8888: @@ -33,6 +37,13 @@ result_t bitmap_save_png(const bitmap_t *bm, const char *filename) fmt = PNG_COLOR_TYPE_RGBA; bytespp = 4; break; + case pixelfmt_p8: + if (bm->palette == NULL) + return result_NOT_SUPPORTED; + fmt = PNG_COLOR_TYPE_PALETTE; + bytespp = 1; + paletted = 1; + break; default: return result_NOT_SUPPORTED; } @@ -74,6 +85,38 @@ result_t bitmap_save_png(const bitmap_t *bm, const char *filename) PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); + if (paletted) + { + png_byte trns[256]; + int have_trns; + int i; + + png_plte = png_malloc(png_ptr, 256 * sizeof(*png_plte)); + if (png_plte == NULL) + { + rc = result_OOM; + goto cleanup; + } + + have_trns = 0; + for (i = 0; i < 256; i++) + { + pixelfmt_rgba8888_t px = bm->palette[i].primary; + + png_plte[i].red = PIXELFMT_Rxxx8888(px); + png_plte[i].green = PIXELFMT_xGxx8888(px); + png_plte[i].blue = PIXELFMT_xxBx8888(px); + + trns[i] = PIXELFMT_xxxA8888(px); + if (trns[i] != 0xFF) + have_trns = 1; + } + + png_set_PLTE(png_ptr, info_ptr, png_plte, 256); + if (have_trns) + png_set_tRNS(png_ptr, info_ptr, trns, 256, NULL); + } + png_write_info(png_ptr, info_ptr); // consider: @@ -90,6 +133,21 @@ result_t bitmap_save_png(const bitmap_t *bm, const char *filename) inrow = bm->base; + if (paletted) + { + const unsigned char *srcrow = bm->base; + + for (y = 0; y < bm->size.h; y++) + { + png_write_row(png_ptr, srcrow); + srcrow += bm->rowbytes; + } + + png_write_end(png_ptr, NULL); + rc = result_OK; + goto cleanup; + } + for (y = 0; y < bm->size.h; y++) { png_bytep pout = &outrow[0]; @@ -131,6 +189,8 @@ result_t bitmap_save_png(const bitmap_t *bm, const char *filename) cleanup: fclose(fp); + if (png_plte != NULL) + png_free(png_ptr, png_plte); png_free_data(png_ptr, info_ptr, PNG_FREE_ALL, -1); png_destroy_write_struct(&png_ptr, NULL); free(outrow); diff --git a/libraries/framebuf/bitmap/test/rle-test.c b/libraries/framebuf/bitmap/test/rle-test.c index a70e95a4..89c69ab8 100644 --- a/libraries/framebuf/bitmap/test/rle-test.c +++ b/libraries/framebuf/bitmap/test/rle-test.c @@ -5,6 +5,7 @@ #include #include #include +#include #include "base/result.h" #include "io/path.h" @@ -370,12 +371,126 @@ static int test_y8(void) /* ----------------------------------------------------------------------- */ +/* Synthesise a p8 bitmap with a known palette, save it as a palette-type PNG, + * reload it and check the format, palette (RGB + tRNS alpha) and pixel indices + * survive the round trip. Then bitmap_convert it to bgrx8888 and spot-check a + * pixel against its palette entry. */ +static int test_p8_png(void) +{ +#ifdef DPTLIB_IMAGES_READ_ONLY + return 1; /* ponytail: save disabled at build time, nothing to round trip */ +#else +#define P8W 24 +#define P8H 8 + bitmap_t bm; + bitmap_t rt; + bitmap_t *deep = NULL; + colour_t pal[256]; + colour_t rtpal[256]; + unsigned char pix[P8W * P8H]; + char fn[] = "/tmp/dptlib-p8-XXXXXX.png"; + int fd; + result_t rc; + int i; + int ok = 1; + + /* palette: greyscale ramp, plus a few non-opaque entries to exercise tRNS */ + for (i = 0; i < 256; i++) + pal[i] = colour_rgba((unsigned char) i, (unsigned char) i, + (unsigned char) i, 0xFF); + pal[0] = colour_rgba(0, 0, 0, 0x00); + pal[1] = colour_rgba(255, 0, 0, 0x80); + pal[200] = colour_rgba(10, 20, 30, 0xFF); + + for (i = 0; i < P8W * P8H; i++) + pix[i] = (unsigned char) ((i * 7 + i / P8W) & 0xFF); + + bitmap_init(&bm, SIZE2D(P8W, P8H), pixelfmt_p8, P8W, pal, pix); + + fd = mkstemps(fn, 4); /* keep the ".png" suffix */ + if (fd < 0) { fprintf(stderr, "p8 png: mkstemps failed\n"); return 0; } + close(fd); + + rc = bitmap_save_png(&bm, fn); + if (rc) { fprintf(stderr, "p8 png: save rc=&%x\n", rc); return 0; } + + rc = bitmap_load_png(&rt, fn); + remove(fn); + if (rc) { fprintf(stderr, "p8 png: load rc=&%x\n", rc); return 0; } + + if (rt.format != pixelfmt_p8) + { + fprintf(stderr, "p8 png: reloaded format &%x not p8\n", rt.format); + ok = 0; goto done; + } + if (rt.size.w != P8W || rt.size.h != P8H) + { + fprintf(stderr, "p8 png: reloaded size wrong\n"); + ok = 0; goto done; + } + if (rt.palette == NULL) + { + fprintf(stderr, "p8 png: reloaded bitmap has no palette\n"); + ok = 0; goto done; + } + + memcpy(rtpal, rt.palette, sizeof rtpal); + for (i = 0; i < 256; i++) + { + if (rtpal[i].primary != pal[i].primary) + { + fprintf(stderr, "p8 png: palette entry %d &%08x != &%08x\n", + i, rtpal[i].primary, pal[i].primary); + ok = 0; goto done; + } + } + + for (i = 0; i < P8W * P8H; i++) + { + if (((const unsigned char *) rt.base)[i] != pix[i]) + { + fprintf(stderr, "p8 png: pixel %d %u != %u\n", + i, ((const unsigned char *) rt.base)[i], pix[i]); + ok = 0; goto done; + } + } + + rc = bitmap_convert(&rt, pixelfmt_bgrx8888, &deep); + if (rc) { fprintf(stderr, "p8 png: convert rc=&%x\n", rc); ok = 0; goto done; } + + { + unsigned int idx = pix[0]; + pixelfmt_bgrx8888_t got = ((const pixelfmt_bgrx8888_t *) deep->base)[0]; + pixelfmt_rgba8888_t want = pal[idx].primary; + + if (PIXELFMT_xxRx8888(got) != PIXELFMT_Rxxx8888(want) + || PIXELFMT_xGxx8888(got) != PIXELFMT_xGxx8888(want) + || PIXELFMT_Bxxx8888(got) != PIXELFMT_xxBx8888(want)) + { + fprintf(stderr, "p8 png: converted pixel 0 &%08x != palette &%08x\n", + got, want); + ok = 0; + } + } + +done: + if (deep != NULL) { free(deep->base); free(deep); } + free(rt.base); + return ok; +#undef P8W +#undef P8H +#endif +} + +/* ----------------------------------------------------------------------- */ + result_t bitmap_rle_test(const char *resources) { int ok = 1; ok &= test_synthetic(); ok &= test_y8(); + ok &= test_p8_png(); ok &= test_blit(resources); return ok ? result_TEST_PASSED : result_TEST_FAILED; diff --git a/libraries/framebuf/bmfont/bmfont.c b/libraries/framebuf/bmfont/bmfont.c index aae597df..c36d75db 100644 --- a/libraries/framebuf/bmfont/bmfont.c +++ b/libraries/framebuf/bmfont/bmfont.c @@ -1497,6 +1497,196 @@ static void bmfont_drawchar_any8888_2w_t(void *vscreen, } } +/* Draw a character, a maximum of one byte wide, to a p8 screen using an + * opaque background. */ +static void bmfont_drawchar_p8_1w_o(void *vscreen, + const void *vglyph, + int top_skip, + int right_skip, + int shift, + int rowbytes, + int charwidth, + int charheight, + pixelfmt_any_t fg, + pixelfmt_any_t bg) +{ + unsigned char *scr = vscreen; + const unsigned char *gly = vglyph; + int stride; + + NOT_USED(shift); + + gly += top_skip; + stride = rowbytes - charwidth; + + while (charheight--) + { + unsigned int row = *gly++; /* 1 byte wide font data */ + row >>= right_skip; /* compensate when right hand clipping */ + + switch (charwidth) /* jump table test */ + { + case 8: *scr++ = (row & (1u << 7)) ? fg : bg; /* fallthrough! */ + case 7: *scr++ = (row & (1u << 6)) ? fg : bg; + case 6: *scr++ = (row & (1u << 5)) ? fg : bg; + case 5: *scr++ = (row & (1u << 4)) ? fg : bg; + case 4: *scr++ = (row & (1u << 3)) ? fg : bg; + case 3: *scr++ = (row & (1u << 2)) ? fg : bg; + case 2: *scr++ = (row & (1u << 1)) ? fg : bg; + case 1: *scr++ = (row & (1u << 0)) ? fg : bg; + } + + scr += stride; + } +} + +/* Draw a character, a maximum of one byte wide, to a p8 screen using a + * transparent background. */ +static void bmfont_drawchar_p8_1w_t(void *vscreen, + const void *vglyph, + int top_skip, + int right_skip, + int shift, + int rowbytes, + int charwidth, + int charheight, + pixelfmt_any_t fg, + pixelfmt_any_t bg) +{ + unsigned char *scr = vscreen; + const unsigned char *gly = vglyph; + int stride; + + NOT_USED(shift); + NOT_USED(bg); + + gly += top_skip; + stride = rowbytes - charwidth; + + while (charheight--) + { + unsigned int row = *gly++; /* 1 byte wide font data */ + row >>= right_skip; /* compensate when right hand clipping */ + + switch (charwidth) /* jump table test */ + { + case 8: if (row & (1u << 7)) *scr = fg; scr++; /* fallthrough! */ + case 7: if (row & (1u << 6)) *scr = fg; scr++; + case 6: if (row & (1u << 5)) *scr = fg; scr++; + case 5: if (row & (1u << 4)) *scr = fg; scr++; + case 4: if (row & (1u << 3)) *scr = fg; scr++; + case 3: if (row & (1u << 2)) *scr = fg; scr++; + case 2: if (row & (1u << 1)) *scr = fg; scr++; + case 1: if (row & (1u << 0)) *scr = fg; scr++; + } + + scr += stride; + } +} + +/* Draw a character, a maximum of two bytes wide, to a p8 screen using an + * opaque background. */ +static void bmfont_drawchar_p8_2w_o(void *vscreen, + const void *vglyph, + int top_skip, + int right_skip, + int shift, + int rowbytes, + int charwidth, + int charheight, + pixelfmt_any_t fg, + pixelfmt_any_t bg) +{ + unsigned char *scr = vscreen; + const unsigned short *gly = vglyph; + int stride; + + NOT_USED(shift); + + gly += top_skip; + stride = rowbytes - charwidth; + + while (charheight--) + { + unsigned int row = *gly++; /* 2 bytes wide font data */ + row >>= right_skip; /* compensate when right hand clipping */ + + switch (charwidth) /* jump table test */ + { + case 16: *scr++ = (row & (1u << 15)) ? fg : bg; /* fallthrough! */ + case 15: *scr++ = (row & (1u << 14)) ? fg : bg; + case 14: *scr++ = (row & (1u << 13)) ? fg : bg; + case 13: *scr++ = (row & (1u << 12)) ? fg : bg; + case 12: *scr++ = (row & (1u << 11)) ? fg : bg; + case 11: *scr++ = (row & (1u << 10)) ? fg : bg; + case 10: *scr++ = (row & (1u << 9)) ? fg : bg; + case 9: *scr++ = (row & (1u << 8)) ? fg : bg; + case 8: *scr++ = (row & (1u << 7)) ? fg : bg; + case 7: *scr++ = (row & (1u << 6)) ? fg : bg; + case 6: *scr++ = (row & (1u << 5)) ? fg : bg; + case 5: *scr++ = (row & (1u << 4)) ? fg : bg; + case 4: *scr++ = (row & (1u << 3)) ? fg : bg; + case 3: *scr++ = (row & (1u << 2)) ? fg : bg; + case 2: *scr++ = (row & (1u << 1)) ? fg : bg; + case 1: *scr++ = (row & (1u << 0)) ? fg : bg; + } + + scr += stride; + } +} + +/* Draw a character, a maximum of two bytes wide, to a p8 screen using a + * transparent background. */ +static void bmfont_drawchar_p8_2w_t(void *vscreen, + const void *vglyph, + int top_skip, + int right_skip, + int shift, + int rowbytes, + int charwidth, + int charheight, + pixelfmt_any_t fg, + pixelfmt_any_t bg) +{ + unsigned char *scr = vscreen; + const unsigned short *gly = vglyph; + int stride; + + NOT_USED(shift); + NOT_USED(bg); + + gly += top_skip; + stride = rowbytes - charwidth; + + while (charheight--) + { + unsigned int row = *gly++; /* 2 bytes wide font data */ + row >>= right_skip; /* compensate when right hand clipping */ + + switch (charwidth) /* jump table test */ + { + case 16: if (row & (1u << 15)) *scr = fg; scr++; /* fallthrough! */ + case 15: if (row & (1u << 14)) *scr = fg; scr++; + case 14: if (row & (1u << 13)) *scr = fg; scr++; + case 13: if (row & (1u << 12)) *scr = fg; scr++; + case 12: if (row & (1u << 11)) *scr = fg; scr++; + case 11: if (row & (1u << 10)) *scr = fg; scr++; + case 10: if (row & (1u << 9)) *scr = fg; scr++; + case 9: if (row & (1u << 8)) *scr = fg; scr++; + case 8: if (row & (1u << 7)) *scr = fg; scr++; + case 7: if (row & (1u << 6)) *scr = fg; scr++; + case 6: if (row & (1u << 5)) *scr = fg; scr++; + case 5: if (row & (1u << 4)) *scr = fg; scr++; + case 4: if (row & (1u << 3)) *scr = fg; scr++; + case 3: if (row & (1u << 2)) *scr = fg; scr++; + case 2: if (row & (1u << 1)) *scr = fg; scr++; + case 1: if (row & (1u << 0)) *scr = fg; scr++; + } + + scr += stride; + } +} + result_t bmfont_draw(bmfont_t *bmfont, screen_t *scr, const char *text, @@ -1542,6 +1732,15 @@ result_t bmfont_draw(bmfont_t *bmfont, } break; + case pixelfmt_p8: + switch (bmfont->glyphrowbytes) + { + case 1: drawfn = (bgalpha < 255) ? bmfont_drawchar_p8_1w_t : bmfont_drawchar_p8_1w_o; break; + case 2: drawfn = (bgalpha < 255) ? bmfont_drawchar_p8_2w_t : bmfont_drawchar_p8_2w_o; break; + default: assert(0); return result_NOT_SUPPORTED; + } + break; + case pixelfmt_bgra8888: case pixelfmt_bgrx8888: switch (bmfont->glyphrowbytes) diff --git a/libraries/framebuf/screen/screen-copy-bitmap-rle.c b/libraries/framebuf/screen/screen-copy-bitmap-rle.c index 3f91735a..41620e75 100644 --- a/libraries/framebuf/screen/screen-copy-bitmap-rle.c +++ b/libraries/framebuf/screen/screen-copy-bitmap-rle.c @@ -141,6 +141,95 @@ static result_t screen_copy_bitmap_rle_p4(screen_t *scr, return result_OK; } +/* As screen_copy_bitmap_rle_p4 but for an 8bpp paletted screen: the deep + * -> index table is a plain byte per entry and the screen store is a plain + * byte, so there is no sub-byte packing. */ +static result_t screen_copy_bitmap_rle_p8(screen_t *scr, + int x, + int y, + const bitmap_t *src, + const box_t *draw_box) +{ + pixelfmt_t base; + int log2bpp; + const uint8_t *blob; + const uint8_t *p; + const uint8_t *end; + uint8_t *dstbase; + pixelfmt_rgba8888_t *scratch; + const pixelmap_t *pm; + int firstrow; + int skip, plot; + int rows; + int i; + + base = pixelfmt_base(src->format); + log2bpp = pixelfmt_log2bpp(base); + + if (log2bpp != 5) + return result_NOT_SUPPORTED; /* v1: 32bpp source only */ + + blob = src->base; + p = blob + bitmap__RLE_HEADER_SIZE; + end = p + bitmap__rle_get32(blob + 4); + + firstrow = draw_box->y0 - y; + for (i = 0; i < firstrow; i++) + p = bitmap__rle_skip_row(p, end, log2bpp); + + skip = draw_box->x0 - x; + plot = draw_box->x1 - draw_box->x0; + rows = draw_box->y1 - draw_box->y0; + + pm = pixelmap_get(pixelfmt_rgba8888, scr->format, scr->palette, 256); + if (pm == NULL) + return result_NOT_SUPPORTED; + + scratch = malloc((size_t) plot * sizeof(*scratch)); + if (scratch == NULL) + return result_OOM; + + dstbase = scr->base; + for (i = 0; i < rows; i++) + { + uint8_t *rowp; + int xx; + + for (xx = 0; xx < plot; xx++) + scratch[xx] = 0; /* zero_skip below only fills covered skip runs */ + + p = bitmap__rle_decode_row(p, end, log2bpp, scratch, skip, plot, 1); + + rowp = dstbase + (size_t) (draw_box->y0 + i) * scr->rowbytes; + + for (xx = 0; xx < plot; xx++) + { + colour_t c; + unsigned int r, g, b, idx; + int dstx; + + c.primary = scratch[xx]; + if (colour_get_alpha(&c) == 0) + continue; + + dstx = draw_box->x0 + xx; + + r = (c.primary >> pm->rshift) & 0xFF; + g = (c.primary >> pm->gshift) & 0xFF; + b = (c.primary >> pm->bshift) & 0xFF; + idx = ((r >> (8 - pm->rbits)) << (pm->gbits + pm->bbits)) + | ((g >> (8 - pm->gbits)) << pm->bbits) + | ( b >> (8 - pm->bbits)); + + rowp[dstx] = pm->entries[idx]; + } + } + + free(scratch); + + return result_OK; +} + result_t screen_copy_bitmap_rle(screen_t *scr, int x, int y, @@ -165,6 +254,8 @@ result_t screen_copy_bitmap_rle(screen_t *scr, if (pixelfmt_log2bpp(scr->format) == 2) return screen_copy_bitmap_rle_p4(scr, x, y, src, draw_box); + if (pixelfmt_log2bpp(scr->format) == 3) + return screen_copy_bitmap_rle_p8(scr, x, y, src, draw_box); base = pixelfmt_base(src->format); diff --git a/libraries/framebuf/screen/screen-draw.c b/libraries/framebuf/screen/screen-draw.c index 2895c7b6..0c3361e4 100644 --- a/libraries/framebuf/screen/screen-draw.c +++ b/libraries/framebuf/screen/screen-draw.c @@ -194,6 +194,23 @@ static void screen_blend_pixel_p4(screen_t *scr, *scrp = (unsigned char) ((*scrp & ~(0xF << shift)) | ((out & 0xF) << shift)); } +static void screen_blend_pixel_p8(screen_t *scr, + int x, + int y, + colour_t colour, + int alpha) +{ + unsigned char *scrp; + unsigned char idx, out; + + scrp = (unsigned char *) scr->base + y * scr->rowbytes + x; + idx = *scrp; + + scr->span->blendconst(&out, &idx, &colour, 1, alpha, scr->palette); + + *scrp = out; +} + static void screen_blend_pixel_32(screen_t *scr, int x, int y, @@ -230,6 +247,7 @@ static void screen_blend_pixel(screen_t *scr, case 0: screen_blend_pixel_p1(scr, x, y, colour, alpha); break; case 1: screen_blend_pixel_p2(scr, x, y, colour, alpha); break; case 2: screen_blend_pixel_p4(scr, x, y, colour, alpha); break; + case 3: screen_blend_pixel_p8(scr, x, y, colour, alpha); break; case 5: screen_blend_pixel_32(scr, x, y, colour, alpha); break; default: @@ -430,6 +448,84 @@ static result_t screen_copy_bitmap_p4(screen_t *scr, return result_OK; } +/* As screen_copy_bitmap_p4 but for the 8bpp screen: alpha-tested transfer, + * optional ordered dither, nearest palette match. p8 is a plain byte per + * pixel, so both the deep->index table lookup and the screen store are + * straight byte accesses with no sub-byte packing. Returns + * result_NOT_SUPPORTED if there is no deep->paletted conversion table for + * the screen's format. */ +static result_t screen_copy_bitmap_p8(screen_t *scr, + int x, + int y, + const bitmap_t *src, + const box_t *draw_box, + int has_alpha, + int dither) +{ + const unsigned char *srcrow; + unsigned char *dstbase; + const pixelmap_t *pm; + int bias[64]; + int do_dither; + int clipped_width, clipped_height; + int yy; + + clipped_width = draw_box->x1 - draw_box->x0; + clipped_height = draw_box->y1 - draw_box->y0; + do_dither = dither_bias_build(bias, + pixelfmt_paletted_nentries(scr->format), + dither); + + srcrow = (const unsigned char *) src->base + (draw_box->y0 - y) * src->rowbytes; + dstbase = scr->base; + + pm = pixelmap_get(pixelfmt_rgba8888, scr->format, scr->palette, 256); + if (pm == NULL) + return result_NOT_SUPPORTED; + + for (yy = 0; yy < clipped_height; yy++) + { + const pixelfmt_rgba8888_t *srcpx; + unsigned char *rowp; + int xx; + + srcpx = (const pixelfmt_rgba8888_t *) srcrow + (draw_box->x0 - x); + rowp = dstbase + (draw_box->y0 + yy) * scr->rowbytes; + + for (xx = 0; xx < clipped_width; xx++) + { + colour_t c; + unsigned int r, g, b, idx; + int dstx; + + c.primary = srcpx[xx]; + if (has_alpha && colour_get_alpha(&c) == 0) + continue; /* fully transparent: leave background alone */ + + dstx = draw_box->x0 + xx; + + r = (c.primary >> pm->rshift) & 0xFF; + g = (c.primary >> pm->gshift) & 0xFF; + b = (c.primary >> pm->bshift) & 0xFF; + if (do_dither) + { + r = dither_channel(r, bias, dstx - x, draw_box->y0 + yy - y); + g = dither_channel(g, bias, dstx - x, draw_box->y0 + yy - y); + b = dither_channel(b, bias, dstx - x, draw_box->y0 + yy - y); + } + idx = ((r >> (8 - pm->rbits)) << (pm->gbits + pm->bbits)) + | ((g >> (8 - pm->gbits)) << pm->bbits) + | ( b >> (8 - pm->bbits)); + + rowp[dstx] = pm->entries[idx]; + } + + srcrow += src->rowbytes; + } + + return result_OK; +} + /* Blit "src" onto the 1bpp screen, its top-left at (x, y), clipped to * "draw_box". Like screen_copy_bitmap_p4 this is an alpha-tested transfer * (skip fully transparent, else nearest of the two palette entries) rather @@ -698,6 +794,7 @@ static result_t screen_copy_bitmap_i(screen_t *scr, case 0: return screen_copy_bitmap_p1(scr, x, y, src, &draw_box, has_alpha, dither); case 1: return screen_copy_bitmap_p2(scr, x, y, src, &draw_box, has_alpha, dither); case 2: return screen_copy_bitmap_p4(scr, x, y, src, &draw_box, has_alpha, dither); + case 3: return screen_copy_bitmap_p8(scr, x, y, src, &draw_box, has_alpha, dither); case 5: screen_copy_bitmap_32(scr, x, y, src, &draw_box, has_alpha); break; default: diff --git a/libraries/framebuf/screen/screen-fill-pattern.c b/libraries/framebuf/screen/screen-fill-pattern.c index 16a4e690..0cd1528a 100644 --- a/libraries/framebuf/screen/screen-fill-pattern.c +++ b/libraries/framebuf/screen/screen-fill-pattern.c @@ -134,6 +134,36 @@ static void screen_fill_pattern_p4(screen_t *scr, } } +static void screen_fill_pattern_p8(screen_t *scr, + const pattern_t *pattern, + const box_t *draw_box, + int stencil, + pattern_runs_t runs) +{ + unsigned char *rowp; + int row, col, x, y; + + rowp = (unsigned char *) scr->base + draw_box->y0 * scr->rowbytes; + for (y = draw_box->y0; y < draw_box->y1; y++) + { + const pixelfmt_any_t *run; + uint8_t bits; + + row = (y - pattern->origin.y) & 7; + run = runs[row]; + bits = pattern->bits[row]; + col = 0; + for (x = draw_box->x0; x < draw_box->x1; x++) + { + if (!stencil || + (bits & (0x80u >> ((x - pattern->origin.x) & 7)))) + rowp[x] = (unsigned char) (run[col] & 0xFF); + col = (col + 1) & 7; + } + rowp += scr->rowbytes; + } +} + static void screen_fill_pattern_32(screen_t *scr, const pattern_t *pattern, const box_t *draw_box, @@ -240,6 +270,10 @@ void screen_fill_pattern(screen_t *scr, screen_fill_pattern_p4(scr, pattern, &draw_box, stencil, runs); break; + case 3: + screen_fill_pattern_p8(scr, pattern, &draw_box, stencil, runs); + break; + case 5: screen_fill_pattern_32(scr, pattern, &draw_box, stencil, fg_fmt, runs); break; diff --git a/libraries/framebuf/screen/test/screen-test.c b/libraries/framebuf/screen/test/screen-test.c index 7ad0e29d..6d90fbd2 100644 --- a/libraries/framebuf/screen/test/screen-test.c +++ b/libraries/framebuf/screen/test/screen-test.c @@ -958,6 +958,173 @@ static result_t test_copy_bitmap_p2(void) return result_TEST_PASSED; } +/* Blit an rgba8888 source onto an 8bpp paletted screen: each pixel quantises + * to the nearest palette entry and is stored as one plain byte. Also covers + * set_pixel / fill_hline / fill_pattern / blend_pixel on the p8 path. */ +static result_t test_copy_bitmap_p8(void) +{ +#define P8_ROWBYTES (WIDTH) + static unsigned char p8pixels[P8_ROWBYTES * HEIGHT]; + static pixelfmt_rgba8888_t srcbuf[16 * 4]; + + screen_t scr; + bitmap_t src; + colour_t pal[256]; + int x, y; + + /* a grey ramp: entry i is (i, i, i), so nearest-match is predictable */ + for (x = 0; x < 256; x++) + pal[x] = colour_rgb(x, x, x); + + /* 16x4 source: left half near-black (-> index ~16), right half near-white + * (-> index ~240) */ + for (y = 0; y < 4; y++) + for (x = 0; x < 16; x++) + srcbuf[y * 16 + x] = (x < 8 ? colour_rgb(0x10, 0x10, 0x10) + : colour_rgb(0xF0, 0xF0, 0xF0)).primary; + + bitmap_init(&src, SIZE2D(16, 4), pixelfmt_rgba8888, + 16 * (int) sizeof(srcbuf[0]), NULL, srcbuf); + + memset(p8pixels, 0, sizeof(p8pixels)); + screen_init(&scr, SIZE2D(WIDTH, HEIGHT), pixelfmt_p8, P8_ROWBYTES, pal, + p8pixels); + + if (screen_copy_bitmap(&scr, 0, 0, &src) != result_OK) + { + printf("screen: copy_bitmap to p8 screen failed\n"); + return result_TEST_FAILED; + } + + /* row 0: cols 0..7 quantise near 0x10, cols 8..15 near 0xF0. The pixelmap + * quantises via 5:6:5 before the nearest-ramp lookup, so allow a small + * slop rather than demanding the exact byte. */ + for (x = 0; x < 8; x++) + { + if (p8pixels[x] > 0x20) + { + printf("screen: p8 blit dark col x=%d too bright (0x%02X)\n", + x, p8pixels[x]); + return result_TEST_FAILED; + } + if (p8pixels[x + 8] < 0xE0) + { + printf("screen: p8 blit light col x=%d too dark (0x%02X)\n", + x + 8, p8pixels[x + 8]); + return result_TEST_FAILED; + } + } + + /* an untouched row past the source stays clear */ + for (x = 0; x < 16; x++) + if (p8pixels[4 * P8_ROWBYTES + x] != 0x00) + { + printf("screen: p8 blit spilled past the source\n"); + return result_TEST_FAILED; + } + + /* screen_set_pixel: entry 200 at x=5 lands as a plain byte */ + memset(p8pixels, 0, sizeof(p8pixels)); + screen_set_pixel(&scr, 5, 0, pal[200]); + if (p8pixels[5] != 200) + { + printf("screen: p8 set_pixel wrong (0x%02X)\n", p8pixels[5]); + return result_TEST_FAILED; + } + + /* screen_fill_hline: 6 pixels of entry 128 from x=2 on row 1 */ + memset(p8pixels, 0, sizeof(p8pixels)); + screen_fill_hline(&scr, 2, 1, 6, pal[128]); + for (x = 2; x < 8; x++) + if (p8pixels[P8_ROWBYTES + x] != 128) + { + printf("screen: p8 fill_hline wrong at x=%d (0x%02X)\n", + x, p8pixels[P8_ROWBYTES + x]); + return result_TEST_FAILED; + } + if (p8pixels[P8_ROWBYTES + 1] != 0 || p8pixels[P8_ROWBYTES + 8] != 0) + { + printf("screen: p8 fill_hline smeared past its run\n"); + return result_TEST_FAILED; + } + + /* screen_fill_pattern: solid all-bits pattern in fg = entry 77 */ + memset(p8pixels, 0, sizeof(p8pixels)); + { + pattern_t pat; + box_t b; + + memset(&pat, 0, sizeof(pat)); + memset(pat.bits, 0xFF, sizeof(pat.bits)); + pat.fg = pal[77]; + pat.bg = pal[0]; + b.x0 = 0; b.y0 = 2; b.x1 = 8; b.y1 = 3; + screen_fill_pattern(&scr, &b, &pat); + for (x = 0; x < 8; x++) + if (p8pixels[2 * P8_ROWBYTES + x] != 77) + { + printf("screen: p8 fill_pattern wrong at x=%d (0x%02X)\n", + x, p8pixels[2 * P8_ROWBYTES + x]); + return result_TEST_FAILED; + } + } + + /* screen_copy_rect on p8 (the whole-byte bytes path, bpp=1): lay down a + * distinct-per-column run, slide it right by 5, check it moved intact and + * did not smear past its destination */ + memset(p8pixels, 0, sizeof(p8pixels)); + for (x = 0; x < 8; x++) + screen_set_pixel(&scr, x, 0, pal[1 + x]); + { + box_t s = { 0, 0, 8, 1 }; + box_t got; + + if (screen_copy_rect(&scr, &s, POINT(5, 0), &got) != result_OK) + { + printf("screen: copy_rect declined a p8 screen\n"); + return result_TEST_FAILED; + } + for (x = 0; x < 8; x++) + if (p8pixels[x + 5] != 1 + x) + { + printf("screen: copy_rect p8 wrong pixel at x=%d (got %d want %d)\n", + x + 5, p8pixels[x + 5], 1 + x); + return result_TEST_FAILED; + } + if (p8pixels[13] != 0) + { + printf("screen: copy_rect p8 smeared past its destination\n"); + return result_TEST_FAILED; + } + } + + /* screen_blend_pixel via an anti-aliased diagonal: the p8 blend path must + * write *some* mid-ramp index between the black background and white line, + * not leave the pixel at 0 and not slam it to 255 */ + memset(p8pixels, 0, sizeof(p8pixels)); + screen_draw_line_wu_float(&scr, 0.0f, 0.0f, 8.0f, 3.0f, + colour_rgb(0xFF, 0xFF, 0xFF)); + { + int seen_partial = 0; + + for (y = 0; y < 4; y++) + for (x = 0; x < 9; x++) + { + unsigned char v = p8pixels[y * P8_ROWBYTES + x]; + if (v > 0 && v < 255) + seen_partial = 1; + } + if (!seen_partial) + { + printf("screen: p8 blend_pixel produced no partial-coverage pixel\n"); + return result_TEST_FAILED; + } + } + + return result_TEST_PASSED; +#undef P8_ROWBYTES +} + /* Unpack the p2 pixel at column "px" of a row's byte buffer (bits 7..6 are * column 0, MSB-first, four to the byte). */ static int p2_pixel_at(const unsigned char *rowbuf, int px) @@ -1191,6 +1358,7 @@ result_t screen_test(const char *resources) test_fill_circle, test_copy_bitmap_p1, test_copy_bitmap_p2, + test_copy_bitmap_p8, test_copy_bitmap_dithered, test_copy_rect_packed }; diff --git a/libraries/framebuf/span-registry/regdata.h b/libraries/framebuf/span-registry/regdata.h index 546e6aee..7d91d961 100644 --- a/libraries/framebuf/span-registry/regdata.h +++ b/libraries/framebuf/span-registry/regdata.h @@ -9,6 +9,7 @@ #include "framebuf/span-p1.h" #include "framebuf/span-p2.h" #include "framebuf/span-p4.h" +#include "framebuf/span-p8.h" #include "framebuf/span-rgbx8888.h" #include "framebuf/span-xbgr8888.h" @@ -20,6 +21,7 @@ static const span_t *spans[] = &span_p1, &span_p2, &span_p4, + &span_p8, &span_rgbx8888, &span_xbgr8888, }; diff --git a/libraries/framebuf/span/p8.c b/libraries/framebuf/span/p8.c new file mode 100644 index 00000000..ee149134 --- /dev/null +++ b/libraries/framebuf/span/p8.c @@ -0,0 +1,97 @@ +/* framebuf/span/p8.c -- P8 (8bpp paletted) format plot methods */ + +#include +#include + +#include "framebuf/colour.h" +#include "framebuf/pixelfmt.h" + +#include "framebuf/span.h" + +#include "framebuf/span-p8.h" + +#define SPAN_P8_NENTRIES 256 + +/* whole-byte format: one palette index per byte, so a run of pixels is a + * plain contiguous byte range and copy is a straight memcpy. */ +static void span_p8_copy(void *vdst, const void *vsrc, int length) +{ + memcpy(vdst, vsrc, (size_t) length); +} + +/* like span_p4_blendconst, but p8 is a plain byte store so there is no + * nibble packing: "src1" is one index per byte, "src2" an array of colour_t + * blended in full RGB precision before re-quantising to the nearest palette + * entry, "context" the palette. */ +static void span_p8_blendconst(void *vdst, + const void *vsrc1, + const void *vsrc2, + int length, + int alpha, + const void *context) +{ + unsigned char *pdst; + const unsigned char *psrc1; + const colour_t *psrc2; + const colour_t *palette; + + pdst = vdst; + psrc1 = vsrc1; + psrc2 = vsrc2; + palette = context; + + while (length--) + { + colour_t old_colour, new_colour, blended; + pixelfmt_rgba8888_t oldpx, newpx; + int old_r, old_g, old_b; + int new_r, new_g, new_b; + int blend_r, blend_g, blend_b; + + old_colour = palette[*psrc1]; + oldpx = old_colour.primary; + old_r = PIXELFMT_Rxxx8888(oldpx); + old_g = PIXELFMT_xGxx8888(oldpx); + old_b = PIXELFMT_xxBx8888(oldpx); + + new_colour = *psrc2; + newpx = new_colour.primary; + new_r = PIXELFMT_Rxxx8888(newpx); + new_g = PIXELFMT_xGxx8888(newpx); + new_b = PIXELFMT_xxBx8888(newpx); + + blend_r = (new_r * alpha + old_r * (255 - alpha)) / 255; + blend_g = (new_g * alpha + old_g * (255 - alpha)) / 255; + blend_b = (new_b * alpha + old_b * (255 - alpha)) / 255; + + blended = colour_rgb(blend_r, blend_g, blend_b); + *pdst = (unsigned char) colour_to_pixel(palette, SPAN_P8_NENTRIES, + blended, pixelfmt_p8); + + pdst++; + psrc1++; + psrc2++; + } +} + +/* "dst" is the row base, "first" the pixel index into it; whole-byte, so + * this is just a byte store. "pixel" is a palette index in its low byte. */ +static void span_p8_fill(void *vdst, + int first, + pixelfmt_any_t pixel, + int length) +{ + unsigned char *base; + + base = vdst; + memset(base + first, (unsigned char) (pixel & 0xFF), (size_t) length); +} + +const span_t span_p8 = +{ + pixelfmt_p8, + span_p8_copy, + span_p8_fill, + span_p8_blendconst, + NULL, /* blendarray: unneeded so far */ +}; diff --git a/libraries/wuss/test/tasks/image.c b/libraries/wuss/test/tasks/image.c index c86e90ff..cd948fb8 100644 --- a/libraries/wuss/test/tasks/image.c +++ b/libraries/wuss/test/tasks/image.c @@ -26,6 +26,37 @@ #define IMAGE_MARGINSZ (NINEPATCHSZ + IMAGE_BORDERSZ) #define IMAGE_EXT ".png" +/* screen_copy_bitmap and screen_copy_ninepatch only understand a deep 32bpp + * source; bitmap_load_png keeps a palette-type PNG as pixelfmt_p8, so convert + * one back to bgrx8888 here rather than teach every blitter a paletted + * source. */ +static result_t load_png_deep(bitmap_t *bm, const char *filename) +{ + result_t rc; + bitmap_t *deep; + + rc = bitmap_load_png(bm, filename); + if (rc != result_OK) + return rc; + + if (bm->format != pixelfmt_p8) + return result_OK; + + rc = bitmap_convert(bm, pixelfmt_bgrx8888, &deep); + if (rc != result_OK) + { + free(bm->base); + return rc; + } + + free(bm->base); + free(bm->palette); + *bm = *deep; + free(deep); + + return result_OK; +} + result_t image_create(wuss_t *wuss, const char *resources, const char *path, @@ -58,14 +89,14 @@ result_t image_create(wuss_t *wuss, return rc; } - rc = bitmap_load_png(&task->bitmap, path); + rc = load_png_deep(&task->bitmap, path); if (rc != result_OK) { free(task); /* nothing registered yet; the spawner will not free it */ return rc; } - rc = bitmap_load_png(&task->ninepatch, background_path); + rc = load_png_deep(&task->ninepatch, background_path); if (rc != result_OK) { free(task->bitmap.base); @@ -200,7 +231,7 @@ static result_t image_click(wuss_window_t *window, leafname); strcpy(buf, filename); - rc = bitmap_load_png(&next, buf); + rc = load_png_deep(&next, buf); if (rc != result_OK) { logf_warning("image: skipping \"%s\" (rc=0x%X)", buf, rc); From be0c392031c721238a01d456bbb16eda3f1ff50b Mon Sep 17 00:00:00 2001 From: David Thomas Date: Fri, 11 Sep 2026 10:18:55 +0100 Subject: [PATCH 024/128] feat(wuss): add screen-palette window; fix(framebuf): p8 image colour swap palette task gains a second 'Screen' window showing the physical screen bitmap's own palette (scr->palette, sized per 1/2/4/8bpp) rather than wuss's 16-entry UI palette; 32bpp has no palette, shown as a label. Mirrors checker.c's two-window-per-task pattern. load_png_deep (image.c) converted a p8-loaded PNG to bgrx8888 before handing it to screen_copy_bitmap/screen_copy_ninepatch, but that whole family assumes source pixels are rgba8888 byte order (see bitmap_load_png's documented invariant) and reads them as such unconditionally. bgrx8888 swaps red and blue against that, producing dull/wrong-hue composites even when the source PNG's palette exactly matches the screen's. Added bmconv_p8_to_rgbx8888 (mirrors bmconv_p8_to_bgrx8888) and switched load_png_deep to it. 26/26 DPTLibTest tests pass; wuss (build-sdl) builds clean. Co-Authored-By: Claude Sonnet 5 --- libraries/framebuf/bitmap/bitmap.c | 58 +++++++++++++ libraries/wuss/test/tasks/image.c | 10 ++- libraries/wuss/test/tasks/palette.c | 127 +++++++++++++++++++++++----- libraries/wuss/test/tasks/palette.h | 11 ++- 4 files changed, 179 insertions(+), 27 deletions(-) diff --git a/libraries/framebuf/bitmap/bitmap.c b/libraries/framebuf/bitmap/bitmap.c index 7aac8829..25c5ac06 100644 --- a/libraries/framebuf/bitmap/bitmap.c +++ b/libraries/framebuf/bitmap/bitmap.c @@ -239,6 +239,61 @@ static result_t bmconv_p8_to_bgrx8888(const bitmap_t *src, bitmap_t **pdst) return rc; } +/* As bmconv_p8_to_bgrx8888 but into rgbx8888 -- for a caller (e.g. + * screen_copy_bitmap) that assumes the R,G,B,A/X byte order documented on + * bitmap_load_png(), rather than the SDL-display byte order the *_to_bgrx8888 + * family targets. */ +static result_t bmconv_p8_to_rgbx8888(const bitmap_t *src, bitmap_t **pdst) +{ + result_t rc; + const pixelmap_t *pm; + const pixelfmt_rgbx8888_t *map; + bitmap_t *dst; + pixelfmt_rgbx8888_t *outpixels; + const unsigned char *inrow; + int x, y; + + assert(src); + assert(src->palette); + + pm = pixelmap_get(pixelfmt_p8, pixelfmt_rgbx8888, src->palette, 256); + if (pm == NULL) + return result_NOT_SUPPORTED; + map = (const pixelfmt_rgbx8888_t *) pm->entries; + + outpixels = malloc(src->size.w * sizeof(pixelfmt_rgbx8888_t) * src->size.h); + if (outpixels == NULL) + return result_OOM; + + dst = malloc(sizeof(*dst)); + if (dst == NULL) + { + free(outpixels); + return result_OOM; + } + + rc = bitmap_init(dst, + src->size, + pixelfmt_rgbx8888, + src->size.w * sizeof(pixelfmt_rgbx8888_t), + NULL, + outpixels); + if (rc) + return rc; + + inrow = src->base; + for (y = 0; y < src->size.h; y++) + { + for (x = 0; x < src->size.w; x++) + *outpixels++ = map[inrow[x]]; + inrow += src->rowbytes; + } + + *pdst = dst; + + return rc; +} + /* As bmconv_p4_to_bgrx8888 but for 1bpp paletted, packed MSB-first (bit 7 is * the leftmost pixel) to match the screen p1 plot helpers. */ static result_t bmconv_p1_to_bgrx8888(const bitmap_t *src, bitmap_t **pdst) @@ -395,6 +450,9 @@ result_t bitmap_convert(const bitmap_t *src, case pixelfmt_bgrx8888: return bmconv_p8_to_bgrx8888(src, dst); + case pixelfmt_rgbx8888: + return bmconv_p8_to_rgbx8888(src, dst); + default: return result_NOT_SUPPORTED; } diff --git a/libraries/wuss/test/tasks/image.c b/libraries/wuss/test/tasks/image.c index cd948fb8..e79cae01 100644 --- a/libraries/wuss/test/tasks/image.c +++ b/libraries/wuss/test/tasks/image.c @@ -27,9 +27,11 @@ #define IMAGE_EXT ".png" /* screen_copy_bitmap and screen_copy_ninepatch only understand a deep 32bpp - * source; bitmap_load_png keeps a palette-type PNG as pixelfmt_p8, so convert - * one back to bgrx8888 here rather than teach every blitter a paletted - * source. */ + * source in R,G,B,A/X byte order (see bitmap_load_png()); bitmap_load_png + * keeps a palette-type PNG as pixelfmt_p8, so convert one back to rgbx8888 + * here rather than teach every blitter a paletted source. rgbx8888, not + * bgrx8888: the latter is BGR (SDL display byte order) and would swap red + * and blue once the blitters re-read it as rgba8888. */ static result_t load_png_deep(bitmap_t *bm, const char *filename) { result_t rc; @@ -42,7 +44,7 @@ static result_t load_png_deep(bitmap_t *bm, const char *filename) if (bm->format != pixelfmt_p8) return result_OK; - rc = bitmap_convert(bm, pixelfmt_bgrx8888, &deep); + rc = bitmap_convert(bm, pixelfmt_rgbx8888, &deep); if (rc != result_OK) { free(bm->base); diff --git a/libraries/wuss/test/tasks/palette.c b/libraries/wuss/test/tasks/palette.c index 105dc642..a6970c54 100644 --- a/libraries/wuss/test/tasks/palette.c +++ b/libraries/wuss/test/tasks/palette.c @@ -11,7 +11,9 @@ #endif #include "base/utils.h" +#include "framebuf/bmfont.h" #include "framebuf/palettes.h" +#include "framebuf/pixelfmt.h" #include "geom/box.h" #include "io/namelist.h" #include "io/path.h" @@ -127,7 +129,6 @@ result_t palette_create(wuss_t *wuss, free(task); /* nothing registered yet; the spawner will not free it */ return rc; } - wuss_task_set_autoclose(task->delegate, 1); rc = wuss_window_create_placed(task->delegate, SIZE2D(100, 100), @@ -138,9 +139,64 @@ result_t palette_create(wuss_t *wuss, SIZE2D(0, 0), &task->window); if (rc != result_OK) + { wuss_task_destroy(task->delegate); /* unregister; its QUIT frees the task block */ + return rc; + } + + rc = wuss_window_create_placed(task->delegate, + SIZE2D(100, 100), + "Screen", + wuss_WINDOW_NO_RESIZE_BLIT, + wuss_BACKDROP_COLOUR(palette_PICO8_BLACK), + SIZE2D(100, 100), + SIZE2D(0, 0), + &task->window2); + if (rc != result_OK) + { + wuss_task_destroy(task->delegate); /* closes "Palette", QUIT frees the block */ + return rc; + } - return rc; + /* both windows up: from here, closing the last one reaps the task and its + * wuss_EVENT_QUIT frees task_data */ + wuss_task_set_autoclose(task->delegate, 1); + + return result_OK; +} + +/* shared by both windows: paint a roughly-square grid of npalette swatches + * across bounds */ +static void palette_draw_grid(screen_t *scr, + const box_t *bounds, + int sx, + int sy, + const colour_t *palette, + int npalette) +{ + int cols, rows; + int cell_w, cell_h; + int i; + + cols = 1; + while (cols * cols < npalette) + cols++; + rows = (npalette + cols - 1) / cols; + + cell_w = (bounds->x1 - bounds->x0) / cols; + cell_h = (bounds->y1 - bounds->y0) / rows; + + for (i = 0; i < npalette; i++) + { + int col, row, x, y; + + col = i % cols; + row = i / cols; + x = bounds->x0 - sx + col * cell_w; + y = bounds->y0 - sy + row * cell_h; + + screen_fill_rect(scr, x, y, SIZE2D(cell_w, cell_h), palette[i]); + } } static result_t palette_redraw(const wuss_event_t *event, void *task_data) @@ -150,9 +206,7 @@ static result_t palette_redraw(const wuss_event_t *event, void *task_data) int npalette; screen_t *scr; const box_t *bounds; - int cols, rows; - int cell_w, cell_h; - int i, sx, sy; + int sx, sy; pc = task_data; palette = wuss_get_palette(pc->wuss, &npalette); @@ -165,26 +219,46 @@ static result_t palette_redraw(const wuss_event_t *event, void *task_data) sx = event->data.redraw.scroll.x; sy = event->data.redraw.scroll.y; - cols = 1; - while (cols * cols < npalette) - cols++; - rows = (npalette + cols - 1) / cols; + palette_draw_grid(scr, bounds, sx, sy, palette, npalette); - cell_w = (bounds->x1 - bounds->x0) / cols; - cell_h = (bounds->y1 - bounds->y0) / rows; + return result_OK; +} - for (i = 0; i < npalette; i++) - { - int col, row, x, y; +/* "Screen" window: the physical screen bitmap's own palette, whatever size + * its pixel format needs (2/4/16/256 for 1/2/4/8bpp); 32bpp has none, so + * just label it */ +static result_t palette_redraw_screen(palette_task_t *pc, + const wuss_event_t *event) +{ + screen_t *scr; + const box_t *bounds; + int sx, sy, npalette; - col = i % cols; - row = i / cols; - x = bounds->x0 - sx + col * cell_w; - y = bounds->y0 - sy + row * cell_h; + scr = event->data.redraw.scr; + bounds = event->data.redraw.bounds; + sx = event->data.redraw.scroll.x; + sy = event->data.redraw.scroll.y; - screen_fill_rect(scr, x, y, SIZE2D(cell_w, cell_h), palette[i]); + if (scr->palette == NULL) + { + static const char label[] = "32bpp (none)"; + bmfont_t *font = wuss_get_font(pc->wuss); + point_t pos = POINT(bounds->x0 - sx + 2, bounds->y0 - sy + 2); + colour_t ink = colour_rgb(0xFF, 0xFF, 0xFF); + colour_t bg = colour_rgba(0, 0, 0, 0); /* transparent */ + + screen_fill_rect(scr, bounds->x0 - sx, bounds->y0 - sy, + SIZE2D(bounds->x1 - bounds->x0, bounds->y1 - bounds->y0), + colour_rgb(0x00, 0x00, 0x00)); + if (font != NULL) + bmfont_draw(font, scr, label, (int) strlen(label), ink, bg, &pos, NULL); + return result_OK; } + npalette = 1 << (1 << pixelfmt_log2bpp(scr->format)); + + palette_draw_grid(scr, bounds, sx, sy, scr->palette, npalette); + return result_OK; } @@ -289,13 +363,13 @@ result_t palette_handle(wuss_window_t *window, { palette_task_t *pc; - NOT_USED(window); - pc = task_data; switch (event->kind) { case wuss_EVENT_REDRAW: + if (window == pc->window2) + return palette_redraw_screen(pc, event); return palette_redraw(event, task_data); case wuss_EVENT_MOUSE: @@ -308,8 +382,17 @@ result_t palette_handle(wuss_window_t *window, pc->menu_handle = NULL; /* wuss closed the chain under us */ return result_OK; + case wuss_EVENT_CLOSE: + if (window == pc->window2) + pc->window2 = NULL; + else + pc->window = NULL; + return result_OK; + case wuss_EVENT_QUIT: - free(task_data); /* calloc'd per instance by the spawner */ + free(task_data); /* one calloc'd block backs both windows; the task + * autocloses once the second one goes, so free it + * here */ return result_OK; default: diff --git a/libraries/wuss/test/tasks/palette.h b/libraries/wuss/test/tasks/palette.h index c699ead8..18dcc18f 100644 --- a/libraries/wuss/test/tasks/palette.h +++ b/libraries/wuss/test/tasks/palette.h @@ -23,12 +23,21 @@ * an Invert toggle. A pick installs the loaded/inverted array via * wuss_set_palette; there is no callback here, since every other interested * party (framebuffer bitmap, physical palette) reacts to the resulting - * wuss_EVENT_PALETTE and reads the array back with wuss_get_palette. */ + * wuss_EVENT_PALETTE and reads the array back with wuss_get_palette. + * + * A second window ("Screen") draws the same kind of grid but for the + * physical screen bitmap's own palette (event->data.redraw.scr->palette), + * which is whatever size the screen's pixel format needs (2/4/16/256 + * entries for 1/2/4/8bpp) rather than the fixed 16-entry system palette + * above; a NULL screen palette (32bpp, no palette) just shows a label. */ typedef struct palette_task { wuss_t *wuss; wuss_task_t *delegate; wuss_window_t *window; + wuss_window_t *window2; /* "Screen" window: the physical screen's + * own palette (1/2/4/8bpp) or a "none" + * label (32bpp has no palette) */ const char *resources; bool invert; From 2e6e133c9c622c8c239f445114d50961445fb096 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Fri, 11 Sep 2026 10:20:45 +0100 Subject: [PATCH 025/128] feat(wuss): fill p8 screen palette tail with the web-safe 216 scr_palette's 240 slots beyond the 16 UI colours were left black. Fill 232 of them with the web-safe (6x6x6, step 0x33) cube instead, so a p8 screen has real colour range beyond the UI palette for nearest-match; the remaining 24 stay black. Co-Authored-By: Claude Sonnet 5 --- apps/wuss/main.c | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/apps/wuss/main.c b/apps/wuss/main.c index 95b765ad..a76496e9 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -318,10 +318,20 @@ static result_t run_wuss(const char *resources, /* bitmap_set_palette reads every entry a paletted format's bitmap_init * needs (up to 256 for p8); pad scr_palette with palette's 16 UI colours - * so closest_palette_entry always has exact matches to find, then black - * for the unused tail. */ + * so closest_palette_entry always has exact matches to find, then the + * web-safe 216 (6x6x6 cube, steps of 0x33) so a p8 screen has real range + * beyond the UI colours for nearest-match, then black for what's left. */ memset(scr_palette, 0, sizeof(scr_palette)); memcpy(scr_palette, palette, sizeof(palette)); + { + int r, g, b, n; + + n = NELEMS(palette); + for (r = 0; r < 6; r++) + for (g = 0; g < 6; g++) + for (b = 0; b < 6; b++) + scr_palette[n++] = colour_rgb(r * 0x33, g * 0x33, b * 0x33); + } rc = bitmap_init(&bm, SIZE2D(scr_width, scr_height), fmt, rowbytes, scr_palette, pixels); From abb6184f4a72ed5788660fa5076237b9e177dbfb Mon Sep 17 00:00:00 2001 From: David Thomas Date: Fri, 11 Sep 2026 10:29:18 +0100 Subject: [PATCH 026/128] fix(wuss): pad screen palette on live palette change, not just startup task_handle_event's wuss_EVENT_PALETTE handler passed wuss's bare 16-entry UI palette straight to bitmap_set_palette, which reads however many entries the screen bitmap's own format needs (256 for p8) -- heap-buffer-overflow on any palette pick while running p8. Factored the pad-to-screen-size logic (16 UI colours, then the web-safe 216, then black) out of run_wuss into tasks_build_screen_palette (tasks.c/.h), shared by both startup and the live palette-change handler, sized via pixelfmt_paletted_nentries(g.bm->format). 26/26 DPTLibTest passes (build-asan and an ASan/UBSan build-sdl); wuss builds clean; a standalone ASan/UBSan harness exercising the fixed pad-then-bitmap_set_palette call shape runs clean. Co-Authored-By: Claude Sonnet 5 --- apps/wuss/main.c | 21 ++++++--------------- apps/wuss/tasks.c | 37 +++++++++++++++++++++++++++++++++---- apps/wuss/tasks.h | 10 ++++++++++ 3 files changed, 49 insertions(+), 19 deletions(-) diff --git a/apps/wuss/main.c b/apps/wuss/main.c index a76496e9..0e694e04 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -317,21 +317,12 @@ static result_t run_wuss(const char *resources, goto Failure; /* bitmap_set_palette reads every entry a paletted format's bitmap_init - * needs (up to 256 for p8); pad scr_palette with palette's 16 UI colours - * so closest_palette_entry always has exact matches to find, then the - * web-safe 216 (6x6x6 cube, steps of 0x33) so a p8 screen has real range - * beyond the UI colours for nearest-match, then black for what's left. */ - memset(scr_palette, 0, sizeof(scr_palette)); - memcpy(scr_palette, palette, sizeof(palette)); - { - int r, g, b, n; - - n = NELEMS(palette); - for (r = 0; r < 6; r++) - for (g = 0; g < 6; g++) - for (b = 0; b < 6; b++) - scr_palette[n++] = colour_rgb(r * 0x33, g * 0x33, b * 0x33); - } + * needs (up to 256 for p8); pad scr_palette with palette's 16 UI colours, + * then the web-safe 216 (so a p8 screen has real range beyond the UI + * colours for nearest-match), then black for what's left. Also done on + * a live palette change; see task_handle_event. */ + tasks_build_screen_palette(scr_palette, NELEMS(scr_palette), + palette, NELEMS(palette)); rc = bitmap_init(&bm, SIZE2D(scr_width, scr_height), fmt, rowbytes, scr_palette, pixels); diff --git a/apps/wuss/tasks.c b/apps/wuss/tasks.c index 66b9a579..a91f4bac 100644 --- a/apps/wuss/tasks.c +++ b/apps/wuss/tasks.c @@ -8,6 +8,7 @@ #include "base/utils.h" #include "framebuf/bitmap.h" #include "framebuf/colour.h" +#include "framebuf/pixelfmt.h" #include "io/path.h" #include "wuss/task.h" #include "wuss/wuss.h" @@ -40,6 +41,24 @@ struct wuss_app_tasks g; +void tasks_build_screen_palette(colour_t *out, + int nout, + const colour_t *ui, + int nui) +{ + int r, g_, b, n; + + memset(out, 0, nout * sizeof(*out)); + + n = MIN(nui, nout); + memcpy(out, ui, n * sizeof(*out)); + + for (r = 0; r < 6 && n < nout; r++) + for (g_ = 0; g_ < 6 && n < nout; g_++) + for (b = 0; b < 6 && n < nout; b++) + out[n++] = colour_rgb(r * 0x33, g_ * 0x33, b * 0x33); +} + /* 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_QUIT handler. On any create failure X_create has already @@ -353,12 +372,22 @@ result_t task_handle_event(wuss_window_t *window, /* wuss_set_palette already updated wuss's own copy and re-cached * furniture; read the new array back and push it on to the framebuffer * bitmap and any physical palette, so callers (e.g. the palette picker) - * never need to know the frontend exists */ + * never need to know the frontend exists. bitmap_set_palette reads + * every entry g.bm's own format needs (up to 256 for p8), not just + * wuss's fixed-size UI palette, so pad out to match -- same as the + * startup array run_wuss builds. */ const colour_t *palette; int npalette; - - palette = wuss_get_palette(g.wuss, &npalette); - bitmap_set_palette(g.bm, palette); + colour_t scr_palette[256]; + int scr_nentries; + + palette = wuss_get_palette(g.wuss, &npalette); + scr_nentries = pixelfmt_paletted_nentries(g.bm->format); + if (scr_nentries > 0) + { + tasks_build_screen_palette(scr_palette, scr_nentries, palette, npalette); + bitmap_set_palette(g.bm, scr_palette); + } wuss_frontend_set_palette(g.frontend, palette, npalette); return result_OK; } diff --git a/apps/wuss/tasks.h b/apps/wuss/tasks.h index 08aa8c56..31ca2ddc 100644 --- a/apps/wuss/tasks.h +++ b/apps/wuss/tasks.h @@ -48,4 +48,14 @@ result_t task_handle_event(wuss_window_t *window, * MENU click over bare backdrop */ result_t tasks_open_launcher(point_t pos); +/* Fill out[0..nout-1]: ui[0..nui-1] copied in, then as much of the web-safe + * 216 (6x6x6 cube, steps of 0x33) as fits, then black for whatever is left. + * Used to build a paletted screen's full-size palette (nout = whatever + * bitmap_set_palette will read for the screen's format) from wuss's + * fixed-size UI palette, both at startup and on a live wuss_EVENT_PALETTE. */ +void tasks_build_screen_palette(colour_t *out, + int nout, + const colour_t *ui, + int nui); + #endif /* WUSS_APP_TASKS_H */ From baeb980d3eb9b3fa307ecea03b901331cd912517 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Fri, 11 Sep 2026 10:56:45 +0100 Subject: [PATCH 027/128] fix(framebuf): key ordered-dither bias on per-channel levels, not entry count dither_bias_build took pixelfmt_paletted_nentries() (raw palette size: 2/4/16/256) as its level count, but the pixelmap quantises R/G/B independently -- 4:4:4 for p1/p2/p4, 5:6:5 for p8 -- so the real per-channel level count never matches the total entry count. For p8 this made the bias gap 255/(256-1) = 1, collapsing the Bayer bias table to {-1,0} and making dithering invisible; p1/p2 were technically wrong too (coarser gap than the real channel depth), just not visibly broken. Add dither_levels_for(pm), keyed on the worst of pm->rbits/gbits/bbits, and reorder each screen_copy_bitmap_p{1,2,4,8} to fetch the pixelmap before building the bias table so it can use the real channel depth. --- libraries/framebuf/screen/screen-draw.c | 35 ++++++++++++++++--------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/libraries/framebuf/screen/screen-draw.c b/libraries/framebuf/screen/screen-draw.c index 0c3361e4..5796f3bd 100644 --- a/libraries/framebuf/screen/screen-draw.c +++ b/libraries/framebuf/screen/screen-draw.c @@ -321,6 +321,13 @@ void screen_fill_square(screen_t *scr, * snapping to the nearer. Building the table hoists the one divide out of the * inner loop -- the per-pixel cost drops to a lookup and an add. * + * "nlevels" must be the per-channel quantisation level count (e.g. 1 << + * pm->rbits), not the palette's total entry count: pixelmap quantises R/G/B + * independently (4:4:4 for p1/p2/p4, 5:6:5 for p8) regardless of how many + * palette entries that yields, so a p8 screen has only 32-64 levels per + * channel despite 256 total entries. Keying the gap off the entry count + * instead makes the bias round to zero on p8 -- the dither vanishes. + * * dither_bias_build fills "tab" and returns 1 when dithering is on and useful * (>= 2 levels), 0 when the caller should skip the bias entirely. * @@ -343,6 +350,14 @@ static int dither_bias_build(int tab[64], int nlevels, int dither) return 1; } +/* Per-channel level count driving the dither bias for a deep->paletted + * pixelmap: the worst (fewest-level) of R/G/B, since one shared bias table + * nudges all three channels. */ +static int dither_levels_for(const pixelmap_t *pm) +{ + return 1 << MIN(pm->rbits, MIN(pm->gbits, pm->bbits)); +} + /* Apply the pre-built bias for sprite-local pixel (sx, sy) to channel value * "v" (0..255), result clamped to 0..255. Coordinates are relative to the * source bitmap's top-left, not the screen, so the dither pattern stays fixed @@ -386,9 +401,6 @@ static result_t screen_copy_bitmap_p4(screen_t *scr, clipped_width = draw_box->x1 - draw_box->x0; clipped_height = draw_box->y1 - draw_box->y0; - do_dither = dither_bias_build(bias, - pixelfmt_paletted_nentries(scr->format), - dither); srcrow = (const unsigned char *) src->base + (draw_box->y0 - y) * src->rowbytes; dstbase = scr->base; @@ -399,6 +411,8 @@ static result_t screen_copy_bitmap_p4(screen_t *scr, if (pm == NULL) return result_NOT_SUPPORTED; + do_dither = dither_bias_build(bias, dither_levels_for(pm), dither); + for (yy = 0; yy < clipped_height; yy++) { const pixelfmt_rgba8888_t *srcpx; @@ -472,9 +486,6 @@ static result_t screen_copy_bitmap_p8(screen_t *scr, clipped_width = draw_box->x1 - draw_box->x0; clipped_height = draw_box->y1 - draw_box->y0; - do_dither = dither_bias_build(bias, - pixelfmt_paletted_nentries(scr->format), - dither); srcrow = (const unsigned char *) src->base + (draw_box->y0 - y) * src->rowbytes; dstbase = scr->base; @@ -483,6 +494,8 @@ static result_t screen_copy_bitmap_p8(screen_t *scr, if (pm == NULL) return result_NOT_SUPPORTED; + do_dither = dither_bias_build(bias, dither_levels_for(pm), dither); + for (yy = 0; yy < clipped_height; yy++) { const pixelfmt_rgba8888_t *srcpx; @@ -552,9 +565,6 @@ static result_t screen_copy_bitmap_p1(screen_t *scr, clipped_width = draw_box->x1 - draw_box->x0; clipped_height = draw_box->y1 - draw_box->y0; - do_dither = dither_bias_build(bias, - pixelfmt_paletted_nentries(scr->format), - dither); srcrow = (const unsigned char *) src->base + (draw_box->y0 - y) * src->rowbytes; dstbase = scr->base; @@ -563,6 +573,8 @@ static result_t screen_copy_bitmap_p1(screen_t *scr, if (pm == NULL) return result_NOT_SUPPORTED; + do_dither = dither_bias_build(bias, dither_levels_for(pm), dither); + for (yy = 0; yy < clipped_height; yy++) { const pixelfmt_rgba8888_t *srcpx; @@ -636,9 +648,6 @@ static result_t screen_copy_bitmap_p2(screen_t *scr, clipped_width = draw_box->x1 - draw_box->x0; clipped_height = draw_box->y1 - draw_box->y0; - do_dither = dither_bias_build(bias, - pixelfmt_paletted_nentries(scr->format), - dither); srcrow = (const unsigned char *) src->base + (draw_box->y0 - y) * src->rowbytes; dstbase = scr->base; @@ -647,6 +656,8 @@ static result_t screen_copy_bitmap_p2(screen_t *scr, if (pm == NULL) return result_NOT_SUPPORTED; + do_dither = dither_bias_build(bias, dither_levels_for(pm), dither); + for (yy = 0; yy < clipped_height; yy++) { const pixelfmt_rgba8888_t *srcpx; From 98cc4d7aeb770b2d14816db6bb7bd543931fa1c5 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Fri, 11 Sep 2026 11:20:55 +0100 Subject: [PATCH 028/128] fix(framebuf): reject paletted source bitmaps in screen_copy_bitmap screen_copy_bitmap_i assumed every source bitmap is 32bpp RGBA/RGBX and cast src->base straight to pixelfmt_rgba8888_t*. resources/wuss/ninepatch.png is a colortype-3 (PLTE) PNG, so bitmap_load_png decodes it to pixelfmt_p8 (1 byte/pixel); the 32bpp stride then read past the 729-byte (27x27) buffer -- ASan heap-buffer-overflow plus a UBSan misaligned load, via wuss__icon_draw_bitmap -> screen_copy_bitmap -> screen_copy_bitmap_p4. Reject any source whose format isn't 32bpp (pixelfmt_log2bpp != 5) with result_NOT_SUPPORTED before the format switch, rather than reading it as one. Existing callers already ignore or tolerate a non-OK return (icons_create's has_sprite gate), so ninepatch decoration just stops drawing rather than crashing; a real paletted-source blit path is future work if it's needed back. Co-Authored-By: Claude Sonnet 5 --- libraries/framebuf/screen/screen-draw.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/libraries/framebuf/screen/screen-draw.c b/libraries/framebuf/screen/screen-draw.c index 5796f3bd..36aac41e 100644 --- a/libraries/framebuf/screen/screen-draw.c +++ b/libraries/framebuf/screen/screen-draw.c @@ -795,6 +795,14 @@ static result_t screen_copy_bitmap_i(screen_t *scr, if (pixelfmt_is_rle(src->format)) return screen_copy_bitmap_rle(scr, x, y, src, &draw_box); + /* Every path below reads the source a 32bpp pixel at a time (see the + * comment below on byte order). A paletted source (p1/p2/p4/p8, e.g. a PNG + * saved with a PLTE chunk) is 1 byte or less per pixel and there is no + * paletted->paletted conversion here, so reject it rather than stride a + * 32bpp read through a narrower buffer. */ + if (pixelfmt_log2bpp(src->format) != 5) + return result_NOT_SUPPORTED; + /* Source pixels loaded from PNG are always laid out R,G,B,A/X byte order * (see bitmap_load_png()), the same layout colour_t::primary uses, so * source pixels can be read directly into a colour_t with no conversion. */ From 9eadfe38de24444e199f25a921c27183fb681949 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Fri, 11 Sep 2026 12:36:26 +0100 Subject: [PATCH 029/128] feat(framebuf): support paletted source bitmaps in screen_copy_bitmap Replace the previous commit's reject-only stub with real support for blitting a paletted (p1/p2/p4/p8) source bitmap -- e.g. a PNG saved with a PLTE chunk -- onto a screen of any format. A shared src_fetch_rgba() helper decodes a source pixel (packed index or plain 32bpp) into an rgba8888 colour_t on the fly, reusing the same paletted->deep pixelmap_get() table the RLE decoder already relies on. Every screen_copy_bitmap_p1/p2/p4/p8/32 helper now reads through it directly instead of the source being unpacked into a malloc'd rgba8888 scratch bitmap up front -- no extra pass or allocation per blit, and the per-pixel cost matches the existing deep-source paths. A paletted source's tRNS-derived alpha (baked into its palette entries by bitmap_load_png) is carried through and alpha-tested exactly like a real rgba8888/bgra8888 source pixel would be. Co-Authored-By: Claude Sonnet 5 --- libraries/framebuf/screen/screen-draw.c | 259 ++++++++++++------- libraries/framebuf/screen/test/screen-test.c | 93 +++++++ 2 files changed, 257 insertions(+), 95 deletions(-) diff --git a/libraries/framebuf/screen/screen-draw.c b/libraries/framebuf/screen/screen-draw.c index 36aac41e..430ca05d 100644 --- a/libraries/framebuf/screen/screen-draw.c +++ b/libraries/framebuf/screen/screen-draw.c @@ -374,6 +374,57 @@ static unsigned int dither_channel(unsigned int v, return (unsigned int) CLAMP((int) v + adj, 0, 255); } +/* Read source pixel (srcx, srcy) -- relative to the source bitmap's own + * top-left, not the screen -- as an rgba8888 colour_t. "srcpm" is the + * paletted->rgba8888 pixelmap for src->format (from pixelmap_get), or NULL + * when src is already a deep rgba8888/bgra8888 bitmap; either way this is the + * one place that knows how to decode a packed p1/p2/p4/p8 index versus a + * plain 32bpp read, so every screen_copy_bitmap_p1/p2/p4/p8/32 helper below + * shares it instead of striding a 32bpp read through a narrower source or + * unpacking to a scratch buffer first. */ +static colour_t src_fetch_rgba(const bitmap_t *src, + const pixelmap_t *srcpm, + int srcx, + int srcy) +{ + colour_t c; + const unsigned char *row; + + row = (const unsigned char *) src->base + srcy * src->rowbytes; + + if (srcpm == NULL) + { + c.primary = ((const pixelfmt_rgba8888_t *) row)[srcx]; + } + else + { + int idx; + + switch (pixelfmt_log2bpp(src->format)) + { + case 0: idx = (row[srcx >> 3] >> (7 - (srcx & 7))) & 1; break; + case 1: idx = (row[srcx >> 2] >> (6 - ((srcx & 3) << 1))) & 3; break; + case 2: idx = (row[srcx >> 1] >> ((srcx & 1) * 4)) & 0xF; break; + default: idx = row[srcx]; break; + } + + c.primary = ((const pixelfmt_rgba8888_t *) srcpm->entries)[idx]; + } + + return c; +} + +/* pixelmap_get() for the paletted->rgba8888 direction used by + * src_fetch_rgba(), or NULL (no lookup needed) when src is already deep. */ +static const pixelmap_t *src_pixelmap_for(const bitmap_t *src) +{ + if (pixelfmt_log2bpp(src->format) == 5) + return NULL; + + return pixelmap_get(src->format, pixelfmt_rgba8888, src->palette, + pixelfmt_paletted_nentries(src->format)); +} + /* Blit "src" onto the paletted screen, its top-left at (x, y), clipped to * "draw_box". No linear channel bits to blend, so this does an alpha-tested * transfer (skip fully transparent, else nearest palette match) rather than @@ -381,8 +432,8 @@ static unsigned int dither_channel(unsigned int v, * source RGB is ordered-dithered per pixel before the nearest-match lookup, * breaking up the banding a shallow palette otherwise shows on a gradient; the * Bayer cell is keyed on the sprite-local pixel so the pattern travels with - * the sprite. Returns result_NOT_SUPPORTED if there is no deep->paletted - * conversion table for the screen's format. */ + * the sprite. Returns result_NOT_SUPPORTED if there is no deep->paletted (or + * paletted->deep, for a paletted source) conversion table. */ static result_t screen_copy_bitmap_p4(screen_t *scr, int x, int y, @@ -391,9 +442,9 @@ static result_t screen_copy_bitmap_p4(screen_t *scr, int has_alpha, int dither) { - const unsigned char *srcrow; unsigned char *dstbase; const pixelmap_t *pm; + const pixelmap_t *srcpm; int bias[64]; int do_dither; int clipped_width, clipped_height; @@ -402,11 +453,13 @@ static result_t screen_copy_bitmap_p4(screen_t *scr, clipped_width = draw_box->x1 - draw_box->x0; clipped_height = draw_box->y1 - draw_box->y0; - srcrow = (const unsigned char *) src->base + (draw_box->y0 - y) * src->rowbytes; dstbase = scr->base; - /* Source pixels here are RGBA8888 byte order (see screen_copy_bitmap). The - * cached RGB->index table turns the inner loop into a mask-and-lookup. */ + srcpm = src_pixelmap_for(src); + if (srcpm == NULL && pixelfmt_log2bpp(src->format) != 5) + return result_NOT_SUPPORTED; + + /* Cached RGB->index table turns the inner loop into a mask-and-lookup. */ pm = pixelmap_get(pixelfmt_rgba8888, scr->format, scr->palette, 16); if (pm == NULL) return result_NOT_SUPPORTED; @@ -415,23 +468,24 @@ static result_t screen_copy_bitmap_p4(screen_t *scr, for (yy = 0; yy < clipped_height; yy++) { - const pixelfmt_rgba8888_t *srcpx; - unsigned char *rowp; - int xx; + unsigned char *rowp; + int srcy; + int xx; - srcpx = (const pixelfmt_rgba8888_t *) srcrow + (draw_box->x0 - x); - rowp = dstbase + (draw_box->y0 + yy) * scr->rowbytes; + srcy = draw_box->y0 - y + yy; + rowp = dstbase + (draw_box->y0 + yy) * scr->rowbytes; for (xx = 0; xx < clipped_width; xx++) { colour_t c; unsigned int r, g, b, idx; - int dstx; + int dstx, srcx; unsigned char *scrp; int shift; pixelfmt_any_t pxl; - c.primary = srcpx[xx]; + srcx = draw_box->x0 + xx - x; + c = src_fetch_rgba(src, srcpm, srcx, srcy); if (has_alpha && colour_get_alpha(&c) == 0) continue; /* fully transparent: leave background alone */ @@ -455,8 +509,6 @@ static result_t screen_copy_bitmap_p4(screen_t *scr, *scrp = (unsigned char) ((*scrp & ~(0xF << shift)) | ((pxl & 0xF) << shift)); } - - srcrow += src->rowbytes; } return result_OK; @@ -476,9 +528,9 @@ static result_t screen_copy_bitmap_p8(screen_t *scr, int has_alpha, int dither) { - const unsigned char *srcrow; unsigned char *dstbase; const pixelmap_t *pm; + const pixelmap_t *srcpm; int bias[64]; int do_dither; int clipped_width, clipped_height; @@ -487,9 +539,12 @@ static result_t screen_copy_bitmap_p8(screen_t *scr, clipped_width = draw_box->x1 - draw_box->x0; clipped_height = draw_box->y1 - draw_box->y0; - srcrow = (const unsigned char *) src->base + (draw_box->y0 - y) * src->rowbytes; dstbase = scr->base; + srcpm = src_pixelmap_for(src); + if (srcpm == NULL && pixelfmt_log2bpp(src->format) != 5) + return result_NOT_SUPPORTED; + pm = pixelmap_get(pixelfmt_rgba8888, scr->format, scr->palette, 256); if (pm == NULL) return result_NOT_SUPPORTED; @@ -498,20 +553,21 @@ static result_t screen_copy_bitmap_p8(screen_t *scr, for (yy = 0; yy < clipped_height; yy++) { - const pixelfmt_rgba8888_t *srcpx; - unsigned char *rowp; - int xx; + unsigned char *rowp; + int srcy; + int xx; - srcpx = (const pixelfmt_rgba8888_t *) srcrow + (draw_box->x0 - x); - rowp = dstbase + (draw_box->y0 + yy) * scr->rowbytes; + srcy = draw_box->y0 - y + yy; + rowp = dstbase + (draw_box->y0 + yy) * scr->rowbytes; for (xx = 0; xx < clipped_width; xx++) { colour_t c; unsigned int r, g, b, idx; - int dstx; + int dstx, srcx; - c.primary = srcpx[xx]; + srcx = draw_box->x0 + xx - x; + c = src_fetch_rgba(src, srcpm, srcx, srcy); if (has_alpha && colour_get_alpha(&c) == 0) continue; /* fully transparent: leave background alone */ @@ -532,8 +588,6 @@ static result_t screen_copy_bitmap_p8(screen_t *scr, rowp[dstx] = pm->entries[idx]; } - - srcrow += src->rowbytes; } return result_OK; @@ -555,9 +609,9 @@ static result_t screen_copy_bitmap_p1(screen_t *scr, int has_alpha, int dither) { - const unsigned char *srcrow; unsigned char *dstbase; const pixelmap_t *pm; + const pixelmap_t *srcpm; int bias[64]; int do_dither; int clipped_width, clipped_height; @@ -566,9 +620,12 @@ static result_t screen_copy_bitmap_p1(screen_t *scr, clipped_width = draw_box->x1 - draw_box->x0; clipped_height = draw_box->y1 - draw_box->y0; - srcrow = (const unsigned char *) src->base + (draw_box->y0 - y) * src->rowbytes; dstbase = scr->base; + srcpm = src_pixelmap_for(src); + if (srcpm == NULL && pixelfmt_log2bpp(src->format) != 5) + return result_NOT_SUPPORTED; + pm = pixelmap_get(pixelfmt_rgba8888, scr->format, scr->palette, 2); if (pm == NULL) return result_NOT_SUPPORTED; @@ -577,22 +634,23 @@ static result_t screen_copy_bitmap_p1(screen_t *scr, for (yy = 0; yy < clipped_height; yy++) { - const pixelfmt_rgba8888_t *srcpx; - unsigned char *rowp; - int xx; + unsigned char *rowp; + int srcy; + int xx; - srcpx = (const pixelfmt_rgba8888_t *) srcrow + (draw_box->x0 - x); - rowp = dstbase + (draw_box->y0 + yy) * scr->rowbytes; + srcy = draw_box->y0 - y + yy; + rowp = dstbase + (draw_box->y0 + yy) * scr->rowbytes; for (xx = 0; xx < clipped_width; xx++) { colour_t c; unsigned int r, g, b, idx; - int dstx, shift; + int dstx, shift, srcx; unsigned char *scrp; pixelfmt_any_t pxl; - c.primary = srcpx[xx]; + srcx = draw_box->x0 + xx - x; + c = src_fetch_rgba(src, srcpm, srcx, srcy); if (has_alpha && colour_get_alpha(&c) == 0) continue; /* fully transparent: leave background alone */ @@ -616,8 +674,6 @@ static result_t screen_copy_bitmap_p1(screen_t *scr, *scrp = (unsigned char) ((*scrp & ~(1 << shift)) | ((pxl & 1) << shift)); } - - srcrow += src->rowbytes; } return result_OK; @@ -638,9 +694,9 @@ static result_t screen_copy_bitmap_p2(screen_t *scr, int has_alpha, int dither) { - const unsigned char *srcrow; unsigned char *dstbase; const pixelmap_t *pm; + const pixelmap_t *srcpm; int bias[64]; int do_dither; int clipped_width, clipped_height; @@ -649,9 +705,12 @@ static result_t screen_copy_bitmap_p2(screen_t *scr, clipped_width = draw_box->x1 - draw_box->x0; clipped_height = draw_box->y1 - draw_box->y0; - srcrow = (const unsigned char *) src->base + (draw_box->y0 - y) * src->rowbytes; dstbase = scr->base; + srcpm = src_pixelmap_for(src); + if (srcpm == NULL && pixelfmt_log2bpp(src->format) != 5) + return result_NOT_SUPPORTED; + pm = pixelmap_get(pixelfmt_rgba8888, scr->format, scr->palette, 4); if (pm == NULL) return result_NOT_SUPPORTED; @@ -660,22 +719,23 @@ static result_t screen_copy_bitmap_p2(screen_t *scr, for (yy = 0; yy < clipped_height; yy++) { - const pixelfmt_rgba8888_t *srcpx; - unsigned char *rowp; - int xx; + unsigned char *rowp; + int srcy; + int xx; - srcpx = (const pixelfmt_rgba8888_t *) srcrow + (draw_box->x0 - x); - rowp = dstbase + (draw_box->y0 + yy) * scr->rowbytes; + srcy = draw_box->y0 - y + yy; + rowp = dstbase + (draw_box->y0 + yy) * scr->rowbytes; for (xx = 0; xx < clipped_width; xx++) { colour_t c; unsigned int r, g, b, idx; - int dstx, shift; + int dstx, shift, srcx; unsigned char *scrp; pixelfmt_any_t pxl; - c.primary = srcpx[xx]; + srcx = draw_box->x0 + xx - x; + c = src_fetch_rgba(src, srcpm, srcx, srcy); if (has_alpha && colour_get_alpha(&c) == 0) continue; /* fully transparent: leave background alone */ @@ -699,45 +759,52 @@ static result_t screen_copy_bitmap_p2(screen_t *scr, *scrp = (unsigned char) ((*scrp & ~(3 << shift)) | ((pxl & 3) << shift)); } - - srcrow += src->rowbytes; } return result_OK; } /* Blit "src" onto the 32bpp screen, its top-left at (x, y), clipped to - * "draw_box", alpha-blending row spans through the span registry. */ -static void screen_copy_bitmap_32(screen_t *scr, - int x, - int y, - const bitmap_t *src, - const box_t *draw_box, - int has_alpha) + * "draw_box", alpha-blending row spans through the span registry. A paletted + * "src" is decoded through "srcpm" (see src_fetch_rgba) a pixel at a time + * into the same colbuf/alphabuf chunk a deep source fills, so the span blend + * call itself doesn't care which kind of source it was. Returns + * result_NOT_SUPPORTED if "src" is paletted and has no paletted->deep + * conversion table. */ +static result_t screen_copy_bitmap_32(screen_t *scr, + int x, + int y, + const bitmap_t *src, + const box_t *draw_box, + int has_alpha) { - pixelfmt_any32_t colbuf[BITMAP_BLIT_CHUNK]; - unsigned char alphabuf[BITMAP_BLIT_CHUNK]; - const unsigned char *srcrow; - pixelfmt_any32_t *dstrow; - int clipped_width, clipped_height; - int yy; + pixelfmt_any32_t colbuf[BITMAP_BLIT_CHUNK]; + unsigned char alphabuf[BITMAP_BLIT_CHUNK]; + const pixelmap_t *srcpm; + pixelfmt_any32_t *dstrow; + int clipped_width, clipped_height; + int yy; clipped_width = draw_box->x1 - draw_box->x0; clipped_height = draw_box->y1 - draw_box->y0; - srcrow = (const unsigned char *) src->base + (draw_box->y0 - y) * src->rowbytes; + srcpm = src_pixelmap_for(src); + if (srcpm == NULL && pixelfmt_log2bpp(src->format) != 5) + return result_NOT_SUPPORTED; + dstrow = scr->base; dstrow += draw_box->y0 * scr->rowbytes / (int) sizeof(*dstrow) + draw_box->x0; for (yy = 0; yy < clipped_height; yy++) { - const pixelfmt_rgba8888_t *srcpx; - pixelfmt_any32_t *dstpx; - int remaining; + pixelfmt_any32_t *dstpx; + int srcy; + int remaining, done; - srcpx = (const pixelfmt_rgba8888_t *) srcrow + (draw_box->x0 - x); + srcy = draw_box->y0 - y + yy; dstpx = dstrow; remaining = clipped_width; + done = 0; while (remaining > 0) { @@ -749,38 +816,42 @@ static void screen_copy_bitmap_32(screen_t *scr, { colour_t c; - c.primary = srcpx[i]; + c = src_fetch_rgba(src, srcpm, draw_box->x0 + done + i - x, srcy); colbuf[i] = colour_to_pixel(scr->palette, 0, c, scr->format); alphabuf[i] = has_alpha ? colour_get_alpha(&c) : PIXELFMT_OPAQUE; } scr->span->blendarray(dstpx, dstpx, colbuf, chunk, alphabuf); - srcpx += chunk; dstpx += chunk; + done += chunk; remaining -= chunk; } - srcrow += src->rowbytes; dstrow += scr->rowbytes / (int) sizeof(*dstrow); } + + return result_OK; } /* Shared body for screen_copy_bitmap and screen_copy_bitmap_dithered. With - * "dither" set the paletted (p1/p2/p4) paths ordered-dither the source RGB - * before the nearest-match lookup; the 32bpp path and the RLE path ignore it - * (a 32bpp screen has the channel depth not to band, and an RLE source is - * pre-quantised UI art). */ + * "dither" set the paletted-screen (p1/p2/p4/p8) paths ordered-dither the + * source RGB before the nearest-match lookup; the 32bpp path and the RLE path + * ignore it (a 32bpp screen has the channel depth not to band, and an RLE + * source is pre-quantised UI art). A paletted source (p1/p2/p4/p8, e.g. a PNG + * saved with a PLTE chunk) is decoded through its own palette a pixel at a + * time by src_fetch_rgba rather than unpacked to a scratch buffer up front. */ static result_t screen_copy_bitmap_i(screen_t *scr, int x, int y, const bitmap_t *src, int dither) { - box_t clip_box; - box_t src_box; - box_t draw_box; - int has_alpha; + box_t clip_box; + box_t src_box; + box_t draw_box; + int has_alpha; + result_t rc; if (screen_get_clip(scr, &clip_box)) return result_OK; /* invalid clipped screen: nothing to draw */ @@ -795,33 +866,31 @@ static result_t screen_copy_bitmap_i(screen_t *scr, if (pixelfmt_is_rle(src->format)) return screen_copy_bitmap_rle(scr, x, y, src, &draw_box); - /* Every path below reads the source a 32bpp pixel at a time (see the - * comment below on byte order). A paletted source (p1/p2/p4/p8, e.g. a PNG - * saved with a PLTE chunk) is 1 byte or less per pixel and there is no - * paletted->paletted conversion here, so reject it rather than stride a - * 32bpp read through a narrower buffer. */ - if (pixelfmt_log2bpp(src->format) != 5) - return result_NOT_SUPPORTED; - /* Source pixels loaded from PNG are always laid out R,G,B,A/X byte order - * (see bitmap_load_png()), the same layout colour_t::primary uses, so - * source pixels can be read directly into a colour_t with no conversion. */ - has_alpha = (src->format == pixelfmt_rgba8888 || src->format == pixelfmt_bgra8888); + * (see bitmap_load_png()), the same layout colour_t::primary uses, so a + * deep source pixel is read directly into a colour_t with no conversion. A + * paletted source's tRNS-derived alpha survives into its palette's + * rgba8888 entries (see bitmap_load_png's plte[i] = colour_rgba(..., a)), + * so it always carries real per-pixel alpha and must be alpha-tested same + * as an rgba8888/bgra8888 source would be. */ + has_alpha = pixelfmt_log2bpp(src->format) != 5 || + src->format == pixelfmt_rgba8888 || src->format == pixelfmt_bgra8888; switch (pixelfmt_log2bpp(scr->format)) { - case 0: return screen_copy_bitmap_p1(scr, x, y, src, &draw_box, has_alpha, dither); - case 1: return screen_copy_bitmap_p2(scr, x, y, src, &draw_box, has_alpha, dither); - case 2: return screen_copy_bitmap_p4(scr, x, y, src, &draw_box, has_alpha, dither); - case 3: return screen_copy_bitmap_p8(scr, x, y, src, &draw_box, has_alpha, dither); - case 5: screen_copy_bitmap_32(scr, x, y, src, &draw_box, has_alpha); break; + case 0: rc = screen_copy_bitmap_p1(scr, x, y, src, &draw_box, has_alpha, dither); break; + case 1: rc = screen_copy_bitmap_p2(scr, x, y, src, &draw_box, has_alpha, dither); break; + case 2: rc = screen_copy_bitmap_p4(scr, x, y, src, &draw_box, has_alpha, dither); break; + case 3: rc = screen_copy_bitmap_p8(scr, x, y, src, &draw_box, has_alpha, dither); break; + case 5: rc = screen_copy_bitmap_32(scr, x, y, src, &draw_box, has_alpha); break; default: assert(!"Unimplemented pixel format"); - return result_NOT_SUPPORTED; + rc = result_NOT_SUPPORTED; + break; } - return result_OK; + return rc; } result_t screen_copy_bitmap(screen_t *scr, int x, int y, const bitmap_t *src) diff --git a/libraries/framebuf/screen/test/screen-test.c b/libraries/framebuf/screen/test/screen-test.c index 6d90fbd2..c0afd945 100644 --- a/libraries/framebuf/screen/test/screen-test.c +++ b/libraries/framebuf/screen/test/screen-test.c @@ -1125,6 +1125,98 @@ static result_t test_copy_bitmap_p8(void) #undef P8_ROWBYTES } +/* screen_copy_bitmap with a *paletted* source (p4) onto a 32bpp screen: the + * unpack path in screen_copy_bitmap_i must decode the packed source indices + * through its own palette before the 32bpp blit runs, landing exact colours + * -- unlike the deep-source tests above there is no quantisation step to + * allow slop for. */ +static result_t test_copy_bitmap_paletted_source(void) +{ +#define P4_SRC_ROWBYTES (8 / 2) + static unsigned char p4srcbuf[P4_SRC_ROWBYTES * 2]; + static pixelfmt_any32_t scrbuf[WIDTH * HEIGHT]; + + screen_t scr; + bitmap_t src; + colour_t srcpal[16]; + colour_t got; + int x; + + /* row 0: indices 0..7 packed two to the byte -- even x in the low nibble, + * odd x in the high nibble (matches screen_set_pixel_p4's own bit order) */ + for (x = 0; x < 8; x++) + p4srcbuf[x >> 1] |= (unsigned char) (x << ((x & 1) ? 4 : 0)); + memset(p4srcbuf + P4_SRC_ROWBYTES, 0, P4_SRC_ROWBYTES); /* row 1: all index 0 */ + + for (x = 0; x < 16; x++) + srcpal[x] = colour_rgb((unsigned char) (x * 16), + (unsigned char) (x * 8), + (unsigned char) (255 - x * 16)); + + bitmap_init(&src, SIZE2D(8, 2), pixelfmt_p4, P4_SRC_ROWBYTES, srcpal, + p4srcbuf); + + memset(scrbuf, 0, sizeof(scrbuf)); + screen_init(&scr, SIZE2D(WIDTH, HEIGHT), pixelfmt_rgbx8888, + WIDTH * (int) sizeof(scrbuf[0]), NULL, scrbuf); + + if (screen_copy_bitmap(&scr, 2, 3, &src) != result_OK) + { + printf("screen: copy_bitmap of a p4 source onto a 32bpp screen failed\n"); + return result_TEST_FAILED; + } + + for (x = 0; x < 8; x++) + { + got.primary = scrbuf[3 * WIDTH + 2 + x]; + if (got.primary != srcpal[x].primary) + { + printf("screen: paletted-source blit wrong at x=%d " + "(got 0x%08X want 0x%08X)\n", + x, got.primary, srcpal[x].primary); + return result_TEST_FAILED; + } + } + + /* a pixel just outside the blitted box on every side stays clear, i.e. the + * unpack scratch buffer's extent matches the clipped draw box exactly */ + if (scrbuf[3 * WIDTH + 1] != 0 || scrbuf[3 * WIDTH + 10] != 0 || + scrbuf[2 * WIDTH + 2] != 0 || scrbuf[5 * WIDTH + 2] != 0) + { + printf("screen: paletted-source blit spilled past its box\n"); + return result_TEST_FAILED; + } + + /* a fully-transparent palette entry (as a PNG's tRNS chunk can produce) + * must alpha-test out, same as a transparent rgba8888/bgra8888 source + * pixel would: the unpack step must carry the tRNS-derived alpha through + * rather than baking every unpacked pixel opaque */ + { + pixelfmt_any32_t sentinel = 0xDEADBEEFu; + + srcpal[0] = colour_rgba(0xFF, 0x00, 0x00, 0x00); /* red, fully transparent */ + bitmap_set_palette(&src, srcpal); /* src.palette is its own copy */ + scrbuf[3 * WIDTH + 2] = sentinel; + + if (screen_copy_bitmap(&scr, 2, 3, &src) != result_OK) + { + printf("screen: copy_bitmap of a p4 source (2nd pass) failed\n"); + return result_TEST_FAILED; + } + + if (scrbuf[3 * WIDTH + 2] != sentinel) + { + printf("screen: paletted-source blit drew a fully-transparent index " + "(0x%08X, background was 0x%08X)\n", + scrbuf[3 * WIDTH + 2], sentinel); + return result_TEST_FAILED; + } + } + + return result_TEST_PASSED; +#undef P4_SRC_ROWBYTES +} + /* Unpack the p2 pixel at column "px" of a row's byte buffer (bits 7..6 are * column 0, MSB-first, four to the byte). */ static int p2_pixel_at(const unsigned char *rowbuf, int px) @@ -1359,6 +1451,7 @@ result_t screen_test(const char *resources) test_copy_bitmap_p1, test_copy_bitmap_p2, test_copy_bitmap_p8, + test_copy_bitmap_paletted_source, test_copy_bitmap_dithered, test_copy_rect_packed }; From 701f940d96d490235ecbdda35a4872e84647f08f Mon Sep 17 00:00:00 2001 From: David Thomas Date: Fri, 11 Sep 2026 13:13:35 +0100 Subject: [PATCH 030/128] refactor(framebuf): hoist has-alpha-channel check into pixelfmt_has_alpha The rgba8888/bgra8888 check in screen_copy_bitmap_i's has_alpha computation is a general pixel-format property, not something specific to that call site. Move it to a pixelfmt_has_alpha() macro alongside pixelfmt_is_rle/pixelfmt_paletted_nentries, covering all four 32bpp alpha formats (bgra8888/rgba8888/abgr8888/argb8888). Co-Authored-By: Claude Sonnet 5 --- include/framebuf/pixelfmt.h | 7 +++ libraries/framebuf/screen/screen-draw.c | 62 ++++++++++++------------- 2 files changed, 38 insertions(+), 31 deletions(-) diff --git a/include/framebuf/pixelfmt.h b/include/framebuf/pixelfmt.h index ff6d2d9f..b42d4a09 100644 --- a/include/framebuf/pixelfmt.h +++ b/include/framebuf/pixelfmt.h @@ -114,6 +114,13 @@ typedef unsigned int pixelfmt_xxxa8888_t; /* any 32bpp alpha */ /** Non-zero if `f` names an RLE-compressed pixel store. */ #define pixelfmt_is_rle(f) (((f) & pixelfmt_RLE_FLAG) != 0) +/** Non-zero if `f` is one of the four 32bpp formats with a real alpha + * channel (`bgra8888`/`rgba8888`/`abgr8888`/`argb8888`), as opposed to a + * 32bpp format with a padding byte (`*x8888`) or a paletted/other format. */ +#define pixelfmt_has_alpha(f) \ + ((f) == pixelfmt_bgra8888 || (f) == pixelfmt_rgba8888 || \ + (f) == pixelfmt_abgr8888 || (f) == pixelfmt_argb8888) + /* ----------------------------------------------------------------------- */ typedef unsigned int pixelfmt_any_t; /* generic/unspecified pixel */ diff --git a/libraries/framebuf/screen/screen-draw.c b/libraries/framebuf/screen/screen-draw.c index 430ca05d..4a5a67b4 100644 --- a/libraries/framebuf/screen/screen-draw.c +++ b/libraries/framebuf/screen/screen-draw.c @@ -405,7 +405,7 @@ static colour_t src_fetch_rgba(const bitmap_t *src, case 0: idx = (row[srcx >> 3] >> (7 - (srcx & 7))) & 1; break; case 1: idx = (row[srcx >> 2] >> (6 - ((srcx & 3) << 1))) & 3; break; case 2: idx = (row[srcx >> 1] >> ((srcx & 1) * 4)) & 0xF; break; - default: idx = row[srcx]; break; + default: idx = row[srcx]; break; } c.primary = ((const pixelfmt_rgba8888_t *) srcpm->entries)[idx]; @@ -442,13 +442,13 @@ static result_t screen_copy_bitmap_p4(screen_t *scr, int has_alpha, int dither) { - unsigned char *dstbase; - const pixelmap_t *pm; - const pixelmap_t *srcpm; - int bias[64]; - int do_dither; - int clipped_width, clipped_height; - int yy; + unsigned char *dstbase; + const pixelmap_t *pm; + const pixelmap_t *srcpm; + int bias[64]; + int do_dither; + int clipped_width, clipped_height; + int yy; clipped_width = draw_box->x1 - draw_box->x0; clipped_height = draw_box->y1 - draw_box->y0; @@ -528,13 +528,13 @@ static result_t screen_copy_bitmap_p8(screen_t *scr, int has_alpha, int dither) { - unsigned char *dstbase; - const pixelmap_t *pm; - const pixelmap_t *srcpm; - int bias[64]; - int do_dither; - int clipped_width, clipped_height; - int yy; + unsigned char *dstbase; + const pixelmap_t *pm; + const pixelmap_t *srcpm; + int bias[64]; + int do_dither; + int clipped_width, clipped_height; + int yy; clipped_width = draw_box->x1 - draw_box->x0; clipped_height = draw_box->y1 - draw_box->y0; @@ -609,13 +609,13 @@ static result_t screen_copy_bitmap_p1(screen_t *scr, int has_alpha, int dither) { - unsigned char *dstbase; - const pixelmap_t *pm; - const pixelmap_t *srcpm; - int bias[64]; - int do_dither; - int clipped_width, clipped_height; - int yy; + unsigned char *dstbase; + const pixelmap_t *pm; + const pixelmap_t *srcpm; + int bias[64]; + int do_dither; + int clipped_width, clipped_height; + int yy; clipped_width = draw_box->x1 - draw_box->x0; clipped_height = draw_box->y1 - draw_box->y0; @@ -694,13 +694,13 @@ static result_t screen_copy_bitmap_p2(screen_t *scr, int has_alpha, int dither) { - unsigned char *dstbase; - const pixelmap_t *pm; - const pixelmap_t *srcpm; - int bias[64]; - int do_dither; - int clipped_width, clipped_height; - int yy; + unsigned char *dstbase; + const pixelmap_t *pm; + const pixelmap_t *srcpm; + int bias[64]; + int do_dither; + int clipped_width, clipped_height; + int yy; clipped_width = draw_box->x1 - draw_box->x0; clipped_height = draw_box->y1 - draw_box->y0; @@ -874,7 +874,7 @@ static result_t screen_copy_bitmap_i(screen_t *scr, * so it always carries real per-pixel alpha and must be alpha-tested same * as an rgba8888/bgra8888 source would be. */ has_alpha = pixelfmt_log2bpp(src->format) != 5 || - src->format == pixelfmt_rgba8888 || src->format == pixelfmt_bgra8888; + pixelfmt_has_alpha(src->format); switch (pixelfmt_log2bpp(scr->format)) { @@ -882,7 +882,7 @@ static result_t screen_copy_bitmap_i(screen_t *scr, case 1: rc = screen_copy_bitmap_p2(scr, x, y, src, &draw_box, has_alpha, dither); break; case 2: rc = screen_copy_bitmap_p4(scr, x, y, src, &draw_box, has_alpha, dither); break; case 3: rc = screen_copy_bitmap_p8(scr, x, y, src, &draw_box, has_alpha, dither); break; - case 5: rc = screen_copy_bitmap_32(scr, x, y, src, &draw_box, has_alpha); break; + case 5: rc = screen_copy_bitmap_32(scr, x, y, src, &draw_box, has_alpha); break; default: assert(!"Unimplemented pixel format"); From 510dd97cbcca3176242cbf4a9669b6bdf74158c1 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Fri, 11 Sep 2026 14:51:18 +0100 Subject: [PATCH 031/128] feat(bmfont): draw text at baseline, not glyph-cell top-left bmfont_draw/bmfont_draw_relief's (x,y) now means the text baseline instead of the top-left of the glyph bounding box. bmfont_create derives ascent and descent at load time from the space glyph's cell: the first two full-width rows of palette index 3 (grid) within the cell body mark the baseline and the cell bottom. ttf2bmfont.py already draws both rows at the font's FreeType-derived metrics. A font predating this convention (no grid rows found) falls back to ascent = charheight, descent = 0. bmfont_get_info() gains ascent and descent out-params. wuss's WUSS_TEXT_BASELINE_ADJUST +1px fudge is removed; wuss__text_draw is now a plain pass-through to bmfont_draw. Every caller doing manual top-left-style vertical positioning (furniture titlebar, icon draw helpers, bmtext multi-line draw, wuss SDL test tasks) is updated to add ascent when converting a layout position to a baseline position. docs/framebuf/bmfont.md and ttf2bmfont.py's header comment updated to document the palette-index-3 grid/baseline convention (also fixes a stale 'must be 4bpp' claim; the format is 2bpp). Co-Authored-By: Claude Sonnet 5 --- docs/framebuf/bmfont.md | 23 +++- include/framebuf/bmfont.h | 20 +++- libraries/framebuf/bmfont/bmfont.c | 114 ++++++++++++++++--- libraries/framebuf/bmfont/test/bmfont-test.c | 16 ++- libraries/text/bmtext/draw.c | 7 +- libraries/wuss/component/info.c | 2 +- libraries/wuss/font/font.h | 8 +- libraries/wuss/font/registry.c | 2 +- libraries/wuss/font/text.c | 13 +-- libraries/wuss/furniture/draw.c | 6 +- libraries/wuss/icon/draw.c | 28 +++-- libraries/wuss/menu/menu.c | 2 +- libraries/wuss/test/tasks/chars.c | 16 +-- libraries/wuss/test/tasks/clock.c | 6 +- libraries/wuss/test/tasks/curve.c | 8 +- libraries/wuss/test/tasks/gradient.c | 8 +- libraries/wuss/test/tasks/icons.c | 7 +- libraries/wuss/test/tasks/minesweeper.c | 18 +-- libraries/wuss/test/tasks/palette.c | 8 +- libraries/wuss/test/tasks/porter-duff.c | 11 +- tools/ttf2bmfont.py | 13 ++- 21 files changed, 242 insertions(+), 94 deletions(-) diff --git a/docs/framebuf/bmfont.md b/docs/framebuf/bmfont.md index 0cbfcafa..b595f8ef 100644 --- a/docs/framebuf/bmfont.md +++ b/docs/framebuf/bmfont.md @@ -20,9 +20,16 @@ or even this: ## PNG Font Format -The PNG should be 32 characters wide: bmfont works out the character dimensions from the total PNG size. +The PNG should be 32 characters wide: bmfont works out the character dimensions from the total PNG size. It should be a paletted (colour type 3), 2 bits-per-pixel image with a four-entry palette: -It should be a four colour PNG where pixels of value 1 are font definitions and pixels of value 2 are advance widths. Note: An 8bpp 4-palette-entry image won't do, it must be 4bpp. +- 0 -- background +- 1 -- glyph ink +- 2 -- advance-width marker (row 0 of each glyph cell: one pixel per column, present wherever that column's glyph should draw, sets the advance width) +- 3 -- grid/baseline (see below) + +Pixel value 3 draws the cell's left sidebearing line (cosmetic only) plus two full-width rows that `bmfont_create` reads back to work out `ascent` and `descent`: one at the font's baseline, one at the cell bottom. It scans the space glyph's cell (grid column 0, which carries no ink to interfere) for the first two full-width rows of value-3 pixels within the cell body; the first is the baseline (its offset from the top of the cell is the ascent) and the second is the cell bottom (its offset from the baseline is the descent). A font predating this convention (no such rows found) falls back to treating the whole cell as ascent, with zero descent. + +`tools/ttf2bmfont.py` generates conforming PNGs from a TTF automatically. `--no-grid` omits all the value-3 pixels, baseline row included -- don't pass it, or `bmfont_create` will fall back to the no-descender default for that font. ## Setup @@ -69,7 +76,17 @@ result_t bmfont_draw(bmfont_t *bmfont, It requires a font handle, the screen to draw to, a pointer to some text, the number of characters to consider, foreground and background colours, and a start position. It returns an end position (optional - pass `NULL` if not required). -The screen origin is at the top left. +The screen origin is at the top left. `pos` and `end_pos` are baseline positions, not the top-left of the glyph cells -- use `bmfont_get_info()`'s `ascent` out-param to convert from a top-left layout position (`baseline_y = top_y + ascent`). + +```C +void bmfont_get_info(bmfont_t *bmfont, + int *width, + int *height, + int *ascent, + int *descent); +``` + +Returns the font's cell width, cell height, ascent and descent (any may be `NULL` if not required). ## Limitations diff --git a/include/framebuf/bmfont.h b/include/framebuf/bmfont.h index c4661672..d270f898 100644 --- a/include/framebuf/bmfont.h +++ b/include/framebuf/bmfont.h @@ -83,13 +83,21 @@ void bmfont_destroy(bmfont_t *bmfont); void bmfont_set_flags(bmfont_t *bmfont, bmfont_flags_t flags); /** - * Read the width and height of the specified bitmap font. + * Read the width, height, ascent and descent of the specified bitmap font. * * \param[in] bmfont Bitmap font to query. * \param[out] width Width of the font in pixels, or NULL if not wanted. * \param[out] height Height of the font in pixels, or NULL if not wanted. + * \param[out] ascent Baseline offset from the top of a glyph cell, in + * pixels, or NULL if not wanted. + * \param[out] descent Offset from the baseline to the bottom of a glyph + * cell, in pixels, or NULL if not wanted. */ -void bmfont_get_info(bmfont_t *bmfont, int *width, int *height); +void bmfont_get_info(bmfont_t *bmfont, + int *width, + int *height, + int *ascent, + int *descent); /** * Read the number of glyphs in the specified bitmap font. Glyphs are laid @@ -128,8 +136,8 @@ result_t bmfont_measure(bmfont_t *bmfont, * \param[in] len Length of the string. * \param[in] fg Foreground colour. * \param[in] bg Background colour. - * \param[in] pos Position of the string in pixels. - * \param[out] end_pos End position of the string in pixels. + * \param[in] pos Baseline start position of the string in pixels. + * \param[out] end_pos Baseline end position of the string in pixels. * \return \ref result_OK on success, or appropriate result code otherwise. */ result_t bmfont_draw(bmfont_t *bmfont, @@ -156,9 +164,9 @@ result_t bmfont_draw(bmfont_t *bmfont, * \param[in] len Length of the string. * \param[in] fg Foreground colour, used for the main pass. * \param[in] shadow Shadow colour, used for the offset pass. - * \param[in] pos Position of the main pass in pixels. + * \param[in] pos Baseline position of the main pass in pixels. * \param[in] offset Shadow displacement in pixels, e.g. { 1, 1 }. - * \param[out] end_pos End position of the string in pixels. + * \param[out] end_pos Baseline end position of the string in pixels. * \return \ref result_OK on success, or appropriate result code otherwise. */ result_t bmfont_draw_relief(bmfont_t *bmfont, diff --git a/libraries/framebuf/bmfont/bmfont.c b/libraries/framebuf/bmfont/bmfont.c index c36d75db..9dfb5b97 100644 --- a/libraries/framebuf/bmfont/bmfont.c +++ b/libraries/framebuf/bmfont/bmfont.c @@ -37,7 +37,8 @@ #define PIXEL_BG_IDX (0) /* background */ #define PIXEL_FG_IDX (1) /* font */ #define PIXEL_WIDTH_IDX (2) /* advance widths */ -#define PIXEL_GRID_IDX (3) /* grid (not presently interpreted) */ +#define PIXEL_GRID_IDX (3) /* grid: left sidebearing (cosmetic), baseline and + * cell-bottom rows (read back for ascent/descent) */ /* -------------------------------------------------------------------------- */ @@ -45,6 +46,8 @@ struct bmfont { png_uint_32 gridwidth, gridheight; /* pixels */ png_uint_32 charwidth, charheight; /* em size in pixels */ + int ascent; /* baseline offset from the top of a glyph cell */ + int descent; /* offset from the baseline to the cell bottom */ int totalchars; void *glyphs; @@ -174,6 +177,67 @@ static int detect_gridheight(const unsigned char *pixels, return 0; } +/** True if row \p y of the cell is full-width PIXEL_GRID_IDX. */ +static int row_is_grid(const unsigned char *pixels, + size_t rowbytes, + png_uint_32 gridwidth, + png_uint_32 y) +{ + const unsigned char *row = pixels + rowbytes * y; + png_uint_32 x; + + for (x = 0; x < gridwidth; x++) + { + int px = (row[x >> 2] >> (6 - 2 * (x & 3))) & 3; + if (px != PIXEL_GRID_IDX) + return 0; + } + + return 1; +} + +/** + * Find the baseline and descent within a glyph cell. + * + * The space glyph (grid cell 0, gid 0) carries no ink, so its grid rows -- + * drawn by ttf2bmfont.py as full-width rows of PIXEL_GRID_IDX pixels across + * the cell, one at the baseline and one at the cell bottom -- are + * unambiguous. The first such row within the cell body is the baseline + * (*ascent is its offset from the top of the cell); the next one found at or + * below it is the cell bottom (*descent is its offset from the baseline). + * Returns 0 if no baseline row is found, e.g. a font predating this + * convention -- the caller falls back to treating the whole cell body as + * ascent, with no descender. + */ +static int detect_baseline_metrics(const unsigned char *pixels, + size_t rowbytes, + png_uint_32 gridwidth, + png_uint_32 gridheight, + int *ascent, + int *descent) +{ + png_uint_32 y; + int baseline_y = -1; + + for (y = 1; y < gridheight; y++) /* row 0 is the advance-width strip */ + { + if (!row_is_grid(pixels, rowbytes, gridwidth, y)) + continue; + + if (baseline_y < 0) + { + baseline_y = (int) y; + continue; + } + + *ascent = baseline_y - 1; /* -1: ascent excludes the strip row */ + *descent = (int) y - baseline_y; + return 1; + } + + return 0; +} + /** Verify the font format and build the advance width table. */ static result_t extract_advance_widths(bmfont_t *bmfont, void *voidpixels, @@ -558,11 +622,20 @@ result_t bmfont_create(const char *png, bmfont_t **pbmfont) bmfont->charheight = gridheight - 1; /* -1 for the advance width row */ bmfont->totalchars = CHARS_PER_ROW * pngheight / gridheight; bmfont->glyphrowbytes = (gridwidth + 7) / 8; /* stored glyph row: 1 or 2 */ + if (!detect_baseline_metrics(pixels, pngrowbytes, + (png_uint_32) gridwidth, + (png_uint_32) gridheight, + &bmfont->ascent, &bmfont->descent)) + { + bmfont->ascent = bmfont->charheight; /* no grid row: full height, no + * descender */ + bmfont->descent = 0; + } - logf_info("bmfont load png: charwidth=%d charheight=%d totalchars=%d " - "glyphrowbytes=%d", - bmfont->charwidth, bmfont->charheight, - bmfont->totalchars, bmfont->glyphrowbytes); + logf_info("bmfont load png: charwidth=%d charheight=%d ascent=%d " + "descent=%d totalchars=%d glyphrowbytes=%d", + bmfont->charwidth, bmfont->charheight, bmfont->ascent, + bmfont->descent, bmfont->totalchars, bmfont->glyphrowbytes); rc = extract_advance_widths(bmfont, pixels, pngwidth, pngheight, pngrowbytes); @@ -607,12 +680,20 @@ void bmfont_set_flags(bmfont_t *bmfont, bmfont_flags_t flags) bmfont->flags = flags; } -void bmfont_get_info(bmfont_t *bmfont, int *width, int *height) +void bmfont_get_info(bmfont_t *bmfont, + int *width, + int *height, + int *ascent, + int *descent) { if (width) - *width = bmfont->charwidth; + *width = bmfont->charwidth; if (height) - *height = bmfont->charheight; + *height = bmfont->charheight; + if (ascent) + *ascent = bmfont->ascent; + if (descent) + *descent = bmfont->descent; } int bmfont_get_count(bmfont_t *bmfont) @@ -1700,9 +1781,14 @@ result_t bmfont_draw(bmfont_t *bmfont, box_t scrclip; box_t drawbox; box_t drawclip; + point_t top; unsigned int bgalpha = colour_get_alpha(&bg); + /* pos is the baseline; glyph cells are drawn from their top, ascent above + * the baseline */ + top = POINT(pos->x, pos->y - bmfont->ascent); + switch (scr->format) { case pixelfmt_p1: @@ -1758,10 +1844,10 @@ result_t bmfont_draw(bmfont_t *bmfont, if (screen_get_clip(scr, &scrclip)) return result_OK; /* invalid clipped screen */ - drawbox.x0 = pos->x; - drawbox.y0 = pos->y; - drawbox.x1 = pos->x + bmfont->charwidth * len; /* worst-case estimate */ - drawbox.y1 = pos->y + bmfont->charheight; + drawbox.x0 = top.x; + drawbox.y0 = top.y; + drawbox.x1 = top.x + bmfont->charwidth * len; /* worst-case estimate */ + drawbox.y1 = top.y + bmfont->charheight; if (!box_intersects(&scrclip, &drawbox)) return result_OK; /* not visible */ @@ -1774,8 +1860,8 @@ result_t bmfont_draw(bmfont_t *bmfont, int bottom_skip = drawclip.y1; /* ensure that any computed positions are inside the clip box */ - unsigned int clamped_pos_x = CLAMP(pos->x, scrclip.x0, scrclip.x1 - 1); - unsigned int clamped_pos_y = CLAMP(pos->y, scrclip.y0, scrclip.y1 - 1); + unsigned int clamped_pos_x = CLAMP(top.x, scrclip.x0, scrclip.x1 - 1); + unsigned int clamped_pos_y = CLAMP(top.y, scrclip.y0, scrclip.y1 - 1); int log2bpp = pixelfmt_log2bpp(scr->format); unsigned char *screen = (unsigned char *) scr->base + clamped_pos_y * scr->rowbytes + ((clamped_pos_x << log2bpp) >> 3); diff --git a/libraries/framebuf/bmfont/test/bmfont-test.c b/libraries/framebuf/bmfont/test/bmfont-test.c index 849d1228..7b9f1625 100644 --- a/libraries/framebuf/bmfont/test/bmfont-test.c +++ b/libraries/framebuf/bmfont/test/bmfont-test.c @@ -295,12 +295,13 @@ static result_t bmfont_clipping_test(bmfontteststate_t *state) { for (transparent = 0; transparent < 2; transparent++) { - int fontwidth, fontheight; + int fontwidth, fontheight, fontascent; int i; bitmap_clear(&state->bm, state->palette[palette_PICO8_DARK_GREEN]); - bmfont_get_info(bmfonts[font].bmfont, &fontwidth, &fontheight); + bmfont_get_info(bmfonts[font].bmfont, &fontwidth, &fontheight, + &fontascent, NULL); for (i = 0; i < NELEMS(centres); i++) { @@ -319,7 +320,7 @@ static result_t bmfont_clipping_test(bmfontteststate_t *state) return rc; pos.x = centres[i].x - stringwidth / 2; - pos.y = centres[i].y - fontheight / 2; + pos.y = centres[i].y - fontheight / 2 + fontascent; bg = transparent ? state->transparent : state->palette[palette_PICO8_GREEN]; @@ -377,7 +378,7 @@ static result_t bmfont_layout_test(bmfontteststate_t *state) for (transparent = 0; transparent < 2; transparent++) { bmfont_t *bmfont = bmfonts[font].bmfont; - int glyphwidth, glyphheight; + int glyphwidth, glyphheight, glyphascent; const char *string; size_t stringlen; point_t origin = {0,0}; @@ -385,7 +386,8 @@ static result_t bmfont_layout_test(bmfontteststate_t *state) bitmap_clear(&state->bm, state->palette[palette_PICO8_DARK_GREEN]); - bmfont_get_info(bmfont, &glyphwidth, &glyphheight); + bmfont_get_info(bmfont, &glyphwidth, &glyphheight, &glyphascent, NULL); + origin.y += glyphascent; for (;;) { @@ -916,7 +918,9 @@ result_t bmfont_test_one_format(const char *resources, bmfont_get_info(bmfonts[font].bmfont, &bmfonts[font].width, - &bmfonts[font].height); + &bmfonts[font].height, + NULL, + NULL); } /* ------------------------------------------------------------------------ */ diff --git a/libraries/text/bmtext/draw.c b/libraries/text/bmtext/draw.c index d64a8de7..3da57137 100644 --- a/libraries/text/bmtext/draw.c +++ b/libraries/text/bmtext/draw.c @@ -17,13 +17,14 @@ void bmtext_draw(bmfont_t *font, int leading, point_t origin) { - int font_height; + int font_height, font_ascent; point_t pos; int i; - bmfont_get_info(font, NULL, &font_height); + bmfont_get_info(font, NULL, &font_height, &font_ascent, NULL); - pos = origin; + pos = origin; + pos.y += font_ascent; /* origin is top-left; bmfont_draw wants the baseline */ for (i = 0; i < nlines; i++) { bmfont_draw(font, scr, lines[i].str, lines[i].len, fg, bg, &pos, NULL); diff --git a/libraries/wuss/component/info.c b/libraries/wuss/component/info.c index d901a3bc..6002962b 100644 --- a/libraries/wuss/component/info.c +++ b/libraries/wuss/component/info.c @@ -108,7 +108,7 @@ result_t wuss_info_create(wuss_info_t **out, fonth = 0; if (font != NULL) - bmfont_get_info(font, NULL, &fonth); + bmfont_get_info(font, NULL, &fonth, NULL, NULL); rowh = MAX(fonth + INFO_ROW_PAD, 12) + INFO_ROW_LEADING; fieldh = rowh - INFO_ROW_LEADING; /* rowh is the pitch; leave a gap */ diff --git a/libraries/wuss/font/font.h b/libraries/wuss/font/font.h index 4343c9f7..0dc4df05 100644 --- a/libraries/wuss/font/font.h +++ b/libraries/wuss/font/font.h @@ -39,10 +39,10 @@ void wuss__fontset_init(struct wuss_fontset *set, * regular and bold faces. */ int wuss__fontset_height(const struct wuss_fontset *set, int slot); -/* Centralised text rendering. Every wuss text draw goes through - * wuss__text_draw so an optical vertical bias (WUSS_TEXT_BASELINE_ADJUST, - * default 1px down) is applied uniformly; wuss__text_measure is a plain - * pass-through kept alongside for a single point of policy. */ +/* Centralised text rendering. wuss__text_draw is a plain pass-through to + * bmfont_draw (pos/end_pos are baseline positions, per bmfont_draw's + * contract), kept alongside wuss__text_measure for a single point of + * policy. */ result_t wuss__text_measure(bmfont_t *font, const char *text, int len, diff --git a/libraries/wuss/font/registry.c b/libraries/wuss/font/registry.c index 5787fc0f..9e65e2d2 100644 --- a/libraries/wuss/font/registry.c +++ b/libraries/wuss/font/registry.c @@ -42,7 +42,7 @@ int wuss__fontset_height(const struct wuss_fontset *set, int slot) if (slot < 0 || slot >= wuss_MAX_FONTS || set->fonts[slot] == NULL) return 0; - bmfont_get_info(set->fonts[slot], NULL, &height); + bmfont_get_info(set->fonts[slot], NULL, &height, NULL, NULL); return height; } diff --git a/libraries/wuss/font/text.c b/libraries/wuss/font/text.c index 920fa729..65300040 100644 --- a/libraries/wuss/font/text.c +++ b/libraries/wuss/font/text.c @@ -6,12 +6,6 @@ #include "font.h" -/* Optical vertical bias applied to every text draw so glyphs sit visually - * centred rather than mathematically centred. Positive shifts text down. */ -#ifndef WUSS_TEXT_BASELINE_ADJUST -#define WUSS_TEXT_BASELINE_ADJUST 1 -#endif - /* ----------------------------------------------------------------------- */ result_t wuss__text_measure(bmfont_t *font, @@ -34,10 +28,5 @@ result_t wuss__text_draw(bmfont_t *font, const point_t *pos, point_t *end_pos) { - point_t adjusted; - - adjusted.x = pos->x; - adjusted.y = pos->y + WUSS_TEXT_BASELINE_ADJUST; - - return bmfont_draw(font, scr, text, len, fg, bg, &adjusted, end_pos); + return bmfont_draw(font, scr, text, len, fg, bg, pos, end_pos); } diff --git a/libraries/wuss/furniture/draw.c b/libraries/wuss/furniture/draw.c index 52db2d24..9d3bcd98 100644 --- a/libraries/wuss/furniture/draw.c +++ b/libraries/wuss/furniture/draw.c @@ -67,7 +67,7 @@ static void draw_title(wuss_t *wuss, { bmfont_t *titlefont; point_t pos; - int text_x0, text_x1, titlelen, split_point; + int text_x0, text_x1, titlelen, split_point, ascent; bmfont_width_t width; box_t text_box, text_clip; @@ -121,8 +121,10 @@ static void draw_title(wuss_t *wuss, titlelen = (int) strlen(window->title); wuss__text_measure(titlefont, window->title, titlelen, text_x1 - text_x0, &split_point, &width); + bmfont_get_info(titlefont, NULL, NULL, &ascent, NULL); + pos.x = (split_point < titlelen) ? text_x0 : text_x0 + MAX(0, ((text_x1 - text_x0) - width) / 2); - pos.y = titlebar->y0 + 2; + pos.y = titlebar->y0 + 2 + ascent; wuss->scr->clip = text_clip; wuss__text_draw(titlefont, wuss->scr, window->title, titlelen, wuss->palette[wuss->furniture_colours.title.fg], diff --git a/libraries/wuss/icon/draw.c b/libraries/wuss/icon/draw.c index e081d647..7b48cd25 100644 --- a/libraries/wuss/icon/draw.c +++ b/libraries/wuss/icon/draw.c @@ -17,8 +17,10 @@ /* Shared per-draw state, resolved once by wuss__icon_draw and handed to each * type's draw helper. "b" is the icon's screen box; "fg" is its resolved * foreground; "font" is the icon's selected weight (falling back to slot 0); - * "font_height" is 0 and "have_font" is 0 when there is no font or no text to - * draw. */ + * "font_height", "font_ascent" and "have_font" are 0 when there is no font or + * no text to draw. Draw helpers vertically centre by "font_height" then add + * "font_ascent" to convert the resulting top-left y to the baseline + * wuss__text_draw expects. */ typedef struct icon_draw_ctx { wuss_t *wuss; @@ -29,6 +31,7 @@ typedef struct icon_draw_ctx box_t b; colour_t fg; int font_height; + int font_ascent; int have_font; } icon_draw_ctx_t; @@ -125,7 +128,7 @@ static int wuss__draw_symbol_glyph(const wuss_t *wuss, { bmfont_t *font; point_t pos; - int font_width, font_height; + int font_width, font_height, font_ascent; font = wuss->fonts.fonts[WUSS_SYMBOL_FONT]; if (font == NULL) @@ -135,9 +138,9 @@ static int wuss__draw_symbol_glyph(const wuss_t *wuss, (unsigned char) glyph - ' ' >= bmfont_get_count(font)) return 0; - bmfont_get_info(font, &font_width, &font_height); + bmfont_get_info(font, &font_width, &font_height, &font_ascent, NULL); pos.x = centre.x - font_width / 2; - pos.y = centre.y - font_height / 2; + pos.y = centre.y - font_height / 2 + font_ascent; bmfont_draw(font, scr, &glyph, 1, ink, ground, &pos, NULL); return 1; } @@ -233,7 +236,7 @@ static void wuss__icon_draw_label(const icon_draw_ctx_t *c) else pos.x = b->x0 + 1; - pos.y = b->y0 + (b->y1 - b->y0 - c->font_height) / 2; + pos.y = b->y0 + (b->y1 - b->y0 - c->font_height) / 2 + c->font_ascent; wuss__text_draw(c->font, c->scr, icon->text, (int) strlen(icon->text), c->fg, bg, &pos, NULL); @@ -284,7 +287,7 @@ static void wuss__icon_draw_frame(const icon_draw_ctx_t *c) point_t pos; pos.x = cap_x; - pos.y = b->y0; + pos.y = b->y0 + c->font_ascent; wuss__text_draw(c->font, c->scr, icon->text, (int) strlen(icon->text), c->fg, bg, &pos, NULL); } @@ -349,7 +352,7 @@ static void wuss__icon_draw_button(const icon_draw_ctx_t *c) interior_w, &split_point, &width); pos.x = b->x0 + ((b->x1 - b->x0) - width) / 2; - pos.y = b->y0 + (b->y1 - b->y0 - c->font_height) / 2; + pos.y = b->y0 + (b->y1 - b->y0 - c->font_height) / 2 + c->font_ascent; if (pressed) { pos.x += 1; @@ -470,7 +473,7 @@ static void wuss__icon_draw_radio_option(const icon_draw_ctx_t *c) NOT_USED(width); pos.x = tx; - pos.y = b->y0 + (b->y1 - b->y0 - c->font_height) / 2; + pos.y = b->y0 + (b->y1 - b->y0 - c->font_height) / 2 + c->font_ascent; wuss__text_draw(c->font, c->scr, icon->text, (int) strlen(icon->text), glyph, bg, &pos, NULL); } @@ -600,7 +603,7 @@ static void wuss__icon_draw_menu_entry(const icon_draw_ctx_t *c) wuss__text_measure(c->font, " ", 1, INT_MAX, NULL, &space_w); pos.x = text_x0 + (int) space_w; - pos.y = b->y0 + (b->y1 - b->y0 - c->font_height) / 2; + pos.y = b->y0 + (b->y1 - b->y0 - c->font_height) / 2 + c->font_ascent; wuss__text_draw(c->font, c->scr, icon->text, (int) strlen(icon->text), text_ink, text_ground, &pos, NULL); } @@ -655,9 +658,12 @@ void wuss__icon_draw(wuss_t *wuss, c.have_font = (c.font != NULL && spec->text[0] != '\0'); if (c.have_font) - bmfont_get_info(c.font, NULL, &c.font_height); + bmfont_get_info(c.font, NULL, &c.font_height, &c.font_ascent, NULL); else + { c.font_height = 0; + c.font_ascent = 0; + } switch (spec->type) { diff --git a/libraries/wuss/menu/menu.c b/libraries/wuss/menu/menu.c index dd8c346d..5ecbb022 100644 --- a/libraries/wuss/menu/menu.c +++ b/libraries/wuss/menu/menu.c @@ -618,7 +618,7 @@ static result_t wuss__menu_spawn(wuss_t *wuss, outline_px = wuss__outline_px_for(menu_flags); titlebar_height = wuss__titlebar_height_for(wuss, menu_flags); - bmfont_get_info(wuss->fonts.fonts[0], NULL, &fh); + bmfont_get_info(wuss->fonts.fonts[0], NULL, &fh, NULL, NULL); pitch = fh + 2 * WUSS_MENU_ROW_PAD; sep_h = 2 * WUSS_MENU_ROW_PAD; diff --git a/libraries/wuss/test/tasks/chars.c b/libraries/wuss/test/tasks/chars.c index 2ea57b92..a35cc089 100644 --- a/libraries/wuss/test/tasks/chars.c +++ b/libraries/wuss/test/tasks/chars.c @@ -38,8 +38,8 @@ static size2d_t chars_window_size(chars_task_t *task, bmfont_t *font) { int fw, fh, ifw, ifh, cell_w, cell_h; - bmfont_get_info(font, &fw, &fh); - bmfont_get_info(wuss_get_font(task->wuss), &ifw, &ifh); + bmfont_get_info(font, &fw, &fh, NULL, NULL); + bmfont_get_info(wuss_get_font(task->wuss), &ifw, &ifh, NULL, NULL); cell_w = MAX(fw, ifw * 3) + CHARS_PAD * 2; cell_h = ifh + fh + CHARS_PAD * 3; return SIZE2D(cell_w * CHARS_COLS, cell_h * CHARS_ROWS); @@ -200,7 +200,8 @@ static result_t chars_redraw(const wuss_event_t *event, void *task_data) screen_t *scr; bmfont_t *sysfont; const box_t *bounds; - int font_width, font_height, sysfont_width, sysfont_height; + int font_width, font_height, font_ascent; + int sysfont_width, sysfont_height, sysfont_ascent; int cell_w, cell_h; int first, count; int i, sx, sy; @@ -214,8 +215,9 @@ static result_t chars_redraw(const wuss_event_t *event, void *task_data) sysfont = wuss_get_font(cc->wuss); - bmfont_get_info(cc->font, &font_width, &font_height); - bmfont_get_info(sysfont, &sysfont_width, &sysfont_height); + bmfont_get_info(cc->font, &font_width, &font_height, &font_ascent, NULL); + bmfont_get_info(sysfont, &sysfont_width, &sysfont_height, &sysfont_ascent, + NULL); cell_w = MAX(font_width, sysfont_width * 3) + CHARS_PAD * 2; cell_h = sysfont_height + font_height + CHARS_PAD * 3; @@ -245,7 +247,7 @@ static result_t chars_redraw(const wuss_event_t *event, void *task_data) snprintf(label, 4, "%d", i); pos.x = x + CHARS_PAD; - pos.y = y + CHARS_PAD; + pos.y = y + CHARS_PAD + sysfont_ascent; bmfont_draw(sysfont, scr, label, (int) strlen(label), cc->mg, cc->bg, &pos, NULL); @@ -254,7 +256,7 @@ static result_t chars_redraw(const wuss_event_t *event, void *task_data) ch = (char) i; pos.x = x + CHARS_PAD; - pos.y = y + CHARS_PAD * 2 + sysfont_height; + pos.y = y + CHARS_PAD * 2 + sysfont_height + font_ascent; bmfont_draw(cc->font, scr, &ch, 1, cc->fg, cc->bg, &pos, NULL); } diff --git a/libraries/wuss/test/tasks/clock.c b/libraries/wuss/test/tasks/clock.c index 5c2e99cb..2ada4ff0 100644 --- a/libraries/wuss/test/tasks/clock.c +++ b/libraries/wuss/test/tasks/clock.c @@ -75,14 +75,14 @@ static void clock_draw_centred(bmfont_t *font, { bmfont_width_t width; point_t pos; - int len, fh; + int len, fh, ascent; len = (int) strlen(text); bmfont_measure(font, text, len, INT_MAX, NULL, &width); - bmfont_get_info(font, NULL, &fh); + bmfont_get_info(font, NULL, &fh, &ascent, NULL); pos.x = (int) (x - width / 2.0); - pos.y = (int) (y - fh / 2.0); + pos.y = (int) (y - fh / 2.0) + ascent; bmfont_draw(font, scr, text, len, fg, colour_rgba(0, 0, 0, 0), &pos, NULL); } diff --git a/libraries/wuss/test/tasks/curve.c b/libraries/wuss/test/tasks/curve.c index 8c8b8368..59e4f976 100644 --- a/libraries/wuss/test/tasks/curve.c +++ b/libraries/wuss/test/tasks/curve.c @@ -246,11 +246,17 @@ static result_t curve_redraw(const wuss_event_t *event, curve_task_t *task) { bmfont_t *font = wuss_get_font(task->wuss); const char *name = curve_kind_name(task->npoints); - point_t pos = POINT(bounds->x0 - sx + 2, bounds->y0 - sy + 2); if (font != NULL) + { + int ascent; + point_t pos; + + bmfont_get_info(font, NULL, NULL, &ascent, NULL); + pos = POINT(bounds->x0 - sx + 2, bounds->y0 - sy + 2 + ascent); bmfont_draw(font, scr, name, (int) strlen(name), task->line, task->bg, &pos, NULL); + } } return result_OK; diff --git a/libraries/wuss/test/tasks/gradient.c b/libraries/wuss/test/tasks/gradient.c index 01f24673..9b2bfb24 100644 --- a/libraries/wuss/test/tasks/gradient.c +++ b/libraries/wuss/test/tasks/gradient.c @@ -150,14 +150,20 @@ static result_t gradient_redraw(const wuss_event_t *event, void *task_data) bmfont_t *font = wuss_get_font(gc->wuss); int dim = gradient_dithers[di].dim; char label[8]; - point_t pos = POINT(bounds->x0 - sx + 2, bounds->y0 - sy + 2); colour_t ink = colour_rgb(0xFF, 0xFF, 0xFF); colour_t bg = colour_rgba(0, 0, 0, 0); /* transparent */ sprintf(label, "%dx%d", dim, dim); if (font != NULL) + { + int ascent; + point_t pos; + + bmfont_get_info(font, NULL, NULL, &ascent, NULL); + pos = POINT(bounds->x0 - sx + 2, bounds->y0 - sy + 2 + ascent); bmfont_draw(font, scr, label, (int) strlen(label), ink, bg, &pos, NULL); + } } return result_OK; diff --git a/libraries/wuss/test/tasks/icons.c b/libraries/wuss/test/tasks/icons.c index 13f0bf89..e46db0c0 100644 --- a/libraries/wuss/test/tasks/icons.c +++ b/libraries/wuss/test/tasks/icons.c @@ -657,6 +657,7 @@ static result_t icons_redraw(const wuss_event_t *event, void *task_data) int doc; int x; int y; + int ascent; tcx = task_data; @@ -668,6 +669,8 @@ static result_t icons_redraw(const wuss_event_t *event, void *task_data) ox = bounds->x0 - scroll.x; oy = bounds->y0 - scroll.y; + bmfont_get_info(tcx->font, NULL, NULL, &ascent, NULL); + /* 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 @@ -688,7 +691,7 @@ static result_t icons_redraw(const wuss_event_t *event, void *task_data) continue; snprintf(buf, sizeof(buf), "%d", doc); - pos = POINT(x + 2, oy + 2); + pos = POINT(x + 2, oy + 2 + ascent); bmfont_draw(tcx->font, scr, buf, (int) strlen(buf), tcx->label, tcx->paper, &pos, NULL); } @@ -704,7 +707,7 @@ static result_t icons_redraw(const wuss_event_t *event, void *task_data) continue; snprintf(buf, sizeof(buf), "%d", doc); - pos = POINT(ox + 2, y + 2); + pos = POINT(ox + 2, y + 2 + ascent); bmfont_draw(tcx->font, scr, buf, (int) strlen(buf), tcx->label, tcx->paper, &pos, NULL); } diff --git a/libraries/wuss/test/tasks/minesweeper.c b/libraries/wuss/test/tasks/minesweeper.c index ceeeea27..0fcbb3eb 100644 --- a/libraries/wuss/test/tasks/minesweeper.c +++ b/libraries/wuss/test/tasks/minesweeper.c @@ -260,14 +260,14 @@ static void minesweeper_draw_cell(minesweeper_task_t *ms, char buf[2]; point_t pos; bmfont_width_t width; - int fh; + int fh, ascent; buf[0] = (char) ('0' + n); buf[1] = '\0'; bmfont_measure(ms->font, buf, 1, INT_MAX, NULL, &width); - bmfont_get_info(ms->font, NULL, &fh); + bmfont_get_info(ms->font, NULL, &fh, &ascent, NULL); pos.x = x + (MINESWEEPER_CELL - width) / 2; - pos.y = y + (MINESWEEPER_CELL - fh) / 2; + pos.y = y + (MINESWEEPER_CELL - fh) / 2 + ascent; bmfont_draw(ms->font, scr, buf, 1, minesweeper_number_colour(n), colour_rgba(0, 0, 0, 0), &pos, NULL); } @@ -282,15 +282,15 @@ static void minesweeper_draw_hud(minesweeper_task_t *ms, char buf[8]; point_t pos; colour_t fg, bg; - int fh; + int fh, ascent; fg = colour_rgb(0xFF, 0x00, 0x00); bg = colour_rgb(0x00, 0x00, 0x00); - bmfont_get_info(ms->font, NULL, &fh); + bmfont_get_info(ms->font, NULL, &fh, &ascent, NULL); sprintf(buf, "%03d", MINESWEEPER_MINES - ms->flags); pos.x = bounds->x0 + MS_BORDER; - pos.y = bounds->y0 + (MS_HUD_H - fh) / 2; + pos.y = bounds->y0 + (MS_HUD_H - fh) / 2 + ascent; bmfont_draw(ms->font, scr, buf, 3, fg, bg, &pos, NULL); sprintf(buf, "%03d", ms->elapsed); @@ -309,17 +309,17 @@ static void minesweeper_draw_banner(minesweeper_task_t *ms, { bmfont_width_t width; point_t pos; - int len, fh, strip_y; + int len, fh, ascent, strip_y; len = (int) strlen(text); bmfont_measure(ms->font, text, len, INT_MAX, NULL, &width); - bmfont_get_info(ms->font, NULL, &fh); + bmfont_get_info(ms->font, NULL, &fh, &ascent, NULL); strip_y = bounds->y0 + MS_HUD_H + (MS_GRID_H + 2 * MS_BORDER - fh) / 2 - 2; screen_fill_rect(scr, bounds->x0, strip_y, SIZE2D(MS_WIDTH, fh + 4), bg); pos.x = bounds->x0 + (MS_WIDTH - width) / 2; - pos.y = strip_y + 2; + pos.y = strip_y + 2 + ascent; bmfont_draw(ms->font, scr, text, len, fg, colour_rgba(0, 0, 0, 0), &pos, NULL); } diff --git a/libraries/wuss/test/tasks/palette.c b/libraries/wuss/test/tasks/palette.c index a6970c54..0c915141 100644 --- a/libraries/wuss/test/tasks/palette.c +++ b/libraries/wuss/test/tasks/palette.c @@ -243,7 +243,6 @@ static result_t palette_redraw_screen(palette_task_t *pc, { static const char label[] = "32bpp (none)"; bmfont_t *font = wuss_get_font(pc->wuss); - point_t pos = POINT(bounds->x0 - sx + 2, bounds->y0 - sy + 2); colour_t ink = colour_rgb(0xFF, 0xFF, 0xFF); colour_t bg = colour_rgba(0, 0, 0, 0); /* transparent */ @@ -251,7 +250,14 @@ static result_t palette_redraw_screen(palette_task_t *pc, SIZE2D(bounds->x1 - bounds->x0, bounds->y1 - bounds->y0), colour_rgb(0x00, 0x00, 0x00)); if (font != NULL) + { + int ascent; + point_t pos; + + bmfont_get_info(font, NULL, NULL, &ascent, NULL); + pos = POINT(bounds->x0 - sx + 2, bounds->y0 - sy + 2 + ascent); bmfont_draw(font, scr, label, (int) strlen(label), ink, bg, &pos, NULL); + } return result_OK; } diff --git a/libraries/wuss/test/tasks/porter-duff.c b/libraries/wuss/test/tasks/porter-duff.c index 91fc78ea..6557ec6d 100644 --- a/libraries/wuss/test/tasks/porter-duff.c +++ b/libraries/wuss/test/tasks/porter-duff.c @@ -329,9 +329,14 @@ static result_t porter_duff_redraw(const wuss_event_t *event, screen_copy_bitmap(scr, bounds->x0 - sx, bounds->y0 - sy, &pd->dst); - name = rule_names[pd->rule]; - pos.x = bounds->x0 - sx + 2; - pos.y = bounds->y0 - sy + PD_SIZE + 2; + { + int ascent; + + bmfont_get_info(pd->font, NULL, NULL, &ascent, NULL); + name = rule_names[pd->rule]; + pos.x = bounds->x0 - sx + 2; + pos.y = bounds->y0 - sy + PD_SIZE + 2 + ascent; + } return bmfont_draw(pd->font, scr, name, (int) strlen(name), pd->fg, pd->bg, &pos, NULL); diff --git a/tools/ttf2bmfont.py b/tools/ttf2bmfont.py index e9e7bb07..fd43873e 100755 --- a/tools/ttf2bmfont.py +++ b/tools/ttf2bmfont.py @@ -6,7 +6,8 @@ * 4-entry palette, order is the contract: 0 = background, 1 = glyph ink, 2 = advance-width marker, 3 = grid - (index 3 is loaded but never interpreted -- it is a visual aid only). + (the left-sidebearing line index 3 draws is a visual aid only, but its + baseline and cell-bottom rows are load-bearing -- see below). * exactly 32 glyph cells per row, packed edge to edge; cellW = pngW / 32, and cellW must be <= 16. * the top row of every cell is the "advance strip": it holds `advance` @@ -15,8 +16,14 @@ * the remaining cellH-1 rows are the glyph bitmap; index-1 pixels are ink. * cell 0 is U+0020 (space); codepoints run contiguously upward. There is no character map and no stored first-codepoint. - * there is no baseline in the format; the grey grid lines this script draws - (left sidebearing, baseline, cell bottom) are purely cosmetic. + * the loader derives the font's ascent and descent from cell 0 (space, + which carries no ink to interfere): the first two full-width rows of + index-3 pixels within the cell body are the baseline and the cell + bottom -- ascent is the baseline's offset from the cell top, descent is + the cell bottom's offset from the baseline. This script draws those rows + at `ascent` and `char_h - 1` (see draw_grid below). --no-grid omits them + along with the other grid lines, and the loader then falls back to no + descender space for that font. Requires freetype-py ( pip install freetype-py ). PNG is written by hand so Pillow is not needed. From 399fc95997e9439757c6ce8b69a29adc3241c935 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Fri, 11 Sep 2026 15:19:44 +0100 Subject: [PATCH 032/128] feat(wuss): expose wuss_text_draw/wuss_text_measure as public API wuss__text_draw/wuss__text_measure (libraries/wuss/font/text.c) were internal-only pass-throughs to bmfont_draw/bmfont_measure. Expose them publicly as wuss_text_draw/wuss_text_measure in include/wuss/wuss.h so client tasks can draw/measure text in a wuss system font without including framebuf/bmfont.h themselves. Take (const wuss_t *wuss, int index, ...) rather than a raw bmfont_t*, matching wuss_get_font_n's slot convention; resolves the slot via wuss_get_font_n and returns result_WUSS_BAD_INDEX if it's out of range or empty. Migrate the SDL test tasks that fetch the font directly via wuss_get_font(wuss) inside their own redraw (chars.c, curve.c, gradient.c, palette.c) to the new wuss_text_draw(wuss, 0, ...). Leave clock.c, porter-duff.c, icons.c and minesweeper.c alone: their font arrives as a plain bmfont_t* create-param (minesweeper.c's is even slot 1, not 0), not looked up via a stored wuss_t*+slot in the draw path. Co-Authored-By: Claude Sonnet 5 --- include/wuss/wuss.h | 57 ++++++++++++++++++++++++++++ libraries/wuss/TODO.txt | 2 - libraries/wuss/font/text.c | 39 +++++++++++++++++++ libraries/wuss/test/tasks/chars.c | 4 +- libraries/wuss/test/tasks/curve.c | 4 +- libraries/wuss/test/tasks/gradient.c | 3 +- libraries/wuss/test/tasks/palette.c | 3 +- 7 files changed, 104 insertions(+), 8 deletions(-) diff --git a/include/wuss/wuss.h b/include/wuss/wuss.h index 0f91ac6d..172897f2 100644 --- a/include/wuss/wuss.h +++ b/include/wuss/wuss.h @@ -590,6 +590,63 @@ wuss_font_class_t wuss_get_font_class_n(const wuss_t *wuss, int index); */ const char *wuss_get_font_name_n(const wuss_t *wuss, int index); +/** + * Measure a run of text in one of wuss's configured fonts (see + * wuss_get_font_n). Equivalent to bmfont_measure, kept alongside + * wuss_text_draw so tasks need not include framebuf/bmfont.h themselves for + * simple text. + * + * \param[in] wuss Window manager. + * \param[in] index Font slot, 0..\ref wuss_MAX_FONTS - 1. + * \param[in] text Text to measure. + * \param[in] len Number of characters to consider. + * \param[in] target_width Width to split within, or \ref + * bmfont_width_UNLIMITED for none. + * \param[out] split_point Character count that fits within \p + * target_width, or NULL if not wanted. + * \param[out] actual_width Width of the (possibly split) text, or NULL if + * not wanted. + * \return \ref result_OK on success, \ref result_WUSS_BAD_INDEX if \p index + * is out of range or its slot is empty. + */ +result_t wuss_text_measure(const wuss_t *wuss, + int index, + const char *text, + int len, + bmfont_width_t target_width, + int *split_point, + bmfont_width_t *actual_width); + +/** + * Draw a run of text in one of wuss's configured fonts (see + * wuss_get_font_n). Equivalent to bmfont_draw, kept alongside + * wuss_text_measure so tasks need not include framebuf/bmfont.h themselves + * for simple text. + * + * \param[in] wuss Window manager. + * \param[in] index Font slot, 0..\ref wuss_MAX_FONTS - 1. + * \param[in] scr Screen to draw to. + * \param[in] text Text to draw. + * \param[in] len Number of characters to consider. + * \param[in] fg Foreground (ink) colour. + * \param[in] bg Background colour, or \ref colour_NONE for + * transparent. + * \param[in] pos Baseline position to start drawing at. + * \param[out] end_pos Baseline position after the drawn text, or NULL if not + * wanted. + * \return \ref result_OK on success, \ref result_WUSS_BAD_INDEX if \p index + * is out of range or its slot is empty. + */ +result_t wuss_text_draw(const wuss_t *wuss, + int index, + screen_t *scr, + const char *text, + int len, + colour_t fg, + colour_t bg, + const point_t *pos, + point_t *end_pos); + /** * The last pointer position seen by wuss_mouse_click or wuss_mouse_move, * screen space. (0,0) until the first mouse event. Handy for opening a diff --git a/libraries/wuss/TODO.txt b/libraries/wuss/TODO.txt index 274bff1e..21795aed 100644 --- a/libraries/wuss/TODO.txt +++ b/libraries/wuss/TODO.txt @@ -24,9 +24,7 @@ LAYER 1 - CORE PLUMBING (no wuss deps; unblocks everything else) LAYER 2 - TEXT & FONTS (needs UTF-8, font-arch decision) -- Wuss text abstraction (wraps bmfont, supplies the Wuss font(s)) - How to get bold etc. into the system? How to select symbols? -- bmfont metrics: baselines and leading - button labels look too high (presently bodged) - More symbols diff --git a/libraries/wuss/font/text.c b/libraries/wuss/font/text.c index 65300040..b81c29b4 100644 --- a/libraries/wuss/font/text.c +++ b/libraries/wuss/font/text.c @@ -30,3 +30,42 @@ result_t wuss__text_draw(bmfont_t *font, { return bmfont_draw(font, scr, text, len, fg, bg, pos, end_pos); } + +/* ----------------------------------------------------------------------- */ + +result_t wuss_text_measure(const wuss_t *wuss, + int index, + const char *text, + int len, + bmfont_width_t target_width, + int *split_point, + bmfont_width_t *actual_width) +{ + bmfont_t *font; + + font = wuss_get_font_n(wuss, index); + if (font == NULL) + return result_WUSS_BAD_INDEX; + + return wuss__text_measure(font, text, len, target_width, split_point, + actual_width); +} + +result_t wuss_text_draw(const wuss_t *wuss, + int index, + screen_t *scr, + const char *text, + int len, + colour_t fg, + colour_t bg, + const point_t *pos, + point_t *end_pos) +{ + bmfont_t *font; + + font = wuss_get_font_n(wuss, index); + if (font == NULL) + return result_WUSS_BAD_INDEX; + + return wuss__text_draw(font, scr, text, len, fg, bg, pos, end_pos); +} diff --git a/libraries/wuss/test/tasks/chars.c b/libraries/wuss/test/tasks/chars.c index a35cc089..f09dccc7 100644 --- a/libraries/wuss/test/tasks/chars.c +++ b/libraries/wuss/test/tasks/chars.c @@ -248,8 +248,8 @@ static result_t chars_redraw(const wuss_event_t *event, void *task_data) snprintf(label, 4, "%d", i); pos.x = x + CHARS_PAD; pos.y = y + CHARS_PAD + sysfont_ascent; - bmfont_draw(sysfont, scr, label, (int) strlen(label), cc->mg, cc->bg, - &pos, NULL); + wuss_text_draw(cc->wuss, 0, scr, label, (int) strlen(label), cc->mg, + cc->bg, &pos, NULL); if (i < first || i >= first + count) continue; /* no glyph for this byte value: leave the cell blank */ diff --git a/libraries/wuss/test/tasks/curve.c b/libraries/wuss/test/tasks/curve.c index 59e4f976..7abec00f 100644 --- a/libraries/wuss/test/tasks/curve.c +++ b/libraries/wuss/test/tasks/curve.c @@ -254,8 +254,8 @@ static result_t curve_redraw(const wuss_event_t *event, curve_task_t *task) bmfont_get_info(font, NULL, NULL, &ascent, NULL); pos = POINT(bounds->x0 - sx + 2, bounds->y0 - sy + 2 + ascent); - bmfont_draw(font, scr, name, (int) strlen(name), - task->line, task->bg, &pos, NULL); + wuss_text_draw(task->wuss, 0, scr, name, (int) strlen(name), + task->line, task->bg, &pos, NULL); } } diff --git a/libraries/wuss/test/tasks/gradient.c b/libraries/wuss/test/tasks/gradient.c index 9b2bfb24..3aa7f87e 100644 --- a/libraries/wuss/test/tasks/gradient.c +++ b/libraries/wuss/test/tasks/gradient.c @@ -162,7 +162,8 @@ static result_t gradient_redraw(const wuss_event_t *event, void *task_data) bmfont_get_info(font, NULL, NULL, &ascent, NULL); pos = POINT(bounds->x0 - sx + 2, bounds->y0 - sy + 2 + ascent); - bmfont_draw(font, scr, label, (int) strlen(label), ink, bg, &pos, NULL); + wuss_text_draw(gc->wuss, 0, scr, label, (int) strlen(label), ink, bg, + &pos, NULL); } } diff --git a/libraries/wuss/test/tasks/palette.c b/libraries/wuss/test/tasks/palette.c index 0c915141..f2a55a46 100644 --- a/libraries/wuss/test/tasks/palette.c +++ b/libraries/wuss/test/tasks/palette.c @@ -256,7 +256,8 @@ static result_t palette_redraw_screen(palette_task_t *pc, bmfont_get_info(font, NULL, NULL, &ascent, NULL); pos = POINT(bounds->x0 - sx + 2, bounds->y0 - sy + 2 + ascent); - bmfont_draw(font, scr, label, (int) strlen(label), ink, bg, &pos, NULL); + wuss_text_draw(pc->wuss, 0, scr, label, (int) strlen(label), ink, bg, + &pos, NULL); } return result_OK; } From 3ffe6453c1a87469a2aa0c17a5b833f283b907fd Mon Sep 17 00:00:00 2001 From: David Thomas Date: Fri, 11 Sep 2026 17:03:41 +0100 Subject: [PATCH 033/128] refactor(wuss): reuse array_grow/alloc_strdup helpers in icon module wuss_icon_create hand-rolled its own capacity-doubling growth for window->icons instead of the module's existing wuss__array_grow wrapper, and both wuss_icon_create and wuss_icon_set_text hand-rolled strdup instead of using wuss__alloc_strdup. Switch both to the shared helpers; drop the now-unused stdlib.h/string.h includes. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/icon/create.c | 33 ++++++++++----------------------- libraries/wuss/icon/set-text.c | 15 +++------------ 2 files changed, 13 insertions(+), 35 deletions(-) diff --git a/libraries/wuss/icon/create.c b/libraries/wuss/icon/create.c index 6d9d4d53..4b281d8d 100644 --- a/libraries/wuss/icon/create.c +++ b/libraries/wuss/icon/create.c @@ -1,8 +1,6 @@ /* wuss/icon/create.c -- create a work-area icon */ #include -#include -#include #ifdef FORTIFY #include "fortify/fortify.h" @@ -14,14 +12,10 @@ result_t wuss_icon_create(wuss_window_t *window, const wuss_icon_spec_t *spec, wuss_icon_t **icon) { - result_t rc; - wuss_t *w; - wuss_icon_t *it; - wuss_icon_t **grown; - wuss_icon_t scratch; - const char *src; - size_t len; - int newcap; + result_t rc; + wuss_t *w; + wuss_icon_t *it; + wuss_icon_t scratch; assert(window != NULL); assert(spec != NULL); @@ -32,15 +26,10 @@ result_t wuss_icon_create(wuss_window_t *window, if (rc != result_OK) return rc; - if (window->nicons == window->cap_icons) - { - newcap = (window->cap_icons == 0) ? 4 : window->cap_icons * 2; - grown = wuss__realloc(w, window->icons, newcap * sizeof(*window->icons)); - if (grown == NULL) - return result_OOM; - window->icons = grown; - window->cap_icons = newcap; - } + if (wuss__array_grow(&w->alloc, (void **) &window->icons, + sizeof(*window->icons), window->nicons, + &window->cap_icons, 1, 4) != 0) + return result_OOM; it = wuss__malloc(w, sizeof(*it)); if (it == NULL) @@ -48,15 +37,13 @@ result_t wuss_icon_create(wuss_window_t *window, *it = scratch; /* scratch.spec.text aliases spec->text; replaced with an owned copy below */ - src = (spec->text != NULL) ? spec->text : ""; - len = strlen(src); - it->spec.text = wuss__malloc(w, len + 1); + it->spec.text = wuss__alloc_strdup(&w->alloc, + spec->text != NULL ? spec->text : ""); if (it->spec.text == NULL) { wuss__free(w, it); return result_OOM; } - memcpy((char *) it->spec.text, src, len + 1); window->icons[window->nicons++] = it; diff --git a/libraries/wuss/icon/set-text.c b/libraries/wuss/icon/set-text.c index 395e006f..75c2c12d 100644 --- a/libraries/wuss/icon/set-text.c +++ b/libraries/wuss/icon/set-text.c @@ -1,8 +1,5 @@ /* wuss/icon/set-text.c -- replace a work-area icon's label */ -#include -#include - #ifdef FORTIFY #include "fortify/fortify.h" #endif @@ -13,19 +10,13 @@ result_t wuss_icon_set_text(wuss_window_t *window, wuss_icon_t *icon, const char *text) { - wuss_t *w; - const char *src; - char *dup; - size_t len; + wuss_t *w; + char *dup; w = window->wuss; - src = (text != NULL) ? text : ""; - len = strlen(src); - - dup = wuss__malloc(w, len + 1); + dup = wuss__alloc_strdup(&w->alloc, text != NULL ? text : ""); if (dup == NULL) return result_OOM; - memcpy(dup, src, len + 1); wuss__free(w, (char *) icon->spec.text); icon->spec.text = dup; From 255c6a19230e462e4dad3668e72683b90023685e Mon Sep 17 00:00:00 2001 From: David Thomas Date: Fri, 11 Sep 2026 18:07:11 +0100 Subject: [PATCH 034/128] refactor(wuss)!: rework menu tick API into data-level and live variants Extract the tick-sync loop out of wuss_menu_open_ticked as wuss_menu_tick_set, switch it and wuss_menu_open_ticked to take a bit-vector (unsigned int) instead of an int array, and rename the in-place setters to make clear which act on menu data versus a currently open chain: wuss_menu_set_ticked -> wuss_menu_tick_exclusive_live wuss_menu_set_item_ticked -> wuss_menu_tick_item_live Add data-level counterparts wuss_menu_tick_exclusive/wuss_menu_tick_item for editing menu items before a (re)open, and wuss_menu_tick_set_live to complete the live side, so every data-level setter now has a live equivalent. Group the declarations in menu.h under banners separating the two families, and update every wuss task caller (chars, greeble, image, palette) to match. Co-Authored-By: Claude Sonnet 5 --- include/wuss/menu.h | 155 +++++++++++++++++++--------- libraries/wuss/menu/menu.c | 107 ++++++++++++++----- libraries/wuss/test/tasks/chars.c | 5 +- libraries/wuss/test/tasks/greeble.c | 10 +- libraries/wuss/test/tasks/image.c | 8 +- libraries/wuss/test/tasks/palette.c | 21 ++-- libraries/wuss/test/tasks/palette.h | 2 +- 7 files changed, 214 insertions(+), 94 deletions(-) diff --git a/include/wuss/menu.h b/include/wuss/menu.h index 440fc811..cb7c82b1 100644 --- a/include/wuss/menu.h +++ b/include/wuss/menu.h @@ -134,13 +134,90 @@ void wuss_menu_close(wuss_menu_handle_t handle); int wuss_menu_is_open(wuss_menu_handle_t handle); /** - * Re-tick a currently open menu level in place, for a task that keeps an + * True when \p ev is a wuss_EVENT_MENU_SELECT whose pick keeps the chain + * open -- an ADJUST-button release (see wuss_menu_open). A SELECT-button + * pick has already closed and freed the chain by the time the event arrives, + * so a task that stored the wuss_menu_open handle must drop it in that case: + * + * \code if (!wuss_menu_should_keep_open(ev)) task->menu_handle = NULL; + * \endcode + * + * \param[in] ev The event passed to the task's handle callback. + * \return Non-zero if \p ev is a MENU_SELECT that leaves the chain open. + */ +int wuss_menu_should_keep_open(const wuss_event_t *ev); + +/* ----------------------------------------------------------------------- */ + +/* Setting ticks on menu data, whether or not it is currently open -- a task + * that edits the item array before a (re)opening wuss_menu_open, rather than + * re-ticking an already-open chain. See below for the _live equivalents that + * act on an open chain and redraw the changed rows in place. */ + +/** + * Set every row's tick from \p ticked (row i ticked iff bit i is set) on a + * menu that need not be open. \p menu is mutated (its items' + * wuss_MENU_ITEM_TICKED bit only); every other flag on each item is left as + * the caller set it. \c menu->nitems must not exceed the bit width of + * unsigned int. + * + * \param[in,out] menu Menu whose items' ticks are set. + * \param[in] ticked Bit i ticks row i; 0 unticks every row. + */ +void wuss_menu_tick_set(wuss_menu_t *menu, unsigned int ticked); + +/** + * Tick \p index and untick every other row of \p menu, whether or not it is + * open. Data-level equivalent of wuss_menu_tick_exclusive_live, for a task + * that edits the item array before a fresh wuss_menu_open rather than + * re-ticking an already-open chain. + * + * \param[in,out] menu Menu whose items' ticks are set. + * \param[in] index Row to tick, or -1 to untick every row. + */ +void wuss_menu_tick_exclusive(wuss_menu_t *menu, int index); + +/** + * Set or clear \p index's tick in \p menu, leaving every other item's tick + * untouched, whether or not the menu is open. Data-level equivalent of + * wuss_menu_tick_item_live, for a menu that tracks more than one independent + * tick (see wuss_menu_tick_item_live for why). + * + * \param[in,out] menu Menu whose item's tick is set. + * \param[in] index Row to update. A no-op if out of range. + * \param[in] ticked Non-zero to tick the row, zero to untick it. + */ +void wuss_menu_tick_item(wuss_menu_t *menu, int index, int ticked); + +/** + * Set every row's tick from \p ticked (as wuss_menu_tick_set), then open \p + * menu -- the tick-sync-then-open a task otherwise hand-rolls before each + * wuss_menu_open. + * + * \param[in] task As wuss_menu_open. + * \param[in] menu As wuss_menu_open, but non-const: its ticks are set. + * \param[in] ticked Bit i ticks row i; 0 unticks every row. + * \param[in] at As wuss_menu_open. + * \param[out] out As wuss_menu_open. + * \return As wuss_menu_open. + */ +result_t wuss_menu_open_ticked(wuss_task_t *task, + wuss_menu_t *menu, + unsigned int ticked, + point_t at, + wuss_menu_handle_t *out); + +/* ----------------------------------------------------------------------- */ + +/* Re-ticking a currently open menu level in place, for a task that keeps an * ADJUST-picked menu open (see wuss_menu_open) and wants its own tick to * change without rebuilding the chain: an ADJUST pick delivers * wuss_EVENT_MENU_SELECT but does not close or redraw the menu, so a task * that only edits its wuss_menu_item_t.flags array never sees it take effect - * on screen. - * + * on screen. These invalidate the changed rows; the data-level functions + * above do not. */ + +/** * Ticks item \p index and unticks every other item of the open level whose * \c menu is \p menu (searched from \p handle's chain), then invalidates the * changed rows. A no-op if \p handle is stale/closed, \p menu is not an open @@ -153,62 +230,46 @@ int wuss_menu_is_open(wuss_menu_handle_t handle); * reached via a wuss_menu_item_t.submenu. * \param[in] index Row to tick, or -1 to untick every row. */ -void wuss_menu_set_ticked(wuss_menu_handle_t handle, - const wuss_menu_t *menu, - int index); +void wuss_menu_tick_exclusive_live(wuss_menu_handle_t handle, + const wuss_menu_t *menu, + int index); /** - * Set or clear a single item's tick on a currently open menu level in place, - * leaving every other item's tick untouched. As wuss_menu_set_ticked, but - * for a menu that tracks more than one independent tick at the same level - * (e.g. a selection plus an unrelated toggle) where unticking every other - * row would clobber state the caller wanted to keep. + * Set every row's tick from \p ticked (row i ticked iff bit i is set) on a + * currently open menu level in place, then invalidates the changed rows. + * Live equivalent of wuss_menu_tick_set. A no-op if \p handle is + * stale/closed or \p menu is not an open level of its chain. \c menu->nitems + * must not exceed the bit width of unsigned int. * * \param[in] handle Chain handle from wuss_menu_open. * \param[in] menu The (sub)menu level to update; matched by pointer * against the description passed to wuss_menu_open or * reached via a wuss_menu_item_t.submenu. - * \param[in] index Row to update. A no-op if out of range. - * \param[in] ticked Non-zero to tick the row, zero to untick it. + * \param[in] ticked Bit i ticks row i; 0 unticks every row. */ -void wuss_menu_set_item_ticked(wuss_menu_handle_t handle, - const wuss_menu_t *menu, - int index, - int ticked); +void wuss_menu_tick_set_live(wuss_menu_handle_t handle, + const wuss_menu_t *menu, + unsigned int ticked); /** - * True when \p ev is a wuss_EVENT_MENU_SELECT whose pick keeps the chain - * open -- an ADJUST-button release (see wuss_menu_open). A SELECT-button - * pick has already closed and freed the chain by the time the event arrives, - * so a task that stored the wuss_menu_open handle must drop it in that case: - * - * \code if (!wuss_menu_should_keep_open(ev)) task->menu_handle = NULL; - * \endcode - * - * \param[in] ev The event passed to the task's handle callback. - * \return Non-zero if \p ev is a MENU_SELECT that leaves the chain open. - */ -int wuss_menu_should_keep_open(const wuss_event_t *ev); - -/** - * Set every row's tick from \p ticked (row i ticked iff ticked[i] - * is non-zero), then open \p menu -- the tick-sync-then-open a task - * otherwise hand-rolls before each wuss_menu_open. \p menu is mutated (its - * items' wuss_MENU_ITEM_TICKED bit only); every other flag on each item is - * left as the caller set it. + * Set or clear a single item's tick on a currently open menu level in place, + * leaving every other item's tick untouched. As + * wuss_menu_tick_exclusive_live, but for a menu that tracks more than one + * independent tick at the same level (e.g. a selection plus an unrelated + * toggle) where unticking every other row would clobber state the caller + * wanted to keep. * - * \param[in] task As wuss_menu_open. - * \param[in] menu As wuss_menu_open, but non-const: its ticks are set. - * \param[in] ticked Array of \c menu->nitems flags; NULL unticks every row. - * \param[in] at As wuss_menu_open. - * \param[out] out As wuss_menu_open. - * \return As wuss_menu_open. + * \param[in] handle Chain handle from wuss_menu_open. + * \param[in] menu The (sub)menu level to update; matched by pointer + * against the description passed to wuss_menu_open or + * reached via a wuss_menu_item_t.submenu. + * \param[in] index Row to update. A no-op if out of range. + * \param[in] ticked Non-zero to tick the row, zero to untick it. */ -result_t wuss_menu_open_ticked(wuss_task_t *task, - wuss_menu_t *menu, - const int *ticked, - point_t at, - wuss_menu_handle_t *out); +void wuss_menu_tick_item_live(wuss_menu_handle_t handle, + const wuss_menu_t *menu, + int index, + int ticked); /* ----------------------------------------------------------------------- */ diff --git a/libraries/wuss/menu/menu.c b/libraries/wuss/menu/menu.c index 5ecbb022..06291dba 100644 --- a/libraries/wuss/menu/menu.c +++ b/libraries/wuss/menu/menu.c @@ -907,23 +907,11 @@ int wuss_menu_should_keep_open(const wuss_event_t *ev) result_t wuss_menu_open_ticked(wuss_task_t *task, wuss_menu_t *menu, - const int *ticked, + unsigned int ticked, point_t at, wuss_menu_handle_t *out) { - wuss_menu_item_t *item; - int i; - - assert(menu != NULL); - - for (i = 0; i < menu->nitems; i++) - { - item = (wuss_menu_item_t *) &menu->items[i]; - if (ticked != NULL && ticked[i]) - item->flags |= wuss_MENU_ITEM_TICKED; - else - item->flags &= ~(wuss_menu_item_flags_t) wuss_MENU_ITEM_TICKED; - } + wuss_menu_tick_set(menu, ticked); return wuss_menu_open(task, menu, at, out); } @@ -961,8 +949,8 @@ int wuss_menu_is_open(wuss_menu_handle_t handle) } /* Find the open chain level showing `menu`, or NULL if `handle` is stale or - * `menu` is not a level of its chain. Shared by wuss_menu_set_ticked and - * wuss_menu_set_item_ticked. */ + * `menu` is not a level of its chain. Shared by wuss_menu_tick_exclusive_live + * and wuss_menu_tick_item_live. */ static struct wuss__menu *wuss__menu_open_level(wuss_menu_handle_t handle, const wuss_menu_t *menu) { @@ -986,9 +974,64 @@ static struct wuss__menu *wuss__menu_open_level(wuss_menu_handle_t handle, return NULL; /* menu is not a level of this chain */ } -void wuss_menu_set_ticked(wuss_menu_handle_t handle, - const wuss_menu_t *menu, - int index) +/* ----------------------------------------------------------------------- */ + +void wuss_menu_tick_set(wuss_menu_t *menu, unsigned int ticked) +{ + wuss_menu_item_t *item; + int i; + + assert(menu != NULL); + assert(menu->nitems <= (int) (sizeof(ticked) * CHAR_BIT)); + + for (i = 0; i < menu->nitems; i++) + { + item = (wuss_menu_item_t *) &menu->items[i]; + if (ticked & (1u << i)) + item->flags |= wuss_MENU_ITEM_TICKED; + else + item->flags &= ~(wuss_menu_item_flags_t) wuss_MENU_ITEM_TICKED; + } +} + +void wuss_menu_tick_exclusive(wuss_menu_t *menu, int index) +{ + wuss_menu_item_t *item; + int i; + + assert(menu != NULL); + + for (i = 0; i < menu->nitems; i++) + { + item = (wuss_menu_item_t *) &menu->items[i]; + if (i == index) + item->flags |= wuss_MENU_ITEM_TICKED; + else + item->flags &= ~(wuss_menu_item_flags_t) wuss_MENU_ITEM_TICKED; + } +} + +void wuss_menu_tick_item(wuss_menu_t *menu, int index, int ticked) +{ + wuss_menu_item_t *item; + + assert(menu != NULL); + + if (index < 0 || index >= menu->nitems) + return; + + item = (wuss_menu_item_t *) &menu->items[index]; + if (ticked) + item->flags |= wuss_MENU_ITEM_TICKED; + else + item->flags &= ~(wuss_menu_item_flags_t) wuss_MENU_ITEM_TICKED; +} + +/* ----------------------------------------------------------------------- */ + +void wuss_menu_tick_exclusive_live(wuss_menu_handle_t handle, + const wuss_menu_t *menu, + int index) { struct wuss__menu *node; int i; @@ -1001,10 +1044,28 @@ void wuss_menu_set_ticked(wuss_menu_handle_t handle, wuss_icon_set_selected(node->window, node->icons[i], i == index); } -void wuss_menu_set_item_ticked(wuss_menu_handle_t handle, - const wuss_menu_t *menu, - int index, - int ticked) +void wuss_menu_tick_set_live(wuss_menu_handle_t handle, + const wuss_menu_t *menu, + unsigned int ticked) +{ + struct wuss__menu *node; + int i; + + assert(menu->nitems <= (int) (sizeof(ticked) * CHAR_BIT)); + + node = wuss__menu_open_level(handle, menu); + if (node == NULL) + return; + + for (i = 0; i < menu->nitems; i++) + wuss_icon_set_selected(node->window, node->icons[i], + (ticked & (1u << i)) != 0); +} + +void wuss_menu_tick_item_live(wuss_menu_handle_t handle, + const wuss_menu_t *menu, + int index, + int ticked) { struct wuss__menu *node; diff --git a/libraries/wuss/test/tasks/chars.c b/libraries/wuss/test/tasks/chars.c index f09dccc7..1f982a10 100644 --- a/libraries/wuss/test/tasks/chars.c +++ b/libraries/wuss/test/tasks/chars.c @@ -307,8 +307,9 @@ result_t chars_handle(wuss_window_t *window, /* ADJUST keeps the chain open without rebuilding it, so the * fresh-open tick set in chars_open_menu is now stale on screen; * retick the still-open chain in place to match cc->current */ - wuss_menu_set_ticked(cc->menu_handle, - wuss_fontmenu_menu(cc->fontmenu), cc->current); + wuss_menu_tick_exclusive_live(cc->menu_handle, + wuss_fontmenu_menu(cc->fontmenu), + cc->current); else /* SELECT has already closed and freed the chain by the time this * event arrives; the handle is stale, don't touch it */ diff --git a/libraries/wuss/test/tasks/greeble.c b/libraries/wuss/test/tasks/greeble.c index c2f9755b..f2565f6a 100644 --- a/libraries/wuss/test/tasks/greeble.c +++ b/libraries/wuss/test/tasks/greeble.c @@ -320,9 +320,9 @@ static result_t greeble_menu_select(greeble_task_t *task, rc = greeble_toggle_randpal(task, task->window); if (wuss_menu_should_keep_open(event)) - wuss_menu_set_item_ticked(task->menu_handle, &g_greeble_menu, - GREEBLE_MENU_RANDPAL, - task->random_prefab_palettes); + wuss_menu_tick_item_live(task->menu_handle, &g_greeble_menu, + GREEBLE_MENU_RANDPAL, + task->random_prefab_palettes); else task->menu_handle = NULL; @@ -349,9 +349,9 @@ result_t greeble_handle(wuss_window_t *window, { /* the menu struct is shared by every greeble window; sync its sole * tick to this window's state before it opens */ - int ticks[1]; + unsigned int ticks; - ticks[GREEBLE_MENU_RANDPAL] = task->random_prefab_palettes; + ticks = task->random_prefab_palettes ? 1u << GREEBLE_MENU_RANDPAL : 0; return wuss_menu_open_ticked(task->delegate, &g_greeble_menu, ticks, wuss_get_pointer(task->wuss), &task->menu_handle); diff --git a/libraries/wuss/test/tasks/image.c b/libraries/wuss/test/tasks/image.c index e79cae01..2ce18ee2 100644 --- a/libraries/wuss/test/tasks/image.c +++ b/libraries/wuss/test/tasks/image.c @@ -273,7 +273,7 @@ static result_t image_open_menu(image_task_t *ic) result_t rc; wuss_menu_t *m; int i; - int ticked[7]; + unsigned int ticked; rc = wuss_menu_create_from_desc(&m, "Image, Info, New..., Open, !Dithering, !Wireframe, >Export, |Quit", @@ -297,9 +297,7 @@ static result_t image_open_menu(image_task_t *ic) wuss_menu_destroy(ic->menu); ic->menu = m; - // TODO: Use a bitfield? - memset(ticked, 0, sizeof(ticked)); - ticked[3] = ic->dithering; + ticked = ic->dithering ? (1u << 3) : 0; return wuss_menu_open_ticked(ic->delegate, ic->menu, @@ -348,7 +346,7 @@ result_t image_handle(wuss_window_t *window, if (!wuss_menu_should_keep_open(event)) ic->menu_handle = NULL; /* SELECT pick already freed the chain */ else if (index == 3) - wuss_menu_set_item_ticked(ic->menu_handle, ic->menu, 3, ic->dithering); + wuss_menu_tick_item_live(ic->menu_handle, ic->menu, 3, ic->dithering); return result_OK; case wuss_EVENT_MENU_CLOSED: diff --git a/libraries/wuss/test/tasks/palette.c b/libraries/wuss/test/tasks/palette.c index f2a55a46..592113e1 100644 --- a/libraries/wuss/test/tasks/palette.c +++ b/libraries/wuss/test/tasks/palette.c @@ -276,12 +276,11 @@ static result_t palette_redraw_screen(palette_task_t *pc, * touches the TICKED bit. */ static result_t palette_menu_open(palette_task_t *pc) { - int ticks[PALETTE_MAX_FILES + 1]; - int i; + unsigned int ticks; - for (i = 0; i < pc->nnames; i++) - ticks[i] = (i == pc->selected); - ticks[PALETTE_MENU_INVERT_INDEX(pc)] = pc->invert; + ticks = 1u << pc->selected; + if (pc->invert) + ticks |= 1u << PALETTE_MENU_INVERT_INDEX(pc); return wuss_menu_open_ticked(pc->delegate, &pc->menu, ticks, wuss_get_pointer(pc->wuss), &pc->menu_handle); @@ -305,7 +304,7 @@ static result_t palette_click(palette_task_t *pc, const wuss_event_t *event) * resulting wuss_EVENT_PALETTE and reads the new array back with * wuss_get_palette. A load failure is silently ignored: the picker just * stays on the previous selection. Ticks are also updated in place via - * wuss_menu_set_item_ticked, so an ADJUST pick (which keeps the chain open) + * wuss_menu_tick_item_live, so an ADJUST pick (which keeps the chain open) * shows the new tick without the menu being rebuilt or moved. */ static result_t palette_menu_select(palette_task_t *pc, const wuss_event_t *event) @@ -325,7 +324,7 @@ static result_t palette_menu_select(palette_task_t *pc, { pc->invert = !pc->invert; if (event->data.menu_select.button & wuss_BUTTON_ADJUST) - wuss_menu_set_item_ticked(pc->menu_handle, &pc->menu, index, pc->invert); + wuss_menu_tick_item_live(pc->menu_handle, &pc->menu, index, pc->invert); } else if (index >= 0 && index < pc->nnames) { @@ -334,10 +333,10 @@ static result_t palette_menu_select(palette_task_t *pc, pc->invert = false; if (event->data.menu_select.button & wuss_BUTTON_ADJUST) { - wuss_menu_set_item_ticked(pc->menu_handle, &pc->menu, old, 0); - wuss_menu_set_item_ticked(pc->menu_handle, &pc->menu, index, 1); - wuss_menu_set_item_ticked(pc->menu_handle, &pc->menu, - PALETTE_MENU_INVERT_INDEX(pc), 0); + wuss_menu_tick_item_live(pc->menu_handle, &pc->menu, old, 0); + wuss_menu_tick_item_live(pc->menu_handle, &pc->menu, index, 1); + wuss_menu_tick_item_live(pc->menu_handle, &pc->menu, + PALETTE_MENU_INVERT_INDEX(pc), 0); } } else diff --git a/libraries/wuss/test/tasks/palette.h b/libraries/wuss/test/tasks/palette.h index 18dcc18f..c5abde2e 100644 --- a/libraries/wuss/test/tasks/palette.h +++ b/libraries/wuss/test/tasks/palette.h @@ -53,7 +53,7 @@ typedef struct palette_task * copy built fresh on each open would not do */ wuss_menu_item_t menu_items[PALETTE_MAX_FILES + 1]; wuss_menu_t menu; - wuss_menu_handle_t menu_handle; /* for wuss_menu_set_item_ticked on an + wuss_menu_handle_t menu_handle; /* for wuss_menu_tick_item_live on an * ADJUST pick, which keeps the chain * open */ } From ba9f063975c4488793400b66b7a998b823d399e5 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Fri, 11 Sep 2026 20:11:44 +0100 Subject: [PATCH 035/128] refactor(wuss)!: menu-desc !/~ pull bool vararg instead of always applying wuss_menu_create_from_desc's '!' (tick) and '~' (shade) prefixes used to apply their flag unconditionally, forcing a caller with live state (e.g. a dithering toggle) to build the menu once then mutate ticks via wuss_menu_open_ticked or the item array afterwards. Each now pulls the next int vararg at the point it is scanned (must be 0 or 1; anything else is result_BAD_ARG) and applies the flag iff 1, so the descriptor can encode live state directly. Threaded va_list through getopt_/getname/getnext to do the pull in place, preserving left-to-right vararg consumption order relative to '>' and %s. Updated the one real caller (image.c's Dithering/Wireframe menu) to drop its post-hoc wuss_menu_open_ticked pass, and extended the wuss-test.c coverage for the true/false/invalid cases. Co-Authored-By: Claude Sonnet 5 --- include/wuss/menu-desc.h | 22 +++++++----- libraries/wuss/helpers/create-from-desc.c | 44 +++++++++++++++++------ libraries/wuss/test/tasks/image.c | 19 +++++----- libraries/wuss/test/wuss-test.c | 25 +++++++++++-- 4 files changed, 79 insertions(+), 31 deletions(-) diff --git a/include/wuss/menu-desc.h b/include/wuss/menu-desc.h index 99e87f5f..0eb6c3ad 100644 --- a/include/wuss/menu-desc.h +++ b/include/wuss/menu-desc.h @@ -36,15 +36,20 @@ extern "C" * leading '|' on an item draws a dashed rule above it, marking a group * boundary; the item itself stays an ordinary interactive row. A '{ ... }' * group after an item is that item's submenu. A per-token prefix '!' ticks - * the item, '~' shades (disables) it, and '>' attaches a submenu pulled as a - * const wuss_menu_t * from the varargs rather than from a following - * '{ }' block. A "%s" in a token substitutes the next const char * - * vararg. The '>' and "%s" varargs are consumed in the order they are - * encountered scanning left to right. + * the item and '~' shades (disables) it, each pulling the next int + * vararg -- must be 0 or 1, applying the flag iff 1 -- rather than applying + * unconditionally, so a task's live state drives the tick/shade directly + * instead of building the menu ticked/shaded throughout and mutating it + * afterwards. '>' attaches a submenu pulled as a const wuss_menu_t + * * from the varargs rather than from a following '{ }' block. A "%s" + * in a token substitutes the next const char * vararg. All of '!', + * '~', '>' and "%s" pull their vararg in the order encountered scanning left + * to right. * * Example: wuss_menu_create_from_desc(&m, "Display, Open, !Grid, - * ~Export, |Quit") -- "Display" is the caption; the menu has four - * items, with a dashed rule above "Quit". + * ~Export, |Quit", grid_on, !can_export) -- "Display" is the caption; + * the menu has four items, with a dashed rule above "Quit", "Grid" ticked + * iff \c grid_on and "Export" shaded iff \c can_export is false. * * The whole tree, including copied label text, is one heap allocation graph * owned by the caller; free it with wuss_menu_destroy. wuss_menu_open treats @@ -54,7 +59,8 @@ extern "C" * failure. * \param[in] desc Descriptor string; its first token is the root caption. * \return \ref result_OK, \ref result_OOM, or \ref result_BAD_ARG for a - * malformed descriptor (unbalanced braces, empty token, too deep). + * malformed descriptor (unbalanced braces, empty token, too deep, or + * a '!'/'~' vararg that is not 0 or 1). */ result_t wuss_menu_create_from_desc(wuss_menu_t **out, const char *desc, ...); diff --git a/libraries/wuss/helpers/create-from-desc.c b/libraries/wuss/helpers/create-from-desc.c index baf3dc35..4531349f 100644 --- a/libraries/wuss/helpers/create-from-desc.c +++ b/libraries/wuss/helpers/create-from-desc.c @@ -43,10 +43,16 @@ typedef struct } Parser; -static void getopt_(Parser *parser) +/* '!' and '~' no longer carry a fixed truth -- each pulls the next int + * vararg (must be 0 or 1) at the point it is scanned, so a task's live state + * drives the flag directly instead of ticking/shading everything then + * mutating the string or item array afterwards. Returns 0 on success, -1 if + * a pulled vararg was not 0/1. */ +static int getopt_(Parser *parser, va_list *ap) { const char *p; int c; + int v; parser->flags = 0; p = parser->p; @@ -54,10 +60,25 @@ static void getopt_(Parser *parser) for (;;) { c = *p; - if (c == '!') parser->flags |= Tick; - else if (c == '>') parser->flags |= SubMenu; - else if (c == '~') parser->flags |= Shade; - else { parser->p = p; return; } + switch (c) + { + case '!': + v = va_arg(*ap, int); + if (v != 0 && v != 1) + return -1; + if (v) + parser->flags |= Tick; + break; + case '>': parser->flags |= SubMenu; break; + case '~': + v = va_arg(*ap, int); + if (v != 0 && v != 1) + return -1; + if (v) + parser->flags |= Shade; + break; + default: parser->p = p; return 0; + } p++; } } @@ -67,11 +88,12 @@ static int isdelim(int c) return c == ',' || c == '|' || c == '{' || c == '}' || c == '\0'; } -static void getname(Parser *parser) +static int getname(Parser *parser, va_list *ap) { const char *p; - getopt_(parser); + if (getopt_(parser, ap)) + return -1; p = parser->p; parser->start = p; @@ -85,16 +107,18 @@ static void getname(Parser *parser) p--; parser->end = p; + return 0; } -static Token getnext(Parser *parser) +static Token getnext(Parser *parser, va_list *ap) { while (isspace((unsigned char) *parser->p)) parser->p++; if (!isdelim(*parser->p)) { - getname(parser); + if (getname(parser, ap)) + return Error; if (parser->start == parser->end) return Error; return Name; @@ -339,7 +363,7 @@ result_t wuss_menu_create_from_desc(wuss_menu_t **out, const char *desc, ...) { Token tok; - tok = getnext(&parser); + tok = getnext(&parser, &ap); if (tok == End) break; diff --git a/libraries/wuss/test/tasks/image.c b/libraries/wuss/test/tasks/image.c index 2ce18ee2..1d6850bc 100644 --- a/libraries/wuss/test/tasks/image.c +++ b/libraries/wuss/test/tasks/image.c @@ -273,11 +273,13 @@ static result_t image_open_menu(image_task_t *ic) result_t rc; wuss_menu_t *m; int i; - unsigned int ticked; + /* '!Dithering' pulls ic->dithering directly, so the row's tick already + * matches live state -- no separate wuss_menu_open_ticked pass needed. + * '!Wireframe' has no backing field; it is a demo row always ticked. */ rc = wuss_menu_create_from_desc(&m, "Image, Info, New..., Open, !Dithering, !Wireframe, >Export, |Quit", - &image_menu_export); + ic->dithering, 1, &image_menu_export); if (rc != result_OK) return rc; @@ -296,14 +298,11 @@ static result_t image_open_menu(image_task_t *ic) wuss_menu_destroy(ic->menu); ic->menu = m; - - ticked = ic->dithering ? (1u << 3) : 0; - - return wuss_menu_open_ticked(ic->delegate, - ic->menu, - ticked, - wuss_get_pointer(ic->wuss), - &ic->menu_handle); + + return wuss_menu_open(ic->delegate, + ic->menu, + wuss_get_pointer(ic->wuss), + &ic->menu_handle); } result_t image_handle(wuss_window_t *window, diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index ef4dece8..853ce576 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -4076,11 +4076,13 @@ result_t wuss_test(const char *resources) /* "Root" is the root caption (first token), then items App, * File{ %s title discarded, Grid(!), Export(~), |Quit }, Help(> borrowed), - * with "File" substituted for both %s. '|Quit' sets DASHED on Quit itself, - * so the submenu holds 3 rows. */ + * with "File" substituted for both %s. '!'/'~' now pull an int vararg + * (nonzero applies the flag) rather than always applying, so Grid's tick + * and Export's shade are passed explicitly. '|Quit' sets DASHED on Quit + * itself, so the submenu holds 3 rows. */ rc = wuss_menu_create_from_desc(&m, "Root, App, %s { %s, !Grid, ~Export, |Quit }, >Help", - "File", "File", &borrowed); + "File", "File", 1, 1, &borrowed); if (rc != result_OK) goto Failure; @@ -4110,11 +4112,28 @@ result_t wuss_test(const char *resources) wuss_menu_destroy(m); + /* same desc, both bools false: '!'/'~' apply nothing */ + rc = wuss_menu_create_from_desc(&m, + "Root, App, %s { %s, !Grid, ~Export, |Quit }, >Help", + "File", "File", 0, 0, &borrowed); + if (rc != result_OK) + goto Failure; + sub = m->items[1].submenu; + if (sub == NULL) goto MenuFail; + if (sub->items[0].flags & wuss_MENU_ITEM_TICKED) goto MenuFail; + if (sub->items[1].flags & wuss_MENU_ITEM_DISABLED) goto MenuFail; + wuss_menu_destroy(m); + /* malformed: unbalanced brace */ rc = wuss_menu_create_from_desc(&m, "T, A { B, C"); if (rc != result_BAD_ARG) goto MenuFail; + /* malformed: '!' vararg not 0/1 */ + rc = wuss_menu_create_from_desc(&m, "T, !Grid", 2); + if (rc != result_BAD_ARG) + goto MenuFail; + rc = result_OK; goto MenuOK; From 83b5503b9688206dfd4cad371ff479175503ae8f Mon Sep 17 00:00:00 2001 From: David Thomas Date: Fri, 11 Sep 2026 20:26:48 +0100 Subject: [PATCH 036/128] fix(wuss): clamp blit-shifted pieces to content box before invalidating wuss__blit_pieces translated each clean source piece by the scroll delta and clipped the result only against occluding windows, never against "clip" (set-scroll's content box). A big enough delta -- a fast scroll, especially on a window shrunk after the scroll position was set -- let the shifted piece spill past the content box. That unclamped blit_dest then went straight into wuss__invalidate_minus, marking screen area outside the window dirty, which the next redraw painted with whatever garbage/background sat there. Intersect the shifted piece against "clip" before the occluder clip step so nothing outside the content box ever reaches blit_dest. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/core/window/invalidate.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/libraries/wuss/core/window/invalidate.c b/libraries/wuss/core/window/invalidate.c index d6b7032a..e0885000 100644 --- a/libraries/wuss/core/window/invalidate.c +++ b/libraries/wuss/core/window/invalidate.c @@ -253,6 +253,16 @@ int wuss__blit_pieces(wuss_window_t *window, int nvis; box_translated(&src[i], dx, dy, &want); + + /* A shifted piece can slide past "clip" (set-scroll's content box) -- + * e.g. a fast scroll on a since-shrunk window -- and wuss__clip_to_visible + * below only clips against occluding windows, not against "clip". Left + * unclamped, the excess ends up in blit_dest and is later handed + * unclipped to wuss__invalidate_minus, marking screen outside the window + * dirty for the next redraw. */ + if (clip != NULL && box_intersection(clip, &want, &want)) + continue; + nvis = wuss__clip_to_visible(window, &want, vis); for (j = 0; j < nvis; j++) { From 2b3150a54044380e171fb5b412a31efbea7174bb Mon Sep 17 00:00:00 2001 From: David Thomas Date: Fri, 11 Sep 2026 21:59:09 +0100 Subject: [PATCH 037/128] feat(wuss): add background colour menu to image task image.c gains a "Background" menu row wired to a wuss_colourmenu_t submenu (wuss_colourmenu_create/_menu/_selected/_destroy), letting the user pick the colour filled behind the 9-patch border band. Picking a colour stores it on the task and invalidates the window instead of touching window chrome, so redraw stays task-owned. The submenu pointer patched into the descriptor-built menu tree is borrowed, not owned -- wuss_menu_destroy previously freed every .submenu it found unconditionally (a create-from-desc.c convention, not a rule of the generic menu API), causing a heap-use-after-free when wuss_colourmenu_destroy freed the same tree again at QUIT. Fixed generically: new wuss_MENU_ITEM_BORROWED_SUBMENU item flag tells wuss_menu_destroy to leave a submenu alone instead of recursing into it. Also replaces wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND) with a new wuss_NO_BACKDROP macro at all 14 call sites for readability. Co-Authored-By: Claude Sonnet 5 --- include/wuss/menu.h | 8 +- include/wuss/wuss.h | 4 + libraries/wuss/TODO.txt | 9 +- libraries/wuss/helpers/create-from-desc.c | 3 +- libraries/wuss/test/tasks/ball.c | 2 +- libraries/wuss/test/tasks/chars.c | 2 +- libraries/wuss/test/tasks/checker.c | 4 +- libraries/wuss/test/tasks/clock.c | 2 +- libraries/wuss/test/tasks/curve.c | 2 +- libraries/wuss/test/tasks/gradient.c | 2 +- libraries/wuss/test/tasks/greeble.c | 2 +- libraries/wuss/test/tasks/image.c | 92 +++++++++----- libraries/wuss/test/tasks/image.h | 5 + libraries/wuss/test/tasks/lissajous.c | 2 +- libraries/wuss/test/tasks/minesweeper.c | 2 +- libraries/wuss/test/tasks/porter-duff.c | 2 +- libraries/wuss/test/tasks/saturn.c | 2 +- libraries/wuss/test/tasks/sofa.c | 2 +- libraries/wuss/test/wuss-test.c | 142 +++++++++++----------- resources/wuss/ninepatch.png | Bin 222 -> 196 bytes 20 files changed, 169 insertions(+), 120 deletions(-) diff --git a/include/wuss/menu.h b/include/wuss/menu.h index cb7c82b1..8ce6e163 100644 --- a/include/wuss/menu.h +++ b/include/wuss/menu.h @@ -52,7 +52,13 @@ typedef enum wuss_menu_item_flags /** Draw a colour chip of \c swatch at the item's left edge, in place of * a tick */ - wuss_MENU_ITEM_SWATCH = 1 << 3 + wuss_MENU_ITEM_SWATCH = 1 << 3, + + /** \c submenu is borrowed (e.g. patched in after the menu was built) and + * must outlive the tree, not be freed with it: wuss_menu_destroy leaves + * it alone instead of recursing into it. Ignored on an item with no + * \c submenu. */ + wuss_MENU_ITEM_BORROWED_SUBMENU = 1 << 4 } wuss_menu_item_flags_t; diff --git a/include/wuss/wuss.h b/include/wuss/wuss.h index 172897f2..1599ec57 100644 --- a/include/wuss/wuss.h +++ b/include/wuss/wuss.h @@ -309,6 +309,10 @@ wuss_backdrop_t; #define wuss_BACKDROP_COLOUR(c) \ ((wuss_backdrop_t) { (c), screen_PATTERN_SOLID, wuss_NO_BACKGROUND }) +/** wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND): no fill at all, background + * painting left to the task. */ +#define wuss_NO_BACKDROP wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND) + /** A patterned wuss_backdrop_t: 8x8 pattern p tiled in colour c over * background colour b. */ #define wuss_BACKDROP_PATTERN(c, p, b) ((wuss_backdrop_t) { (c), (p), (b) }) diff --git a/libraries/wuss/TODO.txt b/libraries/wuss/TODO.txt index 21795aed..25857365 100644 --- a/libraries/wuss/TODO.txt +++ b/libraries/wuss/TODO.txt @@ -20,6 +20,7 @@ LAYER 1 - CORE PLUMBING (no wuss deps; unblocks everything else) blocks: text abstraction, window/menu templates - Icon type taxonomy - Window layout / constraint system / stacking primitives? +- I still think there's over-draw when toggling window sizes. LAYER 2 - TEXT & FONTS (needs UTF-8, font-arch decision) @@ -35,13 +36,13 @@ LAYER 3 - SPRITES (needs masks) - Sprite scaling - Import Kare style icons - Sprites noticably brighter / colour matching needs work +- App sprites LAYER 4 - WINDOW FURNITURE & INTERACTION (needs bug layer, text layer) - Set wuss window config after the fact (goes with palette work) blocks: Furniture icons toggling state, min window size -- Proper furniture: close, minimise, maximise, scrolling (half done) - Minimum window size based on furniture requested (later: squash-able furniture) - Furniture icons toggling state when clicked @@ -51,7 +52,6 @@ LAYER 4 - WINDOW FURNITURE & INTERACTION (needs bug layer, text layer) (outline-click bug fixed; unblocked) - 2D Adjust window scroll drag - Input focus, text entry, furniture tinting -- Icon layout - Furniture colour selection needs improvements (e.g. losing menu readability when palette changed) @@ -68,9 +68,9 @@ LAYER 6 - MULTI-TASK (needs stable windows, templates) - Messaging between tasks blocks: Desktop cohort - Backdrop: register events, or a task that covers it like Pinboard -- Icon bar - a separate task? +- Icon bar - a separate task or module? - Test submenu dialogues -- Dialogue abstraction from app engine? +- Pull in dialogue abstraction from app engine? LAYER 7 - APPS & POLISH (needs most of the above) @@ -91,7 +91,6 @@ NO STRONG DEPS (do any time) - Optimise - Generalise APIs (make more Wimp-like) -- Make as much use of DPTLib functions as possible - Fix coordinates to be exactly like Wimp/RO? - Set overall UI scale (like EIG values) - Window stacks (a-la nested wimp) diff --git a/libraries/wuss/helpers/create-from-desc.c b/libraries/wuss/helpers/create-from-desc.c index 4531349f..49b1d4c9 100644 --- a/libraries/wuss/helpers/create-from-desc.c +++ b/libraries/wuss/helpers/create-from-desc.c @@ -148,7 +148,8 @@ void wuss_menu_destroy(wuss_menu_t *menu) for (i = 0; i < menu->nitems; i++) { free((void *) menu->items[i].text); /* discard const */ - wuss_menu_destroy((wuss_menu_t *) menu->items[i].submenu); + if (!(menu->items[i].flags & wuss_MENU_ITEM_BORROWED_SUBMENU)) + wuss_menu_destroy((wuss_menu_t *) menu->items[i].submenu); } free((void *) menu->title); /* discard const */ free((void *) menu->items); /* discard const */ diff --git a/libraries/wuss/test/tasks/ball.c b/libraries/wuss/test/tasks/ball.c index 353d60e8..bc2c1cf9 100644 --- a/libraries/wuss/test/tasks/ball.c +++ b/libraries/wuss/test/tasks/ball.c @@ -83,7 +83,7 @@ result_t ball_create(wuss_t *wuss, ball_task_t *task) SIZE2D(200, 160), "Bouncing Ball", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(200, 160), SIZE2D(0, 0), &task->window); diff --git a/libraries/wuss/test/tasks/chars.c b/libraries/wuss/test/tasks/chars.c index 1f982a10..9974c35f 100644 --- a/libraries/wuss/test/tasks/chars.c +++ b/libraries/wuss/test/tasks/chars.c @@ -184,7 +184,7 @@ result_t chars_create(wuss_t *wuss, grid, "Chars", wuss_WINDOW_NO_HSCROLL, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, grid, SIZE2D(64, 64), &task->window); diff --git a/libraries/wuss/test/tasks/checker.c b/libraries/wuss/test/tasks/checker.c index a9c55ebe..621d9b90 100644 --- a/libraries/wuss/test/tasks/checker.c +++ b/libraries/wuss/test/tasks/checker.c @@ -46,7 +46,7 @@ result_t checker_create(wuss_t*wuss, checker_task_t *task) SIZE2D(160, 160), "Checker 1", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(160, 160), SIZE2D(0, 0), &task->window); @@ -60,7 +60,7 @@ result_t checker_create(wuss_t*wuss, checker_task_t *task) SIZE2D(160, 160), "Checker 2", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(160, 160), SIZE2D(0, 0), &task->window2); diff --git a/libraries/wuss/test/tasks/clock.c b/libraries/wuss/test/tasks/clock.c index 2ada4ff0..97741000 100644 --- a/libraries/wuss/test/tasks/clock.c +++ b/libraries/wuss/test/tasks/clock.c @@ -94,7 +94,7 @@ static result_t clock_create_window(wuss_t *wuss, SIZE2D(160, 160), "Clock", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(160, 160), SIZE2D(0, 0), &task->window); diff --git a/libraries/wuss/test/tasks/curve.c b/libraries/wuss/test/tasks/curve.c index 7abec00f..2e7ec663 100644 --- a/libraries/wuss/test/tasks/curve.c +++ b/libraries/wuss/test/tasks/curve.c @@ -116,7 +116,7 @@ result_t curve_create(wuss_t *wuss, curve_task_t *task) SIZE2D(220, 160), "Curve", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(220, 160), SIZE2D(0, 0), &task->window); diff --git a/libraries/wuss/test/tasks/gradient.c b/libraries/wuss/test/tasks/gradient.c index 3aa7f87e..8c3d813a 100644 --- a/libraries/wuss/test/tasks/gradient.c +++ b/libraries/wuss/test/tasks/gradient.c @@ -103,7 +103,7 @@ result_t gradient_create(wuss_t *wuss, gradient_task_t *task) SIZE2D(GRADIENT_OPEN_WIDTH, GRADIENT_OPEN_HEIGHT), "Gradient", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(GRADIENT_DOC_WIDTH, GRADIENT_DOC_HEIGHT), SIZE2D(0, 0), &task->window); diff --git a/libraries/wuss/test/tasks/greeble.c b/libraries/wuss/test/tasks/greeble.c index f2565f6a..102596ac 100644 --- a/libraries/wuss/test/tasks/greeble.c +++ b/libraries/wuss/test/tasks/greeble.c @@ -414,7 +414,7 @@ result_t greeble_create(wuss_t *wuss, greeble_task_t *task) grid_px, "Greeble", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, grid_px, SIZE2D(0, 0), &task->window); diff --git a/libraries/wuss/test/tasks/image.c b/libraries/wuss/test/tasks/image.c index 1d6850bc..5ddaad31 100644 --- a/libraries/wuss/test/tasks/image.c +++ b/libraries/wuss/test/tasks/image.c @@ -21,10 +21,10 @@ #include "image.h" -#define NINEPATCHSZ 9 +#define NINEPATCHSZ 9 #define IMAGE_BORDERSZ 8 /* solid inset band drawn inside the ninepatch */ #define IMAGE_MARGINSZ (NINEPATCHSZ + IMAGE_BORDERSZ) -#define IMAGE_EXT ".png" +#define IMAGE_EXT ".png" /* screen_copy_bitmap and screen_copy_ninepatch only understand a deep 32bpp * source in R,G,B,A/X byte order (see bitmap_load_png()); bitmap_load_png @@ -78,6 +78,7 @@ result_t image_create(wuss_t *wuss, task->nnames = 0; task->menu = NULL; task->proginfo = NULL; + task->colourmenu = NULL; task->menu_handle = NULL; task->dithering = 1; @@ -131,7 +132,7 @@ result_t image_create(wuss_t *wuss, SIZE2D(sz.w, sz.h), "Image", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(palette_PICO8_PINK), + wuss_NO_BACKDROP, sz, SIZE2D(32, 32), &task->window); @@ -157,18 +158,26 @@ result_t image_create(wuss_t *wuss, task->proginfo = NULL; } + /* The "Background" menu row's submenu. Hung off the descriptor menu as a + * wuss_menu_item_t.submenu in image_open_menu. A create failure is + * non-fatal -- the task just runs without a Background submenu. */ + if (wuss_colourmenu_create(&task->colourmenu, wuss, "Background") != result_OK) + task->colourmenu = NULL; + return result_OK; } static result_t image_redraw(const wuss_event_t *event, void *task_data) { - image_task_t *ic; - screen_t *scr; - const box_t *bounds; - int sx, sy; - int bx, by; - box_t behind; - box_t band[4]; + image_task_t *ic; + screen_t *scr; + const box_t *bounds; + int sx, sy; + int bx, by; + int bx0, bx1, by1; + box_t behind; + box_t band[4]; + const colour_t *palette; ic = task_data; @@ -178,31 +187,23 @@ static result_t image_redraw(const wuss_event_t *event, void *task_data) sy = event->data.redraw.scroll.y; bx = bounds->x0 - sx + IMAGE_MARGINSZ; by = bounds->y0 - sy + IMAGE_MARGINSZ; + bx0 = bx - IMAGE_BORDERSZ; + bx1 = bx + ic->bitmap.size.w + IMAGE_BORDERSZ; + by1 = by + ic->bitmap.size.h; behind.x0 = bx - IMAGE_MARGINSZ; behind.y0 = by - IMAGE_MARGINSZ; behind.x1 = behind.x0 + ic->bitmap.size.w + IMAGE_MARGINSZ * 2; behind.y1 = behind.y0 + ic->bitmap.size.h + IMAGE_MARGINSZ * 2; - screen_copy_ninepatch(scr, &behind, &ic->ninepatch, 0); + screen_copy_ninepatch(scr, &behind, &ic->ninepatch, screen_NINEPATCH_NO_CENTRE); /* solid 8px band between the ninepatch frame and the image */ - band[0].x0 = bx - IMAGE_BORDERSZ; - band[0].y0 = by - IMAGE_BORDERSZ; - band[0].x1 = bx + ic->bitmap.size.w + IMAGE_BORDERSZ; - band[0].y1 = by; /* top */ - band[1].x0 = bx - IMAGE_BORDERSZ; - band[1].y0 = by + ic->bitmap.size.h; - band[1].x1 = bx + ic->bitmap.size.w + IMAGE_BORDERSZ; - band[1].y1 = by + ic->bitmap.size.h + IMAGE_BORDERSZ; /* bottom */ - band[2].x0 = bx - IMAGE_BORDERSZ; - band[2].y0 = by; - band[2].x1 = bx; - band[2].y1 = by + ic->bitmap.size.h; /* left */ - band[3].x0 = bx + ic->bitmap.size.w; - band[3].y0 = by; - band[3].x1 = bx + ic->bitmap.size.w + IMAGE_BORDERSZ; - band[3].y1 = by + ic->bitmap.size.h; /* right */ - screen_fill_rects(scr, band, 4, colour_rgb(0xFF, 0x77, 0xA8)); /* PICO-8 pink */ + band[0] = (box_t) { bx0, by - IMAGE_BORDERSZ, bx1, by }; /* top */ + band[1] = (box_t) { bx0, by1, bx1, by1 + IMAGE_BORDERSZ }; /* bottom */ + band[2] = (box_t) { bx0, by, bx, by1 }; /* left */ + band[3] = (box_t) { bx + ic->bitmap.size.w, by, bx1, by1 }; /* right */ + palette = wuss_get_palette(ic->wuss, NULL); + screen_fill_rects(scr, band, 4, palette[ic->background]); if (ic->dithering) screen_copy_bitmap_dithered(scr, bx, by, &ic->bitmap); @@ -278,7 +279,8 @@ static result_t image_open_menu(image_task_t *ic) * matches live state -- no separate wuss_menu_open_ticked pass needed. * '!Wireframe' has no backing field; it is a demo row always ticked. */ rc = wuss_menu_create_from_desc(&m, - "Image, Info, New..., Open, !Dithering, !Wireframe, >Export, |Quit", + "Image, Info, New..., Open, !Dithering, !Wireframe, >Export, " + "Background, |Quit", ic->dithering, 1, &image_menu_export); if (rc != result_OK) return rc; @@ -296,6 +298,22 @@ static result_t image_open_menu(image_task_t *ic) break; } + /* Same trick for "Background": point it at the colourmenu's own menu tree + * as a submenu, so it gets the usual arrow-and-hover behaviour. Marked + * BORROWED_SUBMENU so wuss_menu_destroy leaves it alone -- it is owned by + * ic->colourmenu, built once at task creation and reused on every open, + * not by this per-open tree. */ + if (ic->colourmenu != NULL) + for (i = 0; i < m->nitems; i++) + if (m->items[i].text != NULL && strcmp(m->items[i].text, "Background") == 0) + { + wuss_menu_item_t *it = (wuss_menu_item_t *) &m->items[i]; + + it->submenu = wuss_colourmenu_menu(ic->colourmenu); + it->flags |= wuss_MENU_ITEM_BORROWED_SUBMENU; + break; + } + wuss_menu_destroy(ic->menu); ic->menu = m; @@ -334,6 +352,21 @@ result_t image_handle(wuss_window_t *window, return result_OK; case wuss_EVENT_MENU_SELECT: + { + wuss_colour_t picked; + int mine; + + picked = wuss_colourmenu_selected(ic->colourmenu, event, &mine); + if (mine) + { + ic->background = picked; + wuss_window_invalidate_visible(ic->window); + if (!wuss_menu_should_keep_open(event)) + ic->menu_handle = NULL; /* SELECT pick already freed the chain */ + return result_OK; + } + } + menu = event->data.menu_select.menu; index = event->data.menu_select.index; printf("image menu: picked \"%s\"\n", @@ -359,6 +392,7 @@ result_t image_handle(wuss_window_t *window, wuss_menu_close(ic->menu_handle); wuss_menu_destroy(ic->menu); wuss_proginfo_destroy(ic->proginfo); /* closes its dialogue window */ + wuss_colourmenu_destroy(ic->colourmenu); free(ic->bitmap.base); free(ic->ninepatch.base); free(ic); /* task_data was calloc'd per instance by the spawner */ diff --git a/libraries/wuss/test/tasks/image.h b/libraries/wuss/test/tasks/image.h index cb2c7547..b5ee9d1a 100644 --- a/libraries/wuss/test/tasks/image.h +++ b/libraries/wuss/test/tasks/image.h @@ -6,6 +6,7 @@ #ifdef WUSS_APP #include "framebuf/bitmap.h" +#include "wuss/component/colourmenu.h" #include "wuss/component/proginfo.h" #include "wuss/menu.h" #include "wuss/window.h" @@ -35,9 +36,13 @@ typedef struct image_task wuss_proginfo_t *proginfo; /* owned: the "Info" row's standard dialogue, * hung off the menu as a wuss_menu_item_t.window; * freed at QUIT */ + wuss_colourmenu_t *colourmenu; /* owned: the "Background" row's submenu, + * hung off the menu as a wuss_menu_item_t.submenu; + * freed at QUIT */ wuss_menu_handle_t menu_handle; /* the open chain, if any -- closed before the * proginfo it borrows is destroyed at QUIT */ int dithering; /* enable Bayer ordered dithering */ + wuss_colour_t background; } image_task_t; diff --git a/libraries/wuss/test/tasks/lissajous.c b/libraries/wuss/test/tasks/lissajous.c index 69a6c04d..43c95a93 100644 --- a/libraries/wuss/test/tasks/lissajous.c +++ b/libraries/wuss/test/tasks/lissajous.c @@ -51,7 +51,7 @@ result_t lissajous_create(wuss_t *wuss, lissajous_task_t *task) SIZE2D(220, 220), "Lissajous", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(220, 220), SIZE2D(0, 0), &task->window); diff --git a/libraries/wuss/test/tasks/minesweeper.c b/libraries/wuss/test/tasks/minesweeper.c index 0fcbb3eb..1d1e9534 100644 --- a/libraries/wuss/test/tasks/minesweeper.c +++ b/libraries/wuss/test/tasks/minesweeper.c @@ -204,7 +204,7 @@ result_t minesweeper_create(wuss_t *wuss, SIZE2D(MS_WIDTH, MS_HEIGHT), "Minesweeper", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(MS_WIDTH, MS_HEIGHT), SIZE2D(0, 0), &task->window); diff --git a/libraries/wuss/test/tasks/porter-duff.c b/libraries/wuss/test/tasks/porter-duff.c index 6557ec6d..a6fcf297 100644 --- a/libraries/wuss/test/tasks/porter-duff.c +++ b/libraries/wuss/test/tasks/porter-duff.c @@ -205,7 +205,7 @@ result_t porter_duff_create(wuss_t *wuss, SIZE2D(PD_SIZE, PD_SIZE + PD_LABEL_HEIGHT), "Porter-Duff", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(PD_SIZE, PD_SIZE + PD_LABEL_HEIGHT), SIZE2D(0, 0), &task->window); diff --git a/libraries/wuss/test/tasks/saturn.c b/libraries/wuss/test/tasks/saturn.c index c28c4ea9..018df527 100644 --- a/libraries/wuss/test/tasks/saturn.c +++ b/libraries/wuss/test/tasks/saturn.c @@ -95,7 +95,7 @@ result_t saturn_create(wuss_t *wuss, saturn_task_t *task) SIZE2D(SATURN_SIZE, SATURN_SIZE), "Saturn", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(SATURN_SIZE, SATURN_SIZE), SIZE2D(0, 0), &task->window); diff --git a/libraries/wuss/test/tasks/sofa.c b/libraries/wuss/test/tasks/sofa.c index fce70650..90285ee4 100644 --- a/libraries/wuss/test/tasks/sofa.c +++ b/libraries/wuss/test/tasks/sofa.c @@ -385,7 +385,7 @@ result_t sofa_create(wuss_t*wuss, sofa_task_t*task) SIZE2D(180, 160), "Sofa", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(180, 160), SIZE2D(0, 0), &task->window); diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 853ce576..a112eead 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -627,7 +627,7 @@ result_t wuss_test(const char *resources) &box_a, "toosmall", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, box_size(&box_a), SIZE2D(0, 0), &win_a); @@ -650,7 +650,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_BACK | wuss_WINDOW_NO_TOGGLE_SIZE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, box_size(&box_a), SIZE2D(0, 0), &win_a); @@ -671,7 +671,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_BACK | wuss_WINDOW_NO_TOGGLE_SIZE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, box_size(&box_b), SIZE2D(0, 0), &win_b); @@ -991,7 +991,7 @@ result_t wuss_test(const char *resources) box_big.x0 = 10; box_big.y0 = 10; box_big.x1 = 400; box_big.y1 = 400; rc = wuss_window_create(mk_task(wuss, NULL, NULL), &box_big, "BIG", wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(400, 400), SIZE2D(0, 0), &win_big); if (rc != result_OK) goto Failure; @@ -1017,7 +1017,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, box_size(&box_d), SIZE2D(0, 0), &win_d); @@ -1064,7 +1064,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, box_size(&box_e), SIZE2D(0, 0), &win_e); @@ -1084,7 +1084,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, box_size(&box_f), SIZE2D(0, 0), &win_f); @@ -1151,7 +1151,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, box_size(&box_h), SIZE2D(0, 0), &win_h); @@ -1171,7 +1171,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, box_size(&box_g), SIZE2D(0, 0), &win_g); @@ -1232,7 +1232,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, box_size(&box_i), SIZE2D(0, 0), &win_i); @@ -1252,7 +1252,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, box_size(&box_j), SIZE2D(0, 0), &win_j); @@ -1302,7 +1302,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, box_size(&box_m), SIZE2D(0, 0), &win_m); @@ -1354,7 +1354,7 @@ result_t wuss_test(const char *resources) &box_h, "H", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, box_size(&box_h), SIZE2D(0, 0), &win_h); @@ -1372,7 +1372,7 @@ result_t wuss_test(const char *resources) &box_g, "G", wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, box_size(&box_g), SIZE2D(0, 0), &win_g); @@ -1491,7 +1491,7 @@ result_t wuss_test(const char *resources) box_m.x0 = 10; box_m.y0 = 10; box_m.x1 = 210; box_m.y1 = 210; /* 200x200 content, floored at 80x60 */ rc = wuss_window_create(delegate_m, &box_m, "M", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(200, 200), SIZE2D(80, 60), &win_m); if (rc != result_OK) @@ -1541,7 +1541,7 @@ result_t wuss_test(const char *resources) box_t_win.x0 = 10; box_t_win.y0 = 10; box_t_win.x1 = 50; box_t_win.y1 = 50; /* 40x40 content, room to grow to a 150x150 doc without the toggled box needing to be repositioned off (10,10) -- this test is about the in-place grow blit */ rc = wuss_window_create(delegate_t, &box_t_win, "T", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(150, 150), SIZE2D(0, 0), &win_t); if (rc != result_OK) goto Failure; @@ -1708,7 +1708,7 @@ result_t wuss_test(const char *resources) box_r.x0 = 10; box_r.y0 = 10; box_r.x1 = 50; box_r.y1 = 50; /* 40x40 content; doc bigger than that, so it starts scrollable */ rc = wuss_window_create(delegate_r, &box_r, "R", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(70, 70), SIZE2D(0, 0), &win_r); if (rc != result_OK) goto Failure; @@ -1813,7 +1813,7 @@ result_t wuss_test(const char *resources) box_d.x0 = 10; box_d.y0 = 10; box_d.x1 = 90; box_d.y1 = 90; /* 80x80 content; doc taller, so it starts scrollable */ rc = wuss_window_create(delegate_d, &box_d, "D", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(80, 140), SIZE2D(0, 0), &win_d); if (rc != result_OK) goto Failure; @@ -1918,7 +1918,7 @@ result_t wuss_test(const char *resources) box_d.x0 = 10; box_d.y0 = 10; box_d.x1 = 110; box_d.y1 = 90; /* 100x80 content; doc taller, so scrollable */ rc = wuss_window_create(delegate_d, &box_d, "D", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(100, 140), SIZE2D(0, 0), &win_d); if (rc != result_OK) goto Failure; @@ -1930,7 +1930,7 @@ result_t wuss_test(const char *resources) box_o.x0 = split_x; box_o.y0 = content_before.y0 - 5; box_o.x1 = content_before.x1 + 40; box_o.y1 = content_before.y1 + 40; rc = wuss_window_create(delegate_o, &box_o, "O", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(box_o.x1 - box_o.x0, box_o.y1 - box_o.y0), SIZE2D(0, 0), &win_o); if (rc != result_OK) @@ -2030,7 +2030,7 @@ result_t wuss_test(const char *resources) box_s.x0 = 10; box_s.y0 = 10; box_s.x1 = 110; box_s.y1 = 90; /* 100x80 content; doc taller, so scrollable */ rc = wuss_window_create(delegate_s, &box_s, "S", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(100, 200), SIZE2D(0, 0), &win_s); if (rc != result_OK) goto Failure; @@ -2041,7 +2041,7 @@ result_t wuss_test(const char *resources) box_o.x0 = split_x; box_o.y0 = content.y0 - 5; box_o.x1 = content.x1 + 40; box_o.y1 = content.y1 + 40; rc = wuss_window_create(delegate_o, &box_o, "O", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(box_o.x1 - box_o.x0, box_o.y1 - box_o.y0), SIZE2D(0, 0), &win_o); if (rc != result_OK) @@ -2133,7 +2133,7 @@ result_t wuss_test(const char *resources) box_m.x0 = 10; box_m.y0 = 10; box_m.x1 = 110; box_m.y1 = 110; /* 100x100 content; doc taller, so scrollable */ rc = wuss_window_create(delegate_m, &box_m, "M", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(100, 300), SIZE2D(0, 0), &win_m); if (rc != result_OK) goto Failure; @@ -2148,7 +2148,7 @@ result_t wuss_test(const char *resources) box_o.x0 = content.x0 - 5; box_o.y0 = content.y0 + 40; box_o.x1 = content.x1 + 5; box_o.y1 = content.y0 + 70; rc = wuss_window_create(delegate_o, &box_o, "O", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(box_o.x1 - box_o.x0, box_o.y1 - box_o.y0), SIZE2D(0, 0), &win_o); if (rc != result_OK) @@ -2214,7 +2214,7 @@ result_t wuss_test(const char *resources) box_m.x0 = 10; box_m.y0 = 10; box_m.x1 = 110; box_m.y1 = 110; /* 100x100 content; doc taller, scrollable */ rc = wuss_window_create(delegate_m, &box_m, "M", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(100, 300), SIZE2D(0, 0), &win_m); if (rc != result_OK) goto Failure; @@ -2225,7 +2225,7 @@ result_t wuss_test(const char *resources) box_o.x0 = content.x0 + 25; box_o.y0 = content.y0 + 25; box_o.x1 = content.x0 + 75; box_o.y1 = content.y0 + 65; rc = wuss_window_create(delegate_o, &box_o, "O", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(box_o.x1 - box_o.x0, box_o.y1 - box_o.y0), SIZE2D(0, 0), &win_o); if (rc != result_OK) @@ -2286,7 +2286,7 @@ result_t wuss_test(const char *resources) box_nb.x0 = 10; box_nb.y0 = 10; box_nb.x1 = 50; box_nb.y1 = 50; /* 40x40 content, room to grow to a 200x200 doc */ rc = wuss_window_create(delegate_nb, &box_nb, "NB", wuss_WINDOW_NO_RESIZE_BLIT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(200, 200), SIZE2D(0, 0), &win_nb); if (rc != result_OK) goto Failure; @@ -2365,7 +2365,7 @@ result_t wuss_test(const char *resources) box_u.x0 = 80; box_u.y0 = 80; box_u.x1 = 120; box_u.y1 = 120; /* 40x40 content */ rc = wuss_window_create(mk_task(wuss, NULL, NULL), &box_u, "U", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, 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; @@ -2475,7 +2475,7 @@ result_t wuss_test(const char *resources) * Doc big enough that maximize is * screen-limited, not doc-limited. */ rc = wuss_window_create(delegate_v, &box_v, "V", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(200, 200), SIZE2D(0, 0), &win_v); if (rc != result_OK) goto Failure; @@ -2590,7 +2590,7 @@ result_t wuss_test(const char *resources) * doc big enough that maximize is * screen-limited so the window must move */ rc = wuss_window_create(delegate_p, &box_p, "P", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(400, 400), SIZE2D(0, 0), &win_p); if (rc != result_OK) goto Failure; @@ -2727,7 +2727,7 @@ result_t wuss_test(const char *resources) box_z.x0 = 20; box_z.y0 = 20; box_z.x1 = 60; box_z.y1 = 60; /* 40x40 content */ rc = wuss_window_create(delegate_z, &box_z, "Z", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, axis == 0 ? SIZE2D(0, 0) : SIZE2D(60, 0), SIZE2D(0, 0), &win_z); if (rc != result_OK) @@ -2823,7 +2823,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_BACK | wuss_WINDOW_NO_TOGGLE_SIZE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, box_size(&box_k), SIZE2D(0, 0), &win_k); @@ -2843,7 +2843,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_BACK | wuss_WINDOW_NO_TOGGLE_SIZE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, box_size(&box_l), SIZE2D(0, 0), &win_l); @@ -2909,7 +2909,7 @@ result_t wuss_test(const char *resources) "M2", wuss_WINDOW_NO_BACK | wuss_WINDOW_NO_TOGGLE_SIZE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, box_size(&box_m2), SIZE2D(0, 0), &win_m2); @@ -2999,7 +2999,7 @@ result_t wuss_test(const char *resources) box_nb2.x0 = 0; box_nb2.y0 = 0; box_nb2.x1 = 40; box_nb2.y1 = 40; rc = wuss_window_create(delegate_nb2, &box_nb2, "NB2", wuss_WINDOW_NO_RESIZE_BLIT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, box_size(&box_nb2), SIZE2D(0, 0), &win_nb2); @@ -3048,7 +3048,7 @@ result_t wuss_test(const char *resources) box_fl.x0 = 0; box_fl.y0 = 0; box_fl.x1 = 60; box_fl.y1 = 60; rc = wuss_window_create(delegate_fl, &box_fl, "FL", wuss_WINDOW_NO_RESIZE_BLIT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, box_size(&box_fl), SIZE2D(0, 0), &win_fl); @@ -3113,7 +3113,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_BACK | wuss_WINDOW_NO_TOGGLE_SIZE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, box_size(&box_n), SIZE2D(0, 0), &win_n); @@ -3131,7 +3131,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_BACK | wuss_WINDOW_NO_TOGGLE_SIZE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, box_size(&box_o), SIZE2D(0, 0), &win_o); @@ -3215,7 +3215,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, box_size(&box_b), SIZE2D(0, 0), &win_b); @@ -3233,7 +3233,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, box_size(&box_a), SIZE2D(0, 0), &win_a); @@ -3316,7 +3316,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, box_size(&box_b), SIZE2D(0, 0), &win_b); @@ -3334,7 +3334,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, box_size(&box_a), SIZE2D(0, 0), &win_a); @@ -3394,7 +3394,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, box_size(&box_b), SIZE2D(0, 0), &win_b); @@ -3412,7 +3412,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, box_size(&box_a), SIZE2D(0, 0), &win_a); @@ -3505,7 +3505,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, box_size(&box_b), SIZE2D(0, 0), &win_b); @@ -3523,7 +3523,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, box_size(&box_a), SIZE2D(0, 0), &win_a); @@ -3599,7 +3599,7 @@ result_t wuss_test(const char *resources) wuss_WINDOW_NO_BACK | wuss_WINDOW_NO_TOGGLE_SIZE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, box_size(&box_c), SIZE2D(0, 0), &win_c); @@ -3633,7 +3633,7 @@ result_t wuss_test(const char *resources) box_s.x0 = 10; box_s.y0 = 10; box_s.x1 = 60; box_s.y1 = 60; /* 50x50 content onto a 200x200 doc: room to scroll */ rc = wuss_window_create(delegate_s, &box_s, "S", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(200, 200), SIZE2D(0, 0), &win_s); if (rc != result_OK) goto Failure; @@ -3708,7 +3708,7 @@ result_t wuss_test(const char *resources) &box_r, "rules", wuss_WINDOW_DEFAULT, /* all furniture present */ - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(400, 400), SIZE2D(0, 0), &win_r); @@ -3752,7 +3752,7 @@ result_t wuss_test(const char *resources) SIZE2D(40, 30), "P", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(40, 30), SIZE2D(0, 0), &win_p[k]); @@ -3775,7 +3775,7 @@ result_t wuss_test(const char *resources) SIZE2D(40, 30), "P", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(40, 30), SIZE2D(0, 0), &win_p[1]); @@ -3858,7 +3858,7 @@ result_t wuss_test(const char *resources) box_ps.x0 = 0; box_ps.y0 = 0; box_ps.x1 = 60; box_ps.y1 = 60; rc = wuss_window_create(delegate_ps, &box_ps, "PS", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, box_size(&box_ps), SIZE2D(0, 0), &win_ps); if (rc != result_OK) goto Failure; @@ -3922,12 +3922,12 @@ result_t wuss_test(const char *resources) box_q.x0 = 0; box_q.y0 = 0; box_q.x1 = 40; box_q.y1 = 40; rc = wuss_window_create(delegate_q, &box_q, "Q1", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, box_size(&box_q), SIZE2D(0, 0), &win_q1); if (rc != result_OK) goto Failure; rc = wuss_window_create(delegate_q, &box_q, "Q2", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, box_size(&box_q), SIZE2D(0, 0), &win_q2); if (rc != result_OK) goto Failure; @@ -3957,12 +3957,12 @@ result_t wuss_test(const char *resources) box_ac.x0 = 0; box_ac.y0 = 0; box_ac.x1 = 40; box_ac.y1 = 40; rc = wuss_window_create(delegate_ac, &box_ac, "AC1", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, box_size(&box_ac), SIZE2D(0, 0), &win_ac1); if (rc != result_OK) goto Failure; rc = wuss_window_create(delegate_ac, &box_ac, "AC2", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, box_size(&box_ac), SIZE2D(0, 0), &win_ac2); if (rc != result_OK) goto Failure; @@ -4013,7 +4013,7 @@ result_t wuss_test(const char *resources) if (delegate_w1 == NULL) goto Failure; wuss_task_set_autoclose(delegate_w1, 1); rc = wuss_window_create(delegate_w1, &box_w, "W1", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, box_size(&box_w), SIZE2D(0, 0), &win_w1); if (rc != result_OK) goto Failure; @@ -4022,7 +4022,7 @@ result_t wuss_test(const char *resources) if (delegate_w2 == NULL) goto Failure; wuss_task_set_autoclose(delegate_w2, 1); rc = wuss_window_create(delegate_w2, &box_w, "W2", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, box_size(&box_w), SIZE2D(0, 0), &g_close_on_idle_win); if (rc != result_OK) @@ -4032,7 +4032,7 @@ result_t wuss_test(const char *resources) if (delegate_w3 == NULL) goto Failure; wuss_task_set_autoclose(delegate_w3, 1); rc = wuss_window_create(delegate_w3, &box_w, "W3", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, box_size(&box_w), SIZE2D(0, 0), &win_w3); if (rc != result_OK) goto Failure; @@ -4647,7 +4647,7 @@ ColourMenuOK: ; wuss_WINDOW_NO_CLOSE | wuss_WINDOW_NO_BACK | wuss_WINDOW_NO_TOGGLE_SIZE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, box_size(&ba), SIZE2D(0, 0), &wa); if (rc != result_OK) goto MoveDestroy; @@ -4656,7 +4656,7 @@ ColourMenuOK: ; wuss_WINDOW_NO_CLOSE | wuss_WINDOW_NO_BACK | wuss_WINDOW_NO_TOGGLE_SIZE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, box_size(&bb), SIZE2D(0, 0), &wb); if (rc != result_OK) goto MoveDestroy; @@ -4785,7 +4785,7 @@ ColourMenuOK: ; wuss_WINDOW_NO_CLOSE | wuss_WINDOW_NO_BACK | wuss_WINDOW_NO_TOGGLE_SIZE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, box_size(&bq), SIZE2D(0, 0), &wq); if (rc != result_OK) goto QuitDestroy; NOT_USED(wq); @@ -4822,7 +4822,7 @@ ColourMenuOK: ; wuss_WINDOW_NO_CLOSE | wuss_WINDOW_NO_BACK | wuss_WINDOW_NO_TOGGLE_SIZE | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, box_size(&bq), SIZE2D(0, 0), &wq); if (rc != result_OK) goto QuitDestroy; wuss_mouse_click(qwuss, POINT(20, 20), wuss_BUTTON_MENU, @@ -4914,7 +4914,7 @@ ColourMenuOK: ; box_hs.x0 = 5; box_hs.y0 = 5; box_hs.x1 = 125; box_hs.y1 = 85; rc = wuss_window_create(delegate_hs, &box_hs, "HS", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(400, 400), SIZE2D(0, 0), &win_hs); if (rc != result_OK) goto Failure; @@ -5032,7 +5032,7 @@ ColourMenuOK: ; box_cb.x0 = 5; box_cb.y0 = 5; box_cb.x1 = 125; box_cb.y1 = 85; rc = wuss_window_create(delegate_cb, &box_cb, "CB", combos[i], - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(400, 400), SIZE2D(0, 0), &win_cb); if (rc != result_OK) goto Failure; @@ -5096,7 +5096,7 @@ ColourMenuOK: ; box_dv.x0 = 5; box_dv.y0 = 5; box_dv.x1 = 125; box_dv.y1 = 85; rc = wuss_window_create(delegate_dv, &box_dv, "DV", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(400, 400), SIZE2D(0, 0), &win_dv); if (rc != result_OK) goto Failure; @@ -5172,7 +5172,7 @@ ColourMenuOK: ; box_ic.x0 = 5; box_ic.y0 = 5; box_ic.x1 = 125; box_ic.y1 = 105; rc = wuss_window_create(delegate_ic, &box_ic, "IC", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(400, 400), SIZE2D(0, 0), &win_ic); if (rc != result_OK) goto Failure; @@ -5268,7 +5268,7 @@ result_t wuss_test(const char *resources) box_a.x0 = 0; box_a.y0 = 0; box_a.x1 = 100; box_a.y1 = 0; rc = wuss_window_create(mk_task(wuss, NULL, NULL), &box_a, "toosmall", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, box_size(&box_a), SIZE2D(0, 0), &win_a); if (rc != result_WUSS_TOO_SMALL) @@ -5285,7 +5285,7 @@ result_t wuss_test(const char *resources) box_a.x0 = 0; box_a.y0 = 0; box_a.x1 = 100; box_a.y1 = 100; rc = wuss_window_create(delegate_a, &box_a, "A", chromeless, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(400, 400), SIZE2D(0, 0), &win_a); if (rc != result_OK) @@ -5293,7 +5293,7 @@ result_t wuss_test(const char *resources) box_b.x0 = 50; box_b.y0 = 50; box_b.x1 = 150; box_b.y1 = 150; rc = wuss_window_create(delegate_b, &box_b, "B", chromeless, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(400, 400), SIZE2D(0, 0), &win_b); if (rc != result_OK) @@ -5349,7 +5349,7 @@ result_t wuss_test(const char *resources) * so the clamp caps content at the full 200x200 screen */ box_big.x0 = 10; box_big.y0 = 10; box_big.x1 = 400; box_big.y1 = 400; rc = wuss_window_create(mk_task(wuss, NULL, NULL), &box_big, "BIG", chromeless, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(400, 400), SIZE2D(0, 0), &win_big); if (rc != result_OK) diff --git a/resources/wuss/ninepatch.png b/resources/wuss/ninepatch.png index 304e2af151478de591561302e36be15078355983..e1afde0379ed94dcd55564ae81fe010399bdebd8 100644 GIT binary patch delta 94 zcmV-k0HOch0mK22ZC64`L_t(2kz-&W2C%{aZW#<&6aiFOEJ|@nA&X$O9;Xys5RV*I zDX8#pmm@dK@Wun)_`?_Pc+(5sbcP~}#nqSq04h)d(LC>n#{d8T07*qoM6N<$f`Gjw AVgLXD delta 120 zcmV-;0Ehp?0p0Fz4geqsLs?kl{`WORN<>TtM?0Cm1o>13WKcSi zFNGS@U4^-kHIqu9@?Ss!D!nXcvDPP8RIf9*NtvO;V+pGEJ$v05zXQD=yF1Mb%^A<< aE!PuXSsJIoQa@(^0000 Date: Fri, 11 Sep 2026 22:28:53 +0100 Subject: [PATCH 038/128] feat(wuss): add grid-size menu to minesweeper task Adds a Grid Size submenu (24x24, 24x12, 16x16, 16x12, 12x12) to the MENU-button menu, radio-ticked to the current size. Picking one resets the board at the new size and resizes the window to fit. Board dimensions move from compile-time MINESWEEPER_ROWS/COLS/MINES defines to runtime task fields (rows/cols/mines), with the mine/state arrays sized for the largest offered grid (24x24). Geometry macros (MS_WIDTH, MS_HEIGHT, MS_GRID_W/H, MS_TIMER_BOX) now take the task pointer instead of being fixed constants. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/test/tasks/minesweeper.c | 135 +++++++++++++++++------- libraries/wuss/test/tasks/minesweeper.h | 29 +++-- 2 files changed, 119 insertions(+), 45 deletions(-) diff --git a/libraries/wuss/test/tasks/minesweeper.c b/libraries/wuss/test/tasks/minesweeper.c index 1d1e9534..6294b448 100644 --- a/libraries/wuss/test/tasks/minesweeper.c +++ b/libraries/wuss/test/tasks/minesweeper.c @@ -23,21 +23,52 @@ #define MS_BORDER MINESWEEPER_CELL /* one grid unit all round */ #define MS_HUD_H MINESWEEPER_CELL /* extra strip above the border for the * mine counter and timer */ -#define MS_GRID_W (MINESWEEPER_COLS * MINESWEEPER_CELL) -#define MS_GRID_H (MINESWEEPER_ROWS * MINESWEEPER_CELL) -#define MS_WIDTH (MS_GRID_W + 2 * MS_BORDER) -#define MS_HEIGHT (MS_GRID_H + 2 * MS_BORDER + MS_HUD_H) + +/* board geometry depends on the current grid size, so these are functions + * rather than macros; MS_GRID_W/H, MS_WIDTH/HEIGHT below are their obvious + * shorthand */ +#define MS_GRID_W(ms) ((ms)->cols * MINESWEEPER_CELL) +#define MS_GRID_H(ms) ((ms)->rows * MINESWEEPER_CELL) +#define MS_WIDTH(ms) (MS_GRID_W(ms) + 2 * MS_BORDER) +#define MS_HEIGHT(ms) (MS_GRID_H(ms) + 2 * MS_BORDER + MS_HUD_H) /* the timer's 3-digit field, window-local content coords: right-aligned in * the HUD strip, mirroring minesweeper_draw_hud's placement */ -#define MS_TIMER_X0 (MS_WIDTH - MS_BORDER - MINESWEEPER_CELL * 2) -#define MS_TIMER_BOX \ - ((box_t) { MS_TIMER_X0, 0, MS_WIDTH - MS_BORDER, MS_HUD_H }) +#define MS_TIMER_X0(ms) (MS_WIDTH(ms) - MS_BORDER - MINESWEEPER_CELL * 2) +#define MS_TIMER_BOX(ms) \ + ((box_t) { MS_TIMER_X0(ms), 0, MS_WIDTH(ms) - MS_BORDER, MS_HUD_H }) + +/* rows, cols, mines offered by the "Grid Size" submenu, indexed by + * minesweeper_size_t; mine counts follow the classic ~14% density */ +static const struct { int rows, cols, mines; } +g_minesweeper_sizes[minesweeper_NSIZES] = +{ + { 24, 24, 99 }, + { 12, 24, 50 }, + { 16, 16, 40 }, + { 12, 16, 30 }, + { 12, 12, 20 } +}; + +static wuss_menu_item_t g_minesweeper_size_items[] = +{ + { "24x24", wuss_MENU_ITEM_NONE, NULL, NULL }, + { "24x12", wuss_MENU_ITEM_NONE, NULL, NULL }, + { "16x16", wuss_MENU_ITEM_NONE, NULL, NULL }, + { "16x12", wuss_MENU_ITEM_NONE, NULL, NULL }, + { "12x12", wuss_MENU_ITEM_NONE, NULL, NULL } +}; -/* MENU click over the board pops this single-item menu */ +static const wuss_menu_t g_minesweeper_size_menu = +{ + "Grid Size", g_minesweeper_size_items, NELEMS(g_minesweeper_size_items) +}; + +/* MENU click over the board pops this menu */ static const wuss_menu_item_t g_minesweeper_menu_items[] = { - { "New Game", wuss_MENU_ITEM_NONE, NULL, NULL } + { "New Game", wuss_MENU_ITEM_NONE, NULL, NULL }, + { "Grid Size", wuss_MENU_ITEM_NONE, &g_minesweeper_size_menu, NULL } }; static const wuss_menu_t g_minesweeper_menu = @@ -64,9 +95,9 @@ static colour_t minesweeper_number_colour(int n) /* ----------------------------------------------------------------------- */ -static bool minesweeper_in_bounds(int r, int c) +static bool minesweeper_in_bounds(minesweeper_task_t *ms, int r, int c) { - return r >= 0 && r < MINESWEEPER_ROWS && c >= 0 && c < MINESWEEPER_COLS; + return r >= 0 && r < ms->rows && c >= 0 && c < ms->cols; } static int minesweeper_count_neighbours(minesweeper_task_t *ms, int r, int c) @@ -77,7 +108,8 @@ static int minesweeper_count_neighbours(minesweeper_task_t *ms, int r, int c) for (dr = -1; dr <= 1; dr++) for (dc = -1; dc <= 1; dc++) if ((dr || dc) && - minesweeper_in_bounds(r + dr, c + dc) && ms->mine[r + dr][c + dc]) + minesweeper_in_bounds(ms, r + dr, c + dc) && + ms->mine[r + dr][c + dc]) n++; return n; @@ -92,10 +124,10 @@ static void minesweeper_place_mines(minesweeper_task_t *ms, int placed, r, c; placed = 0; - while (placed < MINESWEEPER_MINES) + while (placed < ms->mines) { - r = rand() % MINESWEEPER_ROWS; - c = rand() % MINESWEEPER_COLS; + r = rand() % ms->rows; + c = rand() % ms->cols; if (ms->mine[r][c] || (abs(r - safe_r) <= 1 && abs(c - safe_c) <= 1)) continue; @@ -112,7 +144,7 @@ static void minesweeper_reveal(minesweeper_task_t *ms, int r, int c) { int dr, dc; - if (!minesweeper_in_bounds(r, c) || ms->state[r][c] != minesweeper_HIDDEN) + if (!minesweeper_in_bounds(ms, r, c) || ms->state[r][c] != minesweeper_HIDDEN) return; ms->state[r][c] = minesweeper_REVEALED; @@ -135,8 +167,8 @@ static void minesweeper_reveal_all_mines(minesweeper_task_t *ms) { int r, c; - for (r = 0; r < MINESWEEPER_ROWS; r++) - for (c = 0; c < MINESWEEPER_COLS; c++) + for (r = 0; r < ms->rows; r++) + for (c = 0; c < ms->cols; c++) if (ms->mine[r][c]) ms->state[r][c] = minesweeper_REVEALED; } @@ -145,8 +177,8 @@ static bool minesweeper_check_won(minesweeper_task_t *ms) { int r, c; - for (r = 0; r < MINESWEEPER_ROWS; r++) - for (c = 0; c < MINESWEEPER_COLS; c++) + for (r = 0; r < ms->rows; r++) + for (c = 0; c < ms->cols; c++) if (!ms->mine[r][c] && ms->state[r][c] != minesweeper_REVEALED) return false; @@ -164,6 +196,19 @@ static void minesweeper_reset(minesweeper_task_t *ms) memset(ms->state, 0, sizeof(ms->state)); /* minesweeper_HIDDEN == 0 */ } +/* applies a grid size (rows/cols/mines) and clears the board; does not touch + * the window, so it is also used at creation before task->window exists -- + * the wuss_EVENT_MENU_SELECT handler resizes the window itself afterwards */ +static void minesweeper_set_size(minesweeper_task_t *ms, + minesweeper_size_t size) +{ + ms->size = size; + ms->rows = g_minesweeper_sizes[size].rows; + ms->cols = g_minesweeper_sizes[size].cols; + ms->mines = g_minesweeper_sizes[size].mines; + minesweeper_reset(ms); +} + /* seconds since the first reveal, frozen once the game has ended */ static void minesweeper_tick_clock(minesweeper_task_t *ms) { @@ -186,7 +231,7 @@ result_t minesweeper_create(wuss_t *wuss, task->wuss = wuss; task->font = font; - minesweeper_reset(task); + minesweeper_set_size(task, minesweeper_SIZE_12X12); delegate_desc.handle = minesweeper_handle; delegate_desc.task_data = task; @@ -201,11 +246,11 @@ result_t minesweeper_create(wuss_t *wuss, wuss_task_set_autoclose(delegate, 1); rc = wuss_window_create_placed(delegate, - SIZE2D(MS_WIDTH, MS_HEIGHT), + SIZE2D(MS_WIDTH(task), MS_HEIGHT(task)), "Minesweeper", wuss_WINDOW_DEFAULT, wuss_NO_BACKDROP, - SIZE2D(MS_WIDTH, MS_HEIGHT), + SIZE2D(MS_WIDTH(task), MS_HEIGHT(task)), SIZE2D(0, 0), &task->window); if (rc != result_OK) @@ -288,13 +333,13 @@ static void minesweeper_draw_hud(minesweeper_task_t *ms, bg = colour_rgb(0x00, 0x00, 0x00); bmfont_get_info(ms->font, NULL, &fh, &ascent, NULL); - sprintf(buf, "%03d", MINESWEEPER_MINES - ms->flags); + sprintf(buf, "%03d", ms->mines - ms->flags); pos.x = bounds->x0 + MS_BORDER; pos.y = bounds->y0 + (MS_HUD_H - fh) / 2 + ascent; bmfont_draw(ms->font, scr, buf, 3, fg, bg, &pos, NULL); sprintf(buf, "%03d", ms->elapsed); - pos.x = bounds->x0 + MS_WIDTH - MS_BORDER - MINESWEEPER_CELL * 2; + pos.x = bounds->x0 + MS_WIDTH(ms) - MS_BORDER - MINESWEEPER_CELL * 2; bmfont_draw(ms->font, scr, buf, 3, fg, bg, &pos, NULL); } @@ -315,10 +360,10 @@ static void minesweeper_draw_banner(minesweeper_task_t *ms, bmfont_measure(ms->font, text, len, INT_MAX, NULL, &width); bmfont_get_info(ms->font, NULL, &fh, &ascent, NULL); - strip_y = bounds->y0 + MS_HUD_H + (MS_GRID_H + 2 * MS_BORDER - fh) / 2 - 2; - screen_fill_rect(scr, bounds->x0, strip_y, SIZE2D(MS_WIDTH, fh + 4), bg); + strip_y = bounds->y0 + MS_HUD_H + (MS_GRID_H(ms) + 2 * MS_BORDER - fh) / 2 - 2; + screen_fill_rect(scr, bounds->x0, strip_y, SIZE2D(MS_WIDTH(ms), fh + 4), bg); - pos.x = bounds->x0 + (MS_WIDTH - width) / 2; + pos.x = bounds->x0 + (MS_WIDTH(ms) - width) / 2; pos.y = strip_y + 2 + ascent; bmfont_draw(ms->font, scr, text, len, fg, colour_rgba(0, 0, 0, 0), &pos, NULL); @@ -350,7 +395,7 @@ static result_t minesweeper_redraw(const wuss_event_t *event, /* the window owns every pixel (wuss_NO_BACKGROUND), so the HUD strip's own * backdrop is painted here before the counters go on top */ - screen_fill_rect(scr, bounds->x0, bounds->y0, SIZE2D(MS_WIDTH, MS_HUD_H), + screen_fill_rect(scr, bounds->x0, bounds->y0, SIZE2D(MS_WIDTH(ms), MS_HUD_H), colour_rgb(0x80, 0x80, 0x80)); minesweeper_draw_hud(ms, scr, bounds); @@ -359,13 +404,13 @@ static result_t minesweeper_redraw(const wuss_event_t *event, /* one-cell frame right round the grid */ screen_fill_rect(scr, bounds->x0, board_y0, - SIZE2D(MS_WIDTH, MS_HEIGHT - MS_HUD_H), + SIZE2D(MS_WIDTH(ms), MS_HEIGHT(ms) - MS_HUD_H), colour_rgb(0x80, 0x80, 0x80)); - /* board is small and fixed, so just repaint every cell rather than working - * out which ones overlap event->data.redraw.content */ - for (r = 0; r < MINESWEEPER_ROWS; r++) - for (c = 0; c < MINESWEEPER_COLS; c++) + /* board is small (max 24x24), so just repaint every cell rather than + * working out which ones overlap event->data.redraw.content */ + for (r = 0; r < ms->rows; r++) + for (c = 0; c < ms->cols; c++) minesweeper_draw_cell(ms, scr, r, c, bounds->x0 + MS_BORDER, bounds->y0 + MS_HUD_H + MS_BORDER); @@ -388,8 +433,12 @@ static result_t minesweeper_mouse(minesweeper_task_t *ms, int r, c; if (button & wuss_BUTTON_MENU) + { + wuss_menu_tick_exclusive((wuss_menu_t *) &g_minesweeper_size_menu, + ms->size); return wuss_menu_open(ms->task, &g_minesweeper_menu, wuss_get_pointer(ms->wuss), NULL); + } if (ms->dead || ms->won) { @@ -405,7 +454,7 @@ static result_t minesweeper_mouse(minesweeper_task_t *ms, c = (point.x - MS_BORDER) / MINESWEEPER_CELL; r = (point.y - MS_HUD_H - MS_BORDER) / MINESWEEPER_CELL; if (point.x < MS_BORDER || point.y < MS_HUD_H + MS_BORDER || - !minesweeper_in_bounds(r, c)) + !minesweeper_in_bounds(ms, r, c)) return result_OK; if (button & wuss_BUTTON_SELECT) @@ -477,7 +526,7 @@ result_t minesweeper_handle(wuss_window_t *window, minesweeper_tick_clock(ms); if (ms->elapsed != was) { - box_t timer = MS_TIMER_BOX; + box_t timer = MS_TIMER_BOX(ms); wuss_window_invalidate(ms->window, &timer); } @@ -485,8 +534,18 @@ result_t minesweeper_handle(wuss_window_t *window, return result_OK; case wuss_EVENT_MENU_SELECT: - /* only item is "New Game" */ - minesweeper_reset(ms); + if (event->data.menu_select.menu == &g_minesweeper_size_menu) + minesweeper_set_size(ms, (minesweeper_size_t) + event->data.menu_select.index); + else /* top-level menu; only other item is "New Game" */ + minesweeper_reset(ms); + + { + size2d_t sz = SIZE2D(MS_WIDTH(ms), MS_HEIGHT(ms)); + + wuss_window_resize(ms->window, sz); + wuss_window_set_doc(ms->window, sz); + } wuss_window_invalidate_visible(ms->window); return result_OK; diff --git a/libraries/wuss/test/tasks/minesweeper.h b/libraries/wuss/test/tasks/minesweeper.h index 537fff5e..706e2951 100644 --- a/libraries/wuss/test/tasks/minesweeper.h +++ b/libraries/wuss/test/tasks/minesweeper.h @@ -13,11 +13,24 @@ #include "wuss/task.h" #include "wuss/window.h" -#define MINESWEEPER_COLS 12 -#define MINESWEEPER_ROWS 12 -#define MINESWEEPER_MINES 20 #define MINESWEEPER_CELL 16 /* pixels per cell */ +/* board sizes offered on the "Grid Size" menu; arrays are sized for the + * largest so a resize just changes ms->rows/cols/mines and re-clears */ +#define MINESWEEPER_MAX_ROWS 24 +#define MINESWEEPER_MAX_COLS 24 + +typedef enum minesweeper_size +{ + minesweeper_SIZE_24X24, + minesweeper_SIZE_24X12, + minesweeper_SIZE_16X16, + minesweeper_SIZE_16X12, + minesweeper_SIZE_12X12, + minesweeper_NSIZES +} +minesweeper_size_t; + typedef enum minesweeper_cell_state { minesweeper_HIDDEN, @@ -28,16 +41,18 @@ minesweeper_cell_state_t; /* classic minesweeper: Select reveals a cell (flood-filling neighbouring * zeros), Adjust toggles a flag. Mines are placed on the first reveal so the - * opening click is never a mine. A MENU-button click pops a "New Game" menu - * that resets the board. */ + * opening click is never a mine. A MENU-button click pops a menu with "New + * Game" and a "Grid Size" submenu that resets the board at a new size. */ typedef struct minesweeper_task { wuss_t *wuss; wuss_window_t *window; wuss_task_t *task; /* delegate; opens the New Game menu */ bmfont_t *font; /* borrowed; draws the neighbour counts */ - bool mine[MINESWEEPER_ROWS][MINESWEEPER_COLS]; - minesweeper_cell_state_t state[MINESWEEPER_ROWS][MINESWEEPER_COLS]; + minesweeper_size_t size; + int rows, cols, mines; + bool mine[MINESWEEPER_MAX_ROWS][MINESWEEPER_MAX_COLS]; + minesweeper_cell_state_t state[MINESWEEPER_MAX_ROWS][MINESWEEPER_MAX_COLS]; bool placed; /* mines placed yet? */ bool dead; /* a mine was revealed */ bool won; From 46a97ffc0c0e66636c81719e428627c4df31e2bf Mon Sep 17 00:00:00 2001 From: David Thomas Date: Fri, 11 Sep 2026 23:54:32 +0100 Subject: [PATCH 039/128] fix(wuss): cap interactive resize-drag at the screen size wuss__furniture_drag_resize clamped the dragged width/height against wuss__max_content_on_screen, which measures room left from the window's current top-left rather than the window's own total size. A window moved off the top-left edge (negative x0/y0) could still be dragged past the screen's own width/height, since the pointer clamp in wuss_mouse_move only bounds the pointer, not the resulting box. Switch to wuss__max_content_anywhere_on_screen so the cap is the screen's size on each axis, independent of the window's position. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/furniture/drag-resize.c | 5 +- libraries/wuss/test/wuss-test.c | 94 ++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 1 deletion(-) diff --git a/libraries/wuss/furniture/drag-resize.c b/libraries/wuss/furniture/drag-resize.c index 00acdc0a..ee84ddd2 100644 --- a/libraries/wuss/furniture/drag-resize.c +++ b/libraries/wuss/furniture/drag-resize.c @@ -7,11 +7,12 @@ void wuss__furniture_drag_resize(wuss_window_t *window, point_t p) { box_t content; - size2d_t min; + size2d_t min, max; int width, height; wuss__content_box(window, &content); wuss__min_content(window, &min); + wuss__max_content_anywhere_on_screen(window, &max); /* Subtract the offset recorded at drag start so the point originally * grabbed on the resize icon stays under the pointer, rather than the @@ -20,6 +21,8 @@ void wuss__furniture_drag_resize(wuss_window_t *window, point_t p) 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)); + width = MIN(width, max.w); + height = MIN(height, max.h); wuss_window_resize(window, SIZE2D(width, height)); } diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index a112eead..d555b027 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -1521,6 +1521,100 @@ result_t wuss_test(const char *resources) wuss_window_close(win_m); } + printf("test: drag-resize never grows a window's total size past the screen's, on either axis\n"); + + { + static test_task_t tc_s; + wuss_task_t *delegate_s; + box_t box_s, content, visible; + wuss_window_t *win_s; + + tc_s.redraw_count = 0; + tc_s.mouse_count = 0; + delegate_s = mk_task(wuss, test_handle, &tc_s); + if (delegate_s == NULL) goto Failure; + + box_s.x0 = 10; box_s.y0 = 10; + box_s.x1 = 30; box_s.y1 = 30; /* 20x20 content; doc is 1000x1000, far bigger than the 200x200 screen */ + rc = wuss_window_create(delegate_s, &box_s, "S", wuss_WINDOW_DEFAULT, + wuss_NO_BACKDROP, + SIZE2D(1000, 1000), SIZE2D(0, 0), &win_s); + if (rc != result_OK) + goto Failure; + + wuss_window_get_visible_bounds(win_s, &visible); + + rc = wuss_mouse_click(wuss, POINT(visible.x1 - 3, visible.y1 - 3), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* S's resize icon */ + if (rc != result_OK) + goto Failure; + if (hit != win_s) + goto Failure; + + rc = wuss_mouse_move(wuss, POINT(1000, 1000), &hit); /* drag far past the 200x200 screen */ + if (rc != result_OK) + goto Failure; + + rc = wuss_mouse_click(wuss, POINT(1000, 1000), wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); + if (rc != result_OK) + goto Failure; + + wuss_window_get_visible_bounds(win_s, &visible); + if (visible.x1 - visible.x0 > 200 || visible.y1 - visible.y0 > 200) + goto Failure; /* total footprint capped at the screen size, wherever the window sits */ + + wuss_window_close(win_s); + } + + { + /* Same cap, but for a window moved partway off the top-left of the + * screen: the mouse pointer itself is clamped to the screen (see + * wuss_mouse_move), so a positive content.x0 can never see a resize + * drag reach far enough to overhang -- the gap only shows up once + * content.x0 is negative, letting the drag's on-screen pointer range + * translate into a width bigger than the screen itself. */ + static test_task_t tc_e; + wuss_task_t *delegate_e; + box_t box_e, visible; + wuss_window_t *win_e; + + tc_e.redraw_count = 0; + tc_e.mouse_count = 0; + delegate_e = mk_task(wuss, test_handle, &tc_e); + if (delegate_e == NULL) goto Failure; + + box_e.x0 = 10; box_e.y0 = 10; + box_e.x1 = 30; box_e.y1 = 30; /* 20x20 content; doc is 1000x1000, far bigger than the 200x200 screen */ + rc = wuss_window_create(delegate_e, &box_e, "E", wuss_WINDOW_DEFAULT, + wuss_NO_BACKDROP, + SIZE2D(1000, 1000), SIZE2D(0, 0), &win_e); + if (rc != result_OK) + goto Failure; + + wuss_window_move(win_e, POINT(-100, -100)); /* content top-left off-screen, top-left corner */ + + wuss_window_get_visible_bounds(win_e, &visible); + + rc = wuss_mouse_click(wuss, POINT(visible.x1 - 3, visible.y1 - 3), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* E's resize icon */ + if (rc != result_OK) + goto Failure; + if (hit != win_e) + goto Failure; + + rc = wuss_mouse_move(wuss, POINT(1000, 1000), &hit); /* drag to the far corner; the pointer itself clamps to (199,199) */ + if (rc != result_OK) + goto Failure; + + rc = wuss_mouse_click(wuss, POINT(1000, 1000), wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); + if (rc != result_OK) + goto Failure; + + wuss_window_get_visible_bounds(win_e, &visible); + if (visible.x1 - visible.x0 > 200 || visible.y1 - visible.y0 > 200) + goto Failure; /* total footprint still capped at the screen size, even with a negative x0/y0 */ + + wuss_window_close(win_e); + } + printf("test: toggle-size blits rather than redrawing the whole window\n"); { From 5f46d545d122638fcf4510c2920f1d787455935f Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sat, 12 Sep 2026 11:04:45 +0100 Subject: [PATCH 040/128] fix(bitmap): drop mkstemps from rle-test, use fixed leafname mkstemps is a GNU/BSD extension not present in the RISC OS GCCSDK libc, breaking the RISC OS build with an undefined reference at link time. Use path_join_leafname for a fixed output filename instead, matching the convention already used by the other framebuf test suites. --- libraries/framebuf/bitmap/test/rle-test.c | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/libraries/framebuf/bitmap/test/rle-test.c b/libraries/framebuf/bitmap/test/rle-test.c index 89c69ab8..d0b38494 100644 --- a/libraries/framebuf/bitmap/test/rle-test.c +++ b/libraries/framebuf/bitmap/test/rle-test.c @@ -5,7 +5,6 @@ #include #include #include -#include #include "base/result.h" #include "io/path.h" @@ -388,12 +387,13 @@ static int test_p8_png(void) colour_t pal[256]; colour_t rtpal[256]; unsigned char pix[P8W * P8H]; - char fn[] = "/tmp/dptlib-p8-XXXXXX.png"; - int fd; + const char *fn; result_t rc; int i; int ok = 1; + fn = path_join_leafname("rle-test-p8", "png"); + /* palette: greyscale ramp, plus a few non-opaque entries to exercise tRNS */ for (i = 0; i < 256; i++) pal[i] = colour_rgba((unsigned char) i, (unsigned char) i, @@ -407,10 +407,6 @@ static int test_p8_png(void) bitmap_init(&bm, SIZE2D(P8W, P8H), pixelfmt_p8, P8W, pal, pix); - fd = mkstemps(fn, 4); /* keep the ".png" suffix */ - if (fd < 0) { fprintf(stderr, "p8 png: mkstemps failed\n"); return 0; } - close(fd); - rc = bitmap_save_png(&bm, fn); if (rc) { fprintf(stderr, "p8 png: save rc=&%x\n", rc); return 0; } From 65cca4d3e488cb3ea56e8a86edacefb5e4f7480f Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sat, 12 Sep 2026 12:08:09 +0100 Subject: [PATCH 041/128] fix(io): match RISC OS leafnames' lack of dotted extensions OS_GBPB-returned RISC OS leafnames carry no dotted extension in the string at all -- file type is separate filesystem metadata -- so the three call sites hand-rolling strcmp(leaf+len-4, ".png")-style matching (namelist, bmfont enumerate, wuss icon registry) always missed every file on RISC OS, silently yielding empty font/image/palette sets. Add path_leaf_strip_ext(), a host-aware helper: on RISC OS every leaf matches (unchanged, since there is nothing to strip); elsewhere it behaves as the strcmp+strip the call sites did by hand. Switch all three call sites to it. --- include/io/path.h | 18 ++++++++++ libraries/framebuf/bmfont/enumerate.c | 21 +++++------ libraries/io/namelist/namelist.c | 24 ++++++------- libraries/io/path/path.c | 50 +++++++++++++++++++++++++++ libraries/wuss/icon/registry.c | 19 +--------- 5 files changed, 87 insertions(+), 45 deletions(-) diff --git a/include/io/path.h b/include/io/path.h index 0cb354f4..980f0805 100644 --- a/include/io/path.h +++ b/include/io/path.h @@ -3,6 +3,8 @@ #ifndef DPTLIB_PATH #define DPTLIB_PATH +#include + #define DPTLIB_MAXPATH 256 /* not ideal */ /** @@ -22,4 +24,20 @@ const char *path_join_leafname(const char *leaf, const char *ext); */ const char *path_join_filename(const char *root, int nbranches, ...); +/** + * Tests whether a directory leafname (as returned by dirscan_walk) has the + * dotted extension 'ext', e.g. ".png", under the host convention. On a + * match, writes the leaf with that extension stripped into 'name' (capacity + * 'cap') and returns nonzero. + * + * On RISC OS a leafname carries no extension in the string at all -- file + * type is separate filesystem metadata, not a suffix -- so every leaf + * matches unchanged (a straight copy into 'name'). + * + * \return Nonzero on a match, zero if "leaf" lacks 'ext' or does not fit + * 'cap'. + */ +int path_leaf_strip_ext(const char *leaf, const char *ext, char *name, + size_t cap); + #endif /* DPTLIB_PATH */ diff --git a/libraries/framebuf/bmfont/enumerate.c b/libraries/framebuf/bmfont/enumerate.c index 7fe9dfc8..fe1f8192 100644 --- a/libraries/framebuf/bmfont/enumerate.c +++ b/libraries/framebuf/bmfont/enumerate.c @@ -2,16 +2,15 @@ #include #include -#include #include "base/result.h" #include "framebuf/bmfont.h" #include "io/dirscan.h" +#include "io/path.h" /* ----------------------------------------------------------------------- */ -#define BMFONT_EXT ".png" -#define BMFONT_EXT_LEN 4 +#define BMFONT_EXT ".png" typedef struct { @@ -24,22 +23,18 @@ bmfont_enumerate_ctx_t; static result_t bmfont_enumerate_entry(const char *leaf, void *opaque) { bmfont_enumerate_ctx_t *ctx; - size_t leaflen; char name[256]; char path[512]; - ctx = opaque; - leaflen = strlen(leaf); + ctx = opaque; - if (leaflen <= BMFONT_EXT_LEN || leaflen - BMFONT_EXT_LEN >= sizeof(name)) + if (!path_leaf_strip_ext(leaf, BMFONT_EXT, name, sizeof(name))) return result_OK; - if (strcmp(leaf + leaflen - BMFONT_EXT_LEN, BMFONT_EXT) != 0) - return result_OK; - - memcpy(name, leaf, leaflen - BMFONT_EXT_LEN); - name[leaflen - BMFONT_EXT_LEN] = '\0'; -#ifdef TARGET_RISCOS + /* Built locally rather than via path_join_filename: that returns a single + * static buffer, which one call per entry here would repeatedly clobber + * from under the caller's own "dir" pointer. */ +#ifdef __riscos snprintf(path, sizeof(path), "%s.%s", ctx->dir, leaf); #else snprintf(path, sizeof(path), "%s/%s", ctx->dir, leaf); diff --git a/libraries/io/namelist/namelist.c b/libraries/io/namelist/namelist.c index 43d3964f..84b9ca94 100644 --- a/libraries/io/namelist/namelist.c +++ b/libraries/io/namelist/namelist.c @@ -6,6 +6,7 @@ #include "base/result.h" #include "io/dirscan.h" +#include "io/path.h" #include "io/namelist.h" @@ -17,7 +18,6 @@ typedef struct namelist_ctx size_t stride; int cap; const char *ext; /* NULL or "" for "match everything" */ - size_t extlen; int count; } namelist_ctx_t; @@ -25,32 +25,29 @@ namelist_ctx_t; static result_t namelist_entry(const char *leaf, void *opaque) { namelist_ctx_t *ctx; - size_t leaflen; - size_t namelen; + char name[DPTLIB_MAXPATH]; - ctx = opaque; - leaflen = strlen(leaf); + ctx = opaque; if (ctx->count >= ctx->cap) return result_STOP_WALK; - if (ctx->extlen > 0) + if (ctx->ext != NULL && ctx->ext[0] != '\0') { - if (leaflen <= ctx->extlen || - strcmp(leaf + leaflen - ctx->extlen, ctx->ext) != 0) + if (!path_leaf_strip_ext(leaf, ctx->ext, name, sizeof(name))) return result_OK; - namelen = leaflen - ctx->extlen; } else { - namelen = leaflen; + if (strlen(leaf) + 1 > sizeof(name)) + return result_OK; + strcpy(name, leaf); } - if (namelen + 1 > ctx->stride) + if (strlen(name) + 1 > ctx->stride) return result_OK; /* would not fit its slot */ - memcpy(ctx->names + (size_t) ctx->count * ctx->stride, leaf, namelen); - ctx->names[(size_t) ctx->count * ctx->stride + namelen] = '\0'; + strcpy(ctx->names + (size_t) ctx->count * ctx->stride, name); ctx->count++; return result_OK; @@ -81,7 +78,6 @@ result_t namelist_scan(const char *dir, ctx.stride = stride; ctx.cap = cap; ctx.ext = ext; - ctx.extlen = (ext != NULL) ? strlen(ext) : 0; ctx.count = 0; rc = dirscan_walk(dir, namelist_entry, &ctx); diff --git a/libraries/io/path/path.c b/libraries/io/path/path.c index 42acdc1c..c574983b 100644 --- a/libraries/io/path/path.c +++ b/libraries/io/path/path.c @@ -60,3 +60,53 @@ const char *path_join_filename(const char *root, int nbranches, ...) return buf; } + +int path_leaf_strip_ext(const char *leaf, + const char *ext, + char *name, + size_t cap) +{ + assert(leaf); + assert(ext); + assert(name); + +#ifdef __riscos + + { + size_t leaflen; + + /* No extension in the string to match against -- filetype is separate + * metadata -- so every leaf matches, unchanged. */ + NOT_USED(ext); + + leaflen = strlen(leaf); + if (leaflen + 1 > cap) + return 0; + + memcpy(name, leaf, leaflen + 1); + + return 1; + } + +#else + + { + size_t leaflen; + size_t extlen; + + leaflen = strlen(leaf); + extlen = strlen(ext); + + if (leaflen <= extlen || leaflen - extlen >= cap) + return 0; + if (strcmp(leaf + leaflen - extlen, ext) != 0) + return 0; + + memcpy(name, leaf, leaflen - extlen); + name[leaflen - extlen] = '\0'; + + return 1; + } + +#endif +} diff --git a/libraries/wuss/icon/registry.c b/libraries/wuss/icon/registry.c index 341d9f81..10d9feda 100644 --- a/libraries/wuss/icon/registry.c +++ b/libraries/wuss/icon/registry.c @@ -39,23 +39,6 @@ typedef struct icons_load } icons_load_t; -/* True when "leaf" ends in ".png" (case-sensitive, matching the fixtures). - * Writes the leaf sans that suffix into "name" (capacity "cap"). */ -static int png_leaf(const char *leaf, char *name, size_t cap) -{ - size_t len; - - len = strlen(leaf); - if (len < 4 || len - 4 >= cap) - return 0; - if (strcmp(leaf + len - 4, ".png") != 0) - return 0; - - memcpy(name, leaf, len - 4); - name[len - 4] = '\0'; - return 1; -} - /* The RLE blit (screen_copy_bitmap_rle) decodes straight into the screen's * byte layout -- no per-pixel format convert -- so a compressed icon must * already be in screen order. bitmap_load_png yields rgb(x|a)8888; the screen @@ -116,7 +99,7 @@ static result_t icons_load_entry(const char *leaf, void *opaque) atom_t atom; int oldcap; - if (!png_leaf(leaf, name, sizeof(name))) + if (!path_leaf_strip_ext(leaf, ".png", name, sizeof(name))) return result_OK; /* not a ".png", or name too long -- skip */ path = path_join_filename(st->dir, 1, leaf); From 3d1b82f4ecacdde753009dd677bd4ca3e3530f2a Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sat, 12 Sep 2026 12:08:21 +0100 Subject: [PATCH 042/128] fix(io): stop path_join_leafname appending a bogus RISC OS separator path_join_leafname used "%s/%s" on RISC OS, producing strings like "Nokia/png" instead of a valid pathname -- '/' is a literal leafname character on RISC OS, not a separator, and file type is never part of the string at all. Root cause of chars.c and other font/resource loads failing to open files whose name path_join_leafname had built. RISC OS now just returns the leaf unchanged, matching how file type is really carried (OS_File 18, Set_Type) rather than as a suffix. --- libraries/io/path/path.c | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/libraries/io/path/path.c b/libraries/io/path/path.c index c574983b..65bba30d 100644 --- a/libraries/io/path/path.c +++ b/libraries/io/path/path.c @@ -12,17 +12,23 @@ const char *path_join_leafname(const char *leaf, const char *ext) { static char buf[DPTLIB_MAXPATH]; - const char *fmt = + assert(leaf); + assert(ext); + #ifdef __riscos - "%s/%s"; + + /* No dotted extension in a RISC OS leafname -- file type is separate + * filesystem metadata, set with OS_File 18 (Set_Type), never part of the + * pathname string -- so there is nothing to append. */ + NOT_USED(ext); + + snprintf(buf, sizeof(buf), "%s", leaf); + #else - "%s.%s"; -#endif - assert(leaf); - assert(ext); + snprintf(buf, sizeof(buf), "%s.%s", leaf, ext); - snprintf(buf, sizeof(buf), fmt, leaf, ext); +#endif return buf; } From befe9a5b3ab31cf208e30f0c6051cf09597cc3a7 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sat, 12 Sep 2026 12:08:28 +0100 Subject: [PATCH 043/128] feat(wuss): add Shift-F1 for wuss_INPUT_GARBAGE on RISC OS frontend Matches frontend-sdl.c and the intent already documented in frontend.h: F1 alone forces a full redraw, Shift-F1 corrupts the screen for one frame. F1 moves out of the static g_keys[] scan-code table (which can't vary an entry's kind by modifier) and into its own edge-detected check in wuss_frontend_poll, resolving GARBAGE vs REDRAW_ALL via a live Shift read at the moment F1's press-edge fires. --- apps/wuss/frontend-riscos.c | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/apps/wuss/frontend-riscos.c b/apps/wuss/frontend-riscos.c index f783daa8..08afd975 100644 --- a/apps/wuss/frontend-riscos.c +++ b/apps/wuss/frontend-riscos.c @@ -213,8 +213,14 @@ static wuss_button_t mouse_buttons_to_wuss(int buttons) return b; } +/* INKEY scan codes for keys tested individually, outside g_keys[] below: F1 + * (checked with Shift to pick REDRAW_ALL vs GARBAGE) and Shift itself. */ +#define KEY_SCAN_F1 (-114) +#define KEY_SCAN_SHIFT (-1) + /* Keys the demo reacts to: negative INKEY scan code -> input kind. F4 and - * Escape both quit. */ + * Escape both quit. F1 is handled separately in wuss_frontend_poll, not in + * this table, so it can vary its kind with Shift. */ static const struct { int scan; @@ -223,7 +229,6 @@ static const struct g_keys[] = { { -113, wuss_INPUT_QUIT }, /* Escape */ - { -114, wuss_INPUT_REDRAW_ALL }, /* F1 */ { -116, wuss_INPUT_PIXEL_STRESS }, /* F3 */ { -117, wuss_INPUT_QUIT } /* F4 */ }; @@ -242,6 +247,7 @@ bool wuss_frontend_poll(wuss_frontend_t *fe, wuss_input_t *event) static unsigned int key_was; /* bit i = g_keys[i] was down last poll */ unsigned int key_now = 0; + unsigned int f1_bit = 1u << NELEMS(g_keys); /* one bit past the table */ int mx, my, buttons, t; int px, py; size_t i; @@ -251,6 +257,18 @@ bool wuss_frontend_poll(wuss_frontend_t *fe, wuss_input_t *event) for (i = 0; i < NELEMS(g_keys); i++) if (key_down(g_keys[i].scan)) key_now |= 1u << i; + if (key_down(KEY_SCAN_F1)) + key_now |= f1_bit; + + /* F1 is handled here rather than in g_keys[]: its kind depends on Shift, + * which a static table entry can't express. */ + if ((key_now & f1_bit) && !(key_was & f1_bit)) + { + key_was = key_now; + event->kind = key_down(KEY_SCAN_SHIFT) ? wuss_INPUT_GARBAGE + : wuss_INPUT_REDRAW_ALL; + return true; + } for (i = 0; i < NELEMS(g_keys); i++) { From 93c02ca337890bfe56d61df6149ba4bca77bfab3 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sat, 12 Sep 2026 12:37:20 +0100 Subject: [PATCH 044/128] fix(wuss): partial-redraw minesweeper to stop board flicker Every click repainted the whole HUD strip, frame and all cells even for a single flag toggle or reveal -- minesweeper_redraw ignored the dirty region it was given and minesweeper_mouse always invalidated the full window. minesweeper_reveal now accumulates the board-cell range its flood fill actually touches; SELECT/ADJUST invalidate just that (plus the mine counter on a flag change). minesweeper_redraw skips the HUD repaint when the dirty rect doesn't reach it, and only redraws cells overlapping the dirty rect instead of the whole board. Death/win/new game/resize still invalidate the whole window, since the banner or a board reflow genuinely covers it all. --- libraries/wuss/test/tasks/minesweeper.c | 115 +++++++++++++++++++----- 1 file changed, 93 insertions(+), 22 deletions(-) diff --git a/libraries/wuss/test/tasks/minesweeper.c b/libraries/wuss/test/tasks/minesweeper.c index 6294b448..8f9cea2c 100644 --- a/libraries/wuss/test/tasks/minesweeper.c +++ b/libraries/wuss/test/tasks/minesweeper.c @@ -38,6 +38,19 @@ #define MS_TIMER_BOX(ms) \ ((box_t) { MS_TIMER_X0(ms), 0, MS_WIDTH(ms) - MS_BORDER, MS_HUD_H }) +/* the mines-remaining field, window-local content coords: left-aligned in + * the HUD strip, mirroring minesweeper_draw_hud's placement */ +#define MS_MINES_BOX(ms) \ + ((box_t) { MS_BORDER, 0, MS_BORDER + MINESWEEPER_CELL * 2, MS_HUD_H }) + +/* window-local pixel box of the cell range [r0,r1) x [c0,c1); mirrors + * minesweeper_draw_cell's own x,y placement */ +#define MS_CELLS_BOX(r0, c0, r1, c1) \ + ((box_t) { MS_BORDER + (c0) * MINESWEEPER_CELL, \ + MS_HUD_H + MS_BORDER + (r0) * MINESWEEPER_CELL, \ + MS_BORDER + (c1) * MINESWEEPER_CELL, \ + MS_HUD_H + MS_BORDER + (r1) * MINESWEEPER_CELL }) + /* rows, cols, mines offered by the "Grid Size" submenu, indexed by * minesweeper_size_t; mine counts follow the classic ~14% density */ static const struct { int rows, cols, mines; } @@ -138,12 +151,25 @@ static void minesweeper_place_mines(minesweeper_task_t *ms, ms->placed = true; } +/* inclusive-exclusive board-cell range [r0,r1) x [c0,c1) touched by a flood + * reveal, accumulated by minesweeper_reveal so the caller can invalidate + * just that area instead of the whole board */ +typedef struct { int r0, c0, r1, c1; } minesweeper_cellbox_t; + /* reveals (r,c) and, if it has no adjacent mines, floods outward to its * neighbours; recursion depth is bounded by the (small, fixed) board size */ -static void minesweeper_reveal(minesweeper_task_t *ms, int r, int c) +static void minesweeper_reveal(minesweeper_task_t *ms, + int r, + int c, + minesweeper_cellbox_t *touched) { int dr, dc; + if (r < touched->r0) touched->r0 = r; + if (c < touched->c0) touched->c0 = c; + if (r + 1 > touched->r1) touched->r1 = r + 1; + if (c + 1 > touched->c1) touched->c1 = c + 1; + if (!minesweeper_in_bounds(ms, r, c) || ms->state[r][c] != minesweeper_HIDDEN) return; @@ -159,7 +185,7 @@ static void minesweeper_reveal(minesweeper_task_t *ms, int r, int c) for (dr = -1; dr <= 1; dr++) for (dc = -1; dc <= 1; dc++) if (dr || dc) - minesweeper_reveal(ms, r + dr, c + dc); + minesweeper_reveal(ms, r + dr, c + dc, touched); } /* on death, reveal every mine so the player sees where they all were */ @@ -377,7 +403,7 @@ static result_t minesweeper_redraw(const wuss_event_t *event, const box_t *bounds; const box_t *clip; int board_y0; - bool board_dirty; + int r0, c0, r1, c1; int r, c; ms = task_data; @@ -385,32 +411,46 @@ static result_t minesweeper_redraw(const wuss_event_t *event, bounds = event->data.redraw.bounds; clip = event->data.redraw.content; - /* the HUD strip is cheap and always redrawn; the frame, cells and banner - * only when the dirty region reaches below the strip (an idle timer tick - * invalidates just the timer field, so it skips all of that) */ - board_y0 = bounds->y0 + MS_HUD_H; - board_dirty = clip->y1 > board_y0; + /* the HUD strip and the board below are repainted independently, each only + * when the dirty region actually reaches it -- a flag toggle invalidates + * one cell plus the mine counter, a timer tick just the timer field, so + * either half alone is the common case and this keeps both cheap */ + board_y0 = bounds->y0 + MS_HUD_H; minesweeper_tick_clock(ms); - /* the window owns every pixel (wuss_NO_BACKGROUND), so the HUD strip's own - * backdrop is painted here before the counters go on top */ - screen_fill_rect(scr, bounds->x0, bounds->y0, SIZE2D(MS_WIDTH(ms), MS_HUD_H), - colour_rgb(0x80, 0x80, 0x80)); - minesweeper_draw_hud(ms, scr, bounds); + if (clip->y0 < board_y0) + { + /* the window owns every pixel (wuss_NO_BACKGROUND), so the HUD strip's + * own backdrop is painted here before the counters go on top */ + screen_fill_rect(scr, bounds->x0, bounds->y0, + SIZE2D(MS_WIDTH(ms), MS_HUD_H), + colour_rgb(0x80, 0x80, 0x80)); + minesweeper_draw_hud(ms, scr, bounds); + } - if (!board_dirty) + if (clip->y1 <= board_y0) return result_OK; - /* one-cell frame right round the grid */ + /* one-cell frame right round the grid; drawn under the cells, so only the + * margin strip actually needs filling -- but that strip is thin and + * irregular to clip precisely, and it changes only on a resize/new game, + * which already invalidates the whole window, so no clipping is worth + * doing here */ screen_fill_rect(scr, bounds->x0, board_y0, SIZE2D(MS_WIDTH(ms), MS_HEIGHT(ms) - MS_HUD_H), colour_rgb(0x80, 0x80, 0x80)); - /* board is small (max 24x24), so just repaint every cell rather than - * working out which ones overlap event->data.redraw.content */ - for (r = 0; r < ms->rows; r++) - for (c = 0; c < ms->cols; c++) + /* only the cells overlapping the dirty rect need redrawing */ + r0 = MAX(0, (clip->y0 - board_y0 - MS_BORDER) / MINESWEEPER_CELL); + c0 = MAX(0, (clip->x0 - bounds->x0 - MS_BORDER) / MINESWEEPER_CELL); + r1 = MIN(ms->rows, (clip->y1 - board_y0 - MS_BORDER + MINESWEEPER_CELL - 1) / + MINESWEEPER_CELL); + c1 = MIN(ms->cols, (clip->x1 - bounds->x0 - MS_BORDER + MINESWEEPER_CELL - 1) / + MINESWEEPER_CELL); + + for (r = r0; r < r1; r++) + for (c = c0; c < c1; c++) minesweeper_draw_cell(ms, scr, r, c, bounds->x0 + MS_BORDER, bounds->y0 + MS_HUD_H + MS_BORDER); @@ -459,6 +499,9 @@ static result_t minesweeper_mouse(minesweeper_task_t *ms, if (button & wuss_BUTTON_SELECT) { + minesweeper_cellbox_t touched; + box_t local; + if (ms->state[r][c] == minesweeper_FLAGGED) return result_OK; if (!ms->placed) @@ -466,15 +509,33 @@ static result_t minesweeper_mouse(minesweeper_task_t *ms, minesweeper_place_mines(ms, r, c); ms->start_time = time(NULL); } - minesweeper_reveal(ms, r, c); + + touched.r0 = touched.r1 = r; + touched.c0 = touched.c1 = c; + minesweeper_reveal(ms, r, c, &touched); + if (ms->dead) + { minesweeper_reveal_all_mines(ms); + wuss_window_invalidate_visible(ms->window); /* every mine, plus banner */ + } else + { ms->won = minesweeper_check_won(ms); + if (ms->won) + wuss_window_invalidate_visible(ms->window); /* banner covers the lot */ + else + { + local = MS_CELLS_BOX(touched.r0, touched.c0, touched.r1, touched.c1); + wuss_window_invalidate(ms->window, &local); + } + } minesweeper_tick_clock(ms); } else if (button & wuss_BUTTON_ADJUST) { + box_t local = MS_CELLS_BOX(r, c, r + 1, c + 1); + if (ms->state[r][c] == minesweeper_HIDDEN) { ms->state[r][c] = minesweeper_FLAGGED; @@ -485,14 +546,24 @@ static result_t minesweeper_mouse(minesweeper_task_t *ms, ms->state[r][c] = minesweeper_HIDDEN; ms->flags--; } + else + { + return result_OK; + } + + /* the flag count in the HUD changed too */ + { + box_t mines_box = MS_MINES_BOX(ms); + + wuss_window_invalidate(ms->window, &mines_box); + } + wuss_window_invalidate(ms->window, &local); } else { return result_OK; } - wuss_window_invalidate_visible(ms->window); - return result_OK; } From 4a159d913b332ae6442acd0b856c257a4d903e09 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sat, 12 Sep 2026 16:19:53 +0100 Subject: [PATCH 045/128] fix(wuss): stop invalidate-visible double-applying scroll on NULL box wuss_window_invalidate(window, NULL) built its whole-visible-rect placeholder already in screen space, then ran it through the doc-space-to-screen translation (+content.x0 - scroll) a second time. Once a window was shrunk below its document and then scrolled, that double-applied the scroll and slid the queued dirty rect off to one side of the window. wuss_window_invalidate_visible (called every idle tick by tasks such as saturn.c) hit this every frame once its window was scrolled, so fill_backdrop_excluding_content kept repainting a chunk of bare desktop next to the window -- the reported "refresh outside its window" on shrink-then-scroll. Build the placeholder in doc space instead, so the existing translation lands back on the window's real box. Adds a regression test asserting the queued dirty rect stays within the window's own visible box. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/core/window/invalidate.c | 11 +- libraries/wuss/test/wuss-test.c | 185 ++++++++++++++++++++++++ 2 files changed, 192 insertions(+), 4 deletions(-) diff --git a/libraries/wuss/core/window/invalidate.c b/libraries/wuss/core/window/invalidate.c index e0885000..8571cee3 100644 --- a/libraries/wuss/core/window/invalidate.c +++ b/libraries/wuss/core/window/invalidate.c @@ -384,10 +384,13 @@ void wuss_window_invalidate(wuss_window_t *window, const box_t *local_box) if (local_box == NULL) { - whole.x0 = 0; - whole.y0 = 0; - whole.x1 = content.x1 - content.x0; - whole.y1 = content.y1 - content.y0; + /* doc-space box of the visible rect at the current scroll, so the + * "+ content.x0 - scroll" translation below lands back on the window's + * actual screen box instead of sliding it by scroll a second time. */ + whole.x0 = window->scroll.x; + whole.y0 = window->scroll.y; + whole.x1 = window->scroll.x + (content.x1 - content.x0); + whole.y1 = window->scroll.y + (content.y1 - content.y0); local_box = &whole; } diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index d555b027..4e94022d 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -231,6 +231,33 @@ static result_t paint_handle(wuss_window_t *window, return result_OK; } +/* A redraw handler that ignores "clip" and always floods its whole content + * box with the colour_t task_data points to, relying entirely on wuss-core's + * own clipping (scr->clip during the redraw dispatch) to keep the fill + * inside the window -- matching a task such as saturn.c that redraws its + * full fixed sample space every call rather than restricting itself to the + * dirty piece it was handed. */ +static result_t flood_full_bounds_handle(wuss_window_t *window, + const wuss_event_t *event, + void *task_data) +{ + const box_t *bounds; + const colour_t *colour; + + NOT_USED(window); + + if (event->kind != wuss_EVENT_REDRAW) + return result_OK; + + bounds = event->data.redraw.bounds; + colour = task_data; + screen_fill_rect(event->data.redraw.scr, bounds->x0, bounds->y0, + SIZE2D(bounds->x1 - bounds->x0, bounds->y1 - bounds->y0), + *colour); + + return result_OK; +} + /* ----------------------------------------------------------------------- */ /* Most tests declare their test_task_t on the block stack. A registered @@ -2362,6 +2389,164 @@ result_t wuss_test(const char *resources) wuss_window_close(win_m); } + printf("test: scrolling a window shrunk below its document size never marks a neighbour's screen area dirty\n"); + + { + /* Regression: a window resized down below its document extent (so it + * becomes scrollable), then scrolled by wheel/scrollbar, must not touch + * a neighbour window's own screen area. flood_full_bounds_handle ignores + * "clip" entirely, mirroring a real task (e.g. saturn.c) that redraws + * its full sample space every call rather than honouring clip -- the + * wuss-core clip machinery, not the task, must be what keeps such a + * task's pixels inside its own window. */ + colour_t s_colour, n_colour; + static test_task_t tc_s, tc_n; + wuss_task_t *delegate_s, *delegate_n; + box_t box_s, box_n, content_n; + wuss_window_t *win_s, *win_n; + int nx, ny, bad; + + s_colour = colour_rgb(0xcc, 0xdd, 0xee); + n_colour = colour_rgb(0x40, 0x60, 0x80); + tc_s.redraw_count = 0; tc_s.mouse_count = 0; + tc_n.redraw_count = 0; tc_n.mouse_count = 0; + delegate_s = mk_task(wuss, flood_full_bounds_handle, &s_colour); + if (delegate_s == NULL) goto Failure; + delegate_n = mk_task(wuss, flood_full_bounds_handle, &n_colour); + if (delegate_n == NULL) goto Failure; + + /* S starts full-sized (matches its document, so no scrollbars yet), then + * gets shrunk -- exercising wuss_window_resize's scroll-reclamp path, + * distinct from creating it small to begin with. */ + box_s.x0 = 10; box_s.y0 = 10; + box_s.x1 = 90; box_s.y1 = 90; + rc = wuss_window_create(delegate_s, &box_s, "S", wuss_WINDOW_DEFAULT, + wuss_NO_BACKDROP, + SIZE2D(80, 80), SIZE2D(0, 0), &win_s); + if (rc != result_OK) + goto Failure; + + /* N sits immediately to the right of S's outer box, close enough that a + * blit destination sliding even a few pixels past S's own bounds lands + * on N. */ + box_n.x0 = box_s.x1 + 2; box_n.y0 = 10; + box_n.x1 = box_n.x0 + 40; box_n.y1 = 90; + rc = wuss_window_create(delegate_n, &box_n, "N", wuss_WINDOW_DEFAULT, + wuss_NO_BACKDROP, + SIZE2D(box_n.x1 - box_n.x0, box_n.y1 - box_n.y0), + SIZE2D(0, 0), &win_n); + if (rc != result_OK) + goto Failure; + + rc = wuss_redraw_dirty(wuss); /* flush both creates */ + if (rc != result_OK) + goto Failure; + + /* shrink S well below its 80x80 document -- this window is now + * scrollable */ + rc = wuss_window_resize(win_s, SIZE2D(30, 30)); + if (rc != result_OK) + goto Failure; + rc = wuss_redraw_dirty(wuss); + if (rc != result_OK) + goto Failure; + + /* scroll it, as a wheel/scrollbar-drag would */ + wuss_window_set_scroll(win_s, POINT(20, 20)); + rc = wuss_redraw_dirty(wuss); + if (rc != result_OK) + goto Failure; + + /* every pixel of N's own content (not its titlebar/outline chrome) must + * still show exactly N's own flood colour */ + wuss_window_get_content_bounds(win_n, &content_n); + bad = 0; + for (ny = content_n.y0; ny < content_n.y1 && !bad; ny++) + for (nx = content_n.x0; nx < content_n.x1; nx++) + { + uint32_t px; + + px = ((const uint32_t *) pixels)[ny * 200 + nx]; + if ((px & 0xffffff) != 0x406080) + { + bad = 1; + break; + } + } + if (bad) + goto Failure; /* S's scroll blit painted over N's screen area */ + + wuss_window_close(win_n); + wuss_window_close(win_s); + } + + printf("test: wuss_window_invalidate_visible on a shrunk, scrolled window only dirties its own visible box\n"); + + { + /* Regression: wuss_window_invalidate(window, NULL) built its "whole + * visible rect" box already in screen space (offset by content.x0/y0), + * then translated it a second time by "content.x0 - scroll", so a + * scrolled window's dirty rect ended up shifted by -scroll off to one + * side of the window rather than sitting over it -- e.g. saturn.c's + * per-idle-tick wuss_window_invalidate_visible(), once its window was + * shrunk below its document and then scrolled, queued a dirty rect + * outside the window's own screen area (fill_backdrop_excluding_content + * then repaints that stray area as bare desktop every frame). */ + static test_task_t tc_s; + wuss_task_t *delegate_s; + box_t box_s, dirty; + wuss_window_t *win_s; + colour_t s_colour; + + s_colour = colour_rgb(0xcc, 0xdd, 0xee); + tc_s.redraw_count = 0; tc_s.mouse_count = 0; + delegate_s = mk_task(wuss, flood_full_bounds_handle, &s_colour); + if (delegate_s == NULL) goto Failure; + + box_s.x0 = 60; box_s.y0 = 60; + box_s.x1 = 140; box_s.y1 = 140; + rc = wuss_window_create(delegate_s, &box_s, "S", wuss_WINDOW_DEFAULT, + wuss_NO_BACKDROP, + SIZE2D(80, 80), SIZE2D(0, 0), &win_s); + if (rc != result_OK) + goto Failure; + rc = wuss_redraw_dirty(wuss); /* flush the create */ + if (rc != result_OK) + goto Failure; + + rc = wuss_window_resize(win_s, SIZE2D(30, 30)); + if (rc != result_OK) + goto Failure; + rc = wuss_redraw_dirty(wuss); + if (rc != result_OK) + goto Failure; + + wuss_window_set_scroll(win_s, POINT(20, 20)); + rc = wuss_redraw_dirty(wuss); + if (rc != result_OK) + goto Failure; + + /* mirrors saturn_idle: invalidate the whole visible rect with no + * explicit local_box, exactly what a task's idle handler calls every + * frame -- inspect the queued dirty rect directly rather than via + * pixels, since every window (including S itself) re-clips to its own + * content box on redraw regardless of what the dirty list says, so a + * stray dirty rect only ever shows up as a wasted backdrop repaint, not + * as a colour mismatch. */ + wuss_window_invalidate_visible(win_s); + if (wuss_get_dirty_count(wuss) != 1) + goto Failure; + wuss_get_dirty(wuss, 0, &dirty); + if (!box_contains_box(&dirty, &win_s->visible)) + goto Failure; /* dirty rect strayed outside S's own visible box */ + + rc = wuss_redraw_dirty(wuss); + if (rc != result_OK) + goto Failure; + + wuss_window_close(win_s); + } + printf("test: wuss_WINDOW_NO_RESIZE_BLIT redraws the whole window instead of blitting\n"); { From 3eff11c5fd60d9362ab37538f82b60247e6026ce Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sat, 12 Sep 2026 17:37:37 +0100 Subject: [PATCH 046/128] fix(wuss): correct off-by-one in ball edge-clamp on right/bottom content is a half-open box, so the right/bottom clamp trigger and target were both one pixel past the last valid column/row. The ball settled with x+radius == scroll.x+width instead of scroll.x+width-1, so its invalidation box poked 1px into the furniture strip beyond the window content. --- libraries/wuss/test/tasks/ball.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libraries/wuss/test/tasks/ball.c b/libraries/wuss/test/tasks/ball.c index bc2c1cf9..438b09fa 100644 --- a/libraries/wuss/test/tasks/ball.c +++ b/libraries/wuss/test/tasks/ball.c @@ -208,10 +208,10 @@ static result_t ball_idle(void *task_data) b->x += b->dx; b->y += b->dy; - if (b->x - b->radius < scroll.x) { b->x = scroll.x + b->radius; b->dx = -b->dx; } - else if (b->x + b->radius > scroll.x + width) { b->x = scroll.x + width - b->radius; b->dx = -b->dx; } - if (b->y - b->radius < scroll.y) { b->y = scroll.y + b->radius; b->dy = -b->dy; } - else if (b->y + b->radius > scroll.y + height){ b->y = scroll.y + height - b->radius; b->dy = -b->dy; } + if (b->x - b->radius < scroll.x) { b->x = scroll.x + b->radius; b->dx = -b->dx; } + else if (b->x + b->radius >= scroll.x + width) { b->x = scroll.x + width - 1 - b->radius; b->dx = -b->dx; } + if (b->y - b->radius < scroll.y) { b->y = scroll.y + b->radius; b->dy = -b->dy; } + else if (b->y + b->radius >= scroll.y + height){ b->y = scroll.y + height - 1 - b->radius; b->dy = -b->dy; } local = ball_local_box(MIN(old_x, b->x), MIN(old_y, b->y), MAX(old_x, b->x), MAX(old_y, b->y), From 875f36af911ee6079e16a819e5918e37da4db54c Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sat, 12 Sep 2026 17:37:42 +0100 Subject: [PATCH 047/128] fix(wuss): clamp wuss_window_invalidate to the window's content area A client-supplied box was translated to screen space and handed straight to wuss__invalidate_clipped, which only clips against occluding windows above it in the z-order, not against the window's own furniture. A buggy or edge-hugging client (see the ball task off-by-one) could therefore dirty the titlebar/outline/scrollbars around its content. Intersect the translated box with the content box before invalidating, and add a test covering a swept box straddling each content edge plus one entirely outside content. --- libraries/wuss/core/window/invalidate.c | 10 ++- libraries/wuss/test/wuss-test.c | 106 ++++++++++++++++++++++++ 2 files changed, 114 insertions(+), 2 deletions(-) diff --git a/libraries/wuss/core/window/invalidate.c b/libraries/wuss/core/window/invalidate.c index 8571cee3..472578a5 100644 --- a/libraries/wuss/core/window/invalidate.c +++ b/libraries/wuss/core/window/invalidate.c @@ -378,7 +378,7 @@ void wuss__invalidate_minus(wuss_t *wuss, void wuss_window_invalidate(wuss_window_t *window, const box_t *local_box) { - box_t screen_box, content, whole; + box_t screen_box, content, whole, clipped; wuss__content_box(window, &content); @@ -397,7 +397,13 @@ void wuss_window_invalidate(wuss_window_t *window, const box_t *local_box) box_translated(local_box, content.x0 - window->scroll.x, content.y0 - window->scroll.y, &screen_box); - wuss__invalidate_clipped(window, &screen_box); + /* clients pass arbitrary boxes (e.g. a ball's swept box near an edge); + * clamp to the content area so a client can never dirty the furniture + * around it. */ + if (box_intersection(&screen_box, &content, &clipped)) + return; /* no overlap with content at all */ + + wuss__invalidate_clipped(window, &clipped); } void wuss_window_invalidate_extent(wuss_window_t *window) diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 4e94022d..1713e896 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -5278,6 +5278,112 @@ ColourMenuOK: ; wuss_window_close(win_hs); } + printf("test: wuss_window_invalidate clamps a client box to the content " + "area, never dirtying the furniture around it\n"); + + { + static test_task_t tc_iv; + wuss_task_t *delegate_iv; + wuss_window_t *win_iv; + box_t box_iv, content, local, dirty; + int i, w, h; + + tc_iv.redraw_count = 0; + tc_iv.mouse_count = 0; + delegate_iv = mk_task(wuss, test_handle, &tc_iv); + if (delegate_iv == NULL) goto Failure; + + box_iv.x0 = 5; box_iv.y0 = 5; + box_iv.x1 = 125; box_iv.y1 = 85; + rc = wuss_window_create(delegate_iv, &box_iv, "IV", wuss_WINDOW_DEFAULT, + wuss_NO_BACKDROP, + SIZE2D(400, 400), SIZE2D(0, 0), &win_iv); + if (rc != result_OK) + goto Failure; + + wuss_window_get_content_bounds(win_iv, &content); + + rc = wuss_redraw_dirty(wuss); /* flush the create's own dirty region first */ + if (rc != result_OK) + goto Failure; + + /* a box like a ball's swept range at the bottom-right corner, straddling + * both the content and the furniture (titlebar/outline/scrollbars) + * around it -- content-local, so translate content's screen origin + * back off before building it. */ + w = content.x1 - content.x0; + h = content.y1 - content.y0; + local.x0 = w - 10; + local.y0 = h - 10; + local.x1 = w + 10; /* runs past content.x1 into the furniture */ + local.y1 = h + 10; /* runs past content.y1 into the furniture */ + wuss_window_invalidate(win_iv, &local); + + if (wuss_get_dirty_count(wuss) != 1) + goto Failure; + wuss_get_dirty(wuss, 0, &dirty); + if (dirty.x0 < content.x0 || dirty.y0 < content.y0 || + dirty.x1 > content.x1 || dirty.y1 > content.y1) + goto Failure; /* must be clamped to content, not spill into furniture */ + + rc = wuss_redraw_dirty(wuss); + if (rc != result_OK) + goto Failure; + + /* a box entirely outside the content (pure furniture, e.g. a client bug + * that never clamped at all) must not dirty anything. */ + before_a = tc_iv.redraw_count; + local.x0 = -50; local.y0 = -50; + local.x1 = -10; local.y1 = -10; + wuss_window_invalidate(win_iv, &local); + if (wuss_get_dirty_count(wuss) != 0) + goto Failure; + rc = wuss_redraw_dirty(wuss); + if (rc != result_OK) + goto Failure; + if (tc_iv.redraw_count != before_a) + goto Failure; + + /* sweep every furniture-adjacent edge: a content-local box straddling + * that edge must clamp to it, never crossing into visible's outline. */ + for (i = 0; i < 4; i++) + { + switch (i) + { + case 0: /* left edge */ + local.x0 = -10; local.y0 = 0; + local.x1 = 10; local.y1 = 10; + break; + case 1: /* top edge */ + local.x0 = 0; local.y0 = -10; + local.x1 = 10; local.y1 = 10; + break; + case 2: /* right edge */ + local.x0 = w - 10; local.y0 = 0; + local.x1 = w + 10; local.y1 = 10; + break; + default: /* bottom edge */ + local.x0 = 0; local.y0 = h - 10; + local.x1 = 10; local.y1 = h + 10; + break; + } + + wuss_window_invalidate(win_iv, &local); + if (wuss_get_dirty_count(wuss) > 0) + { + wuss_get_dirty(wuss, 0, &dirty); + if (dirty.x0 < content.x0 || dirty.y0 < content.y0 || + dirty.x1 > content.x1 || dirty.y1 > content.y1) + goto Failure; + } + rc = wuss_redraw_dirty(wuss); + if (rc != result_OK) + goto Failure; + } + + wuss_window_close(win_iv); + } + printf("test: furniture hit tiling holds for every furniture-flag combo\n"); { From b11885a04cbb55fbe138c8b6a26f1de9a15c26fd Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sat, 12 Sep 2026 18:59:45 +0100 Subject: [PATCH 048/128] perf(wuss): translate cached furniture layout on move instead of rebuilding wuss_window_move() called wuss__chrome_invalidate_layout() unconditionally, dropping the whole per-window furniture layout cache (all ~24 pieces) on every drag tick even though a pure translation changes no piece's size -- only its screen position. The next paint of even a one-pixel sliver then rebuilt the entire layout from scratch. Add wuss__furniture_layout_translate(), which shifts every cached piece rect and the titlebar rect by (dx, dy) in place, and call it from wuss_window_move() once the move delta is known instead of invalidating the cache. The hidden-window early-return path still invalidates, since it has no delta to translate by and nothing is drawn from the cache while hidden anyway. Furniture invalidation from clicks/scrolls (wuss__furniture_invalidate*) is untouched -- that path's geometry genuinely changes and a rebuild is correct there. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/TODO.txt | 7 ++++++- libraries/wuss/core/window/move.c | 20 +++++++++++++++----- libraries/wuss/furniture.h | 6 ++++++ libraries/wuss/furniture/layout.c | 14 ++++++++++++++ 4 files changed, 41 insertions(+), 6 deletions(-) diff --git a/libraries/wuss/TODO.txt b/libraries/wuss/TODO.txt index 25857365..b91d4573 100644 --- a/libraries/wuss/TODO.txt +++ b/libraries/wuss/TODO.txt @@ -4,9 +4,14 @@ Ordered by dependency. Each layer builds on the ones above it. Items marked (blocks: X) are prerequisites for X. +Add a panic redraw indicator? +Furniture clicks / scrolls still invalidate the entire furniture set - fix. + (window move fixed: layout cache is now translated in place, not rebuilt) + + + LAYER 0 - BUGS (fix before building on top) -- Various window size toggle ops need checking out - or at least understood - Auto window placement packer still not working as expected blocks: reliable multi-window tasks diff --git a/libraries/wuss/core/window/move.c b/libraries/wuss/core/window/move.c index 6d1357da..edf9a804 100644 --- a/libraries/wuss/core/window/move.c +++ b/libraries/wuss/core/window/move.c @@ -4,7 +4,9 @@ /* p is the window's content top-left; the furniture offset (outline plus * any titlebar) is constant for a given window, so the footprint just - * follows it */ + * follows it. The cached furniture layout is translated in place below + * rather than invalidated -- a pure move changes no piece's size, so there + * is nothing to rebuild. */ void wuss_window_move(wuss_window_t *window, point_t p) { box_t clean[WUSS_MAX_INVALIDATE_PIECES]; @@ -18,10 +20,6 @@ void wuss_window_move(wuss_window_t *window, point_t p) * slot back and stop tracking this window's position */ wuss__release_packed(window); - /* the cached furniture layout holds absolute screen coords, so a move - * stales it -- both exit paths below rewrite window->visible */ - wuss__chrome_invalidate_layout(window); - width = window->visible.x1 - window->visible.x0; height = window->visible.y1 - window->visible.y0; outline_px = wuss__outline_px(window); @@ -32,6 +30,11 @@ void wuss_window_move(wuss_window_t *window, point_t p) * just translate its footprint so it is in place when shown again */ if (window->flags & wuss_WINDOW_HIDDEN) { + /* nothing is drawn from the cache while hidden, but the next unhide must + * not paint it back at the old position, so just drop it -- there's no + * screen-visible dirty region to queue either way */ + wuss__chrome_invalidate_layout(window); + window->visible.x0 = p.x - outline_px; window->visible.y0 = p.y - outline_px - titlebar_height; window->visible.x1 = window->visible.x0 + width; @@ -85,6 +88,13 @@ void wuss_window_move(wuss_window_t *window, point_t p) dx = window->visible.x0 - before.x0; dy = window->visible.y0 - before.y0; + /* a pure translation leaves every cached rect correct relative to the + * window, just offset in screen space -- shift it instead of dropping the + * cache and rebuilding all pieces on the next paint of even a thin + * sliver */ + if (window->furniture_layout.valid) + wuss__furniture_layout_translate(window, dx, dy); + for (i = 0; i < nclean; i++) box_translated(&clean[i], dx, dy, &full_dest[i]); diff --git a/libraries/wuss/furniture.h b/libraries/wuss/furniture.h index 052e4fe9..d0d218cb 100644 --- a/libraries/wuss/furniture.h +++ b/libraries/wuss/furniture.h @@ -63,6 +63,12 @@ wuss__furniture_layout_t; /* Populate window->furniture_layout from the current geometry and flags. */ void wuss__furniture_layout_build(wuss_window_t *window); +/* Shift an already-valid window->furniture_layout by (dx, dy) in place -- + * for a pure window move, where every cached rect stays correct relative to + * the window, just offset in screen space, so there is no need to drop the + * cache and rebuild all pieces from scratch. */ +void wuss__furniture_layout_translate(wuss_window_t *window, int dx, int dy); + /* Which region of a window's border (or its content) a point falls in. */ typedef enum wuss_furniture_region { diff --git a/libraries/wuss/furniture/layout.c b/libraries/wuss/furniture/layout.c index 9888b824..2bf091bd 100644 --- a/libraries/wuss/furniture/layout.c +++ b/libraries/wuss/furniture/layout.c @@ -237,3 +237,17 @@ void wuss__furniture_layout_build(wuss_window_t *window) layout->valid = 1; } + +void wuss__furniture_layout_translate(wuss_window_t *window, int dx, int dy) +{ + wuss__furniture_layout_t *layout; + int i; + + layout = &window->furniture_layout; + + for (i = 0; i < layout->npieces; i++) + box_translated(&layout->pieces[i].rect, dx, dy, &layout->pieces[i].rect); + + if (layout->has_titlebar) + box_translated(&layout->titlebar, dx, dy, &layout->titlebar); +} From 4b47c212dcaea33de0fbe9ff731c27bce43f4b3c Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sat, 12 Sep 2026 19:21:36 +0100 Subject: [PATCH 049/128] perf(wuss): skip a window move that changes nothing wuss_mouse_move() calls wuss_window_move() once per pointer-move event during a drag, not once per actual change of position -- a window pinned against the screen edge, or a backend delivering redundant motion events, can call it repeatedly with the target it's already at. Early-return once the target origin is computed and found equal to the window's current origin (width/height never change here, so an unchanged origin means an unchanged box), before wuss__release_packed(), the furniture-layout translate, and the blit/invalidate machinery all run for no actual change. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/core/window/move.c | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/libraries/wuss/core/window/move.c b/libraries/wuss/core/window/move.c index edf9a804..b0240872 100644 --- a/libraries/wuss/core/window/move.c +++ b/libraries/wuss/core/window/move.c @@ -16,16 +16,26 @@ void wuss_window_move(wuss_window_t *window, point_t p) int dx, dy, nclean, ncopied, i; box_t before, dirty; - /* a manual move desyncs the window from its layout-packer slot; hand the - * slot back and stop tracking this window's position */ - wuss__release_packed(window); - width = window->visible.x1 - window->visible.x0; height = window->visible.y1 - window->visible.y0; outline_px = wuss__outline_px(window); titlebar_height = wuss__titlebar_height(window); before = window->visible; + /* a drag delivers one call per pointer-move event, not one per actual + * change of position -- a window pinned against the screen edge, or a + * backend that reports redundant motion, can call this with the same + * target it's already at. width/height never change here, so an + * unchanged origin means an unchanged box; skip the packer release, + * layout translate and blit/invalidate machinery entirely. */ + if (p.x - outline_px == before.x0 && + p.y - outline_px - titlebar_height == before.y0) + return; + + /* 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); + /* a hidden window has nothing on screen to slide and must paint nothing; * just translate its footprint so it is in place when shown again */ if (window->flags & wuss_WINDOW_HIDDEN) From ccdc2da5c99e032a6b1d7643dadcffb795f8ec89 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sat, 12 Sep 2026 19:24:10 +0100 Subject: [PATCH 050/128] perf(wuss): skip a window resize that changes nothing wuss_mouse_move() calls wuss__furniture_drag_resize() -> wuss_window_ resize() once per pointer-move event during a resize drag, not once per actual change of size -- a window pinned against the screen edge or the minimum-size clamp can call it repeatedly at the size it's already at. Early-return once the target bottom-right corner is computed and found equal to the window's current one (the top-left corner never moves in a resize, so an unchanged bottom-right corner means an unchanged box), before wuss__release_packed() and the scroll-reclamp/invalidate/blit machinery all run for no actual change. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/core/window/resize.c | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/libraries/wuss/core/window/resize.c b/libraries/wuss/core/window/resize.c index c8764223..51aad224 100644 --- a/libraries/wuss/core/window/resize.c +++ b/libraries/wuss/core/window/resize.c @@ -42,9 +42,6 @@ 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); - /* The requested content size is honoured verbatim: a window may end up * overhanging the screen edge (same as one dragged there, or one whose * far corner is off-screen when toggle-size floors it at WUSS_MIN_CONTENT). @@ -57,6 +54,19 @@ result_t wuss_window_resize(wuss_window_t *window, size2d_t size) wuss__content_box(window, &before_content); wuss__furniture_carve_for(window->flags, wuss__button_size(window), &carve); + /* a resize drag delivers one call per pointer-move event, not one per + * actual change of size -- a window pinned against the screen edge or + * the minimum-size clamp can call this repeatedly at the size it's + * already at. The top-left corner never moves here, so an unchanged + * bottom-right corner means an unchanged box; skip the packer release + * and the invalidate/blit machinery entirely. */ + if (window->visible.x0 + size.w + 2 * outline_px + carve.x == before.x1 && + window->visible.y0 + size.h + titlebar_height + 2 * outline_px + carve.y == before.y1) + return result_OK; + + /* a manual resize desyncs the window from its layout-packer slot */ + wuss__release_packed(window); + 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 9a0c2dd74214139550a7e8b010149a384d662f32 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sat, 12 Sep 2026 23:18:21 +0100 Subject: [PATCH 051/128] fix(wuss): exclude stale dirty regions from resize scroll-reclamp blit A resize drag delivers several calls per frame, same as move/scroll. The scroll-reclamp fast-path blit in wuss_window_resize read its source straight from the old content box without checking wuss->dirty[], unlike wuss_window_move and wuss_window_set_scroll. A resize call queued as dirty by an earlier call in the same frame but not yet painted could get slid forward as if it were settled content, corrupting the window. Add a regression test that reproduces this: growing a scrolled-to-max window two resize calls at a time (no flush between), flushing and checking pixels after every pair. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/core/window/resize.c | 27 +++- libraries/wuss/test/wuss-test.c | 192 ++++++++++++++++++++++++++++ 2 files changed, 217 insertions(+), 2 deletions(-) diff --git a/libraries/wuss/core/window/resize.c b/libraries/wuss/core/window/resize.c index 51aad224..ccb2b256 100644 --- a/libraries/wuss/core/window/resize.c +++ b/libraries/wuss/core/window/resize.c @@ -89,17 +89,40 @@ result_t wuss_window_resize(wuss_window_t *window, size2d_t size) if (clamped.x != window->scroll.x || clamped.y != window->scroll.y) { box_t content; + box_t stale[WUSS_MAX_DIRTY]; + box_t clean[WUSS_MAX_INVALIDATE_PIECES]; box_t src[WUSS_MAX_INVALIDATE_PIECES]; box_t copied[WUSS_MAX_INVALIDATE_PIECES]; box_t dirty[WUSS_MAX_INVALIDATE_PIECES]; - int dx, dy, nsrc, ncopied, ndirty, i; + int dx, dy, nstale, nclean, nsrc, ncopied, ndirty, i; dx = clamped.x - old_scroll.x; dy = clamped.y - old_scroll.y; window->scroll = clamped; wuss__content_box(window, &content); - nsrc = wuss__clip_to_visible(window, &before_content, src); + /* A resize drag delivers several calls per frame, same as a fast wheel + * spin (see wuss_window_set_scroll): an earlier call this frame may have + * queued part of "before_content" in wuss->dirty[] without a redraw + * having painted it yet. Sliding that stale ground would smear garbage + * into the window instead of this window's own settled pixels. */ + nstale = 0; + for (i = 0; i < window->wuss->ndirty; i++) + if (box_intersects(&window->wuss->dirty[i], &before_content)) + stale[nstale++] = window->wuss->dirty[i]; + + nclean = wuss__clip_to_visible(window, &before_content, clean); + nsrc = 0; + for (i = 0; i < nclean; i++) + { + box_t kept[WUSS_MAX_INVALIDATE_PIECES]; + int nkept, k; + + nkept = wuss__subtract_boxes(&clean[i], stale, nstale, kept); + for (k = 0; k < nkept && nsrc < WUSS_MAX_INVALIDATE_PIECES; k++) + src[nsrc++] = kept[k]; + } + ncopied = 0; window->wuss->scr->clip = content; for (i = 0; i < nsrc; i++) diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 1713e896..55041038 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -2480,6 +2480,198 @@ result_t wuss_test(const char *resources) wuss_window_close(win_s); } + printf("test: shrinking a window repaints its content right up to the new content box's own edge\n"); + + { + /* Regression: dragging a resize handle to make a window smaller must + * leave no stale sliver between the client's repainted content and the + * furniture (scrollbar/outline) now sitting at the new, smaller edge. */ + static test_task_t tc_s; + wuss_task_t *delegate_s; + box_t box_s, content_s; + wuss_window_t *win_s; + colour_t s_colour; + int nx, ny, bad; + + s_colour = colour_rgb(0xcc, 0xdd, 0xee); + tc_s.redraw_count = 0; tc_s.mouse_count = 0; + delegate_s = mk_task(wuss, flood_full_bounds_handle, &s_colour); + if (delegate_s == NULL) goto Failure; + + box_s.x0 = 10; box_s.y0 = 10; + box_s.x1 = 90; box_s.y1 = 90; + rc = wuss_window_create(delegate_s, &box_s, "S", wuss_WINDOW_DEFAULT, + wuss_NO_BACKDROP, + SIZE2D(80, 80), SIZE2D(200, 200), &win_s); + if (rc != result_OK) + goto Failure; + rc = wuss_redraw_dirty(wuss); /* flush the create, paint S's initial content */ + if (rc != result_OK) + goto Failure; + + /* scroll before shrinking -- the shrink must not leave a stale sliver + * at the new content edge regardless of scroll offset */ + wuss_window_set_scroll(win_s, POINT(20, 20)); + rc = wuss_redraw_dirty(wuss); + if (rc != result_OK) + goto Failure; + + /* an interactive drag delivers many small pointer-move events, each + * calling wuss_window_resize, before the display ever gets to redraw -- + * shrink one pixel at a time with no flush between them, matching that; + * throw in an overshoot-and-correct wobble too, as a real drag does */ + { + int step, w; + + for (step = 0, w = 80; w > 30; w--, step++) + { + rc = wuss_window_resize(win_s, SIZE2D(w, w)); + if (rc != result_OK) + goto Failure; + } + rc = wuss_window_resize(win_s, SIZE2D(35, 35)); /* overshoot back out */ + if (rc != result_OK) + goto Failure; + rc = wuss_window_resize(win_s, SIZE2D(30, 30)); /* settle */ + if (rc != result_OK) + goto Failure; + } + rc = wuss_redraw_dirty(wuss); + if (rc != result_OK) + goto Failure; + + /* every pixel of S's own (new, smaller) content box must show S's + * flood colour right up to content_s.x1-1 / content_s.y1-1 -- no gap + * between the repainted content and the furniture now abutting it */ + wuss_window_get_content_bounds(win_s, &content_s); + bad = 0; + for (ny = content_s.y0; ny < content_s.y1 && !bad; ny++) + for (nx = content_s.x0; nx < content_s.x1; nx++) + { + uint32_t px; + + px = ((const uint32_t *) pixels)[ny * 200 + nx]; + if ((px & 0xffffff) != 0xccddee) + { + bad = 1; + break; + } + } + if (bad) + goto Failure; /* stale sliver left along the new content box's edge */ + + wuss_window_close(win_s); + } + + printf("test: a fast resize-grow past the scroll clamp never blits a not-yet-painted sliver\n"); + + { + /* Regression: wuss_window_resize's scroll-reclamp blit (window scrolled + * to the document's bottom/right, then grown so the bigger viewport + * needs less scroll and the clamp pulls the offset back) reads its blit + * source straight from wuss__clip_to_visible over the *old* content box, + * unlike wuss_window_move/wuss_window_set_scroll which both also strip + * anything still sitting in wuss->dirty[] (queued by an earlier call this + * same frame, not yet painted). An interactive drag delivers several + * resize calls before a redraw ever runs: one grow queues its + * newly-exposed sliver dirty without painting it; that sliver sits + * inside the *next* call's "old content box", so the next call's + * scroll-reclamp blit slides it as if it were this window's own settled + * rendering -- smearing a stale/undrawn sliver that no later invalidate + * ever repaints. paint_handle's per-row blue-encoded doc_y makes a + * wrongly-slid row detectable exactly. */ + static test_task_t tc_s; + wuss_task_t *delegate_s; + box_t box_s, content_s; + wuss_window_t *win_s; + colour_t s_colour; + point_t scroll; + int sy, sx, bad; + + s_colour = colour_rgb(0x11, 0x22, 0x33); + tc_s.redraw_count = 0; tc_s.mouse_count = 0; + delegate_s = mk_task(wuss, paint_handle, &s_colour); + if (delegate_s == NULL) goto Failure; + + box_s.x0 = 10; box_s.y0 = 10; + box_s.x1 = 40; box_s.y1 = 40; /* 30x30 content; a doc far bigger than the + * screen so growing 1px at a time pulls the + * scroll clamp back by exactly 1px per + * call, keeping it non-zero (and so + * re-triggering the reclamp path) across + * the whole grow range below */ + rc = wuss_window_create(delegate_s, &box_s, "S", wuss_WINDOW_DEFAULT, + wuss_NO_BACKDROP, + SIZE2D(1000, 1000), SIZE2D(30, 30), &win_s); + if (rc != result_OK) + goto Failure; + rc = wuss_redraw_dirty(wuss); /* flush the create, paint S's initial content */ + if (rc != result_OK) + goto Failure; + + /* scroll to the document's bottom-right: no further offset possible at + * this size */ + wuss_window_set_scroll(win_s, POINT(970, 970)); + rc = wuss_redraw_dirty(wuss); + if (rc != result_OK) + goto Failure; + + /* grow two calls at a time with no flush between them, as an interactive + * drag delivers several pointer-move events per rendered frame -- each + * bigger size pulls the scroll clamp back by 1px, so every single call + * re-enters the reclamp path, and the second call of each pair reads its + * blit source over ground the first call of the pair only just + * invalidated (its own grown sliver) but never painted. Flushing after + * every pair (rather than only once at the end) matches a real redraw + * loop -- it also stops one pair's leftover full-footprint dirty rect + * from coalescing over a later pair's corruption and masking it. */ + { + int w; + + for (w = 30; w <= 90; w += 2) + { + rc = wuss_window_resize(win_s, SIZE2D(w, w)); + if (rc != result_OK) + goto Failure; + rc = wuss_window_resize(win_s, SIZE2D(w + 1, w + 1)); + if (rc != result_OK) + goto Failure; + + rc = wuss_redraw_dirty(wuss); + if (rc != result_OK) + goto Failure; + + /* every row of S's current content box must show exactly the + * document Y that its current scroll offset puts there -- a + * stale-sliver blit leaves some row(s) showing a stale doc_y (or + * whatever else was sitting in the framebuffer) instead */ + wuss_window_get_content_bounds(win_s, &content_s); + wuss_window_get_scroll(win_s, &scroll); + bad = 0; + for (sy = content_s.y0; sy < content_s.y1 && !bad; sy++) + { + uint32_t px, blue, want; + + want = (uint32_t) ((sy - content_s.y0 + scroll.y) & 0xff); + for (sx = content_s.x0; sx < content_s.x1; sx++) + { + px = ((const uint32_t *) pixels)[sy * 200 + sx]; + blue = px & 0xff; + if (blue != want) + { + bad = 1; + break; + } + } + } + if (bad) + goto Failure; /* a not-yet-painted sliver got blitted as if settled */ + } + } + + wuss_window_close(win_s); + } + printf("test: wuss_window_invalidate_visible on a shrunk, scrolled window only dirties its own visible box\n"); { From 876b0eba9627504e35078a1990e9792e02cd219c Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 13 Sep 2026 00:02:08 +0100 Subject: [PATCH 052/128] fix(wuss): guard resize scroll-reclamp blit against piece-budget overflow Mirror set-scroll.c's overflow handling: if subtracting stale dirty regions from the clean source pieces splits past WUSS_MAX_INVALIDATE_PIECES, fall back to a full repaint instead of silently dropping survivor pieces. A dropped piece was neither blitted nor invalidated, since the invalidate step only covers content minus copied, not content minus every clean piece found. Add a regression test covering a fast resize drag that jumps size by large deltas and reverses direction within one unflushed batch, closer to a real interactive drag than the existing paired-grow test. --- libraries/wuss/core/window/resize.c | 51 +++++++++++++----- libraries/wuss/test/wuss-test.c | 82 +++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+), 14 deletions(-) diff --git a/libraries/wuss/core/window/resize.c b/libraries/wuss/core/window/resize.c index ccb2b256..e632f9f7 100644 --- a/libraries/wuss/core/window/resize.c +++ b/libraries/wuss/core/window/resize.c @@ -94,7 +94,7 @@ result_t wuss_window_resize(wuss_window_t *window, size2d_t size) box_t src[WUSS_MAX_INVALIDATE_PIECES]; box_t copied[WUSS_MAX_INVALIDATE_PIECES]; box_t dirty[WUSS_MAX_INVALIDATE_PIECES]; - int dx, dy, nstale, nclean, nsrc, ncopied, ndirty, i; + int dx, dy, nstale, nclean, nsrc, ncopied, ndirty, i, overflow; dx = clamped.x - old_scroll.x; dy = clamped.y - old_scroll.y; @@ -111,29 +111,50 @@ result_t wuss_window_resize(wuss_window_t *window, size2d_t size) if (box_intersects(&window->wuss->dirty[i], &before_content)) stale[nstale++] = window->wuss->dirty[i]; + /* Each subtract can split a piece into up to four bands, so this can + * overflow the piece budget on a badly fragmented window -- treat that + * as "no safe fast path" (see wuss_window_set_scroll) rather than + * silently dropping survivors: a dropped clean piece never gets + * invalidated either, since the invalidate below only covers "content + * minus copied", not "content minus every clean piece found" -- so a + * silently-dropped piece would leave genuinely stale pre-resize pixels + * on screen with nothing left to repaint them. */ nclean = wuss__clip_to_visible(window, &before_content, clean); nsrc = 0; + overflow = 0; for (i = 0; i < nclean; i++) { box_t kept[WUSS_MAX_INVALIDATE_PIECES]; int nkept, k; nkept = wuss__subtract_boxes(&clean[i], stale, nstale, kept); - for (k = 0; k < nkept && nsrc < WUSS_MAX_INVALIDATE_PIECES; k++) + for (k = 0; k < nkept; k++) + { + if (nsrc == WUSS_MAX_INVALIDATE_PIECES) + { + overflow = 1; + break; + } src[nsrc++] = kept[k]; + } + if (overflow) + break; } ncopied = 0; - window->wuss->scr->clip = content; - for (i = 0; i < nsrc; i++) + if (!overflow) { - box_t got; - - if (screen_copy_rect(window->wuss->scr, &src[i], - POINT(src[i].x0 - dx, src[i].y0 - dy), - &got) == result_OK && - ncopied < WUSS_MAX_INVALIDATE_PIECES) - copied[ncopied++] = got; + window->wuss->scr->clip = content; + for (i = 0; i < nsrc; i++) + { + box_t got; + + if (screen_copy_rect(window->wuss->scr, &src[i], + POINT(src[i].x0 - dx, src[i].y0 - dy), + &got) == result_OK && + ncopied < WUSS_MAX_INVALIDATE_PIECES) + copied[ncopied++] = got; + } } if (ncopied > 0) @@ -148,10 +169,12 @@ result_t wuss_window_resize(wuss_window_t *window, size2d_t size) } else { - /* the scroll-reclamp blit copied nothing (paletted screen, or every - * piece off-screen): repaint the whole content box */ + /* the scroll-reclamp blit copied nothing (paletted screen, every + * piece off-screen, or the piece budget overflowed): repaint the + * whole content box */ logf_warning("wuss_window_resize: scroll-reclamp blit copied nothing, " - "repainting the whole content box (nsrc=%d)", nsrc); + "repainting the whole content box (nsrc=%d, overflow=%d)", + nsrc, overflow); wuss__invalidate_clipped(window, &content); } wuss__chrome_repaint(window); diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 55041038..3f101fe0 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -2672,6 +2672,88 @@ result_t wuss_test(const char *resources) wuss_window_close(win_s); } + printf("test: a fast resize drag that jumps size and reverses direction never blits a not-yet-painted sliver\n"); + + { + /* Same hazard as the paired-grow test above, but matching a real + * interactive drag more closely: the pointer can jump the size by a + * large delta in one event (not just 1px), and can reverse from + * growing to shrinking and back within the same unflushed batch. A + * shrink never re-enters the reclamp path itself (it only ever raises + * the clamp ceiling), but it still queues its own shrunk-away sliver + * dirty -- and the very next call, if it grows back past the clamp, + * must not treat that still-unpainted sliver as settled ground either. */ + static test_task_t tc_e; + wuss_task_t *delegate_e; + box_t box_e, content_e; + wuss_window_t *win_e; + colour_t e_colour; + point_t scroll; + int sy, sx, bad, step; + int sizes[] = { 30, 90, 35, 120, 31, 90, 30, 60, 110, 30 }; + + e_colour = colour_rgb(0x11, 0x22, 0x33); + tc_e.redraw_count = 0; tc_e.mouse_count = 0; + delegate_e = mk_task(wuss, paint_handle, &e_colour); + if (delegate_e == NULL) goto Failure; + + box_e.x0 = 10; box_e.y0 = 10; + box_e.x1 = 40; box_e.y1 = 40; + rc = wuss_window_create(delegate_e, &box_e, "E", wuss_WINDOW_DEFAULT, + wuss_NO_BACKDROP, + SIZE2D(1000, 1000), SIZE2D(30, 30), &win_e); + if (rc != result_OK) + goto Failure; + rc = wuss_redraw_dirty(wuss); + if (rc != result_OK) + goto Failure; + + wuss_window_set_scroll(win_e, POINT(970, 970)); + rc = wuss_redraw_dirty(wuss); + if (rc != result_OK) + goto Failure; + + for (step = 0; step < 9; step += 2) + { + rc = wuss_window_resize(win_e, SIZE2D(sizes[step], sizes[step])); + if (rc != result_OK) + goto Failure; + rc = wuss_window_resize(win_e, SIZE2D(sizes[step + 1], sizes[step + 1])); + if (rc != result_OK) + goto Failure; + + rc = wuss_redraw_dirty(wuss); + if (rc != result_OK) + goto Failure; + + wuss_window_get_content_bounds(win_e, &content_e); + wuss_window_get_scroll(win_e, &scroll); + content_e.x1 = MIN(content_e.x1, 200); + content_e.y1 = MIN(content_e.y1, 200); + bad = 0; + for (sy = content_e.y0; sy < content_e.y1 && !bad; sy++) + { + uint32_t px, blue, want; + + want = (uint32_t) ((sy - content_e.y0 + scroll.y) & 0xff); + for (sx = content_e.x0; sx < content_e.x1; sx++) + { + px = ((const uint32_t *) pixels)[sy * 200 + sx]; + blue = px & 0xff; + if (blue != want) + { + bad = 1; + break; + } + } + } + if (bad) + goto Failure; /* a not-yet-painted sliver got blitted as if settled */ + } + + wuss_window_close(win_e); + } + printf("test: wuss_window_invalidate_visible on a shrunk, scrolled window only dirties its own visible box\n"); { From 1a1dc17b219a6cc77568fb0b21a495c9a628d691 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 13 Sep 2026 00:38:37 +0100 Subject: [PATCH 053/128] fix(wuss): remove duplicate chrome repaint on scroll-reclamp resize wuss_window_resize invalidated the window's furniture twice on any resize that also reclamps scroll (near-guaranteed during a scrolled drag-resize): once at the end of the reclamp block, again unconditionally at the end of the function. Drop the redundant call inside the reclamp block; the tail already covers both the blit and non-blit paths. Add a stress test that fires a long unflushed batch of resize calls (matching a high-poll-rate mouse drag), including an occluding window, and checks no not-yet-painted sliver ever gets blitted as settled content. --- libraries/wuss/core/window/resize.c | 1 - libraries/wuss/test/wuss-test.c | 124 ++++++++++++++++++++++++++++ 2 files changed, 124 insertions(+), 1 deletion(-) diff --git a/libraries/wuss/core/window/resize.c b/libraries/wuss/core/window/resize.c index e632f9f7..a9e43084 100644 --- a/libraries/wuss/core/window/resize.c +++ b/libraries/wuss/core/window/resize.c @@ -177,7 +177,6 @@ result_t wuss_window_resize(wuss_window_t *window, size2d_t size) nsrc, overflow); wuss__invalidate_clipped(window, &content); } - wuss__chrome_repaint(window); } if (window->flags & wuss_WINDOW_NO_RESIZE_BLIT) diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 3f101fe0..d7a28e6c 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -2754,6 +2754,130 @@ result_t wuss_test(const char *resources) wuss_window_close(win_e); } + printf("test: a long unflushed batch of resize calls (matching a high-poll-rate mouse) never blits a not-yet-painted sliver\n"); + + { + /* The tests above batch only 1-2 resize calls per redraw; a real SDL + * drag drains its whole event queue before ever calling + * wuss_redraw_dirty (see apps/wuss/main.c's wuss_frame), so a fast + * high-poll-rate mouse can deliver dozens of resize calls in one + * batch. Stress that scale directly, using a cheap deterministic + * pseudo-random walk rather than a fixed table, so the sequence covers + * far more shapes (including piece-budget overflow) than a short + * hand-picked list ever would. */ + static test_task_t tc_f, tc_occ; + wuss_task_t *delegate_f, *delegate_occ; + box_t box_f, box_occ, content_f; + wuss_window_t *win_f, *win_occ; + colour_t f_colour; + point_t scroll; + unsigned int seed; + int sy, sx, bad, batch, step, sizew, sizeh; + + f_colour = colour_rgb(0x44, 0x55, 0x66); + tc_f.redraw_count = 0; tc_f.mouse_count = 0; + delegate_f = mk_task(wuss, paint_handle, &f_colour); + if (delegate_f == NULL) goto Failure; + + box_f.x0 = 10; box_f.y0 = 10; + box_f.x1 = 40; box_f.y1 = 40; + rc = wuss_window_create(delegate_f, &box_f, "F", wuss_WINDOW_DEFAULT, + wuss_NO_BACKDROP, + SIZE2D(1000, 1000), SIZE2D(30, 30), &win_f); + if (rc != result_OK) + goto Failure; + + /* An occluder created after (so it lands on top of) win_f, sat over its + * corner: real desktops are rarely a single window, and + * wuss__clip_to_visible's occlusion-carving path (untouched by every + * test above) can only interact with the reclamp blit's stale-region + * exclusion when there is something to occlude. New windows land at the + * z_order head (topmost) -- see wuss_window_create. A NULL task_data + * gives paint_handle's flat-fill branch, so the occluded area's colour + * can be told apart from win_f's doc-row pattern by eye if this ever + * fails; the pixel check below instead uses win_occ->visible (its real, + * furniture-adjusted footprint) rather than the requested box, since + * wuss__nudge_visible_onscreen/carve can move or grow it. */ + tc_occ.redraw_count = 0; tc_occ.mouse_count = 0; + delegate_occ = mk_task(wuss, paint_handle, NULL); + if (delegate_occ == NULL) goto Failure; + + box_occ.x0 = 25; box_occ.y0 = 25; + box_occ.x1 = 60; box_occ.y1 = 60; + rc = wuss_window_create(delegate_occ, &box_occ, "OCC", wuss_WINDOW_DEFAULT, + wuss_NO_BACKDROP, + SIZE2D(20, 20), SIZE2D(20, 20), &win_occ); + if (rc != result_OK) + goto Failure; + + rc = wuss_redraw_dirty(wuss); + if (rc != result_OK) + goto Failure; + + wuss_window_set_scroll(win_f, POINT(970, 970)); + rc = wuss_redraw_dirty(wuss); + if (rc != result_OK) + goto Failure; + + seed = 12345u; + sizew = 30; + sizeh = 30; + for (batch = 0; batch < 20; batch++) + { + for (step = 0; step < 25; step++) + { + seed = seed * 1103515245u + 12345u; + sizew += (int) (seed >> 24) % 21 - 10; /* +/-10 px per call */ + seed = seed * 1103515245u + 12345u; + sizeh += (int) (seed >> 24) % 21 - 10; + sizew = CLAMP(sizew, 30, 150); + sizeh = CLAMP(sizeh, 30, 150); + + rc = wuss_window_resize(win_f, SIZE2D(sizew, sizeh)); + if (rc != result_OK) + goto Failure; + } + + rc = wuss_redraw_dirty(wuss); + if (rc != result_OK) + goto Failure; + + wuss_window_get_content_bounds(win_f, &content_f); + wuss_window_get_scroll(win_f, &scroll); + content_f.x1 = MIN(content_f.x1, 200); + content_f.y1 = MIN(content_f.y1, 200); + bad = 0; + for (sy = content_f.y0; sy < content_f.y1 && !bad; sy++) + { + uint32_t px, blue, want; + + want = (uint32_t) ((sy - content_f.y0 + scroll.y) & 0xff); + for (sx = content_f.x0; sx < content_f.x1; sx++) + { + /* skip ground legitimately covered by the occluder's real, + * furniture-adjusted footprint -- it shows win_occ's own colour + * there, not win_f's doc-row pattern */ + if (sx >= win_occ->visible.x0 && sx < win_occ->visible.x1 && + sy >= win_occ->visible.y0 && sy < win_occ->visible.y1) + continue; + + px = ((const uint32_t *) pixels)[sy * 200 + sx]; + blue = px & 0xff; + if (blue != want) + { + bad = 1; + break; + } + } + } + if (bad) + goto Failure; /* a not-yet-painted sliver got blitted as if settled */ + } + + wuss_window_close(win_occ); + wuss_window_close(win_f); + } + printf("test: wuss_window_invalidate_visible on a shrunk, scrolled window only dirties its own visible box\n"); { From 2ed21700b5dcc97fb17b09fd490824c43b8d8fea Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 13 Sep 2026 00:46:37 +0100 Subject: [PATCH 054/128] feat(wuss): ADJUST-click in a scroll well pages away from the pointer SELECT still pages the content towards the click, RISC OS style; ADJUST mirrors that, matching RISC OS's usual SELECT/ADJUST reversal elsewhere in the furniture (e.g. the arrow icons). --- libraries/wuss/core/mouse-click.c | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/libraries/wuss/core/mouse-click.c b/libraries/wuss/core/mouse-click.c index 5a5f7ecc..9554f242 100644 --- a/libraries/wuss/core/mouse-click.c +++ b/libraries/wuss/core/mouse-click.c @@ -204,16 +204,19 @@ result_t wuss_mouse_click(wuss_t *wuss, /* A click landing in the well itself (not on the sausage) pages the * content one visible extent towards the click, RISC OS style, keeping - * one step of overlap. The drag state is still armed below so a press - * that then moves onto the sausage keeps working. */ + * one step of overlap. ADJUST reverses that -- it pages away from the + * click instead, matching RISC OS's SELECT/ADJUST mirroring elsewhere + * (e.g. the arrow icons). The drag state is still armed below so a + * press that then moves onto the sausage keeps working. */ if (region == wuss_FURNITURE_VSCROLL_WELL || region == wuss_FURNITURE_HSCROLL_WELL) { box_t sausage; box_t content; - int page; + int sign, page; wuss__content_box(win, &content); + sign = (button & wuss_BUTTON_ADJUST) ? -1 : 1; if (region == wuss_FURNITURE_VSCROLL_WELL) { @@ -221,9 +224,9 @@ result_t wuss_mouse_click(wuss_t *wuss, page = (content.y1 - content.y0) - WUSS_SCROLL_STEP; page = MAX(page, 1); if (y < sausage.y0) - wuss__scroll_step(win, POINT(0, -page)); + wuss__scroll_step(win, POINT(0, -page * sign)); else if (y > sausage.y1) - wuss__scroll_step(win, POINT(0, page)); + wuss__scroll_step(win, POINT(0, page * sign)); } else { @@ -231,9 +234,9 @@ result_t wuss_mouse_click(wuss_t *wuss, page = (content.x1 - content.x0) - WUSS_SCROLL_STEP; page = MAX(page, 1); if (x < sausage.x0) - wuss__scroll_step(win, POINT(-page, 0)); + wuss__scroll_step(win, POINT(-page * sign, 0)); else if (x > sausage.x1) - wuss__scroll_step(win, POINT(page, 0)); + wuss__scroll_step(win, POINT(page * sign, 0)); } } From a3231e85a50c8e83367ddd4aa3c6b66735210e60 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 13 Sep 2026 00:57:50 +0100 Subject: [PATCH 055/128] fix(wuss): don't arm a sausage drag on a scroll-well click A well click already pages the content once; holding the button after that click was also arming a sausage drag, so moving the pointer without releasing started a live scroll as though the sausage itself had been grabbed. --- libraries/wuss/core/mouse-click.c | 41 ++++++++++++++++++------------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/libraries/wuss/core/mouse-click.c b/libraries/wuss/core/mouse-click.c index 9554f242..fd599e15 100644 --- a/libraries/wuss/core/mouse-click.c +++ b/libraries/wuss/core/mouse-click.c @@ -240,24 +240,31 @@ result_t wuss_mouse_click(wuss_t *wuss, } } - 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) + /* A well click only pages once; it must not also arm a sausage drag, + * or holding the button after the page would start scrolling live + * as though the pointer had grabbed the sausage itself. */ + if (region != wuss_FURNITURE_VSCROLL_WELL && + region != wuss_FURNITURE_HSCROLL_WELL) { - box_t content; - wuss__content_box(win, &content); - wuss->furniture.drag_offset.x = x - content.x1; - wuss->furniture.drag_offset.y = y - content.y1; + 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; From 31ffe31211ce310beeac0299c2d23b3df131caa0 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 13 Sep 2026 15:38:49 +0100 Subject: [PATCH 056/128] perf(wuss): convert only dirty rows on SDL present wuss_frontend_present now takes a dirty box_t*; the SDL backend row-crops the paletted bitmap before bitmap_convert and only feeds the changed rows to SDL_UpdateTexture, instead of converting the whole screen every frame. main.c captures the dirty bounding box (via wuss_get_dirty/wuss_get_dirty_count) before wuss_redraw_dirty clears it, and passes NULL for the full-screen garbage/pixel-stress paths. Column cropping is skipped: p1/p2/p4 pack multiple pixels per byte, so only a byte-aligned x0 would be safe. The RISC OS backend ignores the new argument, as it already ignores the bitmap. --- apps/wuss/frontend-riscos.c | 5 +- apps/wuss/frontend-sdl.c | 98 ++++++++++++++++++++++++------------- apps/wuss/frontend.h | 12 ++++- apps/wuss/main.c | 25 +++++++--- 4 files changed, 97 insertions(+), 43 deletions(-) diff --git a/apps/wuss/frontend-riscos.c b/apps/wuss/frontend-riscos.c index 08afd975..be0dd17b 100644 --- a/apps/wuss/frontend-riscos.c +++ b/apps/wuss/frontend-riscos.c @@ -331,10 +331,13 @@ bool wuss_frontend_poll(wuss_frontend_t *fe, wuss_input_t *event) return false; } -void wuss_frontend_present(wuss_frontend_t *fe, const bitmap_t *bm) +void wuss_frontend_present(wuss_frontend_t *fe, + const bitmap_t *bm, + const box_t *dirty) { NOT_USED(fe); NOT_USED(bm); + NOT_USED(dirty); /* wuss drew straight into screen memory; just pace to the frame rate */ _swix(OS_Byte, _IN(0), 19); diff --git a/apps/wuss/frontend-sdl.c b/apps/wuss/frontend-sdl.c index 15c8287b..d876700a 100644 --- a/apps/wuss/frontend-sdl.c +++ b/apps/wuss/frontend-sdl.c @@ -112,11 +112,7 @@ result_t wuss_frontend_open(int width, scale = WUSS_SDL_DEFAULT_SCALE; scale = CLAMP(scale, WUSS_SDL_MIN_SCALE, WUSS_SDL_MAX_SCALE); - stride = (depth == 32) ? width * 4 /* pixelfmt_bgrx8888: 4 bytes/pixel */ - : (depth == 8) ? width /* pixelfmt_p8: 1 byte/pixel */ - : (depth == 4) ? width / 2 /* pixelfmt_p4: 2 pixels/byte */ - : (depth == 2) ? (width + 3) / 4 /* pixelfmt_p2: 4 pixels/byte */ - : (width + 7) / 8; /* pixelfmt_p1: 8 pixels/byte */ + stride = (width * depth + 7) >> 3; fe = calloc(1, sizeof(*fe)); if (fe == NULL) @@ -206,36 +202,42 @@ bool wuss_frontend_poll(wuss_frontend_t *fe, wuss_input_t *event) return true; case SDL_EVENT_KEY_UP: - if (ev.key.key == SDLK_F4) - event->kind = wuss_INPUT_QUIT; - else if (ev.key.key == SDLK_F1 && (ev.key.mod & SDL_KMOD_SHIFT)) - event->kind = wuss_INPUT_GARBAGE; - else if (ev.key.key == SDLK_F1) - event->kind = wuss_INPUT_REDRAW_ALL; - else if (ev.key.key == SDLK_F3) - event->kind = wuss_INPUT_PIXEL_STRESS; - else if (ev.key.key == SDLK_F2) + switch (ev.key.key) { - int scale; - - /* F2 steps the SDL window zoom up, Shift-F2 down, clamped to - * [WUSS_SDL_MIN_SCALE, WUSS_SDL_MAX_SCALE]: a backend-local zoom the - * demo loop never sees. */ - scale = fe->scale + ((ev.key.mod & SDL_KMOD_SHIFT) ? -1 : 1); - if (scale < WUSS_SDL_MIN_SCALE) - scale = WUSS_SDL_MIN_SCALE; - else if (scale > WUSS_SDL_MAX_SCALE) - scale = WUSS_SDL_MAX_SCALE; - if (scale != fe->scale) + case SDLK_F1: + event->kind = (ev.key.mod & SDL_KMOD_SHIFT) + ? wuss_INPUT_GARBAGE + : wuss_INPUT_REDRAW_ALL; + break; + case SDLK_F2: { - fe->scale = scale; - SDL_SetWindowSize(fe->window, fe->scr_width * scale, - fe->scr_height * scale); + int scale; + + /* F2 steps the SDL window zoom up, Shift-F2 down, clamped to + * [WUSS_SDL_MIN_SCALE, WUSS_SDL_MAX_SCALE]: a backend-local zoom the + * demo loop never sees. */ + scale = fe->scale + ((ev.key.mod & SDL_KMOD_SHIFT) ? -1 : 1); + if (scale < WUSS_SDL_MIN_SCALE) + scale = WUSS_SDL_MIN_SCALE; + else if (scale > WUSS_SDL_MAX_SCALE) + scale = WUSS_SDL_MAX_SCALE; + if (scale != fe->scale) + { + fe->scale = scale; + SDL_SetWindowSize(fe->window, fe->scr_width * scale, + fe->scr_height * scale); + } + continue; } + case SDLK_F3: + event->kind = wuss_INPUT_PIXEL_STRESS; + break; + case SDLK_F4: + event->kind = wuss_INPUT_QUIT; + break; + default: continue; } - else - continue; return true; case SDL_EVENT_MOUSE_BUTTON_DOWN: @@ -291,7 +293,9 @@ bool wuss_frontend_poll(wuss_frontend_t *fe, wuss_input_t *event) } } -void wuss_frontend_present(wuss_frontend_t *fe, const bitmap_t *bm) +void wuss_frontend_present(wuss_frontend_t *fe, + const bitmap_t *bm, + const box_t *dirty) { if (fe->depth == 32) { @@ -299,20 +303,46 @@ void wuss_frontend_present(wuss_frontend_t *fe, const bitmap_t *bm) } else { - bitmap_t *disp; + result_t rc; + bitmap_t rows, *disp; + int y0, y1; + SDL_Rect rect; + + /* Sub-byte formats (p1/p2/p4) pack several pixels per byte, so only a + * whole-row crop is safe without redoing their bit-unpacking maths for an + * arbitrary x0; a dirty rect just narrows which rows get converted. */ + y0 = (dirty != NULL) ? CLAMP(dirty->y0, 0, bm->size.h) : 0; + y1 = (dirty != NULL) ? CLAMP(dirty->y1, 0, bm->size.h) : bm->size.h; + if (y1 <= y0) + goto present; + + rc = bitmap_init(&rows, + SIZE2D(bm->size.w, y1 - y0), + bm->format, + bm->rowbytes, + bm->palette, + (unsigned char *) bm->base + y0 * bm->rowbytes); + if (rc != result_OK) + goto present; /* wuss draws into a paletted bitmap; SDL wants bgrx. bitmap_convert reads * the palette straight off `bm`, which the caller updates when the palette * task's picker menu changes it, so a live palette change just shows up in * the next converted frame. */ - if (bitmap_convert(bm, pixelfmt_bgrx8888, &disp) == result_OK) + if (bitmap_convert(&rows, pixelfmt_bgrx8888, &disp) == result_OK) { - SDL_UpdateTexture(fe->texture, NULL, disp->base, disp->rowbytes); + rect.x = 0; + rect.y = y0; + rect.w = bm->size.w; + rect.h = y1 - y0; + + SDL_UpdateTexture(fe->texture, &rect, disp->base, disp->rowbytes); free(disp->base); free(disp); } } +present: SDL_RenderTexture(fe->renderer, fe->texture, NULL, NULL); SDL_RenderPresent(fe->renderer); diff --git a/apps/wuss/frontend.h b/apps/wuss/frontend.h index 4421d2e9..52aa46ac 100644 --- a/apps/wuss/frontend.h +++ b/apps/wuss/frontend.h @@ -11,6 +11,7 @@ #include "framebuf/bitmap.h" #include "framebuf/colour.h" #include "framebuf/pixelfmt.h" +#include "geom/box.h" #include "geom/point.h" #include "wuss/wuss.h" @@ -85,8 +86,15 @@ result_t wuss_frontend_open(int width, bool wuss_frontend_poll(wuss_frontend_t *frontend, wuss_input_t *event); /* Push the current framebuffer contents to the screen and pace the frame. A - * backend rendering straight into screen memory only waits for vsync here. */ -void wuss_frontend_present(wuss_frontend_t *frontend, const bitmap_t *bm); + * backend rendering straight into screen memory only waits for vsync here. + * + * `dirty`, if non-NULL, bounds the region that actually changed since the + * last present; a backend that has to convert `bm` (paletted -> display + * format) may use it to convert only that rect instead of the whole + * bitmap. NULL means "assume the whole bitmap changed". */ +void wuss_frontend_present(wuss_frontend_t *frontend, + const bitmap_t *bm, + const box_t *dirty); /* Push a new system palette to the physical palette, if the backend owns one. * Called after the palette task's picker menu changes the system palette. diff --git a/apps/wuss/main.c b/apps/wuss/main.c index 0e694e04..71e8bb7c 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -207,16 +207,29 @@ static void wuss_frame(void *arg) for (i = 0; i < n; i++) p[i] = (unsigned char) rand(); - wuss_frontend_present(c->frontend, c->bm); + wuss_frontend_present(c->frontend, c->bm, NULL); + } + else if (pixel_stress_pending) + { + pixel_stress(c->wuss, c->scr_width, c->scr_height); + wuss_frontend_present(c->frontend, c->bm, NULL); } else { - if (pixel_stress_pending) - pixel_stress(c->wuss, c->scr_width, c->scr_height); - else - wuss_redraw_dirty(c->wuss); + box_t dirty, region; + int ndirty, i; + + ndirty = wuss_get_dirty_count(c->wuss); + dirty = (box_t) BOX_INIT; + for (i = 0; i < ndirty; i++) + { + wuss_get_dirty(c->wuss, i, ®ion); + box_union(&dirty, ®ion, &dirty); + } + + wuss_redraw_dirty(c->wuss); - wuss_frontend_present(c->frontend, c->bm); + wuss_frontend_present(c->frontend, c->bm, ndirty > 0 ? &dirty : NULL); } #ifdef __EMSCRIPTEN__ From 18031f34e599ec92b67adb617052515c6dd1df95 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 13 Sep 2026 15:41:39 +0100 Subject: [PATCH 057/128] fix(wuss): allow sausage drag arming on a direct sausage hit wuss_FURNITURE_VSCROLL_WELL/HSCROLL_WELL cover both the well and the sausage itself -- hit-test doesn't distinguish them. a3231e8 blanket-skipped drag arming for that whole region to stop a well click's page-and-hold from also starting a live scroll, but that same guard skipped arming for a direct hit on the sausage, breaking sausage clicks entirely. Track whether the click actually paged (landed off-sausage) and gate the skip on that instead of on region, so a direct sausage hit still arms the drag. --- libraries/wuss/core/mouse-click.c | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/libraries/wuss/core/mouse-click.c b/libraries/wuss/core/mouse-click.c index fd599e15..282454ab 100644 --- a/libraries/wuss/core/mouse-click.c +++ b/libraries/wuss/core/mouse-click.c @@ -196,6 +196,7 @@ result_t wuss_mouse_click(wuss_t *wuss, (button & (wuss_BUTTON_SELECT | wuss_BUTTON_ADJUST))) { point_t scroll; + int paged = 0; /* Only resize raises the window; dragging a scrollbar well must not * reorder the stack. */ @@ -206,8 +207,8 @@ result_t wuss_mouse_click(wuss_t *wuss, * content one visible extent towards the click, RISC OS style, keeping * one step of overlap. ADJUST reverses that -- it pages away from the * click instead, matching RISC OS's SELECT/ADJUST mirroring elsewhere - * (e.g. the arrow icons). The drag state is still armed below so a - * press that then moves onto the sausage keeps working. */ + * (e.g. the arrow icons). A direct hit on the sausage itself pages + * nothing and falls through to arm the drag below. */ if (region == wuss_FURNITURE_VSCROLL_WELL || region == wuss_FURNITURE_HSCROLL_WELL) { @@ -224,9 +225,15 @@ result_t wuss_mouse_click(wuss_t *wuss, page = (content.y1 - content.y0) - WUSS_SCROLL_STEP; page = MAX(page, 1); if (y < sausage.y0) + { wuss__scroll_step(win, POINT(0, -page * sign)); + paged = 1; + } else if (y > sausage.y1) + { wuss__scroll_step(win, POINT(0, page * sign)); + paged = 1; + } } else { @@ -234,17 +241,23 @@ result_t wuss_mouse_click(wuss_t *wuss, page = (content.x1 - content.x0) - WUSS_SCROLL_STEP; page = MAX(page, 1); if (x < sausage.x0) + { wuss__scroll_step(win, POINT(-page * sign, 0)); + paged = 1; + } else if (x > sausage.x1) + { wuss__scroll_step(win, POINT(page * sign, 0)); + paged = 1; + } } } - /* A well click only pages once; it must not also arm a sausage drag, - * or holding the button after the page would start scrolling live - * as though the pointer had grabbed the sausage itself. */ - if (region != wuss_FURNITURE_VSCROLL_WELL && - region != wuss_FURNITURE_HSCROLL_WELL) + /* A well click that paged must not also arm a sausage drag, or + * holding the button after the page would start scrolling live as + * though the pointer had grabbed the sausage itself. A direct hit on + * the sausage (paged == 0 for a well region) still arms normally. */ + if (!paged) { wuss_window_get_scroll(win, &scroll); From 1ae9025d239f6df600080bd2a26a148d1cd6e1a5 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 13 Sep 2026 15:54:56 +0100 Subject: [PATCH 058/128] fix(wuss): track blit-touched regions so partial present stops going stale 31ffe31 made the SDL frontend upload only the rows covered by wuss's dirty tracking, but wuss__blit_pieces (window move's slide, resize/toggle-size's scroll-reclamp blit) writes pixels straight into the backing bitmap via screen_copy_rect without invalidating the destination -- correct before, since present() always re-uploaded the whole screen regardless. Under the row-cropped present those destinations kept stale texture content, showing as missing/stale furniture and content after a move, resize or toggle-size. Add a second region list (wuss->touched, separate from wuss->dirty) fed by every screen_copy_rect destination in wuss__blit_pieces, resize.c and toggle-action.c. wuss_redraw_dirty never consumes it -- the blitted pixels are already correct, so no repaint is owed -- but wuss_get_touched_extent/wuss_clear_touched let a frontend fold it into whatever it re-uploads. main.c now unions the touched extent into the dirty bbox it passes to wuss_frontend_present. Also fixes a latent bug the new field exposed: wuss_create left the whole wuss_t heap block uninitialised except for a few explicitly zeroed fields, so the new ntouched read garbage on first use and corrupted memory in mark_region. Zero it alongside ndirty. --- apps/wuss/main.c | 18 ++++- include/wuss/wuss.h | 25 +++++++ libraries/wuss/core/create.c | 3 +- libraries/wuss/core/impl.h | 3 + libraries/wuss/core/invalidate.c | 93 +++++++++++++++++++----- libraries/wuss/core/window/invalidate.c | 5 ++ libraries/wuss/core/window/resize.c | 13 +++- libraries/wuss/furniture/toggle-action.c | 6 ++ 8 files changed, 138 insertions(+), 28 deletions(-) diff --git a/apps/wuss/main.c b/apps/wuss/main.c index 71e8bb7c..e96c16cb 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -216,20 +216,30 @@ static void wuss_frame(void *arg) } else { - box_t dirty, region; - int ndirty, i; + box_t dirty, region, touched; + int ndirty, i, have_touched, have_any; ndirty = wuss_get_dirty_count(c->wuss); - dirty = (box_t) BOX_INIT; + dirty = (box_t) BOX_INIT; for (i = 0; i < ndirty; i++) { wuss_get_dirty(c->wuss, i, ®ion); box_union(&dirty, ®ion, &dirty); } + /* Pixels a fast blit path (window move/resize, scroll) slid to a new + * position without a repaint: wuss_redraw_dirty won't touch them, but + * present() still has to re-upload them -- fold them into the same + * bounding box before wuss_clear_touched drops them. */ + have_touched = wuss_get_touched_extent(c->wuss, &touched); + have_any = (ndirty > 0) || have_touched; + if (have_touched) + box_union(&dirty, &touched, &dirty); + wuss_redraw_dirty(c->wuss); + wuss_clear_touched(c->wuss); - wuss_frontend_present(c->frontend, c->bm, ndirty > 0 ? &dirty : NULL); + wuss_frontend_present(c->frontend, c->bm, have_any ? &dirty : NULL); } #ifdef __EMSCRIPTEN__ diff --git a/include/wuss/wuss.h b/include/wuss/wuss.h index 1599ec57..a5a9c295 100644 --- a/include/wuss/wuss.h +++ b/include/wuss/wuss.h @@ -738,6 +738,31 @@ int wuss_get_dirty_count(const wuss_t *wuss); */ void wuss_get_dirty(const wuss_t *wuss, int index, box_t *out); +/** + * Fetch the bounding box of every screen-space region "touched" since the + * last wuss_clear_touched -- pixels a fast blit path (window move/resize, + * scroll) slid to a new position without a repaint, because the backing + * bitmap was already correct there. wuss_redraw_dirty never repaints these; + * they exist for a frontend that re-uploads the screen to a display surface + * (e.g. a GPU texture) piecemeal and needs to know such pixels moved even + * though wuss itself did no drawing there this frame. + * + * \param[in] wuss Window manager. + * \param[out] out Filled in with the union of every touched region. + * \return 1 if anything was touched (and \p out was filled in), 0 if nothing + * was. + */ +int wuss_get_touched_extent(const wuss_t *wuss, box_t *out); + +/** + * Clear the regions accumulated for wuss_get_touched_extent. A frontend + * calls this once it has folded the touched extent into whatever it presents + * this frame, so the next frame starts from empty. + * + * \param[in] wuss Window manager. + */ +void wuss_clear_touched(wuss_t *wuss); + /** * Deliver a mouse-down or mouse-up event (action must be wuss_MOUSE_DOWN or * wuss_MOUSE_UP). Hit-tests the topmost window at (x,y). On a down, a diff --git a/libraries/wuss/core/create.c b/libraries/wuss/core/create.c index 9531ccb5..44b4db1f 100644 --- a/libraries/wuss/core/create.c +++ b/libraries/wuss/core/create.c @@ -318,7 +318,8 @@ result_t wuss_create(screen_t *scr, w->menu_eat_up = 0; #endif - w->ndirty = 0; + w->ndirty = 0; + w->ntouched = 0; w->layout = NULL; w->cascade.x = 0; diff --git a/libraries/wuss/core/impl.h b/libraries/wuss/core/impl.h index e641af08..790df6d1 100644 --- a/libraries/wuss/core/impl.h +++ b/libraries/wuss/core/impl.h @@ -144,6 +144,8 @@ struct wuss #endif box_t dirty[WUSS_MAX_DIRTY]; /* accumulated by wuss_invalidate; reset by a redraw */ int ndirty; + box_t touched[WUSS_MAX_DIRTY]; /* accumulated by wuss__touch; reset by wuss_clear_touched */ + int ntouched; packer_t *layout; /* owned; occupied screen area for * wuss_window_create_placed, lazily * created on first auto-placement */ @@ -426,6 +428,7 @@ void wuss__invalidate_minus(wuss_t *wuss, const box_t *whole, const box_t *keep); void wuss__invalidate_uncovered(wuss_window_t *window); +void wuss__touch(wuss_t *wuss, const box_t *box); /* Clip "box" (screen space) down to the parts not already covered by * windows above "window" in the z-order, writing the surviving pieces to diff --git a/libraries/wuss/core/invalidate.c b/libraries/wuss/core/invalidate.c index d1a8535d..c8f3f062 100644 --- a/libraries/wuss/core/invalidate.c +++ b/libraries/wuss/core/invalidate.c @@ -28,38 +28,40 @@ static int box_merge(box_t *a, const box_t *b) return 0; } -result_t wuss_invalidate(wuss_t *wuss, const box_t *box) +/* Shared by wuss_invalidate (wuss->dirty) and wuss__touch (wuss->touched): + * repeatedly fold any existing entry that "cur" now covers, or that shares a + * complete edge with it, into "cur" itself; growing "cur" can bring further + * entries into range, so this settles to a fixed point before "cur" is + * (re)inserted. Bounded by "max", so an O(n^2) settle is cheap. Returns 1 if + * "box" was already fully covered (nothing to do). */ +static int mark_region(box_t *arr, + int *pn, + int max, + const char *what, + const box_t *box) { box_t cur; int changed; - assert(wuss != NULL); - assert(box != NULL); - if (box_is_empty(box)) - return result_OK; + return 1; cur = *box; - /* Repeatedly fold any existing entry that "cur" now covers, or that - * shares a complete edge with it, into "cur" itself; growing "cur" can - * bring further entries into range, so this settles to a fixed point - * before "cur" is (re)inserted. Bounded by WUSS_MAX_DIRTY, so an O(n^2) - * settle is cheap. */ do { int i; changed = 0; - for (i = 0; i < wuss->ndirty; i++) + for (i = 0; i < *pn; i++) { - if (box_contains_box(&cur, &wuss->dirty[i])) - return result_OK; /* already covered */ + if (box_contains_box(&cur, &arr[i])) + return 1; /* already covered */ - if (box_contains_box(&wuss->dirty[i], &cur) || box_merge(&cur, &wuss->dirty[i])) + if (box_contains_box(&arr[i], &cur) || box_merge(&cur, &arr[i])) { - wuss->dirty[i] = wuss->dirty[--wuss->ndirty]; /* absorbed into cur */ + arr[i] = arr[--(*pn)]; /* absorbed into cur */ changed = 1; break; } @@ -67,23 +69,47 @@ result_t wuss_invalidate(wuss_t *wuss, const box_t *box) } while (changed); - if (wuss->ndirty < WUSS_MAX_DIRTY) + if (*pn < max) { - wuss->dirty[wuss->ndirty++] = cur; + arr[(*pn)++] = cur; } else { /* ponytail: array full, fold into the last entry rather than growing * storage; over-approximates that entry's area but stays correct */ - logf_info("wuss_invalidate: %d dirty regions, coalescing " + logf_info("wuss_invalidate: %d %s regions, coalescing " "(%d,%d)-(%d,%d) into the last entry", - WUSS_MAX_DIRTY, cur.x0, cur.y0, cur.x1, cur.y1); - box_union(&wuss->dirty[WUSS_MAX_DIRTY - 1], &cur, &wuss->dirty[WUSS_MAX_DIRTY - 1]); + max, what, cur.x0, cur.y0, cur.x1, cur.y1); + box_union(&arr[max - 1], &cur, &arr[max - 1]); } + return 0; +} + +result_t wuss_invalidate(wuss_t *wuss, const box_t *box) +{ + assert(wuss != NULL); + assert(box != NULL); + + mark_region(wuss->dirty, &wuss->ndirty, WUSS_MAX_DIRTY, "dirty", box); + return result_OK; } +/* Mark a screen-space region as having changed pixels without needing a + * repaint -- e.g. wuss__blit_pieces sliding a window's own content to a new + * position: the backing bitmap is already correct there, but a frontend that + * only re-uploads what wuss_get_touched_extent reports still needs to know + * the pixels moved. Never folded into wuss->dirty, so wuss_redraw_dirty + * never repaints it. */ +void wuss__touch(wuss_t *wuss, const box_t *box) +{ + assert(wuss != NULL); + assert(box != NULL); + + mark_region(wuss->touched, &wuss->ntouched, WUSS_MAX_DIRTY, "touched", box); +} + int wuss_get_dirty_count(const wuss_t *wuss) { assert(wuss != NULL); @@ -99,3 +125,30 @@ void wuss_get_dirty(const wuss_t *wuss, int index, box_t *out) *out = wuss->dirty[index]; } + +int wuss_get_touched_extent(const wuss_t *wuss, box_t *out) +{ + box_t u; + int i; + + assert(wuss != NULL); + assert(out != NULL); + + if (wuss->ntouched == 0) + return 0; + + u = wuss->touched[0]; + for (i = 1; i < wuss->ntouched; i++) + box_union(&u, &wuss->touched[i], &u); + + *out = u; + + return 1; +} + +void wuss_clear_touched(wuss_t *wuss) +{ + assert(wuss != NULL); + + wuss->ntouched = 0; +} diff --git a/libraries/wuss/core/window/invalidate.c b/libraries/wuss/core/window/invalidate.c index 472578a5..34071856 100644 --- a/libraries/wuss/core/window/invalidate.c +++ b/libraries/wuss/core/window/invalidate.c @@ -307,6 +307,11 @@ int wuss__blit_pieces(wuss_window_t *window, * Safe raw -- blit_dest[idx] was already clipped clear of occluders. */ wuss__invalidate_minus(window->wuss, &blit_dest[idx], &got); + /* "got" itself needs no repaint -- screen_copy_rect already gave it + * correct pixels -- but a frontend that only re-uploads what + * wuss_get_touched_extent reports still needs to know they moved. */ + wuss__touch(window->wuss, &got); + if (*ncopied < WUSS_MAX_INVALIDATE_PIECES) copied[(*ncopied)++] = got; } diff --git a/libraries/wuss/core/window/resize.c b/libraries/wuss/core/window/resize.c index a9e43084..38f39ea1 100644 --- a/libraries/wuss/core/window/resize.c +++ b/libraries/wuss/core/window/resize.c @@ -151,9 +151,16 @@ result_t wuss_window_resize(wuss_window_t *window, size2d_t size) if (screen_copy_rect(window->wuss->scr, &src[i], POINT(src[i].x0 - dx, src[i].y0 - dy), - &got) == result_OK && - ncopied < WUSS_MAX_INVALIDATE_PIECES) - copied[ncopied++] = got; + &got) == result_OK) + { + /* "got" is already correct on screen -- the blit reused it -- + * but a frontend re-uploading only wuss_get_touched_extent still + * needs to know it moved. */ + wuss__touch(window->wuss, &got); + + if (ncopied < WUSS_MAX_INVALIDATE_PIECES) + copied[ncopied++] = got; + } } } diff --git a/libraries/wuss/furniture/toggle-action.c b/libraries/wuss/furniture/toggle-action.c index 18c02463..e2c6901f 100644 --- a/libraries/wuss/furniture/toggle-action.c +++ b/libraries/wuss/furniture/toggle-action.c @@ -127,7 +127,13 @@ void wuss__furniture_toggle_size(wuss_window_t *window) if (screen_copy_rect(window->wuss->scr, &blit_src, POINT(blit_dst.x0, blit_dst.y0), &copied) == result_OK) + { + /* "copied" is already correct on screen -- the blit reused it -- + * but a frontend re-uploading only wuss_get_touched_extent still + * needs to know it moved. */ + wuss__touch(window->wuss, &copied); blitted = 1; + } } } From 5e55d31f61c42fc9836da78878f07e0d1ce04648 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 13 Sep 2026 16:39:31 +0100 Subject: [PATCH 059/128] feat(wuss): saturn task gets a Colours menu for fg/bg Colours -> Foreground/Background, each popping a wuss_colourmenu against task->fg / task->bg. Also carries the earlier config-struct refactor (iteration counts moved off #defines into saturn_config_t). --- apps/wuss/tasks.c | 2 +- libraries/wuss/test/tasks/saturn.c | 153 +++++++++++++++++++++++------ libraries/wuss/test/tasks/saturn.h | 35 ++++++- 3 files changed, 154 insertions(+), 36 deletions(-) diff --git a/apps/wuss/tasks.c b/apps/wuss/tasks.c index a91f4bac..c92319fe 100644 --- a/apps/wuss/tasks.c +++ b/apps/wuss/tasks.c @@ -212,7 +212,7 @@ static result_t spawn_saturn(void) saturn_task_t *t = calloc(1, sizeof(*t)); result_t rc; if (t == NULL) return result_OOM; - rc = saturn_create(g.wuss, t); + rc = saturn_create(g.wuss, t, NULL); if (rc != result_OK) return rc; if (t->window == NULL) { free(t); return rc; } return result_OK; diff --git a/libraries/wuss/test/tasks/saturn.c b/libraries/wuss/test/tasks/saturn.c index 018df527..d326dd45 100644 --- a/libraries/wuss/test/tasks/saturn.c +++ b/libraries/wuss/test/tasks/saturn.c @@ -13,6 +13,7 @@ #include "framebuf/palettes.h" #include "geom/box.h" #include "utils/rng.h" +#include "wuss/menu.h" #include "saturn.h" @@ -37,19 +38,15 @@ * x = sqrt(16384-P)/2 -> planet body */ -#define SATURN_SIZE 256 /* window is SATURN_SIZE x SATURN_SIZE */ +#define SATURN_SIZE 256 /* window size is this value squared */ /* The original works in a -128..127 sample space (BBC BASIC signed byte). */ -#define SATURN_HALF 128 /* sample-space centre / bias */ -#define SATURN_RANGE 255 /* saturn_rnd() span: 1..255, then -HALF */ -#define SATURN_FLIP 255 /* RISC OS bottom-up -> wuss top-down: FLIP - v */ -#define SATURN_ENERGY_SHIFT 256 /* (x*x+y*y) energy divisor */ +#define SATURN_HALF (SATURN_SIZE / 2) /* sample-space centre / bias */ +#define SATURN_RANGE (SATURN_SIZE - 1) /* saturn_rnd() span: 1..255, then -HALF */ +#define SATURN_FLIP (SATURN_SIZE - 1) /* RISC OS bottom-up -> wuss top-down: FLIP - v */ +#define SATURN_ENERGY_SHIFT SATURN_SIZE /* (x*x+y*y) energy divisor */ -#define SATURN_RING_ITERS 477 /* loop 1: the ring */ -#define SATURN_BAND_ITERS 1280 /* loop 2: ring shadow band */ -#define SATURN_BODY_ITERS 1280 /* loop 3: planet body */ - -#define SATURN_BODY_R2 16384 /* planet body: keep if r1*r1+r2*r2 < this */ +#define SATURN_BODY_R2 16384 /* planet body: keep if r1*r1+r2*r2 < this */ /* BBC BASIC RND(n>0) returns an integer 1..n. utils/rng's LCG stands in for * it (its high bits, which is where an LCG's usable randomness sits) so a @@ -69,15 +66,51 @@ static int saturn_rnd(int n) /* one signed sample in the original -128..127 space */ #define SATURN_SAMPLE() (saturn_rnd(SATURN_RANGE) - SATURN_HALF) -result_t saturn_create(wuss_t *wuss, saturn_task_t *task) +/* MENU click over the content pops this. "Colours" leads to a submenu with + * one row per task->fg/task->bg, each of which pops a wuss_colourmenu (see + * saturn_create -- the two leaf items' submenu pointers are patched in there, + * once the colourmenus exist). */ +enum { SATURN_MENU_COLOURS = 0 }; +enum { SATURN_COLOURS_MENU_FOREGROUND = 0, SATURN_COLOURS_MENU_BACKGROUND }; + +static wuss_menu_item_t g_saturn_colours_items[] = +{ + { "Foreground", wuss_MENU_ITEM_BORROWED_SUBMENU, NULL, NULL, 0 }, + { "Background", wuss_MENU_ITEM_BORROWED_SUBMENU, NULL, NULL, 0 } +}; + +static wuss_menu_t g_saturn_colours_menu = { - result_t rc; - wuss_task_t *delegate; - wuss_task_desc_t delegate_desc; + "Colours", g_saturn_colours_items, NELEMS(g_saturn_colours_items) +}; - task->bg = colour_rgb(0x00, 0x00, 0x00); - task->fg = colour_rgb(0xFF, 0xFF, 0xFF); - task->seed = 1; +static wuss_menu_item_t g_saturn_menu_items[] = +{ + { "Colours", wuss_MENU_ITEM_NONE, &g_saturn_colours_menu, NULL, 0 } +}; + +static wuss_menu_t g_saturn_menu = +{ + "Saturn", g_saturn_menu_items, NELEMS(g_saturn_menu_items) +}; + +result_t saturn_create(wuss_t *wuss, + saturn_task_t *task, + const saturn_config_t *config) +{ + static const saturn_config_t default_config = SATURN_CONFIG_DEFAULT; + result_t rc; + wuss_task_t *delegate; + wuss_task_desc_t delegate_desc; + + task->wuss = wuss; + task->bg = colour_rgb(0x00, 0x00, 0x00); + task->fg = colour_rgb(0xFF, 0xFF, 0xFF); + task->seed = 1; + task->config = (config != NULL) ? *config : default_config; + task->fg_colourmenu = NULL; + task->bg_colourmenu = NULL; + task->menu_handle = NULL; /* saturn_redraw paints its own background */ delegate_desc.handle = saturn_handle; @@ -89,8 +122,29 @@ result_t saturn_create(wuss_t *wuss, saturn_task_t *task) free(task); /* nothing registered yet; the spawner will not free it */ return rc; } + task->delegate = delegate; /* the task the menu opens against */ wuss_task_set_autoclose(delegate, 1); + rc = wuss_colourmenu_create(&task->fg_colourmenu, wuss, "Foreground"); + if (rc != result_OK) + { + wuss_task_destroy(delegate); + return rc; + } + + rc = wuss_colourmenu_create(&task->bg_colourmenu, wuss, "Background"); + if (rc != result_OK) + { + wuss_colourmenu_destroy(task->fg_colourmenu); + wuss_task_destroy(delegate); + return rc; + } + + g_saturn_colours_items[SATURN_COLOURS_MENU_FOREGROUND].submenu = + wuss_colourmenu_menu(task->fg_colourmenu); + g_saturn_colours_items[SATURN_COLOURS_MENU_BACKGROUND].submenu = + wuss_colourmenu_menu(task->bg_colourmenu); + rc = wuss_window_create_placed(delegate, SIZE2D(SATURN_SIZE, SATURN_SIZE), "Saturn", @@ -100,7 +154,11 @@ result_t saturn_create(wuss_t *wuss, saturn_task_t *task) SIZE2D(0, 0), &task->window); if (rc != result_OK) + { + wuss_colourmenu_destroy(task->bg_colourmenu); + wuss_colourmenu_destroy(task->fg_colourmenu); wuss_task_destroy(delegate); /* unregister; its QUIT frees the task block */ + } return rc; } @@ -143,7 +201,7 @@ static result_t saturn_redraw(const wuss_event_t *event, saturn_task_t *task) saturn_rnd_seed(task->seed); /* loop 1 - the ring: keep points outside the inner disc */ - for (i = 0; i <= SATURN_RING_ITERS; i++) + for (i = 0; i <= task->config.ring_iters; i++) { x = SATURN_SAMPLE(); y = SATURN_SAMPLE(); @@ -154,7 +212,7 @@ static result_t saturn_redraw(const wuss_event_t *event, saturn_task_t *task) } /* loop 2 - ring shadow band: sheared sample with a banded energy gate */ - for (i = 0; i <= SATURN_BAND_ITERS; i++) + for (i = 0; i <= task->config.band_iters; i++) { int r5, r6, r7, e; @@ -170,7 +228,7 @@ static result_t saturn_redraw(const wuss_event_t *event, saturn_task_t *task) } /* loop 3 - planet body: filled half-disc offset right */ - for (i = 0; i <= SATURN_BODY_ITERS; i++) + for (i = 0; i <= task->config.body_iters; i++) { int r1, r2; @@ -188,14 +246,6 @@ static result_t saturn_redraw(const wuss_event_t *event, saturn_task_t *task) return result_OK; } -static result_t saturn_idle(saturn_task_t *task) -{ - task->seed += 0x9E3779B9UL; /* churn: a fresh sketch every null event */ - wuss_window_invalidate_visible(task->window); - - return result_OK; -} - static result_t saturn_mouse(saturn_task_t *task, wuss_mouse_action_t action, wuss_button_t button, @@ -204,6 +254,11 @@ static result_t saturn_mouse(saturn_task_t *task, if (action != wuss_MOUSE_DOWN) return result_OK; + if (button & wuss_BUTTON_MENU) + return wuss_menu_open(task->delegate, &g_saturn_menu, + wuss_get_pointer(task->wuss), + &task->menu_handle); + if (button & wuss_BUTTON_SELECT) { task->seed += 0x9E3779B9UL; /* fresh sketch */ @@ -213,6 +268,38 @@ static result_t saturn_mouse(saturn_task_t *task, return result_OK; } +/* A pick from either colour submenu: resolve against whichever colourmenu it + * came from and store into the matching field. */ +static result_t saturn_menu_select(saturn_task_t *task, + const wuss_event_t *event) +{ + const colour_t *palette; + wuss_colour_t picked; + int npalette, mine; + + palette = wuss_get_palette(task->wuss, &npalette); + + picked = wuss_colourmenu_selected(task->fg_colourmenu, event, &mine); + if (mine) + { + if (picked < npalette) + task->fg = palette[picked]; + wuss_window_invalidate_visible(task->window); + return result_OK; + } + + picked = wuss_colourmenu_selected(task->bg_colourmenu, event, &mine); + if (mine) + { + if (picked < npalette) + task->bg = palette[picked]; + wuss_window_invalidate_visible(task->window); + return result_OK; + } + + return result_OK; +} + result_t saturn_handle(wuss_window_t *window, const wuss_event_t *event, void *task_data) @@ -230,10 +317,16 @@ result_t saturn_handle(wuss_window_t *window, return saturn_mouse(task, event->data.mouse.action, event->data.mouse.button, window); - case wuss_EVENT_IDLE: - return saturn_idle(task); + case wuss_EVENT_MENU_SELECT: + return saturn_menu_select(task, event); + + case wuss_EVENT_MENU_CLOSED: + task->menu_handle = NULL; /* wuss closed the chain under us */ + return result_OK; case wuss_EVENT_QUIT: + wuss_colourmenu_destroy(task->fg_colourmenu); + wuss_colourmenu_destroy(task->bg_colourmenu); free(task); /* task_data was calloc'd per instance by the spawner */ return result_OK; diff --git a/libraries/wuss/test/tasks/saturn.h b/libraries/wuss/test/tasks/saturn.h index d9a66303..8d15209a 100644 --- a/libraries/wuss/test/tasks/saturn.h +++ b/libraries/wuss/test/tasks/saturn.h @@ -5,7 +5,12 @@ #ifdef WUSS_APP +/* ponytail: no #ifdef WUSS_COMPONENTS guard -- it is PUBLIC on DPTLib and ON + * by default, so the wuss app always has the colourmenu component. */ #include "framebuf/colour.h" +#include "wuss/component/colourmenu.h" +#include "wuss/menu.h" +#include "wuss/task.h" #include "wuss/window.h" /* window task: recreates the ringed planet from the loading screen of @@ -15,19 +20,39 @@ * planet body. Select re-seeds the RNG for a fresh sketch; the idle * handler re-seeds every null event so it churns. The plot is * deterministic in the seed. */ +/* iteration counts for the three rejection-sampling loops; saturn_create + * fills in SATURN_CONFIG_DEFAULT values when the caller passes NULL */ +typedef struct saturn_config +{ + int ring_iters; /* loop 1: the ring */ + int band_iters; /* loop 2: ring shadow band */ + int body_iters; /* loop 3: planet body */ +} +saturn_config_t; + +#define SATURN_CONFIG_DEFAULT { 477, 1280, 1280 } + typedef struct saturn_task { - wuss_window_t *window; - colour_t bg, fg; - unsigned long seed; /* RNG state; a Select click bumps it */ + wuss_t *wuss; /* for wuss_get_pointer/wuss_get_palette */ + wuss_window_t *window; + wuss_task_t *delegate; /* the task that owns the menu */ + colour_t bg, fg; + unsigned long seed; /* RNG state; a Select click bumps it */ + saturn_config_t config; + wuss_colourmenu_t *fg_colourmenu, *bg_colourmenu; + wuss_menu_handle_t menu_handle; /* live only between open and a SELECT pick */ } saturn_task_t; wuss_window_fn_t saturn_handle; /* create the planet window against the given wuss instance; "task" is a - * per-instance block owned by the window and freed when it closes */ -result_t saturn_create(wuss_t *wuss, saturn_task_t *task); + * per-instance block owned by the window and freed when it closes. "config" + * may be NULL for SATURN_CONFIG_DEFAULT. */ +result_t saturn_create(wuss_t *wuss, + saturn_task_t *task, + const saturn_config_t *config); #endif /* WUSS_APP */ From 9aeef13a35fc93fbb473d0b09b11e5c9bf4e1ea2 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 13 Sep 2026 22:21:52 +0100 Subject: [PATCH 060/128] feat(wuss): add wuss_ICON_TYPE_SLIDER Horizontal/vertical slider icon: bevelled surround with a sunken groove inset by a fixed gap, drag/click value mapping that rounds to the nearest value rather than truncating, and a min > max reversed-fill mode. Adds the get/set value accessors, a slider test task, and unit tests covering the pixel<->value conversion and the groove-gap inset via a real icon. Co-Authored-By: Claude Sonnet 5 --- CMakeLists.txt | 4 + apps/wuss/main.c | 6 + apps/wuss/tasks.c | 16 ++- include/geom/box.h | 9 ++ include/wuss/icon.h | 69 +++++++++- include/wuss/task.h | 6 +- include/wuss/wuss.h | 27 +++- libraries/geom/box/grow.c | 12 ++ libraries/wuss/core/create.c | 36 +++++ libraries/wuss/core/impl.h | 7 + libraries/wuss/core/mouse-click.c | 9 ++ libraries/wuss/core/mouse-move.c | 22 +++ libraries/wuss/core/rebuild-palettecache.c | 5 + libraries/wuss/icon.h | 53 ++++++++ libraries/wuss/icon/draw.c | 84 +++++++++--- libraries/wuss/icon/from-spec.c | 20 ++- libraries/wuss/icon/get-value.c | 12 ++ libraries/wuss/icon/hit-test.c | 20 ++- libraries/wuss/icon/set-value.c | 35 +++++ libraries/wuss/icon/slider-box.c | 88 ++++++++++++ libraries/wuss/test/tasks/slider.c | 150 +++++++++++++++++++++ libraries/wuss/test/tasks/slider.h | 36 +++++ libraries/wuss/test/wuss-test.c | 150 ++++++++++++++++++++- 23 files changed, 845 insertions(+), 31 deletions(-) create mode 100644 libraries/wuss/icon/get-value.c create mode 100644 libraries/wuss/icon/set-value.c create mode 100644 libraries/wuss/icon/slider-box.c create mode 100644 libraries/wuss/test/tasks/slider.c create mode 100644 libraries/wuss/test/tasks/slider.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 93b41cb5..f5f9c2f1 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -440,6 +440,7 @@ set(WUSS_ICON_SOURCES libraries/wuss/icon/get-selected.c libraries/wuss/icon/get-text.c libraries/wuss/icon/get-type.c + libraries/wuss/icon/get-value.c libraries/wuss/icon/hit-test.c libraries/wuss/icon/invalidate.c libraries/wuss/icon/plot.c @@ -449,6 +450,8 @@ set(WUSS_ICON_SOURCES libraries/wuss/icon/set-hover.c libraries/wuss/icon/set-selected.c libraries/wuss/icon/set-text.c + libraries/wuss/icon/set-value.c + libraries/wuss/icon/slider-box.c libraries/wuss/icon.h) set(WUSS_MENU_SOURCES @@ -759,6 +762,7 @@ if(BUILD_APPS) libraries/wuss/test/tasks/palette.c libraries/wuss/test/tasks/porter-duff.c libraries/wuss/test/tasks/saturn.c + libraries/wuss/test/tasks/slider.c libraries/wuss/test/tasks/sofa.c libraries/wuss/test/tasks/swatches.c libraries/wuss/test/tasks/text.c) diff --git a/apps/wuss/main.c b/apps/wuss/main.c index e96c16cb..155377dd 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -69,6 +69,9 @@ static void fill_chrome_config(wuss_config_t *config, int use_wimp16) config->backdrop.pattern_bg = palette_WIMP16_GREY_50; config->body.window = palette_WIMP16_GREY_87; config->body.menu = palette_WIMP16_WHITE; + config->slider.track = palette_WIMP16_WHITE; + config->slider.value = palette_WIMP16_GREY_50; + config->slider.surround = wuss_NO_BACKGROUND; } else { @@ -93,6 +96,9 @@ static void fill_chrome_config(wuss_config_t *config, int use_wimp16) config->backdrop.pattern_bg = palette_PICO8_LIGHT_GREY; config->body.window = palette_PICO8_LIGHT_GREY; config->body.menu = palette_PICO8_WHITE; + config->slider.track = palette_PICO8_WHITE; + config->slider.value = palette_PICO8_DARK_GREY; + config->slider.surround = wuss_NO_BACKGROUND; } } diff --git a/apps/wuss/tasks.c b/apps/wuss/tasks.c index c92319fe..17aa50d1 100644 --- a/apps/wuss/tasks.c +++ b/apps/wuss/tasks.c @@ -33,6 +33,7 @@ #include "tasks/palette.h" #include "tasks/porter-duff.h" #include "tasks/saturn.h" +#include "tasks/slider.h" #include "tasks/sofa.h" #include "tasks/swatches.h" #include "tasks/text.h" @@ -229,6 +230,17 @@ static result_t spawn_sofa(void) return result_OK; } +static result_t spawn_slider(void) +{ + slider_task_t *t = calloc(1, sizeof(*t)); + result_t rc; + if (t == NULL) return result_OOM; + rc = slider_create(g.wuss, g.daydream_font, t); + if (rc != result_OK) return rc; + if (t->window == NULL) { free(t); return rc; } + return result_OK; +} + static result_t spawn_gradient(void) { gradient_task_t *t = calloc(1, sizeof(*t)); @@ -313,6 +325,7 @@ static const wuss_menu_item_t g_launch_items[] = { "Palette", wuss_MENU_ITEM_NONE, NULL }, { "Porter-Duff", wuss_MENU_ITEM_NONE, NULL }, { "Saturn", wuss_MENU_ITEM_NONE, NULL }, + { "Slider", wuss_MENU_ITEM_NONE, NULL }, { "Sofa", wuss_MENU_ITEM_NONE, NULL }, { "Swatches", wuss_MENU_ITEM_NONE, NULL }, { "Text", wuss_MENU_ITEM_NONE, NULL } @@ -323,7 +336,8 @@ static const task_spawn_fn_t g_launch_spawn[] = spawn_ball, spawn_blank, spawn_chars, spawn_checker, spawn_clock, spawn_curve, spawn_gradient, spawn_greeble, spawn_icons, spawn_image, spawn_lissajous, spawn_minesweeper, spawn_palette, - spawn_porter_duff, spawn_saturn, spawn_sofa, spawn_swatches, spawn_text + spawn_porter_duff, spawn_saturn, spawn_slider, spawn_sofa, spawn_swatches, + spawn_text }; static const wuss_menu_t g_launch_menu = diff --git a/include/geom/box.h b/include/geom/box.h index da7751c6..c4c5555e 100644 --- a/include/geom/box.h +++ b/include/geom/box.h @@ -138,6 +138,15 @@ void box_scalelog2(box_t *b, int log2scale); */ void box_grow(box_t *b, int change); +/** + * Populates "result" with a copy of "b" grown outwards by "change". + * + * \param[in] b The source box. + * \param[in] change Amount to grow (negative to shrink). + * \return The resulting box. + */ +box_t box_grown(const box_t *b, int change); + /** * Rounds a box's coordinates so that they're a multiple of log2 x,y. * diff --git a/include/wuss/icon.h b/include/wuss/icon.h index f399cef5..22c2f8d0 100644 --- a/include/wuss/icon.h +++ b/include/wuss/icon.h @@ -102,6 +102,14 @@ typedef enum wuss_icon_type * text, bg and pattern are ignored. Used between menu rows to render the * line a wuss_ICON_FLAGS_SEPARATOR entry sits below. */ wuss_ICON_TYPE_RULE, + /** A slider: the bounding box is filled with slider.surround, with a fixed + * 4px gap (also slider.surround) around an inner sunken groove + * (spec.u.slider) that fills the remaining space on both axes; its fill + * marks a value between min and max. A click or drag anywhere in the + * inner rect -- fill or bare groove, but not the surround or gap -- jumps + * the value straight to the pointer position; the task is told via + * wuss_EVENT_ICON, continuously while dragging. */ + wuss_ICON_TYPE_SLIDER, /* The following types are reserved: the enum values and validation exist but * no rendering, hit-testing or event routing is wired up yet. A spec using @@ -118,9 +126,6 @@ typedef enum wuss_icon_type /** A field cycling through a fixed set of string values. Not yet * implemented. */ wuss_ICON_TYPE_STRING_SET, - /** A slider: a track with a draggable thumb selecting a value in a range. - * Not yet implemented. */ - wuss_ICON_TYPE_SLIDER, /** A free-drag handle: reports pointer motion to the task while dragged. Not * yet implemented. */ wuss_ICON_TYPE_DRAGGABLE @@ -215,6 +220,23 @@ wuss_icon_flags_t; */ #define wuss_ICON_SET(i) ((i) + 1) +/** + * A wuss_ICON_TYPE_SLIDER's axis: which way its groove runs and, so, which + * end of the groove (inset from the bounding box by the fixed 4px gap on all + * four sides, filling the rest) is pixel 0 of the value fill. + */ +typedef enum wuss_slider_orientation +{ + /** Groove runs left-to-right; value grows rightward, pixel 0 at the + * groove's left. */ + wuss_SLIDER_HORIZONTAL = 0, + /** Groove runs top-to-bottom; value grows upward (low at the bottom, high + * at the top), matching a volume-slider convention -- pixel 0 at the + * groove's bottom. */ + wuss_SLIDER_VERTICAL +} +wuss_slider_orientation_t; + /** * Per-type payload in a wuss_icon_spec. Exactly one arm applies, selected by * wuss_icon_spec::type; the arms mirror the icon's internal storage. Types @@ -278,6 +300,24 @@ typedef union wuss_icon_spec_data wuss_colour_t swatch; } menu_entry; + + /** wuss_ICON_TYPE_SLIDER */ + struct + { + /** Which way the groove runs. wuss_SLIDER_HORIZONTAL (0) is the default + * for a zero-initialised spec. */ + wuss_slider_orientation_t orientation; + /** Value at the groove's start (left for wuss_SLIDER_HORIZONTAL, bottom + * for VERTICAL). May be greater than max to run the fill backwards. */ + int min; + /** + * Value at the groove's end (right for HORIZONTAL, top for VERTICAL). + */ + int max; + /** Initial value, clamped to [min,max] (or [max,min] if min > max). */ + int default_value; + } + slider; } wuss_icon_spec_data_t; @@ -462,6 +502,29 @@ void wuss_icon_set_selected(wuss_window_t *window, wuss_icon_t *icon, int selected); +/** + * Fetch a slider icon's current value. + * + * \param[in] icon Icon to query. + * \return The value, in [min,max] (or [max,min]) as given at creation. + * Always 0 for icon types with no value. + */ +int wuss_icon_get_value(const wuss_icon_t *icon); + +/** + * Set a slider icon's value programmatically, invalidating it so the next + * redraw repaints it. No task event is delivered -- this is the programmatic + * path, distinct from a user click or drag. A no-op for icon types with no + * value. + * + * \param[in] window Window the icon belongs to. + * \param[in] icon Icon to change. + * \param[in] value New value; clamped to the icon's [min,max] range. + */ +void wuss_icon_set_value(wuss_window_t *window, + wuss_icon_t *icon, + int value); + /* ----------------------------------------------------------------------- */ /** diff --git a/include/wuss/task.h b/include/wuss/task.h index c8c693c3..c5ad1530 100644 --- a/include/wuss/task.h +++ b/include/wuss/task.h @@ -178,7 +178,10 @@ typedef struct wuss_event * Label, hidden and disabled icons never raise this; those clicks * fall through as wuss_EVENT_MOUSE. action is DOWN/UP/MOVE; button * is a set of wuss_button_t flags, so test it with '&' rather than - * comparing for equality. In the task view (window == NULL) this is + * comparing for equality. value is the icon's current value for a + * wuss_ICON_TYPE_SLIDER (updated before this event is delivered, so it + * always reflects the click/drag that raised it); meaningless for every + * other icon type. In the task view (window == NULL) this is * reserved for a future shared/dock element and is never currently * emitted. */ struct @@ -186,6 +189,7 @@ typedef struct wuss_event wuss_icon_t *icon; wuss_mouse_action_t action; wuss_button_t button; + int value; } icon; diff --git a/include/wuss/wuss.h b/include/wuss/wuss.h index a5a9c295..e462567a 100644 --- a/include/wuss/wuss.h +++ b/include/wuss/wuss.h @@ -145,7 +145,10 @@ typedef unsigned char wuss_colour_t; * wuss_COLOUR_BUTTON_PRESSED is button.pressed, * wuss_COLOUR_ACCENT is accent.colour, * wuss_COLOUR_BACKDROP is backdrop.colour, - * wuss_COLOUR_WINDOW is body.window, wuss_COLOUR_MENU is body.menu. */ + * wuss_COLOUR_WINDOW is body.window, wuss_COLOUR_MENU is body.menu, + * wuss_COLOUR_SLIDER_TRACK is slider.track, + * wuss_COLOUR_SLIDER_VALUE is slider.value, + * wuss_COLOUR_SLIDER_SURROUND is slider.surround. */ #define wuss_COLOUR_TITLE_BG (wuss_COLOUR_SYMBOLIC + 16) #define wuss_COLOUR_TITLE_FG (wuss_COLOUR_SYMBOLIC + 17) #define wuss_COLOUR_BUTTON_HILIGHT (wuss_COLOUR_SYMBOLIC + 18) @@ -158,6 +161,9 @@ typedef unsigned char wuss_colour_t; #define wuss_COLOUR_BUTTON_PRESSED (wuss_COLOUR_SYMBOLIC + 25) #define wuss_COLOUR_BORDER_DIVIDER (wuss_COLOUR_SYMBOLIC + 26) #define wuss_COLOUR_BUTTON_BG (wuss_COLOUR_SYMBOLIC + 27) +#define wuss_COLOUR_SLIDER_TRACK (wuss_COLOUR_SYMBOLIC + 28) +#define wuss_COLOUR_SLIDER_VALUE (wuss_COLOUR_SYMBOLIC + 29) +#define wuss_COLOUR_SLIDER_SURROUND (wuss_COLOUR_SYMBOLIC + 30) /** 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 @@ -389,6 +395,25 @@ typedef struct wuss_config } accent; + /** + * wuss_ICON_TYPE_SLIDER colours, as indices into the system palette: + * surround is the icon's full bounding box, including the fixed 4px gap + * around the inner slider rect; track is the sunken groove's ground (the + * unfilled portion) within that inner rect; value is the fill marking the + * current value. surround and value default to button.bg and track to + * body.window when config is NULL. Read back through + * wuss_COLOUR_SLIDER_SURROUND / wuss_COLOUR_SLIDER_TRACK / + * wuss_COLOUR_SLIDER_VALUE. Ignored when WUSS_ICONS is off. + */ + struct + { + wuss_colour_t surround; /**< Bounding box and gap around the + * inner rect. */ + wuss_colour_t track; /**< Groove ground (unfilled portion). */ + wuss_colour_t value; /**< Fill marking the current value. */ + } + slider; + /** * Conventional body fills, as system-palette indices: window is the * work-area behind a task's content, menu the pop-up menu background. Read diff --git a/libraries/geom/box/grow.c b/libraries/geom/box/grow.c index e07aee9a..d86963fc 100644 --- a/libraries/geom/box/grow.c +++ b/libraries/geom/box/grow.c @@ -17,3 +17,15 @@ void box_grow(box_t *box, int change) box->x1 += change; box->y1 += change; } + +box_t box_grown(const box_t *b, int change) +{ + box_t result; + + result.x0 = b->x0 - change; + result.y0 = b->y0 - change; + result.x1 = b->x1 + change; + result.y1 = b->y1 + change; + + return result; +} diff --git a/libraries/wuss/core/create.c b/libraries/wuss/core/create.c index 44b4db1f..bf123f89 100644 --- a/libraries/wuss/core/create.c +++ b/libraries/wuss/core/create.c @@ -290,6 +290,42 @@ result_t wuss_create(screen_t *scr, #endif #endif /* WUSS_FURNITURE */ +#ifdef WUSS_ICONS + { + wuss_colour_t track, value, surround; + + if (config != NULL) + { + track = (config->slider.track == wuss_NO_BACKGROUND) + ? w->window_bg + : wuss__resolve_colour(w, config->slider.track); + value = (config->slider.value == wuss_NO_BACKGROUND) + ? w->button_bg + : wuss__resolve_colour(w, config->slider.value); + surround = (config->slider.surround == wuss_NO_BACKGROUND) + ? w->button_bg + : wuss__resolve_colour(w, config->slider.surround); + } + else + { + track = w->window_bg; + value = w->button_bg; + surround = w->button_bg; + } + + if (track >= w->npalette || value >= w->npalette || surround >= w->npalette) + { + wuss__free(w, w->palette); + wuss__free(w, w); + return result_WUSS_BAD_COLOUR; + } + + w->slider_track = track; + w->slider_value = value; + w->slider_surround = surround; + } +#endif + /* Second pass: the chrome colours are stored and concrete now, so fill in * the chrome-role symbolic slots (wuss_COLOUR_TITLE_BG etc.). */ wuss__rebuild_palettecache(w); diff --git a/libraries/wuss/core/impl.h b/libraries/wuss/core/impl.h index 790df6d1..55c9f59d 100644 --- a/libraries/wuss/core/impl.h +++ b/libraries/wuss/core/impl.h @@ -51,6 +51,8 @@ #define WUSS_FRAME_CAPTION_INSET 8 /* x offset of a wuss_ICON_TYPE_FRAME caption from the frame's left edge */ #define WUSS_FRAME_CAPTION_PAD 2 /* gap left in the frame's top edge either side of the caption */ +#define WUSS_SLIDER_GAP 4 /* fixed inset on all four sides of a wuss_ICON_TYPE_SLIDER bbox, between the surround and the inner (clickable) rect */ + /* Font slot (see wuss_create's fonts[]) consulted for menu decoration glyphs: * the selection tick and the submenu arrow. When the slot is empty those are * drawn as vector strokes instead. A font in this slot is expected to carry a @@ -126,6 +128,11 @@ struct wuss wuss_colour_t button_fg; /* button label */ wuss_colour_t button_pressed; /* button face while held */ wuss_colour_t accent; /* default action button fill */ +#endif +#ifdef WUSS_ICONS + wuss_colour_t slider_track; /* slider groove ground */ + wuss_colour_t slider_value; /* slider fill */ + wuss_colour_t slider_surround; /* slider bbox and gap */ #endif wuss_colour_t window_bg; /* work-area body fill; wuss_COLOUR_WINDOW */ wuss_colour_t menu_bg; /* menu body fill; wuss_COLOUR_MENU */ diff --git a/libraries/wuss/core/mouse-click.c b/libraries/wuss/core/mouse-click.c index 282454ab..7fce2322 100644 --- a/libraries/wuss/core/mouse-click.c +++ b/libraries/wuss/core/mouse-click.c @@ -308,6 +308,14 @@ result_t wuss_mouse_click(wuss_t *wuss, wuss->pressed_icon = icon; wuss->pressed_window = win; wuss__icon_invalidate(win, icon); + + /* a slider jumps straight to the click point rather than waiting + * for a completed press/release, and keeps updating on MOVE while + * held (core/mouse-move.c) */ + if (icon->spec.type == wuss_ICON_TYPE_SLIDER) + wuss__icon_set_value(win, icon, + wuss__slider_value_for_point(win, icon, + POINT(x, y))); } else if (action == wuss_MOUSE_UP && wuss__icon_pressed(icon)) { @@ -330,6 +338,7 @@ result_t wuss_mouse_click(wuss_t *wuss, event.data.icon.icon = icon; event.data.icon.action = action; event.data.icon.button = button; + event.data.icon.value = icon->value; return wuss__deliver(win->task, win, &event); } } diff --git a/libraries/wuss/core/mouse-move.c b/libraries/wuss/core/mouse-move.c index 5d4bacc4..5b456069 100644 --- a/libraries/wuss/core/mouse-move.c +++ b/libraries/wuss/core/mouse-move.c @@ -91,6 +91,27 @@ result_t wuss_mouse_move(wuss_t *wuss, point_t p, wuss_window_t **hit) doc_point.x = x - content.x0 + win->scroll.x; doc_point.y = y - content.y0 + win->scroll.y; +#ifdef WUSS_ICONS + /* a slider drag keeps tracking the pointer even once it strays outside + * the icon's own bbox, matching a furniture sausage drag */ + if (wuss->pressed_icon != NULL && wuss->pressed_window == win && + wuss->pressed_icon->spec.type == wuss_ICON_TYPE_SLIDER) + { + wuss_icon_t *icon = wuss->pressed_icon; + + wuss__icon_set_value(win, icon, + wuss__slider_value_for_point(win, icon, + POINT(x, y))); + + event.kind = wuss_EVENT_ICON; + event.data.icon.icon = icon; + event.data.icon.action = wuss_MOUSE_MOVE; + event.data.icon.button = wuss_BUTTON_SELECT; + event.data.icon.value = icon->value; + return wuss__deliver(win->task, win, &event); + } +#endif + #ifdef WUSS_ICONS { wuss_icon_t *icon; @@ -126,6 +147,7 @@ result_t wuss_mouse_move(wuss_t *wuss, point_t p, wuss_window_t **hit) event.data.icon.icon = icon; event.data.icon.action = wuss_MOUSE_MOVE; event.data.icon.button = wuss_BUTTON_SELECT; + event.data.icon.value = icon->value; return wuss__deliver(win->task, win, &event); } } diff --git a/libraries/wuss/core/rebuild-palettecache.c b/libraries/wuss/core/rebuild-palettecache.c index 7b904b4f..3ddffb7b 100644 --- a/libraries/wuss/core/rebuild-palettecache.c +++ b/libraries/wuss/core/rebuild-palettecache.c @@ -55,6 +55,11 @@ void wuss__rebuild_palettecache(wuss_t *wuss) cache[wuss_COLOUR_BUTTON_FG - wuss_COLOUR_SYMBOLIC] = wuss->button_fg; cache[wuss_COLOUR_BUTTON_PRESSED - wuss_COLOUR_SYMBOLIC] = wuss->button_pressed; cache[wuss_COLOUR_ACCENT - wuss_COLOUR_SYMBOLIC] = wuss->accent; +#endif +#ifdef WUSS_ICONS + cache[wuss_COLOUR_SLIDER_TRACK - wuss_COLOUR_SYMBOLIC] = wuss->slider_track; + cache[wuss_COLOUR_SLIDER_VALUE - wuss_COLOUR_SYMBOLIC] = wuss->slider_value; + cache[wuss_COLOUR_SLIDER_SURROUND - wuss_COLOUR_SYMBOLIC] = wuss->slider_surround; #endif if (wuss->backdrop.colour != wuss_NO_BACKGROUND) cache[wuss_COLOUR_BACKDROP - wuss_COLOUR_SYMBOLIC] = wuss->backdrop.colour; diff --git a/libraries/wuss/icon.h b/libraries/wuss/icon.h index cae584e7..7891f1c8 100644 --- a/libraries/wuss/icon.h +++ b/libraries/wuss/icon.h @@ -33,6 +33,9 @@ struct wuss_icon { wuss_icon_spec_t spec; /* bbox in virtual document space; text owned */ wuss_icon_state_t state; + int value; /* wuss_ICON_TYPE_SLIDER: current value, in + * [spec.u.slider.min,max]; spec.u.slider.default_value + * is creation input only, never updated */ }; static inline int wuss__icon_pressed(const wuss_icon_t *icon) @@ -68,6 +71,15 @@ void wuss__icon_select(wuss_window_t *window, wuss_icon_t *icon, int selected); +/* Set icon->value, clamped to spec.u.slider.min/max, invalidating it if the + * clamped value changed. Ignored for types with no value. "icon" must be an + * icon of "window". Shared by wuss_icon_set_value and the slider drag path + * (core/mouse-click.c, core/mouse-move.c), which additionally raise + * wuss_EVENT_ICON with the new value -- this helper never does. */ +void wuss__icon_set_value(wuss_window_t *window, + wuss_icon_t *icon, + int value); + /* Make "icon" (an icon of "window", or NULL) the hovered icon: clears the * hovered flag on the previous wuss->hover_icon and sets it on the new one, * invalidating whichever of the two changed. "window" is ignored when "icon" @@ -84,6 +96,47 @@ void wuss__icon_box_to_screen(const box_t *content, const box_t *bbox, box_t *out); +/* wuss_ICON_TYPE_SLIDER geometry and value/pixel conversion, shared by + * wuss__icon_draw and the click/drag path (core/mouse-click.c, + * core/mouse-move.c) so hit-testing cannot drift from what is drawn. + * "screen_box" is the icon's screen-space box, as wuss__icon_box_to_screen + * gives it. */ + +/* The inner rect: "screen_box" (the icon's full bbox) inset by + * WUSS_SLIDER_GAP on all four sides. This is both the groove drawn inside it + * and the only area wuss__icon_hit_test accepts clicks/drags on; the gap and + * the rest of "screen_box" are painted with slider.surround and never + * interactive. */ +void wuss__slider_groove_box(const box_t *screen_box, box_t *out); + +/* Convert "value" (clamped to [lo,hi], lo <= hi) to a fill length in pixels + * along "groove"'s long axis, 0 at lo and the full long-axis extent at hi. + * For wuss_SLIDER_HORIZONTAL pixel 0 is the groove's left (x0); for VERTICAL + * it is the groove's bottom (y1), so value grows upward on screen even + * though screen y grows downward. */ +int wuss__slider_value_to_px(const box_t *groove, + wuss_slider_orientation_t orientation, + int value, + int lo, + int hi); + +/* Convert a screen-space point's long-axis coordinate back to a value in + * [lo,hi] (lo <= hi), inverting wuss__slider_value_to_px (same pixel-0 end + * per orientation). */ +int wuss__slider_px_to_value(const box_t *groove, + wuss_slider_orientation_t orientation, + point_t screen_point, + int lo, + int hi); + +/* "icon" (a slider icon of "window") maps a raw screen-space point (as passed + * to wuss_mouse_click/wuss_mouse_move) to the value its groove would read at + * that point, honouring a min > max reversed fill. Shared by the click-jump + * and drag-continuation paths (core/mouse-click.c, core/mouse-move.c). */ +int wuss__slider_value_for_point(wuss_window_t *window, + const wuss_icon_t *icon, + point_t screen_point); + /* Invalidate exactly this icon's bbox on "window", via wuss_window_invalidate, * so a set_text / pressed-state / hide change repaints just the icon. "icon" * must be an icon of "window". */ diff --git a/libraries/wuss/icon/draw.c b/libraries/wuss/icon/draw.c index 7b48cd25..ab713338 100644 --- a/libraries/wuss/icon/draw.c +++ b/libraries/wuss/icon/draw.c @@ -44,8 +44,7 @@ static void icon_fill_bevel(screen_t *scr, colour_t light, colour_t dark) { - screen_fill_rect(scr, b->x0, b->y0, - SIZE2D(b->x1 - b->x0, b->y1 - b->y0), fill); + screen_fill_rect(scr, b->x0, b->y0, box_size(b), fill); screen_draw_bevel_edge(scr, b, light, dark); } @@ -66,12 +65,10 @@ static void icon_draw_action_border(screen_t *scr, ring = *b; screen_draw_bevel_edge(scr, &ring, dark, light); - ring = (box_t) BOX_POS_SIZE(b->x0 + 2, b->y0 + 2, - b->x1 - b->x0 - 4, b->y1 - b->y0 - 4); + ring = box_grown(b, -2); screen_draw_bevel_edge(scr, &ring, accent, accent); - ring = (box_t) BOX_POS_SIZE(b->x0 + 4, b->y0 + 4, - b->x1 - b->x0 - 8, b->y1 - b->y0 - 8); + ring = box_grown(b, -4); screen_draw_bevel_edge(scr, &ring, pressed ? dark : light, pressed ? light : dark); } @@ -90,8 +87,7 @@ static void icon_draw_divider_border(screen_t *scr, ring = *b; screen_draw_bevel_edge(scr, &ring, divider, light); - ring = (box_t) BOX_POS_SIZE(b->x0 + 2, b->y0 + 2, - b->x1 - b->x0 - 4, b->y1 - b->y0 - 4); + ring = box_grown(b, -2); screen_draw_bevel_edge(scr, &ring, light, divider); } @@ -183,8 +179,7 @@ static void wuss__icon_draw_label(const icon_draw_ctx_t *c) if (icon->bg != wuss_NO_BACKGROUND) { bg = c->wuss->palette[icon->bg]; - screen_fill_rect(c->scr, b->x0, b->y0, - SIZE2D(b->x1 - b->x0, b->y1 - b->y0), bg); + screen_fill_rect(c->scr, b->x0, b->y0, box_size(b), bg); } else { @@ -329,8 +324,7 @@ static void wuss__icon_draw_button(const icon_draw_ctx_t *c) * "action" surround as wuss_ICON_BORDER_ACTION, distinct from the plain * bevelled buttons around it. */ - screen_fill_rect(c->scr, b->x0, b->y0, - SIZE2D(b->x1 - b->x0, b->y1 - b->y0), base); + screen_fill_rect(c->scr, b->x0, b->y0, box_size(b), base); icon_draw_action_border(c->scr, b, light, dark, c->wuss->palette[c->wuss->accent], pressed); } @@ -420,15 +414,13 @@ static void wuss__icon_draw_radio_option(const icon_draw_ctx_t *c) bg = icon_blend_ground(c, glyph); if (icon->bg != wuss_NO_BACKGROUND) - screen_fill_rect(c->scr, b->x0, b->y0, - SIZE2D(b->x1 - b->x0, b->y1 - b->y0), bg); + screen_fill_rect(c->scr, b->x0, b->y0, box_size(b), bg); else /* no explicit bg to repaint the whole box, but the state we are about to * draw (a smaller radon/radoff swap, or a cleared tick/centre) can leave * fewer pixels than last time -- clear just the glyph box so no stale * glyph shows through. The label to its right never changes on select. */ - screen_fill_rect(c->scr, g.x0, g.y0, - SIZE2D(g.x1 - g.x0, g.y1 - g.y0), bg); + screen_fill_rect(c->scr, g.x0, g.y0, box_size(&g), bg); if (bm != NULL) { @@ -448,8 +440,7 @@ static void wuss__icon_draw_radio_option(const icon_draw_ctx_t *c) else { /* a box; a tick (two strokes) when selected */ - screen_draw_rect(c->scr, g.x0, g.y0, - SIZE2D(g.x1 - g.x0, g.y1 - g.y0), glyph); + screen_draw_rect(c->scr, g.x0, g.y0, box_size(&g), glyph); if (wuss__icon_selected(c->icon)) { screen_draw_line(c->scr, g.x0 + 2, g.y0 + gh / 2, @@ -536,8 +527,7 @@ static void wuss__icon_draw_menu_entry(const icon_draw_ctx_t *c) screen_fill_rect(c->scr, text_x0, b->y0, SIZE2D(text_x1 - text_x0, b->y1 - b->y0), text_ground); else if (icon->bg != wuss_NO_BACKGROUND) - screen_fill_rect(c->scr, b->x0, b->y0, - SIZE2D(b->x1 - b->x0, b->y1 - b->y0), ground); + screen_fill_rect(c->scr, b->x0, b->y0, box_size(b), ground); /* left-edge colour chip (wins over the tick) or tick when selected */ if ((icon->flags & wuss_ICON_FLAGS_SWATCH) && @@ -626,6 +616,55 @@ static void wuss__icon_draw_rule(const icon_draw_ctx_t *c) /* ----------------------------------------------------------------------- */ +static void wuss__icon_draw_slider(const icon_draw_ctx_t *c) +{ + const wuss_icon_spec_t *icon = &c->icon->spec; + colour_t surround, track, value, light, dark; + box_t surround_box, groove, fill; + int lo, hi, px; + + surround = c->wuss->palette[c->wuss->slider_surround]; + track = c->wuss->palette[c->wuss->slider_track]; + value = c->wuss->palette[c->wuss->slider_value]; + light = c->wuss->palette[c->wuss->bevel_light]; + dark = c->wuss->palette[c->wuss->bevel_dark]; + + /* draw the surround */ + + surround_box = box_grown(&c->b, -2); /* inset by bevel width to avoid overdraw */ + screen_fill_rect(c->scr, surround_box.x0, surround_box.y0, + box_size(&surround_box), surround); + + /* draw the bevel */ + + screen_draw_bevel_edge(c->scr, &c->b, dark, light); + + wuss__slider_groove_box(&c->b, &groove); + + /* draw the track */ + + lo = MIN(icon->u.slider.min, icon->u.slider.max); + hi = MAX(icon->u.slider.min, icon->u.slider.max); + px = wuss__slider_value_to_px(&groove, icon->u.slider.orientation, + c->icon->value, lo, hi); + if (icon->u.slider.min > icon->u.slider.max) + px = (icon->u.slider.orientation == wuss_SLIDER_HORIZONTAL) + ? (groove.x1 - groove.x0) - px + : (groove.y1 - groove.y0) - px; + + fill = groove; + if (icon->u.slider.orientation == wuss_SLIDER_HORIZONTAL) + fill.x1 = fill.x0 + px; + else + fill.y0 = fill.y1 - px; + + screen_fill_rect(c->scr, groove.x0, groove.y0, box_size(&groove), track); + if (px > 0) + screen_fill_rect(c->scr, fill.x0, fill.y0, box_size(&fill), value); +} + +/* ----------------------------------------------------------------------- */ + void wuss__icon_draw(wuss_t *wuss, const wuss_window_t *window, const wuss_icon_t *icon, @@ -700,12 +739,15 @@ void wuss__icon_draw(wuss_t *wuss, wuss__icon_draw_rule(&c); break; + case wuss_ICON_TYPE_SLIDER: + wuss__icon_draw_slider(&c); + break; + /* reserved types with no renderer yet: fall back to a plain label */ case wuss_ICON_TYPE_DISPLAY: case wuss_ICON_TYPE_WRITABLE: case wuss_ICON_TYPE_NUMBER: case wuss_ICON_TYPE_STRING_SET: - case wuss_ICON_TYPE_SLIDER: case wuss_ICON_TYPE_DRAGGABLE: wuss__icon_draw_label(&c); break; diff --git a/libraries/wuss/icon/from-spec.c b/libraries/wuss/icon/from-spec.c index 47ab0cc8..5cd42fbd 100644 --- a/libraries/wuss/icon/from-spec.c +++ b/libraries/wuss/icon/from-spec.c @@ -43,10 +43,15 @@ result_t wuss__icon_from_spec(const wuss_t *w, case wuss_ICON_TYPE_WRITABLE: case wuss_ICON_TYPE_NUMBER: case wuss_ICON_TYPE_STRING_SET: - case wuss_ICON_TYPE_SLIDER: case wuss_ICON_TYPE_DRAGGABLE: break; + case wuss_ICON_TYPE_SLIDER: + if (spec->u.slider.orientation != wuss_SLIDER_HORIZONTAL && + spec->u.slider.orientation != wuss_SLIDER_VERTICAL) + return result_WUSS_BAD_ICON; + break; + default: return result_WUSS_BAD_ICON; } @@ -148,5 +153,18 @@ result_t wuss__icon_from_spec(const wuss_t *w, out->state = wuss_ICON_STATE_NONE; + if (spec->type == wuss_ICON_TYPE_SLIDER) + { + int lo, hi; + + lo = MIN(spec->u.slider.min, spec->u.slider.max); + hi = MAX(spec->u.slider.min, spec->u.slider.max); + out->value = CLAMP(spec->u.slider.default_value, lo, hi); + } + else + { + out->value = 0; + } + return result_OK; } diff --git a/libraries/wuss/icon/get-value.c b/libraries/wuss/icon/get-value.c new file mode 100644 index 00000000..0ed9b566 --- /dev/null +++ b/libraries/wuss/icon/get-value.c @@ -0,0 +1,12 @@ +/* wuss/icon/get-value.c -- query a slider icon's current value */ + +#include + +#include "../core/impl.h" + +int wuss_icon_get_value(const wuss_icon_t *icon) +{ + assert(icon != NULL); + + return (icon->spec.type == wuss_ICON_TYPE_SLIDER) ? icon->value : 0; +} diff --git a/libraries/wuss/icon/hit-test.c b/libraries/wuss/icon/hit-test.c index 6626d089..0ee41fcf 100644 --- a/libraries/wuss/icon/hit-test.c +++ b/libraries/wuss/icon/hit-test.c @@ -7,6 +7,7 @@ wuss_icon_t *wuss__icon_hit_test(wuss_window_t *window, point_t doc_point) { wuss_icon_t *it; + box_t box; int i; /* last created wins, matching the draw order in redraw_window */ @@ -24,7 +25,8 @@ wuss_icon_t *wuss__icon_hit_test(wuss_window_t *window, point_t doc_point) else if (it->spec.type != wuss_ICON_TYPE_ACTION && it->spec.type != wuss_ICON_TYPE_RADIO && it->spec.type != wuss_ICON_TYPE_OPTION && - it->spec.type != wuss_ICON_TYPE_MENU_ENTRY) + it->spec.type != wuss_ICON_TYPE_MENU_ENTRY && + it->spec.type != wuss_ICON_TYPE_SLIDER) { continue; } @@ -32,7 +34,21 @@ wuss_icon_t *wuss__icon_hit_test(wuss_window_t *window, point_t doc_point) if (it->spec.flags & (wuss_ICON_FLAGS_HIDDEN | wuss_ICON_FLAGS_DISABLED)) continue; - if (box_contains_point(&it->spec.bbox, doc_point.x, doc_point.y)) + /* only the inner rect (bbox inset by the gap) reacts to clicks; the + * surround and the gap around it are decorative only */ + if (it->spec.type == wuss_ICON_TYPE_SLIDER) + { + box.x0 = it->spec.bbox.x0 + WUSS_SLIDER_GAP; + box.y0 = it->spec.bbox.y0 + WUSS_SLIDER_GAP; + box.x1 = it->spec.bbox.x1 - WUSS_SLIDER_GAP; + box.y1 = it->spec.bbox.y1 - WUSS_SLIDER_GAP; + } + else + { + box = it->spec.bbox; + } + + if (box_contains_point(&box, doc_point.x, doc_point.y)) return it; } diff --git a/libraries/wuss/icon/set-value.c b/libraries/wuss/icon/set-value.c new file mode 100644 index 00000000..97808522 --- /dev/null +++ b/libraries/wuss/icon/set-value.c @@ -0,0 +1,35 @@ +/* wuss/icon/set-value.c -- set a slider icon's current value */ + +#include + +#include "../core/impl.h" + +void wuss__icon_set_value(wuss_window_t *window, + wuss_icon_t *icon, + int value) +{ + int lo, hi, clamped; + + assert(window != NULL); + assert(icon != NULL); + + if (icon->spec.type != wuss_ICON_TYPE_SLIDER) + return; + + lo = MIN(icon->spec.u.slider.min, icon->spec.u.slider.max); + hi = MAX(icon->spec.u.slider.min, icon->spec.u.slider.max); + clamped = CLAMP(value, lo, hi); + + if (clamped == icon->value) + return; + + icon->value = clamped; + wuss__icon_invalidate(window, icon); +} + +void wuss_icon_set_value(wuss_window_t *window, + wuss_icon_t *icon, + int value) +{ + wuss__icon_set_value(window, icon, value); +} diff --git a/libraries/wuss/icon/slider-box.c b/libraries/wuss/icon/slider-box.c new file mode 100644 index 00000000..5dd0029c --- /dev/null +++ b/libraries/wuss/icon/slider-box.c @@ -0,0 +1,88 @@ +/* wuss/icon/slider-box.c -- slider groove geometry and value/pixel mapping */ + +#include "geom/box.h" +#include "geom/point.h" + +#include "../core/impl.h" + +void wuss__slider_groove_box(const box_t *screen_box, box_t *out) +{ + out->x0 = screen_box->x0 + WUSS_SLIDER_GAP; + out->y0 = screen_box->y0 + WUSS_SLIDER_GAP; + out->x1 = screen_box->x1 - WUSS_SLIDER_GAP; + out->y1 = screen_box->y1 - WUSS_SLIDER_GAP; +} + +int wuss__slider_value_to_px(const box_t *groove, + wuss_slider_orientation_t orientation, + int value, + int lo, + int hi) +{ + int span, extent; + + value = CLAMP(value, lo, hi); + span = hi - lo; + extent = (orientation == wuss_SLIDER_HORIZONTAL) + ? groove->x1 - groove->x0 + : groove->y1 - groove->y0; + + if (span == 0) + return 0; + + return (int) ((long) (value - lo) * extent / span); +} + +int wuss__slider_px_to_value(const box_t *groove, + wuss_slider_orientation_t orientation, + point_t screen_point, + int lo, + int hi) +{ + int px, extent, span, value; + + extent = (orientation == wuss_SLIDER_HORIZONTAL) + ? groove->x1 - groove->x0 + : groove->y1 - groove->y0; + /* screen y grows downward but value grows upward for a vertical slider, so + * pixel 0 is the groove's bottom (y1), mirroring HORIZONTAL's pixel 0 at + * its left (x0) */ + px = (orientation == wuss_SLIDER_HORIZONTAL) + ? screen_point.x - groove->x0 + : groove->y1 - screen_point.y; + span = hi - lo; + + if (extent <= 0) + return lo; + + px = CLAMP(px, 0, extent); + value = lo + (int) (((long) px * span + extent / 2) / extent); + + return CLAMP(value, lo, hi); +} + +int wuss__slider_value_for_point(wuss_window_t *window, + const wuss_icon_t *icon, + point_t screen_point) +{ + box_t content, screen_box, groove; + int lo, hi, value; + + wuss__content_box(window, &content); + wuss__icon_box_to_screen(&content, window->scroll, &icon->spec.bbox, + &screen_box); + wuss__slider_groove_box(&screen_box, &groove); + + lo = MIN(icon->spec.u.slider.min, icon->spec.u.slider.max); + hi = MAX(icon->spec.u.slider.min, icon->spec.u.slider.max); + value = wuss__slider_px_to_value(&groove, icon->spec.u.slider.orientation, + screen_point, lo, hi); + + /* min > max runs the fill backwards: the value nominally at the groove's + * start (min) then sits at its far/high pixel end, so mirror the reading + * around the [lo,hi] midpoint */ + if (icon->spec.u.slider.min > icon->spec.u.slider.max) + value = lo + hi - value; + + return value; +} diff --git a/libraries/wuss/test/tasks/slider.c b/libraries/wuss/test/tasks/slider.c new file mode 100644 index 00000000..576b84c0 --- /dev/null +++ b/libraries/wuss/test/tasks/slider.c @@ -0,0 +1,150 @@ +/* wuss/test/tasks/slider.c -- slider icons task */ + +#ifdef WUSS_APP + +#include +#include +#include + +#ifdef FORTIFY +#include "fortify/fortify.h" +#endif + +#include "geom/box.h" +#include "geom/point.h" +#include "geom/size.h" + +#include "slider.h" + +#define SLIDER_DOC_W 240 +#define SLIDER_DOC_H 200 +#define SLIDER_MARGIN 20 + +enum { SLIDER_NSPECS = 3 }; /* horizontal slider, vertical slider, state label */ + +result_t slider_create(wuss_t *wuss, + bmfont_t *font, + slider_task_t *task) +{ + wuss_task_t *delegate; + wuss_task_desc_t delegate_desc; + wuss_icon_spec_t specs[SLIDER_NSPECS]; + wuss_icon_t *made[SLIDER_NSPECS]; + result_t rc; + + task->font = font; + task->label = colour_rgb(0x00, 0x00, 0x00); + task->window = NULL; + task->horiz = NULL; + task->vert = NULL; + task->state = NULL; + + delegate_desc.handle = slider_handle; + delegate_desc.task_data = task; + delegate_desc.name = "slider"; + rc = wuss_task_create(wuss, &delegate_desc, &delegate); + if (rc != result_OK) + { + free(task); /* nothing registered yet; the spawner will not free it */ + return rc; + } + + rc = wuss_window_create_placed(delegate, + SIZE2D(SLIDER_DOC_W, SLIDER_DOC_H), + "Sliders", + wuss_WINDOW_DEFAULT, + wuss_BACKDROP_COLOUR(wuss_COLOUR_WINDOW), + SIZE2D(SLIDER_DOC_W, SLIDER_DOC_H), + SIZE2D(0, 0), + &task->window); + if (rc != result_OK) + { + wuss_task_destroy(delegate); /* QUIT frees the task block */ + return rc; + } + + memset(specs, 0, sizeof(specs)); + + specs[0].bbox = (box_t) BOX_POS_SIZE(SLIDER_MARGIN, 30, + SLIDER_DOC_W - SLIDER_MARGIN * 2, + 20); + specs[0].type = wuss_ICON_TYPE_SLIDER; + specs[0].fg = wuss_COLOUR_BLACK; + specs[0].bg = wuss_NO_BACKGROUND; + specs[0].u.slider.orientation = wuss_SLIDER_HORIZONTAL; + specs[0].u.slider.min = 0; + specs[0].u.slider.max = 100; + specs[0].u.slider.default_value = 25; + + specs[1].bbox = (box_t) BOX_POS_SIZE(SLIDER_DOC_W - SLIDER_MARGIN - 20, + 70, 20, 100); + specs[1].type = wuss_ICON_TYPE_SLIDER; + specs[1].fg = wuss_COLOUR_BLACK; + specs[1].bg = wuss_NO_BACKGROUND; + specs[1].u.slider.orientation = wuss_SLIDER_VERTICAL; + specs[1].u.slider.min = 0; + specs[1].u.slider.max = 10; + specs[1].u.slider.default_value = 10; + + specs[2].bbox = (box_t) BOX_POS_SIZE(SLIDER_MARGIN, 70, 150, 14); + specs[2].type = wuss_ICON_TYPE_LABEL; + specs[2].text = "horizontal: 25"; + specs[2].fg = wuss_COLOUR_BLACK; + specs[2].bg = wuss_NO_BACKGROUND; + + rc = wuss_icon_create_array(task->window, specs, SLIDER_NSPECS, made); + if (rc != result_OK) + { + wuss_task_destroy(delegate); /* closes the window; QUIT frees the block */ + return rc; + } + + task->horiz = made[0]; + task->vert = made[1]; + task->state = made[2]; + + wuss_task_set_autoclose(delegate, 1); + + return result_OK; +} + +static result_t slider_icon(const wuss_event_t *event, void *task_data) +{ + slider_task_t *tcx; + wuss_icon_t *icon; + const char *name; + char buf[48]; + + tcx = task_data; + icon = event->data.icon.icon; + + if (icon != tcx->horiz && icon != tcx->vert) + return result_OK; + + name = (icon == tcx->horiz) ? "horizontal" : "vertical"; + snprintf(buf, sizeof(buf), "%s: %d", name, event->data.icon.value); + + return wuss_icon_set_text(tcx->window, tcx->state, buf); +} + +result_t slider_handle(wuss_window_t *window, + const wuss_event_t *event, + void *task_data) +{ + (void) window; + + switch (event->kind) + { + case wuss_EVENT_ICON: + return slider_icon(event, task_data); + + case wuss_EVENT_QUIT: + free(task_data); /* calloc'd per instance by the spawner */ + return result_OK; + + default: + return result_OK; + } +} + +#endif /* WUSS_APP */ diff --git a/libraries/wuss/test/tasks/slider.h b/libraries/wuss/test/tasks/slider.h new file mode 100644 index 00000000..a127cae8 --- /dev/null +++ b/libraries/wuss/test/tasks/slider.h @@ -0,0 +1,36 @@ +/* wuss/test/tasks/slider.h -- slider icons task */ + +#ifndef TASKS_SLIDER_H +#define TASKS_SLIDER_H + +#ifdef WUSS_APP + +#include "framebuf/bmfont.h" +#include "framebuf/colour.h" +#include "wuss/icon.h" +#include "wuss/task.h" +#include "wuss/window.h" + +/* demonstrates wuss_ICON_TYPE_SLIDER: a horizontal and a vertical slider, each + * with its own min/max/default, and a label echoing whichever last moved */ +typedef struct slider_task +{ + wuss_window_t *window; + bmfont_t *font; + colour_t label; + wuss_icon_t *horiz; + wuss_icon_t *vert; + wuss_icon_t *state; /* label echoing the last-moved slider's value */ +} +slider_task_t; + +wuss_window_fn_t slider_handle; + +/* create the slider window against the given wuss instance */ +result_t slider_create(wuss_t *wuss, + bmfont_t *font, + slider_task_t *task); + +#endif /* WUSS_APP */ + +#endif /* TASKS_SLIDER_H */ diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index d7a28e6c..8b3e64f4 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -38,7 +38,7 @@ #if defined(WUSS_FURNITURE) && defined(WUSS_ICONS) #include "../core/impl.h" #endif -#if defined(WUSS_MENUS) && defined(WUSS_ICONS) +#if defined(WUSS_ICONS) #include "../icon.h" #endif @@ -5906,6 +5906,154 @@ ColourMenuOK: ; wuss_window_close(win_dv); } + printf("test: wuss__slider_value_to_px / wuss__slider_px_to_value convert " + "and round-trip\n"); + + { + box_t groove; + point_t pt; + + groove.x0 = 0; groove.y0 = 0; + groove.x1 = 100; groove.y1 = 20; + + /* endpoints and midpoint of a [0,100] range over a 100px groove: exact, + * no rounding needed */ + if (wuss__slider_value_to_px(&groove, wuss_SLIDER_HORIZONTAL, + 0, 0, 100) != 0) + goto Failure; + if (wuss__slider_value_to_px(&groove, wuss_SLIDER_HORIZONTAL, + 100, 0, 100) != 100) + goto Failure; + if (wuss__slider_value_to_px(&groove, wuss_SLIDER_HORIZONTAL, + 50, 0, 100) != 50) + goto Failure; + + /* a degenerate [lo,hi] span always reads back as pixel 0 */ + if (wuss__slider_value_to_px(&groove, wuss_SLIDER_HORIZONTAL, + 7, 7, 7) != 0) + goto Failure; + + /* px_to_value rounds to the nearest value rather than truncating: a 10px + * groove over [0,3] places value 1 at 3.33px, so pixel 3 (0.9 of the way + * to value 1) must round up to 1, not truncate down to 0 */ + groove.x0 = 0; groove.y0 = 0; + groove.x1 = 10; groove.y1 = 20; + + pt.x = 3; pt.y = 10; + if (wuss__slider_px_to_value(&groove, wuss_SLIDER_HORIZONTAL, + pt, 0, 3) != 1) + goto Failure; + + /* pixel 1 (0.3 of the way to value 1) is closer to 0, and must round + * down rather than up */ + pt.x = 1; pt.y = 10; + if (wuss__slider_px_to_value(&groove, wuss_SLIDER_HORIZONTAL, + pt, 0, 3) != 0) + goto Failure; + + /* an exact half-way pixel rounds up to the higher value: a 4px groove + * over [0,1] puts the 0/1 boundary at pixel 2 */ + groove.x0 = 0; groove.y0 = 0; + groove.x1 = 4; groove.y1 = 20; + + pt.x = 2; pt.y = 10; + if (wuss__slider_px_to_value(&groove, wuss_SLIDER_HORIZONTAL, + pt, 0, 1) != 1) + goto Failure; + + /* a point left of the groove clamps to lo, one past its right edge + * clamps to hi */ + pt.x = -50; pt.y = 10; + if (wuss__slider_px_to_value(&groove, wuss_SLIDER_HORIZONTAL, + pt, 0, 1) != 0) + goto Failure; + pt.x = 50; pt.y = 10; + if (wuss__slider_px_to_value(&groove, wuss_SLIDER_HORIZONTAL, + pt, 0, 1) != 1) + goto Failure; + + /* VERTICAL: pixel 0 is the groove's bottom (y1), value grows upward */ + groove.x0 = 0; groove.y0 = 0; + groove.x1 = 20; groove.y1 = 10; + + pt.x = 10; pt.y = groove.y1; + if (wuss__slider_px_to_value(&groove, wuss_SLIDER_VERTICAL, + pt, 0, 100) != 0) + goto Failure; + pt.x = 10; pt.y = groove.y0; + if (wuss__slider_px_to_value(&groove, wuss_SLIDER_VERTICAL, + pt, 0, 100) != 100) + goto Failure; + } + + printf("test: wuss__slider_value_for_point maps a screen click through a " + "real slider icon, honouring the groove gap and a reversed " + "min > max\n"); + + { + static test_task_t tc_sl; + wuss_task_t *delegate_sl; + wuss_window_t *win_sl; + wuss_icon_t *icon_sl; + wuss_icon_spec_t spec_sl; + box_t box_sl, content_sl, screen_box_sl, groove_sl; + point_t scroll_sl, pt_sl; + + delegate_sl = mk_task(wuss, test_handle, &tc_sl); + if (delegate_sl == NULL) goto Failure; + + box_sl.x0 = 5; box_sl.y0 = 5; box_sl.x1 = 125; box_sl.y1 = 105; + rc = wuss_window_create(delegate_sl, &box_sl, "SL", wuss_WINDOW_DEFAULT, + wuss_NO_BACKDROP, + SIZE2D(400, 400), SIZE2D(0, 0), &win_sl); + if (rc != result_OK) + goto Failure; + + memset(&spec_sl, 0, sizeof(spec_sl)); + spec_sl.bbox = (box_t) BOX_POS_SIZE(0, 0, 108, 20); + spec_sl.type = wuss_ICON_TYPE_SLIDER; + spec_sl.u.slider.orientation = wuss_SLIDER_HORIZONTAL; + spec_sl.u.slider.min = 0; + spec_sl.u.slider.max = 100; + spec_sl.u.slider.default_value = 0; + rc = wuss_icon_create(win_sl, &spec_sl, &icon_sl); + if (rc != result_OK) + goto Failure; + + wuss_window_get_content_bounds(win_sl, &content_sl); + wuss_window_get_scroll(win_sl, &scroll_sl); + wuss__icon_box_to_screen(&content_sl, scroll_sl, &spec_sl.bbox, + &screen_box_sl); + wuss__slider_groove_box(&screen_box_sl, &groove_sl); + + /* a click at the groove's on-screen left/right edges reads back as the + * slider's min/max, confirming wuss__slider_value_for_point applies the + * WUSS_SLIDER_GAP inset rather than reading off the full icon bbox */ + pt_sl.x = groove_sl.x0; pt_sl.y = (groove_sl.y0 + groove_sl.y1) / 2; + if (wuss__slider_value_for_point(win_sl, icon_sl, pt_sl) != 0) + goto Failure; + + pt_sl.x = groove_sl.x1; pt_sl.y = (groove_sl.y0 + groove_sl.y1) / 2; + if (wuss__slider_value_for_point(win_sl, icon_sl, pt_sl) != 100) + goto Failure; + + /* a reversed min > max mirrors the reading: the groove's left pixel + * (nominally "pixel 0") now yields the high end of [lo,hi] */ + wuss_icon_set_value(win_sl, icon_sl, 100); + icon_sl->spec.u.slider.min = 100; + icon_sl->spec.u.slider.max = 0; + + pt_sl.x = groove_sl.x0; pt_sl.y = (groove_sl.y0 + groove_sl.y1) / 2; + if (wuss__slider_value_for_point(win_sl, icon_sl, pt_sl) != 100) + goto Failure; + + pt_sl.x = groove_sl.x1; pt_sl.y = (groove_sl.y0 + groove_sl.y1) / 2; + if (wuss__slider_value_for_point(win_sl, icon_sl, pt_sl) != 0) + goto Failure; + + wuss_window_close(win_sl); + } + printf("test: wuss_icons_load scans resources/wuss/icons and compresses\n"); { From bb85d4eb792febaa6de18ed662c94524e933eb85 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 13 Sep 2026 23:45:56 +0100 Subject: [PATCH 061/128] feat(wuss): saturn task gets a Size... submenu Slider dialogue (128-512, step 64) is a hover-opened menu leaf built once in saturn_create and shown/hidden by wuss via wuss_menu_item_t::window, matching the existing Colours submenu pattern. Apply resizes the window and stores the value in task->config.size, replacing the fixed SATURN_SIZE window-size constant (the fixed 256-sample sketch space is unaffected). Cancel/Apply both dismiss through wuss_menu_close so the borrowed window is hidden, not destroyed. --- libraries/wuss/test/tasks/saturn.c | 240 +++++++++++++++++++++++++++-- libraries/wuss/test/tasks/saturn.h | 17 +- 2 files changed, 242 insertions(+), 15 deletions(-) diff --git a/libraries/wuss/test/tasks/saturn.c b/libraries/wuss/test/tasks/saturn.c index d326dd45..d740c5fc 100644 --- a/libraries/wuss/test/tasks/saturn.c +++ b/libraries/wuss/test/tasks/saturn.c @@ -3,7 +3,9 @@ #ifdef WUSS_APP #include +#include #include +#include #ifdef FORTIFY #include "fortify/fortify.h" @@ -12,6 +14,7 @@ #include "base/utils.h" #include "framebuf/palettes.h" #include "geom/box.h" +#include "geom/size.h" #include "utils/rng.h" #include "wuss/menu.h" @@ -38,7 +41,12 @@ * x = sqrt(16384-P)/2 -> planet body */ -#define SATURN_SIZE 256 /* window size is this value squared */ +/* the sample space below (and so the sketch itself) is always 256 pixels + * square, matching the original's -128..127 signed-byte range; the window's + * on-screen size (task->config.size, set via the Size... dialogue) scales + * independently, so a larger window just shows more margin around the same + * sketch and a smaller one clips it. */ +#define SATURN_SIZE 256 /* The original works in a -128..127 sample space (BBC BASIC signed byte). */ #define SATURN_HALF (SATURN_SIZE / 2) /* sample-space centre / bias */ @@ -69,10 +77,17 @@ static int saturn_rnd(int n) /* MENU click over the content pops this. "Colours" leads to a submenu with * one row per task->fg/task->bg, each of which pops a wuss_colourmenu (see * saturn_create -- the two leaf items' submenu pointers are patched in there, - * once the colourmenus exist). */ -enum { SATURN_MENU_COLOURS = 0 }; + * once the colourmenus exist). "Size" hover-opens task->size_dialogue, a + * borrowed window built once in saturn_create and patched into + * g_saturn_menu_items[SATURN_MENU_SIZE].window there, the same way. */ +enum { SATURN_MENU_COLOURS = 0, SATURN_MENU_SIZE }; enum { SATURN_COLOURS_MENU_FOREGROUND = 0, SATURN_COLOURS_MENU_BACKGROUND }; +/* the size dialogue's icons, in creation order (see saturn_size_dialogue_create) */ +enum { SATURN_SIZE_ICON_LABEL = 0, SATURN_SIZE_ICON_SLIDER, + SATURN_SIZE_ICON_VALUE, SATURN_SIZE_ICON_CANCEL, + SATURN_SIZE_ICON_APPLY, SATURN_SIZE_NICONS }; + static wuss_menu_item_t g_saturn_colours_items[] = { { "Foreground", wuss_MENU_ITEM_BORROWED_SUBMENU, NULL, NULL, 0 }, @@ -86,7 +101,8 @@ static wuss_menu_t g_saturn_colours_menu = static wuss_menu_item_t g_saturn_menu_items[] = { - { "Colours", wuss_MENU_ITEM_NONE, &g_saturn_colours_menu, NULL, 0 } + { "Colours", wuss_MENU_ITEM_NONE, &g_saturn_colours_menu, NULL, 0 }, + { "Size", wuss_MENU_ITEM_BORROWED_SUBMENU, NULL, NULL, 0 } }; static wuss_menu_t g_saturn_menu = @@ -94,6 +110,8 @@ static wuss_menu_t g_saturn_menu = "Saturn", g_saturn_menu_items, NELEMS(g_saturn_menu_items) }; +static result_t saturn_size_dialogue_create(saturn_task_t *task); + result_t saturn_create(wuss_t *wuss, saturn_task_t *task, const saturn_config_t *config) @@ -108,9 +126,14 @@ result_t saturn_create(wuss_t *wuss, task->fg = colour_rgb(0xFF, 0xFF, 0xFF); task->seed = 1; task->config = (config != NULL) ? *config : default_config; - task->fg_colourmenu = NULL; - task->bg_colourmenu = NULL; - task->menu_handle = NULL; + task->fg_colourmenu = NULL; + task->bg_colourmenu = NULL; + task->menu_handle = NULL; + task->size_dialogue = NULL; + task->size_slider = NULL; + task->size_value_label = NULL; + task->size_cancel = NULL; + task->size_apply = NULL; /* saturn_redraw paints its own background */ delegate_desc.handle = saturn_handle; @@ -146,11 +169,11 @@ result_t saturn_create(wuss_t *wuss, wuss_colourmenu_menu(task->bg_colourmenu); rc = wuss_window_create_placed(delegate, - SIZE2D(SATURN_SIZE, SATURN_SIZE), + SIZE2D(task->config.size, task->config.size), "Saturn", wuss_WINDOW_DEFAULT, wuss_NO_BACKDROP, - SIZE2D(SATURN_SIZE, SATURN_SIZE), + SIZE2D(task->config.size, task->config.size), SIZE2D(0, 0), &task->window); if (rc != result_OK) @@ -158,9 +181,20 @@ result_t saturn_create(wuss_t *wuss, wuss_colourmenu_destroy(task->bg_colourmenu); wuss_colourmenu_destroy(task->fg_colourmenu); wuss_task_destroy(delegate); /* unregister; its QUIT frees the task block */ + return rc; + } + + rc = saturn_size_dialogue_create(task); + if (rc != result_OK) + { + wuss_colourmenu_destroy(task->bg_colourmenu); + wuss_colourmenu_destroy(task->fg_colourmenu); + wuss_task_destroy(delegate); /* closes task->window too; QUIT frees the block */ + return rc; } + g_saturn_menu_items[SATURN_MENU_SIZE].window = task->size_dialogue; - return rc; + return result_OK; } /* plot one point in window content space, clipped to the window. x,y are @@ -251,7 +285,7 @@ static result_t saturn_mouse(saturn_task_t *task, wuss_button_t button, wuss_window_t *window) { - if (action != wuss_MOUSE_DOWN) + if (action != wuss_MOUSE_DOWN || window != task->window) return result_OK; if (button & wuss_BUTTON_MENU) @@ -268,8 +302,174 @@ static result_t saturn_mouse(saturn_task_t *task, return result_OK; } -/* A pick from either colour submenu: resolve against whichever colourmenu it - * came from and store into the matching field. */ +/* nearest multiple of SATURN_SIZE_STEP to v, clamped to + * [SATURN_SIZE_MIN,SATURN_SIZE_MAX] */ +static int saturn_size_snap(int v) +{ + v = ((v - SATURN_SIZE_MIN + SATURN_SIZE_STEP / 2) / SATURN_SIZE_STEP) * + SATURN_SIZE_STEP + SATURN_SIZE_MIN; + + return CLAMP(v, SATURN_SIZE_MIN, SATURN_SIZE_MAX); +} + +/* Build the size dialogue once: a label, a slider snapped to + * SATURN_SIZE_STEP, a label echoing the slider's current value, and + * Cancel/Apply buttons. Created hidden -- wuss shows and hides it itself, + * as a menu leaf's borrowed window (see g_saturn_menu_items[SATURN_MENU_SIZE] + * and wuss_menu_item_t::window), so it must outlive the open menu chain and + * is never closed here, only hidden. */ +static result_t saturn_size_dialogue_create(saturn_task_t *task) +{ + wuss_icon_spec_t specs[SATURN_SIZE_NICONS]; + wuss_icon_t *made[SATURN_SIZE_NICONS]; + wuss_icon_spec_t *s; + char buf[16]; + result_t rc; + + rc = wuss_window_create_placed(task->delegate, + SIZE2D(160, 112), + "Size", + wuss_WINDOW_HIDDEN | wuss_WINDOW_NO_CLOSE | + wuss_WINDOW_NO_BACK | + wuss_WINDOW_NO_TOGGLE_SIZE | + wuss_WINDOW_NO_VSCROLL | + wuss_WINDOW_NO_HSCROLL | + wuss_WINDOW_NO_RESIZE, + wuss_BACKDROP_COLOUR(wuss_COLOUR_WINDOW), + SIZE2D(160, 112), + SIZE2D(160, 112), + &task->size_dialogue); + if (rc != result_OK) + return rc; + + memset(specs, 0, sizeof(specs)); + + s = &specs[SATURN_SIZE_ICON_LABEL]; + s->bbox = (box_t) BOX_POS_SIZE(12, 10, 60, 16); + s->type = wuss_ICON_TYPE_LABEL; + s->text = "Size"; + s->fg = wuss_COLOUR_BLACK; + s->bg = wuss_NO_BACKGROUND; + + s = &specs[SATURN_SIZE_ICON_SLIDER]; + s->bbox = (box_t) BOX_POS_SIZE(12, 30, 136, 20); + s->type = wuss_ICON_TYPE_SLIDER; + s->fg = wuss_COLOUR_BLACK; + s->bg = wuss_NO_BACKGROUND; + s->u.slider.orientation = wuss_SLIDER_HORIZONTAL; + s->u.slider.min = SATURN_SIZE_MIN; + s->u.slider.max = SATURN_SIZE_MAX; + s->u.slider.default_value = saturn_size_snap(task->config.size); + + snprintf(buf, sizeof(buf), "%d", s->u.slider.default_value); + s = &specs[SATURN_SIZE_ICON_VALUE]; + s->bbox = (box_t) BOX_POS_SIZE(12, 56, 60, 16); + s->type = wuss_ICON_TYPE_LABEL; + s->text = buf; /* copied by wuss_icon_create_array */ + s->fg = wuss_COLOUR_BLACK; + s->bg = wuss_NO_BACKGROUND; + + s = &specs[SATURN_SIZE_ICON_CANCEL]; + s->bbox = (box_t) BOX_POS_SIZE(12, 74, 60, 22); + s->type = wuss_ICON_TYPE_ACTION; + s->text = "Cancel"; + s->fg = wuss_COLOUR_BLACK; + s->bg = wuss_COLOUR_WINDOW; + + s = &specs[SATURN_SIZE_ICON_APPLY]; + s->bbox = (box_t) BOX_POS_SIZE(88, 74, 60, 34); + s->type = wuss_ICON_TYPE_ACTION; + s->text = "Apply"; + s->fg = wuss_COLOUR_BLACK; + s->bg = wuss_COLOUR_WINDOW; + s->flags = wuss_ICON_FLAGS_DEFAULT; + + rc = wuss_icon_create_array(task->size_dialogue, specs, SATURN_SIZE_NICONS, + made); + if (rc != result_OK) + { + wuss_window_close(task->size_dialogue); /* not yet a menu leaf: safe to close */ + task->size_dialogue = NULL; + return rc; + } + + task->size_slider = made[SATURN_SIZE_ICON_SLIDER]; + task->size_value_label = made[SATURN_SIZE_ICON_VALUE]; + task->size_cancel = made[SATURN_SIZE_ICON_CANCEL]; + task->size_apply = made[SATURN_SIZE_ICON_APPLY]; + + return result_OK; +} + +/* wuss_EVENT_ICON on the size dialogue: slider drag updates the echo label + * (snapped to SATURN_SIZE_STEP); Cancel just dismisses the menu chain; + * Apply resizes the planet window to the chosen size, stores it as the new + * default and dismisses. Dismissing goes through wuss_menu_close rather + * than touching the (borrowed, reused) window directly -- that is what + * hides it, same as a click outside the chain would. */ +static result_t saturn_size_icon(saturn_task_t *task, + const wuss_event_t *event) +{ + wuss_icon_t *icon; + int value; + char buf[16]; + + icon = event->data.icon.icon; + + if (icon == task->size_slider) + { + if (event->data.icon.action != wuss_MOUSE_DOWN && + event->data.icon.action != wuss_MOUSE_MOVE) + return result_OK; + + value = saturn_size_snap(event->data.icon.value); + wuss_icon_set_value(task->size_dialogue, task->size_slider, value); + snprintf(buf, sizeof(buf), "%d", value); + return wuss_icon_set_text(task->size_dialogue, task->size_value_label, + buf); + } + + if (event->data.icon.action != wuss_MOUSE_DOWN) + return result_OK; + + if (icon == task->size_cancel) + { + wuss_menu_close(task->menu_handle); + task->menu_handle = NULL; + return result_OK; + } + + if (icon == task->size_apply) + { + value = wuss_icon_get_value(task->size_slider); + task->config.size = value; + wuss_menu_close(task->menu_handle); + task->menu_handle = NULL; + return wuss_window_resize(task->window, SIZE2D(value, value)); + } + + return result_OK; +} + +/* wuss_EVENT_PRE_SHOW on the size dialogue: resync the slider and its echo + * label to task->config.size, in case Apply (or a config passed to + * saturn_create) changed it since the dialogue was last shown. Always + * allows the show. */ +static result_t saturn_size_pre_show(saturn_task_t *task) +{ + int value; + char buf[16]; + + value = saturn_size_snap(task->config.size); + wuss_icon_set_value(task->size_dialogue, task->size_slider, value); + snprintf(buf, sizeof(buf), "%d", value); + + return wuss_icon_set_text(task->size_dialogue, task->size_value_label, buf); +} + +/* A pick from either colour submenu, resolved against whichever colourmenu + * it came from. "Size" is a wuss_menu_item_t::window leaf, not a leaf pick, + * so it never reaches here -- see saturn_size_icon and saturn_size_pre_show. */ static result_t saturn_menu_select(saturn_task_t *task, const wuss_event_t *event) { @@ -311,12 +511,26 @@ result_t saturn_handle(wuss_window_t *window, switch (event->kind) { case wuss_EVENT_REDRAW: + /* the size dialogue has no content of its own -- just its icons, which + * wuss already drew */ + if (window == task->size_dialogue) + return result_OK; return saturn_redraw(event, task); case wuss_EVENT_MOUSE: return saturn_mouse(task, event->data.mouse.action, event->data.mouse.button, window); + case wuss_EVENT_ICON: + if (window == task->size_dialogue) + return saturn_size_icon(task, event); + return result_OK; + + case wuss_EVENT_PRE_SHOW: + if (window == task->size_dialogue) + return saturn_size_pre_show(task); + return result_OK; + case wuss_EVENT_MENU_SELECT: return saturn_menu_select(task, event); diff --git a/libraries/wuss/test/tasks/saturn.h b/libraries/wuss/test/tasks/saturn.h index 8d15209a..203a82da 100644 --- a/libraries/wuss/test/tasks/saturn.h +++ b/libraries/wuss/test/tasks/saturn.h @@ -9,6 +9,7 @@ * by default, so the wuss app always has the colourmenu component. */ #include "framebuf/colour.h" #include "wuss/component/colourmenu.h" +#include "wuss/icon.h" #include "wuss/menu.h" #include "wuss/task.h" #include "wuss/window.h" @@ -20,17 +21,24 @@ * planet body. Select re-seeds the RNG for a fresh sketch; the idle * handler re-seeds every null event so it churns. The plot is * deterministic in the seed. */ -/* iteration counts for the three rejection-sampling loops; saturn_create +/* iteration counts for the three rejection-sampling loops, plus the window's + * size, in document pixels (the window is always square); saturn_create * fills in SATURN_CONFIG_DEFAULT values when the caller passes NULL */ typedef struct saturn_config { int ring_iters; /* loop 1: the ring */ int band_iters; /* loop 2: ring shadow band */ int body_iters; /* loop 3: planet body */ + int size; /* window size (both axes); set via the Size... dialogue */ } saturn_config_t; -#define SATURN_CONFIG_DEFAULT { 477, 1280, 1280 } +#define SATURN_CONFIG_DEFAULT { 477, 1280, 1280, 256 } + +/* the "Size..." dialogue's slider: [min,max] and the step it snaps to */ +#define SATURN_SIZE_MIN 128 +#define SATURN_SIZE_MAX 512 +#define SATURN_SIZE_STEP 64 typedef struct saturn_task { @@ -42,6 +50,11 @@ typedef struct saturn_task saturn_config_t config; wuss_colourmenu_t *fg_colourmenu, *bg_colourmenu; wuss_menu_handle_t menu_handle; /* live only between open and a SELECT pick */ + wuss_window_t *size_dialogue; /* built once, hidden; a menu leaf */ + wuss_icon_t *size_slider; + wuss_icon_t *size_value_label; + wuss_icon_t *size_cancel; + wuss_icon_t *size_apply; } saturn_task_t; From 75216abf7c6c19aa7655ef7fe0a0a8c6603dde5c Mon Sep 17 00:00:00 2001 From: David Thomas Date: Sun, 13 Sep 2026 23:49:53 +0100 Subject: [PATCH 062/128] fix(wuss): saturn Size dialogue buttons act on Select release Cancel/Apply now fire on wuss_MOUSE_UP with wuss_BUTTON_SELECT, not MOUSE_DOWN: a press that drags off the button before release no longer commits, and an Adjust click leaves the dialogue open per the RISC OS Adjust-doesn't-dismiss convention. --- libraries/wuss/test/tasks/saturn.c | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/libraries/wuss/test/tasks/saturn.c b/libraries/wuss/test/tasks/saturn.c index d740c5fc..f61cab95 100644 --- a/libraries/wuss/test/tasks/saturn.c +++ b/libraries/wuss/test/tasks/saturn.c @@ -402,11 +402,14 @@ static result_t saturn_size_dialogue_create(saturn_task_t *task) } /* wuss_EVENT_ICON on the size dialogue: slider drag updates the echo label - * (snapped to SATURN_SIZE_STEP); Cancel just dismisses the menu chain; - * Apply resizes the planet window to the chosen size, stores it as the new - * default and dismisses. Dismissing goes through wuss_menu_close rather - * than touching the (borrowed, reused) window directly -- that is what - * hides it, same as a click outside the chain would. */ + * (snapped to SATURN_SIZE_STEP) live on DOWN/MOVE; Cancel/Apply act on a + * Select UP, so a press that drags off the button before release is not + * taken as a click, and an Adjust click leaves the dialogue open (RISC OS + * "Adjust doesn't dismiss" convention). Cancel just dismisses the menu + * chain; Apply resizes the planet window to the chosen size, stores it as + * the new default and dismisses. Dismissing goes through wuss_menu_close + * rather than touching the (borrowed, reused) window directly -- that is + * what hides it, same as a click outside the chain would. */ static result_t saturn_size_icon(saturn_task_t *task, const wuss_event_t *event) { @@ -429,7 +432,8 @@ static result_t saturn_size_icon(saturn_task_t *task, buf); } - if (event->data.icon.action != wuss_MOUSE_DOWN) + if (event->data.icon.action != wuss_MOUSE_UP || + !(event->data.icon.button & wuss_BUTTON_SELECT)) return result_OK; if (icon == task->size_cancel) From 3c35d43c12d3149699fd120638968f4f21c4a712 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Mon, 14 Sep 2026 00:17:05 +0100 Subject: [PATCH 063/128] feat(geom): add stack module for box-stack layout Nested HBox/VBox solver with flex weights, cross-axis alignment, per-item min/max clamps and container padding/gap. Implements the design in docs/windowing/stack-sketch.md. Co-Authored-By: Claude Sonnet 5 --- CMakeLists.txt | 7 +- apps/test/main.c | 1 + docs/windowing/stack-sketch.md | 294 +++++++++++++++++++++++++ include/base/result.h | 1 + include/geom/stack.h | 93 ++++++++ include/test/all-tests.h | 3 +- libraries/geom/stack/impl.h | 18 ++ libraries/geom/stack/solve.c | 240 ++++++++++++++++++++ libraries/geom/stack/test/stack-test.c | 116 ++++++++++ libraries/geom/stack/valid-tree.c | 24 ++ 10 files changed, 795 insertions(+), 2 deletions(-) create mode 100644 docs/windowing/stack-sketch.md create mode 100644 include/geom/stack.h create mode 100644 libraries/geom/stack/impl.h create mode 100644 libraries/geom/stack/solve.c create mode 100644 libraries/geom/stack/test/stack-test.c create mode 100644 libraries/geom/stack/valid-tree.c diff --git a/CMakeLists.txt b/CMakeLists.txt index f5f9c2f1..080b60c4 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -104,6 +104,7 @@ set(PUBLIC_HEADERS include/geom/packer.h include/geom/point.h include/geom/size.h + include/geom/stack.h include/io/dirscan.h include/io/namelist.h include/io/path.h @@ -310,7 +311,10 @@ set(GEOM_SOURCES libraries/geom/layout/layout.c libraries/geom/line/line.c libraries/geom/packer/impl.h - libraries/geom/packer/packer.c) + libraries/geom/packer/packer.c + libraries/geom/stack/impl.h + libraries/geom/stack/solve.c + libraries/geom/stack/valid-tree.c) set(IO_SOURCES libraries/io/dirscan/dirscan.c @@ -687,6 +691,7 @@ if(BUILD_TESTS) libraries/geom/box/test/box-test.c libraries/geom/layout/test/layout-test.c libraries/geom/packer/test/packer-test.c + libraries/geom/stack/test/stack-test.c libraries/io/stream/test/stream-test.c libraries/text/txtfmt/test/txtfmt-test.c libraries/utils/array/test/array-test.c diff --git a/apps/test/main.c b/apps/test/main.c index 8438fc20..babfe8ed 100644 --- a/apps/test/main.c +++ b/apps/test/main.c @@ -61,6 +61,7 @@ static const test_t tests[] = { "box", box_test }, { "layout", layout_test }, { "packer", packer_test }, + { "stack", stack_test }, { "stream", stream_test }, diff --git a/docs/windowing/stack-sketch.md b/docs/windowing/stack-sketch.md new file mode 100644 index 00000000..a66e0383 --- /dev/null +++ b/docs/windowing/stack-sketch.md @@ -0,0 +1,294 @@ +# Box-stack layout (design sketch) + +Design notes for the `stack` module. **Implemented** as `include/geom/stack.h` +/ `libraries/geom/stack/`; kept here as the design record. + +## Motivation + +Laying out the *contents* of a window: icons, labels, buttons and fields +arranged into a dialog. This is client-side layout — the stuff a window's +owner draws inside the work area — not window furniture. `wuss`'s own +furniture (titlebar, close and toggle buttons, scrollbars) stays as the +per-item formulas in `libraries/wuss/furniture/*.c` and is out of scope for +this module. + +Without something like this, every dialog hand-codes an inset chain per icon: +readable for three icons, a maintenance sink for thirty, and impossible to +reflow when the window resizes or the font metrics change. A box stack turns a +dialog into a nested description that the solver turns into boxes. + +Not built yet. Build it when the first real dialog needs it. + +## Relationship to `geom/layout` + +There is already a `geom/layout.h` (`layout_place`). It is a **line-flow** +layout: an element list of boxes and `NEWLINE`s, placed by `geom/packer`, +wrapping like text. One `spacing` and one `leading` scalar for the whole spec, +no nesting, no flex. + +This sketch is a different tool — a **box-stack solver**: nested HBox/VBox, +flex weights, cross-axis alignment, per-edge padding. The Monitor dialog below +(aligned label column, nested group box, spread button row) is not expressible +as line-flow. Hence a separate module with a separate name; `layout_place` +stays as-is for flowing text runs. + +## Scope + +In: + +- Nested horizontal and vertical stacks. +- Per-item fixed size, or a flex weight for sharing leftover space. +- Per-item main-axis `min` / `max` clamps. +- Cross-axis alignment (start, centre, end, fill). +- Spacers (a flex item with no output consumer). +- Container inner padding (per edge) and inter-child gap. +- Integer geometry throughout, remainder pixels distributed so child extents + sum exactly to the parent. + +Out (add only when a real layout needs it, each its own follow-up): + +- Shared column widths across independent rows (a label column where every + field starts at the same x). Nested stacks solve each row alone, so the + caller pre-measures: loop over the labels, find the widest, set every label + leaf to that fixed `size`. It is a handful of lines at the call site and + needs nothing from the module. If callers keep writing that loop, the clean + fix is a `GRID` kind with a max-content track — real code, deferred until a + caller exists. +- Grid with row/column spans and per-track sizing. Uniform grids are + expressible as a stack of stacks; true 2D alignment across rows is not, and + that is the trigger to add a real `GRID` kind. +- Linear constraints (`a.right == b.left + 8` solved by Cassowary-style + simplex). Only if a layout genuinely cannot be nested into stacks. +- A retained node tree the caller mutates and re-solves. `wuss` already owns + window state and dirty-region redraw; a second tree to keep in sync is not + worth it. +- A `GROUP` kind that draws its own border and caption. A group box is a + `VBOX` with a wider top `pad` for the caption; the border and caption text + are draw-time work for the caller. The module computes rectangles, not + pixels — no strings, no drawing. +- Chained cross-axis flex (a leaf's cross size driving a sibling's). + +## Model + +One-shot. The caller fills a flat array describing the tree, calls the solver, +and reads a parallel array of boxes. No allocation, no tree pointers, no +retained state. Rebuild the array each resize (or each frame) and solve again; +the solve is cheap. + +The tree is expressed by parent index. `items[0]` is the root. Every other +item names its parent, which must appear earlier in the array (parents before +children). A container's children are every item naming it as parent, in array +order — that order is the stacking order. + +```c +typedef enum stack_kind +{ + stack_KIND_HBOX, /* stack children left to right */ + stack_KIND_VBOX, /* stack children top to bottom */ + stack_KIND_SPACER, /* flexible gap; no output box read by anyone */ + stack_KIND_LEAF /* a thing the caller will position */ +} +stack_kind_t; + +typedef enum stack_align +{ + stack_ALIGN_START, /* against the low cross-axis edge */ + stack_ALIGN_CENTRE, /* centred on the cross axis */ + stack_ALIGN_END, /* against the high cross-axis edge */ + stack_ALIGN_FILL /* span the container's cross extent */ +} +stack_align_t; + +typedef struct stack_item +{ + stack_kind_t kind; + int parent; /* index of the containing item; -1 for the root */ + + int size; /* fixed main-axis extent in px; 0 means "use flex" */ + int flex; /* weight for sharing leftover main-axis space; 0 means "do not grow" */ + int min; /* main-axis lower clamp in px; 0 means none */ + int max; /* main-axis upper clamp in px; 0 means unbounded */ + + stack_align_t align; /* cross-axis placement of this item within its container */ + + int gap; /* containers only: px between adjacent children */ + int pad_l; /* containers only: inner inset, left edge */ + int pad_t; /* containers only: inner inset, top edge */ + int pad_r; /* containers only: inner inset, right edge */ + int pad_b; /* containers only: inner inset, bottom edge */ +} +stack_item_t; + +/* Solve `items[0..n)` into `out[0..n)`. `out[i]` is the computed box for + * `items[i]`, in the same coordinate space as `root`. `root` is the area the + * root item is laid into. Returns result_OK, or result_STACK_BAD_TREE if the + * parent indices are not a valid parents-before-children forest rooted at 0. */ +result_t stack_solve(const stack_item_t *items, + int n, + const box_t *root, + box_t *out); +``` + +`SPACER` gets an `out` box like any item; nothing is expected to read it. It +exists so "push the rest to the right" is `{ .kind = stack_KIND_SPACER, +.flex = 1 }` rather than a special case in the solver. + +## Algorithm + +Recurse over containers, root first. For each container, lay out its direct +children along its main axis; the container's own box is already known (the +root's is `root` inset by its four `pad_*`; a child container's was set when +its parent was processed). + +Let `avail` be the container's main-axis extent, minus the two `pad_*` on that +axis, minus `gap * (childcount - 1)`. + +**Pass 1 — fixed and minimum.** For each child, take its base main extent: +`size` if non-zero, else `min` if non-zero, else 0. Sum these into `used`. + +**Pass 2 — distribute the remainder.** `slack = avail - used`. + +- If `slack > 0`: share it across children with `flex > 0` in proportion to + their weights. Integer division leaves a remainder of at most + `flexchildren - 1` px; hand those out one per child, earliest first, so the + children's extents sum to exactly `avail`. Apply each child's `max` clamp; px + removed by a clamp go back into `slack` for another distribution pass over + the still-unclamped flex children (at most a few passes; stop when no child + clamps). +- If `slack < 0` (children's minimums overflow the container): the container + is smaller than its contents. Do not shrink below `min`. Let children + overflow the high edge. `min` is a hard floor; a caller that wants + shrink-to-fit sets `min` to 0. +- If `slack == 0`: nothing to distribute. + +**Placement.** Walk the children along the main axis from the container's low +edge plus the leading `pad_*` for that axis, advancing by each child's resolved +extent plus `gap`. On the cross axis, place each child within the container +inset by the two cross-axis `pad_*`, per its `align`: `START` at the low edge, +`END` at the high edge, `CENTRE` at `(container_cross - child_cross) / 2` +rounding down, `FILL` sets the child's cross extent to the whole span. A +non-`FILL` child's cross extent is its `size` if the cross axis has one to give +— otherwise `FILL` is the sensible default and a caller wanting an intrinsic +cross size supplies it out of band. + +Write each child's `out` box, then recurse into it if it is a container. + +## Worked example — the Monitor configuration dialog + +The RISC OS dialog it is modelled on: + +``` ++------------------------------------------------+ +| Driver [ Vpod ][v] | +| Monitor [ Samsung 23" (SAM0473) ][v] | +| | +| +- Monitor options --------------------------+ | +| | [ ] Use mode definition file | | +| | Monitor type [ Samsung SyncMaster ] | | +| | Colours [ 64 thousand ][v] | | +| | Resolution [ 1680 x 1050 ][v] | | +| | DPMS level [ Monitor standby ][v] | | +| +--------------------------------------------+ | +| | +| [ Refresh ] [ Cancel ] [ Set ] | ++------------------------------------------------+ +``` + +Structure: + +- The root is a `VBOX`: two label/field rows, the group box, then the button + row. +- **Aligned label column.** "Driver", "Monitor", "Monitor type", "Colours", + "Resolution", "DPMS level" all right-align to one column so the fields start + at a common x. The caller measures the six label strings, takes the widest as + `LW`, and sets every label leaf to `.size = LW, .align = stack_ALIGN_END`. + Rows solved independently still line up because the caller fed them one width. +- **Each row** is an `HBOX`: label leaf (`size = LW`), field leaf (`flex = 1`), + and — only on rows that have one — a fixed-size pop-up icon. A row without the + pop-up just lets the field run to the edge; alignment holds either way. +- **Group box** is a `VBOX` with `.pad_t` big enough for the caption and + border, `.pad_l/r/b` the border inset. The caller draws the frame and the + "Monitor options" text; the module only insets the children. +- **Button row** is an `HBOX`: `Refresh`, a `SPACER`, `Cancel`, a `SPACER`, + `Set`. Two spacers spread the three buttons the way the dialog does. + +Sketched as an array (row children elided after the first for length; `RH` row +height, `PU` pop-up width, `BW`/`BH` button size, `M`/`G` margin and gap, +`CAP` caption height): + +```c +enum +{ + ROOT, + DRV_ROW, DRV_LBL, DRV_FLD, DRV_PU, + MON_ROW, MON_LBL, MON_FLD, MON_PU, + GROUP, + USE_ROW, USE_OPT, /* option (check) icon + its label */ + TYP_ROW, TYP_LBL, TYP_FLD, /* no pop-up on this row */ + COL_ROW, COL_LBL, COL_FLD, COL_PU, + RES_ROW, RES_LBL, RES_FLD, RES_PU, + DPM_ROW, DPM_LBL, DPM_FLD, DPM_PU, + BTN_ROW, B_REFRESH, GAP1, B_CANCEL, GAP2, B_SET, + N_ITEMS +}; + +stack_item_t items[N_ITEMS] = +{ + [ROOT] = { .kind = stack_KIND_VBOX, .parent = -1, + .pad_l = M, .pad_t = M, .pad_r = M, .pad_b = M, .gap = G }, + + /* --- Driver row --- */ + [DRV_ROW] = { .kind = stack_KIND_HBOX, .parent = ROOT, .size = RH, .gap = G, .align = stack_ALIGN_FILL }, + [DRV_LBL] = { .kind = stack_KIND_LEAF, .parent = DRV_ROW, .size = LW, .align = stack_ALIGN_END }, + [DRV_FLD] = { .kind = stack_KIND_LEAF, .parent = DRV_ROW, .flex = 1, .align = stack_ALIGN_FILL }, + [DRV_PU] = { .kind = stack_KIND_LEAF, .parent = DRV_ROW, .size = PU, .align = stack_ALIGN_CENTRE }, + /* Monitor row: same shape, parent = ROOT */ + + /* --- Monitor options group --- */ + [GROUP] = { .kind = stack_KIND_VBOX, .parent = ROOT, .flex = 1, + .pad_l = M, .pad_t = CAP, .pad_r = M, .pad_b = M, .gap = G, .align = stack_ALIGN_FILL }, + /* USE_ROW .. DPM_ROW: HBOX rows, parent = GROUP. + * TYP_ROW carries no *_PU child; its field runs to the group's right pad. */ + + /* --- Button row --- */ + [BTN_ROW] = { .kind = stack_KIND_HBOX, .parent = ROOT, .size = BH, .gap = G, .align = stack_ALIGN_FILL }, + [B_REFRESH]= { .kind = stack_KIND_LEAF, .parent = BTN_ROW, .size = BW, .align = stack_ALIGN_FILL }, + [GAP1] = { .kind = stack_KIND_SPACER, .parent = BTN_ROW, .flex = 1 }, + [B_CANCEL] = { .kind = stack_KIND_LEAF, .parent = BTN_ROW, .size = BW, .align = stack_ALIGN_FILL }, + [GAP2] = { .kind = stack_KIND_SPACER, .parent = BTN_ROW, .flex = 1 }, + [B_SET] = { .kind = stack_KIND_LEAF, .parent = BTN_ROW, .size = BW, .align = stack_ALIGN_FILL }, +}; + +box_t out[N_ITEMS]; + +stack_solve(items, N_ITEMS, &work, out); +/* Every *_FLD box starts at work.x0 + M + LW + G, so the fields line up down + * the whole dialog including the group. Resize the window and the fields + * stretch; the labels, pop-ups and buttons keep their fixed widths. */ +``` + +The hand-coded equivalent recomputes an inset chain per icon and repeats the +label-column arithmetic in every row. The array form measures the labels once +and reflows the rest for free. + +## Notes for the implementer + +- Follows the module split: `include/geom/stack.h` for the API, + `libraries/geom/stack/` for the solver, one function per file where it reads + naturally. Add sources by hand to `CMakeLists.txt`. Sits next to + `geom/layout` (line-flow) and `geom/packer`. +- No dependency on `wuss` or SDL. `geom` (`box_t`, `point_t`, `size2d_t`) and + `base/result.h` only. Testable as a plain core test: build an array, solve, + assert boxes. +- Reserve a `result_BASE_STACK` block in `include/base/result.h` (0x0C00 is the + next free base after `result_BASE_WUSS`); the only code needed at first is + `result_STACK_BAD_TREE` (parent indices not a valid forest). +- Integer only. The rounding rule (remainder px to earliest children) is the + one behaviour a test must pin: three flex-1 children in a 100px box get + 34 / 33 / 33, and the boxes abut with no gap or overlap. A second test pins + the aligned-label pattern: two rows, label leaves both `size = LW`, assert + the two field boxes share an `x0`. +- `wuss` need not depend on this. A window's owner calls `stack_solve` in its + own redraw code and positions its icons from `out[]`; the module sits + alongside `wuss`, not inside it. +``` diff --git a/include/base/result.h b/include/base/result.h index 6515b495..5fc96d06 100644 --- a/include/base/result.h +++ b/include/base/result.h @@ -23,6 +23,7 @@ typedef int result_t; #define result_BASE_LAYOUT 0x0900 #define result_BASE_BITFIFO 0x0A00 #define result_BASE_WUSS 0x0B00 +#define result_BASE_STACK 0x0C00 /* Non-DPTLib bases */ #define result_BASE_MMPLAYER 0x4000 diff --git a/include/geom/stack.h b/include/geom/stack.h new file mode 100644 index 00000000..d971c699 --- /dev/null +++ b/include/geom/stack.h @@ -0,0 +1,93 @@ +/* geom/stack.h -- box-stack layout solver */ + +#ifndef GEOM_STACK_H +#define GEOM_STACK_H + +#ifdef __cplusplus +extern "C" +{ +#endif + +#include "base/result.h" +#include "geom/box.h" + +/* ----------------------------------------------------------------------- */ + +#define result_STACK_BAD_TREE (result_BASE_STACK + 0) + +/* ----------------------------------------------------------------------- */ + +/** The kind of a stack item. */ +typedef enum stack_kind +{ + stack_KIND_HBOX, /**< Stack children left to right. */ + stack_KIND_VBOX, /**< Stack children top to bottom. */ + stack_KIND_SPACER, /**< Flexible gap; no output box read by anyone. */ + stack_KIND_LEAF /**< A thing the caller will position. */ +} +stack_kind_t; + +/** Cross-axis alignment of a stack item within its container. */ +typedef enum stack_align +{ + stack_ALIGN_START, /**< Against the low cross-axis edge. */ + stack_ALIGN_CENTRE, /**< Centred on the cross axis. */ + stack_ALIGN_END, /**< Against the high cross-axis edge. */ + stack_ALIGN_FILL /**< Span the container's cross extent. */ +} +stack_align_t; + +/** One node in a box-stack tree. */ +typedef struct stack_item +{ + stack_kind_t kind; + int parent; /**< Index of the containing item; -1 for the + root. */ + + int size; /**< Fixed main-axis extent in px; 0 means "use + flex". */ + int flex; /**< Weight for sharing leftover main-axis space; + 0 means "do not grow". */ + int min; /**< Main-axis lower clamp in px; 0 means none. */ + int max; /**< Main-axis upper clamp in px; 0 means + unbounded. */ + + stack_align_t align; /**< Cross-axis placement of this item within its + container. */ + + int gap; /**< Containers only: px between adjacent + children. */ + int pad_l; /**< Containers only: inner inset, left edge. */ + int pad_t; /**< Containers only: inner inset, top edge. */ + int pad_r; /**< Containers only: inner inset, right edge. */ + int pad_b; /**< Containers only: inner inset, bottom edge. */ +} +stack_item_t; + +/** + * Solve a box-stack tree into boxes. + * + * `items[0]` is the root. Every other item names its parent by index, which + * must appear earlier in the array (parents before children). A container's + * children are every item naming it as parent, in array order; that order is + * the stacking order. + * + * \param[in] items Array of stack items describing the tree. + * \param[in] n Number of items in the array. + * \param[in] root The area the root item is laid into. + * \param[out] out Array of `n` boxes; `out[i]` receives the computed box + * for `items[i]`, in the same coordinate space as `root`. + * \return \ref result_OK on success, result_STACK_BAD_TREE if the parent + * indices are not a valid parents-before-children forest rooted at + * 0. + */ +result_t stack_solve(const stack_item_t *items, + int n, + const box_t *root, + box_t *out); + +#ifdef __cplusplus +} +#endif + +#endif /* GEOM_STACK_H */ diff --git a/include/test/all-tests.h b/include/test/all-tests.h index 91612cbc..a5aefddc 100644 --- a/include/test/all-tests.h +++ b/include/test/all-tests.h @@ -33,7 +33,8 @@ extern testfn_t bitmap_rle_test, /* geom */ extern testfn_t box_test, layout_test, - packer_test; + packer_test, + stack_test; /* io */ extern testfn_t stream_test; diff --git a/libraries/geom/stack/impl.h b/libraries/geom/stack/impl.h new file mode 100644 index 00000000..22a7f4f3 --- /dev/null +++ b/libraries/geom/stack/impl.h @@ -0,0 +1,18 @@ +/* geom/stack/impl.h -- box-stack layout solver */ + +#ifndef IMPL_H +#define IMPL_H + +#include "geom/stack.h" + +/** + * Affirms if `items[0..n)` form a valid parents-before-children forest + * rooted at index 0. + * + * \param[in] items Array of stack items. + * \param[in] n Number of items in the array. + * \return Non-zero if the tree is valid. + */ +int stack__valid_tree(const stack_item_t *items, int n); + +#endif /* IMPL_H */ diff --git a/libraries/geom/stack/solve.c b/libraries/geom/stack/solve.c new file mode 100644 index 00000000..69d711a8 --- /dev/null +++ b/libraries/geom/stack/solve.c @@ -0,0 +1,240 @@ +/* geom/stack/solve.c -- box-stack layout solver */ + +#include + +#include "base/utils.h" +#include "geom/box.h" + +#include "geom/stack.h" + +#include "impl.h" + +/* ----------------------------------------------------------------------- */ + +/* A child's resolved main-axis extent, and the room left to grow it. */ +typedef struct stack__child +{ + int index; + int extent; + int clamped; /* non-zero once this child can no longer grow */ +} +stack__child_t; + +/* ----------------------------------------------------------------------- */ + +/* Distributes `avail` px of main-axis space across the children of + * `parent`, honouring `size`/`min`/`flex`/`max`, and returns the number of + * children found. `children[i].extent` holds each child's resolved + * main-axis extent on return. */ +static int stack__distribute(const stack_item_t *items, + int n, + int parent, + int avail, + stack__child_t *children) +{ + int nchildren; + int used; + int slack; + int i; + int round; + + nchildren = 0; + used = 0; + + for (i = 0; i < n; i++) + { + if (items[i].parent != parent) + continue; + + children[nchildren].index = i; + children[nchildren].extent = items[i].size ? items[i].size : items[i].min; + children[nchildren].clamped = (items[i].flex == 0); + + used += children[nchildren].extent; + nchildren++; + } + + slack = avail - used; + + /* Hand slack to flexible children in proportion to their weights, then + * the leftover px from integer division one per child, earliest first, + * so extents sum to exactly 'avail'. Clamping a child to its 'max' + * returns its excess to 'slack' for another such pass over the children + * still able to grow; bounded by 'nchildren' passes, as each pass + * clamps at least one more child or terminates. */ + for (round = 0; round < nchildren && slack > 0; round++) + { + int flexweight; + int given; + int c; + + flexweight = 0; + for (c = 0; c < nchildren; c++) + if (!children[c].clamped) + flexweight += items[children[c].index].flex; + + if (flexweight == 0) + break; + + given = 0; + for (c = 0; c < nchildren; c++) + { + int share; + + if (children[c].clamped) + continue; + + share = slack * items[children[c].index].flex / flexweight; + children[c].extent += share; + given += share; + } + + for (c = 0; c < nchildren && given < slack; c++) + { + if (children[c].clamped) + continue; + children[c].extent++; + given++; + } + + for (c = 0; c < nchildren; c++) + { + int item_max; + + if (children[c].clamped) + continue; + + item_max = items[children[c].index].max; + if (item_max > 0 && children[c].extent > item_max) + { + given -= children[c].extent - item_max; + children[c].extent = item_max; + children[c].clamped = 1; + } + } + + slack -= given; + } + + return nchildren; +} + +/* ----------------------------------------------------------------------- */ + +static void stack__place_container(const stack_item_t *items, + int n, + int index, + box_t *out) +{ + int horiz; + stack__child_t children[n]; /* upper bound: at most n-1 children */ + int nchildren; + const box_t *box; + int inner_main0, inner_main1; + int cross0, cross1; + int avail; + int pos; + int c; + + horiz = (items[index].kind == stack_KIND_HBOX); + box = &out[index]; + + if (horiz) + { + inner_main0 = box->x0 + items[index].pad_l; + inner_main1 = box->x1 - items[index].pad_r; + cross0 = box->y0 + items[index].pad_t; + cross1 = box->y1 - items[index].pad_b; + } + else + { + inner_main0 = box->y0 + items[index].pad_t; + inner_main1 = box->y1 - items[index].pad_b; + cross0 = box->x0 + items[index].pad_l; + cross1 = box->x1 - items[index].pad_r; + } + + nchildren = 0; + for (c = 0; c < n; c++) + if (items[c].parent == index) + nchildren++; + + avail = inner_main1 - inner_main0 - items[index].gap * MAX(0, nchildren - 1); + + nchildren = stack__distribute(items, n, index, avail, children); + + pos = inner_main0; + + for (c = 0; c < nchildren; c++) + { + int ci; + box_t *cbox; + int cross_extent; + int cross_pos; + + ci = children[c].index; + cbox = &out[ci]; + + if (c > 0) + pos += items[index].gap; + + switch (items[ci].align) + { + case stack_ALIGN_FILL: + cross_extent = cross1 - cross0; + cross_pos = cross0; + break; + case stack_ALIGN_CENTRE: + cross_extent = items[ci].size ? items[ci].size : (cross1 - cross0); + cross_pos = cross0 + (cross1 - cross0 - cross_extent) / 2; + break; + case stack_ALIGN_END: + cross_extent = items[ci].size ? items[ci].size : (cross1 - cross0); + cross_pos = cross1 - cross_extent; + break; + case stack_ALIGN_START: + default: + cross_extent = items[ci].size ? items[ci].size : (cross1 - cross0); + cross_pos = cross0; + break; + } + + if (horiz) + { + cbox->x0 = pos; + cbox->x1 = pos + children[c].extent; + cbox->y0 = cross_pos; + cbox->y1 = cross_pos + cross_extent; + } + else + { + cbox->y0 = pos; + cbox->y1 = pos + children[c].extent; + cbox->x0 = cross_pos; + cbox->x1 = cross_pos + cross_extent; + } + + pos += children[c].extent; + + if (items[ci].kind == stack_KIND_HBOX || items[ci].kind == stack_KIND_VBOX) + stack__place_container(items, n, ci, out); + } +} + +/* ----------------------------------------------------------------------- */ + +result_t stack_solve(const stack_item_t *items, + int n, + const box_t *root, + box_t *out) +{ + if (!stack__valid_tree(items, n)) + return result_STACK_BAD_TREE; + + out[0] = *root; + + if (items[0].kind == stack_KIND_HBOX || items[0].kind == stack_KIND_VBOX) + stack__place_container(items, n, 0, out); + + return result_OK; +} diff --git a/libraries/geom/stack/test/stack-test.c b/libraries/geom/stack/test/stack-test.c new file mode 100644 index 00000000..29693f06 --- /dev/null +++ b/libraries/geom/stack/test/stack-test.c @@ -0,0 +1,116 @@ +/* geom/stack/test/stack-test.c */ + +#include "base/utils.h" +#include "geom/box.h" + +#include "geom/stack.h" + +#include "test/all-tests.h" + +/* ----------------------------------------------------------------------- */ + +/* Three flex-1 children in a 100px box divide 34/33/33, abutting exactly. */ +static result_t test_flex_split(void) +{ + enum { ROOT, A, B, C, N }; + static const box_t root = { 0, 0, 100, 10 }; + static const stack_item_t items[N] = + { + [ROOT] = { .kind = stack_KIND_HBOX, .parent = -1 }, + [A] = { .kind = stack_KIND_LEAF, .parent = ROOT, .flex = 1 }, + [B] = { .kind = stack_KIND_LEAF, .parent = ROOT, .flex = 1 }, + [C] = { .kind = stack_KIND_LEAF, .parent = ROOT, .flex = 1 }, + }; + + result_t err; + box_t out[N]; + + err = stack_solve(items, N, &root, out); + if (err != result_OK) + return result_TEST_FAILED; + + if (out[A].x0 != 0 || out[A].x1 != 34) + return result_TEST_FAILED; + if (out[B].x0 != 34 || out[B].x1 != 67) + return result_TEST_FAILED; + if (out[C].x0 != 67 || out[C].x1 != 100) + return result_TEST_FAILED; + + return result_TEST_PASSED; +} + +/* Two independent rows with label leaves fed the same fixed width line up + * their field boxes at a common x0. */ +static result_t test_aligned_labels(void) +{ + enum { ROOT, ROW1, LBL1, FLD1, ROW2, LBL2, FLD2, N }; + static const box_t root = { 0, 0, 200, 40 }; + static const stack_item_t items[N] = + { + [ROOT] = { .kind = stack_KIND_VBOX, .parent = -1 }, + [ROW1] = { .kind = stack_KIND_HBOX, .parent = ROOT, .size = 20 }, + [LBL1] = { .kind = stack_KIND_LEAF, .parent = ROW1, .size = 40, .align = stack_ALIGN_END }, + [FLD1] = { .kind = stack_KIND_LEAF, .parent = ROW1, .flex = 1 }, + [ROW2] = { .kind = stack_KIND_HBOX, .parent = ROOT, .size = 20 }, + [LBL2] = { .kind = stack_KIND_LEAF, .parent = ROW2, .size = 40, .align = stack_ALIGN_END }, + [FLD2] = { .kind = stack_KIND_LEAF, .parent = ROW2, .flex = 1 }, + }; + + result_t err; + box_t out[N]; + + err = stack_solve(items, N, &root, out); + if (err != result_OK) + return result_TEST_FAILED; + + if (out[FLD1].x0 != out[FLD2].x0) + return result_TEST_FAILED; + if (out[FLD1].x0 != 40) + return result_TEST_FAILED; + + return result_TEST_PASSED; +} + +/* A parent index pointing forward (or at itself) is rejected. */ +static result_t test_bad_tree(void) +{ + enum { ROOT, A, N }; + static const box_t root = { 0, 0, 10, 10 }; + static const stack_item_t items[N] = + { + [ROOT] = { .kind = stack_KIND_HBOX, .parent = -1 }, + [A] = { .kind = stack_KIND_LEAF, .parent = A }, /* self-referential */ + }; + + result_t err; + box_t out[N]; + + err = stack_solve(items, N, &root, out); + if (err != result_STACK_BAD_TREE) + return result_TEST_FAILED; + + return result_TEST_PASSED; +} + +/* ----------------------------------------------------------------------- */ + +result_t stack_test(const char *resources) +{ + result_t err; + + NOT_USED(resources); + + err = test_flex_split(); + if (err != result_TEST_PASSED) + return err; + + err = test_aligned_labels(); + if (err != result_TEST_PASSED) + return err; + + err = test_bad_tree(); + if (err != result_TEST_PASSED) + return err; + + return result_TEST_PASSED; +} diff --git a/libraries/geom/stack/valid-tree.c b/libraries/geom/stack/valid-tree.c new file mode 100644 index 00000000..0c1de05a --- /dev/null +++ b/libraries/geom/stack/valid-tree.c @@ -0,0 +1,24 @@ +/* geom/stack/valid-tree.c -- validate a stack item array is a forest rooted at 0 */ + +#include "geom/stack.h" + +#include "impl.h" + +/* ----------------------------------------------------------------------- */ + +int stack__valid_tree(const stack_item_t *items, int n) +{ + int i; + + if (n <= 0) + return 0; + + if (items[0].parent != -1) + return 0; + + for (i = 1; i < n; i++) + if (items[i].parent < 0 || items[i].parent >= i) + return 0; + + return 1; +} From e905281fea54529e4f7cd5521a274394c32c1231 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Mon, 14 Sep 2026 09:49:45 +0100 Subject: [PATCH 064/128] test(geom): add 10 stack layout tests with ASCII diagrams Cover VBOX cross-axis, gap, padding, min/max clamp, SPACER, all four align modes, nested containers, and the root-only tree. --- libraries/geom/stack/test/stack-test.c | 405 ++++++++++++++++++++++++- 1 file changed, 401 insertions(+), 4 deletions(-) diff --git a/libraries/geom/stack/test/stack-test.c b/libraries/geom/stack/test/stack-test.c index 29693f06..c73d847b 100644 --- a/libraries/geom/stack/test/stack-test.c +++ b/libraries/geom/stack/test/stack-test.c @@ -9,11 +9,16 @@ /* ----------------------------------------------------------------------- */ -/* Three flex-1 children in a 100px box divide 34/33/33, abutting exactly. */ +/* 0 34 67 100 + * +---------+-----+---------+ + * | A f=1 | B f1| C f=1 | + * +---------+-----+---------+ + * + * Three flex-1 children in a 100px box divide 34/33/33, abutting exactly. */ static result_t test_flex_split(void) { enum { ROOT, A, B, C, N }; - static const box_t root = { 0, 0, 100, 10 }; + static const box_t root = { 0, 0, 100, 10 }; static const stack_item_t items[N] = { [ROOT] = { .kind = stack_KIND_HBOX, .parent = -1 }, @@ -39,7 +44,14 @@ static result_t test_flex_split(void) return result_TEST_PASSED; } -/* Two independent rows with label leaves fed the same fixed width line up +/* 0 40 200 + * +----------+----------------------+ + * | LBL1(40) | FLD1 flex=1 | ROW1 (h=20) + * +----------+----------------------+ + * | LBL2(40) | FLD2 flex=1 | ROW2 (h=20) + * +----------+----------------------+ + * + * Two independent rows with label leaves fed the same fixed width line up * their field boxes at a common x0. */ static result_t test_aligned_labels(void) { @@ -71,7 +83,352 @@ static result_t test_aligned_labels(void) return result_TEST_PASSED; } -/* A parent index pointing forward (or at itself) is rejected. */ +/* 0,0 +--------+ 10,0 + * | A | flex=1 y: 0..50 + * +--------+ + * | B | flex=1 y: 50..100 + * +--------+ + * 0,100 10,100 + * + * A VBOX splits its children top to bottom, cross axis is x not y. */ +static result_t test_vbox_flex_split(void) +{ + enum { ROOT, A, B, N }; + static const box_t root = { 0, 0, 10, 100 }; + static const stack_item_t items[N] = + { + [ROOT] = { .kind = stack_KIND_VBOX, .parent = -1 }, + [A] = { .kind = stack_KIND_LEAF, .parent = ROOT, .flex = 1 }, + [B] = { .kind = stack_KIND_LEAF, .parent = ROOT, .flex = 1 }, + }; + + result_t err; + box_t out[N]; + + err = stack_solve(items, N, &root, out); + if (err != result_OK) + return result_TEST_FAILED; + + if (out[A].y0 != 0 || out[A].y1 != 50) + return result_TEST_FAILED; + if (out[B].y0 != 50 || out[B].y1 != 100) + return result_TEST_FAILED; + + return result_TEST_PASSED; +} + +/* 0 45 gap=10 55 100 + * +---------+ .... +---------+ + * | A flex=1| .... | B flex=1| + * +---------+ .... +---------+ + * + * A gap is inserted between adjacent children but not before the first or + * after the last. */ +static result_t test_gap(void) +{ + enum { ROOT, A, B, N }; + static const box_t root = { 0, 0, 100, 10 }; + static const stack_item_t items[N] = + { + [ROOT] = { .kind = stack_KIND_HBOX, .parent = -1, .gap = 10 }, + [A] = { .kind = stack_KIND_LEAF, .parent = ROOT, .flex = 1 }, + [B] = { .kind = stack_KIND_LEAF, .parent = ROOT, .flex = 1 }, + }; + + result_t err; + box_t out[N]; + + err = stack_solve(items, N, &root, out); + if (err != result_OK) + return result_TEST_FAILED; + + if (out[A].x0 != 0 || out[A].x1 != 45) + return result_TEST_FAILED; + if (out[B].x0 != 55 || out[B].x1 != 100) + return result_TEST_FAILED; + + return result_TEST_PASSED; +} + +/* 0,0 +------------------------+ 100,0 + * | pad_t=6 | + * | +----------------+ | + * |pl | A (fill) |pr | pad_l=5, pad_r=7 + * | +----------------+ | + * | pad_b=8 | + * +------------------------+ + * 0,100 100,100 + * + * Container padding insets the area available to its children on every + * edge. */ +static result_t test_padding(void) +{ + enum { ROOT, A, N }; + static const box_t root = { 0, 0, 100, 100 }; + static const stack_item_t items[N] = + { + [ROOT] = { .kind = stack_KIND_HBOX, .parent = -1, + .pad_l = 5, .pad_t = 6, .pad_r = 7, .pad_b = 8 }, + [A] = { .kind = stack_KIND_LEAF, .parent = ROOT, .flex = 1, + .align = stack_ALIGN_FILL }, + }; + + result_t err; + box_t out[N]; + + err = stack_solve(items, N, &root, out); + if (err != result_OK) + return result_TEST_FAILED; + + if (out[A].x0 != 5 || out[A].x1 != 93) + return result_TEST_FAILED; + if (out[A].y0 != 6 || out[A].y1 != 92) + return result_TEST_FAILED; + + return result_TEST_PASSED; +} + +/* 0 80 100 + * +-------------------+---------+ + * | A min=80 | B flex=1| + * +-------------------+---------+ + * + * A child's 'min' floors its extent below what an even flex split would + * give it; the remaining flex child absorbs the rest. */ +static result_t test_min_clamp(void) +{ + enum { ROOT, A, B, N }; + static const box_t root = { 0, 0, 100, 10 }; + static const stack_item_t items[N] = + { + [ROOT] = { .kind = stack_KIND_HBOX, .parent = -1 }, + [A] = { .kind = stack_KIND_LEAF, .parent = ROOT, .min = 80 }, + [B] = { .kind = stack_KIND_LEAF, .parent = ROOT, .flex = 1 }, + }; + + result_t err; + box_t out[N]; + + err = stack_solve(items, N, &root, out); + if (err != result_OK) + return result_TEST_FAILED; + + if (out[A].x1 - out[A].x0 != 80) + return result_TEST_FAILED; + if (out[B].x1 - out[B].x0 != 20) + return result_TEST_FAILED; + + return result_TEST_PASSED; +} + +/* 0 20 100 + * +--------+----------------------+ + * |A flex=1| B flex=1 (absorbs | + * |max=20 | the excess slack) | + * +--------+----------------------+ + * + * A child's 'max' ceilings the space handed to it by flex growth; the + * excess slack is returned to its sibling. */ +static result_t test_max_clamp(void) +{ + enum { ROOT, A, B, N }; + static const box_t root = { 0, 0, 100, 10 }; + static const stack_item_t items[N] = + { + [ROOT] = { .kind = stack_KIND_HBOX, .parent = -1 }, + [A] = { .kind = stack_KIND_LEAF, .parent = ROOT, .flex = 1, .max = 20 }, + [B] = { .kind = stack_KIND_LEAF, .parent = ROOT, .flex = 1 }, + }; + + result_t err; + box_t out[N]; + + err = stack_solve(items, N, &root, out); + if (err != result_OK) + return result_TEST_FAILED; + + if (out[A].x1 - out[A].x0 != 20) + return result_TEST_FAILED; + if (out[B].x1 - out[B].x0 != 80) + return result_TEST_FAILED; + + return result_TEST_PASSED; +} + +/* 0 10 90 100 + * +------+--------------------+------+ + * |A sz10| SPACER flex=1 |B sz10| + * +------+--------------------+------+ + * + * A SPACER consumes main-axis space like a leaf but is otherwise just + * another flexible child. */ +static result_t test_spacer(void) +{ + enum { ROOT, A, SPACER, B, N }; + static const box_t root = { 0, 0, 100, 10 }; + static const stack_item_t items[N] = + { + [ROOT] = { .kind = stack_KIND_HBOX, .parent = -1 }, + [A] = { .kind = stack_KIND_LEAF, .parent = ROOT, .size = 10 }, + [SPACER] = { .kind = stack_KIND_SPACER, .parent = ROOT, .flex = 1 }, + [B] = { .kind = stack_KIND_LEAF, .parent = ROOT, .size = 10 }, + }; + + result_t err; + box_t out[N]; + + err = stack_solve(items, N, &root, out); + if (err != result_OK) + return result_TEST_FAILED; + + if (out[A].x0 != 0 || out[A].x1 != 10) + return result_TEST_FAILED; + if (out[B].x0 != 90 || out[B].x1 != 100) + return result_TEST_FAILED; + + return result_TEST_PASSED; +} + +/* y=0 +----------------------+ + * | | + * | A | fills full 40px cross extent + * | | + * y=40 +----------------------+ + * + * ALIGN_FILL spans the full cross extent of the container. */ +static result_t test_align_fill(void) +{ + enum { ROOT, A, N }; + static const box_t root = { 0, 0, 100, 40 }; + static const stack_item_t items[N] = + { + [ROOT] = { .kind = stack_KIND_HBOX, .parent = -1 }, + [A] = { .kind = stack_KIND_LEAF, .parent = ROOT, .flex = 1, + .align = stack_ALIGN_FILL }, + }; + + result_t err; + box_t out[N]; + + err = stack_solve(items, N, &root, out); + if (err != result_OK) + return result_TEST_FAILED; + + if (out[A].y0 != 0 || out[A].y1 != 40) + return result_TEST_FAILED; + + return result_TEST_PASSED; +} + +/* y=0 +----------------------+ + * | | + * y=15 | +---A(10)---+ | + * | +-----------+ | + * y=25 | | + * y=40 +----------------------+ + * + * ALIGN_CENTRE centres a sized child within the container's cross + * extent. */ +static result_t test_align_centre(void) +{ + enum { ROOT, A, N }; + static const box_t root = { 0, 0, 100, 40 }; + static const stack_item_t items[N] = + { + [ROOT] = { .kind = stack_KIND_HBOX, .parent = -1 }, + [A] = { .kind = stack_KIND_LEAF, .parent = ROOT, .flex = 1, .size = 10, + .align = stack_ALIGN_CENTRE }, + }; + + result_t err; + box_t out[N]; + + err = stack_solve(items, N, &root, out); + if (err != result_OK) + return result_TEST_FAILED; + + if (out[A].y0 != 15 || out[A].y1 != 25) + return result_TEST_FAILED; + + return result_TEST_PASSED; +} + +/* 0,0 100,0 + * +------------------------------+ + * | ROW (HBOX, fill) | + * | +------------+-------------+ | + * | | A flex=1 | B flex=1 | | + * | +------------+-------------+ | + * +------------------------------+ + * 0,50 100,50 + * + * An HBOX nested inside a VBOX is itself placed and then lays out its own + * children, recursing correctly through mixed axes. */ +static result_t test_nested_containers(void) +{ + enum { ROOT, ROW, A, B, N }; + static const box_t root = { 0, 0, 100, 50 }; + static const stack_item_t items[N] = + { + [ROOT] = { .kind = stack_KIND_VBOX, .parent = -1 }, + [ROW] = { .kind = stack_KIND_HBOX, .parent = ROOT, .flex = 1, + .align = stack_ALIGN_FILL }, + [A] = { .kind = stack_KIND_LEAF, .parent = ROW, .flex = 1 }, + [B] = { .kind = stack_KIND_LEAF, .parent = ROW, .flex = 1 }, + }; + + result_t err; + box_t out[N]; + + err = stack_solve(items, N, &root, out); + if (err != result_OK) + return result_TEST_FAILED; + + if (out[ROW].x0 != 0 || out[ROW].x1 != 100) + return result_TEST_FAILED; + if (out[A].x0 != 0 || out[A].x1 != 50) + return result_TEST_FAILED; + if (out[B].x0 != 50 || out[B].x1 != 100) + return result_TEST_FAILED; + + return result_TEST_PASSED; +} + +/* 1,2 +------+ 3,2 + * | ROOT | no children + * +------+ + * 1,4 3,4 + * + * A tree of just the root, with no children, solves trivially to the root + * box itself. */ +static result_t test_root_only(void) +{ + enum { ROOT, N }; + static const box_t root = { 1, 2, 3, 4 }; + static const stack_item_t items[N] = + { + [ROOT] = { .kind = stack_KIND_HBOX, .parent = -1 }, + }; + + result_t err; + box_t out[N]; + + err = stack_solve(items, N, &root, out); + if (err != result_OK) + return result_TEST_FAILED; + + if (out[ROOT].x0 != 1 || out[ROOT].y0 != 2 || + out[ROOT].x1 != 3 || out[ROOT].y1 != 4) + return result_TEST_FAILED; + + return result_TEST_PASSED; +} + +/* ROOT + * | + * A ---parent---> A (self-loop, invalid) + * + * A parent index pointing forward (or at itself) is rejected. */ static result_t test_bad_tree(void) { enum { ROOT, A, N }; @@ -108,6 +465,46 @@ result_t stack_test(const char *resources) if (err != result_TEST_PASSED) return err; + err = test_vbox_flex_split(); + if (err != result_TEST_PASSED) + return err; + + err = test_gap(); + if (err != result_TEST_PASSED) + return err; + + err = test_padding(); + if (err != result_TEST_PASSED) + return err; + + err = test_min_clamp(); + if (err != result_TEST_PASSED) + return err; + + err = test_max_clamp(); + if (err != result_TEST_PASSED) + return err; + + err = test_spacer(); + if (err != result_TEST_PASSED) + return err; + + err = test_align_fill(); + if (err != result_TEST_PASSED) + return err; + + err = test_align_centre(); + if (err != result_TEST_PASSED) + return err; + + err = test_nested_containers(); + if (err != result_TEST_PASSED) + return err; + + err = test_root_only(); + if (err != result_TEST_PASSED) + return err; + err = test_bad_tree(); if (err != result_TEST_PASSED) return err; From 7cbf5dfb238ef96fe5968af8a2d0111f6ad3f9a2 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Mon, 14 Sep 2026 15:41:52 +0100 Subject: [PATCH 065/128] feat(wuss): saturn size dialogue laid out via stack_solve Replace hardcoded icon bboxes with a stack layout table (g_saturn_size_stack), solved once into a hand-computed minimum window size. stack_solve distributes into a given box rather than measuring intrinsic size, so the minimum is computed directly from the table's own constants instead of via a scratch solve. --- include/geom/stack.h | 42 ++++++---- libraries/geom/stack/solve.c | 8 +- libraries/geom/stack/test/stack-test.c | 20 ++--- libraries/wuss/test/tasks/saturn.c | 107 ++++++++++++++++++++----- 4 files changed, 128 insertions(+), 49 deletions(-) diff --git a/include/geom/stack.h b/include/geom/stack.h index d971c699..625fefef 100644 --- a/include/geom/stack.h +++ b/include/geom/stack.h @@ -41,26 +41,34 @@ stack_align_t; typedef struct stack_item { stack_kind_t kind; - int parent; /**< Index of the containing item; -1 for the - root. */ + int parent; /**< Index of the containing item; -1 for the + root. */ - int size; /**< Fixed main-axis extent in px; 0 means "use - flex". */ - int flex; /**< Weight for sharing leftover main-axis space; - 0 means "do not grow". */ - int min; /**< Main-axis lower clamp in px; 0 means none. */ - int max; /**< Main-axis upper clamp in px; 0 means - unbounded. */ + int axis_size; /**< Fixed main-axis extent in px; 0 means "use + flex". */ + int flex; /**< Weight for sharing leftover main-axis + space; 0 means "do not grow". */ + int min; /**< Main-axis lower clamp in px; 0 means + none. */ + int max; /**< Main-axis upper clamp in px; 0 means + unbounded. */ - stack_align_t align; /**< Cross-axis placement of this item within its - container. */ + stack_align_t align; /**< Cross-axis placement of this item within + its container. */ + int cross_size; /**< Cross-axis extent in px for + stack_ALIGN_CENTRE/stack_ALIGN_END; 0 means + span the container's full cross extent. + Unused for stack_ALIGN_START/FILL. */ - int gap; /**< Containers only: px between adjacent - children. */ - int pad_l; /**< Containers only: inner inset, left edge. */ - int pad_t; /**< Containers only: inner inset, top edge. */ - int pad_r; /**< Containers only: inner inset, right edge. */ - int pad_b; /**< Containers only: inner inset, bottom edge. */ + int gap; /**< Containers only: px between adjacent + children. */ + int pad_l; /**< Containers only: inner inset, left + edge. */ + int pad_t; /**< Containers only: inner inset, top edge. */ + int pad_r; /**< Containers only: inner inset, right + edge. */ + int pad_b; /**< Containers only: inner inset, bottom + edge. */ } stack_item_t; diff --git a/libraries/geom/stack/solve.c b/libraries/geom/stack/solve.c index 69d711a8..4be209a6 100644 --- a/libraries/geom/stack/solve.c +++ b/libraries/geom/stack/solve.c @@ -47,7 +47,7 @@ static int stack__distribute(const stack_item_t *items, continue; children[nchildren].index = i; - children[nchildren].extent = items[i].size ? items[i].size : items[i].min; + children[nchildren].extent = items[i].axis_size ? items[i].axis_size : items[i].min; children[nchildren].clamped = (items[i].flex == 0); used += children[nchildren].extent; @@ -185,16 +185,16 @@ static void stack__place_container(const stack_item_t *items, cross_pos = cross0; break; case stack_ALIGN_CENTRE: - cross_extent = items[ci].size ? items[ci].size : (cross1 - cross0); + cross_extent = items[ci].cross_size ? items[ci].cross_size : (cross1 - cross0); cross_pos = cross0 + (cross1 - cross0 - cross_extent) / 2; break; case stack_ALIGN_END: - cross_extent = items[ci].size ? items[ci].size : (cross1 - cross0); + cross_extent = items[ci].cross_size ? items[ci].cross_size : (cross1 - cross0); cross_pos = cross1 - cross_extent; break; case stack_ALIGN_START: default: - cross_extent = items[ci].size ? items[ci].size : (cross1 - cross0); + cross_extent = items[ci].cross_size ? items[ci].cross_size : (cross1 - cross0); cross_pos = cross0; break; } diff --git a/libraries/geom/stack/test/stack-test.c b/libraries/geom/stack/test/stack-test.c index c73d847b..4b147174 100644 --- a/libraries/geom/stack/test/stack-test.c +++ b/libraries/geom/stack/test/stack-test.c @@ -56,15 +56,17 @@ static result_t test_flex_split(void) static result_t test_aligned_labels(void) { enum { ROOT, ROW1, LBL1, FLD1, ROW2, LBL2, FLD2, N }; - static const box_t root = { 0, 0, 200, 40 }; + static const box_t root = { 0, 0, 200, 40 }; static const stack_item_t items[N] = { [ROOT] = { .kind = stack_KIND_VBOX, .parent = -1 }, - [ROW1] = { .kind = stack_KIND_HBOX, .parent = ROOT, .size = 20 }, - [LBL1] = { .kind = stack_KIND_LEAF, .parent = ROW1, .size = 40, .align = stack_ALIGN_END }, + + [ROW1] = { .kind = stack_KIND_HBOX, .parent = ROOT, .axis_size = 20 }, + [LBL1] = { .kind = stack_KIND_LEAF, .parent = ROW1, .axis_size = 40, .align = stack_ALIGN_END }, [FLD1] = { .kind = stack_KIND_LEAF, .parent = ROW1, .flex = 1 }, - [ROW2] = { .kind = stack_KIND_HBOX, .parent = ROOT, .size = 20 }, - [LBL2] = { .kind = stack_KIND_LEAF, .parent = ROW2, .size = 40, .align = stack_ALIGN_END }, + + [ROW2] = { .kind = stack_KIND_HBOX, .parent = ROOT, .axis_size = 20 }, + [LBL2] = { .kind = stack_KIND_LEAF, .parent = ROW2, .axis_size = 40, .align = stack_ALIGN_END }, [FLD2] = { .kind = stack_KIND_LEAF, .parent = ROW2, .flex = 1 }, }; @@ -269,9 +271,9 @@ static result_t test_spacer(void) static const stack_item_t items[N] = { [ROOT] = { .kind = stack_KIND_HBOX, .parent = -1 }, - [A] = { .kind = stack_KIND_LEAF, .parent = ROOT, .size = 10 }, + [A] = { .kind = stack_KIND_LEAF, .parent = ROOT, .axis_size = 10 }, [SPACER] = { .kind = stack_KIND_SPACER, .parent = ROOT, .flex = 1 }, - [B] = { .kind = stack_KIND_LEAF, .parent = ROOT, .size = 10 }, + [B] = { .kind = stack_KIND_LEAF, .parent = ROOT, .axis_size = 10 }, }; result_t err; @@ -336,8 +338,8 @@ static result_t test_align_centre(void) static const stack_item_t items[N] = { [ROOT] = { .kind = stack_KIND_HBOX, .parent = -1 }, - [A] = { .kind = stack_KIND_LEAF, .parent = ROOT, .flex = 1, .size = 10, - .align = stack_ALIGN_CENTRE }, + [A] = { .kind = stack_KIND_LEAF, .parent = ROOT, .flex = 1, + .cross_size = 10, .align = stack_ALIGN_CENTRE }, }; result_t err; diff --git a/libraries/wuss/test/tasks/saturn.c b/libraries/wuss/test/tasks/saturn.c index f61cab95..8e83bbed 100644 --- a/libraries/wuss/test/tasks/saturn.c +++ b/libraries/wuss/test/tasks/saturn.c @@ -15,6 +15,7 @@ #include "framebuf/palettes.h" #include "geom/box.h" #include "geom/size.h" +#include "geom/stack.h" #include "utils/rng.h" #include "wuss/menu.h" @@ -312,47 +313,115 @@ static int saturn_size_snap(int v) return CLAMP(v, SATURN_SIZE_MIN, SATURN_SIZE_MAX); } +/* stack items for the size dialogue's layout: a VBOX of label / slider / + * value-echo / button-row, with fixed-size spacers standing in for the + * (non-uniform) gaps between them. BTN_ROW is an HBOX with Cancel and + * Apply side by side, gapped and top-aligned (Apply is taller than + * Cancel, both start flush with the row's top edge). */ +enum +{ + ST_ROOT, + ST_ROW, + ST_LABL, + ST_SLDR, + ST_VAL, + + ST_BTNS, + ST_SPCR, + ST_CNCL, + ST_APLY, + + SIZE_STACK__LIMIT +}; + +#define G 4 +#define wuss_STD_SLIDER_HEIGHT 18 +#define wuss_STD_SECONDARY_BUTTON_HEIGHT 26 +#define wuss_STD_PRIMARY_BUTTON_HEIGHT 34 + +static const stack_item_t g_saturn_size_stack[SIZE_STACK__LIMIT] = +{ + [ST_ROOT] = { .kind = stack_KIND_VBOX, .parent = -1, .gap = G, .pad_l = G, .pad_t = G, .pad_r = G, .pad_b = G }, + + [ST_ROW] = { .kind = stack_KIND_HBOX, .parent = ST_ROOT, .axis_size = wuss_STD_SLIDER_HEIGHT, .gap = G, .align = stack_ALIGN_START }, + [ST_LABL] = { .kind = stack_KIND_LEAF, .parent = ST_ROW, .axis_size = 24, .cross_size = 16, .align = stack_ALIGN_CENTRE }, + [ST_SLDR] = { .kind = stack_KIND_LEAF, .parent = ST_ROW, .flex = 1, .min = 64, .cross_size = wuss_STD_SLIDER_HEIGHT, .align = stack_ALIGN_CENTRE }, + [ST_VAL] = { .kind = stack_KIND_LEAF, .parent = ST_ROW, .axis_size = 24, .cross_size = 16, .align = stack_ALIGN_CENTRE }, + + [ST_BTNS] = { .kind = stack_KIND_HBOX, .parent = ST_ROOT, .axis_size = wuss_STD_PRIMARY_BUTTON_HEIGHT, .gap = G, .align = stack_ALIGN_END }, + [ST_SPCR] = { .kind = stack_KIND_SPACER, .parent = ST_BTNS, .flex = 1 }, + [ST_CNCL] = { .kind = stack_KIND_LEAF, .parent = ST_BTNS, .axis_size = 48, .cross_size = wuss_STD_SECONDARY_BUTTON_HEIGHT, .align = stack_ALIGN_CENTRE }, + [ST_APLY] = { .kind = stack_KIND_LEAF, .parent = ST_BTNS, .axis_size = 56, .cross_size = wuss_STD_PRIMARY_BUTTON_HEIGHT, .align = stack_ALIGN_CENTRE }, +}; + /* Build the size dialogue once: a label, a slider snapped to * SATURN_SIZE_STEP, a label echoing the slider's current value, and - * Cancel/Apply buttons. Created hidden -- wuss shows and hides it itself, - * as a menu leaf's borrowed window (see g_saturn_menu_items[SATURN_MENU_SIZE] - * and wuss_menu_item_t::window), so it must outlive the open menu chain and - * is never closed here, only hidden. */ + * Cancel/Apply buttons, positioned by stack_solve. Created hidden -- wuss + * shows and hides it itself, as a menu leaf's borrowed window (see + * g_saturn_menu_items[SATURN_MENU_SIZE] and wuss_menu_item_t::window), so + * it must outlive the open menu chain and is never closed here, only + * hidden. */ static result_t saturn_size_dialogue_create(saturn_task_t *task) { - wuss_icon_spec_t specs[SATURN_SIZE_NICONS]; - wuss_icon_t *made[SATURN_SIZE_NICONS]; + wuss_icon_spec_t specs[SATURN_SIZE_NICONS]; + wuss_icon_t *made[SATURN_SIZE_NICONS]; wuss_icon_spec_t *s; + box_t boxes[SIZE_STACK__LIMIT]; + box_t root; char buf[16]; result_t rc; + int row_w, btns_w; + size2d_t sz; + + /* stack_solve distributes into a box it's given; it can't report a + * subtree's intrinsic minimum size in one call, since flex/spacer items + * always consume whatever slack the root provides. So the minimum + * window size is hand-computed here from the same constants the table + * uses, rather than measured by solving. */ + row_w = 24 + G + 64 + G + 24; + btns_w = 48 + G + 56; + sz.w = MAX(row_w, btns_w) + 2 * G; + sz.h = wuss_STD_SLIDER_HEIGHT + G + wuss_STD_PRIMARY_BUTTON_HEIGHT + 2 * G; rc = wuss_window_create_placed(task->delegate, - SIZE2D(160, 112), + sz, "Size", - wuss_WINDOW_HIDDEN | wuss_WINDOW_NO_CLOSE | - wuss_WINDOW_NO_BACK | + wuss_WINDOW_HIDDEN | + wuss_WINDOW_NO_CLOSE | + wuss_WINDOW_NO_BACK | wuss_WINDOW_NO_TOGGLE_SIZE | - wuss_WINDOW_NO_VSCROLL | - wuss_WINDOW_NO_HSCROLL | - wuss_WINDOW_NO_RESIZE, + wuss_WINDOW_NO_HSCROLL | + wuss_WINDOW_NO_VSCROLL | + wuss_WINDOW_NO_RESIZE | + wuss_WINDOW_NO_REDRAW, wuss_BACKDROP_COLOUR(wuss_COLOUR_WINDOW), - SIZE2D(160, 112), - SIZE2D(160, 112), + sz, + sz, &task->size_dialogue); if (rc != result_OK) return rc; + root = (box_t) BOX_POS_SIZE(0, 0, sz.w, sz.h); + rc = stack_solve(g_saturn_size_stack, NELEMS(g_saturn_size_stack), &root, boxes); + if (rc != result_OK) + { + wuss_window_close(task->size_dialogue); + task->size_dialogue = NULL; + return rc; + } + memset(specs, 0, sizeof(specs)); s = &specs[SATURN_SIZE_ICON_LABEL]; - s->bbox = (box_t) BOX_POS_SIZE(12, 10, 60, 16); + s->bbox = boxes[ST_LABL]; s->type = wuss_ICON_TYPE_LABEL; s->text = "Size"; s->fg = wuss_COLOUR_BLACK; s->bg = wuss_NO_BACKGROUND; + s->flags = wuss_ICON_FLAGS_JUSTIFY_RIGHT; s = &specs[SATURN_SIZE_ICON_SLIDER]; - s->bbox = (box_t) BOX_POS_SIZE(12, 30, 136, 20); + s->bbox = boxes[ST_SLDR]; s->type = wuss_ICON_TYPE_SLIDER; s->fg = wuss_COLOUR_BLACK; s->bg = wuss_NO_BACKGROUND; @@ -363,21 +432,21 @@ static result_t saturn_size_dialogue_create(saturn_task_t *task) snprintf(buf, sizeof(buf), "%d", s->u.slider.default_value); s = &specs[SATURN_SIZE_ICON_VALUE]; - s->bbox = (box_t) BOX_POS_SIZE(12, 56, 60, 16); + s->bbox = boxes[ST_VAL]; s->type = wuss_ICON_TYPE_LABEL; s->text = buf; /* copied by wuss_icon_create_array */ s->fg = wuss_COLOUR_BLACK; s->bg = wuss_NO_BACKGROUND; s = &specs[SATURN_SIZE_ICON_CANCEL]; - s->bbox = (box_t) BOX_POS_SIZE(12, 74, 60, 22); + s->bbox = boxes[ST_CNCL]; s->type = wuss_ICON_TYPE_ACTION; s->text = "Cancel"; s->fg = wuss_COLOUR_BLACK; s->bg = wuss_COLOUR_WINDOW; s = &specs[SATURN_SIZE_ICON_APPLY]; - s->bbox = (box_t) BOX_POS_SIZE(88, 74, 60, 34); + s->bbox = boxes[ST_APLY]; s->type = wuss_ICON_TYPE_ACTION; s->text = "Apply"; s->fg = wuss_COLOUR_BLACK; From f3753eca7bafe4f3d3af593a5b7f56a26329f345 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Mon, 14 Sep 2026 15:54:49 +0100 Subject: [PATCH 066/128] feat(geom): add STACK_* macros for compact stack_item_t tables One macro per stack_item kind (STACK_VBOX/HBOX/LEAF/SPACER, plus _EX variants for the rarer fields) expands to the same designated-init struct literal, so a static table no longer spells out .kind and every field name per row. Zero runtime cost. Macro params are suffixed (p_, axis_, ...) rather than named after the fields they set -- naming a param 'parent' rewrote '.parent' itself during substitution. --- include/geom/stack.h | 41 ++++++++++++++++++++++++++++++ libraries/wuss/test/tasks/saturn.c | 21 +++++++-------- 2 files changed, 52 insertions(+), 10 deletions(-) diff --git a/include/geom/stack.h b/include/geom/stack.h index 625fefef..320a4df5 100644 --- a/include/geom/stack.h +++ b/include/geom/stack.h @@ -72,6 +72,47 @@ typedef struct stack_item } stack_item_t; +/* ----------------------------------------------------------------------- */ + +/* Compact initialisers for a static stack_item_t[] table, covering the + * fields each kind needs in the common case. Parent is still given + * explicitly -- a flat initializer list has no nesting to infer it from -- + * but the field names and .kind are no longer spelled out per item. The + * _EX variants add the fields the plain macro omits, for the occasional + * item that needs them (e.g. root padding, or a leaf that both flexes and + * clamps). There is no non-EX/EX split for stack_KIND_SPACER: flex is its + * only field. */ + +#define STACK_VBOX(p_, axis_, gap_) \ + { .kind = stack_KIND_VBOX, .parent = (p_), \ + .axis_size = (axis_), .gap = (gap_) } + +#define STACK_VBOX_EX(p_, axis_, gap_, padl_, padt_, padr_, padb_) \ + { .kind = stack_KIND_VBOX, .parent = (p_), \ + .axis_size = (axis_), .gap = (gap_), \ + .pad_l = (padl_), .pad_t = (padt_), .pad_r = (padr_), .pad_b = (padb_) } + +#define STACK_HBOX(p_, axis_, gap_, align_) \ + { .kind = stack_KIND_HBOX, .parent = (p_), \ + .axis_size = (axis_), .gap = (gap_), .align = (align_) } + +#define STACK_HBOX_EX(p_, axis_, gap_, align_, padl_, padt_, padr_, padb_) \ + { .kind = stack_KIND_HBOX, .parent = (p_), \ + .axis_size = (axis_), .gap = (gap_), .align = (align_), \ + .pad_l = (padl_), .pad_t = (padt_), .pad_r = (padr_), .pad_b = (padb_) } + +#define STACK_LEAF(p_, axis_, cross_, align_) \ + { .kind = stack_KIND_LEAF, .parent = (p_), \ + .axis_size = (axis_), .cross_size = (cross_), .align = (align_) } + +#define STACK_LEAF_EX(p_, axis_, cross_, align_, flex_, min_, max_) \ + { .kind = stack_KIND_LEAF, .parent = (p_), \ + .axis_size = (axis_), .cross_size = (cross_), .align = (align_), \ + .flex = (flex_), .min = (min_), .max = (max_) } + +#define STACK_SPACER(p_, flex_) \ + { .kind = stack_KIND_SPACER, .parent = (p_), .flex = (flex_) } + /** * Solve a box-stack tree into boxes. * diff --git a/libraries/wuss/test/tasks/saturn.c b/libraries/wuss/test/tasks/saturn.c index 8e83bbed..dff322b0 100644 --- a/libraries/wuss/test/tasks/saturn.c +++ b/libraries/wuss/test/tasks/saturn.c @@ -321,6 +321,7 @@ static int saturn_size_snap(int v) enum { ST_ROOT, + ST_ROW, ST_LABL, ST_SLDR, @@ -341,17 +342,17 @@ enum static const stack_item_t g_saturn_size_stack[SIZE_STACK__LIMIT] = { - [ST_ROOT] = { .kind = stack_KIND_VBOX, .parent = -1, .gap = G, .pad_l = G, .pad_t = G, .pad_r = G, .pad_b = G }, + [ST_ROOT] = STACK_VBOX_EX(-1, 0, G, G, G, G, G), - [ST_ROW] = { .kind = stack_KIND_HBOX, .parent = ST_ROOT, .axis_size = wuss_STD_SLIDER_HEIGHT, .gap = G, .align = stack_ALIGN_START }, - [ST_LABL] = { .kind = stack_KIND_LEAF, .parent = ST_ROW, .axis_size = 24, .cross_size = 16, .align = stack_ALIGN_CENTRE }, - [ST_SLDR] = { .kind = stack_KIND_LEAF, .parent = ST_ROW, .flex = 1, .min = 64, .cross_size = wuss_STD_SLIDER_HEIGHT, .align = stack_ALIGN_CENTRE }, - [ST_VAL] = { .kind = stack_KIND_LEAF, .parent = ST_ROW, .axis_size = 24, .cross_size = 16, .align = stack_ALIGN_CENTRE }, - - [ST_BTNS] = { .kind = stack_KIND_HBOX, .parent = ST_ROOT, .axis_size = wuss_STD_PRIMARY_BUTTON_HEIGHT, .gap = G, .align = stack_ALIGN_END }, - [ST_SPCR] = { .kind = stack_KIND_SPACER, .parent = ST_BTNS, .flex = 1 }, - [ST_CNCL] = { .kind = stack_KIND_LEAF, .parent = ST_BTNS, .axis_size = 48, .cross_size = wuss_STD_SECONDARY_BUTTON_HEIGHT, .align = stack_ALIGN_CENTRE }, - [ST_APLY] = { .kind = stack_KIND_LEAF, .parent = ST_BTNS, .axis_size = 56, .cross_size = wuss_STD_PRIMARY_BUTTON_HEIGHT, .align = stack_ALIGN_CENTRE }, + [ST_ROW] = STACK_HBOX(ST_ROOT, wuss_STD_SLIDER_HEIGHT, G, stack_ALIGN_START), + [ST_LABL] = STACK_LEAF(ST_ROW, 24, 16, stack_ALIGN_CENTRE), + [ST_SLDR] = STACK_LEAF_EX(ST_ROW, 0, wuss_STD_SLIDER_HEIGHT, stack_ALIGN_CENTRE, 1, 64, 0), + [ST_VAL] = STACK_LEAF(ST_ROW, 24, 16, stack_ALIGN_CENTRE), + + [ST_BTNS] = STACK_HBOX(ST_ROOT, wuss_STD_PRIMARY_BUTTON_HEIGHT, G, stack_ALIGN_END), + [ST_SPCR] = STACK_SPACER(ST_BTNS, 1), + [ST_CNCL] = STACK_LEAF(ST_BTNS, 48, wuss_STD_SECONDARY_BUTTON_HEIGHT, stack_ALIGN_CENTRE), + [ST_APLY] = STACK_LEAF(ST_BTNS, 56, wuss_STD_PRIMARY_BUTTON_HEIGHT, stack_ALIGN_CENTRE), }; /* Build the size dialogue once: a label, a slider snapped to From f4edd552a6260aff6d0149300b9c9a2eea45cab6 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Mon, 14 Sep 2026 16:37:16 +0100 Subject: [PATCH 067/128] refactor(wuss): reduce duplication in saturn.c saturn_create: collapse four growing copy-pasted error-cleanup blocks into one goto ladder. saturn_menu_select: extract the fg/bg colourmenu pick handling, identical but for which field/menu, into one helper. saturn_size_dialogue_create: name the leaf sizes the hand-computed window size shares with g_saturn_size_stack, so the two can no longer silently drift apart. --- libraries/wuss/test/tasks/saturn.c | 98 ++++++++++++++++-------------- 1 file changed, 52 insertions(+), 46 deletions(-) diff --git a/libraries/wuss/test/tasks/saturn.c b/libraries/wuss/test/tasks/saturn.c index dff322b0..13589dc1 100644 --- a/libraries/wuss/test/tasks/saturn.c +++ b/libraries/wuss/test/tasks/saturn.c @@ -151,18 +151,11 @@ result_t saturn_create(wuss_t *wuss, rc = wuss_colourmenu_create(&task->fg_colourmenu, wuss, "Foreground"); if (rc != result_OK) - { - wuss_task_destroy(delegate); - return rc; - } + goto fail_delegate; rc = wuss_colourmenu_create(&task->bg_colourmenu, wuss, "Background"); if (rc != result_OK) - { - wuss_colourmenu_destroy(task->fg_colourmenu); - wuss_task_destroy(delegate); - return rc; - } + goto fail_fg_colourmenu; g_saturn_colours_items[SATURN_COLOURS_MENU_FOREGROUND].submenu = wuss_colourmenu_menu(task->fg_colourmenu); @@ -178,24 +171,22 @@ result_t saturn_create(wuss_t *wuss, SIZE2D(0, 0), &task->window); if (rc != result_OK) - { - wuss_colourmenu_destroy(task->bg_colourmenu); - wuss_colourmenu_destroy(task->fg_colourmenu); - wuss_task_destroy(delegate); /* unregister; its QUIT frees the task block */ - return rc; - } + goto fail_bg_colourmenu; rc = saturn_size_dialogue_create(task); if (rc != result_OK) - { - wuss_colourmenu_destroy(task->bg_colourmenu); - wuss_colourmenu_destroy(task->fg_colourmenu); - wuss_task_destroy(delegate); /* closes task->window too; QUIT frees the block */ - return rc; - } + goto fail_bg_colourmenu; /* wuss_task_destroy closes task->window too */ g_saturn_menu_items[SATURN_MENU_SIZE].window = task->size_dialogue; return result_OK; + +fail_bg_colourmenu: + wuss_colourmenu_destroy(task->bg_colourmenu); +fail_fg_colourmenu: + wuss_colourmenu_destroy(task->fg_colourmenu); +fail_delegate: + wuss_task_destroy(delegate); /* unregisters; its QUIT frees the task block */ + return rc; } /* plot one point in window content space, clipped to the window. x,y are @@ -340,19 +331,27 @@ enum #define wuss_STD_SECONDARY_BUTTON_HEIGHT 26 #define wuss_STD_PRIMARY_BUTTON_HEIGHT 34 +/* Leaf main-axis sizes, named so saturn_size_dialogue_create's hand-computed + * minimum window size can share them with the table below instead of + * repeating the numbers as bare literals. */ +#define ST_LABEL_W 24 +#define ST_SLIDER_MIN_W 64 +#define ST_CANCEL_W 48 +#define ST_APPLY_W 56 + static const stack_item_t g_saturn_size_stack[SIZE_STACK__LIMIT] = { [ST_ROOT] = STACK_VBOX_EX(-1, 0, G, G, G, G, G), [ST_ROW] = STACK_HBOX(ST_ROOT, wuss_STD_SLIDER_HEIGHT, G, stack_ALIGN_START), - [ST_LABL] = STACK_LEAF(ST_ROW, 24, 16, stack_ALIGN_CENTRE), - [ST_SLDR] = STACK_LEAF_EX(ST_ROW, 0, wuss_STD_SLIDER_HEIGHT, stack_ALIGN_CENTRE, 1, 64, 0), - [ST_VAL] = STACK_LEAF(ST_ROW, 24, 16, stack_ALIGN_CENTRE), + [ST_LABL] = STACK_LEAF(ST_ROW, ST_LABEL_W, 16, stack_ALIGN_CENTRE), + [ST_SLDR] = STACK_LEAF_EX(ST_ROW, 0, wuss_STD_SLIDER_HEIGHT, stack_ALIGN_CENTRE, 1, ST_SLIDER_MIN_W, 0), + [ST_VAL] = STACK_LEAF(ST_ROW, ST_LABEL_W, 16, stack_ALIGN_CENTRE), [ST_BTNS] = STACK_HBOX(ST_ROOT, wuss_STD_PRIMARY_BUTTON_HEIGHT, G, stack_ALIGN_END), [ST_SPCR] = STACK_SPACER(ST_BTNS, 1), - [ST_CNCL] = STACK_LEAF(ST_BTNS, 48, wuss_STD_SECONDARY_BUTTON_HEIGHT, stack_ALIGN_CENTRE), - [ST_APLY] = STACK_LEAF(ST_BTNS, 56, wuss_STD_PRIMARY_BUTTON_HEIGHT, stack_ALIGN_CENTRE), + [ST_CNCL] = STACK_LEAF(ST_BTNS, ST_CANCEL_W, wuss_STD_SECONDARY_BUTTON_HEIGHT, stack_ALIGN_CENTRE), + [ST_APLY] = STACK_LEAF(ST_BTNS, ST_APPLY_W, wuss_STD_PRIMARY_BUTTON_HEIGHT, stack_ALIGN_CENTRE), }; /* Build the size dialogue once: a label, a slider snapped to @@ -379,8 +378,8 @@ static result_t saturn_size_dialogue_create(saturn_task_t *task) * always consume whatever slack the root provides. So the minimum * window size is hand-computed here from the same constants the table * uses, rather than measured by solving. */ - row_w = 24 + G + 64 + G + 24; - btns_w = 48 + G + 56; + row_w = ST_LABEL_W + G + ST_SLIDER_MIN_W + G + ST_LABEL_W; + btns_w = ST_CANCEL_W + G + ST_APPLY_W; sz.w = MAX(row_w, btns_w) + 2 * G; sz.h = wuss_STD_SLIDER_HEIGHT + G + wuss_STD_PRIMARY_BUTTON_HEIGHT + 2 * G; @@ -541,6 +540,26 @@ static result_t saturn_size_pre_show(saturn_task_t *task) return wuss_icon_set_text(task->size_dialogue, task->size_value_label, buf); } +/* Applies event to *colour if it's a pick from colourmenu, returns whether + * it was. Shared by saturn_menu_select's fg/bg attempts below. */ +static int saturn_menu_select_apply(const wuss_colourmenu_t *colourmenu, + colour_t *colour, + const colour_t *palette, + int npalette, + const wuss_event_t *event) +{ + wuss_colour_t picked; + int mine; + + picked = wuss_colourmenu_selected(colourmenu, event, &mine); + if (!mine) + return 0; + + if (picked < npalette) + *colour = palette[picked]; + return 1; +} + /* A pick from either colour submenu, resolved against whichever colourmenu * it came from. "Size" is a wuss_menu_item_t::window leaf, not a leaf pick, * so it never reaches here -- see saturn_size_icon and saturn_size_pre_show. */ @@ -548,28 +567,15 @@ static result_t saturn_menu_select(saturn_task_t *task, const wuss_event_t *event) { const colour_t *palette; - wuss_colour_t picked; - int npalette, mine; + int npalette; palette = wuss_get_palette(task->wuss, &npalette); - picked = wuss_colourmenu_selected(task->fg_colourmenu, event, &mine); - if (mine) - { - if (picked < npalette) - task->fg = palette[picked]; - wuss_window_invalidate_visible(task->window); - return result_OK; - } - - picked = wuss_colourmenu_selected(task->bg_colourmenu, event, &mine); - if (mine) - { - if (picked < npalette) - task->bg = palette[picked]; + if (saturn_menu_select_apply(task->fg_colourmenu, &task->fg, palette, + npalette, event) || + saturn_menu_select_apply(task->bg_colourmenu, &task->bg, palette, + npalette, event)) wuss_window_invalidate_visible(task->window); - return result_OK; - } return result_OK; } From f5fafde9a285c277f61756eaa949cc00dbd69bc8 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Mon, 14 Sep 2026 20:53:16 +0100 Subject: [PATCH 068/128] feat(wuss)!: flip furniture window flags to opt-in wuss_WINDOW_NO_TITLEBAR/OUTLINE/CLOSE/BACK/TOGGLE_SIZE/VSCROLL/HSCROLL/RESIZE become wuss_WINDOW_TITLEBAR/OUTLINE/CLOSE/BACK/TOGGLE_SIZE/VSCROLL/HSCROLL/RESIZE: set to show, not set to hide. wuss_WINDOW_DEFAULT is now the bitwise OR of all eight, so bare-DEFAULT callers need no changes. wuss_WINDOW_NO_RESIZE_BLIT, wuss_WINDOW_HIDDEN and wuss_WINDOW_NO_REDRAW are unrelated behavioural/runtime flags and keep their names and polarity. Propagates through every furniture geometry/hit-test/draw helper, the menu window flag composition, wuss_info's window flags, three demo tasks, and the SDL wuss-test.c suite (including the combos[] per-bit furniture matrix). Also fixes three wuss-test.c windows that relied on wuss_WINDOW_NO_RESIZE_BLIT alone and got full furniture for free under the old zero default; they now explicitly OR it onto wuss_WINDOW_DEFAULT. Co-Authored-By: Claude Sonnet 5 --- include/wuss/component/info.h | 4 +- include/wuss/component/proginfo.h | 8 +- include/wuss/window.h | 8 +- include/wuss/wuss.h | 57 ++++---- libraries/wuss/component/info.c | 8 +- libraries/wuss/core/impl.h | 16 +-- libraries/wuss/core/scroll-step.c | 4 +- libraries/wuss/furniture/close-box.c | 4 +- libraries/wuss/furniture/draw.c | 10 +- libraries/wuss/furniture/hit-test.c | 20 +-- libraries/wuss/furniture/invalidate.c | 2 +- libraries/wuss/furniture/layout.c | 26 ++-- libraries/wuss/furniture/scroll-box.c | 8 +- libraries/wuss/menu/menu.c | 7 +- libraries/wuss/test/tasks/blank.c | 3 +- libraries/wuss/test/tasks/chars.c | 2 +- libraries/wuss/test/tasks/saturn.c | 10 +- libraries/wuss/test/wuss-test.c | 183 ++++++++++---------------- 18 files changed, 169 insertions(+), 211 deletions(-) diff --git a/include/wuss/component/info.h b/include/wuss/component/info.h index bdaa9844..86c18a3e 100644 --- a/include/wuss/component/info.h +++ b/include/wuss/component/info.h @@ -18,8 +18,8 @@ * menu row at wuss_info_window() and wuss shows it where a submenu would * open. A task can equally wuss_window_set_hidden() it directly. * - * The window is created wuss_WINDOW_NO_CLOSE and stays on the caller's task - * until wuss_info_destroy closes it. That task must therefore not be an + * The window is created without wuss_WINDOW_CLOSE and stays on the caller's + * task until wuss_info_destroy closes it. That task must therefore not be an * autoclose task -- its window list would never empty -- and must outlive * the handle. As for any borrowed wuss_menu_item_t::window, the dialogue * must also outlive every menu chain that references it: close the chain diff --git a/include/wuss/component/proginfo.h b/include/wuss/component/proginfo.h index d9091064..eb090bd3 100644 --- a/include/wuss/component/proginfo.h +++ b/include/wuss/component/proginfo.h @@ -11,10 +11,10 @@ * non-NULL fields of a wuss_proginfo_desc_t to wuss_info_row_t rows and * calls wuss_info_create. wuss_proginfo_t is wuss_info_t, and the destroy / * window calls forward straight through, so all of info.h's lifetime rules - * apply verbatim: the window is wuss_WINDOW_NO_CLOSE, stays on the caller's - * task until wuss_proginfo_destroy, that task must not be an autoclose task - * and must outlive the handle, and the dialogue must outlive every menu - * chain that borrows it as a wuss_menu_item_t::window. + * apply verbatim: the window is created without wuss_WINDOW_CLOSE, stays on + * the caller's task until wuss_proginfo_destroy, that task must not be an + * autoclose task and must outlive the handle, and the dialogue must outlive + * every menu chain that borrows it as a wuss_menu_item_t::window. * * Built only when WUSS_COMPONENTS is defined (which implies WUSS_MENUS). */ diff --git a/include/wuss/window.h b/include/wuss/window.h index a4914833..3d8bbe28 100644 --- a/include/wuss/window.h +++ b/include/wuss/window.h @@ -45,10 +45,10 @@ extern "C" * \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 + * if too long. Ignored unless flags includes + * wuss_WINDOW_TITLEBAR. + * \param[in] flags Appearance flags, e.g. wuss_WINDOW_TITLEBAR / + * wuss_WINDOW_OUTLINE, OR'd together, or * wuss_WINDOW_DEFAULT for the default furniture. * \param[in] bg Content background, filled by Wuss before each redraw: * a flat colour or an 8x8 fill pattern (see diff --git a/include/wuss/wuss.h b/include/wuss/wuss.h index e462567a..054456a3 100644 --- a/include/wuss/wuss.h +++ b/include/wuss/wuss.h @@ -199,48 +199,53 @@ wuss_furniture_palette_t; * * \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. + * furniture bits (wuss_WINDOW_TITLEBAR and below) are ignored. */ typedef enum wuss_window_flags { - /** Default: every furniture region drawn. */ - wuss_WINDOW_DEFAULT = 0, - /** - * No titlebar; content fills the full visible area, and no drag handle - * exists. + * Titlebar present; content is inset below it, and it offers a drag + * handle. Without it, content fills the full visible area and + * wuss_WINDOW_CLOSE/BACK/TOGGLE_SIZE are ignored. */ - wuss_WINDOW_NO_TITLEBAR = 1 << 0, + wuss_WINDOW_TITLEBAR = 1 << 0, - /** No 1px border drawn around the visible area. */ - wuss_WINDOW_NO_OUTLINE = 1 << 1, + /** 1px border drawn around the visible area. */ + wuss_WINDOW_OUTLINE = 1 << 1, /** - * No close icon in the titlebar. Ignored if flags includes - * wuss_WINDOW_NO_TITLEBAR. + * Close icon in the titlebar. Ignored unless flags also includes + * wuss_WINDOW_TITLEBAR. */ - wuss_WINDOW_NO_CLOSE = 1 << 2, + wuss_WINDOW_CLOSE = 1 << 2, /** - * No send-to-back icon in the titlebar. Ignored if flags includes - * wuss_WINDOW_NO_TITLEBAR. + * Send-to-back icon in the titlebar. Ignored unless flags also includes + * wuss_WINDOW_TITLEBAR. */ - wuss_WINDOW_NO_BACK = 1 << 3, + wuss_WINDOW_BACK = 1 << 3, /** - * No toggle-size icon in the titlebar. Ignored if flags includes - * wuss_WINDOW_NO_TITLEBAR. + * Toggle-size icon in the titlebar. Ignored unless flags also includes + * wuss_WINDOW_TITLEBAR. */ - wuss_WINDOW_NO_TOGGLE_SIZE = 1 << 4, + wuss_WINDOW_TOGGLE_SIZE = 1 << 4, - /** No vertical scrollbar on the right edge. */ - wuss_WINDOW_NO_VSCROLL = 1 << 5, + /** Vertical scrollbar on the right edge. */ + wuss_WINDOW_VSCROLL = 1 << 5, - /** No horizontal scrollbar on the bottom edge. */ - wuss_WINDOW_NO_HSCROLL = 1 << 6, + /** Horizontal scrollbar on the bottom edge. */ + wuss_WINDOW_HSCROLL = 1 << 6, - /** No resize icon in the bottom-right corner. */ - wuss_WINDOW_NO_RESIZE = 1 << 7, + /** Resize icon in the bottom-right corner. */ + wuss_WINDOW_RESIZE = 1 << 7, + + /** Default: every furniture region drawn. */ + wuss_WINDOW_DEFAULT = wuss_WINDOW_TITLEBAR | wuss_WINDOW_OUTLINE | + wuss_WINDOW_CLOSE | wuss_WINDOW_BACK | + wuss_WINDOW_TOGGLE_SIZE | + wuss_WINDOW_VSCROLL | wuss_WINDOW_HSCROLL | + wuss_WINDOW_RESIZE, /** * A resize (drag or toggle-size) always fully redraws the window's content @@ -255,8 +260,8 @@ typedef enum wuss_window_flags * keeps its place in the z-order but is not drawn and not hit-tested, so * it neither occludes other windows nor catches the pointer. Its position * can still be changed with wuss_window_move while hidden, ready for when - * it is shown again. Unlike the wuss_WINDOW_NO_* bits this one is toggled - * at runtime, and it is honoured regardless of the WUSS_FURNITURE build + * it is shown again. Unlike the furniture bits this one is toggled at + * runtime, and it is honoured regardless of the WUSS_FURNITURE build * option. */ wuss_WINDOW_HIDDEN = 1 << 9, diff --git a/libraries/wuss/component/info.c b/libraries/wuss/component/info.c index 6002962b..aaa82ec7 100644 --- a/libraries/wuss/component/info.c +++ b/libraries/wuss/component/info.c @@ -36,8 +36,8 @@ #define INFO_FIELD_PAD 4 /* px each side of the text inside a value field */ #define INFO_MAX_ROWS 16 -/* The dialogue is one window on a task the caller passes in. It carries - * wuss_WINDOW_NO_CLOSE so a menu chain that borrows it as a +/* The dialogue is one window on a task the caller passes in. It omits + * wuss_WINDOW_CLOSE so a menu chain that borrows it as a * wuss_menu_item_t::window can't have it pulled from under it, and * wuss_info_destroy closes it explicitly. The task must therefore not be an * autoclose one -- its window list would never empty -- and must outlive the @@ -129,9 +129,7 @@ result_t wuss_info_create(wuss_info_t **out, rc = wuss_window_create(task, &content, title, - wuss_WINDOW_NO_BACK | wuss_WINDOW_NO_CLOSE | - wuss_WINDOW_NO_TOGGLE_SIZE | wuss_WINDOW_NO_VSCROLL | - wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE | + wuss_WINDOW_TITLEBAR | wuss_WINDOW_OUTLINE | wuss_WINDOW_NO_REDRAW | wuss_WINDOW_HIDDEN, wuss_BACKDROP_COLOUR(wuss_COLOUR_WINDOW), SIZE2D(w, h), diff --git a/libraries/wuss/core/impl.h b/libraries/wuss/core/impl.h index 55c9f59d..3e1bde44 100644 --- a/libraries/wuss/core/impl.h +++ b/libraries/wuss/core/impl.h @@ -600,7 +600,7 @@ static inline void wuss__min_content(const wuss_window_t *window, 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; + return (flags & wuss_WINDOW_TITLEBAR) ? wuss->titlebar_height : 0; } static inline int wuss__titlebar_height(const wuss_window_t *window) @@ -624,7 +624,7 @@ static inline void wuss__window_set_toggled(wuss_window_t *window, static inline int wuss__outline_px_for(wuss_window_flags_t flags) { - return (flags & wuss_WINDOW_NO_OUTLINE) ? 0 : 1; + return (flags & wuss_WINDOW_OUTLINE) ? 1 : 0; } static inline int wuss__outline_px(const wuss_window_t *window) @@ -633,7 +633,7 @@ static inline int wuss__outline_px(const wuss_window_t *window) } /* ponytail: falls back to wuss's own titlebar height when the window has - * none, so NO_TITLEBAR windows that still opt into scrollbars/resize match + * none, so titleless 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, @@ -664,12 +664,12 @@ static inline void wuss__furniture_carve_for(wuss_window_flags_t flags, int button_size, point_t *carve) { - carve->x = (flags & wuss_WINDOW_NO_VSCROLL) ? 0 : button_size; - carve->y = (flags & wuss_WINDOW_NO_HSCROLL) ? 0 : button_size; + carve->x = (flags & wuss_WINDOW_VSCROLL) ? button_size : 0; + carve->y = (flags & wuss_WINDOW_HSCROLL) ? button_size : 0; - if (!(flags & wuss_WINDOW_NO_RESIZE) && - (flags & wuss_WINDOW_NO_VSCROLL) && - (flags & wuss_WINDOW_NO_HSCROLL)) + if ((flags & wuss_WINDOW_RESIZE) && + !(flags & wuss_WINDOW_VSCROLL) && + !(flags & wuss_WINDOW_HSCROLL)) { carve->x = button_size; carve->y = button_size; diff --git a/libraries/wuss/core/scroll-step.c b/libraries/wuss/core/scroll-step.c index 23b653b4..146d3387 100644 --- a/libraries/wuss/core/scroll-step.c +++ b/libraries/wuss/core/scroll-step.c @@ -27,9 +27,9 @@ void wuss__scroll_step(wuss_window_t *window, point_t delta) /* A window that declared an axis non-scrollable (e.g. a pop-up menu) has no * scrollbar to clamp against and its geometry assumes scroll == 0, so a wheel * turn over it must not move the content -- doing so corrupts the display. */ - if (window->flags & wuss_WINDOW_NO_HSCROLL) + if (!(window->flags & wuss_WINDOW_HSCROLL)) delta.x = 0; - if (window->flags & wuss_WINDOW_NO_VSCROLL) + if (!(window->flags & wuss_WINDOW_VSCROLL)) delta.y = 0; if (delta.x == 0 && delta.y == 0) return; diff --git a/libraries/wuss/furniture/close-box.c b/libraries/wuss/furniture/close-box.c index 3de81478..18d3c1eb 100644 --- a/libraries/wuss/furniture/close-box.c +++ b/libraries/wuss/furniture/close-box.c @@ -13,7 +13,7 @@ void wuss__close_box(const wuss_window_t *window, box_t *out) size = wuss__button_size(window); out->x0 = titlebar.x0 + inset; - if (!(window->flags & wuss_WINDOW_NO_BACK)) + if (window->flags & wuss_WINDOW_BACK) out->x0 += size + inset; /* shift right, clear of the back icon */ out->y0 = titlebar.y0 + inset; out->x1 = out->x0 + size; @@ -30,7 +30,7 @@ void wuss__close_hit_box(const wuss_window_t *window, box_t *out) wuss__close_box(window, &drawn); - out->x0 = (window->flags & wuss_WINDOW_NO_BACK) ? window->visible.x0 : drawn.x0; + out->x0 = (window->flags & wuss_WINDOW_BACK) ? drawn.x0 : window->visible.x0; out->y0 = window->visible.y0; out->x1 = drawn.x1; out->y1 = drawn.y1; diff --git a/libraries/wuss/furniture/draw.c b/libraries/wuss/furniture/draw.c index 9d3bcd98..3a6a84c8 100644 --- a/libraries/wuss/furniture/draw.c +++ b/libraries/wuss/furniture/draw.c @@ -83,14 +83,14 @@ static void draw_title(wuss_t *wuss, return; text_x0 = titlebar->x0 + 2; - if (!(window->flags & wuss_WINDOW_NO_CLOSE)) + if (window->flags & wuss_WINDOW_CLOSE) { box_t close; wuss__close_box(window, &close); text_x0 = close.x1 + 2; } - else if (!(window->flags & wuss_WINDOW_NO_BACK)) + else if (window->flags & wuss_WINDOW_BACK) { box_t back; @@ -99,7 +99,7 @@ static void draw_title(wuss_t *wuss, } text_x1 = titlebar->x1 - 2; - if (!(window->flags & wuss_WINDOW_NO_TOGGLE_SIZE)) + if (window->flags & wuss_WINDOW_TOGGLE_SIZE) { box_t toggle; @@ -158,14 +158,14 @@ void wuss__furniture_draw(wuss_t *wuss, /* the two scrollbar sausages: geometry depends on window->scroll, so they * are computed live rather than cached */ - if (!(window->flags & wuss_WINDOW_NO_VSCROLL)) + if (window->flags & wuss_WINDOW_VSCROLL) { wuss__vscroll_sausage_box(window, &sausage); fill_furniture_rect(wuss, &sausage, full, wuss->palette[wuss->furniture_colours.scroll.sausages]); } - if (!(window->flags & wuss_WINDOW_NO_HSCROLL)) + if (window->flags & wuss_WINDOW_HSCROLL) { wuss__hscroll_sausage_box(window, &sausage); fill_furniture_rect(wuss, &sausage, full, diff --git a/libraries/wuss/furniture/hit-test.c b/libraries/wuss/furniture/hit-test.c index 04bc36c0..289f8a07 100644 --- a/libraries/wuss/furniture/hit-test.c +++ b/libraries/wuss/furniture/hit-test.c @@ -18,9 +18,9 @@ static wuss_furniture_region_t nearest_edge_region(const wuss_window_t *window, wuss__content_box(window, &content); - has_v = !(window->flags & wuss_WINDOW_NO_VSCROLL); - has_h = !(window->flags & wuss_WINDOW_NO_HSCROLL); - has_title = !(window->flags & wuss_WINDOW_NO_TITLEBAR); + has_v = (window->flags & wuss_WINDOW_VSCROLL) != 0; + has_h = (window->flags & wuss_WINDOW_HSCROLL) != 0; + has_title = (window->flags & wuss_WINDOW_TITLEBAR) != 0; /* bottom edge: the hscroll strip runs its full width */ if (p.y >= content.y1) @@ -59,23 +59,23 @@ wuss_furniture_region_t wuss__furniture_hit_test(const wuss_window_t *window, { box_t box; - if (!(window->flags & wuss_WINDOW_NO_TITLEBAR)) + if (window->flags & wuss_WINDOW_TITLEBAR) { - if (!(window->flags & wuss_WINDOW_NO_BACK)) + if (window->flags & wuss_WINDOW_BACK) { wuss__back_hit_box(window, &box); if (box_contains_point(&box, p.x, p.y)) return wuss_FURNITURE_BACK; } - if (!(window->flags & wuss_WINDOW_NO_CLOSE)) + if (window->flags & wuss_WINDOW_CLOSE) { wuss__close_hit_box(window, &box); if (box_contains_point(&box, p.x, p.y)) return wuss_FURNITURE_CLOSE; } - if (!(window->flags & wuss_WINDOW_NO_TOGGLE_SIZE)) + if (window->flags & wuss_WINDOW_TOGGLE_SIZE) { wuss__toggle_hit_box(window, &box); if (box_contains_point(&box, p.x, p.y)) @@ -87,14 +87,14 @@ wuss_furniture_region_t wuss__furniture_hit_test(const wuss_window_t *window, return wuss_FURNITURE_TITLE; } - if (!(window->flags & wuss_WINDOW_NO_RESIZE)) + if (window->flags & wuss_WINDOW_RESIZE) { wuss__resize_hit_box(window, &box); if (box_contains_point(&box, p.x, p.y)) return wuss_FURNITURE_RESIZE; } - if (!(window->flags & wuss_WINDOW_NO_VSCROLL)) + if (window->flags & wuss_WINDOW_VSCROLL) { wuss__vscroll_up_hit_box(window, &box); if (box_contains_point(&box, p.x, p.y)) @@ -109,7 +109,7 @@ wuss_furniture_region_t wuss__furniture_hit_test(const wuss_window_t *window, return wuss_FURNITURE_VSCROLL_WELL; } - if (!(window->flags & wuss_WINDOW_NO_HSCROLL)) + if (window->flags & wuss_WINDOW_HSCROLL) { wuss__hscroll_left_hit_box(window, &box); if (box_contains_point(&box, p.x, p.y)) diff --git a/libraries/wuss/furniture/invalidate.c b/libraries/wuss/furniture/invalidate.c index 7b668b39..3c81456f 100644 --- a/libraries/wuss/furniture/invalidate.c +++ b/libraries/wuss/furniture/invalidate.c @@ -27,7 +27,7 @@ void wuss__furniture_invalidate_for(wuss_window_t *window, * on still reserves both strips, for the resize icon and the rules. */ wuss__furniture_carve_for(window->flags, wuss__button_size(window), &carve); - if (!(window->flags & wuss_WINDOW_NO_TITLEBAR)) + if (window->flags & wuss_WINDOW_TITLEBAR) { box_t titlebar; diff --git a/libraries/wuss/furniture/layout.c b/libraries/wuss/furniture/layout.c index 2bf091bd..8584cd1e 100644 --- a/libraries/wuss/furniture/layout.c +++ b/libraries/wuss/furniture/layout.c @@ -49,7 +49,7 @@ void wuss__furniture_layout_build(wuss_window_t *window) /* titlebar fill + its icons ------------------------------------------- */ wuss__titlebar_box(window, &titlebar); - if (!(window->flags & wuss_WINDOW_NO_TITLEBAR)) + if (window->flags & wuss_WINDOW_TITLEBAR) { layout->titlebar = titlebar; layout->has_titlebar = 1; @@ -69,7 +69,7 @@ void wuss__furniture_layout_build(wuss_window_t *window) push_piece(layout, &rule, wuss__FURNITURE_PAINT_OUTLINE); } - if (!(window->flags & wuss_WINDOW_NO_CLOSE)) + if (window->flags & wuss_WINDOW_CLOSE) { box_t close; @@ -77,7 +77,7 @@ void wuss__furniture_layout_build(wuss_window_t *window) push_piece(layout, &close, wuss__FURNITURE_PAINT_CLOSE); } - if (!(window->flags & wuss_WINDOW_NO_BACK)) + if (window->flags & wuss_WINDOW_BACK) { box_t back; @@ -85,7 +85,7 @@ void wuss__furniture_layout_build(wuss_window_t *window) push_piece(layout, &back, wuss__FURNITURE_PAINT_BACK); } - if (!(window->flags & wuss_WINDOW_NO_TOGGLE_SIZE)) + if (window->flags & wuss_WINDOW_TOGGLE_SIZE) { box_t toggle; @@ -98,9 +98,9 @@ void wuss__furniture_layout_build(wuss_window_t *window) * Both scrollbars off but a resize icon on: the content box is still * carved back on its right and bottom and no scroll strip paints that * margin, so furniture fills it (see the note in the old draw.c). */ - if ((window->flags & wuss_WINDOW_NO_VSCROLL) && - (window->flags & wuss_WINDOW_NO_HSCROLL) && - !(window->flags & wuss_WINDOW_NO_RESIZE)) + if (!(window->flags & wuss_WINDOW_VSCROLL) && + !(window->flags & wuss_WINDOW_HSCROLL) && + (window->flags & wuss_WINDOW_RESIZE)) { box_t band; @@ -125,15 +125,15 @@ void wuss__furniture_layout_build(wuss_window_t *window) } /* resize icon + its dividing seams ---------------------------------- */ - if (!(window->flags & wuss_WINDOW_NO_RESIZE)) + if (window->flags & wuss_WINDOW_RESIZE) { box_t resize, rule; int top_seam, left_seam; wuss__resize_box(window, &resize); - top_seam = (window->flags & wuss_WINDOW_NO_VSCROLL) ? 0 : WUSS_DIVIDER_PX; - left_seam = (window->flags & wuss_WINDOW_NO_HSCROLL) ? 0 : WUSS_DIVIDER_PX; + top_seam = (window->flags & wuss_WINDOW_VSCROLL) ? WUSS_DIVIDER_PX : 0; + left_seam = (window->flags & wuss_WINDOW_HSCROLL) ? WUSS_DIVIDER_PX : 0; push_piece(layout, &resize, wuss__FURNITURE_PAINT_RESIZE); @@ -157,7 +157,7 @@ void wuss__furniture_layout_build(wuss_window_t *window) } /* vertical scrollbar: arrows + well (sausage is drawn live) --------- */ - if (!(window->flags & wuss_WINDOW_NO_VSCROLL)) + if (window->flags & wuss_WINDOW_VSCROLL) { box_t up, down, well; @@ -172,7 +172,7 @@ void wuss__furniture_layout_build(wuss_window_t *window) } /* horizontal scrollbar: arrows + well ----------------------------- */ - if (!(window->flags & wuss_WINDOW_NO_HSCROLL)) + if (window->flags & wuss_WINDOW_HSCROLL) { box_t left, right, well; @@ -210,7 +210,7 @@ void wuss__furniture_layout_build(wuss_window_t *window) } /* window outline: four one-pixel edges ---------------------------- */ - if (!(window->flags & wuss_WINDOW_NO_OUTLINE)) + if (window->flags & wuss_WINDOW_OUTLINE) { box_t edge; diff --git a/libraries/wuss/furniture/scroll-box.c b/libraries/wuss/furniture/scroll-box.c index c56d69d6..e15678ad 100644 --- a/libraries/wuss/furniture/scroll-box.c +++ b/libraries/wuss/furniture/scroll-box.c @@ -28,8 +28,8 @@ static void scroll_strip(const wuss_window_t *window, outline_px = wuss__outline_px(window); size = wuss__button_size(window); - has_resize = !(window->flags & wuss_WINDOW_NO_RESIZE); - has_vscroll = !(window->flags & wuss_WINDOW_NO_VSCROLL); + has_resize = (window->flags & wuss_WINDOW_RESIZE) != 0; + has_vscroll = (window->flags & wuss_WINDOW_VSCROLL) != 0; if (horizontal) { @@ -74,8 +74,8 @@ static void scroll_strip_hit(const wuss_window_t *window, outline_px = wuss__outline_px(window); size = wuss__button_size(window); - has_resize = !(window->flags & wuss_WINDOW_NO_RESIZE); - has_vscroll = !(window->flags & wuss_WINDOW_NO_VSCROLL); + has_resize = (window->flags & wuss_WINDOW_RESIZE) != 0; + has_vscroll = (window->flags & wuss_WINDOW_VSCROLL) != 0; wuss__furniture_carve_for(window->flags, size, &carve); if (horizontal) diff --git a/libraries/wuss/menu/menu.c b/libraries/wuss/menu/menu.c index 06291dba..cf229767 100644 --- a/libraries/wuss/menu/menu.c +++ b/libraries/wuss/menu/menu.c @@ -610,10 +610,7 @@ static result_t wuss__menu_spawn(wuss_t *wuss, if (menu_task == NULL) return result_OOM; - menu_flags = wuss_WINDOW_NO_CLOSE | wuss_WINDOW_NO_BACK - | wuss_WINDOW_NO_TOGGLE_SIZE - | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL - | wuss_WINDOW_NO_RESIZE; + menu_flags = wuss_WINDOW_TITLEBAR | wuss_WINDOW_OUTLINE; outline_px = wuss__outline_px_for(menu_flags); titlebar_height = wuss__titlebar_height_for(wuss, menu_flags); @@ -690,7 +687,7 @@ static result_t wuss__menu_spawn(wuss_t *wuss, if (doc_h > max_h) { height = max_h; - menu_flags &= (wuss_window_flags_t) ~wuss_WINDOW_NO_VSCROLL; + menu_flags |= wuss_WINDOW_VSCROLL; } else { diff --git a/libraries/wuss/test/tasks/blank.c b/libraries/wuss/test/tasks/blank.c index f1ef9b63..3d00f44d 100644 --- a/libraries/wuss/test/tasks/blank.c +++ b/libraries/wuss/test/tasks/blank.c @@ -42,7 +42,8 @@ result_t blank_create(wuss_t *wuss, blank_task_t *task) rc = wuss_window_create_placed(delegate, SIZE2D(200, 160), NULL, - wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE, + wuss_WINDOW_VSCROLL | wuss_WINDOW_HSCROLL | + wuss_WINDOW_RESIZE, wuss_BACKDROP_COLOUR(task->index), SIZE2D(200, 160), SIZE2D(0, 0), diff --git a/libraries/wuss/test/tasks/chars.c b/libraries/wuss/test/tasks/chars.c index 9974c35f..9139a158 100644 --- a/libraries/wuss/test/tasks/chars.c +++ b/libraries/wuss/test/tasks/chars.c @@ -183,7 +183,7 @@ result_t chars_create(wuss_t *wuss, rc = wuss_window_create_placed(delegate, grid, "Chars", - wuss_WINDOW_NO_HSCROLL, + wuss_WINDOW_DEFAULT & ~wuss_WINDOW_HSCROLL, wuss_NO_BACKDROP, grid, SIZE2D(64, 64), diff --git a/libraries/wuss/test/tasks/saturn.c b/libraries/wuss/test/tasks/saturn.c index 13589dc1..228de5f5 100644 --- a/libraries/wuss/test/tasks/saturn.c +++ b/libraries/wuss/test/tasks/saturn.c @@ -386,13 +386,9 @@ static result_t saturn_size_dialogue_create(saturn_task_t *task) rc = wuss_window_create_placed(task->delegate, sz, "Size", - wuss_WINDOW_HIDDEN | - wuss_WINDOW_NO_CLOSE | - wuss_WINDOW_NO_BACK | - wuss_WINDOW_NO_TOGGLE_SIZE | - wuss_WINDOW_NO_HSCROLL | - wuss_WINDOW_NO_VSCROLL | - wuss_WINDOW_NO_RESIZE | + wuss_WINDOW_TITLEBAR | + wuss_WINDOW_OUTLINE | + wuss_WINDOW_HIDDEN | wuss_WINDOW_NO_REDRAW, wuss_BACKDROP_COLOUR(wuss_COLOUR_WINDOW), sz, diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 8b3e64f4..cc04dcd2 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -674,9 +674,9 @@ result_t wuss_test(const char *resources) rc = wuss_window_create(delegate_a, &box_a, "A", - wuss_WINDOW_NO_BACK | wuss_WINDOW_NO_TOGGLE_SIZE | - wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | - wuss_WINDOW_NO_RESIZE, + wuss_WINDOW_DEFAULT & ~wuss_WINDOW_BACK & + ~wuss_WINDOW_TOGGLE_SIZE & ~wuss_WINDOW_VSCROLL & + ~wuss_WINDOW_HSCROLL & ~wuss_WINDOW_RESIZE, wuss_NO_BACKDROP, box_size(&box_a), SIZE2D(0, 0), @@ -695,9 +695,9 @@ result_t wuss_test(const char *resources) rc = wuss_window_create(delegate_b, &box_b, "B", - wuss_WINDOW_NO_BACK | wuss_WINDOW_NO_TOGGLE_SIZE | - wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | - wuss_WINDOW_NO_RESIZE, + wuss_WINDOW_DEFAULT & ~wuss_WINDOW_BACK & + ~wuss_WINDOW_TOGGLE_SIZE & ~wuss_WINDOW_VSCROLL & + ~wuss_WINDOW_HSCROLL & ~wuss_WINDOW_RESIZE, wuss_NO_BACKDROP, box_size(&box_b), SIZE2D(0, 0), @@ -1016,8 +1016,9 @@ result_t wuss_test(const char *resources) * visible top-left (10-1 outline) back to (0,0), then the clamp caps * the content at 200 - 2*1 - 20(titlebar) tall, 200 - 2*1 wide. */ box_big.x0 = 10; box_big.y0 = 10; box_big.x1 = 400; box_big.y1 = 400; - rc = wuss_window_create(mk_task(wuss, NULL, NULL), &box_big, "BIG", wuss_WINDOW_NO_VSCROLL | - wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, + rc = wuss_window_create(mk_task(wuss, NULL, NULL), &box_big, "BIG", + wuss_WINDOW_DEFAULT & ~wuss_WINDOW_VSCROLL & + ~wuss_WINDOW_HSCROLL & ~wuss_WINDOW_RESIZE, wuss_NO_BACKDROP, SIZE2D(400, 400), SIZE2D(0, 0), &win_big); if (rc != result_OK) @@ -1041,9 +1042,7 @@ result_t wuss_test(const char *resources) rc = wuss_window_create(delegate_d, &box_d, "ignored", - wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | - wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | - wuss_WINDOW_NO_RESIZE, + 0, /* fully chromeless: no titlebar, outline, scrollbars or resize */ wuss_NO_BACKDROP, box_size(&box_d), SIZE2D(0, 0), @@ -1088,9 +1087,7 @@ result_t wuss_test(const char *resources) rc = wuss_window_create(delegate_e, &box_e, NULL, - wuss_WINDOW_NO_TITLEBAR | - wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | - wuss_WINDOW_NO_RESIZE, + wuss_WINDOW_OUTLINE, wuss_NO_BACKDROP, box_size(&box_e), SIZE2D(0, 0), @@ -1108,9 +1105,7 @@ result_t wuss_test(const char *resources) rc = wuss_window_create(delegate_f, &box_f, NULL, - wuss_WINDOW_NO_TITLEBAR | - wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | - wuss_WINDOW_NO_RESIZE, + wuss_WINDOW_OUTLINE, wuss_NO_BACKDROP, box_size(&box_f), SIZE2D(0, 0), @@ -1175,9 +1170,7 @@ result_t wuss_test(const char *resources) rc = wuss_window_create(delegate_h, &box_h, NULL, - wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | - wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | - wuss_WINDOW_NO_RESIZE, + 0, /* fully chromeless: no titlebar, outline, scrollbars or resize */ wuss_NO_BACKDROP, box_size(&box_h), SIZE2D(0, 0), @@ -1195,9 +1188,7 @@ result_t wuss_test(const char *resources) rc = wuss_window_create(delegate_g, &box_g, NULL, - wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | - wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | - wuss_WINDOW_NO_RESIZE, + 0, /* fully chromeless: no titlebar, outline, scrollbars or resize */ wuss_NO_BACKDROP, box_size(&box_g), SIZE2D(0, 0), @@ -1256,9 +1247,7 @@ result_t wuss_test(const char *resources) rc = wuss_window_create(delegate_i, &box_i, NULL, - wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | - wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | - wuss_WINDOW_NO_RESIZE, + 0, /* fully chromeless: no titlebar, outline, scrollbars or resize */ wuss_NO_BACKDROP, box_size(&box_i), SIZE2D(0, 0), @@ -1276,9 +1265,7 @@ result_t wuss_test(const char *resources) rc = wuss_window_create(delegate_j, &box_j, NULL, - wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | - wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | - wuss_WINDOW_NO_RESIZE, + 0, /* fully chromeless: no titlebar, outline, scrollbars or resize */ wuss_NO_BACKDROP, box_size(&box_j), SIZE2D(0, 0), @@ -1326,9 +1313,7 @@ result_t wuss_test(const char *resources) rc = wuss_window_create(delegate_m, &box_m, NULL, - wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | - wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | - wuss_WINDOW_NO_RESIZE, + 0, /* fully chromeless: no titlebar, outline, scrollbars or resize */ wuss_NO_BACKDROP, box_size(&box_m), SIZE2D(0, 0), @@ -1398,7 +1383,8 @@ result_t wuss_test(const char *resources) rc = wuss_window_create(delegate_g, &box_g, "G", - wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL, + wuss_WINDOW_DEFAULT & ~wuss_WINDOW_VSCROLL & + ~wuss_WINDOW_HSCROLL, wuss_NO_BACKDROP, box_size(&box_g), SIZE2D(0, 0), @@ -2962,7 +2948,8 @@ result_t wuss_test(const char *resources) box_nb.x0 = 10; box_nb.y0 = 10; box_nb.x1 = 50; box_nb.y1 = 50; /* 40x40 content, room to grow to a 200x200 doc */ - rc = wuss_window_create(delegate_nb, &box_nb, "NB", wuss_WINDOW_NO_RESIZE_BLIT, + rc = wuss_window_create(delegate_nb, &box_nb, "NB", + wuss_WINDOW_DEFAULT | wuss_WINDOW_NO_RESIZE_BLIT, wuss_NO_BACKDROP, SIZE2D(200, 200), SIZE2D(0, 0), &win_nb); if (rc != result_OK) @@ -3497,9 +3484,9 @@ result_t wuss_test(const char *resources) rc = wuss_window_create(delegate_k, &box_k, "K", - wuss_WINDOW_NO_BACK | wuss_WINDOW_NO_TOGGLE_SIZE | - wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | - wuss_WINDOW_NO_RESIZE, + wuss_WINDOW_DEFAULT & ~wuss_WINDOW_BACK & + ~wuss_WINDOW_TOGGLE_SIZE & ~wuss_WINDOW_VSCROLL & + ~wuss_WINDOW_HSCROLL & ~wuss_WINDOW_RESIZE, wuss_NO_BACKDROP, box_size(&box_k), SIZE2D(0, 0), @@ -3517,9 +3504,9 @@ result_t wuss_test(const char *resources) rc = wuss_window_create(delegate_l, &box_l, "L", - wuss_WINDOW_NO_BACK | wuss_WINDOW_NO_TOGGLE_SIZE | - wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | - wuss_WINDOW_NO_RESIZE, + wuss_WINDOW_DEFAULT & ~wuss_WINDOW_BACK & + ~wuss_WINDOW_TOGGLE_SIZE & ~wuss_WINDOW_VSCROLL & + ~wuss_WINDOW_HSCROLL & ~wuss_WINDOW_RESIZE, wuss_NO_BACKDROP, box_size(&box_l), SIZE2D(0, 0), @@ -3584,8 +3571,9 @@ result_t wuss_test(const char *resources) rc = wuss_window_create(delegate_m2, &box_m2, "M2", - wuss_WINDOW_NO_BACK | wuss_WINDOW_NO_TOGGLE_SIZE | - wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL, + wuss_WINDOW_DEFAULT & ~wuss_WINDOW_BACK & + ~wuss_WINDOW_TOGGLE_SIZE & ~wuss_WINDOW_VSCROLL & + ~wuss_WINDOW_HSCROLL, wuss_NO_BACKDROP, box_size(&box_m2), SIZE2D(0, 0), @@ -3675,7 +3663,8 @@ result_t wuss_test(const char *resources) box_nb2.x0 = 0; box_nb2.y0 = 0; box_nb2.x1 = 40; box_nb2.y1 = 40; - rc = wuss_window_create(delegate_nb2, &box_nb2, "NB2", wuss_WINDOW_NO_RESIZE_BLIT, + rc = wuss_window_create(delegate_nb2, &box_nb2, "NB2", + wuss_WINDOW_DEFAULT | wuss_WINDOW_NO_RESIZE_BLIT, wuss_NO_BACKDROP, box_size(&box_nb2), SIZE2D(0, 0), @@ -3724,7 +3713,8 @@ result_t wuss_test(const char *resources) box_fl.x0 = 0; box_fl.y0 = 0; box_fl.x1 = 60; box_fl.y1 = 60; - rc = wuss_window_create(delegate_fl, &box_fl, "FL", wuss_WINDOW_NO_RESIZE_BLIT, + rc = wuss_window_create(delegate_fl, &box_fl, "FL", + wuss_WINDOW_DEFAULT | wuss_WINDOW_NO_RESIZE_BLIT, wuss_NO_BACKDROP, box_size(&box_fl), SIZE2D(0, 0), @@ -3787,9 +3777,9 @@ result_t wuss_test(const char *resources) box_n.x0 = 0; box_n.y0 = 140; /* clear of any occluder to start */ box_n.x1 = 60; box_n.y1 = 170; rc = wuss_window_create(delegate_n, &box_n, "N", - wuss_WINDOW_NO_BACK | wuss_WINDOW_NO_TOGGLE_SIZE | - wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | - wuss_WINDOW_NO_RESIZE, + wuss_WINDOW_DEFAULT & ~wuss_WINDOW_BACK & + ~wuss_WINDOW_TOGGLE_SIZE & ~wuss_WINDOW_VSCROLL & + ~wuss_WINDOW_HSCROLL & ~wuss_WINDOW_RESIZE, wuss_NO_BACKDROP, box_size(&box_n), SIZE2D(0, 0), @@ -3805,9 +3795,9 @@ result_t wuss_test(const char *resources) box_o.x0 = 90; box_o.y0 = 140; /* N will be dragged partly on top of O */ box_o.x1 = 130; box_o.y1 = 180; rc = wuss_window_create(delegate_o, &box_o, "O", - wuss_WINDOW_NO_BACK | wuss_WINDOW_NO_TOGGLE_SIZE | - wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | - wuss_WINDOW_NO_RESIZE, + wuss_WINDOW_DEFAULT & ~wuss_WINDOW_BACK & + ~wuss_WINDOW_TOGGLE_SIZE & ~wuss_WINDOW_VSCROLL & + ~wuss_WINDOW_HSCROLL & ~wuss_WINDOW_RESIZE, wuss_NO_BACKDROP, box_size(&box_o), SIZE2D(0, 0), @@ -3889,9 +3879,7 @@ result_t wuss_test(const char *resources) box_b.x0 = 20; box_b.y0 = 10; /* left half will sit under A */ box_b.x1 = 80; box_b.y1 = 50; rc = wuss_window_create(delegate_b, &box_b, "B", - wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | - wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | - wuss_WINDOW_NO_RESIZE, + 0, /* fully chromeless: no titlebar, outline, scrollbars or resize */ wuss_NO_BACKDROP, box_size(&box_b), SIZE2D(0, 0), @@ -3907,9 +3895,7 @@ result_t wuss_test(const char *resources) box_a.x0 = 0; box_a.y0 = 0; /* created after B, so A is topmost */ box_a.x1 = 40; box_a.y1 = 100; rc = wuss_window_create(delegate_a, &box_a, "A", - wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | - wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | - wuss_WINDOW_NO_RESIZE, + 0, /* fully chromeless: no titlebar, outline, scrollbars or resize */ wuss_NO_BACKDROP, box_size(&box_a), SIZE2D(0, 0), @@ -3990,9 +3976,7 @@ result_t wuss_test(const char *resources) box_b.x0 = 0; box_b.y0 = 0; /* right part sits under A throughout */ box_b.x1 = 60; box_b.y1 = 40; rc = wuss_window_create(delegate_b, &box_b, "B", - wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | - wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | - wuss_WINDOW_NO_RESIZE, + 0, /* fully chromeless: no titlebar, outline, scrollbars or resize */ wuss_NO_BACKDROP, box_size(&box_b), SIZE2D(0, 0), @@ -4008,9 +3992,7 @@ result_t wuss_test(const char *resources) box_a.x0 = 40; box_a.y0 = 0; /* created after B, so A is topmost */ box_a.x1 = 100; box_a.y1 = 40; rc = wuss_window_create(delegate_a, &box_a, "A", - wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | - wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | - wuss_WINDOW_NO_RESIZE, + 0, /* fully chromeless: no titlebar, outline, scrollbars or resize */ wuss_NO_BACKDROP, box_size(&box_a), SIZE2D(0, 0), @@ -4068,9 +4050,7 @@ result_t wuss_test(const char *resources) box_b.x0 = 0; box_b.y0 = 0; /* middle band sits under A */ box_b.x1 = 60; box_b.y1 = 60; rc = wuss_window_create(delegate_b, &box_b, "B", - wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | - wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | - wuss_WINDOW_NO_RESIZE, + 0, /* fully chromeless: no titlebar, outline, scrollbars or resize */ wuss_NO_BACKDROP, box_size(&box_b), SIZE2D(0, 0), @@ -4086,9 +4066,7 @@ result_t wuss_test(const char *resources) box_a.x0 = 0; box_a.y0 = 20; /* created after B, so A is topmost */ box_a.x1 = 60; box_a.y1 = 40; rc = wuss_window_create(delegate_a, &box_a, "A", - wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | - wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | - wuss_WINDOW_NO_RESIZE, + 0, /* fully chromeless: no titlebar, outline, scrollbars or resize */ wuss_NO_BACKDROP, box_size(&box_a), SIZE2D(0, 0), @@ -4179,9 +4157,7 @@ result_t wuss_test(const char *resources) box_b.x0 = 80; box_b.y0 = 80; /* corner already under A */ box_b.x1 = 140; box_b.y1 = 140; rc = wuss_window_create(delegate_b, &box_b, "B", - wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | - wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | - wuss_WINDOW_NO_RESIZE, + 0, /* fully chromeless: no titlebar, outline, scrollbars or resize */ wuss_NO_BACKDROP, box_size(&box_b), SIZE2D(0, 0), @@ -4197,9 +4173,7 @@ result_t wuss_test(const char *resources) box_a.x0 = 0; box_a.y0 = 0; /* created after B, so A is topmost */ box_a.x1 = 100; box_a.y1 = 100; rc = wuss_window_create(delegate_a, &box_a, "A", - wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | - wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | - wuss_WINDOW_NO_RESIZE, + 0, /* fully chromeless: no titlebar, outline, scrollbars or resize */ wuss_NO_BACKDROP, box_size(&box_a), SIZE2D(0, 0), @@ -4273,9 +4247,9 @@ result_t wuss_test(const char *resources) rc = wuss_window_create(delegate_c, &box_c, "C", - wuss_WINDOW_NO_BACK | wuss_WINDOW_NO_TOGGLE_SIZE | - wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | - wuss_WINDOW_NO_RESIZE, + wuss_WINDOW_DEFAULT & ~wuss_WINDOW_BACK & + ~wuss_WINDOW_TOGGLE_SIZE & ~wuss_WINDOW_VSCROLL & + ~wuss_WINDOW_HSCROLL & ~wuss_WINDOW_RESIZE, wuss_NO_BACKDROP, box_size(&box_c), SIZE2D(0, 0), @@ -5320,19 +5294,13 @@ ColourMenuOK: ; bb.x0 = 130; bb.y0 = 130; bb.x1 = 190; bb.y1 = 190; rc = wuss_window_create(task_a, &ba, "A", - 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, + 0, /* fully chromeless */ wuss_NO_BACKDROP, box_size(&ba), SIZE2D(0, 0), &wa); if (rc != result_OK) goto MoveDestroy; rc = wuss_window_create(task_b, &bb, "B", - 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, + 0, /* fully chromeless */ wuss_NO_BACKDROP, box_size(&bb), SIZE2D(0, 0), &wb); if (rc != result_OK) goto MoveDestroy; @@ -5458,10 +5426,7 @@ ColourMenuOK: ; bq.x0 = 6; bq.y0 = 6; bq.x1 = 60; bq.y1 = 60; rc = wuss_window_create(task_q, &bq, "Q", - wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | - wuss_WINDOW_NO_CLOSE | wuss_WINDOW_NO_BACK | - wuss_WINDOW_NO_TOGGLE_SIZE | wuss_WINDOW_NO_VSCROLL | - wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, + 0, /* fully chromeless */ wuss_NO_BACKDROP, box_size(&bq), SIZE2D(0, 0), &wq); if (rc != result_OK) goto QuitDestroy; @@ -5495,10 +5460,7 @@ ColourMenuOK: ; if (task_q == NULL) { rc = result_OOM; goto QuitDestroy; } qmt.self = task_q; rc = wuss_window_create(task_q, &bq, "Q2", - wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | - wuss_WINDOW_NO_CLOSE | wuss_WINDOW_NO_BACK | - wuss_WINDOW_NO_TOGGLE_SIZE | wuss_WINDOW_NO_VSCROLL | - wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, + 0, /* fully chromeless */ wuss_NO_BACKDROP, box_size(&bq), SIZE2D(0, 0), &wq); if (rc != result_OK) goto QuitDestroy; @@ -5787,15 +5749,17 @@ ColourMenuOK: ; { static const wuss_window_flags_t combos[] = { - wuss_WINDOW_NO_OUTLINE, - wuss_WINDOW_NO_RESIZE, - wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL, - wuss_WINDOW_NO_VSCROLL, /* one scrollbar + resize: carve is 0 on the */ - wuss_WINDOW_NO_HSCROLL, /* stripless axis, so the icon's near edge is */ - wuss_WINDOW_NO_BACK, /* the only thing sizing its hit box */ - wuss_WINDOW_NO_TOGGLE_SIZE, - wuss_WINDOW_NO_CLOSE, - wuss_WINDOW_NO_TITLEBAR + wuss_WINDOW_DEFAULT & ~wuss_WINDOW_OUTLINE, + wuss_WINDOW_DEFAULT & ~wuss_WINDOW_RESIZE, + wuss_WINDOW_DEFAULT & ~wuss_WINDOW_VSCROLL & ~wuss_WINDOW_HSCROLL, + /* one scrollbar + resize: carve is 0 on the stripless axis, so the + * icon's near edge is the only thing sizing its hit box */ + wuss_WINDOW_DEFAULT & ~wuss_WINDOW_VSCROLL, + wuss_WINDOW_DEFAULT & ~wuss_WINDOW_HSCROLL, + wuss_WINDOW_DEFAULT & ~wuss_WINDOW_BACK, + wuss_WINDOW_DEFAULT & ~wuss_WINDOW_TOGGLE_SIZE, + wuss_WINDOW_DEFAULT & ~wuss_WINDOW_CLOSE, + wuss_WINDOW_DEFAULT & ~wuss_WINDOW_TITLEBAR }; static test_task_t tc_cb; wuss_task_t *delegate_cb; @@ -5820,10 +5784,10 @@ ColourMenuOK: ; if (rc != result_OK) goto Failure; - /* NO_TITLEBAR still keeps a 1px top frame with nothing behind it: a + /* No titlebar still keeps a 1px top frame with nothing behind it: a * click there is the documented CONTENT exception. */ - leak = (combos[i] & wuss_WINDOW_NO_TITLEBAR) && - !(combos[i] & wuss_WINDOW_NO_OUTLINE); + leak = !(combos[i] & wuss_WINDOW_TITLEBAR) && + (combos[i] & wuss_WINDOW_OUTLINE); if (!furniture_hit_sweep(win_cb, leak)) { @@ -5835,7 +5799,7 @@ ColourMenuOK: ; /* Where the window keeps its resize icon, the icon's own drawn centre * must hit RESIZE. The sweep alone misses a collapsed hit box: those * pixels just fall to a neighbouring region, still leak-free. */ - if (!(combos[i] & wuss_WINDOW_NO_RESIZE)) + if (combos[i] & wuss_WINDOW_RESIZE) { box_t resize; @@ -5850,7 +5814,7 @@ ColourMenuOK: ; } } - if ((combos[i] & wuss_WINDOW_NO_BACK) && + if (!(combos[i] & wuss_WINDOW_BACK) && wuss__furniture_hit_test(win_cb, POINT(box_cb.x0, box_cb.y0)) != wuss_FURNITURE_CLOSE) { wuss_window_get_visible_bounds(win_cb, &visible); @@ -6170,10 +6134,7 @@ result_t wuss_test(const char *resources) /* 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; + const wuss_window_flags_t chromeless = 0; NOT_USED(resources); From 2f59a69ed97e41392d64983e8562b724e8650353 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Mon, 14 Sep 2026 21:08:23 +0100 Subject: [PATCH 069/128] fix(wuss): slider groove derives from the surround box, not the bevel box Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/icon/draw.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/wuss/icon/draw.c b/libraries/wuss/icon/draw.c index ab713338..a898b957 100644 --- a/libraries/wuss/icon/draw.c +++ b/libraries/wuss/icon/draw.c @@ -639,7 +639,7 @@ static void wuss__icon_draw_slider(const icon_draw_ctx_t *c) screen_draw_bevel_edge(c->scr, &c->b, dark, light); - wuss__slider_groove_box(&c->b, &groove); + wuss__slider_groove_box(&surround_box, &groove); /* draw the track */ From 01715ff7d333b92d9a467165e746bc35db3de902 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Mon, 14 Sep 2026 21:25:02 +0100 Subject: [PATCH 070/128] refactor(wuss): drop redundant size-dialogue redraw guard in saturn_handle The window already carries wuss_WINDOW_NO_REDRAW, so wuss never delivers the event for it; the in-handler check was dead code. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/test/tasks/saturn.c | 4 ---- 1 file changed, 4 deletions(-) diff --git a/libraries/wuss/test/tasks/saturn.c b/libraries/wuss/test/tasks/saturn.c index 228de5f5..65fdd169 100644 --- a/libraries/wuss/test/tasks/saturn.c +++ b/libraries/wuss/test/tasks/saturn.c @@ -587,10 +587,6 @@ result_t saturn_handle(wuss_window_t *window, switch (event->kind) { case wuss_EVENT_REDRAW: - /* the size dialogue has no content of its own -- just its icons, which - * wuss already drew */ - if (window == task->size_dialogue) - return result_OK; return saturn_redraw(event, task); case wuss_EVENT_MOUSE: From 5b5e9be47a735440821d445e9b5bba600ae81c44 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Mon, 14 Sep 2026 21:36:43 +0100 Subject: [PATCH 071/128] style: order static const locals before regular locals in scope static const declarations at the top of a scope now come first, followed by a blank line, then the regular variable locals, each group ordered by first use. --- apps/wuss/main.c | 8 ++++---- libraries/databases/pickle/pickle.c | 1 + libraries/framebuf/bmfont/test/bmfont-test.c | 2 +- libraries/geom/packer/test/packer-test.c | 4 ++-- libraries/utils/pack/test/pack-test.c | 18 +++++++++--------- libraries/wuss/test/tasks/icons.c | 1 + libraries/wuss/test/tasks/palette.c | 1 + libraries/wuss/test/tasks/saturn.c | 1 + libraries/wuss/test/wuss-test.c | 3 +++ 9 files changed, 23 insertions(+), 16 deletions(-) diff --git a/apps/wuss/main.c b/apps/wuss/main.c index 155377dd..b86cd5d7 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -273,15 +273,15 @@ static result_t run_wuss(const char *resources, int depth, int scale) { - const int scr_width = 640; - const int scr_height = 480; + static const char *const names[WUSS_MAIN_NFONTS] = + { "DPT-Digits-Regular", "DPT-Digits-Bold", "Symbols" }; + const int scr_width = 640; + const int scr_height = 480; result_t rc; const char *leafname; const char *filename; bmfont_t *fonts[WUSS_MAIN_NFONTS]; - static const char *const names[WUSS_MAIN_NFONTS] = - { "DPT-Digits-Regular", "DPT-Digits-Bold", "Symbols" }; int nfonts; int i; void *pixels; diff --git a/libraries/databases/pickle/pickle.c b/libraries/databases/pickle/pickle.c index eadb172f..0feb5f75 100644 --- a/libraries/databases/pickle/pickle.c +++ b/libraries/databases/pickle/pickle.c @@ -27,6 +27,7 @@ static result_t pickle__write_header(FILE *f, size_t commentslen) { static const char commentchar[] = "# "; + int rc; rc = fprintf(f, "%s%.*s\n%s\n", diff --git a/libraries/framebuf/bmfont/test/bmfont-test.c b/libraries/framebuf/bmfont/test/bmfont-test.c index 7b9f1625..9972810b 100644 --- a/libraries/framebuf/bmfont/test/bmfont-test.c +++ b/libraries/framebuf/bmfont/test/bmfont-test.c @@ -970,10 +970,10 @@ static result_t bmfont_monospace_test(const char *resources) { static const char sample[] = "WiWiWi.1jm"; + result_t rc; const char *leafname; const char *filename; bmfont_t *bmfont = NULL; - result_t rc; bmfont_width_t onechar; bmfont_width_t prev; int i; diff --git a/libraries/geom/packer/test/packer-test.c b/libraries/geom/packer/test/packer-test.c index ac0015bc..fae67334 100644 --- a/libraries/geom/packer/test/packer-test.c +++ b/libraries/geom/packer/test/packer-test.c @@ -405,10 +405,10 @@ static int test3(void) { static const box_t pagedims = { 0, 0, 100, 100 }; + result_t err; packer_t *packer; const box_t *a, *b, *c; box_t freed; - result_t err; printf("test3: packer_release\n"); @@ -542,12 +542,12 @@ static int test4(void) static const int w[6] = { 40, 55, 40, 70, 40, 50 }; static const int h[6] = { 30, 40, 30, 35, 30, 45 }; + result_t err; packer_t *packer; const box_t *slot; box_t held[6]; box_t full; struct scan s; - result_t err; int cycle; int i; diff --git a/libraries/utils/pack/test/pack-test.c b/libraries/utils/pack/test/pack-test.c index 64d7e04a..2838bd47 100644 --- a/libraries/utils/pack/test/pack-test.c +++ b/libraries/utils/pack/test/pack-test.c @@ -28,13 +28,13 @@ result_t pack_test(const char *resources) }; static const char fmt[] = "2c2s2i"; - const size_t sz = 14; - unsigned char buf[100]; - size_t n; - signed char c1, c2; - short s1, s2; - int l1, l2; + const size_t sz = 14; + unsigned char buf[100]; + size_t n; + signed char c1, c2; + short s1, s2; + int l1, l2; n = pack(buf, fmt, data[0], data[1], data[2], data[3], data[4], data[5]); if (n != sz) @@ -103,10 +103,10 @@ result_t pack_test(const char *resources) 0x00, 0xff }; static const char fmt[] = "*c*s*i"; - const size_t sz = 2 + 4 + 8; - unsigned char buf[100]; - size_t n; + const size_t sz = 2 + 4 + 8; + unsigned char buf[100]; + size_t n; n = pack(buf, fmt, 2, cdata, 2, sdata, 2, ldata); if (n != sz) diff --git a/libraries/wuss/test/tasks/icons.c b/libraries/wuss/test/tasks/icons.c index e46db0c0..4409b5f0 100644 --- a/libraries/wuss/test/tasks/icons.c +++ b/libraries/wuss/test/tasks/icons.c @@ -287,6 +287,7 @@ static void icons_add_bitmaps(icons_layout_t *lay, static void icons_add_iconset(icons_layout_t *lay, const wuss_t *wuss) { static const char *const names[4] = { "opton", "optoff", "radon", "radoff" }; + wuss_icon_spec_t *s; int top; int idx[4]; diff --git a/libraries/wuss/test/tasks/palette.c b/libraries/wuss/test/tasks/palette.c index 592113e1..7e620198 100644 --- a/libraries/wuss/test/tasks/palette.c +++ b/libraries/wuss/test/tasks/palette.c @@ -242,6 +242,7 @@ static result_t palette_redraw_screen(palette_task_t *pc, if (scr->palette == NULL) { static const char label[] = "32bpp (none)"; + bmfont_t *font = wuss_get_font(pc->wuss); colour_t ink = colour_rgb(0xFF, 0xFF, 0xFF); colour_t bg = colour_rgba(0, 0, 0, 0); /* transparent */ diff --git a/libraries/wuss/test/tasks/saturn.c b/libraries/wuss/test/tasks/saturn.c index 65fdd169..12edbf7b 100644 --- a/libraries/wuss/test/tasks/saturn.c +++ b/libraries/wuss/test/tasks/saturn.c @@ -118,6 +118,7 @@ result_t saturn_create(wuss_t *wuss, const saturn_config_t *config) { static const saturn_config_t default_config = SATURN_CONFIG_DEFAULT; + result_t rc; wuss_task_t *delegate; wuss_task_desc_t delegate_desc; diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index cc04dcd2..64ff7e09 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -588,6 +588,7 @@ result_t wuss_test(const char *resources) { 0xFF0000FF }, { 0xFF00FF00 }, { 0xFFFF0000 }, { 0xFFFFFFFF }, { 0xFF000000 } }; + wuss_config_t symcfg; wuss_t *symw; wuss_window_t *symwin; @@ -4722,6 +4723,7 @@ result_t wuss_test(const char *resources) { "Help", borrowed_items, NELEMS(borrowed_items) }; + wuss_menu_t *m; const wuss_menu_t *sub; @@ -5761,6 +5763,7 @@ ColourMenuOK: ; wuss_WINDOW_DEFAULT & ~wuss_WINDOW_CLOSE, wuss_WINDOW_DEFAULT & ~wuss_WINDOW_TITLEBAR }; + static test_task_t tc_cb; wuss_task_t *delegate_cb; box_t box_cb, visible; From ff0394cd6e9d154d0a4260d125d24b5e775d8a42 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Mon, 14 Sep 2026 22:34:47 +0100 Subject: [PATCH 072/128] refactor(wuss): scale saturn's sample space with window size task->config.size now drives the sketch's sample space directly (half/range/flip/energy_shift, plot clip, body-disc radius) instead of a fixed 256 SATURN_SIZE; SATURN_SIZE_DEFAULT remains only as the fallback in SATURN_CONFIG_DEFAULT. Also fixes the Apply button in the Size dialogue: it now sets the window's doc extent alongside the resize (previously left stale), and adds Adjust-click handling per RISC OS convention -- Adjust-Cancel resets the dialogue to the live config, Adjust-Apply applies without dismissing. --- libraries/wuss/test/tasks/saturn.c | 169 +++++++++++++++++------------ 1 file changed, 101 insertions(+), 68 deletions(-) diff --git a/libraries/wuss/test/tasks/saturn.c b/libraries/wuss/test/tasks/saturn.c index 12edbf7b..19aa8da3 100644 --- a/libraries/wuss/test/tasks/saturn.c +++ b/libraries/wuss/test/tasks/saturn.c @@ -42,20 +42,17 @@ * x = sqrt(16384-P)/2 -> planet body */ -/* the sample space below (and so the sketch itself) is always 256 pixels - * square, matching the original's -128..127 signed-byte range; the window's - * on-screen size (task->config.size, set via the Size... dialogue) scales - * independently, so a larger window just shows more margin around the same - * sketch and a smaller one clips it. */ -#define SATURN_SIZE 256 +/* the sample space matches task->config.size (square), scaling the original + * -128..127 signed-byte range up or down with it; SATURN_SIZE_DEFAULT is + * only the value saturn_create falls back to when the caller passes NULL + * (see SATURN_CONFIG_DEFAULT in saturn.h). */ +#define SATURN_SIZE_DEFAULT 256 -/* The original works in a -128..127 sample space (BBC BASIC signed byte). */ -#define SATURN_HALF (SATURN_SIZE / 2) /* sample-space centre / bias */ -#define SATURN_RANGE (SATURN_SIZE - 1) /* saturn_rnd() span: 1..255, then -HALF */ -#define SATURN_FLIP (SATURN_SIZE - 1) /* RISC OS bottom-up -> wuss top-down: FLIP - v */ -#define SATURN_ENERGY_SHIFT SATURN_SIZE /* (x*x+y*y) energy divisor */ +/* The original works in a -128..127 sample space (BBC BASIC signed byte), + * here scaled to whatever size is live; saturn_redraw derives these per-call + * as half/range/flip/energy_shift from task->config.size. */ -#define SATURN_BODY_R2 16384 /* planet body: keep if r1*r1+r2*r2 < this */ +#define SATURN_BODY_R2(half) ((half) * (half)) /* planet body: keep if r1*r1+r2*r2 < this */ /* BBC BASIC RND(n>0) returns an integer 1..n. utils/rng's LCG stands in for * it (its high bits, which is where an LCG's usable randomness sits) so a @@ -72,8 +69,8 @@ static int saturn_rnd(int n) return (int) ((rng_lcg32(&saturn_rnd_state) >> 16) % (uint32_t) n) + 1; } -/* one signed sample in the original -128..127 space */ -#define SATURN_SAMPLE() (saturn_rnd(SATURN_RANGE) - SATURN_HALF) +/* one signed sample in the original -128..127 space, scaled to half/range */ +#define SATURN_SAMPLE(half, range) (saturn_rnd(range) - (half)) /* MENU click over the content pops this. "Colours" leads to a submenu with * one row per task->fg/task->bg, each of which pops a wuss_colourmenu (see @@ -196,11 +193,12 @@ result_t saturn_create(wuss_t *wuss, static void saturn_plot(screen_t *scr, int ox, int oy, + int size, int x, int y, colour_t c) { - if (x < 0 || x >= SATURN_SIZE || y < 0 || y >= SATURN_SIZE) + if (x < 0 || x >= size || y < 0 || y >= size) return; screen_set_pixel(scr, ox + x, oy + y, c); @@ -210,6 +208,7 @@ static result_t saturn_redraw(const wuss_event_t *event, saturn_task_t *task) { screen_t *scr; const box_t *content, *bounds; + int size, half, range, flip, energy_shift; int ox, oy; int i; int x, y, p; @@ -220,8 +219,16 @@ static result_t saturn_redraw(const wuss_event_t *event, saturn_task_t *task) screen_fill_rect(scr, content->x0, content->y0, box_size(content), task->bg); - /* plot in doc space (saturn_plot clips to 0..SATURN_SIZE); origin carries - * the scroll so a scrolled/shrunk window shows the right slice */ + /* sample space scales with the window: -size/2..size/2-1, matching the + * original's -128..127 at task->config.size == SATURN_SIZE_DEFAULT */ + size = task->config.size; + half = size / 2; + range = size - 1; + flip = size - 1; + energy_shift = size; + + /* plot in doc space (saturn_plot clips to 0..size); origin carries the + * scroll so a scrolled/shrunk window shows the right slice */ ox = bounds->x0 - event->data.redraw.scroll.x; oy = bounds->y0 - event->data.redraw.scroll.y; @@ -230,12 +237,11 @@ static result_t saturn_redraw(const wuss_event_t *event, saturn_task_t *task) /* loop 1 - the ring: keep points outside the inner disc */ for (i = 0; i <= task->config.ring_iters; i++) { - x = SATURN_SAMPLE(); - y = SATURN_SAMPLE(); - p = (x * x + y * y) / SATURN_ENERGY_SHIFT; + x = SATURN_SAMPLE(half, range); + y = SATURN_SAMPLE(half, range); + p = (x * x + y * y) / energy_shift; if (p > 17) - saturn_plot(scr, ox, oy, x + SATURN_HALF, SATURN_FLIP - (y + SATURN_HALF), - task->fg); + saturn_plot(scr, ox, oy, size, x + half, flip - (y + half), task->fg); } /* loop 2 - ring shadow band: sheared sample with a banded energy gate */ @@ -243,30 +249,31 @@ static result_t saturn_redraw(const wuss_event_t *event, saturn_task_t *task) { int r5, r6, r7, e; - r5 = SATURN_SAMPLE(); - r6 = SATURN_SAMPLE(); + r5 = SATURN_SAMPLE(half, range); + r6 = SATURN_SAMPLE(half, range); r7 = r5 / 4; x = r6 + r7; y = r6; - p = (x * x + y * y) / SATURN_ENERGY_SHIFT; - e = ((r6 + r7) * (r6 + r7) + r5 * r5 + r6 * r6) / SATURN_ENERGY_SHIFT; + p = (x * x + y * y) / energy_shift; + e = ((r6 + r7) * (r6 + r7) + r5 * r5 + r6 * r6) / energy_shift; if (e >= 32 && e < 80 && (r5 < 0 || p > 16)) - saturn_plot(scr, ox, oy, x + SATURN_HALF, y + SATURN_HALF, task->fg); + saturn_plot(scr, ox, oy, size, x + half, y + half, task->fg); } /* loop 3 - planet body: filled half-disc offset right */ for (i = 0; i <= task->config.body_iters; i++) { - int r1, r2; + int r1, r2, body_r2; - r1 = SATURN_SAMPLE(); - r2 = SATURN_SAMPLE(); - p = r1 * r1 + r2 * r2; - if (p < SATURN_BODY_R2) + r1 = SATURN_SAMPLE(half, range); + r2 = SATURN_SAMPLE(half, range); + p = r1 * r1 + r2 * r2; + body_r2 = SATURN_BODY_R2(half); + if (p < body_r2) { - x = (int) (sqrt((double) (SATURN_BODY_R2 - p)) / 2.0) + SATURN_HALF; - y = SATURN_FLIP - (r2 / 2 + SATURN_HALF); - saturn_plot(scr, ox, oy, x, y, task->fg); + x = (int) (sqrt((double) (body_r2 - p)) / 2.0) + half; + y = flip - (r2 / 2 + half); + saturn_plot(scr, ox, oy, size, x, y, task->fg); } } @@ -467,15 +474,48 @@ static result_t saturn_size_dialogue_create(saturn_task_t *task) return result_OK; } +/* wuss_EVENT_PRE_SHOW on the size dialogue: resync the slider and its echo + * label to task->config.size, in case Apply (or a config passed to + * saturn_create) changed it since the dialogue was last shown. Always + * allows the show. Also used directly by saturn_size_icon to reset the + * dialogue on an Adjust-Cancel click. */ +static result_t saturn_size_pre_show(saturn_task_t *task) +{ + int value; + char buf[16]; + + value = saturn_size_snap(task->config.size); + wuss_icon_set_value(task->size_dialogue, task->size_slider, value); + snprintf(buf, sizeof(buf), "%d", value); + + return wuss_icon_set_text(task->size_dialogue, task->size_value_label, buf); +} + +/* Applies the slider's current value to task->window (resize + doc extent), + * shared by a Select and an Adjust click on Apply. */ +static result_t saturn_size_apply(saturn_task_t *task) +{ + result_t rc; + int value; + + value = wuss_icon_get_value(task->size_slider); + task->config.size = value; + rc = wuss_window_resize(task->window, SIZE2D(value, value)); + if (rc != result_OK) + return rc; + return wuss_window_set_doc(task->window, SIZE2D(value, value)); +} + /* wuss_EVENT_ICON on the size dialogue: slider drag updates the echo label - * (snapped to SATURN_SIZE_STEP) live on DOWN/MOVE; Cancel/Apply act on a - * Select UP, so a press that drags off the button before release is not - * taken as a click, and an Adjust click leaves the dialogue open (RISC OS - * "Adjust doesn't dismiss" convention). Cancel just dismisses the menu - * chain; Apply resizes the planet window to the chosen size, stores it as - * the new default and dismisses. Dismissing goes through wuss_menu_close - * rather than touching the (borrowed, reused) window directly -- that is - * what hides it, same as a click outside the chain would. */ + * (snapped to SATURN_SIZE_STEP) live on DOWN/MOVE; Cancel/Apply act on + * UP (a press that drags off the button before release is not taken as a + * click), split by button per the RISC OS "Adjust doesn't dismiss" + * convention: Select-Cancel and Select-Apply both dismiss the menu chain + * (Select-Apply after applying); Adjust-Cancel resets the dialogue to + * task->config.size instead of dismissing; Adjust-Apply applies but leaves + * the dialogue open. Dismissing goes through wuss_menu_close rather than + * touching the (borrowed, reused) window directly -- that is what hides it, + * same as a click outside the chain would. */ static result_t saturn_size_icon(saturn_task_t *task, const wuss_event_t *event) { @@ -498,45 +538,38 @@ static result_t saturn_size_icon(saturn_task_t *task, buf); } - if (event->data.icon.action != wuss_MOUSE_UP || - !(event->data.icon.button & wuss_BUTTON_SELECT)) + if (event->data.icon.action != wuss_MOUSE_UP) return result_OK; if (icon == task->size_cancel) { - wuss_menu_close(task->menu_handle); - task->menu_handle = NULL; + if (event->data.icon.button & wuss_BUTTON_SELECT) + { + wuss_menu_close(task->menu_handle); + task->menu_handle = NULL; + return result_OK; + } + if (event->data.icon.button & wuss_BUTTON_ADJUST) + return saturn_size_pre_show(task); return result_OK; } if (icon == task->size_apply) { - value = wuss_icon_get_value(task->size_slider); - task->config.size = value; - wuss_menu_close(task->menu_handle); - task->menu_handle = NULL; - return wuss_window_resize(task->window, SIZE2D(value, value)); + if (event->data.icon.button & wuss_BUTTON_SELECT) + { + wuss_menu_close(task->menu_handle); + task->menu_handle = NULL; + return saturn_size_apply(task); + } + if (event->data.icon.button & wuss_BUTTON_ADJUST) + return saturn_size_apply(task); + return result_OK; } return result_OK; } -/* wuss_EVENT_PRE_SHOW on the size dialogue: resync the slider and its echo - * label to task->config.size, in case Apply (or a config passed to - * saturn_create) changed it since the dialogue was last shown. Always - * allows the show. */ -static result_t saturn_size_pre_show(saturn_task_t *task) -{ - int value; - char buf[16]; - - value = saturn_size_snap(task->config.size); - wuss_icon_set_value(task->size_dialogue, task->size_slider, value); - snprintf(buf, sizeof(buf), "%d", value); - - return wuss_icon_set_text(task->size_dialogue, task->size_value_label, buf); -} - /* Applies event to *colour if it's a pick from colourmenu, returns whether * it was. Shared by saturn_menu_select's fg/bg attempts below. */ static int saturn_menu_select_apply(const wuss_colourmenu_t *colourmenu, From ec7e30fd343afdb1763aa0fc9f1b4732e675b3e7 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Mon, 14 Sep 2026 23:52:23 +0100 Subject: [PATCH 073/128] refactor(wuss): move std icon-size constants to icon.h wuss_STD_SLIDER_HEIGHT / wuss_STD_SECONDARY_BUTTON_HEIGHT / wuss_STD_PRIMARY_BUTTON_HEIGHT were defined locally in saturn.c despite the public wuss_ prefix; moved to include/wuss/icon.h so other tasks/dialogues can reuse them. --- include/wuss/icon.h | 11 +++++++++++ libraries/wuss/test/tasks/saturn.c | 29 +++++++++++++---------------- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/include/wuss/icon.h b/include/wuss/icon.h index 22c2f8d0..d80f829f 100644 --- a/include/wuss/icon.h +++ b/include/wuss/icon.h @@ -352,6 +352,17 @@ wuss_icon_spec_t; /* ----------------------------------------------------------------------- */ +/** Standard main-axis size (px) for a wuss_ICON_TYPE_SLIDER. */ +#define wuss_STD_SLIDER_HEIGHT 18 +/** Standard main-axis size (px) for a non-default wuss_ICON_TYPE_ACTION + * button, e.g. Cancel. */ +#define wuss_STD_SECONDARY_BUTTON_HEIGHT 26 +/** Standard main-axis size (px) for a wuss_ICON_FLAGS_DEFAULT + * wuss_ICON_TYPE_ACTION button, e.g. OK/Apply. */ +#define wuss_STD_PRIMARY_BUTTON_HEIGHT 34 + +/* ----------------------------------------------------------------------- */ + /** * 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 diff --git a/libraries/wuss/test/tasks/saturn.c b/libraries/wuss/test/tasks/saturn.c index 19aa8da3..b2432e15 100644 --- a/libraries/wuss/test/tasks/saturn.c +++ b/libraries/wuss/test/tasks/saturn.c @@ -120,19 +120,19 @@ result_t saturn_create(wuss_t *wuss, wuss_task_t *delegate; wuss_task_desc_t delegate_desc; - task->wuss = wuss; - task->bg = colour_rgb(0x00, 0x00, 0x00); - task->fg = colour_rgb(0xFF, 0xFF, 0xFF); - task->seed = 1; - task->config = (config != NULL) ? *config : default_config; - task->fg_colourmenu = NULL; - task->bg_colourmenu = NULL; - task->menu_handle = NULL; - task->size_dialogue = NULL; - task->size_slider = NULL; - task->size_value_label = NULL; - task->size_cancel = NULL; - task->size_apply = NULL; + task->wuss = wuss; + task->bg = colour_rgb(0x00, 0x00, 0x00); + task->fg = colour_rgb(0xFF, 0xFF, 0xFF); + task->seed = 1; + task->config = (config != NULL) ? *config : default_config; + task->fg_colourmenu = NULL; + task->bg_colourmenu = NULL; + task->menu_handle = NULL; + task->size_dialogue = NULL; + task->size_slider = NULL; + task->size_value_label = NULL; + task->size_cancel = NULL; + task->size_apply = NULL; /* saturn_redraw paints its own background */ delegate_desc.handle = saturn_handle; @@ -335,9 +335,6 @@ enum }; #define G 4 -#define wuss_STD_SLIDER_HEIGHT 18 -#define wuss_STD_SECONDARY_BUTTON_HEIGHT 26 -#define wuss_STD_PRIMARY_BUTTON_HEIGHT 34 /* Leaf main-axis sizes, named so saturn_size_dialogue_create's hand-computed * minimum window size can share them with the table below instead of From 2169b8ddf3f4363f98cb90eb54827a5fba1143f9 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Tue, 15 Sep 2026 00:03:54 +0100 Subject: [PATCH 074/128] refactor(wuss): factor icon-spec fill helpers out of saturn_size_dialogue_create wuss_icon_spec_label/_slider/_action in a new helpers/icon-spec.c + public include/wuss/icon-spec.h pair replace the manual wuss_icon_spec_t field assignment in saturn_size_dialogue_create, following the create-from-desc.c helper precedent. --- CMakeLists.txt | 2 + include/wuss/icon-spec.h | 96 ++++++++++++++++++++ libraries/wuss/helpers/icon-spec.c | 66 ++++++++++++++ libraries/wuss/test/tasks/saturn.c | 141 +++++++++++++---------------- libraries/wuss/test/tasks/saturn.h | 1 + 5 files changed, 226 insertions(+), 80 deletions(-) create mode 100644 include/wuss/icon-spec.h create mode 100644 libraries/wuss/helpers/icon-spec.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 080b60c4..54530b75 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -132,6 +132,7 @@ set(PUBLIC_HEADERS if(WUSS_ICONS) list(APPEND PUBLIC_HEADERS include/wuss/icon.h) + list(APPEND PUBLIC_HEADERS include/wuss/icon-spec.h) endif() if(WUSS_MENUS) list(APPEND PUBLIC_HEADERS include/wuss/menu.h) @@ -440,6 +441,7 @@ set(WUSS_ICON_SOURCES libraries/wuss/icon/draw.c libraries/wuss/icon/free.c libraries/wuss/icon/from-spec.c + libraries/wuss/helpers/icon-spec.c libraries/wuss/icon/get-bbox.c libraries/wuss/icon/get-selected.c libraries/wuss/icon/get-text.c diff --git a/include/wuss/icon-spec.h b/include/wuss/icon-spec.h new file mode 100644 index 00000000..51418dde --- /dev/null +++ b/include/wuss/icon-spec.h @@ -0,0 +1,96 @@ +/* wuss/icon-spec.h -- fill helpers for common wuss_icon_spec_t shapes */ + +/** + * \file icon-spec.h + * + * A task laying out a dialogue with wuss_icon_create_array typically builds + * several wuss_icon_spec_t entries that only differ in bbox, text and a + * couple of type-specific fields. These helpers fill one spec in place for + * the three shapes that recur across the test tasks: a plain or + * right-justified label, a horizontal or vertical slider, and an action + * button (optionally the dialogue's default action). Each takes the spec to + * fill plus its distinguishing fields; the caller is still responsible for + * memset(specs, 0, sizeof(specs)) up front and for wuss_icon_create_array. + */ + +#ifndef WUSS_ICON_SPEC_H +#define WUSS_ICON_SPEC_H + +#ifdef __cplusplus +extern "C" +{ +#endif + +#include "geom/box.h" + +#include "wuss/icon.h" + +#ifdef WUSS_ICONS + +/* ----------------------------------------------------------------------- */ + +/** + * Fill \p spec as a wuss_ICON_TYPE_LABEL. + * + * \param[out] spec Spec to fill; any prior contents are overwritten. + * \param[in] bbox Bounding box. + * \param[in] text Label text, borrowed until + * wuss_icon_create(_array) copies it. + * \param[in] fg Text colour. + * \param[in] justify_right Non-zero to right-justify + * (wuss_ICON_FLAGS_JUSTIFY_RIGHT) instead of the + * default left justification. + */ +void wuss_icon_spec_label(wuss_icon_spec_t *spec, + box_t bbox, + const char *text, + wuss_colour_t fg, + int justify_right); + +/** + * Fill \p spec as a wuss_ICON_TYPE_SLIDER. + * + * \param[out] spec Spec to fill; any prior contents are + * overwritten. + * \param[in] bbox Bounding box. + * \param[in] fg Groove/fill colour. + * \param[in] orientation Groove direction. + * \param[in] min Value at the groove's start. + * \param[in] max Value at the groove's end. + * \param[in] default_value Initial value, clamped to [min,max]. + */ +void wuss_icon_spec_slider(wuss_icon_spec_t *spec, + box_t bbox, + wuss_colour_t fg, + wuss_slider_orientation_t orientation, + int min, + int max, + int default_value); + +/** + * Fill \p spec as a wuss_ICON_TYPE_ACTION button. + * + * \param[out] spec Spec to fill; any prior contents are overwritten. + * \param[in] bbox Bounding box. + * \param[in] text Button text, borrowed until wuss_icon_create(_array) + * copies it. + * \param[in] fg Text colour. + * \param[in] bg Fill colour. + * \param[in] is_default Non-zero to draw as the dialogue's default action + * (wuss_ICON_FLAGS_DEFAULT) instead of an ordinary + * button. + */ +void wuss_icon_spec_action(wuss_icon_spec_t *spec, + box_t bbox, + const char *text, + wuss_colour_t fg, + wuss_colour_t bg, + int is_default); + +#endif /* WUSS_ICONS */ + +#ifdef __cplusplus +} +#endif + +#endif /* WUSS_ICON_SPEC_H */ diff --git a/libraries/wuss/helpers/icon-spec.c b/libraries/wuss/helpers/icon-spec.c new file mode 100644 index 00000000..20080a5d --- /dev/null +++ b/libraries/wuss/helpers/icon-spec.c @@ -0,0 +1,66 @@ +/* wuss/helpers/icon-spec.c -- fill helpers for common wuss_icon_spec_t shapes */ + +#include + +#ifdef FORTIFY +#include "fortify/fortify.h" +#endif + +#include "wuss/icon-spec.h" + +#ifdef WUSS_ICONS + +/* ----------------------------------------------------------------------- */ + +void wuss_icon_spec_label(wuss_icon_spec_t *spec, + box_t bbox, + const char *text, + wuss_colour_t fg, + int justify_right) +{ + memset(spec, 0, sizeof(*spec)); + spec->bbox = bbox; + spec->type = wuss_ICON_TYPE_LABEL; + spec->text = text; + spec->fg = fg; + spec->bg = wuss_NO_BACKGROUND; + spec->flags = justify_right ? wuss_ICON_FLAGS_JUSTIFY_RIGHT + : wuss_ICON_FLAGS_NONE; +} + +void wuss_icon_spec_slider(wuss_icon_spec_t *spec, + box_t bbox, + wuss_colour_t fg, + wuss_slider_orientation_t orientation, + int min, + int max, + int default_value) +{ + memset(spec, 0, sizeof(*spec)); + spec->bbox = bbox; + spec->type = wuss_ICON_TYPE_SLIDER; + spec->fg = fg; + spec->bg = wuss_NO_BACKGROUND; + spec->u.slider.orientation = orientation; + spec->u.slider.min = min; + spec->u.slider.max = max; + spec->u.slider.default_value = default_value; +} + +void wuss_icon_spec_action(wuss_icon_spec_t *spec, + box_t bbox, + const char *text, + wuss_colour_t fg, + wuss_colour_t bg, + int is_default) +{ + memset(spec, 0, sizeof(*spec)); + spec->bbox = bbox; + spec->type = wuss_ICON_TYPE_ACTION; + spec->text = text; + spec->fg = fg; + spec->bg = bg; + spec->flags = is_default ? wuss_ICON_FLAGS_DEFAULT : wuss_ICON_FLAGS_NONE; +} + +#endif /* WUSS_ICONS */ diff --git a/libraries/wuss/test/tasks/saturn.c b/libraries/wuss/test/tasks/saturn.c index b2432e15..e7d8d0c4 100644 --- a/libraries/wuss/test/tasks/saturn.c +++ b/libraries/wuss/test/tasks/saturn.c @@ -321,10 +321,15 @@ enum { ST_ROOT, - ST_ROW, - ST_LABL, - ST_SLDR, - ST_VAL, + ST_ROW1, + ST_LABL1, + ST_SLDR1, + ST_VAL1, + + ST_ROW2, + ST_LABL2, + ST_SLDR2, + ST_VAL2, ST_BTNS, ST_SPCR, @@ -339,24 +344,29 @@ enum /* Leaf main-axis sizes, named so saturn_size_dialogue_create's hand-computed * minimum window size can share them with the table below instead of * repeating the numbers as bare literals. */ -#define ST_LABEL_W 24 +#define ST_LABEL_W 24 #define ST_SLIDER_MIN_W 64 -#define ST_CANCEL_W 48 -#define ST_APPLY_W 56 +#define ST_CANCEL_W 48 +#define ST_APPLY_W 56 static const stack_item_t g_saturn_size_stack[SIZE_STACK__LIMIT] = { - [ST_ROOT] = STACK_VBOX_EX(-1, 0, G, G, G, G, G), - - [ST_ROW] = STACK_HBOX(ST_ROOT, wuss_STD_SLIDER_HEIGHT, G, stack_ALIGN_START), - [ST_LABL] = STACK_LEAF(ST_ROW, ST_LABEL_W, 16, stack_ALIGN_CENTRE), - [ST_SLDR] = STACK_LEAF_EX(ST_ROW, 0, wuss_STD_SLIDER_HEIGHT, stack_ALIGN_CENTRE, 1, ST_SLIDER_MIN_W, 0), - [ST_VAL] = STACK_LEAF(ST_ROW, ST_LABEL_W, 16, stack_ALIGN_CENTRE), - - [ST_BTNS] = STACK_HBOX(ST_ROOT, wuss_STD_PRIMARY_BUTTON_HEIGHT, G, stack_ALIGN_END), - [ST_SPCR] = STACK_SPACER(ST_BTNS, 1), - [ST_CNCL] = STACK_LEAF(ST_BTNS, ST_CANCEL_W, wuss_STD_SECONDARY_BUTTON_HEIGHT, stack_ALIGN_CENTRE), - [ST_APLY] = STACK_LEAF(ST_BTNS, ST_APPLY_W, wuss_STD_PRIMARY_BUTTON_HEIGHT, stack_ALIGN_CENTRE), + [ST_ROOT] = STACK_VBOX_EX(-1, 0, G, G, G, G, G), + + [ST_ROW1] = STACK_HBOX(ST_ROOT, wuss_STD_SLIDER_HEIGHT, G, stack_ALIGN_START), + [ST_LABL1] = STACK_LEAF(ST_ROW1, ST_LABEL_W, 16, stack_ALIGN_CENTRE), + [ST_SLDR1] = STACK_LEAF_EX(ST_ROW1, 0, wuss_STD_SLIDER_HEIGHT, stack_ALIGN_CENTRE, 1, ST_SLIDER_MIN_W, 0), + [ST_VAL1] = STACK_LEAF(ST_ROW1, ST_LABEL_W, 16, stack_ALIGN_CENTRE), + + [ST_ROW2] = STACK_HBOX(ST_ROOT, wuss_STD_SLIDER_HEIGHT, G, stack_ALIGN_START), + [ST_LABL2] = STACK_LEAF(ST_ROW2, ST_LABEL_W, 16, stack_ALIGN_CENTRE), + [ST_SLDR2] = STACK_LEAF_EX(ST_ROW2, 0, wuss_STD_SLIDER_HEIGHT, stack_ALIGN_CENTRE, 1, ST_SLIDER_MIN_W, 0), + [ST_VAL2] = STACK_LEAF(ST_ROW2, ST_LABEL_W, 16, stack_ALIGN_CENTRE), + + [ST_BTNS] = STACK_HBOX(ST_ROOT, wuss_STD_PRIMARY_BUTTON_HEIGHT, G, stack_ALIGN_END), + [ST_SPCR] = STACK_SPACER(ST_BTNS, 1), + [ST_CNCL] = STACK_LEAF(ST_BTNS, ST_CANCEL_W, wuss_STD_SECONDARY_BUTTON_HEIGHT, stack_ALIGN_CENTRE), + [ST_APLY] = STACK_LEAF(ST_BTNS, ST_APPLY_W, wuss_STD_PRIMARY_BUTTON_HEIGHT, stack_ALIGN_CENTRE), }; /* Build the size dialogue once: a label, a slider snapped to @@ -368,15 +378,15 @@ static const stack_item_t g_saturn_size_stack[SIZE_STACK__LIMIT] = * hidden. */ static result_t saturn_size_dialogue_create(saturn_task_t *task) { - wuss_icon_spec_t specs[SATURN_SIZE_NICONS]; - wuss_icon_t *made[SATURN_SIZE_NICONS]; - wuss_icon_spec_t *s; - box_t boxes[SIZE_STACK__LIMIT]; - box_t root; - char buf[16]; - result_t rc; - int row_w, btns_w; - size2d_t sz; + wuss_icon_spec_t specs[SATURN_SIZE_NICONS]; + wuss_icon_t *made[SATURN_SIZE_NICONS]; + box_t boxes[SIZE_STACK__LIMIT]; + box_t root; + char buf[16]; + result_t rc; + int row_w, btns_w; + int value; + size2d_t sz; /* stack_solve distributes into a box it's given; it can't report a * subtree's intrinsic minimum size in one call, since flex/spacer items @@ -386,7 +396,7 @@ static result_t saturn_size_dialogue_create(saturn_task_t *task) row_w = ST_LABEL_W + G + ST_SLIDER_MIN_W + G + ST_LABEL_W; btns_w = ST_CANCEL_W + G + ST_APPLY_W; sz.w = MAX(row_w, btns_w) + 2 * G; - sz.h = wuss_STD_SLIDER_HEIGHT + G + wuss_STD_PRIMARY_BUTTON_HEIGHT + 2 * G; + sz.h = (wuss_STD_SLIDER_HEIGHT + G) * 2 + wuss_STD_PRIMARY_BUTTON_HEIGHT + 2 * G; rc = wuss_window_create_placed(task->delegate, sz, @@ -405,63 +415,28 @@ static result_t saturn_size_dialogue_create(saturn_task_t *task) root = (box_t) BOX_POS_SIZE(0, 0, sz.w, sz.h); rc = stack_solve(g_saturn_size_stack, NELEMS(g_saturn_size_stack), &root, boxes); if (rc != result_OK) - { - wuss_window_close(task->size_dialogue); - task->size_dialogue = NULL; - return rc; - } + goto exit; - memset(specs, 0, sizeof(specs)); - - s = &specs[SATURN_SIZE_ICON_LABEL]; - s->bbox = boxes[ST_LABL]; - s->type = wuss_ICON_TYPE_LABEL; - s->text = "Size"; - s->fg = wuss_COLOUR_BLACK; - s->bg = wuss_NO_BACKGROUND; - s->flags = wuss_ICON_FLAGS_JUSTIFY_RIGHT; - - s = &specs[SATURN_SIZE_ICON_SLIDER]; - s->bbox = boxes[ST_SLDR]; - s->type = wuss_ICON_TYPE_SLIDER; - s->fg = wuss_COLOUR_BLACK; - s->bg = wuss_NO_BACKGROUND; - s->u.slider.orientation = wuss_SLIDER_HORIZONTAL; - s->u.slider.min = SATURN_SIZE_MIN; - s->u.slider.max = SATURN_SIZE_MAX; - s->u.slider.default_value = saturn_size_snap(task->config.size); - - snprintf(buf, sizeof(buf), "%d", s->u.slider.default_value); - s = &specs[SATURN_SIZE_ICON_VALUE]; - s->bbox = boxes[ST_VAL]; - s->type = wuss_ICON_TYPE_LABEL; - s->text = buf; /* copied by wuss_icon_create_array */ - s->fg = wuss_COLOUR_BLACK; - s->bg = wuss_NO_BACKGROUND; - - s = &specs[SATURN_SIZE_ICON_CANCEL]; - s->bbox = boxes[ST_CNCL]; - s->type = wuss_ICON_TYPE_ACTION; - s->text = "Cancel"; - s->fg = wuss_COLOUR_BLACK; - s->bg = wuss_COLOUR_WINDOW; - - s = &specs[SATURN_SIZE_ICON_APPLY]; - s->bbox = boxes[ST_APLY]; - s->type = wuss_ICON_TYPE_ACTION; - s->text = "Apply"; - s->fg = wuss_COLOUR_BLACK; - s->bg = wuss_COLOUR_WINDOW; - s->flags = wuss_ICON_FLAGS_DEFAULT; + value = saturn_size_snap(task->config.size); + snprintf(buf, sizeof(buf), "%d", value); + + wuss_icon_spec_label(&specs[SATURN_SIZE_ICON_LABEL], boxes[ST_LABL1], + "Size", wuss_COLOUR_BLACK, 1); + wuss_icon_spec_slider(&specs[SATURN_SIZE_ICON_SLIDER], boxes[ST_SLDR1], + wuss_COLOUR_BLACK, wuss_SLIDER_HORIZONTAL, + SATURN_SIZE_MIN, SATURN_SIZE_MAX, value); + wuss_icon_spec_label(&specs[SATURN_SIZE_ICON_VALUE], boxes[ST_VAL1], + buf, wuss_COLOUR_BLACK, 0); /* buf copied by + wuss_icon_create_array */ + wuss_icon_spec_action(&specs[SATURN_SIZE_ICON_CANCEL], boxes[ST_CNCL], + "Cancel", wuss_COLOUR_BLACK, wuss_COLOUR_WINDOW, 0); + wuss_icon_spec_action(&specs[SATURN_SIZE_ICON_APPLY], boxes[ST_APLY], + "Apply", wuss_COLOUR_BLACK, wuss_COLOUR_WINDOW, 1); rc = wuss_icon_create_array(task->size_dialogue, specs, SATURN_SIZE_NICONS, made); if (rc != result_OK) - { - wuss_window_close(task->size_dialogue); /* not yet a menu leaf: safe to close */ - task->size_dialogue = NULL; - return rc; - } + goto exit; task->size_slider = made[SATURN_SIZE_ICON_SLIDER]; task->size_value_label = made[SATURN_SIZE_ICON_VALUE]; @@ -469,6 +444,12 @@ static result_t saturn_size_dialogue_create(saturn_task_t *task) task->size_apply = made[SATURN_SIZE_ICON_APPLY]; return result_OK; + + +exit: + wuss_window_close(task->size_dialogue); /* not yet a menu leaf: safe to close */ + task->size_dialogue = NULL; + return rc; } /* wuss_EVENT_PRE_SHOW on the size dialogue: resync the slider and its echo diff --git a/libraries/wuss/test/tasks/saturn.h b/libraries/wuss/test/tasks/saturn.h index 203a82da..0be7c736 100644 --- a/libraries/wuss/test/tasks/saturn.h +++ b/libraries/wuss/test/tasks/saturn.h @@ -9,6 +9,7 @@ * by default, so the wuss app always has the colourmenu component. */ #include "framebuf/colour.h" #include "wuss/component/colourmenu.h" +#include "wuss/icon-spec.h" #include "wuss/icon.h" #include "wuss/menu.h" #include "wuss/task.h" From be2fc4345eafb366ec671295cb563fafc6b53d44 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Tue, 15 Sep 2026 09:13:22 +0100 Subject: [PATCH 075/128] refactor(wuss): add wuss_STD_GAP/wuss_STD_INSET, replace saturn.c's local G Standard 4px sibling-gap and edge-inset constants join the existing wuss_STD_*_HEIGHT sizes in icon.h; saturn.c's local #define G 4 is replaced throughout the size dialogue's stack table and hand-computed minimum size. --- include/wuss/icon.h | 4 ++++ libraries/wuss/test/tasks/saturn.c | 18 ++++++++---------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/include/wuss/icon.h b/include/wuss/icon.h index d80f829f..e1a5c63a 100644 --- a/include/wuss/icon.h +++ b/include/wuss/icon.h @@ -360,6 +360,10 @@ wuss_icon_spec_t; /** Standard main-axis size (px) for a wuss_ICON_FLAGS_DEFAULT * wuss_ICON_TYPE_ACTION button, e.g. OK/Apply. */ #define wuss_STD_PRIMARY_BUTTON_HEIGHT 34 +/** Standard gap (px) between two sibling icons/components in a layout. */ +#define wuss_STD_GAP 4 +/** Standard gap (px) between an icon/component and the window edge. */ +#define wuss_STD_INSET 4 /* ----------------------------------------------------------------------- */ diff --git a/libraries/wuss/test/tasks/saturn.c b/libraries/wuss/test/tasks/saturn.c index e7d8d0c4..cd1ae4e3 100644 --- a/libraries/wuss/test/tasks/saturn.c +++ b/libraries/wuss/test/tasks/saturn.c @@ -339,8 +339,6 @@ enum SIZE_STACK__LIMIT }; -#define G 4 - /* Leaf main-axis sizes, named so saturn_size_dialogue_create's hand-computed * minimum window size can share them with the table below instead of * repeating the numbers as bare literals. */ @@ -351,19 +349,19 @@ enum static const stack_item_t g_saturn_size_stack[SIZE_STACK__LIMIT] = { - [ST_ROOT] = STACK_VBOX_EX(-1, 0, G, G, G, G, G), + [ST_ROOT] = STACK_VBOX_EX(-1, 0, wuss_STD_GAP, wuss_STD_INSET, wuss_STD_INSET, wuss_STD_INSET, wuss_STD_INSET), - [ST_ROW1] = STACK_HBOX(ST_ROOT, wuss_STD_SLIDER_HEIGHT, G, stack_ALIGN_START), + [ST_ROW1] = STACK_HBOX(ST_ROOT, wuss_STD_SLIDER_HEIGHT, wuss_STD_GAP, stack_ALIGN_START), [ST_LABL1] = STACK_LEAF(ST_ROW1, ST_LABEL_W, 16, stack_ALIGN_CENTRE), [ST_SLDR1] = STACK_LEAF_EX(ST_ROW1, 0, wuss_STD_SLIDER_HEIGHT, stack_ALIGN_CENTRE, 1, ST_SLIDER_MIN_W, 0), [ST_VAL1] = STACK_LEAF(ST_ROW1, ST_LABEL_W, 16, stack_ALIGN_CENTRE), - [ST_ROW2] = STACK_HBOX(ST_ROOT, wuss_STD_SLIDER_HEIGHT, G, stack_ALIGN_START), + [ST_ROW2] = STACK_HBOX(ST_ROOT, wuss_STD_SLIDER_HEIGHT, wuss_STD_GAP, stack_ALIGN_START), [ST_LABL2] = STACK_LEAF(ST_ROW2, ST_LABEL_W, 16, stack_ALIGN_CENTRE), [ST_SLDR2] = STACK_LEAF_EX(ST_ROW2, 0, wuss_STD_SLIDER_HEIGHT, stack_ALIGN_CENTRE, 1, ST_SLIDER_MIN_W, 0), [ST_VAL2] = STACK_LEAF(ST_ROW2, ST_LABEL_W, 16, stack_ALIGN_CENTRE), - [ST_BTNS] = STACK_HBOX(ST_ROOT, wuss_STD_PRIMARY_BUTTON_HEIGHT, G, stack_ALIGN_END), + [ST_BTNS] = STACK_HBOX(ST_ROOT, wuss_STD_PRIMARY_BUTTON_HEIGHT, wuss_STD_GAP, stack_ALIGN_END), [ST_SPCR] = STACK_SPACER(ST_BTNS, 1), [ST_CNCL] = STACK_LEAF(ST_BTNS, ST_CANCEL_W, wuss_STD_SECONDARY_BUTTON_HEIGHT, stack_ALIGN_CENTRE), [ST_APLY] = STACK_LEAF(ST_BTNS, ST_APPLY_W, wuss_STD_PRIMARY_BUTTON_HEIGHT, stack_ALIGN_CENTRE), @@ -393,10 +391,10 @@ static result_t saturn_size_dialogue_create(saturn_task_t *task) * always consume whatever slack the root provides. So the minimum * window size is hand-computed here from the same constants the table * uses, rather than measured by solving. */ - row_w = ST_LABEL_W + G + ST_SLIDER_MIN_W + G + ST_LABEL_W; - btns_w = ST_CANCEL_W + G + ST_APPLY_W; - sz.w = MAX(row_w, btns_w) + 2 * G; - sz.h = (wuss_STD_SLIDER_HEIGHT + G) * 2 + wuss_STD_PRIMARY_BUTTON_HEIGHT + 2 * G; + row_w = ST_LABEL_W + wuss_STD_GAP + ST_SLIDER_MIN_W + wuss_STD_GAP + ST_LABEL_W; + btns_w = ST_CANCEL_W + wuss_STD_GAP + ST_APPLY_W; + sz.w = MAX(row_w, btns_w) + 2 * wuss_STD_INSET; + sz.h = (wuss_STD_SLIDER_HEIGHT + wuss_STD_GAP) * 2 + wuss_STD_PRIMARY_BUTTON_HEIGHT + 2 * wuss_STD_INSET; rc = wuss_window_create_placed(task->delegate, sz, From fb2d9a4fddc9d3769addc73ed251e60443afe2f8 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Tue, 15 Sep 2026 10:41:31 +0100 Subject: [PATCH 076/128] feat(wuss): baseline rule, blank-row trim and inset in the chars task chars_redraw now draws a dotted rule at each glyph's baseline; the grid window drops contiguous leading/trailing rows with no glyphs (via a shared chars_row_range) and gains a wuss_STD_INSET margin; the dirty rect is filled with the background colour up front so that margin (and any strip past the grid) always redraws. --- libraries/wuss/test/tasks/chars.c | 59 +++++++++++++++++++++++++------ 1 file changed, 48 insertions(+), 11 deletions(-) diff --git a/libraries/wuss/test/tasks/chars.c b/libraries/wuss/test/tasks/chars.c index 9139a158..4848e944 100644 --- a/libraries/wuss/test/tasks/chars.c +++ b/libraries/wuss/test/tasks/chars.c @@ -17,6 +17,7 @@ #include "geom/box.h" #include "geom/point.h" #include "io/path.h" +#include "wuss/icon.h" #include "wuss/menu.h" #include "chars.h" @@ -32,17 +33,41 @@ static char chars_resources[256]; /* ----------------------------------------------------------------------- */ -/* content size for the grid at the given font's cell metrics. Each cell +/* the rows of the CHARS_COLS x CHARS_ROWS grid that hold at least one glyph + * of font: [*first_row, *first_row + *nrows). first_row * CHARS_COLS is the + * byte value of the top-left cell of the first such row. */ +static void chars_row_range(bmfont_t *font, int *first_row, int *nrows) +{ + int first, count, last_row; + + first = ' '; /* bmfont glyphs are laid out contiguously starting here */ + count = bmfont_get_count(font); + /* bmfont indexes its glyph table off a plain char, so a byte value above + * CHAR_MAX would index negatively on a signed-char platform. Never draw + * one, however many glyphs the font claims. */ + if (first + count > CHAR_MAX + 1) + count = CHAR_MAX + 1 - first; + + *first_row = first / CHARS_COLS; + last_row = (first + count - 1) / CHARS_COLS; + *nrows = last_row - *first_row + 1; +} + +/* content size for the grid at the given font's cell metrics, less any + * contiguous leading/trailing rows the font has no glyphs in. Each cell * stacks the index (drawn in the wuss system font) above the glyph. */ static size2d_t chars_window_size(chars_task_t *task, bmfont_t *font) { int fw, fh, ifw, ifh, cell_w, cell_h; + int first_row, nrows; bmfont_get_info(font, &fw, &fh, NULL, NULL); bmfont_get_info(wuss_get_font(task->wuss), &ifw, &ifh, NULL, NULL); cell_w = MAX(fw, ifw * 3) + CHARS_PAD * 2; cell_h = ifh + fh + CHARS_PAD * 3; - return SIZE2D(cell_w * CHARS_COLS, cell_h * CHARS_ROWS); + chars_row_range(font, &first_row, &nrows); + return SIZE2D(cell_w * CHARS_COLS + wuss_STD_INSET * 2, + cell_h * nrows + wuss_STD_INSET * 2); } /* load fonts[idx] if not already in hand; returns it or NULL on failure. @@ -204,6 +229,7 @@ static result_t chars_redraw(const wuss_event_t *event, void *task_data) int sysfont_width, sysfont_height, sysfont_ascent; int cell_w, cell_h; int first, count; + int first_row, nrows; int i, sx, sy; cc = task_data; @@ -228,35 +254,46 @@ static result_t chars_redraw(const wuss_event_t *event, void *task_data) * one, however many glyphs the font claims. */ if (first + count > CHAR_MAX + 1) count = CHAR_MAX + 1 - first; + chars_row_range(cc->font, &first_row, &nrows); + + /* the wuss_STD_INSET margin around the grid falls outside every cell's own + * fill below, so paint the whole dirty rect first -- covers that margin + * and any dirty strip past the last row/column of cells too. */ + screen_fill_rect(scr, bounds->x0, bounds->y0, + SIZE2D(bounds->x1 - bounds->x0, + bounds->y1 - bounds->y0), cc->bg); - for (i = 0; i < CHARS_COLS * CHARS_ROWS; i++) + for (i = 0; i < CHARS_COLS * nrows; i++) { - int col, row, x, y; + int col, row, x, y, byte; char ch; char label[4]; point_t pos; - col = i % CHARS_COLS; - row = i / CHARS_COLS; - x = bounds->x0 - sx + col * cell_w; - y = bounds->y0 - sy + row * cell_h; + col = i % CHARS_COLS; + row = i / CHARS_COLS; + byte = (first_row + row) * CHARS_COLS + col; + x = bounds->x0 - sx + wuss_STD_INSET + col * cell_w; + y = bounds->y0 - sy + wuss_STD_INSET + row * cell_h; screen_fill_rect(scr, x, y, SIZE2D(cell_w, cell_h), cc->bg); screen_draw_line(scr, x, y, x + cell_w - 1, y, cc->mg); screen_draw_line(scr, x, y, x, y + cell_h - 1, cc->mg); - snprintf(label, 4, "%d", i); + snprintf(label, 4, "%d", byte); pos.x = x + CHARS_PAD; pos.y = y + CHARS_PAD + sysfont_ascent; wuss_text_draw(cc->wuss, 0, scr, label, (int) strlen(label), cc->mg, cc->bg, &pos, NULL); - if (i < first || i >= first + count) + if (byte < first || byte >= first + count) continue; /* no glyph for this byte value: leave the cell blank */ - ch = (char) i; + ch = (char) byte; pos.x = x + CHARS_PAD; pos.y = y + CHARS_PAD * 2 + sysfont_height + font_ascent; + screen_draw_dashed_line(scr, x + CHARS_PAD, pos.y, + x + cell_w - 1 - CHARS_PAD, pos.y, 1, 1, cc->mg); bmfont_draw(cc->font, scr, &ch, 1, cc->fg, cc->bg, &pos, NULL); } From 793aaa87333ff49699061887ce9aa7bbb43bb1a1 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Tue, 15 Sep 2026 11:32:50 +0100 Subject: [PATCH 077/128] fix(wuss): vertically centre icon text by ascender only Icon text was centred using ascender+descender (font_height), pulling labels/buttons/radio-option text and menu-entry text visually high since most glyphs never touch the descender band. Centre by font_ascent instead. --- libraries/wuss/icon/draw.c | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/libraries/wuss/icon/draw.c b/libraries/wuss/icon/draw.c index a898b957..cc4f026b 100644 --- a/libraries/wuss/icon/draw.c +++ b/libraries/wuss/icon/draw.c @@ -18,9 +18,11 @@ * type's draw helper. "b" is the icon's screen box; "fg" is its resolved * foreground; "font" is the icon's selected weight (falling back to slot 0); * "font_height", "font_ascent" and "have_font" are 0 when there is no font or - * no text to draw. Draw helpers vertically centre by "font_height" then add - * "font_ascent" to convert the resulting top-left y to the baseline - * wuss__text_draw expects. */ + * no text to draw. Draw helpers vertically centre by "font_ascent" alone (not + * the descender too, so a run of ascender-only text -- e.g. all caps and + * digits -- sits visually centred rather than pulled high by unused descender + * space) then add "font_ascent" again to convert the resulting top-left y to + * the baseline wuss__text_draw expects. */ typedef struct icon_draw_ctx { wuss_t *wuss; @@ -231,7 +233,7 @@ static void wuss__icon_draw_label(const icon_draw_ctx_t *c) else pos.x = b->x0 + 1; - pos.y = b->y0 + (b->y1 - b->y0 - c->font_height) / 2 + c->font_ascent; + pos.y = b->y0 + (b->y1 - b->y0 - c->font_ascent) / 2 + c->font_ascent; wuss__text_draw(c->font, c->scr, icon->text, (int) strlen(icon->text), c->fg, bg, &pos, NULL); @@ -346,7 +348,7 @@ static void wuss__icon_draw_button(const icon_draw_ctx_t *c) interior_w, &split_point, &width); pos.x = b->x0 + ((b->x1 - b->x0) - width) / 2; - pos.y = b->y0 + (b->y1 - b->y0 - c->font_height) / 2 + c->font_ascent; + pos.y = b->y0 + (b->y1 - b->y0 - c->font_ascent) / 2 + c->font_ascent; if (pressed) { pos.x += 1; @@ -464,7 +466,7 @@ static void wuss__icon_draw_radio_option(const icon_draw_ctx_t *c) NOT_USED(width); pos.x = tx; - pos.y = b->y0 + (b->y1 - b->y0 - c->font_height) / 2 + c->font_ascent; + pos.y = b->y0 + (b->y1 - b->y0 - c->font_ascent) / 2 + c->font_ascent; wuss__text_draw(c->font, c->scr, icon->text, (int) strlen(icon->text), glyph, bg, &pos, NULL); } @@ -593,7 +595,7 @@ static void wuss__icon_draw_menu_entry(const icon_draw_ctx_t *c) wuss__text_measure(c->font, " ", 1, INT_MAX, NULL, &space_w); pos.x = text_x0 + (int) space_w; - pos.y = b->y0 + (b->y1 - b->y0 - c->font_height) / 2 + c->font_ascent; + pos.y = b->y0 + (b->y1 - b->y0 - c->font_ascent) / 2 + c->font_ascent; wuss__text_draw(c->font, c->scr, icon->text, (int) strlen(icon->text), text_ink, text_ground, &pos, NULL); } From 1b4e155233c8444a4689c091a6091f4ce3a623cb Mon Sep 17 00:00:00 2001 From: David Thomas Date: Tue, 15 Sep 2026 11:42:16 +0100 Subject: [PATCH 078/128] refactor(wuss): factor icon text baseline-y centring into a helper The four call sites duplicated the same ascender-centring formula; pull it into icon_text_baseline_y(). --- libraries/wuss/icon/draw.c | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/libraries/wuss/icon/draw.c b/libraries/wuss/icon/draw.c index cc4f026b..301efd7d 100644 --- a/libraries/wuss/icon/draw.c +++ b/libraries/wuss/icon/draw.c @@ -18,11 +18,8 @@ * type's draw helper. "b" is the icon's screen box; "fg" is its resolved * foreground; "font" is the icon's selected weight (falling back to slot 0); * "font_height", "font_ascent" and "have_font" are 0 when there is no font or - * no text to draw. Draw helpers vertically centre by "font_ascent" alone (not - * the descender too, so a run of ascender-only text -- e.g. all caps and - * digits -- sits visually centred rather than pulled high by unused descender - * space) then add "font_ascent" again to convert the resulting top-left y to - * the baseline wuss__text_draw expects. */ + * no text to draw. Draw helpers vertically centre text via + * icon_text_baseline_y(). */ typedef struct icon_draw_ctx { wuss_t *wuss; @@ -38,6 +35,14 @@ typedef struct icon_draw_ctx } icon_draw_ctx_t; +/* Baseline y for text centred by ascender alone (not the descender too, so a + * run of ascender-only text -- e.g. all caps and digits -- sits visually + * centred rather than pulled high by unused descender space) in box "b". */ +static int icon_text_baseline_y(const icon_draw_ctx_t *c, const box_t *b) +{ + return b->y0 + (b->y1 - b->y0 - c->font_ascent) / 2 + c->font_ascent; +} + /* ----------------------------------------------------------------------- */ static void icon_fill_bevel(screen_t *scr, @@ -233,7 +238,7 @@ static void wuss__icon_draw_label(const icon_draw_ctx_t *c) else pos.x = b->x0 + 1; - pos.y = b->y0 + (b->y1 - b->y0 - c->font_ascent) / 2 + c->font_ascent; + pos.y = icon_text_baseline_y(c, b); wuss__text_draw(c->font, c->scr, icon->text, (int) strlen(icon->text), c->fg, bg, &pos, NULL); @@ -348,7 +353,7 @@ static void wuss__icon_draw_button(const icon_draw_ctx_t *c) interior_w, &split_point, &width); pos.x = b->x0 + ((b->x1 - b->x0) - width) / 2; - pos.y = b->y0 + (b->y1 - b->y0 - c->font_ascent) / 2 + c->font_ascent; + pos.y = icon_text_baseline_y(c, b); if (pressed) { pos.x += 1; @@ -466,7 +471,7 @@ static void wuss__icon_draw_radio_option(const icon_draw_ctx_t *c) NOT_USED(width); pos.x = tx; - pos.y = b->y0 + (b->y1 - b->y0 - c->font_ascent) / 2 + c->font_ascent; + pos.y = icon_text_baseline_y(c, b); wuss__text_draw(c->font, c->scr, icon->text, (int) strlen(icon->text), glyph, bg, &pos, NULL); } @@ -595,7 +600,7 @@ static void wuss__icon_draw_menu_entry(const icon_draw_ctx_t *c) wuss__text_measure(c->font, " ", 1, INT_MAX, NULL, &space_w); pos.x = text_x0 + (int) space_w; - pos.y = b->y0 + (b->y1 - b->y0 - c->font_ascent) / 2 + c->font_ascent; + pos.y = icon_text_baseline_y(c, b); wuss__text_draw(c->font, c->scr, icon->text, (int) strlen(icon->text), text_ink, text_ground, &pos, NULL); } From 4f36d2504308bc84882c4c6e40f1354de250e770 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Tue, 15 Sep 2026 13:23:26 +0100 Subject: [PATCH 079/128] feat(wuss): show advance width as a light-blue rule in the chars task A light-blue rule under each glyph, spanning pos.x..pos.x+advance, gives an at-a-glance ruler for how far the glyph pushes the pen -- useful for spotting advance-width drift while eyeballing a font. --- libraries/wuss/test/tasks/chars.c | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/libraries/wuss/test/tasks/chars.c b/libraries/wuss/test/tasks/chars.c index 4848e944..e18fa5dd 100644 --- a/libraries/wuss/test/tasks/chars.c +++ b/libraries/wuss/test/tasks/chars.c @@ -295,6 +295,19 @@ static result_t chars_redraw(const wuss_event_t *event, void *task_data) screen_draw_dashed_line(scr, x + CHARS_PAD, pos.y, x + cell_w - 1 - CHARS_PAD, pos.y, 1, 1, cc->mg); bmfont_draw(cc->font, scr, &ch, 1, cc->fg, cc->bg, &pos, NULL); + + /* advance width: a blue rule under the glyph spanning pos.x..pos.x+advance, + * a ruler for how far this glyph pushes the pen */ + { + bmfont_width_t advance; + colour_t blue; + int adv_y; + + bmfont_measure(cc->font, &ch, 1, INT_MAX, NULL, &advance); + blue = colour_rgb(0x66, 0x66, 0xFF); + adv_y = y + cell_h - 1 - CHARS_PAD; + screen_draw_line(scr, pos.x, adv_y, pos.x + advance - 1, adv_y, blue); + } } return result_OK; From c672c01fcabcfc01c2c16ccbd8e38737bd8b4c4f Mon Sep 17 00:00:00 2001 From: David Thomas Date: Tue, 15 Sep 2026 13:25:07 +0100 Subject: [PATCH 080/128] fix(wuss): update fonts for reduced advance width --- resources/bmfonts/04b_03.png | Bin 579 -> 618 bytes resources/bmfonts/04b_25.png | Bin 762 -> 656 bytes resources/bmfonts/DPT-CookeTall.png | Bin 1023 -> 1030 bytes resources/bmfonts/DPT-Daydream.png | Bin 1902 -> 1897 bytes resources/bmfonts/DPT-Digits-Bold.png | Bin 930 -> 932 bytes resources/bmfonts/DPT-Digits-Regular.png | Bin 967 -> 968 bytes resources/bmfonts/DPT-Henry.png | Bin 1833 -> 1840 bytes resources/bmfonts/Nokia.png | Bin 1006 -> 1005 bytes resources/bmfonts/Symbols.png | Bin 265 -> 506 bytes resources/bmfonts/Tiny.png | Bin 378 -> 417 bytes 10 files changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 resources/bmfonts/DPT-Digits-Regular.png diff --git a/resources/bmfonts/04b_03.png b/resources/bmfonts/04b_03.png index 491fd8a97a8bac9b7ebaf9b36e74677772b0afa6..f2d1cb8f59112ad48307075dd9bb13383150e1ba 100644 GIT binary patch delta 605 zcmV-j0;2uH1nLAKiBL{Q4GJ0x0000DNk~Le0001>0000O0s{a5001~1rja2Ue*zgv zL_t(2&yAA7YU3~zMWsrHA=n^cdpXc$@yZ2zFJYN(8c?Xbo=<7WYRFfZtc<_Hh0NXy z=TnAR4Vf#)Ne4=yS_qLGp6@&Fk?s9z*Y{c9llEb?YuoJGUEe>f4qcz^{q^&XblGax zkwNgv9K~9b5ic_2A)`|gXnRc8f7(rD$jCA0)Ot`ir`%AYiijx>a$STmp9IHC;Eyql z7zY?KV;`d@fLz-l4VlnIR9TY-m6*zBMpPbF)_;pOzX~1$3@;UxE>Nt=O{{djR3w** zuK50hjL0)051EQ!gN#w7kdgX~Op&ucf6mwQdj8~)^A`{4`~9Kqy(wIWf3C~=MPY{v zg7>>^-+7OxGN`OWhfb&AhAWZ8*5NnjgppK>S}T?KW|TFOXn_`BPYRVZ(%X~h%}A-m zGbT}B5~Q}ZCOQpt(g0LSCL0gN7NC|>xklIUMu=&aikJsSMUhDEL|yS2oISqH;0Zq> zBWK7MX)wsBN-phKM$G6kf6!`hA$6bV`Ka^j9@}j8xQEC4)m^t;Jnk+Y{nc(E7B z@`n8&N<`Fe3Y(V_Q(s;d9%Dkr63v_t+&SzKSE=uQD{ zFTAE|7Cw66>M>+jFp204+66Q$0a%$)teH}?$Km#A4|0Hh)`r?+Gr4BQl=TZTdhpgi rcJlAd;=9M!4<3KK@9%%$@9X0q9DA5Ye2kcs00000NkvXXu0mjfqG})= delta 566 zcmV-60?GaA1j7U&iBL{Q4GJ0x0000DNk~Le0002M0000O0s{a50MXwX){!9^e*yAI zL_t(I%dJzfisUd1Rbe;50vGo|3V6CJeBk+>ney1LNE!SU8OZiI$kh88S1of-awapo z#lpfF5oFmb*MIoFqQw?VHBwHs-2lsL{Vu8tP?fhIM# zdMaLKpH~|(PX(Rph4S0qIy;xt}F!xHaPcQUX?-Df43m#;=ri` zt;ItGZI2`&L@+d}+&dBfaK?e{x z8{?D?`Ch>&M}_teun-%d>2PU)D-Ns&u^v{vsyzq`whM}|mO>BH0TSmgXnp%nI*bqc z;pleQb?ELo*qs6M@SF4vf5I;9x!Xv{HWJ&r+I)27=?|J-pbpDbo67|U5YFeOTH1t5 z$we?OO^3-f;o2E0fYxNH#q2lX786c_&fOibo#~_B0TQz6z^cY#6%6q7OYJQvz5U2RbSZ20$) z5WHC&P^jwaYmjp>KqZdF`Vs*UbzT<%*gOC_Yu6b&K(aE~0c8WM0RSdx&fB^ppl5;e z`YTh+9niNR2(Y;>wvDRy%dmx(1pqvV#X<-`Gz-F1Er^SGP|brqh?RyIe;|#;(pcJ4 zU&#RK1KZfs7+V?@aBbK3^?3ccK6hEQ-&T4X9WB%08RijGVu51s!P6Bj_7QQ}ZJHBQ zKsqNYsY5Aq7~25ggXLcU$!2Kvg{t&=)6uzG&{o<32q=mk;nsCVfdBslpgMMdrVt5s zKs+VmrO+i~V>$wO1E93Tf70v4Mv1vk8UdRJ{!F$luoV#EGyrb5kcKVTNuZnBx8Rky z#11&8hXB7DgXmf)xd9Nw{h;tPy4!e}x4YE}8s~;76Gx4efTgh)QZ)7XueiI^?lfdj z393O1RZb1?|7!n!tc820{xe#@7vzgB`?3Df%SlI#Zs8MWs_ANZ4 z#-o@PJ7{ZAOv68FjyHtt=T)J;Inj?fC{*3+UQaNgkXwB1mXC0Yq26A zM7PG<1CITZ1hmB_=1i4?OBn`jT`4ExJt{eoq~>@tb`7B3)kgUQ+Ry;1e~3Z&KV{@f z2cw>VT&^fmyJ{3d{m7spH5WmgQE&DZov~QiPg0|NqIZ0shjDDvH|bJ)kP!Z z75@FSK1Hq7+)*@wYMXkDe`HVUqV#Q1dy+Jb<{5?hb;(LfIw@vOyxEJ~soo0fhSi zb10K>h}QtMUjCnh`~dQ?B7Ok*D0&{q322s@nxy9Wf0pYF!fw*+AsG(Yc&RrqyQ{Wg1O&v{O%RaX3?nA#t5y0EuwyqNNn0OwZJb>N zFa|L>0!Y3|-U?W28z(FP?G7jfCO5erAY-5d3Dr)&u>)WOcp!HKOzwXrzzDPRW;}v8 zfIq*d4xl|F&I*7IfcDE3pfi{p6@UhA1$YA_0gxk^9lIQZN#3CWW~af@_$CXS8fBdI zV{$o83SgPb3`uY-m3afTovkxK3DcRbmcfYtNr+Cn&57A`h8l=#JD_O8^HP)-l}yRtA54nH}VGvyIf(Eui+F z0(9@#o-W1KI|IBJgb;={mY@M-a|4#`|0%#E+9|1I3AX~E03aKarcj_)0GvHnS(9LS zYxe*$pJnS70Go1j05Ox{Gk_m_&|ZBQ8$hEkn`s5)NmUoHxqt@9ePaRm0n7id0DlV5 zAPV{bpn%nZF90KO0|;l!xF*sb5RL$Zj{v>`*fqe2brrCsG2~PxJF9XkW3iLJ0u_IA z`L#%2jQ4n#?}hJbp;Gc#q)!AV3yjJS#TRAc6N2(;fw%q^0!>hEd#QoVJ>@FnSNXcW ztbN;>bhpY{5PS(h-_RBP&*wdtv%rp$f2#CsRzJcHK>rDl!v?CTa?v?&`K5b7uM}+6 zM70wX!UD*O#G6c{PSQHDeF7i{W4V9#*nk*?PLU7~`=m%3ny9K{2LPS{R<%UG1BC2C z-n4{UJ@B2RX31KLuR zn`j5S delta 877 zcmV-z1CsoP2>%C=g#l%eh8}-8N-wAS6!wA}Sk-sPzSl$&7~@B0Ix|rAU_^g|g!ln& z@9%JXmwL0Ow6#LB9IEuI<*IEM0ReG#69i;8!-z@xT9v*8?AT36($B~j6sGR z0VLlfZv`xMwG&o=b_bLKlbc)*5F4l=p^npU>;Sj|JdisACifCxgxP<28$5zIfIq*d z4xl|F&I*7IfcDD;pfi{pRe%O=0eAx>0gxk^9lH{PN#0=v%ua))@l6&u9aM1EkI9ud zDS%}zGbF*WU>bl!vep@Z%y)fKiGu)GVwR*uj9?YPbfr_-=ofaRJ!RHZGJHYykcQfKLIEnNHnkE>Q3#Noy+4<>%1S*WM+d zB?I&=Gsrn+Efjx8zDOfJ?E9`wseGYGYKe5=%Oh(mxAMrV3;h;q^ZK|#eo|t7t$+#1 zUM}9M#2jo(&!1~2D69)251C}>j?`F700$fvo9+NBgT8DWlyrZyjnvmIp!J^ubnn=n zuEo~70K5zcAq=Z6!3vPg4OqATrvQ^^r=*f4+ya0KfHIggg#x_-;NrQ;ngq*Ry9bc@ zEL*n%*p#CK$S^UV0sP>D_Ugm90yO%vnHE5qRC57W7tjEuZ>#`6VEz9M;7L_r?_ z6tFq)1psaU;cR~ymqgkF!V!S*5x_S9yA?2ETLoOx7)mOWomC~3v5rtFkp#9NYD=Zp z&PI;Wq=G~^R6gW0|H{%A<2~NxJM&#fsFXai^oigUfl>LP_@ZolLQq~UaD#I}dEZOT zTQ4ZLtJtsl^|JPDYtr2+YeDcO0DVJO^}n3=T+aeKO8$ST(zA{F5q1FjPk4(Se%g@4aUL_t(o!{t~#j`KJSRSGo&yNmCm6z&y3M9Q3GkvU4rAg4$Q z-ylX#l5*hXd!(G7`7(2N%RkK<=J9zQ>h_(15>;bR}dQ(rg+ zeL9%fYvcYyf3ojjPlwkh9l3uU5>6pBT?m&LnnlW|WoT0lLw`{b!jM9nZAf?$ixkch z+M+{pXhg*#wUWa*2MASfB+Uwu;+)yYR+ZAFCa#N`(eE&-P^2-3s76oe6;L||Nl}qT zotmnTeU4Iq1flA{tqPJLun5UdfFcEEo&a01xh-D-M*|cc`3JxRaS8O(S-AQ^*jE9^ z2td)HZLiik`hQZ};V*@UcmV7O(N@45BxVTA5c7>G<`T3U({?0xo&knXIyD18^Ow$4 z=O(^-^@(Z_DOW&QWawSgd%Xc>I=ETb3h#iUEn7X?uI_*;T6L*T5!Qy1u^4gN@QK)v zQWYHVTzp7wIW^zk@ZeIhlXU7b`lxTXQTI`Y(qB>@0DrNg1{l8q;y~)M6|jM4)fcwE zb!x#jhG!?k7+bRgNN<*1Ycm} z*}?#355Q@fzIM|@v7N@vG;OcbZL?*Ies8Aw8BJVG;U@e!%Je08q_}N341G2l7Tm;5 z|Mlr5P@cat;6?v4J4tFNuI$xCK64|0nh5K8glH{_{t-;6zFCea* zTEQJ1aq|{Q^_sAx6+5gDU{1Jn&>NsUGgyE|Fce&i3?Yd6c5|~pqt)m)W~n1P=rO41 zJ%4J;QbSWl6xWO2W?E_OI`Uc+0~|x=ifz5QK2$0P0C4Sq0zme0qz2z&dL<%YtO@}o z$MOn@83ORqMJSTqvPH2{qpt&=i=_eCqF)hAU!z#79vD};GzRYnKoT&$#%urq@&Qo# z28ucm5mKE|7`3CtOAi%{O{oIL)FoIhs(-gAm4~xN#-4x!VvCRgi$!ELb6FbTdeyg3 z6mFN2jxQg(=71+-byZZ)W_V8lx%wUx#q=*w(EU}wb)Wq}V4|L9#W}+X>weivpWL602;60Bq7?bgn-3Qf&tnc^CJMxs(?CY2W;(la-p!TZd(-FN^S{eaR(d? zLhO8;C=w4FxAGyeJ6k5{Nen`=d1wZ-#Sr2SNRl)_pITyyAYFFAR{S;#ChCa}+;y@{ z(iqn+3(kPve?Fx@02U|77eIKA`F|k^*Z>(fegwQ3n~h+ztSL^q8PTM(N5GLi(n zZLj*JEugpYuif=)M?O^g6YW>(yI!G(ezo}}WKaraN0H5cPfz~?KKyjD`Zs|D?B{huj-00tvKctHV<*cQPuuSdu(=e))r=-sVDj$_MAe1%ElVCV!)A zbXLnr%?LgEGBNxi!E$!}-q-mB$|$#RM{DN)3^-a1`3RVQ1RVbv5cN)87+YCd&VXmd z!Dxwt%q}G#T8iPJv6j_Bb@X|Pkwb>}0*ffb@R<*_8qj$UR)oo1aq!712E??)4Py97 zmSR{a_p;iV2v3OptAt>L7=ONU8n-1OLFz(`CD*D?JRf&J4{b)0x#-=~;V!Rz={nhPaU&>@4=w|^&`OyE{>h55ch z)mH~=sZSoxta+hdEQy*KzknZfg=OXySkp~Mqh(K4Wm=M3x5C&Wt}1KPbj|tHwk!FY z3ci1knemN~*sZt4&tVD*g?an(1dP^mE!Q)R-{uEmvB)wIa-|}I7L-H07fll62U->N`BGZ^=M|}U>;L^?X a>(AeJyGvZ|5bN*&0000@L2KQn*(D5h-($MN;o6rVMOpLvm4OKpe06dvLMup>lU0dtU;AuvPCH>Q|N&~8lIk=%I(7((gP3;@kv zI#ZpS`0CXsszIb&0cDY)cTw;42AJvKW??J51CF+A^=!Mk1FC4%r8-4eJC}^bh}(uw z#DGx53$`&lI~m5UMcPJ}Ou9=(nZZfu3HGTI7Z zdTD2%P1U6tin3Hix^zr+51|1M>C`R`Ca-5ik)DxMtdqE}WMQFwQGXuw6v0 zm3eLfXMYxs68DfF1@lE+eMiCLm1YM&ol@M`@(j3!MgXAiA@AxOTiyd^1I%`TlJzC{ z0wYfr1~7X7PSf|rO$^igeJD>oNeH^L5x0qgu2pFqE zK*_PZ0%C>$ymS$Yq_=EQtkmf1fTv<t1k=|j)~W}_l`f6J`vH&yOs_E;K!AJz zl)izY4n%}hXB0;5Xz|iR1!GgHfH8FmmVb-tElTC#tdX%N;2E(+$biKnvYNRp4RF2c zTPO;*OG(FU+Y^6_b zTLGg3t`W+M1Jc0=jq5C3;bK&QLlTblu>$6H4bI6t>&1e1o-+jks!`B{`Ul4h$$yZ> z=`sTrOCubxED8XPS8$RL?N>s;;wQlX?T+~o0B2P|owEbBc09RI*jBeKiftvg1hcpU zjs_uiK8_TLhmBkLkl39plk_A8A=x}M1KMH;aR($x8lX=tF-4FrJ76n*n*|f~LiMabI~tW-EJ;3le))}~-sVDj%74$!hYNCS zP5wsL=&Y8Lnh|>RWn%bEg5~V`wXgFFlu>Tsj@Hco8E~{3@)0n93poBeAnKjGFt)O^ zoB_{@gV7QPnO#agv=qZbV=b$N>ge+nBZmy{1r||=;WHmITJ{DzM&Z zYgwHwjj$RW;a158fS@iP;(%7|bxQ(rsCAMEvk}A09Ry@;!Eo)oq}2phVU@S#Spdvy zzA1157BBh?a2X8HQWWx17b+hj?dsJ`h=;Pvd4b5n;H0jqDEh^xL)w!OZX0gs+i&45HIwlu55 z{Up(sOt2o_emnrGX?6y0xw0?>%L#ey%GT+og&-JCNpg2(VMspQO~OANvO^=N6nZ=I z;pR4xBw0Het5!b5(_1RR3m$)x^1IjqA?dBU{FD#t8o52qhuNQ6wbm2 z7+`dX?q^x3{OIf0m}G!9;BaJ$)RRQm@#0Plf7G|P;sQKYy{Is#h~fP|4dI5=Vlz^iEiX7vmipm$C|_!P}~20rIa z;Xn{kNgN2&*vaEf$}G)UvmNi+)w%I8@fqS(!! z?aPz9D*g$dEoX@T_@~^kQTIX#g%!rzrrVmwP@c={Q0*dcR-=%5|rbdhfonfYmR&?Y|4)RsiOvn2!dq-#0@S z0Qedp;0i#%3Xo2b0T9-CkxQGK0B{{(_8owcHYwE`?I(aYY;x6X5&246Er3l$2rdH% zr)sZYz<&b(!izb8L_yO7fZbHy?xg|{C=Np;t$5bV0mcVn)Y6v*8|9~V?j2HyF@$J3 zvE@@Kgp?|bUHJ0_4)~t@2daQ&-T%J!PXuc$sABQF=&9lcnK1azw-l&1#$2xKd_DQ+ zh_ha++Vvp`c5<*50HF_l6(AI<*zpm~j~t`0;D770hjL9W$ga9I@0XVB3cvy3&F^#~ z3l6AZ+f)ImXt&5to)iua`xXFR0|>YRFk@W-k9=?sD@c6O01&PN%y~{$$##VV*8!|; z5j?wsdk;`9i55VT9JTa~DuR+h$Yfwo1ZxzkAa!GlGAX?me)Ayu qrX_bP>T9Y9r_ItIV)I~nBI++%0hc2%y^CG|0000Al-bVqFy#k2vs>o52qs%h!DV&85 zFu>>%-Dg>-{ORY|m}G#~AQ*)r^(0Yz5{y?E-(egti&O4(bJlqMYO) z)a2<#QFsNQEq|K#FdrZwOb%7p}<{E%s2be<#prnmL^-B8&u=7o>nk~XmX{!a$sW8E10OnNb zWej)#@PF`P4!|gAdH~RyD(Jmb03xPgNGKIgnm9oDfQ?f6+#n-=VyE80LX;s`xJ%5;MazS?0t$Dw+WLE$V9(LhPCbHmw z8n#UpU^TTxcKoDpaM-s1@ESmLD*&^vE1;1N?qLPSCk+7lb%5FIR>^vWMAreVZ4o@X zf?MbS$|cbPaFUahzEVXLGKiTB^ogL2LKP@(WZ_0n6;Vj#ezx}qkBsMy5)YdPF*Lnf oQ6pbdMLcbm{*amn(-TpD0fxAprD0_)HUIzs07*qoM6N<$f=yC(djJ3c diff --git a/resources/bmfonts/DPT-Digits-Regular.png b/resources/bmfonts/DPT-Digits-Regular.png old mode 100755 new mode 100644 index de4cf73af9e66f29a0412f338f37a4f5d09a0b94..511338985b38cef93ca86b67e6a4c845d0cfc8e3 GIT binary patch delta 848 zcmV-W1F!tY2gnDIg?}zdL_t(Y$E}q;ZsRZzg-7q0;ED%8_5w;SzZ-&&Lt{A}r0JCuEr0c#U3avkc}x|%%sind zy)i2jSsu}F^>Y%oHafNdRit+D8X$vZ-*$5|fcAPrv5Fq)MJpr59)^IP^c}^75f4(4 zH9Nwj4ut)CkixR92mR3s%LE-jFIbLq1Q3Sy+9KN31AGjMlPH;jmdb}_ViQ0h8sq`X zc|>v~pi1AHm46K&BqHhdFk~5&2=NX_gMl1*km1dEB!FGPmO?> zC`StssXpzT>>Cwt0tmOpVM`*l&k-lWnh{7M@;SmPa;|asj@e)5()I|m#-Dr#$Ido$ zm8-JmfZt8lWNiw6g<-5q*~@_1brI18!@TeE27>KJ$bWs&M`b>)`l1s=?v3)-)J5JX z!iY$OWHMEHt79&IOQ?|q28snENG-B%SKcLaEDWh^t>pTWh_8P`KH0_$Rqu;kE?Wyo z?H^Tt>t2R3SwPDq$N+l)^sxRk$Xo!ua?!0Bv@>D10MhS*aK;G;CF()`)&%6MR#Ic% z2W1#}%w^7|M`)!vv@DSd)!5n(CGua!*GZ(H zkwm_G;|DEHFmf78Bu8I4mq@{Ld=nCh$K5lIXMa%A@7syF`W~n-8I?iF7$mAO-4k&g zWc+iwpKB>iFIrYFKrB(!U4IZQztCbNQ_%*{Qbq9^>`|+d^;}RFD%;2SRNQ;_sN<9o+?649m;ojQ|9=witLs@Tv2=$&Q_-O&99+Fwsb#Mx*&P-= zKXo^bjl*<*Rd#>X+`s=$wt4hp%YDSq=Mf zVxk-^Mx^?*bFyz$x{M&)8iy^3)FDTl2x|r)i74a04Z9O~N z!dJd3nk{}eMN_mn{1t|=E@v+TYTrdf7Yy^UD;Nm2AAceLO&_^(SoN+GL>Ua{#;$!Z zTpSUJ>}4`ldZkk?e@m#53Zs)-zb1V$0Y^~(_vV^aHNg>(BoT@$)yIi&w zklH+{{?>yGWwL-~S(E|x0O)b?Wstc5dgG#7FlcS!ZV{wE1mTP$5K1(F!mSA?RIQ}O zehgZkKz|h{l;51!9`wjM939ODP<8@Ij#-(5h>(67fec;G5ez&v5j0;6K}X$zs=Usd z9OZ9)l{c)5T>(M(_SxH_-aXeC$$gVIb-RTUn!*>}<~wMm*|sc^3f0)!PbKnS#y3iY z1`_!h4VoTclr)q`$$jNaB92OmO-Ll3cF#DTL4QeqXeZ|Cd!XWER0d^3kf_FVPsDYQ z3D42dzrhb3t9G>=5I7(LUf^Pi}jV z{sPF1LDKt7=pBnuM9WTe>P#ZBi)ANJ#fCnC&W=W(MilO-lZfj2nn(o6_FVof5$GaE zPe`zkNR7&eM9?mRL@dwo5JWYQNZzny^3Zas_Yz6PNY8KGyit|l=`mh2 z@B^msyTnvZ;a71wykrv?_9U@H=(X0!!B%&9QMrlrn7G%ouH6v^ONHlFs zdBGx`)0A189Ve5n64`2L+QEpz$@;U?y(jL?}&U>Q=Hu z)>*dlvyi1inz~S%M*t$wX<;nY7E^c3LC5LFFmZt8AEe{Zz`2U4nTpS2Bh-4)z%cb| zCu41*bbpQ`@W|8;GEfG~9AG`>IE`HaL)vh0E*ua{TKeID)c)emHRW|`LKhbAo$J+< zbrGq6TQ+|wn<`r#O-dbA&V@;<&l9~{F*LOV#@Y`G|pAK1_$$7h8uGZAb+|h>`PQB+$Ael0YoF$Sw6e7eI9@6 z@4M6PJnXjk%dwKm=gq!Ok0v>7&nLzYpU-UKqi7G0sv%M|v-0_;_4RSa;rD(senz=! z&fiNv2mXc4-v>VbrjMj+eD-&e`ExE4vb6vAe&*L^|6Dhv7?8hMP4T2sI!LHOy7tKa zB7aWbv4$#*7?WV9JW3FV8bBm?B58UwuKVKr#6}{_wPTbuBr*C8Mi$&0xj0NxzEwxj)fMrwFwhq?tD+Sv+ zUW2KCHx72&>K=y-)_P6A$O_^;T6B`TZFP<_IPx_(+NRzHXNT}~q+T6s@N&J0xqrMx z(nPVxV=!~sr;Dj|#buA%)wQWJnB2BPYSI|&hfS+HL@qf3SKw~FD&UWt3&n4Cu3KO) zZw)+)QNfxPqYVWxYMYYyqh;Jn=X{(-VD{sLO&x=243<$JyqE#lz30#*949YzkokF4 z93<+t>6TL+Kw511%C~&3;H4R0>VN*Rv^~z{+!fe1@i4gtp9g;29VMCXG?@>Qy(Of)F1M~48 zSP0UNyAXYk#ci#h#ZwI+T3u%4-Q2$z1C8S^H`v zhcc1`TPj1%g+#|CR--R2(+Z}}s?MzKX^}4#h-Jxf=`&VNc_VIs@lf$@#Un80mB*q_ zfbmqN_w+09z}mz^MYYGZsmI`)kH_Hn5ja{w@6C3T3jqVmgvVfau79zqnW*IB3hZ_D zEwEff@vy~LV3At6Ph+qir-Qwx+;FbN2)w!iXV)u_b;Z|oi6{gF+?aD!0mL3|WN1sw zlCF`x&-$IO@$&t4)bxJ(XRCz(GXL&P3W1#}qj)%4)gWKz6Pk}&HE1D#zO01+#&|nl z9v6<=doKborZ;muxl0u>_)Z?wJ^pk)?)U=cJK%5!%>M_BUG!$o88^Z4PUgp!f4&gd oUVa^JD(EB7@OWp4555k=|D7|5;Z>1%s`ZvZ!kyBX zS;>~8aYGS4JgII2l9MfYyH%v;5b2d^#3Mi9K%!5*rxDiS#PoCkJf)ufm_0RuG8csd zbOo3VSh;)wj(@5vmQ=U~o{N}BkCrYpvIYevbAU^Gs)3o#|`p9BM|DnV8C$oTPwC!L6pM) zc;M=1F@KbSWexBia~OxV0E6qeBo__{7A<->Aa%Z^C-_!MM~?m0Vb~<~-5Oj-kwMns=OzQxgq1#Yw{!3b0FwddNYofYbT$cAS!K-fid}93N^O=o(6zP7e62b+Uh0niDH`y7M-^XP5jBrIxZ@HfX{zCH4 z0iXZSBmEiA?ol(RJSHSAo=SLsU$?8tJqCfUnkhOuNU1`*PL=&tlD=gvRa!Ad-cEUt zB7YFHfJpH~(@fR4*2Vb=PAbM!3Yib8>&ofM=XRQ0{mT5ajis6QdKP_?j;6LYOdwH9 z1fy0jq_S}m_T}J0m_sfg?Q#`UTf`~HIeyDU&>L{?-9J*@b5G^gV~RkBQL+MFCSc|z z^M#v`u1jVG+$7*seYI%6WZ)RN za!5idO_VEOufUPpd%*E&2>falbRxC@+y`sz#9}@HKF-pxy`036a{7W_756=gU>d<1LydhCLnvGk=#j zU0kg@E-`Kv*RIaMlzb~lE)IdyVbkdjl}nDm6>z&;6yT4N3&U?tu6w{~yfxrS2nwui zF_VllJ z3Ckr5Uo~{tb4I3NFO5EGN`D@^@>&B%GuQdeZdxEEAZ5p;ICE2kRaYxHgwZ6}S{Y_8 zG&(M|8oIblD=@V-b!O?*7Wp!PSeG1*K11P{191l!A1ZEkJOIYJvMu@qFg{i3J^c!} zXC?8WqKa|t>LGCM$3x)o5pb}9K9S8T4*~`(3myWe^bv%?7}WE8v`Zw}k8nd)(bnmmi z^EF=H??-L#r+?Qv2%z(C-enNjs4#|yqfHI+I-fvpb!yN-0J^M$0BpRUFCQ0<>qlP% z;LJqkdUCH~@Pj_6F+~1!KW_N~%nyM517Q9?z=&cZbI-U793FIjg#7b^!20sDzbl|0 hf%@$O6UPsk{{dQ_(qd_?t(5=(002ovPDHLkV1hj?J0k!9 diff --git a/resources/bmfonts/Nokia.png b/resources/bmfonts/Nokia.png index 90bdfff7713fc0cbcd6f1a645158cc777ee5d188..96482650551d4d15de74c28bf044a120d31d7930 100644 GIT binary patch delta 885 zcmV-*1B(3a2ki%tg@02?L_t(Y$E}sIYU4lkW;5h8j<&V&$Bp^3(9$|i--k-pPpiS;L;wasXnjtw2`oD9KOBUyzG z$Wf09O$kD4y2EaWvFx9pg&4~gQRo#F86Kx9WGJ!p2RSnfPbY+UIP3Z+HvA!zl3Agf@far}&CdNgJQ z+Uj~kodKa0o-Z|6_F&;jHRnJVLR0ZrVMY2WZo;}=sDt ztG(6>F?~|e#T~)O&AbyrXE|IFgyceBPC}Khi>3hAE>uMm@U`(ZxNU!V7wV>I;VD8> zG&PV0;2!^Nd{ML>tb0(6HKBF>Q)&kWe@HgVgn#&IvN4#;NUe~)?aAK8nZ5sgg1$t~ zE+&KQ*aoHKV4O=uHqJ_t5h0UBuOEgoGCU(+9gT(Tr3f7_h0Hnozl1s_W*Fj)olPYM9|;U6Cz6+1>MhE!qD78z`0|-C7|k00000 LNkvXXu0mjfdv>JW delta 886 zcmV-+1Bv|Y2kr-ug@05@L_t(Y$E}sIYU4l7$#9a>i_I)@oOmokL@&r06GAF9)|gG%q!8NE4;nA9{wTDv8BNlWp`D$PAs8zp ztI!TP>QSKyL1;y{*bOn3{qvI$W7#|ky`dt*qf~_qC6@jmXKLZ;gm5%Rch;;rA+7(J zGO6FH5cbIF-hbYrYAma%6Jp6>Y<8-sws&N*%ZidHr1iTfq&3E)bro@cqY!$t^YkdB zceK~@D8BwGV4gsH^!G-rjyN3BUyocan6!>dG8$yk*=AnHEt*?Ds zAOMd?$V*hxi_qUF;rTm5pI;2yd!D~Ak~0s5Z~9PGo`1vI86>c7L2dQBdV-Z5nml0~KclH0 zjah-Vy53M{Kxm2QOAVIYTX<5<+0mKML_Ahlk$#Gsu&x(sVYn!i3v8KLej>EsS~$uT zNU)+ptA9a7%f2EOA!poC2>6K5G*2Aw&<0c4bq}HVYL2tdg`}c%X)*{of{?UW6dE~; zHn7HOuk}JqUsQBx9r*4wnQWxzN|6Q0)&zQ-Et1>Y@qwTKfaIZGU+e4o%a- zQ-r2y4nP`!d;GWdMbUb&?m;!ygx2{lsT~;nDSz246XLtc#$YZZwLYcm*m#|rDr?#YgqEick@#7B-Gq(H8gY4pT4>Y!Vw(1Zs<^IjneIsNyN_OkpM<9Gy zKOPXm@J0w^1Nj2ueC^i&T*)q!Y~g6gaju3LKGp@<@{`>?|M!yp5BA`tKVH!_;s5{u M07*qoM6N<$f{re(XaE2J diff --git a/resources/bmfonts/Symbols.png b/resources/bmfonts/Symbols.png index f4c8a6368e6b4f455d88725f09847bed01f8fc79..d0e2734c802b1e7fe5b8db4a9c3556b024d0eb3c 100644 GIT binary patch delta 383 zcmV-_0f7FA0{R1xgnt2GNkl-b8{A<} zp)X~mLr>yUEM5%j?52OD|G@7wnSA@(FOz{{a{knq92F16QZLYj zM)3^bi&Hp0SkUpJQ9Qz*)>L&hM^fE6qYfmhi8tPpV2T-P#D6WKq(V|b?nUc^U$JZ8 zz-m*UOhUAxo(Uk6;yd1Az6qM@1iA8{nWSqKLwIACum&*#O=X-!a&R!wObgDn2bC&O zTr(ymDA6KNT!ZM{s}L+oPW-X9N$1&6n{pFW`b6kKsMjFvLA_w&-UM+LK>&5~L7t6t zzLf~IAW(ltqIj6CKt@s`44>BdBv7sm?oYIwkzC9u3ZSUvZ(+rm{E-N zj4Bs|_j7Bjv9m5Ts(I*rRo^)F&n1p9M}z1#RGAX(kcvHf)KpX!z+k|@!@$hIfWqKfwHi$X4})vfVg}rbgo!tZ z$AKA-LC(Y*#2SFdAVyLQ3c+U(5k7(&6pF_nR+0>Irj|jtUBn_nib3Eo`i~X-z5btg v@HHMt;IM=@fCLvo(h&|@;z@837ytm058Ko9F!{~^0000K%{pP7^HlGP&1GBY3e&u%|oqB${d4hr&$rJ>47 zs!Fk(v;-=PiGl+H?5ncHj6`EW;JhQ80;!fjH_D?rkgwfPrGG-~)d4LC(-wLJFRv1B zXP%McIyn!Hb6T3c4EyU$tZZK?Fib_uxib7d!4+^(BXA0GG zkP=iY2#`iygg1IR@ZF)xMOFzw0~`phVb*Qhus&=IoA{8HW85^yzEe=hmc92w0VBwN zDRv0&7N04ij5fCWqW@9cr#}k3#b=7B$7yHut6wwWzbFzEJ@clxvLAZ`A<->}9!xX; P00000NkvXXu0mjf6mN-1 delta 278 zcmV+x0qOps1Ns7xZGQ<+OjJek|NH;|0002MC6bmW0002vNklwpvcb0r3NmJus&-Qr7{~l?*I0vwtTJ4&V#*O5p*ml3e4w zPff}x@(G{l)c!Vx&l|@{9djBbz_U97HiB4L;1*fL-BTRqI%ItJL|_`@SkRLUo|Rqv zf_{K_T$<851+l%|{ZKfF`#*}tWUG!vrdZ%o cI78_d$o!mHOb-DMivR!s07*qoM6N<$g7#o}RsaA1 From 010632c790c9b72d081a8204fadec9ea00bc74ad Mon Sep 17 00:00:00 2001 From: David Thomas Date: Tue, 15 Sep 2026 13:25:39 +0100 Subject: [PATCH 081/128] fix(tools): ttf2bmfont ink-advance mode no longer adds a spurious pixel advance-source=ink was ink width + 1, an extra pixel of trailing gap that was never asked for. Use the exact ink width instead. --- tools/ttf2bmfont.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/ttf2bmfont.py b/tools/ttf2bmfont.py index fd43873e..0a9e4b93 100755 --- a/tools/ttf2bmfont.py +++ b/tools/ttf2bmfont.py @@ -120,7 +120,7 @@ def measure_cell(glyphs, pad, advance_source): def _ink_advance(g): if not g["bits"]: return 0 - return g["left"] + g["width"] + 1 + return g["left"] + g["width"] def advance_for(g, cell_w, advance_source): @@ -257,7 +257,7 @@ def main(argv): p.add_argument("--pad", type=int, default=1, help="extra pixels added to the glyph bbox for grid lines") p.add_argument("--advance-source", choices=("ttf", "ink"), default="ttf", - help="'ttf' uses the font's advance, 'ink' uses ink width+1") + help="'ttf' uses the font's advance, 'ink' uses exact ink width") p.add_argument("--no-grid", action="store_true", help="omit the grey guide lines") p.add_argument("--first", type=lambda s: int(s, 0), default=0x20) From bb16d50e40818dcd524b5a3ce68fdf5afd205306 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Tue, 15 Sep 2026 13:41:05 +0100 Subject: [PATCH 082/128] fix(bmfont): add back 1px letter spacing removed from advance widths Advance widths encoded in font PNGs are now 1px shorter than the true pen advance; bmfont_advance_for adds the missing pixel back in as letter spacing at every use site (measure and draw share it). Measuring a string now drops the trailing letter space from the reported width, since nothing follows the last character. Co-Authored-By: Claude Sonnet 5 --- libraries/framebuf/bmfont/bmfont.c | 16 ++++++++++++---- libraries/framebuf/bmfont/test/bmfont-test.c | 13 +++++++++++-- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/libraries/framebuf/bmfont/bmfont.c b/libraries/framebuf/bmfont/bmfont.c index 9dfb5b97..14ebc7e7 100644 --- a/libraries/framebuf/bmfont/bmfont.c +++ b/libraries/framebuf/bmfont/bmfont.c @@ -31,6 +31,10 @@ /* Input font PNGs must have 32 characters per row */ #define CHARS_PER_ROW (32) +/* Input advance widths are encoded 1px short of the true glyph advance; the + * missing pixel is letter spacing, added back in at use, not storage. */ +#define LETTER_SPACING (1) + /* -------------------------------------------------------------------------- */ /* Pixel values */ @@ -63,13 +67,14 @@ struct bmfont /* -------------------------------------------------------------------------- */ -/** The advance width to use for glyph \p gid, honouring the monospace flag. */ +/** The advance width to use for glyph \p gid, honouring the monospace flag. + * Includes the LETTER_SPACING pixel not stored in the font. */ static bmfont_width_t bmfont_advance_for(const bmfont_t *bmfont, int gid) { if (bmfont->flags & bmfont_FLAG_MONOSPACE) - return bmfont->maxadw; + return bmfont->maxadw + LETTER_SPACING; - return bmfont->adw[gid]; + return bmfont->adw[gid] + LETTER_SPACING; } /* -------------------------------------------------------------------------- */ @@ -710,6 +715,7 @@ result_t bmfont_measure(bmfont_t *bmfont, { bmfont_width_t current_width; int len; + int any_drawn; assert(bmfont); assert(text); @@ -718,6 +724,7 @@ result_t bmfont_measure(bmfont_t *bmfont, /* split_point, actual_width may be NULL */ current_width = 0; + any_drawn = 0; for (len = textlen; len; len--) { int c; @@ -736,12 +743,13 @@ result_t bmfont_measure(bmfont_t *bmfont, break; current_width = next_width; + any_drawn = 1; } if (split_point) *split_point = textlen - len; if (actual_width) - *actual_width = current_width; + *actual_width = current_width - (any_drawn ? LETTER_SPACING : 0); return result_OK; } diff --git a/libraries/framebuf/bmfont/test/bmfont-test.c b/libraries/framebuf/bmfont/test/bmfont-test.c index 9972810b..65f784ae 100644 --- a/libraries/framebuf/bmfont/test/bmfont-test.c +++ b/libraries/framebuf/bmfont/test/bmfont-test.c @@ -975,6 +975,7 @@ static result_t bmfont_monospace_test(const char *resources) const char *filename; bmfont_t *bmfont = NULL; bmfont_width_t onechar; + bmfont_width_t step; bmfont_width_t prev; int i; @@ -991,24 +992,32 @@ static result_t bmfont_monospace_test(const char *resources) bmfont_set_flags(bmfont, bmfont_FLAG_MONOSPACE); onechar = 0; + step = 0; prev = 0; for (i = 1; i <= (int) strlen(sample); i++) { bmfont_width_t width = 0; + bmfont_width_t expected; rc = bmfont_measure(bmfont, sample, i, INT_MAX, NULL, &width); if (rc) goto Failure; if (i == 1) + { onechar = width; + step = width + 1; /* per-glyph advance, including letter spacing + * trimmed from the measured string's end */ + } + + expected = onechar + (i - 1) * step; - if (width != onechar * i || width - prev != onechar) + if (width != expected || (i > 1 && width - prev != step)) { fprintf(stderr, "error: monospace width mismatch at len %d: got %d, " "expected %d\n", - i, width, onechar * i); + i, width, expected); goto Failure; } From abacdcd10526ff0c1bbe05ea2bccca81e3739c4e Mon Sep 17 00:00:00 2001 From: David Thomas Date: Tue, 15 Sep 2026 13:42:17 +0100 Subject: [PATCH 083/128] fix(wuss): restore furniture on swatches/text/palette task windows wuss_WINDOW_NO_RESIZE_BLIT was passed alone as the window flags, zeroing every furniture bit instead of just opting out of blit-based resize. OR it with wuss_WINDOW_DEFAULT so titlebar/outline/close etc. survive. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/test/tasks/palette.c | 4 ++-- libraries/wuss/test/tasks/swatches.c | 2 +- libraries/wuss/test/tasks/text.c | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/libraries/wuss/test/tasks/palette.c b/libraries/wuss/test/tasks/palette.c index 7e620198..00746606 100644 --- a/libraries/wuss/test/tasks/palette.c +++ b/libraries/wuss/test/tasks/palette.c @@ -133,7 +133,7 @@ result_t palette_create(wuss_t *wuss, rc = wuss_window_create_placed(task->delegate, SIZE2D(100, 100), "Palette", - wuss_WINDOW_NO_RESIZE_BLIT, /* swatch grid is laid out across the whole window, so a resize must redraw all of it, not just the newly (un)covered edge */ + wuss_WINDOW_DEFAULT | wuss_WINDOW_NO_RESIZE_BLIT, /* swatch grid is laid out across the whole window, so a resize must redraw all of it, not just the newly (un)covered edge */ wuss_BACKDROP_COLOUR(palette_PICO8_BLACK), SIZE2D(100, 100), SIZE2D(0, 0), @@ -147,7 +147,7 @@ result_t palette_create(wuss_t *wuss, rc = wuss_window_create_placed(task->delegate, SIZE2D(100, 100), "Screen", - wuss_WINDOW_NO_RESIZE_BLIT, + wuss_WINDOW_DEFAULT | wuss_WINDOW_NO_RESIZE_BLIT, wuss_BACKDROP_COLOUR(palette_PICO8_BLACK), SIZE2D(100, 100), SIZE2D(0, 0), diff --git a/libraries/wuss/test/tasks/swatches.c b/libraries/wuss/test/tasks/swatches.c index a35f5e08..f3138e3f 100644 --- a/libraries/wuss/test/tasks/swatches.c +++ b/libraries/wuss/test/tasks/swatches.c @@ -103,7 +103,7 @@ result_t swatches_create(wuss_t *wuss, swatches_task_t *task) rc = wuss_window_create_placed(delegate, SIZE2D(SWATCHES_DOC_W, 140), "Swatches", - wuss_WINDOW_NO_RESIZE_BLIT, /* grid spans the whole window; a resize redraws all of it */ + wuss_WINDOW_DEFAULT | wuss_WINDOW_NO_RESIZE_BLIT, /* grid spans the whole window; a resize redraws all of it */ wuss_BACKDROP_COLOUR(wuss_COLOUR_WINDOW), SIZE2D(SWATCHES_DOC_W, SWATCHES_DOC_H), SIZE2D(0, 0), diff --git a/libraries/wuss/test/tasks/text.c b/libraries/wuss/test/tasks/text.c index f2c70d9d..ca5548f6 100644 --- a/libraries/wuss/test/tasks/text.c +++ b/libraries/wuss/test/tasks/text.c @@ -219,7 +219,7 @@ result_t text_create(wuss_t *wuss, rc = wuss_window_create_placed(delegate, sz, "Sample Text", - wuss_WINDOW_NO_RESIZE_BLIT, /* paragraph reflows across the whole window, so a resize must redraw all of it, not just the newly (un)covered edge */ + wuss_WINDOW_DEFAULT | wuss_WINDOW_NO_RESIZE_BLIT, /* paragraph reflows across the whole window, so a resize must redraw all of it, not just the newly (un)covered edge */ wuss_BACKDROP_COLOUR(wuss_COLOUR_WINDOW), sz, SIZE2D(0, 0), From 00588b77882f1adc1fd2579308e1794b5d49ef0c Mon Sep 17 00:00:00 2001 From: David Thomas Date: Tue, 15 Sep 2026 14:01:16 +0100 Subject: [PATCH 084/128] fix(bmfont): stop clipping full-width glyphs out of existence bmfont_advance_for now returns cellwidth + LETTER_SPACING, but bmfont_draw used that full advance directly as the drawable pixel width. For glyphs whose stored advance fills the whole cell (M, W, ...) this pushed clippedcharwidth past charwidth, driving right_skip negative and corrupting the row read -- the glyph vanished. Split each glyph's draw into the cell itself (clamped to charwidth) and any leftover advance, drawn from a static zero-filled glyph buffer that the existing per-format drawfn already renders as pure background (opaque) or leaves untouched (transparent). Both segments go through the same left_skip/remaining clipping and screen-pointer advance so strings stay pixel-aligned. Co-Authored-By: Claude Sonnet 5 --- libraries/framebuf/bmfont/bmfont.c | 91 ++++++++++++++++++++++-------- 1 file changed, 68 insertions(+), 23 deletions(-) diff --git a/libraries/framebuf/bmfont/bmfont.c b/libraries/framebuf/bmfont/bmfont.c index 14ebc7e7..6d572df8 100644 --- a/libraries/framebuf/bmfont/bmfont.c +++ b/libraries/framebuf/bmfont/bmfont.c @@ -1928,44 +1928,89 @@ result_t bmfont_draw(bmfont_t *bmfont, continue; } - /* draw */ + /* draw: the glyph cell itself, then any leftover advance (letter + * spacing, or monospace padding) as a second, glyph-less segment of + * the same width the drawfn already knows how to paint as pure + * background (opaque) or leave untouched (transparent) -- a row of + * zero bits reads as "no ink" either way. */ { - glyph = (unsigned char *) bmfont->glyphs + gid * glyphbytes; + static const unsigned char zero_glyph[128] = { 0 }; - int clippedcharwidth = MIN(remaining, advance - left_skip); /* in pixels */ + int cellwidth; + int spacing; + int clippedcellwidth; - if (clippedcharwidth <= 0) + glyph = (unsigned char *) bmfont->glyphs + gid * glyphbytes; + cellwidth = MIN(advance, charwidth); + spacing = advance - cellwidth; + + clippedcellwidth = MIN(remaining, cellwidth - left_skip); /* in pixels */ + + if (clippedcellwidth > 0) + { + int right_skip = charwidth - left_skip - clippedcellwidth; + + drawfn(screen, glyph, + top_skip, right_skip, + shift, rowbytes, + clippedcellwidth, clippedcharheight, + nativefg, nativebg); + + if ((remaining -= clippedcellwidth) <= 0) + break; /* stop drawing */ + + { + int pixelsused_bits = clippedcellwidth << log2bpp; + int nbits = shift + pixelsused_bits; + screen = (unsigned char *) screen + (nbits >> 3); + shift = nbits & 7; + } + + left_skip = 0; + } + else { /* still entirely left-clipped: the visible clip is narrower than * this character's remaining left-clip amount. Skip it exactly * like the whole-character skip above, without touching * "remaining" or the screen pointer/shift, since nothing of it * was drawn. */ - left_skip -= advance; - continue; + left_skip -= cellwidth; } + if (spacing > 0 && left_skip < spacing) { - int right_skip = charwidth - left_skip - clippedcharwidth; - - drawfn(screen, glyph, - top_skip, right_skip, - shift, rowbytes, - clippedcharwidth, clippedcharheight, - nativefg, nativebg); - } + int clippedspacing = MIN(remaining, spacing - left_skip); - if ((remaining -= clippedcharwidth) <= 0) - break; /* stop drawing */ + if (clippedspacing > 0) + { + assert((size_t) clippedcharheight * bmfont->glyphrowbytes <= + sizeof(zero_glyph)); + + drawfn(screen, zero_glyph, + top_skip, 0, + shift, rowbytes, + clippedspacing, clippedcharheight, + nativefg, nativebg); + + if ((remaining -= clippedspacing) <= 0) + break; /* stop drawing */ + + { + int pixelsused_bits = clippedspacing << log2bpp; + int nbits = shift + pixelsused_bits; + screen = (unsigned char *) screen + (nbits >> 3); + shift = nbits & 7; + } + } - /* advance the screen pointer */ - int pixelsused_bits = clippedcharwidth << log2bpp; - int nbits = shift + pixelsused_bits; - screen = (unsigned char *) screen + (nbits >> 3); - shift = nbits & 7; + left_skip = 0; + } + else if (spacing > 0) + { + left_skip -= spacing; + } } - - left_skip = 0; } if (end_pos) From c95bb83c4ab2d224e904ec57406ef0b4f2e84274 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Tue, 15 Sep 2026 16:16:21 +0100 Subject: [PATCH 085/128] feat(wuss): add iterations slider to saturn size dialogue Split loop 1 renamed ring->stars, loop 2 renamed band->ring, matching what each pass actually draws. Size dialogue now has a second label+slider+value row for stars_iters, alongside the existing size row. Renamed the pre-show and apply helpers (saturn_size_* -> saturn_sizedlg_*) since the dialogue now does more than the size field. TODO: notes on fast-scroll invalidation bug and slider-row boilerplate. --- libraries/wuss/TODO.txt | 5 + libraries/wuss/test/tasks/saturn.c | 150 ++++++++++++++++++----------- libraries/wuss/test/tasks/saturn.h | 6 +- 3 files changed, 103 insertions(+), 58 deletions(-) diff --git a/libraries/wuss/TODO.txt b/libraries/wuss/TODO.txt index b91d4573..1b4b1e61 100644 --- a/libraries/wuss/TODO.txt +++ b/libraries/wuss/TODO.txt @@ -5,9 +5,14 @@ Items marked (blocks: X) are prerequisites for X. Add a panic redraw indicator? + Furniture clicks / scrolls still invalidate the entire furniture set - fix. (window move fixed: layout cache is now translated in place, not rebuilt) +Fast scrolling can invalidate areas and fail to copy the expected known good screen content. + -- Tried and tried to get Claude to fix but no good. + +Too much slider boilerplate -- need to make label+slider+value a standard component. LAYER 0 - BUGS (fix before building on top) diff --git a/libraries/wuss/test/tasks/saturn.c b/libraries/wuss/test/tasks/saturn.c index cd1ae4e3..ab7bb462 100644 --- a/libraries/wuss/test/tasks/saturn.c +++ b/libraries/wuss/test/tasks/saturn.c @@ -33,11 +33,11 @@ * bottom-up and wuss's run top-down, so that same expression - kept verbatim * here, only without the *4 - lands the sketch the same way up as it drew on * a BBC/RISC OS screen. The shear in loop 2 is asymmetric in y, so this is - * the term that actually decides which way the ring's shadow band tilts. + * the term that actually decides which way the ring's shadow tilts. * - * loop 1: X%,Y% in -127..127; keep if (X*X+Y*Y)/256 > 17 -> the ring + * loop 1: X%,Y% in -127..127; keep if (X*X+Y*Y)/256 > 17 -> the stars * loop 2: sheared X% = R6+R5/4, Y% = R6; keep if 32<=E<80 and - * (R5<0 or (X*X+Y*Y)/256 > 16) -> ring shadow band + * (R5<0 or (X*X+Y*Y)/256 > 16) -> the ring * loop 3: R1,R2 random; keep if R1*R1+R2*R2 < 16384; * x = sqrt(16384-P)/2 -> planet body */ @@ -82,9 +82,20 @@ enum { SATURN_MENU_COLOURS = 0, SATURN_MENU_SIZE }; enum { SATURN_COLOURS_MENU_FOREGROUND = 0, SATURN_COLOURS_MENU_BACKGROUND }; /* the size dialogue's icons, in creation order (see saturn_size_dialogue_create) */ -enum { SATURN_SIZE_ICON_LABEL = 0, SATURN_SIZE_ICON_SLIDER, - SATURN_SIZE_ICON_VALUE, SATURN_SIZE_ICON_CANCEL, - SATURN_SIZE_ICON_APPLY, SATURN_SIZE_NICONS }; +enum { + SATURN_SIZE_ICON_LABEL1 = 0, + SATURN_SIZE_ICON_SLIDER1, + SATURN_SIZE_ICON_VALUE1, + + SATURN_SIZE_ICON_LABEL2, + SATURN_SIZE_ICON_SLIDER2, + SATURN_SIZE_ICON_VALUE2, + + SATURN_SIZE_ICON_CANCEL, + SATURN_SIZE_ICON_APPLY, + + SATURN_SIZE_NICONS +}; static wuss_menu_item_t g_saturn_colours_items[] = { @@ -234,8 +245,8 @@ static result_t saturn_redraw(const wuss_event_t *event, saturn_task_t *task) saturn_rnd_seed(task->seed); - /* loop 1 - the ring: keep points outside the inner disc */ - for (i = 0; i <= task->config.ring_iters; i++) + /* loop 1 - the stars: keep points outside the inner disc */ + for (i = 0; i <= task->config.stars_iters; i++) { x = SATURN_SAMPLE(half, range); y = SATURN_SAMPLE(half, range); @@ -244,8 +255,8 @@ static result_t saturn_redraw(const wuss_event_t *event, saturn_task_t *task) saturn_plot(scr, ox, oy, size, x + half, flip - (y + half), task->fg); } - /* loop 2 - ring shadow band: sheared sample with a banded energy gate */ - for (i = 0; i <= task->config.band_iters; i++) + /* loop 2 - the ring: sheared sample with a banded energy gate */ + for (i = 0; i <= task->config.ring_iters; i++) { int r5, r6, r7, e; @@ -320,7 +331,7 @@ static int saturn_size_snap(int v) enum { ST_ROOT, - + ST_ROW1, ST_LABL1, ST_SLDR1, @@ -335,14 +346,15 @@ enum ST_SPCR, ST_CNCL, ST_APLY, - + SIZE_STACK__LIMIT }; /* Leaf main-axis sizes, named so saturn_size_dialogue_create's hand-computed * minimum window size can share them with the table below instead of * repeating the numbers as bare literals. */ -#define ST_LABEL_W 24 +#define ST_LABEL_W (5*6) /* enough for "Iters" */ +#define ST_LABEL2_W (4*6) /* enough for "100%" ? */ #define ST_SLIDER_MIN_W 64 #define ST_CANCEL_W 48 #define ST_APPLY_W 56 @@ -354,12 +366,12 @@ static const stack_item_t g_saturn_size_stack[SIZE_STACK__LIMIT] = [ST_ROW1] = STACK_HBOX(ST_ROOT, wuss_STD_SLIDER_HEIGHT, wuss_STD_GAP, stack_ALIGN_START), [ST_LABL1] = STACK_LEAF(ST_ROW1, ST_LABEL_W, 16, stack_ALIGN_CENTRE), [ST_SLDR1] = STACK_LEAF_EX(ST_ROW1, 0, wuss_STD_SLIDER_HEIGHT, stack_ALIGN_CENTRE, 1, ST_SLIDER_MIN_W, 0), - [ST_VAL1] = STACK_LEAF(ST_ROW1, ST_LABEL_W, 16, stack_ALIGN_CENTRE), + [ST_VAL1] = STACK_LEAF(ST_ROW1, ST_LABEL2_W, 16, stack_ALIGN_CENTRE), [ST_ROW2] = STACK_HBOX(ST_ROOT, wuss_STD_SLIDER_HEIGHT, wuss_STD_GAP, stack_ALIGN_START), [ST_LABL2] = STACK_LEAF(ST_ROW2, ST_LABEL_W, 16, stack_ALIGN_CENTRE), [ST_SLDR2] = STACK_LEAF_EX(ST_ROW2, 0, wuss_STD_SLIDER_HEIGHT, stack_ALIGN_CENTRE, 1, ST_SLIDER_MIN_W, 0), - [ST_VAL2] = STACK_LEAF(ST_ROW2, ST_LABEL_W, 16, stack_ALIGN_CENTRE), + [ST_VAL2] = STACK_LEAF(ST_ROW2, ST_LABEL2_W, 16, stack_ALIGN_CENTRE), [ST_BTNS] = STACK_HBOX(ST_ROOT, wuss_STD_PRIMARY_BUTTON_HEIGHT, wuss_STD_GAP, stack_ALIGN_END), [ST_SPCR] = STACK_SPACER(ST_BTNS, 1), @@ -391,7 +403,7 @@ static result_t saturn_size_dialogue_create(saturn_task_t *task) * always consume whatever slack the root provides. So the minimum * window size is hand-computed here from the same constants the table * uses, rather than measured by solving. */ - row_w = ST_LABEL_W + wuss_STD_GAP + ST_SLIDER_MIN_W + wuss_STD_GAP + ST_LABEL_W; + row_w = ST_LABEL_W + wuss_STD_GAP + ST_SLIDER_MIN_W + wuss_STD_GAP + ST_LABEL2_W; btns_w = ST_CANCEL_W + wuss_STD_GAP + ST_APPLY_W; sz.w = MAX(row_w, btns_w) + 2 * wuss_STD_INSET; sz.h = (wuss_STD_SLIDER_HEIGHT + wuss_STD_GAP) * 2 + wuss_STD_PRIMARY_BUTTON_HEIGHT + 2 * wuss_STD_INSET; @@ -418,32 +430,32 @@ static result_t saturn_size_dialogue_create(saturn_task_t *task) value = saturn_size_snap(task->config.size); snprintf(buf, sizeof(buf), "%d", value); - wuss_icon_spec_label(&specs[SATURN_SIZE_ICON_LABEL], boxes[ST_LABL1], - "Size", wuss_COLOUR_BLACK, 1); - wuss_icon_spec_slider(&specs[SATURN_SIZE_ICON_SLIDER], boxes[ST_SLDR1], - wuss_COLOUR_BLACK, wuss_SLIDER_HORIZONTAL, - SATURN_SIZE_MIN, SATURN_SIZE_MAX, value); - wuss_icon_spec_label(&specs[SATURN_SIZE_ICON_VALUE], boxes[ST_VAL1], - buf, wuss_COLOUR_BLACK, 0); /* buf copied by - wuss_icon_create_array */ - wuss_icon_spec_action(&specs[SATURN_SIZE_ICON_CANCEL], boxes[ST_CNCL], - "Cancel", wuss_COLOUR_BLACK, wuss_COLOUR_WINDOW, 0); - wuss_icon_spec_action(&specs[SATURN_SIZE_ICON_APPLY], boxes[ST_APLY], - "Apply", wuss_COLOUR_BLACK, wuss_COLOUR_WINDOW, 1); + wuss_icon_spec_label(&specs[SATURN_SIZE_ICON_LABEL1], boxes[ST_LABL1], "Size", wuss_COLOUR_BLACK, 1); + wuss_icon_spec_slider(&specs[SATURN_SIZE_ICON_SLIDER1], boxes[ST_SLDR1], wuss_COLOUR_BLACK, wuss_SLIDER_HORIZONTAL, SATURN_SIZE_MIN, SATURN_SIZE_MAX, value); + wuss_icon_spec_label(&specs[SATURN_SIZE_ICON_VALUE1], boxes[ST_VAL1], buf, wuss_COLOUR_BLACK, 0); /* buf copied by wuss_icon_create_array */ + + wuss_icon_spec_label(&specs[SATURN_SIZE_ICON_LABEL2], boxes[ST_LABL2], "Iters", wuss_COLOUR_BLACK, 1); + wuss_icon_spec_slider(&specs[SATURN_SIZE_ICON_SLIDER2], boxes[ST_SLDR2], wuss_COLOUR_BLACK, wuss_SLIDER_HORIZONTAL, 10, 1000, value); + wuss_icon_spec_label(&specs[SATURN_SIZE_ICON_VALUE2], boxes[ST_VAL2], buf, wuss_COLOUR_BLACK, 0); /* buf copied by wuss_icon_create_array */ + + wuss_icon_spec_action(&specs[SATURN_SIZE_ICON_CANCEL], boxes[ST_CNCL], "Cancel", wuss_COLOUR_BLACK, wuss_COLOUR_WINDOW, 0); + wuss_icon_spec_action(&specs[SATURN_SIZE_ICON_APPLY], boxes[ST_APLY], "Apply", wuss_COLOUR_BLACK, wuss_COLOUR_WINDOW, 1); rc = wuss_icon_create_array(task->size_dialogue, specs, SATURN_SIZE_NICONS, made); if (rc != result_OK) goto exit; - task->size_slider = made[SATURN_SIZE_ICON_SLIDER]; - task->size_value_label = made[SATURN_SIZE_ICON_VALUE]; - task->size_cancel = made[SATURN_SIZE_ICON_CANCEL]; - task->size_apply = made[SATURN_SIZE_ICON_APPLY]; + task->size_slider = made[SATURN_SIZE_ICON_SLIDER1]; + task->size_value_label = made[SATURN_SIZE_ICON_VALUE1]; + task->size_slider2 = made[SATURN_SIZE_ICON_SLIDER2]; + task->size_value2_label = made[SATURN_SIZE_ICON_VALUE2]; + task->size_cancel = made[SATURN_SIZE_ICON_CANCEL]; + task->size_apply = made[SATURN_SIZE_ICON_APPLY]; return result_OK; - - + + exit: wuss_window_close(task->size_dialogue); /* not yet a menu leaf: safe to close */ task->size_dialogue = NULL; @@ -453,33 +465,43 @@ static result_t saturn_size_dialogue_create(saturn_task_t *task) /* wuss_EVENT_PRE_SHOW on the size dialogue: resync the slider and its echo * label to task->config.size, in case Apply (or a config passed to * saturn_create) changed it since the dialogue was last shown. Always - * allows the show. Also used directly by saturn_size_icon to reset the - * dialogue on an Adjust-Cancel click. */ -static result_t saturn_size_pre_show(saturn_task_t *task) + * allows the show. Also used directly by saturn_size_dialogue_icon to reset + * the dialogue on an Adjust-Cancel click. */ +static result_t saturn_sizedlg_pre_show(saturn_task_t *task) { - int value; - char buf[16]; + result_t rc; + int value; + char buf[16]; value = saturn_size_snap(task->config.size); wuss_icon_set_value(task->size_dialogue, task->size_slider, value); snprintf(buf, sizeof(buf), "%d", value); + rc = wuss_icon_set_text(task->size_dialogue, task->size_value_label, buf); - return wuss_icon_set_text(task->size_dialogue, task->size_value_label, buf); + value = task->config.stars_iters; + wuss_icon_set_value(task->size_dialogue, task->size_slider2, value); + snprintf(buf, sizeof(buf), "%d", value); + rc = wuss_icon_set_text(task->size_dialogue, task->size_value2_label, buf); + + return rc; } /* Applies the slider's current value to task->window (resize + doc extent), * shared by a Select and an Adjust click on Apply. */ -static result_t saturn_size_apply(saturn_task_t *task) +static result_t saturn_sizedlg_apply(saturn_task_t *task) { result_t rc; - int value; - - value = wuss_icon_get_value(task->size_slider); - task->config.size = value; - rc = wuss_window_resize(task->window, SIZE2D(value, value)); + int size_value; + int iters_value; + + size_value = wuss_icon_get_value(task->size_slider); + task->config.size = size_value; + iters_value = wuss_icon_get_value(task->size_slider2); + task->config.stars_iters = iters_value; + rc = wuss_window_resize(task->window, SIZE2D(size_value, size_value)); if (rc != result_OK) return rc; - return wuss_window_set_doc(task->window, SIZE2D(value, value)); + return wuss_window_set_doc(task->window, SIZE2D(size_value, size_value)); } /* wuss_EVENT_ICON on the size dialogue: slider drag updates the echo label @@ -492,8 +514,8 @@ static result_t saturn_size_apply(saturn_task_t *task) * the dialogue open. Dismissing goes through wuss_menu_close rather than * touching the (borrowed, reused) window directly -- that is what hides it, * same as a click outside the chain would. */ -static result_t saturn_size_icon(saturn_task_t *task, - const wuss_event_t *event) +static result_t saturn_size_dialogue_icon(saturn_task_t *task, + const wuss_event_t *event) { wuss_icon_t *icon; int value; @@ -501,6 +523,8 @@ static result_t saturn_size_icon(saturn_task_t *task, icon = event->data.icon.icon; + // FIXME DPT: mouse move events arrive even when not dragging the bar... + if (icon == task->size_slider) { if (event->data.icon.action != wuss_MOUSE_DOWN && @@ -508,12 +532,25 @@ static result_t saturn_size_icon(saturn_task_t *task, return result_OK; value = saturn_size_snap(event->data.icon.value); - wuss_icon_set_value(task->size_dialogue, task->size_slider, value); + wuss_icon_set_value(task->size_dialogue, task->size_slider, value); /* set again once snapped */ snprintf(buf, sizeof(buf), "%d", value); return wuss_icon_set_text(task->size_dialogue, task->size_value_label, buf); } + if (icon == task->size_slider2) + { + if (event->data.icon.action != wuss_MOUSE_DOWN && + event->data.icon.action != wuss_MOUSE_MOVE) + return result_OK; + + value = event->data.icon.value; + // not snapping -- wuss_icon_set_value(task->size_dialogue, task->size_slider2, value); + snprintf(buf, sizeof(buf), "%d", value); + return wuss_icon_set_text(task->size_dialogue, task->size_value2_label, + buf); + } + if (event->data.icon.action != wuss_MOUSE_UP) return result_OK; @@ -526,7 +563,7 @@ static result_t saturn_size_icon(saturn_task_t *task, return result_OK; } if (event->data.icon.button & wuss_BUTTON_ADJUST) - return saturn_size_pre_show(task); + return saturn_sizedlg_pre_show(task); return result_OK; } @@ -536,10 +573,10 @@ static result_t saturn_size_icon(saturn_task_t *task, { wuss_menu_close(task->menu_handle); task->menu_handle = NULL; - return saturn_size_apply(task); + return saturn_sizedlg_apply(task); } if (event->data.icon.button & wuss_BUTTON_ADJUST) - return saturn_size_apply(task); + return saturn_sizedlg_apply(task); return result_OK; } @@ -568,7 +605,8 @@ static int saturn_menu_select_apply(const wuss_colourmenu_t *colourmenu, /* A pick from either colour submenu, resolved against whichever colourmenu * it came from. "Size" is a wuss_menu_item_t::window leaf, not a leaf pick, - * so it never reaches here -- see saturn_size_icon and saturn_size_pre_show. */ + * so it never reaches here -- see saturn_size_dialogue_icon and + * saturn_sizedlg_pre_show. */ static result_t saturn_menu_select(saturn_task_t *task, const wuss_event_t *event) { @@ -605,12 +643,12 @@ result_t saturn_handle(wuss_window_t *window, case wuss_EVENT_ICON: if (window == task->size_dialogue) - return saturn_size_icon(task, event); + return saturn_size_dialogue_icon(task, event); return result_OK; case wuss_EVENT_PRE_SHOW: if (window == task->size_dialogue) - return saturn_size_pre_show(task); + return saturn_sizedlg_pre_show(task); return result_OK; case wuss_EVENT_MENU_SELECT: diff --git a/libraries/wuss/test/tasks/saturn.h b/libraries/wuss/test/tasks/saturn.h index 0be7c736..d1753ea9 100644 --- a/libraries/wuss/test/tasks/saturn.h +++ b/libraries/wuss/test/tasks/saturn.h @@ -27,8 +27,8 @@ * fills in SATURN_CONFIG_DEFAULT values when the caller passes NULL */ typedef struct saturn_config { - int ring_iters; /* loop 1: the ring */ - int band_iters; /* loop 2: ring shadow band */ + int stars_iters; /* loop 1: the ring */ + int ring_iters; /* loop 2: ring shadow band */ int body_iters; /* loop 3: planet body */ int size; /* window size (both axes); set via the Size... dialogue */ } @@ -54,6 +54,8 @@ typedef struct saturn_task wuss_window_t *size_dialogue; /* built once, hidden; a menu leaf */ wuss_icon_t *size_slider; wuss_icon_t *size_value_label; + wuss_icon_t *size_slider2; + wuss_icon_t *size_value2_label; wuss_icon_t *size_cancel; wuss_icon_t *size_apply; } From caebc44c0542a9519d8504a273457cd6ccbaa6e7 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Tue, 15 Sep 2026 17:03:03 +0100 Subject: [PATCH 086/128] feat(bmfont): add letter and word spacing options bmfont_measure/bmfont_draw/bmfont_draw_relief take an optional bmfont_spacing_t (letter_spacing added to every glyph, word_spacing added on top for space glyphs), threaded through bmtext and wuss call sites as NULL where unused. Also fixes a pre-existing bmfont_draw clip-box underestimate: drawbox.x1's worst-case width didn't account for extra advance from spacing, which is what the old 'tracking' comment was warning about. Adds bmfont_spacing_test covering letter_spacing total delta, word_spacing delta, and bmfont_draw end_pos under spacing. Co-Authored-By: Claude Sonnet 5 --- include/framebuf/bmfont.h | 68 +++++--- libraries/framebuf/bmfont/bmfont.c | 136 ++++++++++------ libraries/framebuf/bmfont/test/bmfont-test.c | 162 ++++++++++++++++++- libraries/text/bmtext/draw.c | 3 +- libraries/text/bmtext/layout.c | 4 +- libraries/wuss/font/text.c | 4 +- libraries/wuss/icon/draw.c | 2 +- libraries/wuss/test/tasks/chars.c | 4 +- libraries/wuss/test/tasks/clock.c | 5 +- libraries/wuss/test/tasks/icons.c | 4 +- libraries/wuss/test/tasks/minesweeper.c | 14 +- libraries/wuss/test/tasks/porter-duff.c | 2 +- 12 files changed, 318 insertions(+), 90 deletions(-) diff --git a/include/framebuf/bmfont.h b/include/framebuf/bmfont.h index d270f898..db613398 100644 --- a/include/framebuf/bmfont.h +++ b/include/framebuf/bmfont.h @@ -25,6 +25,22 @@ enum /** A set of bmfont_FLAG_* values. */ typedef unsigned int bmfont_flags_t; +/** + * Extra spacing to apply on top of a font's own advance widths, passed to + * bmfont_measure(), bmfont_draw() and bmfont_draw_relief(). A NULL pointer + * means no extra spacing (as if both fields were zero). + */ +typedef struct bmfont_spacing +{ + int letter_spacing; /**< Added to every glyph's advance width, in pixels. + * May be negative to tighten; a large negative + * value can make glyphs overlap or run + * backwards. */ + int word_spacing; /**< Added to space (' ') glyphs, on top of + * letter_spacing, in pixels. */ +} +bmfont_spacing_t; + /** * Create a new bitmap font from a PNG format font file. * @@ -115,17 +131,19 @@ int bmfont_get_count(bmfont_t *bmfont); * \param[in] bmfont Bitmap font to measure. * \param[in] text String to measure. * \param[in] len Length of the string. + * \param[in] spacing Extra letter/word spacing, or NULL for none. * \param[in] target_width Target width in pixels. * \param[out] split_point Split point in pixels. * \param[out] actual_width Actual width of the split string in pixels. * \return \ref result_OK on success, or appropriate result code otherwise. */ -result_t bmfont_measure(bmfont_t *bmfont, - const char *text, - int len, - bmfont_width_t target_width, - int *split_point, - bmfont_width_t *actual_width); +result_t bmfont_measure(bmfont_t *bmfont, + const char *text, + int len, + const bmfont_spacing_t *spacing, + bmfont_width_t target_width, + int *split_point, + bmfont_width_t *actual_width); /** * Draw the given string using the specified font, position and colours. @@ -136,18 +154,20 @@ result_t bmfont_measure(bmfont_t *bmfont, * \param[in] len Length of the string. * \param[in] fg Foreground colour. * \param[in] bg Background colour. + * \param[in] spacing Extra letter/word spacing, or NULL for none. * \param[in] pos Baseline start position of the string in pixels. * \param[out] end_pos Baseline end position of the string in pixels. * \return \ref result_OK on success, or appropriate result code otherwise. */ -result_t bmfont_draw(bmfont_t *bmfont, - screen_t *scr, - const char *text, - int len, - colour_t fg, - colour_t bg, - const point_t *pos, - point_t *end_pos); +result_t bmfont_draw(bmfont_t *bmfont, + screen_t *scr, + const char *text, + int len, + colour_t fg, + colour_t bg, + const bmfont_spacing_t *spacing, + const point_t *pos, + point_t *end_pos); /** * Draw the given string twice to produce a relief (drop-shadow) effect: a @@ -164,19 +184,21 @@ result_t bmfont_draw(bmfont_t *bmfont, * \param[in] len Length of the string. * \param[in] fg Foreground colour, used for the main pass. * \param[in] shadow Shadow colour, used for the offset pass. + * \param[in] spacing Extra letter/word spacing, or NULL for none. * \param[in] pos Baseline position of the main pass in pixels. * \param[in] offset Shadow displacement in pixels, e.g. { 1, 1 }. * \param[out] end_pos Baseline end position of the string in pixels. * \return \ref result_OK on success, or appropriate result code otherwise. */ -result_t bmfont_draw_relief(bmfont_t *bmfont, - screen_t *scr, - const char *text, - int len, - colour_t fg, - colour_t shadow, - const point_t *pos, - const point_t *offset, - point_t *end_pos); +result_t bmfont_draw_relief(bmfont_t *bmfont, + screen_t *scr, + const char *text, + int len, + colour_t fg, + colour_t shadow, + const bmfont_spacing_t *spacing, + const point_t *pos, + const point_t *offset, + point_t *end_pos); #endif /* DPTLIB_BMFONT_H */ diff --git a/libraries/framebuf/bmfont/bmfont.c b/libraries/framebuf/bmfont/bmfont.c index 6d572df8..abb75d64 100644 --- a/libraries/framebuf/bmfont/bmfont.c +++ b/libraries/framebuf/bmfont/bmfont.c @@ -67,14 +67,28 @@ struct bmfont /* -------------------------------------------------------------------------- */ -/** The advance width to use for glyph \p gid, honouring the monospace flag. - * Includes the LETTER_SPACING pixel not stored in the font. */ -static bmfont_width_t bmfont_advance_for(const bmfont_t *bmfont, int gid) +/** The advance width to use for glyph \p gid, honouring the monospace flag + * and any caller-supplied extra letter/word spacing. Includes the + * LETTER_SPACING pixel not stored in the font. */ +static bmfont_width_t bmfont_advance_for(const bmfont_t *bmfont, + int gid, + int c, + const bmfont_spacing_t *spacing) { - if (bmfont->flags & bmfont_FLAG_MONOSPACE) - return bmfont->maxadw + LETTER_SPACING; + bmfont_width_t adw; - return bmfont->adw[gid] + LETTER_SPACING; + adw = (bmfont->flags & bmfont_FLAG_MONOSPACE) ? bmfont->maxadw : + bmfont->adw[gid]; + adw += LETTER_SPACING; + + if (spacing) + { + adw += spacing->letter_spacing; + if (c == ' ') + adw += spacing->word_spacing; + } + + return adw; } /* -------------------------------------------------------------------------- */ @@ -706,16 +720,18 @@ int bmfont_get_count(bmfont_t *bmfont) return bmfont->totalchars; } -result_t bmfont_measure(bmfont_t *bmfont, - const char *text, - int textlen, - bmfont_width_t target_width, - int *split_point, - bmfont_width_t *actual_width) +result_t bmfont_measure(bmfont_t *bmfont, + const char *text, + int textlen, + const bmfont_spacing_t *spacing, + bmfont_width_t target_width, + int *split_point, + bmfont_width_t *actual_width) { bmfont_width_t current_width; int len; int any_drawn; + int last_c; assert(bmfont); assert(text); @@ -725,6 +741,7 @@ result_t bmfont_measure(bmfont_t *bmfont, current_width = 0; any_drawn = 0; + last_c = 0; for (len = textlen; len; len--) { int c; @@ -736,7 +753,8 @@ result_t bmfont_measure(bmfont_t *bmfont, continue; gid = c - ' '; - advance = (gid < bmfont->totalchars) ? bmfont_advance_for(bmfont, gid) : 0; + advance = (gid < bmfont->totalchars) ? + bmfont_advance_for(bmfont, gid, c, spacing) : 0; next_width = current_width + advance; if (next_width > target_width) @@ -744,12 +762,28 @@ result_t bmfont_measure(bmfont_t *bmfont, current_width = next_width; any_drawn = 1; + last_c = c; } if (split_point) *split_point = textlen - len; if (actual_width) - *actual_width = current_width - (any_drawn ? LETTER_SPACING : 0); + { + bmfont_width_t trailing_trim = 0; + + if (any_drawn) + { + trailing_trim = LETTER_SPACING; + if (spacing) + { + trailing_trim += spacing->letter_spacing; + if (last_c == ' ') + trailing_trim += spacing->word_spacing; + } + } + + *actual_width = current_width - trailing_trim; + } return result_OK; } @@ -1776,14 +1810,15 @@ static void bmfont_drawchar_p8_2w_t(void *vscreen, } } -result_t bmfont_draw(bmfont_t *bmfont, - screen_t *scr, - const char *text, - int len, - colour_t fg, - colour_t bg, - const point_t *pos, - point_t *end_pos) +result_t bmfont_draw(bmfont_t *bmfont, + screen_t *scr, + const char *text, + int len, + colour_t fg, + colour_t bg, + const bmfont_spacing_t *spacing, + const point_t *pos, + point_t *end_pos) { bmfont_drawchar_t *drawfn; box_t scrclip; @@ -1852,10 +1887,17 @@ result_t bmfont_draw(bmfont_t *bmfont, if (screen_get_clip(scr, &scrclip)) return result_OK; /* invalid clipped screen */ - drawbox.x0 = top.x; - drawbox.y0 = top.y; - drawbox.x1 = top.x + bmfont->charwidth * len; /* worst-case estimate */ - drawbox.y1 = top.y + bmfont->charheight; + { + int extra_per_char; + + extra_per_char = spacing ? + MAX(spacing->letter_spacing + spacing->word_spacing, 0) : 0; + + drawbox.x0 = top.x; + drawbox.y0 = top.y; + drawbox.x1 = top.x + (bmfont->charwidth + extra_per_char) * len; /* worst-case estimate */ + drawbox.y1 = top.y + bmfont->charheight; + } if (!box_intersects(&scrclip, &drawbox)) return result_OK; /* not visible */ @@ -1885,8 +1927,6 @@ result_t bmfont_draw(bmfont_t *bmfont, pixelfmt_any_t nativebg = colour_to_pixel(scr->palette, scr->palette ? 1 << (1 << log2bpp) : 0, bg, scr->format); int remaining = scrclip.x1 - clamped_pos_x; /* in pixels */ - const int tracking = 0; /* note: +ve can break stuff! */ - int x = pos->x; /* box_intersects said something is visible, but a glyph can still be fully @@ -1917,7 +1957,7 @@ result_t bmfont_draw(bmfont_t *bmfont, if (gid >= bmfont->totalchars) continue; - advance = bmfont_advance_for(bmfont, gid) + tracking; + advance = bmfont_advance_for(bmfont, gid, c, spacing); x += advance; @@ -1937,12 +1977,12 @@ result_t bmfont_draw(bmfont_t *bmfont, static const unsigned char zero_glyph[128] = { 0 }; int cellwidth; - int spacing; + int cellpad; int clippedcellwidth; glyph = (unsigned char *) bmfont->glyphs + gid * glyphbytes; cellwidth = MIN(advance, charwidth); - spacing = advance - cellwidth; + cellpad = advance - cellwidth; clippedcellwidth = MIN(remaining, cellwidth - left_skip); /* in pixels */ @@ -1978,9 +2018,9 @@ result_t bmfont_draw(bmfont_t *bmfont, left_skip -= cellwidth; } - if (spacing > 0 && left_skip < spacing) + if (cellpad > 0 && left_skip < cellpad) { - int clippedspacing = MIN(remaining, spacing - left_skip); + int clippedspacing = MIN(remaining, cellpad - left_skip); if (clippedspacing > 0) { @@ -2006,9 +2046,9 @@ result_t bmfont_draw(bmfont_t *bmfont, left_skip = 0; } - else if (spacing > 0) + else if (cellpad > 0) { - left_skip -= spacing; + left_skip -= cellpad; } } } @@ -2031,7 +2071,7 @@ result_t bmfont_draw(bmfont_t *bmfont, if (gid >= bmfont->totalchars) continue; - advance = bmfont_advance_for(bmfont, gid) + tracking; + advance = bmfont_advance_for(bmfont, gid, c, spacing); x += advance; } @@ -2045,15 +2085,16 @@ result_t bmfont_draw(bmfont_t *bmfont, /* -------------------------------------------------------------------------- */ -result_t bmfont_draw_relief(bmfont_t *bmfont, - screen_t *scr, - const char *text, - int len, - colour_t fg, - colour_t shadow, - const point_t *pos, - const point_t *offset, - point_t *end_pos) +result_t bmfont_draw_relief(bmfont_t *bmfont, + screen_t *scr, + const char *text, + int len, + colour_t fg, + colour_t shadow, + const bmfont_spacing_t *spacing, + const point_t *pos, + const point_t *offset, + point_t *end_pos) { result_t rc; colour_t transparent; @@ -2062,12 +2103,13 @@ result_t bmfont_draw_relief(bmfont_t *bmfont, transparent = colour_rgba(0, 0, 0, 0); shadowpos = POINT(pos->x + offset->x, pos->y + offset->y); - rc = bmfont_draw(bmfont, scr, text, len, shadow, transparent, + rc = bmfont_draw(bmfont, scr, text, len, shadow, transparent, spacing, &shadowpos, NULL); if (rc) return rc; - return bmfont_draw(bmfont, scr, text, len, fg, transparent, pos, end_pos); + return bmfont_draw(bmfont, scr, text, len, fg, transparent, spacing, pos, + end_pos); } /* -------------------------------------------------------------------------- */ diff --git a/libraries/framebuf/bmfont/test/bmfont-test.c b/libraries/framebuf/bmfont/test/bmfont-test.c index 65f784ae..f7ce7d5b 100644 --- a/libraries/framebuf/bmfont/test/bmfont-test.c +++ b/libraries/framebuf/bmfont/test/bmfont-test.c @@ -313,6 +313,7 @@ static result_t bmfont_clipping_test(bmfontteststate_t *state) rc = bmfont_measure(bmfonts[font].bmfont, lorem_ipsum, nchars, + NULL, INT_MAX, NULL, &stringwidth); @@ -334,6 +335,7 @@ static result_t bmfont_clipping_test(bmfontteststate_t *state) nchars, state->palette[palette_PICO8_WHITE], state->palette[palette_PICO8_BLACK], + NULL, &pos, &shadow_offset, NULL /*endpos*/); @@ -348,6 +350,7 @@ static result_t bmfont_clipping_test(bmfontteststate_t *state) nchars, state->palette[palette_PICO8_WHITE], bg, + NULL, &pos, NULL /*endpos*/); if (rc) @@ -406,6 +409,7 @@ static result_t bmfont_layout_test(bmfontteststate_t *state) (void) bmfont_measure(bmfont, string, (int) stringlen, + NULL, state->scr_width - origin.x, &absolute_break, /* if no break returns strlen */ &width); @@ -441,6 +445,7 @@ static result_t bmfont_layout_test(bmfontteststate_t *state) friendly_break, state->palette[palette_PICO8_WHITE], bg, + NULL, &origin, &endpos); if (rc) @@ -630,6 +635,7 @@ static result_t bmfont_interactive_test(bmfontteststate_t *state) message, (int) msglen, state->palette[palette_PICO8_BLACK], bg, + NULL, &origin, NULL /*endpos*/); @@ -641,6 +647,7 @@ static result_t bmfont_interactive_test(bmfontteststate_t *state) message, (int) msglen, fg, bg, + NULL, &origin, NULL /*endpos*/); @@ -654,6 +661,7 @@ static result_t bmfont_interactive_test(bmfontteststate_t *state) message, (int) msglen, fg, bg, + NULL, &origin, NULL /*endpos*/); @@ -999,7 +1007,7 @@ static result_t bmfont_monospace_test(const char *resources) bmfont_width_t width = 0; bmfont_width_t expected; - rc = bmfont_measure(bmfont, sample, i, INT_MAX, NULL, &width); + rc = bmfont_measure(bmfont, sample, i, NULL, INT_MAX, NULL, &width); if (rc) goto Failure; @@ -1028,6 +1036,154 @@ static result_t bmfont_monospace_test(const char *resources) return result_TEST_PASSED; +Failure: + bmfont_destroy(bmfont); + return result_TEST_FAILED; +} + +/* ----------------------------------------------------------------------- */ + +/* letter_spacing adds a fixed amount between every pair of glyphs (N-1 gaps + * for N glyphs); word_spacing adds on top of that for space glyphs only. + * Also exercises bmfont_draw with a non-NULL spacing -- this is the path + * whose worst-case clip-box estimate used to go stale when tracking was + * nonzero, so a mismatch here would show up as dropped or corrupted glyphs + * under ASan/UBSan rather than just a wrong measurement. */ +static result_t bmfont_spacing_test(const char *resources) +{ + static const char letters[] = "WiWiWiWiWi"; + static const char worded[] = "a a"; + + const bmfont_spacing_t letter_only = { 3, 0 }; + const bmfont_spacing_t word_only = { 0, 5 }; + + result_t rc; + const char *leafname; + const char *filename; + bmfont_t *bmfont = NULL; + bmfont_width_t plain_word_width; + bmfont_width_t spaced_word_width; + point_t pos; + point_t end_pos; + int i; + + leafname = path_join_leafname("MS Sans Serif", "png"); + filename = path_join_filename(resources, 3, "resources", "bmfonts", leafname); + + rc = bmfont_create(filename, &bmfont); + if (rc) + { + fprintf(stderr, "Error: Failed to load font %s\n", filename); + return result_TEST_FAILED; + } + + /* letter_spacing adds a fixed gap between glyphs, not after the last one + * (bmfont_measure trims the trailing gap, same as it already does for the + * font's own built-in letter spacing) -- so an N-character run has N-1 + * spaced gaps. "WiWiWiWiWi" mixes glyphs of different base advance widths, + * so the per-character step isn't itself constant in a proportional font + * (unlike bmfont_monospace_test) -- only the total added width is, so + * that's the invariant checked here. */ + for (i = 1; i <= (int) strlen(letters); i++) + { + bmfont_width_t unspaced = 0; + bmfont_width_t spaced = 0; + + rc = bmfont_measure(bmfont, letters, i, NULL, INT_MAX, NULL, &unspaced); + if (rc) + goto Failure; + rc = bmfont_measure(bmfont, letters, i, &letter_only, INT_MAX, NULL, + &spaced); + if (rc) + goto Failure; + + if (spaced - unspaced != letter_only.letter_spacing * (i - 1)) + { + fprintf(stderr, + "error: letter_spacing total mismatch at len %d: " + "spaced=%d unspaced=%d\n", + i, spaced, unspaced); + goto Failure; + } + } + + /* word_spacing: only the two space glyphs in "a a" should widen, by + * exactly word_spacing each -- the letters either side are untouched. */ + rc = bmfont_measure(bmfont, worded, (int) strlen(worded), NULL, INT_MAX, + NULL, &plain_word_width); + if (rc) + goto Failure; + rc = bmfont_measure(bmfont, worded, (int) strlen(worded), &word_only, + INT_MAX, NULL, &spaced_word_width); + if (rc) + goto Failure; + + if (spaced_word_width - plain_word_width != word_only.word_spacing) + { + fprintf(stderr, + "error: word_spacing mismatch: plain=%d spaced=%d " + "expected delta=%d\n", + plain_word_width, spaced_word_width, word_only.word_spacing); + goto Failure; + } + + /* bmfont_draw with spacing must not mis-clip: end_pos is the pen position + * (unlike bmfont_measure's ink width, it includes the trailing glyph's + * own advance -- the font's built-in LETTER_SPACING pixel, uninvolved + * here since word_only has zero letter_spacing and "a a" ends on 'a' not + * ' '), on a screen wide enough that clipping cannot silently truncate + * the string. */ + { + bitmap_t bm; + screen_t scr; + colour_t black = colour_rgb(0, 0, 0); + colour_t white = colour_rgb(0xFF, 0xFF, 0xFF); + bmfont_width_t expected_end_x; + int scr_width; + int rowbytes; + void *pixels; + + scr_width = spaced_word_width + 32; + rowbytes = (scr_width << pixelfmt_log2bpp(pixelfmt_bgrx8888)) / 8; + pixels = malloc(rowbytes * 32); + if (pixels == NULL) + { + rc = result_OOM; + goto Failure; + } + + bitmap_init(&bm, SIZE2D(scr_width, 32), pixelfmt_bgrx8888, rowbytes, + NULL, pixels); + bitmap_clear(&bm, black); + screen_for_bitmap(&scr, &bm); + + pos = POINT(4, 16); + rc = bmfont_draw(bmfont, &scr, worded, (int) strlen(worded), white, + black, &word_only, &pos, &end_pos); + free(pixels); + if (rc) + goto Failure; + + /* "a a" ends on 'a': measure trims one pixel of the font's built-in + * letter spacing (no custom letter_spacing or trailing word_spacing + * here -- see bmfont_monospace_test's "width + 1" for the same + * constant), so the untrimmed pen position is the measured width plus + * that one pixel back. */ + expected_end_x = pos.x + spaced_word_width + 1; + if (end_pos.x != expected_end_x || end_pos.y != pos.y) + { + fprintf(stderr, + "error: bmfont_draw end_pos mismatch with spacing: " + "got (%d,%d), expected (%d,%d)\n", + end_pos.x, end_pos.y, expected_end_x, pos.y); + goto Failure; + } + } + + bmfont_destroy(bmfont); + return result_TEST_PASSED; + + Failure: bmfont_destroy(bmfont); return result_TEST_FAILED; @@ -1062,6 +1218,10 @@ result_t bmfont_test(const char *resources) if (rc != result_TEST_PASSED) return rc; + rc = bmfont_spacing_test(resources); + if (rc != result_TEST_PASSED) + return rc; + for (i = 0; i < NELEMS(tab); i++) { rc = bmfont_test_one_format(resources, diff --git a/libraries/text/bmtext/draw.c b/libraries/text/bmtext/draw.c index 3da57137..f7ee2888 100644 --- a/libraries/text/bmtext/draw.c +++ b/libraries/text/bmtext/draw.c @@ -27,7 +27,8 @@ void bmtext_draw(bmfont_t *font, pos.y += font_ascent; /* origin is top-left; bmfont_draw wants the baseline */ for (i = 0; i < nlines; i++) { - bmfont_draw(font, scr, lines[i].str, lines[i].len, fg, bg, &pos, NULL); + bmfont_draw(font, scr, lines[i].str, lines[i].len, fg, bg, NULL, &pos, + NULL); pos.y += font_height + leading; } } diff --git a/libraries/text/bmtext/layout.c b/libraries/text/bmtext/layout.c index b6f30b67..226f6852 100644 --- a/libraries/text/bmtext/layout.c +++ b/libraries/text/bmtext/layout.c @@ -1,6 +1,7 @@ /* text/bmtext/layout.c -- break a string into pixel-fitted lines */ #include +#include #include "framebuf/bmfont.h" @@ -23,7 +24,8 @@ int bmtext_layout(bmfont_t *font, bmfont_width_t width; int friendly_break; - bmfont_measure(font, string, stringlen, wrap_width, &absolute_break, &width); + bmfont_measure(font, string, stringlen, NULL, wrap_width, &absolute_break, + &width); friendly_break = absolute_break; if (absolute_break < stringlen) diff --git a/libraries/wuss/font/text.c b/libraries/wuss/font/text.c index b81c29b4..47b04806 100644 --- a/libraries/wuss/font/text.c +++ b/libraries/wuss/font/text.c @@ -15,7 +15,7 @@ result_t wuss__text_measure(bmfont_t *font, int *split_point, bmfont_width_t *actual_width) { - return bmfont_measure(font, text, len, target_width, split_point, + return bmfont_measure(font, text, len, NULL, target_width, split_point, actual_width); } @@ -28,7 +28,7 @@ result_t wuss__text_draw(bmfont_t *font, const point_t *pos, point_t *end_pos) { - return bmfont_draw(font, scr, text, len, fg, bg, pos, end_pos); + return bmfont_draw(font, scr, text, len, fg, bg, NULL, pos, end_pos); } /* ----------------------------------------------------------------------- */ diff --git a/libraries/wuss/icon/draw.c b/libraries/wuss/icon/draw.c index 301efd7d..7872a8d0 100644 --- a/libraries/wuss/icon/draw.c +++ b/libraries/wuss/icon/draw.c @@ -144,7 +144,7 @@ static int wuss__draw_symbol_glyph(const wuss_t *wuss, bmfont_get_info(font, &font_width, &font_height, &font_ascent, NULL); pos.x = centre.x - font_width / 2; pos.y = centre.y - font_height / 2 + font_ascent; - bmfont_draw(font, scr, &glyph, 1, ink, ground, &pos, NULL); + bmfont_draw(font, scr, &glyph, 1, ink, ground, NULL, &pos, NULL); return 1; } diff --git a/libraries/wuss/test/tasks/chars.c b/libraries/wuss/test/tasks/chars.c index e18fa5dd..87375d4f 100644 --- a/libraries/wuss/test/tasks/chars.c +++ b/libraries/wuss/test/tasks/chars.c @@ -294,7 +294,7 @@ static result_t chars_redraw(const wuss_event_t *event, void *task_data) pos.y = y + CHARS_PAD * 2 + sysfont_height + font_ascent; screen_draw_dashed_line(scr, x + CHARS_PAD, pos.y, x + cell_w - 1 - CHARS_PAD, pos.y, 1, 1, cc->mg); - bmfont_draw(cc->font, scr, &ch, 1, cc->fg, cc->bg, &pos, NULL); + bmfont_draw(cc->font, scr, &ch, 1, cc->fg, cc->bg, NULL, &pos, NULL); /* advance width: a blue rule under the glyph spanning pos.x..pos.x+advance, * a ruler for how far this glyph pushes the pen */ @@ -303,7 +303,7 @@ static result_t chars_redraw(const wuss_event_t *event, void *task_data) colour_t blue; int adv_y; - bmfont_measure(cc->font, &ch, 1, INT_MAX, NULL, &advance); + bmfont_measure(cc->font, &ch, 1, NULL, INT_MAX, NULL, &advance); blue = colour_rgb(0x66, 0x66, 0xFF); adv_y = y + cell_h - 1 - CHARS_PAD; screen_draw_line(scr, pos.x, adv_y, pos.x + advance - 1, adv_y, blue); diff --git a/libraries/wuss/test/tasks/clock.c b/libraries/wuss/test/tasks/clock.c index 97741000..08787ed4 100644 --- a/libraries/wuss/test/tasks/clock.c +++ b/libraries/wuss/test/tasks/clock.c @@ -78,12 +78,13 @@ static void clock_draw_centred(bmfont_t *font, int len, fh, ascent; len = (int) strlen(text); - bmfont_measure(font, text, len, INT_MAX, NULL, &width); + bmfont_measure(font, text, len, NULL, INT_MAX, NULL, &width); bmfont_get_info(font, NULL, &fh, &ascent, NULL); pos.x = (int) (x - width / 2.0); pos.y = (int) (y - fh / 2.0) + ascent; - bmfont_draw(font, scr, text, len, fg, colour_rgba(0, 0, 0, 0), &pos, NULL); + bmfont_draw(font, scr, text, len, fg, colour_rgba(0, 0, 0, 0), NULL, &pos, + NULL); } static result_t clock_create_window(wuss_t *wuss, diff --git a/libraries/wuss/test/tasks/icons.c b/libraries/wuss/test/tasks/icons.c index 4409b5f0..8598c64c 100644 --- a/libraries/wuss/test/tasks/icons.c +++ b/libraries/wuss/test/tasks/icons.c @@ -694,7 +694,7 @@ static result_t icons_redraw(const wuss_event_t *event, void *task_data) snprintf(buf, sizeof(buf), "%d", doc); pos = POINT(x + 2, oy + 2 + ascent); bmfont_draw(tcx->font, scr, buf, (int) strlen(buf), - tcx->label, tcx->paper, &pos, NULL); + tcx->label, tcx->paper, NULL, &pos, NULL); } /* y-axis ruler: document y printed just right of the x=0 line, at each @@ -710,7 +710,7 @@ static result_t icons_redraw(const wuss_event_t *event, void *task_data) snprintf(buf, sizeof(buf), "%d", doc); pos = POINT(ox + 2, y + 2 + ascent); bmfont_draw(tcx->font, scr, buf, (int) strlen(buf), - tcx->label, tcx->paper, &pos, NULL); + tcx->label, tcx->paper, NULL, &pos, NULL); } return result_OK; diff --git a/libraries/wuss/test/tasks/minesweeper.c b/libraries/wuss/test/tasks/minesweeper.c index 8f9cea2c..25aeec63 100644 --- a/libraries/wuss/test/tasks/minesweeper.c +++ b/libraries/wuss/test/tasks/minesweeper.c @@ -335,12 +335,12 @@ static void minesweeper_draw_cell(minesweeper_task_t *ms, buf[0] = (char) ('0' + n); buf[1] = '\0'; - bmfont_measure(ms->font, buf, 1, INT_MAX, NULL, &width); + bmfont_measure(ms->font, buf, 1, NULL, INT_MAX, NULL, &width); bmfont_get_info(ms->font, NULL, &fh, &ascent, NULL); pos.x = x + (MINESWEEPER_CELL - width) / 2; pos.y = y + (MINESWEEPER_CELL - fh) / 2 + ascent; bmfont_draw(ms->font, scr, buf, 1, minesweeper_number_colour(n), - colour_rgba(0, 0, 0, 0), &pos, NULL); + colour_rgba(0, 0, 0, 0), NULL, &pos, NULL); } } @@ -362,11 +362,11 @@ static void minesweeper_draw_hud(minesweeper_task_t *ms, sprintf(buf, "%03d", ms->mines - ms->flags); pos.x = bounds->x0 + MS_BORDER; pos.y = bounds->y0 + (MS_HUD_H - fh) / 2 + ascent; - bmfont_draw(ms->font, scr, buf, 3, fg, bg, &pos, NULL); + bmfont_draw(ms->font, scr, buf, 3, fg, bg, NULL, &pos, NULL); sprintf(buf, "%03d", ms->elapsed); pos.x = bounds->x0 + MS_WIDTH(ms) - MS_BORDER - MINESWEEPER_CELL * 2; - bmfont_draw(ms->font, scr, buf, 3, fg, bg, &pos, NULL); + bmfont_draw(ms->font, scr, buf, 3, fg, bg, NULL, &pos, NULL); } /* draws a translucent-looking banner strip across the middle of the board; @@ -383,7 +383,7 @@ static void minesweeper_draw_banner(minesweeper_task_t *ms, int len, fh, ascent, strip_y; len = (int) strlen(text); - bmfont_measure(ms->font, text, len, INT_MAX, NULL, &width); + bmfont_measure(ms->font, text, len, NULL, INT_MAX, NULL, &width); bmfont_get_info(ms->font, NULL, &fh, &ascent, NULL); strip_y = bounds->y0 + MS_HUD_H + (MS_GRID_H(ms) + 2 * MS_BORDER - fh) / 2 - 2; @@ -391,8 +391,8 @@ static void minesweeper_draw_banner(minesweeper_task_t *ms, pos.x = bounds->x0 + (MS_WIDTH(ms) - width) / 2; pos.y = strip_y + 2 + ascent; - bmfont_draw(ms->font, scr, text, len, fg, colour_rgba(0, 0, 0, 0), &pos, - NULL); + bmfont_draw(ms->font, scr, text, len, fg, colour_rgba(0, 0, 0, 0), NULL, + &pos, NULL); } static result_t minesweeper_redraw(const wuss_event_t *event, diff --git a/libraries/wuss/test/tasks/porter-duff.c b/libraries/wuss/test/tasks/porter-duff.c index a6fcf297..b69e7a28 100644 --- a/libraries/wuss/test/tasks/porter-duff.c +++ b/libraries/wuss/test/tasks/porter-duff.c @@ -339,7 +339,7 @@ static result_t porter_duff_redraw(const wuss_event_t *event, } return bmfont_draw(pd->font, scr, name, (int) strlen(name), - pd->fg, pd->bg, &pos, NULL); + pd->fg, pd->bg, NULL, &pos, NULL); } static result_t porter_duff_idle(void *task_data) From 311c3ac5f8c267b6f35403022636e276ad50e065 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Tue, 15 Sep 2026 17:30:56 +0100 Subject: [PATCH 087/128] docs(changelog): record work since wuss-picklepuss merge Covers 108 commits (f1e3710..caebc44): icon per-type union and ACTION rename, wuss/font module extraction, p8 paletted screen support, dithered blit, furniture opt-in flag flip, menu tick API split, stack layout module, slider icon, saturn Colours/Size dialogues, baseline-relative bmfont rendering, and the assorted redraw/scroll/resize correctness fixes found along the way. Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 265 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 265 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c47943a8..334a4bcb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,97 @@ _Unreleased_ until one is cut. ### Added +- `wuss_ICON_TYPE_SLIDER` — horizontal/vertical slider icon: bevelled surround + with a sunken groove inset by a fixed gap, drag/click value mapping that + rounds to the nearest value rather than truncating, and a min > max + reversed-fill mode. `wuss_icon_get_value()` / `wuss_icon_set_value()` read + and write it. +- `geom/stack` — a nested HBox/VBox box-stack layout solver with flex + weights, cross-axis alignment, per-item min/max clamps and container + padding/gap (`docs/windowing/stack-sketch.md`); `STACK_VBOX`/`STACK_HBOX`/ + `STACK_LEAF`/`STACK_SPACER` (plus `_EX` variants) compact a static + `stack_item_t` table to one designated-init literal per row. The saturn + Size dialogue is laid out through it. +- `wuss/icon-spec.h` — `wuss_icon_spec_label()` / `_slider()` / `_action()` + fill a `wuss_icon_spec_t` for the common cases, replacing manual + field-by-field assignment (following the `create-from-desc.c` helper + precedent). +- `wuss_STD_GAP` / `wuss_STD_INSET` / `wuss_STD_SLIDER_HEIGHT` / + `wuss_STD_SECONDARY_BUTTON_HEIGHT` / `wuss_STD_PRIMARY_BUTTON_HEIGHT` — + standard sibling-gap, edge-inset and icon-height constants in + `wuss/icon.h`, generalised out of what was local to `saturn.c`. +- The saturn task gets a Colours menu (Foreground/Background via + `wuss_colourmenu_t`) and a Size... submenu — a hover-opened dialogue + (`wuss_menu_item_t::window`) with a slider for the sketch size and a + second slider for the stars-loop iteration count, Apply/Cancel/ + Adjust-Apply/Adjust-Cancel following RISC OS convention. +- The `wuss` chars task draws a dotted baseline rule and a light-blue + advance-width rule under each glyph (useful for eyeballing advance-width + drift), trims contiguous leading/trailing blank grid rows, and gains a + `wuss_STD_INSET` margin. +- `pixelfmt_p8` — 8bpp paletted screen support across the + framebuf/wuss/image-io stack: `span_p8`, screen blend/copy/fill-pattern/ + RLE-blit p8 branches, `bmfont` p8 glyph drawing, `bitmap_convert()` + p8→bgrx8888/rgbx8888, PNG load/save keeping a palette-type PNG as + `pixelfmt_p8` instead of expanding it, and `--depth 8` on the `wuss` SDL + frontend. The palette task gains a second "Screen" window showing the + physical screen bitmap's own palette; the p8 tail beyond the 16 UI colours + is filled with the web-safe 216-colour cube. +- `screen_copy_bitmap_dithered()` — blits onto a paletted screen with + per-pixel 8x8 Bayer ordered dithering before the nearest-palette lookup, + phased to screen coordinates so it stays put across redraws; + `pattern_bayer_threshold()` exposes the underlying matrix. The `wuss` + image task gains a Dithering menu toggle. +- `screen_copy_bitmap()` now blits a paletted (p1/p2/p4/p8) source bitmap + onto a screen of any format via a shared `src_fetch_rgba()` decode + helper, reusing the existing paletted→deep `pixelmap_get()` table with no + extra allocation or pass; a paletted source's tRNS-derived alpha is + honoured. +- `screen_copy_rect()` now supports 1bpp and 2bpp screens (generalised from + the existing 4bpp packed-pixel path), so window move/scroll blit fast + paths work on low-bpp screens instead of falling back to a full repaint. +- `wuss_EVENT_POINTER_ENTER` / `wuss_EVENT_POINTER_EXIT` — window-scoped + events fired when the pointer crosses onto or off a window's whole + on-screen footprint (content or furniture alike); exactly one ENTER is + outstanding per window, always balanced by a later EXIT. +- `wuss_text_draw()` / `wuss_text_measure()` — public wrappers exposing the + existing internal `bmfont_draw`/`bmfont_measure` pass-throughs, so a + client task can draw/measure text in a wuss system font without including + `framebuf/bmfont.h`. +- The minesweeper task gains a Grid Size submenu (24x24 down to 12x12, + radio-ticked to the current size); board dimensions move from + compile-time defines to runtime task fields. +- The image task gains a Background colour menu (`wuss_colourmenu_t`) for + the fill behind the 9-patch border band; `wuss_MENU_ITEM_BORROWED_SUBMENU` + marks a submenu `wuss_menu_destroy()` should not recurse into, and + `wuss_NO_BACKDROP` replaces the `wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND)` + idiom. +- `wuss_menu_create_from_desc()`'s `!`/`~` (tick/shade) prefixes now + optionally pull a bool vararg at the point they're scanned instead of + always applying, so a descriptor can encode live state (e.g. a toggle's + current value) directly. +- `wuss_menu_tick_exclusive()` / `wuss_menu_tick_item()` / + `wuss_menu_tick_set_live()` — data-level tick setters (for editing menu + items before a (re)open) completing the existing live-chain setter family + (`wuss_menu_tick_exclusive_live`, `wuss_menu_tick_item_live`, renamed from + `wuss_menu_set_ticked`/`wuss_menu_set_item_ticked`). +- Shift-F1 on the RISC OS frontend now forces `wuss_INPUT_GARBAGE` + (single-frame screen corruption), matching the existing SDL frontend and + `frontend.h`'s documented F1/Shift-F1 intent. +- `path_leaf_strip_ext()` — host-aware `.ext`-stripping helper; on RISC OS a + leafname carries no dotted extension at all (file type is separate + metadata), so the three call sites hand-rolling `strcmp`-based stripping + (namelist, bmfont enumerate, wuss icon registry) previously matched + nothing there. +- `bmfont` glyphs are now drawn at the text baseline rather than the + glyph-cell top-left: `bmfont_create()` derives ascent/descent at load + time from the space glyph's grid rows, `bmfont_get_info()` gains ascent/ + descent out-params, and every caller doing manual top-left positioning is + updated to add ascent. +- Six reserved `wuss_ICON_TYPE_*` constants (`DISPLAY`, `WRITABLE`, + `NUMBER`, `STRING_SET`, `SLIDER`, `DRAGGABLE`) after `RULE`, validated + from a spec but (bar SLIDER, now implemented) not yet drawn/hit-tested/ + routed. - `bmfont_draw_relief()` — draws a string twice with a transparent background, a shadow pass in a given colour at `pos + offset` then the main pass at `pos`, factoring the hand-rolled two-call drop-shadow idiom @@ -264,6 +355,66 @@ _Unreleased_ until one is cut. ### Changed +- **Breaking:** furniture window flags flip to opt-in: + `wuss_WINDOW_NO_TITLEBAR`/`_OUTLINE`/`_CLOSE`/`_BACK`/`_TOGGLE_SIZE`/ + `_VSCROLL`/`_HSCROLL`/`_RESIZE` become `wuss_WINDOW_TITLEBAR`/`_OUTLINE`/ + `_CLOSE`/`_BACK`/`_TOGGLE_SIZE`/`_VSCROLL`/`_HSCROLL`/`_RESIZE` (set to + show, not set to hide); `wuss_WINDOW_DEFAULT` is the OR of all eight, so a + bare-`DEFAULT` caller needs no change, but any caller that combined + `DEFAULT` with a single furniture flag (e.g. `wuss_WINDOW_NO_RESIZE_BLIT` + alone) must now OR `DEFAULT` in explicitly to keep its chrome. +- **Breaking:** menu tick setters are renamed to separate data-level and + live-chain variants: `wuss_menu_set_ticked` → `wuss_menu_tick_exclusive_live`, + `wuss_menu_set_item_ticked` → `wuss_menu_tick_item_live`; + `wuss_menu_open_ticked` and the new `wuss_menu_tick_set` take a bit-vector + (`unsigned int`) instead of an `int` array. +- struct `wuss_icon`'s mutually-exclusive per-type fields (label border, + pattern tile, bitmap image, radio group, menu-entry swatch) move into a + per-type union, mirrored on `wuss_icon_spec_t`, then the spec is nested + directly inside `struct wuss_icon` as `{ spec; state; }` — + `icon->FIELD` becomes `icon->spec.FIELD`. `wuss_icon_get_window()` and the + icon→window back-pointer are removed; the four mutators that needed it + (delete, set_text, set_hidden, set_selected) now take the window as an + explicit argument. +- `wuss_ICON_TYPE_BUTTON` is renamed `wuss_ICON_TYPE_ACTION` throughout the + icon API, implementation, tests and docs. +- Fonts move out of loose `struct wuss` fields into a core-owned + `libraries/wuss/font/` module (`struct wuss_fontset`, + `wuss__fontset_init/_height`, the `wuss_get_font*` accessors, the + centralised text renderer); `wuss`'s four loose font fields collapse to + one embedded fontset. +- The `wuss` demo's SDL present path converts and uploads only dirty rows + instead of the whole screen every frame (`wuss_frontend_present` now + takes a dirty box); a new `wuss->touched` region list (distinct from + `wuss->dirty`) tracks pixels written directly by `wuss__blit_pieces`/ + resize/toggle-size so the frontend still re-uploads blitted-but-not- + repainted destinations. +- `wuss_window_move()` translates the cached furniture-layout rects by the + move delta instead of invalidating and rebuilding the whole per-window + layout cache on every drag tick; `wuss_window_move()` and the resize path + both now early-return once the target box is found equal to the window's + current one, skipping the blit/invalidate machinery for a no-op drag tick. +- Toggle-size now grows a window to fill the whole screen (capped per axis + at the document extent, or uncapped when that's 0), nudging the top-left + toward the origin by the minimum needed to fit, rather than pinning the + top-left and only growing the bottom-right; restore returns the exact + pre-toggle box. +- Adjust-clicking a scroll well now pages away from the click point + (mirroring Select's page-towards), matching RISC OS's usual Select/Adjust + reversal elsewhere in the furniture. +- `screen_set_pixel_8()` is renamed `screen_set_pixel_p8()`, matching the + `_p1`/`_p2`/`_p4` naming of the other paletted set-pixel helpers. +- Advance widths encoded in font PNGs are now 1px shorter than the true pen + advance (`bmfont_advance_for()` adds the missing pixel back in as letter + spacing at every use site); the bundled bitmap fonts (`04b_03`, `04b_25`, + the `DPT-*` faces, `Nokia`, `Symbols`, `Tiny`) are regenerated to match, + and `tools/ttf2bmfont.py`'s ink-advance mode drops a spurious extra pixel + it previously added. +- The bundled bitmap fonts are renamed with a provenance prefix (`DPT-` for + the in-house faces, `ZX-` for GliderRider). +- `screen_copy_bitmap_i`'s has-alpha-channel check is hoisted into a general + `pixelfmt_has_alpha()` macro alongside `pixelfmt_is_rle`/ + `pixelfmt_paletted_nentries`. - **Breaking:** `wuss_window_invalidate_all()` is renamed `wuss_window_invalidate_visible()` -- "all" misread as the whole document when it only ever covered the visible content rectangle. Same behaviour; @@ -412,6 +563,120 @@ _Unreleased_ until one is cut. ### Fixed +- `bmfont_draw()` clipped full-width glyphs (M, W, ...) out of existence: + `bmfont_advance_for()`'s advance (cell width + the new 1px letter + spacing) was used directly as the drawable pixel width, pushing + `clippedcharwidth` past `charwidth` and driving `right_skip` negative. + Each glyph's draw now splits into the cell itself (clamped to + `charwidth`) and any leftover advance, drawn from a zero-filled glyph + buffer. +- Several furniture task windows (swatches, text, palette) passed + `wuss_WINDOW_NO_RESIZE_BLIT` alone as their window flags, zeroing every + furniture bit under the new opt-in flag polarity instead of just opting + out of blit-based resize; now OR'd with `wuss_WINDOW_DEFAULT`. +- The saturn Size dialogue's Cancel/Apply buttons now act on Select release + (`wuss_MOUSE_UP`) instead of press, so a press dragged off the button no + longer commits and an Adjust click leaves the dialogue open per RISC OS + convention; the slider groove now derives from the surround box rather + than the bevel box. +- Icon text was vertically centred using ascender+descender (`font_height`), + pulling label/button/radio-option/menu-entry text visually high since + most glyphs never touch the descender band; centred by `font_ascent` + instead, factored into a shared `icon_text_baseline_y()` helper. +- The chars grid's document extent wasn't updated on a font switch, so the + scroll range stayed sized to the original font — a larger font put grid + cells out of scroll reach, a smaller one left dead scroll space. +- A borrowed-window menu item (`wuss_menu_item_t::window`) could open + off-screen near the parent menu's right edge, since `wuss_window_move` + deliberately doesn't clamp; extracted `wuss_window_create()`'s on-screen + nudge into a shared helper and applied it after the anchor move. +- The RADIO/OPTION icon glyph square was always font-height, truncating a + larger state-sprite blit and ignoring its real width for the label + offset; now sized from the state bitmap when present. Its box now also + grows to fit the larger of its two state bitmaps so a select-time + invalidate covers the whole overhanging sprite, and the box is now + cleared before redraw when the icon has no explicit background + (previously a shrinking glyph swap left stale pixels). +- A submenu dragged back over its own parent's titlebar was closed + mid-drag, because the IDLE-tick "pointer over parent titlebar" check + didn't verify the parent was actually frontmost there; now requires + `wuss__window_at` to resolve to the parent itself. Separately, a menu + chain's submenu now only opens while the pointer is in the row's arrow + gutter (not anywhere on the row) and closes on re-entry; and the parent + menu's title bar being hovered now closes an open child chain (previously + `wuss_mouse_move`'s early-return over furniture meant nothing heard about + it). +- `screen_copy_bitmap()` read a paletted source bitmap (e.g. a PLTE-chunk + PNG) as if it were 32bpp RGBA, running past the buffer end; first + hardened to reject non-32bpp sources, then given real paletted-source + support via a shared `src_fetch_rgba()` decode path. +- The ordered-dither bias table was keyed on raw palette entry count + instead of the true per-channel quantisation level (4:4:4 for p1/p2/p4, + 5:6:5 for p8), collapsing the p8 Bayer bias to `{-1,0}` and making + dithering invisible; and the dither cell was keyed on destination screen + coordinates rather than sprite-local ones, so the pattern crawled across + a moving sprite instead of staying fixed to it. +- The generalised packed-copy path (`screen_copy_rect_packed`, covering + p1/p2/p4) used LSB-first within-byte indexing for every depth, but p1/p2 + are packed MSB-first — a non-byte-aligned move on a 2bpp screen picked + the wrong pixel and shuffled the row. +- The screen palette wasn't repadded on a live palette change, only at + startup: picking a palette entry while running p8 fed wuss's bare + 16-entry UI palette straight to `bitmap_set_palette()`, which reads 256 + entries for p8 — a heap-buffer-overflow. The pad-to-screen-size logic is + now shared between startup and the live-change handler. +- A p8-loaded PNG converted to `bgrx8888` before reaching + `screen_copy_bitmap`/`screen_copy_ninepatch` produced dull/wrong-hue + composites, since that blit family assumes `rgba8888` byte order; + switched to a matching `bmconv_p8_to_rgbx8888`. +- `wuss_window_invalidate(window, NULL)` built its whole-visible-rect + placeholder already in screen space then ran it through the doc-space- + to-screen translation a second time, double-applying scroll once a + window was shrunk below its document and scrolled, sliding queued dirty + rects off to one side; the placeholder is now built in doc space. +- A client-supplied `wuss_window_invalidate()` box was never clipped + against the window's own furniture (only against occluding windows above + it), so an edge-hugging client could dirty the titlebar/scrollbars/ + outline around its content; now intersected with the content box first. +- The ball task's edge-clamp was off-by-one on the right/bottom (content + being a half-open box), poking the invalidation 1px into the furniture + strip. +- A resize drag's scroll-reclamp fast-path blit read its source without + checking `wuss->dirty[]` (unlike move/set-scroll), letting an earlier + unpainted dirty region in the same frame get slid forward as settled + content; a follow-up guarded the same path against + `WUSS_MAX_INVALIDATE_PIECES` overflow (falling back to a full repaint + instead of silently dropping survivor pieces), and a duplicate + furniture-repaint call on the same reclamp path was removed. +- `wuss__blit_pieces` translated clean source pieces by the scroll delta + and clipped only against occluding windows, never against the content + box itself — a fast scroll (especially after a shrink) could blit and + then invalidate screen area outside the window, painted with stale/ + garbage content on the next redraw; now intersected against the content + box first. +- A scroll-well click's page-and-hold also armed a sausage drag, turning a + held button into a live scroll; then, once guarded off by region, that + guard also blocked a *direct* sausage hit from arming a drag at all — + now gated on whether the click actually paged, not on which region it + hit. +- Interactive resize-drag could be dragged past the screen's own + width/height when the window's top-left was off-screen (negative x0/y0), + since the clamp measured room left from the window's current position + rather than the screen's total size. +- `path_join_leafname()` on RISC OS produced invalid pathnames like + `Nokia/png` (`/` is a literal leafname character there, not a separator, + and file type isn't part of the string at all). +- `mkstemps` (a GNU/BSD extension absent from the RISC OS GCCSDK libc) in + `rle-test.c` broke the RISC OS build; replaced with a fixed leafname via + `path_join_leafname`. +- Minesweeper repainted the whole HUD strip and every board cell on any + click, even a single flag toggle; reveal now accumulates just the + board-cell range its flood fill touched and redraw skips the HUD/ + off-dirty cells accordingly, cutting the flicker. +- curve/porter-duff/gradient task content (a type label, a checkerboard, a + composited bitmap) was pinned to the window corner instead of tracking + the document scroll offset, so it stayed visually fixed while the rest + of the content scrolled underneath it. - `wuss_window_move()`'s fast path no longer slides pixels still queued in `wuss->dirty[]` from an earlier invalidation this frame onto the window's new position: pending dirty regions are stripped from the From 8347cd9ef7730b65d3f5b5aaf3fab4859d173ea2 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Tue, 15 Sep 2026 17:42:36 +0100 Subject: [PATCH 088/128] feat(wuss): add ring and body iteration sliders to saturn size dialogue Extends the existing Iters slider pattern with two more rows so ring_iters and body_iters are tunable from the Size... dialogue alongside stars_iters and size. --- libraries/wuss/test/tasks/saturn.c | 98 ++++++++++++++++++++++++++++-- libraries/wuss/test/tasks/saturn.h | 4 ++ 2 files changed, 97 insertions(+), 5 deletions(-) diff --git a/libraries/wuss/test/tasks/saturn.c b/libraries/wuss/test/tasks/saturn.c index ab7bb462..42ce88cc 100644 --- a/libraries/wuss/test/tasks/saturn.c +++ b/libraries/wuss/test/tasks/saturn.c @@ -91,6 +91,14 @@ enum { SATURN_SIZE_ICON_SLIDER2, SATURN_SIZE_ICON_VALUE2, + SATURN_SIZE_ICON_LABEL3, + SATURN_SIZE_ICON_SLIDER3, + SATURN_SIZE_ICON_VALUE3, + + SATURN_SIZE_ICON_LABEL4, + SATURN_SIZE_ICON_SLIDER4, + SATURN_SIZE_ICON_VALUE4, + SATURN_SIZE_ICON_CANCEL, SATURN_SIZE_ICON_APPLY, @@ -140,10 +148,16 @@ result_t saturn_create(wuss_t *wuss, task->bg_colourmenu = NULL; task->menu_handle = NULL; task->size_dialogue = NULL; - task->size_slider = NULL; - task->size_value_label = NULL; - task->size_cancel = NULL; - task->size_apply = NULL; + task->size_slider = NULL; + task->size_value_label = NULL; + task->size_slider2 = NULL; + task->size_value2_label = NULL; + task->size_slider3 = NULL; + task->size_value3_label = NULL; + task->size_slider4 = NULL; + task->size_value4_label = NULL; + task->size_cancel = NULL; + task->size_apply = NULL; /* saturn_redraw paints its own background */ delegate_desc.handle = saturn_handle; @@ -342,6 +356,16 @@ enum ST_SLDR2, ST_VAL2, + ST_ROW3, + ST_LABL3, + ST_SLDR3, + ST_VAL3, + + ST_ROW4, + ST_LABL4, + ST_SLDR4, + ST_VAL4, + ST_BTNS, ST_SPCR, ST_CNCL, @@ -373,6 +397,16 @@ static const stack_item_t g_saturn_size_stack[SIZE_STACK__LIMIT] = [ST_SLDR2] = STACK_LEAF_EX(ST_ROW2, 0, wuss_STD_SLIDER_HEIGHT, stack_ALIGN_CENTRE, 1, ST_SLIDER_MIN_W, 0), [ST_VAL2] = STACK_LEAF(ST_ROW2, ST_LABEL2_W, 16, stack_ALIGN_CENTRE), + [ST_ROW3] = STACK_HBOX(ST_ROOT, wuss_STD_SLIDER_HEIGHT, wuss_STD_GAP, stack_ALIGN_START), + [ST_LABL3] = STACK_LEAF(ST_ROW3, ST_LABEL_W, 16, stack_ALIGN_CENTRE), + [ST_SLDR3] = STACK_LEAF_EX(ST_ROW3, 0, wuss_STD_SLIDER_HEIGHT, stack_ALIGN_CENTRE, 1, ST_SLIDER_MIN_W, 0), + [ST_VAL3] = STACK_LEAF(ST_ROW3, ST_LABEL2_W, 16, stack_ALIGN_CENTRE), + + [ST_ROW4] = STACK_HBOX(ST_ROOT, wuss_STD_SLIDER_HEIGHT, wuss_STD_GAP, stack_ALIGN_START), + [ST_LABL4] = STACK_LEAF(ST_ROW4, ST_LABEL_W, 16, stack_ALIGN_CENTRE), + [ST_SLDR4] = STACK_LEAF_EX(ST_ROW4, 0, wuss_STD_SLIDER_HEIGHT, stack_ALIGN_CENTRE, 1, ST_SLIDER_MIN_W, 0), + [ST_VAL4] = STACK_LEAF(ST_ROW4, ST_LABEL2_W, 16, stack_ALIGN_CENTRE), + [ST_BTNS] = STACK_HBOX(ST_ROOT, wuss_STD_PRIMARY_BUTTON_HEIGHT, wuss_STD_GAP, stack_ALIGN_END), [ST_SPCR] = STACK_SPACER(ST_BTNS, 1), [ST_CNCL] = STACK_LEAF(ST_BTNS, ST_CANCEL_W, wuss_STD_SECONDARY_BUTTON_HEIGHT, stack_ALIGN_CENTRE), @@ -406,7 +440,7 @@ static result_t saturn_size_dialogue_create(saturn_task_t *task) row_w = ST_LABEL_W + wuss_STD_GAP + ST_SLIDER_MIN_W + wuss_STD_GAP + ST_LABEL2_W; btns_w = ST_CANCEL_W + wuss_STD_GAP + ST_APPLY_W; sz.w = MAX(row_w, btns_w) + 2 * wuss_STD_INSET; - sz.h = (wuss_STD_SLIDER_HEIGHT + wuss_STD_GAP) * 2 + wuss_STD_PRIMARY_BUTTON_HEIGHT + 2 * wuss_STD_INSET; + sz.h = (wuss_STD_SLIDER_HEIGHT + wuss_STD_GAP) * 4 + wuss_STD_PRIMARY_BUTTON_HEIGHT + 2 * wuss_STD_INSET; rc = wuss_window_create_placed(task->delegate, sz, @@ -438,6 +472,18 @@ static result_t saturn_size_dialogue_create(saturn_task_t *task) wuss_icon_spec_slider(&specs[SATURN_SIZE_ICON_SLIDER2], boxes[ST_SLDR2], wuss_COLOUR_BLACK, wuss_SLIDER_HORIZONTAL, 10, 1000, value); wuss_icon_spec_label(&specs[SATURN_SIZE_ICON_VALUE2], boxes[ST_VAL2], buf, wuss_COLOUR_BLACK, 0); /* buf copied by wuss_icon_create_array */ + value = task->config.ring_iters; + snprintf(buf, sizeof(buf), "%d", value); + wuss_icon_spec_label(&specs[SATURN_SIZE_ICON_LABEL3], boxes[ST_LABL3], "Ring", wuss_COLOUR_BLACK, 1); + wuss_icon_spec_slider(&specs[SATURN_SIZE_ICON_SLIDER3], boxes[ST_SLDR3], wuss_COLOUR_BLACK, wuss_SLIDER_HORIZONTAL, 10, 4000, value); + wuss_icon_spec_label(&specs[SATURN_SIZE_ICON_VALUE3], boxes[ST_VAL3], buf, wuss_COLOUR_BLACK, 0); /* buf copied by wuss_icon_create_array */ + + value = task->config.body_iters; + snprintf(buf, sizeof(buf), "%d", value); + wuss_icon_spec_label(&specs[SATURN_SIZE_ICON_LABEL4], boxes[ST_LABL4], "Body", wuss_COLOUR_BLACK, 1); + wuss_icon_spec_slider(&specs[SATURN_SIZE_ICON_SLIDER4], boxes[ST_SLDR4], wuss_COLOUR_BLACK, wuss_SLIDER_HORIZONTAL, 10, 4000, value); + wuss_icon_spec_label(&specs[SATURN_SIZE_ICON_VALUE4], boxes[ST_VAL4], buf, wuss_COLOUR_BLACK, 0); /* buf copied by wuss_icon_create_array */ + wuss_icon_spec_action(&specs[SATURN_SIZE_ICON_CANCEL], boxes[ST_CNCL], "Cancel", wuss_COLOUR_BLACK, wuss_COLOUR_WINDOW, 0); wuss_icon_spec_action(&specs[SATURN_SIZE_ICON_APPLY], boxes[ST_APLY], "Apply", wuss_COLOUR_BLACK, wuss_COLOUR_WINDOW, 1); @@ -450,6 +496,10 @@ static result_t saturn_size_dialogue_create(saturn_task_t *task) task->size_value_label = made[SATURN_SIZE_ICON_VALUE1]; task->size_slider2 = made[SATURN_SIZE_ICON_SLIDER2]; task->size_value2_label = made[SATURN_SIZE_ICON_VALUE2]; + task->size_slider3 = made[SATURN_SIZE_ICON_SLIDER3]; + task->size_value3_label = made[SATURN_SIZE_ICON_VALUE3]; + task->size_slider4 = made[SATURN_SIZE_ICON_SLIDER4]; + task->size_value4_label = made[SATURN_SIZE_ICON_VALUE4]; task->size_cancel = made[SATURN_SIZE_ICON_CANCEL]; task->size_apply = made[SATURN_SIZE_ICON_APPLY]; @@ -483,6 +533,16 @@ static result_t saturn_sizedlg_pre_show(saturn_task_t *task) snprintf(buf, sizeof(buf), "%d", value); rc = wuss_icon_set_text(task->size_dialogue, task->size_value2_label, buf); + value = task->config.ring_iters; + wuss_icon_set_value(task->size_dialogue, task->size_slider3, value); + snprintf(buf, sizeof(buf), "%d", value); + rc = wuss_icon_set_text(task->size_dialogue, task->size_value3_label, buf); + + value = task->config.body_iters; + wuss_icon_set_value(task->size_dialogue, task->size_slider4, value); + snprintf(buf, sizeof(buf), "%d", value); + rc = wuss_icon_set_text(task->size_dialogue, task->size_value4_label, buf); + return rc; } @@ -498,6 +558,10 @@ static result_t saturn_sizedlg_apply(saturn_task_t *task) task->config.size = size_value; iters_value = wuss_icon_get_value(task->size_slider2); task->config.stars_iters = iters_value; + iters_value = wuss_icon_get_value(task->size_slider3); + task->config.ring_iters = iters_value; + iters_value = wuss_icon_get_value(task->size_slider4); + task->config.body_iters = iters_value; rc = wuss_window_resize(task->window, SIZE2D(size_value, size_value)); if (rc != result_OK) return rc; @@ -551,6 +615,30 @@ static result_t saturn_size_dialogue_icon(saturn_task_t *task, buf); } + if (icon == task->size_slider3) + { + if (event->data.icon.action != wuss_MOUSE_DOWN && + event->data.icon.action != wuss_MOUSE_MOVE) + return result_OK; + + value = event->data.icon.value; + snprintf(buf, sizeof(buf), "%d", value); + return wuss_icon_set_text(task->size_dialogue, task->size_value3_label, + buf); + } + + if (icon == task->size_slider4) + { + if (event->data.icon.action != wuss_MOUSE_DOWN && + event->data.icon.action != wuss_MOUSE_MOVE) + return result_OK; + + value = event->data.icon.value; + snprintf(buf, sizeof(buf), "%d", value); + return wuss_icon_set_text(task->size_dialogue, task->size_value4_label, + buf); + } + if (event->data.icon.action != wuss_MOUSE_UP) return result_OK; diff --git a/libraries/wuss/test/tasks/saturn.h b/libraries/wuss/test/tasks/saturn.h index d1753ea9..b3d6efbd 100644 --- a/libraries/wuss/test/tasks/saturn.h +++ b/libraries/wuss/test/tasks/saturn.h @@ -56,6 +56,10 @@ typedef struct saturn_task wuss_icon_t *size_value_label; wuss_icon_t *size_slider2; wuss_icon_t *size_value2_label; + wuss_icon_t *size_slider3; + wuss_icon_t *size_value3_label; + wuss_icon_t *size_slider4; + wuss_icon_t *size_value4_label; wuss_icon_t *size_cancel; wuss_icon_t *size_apply; } From 3c165fc42ade7908d09dd0ae9eec64edee346939 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Tue, 15 Sep 2026 17:45:09 +0100 Subject: [PATCH 089/128] refactor(wuss): fold slider task into icons work-area demo Removes the standalone Slider window/task and adds its horizontal/vertical slider pair as a new group in the Icons window, dropping one top-level demo window. --- CMakeLists.txt | 1 - apps/wuss/tasks.c | 15 +-- libraries/wuss/test/tasks/icons.c | 83 +++++++++++++++- libraries/wuss/test/tasks/icons.h | 11 ++- libraries/wuss/test/tasks/slider.c | 150 ----------------------------- libraries/wuss/test/tasks/slider.h | 36 ------- 6 files changed, 87 insertions(+), 209 deletions(-) delete mode 100644 libraries/wuss/test/tasks/slider.c delete mode 100644 libraries/wuss/test/tasks/slider.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 54530b75..1f107fa7 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -769,7 +769,6 @@ if(BUILD_APPS) libraries/wuss/test/tasks/palette.c libraries/wuss/test/tasks/porter-duff.c libraries/wuss/test/tasks/saturn.c - libraries/wuss/test/tasks/slider.c libraries/wuss/test/tasks/sofa.c libraries/wuss/test/tasks/swatches.c libraries/wuss/test/tasks/text.c) diff --git a/apps/wuss/tasks.c b/apps/wuss/tasks.c index 17aa50d1..c744cc72 100644 --- a/apps/wuss/tasks.c +++ b/apps/wuss/tasks.c @@ -33,7 +33,6 @@ #include "tasks/palette.h" #include "tasks/porter-duff.h" #include "tasks/saturn.h" -#include "tasks/slider.h" #include "tasks/sofa.h" #include "tasks/swatches.h" #include "tasks/text.h" @@ -230,17 +229,6 @@ static result_t spawn_sofa(void) return result_OK; } -static result_t spawn_slider(void) -{ - slider_task_t *t = calloc(1, sizeof(*t)); - result_t rc; - if (t == NULL) return result_OOM; - rc = slider_create(g.wuss, g.daydream_font, t); - if (rc != result_OK) return rc; - if (t->window == NULL) { free(t); return rc; } - return result_OK; -} - static result_t spawn_gradient(void) { gradient_task_t *t = calloc(1, sizeof(*t)); @@ -325,7 +313,6 @@ static const wuss_menu_item_t g_launch_items[] = { "Palette", wuss_MENU_ITEM_NONE, NULL }, { "Porter-Duff", wuss_MENU_ITEM_NONE, NULL }, { "Saturn", wuss_MENU_ITEM_NONE, NULL }, - { "Slider", wuss_MENU_ITEM_NONE, NULL }, { "Sofa", wuss_MENU_ITEM_NONE, NULL }, { "Swatches", wuss_MENU_ITEM_NONE, NULL }, { "Text", wuss_MENU_ITEM_NONE, NULL } @@ -336,7 +323,7 @@ static const task_spawn_fn_t g_launch_spawn[] = spawn_ball, spawn_blank, spawn_chars, spawn_checker, spawn_clock, spawn_curve, spawn_gradient, spawn_greeble, spawn_icons, spawn_image, spawn_lissajous, spawn_minesweeper, spawn_palette, - spawn_porter_duff, spawn_saturn, spawn_slider, spawn_sofa, spawn_swatches, + spawn_porter_duff, spawn_saturn, spawn_sofa, spawn_swatches, spawn_text }; diff --git a/libraries/wuss/test/tasks/icons.c b/libraries/wuss/test/tasks/icons.c index 8598c64c..8601f4a4 100644 --- a/libraries/wuss/test/tasks/icons.c +++ b/libraries/wuss/test/tasks/icons.c @@ -23,7 +23,7 @@ #include "icons.h" #define ICONS_DOC_W 260 -#define ICONS_DOC_H 1160 /* taller than the window, so scrolling is exercised */ +#define ICONS_DOC_H 1310 /* taller than the window, so scrolling is exercised */ #define ICONS_MARGIN 28 /* left edge of everything except frame captions */ #define ICONS_ROW 20 /* vertical pitch between stacked simple icons */ @@ -40,10 +40,11 @@ enum ICONS_N_ICONSET = 5, /* frame + opton + optoff + radon + radoff from the loaded set */ ICONS_N_PATTERN = 2, /* frame + one PATTERN swatch */ ICONS_N_BORDERS = 6, /* frame + GROOVE + RIDGE + ACTION + DIVIDER labels */ + ICONS_N_SLIDERS = 4, /* frame + horizontal + vertical slider + state label */ ICONS_N_MENU = 8, /* frame + plain, ticked, swatch, submenu, disabled, rule, separator entry */ ICONS_NSPECS = ICONS_N_INTRO + ICONS_N_BUTTONS + ICONS_N_RADIOS + ICONS_N_BITMAPS + ICONS_N_ICONSET + ICONS_N_PATTERN + - ICONS_N_BORDERS + ICONS_N_MENU + ICONS_N_BORDERS + ICONS_N_SLIDERS + ICONS_N_MENU }; /* Running state threaded through the icons_add_* helpers: where to write the @@ -417,6 +418,63 @@ static void icons_add_borders(icons_layout_t *lay) lay->y = top + 166; } +/* A grouping frame captioned "Sliders", holding a horizontal slider, a + * vertical slider beside it and a label echoing whichever last moved. + * Returns the horizontal/vertical slider and echo-label indices via + * horiz/vert/state. */ +static void icons_add_sliders(icons_layout_t *lay, + int *horiz, + int *vert, + int *state) +{ + wuss_icon_spec_t *s; + int top; + + top = lay->y; + s = &lay->specs[lay->n]; + s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN, top, 200, 130); + s->type = wuss_ICON_TYPE_FRAME; + s->text = "Sliders"; + s->fg = lay->black; + s->bg = wuss_NO_BACKGROUND; + lay->n++; + + *horiz = lay->n; + s = &lay->specs[lay->n]; + s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN + 10, top + 20, 160, 20); + s->type = wuss_ICON_TYPE_SLIDER; + s->fg = lay->black; + s->bg = wuss_NO_BACKGROUND; + s->u.slider.orientation = wuss_SLIDER_HORIZONTAL; + s->u.slider.min = 0; + s->u.slider.max = 100; + s->u.slider.default_value = 25; + lay->n++; + + *vert = lay->n; + s = &lay->specs[lay->n]; + s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN + 160, top + 50, 20, 70); + s->type = wuss_ICON_TYPE_SLIDER; + s->fg = lay->black; + s->bg = wuss_NO_BACKGROUND; + s->u.slider.orientation = wuss_SLIDER_VERTICAL; + s->u.slider.min = 0; + s->u.slider.max = 10; + s->u.slider.default_value = 10; + lay->n++; + + *state = lay->n; + s = &lay->specs[lay->n]; + s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN + 10, top + 50, 140, 14); + s->type = wuss_ICON_TYPE_LABEL; + s->text = "horizontal: 25"; + s->fg = lay->black; + s->bg = wuss_NO_BACKGROUND; + lay->n++; + + lay->y = top + 146; +} + /* A grouping frame captioned "Menu", holding a menu-entry strip: plain, * ticked, a swatch entry, a submenu entry, a disabled entry, then a dashed * rule and a SEPARATOR-flagged entry below it. Hover the pointer over any live @@ -516,6 +574,7 @@ result_t icons_create(wuss_t *wuss, icons_layout_t lay; const char *sprite_path; int i_button, i_counter, i_opt, i_state, i_hotspot, i_ticked; + int i_shoriz, i_svert, i_sstate; result_t rc; task->font = font; @@ -530,8 +589,11 @@ result_t icons_create(wuss_t *wuss, task->count = 0; task->opt = NULL; task->state = NULL; - task->has_sprite = 0; - task->hotspot = NULL; + task->has_sprite = 0; + task->hotspot = NULL; + task->slider_horiz = NULL; + task->slider_vert = NULL; + task->slider_state = NULL; sprite_path = path_join_filename(resources, 3, "resources", "wuss", path_join_leafname("ninepatch", "png")); @@ -601,6 +663,7 @@ result_t icons_create(wuss_t *wuss, icons_add_iconset(&lay, wuss); icons_add_pattern(&lay); icons_add_borders(&lay); + icons_add_sliders(&lay, &i_shoriz, &i_svert, &i_sstate); icons_add_menu(&lay, &i_ticked); rc = wuss_icon_create_array(task->window, specs, lay.n, made); @@ -613,6 +676,9 @@ result_t icons_create(wuss_t *wuss, task->state = made[i_state]; if (i_hotspot >= 0) task->hotspot = made[i_hotspot]; + task->slider_horiz = made[i_shoriz]; + task->slider_vert = made[i_svert]; + task->slider_state = made[i_sstate]; wuss_icon_set_selected(task->window, made[i_ticked], 1); /* "Show grid" starts ticked */ /* fully built: from here a last-window close reaps the task and its @@ -725,6 +791,15 @@ static result_t icons_icon(const wuss_event_t *event, void *task_data) tcx = task_data; icon = event->data.icon.icon; + if (icon == tcx->slider_horiz || icon == tcx->slider_vert) + { + const char *name; + + name = (icon == tcx->slider_horiz) ? "horizontal" : "vertical"; + snprintf(buf, sizeof(buf), "%s: %d", name, event->data.icon.value); + return wuss_icon_set_text(tcx->window, tcx->slider_state, buf); + } + /* a radio/option latches on MOUSE_UP -- report the state then */ if (event->data.icon.action == wuss_MOUSE_UP && (wuss_icon_get_type(icon) == wuss_ICON_TYPE_RADIO || diff --git a/libraries/wuss/test/tasks/icons.h b/libraries/wuss/test/tasks/icons.h index 775a2d2d..c2b8c923 100644 --- a/libraries/wuss/test/tasks/icons.h +++ b/libraries/wuss/test/tasks/icons.h @@ -12,9 +12,9 @@ #include "wuss/window.h" /* demonstrates wuss-managed work-area icons: labelled groups covering every - * icon type (buttons, radios/options, bitmaps, a pattern swatch, a menu-entry - * strip) plus a button placed far down the document to show icons scroll with - * the content and stay clickable */ + * icon type (buttons, radios/options, bitmaps, a pattern swatch, sliders, a + * menu-entry strip) plus a button placed far down the document to show icons + * scroll with the content and stay clickable */ typedef struct icons_task { wuss_window_t *window; @@ -28,7 +28,10 @@ typedef struct icons_task wuss_icon_t *state; /* label echoing radio/option selection */ bitmap_t sprite; /* borrowed by the two BITMAP icons; freed on close */ int has_sprite; - wuss_icon_t *hotspot; /* the interactive BITMAP icon */ + wuss_icon_t *hotspot; /* the interactive BITMAP icon */ + wuss_icon_t *slider_horiz; /* horizontal slider */ + wuss_icon_t *slider_vert; /* vertical slider */ + wuss_icon_t *slider_state; /* label echoing whichever slider last moved */ } icons_task_t; diff --git a/libraries/wuss/test/tasks/slider.c b/libraries/wuss/test/tasks/slider.c deleted file mode 100644 index 576b84c0..00000000 --- a/libraries/wuss/test/tasks/slider.c +++ /dev/null @@ -1,150 +0,0 @@ -/* wuss/test/tasks/slider.c -- slider icons task */ - -#ifdef WUSS_APP - -#include -#include -#include - -#ifdef FORTIFY -#include "fortify/fortify.h" -#endif - -#include "geom/box.h" -#include "geom/point.h" -#include "geom/size.h" - -#include "slider.h" - -#define SLIDER_DOC_W 240 -#define SLIDER_DOC_H 200 -#define SLIDER_MARGIN 20 - -enum { SLIDER_NSPECS = 3 }; /* horizontal slider, vertical slider, state label */ - -result_t slider_create(wuss_t *wuss, - bmfont_t *font, - slider_task_t *task) -{ - wuss_task_t *delegate; - wuss_task_desc_t delegate_desc; - wuss_icon_spec_t specs[SLIDER_NSPECS]; - wuss_icon_t *made[SLIDER_NSPECS]; - result_t rc; - - task->font = font; - task->label = colour_rgb(0x00, 0x00, 0x00); - task->window = NULL; - task->horiz = NULL; - task->vert = NULL; - task->state = NULL; - - delegate_desc.handle = slider_handle; - delegate_desc.task_data = task; - delegate_desc.name = "slider"; - rc = wuss_task_create(wuss, &delegate_desc, &delegate); - if (rc != result_OK) - { - free(task); /* nothing registered yet; the spawner will not free it */ - return rc; - } - - rc = wuss_window_create_placed(delegate, - SIZE2D(SLIDER_DOC_W, SLIDER_DOC_H), - "Sliders", - wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_COLOUR_WINDOW), - SIZE2D(SLIDER_DOC_W, SLIDER_DOC_H), - SIZE2D(0, 0), - &task->window); - if (rc != result_OK) - { - wuss_task_destroy(delegate); /* QUIT frees the task block */ - return rc; - } - - memset(specs, 0, sizeof(specs)); - - specs[0].bbox = (box_t) BOX_POS_SIZE(SLIDER_MARGIN, 30, - SLIDER_DOC_W - SLIDER_MARGIN * 2, - 20); - specs[0].type = wuss_ICON_TYPE_SLIDER; - specs[0].fg = wuss_COLOUR_BLACK; - specs[0].bg = wuss_NO_BACKGROUND; - specs[0].u.slider.orientation = wuss_SLIDER_HORIZONTAL; - specs[0].u.slider.min = 0; - specs[0].u.slider.max = 100; - specs[0].u.slider.default_value = 25; - - specs[1].bbox = (box_t) BOX_POS_SIZE(SLIDER_DOC_W - SLIDER_MARGIN - 20, - 70, 20, 100); - specs[1].type = wuss_ICON_TYPE_SLIDER; - specs[1].fg = wuss_COLOUR_BLACK; - specs[1].bg = wuss_NO_BACKGROUND; - specs[1].u.slider.orientation = wuss_SLIDER_VERTICAL; - specs[1].u.slider.min = 0; - specs[1].u.slider.max = 10; - specs[1].u.slider.default_value = 10; - - specs[2].bbox = (box_t) BOX_POS_SIZE(SLIDER_MARGIN, 70, 150, 14); - specs[2].type = wuss_ICON_TYPE_LABEL; - specs[2].text = "horizontal: 25"; - specs[2].fg = wuss_COLOUR_BLACK; - specs[2].bg = wuss_NO_BACKGROUND; - - rc = wuss_icon_create_array(task->window, specs, SLIDER_NSPECS, made); - if (rc != result_OK) - { - wuss_task_destroy(delegate); /* closes the window; QUIT frees the block */ - return rc; - } - - task->horiz = made[0]; - task->vert = made[1]; - task->state = made[2]; - - wuss_task_set_autoclose(delegate, 1); - - return result_OK; -} - -static result_t slider_icon(const wuss_event_t *event, void *task_data) -{ - slider_task_t *tcx; - wuss_icon_t *icon; - const char *name; - char buf[48]; - - tcx = task_data; - icon = event->data.icon.icon; - - if (icon != tcx->horiz && icon != tcx->vert) - return result_OK; - - name = (icon == tcx->horiz) ? "horizontal" : "vertical"; - snprintf(buf, sizeof(buf), "%s: %d", name, event->data.icon.value); - - return wuss_icon_set_text(tcx->window, tcx->state, buf); -} - -result_t slider_handle(wuss_window_t *window, - const wuss_event_t *event, - void *task_data) -{ - (void) window; - - switch (event->kind) - { - case wuss_EVENT_ICON: - return slider_icon(event, task_data); - - case wuss_EVENT_QUIT: - free(task_data); /* calloc'd per instance by the spawner */ - return result_OK; - - default: - return result_OK; - } -} - -#endif /* WUSS_APP */ diff --git a/libraries/wuss/test/tasks/slider.h b/libraries/wuss/test/tasks/slider.h deleted file mode 100644 index a127cae8..00000000 --- a/libraries/wuss/test/tasks/slider.h +++ /dev/null @@ -1,36 +0,0 @@ -/* wuss/test/tasks/slider.h -- slider icons task */ - -#ifndef TASKS_SLIDER_H -#define TASKS_SLIDER_H - -#ifdef WUSS_APP - -#include "framebuf/bmfont.h" -#include "framebuf/colour.h" -#include "wuss/icon.h" -#include "wuss/task.h" -#include "wuss/window.h" - -/* demonstrates wuss_ICON_TYPE_SLIDER: a horizontal and a vertical slider, each - * with its own min/max/default, and a label echoing whichever last moved */ -typedef struct slider_task -{ - wuss_window_t *window; - bmfont_t *font; - colour_t label; - wuss_icon_t *horiz; - wuss_icon_t *vert; - wuss_icon_t *state; /* label echoing the last-moved slider's value */ -} -slider_task_t; - -wuss_window_fn_t slider_handle; - -/* create the slider window against the given wuss instance */ -result_t slider_create(wuss_t *wuss, - bmfont_t *font, - slider_task_t *task); - -#endif /* WUSS_APP */ - -#endif /* TASKS_SLIDER_H */ From ebdc739ac70746f97c51f299efea53be26019130 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Tue, 15 Sep 2026 17:45:30 +0100 Subject: [PATCH 090/128] docs(wuss): note slider client-side snapping and pointer drift --- libraries/wuss/TODO.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libraries/wuss/TODO.txt b/libraries/wuss/TODO.txt index 1b4b1e61..b0c981c3 100644 --- a/libraries/wuss/TODO.txt +++ b/libraries/wuss/TODO.txt @@ -13,6 +13,8 @@ Fast scrolling can invalidate areas and fail to copy the expected known good scr -- Tried and tried to get Claude to fix but no good. Too much slider boilerplate -- need to make label+slider+value a standard component. +It also has the client do the rounding/snapping which isn't ideal. +Also saw some pointer drift? LAYER 0 - BUGS (fix before building on top) From d7dd949d91d133a3f109a7d1defb36dcf34dd91a Mon Sep 17 00:00:00 2001 From: David Thomas Date: Tue, 15 Sep 2026 17:52:08 +0100 Subject: [PATCH 091/128] refactor(wuss): rename saturn Size dialogue to Configuration Iters label renamed to Stars for clarity now Ring/Body sliders sit alongside it. --- libraries/wuss/test/tasks/saturn.c | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/libraries/wuss/test/tasks/saturn.c b/libraries/wuss/test/tasks/saturn.c index 42ce88cc..e3da5357 100644 --- a/libraries/wuss/test/tasks/saturn.c +++ b/libraries/wuss/test/tasks/saturn.c @@ -75,7 +75,7 @@ static int saturn_rnd(int n) /* MENU click over the content pops this. "Colours" leads to a submenu with * one row per task->fg/task->bg, each of which pops a wuss_colourmenu (see * saturn_create -- the two leaf items' submenu pointers are patched in there, - * once the colourmenus exist). "Size" hover-opens task->size_dialogue, a + * once the colourmenus exist). "Configuration" hover-opens task->size_dialogue, a * borrowed window built once in saturn_create and patched into * g_saturn_menu_items[SATURN_MENU_SIZE].window there, the same way. */ enum { SATURN_MENU_COLOURS = 0, SATURN_MENU_SIZE }; @@ -119,7 +119,7 @@ static wuss_menu_t g_saturn_colours_menu = static wuss_menu_item_t g_saturn_menu_items[] = { { "Colours", wuss_MENU_ITEM_NONE, &g_saturn_colours_menu, NULL, 0 }, - { "Size", wuss_MENU_ITEM_BORROWED_SUBMENU, NULL, NULL, 0 } + { "Configuration", wuss_MENU_ITEM_BORROWED_SUBMENU, NULL, NULL, 0 } }; static wuss_menu_t g_saturn_menu = @@ -171,6 +171,8 @@ result_t saturn_create(wuss_t *wuss, } task->delegate = delegate; /* the task the menu opens against */ wuss_task_set_autoclose(delegate, 1); + + // DPT FIXME: why have multiple colour menus when only one can be visible at a time? rc = wuss_colourmenu_create(&task->fg_colourmenu, wuss, "Foreground"); if (rc != result_OK) @@ -378,7 +380,7 @@ enum * minimum window size can share them with the table below instead of * repeating the numbers as bare literals. */ #define ST_LABEL_W (5*6) /* enough for "Iters" */ -#define ST_LABEL2_W (4*6) /* enough for "100%" ? */ +#define ST_LABEL2_W (4*6) /* enough for "1280" */ #define ST_SLIDER_MIN_W 64 #define ST_CANCEL_W 48 #define ST_APPLY_W 56 @@ -444,7 +446,7 @@ static result_t saturn_size_dialogue_create(saturn_task_t *task) rc = wuss_window_create_placed(task->delegate, sz, - "Size", + "Configuration", wuss_WINDOW_TITLEBAR | wuss_WINDOW_OUTLINE | wuss_WINDOW_HIDDEN | @@ -468,7 +470,7 @@ static result_t saturn_size_dialogue_create(saturn_task_t *task) wuss_icon_spec_slider(&specs[SATURN_SIZE_ICON_SLIDER1], boxes[ST_SLDR1], wuss_COLOUR_BLACK, wuss_SLIDER_HORIZONTAL, SATURN_SIZE_MIN, SATURN_SIZE_MAX, value); wuss_icon_spec_label(&specs[SATURN_SIZE_ICON_VALUE1], boxes[ST_VAL1], buf, wuss_COLOUR_BLACK, 0); /* buf copied by wuss_icon_create_array */ - wuss_icon_spec_label(&specs[SATURN_SIZE_ICON_LABEL2], boxes[ST_LABL2], "Iters", wuss_COLOUR_BLACK, 1); + wuss_icon_spec_label(&specs[SATURN_SIZE_ICON_LABEL2], boxes[ST_LABL2], "Stars", wuss_COLOUR_BLACK, 1); wuss_icon_spec_slider(&specs[SATURN_SIZE_ICON_SLIDER2], boxes[ST_SLDR2], wuss_COLOUR_BLACK, wuss_SLIDER_HORIZONTAL, 10, 1000, value); wuss_icon_spec_label(&specs[SATURN_SIZE_ICON_VALUE2], boxes[ST_VAL2], buf, wuss_COLOUR_BLACK, 0); /* buf copied by wuss_icon_create_array */ @@ -692,7 +694,7 @@ static int saturn_menu_select_apply(const wuss_colourmenu_t *colourmenu, } /* A pick from either colour submenu, resolved against whichever colourmenu - * it came from. "Size" is a wuss_menu_item_t::window leaf, not a leaf pick, + * it came from. "Configuration" is a wuss_menu_item_t::window leaf, not a leaf pick, * so it never reaches here -- see saturn_size_dialogue_icon and * saturn_sizedlg_pre_show. */ static result_t saturn_menu_select(saturn_task_t *task, From ad06443b5df2acc923524230c0b8443c185e1853 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Tue, 15 Sep 2026 18:29:09 +0100 Subject: [PATCH 092/128] refactor(wuss): introduce slider-row abstraction, use in saturn size dialogue saturn.c's size dialogue had four near-identical blocks of manual slider/label spec-fill, value-echo formatting and drag-event handling. Add wuss_slider_row_t plus wuss_icon_spec_slider_row/wuss_slider_row_bind/ wuss_slider_row_set/wuss_slider_row_event to icon-spec.h/.c, bundling a slider with its value label and centralising the printf-style formatting. saturn.c now loops over a small row-descriptor table and a saturn_task_t::size_rows[SATURN_SIZEDLG_NROWS] array instead of eight named icon fields and four duplicated code blocks. --- include/wuss/icon-spec.h | 105 ++++++++++++++ libraries/wuss/helpers/icon-spec.c | 68 +++++++++ libraries/wuss/test/tasks/saturn.c | 213 ++++++++++++----------------- libraries/wuss/test/tasks/saturn.h | 20 +-- 4 files changed, 273 insertions(+), 133 deletions(-) diff --git a/include/wuss/icon-spec.h b/include/wuss/icon-spec.h index 51418dde..09df00a0 100644 --- a/include/wuss/icon-spec.h +++ b/include/wuss/icon-spec.h @@ -21,14 +21,119 @@ extern "C" { #endif +#include "base/result.h" #include "geom/box.h" #include "wuss/icon.h" +#include "wuss/window.h" #ifdef WUSS_ICONS /* ----------------------------------------------------------------------- */ +/** + * A slider paired with a label that echoes its current value, the recurring + * "Label: [slider] value" row seen in dialogues across the test tasks (see + * saturn.c's size dialogue and icons.c's Sliders group). The value label's + * text is formatted by \c fmt (a single printf-style "%d" conversion; NULL + * defaults to "%d") every time \ref wuss_slider_row_set or \ref + * wuss_slider_row_event changes the value, so callers never format it by + * hand. + */ +typedef struct wuss_slider_row +{ + /** The slider icon. Not owned; destroyed with its window as normal. */ + wuss_icon_t *slider; + /** The label icon echoing the slider's value. Not owned. */ + wuss_icon_t *value; + /** printf-style format for the value label, one "%d" conversion. + * Borrowed; must outlive the row (a string literal in practice). NULL + * means "%d". */ + const char *fmt; +} +wuss_slider_row_t; + +/** + * Fill \p slider_spec and \p value_spec as a wuss_ICON_TYPE_SLIDER and its + * paired wuss_ICON_TYPE_LABEL value echo, ready for \ref + * wuss_icon_create_array alongside the row's own text label (built + * separately with \ref wuss_icon_spec_label). Call \ref wuss_slider_row_bind + * on the two created icons afterwards to get a usable \ref + * wuss_slider_row_t. + * + * \param[out] slider_spec Spec to fill as the slider; overwritten. + * \param[out] value_spec Spec to fill as the value label; overwritten. + * \param[in] slider_bbox Slider's bounding box. + * \param[in] value_bbox Value label's bounding box. + * \param[in] fg Groove/fill and text colour. + * \param[in] orientation Groove direction. + * \param[in] min Value at the groove's start. + * \param[in] max Value at the groove's end. + * \param[in] default_value Initial value, clamped to [min,max]. + * \param[in] fmt printf-style format for the value label, one + * "%d" conversion; NULL for "%d". Borrowed; must + * outlive the row. + */ +void wuss_icon_spec_slider_row(wuss_icon_spec_t *slider_spec, + wuss_icon_spec_t *value_spec, + box_t slider_bbox, + box_t value_bbox, + wuss_colour_t fg, + wuss_slider_orientation_t orientation, + int min, + int max, + int default_value, + const char *fmt); + +/** + * Bind a \ref wuss_slider_row_t to the icons \ref wuss_icon_create_array + * made from a pair of specs filled by \ref wuss_icon_spec_slider_row. + * + * \param[out] row Row to fill. + * \param[in] slider The created slider icon. + * \param[in] value The created value-label icon. + * \param[in] fmt Same format string passed to \ref + * wuss_icon_spec_slider_row; borrowed, must outlive the + * row. NULL for "%d". + */ +void wuss_slider_row_bind(wuss_slider_row_t *row, + wuss_icon_t *slider, + wuss_icon_t *value, + const char *fmt); + +/** + * Set a slider row's value programmatically -- the slider fill and its + * formatted value label both update, invalidating each so the next redraw + * repaints them. No task event is delivered, matching \ref + * wuss_icon_set_value. + * + * \param[in] window Window the row's icons belong to. + * \param[in] row Row to change. + * \param[in] value New value; clamped to the slider's [min,max] range. + * \return \ref result_OK, or \ref result_OOM from formatting the label. + */ +result_t wuss_slider_row_set(wuss_window_t *window, + const wuss_slider_row_t *row, + int value); + +/** + * Handle a wuss_EVENT_ICON delivered to the row's window: if it names this + * row's slider and is a drag (MOUSE_DOWN or MOUSE_MOVE), reformats the value + * label to match and reports the new value. Any other icon or action is left + * untouched. + * + * \param[in] window Window the row's icons belong to. + * \param[in] row Row to check against. + * \param[in] event The wuss_EVENT_ICON event. + * \param[out] value Set to the new value when handled. May be NULL. + * \return Non-zero if \p event was a drag on this row's slider (and so was + * handled), zero otherwise. + */ +int wuss_slider_row_event(wuss_window_t *window, + const wuss_slider_row_t *row, + const wuss_event_t *event, + int *value); + /** * Fill \p spec as a wuss_ICON_TYPE_LABEL. * diff --git a/libraries/wuss/helpers/icon-spec.c b/libraries/wuss/helpers/icon-spec.c index 20080a5d..1d5809bf 100644 --- a/libraries/wuss/helpers/icon-spec.c +++ b/libraries/wuss/helpers/icon-spec.c @@ -1,5 +1,6 @@ /* wuss/helpers/icon-spec.c -- fill helpers for common wuss_icon_spec_t shapes */ +#include #include #ifdef FORTIFY @@ -10,6 +11,9 @@ #ifdef WUSS_ICONS +/* value label buffer: ample for any int plus a short unit suffix in fmt */ +#define SLIDER_ROW_BUF 32 + /* ----------------------------------------------------------------------- */ void wuss_icon_spec_label(wuss_icon_spec_t *spec, @@ -63,4 +67,68 @@ void wuss_icon_spec_action(wuss_icon_spec_t *spec, spec->flags = is_default ? wuss_ICON_FLAGS_DEFAULT : wuss_ICON_FLAGS_NONE; } +void wuss_icon_spec_slider_row(wuss_icon_spec_t *slider_spec, + wuss_icon_spec_t *value_spec, + box_t slider_bbox, + box_t value_bbox, + wuss_colour_t fg, + wuss_slider_orientation_t orientation, + int min, + int max, + int default_value, + const char *fmt) +{ + char buf[SLIDER_ROW_BUF]; + + wuss_icon_spec_slider(slider_spec, slider_bbox, fg, orientation, min, max, + default_value); + snprintf(buf, sizeof(buf), fmt ? fmt : "%d", default_value); + wuss_icon_spec_label(value_spec, value_bbox, buf, fg, 0); +} + +void wuss_slider_row_bind(wuss_slider_row_t *row, + wuss_icon_t *slider, + wuss_icon_t *value, + const char *fmt) +{ + row->slider = slider; + row->value = value; + row->fmt = fmt; +} + +result_t wuss_slider_row_set(wuss_window_t *window, + const wuss_slider_row_t *row, + int value) +{ + char buf[SLIDER_ROW_BUF]; + + wuss_icon_set_value(window, row->slider, value); + snprintf(buf, sizeof(buf), row->fmt ? row->fmt : "%d", + wuss_icon_get_value(row->slider)); + return wuss_icon_set_text(window, row->value, buf); +} + +int wuss_slider_row_event(wuss_window_t *window, + const wuss_slider_row_t *row, + const wuss_event_t *event, + int *value) +{ + char buf[SLIDER_ROW_BUF]; + int v; + + if (event->kind != wuss_EVENT_ICON || event->data.icon.icon != row->slider) + return 0; + if (event->data.icon.action != wuss_MOUSE_DOWN && + event->data.icon.action != wuss_MOUSE_MOVE) + return 0; + + v = event->data.icon.value; + snprintf(buf, sizeof(buf), row->fmt ? row->fmt : "%d", v); + wuss_icon_set_text(window, row->value, buf); + + if (value != NULL) + *value = v; + return 1; +} + #endif /* WUSS_ICONS */ diff --git a/libraries/wuss/test/tasks/saturn.c b/libraries/wuss/test/tasks/saturn.c index e3da5357..17b6ba85 100644 --- a/libraries/wuss/test/tasks/saturn.c +++ b/libraries/wuss/test/tasks/saturn.c @@ -3,6 +3,7 @@ #ifdef WUSS_APP #include +#include #include #include #include @@ -81,7 +82,8 @@ static int saturn_rnd(int n) enum { SATURN_MENU_COLOURS = 0, SATURN_MENU_SIZE }; enum { SATURN_COLOURS_MENU_FOREGROUND = 0, SATURN_COLOURS_MENU_BACKGROUND }; -/* the size dialogue's icons, in creation order (see saturn_size_dialogue_create) */ +/* the size dialogue's icons, in creation order (see saturn_size_dialogue_create): + * one label/slider/value triple per SATURN_SIZEDLG_ROW_*, then the buttons */ enum { SATURN_SIZE_ICON_LABEL1 = 0, SATURN_SIZE_ICON_SLIDER1, @@ -105,6 +107,33 @@ enum { SATURN_SIZE_NICONS }; +/* per-row static description, indexed by SATURN_SIZEDLG_ROW_*: label text, + * slider [min,max] and the byte offset of the saturn_config_t field the row + * reads/writes (via *(int *) ((char *) &task->config + offset)), shared by + * saturn_size_dialogue_create (to fill specs) and + * saturn_sizedlg_pre_show/saturn_sizedlg_apply (to sync task->config) */ +typedef struct saturn_sizedlg_rowdesc +{ + const char *label; + int min, max; + size_t config_offset; +} +saturn_sizedlg_rowdesc_t; + +static const saturn_sizedlg_rowdesc_t g_saturn_sizedlg_rows[SATURN_SIZEDLG_NROWS] = +{ + [SATURN_SIZEDLG_ROW_SIZE] = { "Size", SATURN_SIZE_MIN, SATURN_SIZE_MAX, offsetof(saturn_config_t, size) }, + [SATURN_SIZEDLG_ROW_STARS] = { "Stars", 10, 1000, offsetof(saturn_config_t, stars_iters) }, + [SATURN_SIZEDLG_ROW_RING] = { "Ring", 10, 4000, offsetof(saturn_config_t, ring_iters) }, + [SATURN_SIZEDLG_ROW_BODY] = { "Body", 10, 4000, offsetof(saturn_config_t, body_iters) }, +}; + +/* task->config field a row reads/writes, per g_saturn_sizedlg_rows */ +static int *saturn_sizedlg_field(saturn_task_t *task, int row) +{ + return (int *) ((char *) &task->config + g_saturn_sizedlg_rows[row].config_offset); +} + static wuss_menu_item_t g_saturn_colours_items[] = { { "Foreground", wuss_MENU_ITEM_BORROWED_SUBMENU, NULL, NULL, 0 }, @@ -148,16 +177,9 @@ result_t saturn_create(wuss_t *wuss, task->bg_colourmenu = NULL; task->menu_handle = NULL; task->size_dialogue = NULL; - task->size_slider = NULL; - task->size_value_label = NULL; - task->size_slider2 = NULL; - task->size_value2_label = NULL; - task->size_slider3 = NULL; - task->size_value3_label = NULL; - task->size_slider4 = NULL; - task->size_value4_label = NULL; - task->size_cancel = NULL; - task->size_apply = NULL; + memset(task->size_rows, 0, sizeof(task->size_rows)); + task->size_cancel = NULL; + task->size_apply = NULL; /* saturn_redraw paints its own background */ delegate_desc.handle = saturn_handle; @@ -427,11 +449,16 @@ static result_t saturn_size_dialogue_create(saturn_task_t *task) wuss_icon_spec_t specs[SATURN_SIZE_NICONS]; wuss_icon_t *made[SATURN_SIZE_NICONS]; box_t boxes[SIZE_STACK__LIMIT]; + static const int label_box[SATURN_SIZEDLG_NROWS] = { ST_LABL1, ST_LABL2, ST_LABL3, ST_LABL4 }; + static const int slider_box[SATURN_SIZEDLG_NROWS] = { ST_SLDR1, ST_SLDR2, ST_SLDR3, ST_SLDR4 }; + static const int value_box[SATURN_SIZEDLG_NROWS] = { ST_VAL1, ST_VAL2, ST_VAL3, ST_VAL4 }; + static const int label_icon[SATURN_SIZEDLG_NROWS] = { SATURN_SIZE_ICON_LABEL1, SATURN_SIZE_ICON_LABEL2, SATURN_SIZE_ICON_LABEL3, SATURN_SIZE_ICON_LABEL4 }; + static const int slider_icon[SATURN_SIZEDLG_NROWS] = { SATURN_SIZE_ICON_SLIDER1, SATURN_SIZE_ICON_SLIDER2, SATURN_SIZE_ICON_SLIDER3, SATURN_SIZE_ICON_SLIDER4 }; + static const int value_icon[SATURN_SIZEDLG_NROWS] = { SATURN_SIZE_ICON_VALUE1, SATURN_SIZE_ICON_VALUE2, SATURN_SIZE_ICON_VALUE3, SATURN_SIZE_ICON_VALUE4 }; box_t root; - char buf[16]; result_t rc; int row_w, btns_w; - int value; + int value, row; size2d_t sz; /* stack_solve distributes into a box it's given; it can't report a @@ -463,28 +490,21 @@ static result_t saturn_size_dialogue_create(saturn_task_t *task) if (rc != result_OK) goto exit; - value = saturn_size_snap(task->config.size); - snprintf(buf, sizeof(buf), "%d", value); - - wuss_icon_spec_label(&specs[SATURN_SIZE_ICON_LABEL1], boxes[ST_LABL1], "Size", wuss_COLOUR_BLACK, 1); - wuss_icon_spec_slider(&specs[SATURN_SIZE_ICON_SLIDER1], boxes[ST_SLDR1], wuss_COLOUR_BLACK, wuss_SLIDER_HORIZONTAL, SATURN_SIZE_MIN, SATURN_SIZE_MAX, value); - wuss_icon_spec_label(&specs[SATURN_SIZE_ICON_VALUE1], boxes[ST_VAL1], buf, wuss_COLOUR_BLACK, 0); /* buf copied by wuss_icon_create_array */ - - wuss_icon_spec_label(&specs[SATURN_SIZE_ICON_LABEL2], boxes[ST_LABL2], "Stars", wuss_COLOUR_BLACK, 1); - wuss_icon_spec_slider(&specs[SATURN_SIZE_ICON_SLIDER2], boxes[ST_SLDR2], wuss_COLOUR_BLACK, wuss_SLIDER_HORIZONTAL, 10, 1000, value); - wuss_icon_spec_label(&specs[SATURN_SIZE_ICON_VALUE2], boxes[ST_VAL2], buf, wuss_COLOUR_BLACK, 0); /* buf copied by wuss_icon_create_array */ - - value = task->config.ring_iters; - snprintf(buf, sizeof(buf), "%d", value); - wuss_icon_spec_label(&specs[SATURN_SIZE_ICON_LABEL3], boxes[ST_LABL3], "Ring", wuss_COLOUR_BLACK, 1); - wuss_icon_spec_slider(&specs[SATURN_SIZE_ICON_SLIDER3], boxes[ST_SLDR3], wuss_COLOUR_BLACK, wuss_SLIDER_HORIZONTAL, 10, 4000, value); - wuss_icon_spec_label(&specs[SATURN_SIZE_ICON_VALUE3], boxes[ST_VAL3], buf, wuss_COLOUR_BLACK, 0); /* buf copied by wuss_icon_create_array */ - - value = task->config.body_iters; - snprintf(buf, sizeof(buf), "%d", value); - wuss_icon_spec_label(&specs[SATURN_SIZE_ICON_LABEL4], boxes[ST_LABL4], "Body", wuss_COLOUR_BLACK, 1); - wuss_icon_spec_slider(&specs[SATURN_SIZE_ICON_SLIDER4], boxes[ST_SLDR4], wuss_COLOUR_BLACK, wuss_SLIDER_HORIZONTAL, 10, 4000, value); - wuss_icon_spec_label(&specs[SATURN_SIZE_ICON_VALUE4], boxes[ST_VAL4], buf, wuss_COLOUR_BLACK, 0); /* buf copied by wuss_icon_create_array */ + for (row = 0; row < SATURN_SIZEDLG_NROWS; row++) + { + const saturn_sizedlg_rowdesc_t *desc = &g_saturn_sizedlg_rows[row]; + + value = (row == SATURN_SIZEDLG_ROW_SIZE) ? + saturn_size_snap(task->config.size) : + *saturn_sizedlg_field(task, row); + + wuss_icon_spec_label(&specs[label_icon[row]], boxes[label_box[row]], + desc->label, wuss_COLOUR_BLACK, 1); + wuss_icon_spec_slider_row(&specs[slider_icon[row]], &specs[value_icon[row]], + boxes[slider_box[row]], boxes[value_box[row]], + wuss_COLOUR_BLACK, wuss_SLIDER_HORIZONTAL, + desc->min, desc->max, value, NULL); + } wuss_icon_spec_action(&specs[SATURN_SIZE_ICON_CANCEL], boxes[ST_CNCL], "Cancel", wuss_COLOUR_BLACK, wuss_COLOUR_WINDOW, 0); wuss_icon_spec_action(&specs[SATURN_SIZE_ICON_APPLY], boxes[ST_APLY], "Apply", wuss_COLOUR_BLACK, wuss_COLOUR_WINDOW, 1); @@ -494,16 +514,11 @@ static result_t saturn_size_dialogue_create(saturn_task_t *task) if (rc != result_OK) goto exit; - task->size_slider = made[SATURN_SIZE_ICON_SLIDER1]; - task->size_value_label = made[SATURN_SIZE_ICON_VALUE1]; - task->size_slider2 = made[SATURN_SIZE_ICON_SLIDER2]; - task->size_value2_label = made[SATURN_SIZE_ICON_VALUE2]; - task->size_slider3 = made[SATURN_SIZE_ICON_SLIDER3]; - task->size_value3_label = made[SATURN_SIZE_ICON_VALUE3]; - task->size_slider4 = made[SATURN_SIZE_ICON_SLIDER4]; - task->size_value4_label = made[SATURN_SIZE_ICON_VALUE4]; - task->size_cancel = made[SATURN_SIZE_ICON_CANCEL]; - task->size_apply = made[SATURN_SIZE_ICON_APPLY]; + for (row = 0; row < SATURN_SIZEDLG_NROWS; row++) + wuss_slider_row_bind(&task->size_rows[row], made[slider_icon[row]], + made[value_icon[row]], NULL); + task->size_cancel = made[SATURN_SIZE_ICON_CANCEL]; + task->size_apply = made[SATURN_SIZE_ICON_APPLY]; return result_OK; @@ -522,28 +537,16 @@ static result_t saturn_size_dialogue_create(saturn_task_t *task) static result_t saturn_sizedlg_pre_show(saturn_task_t *task) { result_t rc; - int value; - char buf[16]; + int row, value; - value = saturn_size_snap(task->config.size); - wuss_icon_set_value(task->size_dialogue, task->size_slider, value); - snprintf(buf, sizeof(buf), "%d", value); - rc = wuss_icon_set_text(task->size_dialogue, task->size_value_label, buf); - - value = task->config.stars_iters; - wuss_icon_set_value(task->size_dialogue, task->size_slider2, value); - snprintf(buf, sizeof(buf), "%d", value); - rc = wuss_icon_set_text(task->size_dialogue, task->size_value2_label, buf); - - value = task->config.ring_iters; - wuss_icon_set_value(task->size_dialogue, task->size_slider3, value); - snprintf(buf, sizeof(buf), "%d", value); - rc = wuss_icon_set_text(task->size_dialogue, task->size_value3_label, buf); - - value = task->config.body_iters; - wuss_icon_set_value(task->size_dialogue, task->size_slider4, value); - snprintf(buf, sizeof(buf), "%d", value); - rc = wuss_icon_set_text(task->size_dialogue, task->size_value4_label, buf); + rc = result_OK; + for (row = 0; row < SATURN_SIZEDLG_NROWS; row++) + { + value = (row == SATURN_SIZEDLG_ROW_SIZE) ? + saturn_size_snap(task->config.size) : + *saturn_sizedlg_field(task, row); + rc = wuss_slider_row_set(task->size_dialogue, &task->size_rows[row], value); + } return rc; } @@ -553,17 +556,13 @@ static result_t saturn_sizedlg_pre_show(saturn_task_t *task) static result_t saturn_sizedlg_apply(saturn_task_t *task) { result_t rc; - int size_value; - int iters_value; - - size_value = wuss_icon_get_value(task->size_slider); - task->config.size = size_value; - iters_value = wuss_icon_get_value(task->size_slider2); - task->config.stars_iters = iters_value; - iters_value = wuss_icon_get_value(task->size_slider3); - task->config.ring_iters = iters_value; - iters_value = wuss_icon_get_value(task->size_slider4); - task->config.body_iters = iters_value; + int row, size_value; + + for (row = 0; row < SATURN_SIZEDLG_NROWS; row++) + *saturn_sizedlg_field(task, row) = + wuss_icon_get_value(task->size_rows[row].slider); + + size_value = task->config.size; rc = wuss_window_resize(task->window, SIZE2D(size_value, size_value)); if (rc != result_OK) return rc; @@ -584,61 +583,25 @@ static result_t saturn_size_dialogue_icon(saturn_task_t *task, const wuss_event_t *event) { wuss_icon_t *icon; - int value; - char buf[16]; + int row, value; icon = event->data.icon.icon; // FIXME DPT: mouse move events arrive even when not dragging the bar... - if (icon == task->size_slider) - { - if (event->data.icon.action != wuss_MOUSE_DOWN && - event->data.icon.action != wuss_MOUSE_MOVE) - return result_OK; - - value = saturn_size_snap(event->data.icon.value); - wuss_icon_set_value(task->size_dialogue, task->size_slider, value); /* set again once snapped */ - snprintf(buf, sizeof(buf), "%d", value); - return wuss_icon_set_text(task->size_dialogue, task->size_value_label, - buf); - } - - if (icon == task->size_slider2) + /* row 0 (Size) additionally snaps to SATURN_SIZE_STEP; the others take the + * dragged value as-is. wuss_slider_row_event already reformats the value + * label -- row 0 just re-derives and re-sets the snapped value on top. */ + for (row = 0; row < SATURN_SIZEDLG_NROWS; row++) { - if (event->data.icon.action != wuss_MOUSE_DOWN && - event->data.icon.action != wuss_MOUSE_MOVE) - return result_OK; - - value = event->data.icon.value; - // not snapping -- wuss_icon_set_value(task->size_dialogue, task->size_slider2, value); - snprintf(buf, sizeof(buf), "%d", value); - return wuss_icon_set_text(task->size_dialogue, task->size_value2_label, - buf); - } + if (!wuss_slider_row_event(task->size_dialogue, &task->size_rows[row], + event, &value)) + continue; - if (icon == task->size_slider3) - { - if (event->data.icon.action != wuss_MOUSE_DOWN && - event->data.icon.action != wuss_MOUSE_MOVE) - return result_OK; - - value = event->data.icon.value; - snprintf(buf, sizeof(buf), "%d", value); - return wuss_icon_set_text(task->size_dialogue, task->size_value3_label, - buf); - } - - if (icon == task->size_slider4) - { - if (event->data.icon.action != wuss_MOUSE_DOWN && - event->data.icon.action != wuss_MOUSE_MOVE) - return result_OK; - - value = event->data.icon.value; - snprintf(buf, sizeof(buf), "%d", value); - return wuss_icon_set_text(task->size_dialogue, task->size_value4_label, - buf); + if (row == SATURN_SIZEDLG_ROW_SIZE) + return wuss_slider_row_set(task->size_dialogue, &task->size_rows[row], + saturn_size_snap(value)); + return result_OK; } if (event->data.icon.action != wuss_MOUSE_UP) diff --git a/libraries/wuss/test/tasks/saturn.h b/libraries/wuss/test/tasks/saturn.h index b3d6efbd..5599e08b 100644 --- a/libraries/wuss/test/tasks/saturn.h +++ b/libraries/wuss/test/tasks/saturn.h @@ -41,6 +41,17 @@ saturn_config_t; #define SATURN_SIZE_MAX 512 #define SATURN_SIZE_STEP 64 +/* the size dialogue's slider rows, in creation/layout order */ +enum +{ + SATURN_SIZEDLG_ROW_SIZE = 0, + SATURN_SIZEDLG_ROW_STARS, + SATURN_SIZEDLG_ROW_RING, + SATURN_SIZEDLG_ROW_BODY, + + SATURN_SIZEDLG_NROWS +}; + typedef struct saturn_task { wuss_t *wuss; /* for wuss_get_pointer/wuss_get_palette */ @@ -52,14 +63,7 @@ typedef struct saturn_task wuss_colourmenu_t *fg_colourmenu, *bg_colourmenu; wuss_menu_handle_t menu_handle; /* live only between open and a SELECT pick */ wuss_window_t *size_dialogue; /* built once, hidden; a menu leaf */ - wuss_icon_t *size_slider; - wuss_icon_t *size_value_label; - wuss_icon_t *size_slider2; - wuss_icon_t *size_value2_label; - wuss_icon_t *size_slider3; - wuss_icon_t *size_value3_label; - wuss_icon_t *size_slider4; - wuss_icon_t *size_value4_label; + wuss_slider_row_t size_rows[SATURN_SIZEDLG_NROWS]; /* size, stars, ring, body */ wuss_icon_t *size_cancel; wuss_icon_t *size_apply; } From 7127d869722e03f9956bc6b7ef164ec79701656d Mon Sep 17 00:00:00 2001 From: David Thomas Date: Tue, 15 Sep 2026 19:06:52 +0100 Subject: [PATCH 093/128] refactor(wuss): centralise slider-row snapping via a step field wuss_slider_row_t gains min/max/step; wuss_icon_spec_slider_row and wuss_slider_row_bind take a step argument, and wuss_slider_row_set/ wuss_slider_row_event snap to it internally (wuss__slider_row_snap in icon-spec.c). This removes saturn_size_snap and its special-casing of the Size row from saturn.c entirely -- the row-descriptor table now just carries SATURN_SIZE_STEP (0 for the other rows). --- include/wuss/icon-spec.h | 40 ++++++++++++++----- libraries/wuss/helpers/icon-spec.c | 29 +++++++++++++- libraries/wuss/test/tasks/saturn.c | 64 +++++++++++------------------- 3 files changed, 81 insertions(+), 52 deletions(-) diff --git a/include/wuss/icon-spec.h b/include/wuss/icon-spec.h index 09df00a0..a4390e56 100644 --- a/include/wuss/icon-spec.h +++ b/include/wuss/icon-spec.h @@ -38,7 +38,9 @@ extern "C" * text is formatted by \c fmt (a single printf-style "%d" conversion; NULL * defaults to "%d") every time \ref wuss_slider_row_set or \ref * wuss_slider_row_event changes the value, so callers never format it by - * hand. + * hand. If \c step is non-zero, both functions also snap the value to the + * nearest multiple of \c step above \c min, so callers never hand-roll that + * either. */ typedef struct wuss_slider_row { @@ -50,6 +52,12 @@ typedef struct wuss_slider_row * Borrowed; must outlive the row (a string literal in practice). NULL * means "%d". */ const char *fmt; + /** Value at the slider's groove start and end; snapping rounds to a + * multiple of \c step away from \c min and clamps to [min,max]. Match + * the \c min/\c max passed to \ref wuss_icon_spec_slider_row. */ + int min, max; + /** Snapping step; 0 means "no snapping" (any value is kept as-is). */ + int step; } wuss_slider_row_t; @@ -69,10 +77,13 @@ wuss_slider_row_t; * \param[in] orientation Groove direction. * \param[in] min Value at the groove's start. * \param[in] max Value at the groove's end. - * \param[in] default_value Initial value, clamped to [min,max]. + * \param[in] default_value Initial value, clamped to [min,max] and (if \p + * step is non-zero) snapped to a multiple of it. * \param[in] fmt printf-style format for the value label, one * "%d" conversion; NULL for "%d". Borrowed; must * outlive the row. + * \param[in] step Snapping step passed through to \ref + * wuss_slider_row_bind; 0 for none. */ void wuss_icon_spec_slider_row(wuss_icon_spec_t *slider_spec, wuss_icon_spec_t *value_spec, @@ -83,7 +94,8 @@ void wuss_icon_spec_slider_row(wuss_icon_spec_t *slider_spec, int min, int max, int default_value, - const char *fmt); + const char *fmt, + int step); /** * Bind a \ref wuss_slider_row_t to the icons \ref wuss_icon_create_array @@ -95,11 +107,18 @@ void wuss_icon_spec_slider_row(wuss_icon_spec_t *slider_spec, * \param[in] fmt Same format string passed to \ref * wuss_icon_spec_slider_row; borrowed, must outlive the * row. NULL for "%d". + * \param[in] min Same \c min passed to \ref wuss_icon_spec_slider_row. + * \param[in] max Same \c max passed to \ref wuss_icon_spec_slider_row. + * \param[in] step Same \c step passed to \ref wuss_icon_spec_slider_row; + * 0 for no snapping. */ void wuss_slider_row_bind(wuss_slider_row_t *row, wuss_icon_t *slider, wuss_icon_t *value, - const char *fmt); + const char *fmt, + int min, + int max, + int step); /** * Set a slider row's value programmatically -- the slider fill and its @@ -109,7 +128,8 @@ void wuss_slider_row_bind(wuss_slider_row_t *row, * * \param[in] window Window the row's icons belong to. * \param[in] row Row to change. - * \param[in] value New value; clamped to the slider's [min,max] range. + * \param[in] value New value; snapped to \c row->step (if non-zero) and + * clamped to [row->min,row->max]. * \return \ref result_OK, or \ref result_OOM from formatting the label. */ result_t wuss_slider_row_set(wuss_window_t *window, @@ -118,14 +138,16 @@ result_t wuss_slider_row_set(wuss_window_t *window, /** * Handle a wuss_EVENT_ICON delivered to the row's window: if it names this - * row's slider and is a drag (MOUSE_DOWN or MOUSE_MOVE), reformats the value - * label to match and reports the new value. Any other icon or action is left - * untouched. + * row's slider and is a drag (MOUSE_DOWN or MOUSE_MOVE), snaps the value to + * \c row->step (if non-zero, re-setting the slider itself so it visibly + * jumps to the snapped position), reformats the value label to match, and + * reports the new value. Any other icon or action is left untouched. * * \param[in] window Window the row's icons belong to. * \param[in] row Row to check against. * \param[in] event The wuss_EVENT_ICON event. - * \param[out] value Set to the new value when handled. May be NULL. + * \param[out] value Set to the new (already-snapped) value when handled. + * May be NULL. * \return Non-zero if \p event was a drag on this row's slider (and so was * handled), zero otherwise. */ diff --git a/libraries/wuss/helpers/icon-spec.c b/libraries/wuss/helpers/icon-spec.c index 1d5809bf..08a0590b 100644 --- a/libraries/wuss/helpers/icon-spec.c +++ b/libraries/wuss/helpers/icon-spec.c @@ -7,6 +7,7 @@ #include "fortify/fortify.h" #endif +#include "base/utils.h" #include "wuss/icon-spec.h" #ifdef WUSS_ICONS @@ -14,6 +15,14 @@ /* value label buffer: ample for any int plus a short unit suffix in fmt */ #define SLIDER_ROW_BUF 32 +/* nearest multiple of step above min, clamped to [min,max] */ +static int wuss__slider_row_snap(int v, int min, int max, int step) +{ + v = ((v - min + step / 2) / step) * step + min; + + return CLAMP(v, min, max); +} + /* ----------------------------------------------------------------------- */ void wuss_icon_spec_label(wuss_icon_spec_t *spec, @@ -76,10 +85,13 @@ void wuss_icon_spec_slider_row(wuss_icon_spec_t *slider_spec, int min, int max, int default_value, - const char *fmt) + const char *fmt, + int step) { char buf[SLIDER_ROW_BUF]; + if (step != 0) + default_value = wuss__slider_row_snap(default_value, min, max, step); wuss_icon_spec_slider(slider_spec, slider_bbox, fg, orientation, min, max, default_value); snprintf(buf, sizeof(buf), fmt ? fmt : "%d", default_value); @@ -89,11 +101,17 @@ void wuss_icon_spec_slider_row(wuss_icon_spec_t *slider_spec, void wuss_slider_row_bind(wuss_slider_row_t *row, wuss_icon_t *slider, wuss_icon_t *value, - const char *fmt) + const char *fmt, + int min, + int max, + int step) { row->slider = slider; row->value = value; row->fmt = fmt; + row->min = min; + row->max = max; + row->step = step; } result_t wuss_slider_row_set(wuss_window_t *window, @@ -102,6 +120,8 @@ result_t wuss_slider_row_set(wuss_window_t *window, { char buf[SLIDER_ROW_BUF]; + if (row->step != 0) + value = wuss__slider_row_snap(value, row->min, row->max, row->step); wuss_icon_set_value(window, row->slider, value); snprintf(buf, sizeof(buf), row->fmt ? row->fmt : "%d", wuss_icon_get_value(row->slider)); @@ -123,6 +143,11 @@ int wuss_slider_row_event(wuss_window_t *window, return 0; v = event->data.icon.value; + if (row->step != 0) + { + v = wuss__slider_row_snap(v, row->min, row->max, row->step); + wuss_icon_set_value(window, row->slider, v); /* visibly jump to it */ + } snprintf(buf, sizeof(buf), row->fmt ? row->fmt : "%d", v); wuss_icon_set_text(window, row->value, buf); diff --git a/libraries/wuss/test/tasks/saturn.c b/libraries/wuss/test/tasks/saturn.c index 17b6ba85..2655f614 100644 --- a/libraries/wuss/test/tasks/saturn.c +++ b/libraries/wuss/test/tasks/saturn.c @@ -115,17 +115,17 @@ enum { typedef struct saturn_sizedlg_rowdesc { const char *label; - int min, max; + int min, max, step; size_t config_offset; } saturn_sizedlg_rowdesc_t; static const saturn_sizedlg_rowdesc_t g_saturn_sizedlg_rows[SATURN_SIZEDLG_NROWS] = { - [SATURN_SIZEDLG_ROW_SIZE] = { "Size", SATURN_SIZE_MIN, SATURN_SIZE_MAX, offsetof(saturn_config_t, size) }, - [SATURN_SIZEDLG_ROW_STARS] = { "Stars", 10, 1000, offsetof(saturn_config_t, stars_iters) }, - [SATURN_SIZEDLG_ROW_RING] = { "Ring", 10, 4000, offsetof(saturn_config_t, ring_iters) }, - [SATURN_SIZEDLG_ROW_BODY] = { "Body", 10, 4000, offsetof(saturn_config_t, body_iters) }, + [SATURN_SIZEDLG_ROW_SIZE] = { "Size", SATURN_SIZE_MIN, SATURN_SIZE_MAX, SATURN_SIZE_STEP, offsetof(saturn_config_t, size) }, + [SATURN_SIZEDLG_ROW_STARS] = { "Stars", 1, 9999, 0, offsetof(saturn_config_t, stars_iters) }, + [SATURN_SIZEDLG_ROW_RING] = { "Ring", 1, 9999, 0, offsetof(saturn_config_t, ring_iters) }, + [SATURN_SIZEDLG_ROW_BODY] = { "Body", 1, 9999, 0, offsetof(saturn_config_t, body_iters) }, }; /* task->config field a row reads/writes, per g_saturn_sizedlg_rows */ @@ -351,16 +351,6 @@ static result_t saturn_mouse(saturn_task_t *task, return result_OK; } -/* nearest multiple of SATURN_SIZE_STEP to v, clamped to - * [SATURN_SIZE_MIN,SATURN_SIZE_MAX] */ -static int saturn_size_snap(int v) -{ - v = ((v - SATURN_SIZE_MIN + SATURN_SIZE_STEP / 2) / SATURN_SIZE_STEP) * - SATURN_SIZE_STEP + SATURN_SIZE_MIN; - - return CLAMP(v, SATURN_SIZE_MIN, SATURN_SIZE_MAX); -} - /* stack items for the size dialogue's layout: a VBOX of label / slider / * value-echo / button-row, with fixed-size spacers standing in for the * (non-uniform) gaps between them. BTN_ROW is an HBOX with Cancel and @@ -446,17 +436,18 @@ static const stack_item_t g_saturn_size_stack[SIZE_STACK__LIMIT] = * hidden. */ static result_t saturn_size_dialogue_create(saturn_task_t *task) { - wuss_icon_spec_t specs[SATURN_SIZE_NICONS]; - wuss_icon_t *made[SATURN_SIZE_NICONS]; - box_t boxes[SIZE_STACK__LIMIT]; static const int label_box[SATURN_SIZEDLG_NROWS] = { ST_LABL1, ST_LABL2, ST_LABL3, ST_LABL4 }; static const int slider_box[SATURN_SIZEDLG_NROWS] = { ST_SLDR1, ST_SLDR2, ST_SLDR3, ST_SLDR4 }; static const int value_box[SATURN_SIZEDLG_NROWS] = { ST_VAL1, ST_VAL2, ST_VAL3, ST_VAL4 }; static const int label_icon[SATURN_SIZEDLG_NROWS] = { SATURN_SIZE_ICON_LABEL1, SATURN_SIZE_ICON_LABEL2, SATURN_SIZE_ICON_LABEL3, SATURN_SIZE_ICON_LABEL4 }; static const int slider_icon[SATURN_SIZEDLG_NROWS] = { SATURN_SIZE_ICON_SLIDER1, SATURN_SIZE_ICON_SLIDER2, SATURN_SIZE_ICON_SLIDER3, SATURN_SIZE_ICON_SLIDER4 }; static const int value_icon[SATURN_SIZEDLG_NROWS] = { SATURN_SIZE_ICON_VALUE1, SATURN_SIZE_ICON_VALUE2, SATURN_SIZE_ICON_VALUE3, SATURN_SIZE_ICON_VALUE4 }; - box_t root; + result_t rc; + wuss_icon_spec_t specs[SATURN_SIZE_NICONS]; + wuss_icon_t *made[SATURN_SIZE_NICONS]; + box_t boxes[SIZE_STACK__LIMIT]; + box_t root; int row_w, btns_w; int value, row; size2d_t sz; @@ -494,16 +485,14 @@ static result_t saturn_size_dialogue_create(saturn_task_t *task) { const saturn_sizedlg_rowdesc_t *desc = &g_saturn_sizedlg_rows[row]; - value = (row == SATURN_SIZEDLG_ROW_SIZE) ? - saturn_size_snap(task->config.size) : - *saturn_sizedlg_field(task, row); + value = *saturn_sizedlg_field(task, row); wuss_icon_spec_label(&specs[label_icon[row]], boxes[label_box[row]], desc->label, wuss_COLOUR_BLACK, 1); wuss_icon_spec_slider_row(&specs[slider_icon[row]], &specs[value_icon[row]], boxes[slider_box[row]], boxes[value_box[row]], wuss_COLOUR_BLACK, wuss_SLIDER_HORIZONTAL, - desc->min, desc->max, value, NULL); + desc->min, desc->max, value, NULL, desc->step); } wuss_icon_spec_action(&specs[SATURN_SIZE_ICON_CANCEL], boxes[ST_CNCL], "Cancel", wuss_COLOUR_BLACK, wuss_COLOUR_WINDOW, 0); @@ -516,7 +505,10 @@ static result_t saturn_size_dialogue_create(saturn_task_t *task) for (row = 0; row < SATURN_SIZEDLG_NROWS; row++) wuss_slider_row_bind(&task->size_rows[row], made[slider_icon[row]], - made[value_icon[row]], NULL); + made[value_icon[row]], NULL, + g_saturn_sizedlg_rows[row].min, + g_saturn_sizedlg_rows[row].max, + g_saturn_sizedlg_rows[row].step); task->size_cancel = made[SATURN_SIZE_ICON_CANCEL]; task->size_apply = made[SATURN_SIZE_ICON_APPLY]; @@ -542,9 +534,7 @@ static result_t saturn_sizedlg_pre_show(saturn_task_t *task) rc = result_OK; for (row = 0; row < SATURN_SIZEDLG_NROWS; row++) { - value = (row == SATURN_SIZEDLG_ROW_SIZE) ? - saturn_size_snap(task->config.size) : - *saturn_sizedlg_field(task, row); + value = *saturn_sizedlg_field(task, row); rc = wuss_slider_row_set(task->size_dialogue, &task->size_rows[row], value); } @@ -583,26 +573,18 @@ static result_t saturn_size_dialogue_icon(saturn_task_t *task, const wuss_event_t *event) { wuss_icon_t *icon; - int row, value; + int row; icon = event->data.icon.icon; // FIXME DPT: mouse move events arrive even when not dragging the bar... - /* row 0 (Size) additionally snaps to SATURN_SIZE_STEP; the others take the - * dragged value as-is. wuss_slider_row_event already reformats the value - * label -- row 0 just re-derives and re-sets the snapped value on top. */ + /* wuss_slider_row_event snaps to row->step (Size only) and reformats the + * value label itself, so a hit here needs nothing further. */ for (row = 0; row < SATURN_SIZEDLG_NROWS; row++) - { - if (!wuss_slider_row_event(task->size_dialogue, &task->size_rows[row], - event, &value)) - continue; - - if (row == SATURN_SIZEDLG_ROW_SIZE) - return wuss_slider_row_set(task->size_dialogue, &task->size_rows[row], - saturn_size_snap(value)); - return result_OK; - } + if (wuss_slider_row_event(task->size_dialogue, &task->size_rows[row], + event, NULL)) + return result_OK; if (event->data.icon.action != wuss_MOUSE_UP) return result_OK; From e05d74a7769df132ea00ad8427958fa1385b8e10 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Tue, 15 Sep 2026 19:45:59 +0100 Subject: [PATCH 094/128] fix(wuss): stop spurious MOUSE_MOVE icon events on a slider under hover wuss_mouse_move's generic icon-hover path fired a wuss_EVENT_ICON/ wuss_MOUSE_MOVE for any icon under the pointer, with wuss_BUTTON_SELECT hardcoded regardless of whether a button was actually held. For a slider this meant drag-echo code ran just from mouse-over, not an actual drag. Gate that event on the icon being the currently pressed one, but only for wuss_ICON_TYPE_SLIDER -- menu rows and buttons still need the plain-hover MOVE to light up/open on mouse-over. Removes the now-stale FIXME in saturn.c's icon handler. --- libraries/wuss/core/mouse-move.c | 7 ++++++- libraries/wuss/test/tasks/saturn.c | 12 +++++------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/libraries/wuss/core/mouse-move.c b/libraries/wuss/core/mouse-move.c index 5b456069..70f7d685 100644 --- a/libraries/wuss/core/mouse-move.c +++ b/libraries/wuss/core/mouse-move.c @@ -141,7 +141,12 @@ result_t wuss_mouse_move(wuss_t *wuss, point_t p, wuss_window_t **hit) } } - if (icon != NULL) + /* a slider only wants MOVE while its own drag is tracked above; without + * this it would also get one on every plain hover, with no button + * actually held. Other icon types (menu rows, buttons) rely on this + * hover MOVE to light up/open on mouse-over, so keep it for them. */ + if (icon != NULL && + (icon->spec.type != wuss_ICON_TYPE_SLIDER || icon == wuss->pressed_icon)) { event.kind = wuss_EVENT_ICON; event.data.icon.icon = icon; diff --git a/libraries/wuss/test/tasks/saturn.c b/libraries/wuss/test/tasks/saturn.c index 2655f614..558bd3fe 100644 --- a/libraries/wuss/test/tasks/saturn.c +++ b/libraries/wuss/test/tasks/saturn.c @@ -436,12 +436,12 @@ static const stack_item_t g_saturn_size_stack[SIZE_STACK__LIMIT] = * hidden. */ static result_t saturn_size_dialogue_create(saturn_task_t *task) { - static const int label_box[SATURN_SIZEDLG_NROWS] = { ST_LABL1, ST_LABL2, ST_LABL3, ST_LABL4 }; - static const int slider_box[SATURN_SIZEDLG_NROWS] = { ST_SLDR1, ST_SLDR2, ST_SLDR3, ST_SLDR4 }; - static const int value_box[SATURN_SIZEDLG_NROWS] = { ST_VAL1, ST_VAL2, ST_VAL3, ST_VAL4 }; - static const int label_icon[SATURN_SIZEDLG_NROWS] = { SATURN_SIZE_ICON_LABEL1, SATURN_SIZE_ICON_LABEL2, SATURN_SIZE_ICON_LABEL3, SATURN_SIZE_ICON_LABEL4 }; + static const int label_box[SATURN_SIZEDLG_NROWS] = { ST_LABL1, ST_LABL2, ST_LABL3, ST_LABL4 }; + static const int slider_box[SATURN_SIZEDLG_NROWS] = { ST_SLDR1, ST_SLDR2, ST_SLDR3, ST_SLDR4 }; + static const int value_box[SATURN_SIZEDLG_NROWS] = { ST_VAL1, ST_VAL2, ST_VAL3, ST_VAL4 }; + static const int label_icon[SATURN_SIZEDLG_NROWS] = { SATURN_SIZE_ICON_LABEL1, SATURN_SIZE_ICON_LABEL2, SATURN_SIZE_ICON_LABEL3, SATURN_SIZE_ICON_LABEL4 }; static const int slider_icon[SATURN_SIZEDLG_NROWS] = { SATURN_SIZE_ICON_SLIDER1, SATURN_SIZE_ICON_SLIDER2, SATURN_SIZE_ICON_SLIDER3, SATURN_SIZE_ICON_SLIDER4 }; - static const int value_icon[SATURN_SIZEDLG_NROWS] = { SATURN_SIZE_ICON_VALUE1, SATURN_SIZE_ICON_VALUE2, SATURN_SIZE_ICON_VALUE3, SATURN_SIZE_ICON_VALUE4 }; + static const int value_icon[SATURN_SIZEDLG_NROWS] = { SATURN_SIZE_ICON_VALUE1, SATURN_SIZE_ICON_VALUE2, SATURN_SIZE_ICON_VALUE3, SATURN_SIZE_ICON_VALUE4 }; result_t rc; wuss_icon_spec_t specs[SATURN_SIZE_NICONS]; @@ -577,8 +577,6 @@ static result_t saturn_size_dialogue_icon(saturn_task_t *task, icon = event->data.icon.icon; - // FIXME DPT: mouse move events arrive even when not dragging the bar... - /* wuss_slider_row_event snaps to row->step (Size only) and reformats the * value label itself, so a hit here needs nothing further. */ for (row = 0; row < SATURN_SIZEDLG_NROWS; row++) From b83fd68c024d4d242af26c4b5991ef9890f61451 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Wed, 16 Sep 2026 00:54:20 +0100 Subject: [PATCH 095/128] refactor(wuss): group saturn conf fields, size dialogue from stack_smallest saturn_task_t's conf_dialogue/conf_rows/conf_cancel/conf_apply move into a saturn_conf_t sub-struct. The Configuration dialogue's window size is now derived from a new geom/stack API, stack_smallest, rather than hand-computed literals that could drift from the stack tree. stack_smallest measures the smallest root box a stack tree can be solved into without any flex item shrinking below its min/axis_size, by solving with zero slack (sum of children's minima). Covered by a new stack-test case. The fg/bg colourmenu duplication is kept as two instances (wuss submenu leaves have no pre-open hook to retitle a shared instance, unlike window leaves' PRE_SHOW) -- documented with a comment and a TODO for a future MenuWarning-style dynamic-submenu event. Co-Authored-By: Claude Sonnet 5 --- CMakeLists.txt | 1 + include/geom/stack.h | 22 ++++++ libraries/geom/stack/measure-min.c | 91 ++++++++++++++++++++++ libraries/geom/stack/test/stack-test.c | 51 +++++++++++++ libraries/wuss/TODO.txt | 6 +- libraries/wuss/test/tasks/saturn.c | 102 ++++++++++++------------- libraries/wuss/test/tasks/saturn.h | 24 ++++-- 7 files changed, 236 insertions(+), 61 deletions(-) create mode 100644 libraries/geom/stack/measure-min.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 1f107fa7..d6fd480b 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -314,6 +314,7 @@ set(GEOM_SOURCES libraries/geom/packer/impl.h libraries/geom/packer/packer.c libraries/geom/stack/impl.h + libraries/geom/stack/measure-min.c libraries/geom/stack/solve.c libraries/geom/stack/valid-tree.c) diff --git a/include/geom/stack.h b/include/geom/stack.h index 320a4df5..2073f44e 100644 --- a/include/geom/stack.h +++ b/include/geom/stack.h @@ -10,6 +10,7 @@ extern "C" #include "base/result.h" #include "geom/box.h" +#include "geom/size.h" /* ----------------------------------------------------------------------- */ @@ -135,6 +136,27 @@ result_t stack_solve(const stack_item_t *items, const box_t *root, box_t *out); +/** + * Measure the smallest root box the tree can be solved into without any + * flexible item shrinking below its `min`/`axis_size` -- the size to pass + * `stack_solve` (and, typically, a window's create call) so every item sits + * at its minimum rather than being handed extra slack. Each container's + * minimum main-axis extent is the sum of its children's minima plus gaps and + * insets; its minimum cross-axis extent is the largest of its children's (a + * child contributes its `cross_size`, or 0 if unset, since an unset cross + * size means "span the parent" rather than "needs this much"). + * + * \param[in] items Array of stack items describing the tree. + * \param[in] n Number of items in the array. + * \param[out] out Set to the root item's minimum size. + * \return \ref result_OK on success, result_STACK_BAD_TREE if the parent + * indices are not a valid parents-before-children forest rooted at + * 0. + */ +result_t stack_smallest(const stack_item_t *items, + int n, + size2d_t *out); + #ifdef __cplusplus } #endif diff --git a/libraries/geom/stack/measure-min.c b/libraries/geom/stack/measure-min.c new file mode 100644 index 00000000..2e5d7900 --- /dev/null +++ b/libraries/geom/stack/measure-min.c @@ -0,0 +1,91 @@ +/* geom/stack/measure-min.c -- minimum-size measurement for a box-stack tree */ + +#include "base/utils.h" +#include "geom/box.h" +#include "geom/size.h" + +#include "geom/stack.h" + +#include "impl.h" + +/* ----------------------------------------------------------------------- */ + +/* A leaf's own minimum extent needs no recursion; a container's is derived + * from its children, recursing depth-first so each child's minimum is known + * before its parent sums/maxes over it. */ +static void stack__measure(const stack_item_t *items, + int n, + int index, + size2d_t *mins) +{ + int horiz; + int main_min, cross_min; + int nchildren; + int i; + + horiz = (items[index].kind == stack_KIND_HBOX); + + main_min = 0; + cross_min = 0; + nchildren = 0; + + for (i = 0; i < n; i++) + { + int child_main, child_cross; + + if (items[i].parent != index) + continue; + + if (items[i].kind == stack_KIND_HBOX || items[i].kind == stack_KIND_VBOX) + { + stack__measure(items, n, i, mins); + child_main = horiz ? mins[i].w : mins[i].h; + child_cross = horiz ? mins[i].h : mins[i].w; + } + else if (items[i].kind == stack_KIND_SPACER) + { + child_main = 0; + child_cross = 0; + } + else /* stack_KIND_LEAF */ + { + child_main = items[i].axis_size ? items[i].axis_size : items[i].min; + child_cross = items[i].cross_size; + } + + main_min = main_min + child_main + (nchildren > 0 ? items[index].gap : 0); + cross_min = MAX(cross_min, child_cross); + nchildren++; + } + + if (items[index].axis_size) + main_min = MAX(main_min, items[index].axis_size); + else + main_min = MAX(main_min, items[index].min); + + if (horiz) + { + mins[index].w = main_min + items[index].pad_l + items[index].pad_r; + mins[index].h = cross_min + items[index].pad_t + items[index].pad_b; + } + else + { + mins[index].w = cross_min + items[index].pad_l + items[index].pad_r; + mins[index].h = main_min + items[index].pad_t + items[index].pad_b; + } +} + +result_t stack_smallest(const stack_item_t *items, + int n, + size2d_t *out) +{ + size2d_t mins[n]; + + if (!stack__valid_tree(items, n)) + return result_STACK_BAD_TREE; + + stack__measure(items, n, 0, mins); + *out = mins[0]; + + return result_OK; +} diff --git a/libraries/geom/stack/test/stack-test.c b/libraries/geom/stack/test/stack-test.c index 4b147174..199aa744 100644 --- a/libraries/geom/stack/test/stack-test.c +++ b/libraries/geom/stack/test/stack-test.c @@ -451,6 +451,53 @@ static result_t test_bad_tree(void) return result_TEST_PASSED; } +/* ROOT (VBOX, pad 2 all round) + * | + * +-- ROW (HBOX, gap=3) + * +-- LBL min=20 + * +-- FLD flex=1, min=15 + * + * ROW's minimum main axis is 20+3+15=38; ROOT's minimum is that plus its own + * padding (2+2=4) on each axis -- width 42, height (ROW has no explicit + * axis_size/min, so 0) 4. Measuring, then solving into exactly that size, + * must leave every leaf at its minimum (no slack for FLD's flex to grow + * into). */ +static result_t test_measure_min(void) +{ + enum { ROOT, ROW, LBL, FLD, N }; + static const stack_item_t items[N] = + { + [ROOT] = { .kind = stack_KIND_VBOX, .parent = -1, + .pad_l = 2, .pad_t = 2, .pad_r = 2, .pad_b = 2 }, + [ROW] = { .kind = stack_KIND_HBOX, .parent = ROOT, .gap = 3 }, + [LBL] = { .kind = stack_KIND_LEAF, .parent = ROW, .min = 20 }, + [FLD] = { .kind = stack_KIND_LEAF, .parent = ROW, .flex = 1, .min = 15 }, + }; + + result_t err; + size2d_t sz; + box_t root, out[N]; + + err = stack_smallest(items, N, &sz); + if (err != result_OK) + return result_TEST_FAILED; + + if (sz.w != 42 || sz.h != 4) + return result_TEST_FAILED; + + root = (box_t) BOX_POS_SIZE(0, 0, sz.w, sz.h); + err = stack_solve(items, N, &root, out); + if (err != result_OK) + return result_TEST_FAILED; + + if (out[LBL].x1 - out[LBL].x0 != 20) + return result_TEST_FAILED; + if (out[FLD].x1 - out[FLD].x0 != 15) + return result_TEST_FAILED; + + return result_TEST_PASSED; +} + /* ----------------------------------------------------------------------- */ result_t stack_test(const char *resources) @@ -511,5 +558,9 @@ result_t stack_test(const char *resources) if (err != result_TEST_PASSED) return err; + err = test_measure_min(); + if (err != result_TEST_PASSED) + return err; + return result_TEST_PASSED; } diff --git a/libraries/wuss/TODO.txt b/libraries/wuss/TODO.txt index b0c981c3..b0471e97 100644 --- a/libraries/wuss/TODO.txt +++ b/libraries/wuss/TODO.txt @@ -14,7 +14,6 @@ Fast scrolling can invalidate areas and fail to copy the expected known good scr Too much slider boilerplate -- need to make label+slider+value a standard component. It also has the client do the rounding/snapping which isn't ideal. -Also saw some pointer drift? LAYER 0 - BUGS (fix before building on top) @@ -83,6 +82,11 @@ LAYER 6 - MULTI-TASK (needs stable windows, templates) - Icon bar - a separate task or module? - Test submenu dialogues - Pull in dialogue abstraction from app engine? +- Dynamic submenus: a pre-open event (like RISC OS's MenuWarning) fired + before a submenu-style item opens, so a task can retitle/retarget a + shared submenu instance first. Currently only item->window leaves get + a pre-open hook (wuss_EVENT_PRE_SHOW); item->submenu leaves have none. + (see saturn.c's fg/bg colourmenu split, kept as two instances for this reason) LAYER 7 - APPS & POLISH (needs most of the above) diff --git a/libraries/wuss/test/tasks/saturn.c b/libraries/wuss/test/tasks/saturn.c index 558bd3fe..0c868261 100644 --- a/libraries/wuss/test/tasks/saturn.c +++ b/libraries/wuss/test/tasks/saturn.c @@ -82,7 +82,7 @@ static int saturn_rnd(int n) enum { SATURN_MENU_COLOURS = 0, SATURN_MENU_SIZE }; enum { SATURN_COLOURS_MENU_FOREGROUND = 0, SATURN_COLOURS_MENU_BACKGROUND }; -/* the size dialogue's icons, in creation order (see saturn_size_dialogue_create): +/* the size dialogue's icons, in creation order (see saturn_conf_dialogue_create): * one label/slider/value triple per SATURN_SIZEDLG_ROW_*, then the buttons */ enum { SATURN_SIZE_ICON_LABEL1 = 0, @@ -110,7 +110,7 @@ enum { /* per-row static description, indexed by SATURN_SIZEDLG_ROW_*: label text, * slider [min,max] and the byte offset of the saturn_config_t field the row * reads/writes (via *(int *) ((char *) &task->config + offset)), shared by - * saturn_size_dialogue_create (to fill specs) and + * saturn_conf_dialogue_create (to fill specs) and * saturn_sizedlg_pre_show/saturn_sizedlg_apply (to sync task->config) */ typedef struct saturn_sizedlg_rowdesc { @@ -156,7 +156,7 @@ static wuss_menu_t g_saturn_menu = "Saturn", g_saturn_menu_items, NELEMS(g_saturn_menu_items) }; -static result_t saturn_size_dialogue_create(saturn_task_t *task); +static result_t saturn_conf_dialogue_create(saturn_task_t *task); result_t saturn_create(wuss_t *wuss, saturn_task_t *task, @@ -164,9 +164,9 @@ result_t saturn_create(wuss_t *wuss, { static const saturn_config_t default_config = SATURN_CONFIG_DEFAULT; - result_t rc; - wuss_task_t *delegate; - wuss_task_desc_t delegate_desc; + result_t rc; + wuss_task_t *delegate; + wuss_task_desc_t delegate_desc; task->wuss = wuss; task->bg = colour_rgb(0x00, 0x00, 0x00); @@ -176,10 +176,10 @@ result_t saturn_create(wuss_t *wuss, task->fg_colourmenu = NULL; task->bg_colourmenu = NULL; task->menu_handle = NULL; - task->size_dialogue = NULL; - memset(task->size_rows, 0, sizeof(task->size_rows)); - task->size_cancel = NULL; - task->size_apply = NULL; + task->conf.dialogue = NULL; + memset(task->conf.rows, 0, sizeof(task->conf.rows)); + task->conf.cancel = NULL; + task->conf.apply = NULL; /* saturn_redraw paints its own background */ delegate_desc.handle = saturn_handle; @@ -193,9 +193,11 @@ result_t saturn_create(wuss_t *wuss, } task->delegate = delegate; /* the task the menu opens against */ wuss_task_set_autoclose(delegate, 1); - - // DPT FIXME: why have multiple colour menus when only one can be visible at a time? + /* one instance per target, not shared: a submenu leaf (wuss_MENU_ITEM_BORROWED_SUBMENU) + * opens on hover with no pre-open hook to retitle/retarget a shared + * colourmenu first -- only item->window leaves get that (wuss_EVENT_PRE_SHOW), + * and colourmenu owns its own wuss_menu_t, not a window. */ rc = wuss_colourmenu_create(&task->fg_colourmenu, wuss, "Foreground"); if (rc != result_OK) goto fail_delegate; @@ -220,10 +222,11 @@ result_t saturn_create(wuss_t *wuss, if (rc != result_OK) goto fail_bg_colourmenu; - rc = saturn_size_dialogue_create(task); + rc = saturn_conf_dialogue_create(task); if (rc != result_OK) goto fail_bg_colourmenu; /* wuss_task_destroy closes task->window too */ - g_saturn_menu_items[SATURN_MENU_SIZE].window = task->size_dialogue; + + g_saturn_menu_items[SATURN_MENU_SIZE].window = task->conf.dialogue; return result_OK; @@ -388,7 +391,7 @@ enum SIZE_STACK__LIMIT }; -/* Leaf main-axis sizes, named so saturn_size_dialogue_create's hand-computed +/* Leaf main-axis sizes, named so saturn_conf_dialogue_create's hand-computed * minimum window size can share them with the table below instead of * repeating the numbers as bare literals. */ #define ST_LABEL_W (5*6) /* enough for "Iters" */ @@ -397,7 +400,7 @@ enum #define ST_CANCEL_W 48 #define ST_APPLY_W 56 -static const stack_item_t g_saturn_size_stack[SIZE_STACK__LIMIT] = +static const stack_item_t g_saturn_conf_stack[SIZE_STACK__LIMIT] = { [ST_ROOT] = STACK_VBOX_EX(-1, 0, wuss_STD_GAP, wuss_STD_INSET, wuss_STD_INSET, wuss_STD_INSET, wuss_STD_INSET), @@ -434,7 +437,7 @@ static const stack_item_t g_saturn_size_stack[SIZE_STACK__LIMIT] = * g_saturn_menu_items[SATURN_MENU_SIZE] and wuss_menu_item_t::window), so * it must outlive the open menu chain and is never closed here, only * hidden. */ -static result_t saturn_size_dialogue_create(saturn_task_t *task) +static result_t saturn_conf_dialogue_create(saturn_task_t *task) { static const int label_box[SATURN_SIZEDLG_NROWS] = { ST_LABL1, ST_LABL2, ST_LABL3, ST_LABL4 }; static const int slider_box[SATURN_SIZEDLG_NROWS] = { ST_SLDR1, ST_SLDR2, ST_SLDR3, ST_SLDR4 }; @@ -448,19 +451,14 @@ static result_t saturn_size_dialogue_create(saturn_task_t *task) wuss_icon_t *made[SATURN_SIZE_NICONS]; box_t boxes[SIZE_STACK__LIMIT]; box_t root; - int row_w, btns_w; int value, row; size2d_t sz; - /* stack_solve distributes into a box it's given; it can't report a - * subtree's intrinsic minimum size in one call, since flex/spacer items - * always consume whatever slack the root provides. So the minimum - * window size is hand-computed here from the same constants the table - * uses, rather than measured by solving. */ - row_w = ST_LABEL_W + wuss_STD_GAP + ST_SLIDER_MIN_W + wuss_STD_GAP + ST_LABEL2_W; - btns_w = ST_CANCEL_W + wuss_STD_GAP + ST_APPLY_W; - sz.w = MAX(row_w, btns_w) + 2 * wuss_STD_INSET; - sz.h = (wuss_STD_SLIDER_HEIGHT + wuss_STD_GAP) * 4 + wuss_STD_PRIMARY_BUTTON_HEIGHT + 2 * wuss_STD_INSET; + /* smallest window the layout can be solved into without any flex item + * (the sliders) growing past its minimum */ + rc = stack_smallest(g_saturn_conf_stack, NELEMS(g_saturn_conf_stack), &sz); + if (rc != result_OK) + return rc; rc = wuss_window_create_placed(task->delegate, sz, @@ -472,12 +470,12 @@ static result_t saturn_size_dialogue_create(saturn_task_t *task) wuss_BACKDROP_COLOUR(wuss_COLOUR_WINDOW), sz, sz, - &task->size_dialogue); + &task->conf.dialogue); if (rc != result_OK) return rc; root = (box_t) BOX_POS_SIZE(0, 0, sz.w, sz.h); - rc = stack_solve(g_saturn_size_stack, NELEMS(g_saturn_size_stack), &root, boxes); + rc = stack_solve(g_saturn_conf_stack, NELEMS(g_saturn_conf_stack), &root, boxes); if (rc != result_OK) goto exit; @@ -498,35 +496,35 @@ static result_t saturn_size_dialogue_create(saturn_task_t *task) wuss_icon_spec_action(&specs[SATURN_SIZE_ICON_CANCEL], boxes[ST_CNCL], "Cancel", wuss_COLOUR_BLACK, wuss_COLOUR_WINDOW, 0); wuss_icon_spec_action(&specs[SATURN_SIZE_ICON_APPLY], boxes[ST_APLY], "Apply", wuss_COLOUR_BLACK, wuss_COLOUR_WINDOW, 1); - rc = wuss_icon_create_array(task->size_dialogue, specs, SATURN_SIZE_NICONS, + rc = wuss_icon_create_array(task->conf.dialogue, specs, SATURN_SIZE_NICONS, made); if (rc != result_OK) goto exit; for (row = 0; row < SATURN_SIZEDLG_NROWS; row++) - wuss_slider_row_bind(&task->size_rows[row], made[slider_icon[row]], + wuss_slider_row_bind(&task->conf.rows[row], made[slider_icon[row]], made[value_icon[row]], NULL, g_saturn_sizedlg_rows[row].min, g_saturn_sizedlg_rows[row].max, g_saturn_sizedlg_rows[row].step); - task->size_cancel = made[SATURN_SIZE_ICON_CANCEL]; - task->size_apply = made[SATURN_SIZE_ICON_APPLY]; + task->conf.cancel = made[SATURN_SIZE_ICON_CANCEL]; + task->conf.apply = made[SATURN_SIZE_ICON_APPLY]; return result_OK; exit: - wuss_window_close(task->size_dialogue); /* not yet a menu leaf: safe to close */ - task->size_dialogue = NULL; + wuss_window_close(task->conf.dialogue); /* not yet a menu leaf: safe to close */ + task->conf.dialogue = NULL; return rc; } /* wuss_EVENT_PRE_SHOW on the size dialogue: resync the slider and its echo * label to task->config.size, in case Apply (or a config passed to * saturn_create) changed it since the dialogue was last shown. Always - * allows the show. Also used directly by saturn_size_dialogue_icon to reset + * allows the show. Also used directly by saturn_conf_dialogue_icon to reset * the dialogue on an Adjust-Cancel click. */ -static result_t saturn_sizedlg_pre_show(saturn_task_t *task) +static result_t saturn_conf_pre_show(saturn_task_t *task) { result_t rc; int row, value; @@ -535,7 +533,7 @@ static result_t saturn_sizedlg_pre_show(saturn_task_t *task) for (row = 0; row < SATURN_SIZEDLG_NROWS; row++) { value = *saturn_sizedlg_field(task, row); - rc = wuss_slider_row_set(task->size_dialogue, &task->size_rows[row], value); + rc = wuss_slider_row_set(task->conf.dialogue, &task->conf.rows[row], value); } return rc; @@ -543,14 +541,14 @@ static result_t saturn_sizedlg_pre_show(saturn_task_t *task) /* Applies the slider's current value to task->window (resize + doc extent), * shared by a Select and an Adjust click on Apply. */ -static result_t saturn_sizedlg_apply(saturn_task_t *task) +static result_t saturn_conf_apply(saturn_task_t *task) { result_t rc; int row, size_value; for (row = 0; row < SATURN_SIZEDLG_NROWS; row++) *saturn_sizedlg_field(task, row) = - wuss_icon_get_value(task->size_rows[row].slider); + wuss_icon_get_value(task->conf.rows[row].slider); size_value = task->config.size; rc = wuss_window_resize(task->window, SIZE2D(size_value, size_value)); @@ -569,7 +567,7 @@ static result_t saturn_sizedlg_apply(saturn_task_t *task) * the dialogue open. Dismissing goes through wuss_menu_close rather than * touching the (borrowed, reused) window directly -- that is what hides it, * same as a click outside the chain would. */ -static result_t saturn_size_dialogue_icon(saturn_task_t *task, +static result_t saturn_conf_dialogue_icon(saturn_task_t *task, const wuss_event_t *event) { wuss_icon_t *icon; @@ -580,14 +578,14 @@ static result_t saturn_size_dialogue_icon(saturn_task_t *task, /* wuss_slider_row_event snaps to row->step (Size only) and reformats the * value label itself, so a hit here needs nothing further. */ for (row = 0; row < SATURN_SIZEDLG_NROWS; row++) - if (wuss_slider_row_event(task->size_dialogue, &task->size_rows[row], + if (wuss_slider_row_event(task->conf.dialogue, &task->conf.rows[row], event, NULL)) return result_OK; if (event->data.icon.action != wuss_MOUSE_UP) return result_OK; - if (icon == task->size_cancel) + if (icon == task->conf.cancel) { if (event->data.icon.button & wuss_BUTTON_SELECT) { @@ -596,20 +594,20 @@ static result_t saturn_size_dialogue_icon(saturn_task_t *task, return result_OK; } if (event->data.icon.button & wuss_BUTTON_ADJUST) - return saturn_sizedlg_pre_show(task); + return saturn_conf_pre_show(task); return result_OK; } - if (icon == task->size_apply) + if (icon == task->conf.apply) { if (event->data.icon.button & wuss_BUTTON_SELECT) { wuss_menu_close(task->menu_handle); task->menu_handle = NULL; - return saturn_sizedlg_apply(task); + return saturn_conf_apply(task); } if (event->data.icon.button & wuss_BUTTON_ADJUST) - return saturn_sizedlg_apply(task); + return saturn_conf_apply(task); return result_OK; } @@ -638,7 +636,7 @@ static int saturn_menu_select_apply(const wuss_colourmenu_t *colourmenu, /* A pick from either colour submenu, resolved against whichever colourmenu * it came from. "Configuration" is a wuss_menu_item_t::window leaf, not a leaf pick, - * so it never reaches here -- see saturn_size_dialogue_icon and + * so it never reaches here -- see saturn_conf_dialogue_icon and * saturn_sizedlg_pre_show. */ static result_t saturn_menu_select(saturn_task_t *task, const wuss_event_t *event) @@ -675,13 +673,13 @@ result_t saturn_handle(wuss_window_t *window, event->data.mouse.button, window); case wuss_EVENT_ICON: - if (window == task->size_dialogue) - return saturn_size_dialogue_icon(task, event); + if (window == task->conf.dialogue) + return saturn_conf_dialogue_icon(task, event); return result_OK; case wuss_EVENT_PRE_SHOW: - if (window == task->size_dialogue) - return saturn_sizedlg_pre_show(task); + if (window == task->conf.dialogue) + return saturn_conf_pre_show(task); return result_OK; case wuss_EVENT_MENU_SELECT: diff --git a/libraries/wuss/test/tasks/saturn.h b/libraries/wuss/test/tasks/saturn.h index 5599e08b..9553eef1 100644 --- a/libraries/wuss/test/tasks/saturn.h +++ b/libraries/wuss/test/tasks/saturn.h @@ -28,9 +28,9 @@ typedef struct saturn_config { int stars_iters; /* loop 1: the ring */ - int ring_iters; /* loop 2: ring shadow band */ - int body_iters; /* loop 3: planet body */ - int size; /* window size (both axes); set via the Size... dialogue */ + int ring_iters; /* loop 2: ring shadow band */ + int body_iters; /* loop 3: planet body */ + int size; /* window size (both axes) */ } saturn_config_t; @@ -52,20 +52,28 @@ enum SATURN_SIZEDLG_NROWS }; +/* the size dialogue: built once, hidden; a menu leaf (see + * g_saturn_menu_items[SATURN_MENU_SIZE].window) */ +typedef struct saturn_conf +{ + wuss_window_t *dialogue; + wuss_slider_row_t rows[SATURN_SIZEDLG_NROWS]; /* size, stars, ring, body */ + wuss_icon_t *cancel; + wuss_icon_t *apply; +} +saturn_conf_t; + typedef struct saturn_task { wuss_t *wuss; /* for wuss_get_pointer/wuss_get_palette */ wuss_window_t *window; wuss_task_t *delegate; /* the task that owns the menu */ colour_t bg, fg; - unsigned long seed; /* RNG state; a Select click bumps it */ + unsigned long seed; /* RNG state; a Select click bumps it */ saturn_config_t config; wuss_colourmenu_t *fg_colourmenu, *bg_colourmenu; wuss_menu_handle_t menu_handle; /* live only between open and a SELECT pick */ - wuss_window_t *size_dialogue; /* built once, hidden; a menu leaf */ - wuss_slider_row_t size_rows[SATURN_SIZEDLG_NROWS]; /* size, stars, ring, body */ - wuss_icon_t *size_cancel; - wuss_icon_t *size_apply; + saturn_conf_t conf; } saturn_task_t; From cb64317877b7f5264321f4500124acd41b24dcfb Mon Sep 17 00:00:00 2001 From: David Thomas Date: Wed, 16 Sep 2026 08:41:38 +0100 Subject: [PATCH 096/128] refactor(wuss): invert window titlebar/outline flags to NO_ variants wuss_WINDOW_TITLEBAR/OUTLINE become wuss_WINDOW_NO_TITLEBAR/NO_OUTLINE, so wuss_WINDOW_DEFAULT no longer needs to OR them in and a caller wanting chromeless furniture just sets the NO_ bit instead of clearing a positive one. Co-Authored-By: Claude Sonnet 5 --- include/wuss/window.h | 8 ++-- include/wuss/wuss.h | 31 ++++++------ libraries/wuss/component/info.c | 1 - libraries/wuss/core/impl.h | 4 +- libraries/wuss/furniture/hit-test.c | 4 +- libraries/wuss/furniture/invalidate.c | 2 +- libraries/wuss/furniture/layout.c | 4 +- libraries/wuss/menu/menu.c | 2 +- libraries/wuss/test/tasks/saturn.c | 5 +- libraries/wuss/test/wuss-test.c | 69 +++++++++++++++++---------- 10 files changed, 73 insertions(+), 57 deletions(-) diff --git a/include/wuss/window.h b/include/wuss/window.h index 3d8bbe28..d02a4f9c 100644 --- a/include/wuss/window.h +++ b/include/wuss/window.h @@ -45,10 +45,10 @@ extern "C" * \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 unless flags includes - * wuss_WINDOW_TITLEBAR. - * \param[in] flags Appearance flags, e.g. wuss_WINDOW_TITLEBAR / - * wuss_WINDOW_OUTLINE, OR'd together, or + * if too long. Ignored if flags includes + * wuss_WINDOW_NO_TITLEBAR. + * \param[in] flags Appearance flags, e.g. wuss_WINDOW_CLOSE / + * wuss_WINDOW_VSCROLL, OR'd together, or * wuss_WINDOW_DEFAULT for the default furniture. * \param[in] bg Content background, filled by Wuss before each redraw: * a flat colour or an 8x8 fill pattern (see diff --git a/include/wuss/wuss.h b/include/wuss/wuss.h index 054456a3..96070b57 100644 --- a/include/wuss/wuss.h +++ b/include/wuss/wuss.h @@ -199,35 +199,37 @@ wuss_furniture_palette_t; * * \note When the library is built with the WUSS_FURNITURE CMake option off, * every window is chromeless regardless of these flags and the - * furniture bits (wuss_WINDOW_TITLEBAR and below) are ignored. + * furniture bits (wuss_WINDOW_NO_TITLEBAR and below) are ignored. */ typedef enum wuss_window_flags { /** - * Titlebar present; content is inset below it, and it offers a drag - * handle. Without it, content fills the full visible area and - * wuss_WINDOW_CLOSE/BACK/TOGGLE_SIZE are ignored. + * Titlebar suppressed; content fills the full visible area and + * wuss_WINDOW_CLOSE/BACK/TOGGLE_SIZE are ignored. Without this flag, a + * titlebar is present, content is inset below it, and it offers a drag + * handle. */ - wuss_WINDOW_TITLEBAR = 1 << 0, + wuss_WINDOW_NO_TITLEBAR = 1 << 0, - /** 1px border drawn around the visible area. */ - wuss_WINDOW_OUTLINE = 1 << 1, + /** 1px border suppressed; without this flag it is drawn around the + * visible area. */ + wuss_WINDOW_NO_OUTLINE = 1 << 1, /** - * Close icon in the titlebar. Ignored unless flags also includes - * wuss_WINDOW_TITLEBAR. + * Close icon in the titlebar. Ignored if flags also includes + * wuss_WINDOW_NO_TITLEBAR. */ wuss_WINDOW_CLOSE = 1 << 2, /** - * Send-to-back icon in the titlebar. Ignored unless flags also includes - * wuss_WINDOW_TITLEBAR. + * Send-to-back icon in the titlebar. Ignored if flags also includes + * wuss_WINDOW_NO_TITLEBAR. */ wuss_WINDOW_BACK = 1 << 3, /** - * Toggle-size icon in the titlebar. Ignored unless flags also includes - * wuss_WINDOW_TITLEBAR. + * Toggle-size icon in the titlebar. Ignored if flags also includes + * wuss_WINDOW_NO_TITLEBAR. */ wuss_WINDOW_TOGGLE_SIZE = 1 << 4, @@ -241,8 +243,7 @@ typedef enum wuss_window_flags wuss_WINDOW_RESIZE = 1 << 7, /** Default: every furniture region drawn. */ - wuss_WINDOW_DEFAULT = wuss_WINDOW_TITLEBAR | wuss_WINDOW_OUTLINE | - wuss_WINDOW_CLOSE | wuss_WINDOW_BACK | + wuss_WINDOW_DEFAULT = wuss_WINDOW_CLOSE | wuss_WINDOW_BACK | wuss_WINDOW_TOGGLE_SIZE | wuss_WINDOW_VSCROLL | wuss_WINDOW_HSCROLL | wuss_WINDOW_RESIZE, diff --git a/libraries/wuss/component/info.c b/libraries/wuss/component/info.c index aaa82ec7..9f6911f6 100644 --- a/libraries/wuss/component/info.c +++ b/libraries/wuss/component/info.c @@ -129,7 +129,6 @@ result_t wuss_info_create(wuss_info_t **out, rc = wuss_window_create(task, &content, title, - wuss_WINDOW_TITLEBAR | wuss_WINDOW_OUTLINE | wuss_WINDOW_NO_REDRAW | wuss_WINDOW_HIDDEN, wuss_BACKDROP_COLOUR(wuss_COLOUR_WINDOW), SIZE2D(w, h), diff --git a/libraries/wuss/core/impl.h b/libraries/wuss/core/impl.h index 3e1bde44..a14b5552 100644 --- a/libraries/wuss/core/impl.h +++ b/libraries/wuss/core/impl.h @@ -600,7 +600,7 @@ static inline void wuss__min_content(const wuss_window_t *window, static inline int wuss__titlebar_height_for(const wuss_t *wuss, wuss_window_flags_t flags) { - return (flags & wuss_WINDOW_TITLEBAR) ? wuss->titlebar_height : 0; + return (flags & wuss_WINDOW_NO_TITLEBAR) ? 0 : wuss->titlebar_height; } static inline int wuss__titlebar_height(const wuss_window_t *window) @@ -624,7 +624,7 @@ static inline void wuss__window_set_toggled(wuss_window_t *window, static inline int wuss__outline_px_for(wuss_window_flags_t flags) { - return (flags & wuss_WINDOW_OUTLINE) ? 1 : 0; + return (flags & wuss_WINDOW_NO_OUTLINE) ? 0 : 1; } static inline int wuss__outline_px(const wuss_window_t *window) diff --git a/libraries/wuss/furniture/hit-test.c b/libraries/wuss/furniture/hit-test.c index 289f8a07..ee2cc06e 100644 --- a/libraries/wuss/furniture/hit-test.c +++ b/libraries/wuss/furniture/hit-test.c @@ -20,7 +20,7 @@ static wuss_furniture_region_t nearest_edge_region(const wuss_window_t *window, has_v = (window->flags & wuss_WINDOW_VSCROLL) != 0; has_h = (window->flags & wuss_WINDOW_HSCROLL) != 0; - has_title = (window->flags & wuss_WINDOW_TITLEBAR) != 0; + has_title = (window->flags & wuss_WINDOW_NO_TITLEBAR) == 0; /* bottom edge: the hscroll strip runs its full width */ if (p.y >= content.y1) @@ -59,7 +59,7 @@ wuss_furniture_region_t wuss__furniture_hit_test(const wuss_window_t *window, { box_t box; - if (window->flags & wuss_WINDOW_TITLEBAR) + if (!(window->flags & wuss_WINDOW_NO_TITLEBAR)) { if (window->flags & wuss_WINDOW_BACK) { diff --git a/libraries/wuss/furniture/invalidate.c b/libraries/wuss/furniture/invalidate.c index 3c81456f..7b668b39 100644 --- a/libraries/wuss/furniture/invalidate.c +++ b/libraries/wuss/furniture/invalidate.c @@ -27,7 +27,7 @@ void wuss__furniture_invalidate_for(wuss_window_t *window, * on still reserves both strips, for the resize icon and the rules. */ wuss__furniture_carve_for(window->flags, wuss__button_size(window), &carve); - if (window->flags & wuss_WINDOW_TITLEBAR) + if (!(window->flags & wuss_WINDOW_NO_TITLEBAR)) { box_t titlebar; diff --git a/libraries/wuss/furniture/layout.c b/libraries/wuss/furniture/layout.c index 8584cd1e..ed9b81f7 100644 --- a/libraries/wuss/furniture/layout.c +++ b/libraries/wuss/furniture/layout.c @@ -49,7 +49,7 @@ void wuss__furniture_layout_build(wuss_window_t *window) /* titlebar fill + its icons ------------------------------------------- */ wuss__titlebar_box(window, &titlebar); - if (window->flags & wuss_WINDOW_TITLEBAR) + if (!(window->flags & wuss_WINDOW_NO_TITLEBAR)) { layout->titlebar = titlebar; layout->has_titlebar = 1; @@ -210,7 +210,7 @@ void wuss__furniture_layout_build(wuss_window_t *window) } /* window outline: four one-pixel edges ---------------------------- */ - if (window->flags & wuss_WINDOW_OUTLINE) + if (!(window->flags & wuss_WINDOW_NO_OUTLINE)) { box_t edge; diff --git a/libraries/wuss/menu/menu.c b/libraries/wuss/menu/menu.c index cf229767..4541183d 100644 --- a/libraries/wuss/menu/menu.c +++ b/libraries/wuss/menu/menu.c @@ -610,7 +610,7 @@ static result_t wuss__menu_spawn(wuss_t *wuss, if (menu_task == NULL) return result_OOM; - menu_flags = wuss_WINDOW_TITLEBAR | wuss_WINDOW_OUTLINE; + menu_flags = 0; outline_px = wuss__outline_px_for(menu_flags); titlebar_height = wuss__titlebar_height_for(wuss, menu_flags); diff --git a/libraries/wuss/test/tasks/saturn.c b/libraries/wuss/test/tasks/saturn.c index 0c868261..ced6351e 100644 --- a/libraries/wuss/test/tasks/saturn.c +++ b/libraries/wuss/test/tasks/saturn.c @@ -463,10 +463,7 @@ static result_t saturn_conf_dialogue_create(saturn_task_t *task) rc = wuss_window_create_placed(task->delegate, sz, "Configuration", - wuss_WINDOW_TITLEBAR | - wuss_WINDOW_OUTLINE | - wuss_WINDOW_HIDDEN | - wuss_WINDOW_NO_REDRAW, + wuss_WINDOW_HIDDEN | wuss_WINDOW_NO_REDRAW, wuss_BACKDROP_COLOUR(wuss_COLOUR_WINDOW), sz, sz, diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 64ff7e09..67a249c0 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -1043,7 +1043,8 @@ result_t wuss_test(const char *resources) rc = wuss_window_create(delegate_d, &box_d, "ignored", - 0, /* fully chromeless: no titlebar, outline, scrollbars or resize */ + /* fully chromeless: no titlebar, outline, scrollbars or resize */ + wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE, wuss_NO_BACKDROP, box_size(&box_d), SIZE2D(0, 0), @@ -1088,7 +1089,7 @@ result_t wuss_test(const char *resources) rc = wuss_window_create(delegate_e, &box_e, NULL, - wuss_WINDOW_OUTLINE, + wuss_WINDOW_NO_TITLEBAR, wuss_NO_BACKDROP, box_size(&box_e), SIZE2D(0, 0), @@ -1106,7 +1107,7 @@ result_t wuss_test(const char *resources) rc = wuss_window_create(delegate_f, &box_f, NULL, - wuss_WINDOW_OUTLINE, + wuss_WINDOW_NO_TITLEBAR, wuss_NO_BACKDROP, box_size(&box_f), SIZE2D(0, 0), @@ -1171,7 +1172,8 @@ result_t wuss_test(const char *resources) rc = wuss_window_create(delegate_h, &box_h, NULL, - 0, /* fully chromeless: no titlebar, outline, scrollbars or resize */ + /* fully chromeless: no titlebar, outline, scrollbars or resize */ + wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE, wuss_NO_BACKDROP, box_size(&box_h), SIZE2D(0, 0), @@ -1189,7 +1191,8 @@ result_t wuss_test(const char *resources) rc = wuss_window_create(delegate_g, &box_g, NULL, - 0, /* fully chromeless: no titlebar, outline, scrollbars or resize */ + /* fully chromeless: no titlebar, outline, scrollbars or resize */ + wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE, wuss_NO_BACKDROP, box_size(&box_g), SIZE2D(0, 0), @@ -1248,7 +1251,8 @@ result_t wuss_test(const char *resources) rc = wuss_window_create(delegate_i, &box_i, NULL, - 0, /* fully chromeless: no titlebar, outline, scrollbars or resize */ + /* fully chromeless: no titlebar, outline, scrollbars or resize */ + wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE, wuss_NO_BACKDROP, box_size(&box_i), SIZE2D(0, 0), @@ -1266,7 +1270,8 @@ result_t wuss_test(const char *resources) rc = wuss_window_create(delegate_j, &box_j, NULL, - 0, /* fully chromeless: no titlebar, outline, scrollbars or resize */ + /* fully chromeless: no titlebar, outline, scrollbars or resize */ + wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE, wuss_NO_BACKDROP, box_size(&box_j), SIZE2D(0, 0), @@ -1314,7 +1319,8 @@ result_t wuss_test(const char *resources) rc = wuss_window_create(delegate_m, &box_m, NULL, - 0, /* fully chromeless: no titlebar, outline, scrollbars or resize */ + /* fully chromeless: no titlebar, outline, scrollbars or resize */ + wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE, wuss_NO_BACKDROP, box_size(&box_m), SIZE2D(0, 0), @@ -3880,7 +3886,8 @@ result_t wuss_test(const char *resources) box_b.x0 = 20; box_b.y0 = 10; /* left half will sit under A */ box_b.x1 = 80; box_b.y1 = 50; rc = wuss_window_create(delegate_b, &box_b, "B", - 0, /* fully chromeless: no titlebar, outline, scrollbars or resize */ + /* fully chromeless: no titlebar, outline, scrollbars or resize */ + wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE, wuss_NO_BACKDROP, box_size(&box_b), SIZE2D(0, 0), @@ -3896,7 +3903,8 @@ result_t wuss_test(const char *resources) box_a.x0 = 0; box_a.y0 = 0; /* created after B, so A is topmost */ box_a.x1 = 40; box_a.y1 = 100; rc = wuss_window_create(delegate_a, &box_a, "A", - 0, /* fully chromeless: no titlebar, outline, scrollbars or resize */ + /* fully chromeless: no titlebar, outline, scrollbars or resize */ + wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE, wuss_NO_BACKDROP, box_size(&box_a), SIZE2D(0, 0), @@ -3977,7 +3985,8 @@ result_t wuss_test(const char *resources) box_b.x0 = 0; box_b.y0 = 0; /* right part sits under A throughout */ box_b.x1 = 60; box_b.y1 = 40; rc = wuss_window_create(delegate_b, &box_b, "B", - 0, /* fully chromeless: no titlebar, outline, scrollbars or resize */ + /* fully chromeless: no titlebar, outline, scrollbars or resize */ + wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE, wuss_NO_BACKDROP, box_size(&box_b), SIZE2D(0, 0), @@ -3993,7 +4002,8 @@ result_t wuss_test(const char *resources) box_a.x0 = 40; box_a.y0 = 0; /* created after B, so A is topmost */ box_a.x1 = 100; box_a.y1 = 40; rc = wuss_window_create(delegate_a, &box_a, "A", - 0, /* fully chromeless: no titlebar, outline, scrollbars or resize */ + /* fully chromeless: no titlebar, outline, scrollbars or resize */ + wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE, wuss_NO_BACKDROP, box_size(&box_a), SIZE2D(0, 0), @@ -4051,7 +4061,8 @@ result_t wuss_test(const char *resources) box_b.x0 = 0; box_b.y0 = 0; /* middle band sits under A */ box_b.x1 = 60; box_b.y1 = 60; rc = wuss_window_create(delegate_b, &box_b, "B", - 0, /* fully chromeless: no titlebar, outline, scrollbars or resize */ + /* fully chromeless: no titlebar, outline, scrollbars or resize */ + wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE, wuss_NO_BACKDROP, box_size(&box_b), SIZE2D(0, 0), @@ -4067,7 +4078,8 @@ result_t wuss_test(const char *resources) box_a.x0 = 0; box_a.y0 = 20; /* created after B, so A is topmost */ box_a.x1 = 60; box_a.y1 = 40; rc = wuss_window_create(delegate_a, &box_a, "A", - 0, /* fully chromeless: no titlebar, outline, scrollbars or resize */ + /* fully chromeless: no titlebar, outline, scrollbars or resize */ + wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE, wuss_NO_BACKDROP, box_size(&box_a), SIZE2D(0, 0), @@ -4158,7 +4170,8 @@ result_t wuss_test(const char *resources) box_b.x0 = 80; box_b.y0 = 80; /* corner already under A */ box_b.x1 = 140; box_b.y1 = 140; rc = wuss_window_create(delegate_b, &box_b, "B", - 0, /* fully chromeless: no titlebar, outline, scrollbars or resize */ + /* fully chromeless: no titlebar, outline, scrollbars or resize */ + wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE, wuss_NO_BACKDROP, box_size(&box_b), SIZE2D(0, 0), @@ -4174,7 +4187,8 @@ result_t wuss_test(const char *resources) box_a.x0 = 0; box_a.y0 = 0; /* created after B, so A is topmost */ box_a.x1 = 100; box_a.y1 = 100; rc = wuss_window_create(delegate_a, &box_a, "A", - 0, /* fully chromeless: no titlebar, outline, scrollbars or resize */ + /* fully chromeless: no titlebar, outline, scrollbars or resize */ + wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE, wuss_NO_BACKDROP, box_size(&box_a), SIZE2D(0, 0), @@ -5296,13 +5310,15 @@ ColourMenuOK: ; bb.x0 = 130; bb.y0 = 130; bb.x1 = 190; bb.y1 = 190; rc = wuss_window_create(task_a, &ba, "A", - 0, /* fully chromeless */ + /* fully chromeless: no titlebar, outline, scrollbars or resize */ + wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE, wuss_NO_BACKDROP, box_size(&ba), SIZE2D(0, 0), &wa); if (rc != result_OK) goto MoveDestroy; rc = wuss_window_create(task_b, &bb, "B", - 0, /* fully chromeless */ + /* fully chromeless: no titlebar, outline, scrollbars or resize */ + wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE, wuss_NO_BACKDROP, box_size(&bb), SIZE2D(0, 0), &wb); if (rc != result_OK) goto MoveDestroy; @@ -5428,7 +5444,8 @@ ColourMenuOK: ; bq.x0 = 6; bq.y0 = 6; bq.x1 = 60; bq.y1 = 60; rc = wuss_window_create(task_q, &bq, "Q", - 0, /* fully chromeless */ + /* fully chromeless: no titlebar, outline, scrollbars or resize */ + wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE, wuss_NO_BACKDROP, box_size(&bq), SIZE2D(0, 0), &wq); if (rc != result_OK) goto QuitDestroy; @@ -5462,7 +5479,8 @@ ColourMenuOK: ; if (task_q == NULL) { rc = result_OOM; goto QuitDestroy; } qmt.self = task_q; rc = wuss_window_create(task_q, &bq, "Q2", - 0, /* fully chromeless */ + /* fully chromeless: no titlebar, outline, scrollbars or resize */ + wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE, wuss_NO_BACKDROP, box_size(&bq), SIZE2D(0, 0), &wq); if (rc != result_OK) goto QuitDestroy; @@ -5751,7 +5769,7 @@ ColourMenuOK: ; { static const wuss_window_flags_t combos[] = { - wuss_WINDOW_DEFAULT & ~wuss_WINDOW_OUTLINE, + wuss_WINDOW_DEFAULT | wuss_WINDOW_NO_OUTLINE, wuss_WINDOW_DEFAULT & ~wuss_WINDOW_RESIZE, wuss_WINDOW_DEFAULT & ~wuss_WINDOW_VSCROLL & ~wuss_WINDOW_HSCROLL, /* one scrollbar + resize: carve is 0 on the stripless axis, so the @@ -5761,7 +5779,7 @@ ColourMenuOK: ; wuss_WINDOW_DEFAULT & ~wuss_WINDOW_BACK, wuss_WINDOW_DEFAULT & ~wuss_WINDOW_TOGGLE_SIZE, wuss_WINDOW_DEFAULT & ~wuss_WINDOW_CLOSE, - wuss_WINDOW_DEFAULT & ~wuss_WINDOW_TITLEBAR + wuss_WINDOW_DEFAULT | wuss_WINDOW_NO_TITLEBAR }; static test_task_t tc_cb; @@ -5789,8 +5807,8 @@ ColourMenuOK: ; /* No titlebar still keeps a 1px top frame with nothing behind it: a * click there is the documented CONTENT exception. */ - leak = !(combos[i] & wuss_WINDOW_TITLEBAR) && - (combos[i] & wuss_WINDOW_OUTLINE); + leak = (combos[i] & wuss_WINDOW_NO_TITLEBAR) && + !(combos[i] & wuss_WINDOW_NO_OUTLINE); if (!furniture_hit_sweep(win_cb, leak)) { @@ -6137,7 +6155,8 @@ result_t wuss_test(const char *resources) /* 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 = 0; + const wuss_window_flags_t chromeless = wuss_WINDOW_NO_TITLEBAR | + wuss_WINDOW_NO_OUTLINE; NOT_USED(resources); From 3ce7016c6438ef63ab2d8d5a0caae7bbf8f78da7 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Wed, 16 Sep 2026 09:19:34 +0100 Subject: [PATCH 097/128] feat(wuss): add dialogue component, refactor saturn Configure onto it wuss_dialogue_t wraps window creation, a fillout-on-PRE_SHOW hook and an icon-click action table, so a task no longer has to hand-roll window creation and Cancel/Apply dispatch for an interactive dialogue. Explicitly decoupled from menus: a dialogue shown as a menu leaf is dismissed by the caller's own action callback calling wuss_menu_close. saturn.c's Configure dialogue is the first user, refactored from manual wuss_window_create plus inline Cancel/Apply branches onto wuss_dialogue_create/wuss_dialogue_set_actions. Co-Authored-By: Claude Sonnet 5 --- CMakeLists.txt | 4 +- include/wuss/component/dialogue.h | 197 ++++++++++++++++++++++++++++ libraries/wuss/component/dialogue.c | 147 +++++++++++++++++++++ libraries/wuss/test/tasks/saturn.c | 161 +++++++++++++---------- libraries/wuss/test/tasks/saturn.h | 3 +- 5 files changed, 442 insertions(+), 70 deletions(-) create mode 100644 include/wuss/component/dialogue.h create mode 100644 libraries/wuss/component/dialogue.c diff --git a/CMakeLists.txt b/CMakeLists.txt index d6fd480b..9706daf5 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -143,6 +143,7 @@ if(WUSS_COMPONENTS) list(APPEND PUBLIC_HEADERS include/wuss/component/colourmenu.h) list(APPEND PUBLIC_HEADERS include/wuss/component/info.h) list(APPEND PUBLIC_HEADERS include/wuss/component/proginfo.h) + list(APPEND PUBLIC_HEADERS include/wuss/component/dialogue.h) endif() # The public headers must be set as properties of the library, not as @@ -470,7 +471,8 @@ set(WUSS_COMPONENT_SOURCES libraries/wuss/component/fontmenu.c libraries/wuss/component/colourmenu.c libraries/wuss/component/info.c - libraries/wuss/component/proginfo.c) + libraries/wuss/component/proginfo.c + libraries/wuss/component/dialogue.c) set(WUSS_SOURCES ${WUSS_CORE_SOURCES}) if(WUSS_FURNITURE) diff --git a/include/wuss/component/dialogue.h b/include/wuss/component/dialogue.h new file mode 100644 index 00000000..82be3d49 --- /dev/null +++ b/include/wuss/component/dialogue.h @@ -0,0 +1,197 @@ +/* wuss/component/dialogue.h -- interactive dialogue base class */ + +/** + * \file dialogue.h + * + * A shared wuss component: the window-and-button shell behind an interactive + * dialogue (a Configure box, a form with OK/Cancel), so a task need not + * hand-roll its own window creation, fillout-on-show wiring and per-button + * event dispatch. + * + * A wuss_dialogue owns one hidden window on a task the caller passes in. The + * caller builds its own content (icons, layout) on that window after + * wuss_dialogue_create returns, then registers a small table of actions -- + * an icon and a callback -- with wuss_dialogue_set_actions. From the task's + * own wuss_EVENT_ICON handler, a click on any registered icon is dispatched + * to that action's callback via wuss_dialogue_handle_icon; every other icon + * event (e.g. a slider drag) is left to the caller to handle first, as + * wuss_dialogue_handle_icon only fires on wuss_MOUSE_UP against a registered + * icon and returns 0 (not consumed) otherwise. From the task's + * wuss_EVENT_PRE_SHOW handler, wuss_dialogue_handle_pre_show calls the + * fillout callback (if any) to resync the dialogue's icons to live state + * before it is revealed. + * + * This component does not know about menus: a dialogue shown as a + * wuss_menu_item_t::window leaf is dismissed by the task's own action + * callback calling wuss_menu_close, exactly as it already must decide + * whether a Select or Adjust click on Cancel/Apply/OK dismisses at all (the + * RISC OS convention: Select dismisses, Adjust applies/resets and leaves the + * dialogue open). A standalone dialogue dismisses itself with + * wuss_dialogue_hide. + * + * Built only when WUSS_COMPONENTS is defined (which implies WUSS_MENUS). + */ + +#ifndef WUSS_COMPONENT_DIALOGUE_H +#define WUSS_COMPONENT_DIALOGUE_H + +#ifdef __cplusplus +extern "C" +{ +#endif + +#include "base/result.h" +#include "geom/size.h" + +#include "wuss/icon.h" +#include "wuss/task.h" +#include "wuss/window.h" + +/* ----------------------------------------------------------------------- */ + +/** Opaque handle: owns the dialogue window and its action table. */ +typedef struct wuss_dialogue wuss_dialogue_t; + +/** + * Fillout callback: resync the dialogue's icons to live state. Called by + * wuss_dialogue_handle_pre_show, and so on every reveal of the dialogue. + * + * \param[in] opaque As passed to wuss_dialogue_create. + * \return \ref result_OK to allow the reveal, else a code that vetoes it + * (see wuss_EVENT_PRE_SHOW). + */ +typedef result_t (wuss_dialogue_fillout_fn_t)(void *opaque); + +/** + * Action callback: a registered icon was clicked. Called by + * wuss_dialogue_handle_icon. + * + * \param[in] opaque As passed to wuss_dialogue_create. + * \param[in] button Button(s) released, as wuss_event_t::data::icon::button. + * Test with '&': RISC OS convention is Select dismisses, + * Adjust applies/resets without dismissing. + * \return \ref result_OK, or an appropriate result code; propagated back + * through wuss_dialogue_handle_icon to the task's own handler. + */ +typedef result_t (wuss_dialogue_action_fn_t)(void *opaque, + wuss_button_t button); + +/** One entry in a dialogue's action table: an icon and the callback a click + * on it dispatches to. */ +typedef struct wuss_dialogue_action +{ + wuss_icon_t *icon; /**< Icon this action fires for. */ + wuss_dialogue_action_fn_t *fn; /**< Callback; never NULL in a + * registered entry. */ +} +wuss_dialogue_action_t; + +/** + * Upper bound on the number of entries wuss_dialogue_set_actions accepts. + */ +#define WUSS_DIALOGUE_MAX_ACTIONS 16 + +/* ----------------------------------------------------------------------- */ + +/** + * Create a dialogue: a hidden window on \p task, sized \p size, titled \p + * title. The caller builds its own icons on the returned window's + * wuss_dialogue_window afterwards. + * + * \param[out] out Filled with the new handle on success, untouched on + * failure. + * \param[in] task Task the dialogue window is created on. Must not be an + * autoclose task and must outlive the handle. + * \param[in] size Content size; also used as both \c doc and \c min_doc, + * so the window never scrolls or resizes on its own. + * \param[in] title Titlebar caption, or NULL for none; borrowed and + * copied by wuss_window_create. + * \param[in] fillout Called by wuss_dialogue_handle_pre_show on every + * reveal, or NULL for none. + * \param[in] opaque Passed back to \p fillout and to every action + * callback. + * \return \ref result_OK on success, \ref result_OOM, \ref result_NULL_ARG + * if \p out or \p task is NULL, or a wuss_window_create code. + */ +result_t wuss_dialogue_create(wuss_dialogue_t **out, + wuss_task_t *task, + size2d_t size, + const char *title, + wuss_dialogue_fillout_fn_t *fillout, + void *opaque); + +/** + * Free a dialogue: closes its window (and so the icons on it). Safe to pass + * NULL. + * + * \param[in] doomed Handle to free, or NULL. + */ +void wuss_dialogue_destroy(wuss_dialogue_t *doomed); + +/** + * Register the dialogue's action table, replacing any previous one. Does not + * take ownership of the icons named in it -- they belong to the dialogue's + * window, as created by the caller. + * + * \param[in] dialogue Handle. + * \param[in] actions Action table; copied. + * \param[in] nactions Number of entries, 0..\ref WUSS_DIALOGUE_MAX_ACTIONS. + * \return \ref result_OK on success, \ref result_BAD_ARG if \p nactions is + * out of range. + */ +result_t wuss_dialogue_set_actions(wuss_dialogue_t *dialogue, + const wuss_dialogue_action_t *actions, + int nactions); + +/** + * Dispatch a wuss_EVENT_ICON from the task's own handler: if \p event is a + * wuss_MOUSE_UP on an icon registered via wuss_dialogue_set_actions, calls + * that action's callback and returns 1 (with *out_result set to its return + * value). Otherwise returns 0 and leaves *out_result untouched, for the + * caller to handle the event itself (e.g. a slider drag) or ignore it. + * + * \param[in] dialogue Handle. + * \param[in] event The wuss_EVENT_ICON event. + * \param[out] out_result Filled with the action callback's return value if + * (and only if) this call returns 1. + * \return 1 if the event was dispatched to a registered action, else 0. + */ +int wuss_dialogue_handle_icon(wuss_dialogue_t *dialogue, + const wuss_event_t *event, + result_t *out_result); + +/** + * Handle a wuss_EVENT_PRE_SHOW from the task's own handler: calls the + * fillout callback passed to wuss_dialogue_create, if any. + * + * \param[in] dialogue Handle. + * \return \ref result_OK if there is no fillout callback, else its return + * value. + */ +result_t wuss_dialogue_handle_pre_show(wuss_dialogue_t *dialogue); + +/** + * Hide the dialogue without destroying it, as wuss_window_set_hidden(window, + * 1). For a standalone dialogue (not a wuss_menu_item_t::window leaf, which + * dismisses via wuss_menu_close instead). + * + * \param[in] dialogue Handle. + * \return \ref result_OK. + */ +result_t wuss_dialogue_hide(wuss_dialogue_t *dialogue); + +/** + * The dialogue's window, for building content on it, using it as a + * wuss_menu_item_t::window, or showing/hiding directly. Borrowed; valid + * until wuss_dialogue_destroy. + * + * \param[in] dialogue Handle. + * \return The window, or NULL if \p dialogue is NULL. + */ +wuss_window_t *wuss_dialogue_window(const wuss_dialogue_t *dialogue); + +#ifdef __cplusplus +} +#endif + +#endif /* WUSS_COMPONENT_DIALOGUE_H */ diff --git a/libraries/wuss/component/dialogue.c b/libraries/wuss/component/dialogue.c new file mode 100644 index 00000000..fba2b459 --- /dev/null +++ b/libraries/wuss/component/dialogue.c @@ -0,0 +1,147 @@ +/* wuss/component/dialogue.c -- interactive dialogue base class */ + +#include +#include + +#ifdef FORTIFY +#include "fortify/fortify.h" +#endif + +#include "base/result.h" +#include "geom/box.h" + +#include "wuss/task.h" +#include "wuss/window.h" + +#include "wuss/component/dialogue.h" + +#include "../core/impl.h" + +/* ----------------------------------------------------------------------- */ + +/* The window is created hidden, without wuss_WINDOW_CLOSE, for the same + * reason as wuss/component/info.c: it may be borrowed as a + * wuss_menu_item_t::window, and the caller (not this component) decides when + * a Cancel/OK click dismisses it. */ +struct wuss_dialogue +{ + wuss_alloc_t alloc; /* copied hooks; wuss_t itself not retained */ + wuss_window_t *window; /* owned; closed by wuss_dialogue_destroy */ + wuss_dialogue_fillout_fn_t *fillout; + void *opaque; + wuss_dialogue_action_t actions[WUSS_DIALOGUE_MAX_ACTIONS]; + int nactions; +}; + +/* ----------------------------------------------------------------------- */ + +result_t wuss_dialogue_create(wuss_dialogue_t **out, + wuss_task_t *task, + size2d_t size, + const char *title, + wuss_dialogue_fillout_fn_t *fillout, + void *opaque) +{ + result_t rc; + wuss_dialogue_t *dialogue; + box_t content; + + if (out == NULL || task == NULL) + return result_NULL_ARG; + + dialogue = task->wuss->alloc.malloc(sizeof(*dialogue)); + if (dialogue == NULL) + return result_OOM; + + dialogue->alloc = task->wuss->alloc; + dialogue->window = NULL; + dialogue->fillout = fillout; + dialogue->opaque = opaque; + dialogue->nactions = 0; + + content = (box_t) BOX_POS_SIZE(0, 0, size.w, size.h); + rc = wuss_window_create(task, + &content, + title, + wuss_WINDOW_HIDDEN | wuss_WINDOW_NO_REDRAW, + wuss_BACKDROP_COLOUR(wuss_COLOUR_WINDOW), + size, + size, + &dialogue->window); + if (rc != result_OK) + { + dialogue->alloc.free(dialogue); + return rc; + } + + *out = dialogue; + return result_OK; +} + +void wuss_dialogue_destroy(wuss_dialogue_t *doomed) +{ + wuss_alloc_t alloc; + + if (doomed == NULL) + return; + + alloc = doomed->alloc; + wuss_window_close(doomed->window); /* frees the window and its icons */ + alloc.free(doomed); +} + +/* ----------------------------------------------------------------------- */ + +result_t wuss_dialogue_set_actions(wuss_dialogue_t *dialogue, + const wuss_dialogue_action_t *actions, + int nactions) +{ + if (nactions < 0 || nactions > WUSS_DIALOGUE_MAX_ACTIONS) + return result_BAD_ARG; + + dialogue->nactions = nactions; + if (nactions > 0) + memcpy(dialogue->actions, actions, sizeof(*actions) * (size_t) nactions); + + return result_OK; +} + +int wuss_dialogue_handle_icon(wuss_dialogue_t *dialogue, + const wuss_event_t *event, + result_t *out_result) +{ + int i; + + if (event->data.icon.action != wuss_MOUSE_UP) + return 0; + + for (i = 0; i < dialogue->nactions; i++) + { + if (dialogue->actions[i].icon == event->data.icon.icon) + { + *out_result = dialogue->actions[i].fn(dialogue->opaque, + event->data.icon.button); + return 1; + } + } + + return 0; +} + +result_t wuss_dialogue_handle_pre_show(wuss_dialogue_t *dialogue) +{ + if (dialogue->fillout == NULL) + return result_OK; + + return dialogue->fillout(dialogue->opaque); +} + +result_t wuss_dialogue_hide(wuss_dialogue_t *dialogue) +{ + return wuss_window_set_hidden(dialogue->window, 1); +} + +wuss_window_t *wuss_dialogue_window(const wuss_dialogue_t *dialogue) +{ + return dialogue ? dialogue->window : NULL; +} diff --git a/libraries/wuss/test/tasks/saturn.c b/libraries/wuss/test/tasks/saturn.c index ced6351e..0a348682 100644 --- a/libraries/wuss/test/tasks/saturn.c +++ b/libraries/wuss/test/tasks/saturn.c @@ -18,6 +18,7 @@ #include "geom/size.h" #include "geom/stack.h" #include "utils/rng.h" +#include "wuss/component/dialogue.h" #include "wuss/menu.h" #include "saturn.h" @@ -157,6 +158,9 @@ static wuss_menu_t g_saturn_menu = }; static result_t saturn_conf_dialogue_create(saturn_task_t *task); +static result_t saturn_conf_fillout(void *opaque); +static result_t saturn_conf_cancel(void *opaque, wuss_button_t button); +static result_t saturn_conf_apply_action(void *opaque, wuss_button_t button); result_t saturn_create(wuss_t *wuss, saturn_task_t *task, @@ -226,7 +230,8 @@ result_t saturn_create(wuss_t *wuss, if (rc != result_OK) goto fail_bg_colourmenu; /* wuss_task_destroy closes task->window too */ - g_saturn_menu_items[SATURN_MENU_SIZE].window = task->conf.dialogue; + g_saturn_menu_items[SATURN_MENU_SIZE].window = + wuss_dialogue_window(task->conf.dialogue); return result_OK; @@ -460,14 +465,8 @@ static result_t saturn_conf_dialogue_create(saturn_task_t *task) if (rc != result_OK) return rc; - rc = wuss_window_create_placed(task->delegate, - sz, - "Configuration", - wuss_WINDOW_HIDDEN | wuss_WINDOW_NO_REDRAW, - wuss_BACKDROP_COLOUR(wuss_COLOUR_WINDOW), - sz, - sz, - &task->conf.dialogue); + rc = wuss_dialogue_create(&task->conf.dialogue, task->delegate, sz, + "Configuration", saturn_conf_fillout, task); if (rc != result_OK) return rc; @@ -493,8 +492,8 @@ static result_t saturn_conf_dialogue_create(saturn_task_t *task) wuss_icon_spec_action(&specs[SATURN_SIZE_ICON_CANCEL], boxes[ST_CNCL], "Cancel", wuss_COLOUR_BLACK, wuss_COLOUR_WINDOW, 0); wuss_icon_spec_action(&specs[SATURN_SIZE_ICON_APPLY], boxes[ST_APLY], "Apply", wuss_COLOUR_BLACK, wuss_COLOUR_WINDOW, 1); - rc = wuss_icon_create_array(task->conf.dialogue, specs, SATURN_SIZE_NICONS, - made); + rc = wuss_icon_create_array(wuss_dialogue_window(task->conf.dialogue), + specs, SATURN_SIZE_NICONS, made); if (rc != result_OK) goto exit; @@ -507,30 +506,48 @@ static result_t saturn_conf_dialogue_create(saturn_task_t *task) task->conf.cancel = made[SATURN_SIZE_ICON_CANCEL]; task->conf.apply = made[SATURN_SIZE_ICON_APPLY]; + { + wuss_dialogue_action_t actions[2]; + + actions[0].icon = task->conf.cancel; + actions[0].fn = saturn_conf_cancel; + actions[1].icon = task->conf.apply; + actions[1].fn = saturn_conf_apply_action; + + rc = wuss_dialogue_set_actions(task->conf.dialogue, actions, + NELEMS(actions)); + if (rc != result_OK) + goto exit; + } + return result_OK; exit: - wuss_window_close(task->conf.dialogue); /* not yet a menu leaf: safe to close */ + wuss_dialogue_destroy(task->conf.dialogue); /* not yet a menu leaf: safe to close */ task->conf.dialogue = NULL; return rc; } -/* wuss_EVENT_PRE_SHOW on the size dialogue: resync the slider and its echo - * label to task->config.size, in case Apply (or a config passed to - * saturn_create) changed it since the dialogue was last shown. Always - * allows the show. Also used directly by saturn_conf_dialogue_icon to reset - * the dialogue on an Adjust-Cancel click. */ -static result_t saturn_conf_pre_show(saturn_task_t *task) +/* Dialogue fillout callback: resync the slider and its echo label to + * task->config.size, in case Apply (or a config passed to saturn_create) + * changed it since the dialogue was last shown. Called by + * wuss_dialogue_handle_pre_show on every reveal, and directly by + * saturn_conf_cancel to reset the dialogue on an Adjust-Cancel click. */ +static result_t saturn_conf_fillout(void *opaque) { - result_t rc; - int row, value; + result_t rc; + saturn_task_t *task; + int row, value; + + task = opaque; rc = result_OK; for (row = 0; row < SATURN_SIZEDLG_NROWS; row++) { value = *saturn_sizedlg_field(task, row); - rc = wuss_slider_row_set(task->conf.dialogue, &task->conf.rows[row], value); + rc = wuss_slider_row_set(wuss_dialogue_window(task->conf.dialogue), + &task->conf.rows[row], value); } return rc; @@ -554,59 +571,66 @@ static result_t saturn_conf_apply(saturn_task_t *task) return wuss_window_set_doc(task->window, SIZE2D(size_value, size_value)); } -/* wuss_EVENT_ICON on the size dialogue: slider drag updates the echo label - * (snapped to SATURN_SIZE_STEP) live on DOWN/MOVE; Cancel/Apply act on - * UP (a press that drags off the button before release is not taken as a - * click), split by button per the RISC OS "Adjust doesn't dismiss" - * convention: Select-Cancel and Select-Apply both dismiss the menu chain - * (Select-Apply after applying); Adjust-Cancel resets the dialogue to - * task->config.size instead of dismissing; Adjust-Apply applies but leaves - * the dialogue open. Dismissing goes through wuss_menu_close rather than - * touching the (borrowed, reused) window directly -- that is what hides it, - * same as a click outside the chain would. */ -static result_t saturn_conf_dialogue_icon(saturn_task_t *task, - const wuss_event_t *event) +/* Dialogue action callback for Cancel, split by button per the RISC OS + * "Adjust doesn't dismiss" convention: Select dismisses the menu chain; + * Adjust resets the dialogue to task->config.size instead. Dismissing goes + * through wuss_menu_close rather than touching the (borrowed, reused) window + * directly -- that is what hides it, same as a click outside the chain + * would. */ +static result_t saturn_conf_cancel(void *opaque, wuss_button_t button) { - wuss_icon_t *icon; - int row; - - icon = event->data.icon.icon; - - /* wuss_slider_row_event snaps to row->step (Size only) and reformats the - * value label itself, so a hit here needs nothing further. */ - for (row = 0; row < SATURN_SIZEDLG_NROWS; row++) - if (wuss_slider_row_event(task->conf.dialogue, &task->conf.rows[row], - event, NULL)) - return result_OK; + saturn_task_t *task; - if (event->data.icon.action != wuss_MOUSE_UP) - return result_OK; + task = opaque; - if (icon == task->conf.cancel) + if (button & wuss_BUTTON_SELECT) { - if (event->data.icon.button & wuss_BUTTON_SELECT) - { - wuss_menu_close(task->menu_handle); - task->menu_handle = NULL; - return result_OK; - } - if (event->data.icon.button & wuss_BUTTON_ADJUST) - return saturn_conf_pre_show(task); + wuss_menu_close(task->menu_handle); + task->menu_handle = NULL; return result_OK; } + if (button & wuss_BUTTON_ADJUST) + return saturn_conf_fillout(task); + return result_OK; +} + +/* Dialogue action callback for Apply: Select applies and dismisses; Adjust + * applies but leaves the dialogue open. */ +static result_t saturn_conf_apply_action(void *opaque, wuss_button_t button) +{ + saturn_task_t *task; + + task = opaque; - if (icon == task->conf.apply) + if (button & wuss_BUTTON_SELECT) { - if (event->data.icon.button & wuss_BUTTON_SELECT) - { - wuss_menu_close(task->menu_handle); - task->menu_handle = NULL; - return saturn_conf_apply(task); - } - if (event->data.icon.button & wuss_BUTTON_ADJUST) - return saturn_conf_apply(task); - return result_OK; + wuss_menu_close(task->menu_handle); + task->menu_handle = NULL; + return saturn_conf_apply(task); } + if (button & wuss_BUTTON_ADJUST) + return saturn_conf_apply(task); + return result_OK; +} + +/* wuss_EVENT_ICON on the size dialogue: slider drag updates the echo label + * (snapped to SATURN_SIZE_STEP) live on DOWN/MOVE, handled here directly; + * a Cancel/Apply click (UP only -- a press that drags off the button before + * release is not taken as a click) is dispatched through the dialogue's + * action table. */ +static result_t saturn_conf_dialogue_icon(saturn_task_t *task, + const wuss_event_t *event) +{ + result_t rc; + int row; + + for (row = 0; row < SATURN_SIZEDLG_NROWS; row++) + if (wuss_slider_row_event(wuss_dialogue_window(task->conf.dialogue), + &task->conf.rows[row], event, NULL)) + return result_OK; + + if (wuss_dialogue_handle_icon(task->conf.dialogue, event, &rc)) + return rc; return result_OK; } @@ -670,13 +694,13 @@ result_t saturn_handle(wuss_window_t *window, event->data.mouse.button, window); case wuss_EVENT_ICON: - if (window == task->conf.dialogue) + if (window == wuss_dialogue_window(task->conf.dialogue)) return saturn_conf_dialogue_icon(task, event); return result_OK; case wuss_EVENT_PRE_SHOW: - if (window == task->conf.dialogue) - return saturn_conf_pre_show(task); + if (window == wuss_dialogue_window(task->conf.dialogue)) + return wuss_dialogue_handle_pre_show(task->conf.dialogue); return result_OK; case wuss_EVENT_MENU_SELECT: @@ -689,6 +713,7 @@ result_t saturn_handle(wuss_window_t *window, case wuss_EVENT_QUIT: wuss_colourmenu_destroy(task->fg_colourmenu); wuss_colourmenu_destroy(task->bg_colourmenu); + wuss_dialogue_destroy(task->conf.dialogue); free(task); /* task_data was calloc'd per instance by the spawner */ return result_OK; diff --git a/libraries/wuss/test/tasks/saturn.h b/libraries/wuss/test/tasks/saturn.h index 9553eef1..a704e384 100644 --- a/libraries/wuss/test/tasks/saturn.h +++ b/libraries/wuss/test/tasks/saturn.h @@ -9,6 +9,7 @@ * by default, so the wuss app always has the colourmenu component. */ #include "framebuf/colour.h" #include "wuss/component/colourmenu.h" +#include "wuss/component/dialogue.h" #include "wuss/icon-spec.h" #include "wuss/icon.h" #include "wuss/menu.h" @@ -56,7 +57,7 @@ enum * g_saturn_menu_items[SATURN_MENU_SIZE].window) */ typedef struct saturn_conf { - wuss_window_t *dialogue; + wuss_dialogue_t *dialogue; wuss_slider_row_t rows[SATURN_SIZEDLG_NROWS]; /* size, stars, ring, body */ wuss_icon_t *cancel; wuss_icon_t *apply; From 8e7411aac5ec837df7675bbb242e58595e9c0d4f Mon Sep 17 00:00:00 2001 From: David Thomas Date: Wed, 16 Sep 2026 10:03:52 +0100 Subject: [PATCH 098/128] feat(wuss): add relayoutable wuss_info_t, shared wuss_proginfo_t dialogue wuss_info_t gains wuss_info_set_rows to resize and rebuild its icons in place, and wuss_dialogue_t gains wuss_dialogue_create_on_window / wuss_dialogue_set_opaque to wrap an already-created window instead of owning one. Composing the two lets wuss_proginfo_t become a single shared dialogue rather than a private instance per task: a caller sets its desc with wuss_proginfo_set_desc before showing it, and the dialogue relays out to fit via its fillout callback on the next wuss_EVENT_PRE_SHOW. image.c is migrated onto the new proginfo API, and apps/wuss now has a single system-wide Info dialogue hung off the backdrop's Tasks menu, owned by g.menu_task and shared with every launched demo. Co-Authored-By: Claude Sonnet 5 --- apps/wuss/main.c | 17 +++ apps/wuss/tasks.c | 30 ++++- apps/wuss/tasks.h | 3 + include/wuss/component/dialogue.h | 45 ++++++- include/wuss/component/info.h | 20 +++ include/wuss/component/proginfo.h | 100 +++++++++----- libraries/wuss/component/dialogue.c | 67 ++++++++-- libraries/wuss/component/info.c | 201 +++++++++++++++++++--------- libraries/wuss/component/proginfo.c | 120 +++++++++++++++-- libraries/wuss/test/tasks/image.c | 5 + libraries/wuss/test/tasks/saturn.c | 2 +- 11 files changed, 488 insertions(+), 122 deletions(-) diff --git a/apps/wuss/main.c b/apps/wuss/main.c index b86cd5d7..1be63d5e 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -408,6 +408,22 @@ static result_t run_wuss(const char *resources, goto Failure; } + { + /* The "Info" task-menu row's shared dialogue. Hung off g_task_menu as a + * wuss_menu_item_t.window in tasks.c. A create failure is non-fatal -- + * the row just does nothing. */ + static const wuss_proginfo_desc_t desc = + { + "Wuss demo", + "Window manager test environment", + "(c) DPTLib contributors", + "1.0 (" __DATE__ ")" + }; + + if (wuss_proginfo_create(&g.proginfo, g.menu_task, &desc) != result_OK) + g.proginfo = NULL; + } + g.quit = false; wuss_redraw(wuss); @@ -441,6 +457,7 @@ static result_t run_wuss(const char *resources, * frees every registered task node, but not the per-instance task_data * block a spawn_* calloc'd, so any task window left open at quit leaks that * block. Harmless at process exit. */ + wuss_proginfo_destroy(g.proginfo); /* closes its dialogue window */ wuss_destroy(wuss); /* also sweeps g.menu_task and closes any open chain */ for (i = 0; i < nfonts; i++) diff --git a/apps/wuss/tasks.c b/apps/wuss/tasks.c index c744cc72..e588d5c9 100644 --- a/apps/wuss/tasks.c +++ b/apps/wuss/tasks.c @@ -338,14 +338,28 @@ static result_t spawn_quit(void) return result_OK; } -static const wuss_menu_item_t g_task_items[] = +/* Indices into g_task_items / g_task_spawn -- keep both tables in this + * order. */ +enum { - { "Launch", wuss_MENU_ITEM_NONE, &g_launch_menu, NULL }, - { "Quit Wuss", wuss_MENU_ITEM_DASHED, NULL, NULL } + TASK_ITEM_INFO, + TASK_ITEM_LAUNCH, + TASK_ITEM_QUIT +}; + +/* g_task_items' "Info" row's .window is filled in by tasks_open_launcher + * (built from g.proginfo, which does not exist until run_wuss creates it) -- + * the table itself cannot name it at compile time. */ +static wuss_menu_item_t g_task_items[] = +{ + { "Info", wuss_MENU_ITEM_NONE, NULL, NULL }, + { "Launch", wuss_MENU_ITEM_NONE, &g_launch_menu, NULL }, + { "Quit Wuss", wuss_MENU_ITEM_NONE, NULL, NULL } }; static const task_spawn_fn_t g_task_spawn[] = { + NULL, /* "Info" -> wuss_menu_item_t.window leaf, no spawn */ NULL, /* "Launch" -> submenu g_launch_menu */ spawn_quit }; @@ -365,7 +379,6 @@ result_t task_handle_event(wuss_window_t *window, const wuss_menu_t *menu; int index; - NOT_USED(window); NOT_USED(task_data); if (event->kind == wuss_EVENT_PALETTE) @@ -393,6 +406,13 @@ result_t task_handle_event(wuss_window_t *window, return result_OK; } + if (event->kind == wuss_EVENT_PRE_SHOW) + { + if (window == wuss_proginfo_window(g.proginfo)) + return wuss_proginfo_handle_pre_show(g.proginfo); + return result_OK; + } + if (event->kind != wuss_EVENT_MENU_SELECT) return result_OK; @@ -418,5 +438,7 @@ result_t task_handle_event(wuss_window_t *window, result_t tasks_open_launcher(point_t pos) { + g_task_items[TASK_ITEM_INFO].window = wuss_proginfo_window(g.proginfo); + return wuss_menu_open(g.menu_task, &g_task_menu, pos, NULL); } diff --git a/apps/wuss/tasks.h b/apps/wuss/tasks.h index 31ca2ddc..cd48c5ef 100644 --- a/apps/wuss/tasks.h +++ b/apps/wuss/tasks.h @@ -11,6 +11,7 @@ #include "geom/point.h" #include "wuss/task.h" #include "wuss/wuss.h" +#include "wuss/component/proginfo.h" #include "frontend.h" @@ -23,6 +24,8 @@ extern struct wuss_app_tasks { wuss_t *wuss; wuss_task_t *menu_task; /* owns the task launcher menus */ + wuss_proginfo_t *proginfo; /* shared "Info" dialogue on menu_task; see + * tasks_open_launcher's "Info" row */ colour_t *palette; int npalette; const char *palette_name; /* startup *.hex leafname, for ticking the diff --git a/include/wuss/component/dialogue.h b/include/wuss/component/dialogue.h index 82be3d49..a8033159 100644 --- a/include/wuss/component/dialogue.h +++ b/include/wuss/component/dialogue.h @@ -46,6 +46,7 @@ extern "C" #include "wuss/icon.h" #include "wuss/task.h" #include "wuss/window.h" +#include "wuss/wuss.h" /* ----------------------------------------------------------------------- */ @@ -121,13 +122,53 @@ result_t wuss_dialogue_create(wuss_dialogue_t **out, void *opaque); /** - * Free a dialogue: closes its window (and so the icons on it). Safe to pass - * NULL. + * Create a dialogue wrapping an already-created window, e.g. one owned and + * laid out by another component such as wuss_info_t, instead of creating its + * own. wuss_dialogue_destroy then leaves \p window alone -- the caller (or + * the component that owns it) is responsible for closing it. + * + * Use this to give an existing window fillout/action-table dispatch without + * duplicating its layout logic; see wuss_proginfo_t for the motivating case + * of one dialogue shared and refilled by several callers. + * + * \param[out] out Filled with the new handle on success, untouched on + * failure. + * \param[in] wuss The window's owning wuss instance. + * \param[in] window Existing window; borrowed, not closed by + * wuss_dialogue_destroy. + * \param[in] fillout Called by wuss_dialogue_handle_pre_show on every + * reveal, or NULL for none. + * \param[in] opaque Passed back to \p fillout and to every action + * callback; see also wuss_dialogue_set_opaque. + * \return \ref result_OK on success, \ref result_OOM, \ref result_NULL_ARG + * if \p out, \p wuss or \p window is NULL. + */ +result_t wuss_dialogue_create_on_window(wuss_dialogue_t **out, + wuss_t *wuss, + wuss_window_t *window, + wuss_dialogue_fillout_fn_t *fillout, + void *opaque); + +/** + * Free a dialogue: closes its window (and so the icons on it), unless it was + * built with wuss_dialogue_create_on_window, in which case the window is + * left for its owning component to close. Safe to pass NULL. * * \param[in] doomed Handle to free, or NULL. */ void wuss_dialogue_destroy(wuss_dialogue_t *doomed); +/** + * Change the opaque pointer passed to the fillout and action callbacks, + * replacing the one given at creation. For a dialogue shared by several + * callers (e.g. one wuss_proginfo_t reused across tasks), call this to + * retarget it at the caller about to show it, before revealing it. + * + * \param[in] dialogue Handle. + * \param[in] opaque New opaque pointer. + */ +void wuss_dialogue_set_opaque(wuss_dialogue_t *dialogue, void *opaque); + /** * Register the dialogue's action table, replacing any previous one. Does not * take ownership of the icons named in it -- they belong to the dialogue's diff --git a/include/wuss/component/info.h b/include/wuss/component/info.h index 86c18a3e..5c4a11e3 100644 --- a/include/wuss/component/info.h +++ b/include/wuss/component/info.h @@ -86,6 +86,26 @@ result_t wuss_info_create(wuss_info_t **out, const wuss_info_row_t *rows, int nrows); +/** + * Replace an info dialogue's rows in place: recomputes column widths and row + * pitch for \p rows, resizes the window to fit, and rebuilds its label:value + * icons. For a dialogue shared by several callers (e.g. one wuss_proginfo_t + * reused by every task), call this from a wuss_dialogue_fillout_fn_t just + * before each reveal so the window is always sized for whichever caller is + * about to show it. + * + * \param[in] info Handle. + * \param[in] rows Row array; each label and value is borrowed and copied. + * \param[in] nrows Number of rows; must be >= 1. Need not match the count \p + * info was created with. + * \return \ref result_OK, \ref result_OOM, \ref result_NULL_ARG if \p info + * or \p rows is NULL, \ref result_BAD_ARG if \p nrows is < 1, or a + * wuss_window_resize / wuss_icon_create_array code. + */ +result_t wuss_info_set_rows(wuss_info_t *info, + const wuss_info_row_t *rows, + int nrows); + /** * Free an info dialogue: closes its window and frees every string in it. * Safe to pass NULL. diff --git a/include/wuss/component/proginfo.h b/include/wuss/component/proginfo.h index eb090bd3..498d901b 100644 --- a/include/wuss/component/proginfo.h +++ b/include/wuss/component/proginfo.h @@ -1,20 +1,33 @@ -/* wuss/component/proginfo.h -- a "program information" standard dialogue */ +/* wuss/component/proginfo.h -- a shared "program information" dialogue */ /** * \file proginfo.h * * A shared wuss component: the RISC OS "Info" / Toolbox ProgInfo dialogue in - * miniature -- a small fixed window of Name / Purpose / Author / Version - * rows a task fills in once, then hangs off its Menu-button pop-up. + * miniature -- a small window of Name / Purpose / Author / Version rows, + * built once and reusable by every task that wants an "Info" row on its + * Menu-button pop-up, rather than each task carrying its own copy. * - * This is a thin cap over \ref info.h: wuss_proginfo_create maps the - * non-NULL fields of a wuss_proginfo_desc_t to wuss_info_row_t rows and - * calls wuss_info_create. wuss_proginfo_t is wuss_info_t, and the destroy / - * window calls forward straight through, so all of info.h's lifetime rules - * apply verbatim: the window is created without wuss_WINDOW_CLOSE, stays on - * the caller's task until wuss_proginfo_destroy, that task must not be an - * autoclose task and must outlive the handle, and the dialogue must outlive - * every menu chain that borrows it as a wuss_menu_item_t::window. + * wuss_proginfo_t composes a wuss_info_t (the label:value grid and its + * window) with a wuss_dialogue_t (fillout-on-show dispatch, via + * wuss_dialogue_create_on_window). A caller that wants to show it points its + * "Info" wuss_menu_item_t::window at wuss_proginfo_window(), then calls + * wuss_proginfo_set_desc() with its own field values before opening the menu + * chain. The stored desc is only applied -- resizing the window and + * rewriting its rows via wuss_info_set_rows -- when the dialogue is actually + * revealed, from the home task's wuss_EVENT_PRE_SHOW handler via + * wuss_dialogue_handle_pre_show(); see wuss_proginfo_handle_pre_show(), a + * thin cap over that call. This way the same window is correctly sized for + * whichever caller most recently asked to show it, even though its text + * differs per caller. + * + * The dialogue lives on one "home" task -- normally whichever task's caller + * created it first, or a dedicated task if several unrelated tasks share it + * -- and every wuss_EVENT_PRE_SHOW for wuss_proginfo_window() must reach + * that task's handler and be forwarded to wuss_proginfo_handle_pre_show(). + * As for wuss_info_t, that task must not be an autoclose task and must + * outlive the handle, and the dialogue must outlive every menu chain that + * borrows it as a wuss_menu_item_t::window. * * Built only when WUSS_COMPONENTS is defined (which implies WUSS_MENUS). */ @@ -38,8 +51,8 @@ extern "C" /** * The rows of a program-information dialogue. Every field is borrowed and - * copied; a NULL field is left out entirely, so a zero-initialised desc with - * only \c name set yields a one-row dialogue. + * copied; a NULL field is left out entirely, so a desc with only \c name set + * yields a one-row dialogue. */ typedef struct wuss_proginfo_desc { @@ -52,31 +65,30 @@ typedef struct wuss_proginfo_desc wuss_proginfo_desc_t; /** - * Opaque handle: a wuss_info_t. Owns the dialogue window and its strings. + * Opaque handle: owns the dialogue's window, wuss_info_t and + * wuss_dialogue_t. */ -typedef wuss_info_t wuss_proginfo_t; +typedef struct wuss_proginfo wuss_proginfo_t; /* ----------------------------------------------------------------------- */ /** * Build a program-information dialogue: a hidden window on \p task, laid out * with one label row per non-NULL field of \p desc, in the order Name, - * Purpose, Author, Version. - * - * The window's on-screen size is derived from \p task's font metrics and the - * text in \p desc; it is created with wuss_WINDOW_HIDDEN, so nothing shows - * until the caller reveals it (directly, or via a wuss_menu_item_t::window - * that names wuss_proginfo_window()). + * Purpose, Author, Version. \p desc need only be some starting size -- e.g. + * the first caller's own values -- since wuss_proginfo_set_desc relays out + * and resizes the window for whoever is about to show it next. * * \param[out] out Filled with the new handle on success, untouched on * failure. - * \param[in] task Task the dialogue window is created on. Must not be an - * autoclose task and must outlive the handle. - * \param[in] desc Field values; each borrowed and copied. At least \c name - * must be non-NULL. + * \param[in] task Task the dialogue window is created on ("home task"; see + * \ref proginfo.h). Must not be an autoclose task and must + * outlive the handle. + * \param[in] desc Initial field values; each borrowed and copied. At least + * \c name must be non-NULL. * \return \ref result_OK, \ref result_OOM, \ref result_NULL_ARG if \p out, - * \p task, \p desc or \c desc->name is NULL, or a wuss_info_create - * code. + * \p task, \p desc or \c desc->name is NULL, or a wuss_info_create / + * wuss_dialogue_create_on_window code. */ result_t wuss_proginfo_create(wuss_proginfo_t **out, wuss_task_t *task, @@ -84,21 +96,43 @@ result_t wuss_proginfo_create(wuss_proginfo_t **out, /** * Free a program-information dialogue: closes its window and frees every - * string in it. Safe to pass NULL. An alias for wuss_info_destroy. + * string in it. Safe to pass NULL. * * \param[in] doomed Handle to free, or NULL. */ -#define wuss_proginfo_destroy wuss_info_destroy +void wuss_proginfo_destroy(wuss_proginfo_t *doomed); + +/** + * Point the dialogue at \p desc's field values, to be applied -- resizing + * the window and rewriting its rows -- the next time it is revealed. Call + * this before opening the menu chain that shows wuss_proginfo_window(), so + * whichever caller last asked gets its own values. + * + * \param[in] pi Handle. + * \param[in] desc Field values; each borrowed and copied when applied. At + * least \c name must be non-NULL. + */ +void wuss_proginfo_set_desc(wuss_proginfo_t *pi, + const wuss_proginfo_desc_t *desc); + +/** + * Handle a wuss_EVENT_PRE_SHOW for wuss_proginfo_window() from the home + * task's own handler: applies the desc last passed to + * wuss_proginfo_set_desc, as wuss_dialogue_handle_pre_show. + * + * \param[in] pi Handle. + * \return \ref result_OK, or a wuss_info_set_rows code. + */ +result_t wuss_proginfo_handle_pre_show(wuss_proginfo_t *pi); /** - * The dialogue's window, for use as a wuss_menu_item_t::window or to show - * and hide directly. Borrowed; valid until wuss_proginfo_destroy. NULL only - * if \p pi is NULL. An alias for wuss_info_window. + * The dialogue's window, for use as a wuss_menu_item_t::window. Borrowed; + * valid until wuss_proginfo_destroy. NULL only if \p pi is NULL. * * \param[in] pi Handle. * \return The window, or NULL. */ -#define wuss_proginfo_window wuss_info_window +wuss_window_t *wuss_proginfo_window(const wuss_proginfo_t *pi); #ifdef __cplusplus } diff --git a/libraries/wuss/component/dialogue.c b/libraries/wuss/component/dialogue.c index fba2b459..3677d1ed 100644 --- a/libraries/wuss/component/dialogue.c +++ b/libraries/wuss/component/dialogue.c @@ -22,11 +22,17 @@ /* The window is created hidden, without wuss_WINDOW_CLOSE, for the same * reason as wuss/component/info.c: it may be borrowed as a * wuss_menu_item_t::window, and the caller (not this component) decides when - * a Cancel/OK click dismisses it. */ + * a Cancel/OK click dismisses it. + * + * owns_window is 0 for a dialogue built via wuss_dialogue_create_on_window + * (e.g. composed with a wuss_info_t, which owns and resizes the window + * itself): wuss_dialogue_destroy must then leave the window alone, for the + * other component's own destroy call to close. */ struct wuss_dialogue { wuss_alloc_t alloc; /* copied hooks; wuss_t itself not retained */ - wuss_window_t *window; /* owned; closed by wuss_dialogue_destroy */ + wuss_window_t *window; + int owns_window; wuss_dialogue_fillout_fn_t *fillout; void *opaque; wuss_dialogue_action_t actions[WUSS_DIALOGUE_MAX_ACTIONS]; @@ -35,6 +41,25 @@ struct wuss_dialogue /* ----------------------------------------------------------------------- */ +static wuss_dialogue_t *dialogue_alloc(wuss_alloc_t alloc, + wuss_dialogue_fillout_fn_t *fillout, + void *opaque) +{ + wuss_dialogue_t *dialogue; + + dialogue = alloc.malloc(sizeof(*dialogue)); + if (dialogue == NULL) + return NULL; + + dialogue->alloc = alloc; + dialogue->window = NULL; + dialogue->fillout = fillout; + dialogue->opaque = opaque; + dialogue->nactions = 0; + + return dialogue; +} + result_t wuss_dialogue_create(wuss_dialogue_t **out, wuss_task_t *task, size2d_t size, @@ -49,15 +74,10 @@ result_t wuss_dialogue_create(wuss_dialogue_t **out, if (out == NULL || task == NULL) return result_NULL_ARG; - dialogue = task->wuss->alloc.malloc(sizeof(*dialogue)); + dialogue = dialogue_alloc(task->wuss->alloc, fillout, opaque); if (dialogue == NULL) return result_OOM; - - dialogue->alloc = task->wuss->alloc; - dialogue->window = NULL; - dialogue->fillout = fillout; - dialogue->opaque = opaque; - dialogue->nactions = 0; + dialogue->owns_window = 1; content = (box_t) BOX_POS_SIZE(0, 0, size.w, size.h); rc = wuss_window_create(task, @@ -78,6 +98,27 @@ result_t wuss_dialogue_create(wuss_dialogue_t **out, return result_OK; } +result_t wuss_dialogue_create_on_window(wuss_dialogue_t **out, + wuss_t *wuss, + wuss_window_t *window, + wuss_dialogue_fillout_fn_t *fillout, + void *opaque) +{ + wuss_dialogue_t *dialogue; + + if (out == NULL || wuss == NULL || window == NULL) + return result_NULL_ARG; + + dialogue = dialogue_alloc(wuss->alloc, fillout, opaque); + if (dialogue == NULL) + return result_OOM; + dialogue->owns_window = 0; + dialogue->window = window; + + *out = dialogue; + return result_OK; +} + void wuss_dialogue_destroy(wuss_dialogue_t *doomed) { wuss_alloc_t alloc; @@ -86,12 +127,18 @@ void wuss_dialogue_destroy(wuss_dialogue_t *doomed) return; alloc = doomed->alloc; - wuss_window_close(doomed->window); /* frees the window and its icons */ + if (doomed->owns_window) + wuss_window_close(doomed->window); /* frees the window and its icons */ alloc.free(doomed); } /* ----------------------------------------------------------------------- */ +void wuss_dialogue_set_opaque(wuss_dialogue_t *dialogue, void *opaque) +{ + dialogue->opaque = opaque; +} + result_t wuss_dialogue_set_actions(wuss_dialogue_t *dialogue, const wuss_dialogue_action_t *actions, int nactions) diff --git a/libraries/wuss/component/info.c b/libraries/wuss/component/info.c index 9f6911f6..7e0c9e6c 100644 --- a/libraries/wuss/component/info.c +++ b/libraries/wuss/component/info.c @@ -41,11 +41,17 @@ * wuss_menu_item_t::window can't have it pulled from under it, and * wuss_info_destroy closes it explicitly. The task must therefore not be an * autoclose one -- its window list would never empty -- and must outlive the - * handle. */ + * handle. + * + * icons/nicons track the live label:value icons so wuss_info_set_rows can + * delete them before rebuilding at a new size -- wuss has no "clear all + * icons on this window" call. */ struct wuss_info { - wuss_alloc_t alloc; /* copied hooks; the wuss_t itself is not retained */ - wuss_window_t *window; /* owned; closed by wuss_info_destroy */ + wuss_alloc_t alloc; /* copied hooks; wuss_t itself not retained */ + wuss_window_t *window; /* owned; closed by wuss_info_destroy */ + wuss_icon_t *icons[INFO_MAX_ROWS * 2]; + int nicons; }; /* ----------------------------------------------------------------------- */ @@ -64,36 +70,24 @@ static int info_widen(bmfont_t *font, const char *s, int cur) return MAX(cur, (int) w); } -result_t wuss_info_create(wuss_info_t **out, - wuss_task_t *task, - const char *title, - const wuss_info_row_t *rows, - int nrows) +/* Computes the label:value grid's column widths, row pitch and overall size + * for \p rows against \p font (may be NULL). Shared by wuss_info_create (to + * size the window it creates) and wuss_info_set_rows (to resize the window + * it reuses). */ +static void info_measure(bmfont_t *font, + const wuss_info_row_t *rows, + int nrows, + int *out_labelw, + int *out_valuew, + int *out_rowh, + int *out_fieldh, + int *out_w, + int *out_h) { - result_t rc; - wuss_t *wuss; - bmfont_t *font; - wuss_info_t *info; - int fonth; - int rowh; - int fieldh; - int labelw; - int valuew; - int w; - int h; - box_t content; - wuss_icon_spec_t specs[INFO_MAX_ROWS * 2]; - int i; - - if (out == NULL || task == NULL || title == NULL || rows == NULL) - return result_NULL_ARG; - if (nrows < 1 || nrows > INFO_MAX_ROWS) - return result_BAD_ARG; - - memset(specs, 0, sizeof(specs)); /* leave every unset spec.u arm zeroed */ - - wuss = task->wuss; - font = wuss_get_font(wuss); + int fonth; + int labelw, valuew; + int rowh, fieldh; + int i; /* Column widths: the widest label on the left, the widest value on the * right. A NULL font leaves both at 0; the icons still lay out, just with @@ -115,36 +109,30 @@ result_t wuss_info_create(wuss_info_t **out, labelw = MAX(labelw, 1); valuew = MAX(valuew, 1) + INFO_FIELD_PAD * 2; /* groove border + inset */ - w = INFO_MARGIN * 2 + labelw + INFO_GAP + valuew; + *out_labelw = labelw; + *out_valuew = valuew; + *out_rowh = rowh; + *out_fieldh = fieldh; + *out_w = INFO_MARGIN * 2 + labelw + INFO_GAP + valuew; /* rowh is the pitch (field + leading); the last row has no trailing leading */ - h = INFO_MARGIN * 2 + rowh * nrows - INFO_ROW_LEADING; - - info = wuss->alloc.malloc(sizeof(*info)); - if (info == NULL) - return result_OOM; - info->alloc = wuss->alloc; /* the copy outlives the wuss_t */ - info->window = NULL; + *out_h = INFO_MARGIN * 2 + rowh * nrows - INFO_ROW_LEADING; +} - content = (box_t) BOX_POS_SIZE(0, 0, w, h); - rc = wuss_window_create(task, - &content, - title, - wuss_WINDOW_NO_REDRAW | wuss_WINDOW_HIDDEN, - wuss_BACKDROP_COLOUR(wuss_COLOUR_WINDOW), - SIZE2D(w, h), - SIZE2D(w, h), - &info->window); - if (rc != result_OK) - { - info->alloc.free(info); - return rc; - } +/* Fills \p specs (>= nrows * 2 entries) with one right-justified label and + * one centred value per row, laid out per info_measure's labelw/valuew/rowh/ + * fieldh. Zeroed first so the icon-spec fields this component does not set + * (pattern, bitmap, group, swatch) are their safe defaults. */ +static void info_fill_specs(wuss_icon_spec_t *specs, + const wuss_info_row_t *rows, + int nrows, + int labelw, + int valuew, + int rowh, + int fieldh) +{ + int i; - /* One right-justified label and one centred value per row. Zeroed so the - * icon-spec fields this component does not set (pattern, bitmap, group, - * swatch) are their safe defaults. wuss_icon_create deep-copies each spec - * and its text, so this stack array can go out of scope after the call. */ - memset(specs, 0, sizeof(specs)); + memset(specs, 0, sizeof(*specs) * (size_t) nrows * 2); for (i = 0; i < nrows; i++) { wuss_icon_spec_t *label; @@ -171,19 +159,112 @@ result_t wuss_info_create(wuss_info_t **out, value->u.label.border = wuss_ICON_BORDER_GROOVE; /* RISC OS display field */ value->flags = wuss_ICON_FLAGS_JUSTIFY_CENTRE; } +} - rc = wuss_icon_create_array(info->window, specs, nrows * 2, NULL); +result_t wuss_info_create(wuss_info_t **out, + wuss_task_t *task, + const char *title, + const wuss_info_row_t *rows, + int nrows) +{ + result_t rc; + wuss_t *wuss; + bmfont_t *font; + wuss_info_t *info; + int labelw, valuew; + int rowh, fieldh; + int w, h; + box_t content; + wuss_icon_spec_t specs[INFO_MAX_ROWS * 2]; + + if (out == NULL || task == NULL || title == NULL || rows == NULL) + return result_NULL_ARG; + if (nrows < 1 || nrows > INFO_MAX_ROWS) + return result_BAD_ARG; + + wuss = task->wuss; + font = wuss_get_font(wuss); + info_measure(font, rows, nrows, &labelw, &valuew, &rowh, &fieldh, &w, &h); + + info = wuss->alloc.malloc(sizeof(*info)); + if (info == NULL) + return result_OOM; + info->alloc = wuss->alloc; /* the copy outlives the wuss_t */ + info->window = NULL; + info->nicons = 0; + + content = (box_t) BOX_POS_SIZE(0, 0, w, h); + rc = wuss_window_create(task, + &content, + title, + wuss_WINDOW_NO_REDRAW | wuss_WINDOW_HIDDEN, + wuss_BACKDROP_COLOUR(wuss_COLOUR_WINDOW), + SIZE2D(w, h), + SIZE2D(w, h), + &info->window); + if (rc != result_OK) + { + info->alloc.free(info); + return rc; + } + + /* wuss_icon_create deep-copies each spec and its text, so this stack array + * can go out of scope once wuss_icon_create_array returns. */ + info_fill_specs(specs, rows, nrows, labelw, valuew, rowh, fieldh); + rc = wuss_icon_create_array(info->window, specs, nrows * 2, info->icons); if (rc != result_OK) { wuss_window_close(info->window); /* frees the window and any icons on it */ info->alloc.free(info); return rc; } + info->nicons = nrows * 2; *out = info; return result_OK; } +result_t wuss_info_set_rows(wuss_info_t *info, + const wuss_info_row_t *rows, + int nrows) +{ + result_t rc; + bmfont_t *font; + int labelw, valuew; + int rowh, fieldh; + int w, h; + wuss_icon_spec_t specs[INFO_MAX_ROWS * 2]; + wuss_icon_t *made[INFO_MAX_ROWS * 2]; + int i; + + if (info == NULL || rows == NULL) + return result_NULL_ARG; + if (nrows < 1 || nrows > INFO_MAX_ROWS) + return result_BAD_ARG; + + font = wuss_get_font(info->window->wuss); + info_measure(font, rows, nrows, &labelw, &valuew, &rowh, &fieldh, &w, &h); + + rc = wuss_window_resize(info->window, SIZE2D(w, h)); + if (rc != result_OK) + return rc; + rc = wuss_window_set_doc(info->window, SIZE2D(w, h)); + if (rc != result_OK) + return rc; + + info_fill_specs(specs, rows, nrows, labelw, valuew, rowh, fieldh); + rc = wuss_icon_create_array(info->window, specs, nrows * 2, made); + if (rc != result_OK) + return rc; + + for (i = 0; i < info->nicons; i++) + wuss_icon_delete(info->window, info->icons[i]); + memcpy(info->icons, made, sizeof(*made) * (size_t) nrows * 2); + info->nicons = nrows * 2; + + return result_OK; +} + void wuss_info_destroy(wuss_info_t *doomed) { wuss_alloc_t alloc; diff --git a/libraries/wuss/component/proginfo.c b/libraries/wuss/component/proginfo.c index 991865d4..21e8f467 100644 --- a/libraries/wuss/component/proginfo.c +++ b/libraries/wuss/component/proginfo.c @@ -1,6 +1,7 @@ -/* wuss/component/proginfo.c -- a "program information" standard dialogue */ +/* wuss/component/proginfo.c -- a shared "program information" dialogue */ #include +#include #ifdef FORTIFY #include "fortify/fortify.h" @@ -9,24 +10,38 @@ #include "base/result.h" #include "wuss/task.h" +#include "wuss/window.h" +#include "wuss/component/dialogue.h" #include "wuss/component/info.h" #include "wuss/component/proginfo.h" -/* ----------------------------------------------------------------------- */ +#include "../core/impl.h" -/* ponytail: this whole component is now the desc -> rows mapping below; - * wuss/component/info.c does the window, the measuring and the layout. */ +/* ----------------------------------------------------------------------- */ -result_t wuss_proginfo_create(wuss_proginfo_t **out, - wuss_task_t *task, - const wuss_proginfo_desc_t *desc) +/* Owns a wuss_info_t (window, layout) wrapped by a wuss_dialogue_t (fillout + * dispatch only -- wuss_dialogue_create_on_window, so it does not own or + * close info's window). pending is the desc last passed to + * wuss_proginfo_set_desc, applied to info by wuss_proginfo_handle_pre_show; + * it is only read back then, so no copy is needed. */ +struct wuss_proginfo { - wuss_info_row_t rows[4]; - int nrows; + wuss_alloc_t alloc; /* copied hooks; wuss_t itself not retained */ + wuss_info_t *info; + wuss_dialogue_t *dialogue; + wuss_proginfo_desc_t pending; +}; - if (out == NULL || task == NULL || desc == NULL || desc->name == NULL) - return result_NULL_ARG; +/* ----------------------------------------------------------------------- */ + +/* Fills rows (>= 4 entries) from desc's non-NULL fields, in Name, Purpose, + * Author, Version order. Shared by wuss_proginfo_create (initial layout) and + * proginfo_fillout (relayout on show). */ +static int proginfo_rows(const wuss_proginfo_desc_t *desc, + wuss_info_row_t *rows) +{ + int nrows; nrows = 0; if (desc->name != NULL) @@ -54,5 +69,86 @@ result_t wuss_proginfo_create(wuss_proginfo_t **out, nrows++; } - return wuss_info_create(out, task, "About this program", rows, nrows); + return nrows; +} + +static result_t proginfo_fillout(void *opaque) +{ + wuss_proginfo_t *pi; + wuss_info_row_t rows[4]; + int nrows; + + pi = opaque; + nrows = proginfo_rows(&pi->pending, rows); + + return wuss_info_set_rows(pi->info, rows, nrows); +} + +result_t wuss_proginfo_create(wuss_proginfo_t **out, + wuss_task_t *task, + const wuss_proginfo_desc_t *desc) +{ + result_t rc; + wuss_proginfo_t *pi; + wuss_info_row_t rows[4]; + int nrows; + + if (out == NULL || task == NULL || desc == NULL || desc->name == NULL) + return result_NULL_ARG; + + pi = task->wuss->alloc.malloc(sizeof(*pi)); + if (pi == NULL) + return result_OOM; + pi->alloc = task->wuss->alloc; + pi->pending = *desc; + + nrows = proginfo_rows(desc, rows); + rc = wuss_info_create(&pi->info, task, "About this program", rows, nrows); + if (rc != result_OK) + { + task->wuss->alloc.free(pi); + return rc; + } + + rc = wuss_dialogue_create_on_window(&pi->dialogue, task->wuss, + wuss_info_window(pi->info), + proginfo_fillout, pi); + if (rc != result_OK) + { + wuss_info_destroy(pi->info); + task->wuss->alloc.free(pi); + return rc; + } + + *out = pi; + return result_OK; +} + +void wuss_proginfo_destroy(wuss_proginfo_t *doomed) +{ + wuss_alloc_t alloc; + + if (doomed == NULL) + return; + + alloc = doomed->alloc; + wuss_dialogue_destroy(doomed->dialogue); + wuss_info_destroy(doomed->info); + alloc.free(doomed); +} + +void wuss_proginfo_set_desc(wuss_proginfo_t *pi, + const wuss_proginfo_desc_t *desc) +{ + pi->pending = *desc; +} + +result_t wuss_proginfo_handle_pre_show(wuss_proginfo_t *pi) +{ + return wuss_dialogue_handle_pre_show(pi->dialogue); +} + +wuss_window_t *wuss_proginfo_window(const wuss_proginfo_t *pi) +{ + return pi ? wuss_dialogue_window(pi->dialogue) : NULL; } diff --git a/libraries/wuss/test/tasks/image.c b/libraries/wuss/test/tasks/image.c index 5ddaad31..aa210bb9 100644 --- a/libraries/wuss/test/tasks/image.c +++ b/libraries/wuss/test/tasks/image.c @@ -385,6 +385,11 @@ result_t image_handle(wuss_window_t *window, ic->menu_handle = NULL; /* wuss closed the chain under us */ return result_OK; + case wuss_EVENT_PRE_SHOW: + if (window == wuss_proginfo_window(ic->proginfo)) + return wuss_proginfo_handle_pre_show(ic->proginfo); + return result_OK; + case wuss_EVENT_QUIT: /* close any open chain first: it may hold the proginfo window as a * borrowed wuss_menu_item_t.window, and destroying that below would leave diff --git a/libraries/wuss/test/tasks/saturn.c b/libraries/wuss/test/tasks/saturn.c index 0a348682..84fc6e73 100644 --- a/libraries/wuss/test/tasks/saturn.c +++ b/libraries/wuss/test/tasks/saturn.c @@ -466,7 +466,7 @@ static result_t saturn_conf_dialogue_create(saturn_task_t *task) return rc; rc = wuss_dialogue_create(&task->conf.dialogue, task->delegate, sz, - "Configuration", saturn_conf_fillout, task); + "Configuration", saturn_conf_fillout, task); if (rc != result_OK) return rc; From e273f49b2701bbd8e25f60f4ff04809aad1c16c0 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Wed, 16 Sep 2026 10:25:59 +0100 Subject: [PATCH 099/128] fix(bmfonts): Fix MS Sans Serif advance widths --- resources/bmfonts/MS Sans Serif.png | Bin 1802 -> 1803 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/resources/bmfonts/MS Sans Serif.png b/resources/bmfonts/MS Sans Serif.png index 969ba1e69084896336ec315b97e65e597e4d6808..70c40692c75744dd057f74d03c486fab6fa3ce8c 100644 GIT binary patch delta 1763 zcmV<91|0c{4vP+uJb$`LL_t(|+U;7+isLvCu7Ny24zuqggZu7*m^t=SEPEQ9N69hd zDQr;gHpKKXPcihV@s_GoQb~4_?V0v00}~3hES2=>_fx6Va5}&q;DmC3(;iMo7h=kR zmI_nP^po9jV1{oCAyFQ&F)SJqQ5Y{aU8CPXR&?tlkH14vbqx zcL0n4a3lbOgcm8Ev+N<2pnr*3r=tr=m`xAhb64b19&qS( zA6#!m(@@bibO4u6rlTD{(w?kobz|IY6$fCraVewZ1`>5@m<1abZlK;p9^+MuG2?65 zN?ky40Vu!&R9i{kdSweaK)cDQ9?WiQg{QdEh(Q2MuGQxm0C<4j$(BNBw>b^t)_nj_ z2|#%l0Dmj7%p7w7)TpJ>n&zXZ95rcxvh)Bv?mPz?pd;lDJg^#~FIB&t^PJNk@~A} zbKs&6XNXFLxKh?<)5FZr3@LlIM=9bNSL=W(dyMwzkxCtjhVPkQ3z{m#};M0rd9((jf`}#SH+T&aK|M&ojiH_1dyy9vvyd_N8OZ#v7v? z`S$7}n9ID&M2nP(hAeXG^*ViHo0Srq$A4^UZZ1Tlau@foO;`3Zo7NI(W?9W*D*c|9 zZPUZ=Ms~j(;A~a~=(oQ+K)v=H^lJm`;LjvK0QlalY}u{8lUKJ*TQNYj@6$U+Ptiqs z0Dsv(O70O7D^~ki!>#{+^_wmV+WS8aa8YMHdqB^Xzeli~mE~HI^kVe{n{FqOUVrW& zv3x=G2CD+Cm44$;qfJOVrj&!%249?scN@;FHcsqDd49Xp9O9-GMdFx!Dt+s0deyE~ z4@zkYOZ3@|;=UNf*m8~ilrYuPMmaadhA2{iow)yczW9T3_3f1DSV#|Cb>h}6%h=?d z3~|Ahr}6?~fD0kJ%t#No z%(;?THE|W)2jCv}CCx$U8XbqosPXMof#L2lp98pA+);((obrCtk?!~t*_UjOhTrRq zZflkDwaFF!;aa}BuwgFB-XhG>Q^OrEsZ|?r;r(E4YQF1$Xzm)Oqq9wqGk-blqg!+z zU!j;m7aXVu<#r8n^y%JF@7@Dw?{(_3*vW%I@RIQcylmqW0>*o_x;U{{o_OyE!*Lu; z+*W~Y26ZzHv{8t23o9y2@&IFH4~2$$fL0<-YQvto>F4fT_(z4r3y9~*{c>!H- z5R@`p1 z(JGn)IOoZ{2h2szA(~0uxVyiSaCyJ-HJ==)T9{&W${Q8=`<89mt$%Hse*efhR0T#H}^V=mG(P=6xR?v0rYs!b&B)dD8? zIW6?348Xlt8&fNQdI11ncJy>|Vofj|_?!#?Kmpjs)L<^#1Yj%yxDj;J=t(qtlK}M6 zY!?7J8bM82qh#_31_dyr0B6^&6o6|DV#OTbLaG{|TLExtuKQ+FA=Xi1#T^E_KpexcWi6ga`P zUPRw%CaW-?4U^328zoaV^PPW=J`c>G$fgJ3MM~!^dw)nJXu2Z&al*6dK^XUrJjxSp z`rQY&+mIO=+J*>l`2;=f_=)zUrPb}yuvHx3o*T@Wq%@Eys<|Sw!RN-GQp2|^>8B>>mUUI?bw02f|@*(z!x0B33dpAZ0? znNQeAuTIbi@M7oodA5!VTYNg2M9RF*?G+PGGk+#9^BEHpmp)RO$kOFF?tFPc1(&d2 zg_{ExeKgiU>{n_-ea5O{Cp8ZjFUXKeMMVVL{{86rDkZ3#e(etyEd$Bacji%or z9@db`+X2Rm?8XHEq+PfH05<`g7aFJN9sm^xz{Cd*ojDUNVV8K2rkN^s4dzU8G&Mv4 zjeod9>`jeyWaU%D9S1E`tr6O&g^QY0rxN!|0|d4hG{B_VwGgX;oUWdWQh-qa&cwAB zxxy4ipgRsG2dGy7h7!Pd3m~1M08p&}_heQlYhi0 zcaT`WqIQE<;k=b`gQ!7)cML~T2RVB?3FDz8#+rL18WVb}9bR0I*Aw!C9UW{%x2&Cah zb?q8yKj-mH+Tl@N;p(@|jeAE8TZ2GE{eSg!RALyGFKFvEe z-LJFh{(bVtebeE?0Rrz|$Xg0W{MvbNW=&NKpiqUE=?IGVAJP%5od*MHX0?`8g6I0~ zA)nu@=>evt{FCSCo!lEAK-DsY(51-8F;p=A^M5Lkg$Xsl1ZeAM1 zADJ2epdgmMrk4SV6%q5USreW-_PL|RA$qs-zL#V8nk=<2eu$D;f+%|IRF3v07*qoM6N<$ Eg7q;ks{jB1 From 72023389207476f5c5f220c2e25915afde69ac23 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Wed, 16 Sep 2026 12:02:42 +0100 Subject: [PATCH 100/128] feat(wuss): add Info row to Saturn task's menu Hangs the shared wuss_proginfo_t dialogue off a new top menu item, same pattern as tasks/image.c. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/test/tasks/saturn.c | 34 +++++++++++++++++++++++++----- libraries/wuss/test/tasks/saturn.h | 2 ++ 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/libraries/wuss/test/tasks/saturn.c b/libraries/wuss/test/tasks/saturn.c index 84fc6e73..37303b2f 100644 --- a/libraries/wuss/test/tasks/saturn.c +++ b/libraries/wuss/test/tasks/saturn.c @@ -19,6 +19,7 @@ #include "geom/stack.h" #include "utils/rng.h" #include "wuss/component/dialogue.h" +#include "wuss/component/proginfo.h" #include "wuss/menu.h" #include "saturn.h" @@ -77,10 +78,11 @@ static int saturn_rnd(int n) /* MENU click over the content pops this. "Colours" leads to a submenu with * one row per task->fg/task->bg, each of which pops a wuss_colourmenu (see * saturn_create -- the two leaf items' submenu pointers are patched in there, - * once the colourmenus exist). "Configuration" hover-opens task->size_dialogue, a - * borrowed window built once in saturn_create and patched into - * g_saturn_menu_items[SATURN_MENU_SIZE].window there, the same way. */ -enum { SATURN_MENU_COLOURS = 0, SATURN_MENU_SIZE }; + * once the colourmenus exist). "Configuration" and "Info" hover-open + * task->size_dialogue / task->proginfo's window, both borrowed windows built + * once in saturn_create and patched into g_saturn_menu_items[...].window + * there, the same way. */ +enum { SATURN_MENU_INFO = 0, SATURN_MENU_COLOURS, SATURN_MENU_SIZE }; enum { SATURN_COLOURS_MENU_FOREGROUND = 0, SATURN_COLOURS_MENU_BACKGROUND }; /* the size dialogue's icons, in creation order (see saturn_conf_dialogue_create): @@ -148,6 +150,7 @@ static wuss_menu_t g_saturn_colours_menu = static wuss_menu_item_t g_saturn_menu_items[] = { + { "Info", wuss_MENU_ITEM_BORROWED_SUBMENU, NULL, NULL, 0 }, { "Colours", wuss_MENU_ITEM_NONE, &g_saturn_colours_menu, NULL, 0 }, { "Configuration", wuss_MENU_ITEM_BORROWED_SUBMENU, NULL, NULL, 0 } }; @@ -184,6 +187,7 @@ result_t saturn_create(wuss_t *wuss, memset(task->conf.rows, 0, sizeof(task->conf.rows)); task->conf.cancel = NULL; task->conf.apply = NULL; + task->proginfo = NULL; /* saturn_redraw paints its own background */ delegate_desc.handle = saturn_handle; @@ -229,10 +233,27 @@ result_t saturn_create(wuss_t *wuss, rc = saturn_conf_dialogue_create(task); if (rc != result_OK) goto fail_bg_colourmenu; /* wuss_task_destroy closes task->window too */ - + g_saturn_menu_items[SATURN_MENU_SIZE].window = wuss_dialogue_window(task->conf.dialogue); + /* The "Info" menu row's standard dialogue. A create failure is non-fatal + * -- the task just runs without an Info dialogue (see image.c). */ + { + static const wuss_proginfo_desc_t desc = + { + "Saturn", + "Elite loading-screen planet, recreated", + "(c) DPTLib contributors", + "1.0 (" __DATE__ ")" + }; + + if (wuss_proginfo_create(&task->proginfo, delegate, &desc) != result_OK) + task->proginfo = NULL; + } + g_saturn_menu_items[SATURN_MENU_INFO].window = + wuss_proginfo_window(task->proginfo); + return result_OK; fail_bg_colourmenu: @@ -701,6 +722,8 @@ result_t saturn_handle(wuss_window_t *window, case wuss_EVENT_PRE_SHOW: if (window == wuss_dialogue_window(task->conf.dialogue)) return wuss_dialogue_handle_pre_show(task->conf.dialogue); + if (window == wuss_proginfo_window(task->proginfo)) + return wuss_proginfo_handle_pre_show(task->proginfo); return result_OK; case wuss_EVENT_MENU_SELECT: @@ -714,6 +737,7 @@ result_t saturn_handle(wuss_window_t *window, wuss_colourmenu_destroy(task->fg_colourmenu); wuss_colourmenu_destroy(task->bg_colourmenu); wuss_dialogue_destroy(task->conf.dialogue); + wuss_proginfo_destroy(task->proginfo); free(task); /* task_data was calloc'd per instance by the spawner */ return result_OK; diff --git a/libraries/wuss/test/tasks/saturn.h b/libraries/wuss/test/tasks/saturn.h index a704e384..30f5bf38 100644 --- a/libraries/wuss/test/tasks/saturn.h +++ b/libraries/wuss/test/tasks/saturn.h @@ -10,6 +10,7 @@ #include "framebuf/colour.h" #include "wuss/component/colourmenu.h" #include "wuss/component/dialogue.h" +#include "wuss/component/proginfo.h" #include "wuss/icon-spec.h" #include "wuss/icon.h" #include "wuss/menu.h" @@ -75,6 +76,7 @@ typedef struct saturn_task wuss_colourmenu_t *fg_colourmenu, *bg_colourmenu; wuss_menu_handle_t menu_handle; /* live only between open and a SELECT pick */ saturn_conf_t conf; + wuss_proginfo_t *proginfo; } saturn_task_t; From a6593a93a9a48b8576b53c841cca8ad166c1c225 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Wed, 16 Sep 2026 12:30:20 +0100 Subject: [PATCH 101/128] refactor(geom): tighten stack solver declaration alignment Column now sits one space past each block's own longest type, rather than a wider shared column; also collapses a couple of trivial if/else pairs into ternaries. Co-Authored-By: Claude Sonnet 5 --- libraries/geom/stack/measure-min.c | 11 +++----- libraries/geom/stack/solve.c | 40 +++++++++++++++--------------- 2 files changed, 24 insertions(+), 27 deletions(-) diff --git a/libraries/geom/stack/measure-min.c b/libraries/geom/stack/measure-min.c index 2e5d7900..68fd41e0 100644 --- a/libraries/geom/stack/measure-min.c +++ b/libraries/geom/stack/measure-min.c @@ -53,25 +53,22 @@ static void stack__measure(const stack_item_t *items, child_cross = items[i].cross_size; } - main_min = main_min + child_main + (nchildren > 0 ? items[index].gap : 0); + main_min += child_main + (nchildren > 0 ? items[index].gap : 0); cross_min = MAX(cross_min, child_cross); nchildren++; } - if (items[index].axis_size) - main_min = MAX(main_min, items[index].axis_size); - else - main_min = MAX(main_min, items[index].min); + main_min = MAX(main_min, items[index].axis_size ? items[index].axis_size : items[index].min); if (horiz) { - mins[index].w = main_min + items[index].pad_l + items[index].pad_r; + mins[index].w = main_min + items[index].pad_l + items[index].pad_r; mins[index].h = cross_min + items[index].pad_t + items[index].pad_b; } else { mins[index].w = cross_min + items[index].pad_l + items[index].pad_r; - mins[index].h = main_min + items[index].pad_t + items[index].pad_b; + mins[index].h = main_min + items[index].pad_t + items[index].pad_b; } } diff --git a/libraries/geom/stack/solve.c b/libraries/geom/stack/solve.c index 4be209a6..c8581a30 100644 --- a/libraries/geom/stack/solve.c +++ b/libraries/geom/stack/solve.c @@ -32,11 +32,11 @@ static int stack__distribute(const stack_item_t *items, int avail, stack__child_t *children) { - int nchildren; - int used; - int slack; - int i; - int round; + int nchildren; + int used; + int slack; + int i; + int round; nchildren = 0; used = 0; @@ -86,7 +86,7 @@ static int stack__distribute(const stack_item_t *items, share = slack * items[children[c].index].flex / flexweight; children[c].extent += share; - given += share; + given += share; } for (c = 0; c < nchildren && given < slack; c++) @@ -108,7 +108,7 @@ static int stack__distribute(const stack_item_t *items, if (item_max > 0 && children[c].extent > item_max) { given -= children[c].extent - item_max; - children[c].extent = item_max; + children[c].extent = item_max; children[c].clamped = 1; } } @@ -126,15 +126,15 @@ static void stack__place_container(const stack_item_t *items, int index, box_t *out) { - int horiz; - stack__child_t children[n]; /* upper bound: at most n-1 children */ - int nchildren; - const box_t *box; - int inner_main0, inner_main1; - int cross0, cross1; - int avail; - int pos; - int c; + int horiz; + stack__child_t children[n]; /* upper bound: at most n-1 children */ + int nchildren; + const box_t *box; + int inner_main0, inner_main1; + int cross0, cross1; + int avail; + int pos; + int c; horiz = (items[index].kind == stack_KIND_HBOX); box = &out[index]; @@ -167,10 +167,10 @@ static void stack__place_container(const stack_item_t *items, for (c = 0; c < nchildren; c++) { - int ci; - box_t *cbox; - int cross_extent; - int cross_pos; + int ci; + box_t *cbox; + int cross_extent; + int cross_pos; ci = children[c].index; cbox = &out[ci]; From e4e4fa1b98c43f268bd2bd78329e102a9c7f84cb Mon Sep 17 00:00:00 2001 From: David Thomas Date: Wed, 16 Sep 2026 12:54:42 +0100 Subject: [PATCH 102/128] style: tighten declaration-block alignment across libraries/ Adds tools/realign_decls.py, which re-pads the type-to-name column of contiguous same-indent variable declaration blocks so it sits exactly one space past the block's own longest type token, matching the tighter style already adopted in geom/stack. Handles pointer '*' columns, array subscripts, blank/comment lines within an otherwise-continuous run, and call-bearing initializers (counted for width, never rewritten). Skips struct/enum/union bodies. Run across all of libraries/ (excluding fortify/ and test/); build and full test suite verified passing. Co-Authored-By: Claude Sonnet 5 --- libraries/databases/filename-db/filename-db.c | 8 +- libraries/databases/pickle/unpickle.c | 12 +- libraries/databases/tag-db/tag-db.c | 14 +- libraries/datastruct/atom/new.c | 16 +- libraries/datastruct/bitvec/ensure.c | 4 +- libraries/datastruct/cache/cache.c | 4 +- libraries/datastruct/vector/set-width.c | 6 +- libraries/framebuf/bitmap/load.c | 10 +- libraries/framebuf/bitmap/save.c | 6 +- libraries/framebuf/bmfont/bmfont.c | 8 +- libraries/framebuf/bmfont/enumerate.c | 4 +- libraries/framebuf/pixelmap/get.c | 10 +- libraries/framebuf/screen/screen-copy-rect.c | 6 +- libraries/framebuf/screen/screen-draw.c | 4 +- libraries/framebuf/span/p1.c | 8 +- libraries/framebuf/span/p4.c | 8 +- libraries/io/dirscan/dirscan.c | 4 +- libraries/io/path/path.c | 6 +- libraries/wuss/component/dialogue.c | 6 +- libraries/wuss/component/proginfo.c | 12 +- libraries/wuss/core/create.c | 10 +- libraries/wuss/core/mouse-click.c | 10 +- libraries/wuss/core/set-backdrop.c | 4 +- libraries/wuss/core/window/close.c | 6 +- libraries/wuss/core/window/create-placed.c | 16 +- libraries/wuss/core/window/create.c | 4 +- libraries/wuss/core/window/invalidate.c | 2 +- libraries/wuss/core/window/move.c | 12 +- libraries/wuss/menu/menu.c | 6 +- tools/realign_decls.py | 213 ++++++++++++++++++ 30 files changed, 326 insertions(+), 113 deletions(-) create mode 100644 tools/realign_decls.py diff --git a/libraries/databases/filename-db/filename-db.c b/libraries/databases/filename-db/filename-db.c index 9ba41f73..e639c6f2 100644 --- a/libraries/databases/filename-db/filename-db.c +++ b/libraries/databases/filename-db/filename-db.c @@ -106,7 +106,7 @@ static result_t unformat_key(const char *buf, void **key, void *opaque) { - result_t err; + result_t err; unsigned char hash[digestdb_DIGESTSZ]; int kindex; @@ -134,7 +134,7 @@ static result_t unformat_value(const char *buf, void **value, void *opaque) { - result_t err; + result_t err; filenamedb_t *db = opaque; atom_t vindex; @@ -164,7 +164,7 @@ static const pickle_unformat_methods_t unformat_methods = result_t filenamedb_open(const char *filename, filenamedb_t **pdb) { - result_t err; + result_t err; char *filenamecopy = NULL; atom_set_t *filenames = NULL; hash_t *hash = NULL; @@ -316,7 +316,7 @@ result_t filenamedb_add(filenamedb_t *db, const char *id, const char *filename) { - result_t err; + result_t err; int kindex; atom_t vindex; const unsigned char *key; diff --git a/libraries/databases/pickle/unpickle.c b/libraries/databases/pickle/unpickle.c index a0c2be20..aa803b22 100644 --- a/libraries/databases/pickle/unpickle.c +++ b/libraries/databases/pickle/unpickle.c @@ -46,13 +46,13 @@ static result_t unpickle__parse_line(unpickle__state_t *state, { result_t err; - char *key; - char *keyend; - void *fmtkey; + char *key; + char *keyend; + void *fmtkey; - char *value; - char *valueend; - void *fmtvalue; + char *value; + char *valueend; + void *fmtvalue; key = buf; diff --git a/libraries/databases/tag-db/tag-db.c b/libraries/databases/tag-db/tag-db.c index 95865d17..af6b12d5 100644 --- a/libraries/databases/tag-db/tag-db.c +++ b/libraries/databases/tag-db/tag-db.c @@ -210,7 +210,7 @@ static void destroy_hash_value(void *value) result_t tagdb_open(const char *filename, tagdb_t **pdb) { - result_t err; + result_t err; char *filenamecopy = NULL; atom_set_t *tags = NULL; hash_t *hash = NULL; @@ -890,9 +890,9 @@ static int getidbytags_cb(const void *key, const void *value, void *opaque) { result_t err; struct enumerate_state *state = opaque; - const bitvec_t *v; - bitvec_t *have; - int eq; + const bitvec_t *v; + bitvec_t *have; + int eq; /* work out where we are by counting callbacks (ugh) */ if (state->count++ < state->start) @@ -927,9 +927,9 @@ result_t tagdb_enumerate_ids_by_tags(tagdb_t *db, unsigned char *buf, size_t bufsz) { - result_t err; - bitvec_t *want = NULL; - int i; + result_t err; + bitvec_t *want = NULL; + int i; struct enumerate_state state; assert(db); diff --git a/libraries/datastruct/atom/new.c b/libraries/datastruct/atom/new.c index f8a3b23a..d6cbc01a 100644 --- a/libraries/datastruct/atom/new.c +++ b/libraries/datastruct/atom/new.c @@ -155,14 +155,14 @@ result_t atom_new(atom_set_t *s, size_t sizet_length, atom_t *patom) { - result_t err; - atom_t atom; - int length; - locpool_t *pend; - locpool_t *p; - loc_t *lend; - loc_t *l; - unsigned int i; + result_t err; + atom_t atom; + int length; + locpool_t *pend; + locpool_t *p; + loc_t *lend; + loc_t *l; + unsigned int i; assert(s); assert(block); diff --git a/libraries/datastruct/bitvec/ensure.c b/libraries/datastruct/bitvec/ensure.c index 02f74c0c..249a1945 100644 --- a/libraries/datastruct/bitvec/ensure.c +++ b/libraries/datastruct/bitvec/ensure.c @@ -17,8 +17,8 @@ result_t bitvec_ensure(bitvec_t *v, unsigned int need) { if (need > v->length) { - unsigned int length; - bitvec_T *vec; + unsigned int length; + bitvec_T *vec; length = need; diff --git a/libraries/datastruct/cache/cache.c b/libraries/datastruct/cache/cache.c index d911b8be..623fd7a6 100644 --- a/libraries/datastruct/cache/cache.c +++ b/libraries/datastruct/cache/cache.c @@ -581,8 +581,8 @@ result_t cache_put(cache_t *c, { const size_t quantum = sizeof(free_t); - size_t rounded_length; - void *storeptr; + size_t rounded_length; + void *storeptr; assert(c != NULL); assert(data != NULL); diff --git a/libraries/datastruct/vector/set-width.c b/libraries/datastruct/vector/set-width.c index 26d4fd61..705038d0 100644 --- a/libraries/datastruct/vector/set-width.c +++ b/libraries/datastruct/vector/set-width.c @@ -15,9 +15,9 @@ result_t vector_set_width(vector_t *v, size_t width) { - void *newbase; - size_t currsz; - size_t newsz; + void *newbase; + size_t currsz; + size_t newsz; if (width == 0) return result_BAD_ARG; diff --git a/libraries/framebuf/bitmap/load.c b/libraries/framebuf/bitmap/load.c index fe0992d6..28be7f01 100644 --- a/libraries/framebuf/bitmap/load.c +++ b/libraries/framebuf/bitmap/load.c @@ -94,11 +94,11 @@ result_t bitmap_load_png(bitmap_t *bm, const char *filename) * carrying its PLTE across as the bitmap's palette, rather than * expanding to RGB */ { - png_colorp png_palette; - int nplte; - png_bytep trns = NULL; - int ntrns = 0; - int i; + png_colorp png_palette; + int nplte; + png_bytep trns = NULL; + int ntrns = 0; + int i; png_set_packing(png_ptr); diff --git a/libraries/framebuf/bitmap/save.c b/libraries/framebuf/bitmap/save.c index c5eea7be..c30590cb 100755 --- a/libraries/framebuf/bitmap/save.c +++ b/libraries/framebuf/bitmap/save.c @@ -16,9 +16,9 @@ result_t bitmap_save_png(const bitmap_t *bm, const char *filename) result_t rc; volatile pixelfmt_t fmt; volatile size_t bytespp; - volatile int paletted; - FILE *fp; - png_structp png_ptr = NULL; + volatile int paletted; + FILE *fp; + png_structp png_ptr = NULL; png_infop volatile info_ptr = NULL; png_bytep volatile outrow = NULL; png_colorp volatile png_plte = NULL; diff --git a/libraries/framebuf/bmfont/bmfont.c b/libraries/framebuf/bmfont/bmfont.c index abb75d64..49d22eab 100644 --- a/libraries/framebuf/bmfont/bmfont.c +++ b/libraries/framebuf/bmfont/bmfont.c @@ -203,7 +203,7 @@ static int row_is_grid(const unsigned char *pixels, png_uint_32 y) { const unsigned char *row = pixels + rowbytes * y; - png_uint_32 x; + png_uint_32 x; for (x = 0; x < gridwidth; x++) { @@ -1720,9 +1720,9 @@ static void bmfont_drawchar_p8_2w_o(void *vscreen, pixelfmt_any_t fg, pixelfmt_any_t bg) { - unsigned char *scr = vscreen; - const unsigned short *gly = vglyph; - int stride; + unsigned char *scr = vscreen; + const unsigned short *gly = vglyph; + int stride; NOT_USED(shift); diff --git a/libraries/framebuf/bmfont/enumerate.c b/libraries/framebuf/bmfont/enumerate.c index fe1f8192..4343a711 100644 --- a/libraries/framebuf/bmfont/enumerate.c +++ b/libraries/framebuf/bmfont/enumerate.c @@ -23,8 +23,8 @@ bmfont_enumerate_ctx_t; static result_t bmfont_enumerate_entry(const char *leaf, void *opaque) { bmfont_enumerate_ctx_t *ctx; - char name[256]; - char path[512]; + char name[256]; + char path[512]; ctx = opaque; diff --git a/libraries/framebuf/pixelmap/get.c b/libraries/framebuf/pixelmap/get.c index 720a986b..f04cf771 100644 --- a/libraries/framebuf/pixelmap/get.c +++ b/libraries/framebuf/pixelmap/get.c @@ -175,12 +175,12 @@ const pixelmap_t *pixelmap_get(pixelfmt_t srcfmt, int nentries) { static struct pixelmap__cacheent cache[PIXELMAP_CACHE_SIZE]; - static unsigned int clock; + static unsigned int clock; - unsigned int palhash; - int victim; - unsigned int oldest; - int i; + unsigned int palhash; + int victim; + unsigned int oldest; + int i; if (palette == NULL) return NULL; diff --git a/libraries/framebuf/screen/screen-copy-rect.c b/libraries/framebuf/screen/screen-copy-rect.c index 00ec31c2..11b0da16 100644 --- a/libraries/framebuf/screen/screen-copy-rect.c +++ b/libraries/framebuf/screen/screen-copy-rect.c @@ -56,9 +56,9 @@ static result_t screen_copy_rect_packed(screen_t *scr, { int sx, sy, dxp, dyp; const unsigned char *scrp_s; - unsigned char *scrp_d; - int shift_s, shift_d; - unsigned char pix; + unsigned char *scrp_d; + int shift_s, shift_d; + unsigned char pix; sx = s->x0 + col; sy = s->y0 + row; dxp = d->x0 + col; dyp = d->y0 + row; diff --git a/libraries/framebuf/screen/screen-draw.c b/libraries/framebuf/screen/screen-draw.c index 4a5a67b4..8e885f88 100644 --- a/libraries/framebuf/screen/screen-draw.c +++ b/libraries/framebuf/screen/screen-draw.c @@ -798,8 +798,8 @@ static result_t screen_copy_bitmap_32(screen_t *scr, for (yy = 0; yy < clipped_height; yy++) { pixelfmt_any32_t *dstpx; - int srcy; - int remaining, done; + int srcy; + int remaining, done; srcy = draw_box->y0 - y + yy; dstpx = dstrow; diff --git a/libraries/framebuf/span/p1.c b/libraries/framebuf/span/p1.c index b1e32d56..9e529a98 100644 --- a/libraries/framebuf/span/p1.c +++ b/libraries/framebuf/span/p1.c @@ -36,11 +36,11 @@ static void span_p1_blendconst(void *vdst, while (length--) { - colour_t old_colour, new_colour, blended; + colour_t old_colour, new_colour, blended; pixelfmt_rgba8888_t oldpx, newpx; - int old_r, old_g, old_b; - int new_r, new_g, new_b; - int blend_r, blend_g, blend_b; + int old_r, old_g, old_b; + int new_r, new_g, new_b; + int blend_r, blend_g, blend_b; old_colour = palette[*psrc1 & 1]; oldpx = old_colour.primary; diff --git a/libraries/framebuf/span/p4.c b/libraries/framebuf/span/p4.c index e612bce2..c0848b45 100644 --- a/libraries/framebuf/span/p4.c +++ b/libraries/framebuf/span/p4.c @@ -36,11 +36,11 @@ static void span_p4_blendconst(void *vdst, while (length--) { - colour_t old_colour, new_colour, blended; + colour_t old_colour, new_colour, blended; pixelfmt_rgba8888_t oldpx, newpx; - int old_r, old_g, old_b; - int new_r, new_g, new_b; - int blend_r, blend_g, blend_b; + int old_r, old_g, old_b; + int new_r, new_g, new_b; + int blend_r, blend_g, blend_b; old_colour = palette[*psrc1]; oldpx = old_colour.primary; diff --git a/libraries/io/dirscan/dirscan.c b/libraries/io/dirscan/dirscan.c index fd70def7..c4fa0850 100644 --- a/libraries/io/dirscan/dirscan.c +++ b/libraries/io/dirscan/dirscan.c @@ -111,8 +111,8 @@ result_t dirscan_walk(const char *dir, dirscan_fn *fn, void *opaque) result_t dirscan_walk(const char *dir, dirscan_fn *fn, void *opaque) { - result_t rc; - DIR *dp; + result_t rc; + DIR *dp; struct dirent *de; if (dir == NULL || fn == NULL) diff --git a/libraries/io/path/path.c b/libraries/io/path/path.c index 65bba30d..d2b18e17 100644 --- a/libraries/io/path/path.c +++ b/libraries/io/path/path.c @@ -43,9 +43,9 @@ const char *path_join_filename(const char *root, int nbranches, ...) #else "/"; #endif - va_list args; - size_t used; - int rc; + va_list args; + size_t used; + int rc; assert(root); assert(nbranches < 1000); diff --git a/libraries/wuss/component/dialogue.c b/libraries/wuss/component/dialogue.c index 3677d1ed..f4974877 100644 --- a/libraries/wuss/component/dialogue.c +++ b/libraries/wuss/component/dialogue.c @@ -67,9 +67,9 @@ result_t wuss_dialogue_create(wuss_dialogue_t **out, wuss_dialogue_fillout_fn_t *fillout, void *opaque) { - result_t rc; - wuss_dialogue_t *dialogue; - box_t content; + result_t rc; + wuss_dialogue_t *dialogue; + box_t content; if (out == NULL || task == NULL) return result_NULL_ARG; diff --git a/libraries/wuss/component/proginfo.c b/libraries/wuss/component/proginfo.c index 21e8f467..bf064ebe 100644 --- a/libraries/wuss/component/proginfo.c +++ b/libraries/wuss/component/proginfo.c @@ -75,8 +75,8 @@ static int proginfo_rows(const wuss_proginfo_desc_t *desc, static result_t proginfo_fillout(void *opaque) { wuss_proginfo_t *pi; - wuss_info_row_t rows[4]; - int nrows; + wuss_info_row_t rows[4]; + int nrows; pi = opaque; nrows = proginfo_rows(&pi->pending, rows); @@ -88,10 +88,10 @@ result_t wuss_proginfo_create(wuss_proginfo_t **out, wuss_task_t *task, const wuss_proginfo_desc_t *desc) { - result_t rc; - wuss_proginfo_t *pi; - wuss_info_row_t rows[4]; - int nrows; + result_t rc; + wuss_proginfo_t *pi; + wuss_info_row_t rows[4]; + int nrows; if (out == NULL || task == NULL || desc == NULL || desc->name == NULL) return result_NULL_ARG; diff --git a/libraries/wuss/core/create.c b/libraries/wuss/core/create.c index bf123f89..4581c7bd 100644 --- a/libraries/wuss/core/create.c +++ b/libraries/wuss/core/create.c @@ -63,15 +63,15 @@ result_t wuss_create(screen_t *scr, const wuss_alloc_t *alloc, wuss_t **wuss) { - wuss_alloc_t al; - wuss_t *w; + wuss_alloc_t al; + wuss_t *w; #ifdef WUSS_FURNITURE wuss_furniture_palette_t pal; - wuss_colour_t bg, fg; + wuss_colour_t bg, fg; #endif #if defined(WUSS_FURNITURE) || defined(WUSS_ICONS) - wuss_colour_t blight, bdark, bdivider; - wuss_colour_t btnbg, btnfg, btnpressed, accent; + wuss_colour_t blight, bdark, bdivider; + wuss_colour_t btnbg, btnfg, btnpressed, accent; #endif #ifdef WUSS_FURNITURE int font_height; diff --git a/libraries/wuss/core/mouse-click.c b/libraries/wuss/core/mouse-click.c index 7fce2322..e04dceda 100644 --- a/libraries/wuss/core/mouse-click.c +++ b/libraries/wuss/core/mouse-click.c @@ -8,9 +8,9 @@ result_t wuss_mouse_click(wuss_t *wuss, wuss_mouse_action_t action, wuss_window_t **hit) { - wuss_window_t *win; - wuss_event_t event; - int x, y; + wuss_window_t *win; + wuss_event_t event; + int x, y; x = p.x; y = p.y; @@ -287,8 +287,8 @@ result_t wuss_mouse_click(wuss_t *wuss, if (win->task->handle != NULL) { - box_t content; - point_t doc_point; + box_t content; + point_t doc_point; wuss__content_box(win, &content); doc_point.x = x - content.x0 + win->scroll.x; diff --git a/libraries/wuss/core/set-backdrop.c b/libraries/wuss/core/set-backdrop.c index 48a9f145..c7fccdf6 100644 --- a/libraries/wuss/core/set-backdrop.c +++ b/libraries/wuss/core/set-backdrop.c @@ -8,8 +8,8 @@ result_t wuss_set_backdrop(wuss_t *wuss, const wuss_backdrop_t *backdrop) { - result_t rc; - box_t screen; + result_t rc; + box_t screen; wuss_backdrop_t resolved; assert(wuss != NULL); diff --git a/libraries/wuss/core/window/close.c b/libraries/wuss/core/window/close.c index 2f2e6dcf..8e7361d1 100644 --- a/libraries/wuss/core/window/close.c +++ b/libraries/wuss/core/window/close.c @@ -10,9 +10,9 @@ void wuss_window_close(wuss_window_t *doomed) { - wuss_task_t *task; - wuss_event_t event; - wuss_t *wuss; + wuss_task_t *task; + wuss_event_t event; + wuss_t *wuss; if (doomed == NULL) return; diff --git a/libraries/wuss/core/window/create-placed.c b/libraries/wuss/core/window/create-placed.c index 7c1b7395..da918e97 100644 --- a/libraries/wuss/core/window/create-placed.c +++ b/libraries/wuss/core/window/create-placed.c @@ -83,14 +83,14 @@ result_t wuss_window_create_placed(wuss_task_t *task, size2d_t min_doc, wuss_window_t **window) { - result_t rc; - wuss_t *wuss; - int left, top, right, bottom; - int fw, fh; - box_t screen, content, consumed; - point_t origin; - const box_t *slot; - int tracked; + result_t rc; + wuss_t *wuss; + int left, top, right, bottom; + int fw, fh; + box_t screen, content, consumed; + point_t origin; + const box_t *slot; + int tracked; assert(task != NULL); assert(window != NULL); diff --git a/libraries/wuss/core/window/create.c b/libraries/wuss/core/window/create.c index 730afc28..5b5d9b99 100644 --- a/libraries/wuss/core/window/create.c +++ b/libraries/wuss/core/window/create.c @@ -21,8 +21,8 @@ result_t wuss_window_create(wuss_task_t *task, { wuss_t *wuss; wuss_window_t *win; - int width, height, outline_px, titlebar_height; - point_t carve; + int width, height, outline_px, titlebar_height; + point_t carve; assert(task != NULL); assert(content != NULL); diff --git a/libraries/wuss/core/window/invalidate.c b/libraries/wuss/core/window/invalidate.c index 34071856..1eb9d7d5 100644 --- a/libraries/wuss/core/window/invalidate.c +++ b/libraries/wuss/core/window/invalidate.c @@ -62,7 +62,7 @@ int wuss__clip_to_visible(wuss_window_t *window, for (e = window->wuss->z_order.next; e != &window->link; e = e->next) { wuss_window_t *occluder; - int nnext, p; + int nnext, p; occluder = wuss__window_from_link(e); if (occluder->flags & wuss_WINDOW_HIDDEN) diff --git a/libraries/wuss/core/window/move.c b/libraries/wuss/core/window/move.c index b0240872..d013c7aa 100644 --- a/libraries/wuss/core/window/move.c +++ b/libraries/wuss/core/window/move.c @@ -9,12 +9,12 @@ * is nothing to rebuild. */ void wuss_window_move(wuss_window_t *window, point_t p) { - box_t clean[WUSS_MAX_INVALIDATE_PIECES]; - box_t full_dest[WUSS_MAX_INVALIDATE_PIECES]; - box_t copied[WUSS_MAX_INVALIDATE_PIECES]; - int width, height, outline_px, titlebar_height; - int dx, dy, nclean, ncopied, i; - box_t before, dirty; + box_t clean[WUSS_MAX_INVALIDATE_PIECES]; + box_t full_dest[WUSS_MAX_INVALIDATE_PIECES]; + box_t copied[WUSS_MAX_INVALIDATE_PIECES]; + int width, height, outline_px, titlebar_height; + int dx, dy, nclean, ncopied, i; + box_t before, dirty; width = window->visible.x1 - window->visible.x0; height = window->visible.y1 - window->visible.y0; diff --git a/libraries/wuss/menu/menu.c b/libraries/wuss/menu/menu.c index 4541183d..9d73a57c 100644 --- a/libraries/wuss/menu/menu.c +++ b/libraries/wuss/menu/menu.c @@ -661,9 +661,9 @@ static result_t wuss__menu_spawn(wuss_t *wuss, if (menu->title != NULL && menu->title[0] != '\0') { bmfont_t *titlefont; - int titlelen; - int split; - bmfont_width_t title_w; + int titlelen; + int split; + bmfont_width_t title_w; titlefont = (wuss->fonts.nfonts > 1 && wuss->fonts.fonts[1] != NULL) ? wuss->fonts.fonts[1] diff --git a/tools/realign_decls.py b/tools/realign_decls.py new file mode 100644 index 00000000..158885ff --- /dev/null +++ b/tools/realign_decls.py @@ -0,0 +1,213 @@ +#!/usr/bin/env python3 +"""realign_decls.py -- tighten C declaration-block alignment + +For each contiguous block of simple variable declarations at the top of a +function scope, re-pads the column between the type and the name/'*' so it +sits exactly one space past the block's own longest type token, rather than +whatever wider column happened to be in use before. + +Only touches declaration lines (`type name[, name2][ = init];`, optionally +`static`/`const` qualified, optionally array-subscripted, optionally with a +leading `*` on the name), and only inside executable-code braces -- never +inside a struct/enum/union body. Leaves function signatures, macros, +comments and all other code untouched. + +Usage: realign_decls.py FILE [FILE ...] +Edits files in place. +""" +import re +import sys + +DECL_RE = re.compile( + r'^(?P[ \t]*)' + r'(?P(?:static\s+|const\s+)*)' + r'(?P[A-Za-z_][A-Za-z0-9_]*(?:\s+(?:const|unsigned|signed|long|short|int|char)\b)*)' + r'(?P\s+)' + r'(?P\*+\s*)?' + r'(?P[A-Za-z_][A-Za-z0-9_]*)' + r'(?P(?:\s*\[[^\]]*\])*\s*(?:,\s*\*{0,3}\s*[A-Za-z_][A-Za-z0-9_]*(?:\s*\[[^\]]*\])*)*\s*(?:=[^;]*)?;.*)$' +) + +KEYWORDS = { + 'return', 'if', 'else', 'for', 'while', 'do', 'switch', 'break', + 'continue', 'goto', 'sizeof', 'typedef', 'struct', 'enum', 'union', + 'default', 'case', +} + +# a line that (on its own, or Allman-style with '{' on the next line) +# opens a struct/enum/union body -- its members must never be treated as a +# realignable declaration block +STRUCT_KW_RE = re.compile(r'\b(struct|enum|union)\b[^;{]*$') +STRUCT_OPEN_SAMELINE_RE = re.compile(r'\b(struct|enum|union)\b[^;{]*\{\s*$') +BARE_BRACE_RE = re.compile(r'^\s*\{\s*$') + +# lines that never end a declaration block and never count as a decl: +# blank lines and comment-only lines. They're transparent -- a block can +# run across them without breaking. +TRANSPARENT_RE = re.compile(r'^\s*(//.*|/\*.*\*/\s*)?$') + + +def parse_decl(line): + m = DECL_RE.match(line) + if not m: + return None + type_tok = (m.group('qual') + m.group('type')).strip() + if type_tok.split()[-1] in KEYWORDS: + return None + tail = m.group('name') + m.group('tail') + body = tail.split(';')[0] + has_call = '(' in body + return { + 'indent': m.group('indent'), + 'type': type_tok, + 'stars': (m.group('stars') or ''), + 'rest': tail, + 'editable': not has_call, # counts toward width either way + 'has_init': '=' in body, + } + + +def struct_body_lines(lines): + """Return the set of line indices that lie inside a struct/enum/union + body -- opened either by '... struct X {' on one line, or Allman-style + with the keyword line followed by a bare '{' -- so declaration scanning + can skip them.""" + in_struct = set() + stack = [] # stack of bool: True if this brace level is a struct body + pending_struct = False # previous line was a bare struct/enum/union kw + for i, line in enumerate(lines): + is_struct_open = bool(STRUCT_OPEN_SAMELINE_RE.search(line)) + if not is_struct_open and pending_struct and BARE_BRACE_RE.match(line): + is_struct_open = True + pending_struct = bool(STRUCT_KW_RE.search(line.strip())) and \ + not line.strip().endswith(';') and '{' not in line + for ch in line: + if ch == '{': + stack.append(is_struct_open) + is_struct_open = False + elif ch == '}': + if stack: + stack.pop() + if stack and stack[-1]: + in_struct.add(i) + return in_struct + + +def find_blocks(lines, skip): + """Yield (start, end, entries) for consecutive declaration lines sharing + the same indent, tolerating blank/comment-only lines in between (they + don't break the run, but aren't decls either), skipping any line index + in `skip`. entries[k] is None for a transparent (blank/comment) line.""" + blocks = [] + i = 0 + n = len(lines) + while i < n: + if i in skip: + i += 1 + continue + parsed = parse_decl(lines[i]) + if parsed is None: + i += 1 + continue + indent = parsed['indent'] + start = i + j = i + entries = [] + last_real = parsed + pending_transparent = False + while j < n and j not in skip: + line = lines[j] + p = parse_decl(line) + if p is not None and p['indent'] == indent: + if pending_transparent and p['has_init'] != last_real['has_init']: + # a blank/comment line separating an initialised decl + # from an uninitialised one (or vice versa) marks a + # deliberate group break (see + # feedback_var_declaration_order) -- stop here, don't + # bridge the two groups into one aligned block. + break + entries.append(p) + last_real = p + pending_transparent = False + j += 1 + continue + if p is None and TRANSPARENT_RE.match(line): + entries.append(None) + pending_transparent = True + j += 1 + continue + break + # trim trailing transparent lines -- they belong to whatever follows, + # not to this block + while entries and entries[-1] is None: + entries.pop() + j -= 1 + blocks.append((start, j, entries)) + i = j + return blocks + + +def reformat_block(lines, start, end, entries): + # the name (or, for a pointer, its '*') must start in the same column + # for every editable line in the block; a pointer's '*' occupies a + # column of its own, so it counts toward each line's "effective" + # type-side width. Lines with a call in the initializer, and blank/ + # comment lines, count toward the width but are never rewritten. + def effective_len(d): + return len(d['type']) + len(d['stars'].rstrip()) + + real = [d for d in entries if d is not None] + name_col = max(effective_len(d) for d in real) + 1 + for k, idx in enumerate(range(start, end)): + d = entries[k] + if d is None or not d['editable']: + continue + type_tok = d['type'] + stars = d['stars'].rstrip() + rest = d['rest'] + pad = name_col - effective_len(d) + new_line = '{}{}{}{}{}'.format( + d['indent'], type_tok, ' ' * pad, stars, rest) + lines[idx] = new_line + + +def process_file(path): + with open(path, 'r') as f: + text = f.read() + had_trailing_newline = text.endswith('\n') + lines = text.split('\n') + if had_trailing_newline: + lines = lines[:-1] + + skip = struct_body_lines(lines) + blocks = find_blocks(lines, skip) + changed = False + for start, end, entries in blocks: + real = [d for d in entries if d is not None] + if len(real) < 2: + continue + before = lines[start:end] + reformat_block(lines, start, end, entries) + if lines[start:end] != before: + changed = True + + if changed: + out = '\n'.join(lines) + if had_trailing_newline: + out += '\n' + with open(path, 'w') as f: + f.write(out) + return changed + + +def main(argv): + any_changed = False + for path in argv[1:]: + if process_file(path): + print('reformatted: {}'.format(path)) + any_changed = True + return 0 + + +if __name__ == '__main__': + sys.exit(main(sys.argv)) From ab55dc0f0c8ae2e5895c02a06ee8d675f838dfc8 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Wed, 16 Sep 2026 12:57:07 +0100 Subject: [PATCH 103/128] chore: move formatting scripts to tools/fmt/ Groups realign_decls.py, wrap_protos.py and wrap_doxygen.py under tools/fmt/ and updates CLAUDE.md's references. Co-Authored-By: Claude Sonnet 5 --- tools/{ => fmt}/realign_decls.py | 0 tools/{ => fmt}/wrap_doxygen.py | 0 tools/{ => fmt}/wrap_protos.py | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename tools/{ => fmt}/realign_decls.py (100%) rename tools/{ => fmt}/wrap_doxygen.py (100%) rename tools/{ => fmt}/wrap_protos.py (100%) diff --git a/tools/realign_decls.py b/tools/fmt/realign_decls.py similarity index 100% rename from tools/realign_decls.py rename to tools/fmt/realign_decls.py diff --git a/tools/wrap_doxygen.py b/tools/fmt/wrap_doxygen.py similarity index 100% rename from tools/wrap_doxygen.py rename to tools/fmt/wrap_doxygen.py diff --git a/tools/wrap_protos.py b/tools/fmt/wrap_protos.py similarity index 100% rename from tools/wrap_protos.py rename to tools/fmt/wrap_protos.py From 85eba080f1d875c81160369d9f3715fd1d5e1415 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Wed, 16 Sep 2026 12:57:14 +0100 Subject: [PATCH 104/128] docs: fix tools/fmt path in CLAUDE.md Co-Authored-By: Claude Sonnet 5 --- CLAUDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index feeb7c96..3aa40f61 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -94,7 +94,7 @@ New source/header files must be added by hand to the relevant `set(..._SOURCES . - 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). +- After editing any `.c`/`.h` function prototype or definition, run `python3 tools/fmt/wrap_protos.py `; after editing a header's Doxygen, run `python3 tools/fmt/wrap_doxygen.py ` (skip vendored headers). ## Commit messages From dddbc4b7e821d75bcfb5d69af26efa7badf6eb85 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Wed, 16 Sep 2026 12:59:22 +0100 Subject: [PATCH 105/128] test: move tool self-checks to tools/fmt/, add one for realign_decls Moves test_wrap_protos.py and test_wrap_doxygen.py alongside their scripts. Adds test_realign_decls.py covering the star-column padding, struct-body exclusion, array subscripts, blank/comment-line bridging, and call-initializer/has_init group-break rules worked out while debugging the script. Factors process_file()'s in-memory logic out into a pure reformat_text(text) helper so the tests don't need to touch disk. Also adds a standing rule in CLAUDE.md to run realign_decls.py alongside wrap_protos.py/wrap_doxygen.py before committing .c/.h changes. Co-Authored-By: Claude Sonnet 5 --- CLAUDE.md | 1 + tools/fmt/realign_decls.py | 18 ++-- tools/fmt/test_realign_decls.py | 121 +++++++++++++++++++++++++++ tools/{ => fmt}/test_wrap_doxygen.py | 0 tools/{ => fmt}/test_wrap_protos.py | 0 5 files changed, 134 insertions(+), 6 deletions(-) create mode 100644 tools/fmt/test_realign_decls.py rename tools/{ => fmt}/test_wrap_doxygen.py (100%) rename tools/{ => fmt}/test_wrap_protos.py (100%) diff --git a/CLAUDE.md b/CLAUDE.md index 3aa40f61..73681d9d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -95,6 +95,7 @@ New source/header files must be added by hand to the relevant `set(..._SOURCES . - 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/fmt/wrap_protos.py `; after editing a header's Doxygen, run `python3 tools/fmt/wrap_doxygen.py ` (skip vendored headers). +- Before committing any `.c`/`.h` changes, run `python3 tools/fmt/realign_decls.py ` (declaration-block alignment) alongside `wrap_protos.py`/`wrap_doxygen.py` on the same files. ## Commit messages diff --git a/tools/fmt/realign_decls.py b/tools/fmt/realign_decls.py index 158885ff..01e29032 100644 --- a/tools/fmt/realign_decls.py +++ b/tools/fmt/realign_decls.py @@ -171,9 +171,8 @@ def effective_len(d): lines[idx] = new_line -def process_file(path): - with open(path, 'r') as f: - text = f.read() +def reformat_text(text): + """Reformat declaration blocks in `text`. Returns (new_text, changed).""" had_trailing_newline = text.endswith('\n') lines = text.split('\n') if had_trailing_newline: @@ -191,10 +190,17 @@ def process_file(path): if lines[start:end] != before: changed = True + out = '\n'.join(lines) + if had_trailing_newline: + out += '\n' + return out, changed + + +def process_file(path): + with open(path, 'r') as f: + text = f.read() + out, changed = reformat_text(text) if changed: - out = '\n'.join(lines) - if had_trailing_newline: - out += '\n' with open(path, 'w') as f: f.write(out) return changed diff --git a/tools/fmt/test_realign_decls.py b/tools/fmt/test_realign_decls.py new file mode 100644 index 00000000..56c68471 --- /dev/null +++ b/tools/fmt/test_realign_decls.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +"""assert-based self-check for realign_decls.py""" +from realign_decls import reformat_text + + +def one(src): + out, changed = reformat_text(src) + return out if changed else None + + +def test_pointer_star_column(): + src = ( + " int ci;\n" + " box_t *cbox;\n" + " int cross_extent;\n" + ) + assert one(src) == ( + " int ci;\n" + " box_t *cbox;\n" + " int cross_extent;\n" + ) + + +def test_already_aligned_untouched(): + src = ( + " int ci;\n" + " box_t *cbox;\n" + ) + assert one(src) is None + + +def test_struct_body_untouched(): + src = ( + "typedef struct foo\n" + "{\n" + " int a;\n" + " box_t *b;\n" + "}\n" + "foo_t;\n" + ) + assert one(src) is None + + +def test_array_subscript(): + src = ( + " stack__child_t children[n];\n" + " int nchildren;\n" + ) + assert one(src) == ( + " stack__child_t children[n];\n" + " int nchildren;\n" + ) + + +def test_blank_and_comment_bridge_same_group(): + # both lines uninitialised -> the blank/comment gap is transparent + src = ( + " int left_skip;\n" + "\n" + " /* note */\n" + " unsigned int clamped_pos_x;\n" + ) + assert one(src) == ( + " int left_skip;\n" + "\n" + " /* note */\n" + " unsigned int clamped_pos_x;\n" + ) + + +def test_call_initializer_counts_width_but_not_rewritten(): + src = ( + " int log2bpp;\n" + " pixelfmt_any_t nativefg = colour_to_pixel(scr->palette, fg);\n" + ) + out = one(src) + assert out == ( + " int log2bpp;\n" + " pixelfmt_any_t nativefg = colour_to_pixel(scr->palette, fg);\n" + ) + + +def test_init_group_break_across_blank_line(): + # 'quantum' is initialised; the following group is not -- the blank + # line between them must NOT bridge the two into one aligned block + src = ( + " const size_t quantum = sizeof(free_t);\n" + "\n" + " int nentries;\n" + " cache_t *c;\n" + ) + assert one(src) == ( + " const size_t quantum = sizeof(free_t);\n" + "\n" + " int nentries;\n" + " cache_t *c;\n" + ) + + +def test_single_decl_untouched(): + assert one(" int only_one;\n") is None + + +def test_idempotent(): + src = ( + " int ci;\n" + " box_t *cbox;\n" + ) + out1 = one(src) + out2 = one(out1) + assert out2 is None + + +if __name__ == '__main__': + import sys + + mod = sys.modules[__name__] + for name in dir(mod): + if name.startswith('test_'): + getattr(mod, name)() + print('all tests passed') diff --git a/tools/test_wrap_doxygen.py b/tools/fmt/test_wrap_doxygen.py similarity index 100% rename from tools/test_wrap_doxygen.py rename to tools/fmt/test_wrap_doxygen.py diff --git a/tools/test_wrap_protos.py b/tools/fmt/test_wrap_protos.py similarity index 100% rename from tools/test_wrap_protos.py rename to tools/fmt/test_wrap_protos.py From 72120d5435e0d78e437860d5316b727c46e66043 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Wed, 16 Sep 2026 13:04:47 +0100 Subject: [PATCH 106/128] style: rewrap prototype alignment via wrap_protos.py Ran realign_decls.py, wrap_protos.py and wrap_doxygen.py across all DPTLib sources (excluding fortify/ and test/). realign_decls and wrap_doxygen made no changes (already clean); wrap_protos rewrapped three prototypes whose parameter columns had drifted: path_leaf_strip_ext (now one param per line), wuss_slider_row_event and wuss__fontset_init (star-to-name column tightened). Build and full test suite verified passing. Co-Authored-By: Claude Sonnet 5 --- include/io/path.h | 4 +++- include/wuss/icon-spec.h | 4 ++-- libraries/wuss/font/font.h | 6 +++--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/include/io/path.h b/include/io/path.h index 980f0805..205edc48 100644 --- a/include/io/path.h +++ b/include/io/path.h @@ -37,7 +37,9 @@ const char *path_join_filename(const char *root, int nbranches, ...); * \return Nonzero on a match, zero if "leaf" lacks 'ext' or does not fit * 'cap'. */ -int path_leaf_strip_ext(const char *leaf, const char *ext, char *name, +int path_leaf_strip_ext(const char *leaf, + const char *ext, + char *name, size_t cap); #endif /* DPTLIB_PATH */ diff --git a/include/wuss/icon-spec.h b/include/wuss/icon-spec.h index a4390e56..cec884d0 100644 --- a/include/wuss/icon-spec.h +++ b/include/wuss/icon-spec.h @@ -153,8 +153,8 @@ result_t wuss_slider_row_set(wuss_window_t *window, */ int wuss_slider_row_event(wuss_window_t *window, const wuss_slider_row_t *row, - const wuss_event_t *event, - int *value); + const wuss_event_t *event, + int *value); /** * Fill \p spec as a wuss_ICON_TYPE_LABEL. diff --git a/libraries/wuss/font/font.h b/libraries/wuss/font/font.h index 0dc4df05..8374ff56 100644 --- a/libraries/wuss/font/font.h +++ b/libraries/wuss/font/font.h @@ -30,9 +30,9 @@ struct wuss_fontset /* Copy the first "nfonts" descriptors into "set" and NULL the rest. "nfonts" * has already been range-checked by the caller (0..wuss_MAX_FONTS). */ -void wuss__fontset_init(struct wuss_fontset *set, - const wuss_font_desc_t *fonts, - int nfonts); +void wuss__fontset_init(struct wuss_fontset *set, + const wuss_font_desc_t *fonts, + int nfonts); /* Pixel height of the font in "slot", or 0 if that slot is empty or out of * range. Used by wuss_create to size the titlebar to the tallest of the From 6545643a50afc79691834a5f8b2bf92e8d1adffd Mon Sep 17 00:00:00 2001 From: David Thomas Date: Wed, 16 Sep 2026 13:37:30 +0100 Subject: [PATCH 107/128] refactor(wuss): return task pointer and add X_destroy from all demo tasks Each *_create now takes a trailing X_task_t **out so callers who need the allocated block can get it back; tasks.c passes NULL everywhere since none of its spawn_* wrappers need it. Each module also gains an X_destroy paired with X_create, called from the module's own wuss_EVENT_QUIT handler instead of an inline free -- modules that own extra heap state (colourmenus, fontmenus, menus, proginfo, bitmaps, sprites) tear that down in X_destroy before freeing the block, matching what their QUIT handlers already did. Co-Authored-By: Claude Sonnet 5 --- apps/wuss/tasks.c | 180 +++++------------------- libraries/wuss/test/tasks/ball.c | 24 +++- libraries/wuss/test/tasks/ball.h | 11 +- libraries/wuss/test/tasks/blank.c | 22 ++- libraries/wuss/test/tasks/blank.h | 8 +- libraries/wuss/test/tasks/chars.c | 47 ++++--- libraries/wuss/test/tasks/chars.h | 15 +- libraries/wuss/test/tasks/checker.c | 17 ++- libraries/wuss/test/tasks/checker.h | 9 +- libraries/wuss/test/tasks/clock.c | 28 +++- libraries/wuss/test/tasks/clock.h | 9 +- libraries/wuss/test/tasks/curve.c | 22 ++- libraries/wuss/test/tasks/curve.h | 9 +- libraries/wuss/test/tasks/gradient.c | 26 +++- libraries/wuss/test/tasks/gradient.h | 8 +- libraries/wuss/test/tasks/greeble.c | 25 +++- libraries/wuss/test/tasks/greeble.h | 9 +- libraries/wuss/test/tasks/icons.c | 37 +++-- libraries/wuss/test/tasks/icons.h | 15 +- libraries/wuss/test/tasks/image.c | 51 ++++--- libraries/wuss/test/tasks/image.h | 17 ++- libraries/wuss/test/tasks/lissajous.c | 28 +++- libraries/wuss/test/tasks/lissajous.h | 9 +- libraries/wuss/test/tasks/minesweeper.c | 50 ++++--- libraries/wuss/test/tasks/minesweeper.h | 13 +- libraries/wuss/test/tasks/palette.c | 39 +++-- libraries/wuss/test/tasks/palette.h | 14 +- libraries/wuss/test/tasks/porter-duff.c | 43 ++++-- libraries/wuss/test/tasks/porter-duff.h | 18 ++- libraries/wuss/test/tasks/saturn.c | 29 ++-- libraries/wuss/test/tasks/saturn.h | 15 +- libraries/wuss/test/tasks/sofa.c | 24 +++- libraries/wuss/test/tasks/sofa.h | 9 +- libraries/wuss/test/tasks/swatches.c | 19 ++- libraries/wuss/test/tasks/swatches.h | 9 +- libraries/wuss/test/tasks/text.c | 42 ++++-- libraries/wuss/test/tasks/text.h | 13 +- 37 files changed, 603 insertions(+), 360 deletions(-) diff --git a/apps/wuss/tasks.c b/apps/wuss/tasks.c index e588d5c9..69b001fa 100644 --- a/apps/wuss/tasks.c +++ b/apps/wuss/tasks.c @@ -59,79 +59,45 @@ void tasks_build_screen_palette(colour_t *out, out[n++] = colour_rgb(r * 0x33, g_ * 0x33, b * 0x33); } -/* 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_QUIT handler. On any create failure X_create has already - * torn down whatever it built and freed the block itself, so the only block - * the spawner frees is the font-less chars case: create returns OK but opens - * no window, leaving the block with no owner. */ +/* Each spawn is a thin passthrough to its module's X_create, which allocates + * the per-instance task block itself; the block is owned by its window and + * freed by X_destroy, called from the task's wuss_EVENT_QUIT handler (or, + * for the font-less chars case, never allocated at all -- see + * chars_create). None of these spawns need the task pointer X_create can + * hand back, so they all pass NULL for it. */ 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, t); - if (rc != result_OK) return rc; - if (t->window == NULL) { free(t); return rc; } - return result_OK; + return ball_create(g.wuss, NULL); } 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.resources, t); - if (rc != result_OK) return rc; - if (t->window == NULL) { free(t); return rc; } - return result_OK; + return text_create(g.wuss, g.resources, NULL); } 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, t); - if (rc != result_OK) return rc; - if (t->window == NULL) { free(t); return rc; } - return result_OK; + return blank_create(g.wuss, NULL); } 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.resources, t); - if (rc != result_OK) return rc; - if (t->window == NULL) { free(t); return rc; } - return result_OK; + return chars_create(g.wuss, g.resources, NULL); } 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.resources, g.palette_name, t); - if (rc != result_OK) return rc; - if (t->window == NULL) { free(t); return rc; } - return result_OK; + return palette_create(g.wuss, g.resources, g.palette_name, NULL); } static result_t spawn_image(void) { - result_t rc; - image_task_t *t; - const char *leafname; - const char *filename; - char buf[DPTLIB_MAXPATH]; - char ninepatch[DPTLIB_MAXPATH]; - - t = calloc(1, sizeof(*t)); - if (t == NULL) return result_OOM; + result_t rc; + const char *leafname; + const char *filename; + char buf[DPTLIB_MAXPATH]; + char ninepatch[DPTLIB_MAXPATH]; leafname = path_join_leafname("jessica", "png"); filename = path_join_filename(g.resources, 3, "resources", "images", leafname); @@ -143,151 +109,83 @@ static result_t spawn_image(void) strcpy(ninepatch, filename); logf_info("wuss: image task loading \"%s\" + \"%s\"", buf, ninepatch); - rc = image_create(g.wuss, g.resources, buf, ninepatch, t); + rc = image_create(g.wuss, g.resources, buf, ninepatch, NULL); if (rc != result_OK) logf_error("wuss: image_create(\"%s\") failed, rc=0x%X (%s)", buf, rc, result_string(rc)); - if (rc != result_OK) return rc; - if (t->window == NULL) { free(t); return rc; } - return result_OK; + return rc; } 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, t); - if (rc != result_OK) return rc; - if (t->window == NULL) { free(t); return rc; } - return result_OK; + return checker_create(g.wuss, NULL); } static result_t spawn_clock(void) { - clock_task_t *t = calloc(1, sizeof(*t)); - result_t rc; - if (t == NULL) return result_OOM; - rc = clock_create(g.wuss, g.daydream_font, t); - if (rc != result_OK) return rc; - if (t->window == NULL) { free(t); return rc; } - return result_OK; + return clock_create(g.wuss, g.daydream_font, NULL); } 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, t); - if (rc != result_OK) return rc; - if (t->window == NULL) { free(t); return rc; } - return result_OK; + return curve_create(g.wuss, NULL); } static result_t spawn_lissajous(void) { - lissajous_task_t *t = calloc(1, sizeof(*t)); - result_t rc; - if (t == NULL) return result_OOM; - rc = lissajous_create(g.wuss, t); - if (rc != result_OK) return rc; - if (t->window == NULL) { free(t); return rc; } - return result_OK; + return lissajous_create(g.wuss, NULL); } static result_t spawn_minesweeper(void) { - minesweeper_task_t *t = calloc(1, sizeof(*t)); - result_t rc; - if (t == NULL) return result_OOM; - rc = minesweeper_create(g.wuss, g.bold_font, t); - if (rc != result_OK) return rc; - if (t->window == NULL) { free(t); return rc; } - return result_OK; + return minesweeper_create(g.wuss, g.bold_font, NULL); } static result_t spawn_saturn(void) { - saturn_task_t *t = calloc(1, sizeof(*t)); - result_t rc; - if (t == NULL) return result_OOM; - rc = saturn_create(g.wuss, t, NULL); - if (rc != result_OK) return rc; - if (t->window == NULL) { free(t); return rc; } - return result_OK; + return saturn_create(g.wuss, NULL, NULL); } 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, t); - if (rc != result_OK) return rc; - if (t->window == NULL) { free(t); return rc; } - return result_OK; + return sofa_create(g.wuss, NULL); } 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) return rc; - if (t->window == NULL) { free(t); return rc; } - return result_OK; + return gradient_create(g.wuss, NULL); } static result_t spawn_greeble(void) { - greeble_task_t *t = calloc(1, sizeof(*t)); - result_t rc; - if (t == NULL) return result_OOM; - rc = greeble_create(g.wuss, t); - if (rc != result_OK) return rc; - if (t->window == NULL) { free(t); return rc; } - return result_OK; + return greeble_create(g.wuss, NULL); } 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.daydream_font, g.resources, t); + result_t rc; + + rc = icons_create(g.wuss, g.daydream_font, g.resources, NULL); if (rc != result_OK) logf_error("wuss: icons_create (resources \"%s\") failed, rc=0x%X (%s)", g.resources, rc, result_string(rc)); - if (rc != result_OK) return rc; - if (t->window == NULL) { free(t); return rc; } - return result_OK; + return rc; } static result_t spawn_swatches(void) { - swatches_task_t *t = calloc(1, sizeof(*t)); - result_t rc; - if (t == NULL) return result_OOM; - rc = swatches_create(g.wuss, t); - if (rc != result_OK) return rc; - if (t->window == NULL) { free(t); return rc; } - return result_OK; + return swatches_create(g.wuss, NULL); } 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); + result_t rc; + + rc = porter_duff_create(g.wuss, g.palette, g.daydream_font, g.resources, NULL); if (rc != result_OK) logf_error("wuss: porter_duff_create (resources \"%s\") failed, " "rc=0x%X (%s)", g.resources, rc, result_string(rc)); - if (rc != result_OK) return rc; - if (t->window == NULL) { free(t); return rc; } - return result_OK; + return rc; } /* The task launcher is a MENU-button pop-up over the backdrop rather than a @@ -391,9 +289,9 @@ result_t task_handle_event(wuss_window_t *window, * wuss's fixed-size UI palette, so pad out to match -- same as the * startup array run_wuss builds. */ const colour_t *palette; - int npalette; - colour_t scr_palette[256]; - int scr_nentries; + int npalette; + colour_t scr_palette[256]; + int scr_nentries; palette = wuss_get_palette(g.wuss, &npalette); scr_nentries = pixelfmt_paletted_nentries(g.bm->format); diff --git a/libraries/wuss/test/tasks/ball.c b/libraries/wuss/test/tasks/ball.c index 438b09fa..02210b8c 100644 --- a/libraries/wuss/test/tasks/ball.c +++ b/libraries/wuss/test/tasks/ball.c @@ -51,12 +51,17 @@ static box_t ball_local_box(int vx0, int vy0, int vx1, int vy1, int radius) return local; } -result_t ball_create(wuss_t *wuss, ball_task_t *task) +result_t ball_create(wuss_t *wuss, ball_task_t **out) { result_t rc; + ball_task_t *task; wuss_task_t *delegate; wuss_task_desc_t delegate_desc; + task = calloc(1, sizeof(*task)); + if (task == NULL) + return result_OOM; + task->bg = colour_rgb(0xFF, 0x00, 0x00); task->nballs = 1; @@ -74,7 +79,7 @@ result_t ball_create(wuss_t *wuss, ball_task_t *task) rc = wuss_task_create(wuss, &delegate_desc, &delegate); if (rc != result_OK) { - free(task); /* nothing registered yet; the spawner will not free it */ + free(task); /* nothing registered yet; nobody else owns it */ return rc; } wuss_task_set_autoclose(delegate, 1); @@ -88,9 +93,20 @@ result_t ball_create(wuss_t *wuss, ball_task_t *task) SIZE2D(0, 0), &task->window); if (rc != result_OK) + { wuss_task_destroy(delegate); /* unregister; its QUIT frees the task block */ + return rc; + } + + if (out) + *out = task; - return rc; + return result_OK; +} + +void ball_destroy(ball_task_t *task) +{ + free(task); } static result_t ball_redraw(const wuss_event_t *event, void *task_data) @@ -244,7 +260,7 @@ result_t ball_handle(wuss_window_t *window, return ball_idle(task_data); case wuss_EVENT_QUIT: - free(bc); /* task_data was calloc'd per instance by the spawner */ + ball_destroy(bc); return result_OK; default: diff --git a/libraries/wuss/test/tasks/ball.h b/libraries/wuss/test/tasks/ball.h index a1b8d9f2..1171c574 100644 --- a/libraries/wuss/test/tasks/ball.h +++ b/libraries/wuss/test/tasks/ball.h @@ -35,9 +35,14 @@ ball_task_t; wuss_window_fn_t ball_handle; -/* create the bouncing-ball window against the given wuss instance; "task" is a - * per-instance block owned by the window and freed when it closes */ -result_t ball_create(wuss_t *wuss, ball_task_t *task); +/* create the bouncing-ball window against the given wuss instance; the task + * block is allocated here, owned by the window, and freed when it closes. + * if out is non-NULL, the task block is also returned through it */ +result_t ball_create(wuss_t *wuss, ball_task_t **out); + +/* free a task block allocated by ball_create; normally called by the + * window's wuss_EVENT_QUIT handler, not by callers directly */ +void ball_destroy(ball_task_t *task); #endif /* WUSS_APP */ diff --git a/libraries/wuss/test/tasks/blank.c b/libraries/wuss/test/tasks/blank.c index 3d00f44d..3d36e7b3 100644 --- a/libraries/wuss/test/tasks/blank.c +++ b/libraries/wuss/test/tasks/blank.c @@ -17,12 +17,17 @@ #define BLANK_CYCLE_FRAMES 30 /* colour advances every half-second at 60fps */ -result_t blank_create(wuss_t *wuss, blank_task_t *task) +result_t blank_create(wuss_t *wuss, blank_task_t **out) { result_t rc; + blank_task_t *task; wuss_task_t *delegate; wuss_task_desc_t delegate_desc; + task = calloc(1, sizeof(*task)); + if (task == NULL) + return result_OOM; + task->npalette = 16; // TODO: Read max palette index from wuss task->index = 0; task->frame_count = 0; @@ -49,9 +54,20 @@ result_t blank_create(wuss_t *wuss, blank_task_t *task) SIZE2D(0, 0), &task->window); if (rc != result_OK) + { wuss_task_destroy(delegate); /* unregister; its QUIT frees the task block */ + return rc; + } - return rc; + if (out) + *out = task; + + return result_OK; +} + +void blank_destroy(blank_task_t *task) +{ + free(task); } static result_t blank_idle(void *task_data) @@ -81,7 +97,7 @@ result_t blank_handle(wuss_window_t *window, { if (event->kind == wuss_EVENT_QUIT) { - free(task_data); /* calloc'd per instance by the spawner */ + blank_destroy(task_data); return result_OK; } diff --git a/libraries/wuss/test/tasks/blank.h b/libraries/wuss/test/tasks/blank.h index 34ea5459..fb8c53d0 100644 --- a/libraries/wuss/test/tasks/blank.h +++ b/libraries/wuss/test/tasks/blank.h @@ -23,9 +23,13 @@ blank_task_t; wuss_window_fn_t blank_handle; -/* create the colour-cycling blank window against the given wuss instance */ -result_t blank_create(wuss_t *wuss, blank_task_t *task); +/* create the colour-cycling blank window against the given wuss instance; + * if out is non-NULL, the task block is also returned through it */ +result_t blank_create(wuss_t *wuss, blank_task_t **out); +/* free a task block allocated by blank_create; normally called by the + * window's wuss_EVENT_QUIT handler, not by callers directly */ +void blank_destroy(blank_task_t *task); #endif /* WUSS_APP */ diff --git a/libraries/wuss/test/tasks/chars.c b/libraries/wuss/test/tasks/chars.c index 87375d4f..fe139c71 100644 --- a/libraries/wuss/test/tasks/chars.c +++ b/libraries/wuss/test/tasks/chars.c @@ -138,11 +138,12 @@ static result_t chars_open_menu(chars_task_t *task) /* ----------------------------------------------------------------------- */ -result_t chars_create(wuss_t *wuss, - const char *resources, - chars_task_t *task) +result_t chars_create(wuss_t *wuss, + const char *resources, + chars_task_t **out) { result_t rc; + chars_task_t *task; wuss_task_t *delegate; wuss_task_desc_t delegate_desc; bmfont_t *font; @@ -152,10 +153,11 @@ result_t chars_create(wuss_t *wuss, font = wuss_get_font(wuss); if (font == NULL) - { - task->window = NULL; - return result_OK; - } + return result_OK; /* no window opened; nothing to free */ + + task = calloc(1, sizeof(*task)); + if (task == NULL) + return result_OOM; strncpy(chars_resources, resources, sizeof(chars_resources) - 1); chars_resources[sizeof(chars_resources) - 1] = '\0'; @@ -214,9 +216,27 @@ result_t chars_create(wuss_t *wuss, SIZE2D(64, 64), &task->window); if (rc != result_OK) + { wuss_task_destroy(delegate); /* unregister; its QUIT frees the task block */ + return rc; + } - return rc; + if (out) + *out = task; + + return result_OK; +} + +void chars_destroy(chars_task_t *task) +{ + int i; + + for (i = 0; i < task->nfonts; i++) + if (task->fonts[i] != NULL) + bmfont_destroy(task->fonts[i]); + free(task->fonts); + wuss_fontmenu_destroy(task->fontmenu); + free(task); } static result_t chars_redraw(const wuss_event_t *event, void *task_data) @@ -324,16 +344,7 @@ result_t chars_handle(wuss_window_t *window, switch (event->kind) { case wuss_EVENT_QUIT: - { - int i; - - for (i = 0; i < cc->nfonts; i++) - if (cc->fonts[i] != NULL) - bmfont_destroy(cc->fonts[i]); - free(cc->fonts); - wuss_fontmenu_destroy(cc->fontmenu); - free(cc); /* calloc'd per instance by the spawner */ - } + chars_destroy(cc); return result_OK; case wuss_EVENT_MOUSE: diff --git a/libraries/wuss/test/tasks/chars.h b/libraries/wuss/test/tasks/chars.h index 9fa940fa..b603b41d 100644 --- a/libraries/wuss/test/tasks/chars.h +++ b/libraries/wuss/test/tasks/chars.h @@ -37,10 +37,17 @@ wuss_window_fn_t chars_handle; /* create the glyph-grid window against the given wuss instance; does * nothing and returns result_OK if wuss has no system font. resources is the - * root the font picker loads bmfonts from (resources/bmfonts/.png). */ -result_t chars_create(wuss_t *wuss, - const char *resources, - chars_task_t *task); + * root the font picker loads bmfonts from (resources/bmfonts/.png). + * if out is non-NULL, the task block is also returned through it -- but only + * on the path where one is actually allocated; *out is left untouched on the + * font-less early return, same as on any other non-result_OK path */ +result_t chars_create(wuss_t *wuss, + const char *resources, + chars_task_t **out); + +/* free a task block allocated by chars_create; normally called by the + * window's wuss_EVENT_QUIT handler, not by callers directly */ +void chars_destroy(chars_task_t *task); #endif /* WUSS_APP */ diff --git a/libraries/wuss/test/tasks/checker.c b/libraries/wuss/test/tasks/checker.c index 621d9b90..d9c7e5a5 100644 --- a/libraries/wuss/test/tasks/checker.c +++ b/libraries/wuss/test/tasks/checker.c @@ -18,12 +18,17 @@ #define CHECKER_BAND_MIN 1 #define CHECKER_BAND_MAX 32 -result_t checker_create(wuss_t*wuss, checker_task_t *task) +result_t checker_create(wuss_t *wuss, checker_task_t **out) { result_t rc; + checker_task_t *task; wuss_task_t *delegate; wuss_task_desc_t delegate_desc; + task = calloc(1, sizeof(*task)); + if (task == NULL) + return result_OOM; + task->black = colour_rgb(0x00, 0x00, 0x00); task->white = colour_rgb(0xFF, 0xFF, 0xFF); task->pattern = checker_PATTERN_CHECKERBOARD; @@ -74,9 +79,17 @@ result_t checker_create(wuss_t*wuss, checker_task_t *task) * wuss_EVENT_QUIT frees task_data */ wuss_task_set_autoclose(delegate, 1); + if (out) + *out = task; + return result_OK; } +void checker_destroy(checker_task_t *task) +{ + free(task); +} + static result_t checker_redraw(wuss_window_t *window, const wuss_event_t *event, void *task_data) @@ -185,7 +198,7 @@ result_t checker_handle(wuss_window_t *window, case wuss_EVENT_QUIT: /* one calloc'd block backs both windows; the task autocloses once the * second window goes, so free it here */ - free(cc); + checker_destroy(cc); return result_OK; default: diff --git a/libraries/wuss/test/tasks/checker.h b/libraries/wuss/test/tasks/checker.h index 63f7d4b2..da576f09 100644 --- a/libraries/wuss/test/tasks/checker.h +++ b/libraries/wuss/test/tasks/checker.h @@ -32,8 +32,13 @@ checker_task_t; wuss_window_fn_t checker_handle; -/* create the two checkerboard windows against the given wuss instance */ -result_t checker_create(wuss_t *wuss, checker_task_t *task); +/* create the two checkerboard windows against the given wuss instance; + * if out is non-NULL, the task block is also returned through it */ +result_t checker_create(wuss_t *wuss, checker_task_t **out); + +/* free a task block allocated by checker_create; normally called by the + * window's wuss_EVENT_QUIT handler, not by callers directly */ +void checker_destroy(checker_task_t *task); #endif /* WUSS_APP */ diff --git a/libraries/wuss/test/tasks/clock.c b/libraries/wuss/test/tasks/clock.c index 08787ed4..a11eb68f 100644 --- a/libraries/wuss/test/tasks/clock.c +++ b/libraries/wuss/test/tasks/clock.c @@ -101,12 +101,17 @@ static result_t clock_create_window(wuss_t *wuss, &task->window); } -result_t clock_create(wuss_t *wuss, bmfont_t *font, clock_task_t *task) +result_t clock_create(wuss_t *wuss, bmfont_t *font, clock_task_t **out) { result_t rc; + clock_task_t *task; wuss_task_t *delegate; wuss_task_desc_t delegate_desc; + task = calloc(1, sizeof(*task)); + if (task == NULL) + return result_OOM; + task->font = font; task->bg = colour_rgb(0x1D, 0x2B, 0x53); task->bezel = colour_rgb(0xFF, 0xF1, 0xE8); @@ -128,9 +133,20 @@ result_t clock_create(wuss_t *wuss, bmfont_t *font, clock_task_t *task) rc = clock_create_window(wuss, task, delegate); if (rc != result_OK) + { wuss_task_destroy(delegate); /* unregister; its QUIT frees the task block */ + return rc; + } + + if (out) + *out = task; - return rc; + return result_OK; +} + +void clock_destroy(clock_task_t *task) +{ + free(task); } static result_t clock_redraw(const wuss_event_t *event, void *task_data) @@ -140,9 +156,9 @@ static result_t clock_redraw(const wuss_event_t *event, void *task_data) const box_t *content, *bounds; time_t now; struct tm *lt; - double cx, cy, r; - double hour_angle, minute_angle, second_angle; - int i, sx, sy; + double cx, cy, r; + double hour_angle, minute_angle, second_angle; + int i, sx, sy; cc = task_data; @@ -246,7 +262,7 @@ result_t clock_handle(wuss_window_t *window, return result_OK; case wuss_EVENT_QUIT: - free(cc); /* task_data was calloc'd per instance by the spawner */ + clock_destroy(cc); return result_OK; default: diff --git a/libraries/wuss/test/tasks/clock.h b/libraries/wuss/test/tasks/clock.h index 8fa72414..84238c30 100644 --- a/libraries/wuss/test/tasks/clock.h +++ b/libraries/wuss/test/tasks/clock.h @@ -26,8 +26,13 @@ clock_task_t; wuss_window_fn_t clock_handle; /* create the clock window against the given wuss instance, lettering the - * face with the given (caller-owned) font */ -result_t clock_create(wuss_t *wuss, bmfont_t *font, clock_task_t *task); + * face with the given (caller-owned) font. if out is non-NULL, the task + * block is also returned through it */ +result_t clock_create(wuss_t *wuss, bmfont_t *font, clock_task_t **out); + +/* free a task block allocated by clock_create; normally called by the + * window's wuss_EVENT_QUIT handler, not by callers directly */ +void clock_destroy(clock_task_t *task); #endif /* WUSS_APP */ diff --git a/libraries/wuss/test/tasks/curve.c b/libraries/wuss/test/tasks/curve.c index 2e7ec663..f00c703a 100644 --- a/libraries/wuss/test/tasks/curve.c +++ b/libraries/wuss/test/tasks/curve.c @@ -79,12 +79,17 @@ static int curve_convex_hull(const point_t *src, int n, point_t *hull) return k; /* hull[0] == hull[k - 1], a closed loop */ } -result_t curve_create(wuss_t *wuss, curve_task_t *task) +result_t curve_create(wuss_t *wuss, curve_task_t **out) { result_t rc; + curve_task_t *task; wuss_task_t *delegate; wuss_task_desc_t delegate_desc; + task = calloc(1, sizeof(*task)); + if (task == NULL) + return result_OOM; + task->bg = colour_rgb(0xFF, 0xFF, 0xFF); task->line = colour_rgb(0x00, 0x00, 0x00); task->blob = colour_rgb(0xFF, 0x00, 0x00); @@ -121,9 +126,20 @@ result_t curve_create(wuss_t *wuss, curve_task_t *task) SIZE2D(0, 0), &task->window); if (rc != result_OK) + { wuss_task_destroy(delegate); /* unregister; its QUIT frees the task block */ + return rc; + } + + if (out) + *out = task; - return rc; + return result_OK; +} + +void curve_destroy(curve_task_t *task) +{ + free(task); } static int blob_hit(const point_t *p, int x, int y) @@ -348,7 +364,7 @@ result_t curve_handle(wuss_window_t *window, return curve_scroll(task, event->data.scroll.delta, window); case wuss_EVENT_QUIT: - free(task); /* task_data was calloc'd per instance by the spawner */ + curve_destroy(task); return result_OK; default: diff --git a/libraries/wuss/test/tasks/curve.h b/libraries/wuss/test/tasks/curve.h index 8c324150..fb729f0e 100644 --- a/libraries/wuss/test/tasks/curve.h +++ b/libraries/wuss/test/tasks/curve.h @@ -31,8 +31,13 @@ curve_task_t; wuss_window_fn_t curve_handle; -/* create the curve window against the given wuss instance */ -result_t curve_create(wuss_t *wuss, curve_task_t *task); +/* create the curve window against the given wuss instance; if out is + * non-NULL, the task block is also returned through it */ +result_t curve_create(wuss_t *wuss, curve_task_t **out); + +/* free a task block allocated by curve_create; normally called by the + * window's wuss_EVENT_QUIT handler, not by callers directly */ +void curve_destroy(curve_task_t *task); #endif /* WUSS_APP */ diff --git a/libraries/wuss/test/tasks/gradient.c b/libraries/wuss/test/tasks/gradient.c index 8c3d813a..b370e3e0 100644 --- a/libraries/wuss/test/tasks/gradient.c +++ b/libraries/wuss/test/tasks/gradient.c @@ -78,12 +78,17 @@ static int dither(int index, int v, int x, int y) return CLAMP(v + (m * 16 / (n - 1)) - 8, 0, 255); } -result_t gradient_create(wuss_t *wuss, gradient_task_t *task) +result_t gradient_create(wuss_t *wuss, gradient_task_t **out) { result_t rc; + gradient_task_t *task; wuss_task_t *delegate; wuss_task_desc_t delegate_desc; + task = calloc(1, sizeof(*task)); + if (task == NULL) + return result_OOM; + task->wuss = wuss; task->dither_index = 1; /* 4x4, matching the original */ @@ -108,9 +113,20 @@ result_t gradient_create(wuss_t *wuss, gradient_task_t *task) SIZE2D(0, 0), &task->window); if (rc != result_OK) + { wuss_task_destroy(delegate); /* unregister; its QUIT frees the task block */ + return rc; + } + + if (out) + *out = task; - return rc; + return result_OK; +} + +void gradient_destroy(gradient_task_t *task) +{ + free(task); } static result_t gradient_redraw(const wuss_event_t *event, void *task_data) @@ -148,8 +164,8 @@ static result_t gradient_redraw(const wuss_event_t *event, void *task_data) * not the per-redraw dirty piece, so it is drawn whole on a partial redraw */ { bmfont_t *font = wuss_get_font(gc->wuss); - int dim = gradient_dithers[di].dim; - char label[8]; + int dim = gradient_dithers[di].dim; + char label[8]; colour_t ink = colour_rgb(0xFF, 0xFF, 0xFF); colour_t bg = colour_rgba(0, 0, 0, 0); /* transparent */ @@ -215,7 +231,7 @@ result_t gradient_handle(wuss_window_t *window, return gradient_mouse(event, task_data); case wuss_EVENT_QUIT: - free(gc); /* task_data was calloc'd per instance by the spawner */ + gradient_destroy(gc); return result_OK; default: diff --git a/libraries/wuss/test/tasks/gradient.h b/libraries/wuss/test/tasks/gradient.h index e6ce817e..09d8a007 100644 --- a/libraries/wuss/test/tasks/gradient.h +++ b/libraries/wuss/test/tasks/gradient.h @@ -21,9 +21,13 @@ gradient_task_t; wuss_window_fn_t gradient_handle; -/* create the gradient window against the given wuss instance */ -result_t gradient_create(wuss_t *wuss, gradient_task_t *task); +/* create the gradient window against the given wuss instance; if out is + * non-NULL, the task block is also returned through it */ +result_t gradient_create(wuss_t *wuss, gradient_task_t **out); +/* free a task block allocated by gradient_create; normally called by the + * window's wuss_EVENT_QUIT handler, not by callers directly */ +void gradient_destroy(gradient_task_t *task); #endif /* WUSS_APP */ diff --git a/libraries/wuss/test/tasks/greeble.c b/libraries/wuss/test/tasks/greeble.c index 102596ac..0d39cae6 100644 --- a/libraries/wuss/test/tasks/greeble.c +++ b/libraries/wuss/test/tasks/greeble.c @@ -217,10 +217,10 @@ static void greeble_stamp(screen_t *scr, static result_t greeble_redraw(const wuss_event_t *event, greeble_task_t *task) { - screen_t *scr; - const box_t *content, *bounds; - colour_t palette[GREEBLE_NPALETTE][4]; - int p, r, c, sx, sy, ox, oy; + screen_t *scr; + const box_t *content, *bounds; + colour_t palette[GREEBLE_NPALETTE][4]; + int p, r, c, sx, sy, ox, oy; scr = event->data.redraw.scr; content = event->data.redraw.content; @@ -370,7 +370,7 @@ result_t greeble_handle(wuss_window_t *window, return result_OK; case wuss_EVENT_QUIT: - free(task); /* task_data was calloc'd per instance by the spawner */ + greeble_destroy(task); return result_OK; default: @@ -378,14 +378,19 @@ result_t greeble_handle(wuss_window_t *window, } } -result_t greeble_create(wuss_t *wuss, greeble_task_t *task) +result_t greeble_create(wuss_t *wuss, greeble_task_t **out) { result_t rc; + greeble_task_t *task; wuss_task_t *delegate; wuss_task_desc_t delegate_desc; size2d_t grid_px; box_t content; + task = calloc(1, sizeof(*task)); + if (task == NULL) + return result_OOM; + /* the full generator grid in pixels: content sized so cols/rows land * exactly on the GREEBLE_MAX_* caps in 8-pixel tiles */ grid_px = SIZE2D(GREEBLE_MAX_COLS * GREEBLE_TILE_PX, @@ -427,7 +432,15 @@ result_t greeble_create(wuss_t *wuss, greeble_task_t *task) wuss_window_get_content_bounds(task->window, &content); greeble_relayout(task, &content); + if (out) + *out = task; + return result_OK; } +void greeble_destroy(greeble_task_t *task) +{ + free(task); +} + #endif /* WUSS_APP */ diff --git a/libraries/wuss/test/tasks/greeble.h b/libraries/wuss/test/tasks/greeble.h index e44a0bd6..9c843253 100644 --- a/libraries/wuss/test/tasks/greeble.h +++ b/libraries/wuss/test/tasks/greeble.h @@ -47,8 +47,13 @@ greeble_task_t; wuss_window_fn_t greeble_handle; -/* create the greebling window against the given wuss instance */ -result_t greeble_create(wuss_t *wuss, greeble_task_t *task); +/* create the greebling window against the given wuss instance; if out is + * non-NULL, the task block is also returned through it */ +result_t greeble_create(wuss_t *wuss, greeble_task_t **out); + +/* free a task block allocated by greeble_create; normally called by the + * window's wuss_EVENT_QUIT handler, not by callers directly */ +void greeble_destroy(greeble_task_t *task); #endif /* WUSS_APP */ diff --git a/libraries/wuss/test/tasks/icons.c b/libraries/wuss/test/tasks/icons.c index 8601f4a4..a766d045 100644 --- a/libraries/wuss/test/tasks/icons.c +++ b/libraries/wuss/test/tasks/icons.c @@ -289,10 +289,10 @@ static void icons_add_iconset(icons_layout_t *lay, const wuss_t *wuss) { static const char *const names[4] = { "opton", "optoff", "radon", "radoff" }; - wuss_icon_spec_t *s; - int top; - int idx[4]; - int i; + wuss_icon_spec_t *s; + int top; + int idx[4]; + int i; for (i = 0; i < 4; i++) { @@ -562,21 +562,26 @@ static void icons_add_menu(icons_layout_t *lay, int *ticked) /* ----------------------------------------------------------------------- */ -result_t icons_create(wuss_t *wuss, - bmfont_t *font, - const char *resources, - icons_task_t *task) +result_t icons_create(wuss_t *wuss, + bmfont_t *font, + const char *resources, + icons_task_t **out) { wuss_task_t *delegate; wuss_task_desc_t delegate_desc; wuss_icon_spec_t specs[ICONS_NSPECS]; wuss_icon_t *made[ICONS_NSPECS]; icons_layout_t lay; + icons_task_t *task; const char *sprite_path; int i_button, i_counter, i_opt, i_state, i_hotspot, i_ticked; int i_shoriz, i_svert, i_sstate; result_t rc; + task = calloc(1, sizeof(*task)); + if (task == NULL) + return result_OOM; + task->font = font; task->label = colour_rgb(0x00, 0x00, 0x00); /* only the ruler-text glyph blend; approximates the wuss_COLOUR_WINDOW / @@ -620,7 +625,7 @@ result_t icons_create(wuss_t *wuss, { if (task->has_sprite) free(task->sprite.base); - free(task); /* nothing registered yet; the spawner will not free it */ + free(task); /* nothing registered yet; nobody else owns it */ return rc; } @@ -685,6 +690,9 @@ result_t icons_create(wuss_t *wuss, * wuss_EVENT_QUIT frees task_data */ wuss_task_set_autoclose(delegate, 1); + if (out) + *out = task; + return result_OK; failure: @@ -692,6 +700,13 @@ result_t icons_create(wuss_t *wuss, return rc; } +void icons_destroy(icons_task_t *task) +{ + if (task->has_sprite) + free(task->sprite.base); + free(task); +} + #define ICONS_GRID 16 /* document-space pitch of the backdrop grid */ #define ICONS_AXIS_LABEL 64 /* label every Nth grid line along each axis */ @@ -840,9 +855,7 @@ result_t icons_handle(wuss_window_t *window, return icons_icon(event, task_data); case wuss_EVENT_QUIT: - if (tcx->has_sprite) - free(tcx->sprite.base); - free(tcx); /* calloc'd per instance by the spawner */ + icons_destroy(tcx); return result_OK; default: diff --git a/libraries/wuss/test/tasks/icons.h b/libraries/wuss/test/tasks/icons.h index c2b8c923..495a2f8c 100644 --- a/libraries/wuss/test/tasks/icons.h +++ b/libraries/wuss/test/tasks/icons.h @@ -37,11 +37,16 @@ icons_task_t; wuss_window_fn_t icons_handle; -/* create the icons window against the given wuss instance */ -result_t icons_create(wuss_t *wuss, - bmfont_t *font, - const char *resources, - icons_task_t *task); +/* create the icons window against the given wuss instance; if out is + * non-NULL, the task block is also returned through it */ +result_t icons_create(wuss_t *wuss, + bmfont_t *font, + const char *resources, + icons_task_t **out); + +/* free a task block allocated by icons_create; normally called by the + * window's wuss_EVENT_QUIT handler, not by callers directly */ +void icons_destroy(icons_task_t *task); #endif /* WUSS_APP */ diff --git a/libraries/wuss/test/tasks/image.c b/libraries/wuss/test/tasks/image.c index aa210bb9..21ca6ab8 100644 --- a/libraries/wuss/test/tasks/image.c +++ b/libraries/wuss/test/tasks/image.c @@ -59,18 +59,23 @@ static result_t load_png_deep(bitmap_t *bm, const char *filename) return result_OK; } -result_t image_create(wuss_t *wuss, - const char *resources, - const char *path, - const char *background_path, - image_task_t *task) +result_t image_create(wuss_t *wuss, + const char *resources, + const char *path, + const char *background_path, + image_task_t **out) { result_t rc; + image_task_t *task; wuss_task_t *delegate; wuss_task_desc_t delegate_desc; const char *images_dir; size2d_t sz; + task = calloc(1, sizeof(*task)); + if (task == NULL) + return result_OOM; + task->wuss = wuss; task->delegate = NULL; task->resources = resources; @@ -88,14 +93,14 @@ result_t image_create(wuss_t *wuss, &task->nnames); if (rc != result_OK) { - free(task); /* nothing registered yet; the spawner will not free it */ + free(task); /* nothing registered yet; nobody else owns it */ return rc; } rc = load_png_deep(&task->bitmap, path); if (rc != result_OK) { - free(task); /* nothing registered yet; the spawner will not free it */ + free(task); /* nothing registered yet; nobody else owns it */ return rc; } @@ -103,7 +108,7 @@ result_t image_create(wuss_t *wuss, if (rc != result_OK) { free(task->bitmap.base); - free(task); /* nothing registered yet; the spawner will not free it */ + free(task); /* nothing registered yet; nobody else owns it */ return rc; } @@ -116,7 +121,7 @@ result_t image_create(wuss_t *wuss, { free(task->bitmap.base); free(task->ninepatch.base); - free(task); /* nothing registered yet; the spawner will not free it */ + free(task); /* nothing registered yet; nobody else owns it */ return rc; } task->delegate = delegate; @@ -164,9 +169,26 @@ result_t image_create(wuss_t *wuss, if (wuss_colourmenu_create(&task->colourmenu, wuss, "Background") != result_OK) task->colourmenu = NULL; + if (out) + *out = task; + return result_OK; } +void image_destroy(image_task_t *task) +{ + /* close any open chain first: it may hold the proginfo window as a + * borrowed wuss_menu_item_t.window, and destroying that below would leave + * the chain pointing at freed memory */ + wuss_menu_close(task->menu_handle); + wuss_menu_destroy(task->menu); + wuss_proginfo_destroy(task->proginfo); /* closes its dialogue window */ + wuss_colourmenu_destroy(task->colourmenu); + free(task->bitmap.base); + free(task->ninepatch.base); + free(task); /* task_data was calloc'd per instance by the spawner */ +} + static result_t image_redraw(const wuss_event_t *event, void *task_data) { image_task_t *ic; @@ -391,16 +413,7 @@ result_t image_handle(wuss_window_t *window, return result_OK; case wuss_EVENT_QUIT: - /* close any open chain first: it may hold the proginfo window as a - * borrowed wuss_menu_item_t.window, and destroying that below would leave - * the chain pointing at freed memory */ - wuss_menu_close(ic->menu_handle); - wuss_menu_destroy(ic->menu); - wuss_proginfo_destroy(ic->proginfo); /* closes its dialogue window */ - wuss_colourmenu_destroy(ic->colourmenu); - free(ic->bitmap.base); - free(ic->ninepatch.base); - free(ic); /* task_data was calloc'd per instance by the spawner */ + image_destroy(ic); return result_OK; default: diff --git a/libraries/wuss/test/tasks/image.h b/libraries/wuss/test/tasks/image.h index b5ee9d1a..fa116c66 100644 --- a/libraries/wuss/test/tasks/image.h +++ b/libraries/wuss/test/tasks/image.h @@ -51,12 +51,17 @@ wuss_window_fn_t image_handle; /* load the PNG at path (and the 9-patch PNG at background_path, drawn tiled * behind it) and create its window against the given wuss instance. * resources is the root resource directory, kept so a click can cycle - * through resources/images (PNG files) by reloading a different leafname. */ -result_t image_create(wuss_t *wuss, - const char *resources, - const char *path, - const char *background_path, - image_task_t *task); + * through resources/images (PNG files) by reloading a different leafname. + * if out is non-NULL, the task block is also returned through it */ +result_t image_create(wuss_t *wuss, + const char *resources, + const char *path, + const char *background_path, + image_task_t **out); + +/* free a task block allocated by image_create; normally called by the + * window's wuss_EVENT_QUIT handler, not by callers directly */ +void image_destroy(image_task_t *task); #endif /* WUSS_APP */ diff --git a/libraries/wuss/test/tasks/lissajous.c b/libraries/wuss/test/tasks/lissajous.c index 43c95a93..9b440457 100644 --- a/libraries/wuss/test/tasks/lissajous.c +++ b/libraries/wuss/test/tasks/lissajous.c @@ -21,11 +21,16 @@ static const int lissajous_freqs[][2] = { 3, 2 }, { 5, 4 }, { 3, 4 }, { 5, 6 }, { 1, 2 }, { 7, 4 } }; -result_t lissajous_create(wuss_t *wuss, lissajous_task_t *task) +result_t lissajous_create(wuss_t *wuss, lissajous_task_t **out) { - result_t rc; - wuss_task_t *delegate; - wuss_task_desc_t delegate_desc; + result_t rc; + lissajous_task_t *task; + wuss_task_t *delegate; + wuss_task_desc_t delegate_desc; + + task = calloc(1, sizeof(*task)); + if (task == NULL) + return result_OOM; task->bg = colour_rgb(0x00, 0x00, 0x00); task->fg = colour_rgb(0x00, 0xFF, 0x00); @@ -56,9 +61,20 @@ result_t lissajous_create(wuss_t *wuss, lissajous_task_t *task) SIZE2D(0, 0), &task->window); if (rc != result_OK) + { wuss_task_destroy(delegate); /* unregister; its QUIT frees the task block */ + return rc; + } + + if (out) + *out = task; - return rc; + return result_OK; +} + +void lissajous_destroy(lissajous_task_t *task) +{ + free(task); } static result_t lissajous_redraw(const wuss_event_t *event, void *task_data) @@ -169,7 +185,7 @@ result_t lissajous_handle(wuss_window_t *window, return lissajous_idle(task_data); case wuss_EVENT_QUIT: - free(lc); /* task_data was calloc'd per instance by the spawner */ + lissajous_destroy(lc); return result_OK; default: diff --git a/libraries/wuss/test/tasks/lissajous.h b/libraries/wuss/test/tasks/lissajous.h index 2fd194e7..20a5cc75 100644 --- a/libraries/wuss/test/tasks/lissajous.h +++ b/libraries/wuss/test/tasks/lissajous.h @@ -27,8 +27,13 @@ lissajous_task_t; wuss_window_fn_t lissajous_handle; /* create the Lissajous window against the given wuss instance; "task" is a - * per-instance block owned by the window and freed when it closes */ -result_t lissajous_create(wuss_t *wuss, lissajous_task_t *task); + * per-instance block owned by the window and freed when it closes. if out is + * non-NULL, the task block is also returned through it */ +result_t lissajous_create(wuss_t *wuss, lissajous_task_t **out); + +/* free a task block allocated by lissajous_create; normally called by the + * window's wuss_EVENT_QUIT handler, not by callers directly */ +void lissajous_destroy(lissajous_task_t *task); #endif /* WUSS_APP */ diff --git a/libraries/wuss/test/tasks/minesweeper.c b/libraries/wuss/test/tasks/minesweeper.c index 25aeec63..7fd1f78f 100644 --- a/libraries/wuss/test/tasks/minesweeper.c +++ b/libraries/wuss/test/tasks/minesweeper.c @@ -247,13 +247,18 @@ static void minesweeper_tick_clock(minesweeper_task_t *ms) /* ----------------------------------------------------------------------- */ -result_t minesweeper_create(wuss_t *wuss, - bmfont_t *font, - minesweeper_task_t *task) +result_t minesweeper_create(wuss_t *wuss, + bmfont_t *font, + minesweeper_task_t **out) { - result_t rc; - wuss_task_t *delegate; - wuss_task_desc_t delegate_desc; + result_t rc; + minesweeper_task_t *task; + wuss_task_t *delegate; + wuss_task_desc_t delegate_desc; + + task = calloc(1, sizeof(*task)); + if (task == NULL) + return result_OOM; task->wuss = wuss; task->font = font; @@ -265,7 +270,7 @@ result_t minesweeper_create(wuss_t *wuss, rc = wuss_task_create(wuss, &delegate_desc, &delegate); if (rc != result_OK) { - free(task); /* nothing registered yet; the spawner will not free it */ + free(task); /* nothing registered yet; nobody else owns it */ return rc; } task->task = delegate; @@ -280,9 +285,20 @@ result_t minesweeper_create(wuss_t *wuss, SIZE2D(0, 0), &task->window); if (rc != result_OK) + { wuss_task_destroy(delegate); /* unregister; its QUIT frees the task block */ + return rc; + } + + if (out) + *out = task; - return rc; + return result_OK; +} + +void minesweeper_destroy(minesweeper_task_t *task) +{ + free(task); } static void minesweeper_draw_cell(minesweeper_task_t *ms, @@ -350,8 +366,8 @@ static void minesweeper_draw_hud(minesweeper_task_t *ms, screen_t *scr, const box_t *bounds) { - char buf[8]; - point_t pos; + char buf[8]; + point_t pos; colour_t fg, bg; int fh, ascent; @@ -399,12 +415,12 @@ static result_t minesweeper_redraw(const wuss_event_t *event, void *task_data) { minesweeper_task_t *ms; - screen_t *scr; - const box_t *bounds; - const box_t *clip; - int board_y0; - int r0, c0, r1, c1; - int r, c; + screen_t *scr; + const box_t *bounds; + const box_t *clip; + int board_y0; + int r0, c0, r1, c1; + int r, c; ms = task_data; scr = event->data.redraw.scr; @@ -621,7 +637,7 @@ result_t minesweeper_handle(wuss_window_t *window, return result_OK; case wuss_EVENT_QUIT: - free(ms); /* task_data was calloc'd per instance by the spawner */ + minesweeper_destroy(ms); return result_OK; default: diff --git a/libraries/wuss/test/tasks/minesweeper.h b/libraries/wuss/test/tasks/minesweeper.h index 706e2951..20dab5b9 100644 --- a/libraries/wuss/test/tasks/minesweeper.h +++ b/libraries/wuss/test/tasks/minesweeper.h @@ -65,10 +65,15 @@ minesweeper_task_t; wuss_window_fn_t minesweeper_handle; /* create the minesweeper window against the given wuss instance, lettering - * neighbour counts with the given (caller-owned) font */ -result_t minesweeper_create(wuss_t *wuss, - bmfont_t *font, - minesweeper_task_t *task); + * neighbour counts with the given (caller-owned) font. + * if out is non-NULL, the task block is also returned through it */ +result_t minesweeper_create(wuss_t *wuss, + bmfont_t *font, + minesweeper_task_t **out); + +/* free a task block allocated by minesweeper_create; normally called by the + * window's wuss_EVENT_QUIT handler, not by callers directly */ +void minesweeper_destroy(minesweeper_task_t *task); #endif /* WUSS_APP */ diff --git a/libraries/wuss/test/tasks/palette.c b/libraries/wuss/test/tasks/palette.c index 00746606..5e89d4ff 100644 --- a/libraries/wuss/test/tasks/palette.c +++ b/libraries/wuss/test/tasks/palette.c @@ -73,16 +73,21 @@ result_t palette_load_hex(const char *resources, /* ----------------------------------------------------------------------- */ -result_t palette_create(wuss_t *wuss, - const char *resources, - const char *startup_name, - palette_task_t *task) +result_t palette_create(wuss_t *wuss, + const char *resources, + const char *startup_name, + palette_task_t **out) { - result_t rc; + result_t rc; + palette_task_t *task; wuss_task_desc_t delegate_desc; - const char *dir; - char dirbuf[DPTLIB_MAXPATH]; - int i; + const char *dir; + char dirbuf[DPTLIB_MAXPATH]; + int i; + + task = calloc(1, sizeof(*task)); + if (task == NULL) + return result_OOM; task->wuss = wuss; task->resources = resources; @@ -162,9 +167,17 @@ result_t palette_create(wuss_t *wuss, * wuss_EVENT_QUIT frees task_data */ wuss_task_set_autoclose(task->delegate, 1); + if (out) + *out = task; + return result_OK; } +void palette_destroy(palette_task_t *task) +{ + free(task); +} + /* shared by both windows: paint a roughly-square grid of npalette swatches * across bounds */ static void palette_draw_grid(screen_t *scr, @@ -203,10 +216,10 @@ static result_t palette_redraw(const wuss_event_t *event, void *task_data) { palette_task_t *pc; const colour_t *palette; - int npalette; + int npalette; screen_t *scr; const box_t *bounds; - int sx, sy; + int sx, sy; pc = task_data; palette = wuss_get_palette(pc->wuss, &npalette); @@ -397,9 +410,9 @@ result_t palette_handle(wuss_window_t *window, return result_OK; case wuss_EVENT_QUIT: - free(task_data); /* one calloc'd block backs both windows; the task - * autocloses once the second one goes, so free it - * here */ + /* one calloc'd block backs both windows; the task autocloses once the + * second one goes, so free it here */ + palette_destroy(pc); return result_OK; default: diff --git a/libraries/wuss/test/tasks/palette.h b/libraries/wuss/test/tasks/palette.h index c5abde2e..d613a7c9 100644 --- a/libraries/wuss/test/tasks/palette.h +++ b/libraries/wuss/test/tasks/palette.h @@ -75,12 +75,16 @@ result_t palette_load_hex(const char *resources, * build the picker menu. The swatch grid always draws wuss's current system * palette (wuss_get_palette), so `startup_name` -- its *.hex leafname, no * extension -- is used only to tick the matching row in the picker menu; NULL - * ticks none. */ -result_t palette_create(wuss_t *wuss, - const char *resources, - const char *startup_name, - palette_task_t *task); + * ticks none. if out is non-NULL, the task block is also returned through + * it. */ +result_t palette_create(wuss_t *wuss, + const char *resources, + const char *startup_name, + palette_task_t **out); +/* free a task block allocated by palette_create; normally called by the + * window's wuss_EVENT_QUIT handler, not by callers directly */ +void palette_destroy(palette_task_t *task); #endif /* WUSS_APP */ diff --git a/libraries/wuss/test/tasks/porter-duff.c b/libraries/wuss/test/tasks/porter-duff.c index b69e7a28..fb6b042e 100644 --- a/libraries/wuss/test/tasks/porter-duff.c +++ b/libraries/wuss/test/tasks/porter-duff.c @@ -157,15 +157,20 @@ static result_t load_demo_png(bitmap_t *bm, /* ----------------------------------------------------------------------- */ -result_t porter_duff_create(wuss_t *wuss, - const colour_t *palette, - bmfont_t *font, - const char *resources, - porter_duff_task_t *task) +result_t porter_duff_create(wuss_t *wuss, + const colour_t *palette, + bmfont_t *font, + const char *resources, + porter_duff_task_t **out) { - result_t rc; - wuss_task_t *delegate; - wuss_task_desc_t delegate_desc; + result_t rc; + porter_duff_task_t *task; + wuss_task_t *delegate; + wuss_task_desc_t delegate_desc; + + task = calloc(1, sizeof(*task)); + if (task == NULL) + return result_OOM; task->font = font; task->rule = composite_RULE_CLEAR; @@ -198,7 +203,7 @@ result_t porter_duff_create(wuss_t *wuss, delegate_desc.name = "porter-duff"; rc = wuss_task_create(wuss, &delegate_desc, &delegate); if (rc != result_OK) - goto free_dst; /* nothing registered yet; the spawner will not free task */ + goto free_dst; /* nothing registered yet; nobody else owns task */ wuss_task_set_autoclose(delegate, 1); rc = wuss_window_create_placed(delegate, @@ -215,6 +220,9 @@ result_t porter_duff_create(wuss_t *wuss, return rc; } + if (out) + *out = task; + return result_OK; free_dst: @@ -225,11 +233,20 @@ result_t porter_duff_create(wuss_t *wuss, free(task->b.base); free_a: free(task->a.base); - free(task); /* no task was registered on any goto here; spawner won't free */ + free(task); /* no task was registered on any goto here; nobody else owns it */ return rc; } +void porter_duff_destroy(porter_duff_task_t *task) +{ + free(task->dst.base); + free(task->src.base); + free(task->b.base); + free(task->a.base); + free(task); +} + /* ----------------------------------------------------------------------- */ /* Triangle ramp: 0 at the start of the rule's turn, 255 at its midpoint, back @@ -418,11 +435,7 @@ result_t porter_duff_handle(wuss_window_t *window, return porter_duff_idle(task_data); case wuss_EVENT_QUIT: - 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 */ + porter_duff_destroy(pd); return result_OK; default: diff --git a/libraries/wuss/test/tasks/porter-duff.h b/libraries/wuss/test/tasks/porter-duff.h index c617aa2b..783755ba 100644 --- a/libraries/wuss/test/tasks/porter-duff.h +++ b/libraries/wuss/test/tasks/porter-duff.h @@ -35,13 +35,17 @@ porter_duff_task_t; wuss_window_fn_t porter_duff_handle; /* load the two demo images and create the window against the given wuss - * instance; resources is the DPTLib repo root, for locating the bundled PNGs */ -result_t porter_duff_create(wuss_t *wuss, - const colour_t *palette, - bmfont_t *font, - const char *resources, - porter_duff_task_t *task); - + * instance; resources is the DPTLib repo root, for locating the bundled PNGs; + * if out is non-NULL, the task block is also returned through it */ +result_t porter_duff_create(wuss_t *wuss, + const colour_t *palette, + bmfont_t *font, + const char *resources, + porter_duff_task_t **out); + +/* free a task block allocated by porter_duff_create; normally called by the + * window's wuss_EVENT_QUIT handler, not by callers directly */ +void porter_duff_destroy(porter_duff_task_t *task); #endif /* WUSS_APP */ diff --git a/libraries/wuss/test/tasks/saturn.c b/libraries/wuss/test/tasks/saturn.c index 37303b2f..6e964b6e 100644 --- a/libraries/wuss/test/tasks/saturn.c +++ b/libraries/wuss/test/tasks/saturn.c @@ -166,15 +166,20 @@ static result_t saturn_conf_cancel(void *opaque, wuss_button_t button); static result_t saturn_conf_apply_action(void *opaque, wuss_button_t button); result_t saturn_create(wuss_t *wuss, - saturn_task_t *task, - const saturn_config_t *config) + const saturn_config_t *config, + saturn_task_t **out) { static const saturn_config_t default_config = SATURN_CONFIG_DEFAULT; result_t rc; + saturn_task_t *task; wuss_task_t *delegate; wuss_task_desc_t delegate_desc; + task = calloc(1, sizeof(*task)); + if (task == NULL) + return result_OOM; + task->wuss = wuss; task->bg = colour_rgb(0x00, 0x00, 0x00); task->fg = colour_rgb(0xFF, 0xFF, 0xFF); @@ -196,7 +201,7 @@ result_t saturn_create(wuss_t *wuss, rc = wuss_task_create(wuss, &delegate_desc, &delegate); if (rc != result_OK) { - free(task); /* nothing registered yet; the spawner will not free it */ + free(task); /* nothing registered yet; nobody else owns it */ return rc; } task->delegate = delegate; /* the task the menu opens against */ @@ -254,6 +259,9 @@ result_t saturn_create(wuss_t *wuss, g_saturn_menu_items[SATURN_MENU_INFO].window = wuss_proginfo_window(task->proginfo); + if (out) + *out = task; + return result_OK; fail_bg_colourmenu: @@ -265,6 +273,15 @@ result_t saturn_create(wuss_t *wuss, return rc; } +void saturn_destroy(saturn_task_t *task) +{ + wuss_colourmenu_destroy(task->fg_colourmenu); + wuss_colourmenu_destroy(task->bg_colourmenu); + wuss_dialogue_destroy(task->conf.dialogue); + wuss_proginfo_destroy(task->proginfo); + free(task); /* task_data was calloc'd per instance by the spawner */ +} + /* plot one point in window content space, clipped to the window. x,y are * already in the window's top-down pixel space (callers apply the RISC OS * (255 - ...) flip). */ @@ -734,11 +751,7 @@ result_t saturn_handle(wuss_window_t *window, return result_OK; case wuss_EVENT_QUIT: - wuss_colourmenu_destroy(task->fg_colourmenu); - wuss_colourmenu_destroy(task->bg_colourmenu); - wuss_dialogue_destroy(task->conf.dialogue); - wuss_proginfo_destroy(task->proginfo); - free(task); /* task_data was calloc'd per instance by the spawner */ + saturn_destroy(task); return result_OK; default: diff --git a/libraries/wuss/test/tasks/saturn.h b/libraries/wuss/test/tasks/saturn.h index 30f5bf38..e72ee934 100644 --- a/libraries/wuss/test/tasks/saturn.h +++ b/libraries/wuss/test/tasks/saturn.h @@ -82,12 +82,17 @@ saturn_task_t; wuss_window_fn_t saturn_handle; -/* create the planet window against the given wuss instance; "task" is a - * per-instance block owned by the window and freed when it closes. "config" - * may be NULL for SATURN_CONFIG_DEFAULT. */ +/* create the planet window against the given wuss instance; the task block is + * allocated here, owned by the window and freed when it closes. "config" may + * be NULL for SATURN_CONFIG_DEFAULT. + * if out is non-NULL, the task block is also returned through it */ result_t saturn_create(wuss_t *wuss, - saturn_task_t *task, - const saturn_config_t *config); + const saturn_config_t *config, + saturn_task_t **out); + +/* free a task block allocated by saturn_create; normally called by the + * window's wuss_EVENT_QUIT handler, not by callers directly */ +void saturn_destroy(saturn_task_t *task); #endif /* WUSS_APP */ diff --git a/libraries/wuss/test/tasks/sofa.c b/libraries/wuss/test/tasks/sofa.c index 90285ee4..6e1dc62f 100644 --- a/libraries/wuss/test/tasks/sofa.c +++ b/libraries/wuss/test/tasks/sofa.c @@ -354,12 +354,17 @@ static void draw_vertex_dots(screen_t *scr, colour); } -result_t sofa_create(wuss_t*wuss, sofa_task_t*task) +result_t sofa_create(wuss_t *wuss, sofa_task_t **out) { result_t rc; + sofa_task_t *task; wuss_task_t *delegate; wuss_task_desc_t delegate_desc; + task = calloc(1, sizeof(*task)); + if (task == NULL) + return result_OOM; + task->bg = colour_rgb(0x7E, 0x25, 0x53); task->line = colour_rgb(0xFF, 0xA3, 0x00); task->dot = colour_rgb(0xFF, 0xFF, 0xFF); @@ -390,9 +395,20 @@ result_t sofa_create(wuss_t*wuss, sofa_task_t*task) SIZE2D(0, 0), &task->window); if (rc != result_OK) + { wuss_task_destroy(delegate); /* unregister; its QUIT frees the task block */ + return rc; + } + + if (out) + *out = task; - return rc; + return result_OK; +} + +void sofa_destroy(sofa_task_t *task) +{ + free(task); } static result_t sofa_redraw(const wuss_event_t *event, void *task_data) @@ -487,7 +503,7 @@ static result_t sofa_redraw(const wuss_event_t *event, void *task_data) { const wireframe_t *wf; fix8_point_t screen[NELEMS(dodeca_vertices)]; /* largest solid */ - int i; + int i; wf = &polyhedra[sc->shape - sofa_SHAPE_TETRAHEDRON]; @@ -597,7 +613,7 @@ result_t sofa_handle(wuss_window_t *window, return sofa_idle(task_data); case wuss_EVENT_QUIT: - free(sc); /* task_data was calloc'd per instance by the spawner */ + sofa_destroy(sc); return result_OK; default: diff --git a/libraries/wuss/test/tasks/sofa.h b/libraries/wuss/test/tasks/sofa.h index 732d5dc5..d34c46a1 100644 --- a/libraries/wuss/test/tasks/sofa.h +++ b/libraries/wuss/test/tasks/sofa.h @@ -42,8 +42,13 @@ sofa_task_t; wuss_window_fn_t sofa_handle; -/* create the sofa window against the given wuss instance */ -result_t sofa_create(wuss_t*wuss, sofa_task_t*task); +/* create the sofa window against the given wuss instance; if out is + * non-NULL, the task block is also returned through it */ +result_t sofa_create(wuss_t *wuss, sofa_task_t **out); + +/* free a task block allocated by sofa_create; normally called by the + * window's wuss_EVENT_QUIT handler, not by callers directly */ +void sofa_destroy(sofa_task_t *task); #endif /* WUSS_APP */ diff --git a/libraries/wuss/test/tasks/swatches.c b/libraries/wuss/test/tasks/swatches.c index f3138e3f..7cba50b7 100644 --- a/libraries/wuss/test/tasks/swatches.c +++ b/libraries/wuss/test/tasks/swatches.c @@ -70,12 +70,17 @@ static result_t swatches_redraw(swatches_task_t *task, return result_OK; } -result_t swatches_create(wuss_t *wuss, swatches_task_t *task) +result_t swatches_create(wuss_t *wuss, swatches_task_t **out) { result_t rc; + swatches_task_t *task; wuss_task_t *delegate; wuss_task_desc_t delegate_desc; + task = calloc(1, sizeof(*task)); + if (task == NULL) + return result_OOM; + task->wuss = wuss; task->window = NULL; task->task = NULL; @@ -119,9 +124,18 @@ result_t swatches_create(wuss_t *wuss, swatches_task_t *task) * wuss_EVENT_QUIT frees task_data */ wuss_task_set_autoclose(delegate, 1); + if (out) + *out = task; + return result_OK; } +void swatches_destroy(swatches_task_t *task) +{ + wuss_colourmenu_destroy(task->colourmenu); + free(task); +} + /* A SELECT click on a cell makes that cell -- its fill pattern, its palette * colour as the ink, the current paper -- the desktop backdrop. A MENU click * pops the colour menu; its pick becomes the paper. The grid has no retained @@ -199,8 +213,7 @@ result_t swatches_handle(wuss_window_t *window, return swatches_menu_select(task, event); case wuss_EVENT_QUIT: - wuss_colourmenu_destroy(task->colourmenu); - free(task_data); /* calloc'd per instance by the spawner */ + swatches_destroy(task); return result_OK; default: diff --git a/libraries/wuss/test/tasks/swatches.h b/libraries/wuss/test/tasks/swatches.h index 71a6877c..48661259 100644 --- a/libraries/wuss/test/tasks/swatches.h +++ b/libraries/wuss/test/tasks/swatches.h @@ -30,8 +30,13 @@ swatches_task_t; wuss_window_fn_t swatches_handle; -/* create the swatches window against the given wuss instance */ -result_t swatches_create(wuss_t *wuss, swatches_task_t *task); +/* create the swatches window against the given wuss instance; if out is + * non-NULL, the task block is also returned through it */ +result_t swatches_create(wuss_t *wuss, swatches_task_t **out); + +/* free a task block allocated by swatches_create; normally called by the + * window's wuss_EVENT_QUIT handler, not by callers directly */ +void swatches_destroy(swatches_task_t *task); #endif /* WUSS_APP */ diff --git a/libraries/wuss/test/tasks/text.c b/libraries/wuss/test/tasks/text.c index ca5548f6..579efdf8 100644 --- a/libraries/wuss/test/tasks/text.c +++ b/libraries/wuss/test/tasks/text.c @@ -138,17 +138,22 @@ static result_t text_open_menu(text_task_t *task) /* ----------------------------------------------------------------------- */ -result_t text_create(wuss_t *wuss, - const char *resources, - text_task_t *task) +result_t text_create(wuss_t *wuss, + const char *resources, + text_task_t **out) { result_t rc; + text_task_t *task; wuss_task_t *delegate; wuss_task_desc_t delegate_desc; const char *bmfonts_dir; const wuss_menu_t *menu; size2d_t sz; + task = calloc(1, sizeof(*task)); + if (task == NULL) + return result_OOM; + task->wuss = wuss; task->font = wuss_get_font(wuss); task->current = -1; /* the wuss system font is none of the picker's */ @@ -225,9 +230,27 @@ result_t text_create(wuss_t *wuss, SIZE2D(0, 0), &task->window); if (rc != result_OK) + { wuss_task_destroy(delegate); /* unregister; its QUIT frees the task block */ + return rc; + } - return rc; + if (out) + *out = task; + + return result_OK; +} + +void text_destroy(text_task_t *task) +{ + int i; + + for (i = 0; i < task->nfonts; i++) + if (task->fonts[i] != NULL) + bmfont_destroy(task->fonts[i]); + free(task->fonts); + wuss_fontmenu_destroy(task->fontmenu); + free(task); } #define INSET 4 @@ -342,16 +365,7 @@ result_t text_handle(wuss_window_t *window, return result_OK; case wuss_EVENT_QUIT: - { - int i; - - for (i = 0; i < tcx->nfonts; i++) - if (tcx->fonts[i] != NULL) - bmfont_destroy(tcx->fonts[i]); - free(tcx->fonts); - wuss_fontmenu_destroy(tcx->fontmenu); - free(tcx); /* task_data was calloc'd per instance by the spawner */ - } + text_destroy(tcx); return result_OK; case wuss_EVENT_IDLE: diff --git a/libraries/wuss/test/tasks/text.h b/libraries/wuss/test/tasks/text.h index 5c0da5cc..0e3df338 100644 --- a/libraries/wuss/test/tasks/text.h +++ b/libraries/wuss/test/tasks/text.h @@ -51,10 +51,15 @@ wuss_window_fn_t text_handle; /* create the sample-text window against the given wuss instance. * resources is the root the font picker loads bmfonts from - * (resources/bmfonts/.png). */ -result_t text_create(wuss_t *wuss, - const char *resources, - text_task_t *task); + * (resources/bmfonts/.png). if out is non-NULL, the task block is also + * returned through it */ +result_t text_create(wuss_t *wuss, + const char *resources, + text_task_t **out); + +/* free a task block allocated by text_create; normally called by the + * window's wuss_EVENT_QUIT handler, not by callers directly */ +void text_destroy(text_task_t *task); #endif /* WUSS_APP */ From 882931b4ac23175e50a63e970dd8ce5400058a5e Mon Sep 17 00:00:00 2001 From: David Thomas Date: Wed, 16 Sep 2026 14:11:24 +0100 Subject: [PATCH 108/128] feat(wuss): add wuss_get_resources, drop resources param from demo tasks wuss_create now takes a resources root path, stored on wuss_t and retrievable via wuss_get_resources(). text, chars, palette, image, icons, and porter-duff task modules no longer take their own resources parameter -- they fetch it from wuss directly, so callers stop threading the path through every _create call. Co-Authored-By: Claude Sonnet 5 --- CMakeLists.txt | 1 + apps/wuss/main.c | 47 +-- apps/wuss/tasks.c | 28 +- apps/wuss/tasks.h | 1 - include/wuss/wuss.h | 27 +- libraries/wuss/core/create.c | 2 + libraries/wuss/core/get-resources.c | 12 + libraries/wuss/core/impl.h | 3 + libraries/wuss/test/tasks/chars.c | 21 +- libraries/wuss/test/tasks/chars.h | 8 +- libraries/wuss/test/tasks/icons.c | 3 +- libraries/wuss/test/tasks/icons.h | 6 +- libraries/wuss/test/tasks/image.c | 12 +- libraries/wuss/test/tasks/image.h | 8 +- libraries/wuss/test/tasks/palette.c | 9 +- libraries/wuss/test/tasks/palette.h | 15 +- libraries/wuss/test/tasks/porter-duff.c | 4 +- libraries/wuss/test/tasks/porter-duff.h | 6 +- libraries/wuss/test/tasks/text.c | 12 +- libraries/wuss/test/tasks/text.h | 13 +- libraries/wuss/test/wuss-test.c | 536 ++++++++++++------------ 21 files changed, 399 insertions(+), 375 deletions(-) create mode 100644 libraries/wuss/core/get-resources.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 9706daf5..a63f486d 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -382,6 +382,7 @@ set(WUSS_CORE_SOURCES libraries/wuss/core/destroy.c libraries/wuss/core/get-palette.c libraries/wuss/core/get-pointer.c + libraries/wuss/core/get-resources.c libraries/wuss/core/idle.c libraries/wuss/core/impl.h libraries/wuss/core/invalidate.c diff --git a/apps/wuss/main.c b/apps/wuss/main.c index 1be63d5e..0bea03de 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -144,9 +144,9 @@ struct wuss_frame_ctx static void wuss_frame(void *arg) { struct wuss_frame_ctx *c = arg; - wuss_input_t ev; - bool pixel_stress_pending = false; - bool garbage_pending = false; + wuss_input_t ev; + bool pixel_stress_pending = false; + bool garbage_pending = false; while (wuss_frontend_poll(c->frontend, &ev)) { @@ -276,27 +276,27 @@ static result_t run_wuss(const char *resources, static const char *const names[WUSS_MAIN_NFONTS] = { "DPT-Digits-Regular", "DPT-Digits-Bold", "Symbols" }; - const int scr_width = 640; - const int scr_height = 480; - result_t rc; - const char *leafname; - const char *filename; - bmfont_t *fonts[WUSS_MAIN_NFONTS]; - int nfonts; - int i; - void *pixels; - int rowbytes; - pixelfmt_t fmt; - bitmap_t bm; - screen_t scr; - colour_t palette[16]; /* the fixed-size UI palette */ - colour_t scr_palette[256]; /* palette[] padded out to whatever + const int scr_width = 640; + const int scr_height = 480; + result_t rc; + const char *leafname; + const char *filename; + bmfont_t *fonts[WUSS_MAIN_NFONTS]; + int nfonts; + int i; + void *pixels; + int rowbytes; + pixelfmt_t fmt; + bitmap_t bm; + screen_t scr; + colour_t palette[16]; /* the fixed-size UI palette */ + colour_t scr_palette[256]; /* palette[] padded out to whatever * count the chosen depth's bitmap * needs (p8 reads all 256) */ - wuss_t *wuss; - wuss_frontend_t *frontend; - bool use_wimp16; - int palette_index; + wuss_t *wuss; + wuss_frontend_t *frontend; + bool use_wimp16; + int palette_index; { /* palette_name (from -palette, default "PICO-8") names a *.hex file under @@ -380,7 +380,7 @@ static result_t run_wuss(const char *resources, } rc = wuss_create(&scr, descs, nfonts, palette, NELEMS(palette), &config, - NULL, &wuss); + NULL, resources, &wuss); logf_info("wuss: wuss_create -> rc=0x%X (%s)", rc, result_string(rc)); if (rc != result_OK) goto Failure; @@ -389,7 +389,6 @@ static result_t run_wuss(const char *resources, g.wuss = wuss; g.palette = palette; g.npalette = NELEMS(palette); - g.resources = resources; g.daydream_font = fonts[0]; /* tasks draw with the regular weight */ g.bold_font = fonts[1]; g.frontend = frontend; diff --git a/apps/wuss/tasks.c b/apps/wuss/tasks.c index 69b001fa..34523366 100644 --- a/apps/wuss/tasks.c +++ b/apps/wuss/tasks.c @@ -73,7 +73,7 @@ static result_t spawn_ball(void) static result_t spawn_text(void) { - return text_create(g.wuss, g.resources, NULL); + return text_create(g.wuss, NULL); } static result_t spawn_blank(void) @@ -83,33 +83,35 @@ static result_t spawn_blank(void) static result_t spawn_chars(void) { - return chars_create(g.wuss, g.resources, NULL); + return chars_create(g.wuss, NULL); } static result_t spawn_palette(void) { - return palette_create(g.wuss, g.resources, g.palette_name, NULL); + return palette_create(g.wuss, g.palette_name, NULL); } static result_t spawn_image(void) { result_t rc; + const char *resources; const char *leafname; const char *filename; char buf[DPTLIB_MAXPATH]; char ninepatch[DPTLIB_MAXPATH]; - leafname = path_join_leafname("jessica", "png"); - filename = path_join_filename(g.resources, 3, "resources", "images", leafname); + resources = wuss_get_resources(g.wuss); + leafname = path_join_leafname("jessica", "png"); + filename = path_join_filename(resources, 3, "resources", "images", leafname); strcpy(buf, filename); /* path_join_filename returns a shared static buffer; copy before the next * call (image_create's own dirscan join) clobbers it */ - filename = path_join_filename(g.resources, 3, "resources", "wuss", + filename = path_join_filename(resources, 3, "resources", "wuss", path_join_leafname("ninepatch", "png")); strcpy(ninepatch, filename); logf_info("wuss: image task loading \"%s\" + \"%s\"", buf, ninepatch); - rc = image_create(g.wuss, g.resources, buf, ninepatch, NULL); + rc = image_create(g.wuss, buf, ninepatch, NULL); if (rc != result_OK) logf_error("wuss: image_create(\"%s\") failed, rc=0x%X (%s)", buf, rc, result_string(rc)); @@ -165,10 +167,10 @@ static result_t spawn_icons(void) { result_t rc; - rc = icons_create(g.wuss, g.daydream_font, g.resources, NULL); + rc = icons_create(g.wuss, g.daydream_font, NULL); if (rc != result_OK) - logf_error("wuss: icons_create (resources \"%s\") failed, rc=0x%X (%s)", - g.resources, rc, result_string(rc)); + logf_error("wuss: icons_create failed, rc=0x%X (%s)", + rc, result_string(rc)); return rc; } @@ -181,10 +183,10 @@ static result_t spawn_porter_duff(void) { result_t rc; - rc = porter_duff_create(g.wuss, g.palette, g.daydream_font, g.resources, NULL); + rc = porter_duff_create(g.wuss, g.palette, g.daydream_font, NULL); if (rc != result_OK) - logf_error("wuss: porter_duff_create (resources \"%s\") failed, " - "rc=0x%X (%s)", g.resources, rc, result_string(rc)); + logf_error("wuss: porter_duff_create failed, rc=0x%X (%s)", + rc, result_string(rc)); return rc; } diff --git a/apps/wuss/tasks.h b/apps/wuss/tasks.h index cd48c5ef..762de915 100644 --- a/apps/wuss/tasks.h +++ b/apps/wuss/tasks.h @@ -30,7 +30,6 @@ extern struct wuss_app_tasks int npalette; const char *palette_name; /* startup *.hex leafname, for ticking the * picker menu's initial selection */ - const char *resources; bmfont_t *daydream_font; bmfont_t *bold_font; bool quit; /* set by the "Quit Wuss" task-menu entry */ diff --git a/include/wuss/wuss.h b/include/wuss/wuss.h index 96070b57..4943e261 100644 --- a/include/wuss/wuss.h +++ b/include/wuss/wuss.h @@ -499,11 +499,16 @@ wuss_font_desc_t; * default palette. * \param[in] npalette Number of entries in palette. Ignored if palette is * NULL. - * \param[in] config Creation-time configuration, or NULL for defaults. - * \param[in] alloc Allocator hooks, copied in, or NULL for \ref - * wuss_alloc (plain stdlib). Must outlive nothing -- - * only the three function pointers are kept. - * \param[out] wuss Newly created window manager. + * \param[in] config Creation-time configuration, or NULL for defaults. + * \param[in] alloc Allocator hooks, copied in, or NULL for \ref + * wuss_alloc (plain stdlib). Must outlive nothing -- + * only the three function pointers are kept. + * \param[in] resources Borrowed root path a caller-defined convention (e.g. + * a demo app's task modules) can use to locate its own + * bundled files; wuss itself never reads it. Must + * outlive the wuss_t. NULL if not needed -- see + * wuss_get_resources. + * \param[out] wuss Newly created window manager. * \return \ref result_OK on success, \ref result_BAD_ARG if \p nfonts is * negative or exceeds \ref wuss_MAX_FONTS, \ref * result_WUSS_BAD_COLOUR if any of config's palette entries are out @@ -516,8 +521,20 @@ result_t wuss_create(screen_t *scr, int npalette, const wuss_config_t *config, const wuss_alloc_t *alloc, + const char *resources, wuss_t **wuss); +/** + * Fetch the resources root path passed to wuss_create (see its \p resources + * parameter), for a task to locate its own bundled files without every + * caller threading the path through its own create function. + * + * \param[in] wuss Window manager. + * \return The resources root, borrowed, valid until wuss_destroy; or NULL if + * wuss_create was given none. + */ +const char *wuss_get_resources(const wuss_t *wuss); + /** * Replace the system palette partway through a session. * diff --git a/libraries/wuss/core/create.c b/libraries/wuss/core/create.c index 4581c7bd..7fd54a0c 100644 --- a/libraries/wuss/core/create.c +++ b/libraries/wuss/core/create.c @@ -61,6 +61,7 @@ result_t wuss_create(screen_t *scr, int npalette, const wuss_config_t *config, const wuss_alloc_t *alloc, + const char *resources, wuss_t **wuss) { wuss_alloc_t al; @@ -331,6 +332,7 @@ result_t wuss_create(screen_t *scr, wuss__rebuild_palettecache(w); w->scr = scr; + w->resources = resources; w->pointer_window = NULL; #ifdef WUSS_FURNITURE w->furniture.dragging = NULL; diff --git a/libraries/wuss/core/get-resources.c b/libraries/wuss/core/get-resources.c new file mode 100644 index 00000000..3d91bc53 --- /dev/null +++ b/libraries/wuss/core/get-resources.c @@ -0,0 +1,12 @@ +/* wuss/get-resources.c -- wuss - minimal window manager */ + +#include + +#include "impl.h" + +const char *wuss_get_resources(const wuss_t *wuss) +{ + assert(wuss != NULL); + + return wuss->resources; +} diff --git a/libraries/wuss/core/impl.h b/libraries/wuss/core/impl.h index a14b5552..ef677853 100644 --- a/libraries/wuss/core/impl.h +++ b/libraries/wuss/core/impl.h @@ -105,6 +105,9 @@ struct wuss_task struct wuss { screen_t *scr; + const char *resources; /* borrowed root path from wuss_create, + * for wuss_get_resources; caller + * convention only, unused internally */ struct wuss_fontset fonts; /* font slots from wuss_create; slot 0 * is the system font. See font/font.h */ wuss_alloc_t alloc; /* malloc/realloc/free hooks, copied in diff --git a/libraries/wuss/test/tasks/chars.c b/libraries/wuss/test/tasks/chars.c index fe139c71..19090263 100644 --- a/libraries/wuss/test/tasks/chars.c +++ b/libraries/wuss/test/tasks/chars.c @@ -26,11 +26,6 @@ #define CHARS_ROWS 16 #define CHARS_PAD 1 -/* keep the resources root the task was created with; the picker loads a font - * on demand by leafname and chars_create does not stash it elsewhere. - * ponytail: one demo, one instance at a time -- a file-scope copy is fine. */ -static char chars_resources[256]; - /* ----------------------------------------------------------------------- */ /* the rows of the CHARS_COLS x CHARS_ROWS grid that hold at least one glyph @@ -77,6 +72,7 @@ static bmfont_t *chars_load_font(chars_task_t *task, const char *name) { result_t rc; + const char *resources; const char *leaf; const char *filename; bmfont_t *font; @@ -84,8 +80,9 @@ static bmfont_t *chars_load_font(chars_task_t *task, if (task->fonts[idx] != NULL) return task->fonts[idx]; - leaf = path_join_leafname(name, "png"); - filename = path_join_filename(chars_resources, 3, "resources", "bmfonts", + resources = wuss_get_resources(task->wuss); + leaf = path_join_leafname(name, "png"); + filename = path_join_filename(resources, 3, "resources", "bmfonts", leaf); rc = bmfont_create(filename, &font); @@ -138,15 +135,14 @@ static result_t chars_open_menu(chars_task_t *task) /* ----------------------------------------------------------------------- */ -result_t chars_create(wuss_t *wuss, - const char *resources, - chars_task_t **out) +result_t chars_create(wuss_t *wuss, chars_task_t **out) { result_t rc; chars_task_t *task; wuss_task_t *delegate; wuss_task_desc_t delegate_desc; bmfont_t *font; + const char *resources; const char *bmfonts_dir; const wuss_menu_t *menu; size2d_t grid; @@ -159,8 +155,7 @@ result_t chars_create(wuss_t *wuss, if (task == NULL) return result_OOM; - strncpy(chars_resources, resources, sizeof(chars_resources) - 1); - chars_resources[sizeof(chars_resources) - 1] = '\0'; + resources = wuss_get_resources(wuss); task->wuss = wuss; task->font = font; @@ -172,7 +167,7 @@ result_t chars_create(wuss_t *wuss, /* the picker: every ".png" font under resources/bmfonts, sorted, less any * SYSTEM-class font (e.g. the one wuss draws menu ticks/arrows from) */ - bmfonts_dir = path_join_filename(chars_resources, 2, "resources", "bmfonts"); + bmfonts_dir = path_join_filename(resources, 2, "resources", "bmfonts"); rc = wuss_fontmenu_create(&task->fontmenu, bmfonts_dir, "Font", wuss, NULL); if (rc != result_OK) { diff --git a/libraries/wuss/test/tasks/chars.h b/libraries/wuss/test/tasks/chars.h index b603b41d..79e92310 100644 --- a/libraries/wuss/test/tasks/chars.h +++ b/libraries/wuss/test/tasks/chars.h @@ -36,14 +36,12 @@ chars_task_t; wuss_window_fn_t chars_handle; /* create the glyph-grid window against the given wuss instance; does - * nothing and returns result_OK if wuss has no system font. resources is the - * root the font picker loads bmfonts from (resources/bmfonts/.png). + * nothing and returns result_OK if wuss has no system font. the font picker + * loads bmfonts from wuss_get_resources(wuss)/resources/bmfonts/.png. * if out is non-NULL, the task block is also returned through it -- but only * on the path where one is actually allocated; *out is left untouched on the * font-less early return, same as on any other non-result_OK path */ -result_t chars_create(wuss_t *wuss, - const char *resources, - chars_task_t **out); +result_t chars_create(wuss_t *wuss, chars_task_t **out); /* free a task block allocated by chars_create; normally called by the * window's wuss_EVENT_QUIT handler, not by callers directly */ diff --git a/libraries/wuss/test/tasks/icons.c b/libraries/wuss/test/tasks/icons.c index a766d045..6e3f5cb7 100644 --- a/libraries/wuss/test/tasks/icons.c +++ b/libraries/wuss/test/tasks/icons.c @@ -564,7 +564,6 @@ static void icons_add_menu(icons_layout_t *lay, int *ticked) result_t icons_create(wuss_t *wuss, bmfont_t *font, - const char *resources, icons_task_t **out) { wuss_task_t *delegate; @@ -573,6 +572,7 @@ result_t icons_create(wuss_t *wuss, wuss_icon_t *made[ICONS_NSPECS]; icons_layout_t lay; icons_task_t *task; + const char *resources; const char *sprite_path; int i_button, i_counter, i_opt, i_state, i_hotspot, i_ticked; int i_shoriz, i_svert, i_sstate; @@ -600,6 +600,7 @@ result_t icons_create(wuss_t *wuss, task->slider_vert = NULL; task->slider_state = NULL; + resources = wuss_get_resources(wuss); sprite_path = path_join_filename(resources, 3, "resources", "wuss", path_join_leafname("ninepatch", "png")); if (bitmap_load_png(&task->sprite, sprite_path) == result_OK) diff --git a/libraries/wuss/test/tasks/icons.h b/libraries/wuss/test/tasks/icons.h index 495a2f8c..a325dd1a 100644 --- a/libraries/wuss/test/tasks/icons.h +++ b/libraries/wuss/test/tasks/icons.h @@ -37,11 +37,11 @@ icons_task_t; wuss_window_fn_t icons_handle; -/* create the icons window against the given wuss instance; if out is - * non-NULL, the task block is also returned through it */ +/* create the icons window against the given wuss instance; the sprite and + * icon set are loaded from wuss_get_resources(wuss)/resources/wuss/. if out + * is non-NULL, the task block is also returned through it */ result_t icons_create(wuss_t *wuss, bmfont_t *font, - const char *resources, icons_task_t **out); /* free a task block allocated by icons_create; normally called by the diff --git a/libraries/wuss/test/tasks/image.c b/libraries/wuss/test/tasks/image.c index 21ca6ab8..9c8d6897 100644 --- a/libraries/wuss/test/tasks/image.c +++ b/libraries/wuss/test/tasks/image.c @@ -60,7 +60,6 @@ static result_t load_png_deep(bitmap_t *bm, const char *filename) } result_t image_create(wuss_t *wuss, - const char *resources, const char *path, const char *background_path, image_task_t **out) @@ -69,6 +68,7 @@ result_t image_create(wuss_t *wuss, image_task_t *task; wuss_task_t *delegate; wuss_task_desc_t delegate_desc; + const char *resources; const char *images_dir; size2d_t sz; @@ -78,7 +78,6 @@ result_t image_create(wuss_t *wuss, task->wuss = wuss; task->delegate = NULL; - task->resources = resources; task->index = 0; task->nnames = 0; task->menu = NULL; @@ -87,6 +86,7 @@ result_t image_create(wuss_t *wuss, task->menu_handle = NULL; task->dithering = 1; + resources = wuss_get_resources(wuss); images_dir = path_join_filename(resources, 2, "resources", "images"); rc = namelist_scan(images_dir, IMAGE_EXT, task->names[0], sizeof(task->names[0]), IMAGE_MAX_NAMES, 0 /* unsorted */, @@ -240,6 +240,7 @@ static result_t image_click(wuss_window_t *window, int step) { result_t rc; + const char *resources; const char *leafname; const char *filename; char buf[DPTLIB_MAXPATH]; @@ -251,9 +252,10 @@ static result_t image_click(wuss_window_t *window, ic->index = (ic->index + step + ic->nnames) % ic->nnames; - leafname = path_join_leafname(ic->names[ic->index], "png"); - filename = path_join_filename(ic->resources, 3, "resources", "images", - leafname); + resources = wuss_get_resources(ic->wuss); + leafname = path_join_leafname(ic->names[ic->index], "png"); + filename = path_join_filename(resources, 3, "resources", "images", + leafname); strcpy(buf, filename); rc = load_png_deep(&next, buf); diff --git a/libraries/wuss/test/tasks/image.h b/libraries/wuss/test/tasks/image.h index fa116c66..cafaba23 100644 --- a/libraries/wuss/test/tasks/image.h +++ b/libraries/wuss/test/tasks/image.h @@ -24,7 +24,6 @@ typedef struct image_task wuss_task_t *delegate; /* the wuss task backing this window */ bitmap_t bitmap; /* owned: base freed by the caller when done */ bitmap_t ninepatch; /* owned: 9-patch tiled behind the main image */ - const char *resources; /* root passed to path_join_filename on click */ int index; /* current position in names[] */ char names[IMAGE_MAX_NAMES][IMAGE_MAX_NAME_LEN]; /* owned: * leafnames (extension stripped) found under @@ -49,12 +48,11 @@ image_task_t; wuss_window_fn_t image_handle; /* load the PNG at path (and the 9-patch PNG at background_path, drawn tiled - * behind it) and create its window against the given wuss instance. - * resources is the root resource directory, kept so a click can cycle - * through resources/images (PNG files) by reloading a different leafname. + * behind it) and create its window against the given wuss instance. a click + * cycles through resources/images (PNG files) by reloading a different + * leafname, using wuss_get_resources(wuss) as the root resource directory. * if out is non-NULL, the task block is also returned through it */ result_t image_create(wuss_t *wuss, - const char *resources, const char *path, const char *background_path, image_task_t **out); diff --git a/libraries/wuss/test/tasks/palette.c b/libraries/wuss/test/tasks/palette.c index 5e89d4ff..ca2ae32f 100644 --- a/libraries/wuss/test/tasks/palette.c +++ b/libraries/wuss/test/tasks/palette.c @@ -74,13 +74,13 @@ result_t palette_load_hex(const char *resources, /* ----------------------------------------------------------------------- */ result_t palette_create(wuss_t *wuss, - const char *resources, const char *startup_name, palette_task_t **out) { result_t rc; palette_task_t *task; wuss_task_desc_t delegate_desc; + const char *resources; const char *dir; char dirbuf[DPTLIB_MAXPATH]; int i; @@ -90,12 +90,12 @@ result_t palette_create(wuss_t *wuss, return result_OOM; task->wuss = wuss; - task->resources = resources; task->invert = false; task->nnames = 0; task->selected = 0; - dir = path_join_filename(resources, 2, "resources", "palettes"); + resources = wuss_get_resources(wuss); + dir = path_join_filename(resources, 2, "resources", "palettes"); strcpy(dirbuf, dir); /* path_join_filename's buffer is reused by the scan */ namelist_scan(dirbuf, PALETTE_HEX_EXT, task->names[0], sizeof(task->names[0]), PALETTE_MAX_FILES, 1 /* sorted */, @@ -364,7 +364,8 @@ static result_t palette_menu_select(palette_task_t *pc, if (pc->nnames == 0) return result_OK; - rc = palette_load_hex(pc->resources, pc->names[pc->selected], loaded); + rc = palette_load_hex(wuss_get_resources(pc->wuss), pc->names[pc->selected], + loaded); if (rc != result_OK) return result_OK; diff --git a/libraries/wuss/test/tasks/palette.h b/libraries/wuss/test/tasks/palette.h index d613a7c9..74450620 100644 --- a/libraries/wuss/test/tasks/palette.h +++ b/libraries/wuss/test/tasks/palette.h @@ -38,7 +38,6 @@ typedef struct palette_task wuss_window_t *window2; /* "Screen" window: the physical screen's * own palette (1/2/4/8bpp) or a "none" * label (32bpp has no palette) */ - const char *resources; bool invert; /* *.hex files found under resources/palettes at create time, leafname @@ -70,15 +69,13 @@ result_t palette_load_hex(const char *resources, colour_t *out); /* create the palette-swatch-grid window against the given wuss instance. - * `resources` is the resources root (as passed to path_join_filename, i.e. - * "resources/palettes" holds the *.hex files); it is scanned once here to - * build the picker menu. The swatch grid always draws wuss's current system - * palette (wuss_get_palette), so `startup_name` -- its *.hex leafname, no - * extension -- is used only to tick the matching row in the picker menu; NULL - * ticks none. if out is non-NULL, the task block is also returned through - * it. */ + * wuss_get_resources(wuss)/resources/palettes holds the *.hex files; it is + * scanned once here to build the picker menu. The swatch grid always draws + * wuss's current system palette (wuss_get_palette), so `startup_name` -- its + * *.hex leafname, no extension -- is used only to tick the matching row in + * the picker menu; NULL ticks none. if out is non-NULL, the task block is + * also returned through it. */ result_t palette_create(wuss_t *wuss, - const char *resources, const char *startup_name, palette_task_t **out); diff --git a/libraries/wuss/test/tasks/porter-duff.c b/libraries/wuss/test/tasks/porter-duff.c index fb6b042e..3afff772 100644 --- a/libraries/wuss/test/tasks/porter-duff.c +++ b/libraries/wuss/test/tasks/porter-duff.c @@ -160,13 +160,13 @@ static result_t load_demo_png(bitmap_t *bm, result_t porter_duff_create(wuss_t *wuss, const colour_t *palette, bmfont_t *font, - const char *resources, porter_duff_task_t **out) { result_t rc; porter_duff_task_t *task; wuss_task_t *delegate; wuss_task_desc_t delegate_desc; + const char *resources; task = calloc(1, sizeof(*task)); if (task == NULL) @@ -181,6 +181,8 @@ result_t porter_duff_create(wuss_t *wuss, task->fg = palette[palette_PICO8_WHITE]; task->bg = palette[palette_PICO8_BLACK]; + resources = wuss_get_resources(wuss); + rc = load_demo_png(&task->a, resources, "A"); if (rc != result_OK) return rc; diff --git a/libraries/wuss/test/tasks/porter-duff.h b/libraries/wuss/test/tasks/porter-duff.h index 783755ba..e4738bd9 100644 --- a/libraries/wuss/test/tasks/porter-duff.h +++ b/libraries/wuss/test/tasks/porter-duff.h @@ -35,12 +35,12 @@ porter_duff_task_t; wuss_window_fn_t porter_duff_handle; /* load the two demo images and create the window against the given wuss - * instance; resources is the DPTLib repo root, for locating the bundled PNGs; - * if out is non-NULL, the task block is also returned through it */ + * instance; the images are loaded from wuss_get_resources(wuss) (the DPTLib + * repo root); if out is non-NULL, the task block is also returned through + * it */ result_t porter_duff_create(wuss_t *wuss, const colour_t *palette, bmfont_t *font, - const char *resources, porter_duff_task_t **out); /* free a task block allocated by porter_duff_create; normally called by the diff --git a/libraries/wuss/test/tasks/text.c b/libraries/wuss/test/tasks/text.c index 579efdf8..2c3263ae 100644 --- a/libraries/wuss/test/tasks/text.c +++ b/libraries/wuss/test/tasks/text.c @@ -105,7 +105,7 @@ static result_t text_set_font(text_task_t *task, int idx, const char *name) if (idx < 0 || idx >= task->nfonts || idx == task->current) return result_OK; - font = text_load_font(task, task->resources, idx, name); + font = text_load_font(task, wuss_get_resources(task->wuss), idx, name); if (font == NULL) return result_OK; /* leave the current font in place */ @@ -138,14 +138,13 @@ static result_t text_open_menu(text_task_t *task) /* ----------------------------------------------------------------------- */ -result_t text_create(wuss_t *wuss, - const char *resources, - text_task_t **out) +result_t text_create(wuss_t *wuss, text_task_t **out) { result_t rc; text_task_t *task; wuss_task_t *delegate; wuss_task_desc_t delegate_desc; + const char *resources; const char *bmfonts_dir; const wuss_menu_t *menu; size2d_t sz; @@ -164,12 +163,11 @@ result_t text_create(wuss_t *wuss, task->frame_count = 0; task->resizing = true; - strncpy(task->resources, resources, sizeof(task->resources) - 1); - task->resources[sizeof(task->resources) - 1] = '\0'; + resources = wuss_get_resources(wuss); /* the picker: every ".png" font under resources/bmfonts, sorted, less any * SYSTEM-class font (e.g. the one wuss draws menu ticks/arrows from) */ - bmfonts_dir = path_join_filename(task->resources, 2, "resources", "bmfonts"); + bmfonts_dir = path_join_filename(resources, 2, "resources", "bmfonts"); rc = wuss_fontmenu_create(&task->fontmenu, bmfonts_dir, "Font", wuss, NULL); if (rc != result_OK) { diff --git a/libraries/wuss/test/tasks/text.h b/libraries/wuss/test/tasks/text.h index 0e3df338..e9b929b8 100644 --- a/libraries/wuss/test/tasks/text.h +++ b/libraries/wuss/test/tasks/text.h @@ -38,7 +38,6 @@ typedef struct text_task const char *text; /* text_samples[sample].text; what text_redraw * lays out and draws */ colour_t bg, fg; - char resources[256]; /* root the picker loads bmfonts from */ int base_width; /* content width when the window was made */ int base_height; /* content height when the window was made */ int frame_count; @@ -49,13 +48,11 @@ text_task_t; wuss_window_fn_t text_handle; -/* create the sample-text window against the given wuss instance. - * resources is the root the font picker loads bmfonts from - * (resources/bmfonts/.png). if out is non-NULL, the task block is also - * returned through it */ -result_t text_create(wuss_t *wuss, - const char *resources, - text_task_t **out); +/* create the sample-text window against the given wuss instance; the font + * picker loads bmfonts from wuss_get_resources(wuss)/resources/bmfonts/ + * .png. if out is non-NULL, the task block is also returned through + * it */ +result_t text_create(wuss_t *wuss, text_task_t **out); /* free a task block allocated by text_create; normally called by the * window's wuss_EVENT_QUIT handler, not by callers directly */ diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 67a249c0..0369b309 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -423,8 +423,8 @@ static int dirty_union_area(wuss_t *wuss, const box_t *bounds) { static unsigned char covered[512 * 512]; - box_t region; - int w, h, i, x, y, area; + box_t region; + int w, h, i, x, y, area; w = bounds->x1 - bounds->x0; h = bounds->y1 - bounds->y0; @@ -556,7 +556,7 @@ result_t wuss_test(const char *resources) bad_config.button.fg = 0; bad_config.button.pressed = 0; bad_config.accent.colour = 0; - rc = wuss_create(&scr, NULL, 0, NULL, 0, &bad_config, NULL, &bad_wuss); + rc = wuss_create(&scr, NULL, 0, NULL, 0, &bad_config, NULL, NULL, &bad_wuss); if (rc != result_WUSS_BAD_COLOUR) goto Failure; @@ -565,7 +565,7 @@ result_t wuss_test(const char *resources) { wuss_t *custom_wuss; - rc = wuss_create(&scr, NULL, 0, custom_palette, 2, NULL, NULL, &custom_wuss); + rc = wuss_create(&scr, NULL, 0, custom_palette, 2, NULL, NULL, NULL, &custom_wuss); if (rc != result_OK) goto Failure; wuss_destroy(custom_wuss); @@ -573,7 +573,7 @@ result_t wuss_test(const char *resources) printf("test: wuss_create with default palette\n"); - rc = wuss_create(&scr, NULL, 0, NULL, 0, NULL, NULL, &wuss); + rc = wuss_create(&scr, NULL, 0, NULL, 0, NULL, NULL, NULL, &wuss); if (rc != result_OK) goto Failure; @@ -589,11 +589,11 @@ result_t wuss_test(const char *resources) { 0xFFFFFFFF }, { 0xFF000000 } }; - wuss_config_t symcfg; - wuss_t *symw; - wuss_window_t *symwin; - wuss_task_t *symdel; - box_t symbox; + wuss_config_t symcfg; + wuss_t *symw; + wuss_window_t *symwin; + wuss_task_t *symdel; + box_t symbox; memset(&symcfg, 0, sizeof(symcfg)); symcfg.furniture.title.bg = wuss_COLOUR_BLUE; /* -> index 2 */ @@ -602,7 +602,7 @@ result_t wuss_test(const char *resources) symcfg.body.window = wuss_COLOUR_RED; /* -> index 0 */ symcfg.body.menu = wuss_COLOUR_WHITE; /* -> index 3 */ - rc = wuss_create(&scr, NULL, 0, sympal, 5, &symcfg, NULL, &symw); + rc = wuss_create(&scr, NULL, 0, sympal, 5, &symcfg, NULL, NULL, &symw); if (rc != result_OK) goto Failure; @@ -1074,10 +1074,10 @@ result_t wuss_test(const char *resources) printf("test: content click on a title-less window does not change z-order\n"); { - static test_task_t tc_e, tc_f; - wuss_task_t *delegate_e, *delegate_f; - box_t box_e, box_f; - wuss_window_t *win_e, *win_f; + static test_task_t tc_e, tc_f; + wuss_task_t *delegate_e, *delegate_f; + box_t box_e, box_f; + wuss_window_t *win_e, *win_f; tc_e.redraw_count = 0; tc_e.mouse_count = 0; @@ -1156,11 +1156,11 @@ result_t wuss_test(const char *resources) printf("test: moving/resizing a window entirely behind an occluder has no visible effect\n"); { - static test_task_t tc_h, tc_g; - wuss_task_t *delegate_h, *delegate_g; - box_t box_h, box_g; - wuss_window_t *win_h, *win_g; - int before_h, before_g; + static test_task_t tc_h, tc_g; + wuss_task_t *delegate_h, *delegate_g; + box_t box_h, box_g; + wuss_window_t *win_h, *win_g; + int before_h, before_g; tc_h.redraw_count = 0; tc_h.mouse_count = 0; @@ -1236,10 +1236,10 @@ result_t wuss_test(const char *resources) printf("test: bring-to-front only invalidates the newly-uncovered part\n"); { - static test_task_t tc_i, tc_j; - wuss_task_t *delegate_i, *delegate_j; - box_t box_i, box_j, dirty; - wuss_window_t *win_i, *win_j; + static test_task_t tc_i, tc_j; + wuss_task_t *delegate_i, *delegate_j; + box_t box_i, box_j, dirty; + wuss_window_t *win_i, *win_j; tc_i.redraw_count = 0; tc_i.mouse_count = 0; @@ -1303,11 +1303,11 @@ result_t wuss_test(const char *resources) printf("test: dragging off-screen and back on repaints the reappearing edge\n"); { - static test_task_t tc_m; - wuss_task_t *delegate_m; - box_t box_m; - wuss_window_t *win_m; - int before_m; + static test_task_t tc_m; + wuss_task_t *delegate_m; + box_t box_m; + wuss_window_t *win_m; + int before_m; tc_m.redraw_count = 0; tc_m.mouse_count = 0; @@ -1357,10 +1357,10 @@ result_t wuss_test(const char *resources) printf("test: Adjust-click on a window's back icon brings it to front\n"); { - static test_task_t tc_g, tc_h; - wuss_task_t *delegate_g, *delegate_h; - box_t box_g, box_h; - wuss_window_t *win_g, *win_h; + static test_task_t tc_g, tc_h; + wuss_task_t *delegate_g, *delegate_h; + box_t box_g, box_h; + wuss_window_t *win_g, *win_h; tc_h.redraw_count = 0; tc_h.mouse_count = 0; @@ -1498,10 +1498,10 @@ result_t wuss_test(const char *resources) printf("test: drag-resize stops at min_doc, not just WUSS_MIN_CONTENT\n"); { - static test_task_t tc_m; - wuss_task_t *delegate_m; - box_t box_m, content, visible; - wuss_window_t *win_m; + static test_task_t tc_m; + wuss_task_t *delegate_m; + box_t box_m, content, visible; + wuss_window_t *win_m; tc_m.redraw_count = 0; tc_m.mouse_count = 0; @@ -1544,10 +1544,10 @@ result_t wuss_test(const char *resources) printf("test: drag-resize never grows a window's total size past the screen's, on either axis\n"); { - static test_task_t tc_s; - wuss_task_t *delegate_s; - box_t box_s, content, visible; - wuss_window_t *win_s; + static test_task_t tc_s; + wuss_task_t *delegate_s; + box_t box_s, content, visible; + wuss_window_t *win_s; tc_s.redraw_count = 0; tc_s.mouse_count = 0; @@ -1592,10 +1592,10 @@ result_t wuss_test(const char *resources) * drag reach far enough to overhang -- the gap only shows up once * content.x0 is negative, letting the drag's on-screen pointer range * translate into a width bigger than the screen itself. */ - static test_task_t tc_e; - wuss_task_t *delegate_e; - box_t box_e, visible; - wuss_window_t *win_e; + static test_task_t tc_e; + wuss_task_t *delegate_e; + box_t box_e, visible; + wuss_window_t *win_e; tc_e.redraw_count = 0; tc_e.mouse_count = 0; @@ -1638,14 +1638,14 @@ result_t wuss_test(const char *resources) printf("test: toggle-size blits rather than redrawing the whole window\n"); { - static test_task_t tc_t; - wuss_task_t *delegate_t; - box_t box_t_win, before, after, titlebar, toggle; - wuss_window_t *win_t; - int outline_px, titlebar_height, inset, icon; - int i, dirty_area, full_area, cx, cy, old_icon_x, old_icon_y, found; - int interior_x, interior_y, interior_dirty; - int old_vscroll_x, old_vscroll_y, old_vscroll_found; + static test_task_t tc_t; + wuss_task_t *delegate_t; + box_t box_t_win, before, after, titlebar, toggle; + wuss_window_t *win_t; + int outline_px, titlebar_height, inset, icon; + int i, dirty_area, full_area, cx, cy, old_icon_x, old_icon_y, found; + int interior_x, interior_y, interior_dirty; + int old_vscroll_x, old_vscroll_y, old_vscroll_found; tc_t.redraw_count = 0; tc_t.mouse_count = 0; @@ -1807,12 +1807,12 @@ result_t wuss_test(const char *resources) printf("test: toggle-size that forces a scroll re-clamp invalidates the content it's about to redraw at the new offset, not just the blit's edge sliver\n"); { - static test_task_t tc_r; - wuss_task_t *delegate_r; - box_t box_r, before, titlebar, toggle; - wuss_window_t *win_r; - int outline_px, titlebar_height, inset, icon; - int i, interior_x, interior_y, interior_dirty, cx, cy; + static test_task_t tc_r; + wuss_task_t *delegate_r; + box_t box_r, before, titlebar, toggle; + wuss_window_t *win_r; + int outline_px, titlebar_height, inset, icon; + int i, interior_x, interior_y, interior_dirty, cx, cy; tc_r.redraw_count = 0; tc_r.mouse_count = 0; @@ -1910,14 +1910,14 @@ result_t wuss_test(const char *resources) * grow forces a scroll re-clamp, the overlap of the old and new content * boxes is genuine on-screen content that just needs sliding by the * scroll delta -- only the newly-exposed strip needs a real repaint. */ - static test_task_t tc_d; - wuss_task_t *delegate_d; - box_t box_d, content_before; - wuss_window_t *win_d; - point_t scroll_d; - int i; - int top_x, top_y, mid_x, mid_y, bottom_x, bottom_y; - int top_dirty, mid_dirty, bottom_dirty; + static test_task_t tc_d; + wuss_task_t *delegate_d; + box_t box_d, content_before; + wuss_window_t *win_d; + point_t scroll_d; + int i; + int top_x, top_y, mid_x, mid_y, bottom_x, bottom_y; + int top_dirty, mid_dirty, bottom_dirty; tc_d.redraw_count = 0; tc_d.mouse_count = 0; @@ -2013,14 +2013,14 @@ result_t wuss_test(const char *resources) * the occluder's own correct pixels, so it must stay clean -- dirtying * it would redraw the occluding window for nothing. Only the un-occluded * newly-revealed top strip needs a repaint. */ - static test_task_t tc_d, tc_o; - wuss_task_t *delegate_d, *delegate_o; - box_t box_d, box_o, content_before; - wuss_window_t *win_d, *win_o; - point_t scroll_d; - int i, split_x; - int occluded_x, occluded_y, kept_x, kept_y, strip_x, strip_y; - int occluded_dirty, kept_dirty, strip_dirty; + static test_task_t tc_d, tc_o; + wuss_task_t *delegate_d, *delegate_o; + box_t box_d, box_o, content_before; + wuss_window_t *win_d, *win_o; + point_t scroll_d; + int i, split_x; + int occluded_x, occluded_y, kept_x, kept_y, strip_x, strip_y; + int occluded_dirty, kept_dirty, strip_dirty; tc_d.redraw_count = 0; tc_d.mouse_count = 0; tc_o.redraw_count = 0; tc_o.mouse_count = 0; @@ -2126,13 +2126,13 @@ result_t wuss_test(const char *resources) * the occluder's own correct pixels, so it must stay clean -- dirtying * it would redraw the occluding window. Only the un-occluded * newly-exposed edge strip needs a repaint. */ - static test_task_t tc_s, tc_o; - wuss_task_t *delegate_s, *delegate_o; - box_t box_s, box_o, content; - wuss_window_t *win_s, *win_o; - int i, split_x; - int occluded_x, occluded_y, kept_x, kept_y, strip_x, strip_y; - int occluded_dirty, kept_dirty, strip_dirty; + static test_task_t tc_s, tc_o; + wuss_task_t *delegate_s, *delegate_o; + box_t box_s, box_o, content; + wuss_window_t *win_s, *win_o; + int i, split_x; + int occluded_x, occluded_y, kept_x, kept_y, strip_x, strip_y; + int occluded_dirty, kept_dirty, strip_dirty; tc_s.redraw_count = 0; tc_s.mouse_count = 0; tc_o.redraw_count = 0; tc_o.mouse_count = 0; @@ -2228,13 +2228,13 @@ result_t wuss_test(const char *resources) * With the fix each destination is clipped against the occluders and only * the un-occluded sub-pieces are blitted, leaving O's own pixels intact. * Read the framebuffer behind O to check. */ - colour_t cm; - uint32_t fb_m, fb_o; - static test_task_t tc_m, tc_o; - wuss_task_t *delegate_m, *delegate_o; - box_t box_m, box_o, content; - wuss_window_t *win_m, *win_o; - int occ_x, occ_y, mid_x, mid_y, delta; + colour_t cm; + uint32_t fb_m, fb_o; + static test_task_t tc_m, tc_o; + wuss_task_t *delegate_m, *delegate_o; + box_t box_m, box_o, content; + wuss_window_t *win_m, *win_o; + int occ_x, occ_y, mid_x, mid_y, delta; cm = colour_rgb(0x11, 0x22, 0x33); tc_m.redraw_count = 0; tc_m.mouse_count = 0; @@ -2310,12 +2310,12 @@ result_t wuss_test(const char *resources) * bottom edge end up double-shifted. The blit must be ordered (or fall * back). paint_handle paints M as one-pixel rows whose blue channel * encodes document Y, so a mis-shifted row is detectable exactly. */ - colour_t cm; - static test_task_t tc_m, tc_o; - wuss_task_t *delegate_m, *delegate_o; - box_t box_m, box_o, content, ovis; - wuss_window_t *win_m, *win_o; - int gx, gy, delta, bad; + colour_t cm; + static test_task_t tc_m, tc_o; + wuss_task_t *delegate_m, *delegate_o; + box_t box_m, box_o, content, ovis; + wuss_window_t *win_m, *win_o; + int gx, gy, delta, bad; cm = colour_rgb(0x11, 0x22, 0x33); tc_m.redraw_count = 0; tc_m.mouse_count = 0; @@ -2392,12 +2392,12 @@ result_t wuss_test(const char *resources) * its full sample space every call rather than honouring clip -- the * wuss-core clip machinery, not the task, must be what keeps such a * task's pixels inside its own window. */ - colour_t s_colour, n_colour; - static test_task_t tc_s, tc_n; - wuss_task_t *delegate_s, *delegate_n; - box_t box_s, box_n, content_n; - wuss_window_t *win_s, *win_n; - int nx, ny, bad; + colour_t s_colour, n_colour; + static test_task_t tc_s, tc_n; + wuss_task_t *delegate_s, *delegate_n; + box_t box_s, box_n, content_n; + wuss_window_t *win_s, *win_n; + int nx, ny, bad; s_colour = colour_rgb(0xcc, 0xdd, 0xee); n_colour = colour_rgb(0x40, 0x60, 0x80); @@ -2480,11 +2480,11 @@ result_t wuss_test(const char *resources) * leave no stale sliver between the client's repainted content and the * furniture (scrollbar/outline) now sitting at the new, smaller edge. */ static test_task_t tc_s; - wuss_task_t *delegate_s; - box_t box_s, content_s; - wuss_window_t *win_s; - colour_t s_colour; - int nx, ny, bad; + wuss_task_t *delegate_s; + box_t box_s, content_s; + wuss_window_t *win_s; + colour_t s_colour; + int nx, ny, bad; s_colour = colour_rgb(0xcc, 0xdd, 0xee); tc_s.redraw_count = 0; tc_s.mouse_count = 0; @@ -2574,12 +2574,12 @@ result_t wuss_test(const char *resources) * ever repaints. paint_handle's per-row blue-encoded doc_y makes a * wrongly-slid row detectable exactly. */ static test_task_t tc_s; - wuss_task_t *delegate_s; - box_t box_s, content_s; - wuss_window_t *win_s; - colour_t s_colour; - point_t scroll; - int sy, sx, bad; + wuss_task_t *delegate_s; + box_t box_s, content_s; + wuss_window_t *win_s; + colour_t s_colour; + point_t scroll; + int sy, sx, bad; s_colour = colour_rgb(0x11, 0x22, 0x33); tc_s.redraw_count = 0; tc_s.mouse_count = 0; @@ -2677,13 +2677,13 @@ result_t wuss_test(const char *resources) * dirty -- and the very next call, if it grows back past the clamp, * must not treat that still-unpainted sliver as settled ground either. */ static test_task_t tc_e; - wuss_task_t *delegate_e; - box_t box_e, content_e; - wuss_window_t *win_e; - colour_t e_colour; - point_t scroll; - int sy, sx, bad, step; - int sizes[] = { 30, 90, 35, 120, 31, 90, 30, 60, 110, 30 }; + wuss_task_t *delegate_e; + box_t box_e, content_e; + wuss_window_t *win_e; + colour_t e_colour; + point_t scroll; + int sy, sx, bad, step; + int sizes[] = { 30, 90, 35, 120, 31, 90, 30, 60, 110, 30 }; e_colour = colour_rgb(0x11, 0x22, 0x33); tc_e.redraw_count = 0; tc_e.mouse_count = 0; @@ -2759,13 +2759,13 @@ result_t wuss_test(const char *resources) * far more shapes (including piece-budget overflow) than a short * hand-picked list ever would. */ static test_task_t tc_f, tc_occ; - wuss_task_t *delegate_f, *delegate_occ; - box_t box_f, box_occ, content_f; - wuss_window_t *win_f, *win_occ; - colour_t f_colour; - point_t scroll; - unsigned int seed; - int sy, sx, bad, batch, step, sizew, sizeh; + wuss_task_t *delegate_f, *delegate_occ; + box_t box_f, box_occ, content_f; + wuss_window_t *win_f, *win_occ; + colour_t f_colour; + point_t scroll; + unsigned int seed; + int sy, sx, bad, batch, step, sizew, sizeh; f_colour = colour_rgb(0x44, 0x55, 0x66); tc_f.redraw_count = 0; tc_f.mouse_count = 0; @@ -2884,10 +2884,10 @@ result_t wuss_test(const char *resources) * outside the window's own screen area (fill_backdrop_excluding_content * then repaints that stray area as bare desktop every frame). */ static test_task_t tc_s; - wuss_task_t *delegate_s; - box_t box_s, dirty; - wuss_window_t *win_s; - colour_t s_colour; + wuss_task_t *delegate_s; + box_t box_s, dirty; + wuss_window_t *win_s; + colour_t s_colour; s_colour = colour_rgb(0xcc, 0xdd, 0xee); tc_s.redraw_count = 0; tc_s.mouse_count = 0; @@ -2941,12 +2941,12 @@ result_t wuss_test(const char *resources) printf("test: wuss_WINDOW_NO_RESIZE_BLIT redraws the whole window instead of blitting\n"); { - static test_task_t tc_nb; - wuss_task_t *delegate_nb; - box_t box_nb, before, titlebar, toggle; - wuss_window_t *win_nb; - int outline_px, titlebar_height, inset, icon; - int i, cx, cy, interior_x, interior_y, interior_dirty; + static test_task_t tc_nb; + wuss_task_t *delegate_nb; + box_t box_nb, before, titlebar, toggle; + wuss_window_t *win_nb; + int outline_px, titlebar_height, inset, icon; + int i, cx, cy, interior_x, interior_y, interior_dirty; tc_nb.redraw_count = 0; tc_nb.mouse_count = 0; @@ -3021,12 +3021,12 @@ result_t wuss_test(const char *resources) printf("test: toggle-size maximize accounts for scrollbar furniture, not just outline/titlebar\n"); { - static test_task_t tc_u; - wuss_task_t *delegate_u; - box_t box_u, ub, titlebar, toggle; - wuss_window_t *win_u; - int outline_px, titlebar_height, inset, icon; - int cx, cy; + static test_task_t tc_u; + wuss_task_t *delegate_u; + box_t box_u, ub, titlebar, toggle; + wuss_window_t *win_u; + int outline_px, titlebar_height, inset, icon; + int cx, cy; tc_u.redraw_count = 0; tc_u.mouse_count = 0; @@ -3119,12 +3119,12 @@ result_t wuss_test(const char *resources) printf("test: toggle-size maximize stays a valid box for a window dragged off the right/bottom edge\n"); { - static test_task_t tc_v; - wuss_task_t *delegate_v; - box_t box_v, vb, titlebar, toggle; - wuss_window_t *win_v; - int outline_px, titlebar_height, inset, icon; - int cx, cy; + static test_task_t tc_v; + wuss_task_t *delegate_v; + box_t box_v, vb, titlebar, toggle; + wuss_window_t *win_v; + int outline_px, titlebar_height, inset, icon; + int cx, cy; memset(&tc_v, 0, sizeof(tc_v)); tc_v.veto_pre_close = 1; /* this test pokes furniture with a window in a @@ -3475,11 +3475,11 @@ result_t wuss_test(const char *resources) printf("test: dragging a back-most window with nothing above it still blits\n"); { - static test_task_t tc_k, tc_l; - wuss_task_t *delegate_k, *delegate_l; - box_t box_k, box_l; - wuss_window_t *win_k, *win_l; - int before_k, before_l; + static test_task_t tc_k, tc_l; + wuss_task_t *delegate_k, *delegate_l; + box_t box_k, box_l; + wuss_window_t *win_k, *win_l; + int before_k, before_l; tc_k.redraw_count = 0; tc_k.mouse_count = 0; @@ -3562,11 +3562,11 @@ result_t wuss_test(const char *resources) printf("test: resizing a window only invalidates the grown/shrunk sliver\n"); { - static test_task_t tc_m2; - wuss_task_t *delegate_m2; - box_t box_m2, before2, after2, region; - wuss_window_t *win_m2; - int i, dirty_area, full_area, interior_x, interior_y, interior_dirty; + static test_task_t tc_m2; + wuss_task_t *delegate_m2; + box_t box_m2, before2, after2, region; + wuss_window_t *win_m2; + int i, dirty_area, full_area, interior_x, interior_y, interior_dirty; tc_m2.redraw_count = 0; tc_m2.mouse_count = 0; @@ -3657,11 +3657,11 @@ result_t wuss_test(const char *resources) printf("test: resizing a wuss_WINDOW_NO_RESIZE_BLIT window redraws it fully\n"); { - static test_task_t tc_nb2; - wuss_task_t *delegate_nb2; - box_t box_nb2, before3, after3, region; - wuss_window_t *win_nb2; - int i, dirty_area, full_area; + static test_task_t tc_nb2; + wuss_task_t *delegate_nb2; + box_t box_nb2, before3, after3, region; + wuss_window_t *win_nb2; + int i, dirty_area, full_area; tc_nb2.redraw_count = 0; tc_nb2.mouse_count = 0; @@ -3707,11 +3707,11 @@ result_t wuss_test(const char *resources) "furniture layout so the chrome redraws at the new width\n"); { - static test_task_t tc_fl; - wuss_task_t *delegate_fl; - box_t box_fl, after_fl; - wuss_window_t *win_fl; - int i, old_titlebar_x1, found; + static test_task_t tc_fl; + wuss_task_t *delegate_fl; + box_t box_fl, after_fl; + wuss_window_t *win_fl; + int i, old_titlebar_x1, found; tc_fl.redraw_count = 0; tc_fl.mouse_count = 0; @@ -3770,11 +3770,11 @@ result_t wuss_test(const char *resources) printf("test: dragging a clear window onto an occluder leaves the occluder untouched\n"); { - static test_task_t tc_n, tc_o; - wuss_task_t *delegate_n, *delegate_o; - box_t box_n, box_o, visible_o, exposed, occluded, region; - wuss_window_t *win_n, *win_o; - int i, exposed_dirty, occluded_dirty; + static test_task_t tc_n, tc_o; + wuss_task_t *delegate_n, *delegate_o; + box_t box_n, box_o, visible_o, exposed, occluded, region; + wuss_window_t *win_n, *win_o; + int i, exposed_dirty, occluded_dirty; tc_n.redraw_count = 0; tc_n.mouse_count = 0; @@ -3871,12 +3871,12 @@ result_t wuss_test(const char *resources) printf("test: moving a partly-occluded window blits its clean part and only repaints the occluded part\n"); { - static test_task_t tc_a, tc_b; - wuss_task_t *delegate_a, *delegate_b; - box_t box_a, box_b, visible_a, visible_b_before; - box_t clean_new, hidden_new, region; - wuss_window_t *win_a, *win_b; - int i, dx, clean_dirty, hidden_dirty; + static test_task_t tc_a, tc_b; + wuss_task_t *delegate_a, *delegate_b; + box_t box_a, box_b, visible_a, visible_b_before; + box_t clean_new, hidden_new, region; + wuss_window_t *win_a, *win_b; + int i, dx, clean_dirty, hidden_dirty; tc_b.redraw_count = 0; tc_b.mouse_count = 0; @@ -3970,12 +3970,12 @@ result_t wuss_test(const char *resources) printf("test: moving a window whose occluded piece was never blitted doesn't redraw the occluder\n"); { - static test_task_t tc_a, tc_b; - wuss_task_t *delegate_a, *delegate_b; - box_t box_a, box_b, visible_a, visible_b_before; - box_t region; - wuss_window_t *win_a, *win_b; - int i, occluder_dirty; + static test_task_t tc_a, tc_b; + wuss_task_t *delegate_a, *delegate_b; + box_t box_a, box_b, visible_a, visible_b_before; + box_t region; + wuss_window_t *win_a, *win_b; + int i, occluder_dirty; tc_b.redraw_count = 0; tc_b.mouse_count = 0; @@ -4046,12 +4046,12 @@ result_t wuss_test(const char *resources) printf("test: moving a window split by a mid-band occluder past the gap between bands blits both bands in a safe order\n"); { - static test_task_t tc_a, tc_b; - wuss_task_t *delegate_a, *delegate_b; - box_t box_a, box_b, visible_b_before; - box_t occluded_overlap, hidden_new, region; - wuss_window_t *win_a, *win_b; - int i, occluded_dirty, hidden_dirty; + static test_task_t tc_a, tc_b; + wuss_task_t *delegate_a, *delegate_b; + box_t box_a, box_b, visible_b_before; + box_t occluded_overlap, hidden_new, region; + wuss_window_t *win_a, *win_b; + int i, occluded_dirty, hidden_dirty; tc_b.redraw_count = 0; tc_b.mouse_count = 0; @@ -4156,11 +4156,11 @@ result_t wuss_test(const char *resources) printf("test: dragging a window deeper under a corner occluder blits both L-shaped pieces in a safe order\n"); { - static test_task_t tc_a, tc_b; - wuss_task_t *delegate_a, *delegate_b; - box_t box_a, box_b, visible_b_before, visible_a, region; - wuss_window_t *win_a, *win_b; - int i; + static test_task_t tc_a, tc_b; + wuss_task_t *delegate_a, *delegate_b; + box_t box_a, box_b, visible_b_before, visible_a, region; + wuss_window_t *win_a, *win_b; + int i; tc_b.redraw_count = 0; tc_b.mouse_count = 0; @@ -4220,8 +4220,8 @@ result_t wuss_test(const char *resources) /* The blit must have actually happened, not fallen back: B's own * footprint (outside A) must not be dirtied wholesale. */ { - box_t visible_b_after, whole_footprint, dirty_area_box; - int dirty_area, footprint_area; + box_t visible_b_after, whole_footprint, dirty_area_box; + int dirty_area, footprint_area; wuss_window_get_visible_bounds(win_b, &visible_b_after); box_union(&visible_b_before, &visible_b_after, &whole_footprint); @@ -4287,11 +4287,11 @@ result_t wuss_test(const char *resources) printf("test: mouse and scroll events arrive in virtual content space, with the scroll offset applied exactly once\n"); { - static test_task_t tc_s = { 0 }; - wuss_task_t *delegate_s; - box_t box_s, content_s; - wuss_window_t *win_s; - point_t scroll; + static test_task_t tc_s = { 0 }; + wuss_task_t *delegate_s; + box_t box_s, content_s; + wuss_window_t *win_s; + point_t scroll; delegate_s = mk_task(wuss, test_handle, &tc_s); if (delegate_s == NULL) goto Failure; @@ -4357,10 +4357,10 @@ result_t wuss_test(const char *resources) * content from them -- is added outside the requested content box, never * carved out of it, so what the caller asks for is what it gets, both at * creation and after a resize. */ - static test_task_t tc_r; - wuss_task_t *delegate_r; - box_t box_r, content_r; - wuss_window_t *win_r; + static test_task_t tc_r; + wuss_task_t *delegate_r; + box_t box_r, content_r; + wuss_window_t *win_r; memset(&tc_r, 0, sizeof(tc_r)); delegate_r = mk_task(wuss, test_handle, &tc_r); @@ -4402,11 +4402,11 @@ result_t wuss_test(const char *resources) { /* Windows created without a position are packed towards the top-left and * must not overlap; closing one frees its slot for the next create. */ - static test_task_t tc_p[4]; - wuss_task_t *delegate_p; - wuss_window_t *win_p[4]; - box_t vis[4], probe; - int k, m; + static test_task_t tc_p[4]; + wuss_task_t *delegate_p; + wuss_window_t *win_p[4]; + box_t vis[4], probe; + int k, m; memset(tc_p, 0, sizeof(tc_p)); delegate_p = mk_task(wuss, test_handle, &tc_p[0]); @@ -4469,8 +4469,8 @@ result_t wuss_test(const char *resources) static const colour_t too_long[3] = { { 0xFF000000 }, { 0xFF808080 }, { 0xFFFFFFFF } }; - static test_task_t tc_pa, tc_pb; - wuss_task_t *delegate_pa, *delegate_pb; + static test_task_t tc_pa, tc_pb; + wuss_task_t *delegate_pa, *delegate_pb; /* every earlier block-local test_task_t is out of scope now; drop the * tasks that still point at it so the broadcast counts are exactly the @@ -4511,10 +4511,10 @@ result_t wuss_test(const char *resources) printf("test: wuss_window_set_hidden fires PRE_SHOW; a veto keeps the window hidden\n"); { - static test_task_t tc_ps; - wuss_task_t *delegate_ps; - box_t box_ps; - wuss_window_t *win_ps; + static test_task_t tc_ps; + wuss_task_t *delegate_ps; + box_t box_ps; + wuss_window_t *win_ps; memset(&tc_ps, 0, sizeof(tc_ps)); delegate_ps = mk_task(wuss, test_handle, &tc_ps); @@ -4576,10 +4576,10 @@ result_t wuss_test(const char *resources) printf("test: wuss_task_destroy sends one wuss_EVENT_QUIT and closes the task's windows\n"); { - static test_task_t tc_q; - wuss_task_t *delegate_q; - box_t box_q; - wuss_window_t *win_q1, *win_q2; + static test_task_t tc_q; + wuss_task_t *delegate_q; + box_t box_q; + wuss_window_t *win_q1, *win_q2; memset(&tc_q, 0, sizeof(tc_q)); delegate_q = mk_task(wuss, test_handle, &tc_q); @@ -4611,9 +4611,9 @@ result_t wuss_test(const char *resources) printf("test: an autoclose task self-destructs when its last window closes and stops receiving broadcasts\n"); { static test_task_t tc_ac; - wuss_task_t *delegate_ac; - box_t box_ac; - wuss_window_t *win_ac1, *win_ac2; + wuss_task_t *delegate_ac; + box_t box_ac; + wuss_window_t *win_ac1, *win_ac2; memset(&tc_ac, 0, sizeof(tc_ac)); delegate_ac = mk_task(wuss, test_handle, &tc_ac); @@ -4661,9 +4661,9 @@ result_t wuss_test(const char *resources) printf("test: a task reaping itself from inside wuss_idle does not derail the task walk\n"); { static test_task_t tc_w1, tc_w2, tc_w3; - wuss_task_t *delegate_w1, *delegate_w2, *delegate_w3; - box_t box_w; - wuss_window_t *win_w1, *win_w3; + wuss_task_t *delegate_w1, *delegate_w2, *delegate_w3; + box_t box_w; + wuss_window_t *win_w1, *win_w3; memset(&tc_w1, 0, sizeof(tc_w1)); memset(&tc_w2, 0, sizeof(tc_w2)); @@ -4738,7 +4738,7 @@ result_t wuss_test(const char *resources) "Help", borrowed_items, NELEMS(borrowed_items) }; - wuss_menu_t *m; + wuss_menu_t *m; const wuss_menu_t *sub; /* "Root" is the root caption (first token), then items App, @@ -4865,10 +4865,10 @@ MenuOK: ; /* a SYSTEM-class wuss font is dropped from the menu; NONE-class and * unnamed slots are not */ { - const char *sysfontfile; - bmfont_t *sysfont; - wuss_font_desc_t sysdescs[2]; - wuss_t *syswuss; + const char *sysfontfile; + bmfont_t *sysfont; + wuss_font_desc_t sysdescs[2]; + wuss_t *syswuss; sysfontfile = path_join_filename(resources, 3, "resources", "bmfonts", path_join_leafname("Symbols", "png")); @@ -4882,7 +4882,7 @@ MenuOK: ; sysdescs[1].font_class = wuss_FONT_CLASS_NONE; sysdescs[1].name = "Tiny"; /* NONE class: not skipped though named */ - rc = wuss_create(&scr, sysdescs, 2, NULL, 0, NULL, NULL, &syswuss); + rc = wuss_create(&scr, sysdescs, 2, NULL, 0, NULL, NULL, NULL, &syswuss); if (rc != result_OK) { bmfont_destroy(sysfont); @@ -5004,15 +5004,15 @@ ColourMenuOK: ; "Pick", flash_items, NELEMS(flash_items) }; - const char *fontfile; - bmfont_t *font = NULL; - wuss_font_desc_t fdesc; - screen_t fscr; - bitmap_t fbm; - void *fpixels; - wuss_t *fwuss; - test_task_t ftc; - wuss_task_t *fowner; + const char *fontfile; + bmfont_t *font = NULL; + wuss_font_desc_t fdesc; + screen_t fscr; + bitmap_t fbm; + void *fpixels; + wuss_t *fwuss; + test_task_t ftc; + wuss_task_t *fowner; struct wuss__menu *chain; int i; @@ -5037,7 +5037,7 @@ ColourMenuOK: ; fdesc.font = font; fdesc.font_class = wuss_FONT_CLASS_NONE; fdesc.name = NULL; - rc = wuss_create(&fscr, &fdesc, 1, NULL, 0, NULL, NULL, &fwuss); + rc = wuss_create(&fscr, &fdesc, 1, NULL, 0, NULL, NULL, NULL, &fwuss); if (rc != result_OK) goto FlashFailFree; memset(&ftc, 0, sizeof(ftc)); @@ -5137,15 +5137,15 @@ ColourMenuOK: ; "Root", sm_items, NELEMS(sm_items) }; - const char *fontfile; - bmfont_t *font = NULL; - wuss_font_desc_t fdesc; - screen_t sscr; - bitmap_t sbm; - void *spixels; - wuss_t *swuss; - test_task_t stc; - wuss_task_t *sowner; + const char *fontfile; + bmfont_t *font = NULL; + wuss_font_desc_t fdesc; + screen_t sscr; + bitmap_t sbm; + void *spixels; + wuss_t *swuss; + test_task_t stc; + wuss_task_t *sowner; struct wuss__menu *root; struct wuss__menu *sub; @@ -5168,7 +5168,7 @@ ColourMenuOK: ; fdesc.font = font; fdesc.font_class = wuss_FONT_CLASS_NONE; fdesc.name = NULL; - rc = wuss_create(&sscr, &fdesc, 1, NULL, 0, NULL, NULL, &swuss); + rc = wuss_create(&sscr, &fdesc, 1, NULL, 0, NULL, NULL, NULL, &swuss); if (rc != result_OK) goto SubHiFailFree; memset(&stc, 0, sizeof(stc)); @@ -5258,17 +5258,17 @@ ColourMenuOK: ; static const wuss_menu_t menu_a = { "A", menu_a_items, NELEMS(menu_a_items) }; static const wuss_menu_t menu_b = { "B", menu_b_items, NELEMS(menu_b_items) }; - const char *fontfile; - bmfont_t *font = NULL; - wuss_font_desc_t fdesc; - screen_t mscr; - bitmap_t mbm; - void *mpixels; - wuss_t *mwuss; - menu_task_t mta, mtb; - wuss_task_t *task_a, *task_b; - wuss_window_t *wa, *wb; - box_t ba, bb; + const char *fontfile; + bmfont_t *font = NULL; + wuss_font_desc_t fdesc; + screen_t mscr; + bitmap_t mbm; + void *mpixels; + wuss_t *mwuss; + menu_task_t mta, mtb; + wuss_task_t *task_a, *task_b; + wuss_window_t *wa, *wb; + box_t ba, bb; fontfile = path_join_filename(resources, 3, "resources", "bmfonts", path_join_leafname("Tiny", "png")); @@ -5289,7 +5289,7 @@ ColourMenuOK: ; fdesc.font = font; fdesc.font_class = wuss_FONT_CLASS_NONE; fdesc.name = NULL; - rc = wuss_create(&mscr, &fdesc, 1, NULL, 0, NULL, NULL, &mwuss); + rc = wuss_create(&mscr, &fdesc, 1, NULL, 0, NULL, NULL, NULL, &mwuss); if (rc != result_OK) goto MoveFailFree; memset(&mta, 0, sizeof(mta)); @@ -5427,7 +5427,7 @@ ColourMenuOK: ; fdesc.font = font; fdesc.font_class = wuss_FONT_CLASS_NONE; fdesc.name = NULL; - rc = wuss_create(&qscr, &fdesc, 1, NULL, 0, NULL, NULL, &qwuss); + rc = wuss_create(&qscr, &fdesc, 1, NULL, 0, NULL, NULL, NULL, &qwuss); if (rc != result_OK) goto QuitFreeOnly; memset(&qmt, 0, sizeof(qmt)); @@ -6174,7 +6174,7 @@ result_t wuss_test(const char *resources) printf("test: wuss_create (core)\n"); - rc = wuss_create(&scr, NULL, 0, NULL, 0, NULL, NULL, &wuss); + rc = wuss_create(&scr, NULL, 0, NULL, 0, NULL, NULL, NULL, &wuss); if (rc != result_OK) goto Failure; From 226035b1b9bffc52699e804122d7aa5ccbddc7f8 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Wed, 16 Sep 2026 14:46:14 +0100 Subject: [PATCH 109/128] refactor(wuss): normalize demo task X_create to (wuss, out) image_create now locates its own ninepatch and initial image via wuss_get_resources instead of taking them as args. clock, minesweeper, saturn, palette, porter-duff and icons drop their remaining font/palette/config/startup-name params, sourcing them from wuss_get_font_n/wuss_get_palette/a hardcoded default/self-comparison against the live system palette. Every demo X_create now shares the shape (wuss_t *, X_task_t **out), so apps/wuss/tasks.c collapses its 18 hand-written spawn_* wrappers into one spawn_generic plus a {name, create} table. Dead g.palette/g.npalette/g.palette_name/g.daydream_font/g.bold_font globals removed. Co-Authored-By: Claude Sonnet 5 --- apps/wuss/main.c | 6 - apps/wuss/tasks.c | 181 ++++++------------------ apps/wuss/tasks.h | 7 - libraries/wuss/test/tasks/clock.c | 4 +- libraries/wuss/test/tasks/clock.h | 6 +- libraries/wuss/test/tasks/icons.c | 6 +- libraries/wuss/test/tasks/icons.h | 12 +- libraries/wuss/test/tasks/image.c | 16 ++- libraries/wuss/test/tasks/image.h | 16 +-- libraries/wuss/test/tasks/minesweeper.c | 6 +- libraries/wuss/test/tasks/minesweeper.h | 6 +- libraries/wuss/test/tasks/palette.c | 19 ++- libraries/wuss/test/tasks/palette.h | 12 +- libraries/wuss/test/tasks/porter-duff.c | 9 +- libraries/wuss/test/tasks/porter-duff.h | 13 +- libraries/wuss/test/tasks/saturn.c | 6 +- libraries/wuss/test/tasks/saturn.h | 10 +- 17 files changed, 115 insertions(+), 220 deletions(-) diff --git a/apps/wuss/main.c b/apps/wuss/main.c index 0bea03de..1629a288 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -314,8 +314,6 @@ static result_t run_wuss(const char *resources, palette_name, rc, result_string(rc)); goto Failure; } - - g.palette_name = palette_name; } logf_info("wuss: resources root = \"%s\"", resources); @@ -387,10 +385,6 @@ static result_t run_wuss(const char *resources, } g.wuss = wuss; - g.palette = palette; - g.npalette = NELEMS(palette); - g.daydream_font = fonts[0]; /* tasks draw with the regular weight */ - g.bold_font = fonts[1]; g.frontend = frontend; g.bm = &bm; diff --git a/apps/wuss/tasks.c b/apps/wuss/tasks.c index 34523366..49ea1704 100644 --- a/apps/wuss/tasks.c +++ b/apps/wuss/tasks.c @@ -9,7 +9,6 @@ #include "framebuf/bitmap.h" #include "framebuf/colour.h" #include "framebuf/pixelfmt.h" -#include "io/path.h" #include "wuss/task.h" #include "wuss/wuss.h" #include "wuss/window.h" @@ -59,134 +58,23 @@ void tasks_build_screen_palette(colour_t *out, out[n++] = colour_rgb(r * 0x33, g_ * 0x33, b * 0x33); } -/* Each spawn is a thin passthrough to its module's X_create, which allocates - * the per-instance task block itself; the block is owned by its window and - * freed by X_destroy, called from the task's wuss_EVENT_QUIT handler (or, - * for the font-less chars case, never allocated at all -- see - * chars_create). None of these spawns need the task pointer X_create can - * hand back, so they all pass NULL for it. */ +/* Every demo task's X_create now shares one shape: (wuss_t *, X_task_t **). + * The task block it allocates is owned by its window and freed by + * X_destroy, called from the task's wuss_EVENT_QUIT handler; callers here + * never need the block back, so every X_create is invoked with NULL for it, + * which is type-compatible however the second parameter is spelt. That lets + * a single generic spawn walk a table of {name, X_create} instead of one + * hand-written wrapper per task. */ +typedef result_t (*task_create_fn_t)(wuss_t *wuss, void **out); -static result_t spawn_ball(void) -{ - return ball_create(g.wuss, NULL); -} - -static result_t spawn_text(void) -{ - return text_create(g.wuss, NULL); -} - -static result_t spawn_blank(void) -{ - return blank_create(g.wuss, NULL); -} - -static result_t spawn_chars(void) -{ - return chars_create(g.wuss, NULL); -} - -static result_t spawn_palette(void) -{ - return palette_create(g.wuss, g.palette_name, NULL); -} - -static result_t spawn_image(void) -{ - result_t rc; - const char *resources; - const char *leafname; - const char *filename; - char buf[DPTLIB_MAXPATH]; - char ninepatch[DPTLIB_MAXPATH]; - - resources = wuss_get_resources(g.wuss); - leafname = path_join_leafname("jessica", "png"); - filename = path_join_filename(resources, 3, "resources", "images", leafname); - strcpy(buf, filename); - /* path_join_filename returns a shared static buffer; copy before the next - * call (image_create's own dirscan join) clobbers it */ - filename = path_join_filename(resources, 3, "resources", "wuss", - path_join_leafname("ninepatch", "png")); - strcpy(ninepatch, filename); - - logf_info("wuss: image task loading \"%s\" + \"%s\"", buf, ninepatch); - rc = image_create(g.wuss, buf, ninepatch, NULL); - if (rc != result_OK) - logf_error("wuss: image_create(\"%s\") failed, rc=0x%X (%s)", buf, rc, - result_string(rc)); - return rc; -} - -static result_t spawn_checker(void) -{ - return checker_create(g.wuss, NULL); -} - -static result_t spawn_clock(void) -{ - return clock_create(g.wuss, g.daydream_font, NULL); -} - -static result_t spawn_curve(void) -{ - return curve_create(g.wuss, NULL); -} - -static result_t spawn_lissajous(void) -{ - return lissajous_create(g.wuss, NULL); -} - -static result_t spawn_minesweeper(void) -{ - return minesweeper_create(g.wuss, g.bold_font, NULL); -} - -static result_t spawn_saturn(void) -{ - return saturn_create(g.wuss, NULL, NULL); -} - -static result_t spawn_sofa(void) -{ - return sofa_create(g.wuss, NULL); -} - -static result_t spawn_gradient(void) -{ - return gradient_create(g.wuss, NULL); -} - -static result_t spawn_greeble(void) -{ - return greeble_create(g.wuss, NULL); -} - -static result_t spawn_icons(void) -{ - result_t rc; - - rc = icons_create(g.wuss, g.daydream_font, NULL); - if (rc != result_OK) - logf_error("wuss: icons_create failed, rc=0x%X (%s)", - rc, result_string(rc)); - return rc; -} - -static result_t spawn_swatches(void) -{ - return swatches_create(g.wuss, NULL); -} - -static result_t spawn_porter_duff(void) +static result_t spawn_task(const char *name, task_create_fn_t create) { result_t rc; - rc = porter_duff_create(g.wuss, g.palette, g.daydream_font, NULL); + rc = create(g.wuss, NULL); if (rc != result_OK) - logf_error("wuss: porter_duff_create failed, rc=0x%X (%s)", - rc, result_string(rc)); + logf_error("wuss: %s_create failed, rc=0x%X (%s)", name, rc, + result_string(rc)); return rc; } @@ -195,7 +83,36 @@ static result_t spawn_porter_duff(void) * in lock-step: picking row i of that menu calls its spawn[i]. */ typedef result_t (*task_spawn_fn_t)(void); -/* "Launch" submenu: the demo tasks. */ +/* "Launch" submenu: the demo tasks. g_launch_items[i] and g_launch_tasks[i] + * are picked by the same menu row index i -- keep both tables in this + * order. */ +static const struct +{ + const char *name; + task_create_fn_t create; +} +g_launch_tasks[] = +{ + { "Ball", (task_create_fn_t) ball_create }, + { "Blank", (task_create_fn_t) blank_create }, + { "Chars", (task_create_fn_t) chars_create }, + { "Checker", (task_create_fn_t) checker_create }, + { "Clock", (task_create_fn_t) clock_create }, + { "Curve", (task_create_fn_t) curve_create }, + { "Gradient", (task_create_fn_t) gradient_create }, + { "Greeble", (task_create_fn_t) greeble_create }, + { "Icons", (task_create_fn_t) icons_create }, + { "Image", (task_create_fn_t) image_create }, + { "Lissajous", (task_create_fn_t) lissajous_create }, + { "Minesweeper", (task_create_fn_t) minesweeper_create }, + { "Palette", (task_create_fn_t) palette_create }, + { "Porter-Duff", (task_create_fn_t) porter_duff_create }, + { "Saturn", (task_create_fn_t) saturn_create }, + { "Sofa", (task_create_fn_t) sofa_create }, + { "Swatches", (task_create_fn_t) swatches_create }, + { "Text", (task_create_fn_t) text_create } +}; + static const wuss_menu_item_t g_launch_items[] = { { "Ball", wuss_MENU_ITEM_NONE, NULL }, @@ -218,15 +135,6 @@ static const wuss_menu_item_t g_launch_items[] = { "Text", wuss_MENU_ITEM_NONE, NULL } }; -static const task_spawn_fn_t g_launch_spawn[] = -{ - spawn_ball, spawn_blank, spawn_chars, spawn_checker, spawn_clock, spawn_curve, - spawn_gradient, spawn_greeble, spawn_icons, spawn_image, spawn_lissajous, - spawn_minesweeper, spawn_palette, - spawn_porter_duff, spawn_saturn, spawn_sofa, spawn_swatches, - spawn_text -}; - static const wuss_menu_t g_launch_menu = { "Launch", g_launch_items, NELEMS(g_launch_items) @@ -321,8 +229,9 @@ result_t task_handle_event(wuss_window_t *window, if (menu == &g_launch_menu) { - if (index >= 0 && index < (int) NELEMS(g_launch_spawn)) - (void) g_launch_spawn[index](); + if (index >= 0 && index < (int) NELEMS(g_launch_tasks)) + (void) spawn_task(g_launch_tasks[index].name, + g_launch_tasks[index].create); return result_OK; } diff --git a/apps/wuss/tasks.h b/apps/wuss/tasks.h index 762de915..75f8e928 100644 --- a/apps/wuss/tasks.h +++ b/apps/wuss/tasks.h @@ -6,7 +6,6 @@ #include #include "framebuf/bitmap.h" -#include "framebuf/bmfont.h" #include "framebuf/colour.h" #include "geom/point.h" #include "wuss/task.h" @@ -26,12 +25,6 @@ extern struct wuss_app_tasks wuss_task_t *menu_task; /* owns the task launcher menus */ wuss_proginfo_t *proginfo; /* shared "Info" dialogue on menu_task; see * tasks_open_launcher's "Info" row */ - colour_t *palette; - int npalette; - const char *palette_name; /* startup *.hex leafname, for ticking the - * picker menu's initial selection */ - bmfont_t *daydream_font; - bmfont_t *bold_font; bool quit; /* set by the "Quit Wuss" task-menu entry */ wuss_frontend_t *frontend; /* pushed to on wuss_EVENT_PALETTE; see * task_handle_event */ diff --git a/libraries/wuss/test/tasks/clock.c b/libraries/wuss/test/tasks/clock.c index a11eb68f..bfb50be7 100644 --- a/libraries/wuss/test/tasks/clock.c +++ b/libraries/wuss/test/tasks/clock.c @@ -101,7 +101,7 @@ static result_t clock_create_window(wuss_t *wuss, &task->window); } -result_t clock_create(wuss_t *wuss, bmfont_t *font, clock_task_t **out) +result_t clock_create(wuss_t *wuss, clock_task_t **out) { result_t rc; clock_task_t *task; @@ -112,7 +112,7 @@ result_t clock_create(wuss_t *wuss, bmfont_t *font, clock_task_t **out) if (task == NULL) return result_OOM; - task->font = font; + task->font = wuss_get_font_n(wuss, 0); task->bg = colour_rgb(0x1D, 0x2B, 0x53); task->bezel = colour_rgb(0xFF, 0xF1, 0xE8); task->hand = colour_rgb(0xFF, 0xF1, 0xE8); diff --git a/libraries/wuss/test/tasks/clock.h b/libraries/wuss/test/tasks/clock.h index 84238c30..1e8ae8e0 100644 --- a/libraries/wuss/test/tasks/clock.h +++ b/libraries/wuss/test/tasks/clock.h @@ -26,9 +26,9 @@ clock_task_t; wuss_window_fn_t clock_handle; /* create the clock window against the given wuss instance, lettering the - * face with the given (caller-owned) font. if out is non-NULL, the task - * block is also returned through it */ -result_t clock_create(wuss_t *wuss, bmfont_t *font, clock_task_t **out); + * face with wuss's own regular font (wuss_get_font_n(wuss, 0)). if out is + * non-NULL, the task block is also returned through it */ +result_t clock_create(wuss_t *wuss, clock_task_t **out); /* free a task block allocated by clock_create; normally called by the * window's wuss_EVENT_QUIT handler, not by callers directly */ diff --git a/libraries/wuss/test/tasks/icons.c b/libraries/wuss/test/tasks/icons.c index 6e3f5cb7..c3b50346 100644 --- a/libraries/wuss/test/tasks/icons.c +++ b/libraries/wuss/test/tasks/icons.c @@ -562,9 +562,7 @@ static void icons_add_menu(icons_layout_t *lay, int *ticked) /* ----------------------------------------------------------------------- */ -result_t icons_create(wuss_t *wuss, - bmfont_t *font, - icons_task_t **out) +result_t icons_create(wuss_t *wuss, icons_task_t **out) { wuss_task_t *delegate; wuss_task_desc_t delegate_desc; @@ -582,7 +580,7 @@ result_t icons_create(wuss_t *wuss, if (task == NULL) return result_OOM; - task->font = font; + task->font = wuss_get_font_n(wuss, 0); task->label = colour_rgb(0x00, 0x00, 0x00); /* only the ruler-text glyph blend; approximates the wuss_COLOUR_WINDOW / * wuss_COLOUR_BACKDROP crosshatch the rulers sit on -- no public call diff --git a/libraries/wuss/test/tasks/icons.h b/libraries/wuss/test/tasks/icons.h index a325dd1a..c586ef34 100644 --- a/libraries/wuss/test/tasks/icons.h +++ b/libraries/wuss/test/tasks/icons.h @@ -37,12 +37,12 @@ icons_task_t; wuss_window_fn_t icons_handle; -/* create the icons window against the given wuss instance; the sprite and - * icon set are loaded from wuss_get_resources(wuss)/resources/wuss/. if out - * is non-NULL, the task block is also returned through it */ -result_t icons_create(wuss_t *wuss, - bmfont_t *font, - icons_task_t **out); +/* create the icons window against the given wuss instance, lettering the + * axis rulers with wuss's own regular font (wuss_get_font_n(wuss, 0)); the + * sprite and icon set are loaded from + * wuss_get_resources(wuss)/resources/wuss/. if out is non-NULL, the task + * block is also returned through it */ +result_t icons_create(wuss_t *wuss, icons_task_t **out); /* free a task block allocated by icons_create; normally called by the * window's wuss_EVENT_QUIT handler, not by callers directly */ diff --git a/libraries/wuss/test/tasks/image.c b/libraries/wuss/test/tasks/image.c index 9c8d6897..c04da8b5 100644 --- a/libraries/wuss/test/tasks/image.c +++ b/libraries/wuss/test/tasks/image.c @@ -59,10 +59,7 @@ static result_t load_png_deep(bitmap_t *bm, const char *filename) return result_OK; } -result_t image_create(wuss_t *wuss, - const char *path, - const char *background_path, - image_task_t **out) +result_t image_create(wuss_t *wuss, image_task_t **out) { result_t rc; image_task_t *task; @@ -70,6 +67,8 @@ result_t image_create(wuss_t *wuss, wuss_task_desc_t delegate_desc; const char *resources; const char *images_dir; + const char *path; + const char *background_path; size2d_t sz; task = calloc(1, sizeof(*task)); @@ -96,7 +95,14 @@ result_t image_create(wuss_t *wuss, free(task); /* nothing registered yet; nobody else owns it */ return rc; } + if (task->nnames == 0) + { + free(task); /* nothing registered yet; nobody else owns it */ + return result_BAD_ARG; /* no PNGs found under resources/images */ + } + path = path_join_filename(resources, 3, "resources", "images", + path_join_leafname(task->names[0], "png")); rc = load_png_deep(&task->bitmap, path); if (rc != result_OK) { @@ -104,6 +110,8 @@ result_t image_create(wuss_t *wuss, return rc; } + background_path = path_join_filename(resources, 2, "resources", "wuss", + path_join_leafname("ninepatch", "png")); rc = load_png_deep(&task->ninepatch, background_path); if (rc != result_OK) { diff --git a/libraries/wuss/test/tasks/image.h b/libraries/wuss/test/tasks/image.h index cafaba23..405058f2 100644 --- a/libraries/wuss/test/tasks/image.h +++ b/libraries/wuss/test/tasks/image.h @@ -47,15 +47,13 @@ image_task_t; wuss_window_fn_t image_handle; -/* load the PNG at path (and the 9-patch PNG at background_path, drawn tiled - * behind it) and create its window against the given wuss instance. a click - * cycles through resources/images (PNG files) by reloading a different - * leafname, using wuss_get_resources(wuss) as the root resource directory. - * if out is non-NULL, the task block is also returned through it */ -result_t image_create(wuss_t *wuss, - const char *path, - const char *background_path, - image_task_t **out); +/* scan wuss_get_resources(wuss)/resources/images for PNGs, load the first + * one found (and the 9-patch PNG at + * wuss_get_resources(wuss)/resources/wuss/ninepatch.png, drawn tiled behind + * it), and create its window against the given wuss instance. a click cycles + * to a different leafname from the scan. if out is non-NULL, the task block + * is also returned through it */ +result_t image_create(wuss_t *wuss, image_task_t **out); /* free a task block allocated by image_create; normally called by the * window's wuss_EVENT_QUIT handler, not by callers directly */ diff --git a/libraries/wuss/test/tasks/minesweeper.c b/libraries/wuss/test/tasks/minesweeper.c index 7fd1f78f..3432976f 100644 --- a/libraries/wuss/test/tasks/minesweeper.c +++ b/libraries/wuss/test/tasks/minesweeper.c @@ -247,9 +247,7 @@ static void minesweeper_tick_clock(minesweeper_task_t *ms) /* ----------------------------------------------------------------------- */ -result_t minesweeper_create(wuss_t *wuss, - bmfont_t *font, - minesweeper_task_t **out) +result_t minesweeper_create(wuss_t *wuss, minesweeper_task_t **out) { result_t rc; minesweeper_task_t *task; @@ -261,7 +259,7 @@ result_t minesweeper_create(wuss_t *wuss, return result_OOM; task->wuss = wuss; - task->font = font; + task->font = wuss_get_font_n(wuss, 1); minesweeper_set_size(task, minesweeper_SIZE_12X12); delegate_desc.handle = minesweeper_handle; diff --git a/libraries/wuss/test/tasks/minesweeper.h b/libraries/wuss/test/tasks/minesweeper.h index 20dab5b9..d686f0a1 100644 --- a/libraries/wuss/test/tasks/minesweeper.h +++ b/libraries/wuss/test/tasks/minesweeper.h @@ -65,11 +65,9 @@ minesweeper_task_t; wuss_window_fn_t minesweeper_handle; /* create the minesweeper window against the given wuss instance, lettering - * neighbour counts with the given (caller-owned) font. + * neighbour counts with wuss's own bold font (wuss_get_font_n(wuss, 1)). * if out is non-NULL, the task block is also returned through it */ -result_t minesweeper_create(wuss_t *wuss, - bmfont_t *font, - minesweeper_task_t **out); +result_t minesweeper_create(wuss_t *wuss, minesweeper_task_t **out); /* free a task block allocated by minesweeper_create; normally called by the * window's wuss_EVENT_QUIT handler, not by callers directly */ diff --git a/libraries/wuss/test/tasks/palette.c b/libraries/wuss/test/tasks/palette.c index ca2ae32f..5dfaa5c2 100644 --- a/libraries/wuss/test/tasks/palette.c +++ b/libraries/wuss/test/tasks/palette.c @@ -73,9 +73,7 @@ result_t palette_load_hex(const char *resources, /* ----------------------------------------------------------------------- */ -result_t palette_create(wuss_t *wuss, - const char *startup_name, - palette_task_t **out) +result_t palette_create(wuss_t *wuss, palette_task_t **out) { result_t rc; palette_task_t *task; @@ -83,6 +81,8 @@ result_t palette_create(wuss_t *wuss, const char *resources; const char *dir; char dirbuf[DPTLIB_MAXPATH]; + const colour_t *current; + int ncurrent; int i; task = calloc(1, sizeof(*task)); @@ -101,13 +101,22 @@ result_t palette_create(wuss_t *wuss, sizeof(task->names[0]), PALETTE_MAX_FILES, 1 /* sorted */, &task->nnames); - if (startup_name != NULL) + /* tick whichever *.hex file matches wuss's current system palette, so the + * picker starts in sync with what wuss_create actually loaded, without the + * caller having to tell us its leafname separately */ + current = wuss_get_palette(wuss, &ncurrent); + if (ncurrent == PALETTE_NCOLOURS) for (i = 0; i < task->nnames; i++) - if (strcmp(task->names[i], startup_name) == 0) + { + colour_t candidate[PALETTE_NCOLOURS]; + + if (palette_load_hex(resources, task->names[i], candidate) == result_OK && + memcmp(candidate, current, sizeof(candidate)) == 0) { task->selected = i; break; } + } /* built once; ticks are refreshed from task->selected/invert on each open */ for (i = 0; i < task->nnames; i++) diff --git a/libraries/wuss/test/tasks/palette.h b/libraries/wuss/test/tasks/palette.h index 74450620..1d966c9c 100644 --- a/libraries/wuss/test/tasks/palette.h +++ b/libraries/wuss/test/tasks/palette.h @@ -70,14 +70,10 @@ result_t palette_load_hex(const char *resources, /* create the palette-swatch-grid window against the given wuss instance. * wuss_get_resources(wuss)/resources/palettes holds the *.hex files; it is - * scanned once here to build the picker menu. The swatch grid always draws - * wuss's current system palette (wuss_get_palette), so `startup_name` -- its - * *.hex leafname, no extension -- is used only to tick the matching row in - * the picker menu; NULL ticks none. if out is non-NULL, the task block is - * also returned through it. */ -result_t palette_create(wuss_t *wuss, - const char *startup_name, - palette_task_t **out); + * scanned once here to build the picker menu, ticking whichever file's + * content matches wuss's current system palette (wuss_get_palette), if any. + * if out is non-NULL, the task block is also returned through it. */ +result_t palette_create(wuss_t *wuss, palette_task_t **out); /* free a task block allocated by palette_create; normally called by the * window's wuss_EVENT_QUIT handler, not by callers directly */ diff --git a/libraries/wuss/test/tasks/porter-duff.c b/libraries/wuss/test/tasks/porter-duff.c index 3afff772..eef37129 100644 --- a/libraries/wuss/test/tasks/porter-duff.c +++ b/libraries/wuss/test/tasks/porter-duff.c @@ -157,22 +157,21 @@ static result_t load_demo_png(bitmap_t *bm, /* ----------------------------------------------------------------------- */ -result_t porter_duff_create(wuss_t *wuss, - const colour_t *palette, - bmfont_t *font, - porter_duff_task_t **out) +result_t porter_duff_create(wuss_t *wuss, porter_duff_task_t **out) { result_t rc; porter_duff_task_t *task; wuss_task_t *delegate; wuss_task_desc_t delegate_desc; const char *resources; + const colour_t *palette; task = calloc(1, sizeof(*task)); if (task == NULL) return result_OOM; - task->font = font; + palette = wuss_get_palette(wuss, NULL); + task->font = wuss_get_font_n(wuss, 0); task->rule = composite_RULE_CLEAR; task->frame = 0; task->frames_per_rule = PD_FRAMES_DEFAULT; diff --git a/libraries/wuss/test/tasks/porter-duff.h b/libraries/wuss/test/tasks/porter-duff.h index e4738bd9..5e15dde5 100644 --- a/libraries/wuss/test/tasks/porter-duff.h +++ b/libraries/wuss/test/tasks/porter-duff.h @@ -35,13 +35,12 @@ porter_duff_task_t; wuss_window_fn_t porter_duff_handle; /* load the two demo images and create the window against the given wuss - * instance; the images are loaded from wuss_get_resources(wuss) (the DPTLib - * repo root); if out is non-NULL, the task block is also returned through - * it */ -result_t porter_duff_create(wuss_t *wuss, - const colour_t *palette, - bmfont_t *font, - porter_duff_task_t **out); + * instance, colouring the checkerboard/label from wuss's own palette + * (wuss_get_palette) and lettering the rule name with wuss's own regular + * font (wuss_get_font_n(wuss, 0)); the images are loaded from + * wuss_get_resources(wuss) (the DPTLib repo root); if out is non-NULL, the + * task block is also returned through it */ +result_t porter_duff_create(wuss_t *wuss, porter_duff_task_t **out); /* free a task block allocated by porter_duff_create; normally called by the * window's wuss_EVENT_QUIT handler, not by callers directly */ diff --git a/libraries/wuss/test/tasks/saturn.c b/libraries/wuss/test/tasks/saturn.c index 6e964b6e..f602c1e1 100644 --- a/libraries/wuss/test/tasks/saturn.c +++ b/libraries/wuss/test/tasks/saturn.c @@ -165,9 +165,7 @@ static result_t saturn_conf_fillout(void *opaque); static result_t saturn_conf_cancel(void *opaque, wuss_button_t button); static result_t saturn_conf_apply_action(void *opaque, wuss_button_t button); -result_t saturn_create(wuss_t *wuss, - const saturn_config_t *config, - saturn_task_t **out) +result_t saturn_create(wuss_t *wuss, saturn_task_t **out) { static const saturn_config_t default_config = SATURN_CONFIG_DEFAULT; @@ -184,7 +182,7 @@ result_t saturn_create(wuss_t *wuss, task->bg = colour_rgb(0x00, 0x00, 0x00); task->fg = colour_rgb(0xFF, 0xFF, 0xFF); task->seed = 1; - task->config = (config != NULL) ? *config : default_config; + task->config = default_config; task->fg_colourmenu = NULL; task->bg_colourmenu = NULL; task->menu_handle = NULL; diff --git a/libraries/wuss/test/tasks/saturn.h b/libraries/wuss/test/tasks/saturn.h index e72ee934..7c1d0eec 100644 --- a/libraries/wuss/test/tasks/saturn.h +++ b/libraries/wuss/test/tasks/saturn.h @@ -82,13 +82,11 @@ saturn_task_t; wuss_window_fn_t saturn_handle; -/* create the planet window against the given wuss instance; the task block is - * allocated here, owned by the window and freed when it closes. "config" may - * be NULL for SATURN_CONFIG_DEFAULT. +/* create the planet window against the given wuss instance, always starting + * at SATURN_CONFIG_DEFAULT; the task block is allocated here, owned by the + * window and freed when it closes. * if out is non-NULL, the task block is also returned through it */ -result_t saturn_create(wuss_t *wuss, - const saturn_config_t *config, - saturn_task_t **out); +result_t saturn_create(wuss_t *wuss, saturn_task_t **out); /* free a task block allocated by saturn_create; normally called by the * window's wuss_EVENT_QUIT handler, not by callers directly */ From 2bfdbbe40f127b92327fdacc7085eca6532fae5b Mon Sep 17 00:00:00 2001 From: David Thomas Date: Wed, 16 Sep 2026 16:49:24 +0100 Subject: [PATCH 110/128] feat(wuss): rename global g to g_tasks, spawn Saturn at startup g -> g_tasks avoids shadowing the g/b loop locals in tasks_build_screen_palette. saturn_create at startup fixes a use-after-free in saturn_destroy: wuss_destroy sweeps tasks (freeing the dialogue window borrowed onto the Configuration menu leaf) before closing any leftover open chain, so saturn_destroy now closes its own menu_handle first if still open. Co-Authored-By: Claude Sonnet 5 --- apps/wuss/main.c | 26 ++++++++++++++++---------- apps/wuss/tasks.c | 28 ++++++++++++++-------------- apps/wuss/tasks.h | 2 +- libraries/wuss/test/tasks/saturn.c | 12 ++++++++++++ 4 files changed, 43 insertions(+), 25 deletions(-) diff --git a/apps/wuss/main.c b/apps/wuss/main.c index 1629a288..316ef6d5 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -36,6 +36,7 @@ #include "tasks.h" #include "tasks/palette.h" /* palette_load_hex for the startup *.hex */ +#include "tasks/saturn.h" /* saturn_create at startup */ /* ----------------------------------------------------------------------- */ @@ -153,7 +154,7 @@ static void wuss_frame(void *arg) switch (ev.kind) { case wuss_INPUT_QUIT: - g.quit = true; + g_tasks.quit = true; break; case wuss_INPUT_REDRAW_ALL: @@ -384,9 +385,9 @@ static result_t run_wuss(const char *resources, goto Failure; } - g.wuss = wuss; - g.frontend = frontend; - g.bm = &bm; + g_tasks.wuss = wuss; + g_tasks.frontend = frontend; + g_tasks.bm = &bm; { wuss_task_desc_t desc; @@ -394,7 +395,7 @@ static result_t run_wuss(const char *resources, desc.handle = task_handle_event; desc.task_data = NULL; desc.name = "menu"; - rc = wuss_task_create(wuss, &desc, &g.menu_task); + rc = wuss_task_create(wuss, &desc, &g_tasks.menu_task); logf_info("wuss: wuss_task_create(menu) -> rc=0x%X (%s)", rc, result_string(rc)); if (rc != result_OK) @@ -413,11 +414,16 @@ static result_t run_wuss(const char *resources, "1.0 (" __DATE__ ")" }; - if (wuss_proginfo_create(&g.proginfo, g.menu_task, &desc) != result_OK) - g.proginfo = NULL; + if (wuss_proginfo_create(&g_tasks.proginfo, g_tasks.menu_task, &desc) != result_OK) + g_tasks.proginfo = NULL; } - g.quit = false; + g_tasks.quit = false; + + rc = saturn_create(wuss, NULL); + logf_info("wuss: saturn_create -> rc=0x%X (%s)", rc, result_string(rc)); + if (rc != result_OK) + goto Failure; wuss_redraw(wuss); @@ -441,7 +447,7 @@ static result_t run_wuss(const char *resources, * for requestAnimationFrame pacing. */ emscripten_set_main_loop_arg(wuss_frame, &ctx, 0, 1); #else - while (!g.quit) + while (!g_tasks.quit) wuss_frame(&ctx); #endif } @@ -450,7 +456,7 @@ static result_t run_wuss(const char *resources, * frees every registered task node, but not the per-instance task_data * block a spawn_* calloc'd, so any task window left open at quit leaks that * block. Harmless at process exit. */ - wuss_proginfo_destroy(g.proginfo); /* closes its dialogue window */ + wuss_proginfo_destroy(g_tasks.proginfo); /* closes its dialogue window */ wuss_destroy(wuss); /* also sweeps g.menu_task and closes any open chain */ for (i = 0; i < nfonts; i++) diff --git a/apps/wuss/tasks.c b/apps/wuss/tasks.c index 49ea1704..09b2215f 100644 --- a/apps/wuss/tasks.c +++ b/apps/wuss/tasks.c @@ -38,14 +38,14 @@ /* ----------------------------------------------------------------------- */ -struct wuss_app_tasks g; +struct wuss_app_tasks g_tasks; void tasks_build_screen_palette(colour_t *out, int nout, const colour_t *ui, int nui) { - int r, g_, b, n; + int r, g, b, n; memset(out, 0, nout * sizeof(*out)); @@ -53,9 +53,9 @@ void tasks_build_screen_palette(colour_t *out, memcpy(out, ui, n * sizeof(*out)); for (r = 0; r < 6 && n < nout; r++) - for (g_ = 0; g_ < 6 && n < nout; g_++) + for (g = 0; g < 6 && n < nout; g++) for (b = 0; b < 6 && n < nout; b++) - out[n++] = colour_rgb(r * 0x33, g_ * 0x33, b * 0x33); + out[n++] = colour_rgb(r * 0x33, g * 0x33, b * 0x33); } /* Every demo task's X_create now shares one shape: (wuss_t *, X_task_t **). @@ -71,7 +71,7 @@ static result_t spawn_task(const char *name, task_create_fn_t create) { result_t rc; - rc = create(g.wuss, NULL); + rc = create(g_tasks.wuss, NULL); if (rc != result_OK) logf_error("wuss: %s_create failed, rc=0x%X (%s)", name, rc, result_string(rc)); @@ -142,7 +142,7 @@ static const wuss_menu_t g_launch_menu = static result_t spawn_quit(void) { - g.quit = true; + g_tasks.quit = true; return result_OK; } @@ -203,21 +203,21 @@ result_t task_handle_event(wuss_window_t *window, colour_t scr_palette[256]; int scr_nentries; - palette = wuss_get_palette(g.wuss, &npalette); - scr_nentries = pixelfmt_paletted_nentries(g.bm->format); + palette = wuss_get_palette(g_tasks.wuss, &npalette); + scr_nentries = pixelfmt_paletted_nentries(g_tasks.bm->format); if (scr_nentries > 0) { tasks_build_screen_palette(scr_palette, scr_nentries, palette, npalette); - bitmap_set_palette(g.bm, scr_palette); + bitmap_set_palette(g_tasks.bm, scr_palette); } - wuss_frontend_set_palette(g.frontend, palette, npalette); + wuss_frontend_set_palette(g_tasks.frontend, palette, npalette); return result_OK; } if (event->kind == wuss_EVENT_PRE_SHOW) { - if (window == wuss_proginfo_window(g.proginfo)) - return wuss_proginfo_handle_pre_show(g.proginfo); + if (window == wuss_proginfo_window(g_tasks.proginfo)) + return wuss_proginfo_handle_pre_show(g_tasks.proginfo); return result_OK; } @@ -247,7 +247,7 @@ result_t task_handle_event(wuss_window_t *window, result_t tasks_open_launcher(point_t pos) { - g_task_items[TASK_ITEM_INFO].window = wuss_proginfo_window(g.proginfo); + g_task_items[TASK_ITEM_INFO].window = wuss_proginfo_window(g_tasks.proginfo); - return wuss_menu_open(g.menu_task, &g_task_menu, pos, NULL); + return wuss_menu_open(g_tasks.menu_task, &g_task_menu, pos, NULL); } diff --git a/apps/wuss/tasks.h b/apps/wuss/tasks.h index 75f8e928..436524e8 100644 --- a/apps/wuss/tasks.h +++ b/apps/wuss/tasks.h @@ -30,7 +30,7 @@ extern struct wuss_app_tasks * task_handle_event */ bitmap_t *bm; /* framebuffer bitmap, likewise */ } -g; +g_tasks; /* the menu-task event handler: dispatches every wuss_EVENT_MENU_SELECT and * relays wuss_EVENT_PALETTE to the frontend. Passed as wuss_task_desc.handle diff --git a/libraries/wuss/test/tasks/saturn.c b/libraries/wuss/test/tasks/saturn.c index f602c1e1..ee3fdcc5 100644 --- a/libraries/wuss/test/tasks/saturn.c +++ b/libraries/wuss/test/tasks/saturn.c @@ -273,6 +273,18 @@ result_t saturn_create(wuss_t *wuss, saturn_task_t **out) void saturn_destroy(saturn_task_t *task) { + /* task->conf.dialogue's window is borrowed into g_saturn_menu_items as a + * submenu leaf; if the chain is still open at QUIT (wuss_destroy sweeps + * tasks before closing any leftover chain -- see its comment) that leaf's + * node->window would dangle once wuss_dialogue_destroy below frees it. + * Close our own chain first, same as wuss_destroy expects every task to + * do for whatever it still holds. */ + if (task->menu_handle != NULL) + { + wuss_menu_close(task->menu_handle); + task->menu_handle = NULL; + } + wuss_colourmenu_destroy(task->fg_colourmenu); wuss_colourmenu_destroy(task->bg_colourmenu); wuss_dialogue_destroy(task->conf.dialogue); From 60749862d872d3fef8a8a9df31970b59f39bd3f6 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Wed, 16 Sep 2026 17:11:55 +0100 Subject: [PATCH 111/128] fix(wuss): scale saturn's energy-gate constants with window size The stars/ring exclusion thresholds were literal constants tuned for size==256; since p's range grows linearly with size, a larger window shrank the exclusion disc relative to half and let stars/ring points land inside the planet body. Scale them by size/SATURN_SIZE_DEFAULT. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/test/tasks/saturn.c | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/libraries/wuss/test/tasks/saturn.c b/libraries/wuss/test/tasks/saturn.c index ee3fdcc5..fd358550 100644 --- a/libraries/wuss/test/tasks/saturn.c +++ b/libraries/wuss/test/tasks/saturn.c @@ -314,6 +314,7 @@ static result_t saturn_redraw(const wuss_event_t *event, saturn_task_t *task) screen_t *scr; const box_t *content, *bounds; int size, half, range, flip, energy_shift; + int stars_p, ring_p, e_lo, e_hi; int ox, oy; int i; int x, y, p; @@ -332,6 +333,15 @@ static result_t saturn_redraw(const wuss_event_t *event, saturn_task_t *task) flip = size - 1; energy_shift = size; + /* p's range grows linearly with size (p_max = size/2), so the original's + * fixed energy-gate constants (17, 16, 32, 80 at size==256) must scale + * with size too, or the exclusion disc shrinks relative to half as size + * grows and the stars/ring creep into the planet body. */ + stars_p = 17 * size / SATURN_SIZE_DEFAULT; + ring_p = 16 * size / SATURN_SIZE_DEFAULT; + e_lo = 32 * size / SATURN_SIZE_DEFAULT; + e_hi = 80 * size / SATURN_SIZE_DEFAULT; + /* plot in doc space (saturn_plot clips to 0..size); origin carries the * scroll so a scrolled/shrunk window shows the right slice */ ox = bounds->x0 - event->data.redraw.scroll.x; @@ -345,7 +355,7 @@ static result_t saturn_redraw(const wuss_event_t *event, saturn_task_t *task) x = SATURN_SAMPLE(half, range); y = SATURN_SAMPLE(half, range); p = (x * x + y * y) / energy_shift; - if (p > 17) + if (p > stars_p) saturn_plot(scr, ox, oy, size, x + half, flip - (y + half), task->fg); } @@ -361,7 +371,7 @@ static result_t saturn_redraw(const wuss_event_t *event, saturn_task_t *task) y = r6; p = (x * x + y * y) / energy_shift; e = ((r6 + r7) * (r6 + r7) + r5 * r5 + r6 * r6) / energy_shift; - if (e >= 32 && e < 80 && (r5 < 0 || p > 16)) + if (e >= e_lo && e < e_hi && (r5 < 0 || p > ring_p)) saturn_plot(scr, ox, oy, size, x + half, y + half, task->fg); } From d2f32e6c43576bf7bf29446a03efb20d49aa9ddc Mon Sep 17 00:00:00 2001 From: David Thomas Date: Wed, 16 Sep 2026 18:15:31 +0100 Subject: [PATCH 112/128] feat(wuss): add Default button to saturn's config dialogue Resets task->config to SATURN_CONFIG_DEFAULT, refills the sliders, and applies it; Select also closes the menu chain, Adjust leaves the dialogue open showing the reset values. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/test/tasks/saturn.c | 66 ++++++++++++++++++++++++------ libraries/wuss/test/tasks/saturn.h | 1 + 2 files changed, 55 insertions(+), 12 deletions(-) diff --git a/libraries/wuss/test/tasks/saturn.c b/libraries/wuss/test/tasks/saturn.c index fd358550..46308400 100644 --- a/libraries/wuss/test/tasks/saturn.c +++ b/libraries/wuss/test/tasks/saturn.c @@ -104,6 +104,7 @@ enum { SATURN_SIZE_ICON_SLIDER4, SATURN_SIZE_ICON_VALUE4, + SATURN_SIZE_ICON_DEFAULT, SATURN_SIZE_ICON_CANCEL, SATURN_SIZE_ICON_APPLY, @@ -164,6 +165,8 @@ static result_t saturn_conf_dialogue_create(saturn_task_t *task); static result_t saturn_conf_fillout(void *opaque); static result_t saturn_conf_cancel(void *opaque, wuss_button_t button); static result_t saturn_conf_apply_action(void *opaque, wuss_button_t button); +static result_t saturn_conf_default_action(void *opaque, + wuss_button_t button); result_t saturn_create(wuss_t *wuss, saturn_task_t **out) { @@ -188,6 +191,7 @@ result_t saturn_create(wuss_t *wuss, saturn_task_t **out) task->menu_handle = NULL; task->conf.dialogue = NULL; memset(task->conf.rows, 0, sizeof(task->conf.rows)); + task->conf.deflt = NULL; task->conf.cancel = NULL; task->conf.apply = NULL; task->proginfo = NULL; @@ -447,7 +451,7 @@ enum ST_VAL4, ST_BTNS, - ST_SPCR, + ST_DFLT, ST_CNCL, ST_APLY, @@ -457,11 +461,14 @@ enum /* Leaf main-axis sizes, named so saturn_conf_dialogue_create's hand-computed * minimum window size can share them with the table below instead of * repeating the numbers as bare literals. */ -#define ST_LABEL_W (5*6) /* enough for "Iters" */ -#define ST_LABEL2_W (4*6) /* enough for "1280" */ -#define ST_SLIDER_MIN_W 64 -#define ST_CANCEL_W 48 -#define ST_APPLY_W 56 +#define ST_LABEL_W (5*6) /* enough for "Iters" */ +#define ST_LABEL2_W (4*6) /* enough for "9999" */ +#define ST_SLIDER_MIN_W (64) +#define ST_ACTION_WIDTH(W) ((W)*6+2*4) +#define ST_DEFAULT_WIDTH(W) ((W)*6+2*6) +#define ST_DEFAULT_W ST_ACTION_WIDTH(9) +#define ST_CANCEL_W ST_ACTION_WIDTH(9) +#define ST_APPLY_W ST_DEFAULT_WIDTH(9) static const stack_item_t g_saturn_conf_stack[SIZE_STACK__LIMIT] = { @@ -488,7 +495,7 @@ static const stack_item_t g_saturn_conf_stack[SIZE_STACK__LIMIT] = [ST_VAL4] = STACK_LEAF(ST_ROW4, ST_LABEL2_W, 16, stack_ALIGN_CENTRE), [ST_BTNS] = STACK_HBOX(ST_ROOT, wuss_STD_PRIMARY_BUTTON_HEIGHT, wuss_STD_GAP, stack_ALIGN_END), - [ST_SPCR] = STACK_SPACER(ST_BTNS, 1), + [ST_DFLT] = STACK_LEAF(ST_BTNS, ST_DEFAULT_W, wuss_STD_SECONDARY_BUTTON_HEIGHT, stack_ALIGN_CENTRE), [ST_CNCL] = STACK_LEAF(ST_BTNS, ST_CANCEL_W, wuss_STD_SECONDARY_BUTTON_HEIGHT, stack_ALIGN_CENTRE), [ST_APLY] = STACK_LEAF(ST_BTNS, ST_APPLY_W, wuss_STD_PRIMARY_BUTTON_HEIGHT, stack_ALIGN_CENTRE), }; @@ -547,6 +554,7 @@ static result_t saturn_conf_dialogue_create(saturn_task_t *task) desc->min, desc->max, value, NULL, desc->step); } + wuss_icon_spec_action(&specs[SATURN_SIZE_ICON_DEFAULT], boxes[ST_DFLT], "Default", wuss_COLOUR_BLACK, wuss_COLOUR_WINDOW, 0); wuss_icon_spec_action(&specs[SATURN_SIZE_ICON_CANCEL], boxes[ST_CNCL], "Cancel", wuss_COLOUR_BLACK, wuss_COLOUR_WINDOW, 0); wuss_icon_spec_action(&specs[SATURN_SIZE_ICON_APPLY], boxes[ST_APLY], "Apply", wuss_COLOUR_BLACK, wuss_COLOUR_WINDOW, 1); @@ -561,16 +569,19 @@ static result_t saturn_conf_dialogue_create(saturn_task_t *task) g_saturn_sizedlg_rows[row].min, g_saturn_sizedlg_rows[row].max, g_saturn_sizedlg_rows[row].step); + task->conf.deflt = made[SATURN_SIZE_ICON_DEFAULT]; task->conf.cancel = made[SATURN_SIZE_ICON_CANCEL]; task->conf.apply = made[SATURN_SIZE_ICON_APPLY]; { - wuss_dialogue_action_t actions[2]; + wuss_dialogue_action_t actions[3]; - actions[0].icon = task->conf.cancel; - actions[0].fn = saturn_conf_cancel; - actions[1].icon = task->conf.apply; - actions[1].fn = saturn_conf_apply_action; + actions[0].icon = task->conf.deflt; + actions[0].fn = saturn_conf_default_action; + actions[1].icon = task->conf.cancel; + actions[1].fn = saturn_conf_cancel; + actions[2].icon = task->conf.apply; + actions[2].fn = saturn_conf_apply_action; rc = wuss_dialogue_set_actions(task->conf.dialogue, actions, NELEMS(actions)); @@ -671,6 +682,37 @@ static result_t saturn_conf_apply_action(void *opaque, wuss_button_t button) return result_OK; } +/* Dialogue action callback for Default: resets task->config to + * SATURN_CONFIG_DEFAULT, refills the sliders/echo labels to match, and + * applies it (window resize + doc extent) -- Select then dismisses the + * menu chain same as Apply's Select; Adjust leaves the dialogue open, + * showing the reset values. */ +static result_t saturn_conf_default_action(void *opaque, + wuss_button_t button) +{ + static const saturn_config_t default_config = SATURN_CONFIG_DEFAULT; + + result_t rc; + saturn_task_t *task; + + task = opaque; + + task->config = default_config; + rc = saturn_conf_fillout(task); + if (rc != result_OK) + return rc; + + if (button & wuss_BUTTON_SELECT) + { + wuss_menu_close(task->menu_handle); + task->menu_handle = NULL; + return saturn_conf_apply(task); + } + if (button & wuss_BUTTON_ADJUST) + return saturn_conf_apply(task); + return result_OK; +} + /* wuss_EVENT_ICON on the size dialogue: slider drag updates the echo label * (snapped to SATURN_SIZE_STEP) live on DOWN/MOVE, handled here directly; * a Cancel/Apply click (UP only -- a press that drags off the button before diff --git a/libraries/wuss/test/tasks/saturn.h b/libraries/wuss/test/tasks/saturn.h index 7c1d0eec..000234b4 100644 --- a/libraries/wuss/test/tasks/saturn.h +++ b/libraries/wuss/test/tasks/saturn.h @@ -60,6 +60,7 @@ typedef struct saturn_conf { wuss_dialogue_t *dialogue; wuss_slider_row_t rows[SATURN_SIZEDLG_NROWS]; /* size, stars, ring, body */ + wuss_icon_t *deflt; wuss_icon_t *cancel; wuss_icon_t *apply; } From d9ff21247fbbd056e35721cf97c49778e599d39f Mon Sep 17 00:00:00 2001 From: David Thomas Date: Wed, 16 Sep 2026 18:26:42 +0100 Subject: [PATCH 113/128] feat(wuss): split demo launcher into Games/Tests/Utilities/Visuals The flat Launch submenu had grown to 18 entries; group by kind and hang the category submenus directly off the top-level task menu instead of behind an extra Launch level. Co-Authored-By: Claude Sonnet 5 --- apps/wuss/tasks.c | 150 +++++++++++++++++++++++++++++++++------------- 1 file changed, 110 insertions(+), 40 deletions(-) diff --git a/apps/wuss/tasks.c b/apps/wuss/tasks.c index 09b2215f..414794c0 100644 --- a/apps/wuss/tasks.c +++ b/apps/wuss/tasks.c @@ -83,61 +83,99 @@ static result_t spawn_task(const char *name, task_create_fn_t create) * in lock-step: picking row i of that menu calls its spawn[i]. */ typedef result_t (*task_spawn_fn_t)(void); -/* "Launch" submenu: the demo tasks. g_launch_items[i] and g_launch_tasks[i] - * are picked by the same menu row index i -- keep both tables in this - * order. */ +/* Category submenus, hung directly off the top-level task menu (see + * g_task_items below): g_*_items[i] and g_*_tasks[i] are picked by the same + * menu row index i -- keep each pair's tables in that order. Each category + * menu is dispatched in task_handle_event by matching + * event->data.menu_select.menu against the category's g_*_menu address. */ static const struct { const char *name; task_create_fn_t create; } -g_launch_tasks[] = +g_games_tasks[] = { { "Ball", (task_create_fn_t) ball_create }, - { "Blank", (task_create_fn_t) blank_create }, - { "Chars", (task_create_fn_t) chars_create }, + { "Minesweeper", (task_create_fn_t) minesweeper_create } +}, +g_tests_tasks[] = +{ { "Checker", (task_create_fn_t) checker_create }, - { "Clock", (task_create_fn_t) clock_create }, { "Curve", (task_create_fn_t) curve_create }, - { "Gradient", (task_create_fn_t) gradient_create }, { "Greeble", (task_create_fn_t) greeble_create }, { "Icons", (task_create_fn_t) icons_create }, - { "Image", (task_create_fn_t) image_create }, - { "Lissajous", (task_create_fn_t) lissajous_create }, - { "Minesweeper", (task_create_fn_t) minesweeper_create }, - { "Palette", (task_create_fn_t) palette_create }, { "Porter-Duff", (task_create_fn_t) porter_duff_create }, - { "Saturn", (task_create_fn_t) saturn_create }, { "Sofa", (task_create_fn_t) sofa_create }, - { "Swatches", (task_create_fn_t) swatches_create }, { "Text", (task_create_fn_t) text_create } +}, +g_utilities_tasks[] = +{ + { "Chars", (task_create_fn_t) chars_create }, + { "Clock", (task_create_fn_t) clock_create }, + { "Palette", (task_create_fn_t) palette_create }, + { "Swatches", (task_create_fn_t) swatches_create } +}, +g_visuals_tasks[] = +{ + { "Blank", (task_create_fn_t) blank_create }, + { "Gradient", (task_create_fn_t) gradient_create }, + { "Image", (task_create_fn_t) image_create }, + { "Lissajous", (task_create_fn_t) lissajous_create }, + { "Saturn", (task_create_fn_t) saturn_create } }; -static const wuss_menu_item_t g_launch_items[] = +static const wuss_menu_item_t g_games_items[] = { { "Ball", wuss_MENU_ITEM_NONE, NULL }, - { "Blank", wuss_MENU_ITEM_NONE, NULL }, - { "Chars", wuss_MENU_ITEM_NONE, NULL }, + { "Minesweeper", wuss_MENU_ITEM_NONE, NULL } +}; + +static const wuss_menu_item_t g_tests_items[] = +{ { "Checker", wuss_MENU_ITEM_NONE, NULL }, - { "Clock", wuss_MENU_ITEM_NONE, NULL }, { "Curve", wuss_MENU_ITEM_NONE, NULL }, - { "Gradient", wuss_MENU_ITEM_NONE, NULL }, { "Greeble", wuss_MENU_ITEM_NONE, NULL }, { "Icons", wuss_MENU_ITEM_NONE, NULL }, - { "Image", wuss_MENU_ITEM_NONE, NULL }, - { "Lissajous", wuss_MENU_ITEM_NONE, NULL }, - { "Minesweeper", wuss_MENU_ITEM_NONE, NULL }, - { "Palette", wuss_MENU_ITEM_NONE, NULL }, { "Porter-Duff", wuss_MENU_ITEM_NONE, NULL }, - { "Saturn", wuss_MENU_ITEM_NONE, NULL }, { "Sofa", wuss_MENU_ITEM_NONE, NULL }, - { "Swatches", wuss_MENU_ITEM_NONE, NULL }, { "Text", wuss_MENU_ITEM_NONE, NULL } }; -static const wuss_menu_t g_launch_menu = +static const wuss_menu_item_t g_utilities_items[] = +{ + { "Chars", wuss_MENU_ITEM_NONE, NULL }, + { "Clock", wuss_MENU_ITEM_NONE, NULL }, + { "Palette", wuss_MENU_ITEM_NONE, NULL }, + { "Swatches", wuss_MENU_ITEM_NONE, NULL } +}; + +static const wuss_menu_item_t g_visuals_items[] = +{ + { "Blank", wuss_MENU_ITEM_NONE, NULL }, + { "Gradient", wuss_MENU_ITEM_NONE, NULL }, + { "Image", wuss_MENU_ITEM_NONE, NULL }, + { "Lissajous", wuss_MENU_ITEM_NONE, NULL }, + { "Saturn", wuss_MENU_ITEM_NONE, NULL } +}; + +static const wuss_menu_t g_games_menu = +{ + "Games", g_games_items, NELEMS(g_games_items) +}; + +static const wuss_menu_t g_tests_menu = +{ + "Tests", g_tests_items, NELEMS(g_tests_items) +}; + +static const wuss_menu_t g_utilities_menu = { - "Launch", g_launch_items, NELEMS(g_launch_items) + "Utilities", g_utilities_items, NELEMS(g_utilities_items) +}; + +static const wuss_menu_t g_visuals_menu = +{ + "Visuals", g_visuals_items, NELEMS(g_visuals_items) }; static result_t spawn_quit(void) @@ -151,7 +189,10 @@ static result_t spawn_quit(void) enum { TASK_ITEM_INFO, - TASK_ITEM_LAUNCH, + TASK_ITEM_GAMES, + TASK_ITEM_TESTS, + TASK_ITEM_UTILITIES, + TASK_ITEM_VISUALS, TASK_ITEM_QUIT }; @@ -160,26 +201,33 @@ enum * the table itself cannot name it at compile time. */ static wuss_menu_item_t g_task_items[] = { - { "Info", wuss_MENU_ITEM_NONE, NULL, NULL }, - { "Launch", wuss_MENU_ITEM_NONE, &g_launch_menu, NULL }, - { "Quit Wuss", wuss_MENU_ITEM_NONE, NULL, NULL } + { "Info", wuss_MENU_ITEM_NONE, NULL, NULL }, + { "Games", wuss_MENU_ITEM_NONE, &g_games_menu, NULL }, + { "Tests", wuss_MENU_ITEM_NONE, &g_tests_menu, NULL }, + { "Utilities", wuss_MENU_ITEM_NONE, &g_utilities_menu, NULL }, + { "Visuals", wuss_MENU_ITEM_NONE, &g_visuals_menu, NULL }, + { "Quit Wuss", wuss_MENU_ITEM_NONE, NULL, NULL } }; static const task_spawn_fn_t g_task_spawn[] = { NULL, /* "Info" -> wuss_menu_item_t.window leaf, no spawn */ - NULL, /* "Launch" -> submenu g_launch_menu */ + NULL, /* "Games" -> submenu g_games_menu */ + NULL, /* "Tests" -> submenu g_tests_menu */ + NULL, /* "Utilities" -> submenu g_utilities_menu */ + NULL, /* "Visuals" -> submenu g_visuals_menu */ spawn_quit }; static const wuss_menu_t g_task_menu = { - "Tasks", g_task_items, NELEMS(g_task_items) + "Wuss", g_task_items, NELEMS(g_task_items) }; -/* g_task_menu / g_launch_menu picks are dispatched by index. Every menu is - * opened by g.menu_task, so one handler sees every wuss_EVENT_MENU_SELECT and - * tells them apart by data.menu_select.menu. */ +/* g_task_menu picks are dispatched by index; g_games_menu/g_tests_menu/ + * g_utilities_menu/g_visuals_menu picks by pointer identity below. Every + * menu is opened by g.menu_task, so one handler sees every + * wuss_EVENT_MENU_SELECT and tells them apart by data.menu_select.menu. */ result_t task_handle_event(wuss_window_t *window, const wuss_event_t *event, void *task_data) @@ -227,11 +275,33 @@ result_t task_handle_event(wuss_window_t *window, menu = event->data.menu_select.menu; index = event->data.menu_select.index; - if (menu == &g_launch_menu) + if (menu == &g_games_menu) + { + if (index >= 0 && index < (int) NELEMS(g_games_tasks)) + (void) spawn_task(g_games_tasks[index].name, g_games_tasks[index].create); + return result_OK; + } + + if (menu == &g_tests_menu) + { + if (index >= 0 && index < (int) NELEMS(g_tests_tasks)) + (void) spawn_task(g_tests_tasks[index].name, g_tests_tasks[index].create); + return result_OK; + } + + if (menu == &g_utilities_menu) + { + if (index >= 0 && index < (int) NELEMS(g_utilities_tasks)) + (void) spawn_task(g_utilities_tasks[index].name, + g_utilities_tasks[index].create); + return result_OK; + } + + if (menu == &g_visuals_menu) { - if (index >= 0 && index < (int) NELEMS(g_launch_tasks)) - (void) spawn_task(g_launch_tasks[index].name, - g_launch_tasks[index].create); + if (index >= 0 && index < (int) NELEMS(g_visuals_tasks)) + (void) spawn_task(g_visuals_tasks[index].name, + g_visuals_tasks[index].create); return result_OK; } From 92ca06a2004b56b3d1b28910c6b85b9a7c53c4f0 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Wed, 16 Sep 2026 18:31:36 +0100 Subject: [PATCH 114/128] fix(bmfonts): Reduce Digits Bold space width --- resources/bmfonts/DPT-Digits-Bold.png | Bin 932 -> 929 bytes resources/bmfonts/MS Sans Serif.png | Bin 1803 -> 1807 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/resources/bmfonts/DPT-Digits-Bold.png b/resources/bmfonts/DPT-Digits-Bold.png index 315c145693b79fb10760b2c3c2bd3da1c695520b..f7d2eb371d011169e9c5266d1634105f86723570 100644 GIT binary patch delta 809 zcmV+^1J?Yc2cZX$g?|T0L_t(Y$Guidjv_G(^$}Ri+$Tt|H&U!xjW`M%g(cz?#i9<- zQs)x0XF1CFlW$R_hL&cKJMD7pyf}$p0=F}W+*r=HYf`QNHxss8RJp)aTd|zAU6K@d z$$|Vm0tA+yB9jxHTdziL@9I$Gju24&RpC$aqFBo^_Wo>Ozz%@3X#pnn3>lz!j@SXP8)dfyFzPB1r^zEa zH)a6#BU}Ku^H@A0N-tr`OvDyk1KIhGc@GRAc!~TxQ~;Ta0$6L2Vz)t2VK~a^9{9R> zwon{i0eFd~9e+%F@C4)29uxq}AZRK+Puwya^AeQ|62Mq8K==@VngO4pDbK*?eUo@q z$oBx)X_e4J&n@Sj6<(b=7Uwd(P~lv8luy zZ9C~IQ9c@gKl*adM!Ft7XHm;Q-h%8V*z&x@Wau8;|X|9newCq1|gGyJrPVPR6**-7EyYt2udoCQ+j{M$aGpMald*Hebc)Z n^&&M@gyU-I53zYLy%6;m$^(}JnVG*300000NkvXXu0mjf6@_#F delta 812 zcmV+{1JnGW2c!p(g?|c3L_t(Y$Guidj@vK{o52qhuNQ6wbm2 z7+`dX?q^x3{OIf0m}G!9;BaJ$)RRQm@#0Plf7G|P;sQKYy{Is#h~fP|4dI5=Vlz^iEiX7vmipm$C|_!P}~20rIa z;Xn{kNgN2&*vaEf$}G)UvmNi+)w%I8@fqS(!! z?aPz9D*g$dEoX@T_@~^kQTIX#g%!rzrrVmwP@c={Q0*dcR-=%5|rbdhfonfYmR&?Y|4)RsiOvn2!dq-#0@S z0Qedp;0i#%3Xo2b0T9-CkxQGK0B{{(_8owcHYwE`?I(aYY;x6X5&246Er3l$2rdH% zr)sZYz<&b(!izb8L_yO7fZbHy?xg|{C=Np;t$5bV0mcVn)Y6v*8|9~V?j2HyF@$J3 zvE@@Kgp?|bUHJ0_4)~t@2daQ&-T%J!PXuc$sABQF=&9lcnK1azw-l&1#$2xKd_DQ+ zh_ha++Vvp`c5<*50HF_l6(AI<*zpm~j~t`0;D770hjL9W$ga9I@0XVB3cvy3&F^#~ z3l6AZ+f)ImXt&5to)iua`xXFR0|>YRFk@W-k9=?sD@c6O01&PN%y~{$$##VV*8!|; z5j?wsdk;`9i55VT9JTa~DuR+h$Yfwo1ZxzkAa!GlGAX?me)Ayu qrX_bP>T9Y9r_ItIV)I~nBI++%0hc2%y^CG|0000Oem)eHc@Y$v3&Kmpj=RA4Se0x&EAxDm9}=t(p?lK^zm zY#RVt8bL)xgB0=zdIiv@0B6^&6o6~>V#OTbLaG{|T>)@vuKQT6}gk=-vF(RBdGh0f2{Z#k@lb5Ju4JeSkIq zTtznkKma%srhZ6`9BtqLrfF&>{9T)Abbp?w7kYNdb@D&XG$c;vb($A^p9Jnor6kxlo)iOT^0WRHO$tZ6FiK3c&Wj2^@jNL{a<5lZ& z;9DuAEkk($D2)fG3n?$Wa&K~gc9T=Rfo;(XPjRCWg8-OZ>#s8a;{gVgf>P5KISsq5 z`wl=Y0Dsj@0M|&*%rOUW8de&$WRJ2&YSI8zYE;rVz0Hqk&ks#DkxCG#O*$KgP8sNf9Fk3}+1mHpq;4=b% zGxG%->C_1t0p9G?yiV3}VS}loNucI=N*#xt(gF{n96EDGTEZ^#Buz6_Y-=nTi&50qBy5@|6C!KyLomG?LLm+vHj-}>`Wf}^uf_% z43X}Qzw966?ZG5o>|VuL!>#{+jhilt^Y?!p;G)iYj)0zOe~(~c-sPQNlW<;#>VLM9 z8RQNU>zCAS@G6|QGHehvDDaNqNNO)l-CV|Zs}WY5z?^SI8PH|AWMZyOcX5cKN^0o zGrF~^XxRagrGa23k$n8lZsAm9{&PT&h^F-cT zMRNcQp3FzUT!aD9Ol*U9e>I;H9f#Mm%s87y^}lR1E^a15V{l@IED(wfBsJevM`|r7$FUyiVeyc0H^uk zhhmXJ^pU9n02aj3*I@KzfW-_fx6Va5}&q;DmC3(;iMo7h=kR zmI_nP^po9jV1{oCAyFQ&F)SJqQ5Y{aU8CPXR&?tlkH14vbqx zcL0n4a3lbOgcm8Ev+N<2pnr*3r=tr=m`xAhb64b19&qS( zA6#!m(@@bibO4u6rlTD{(w?kobz|IY6$fCraVewZ1`>5@m<1abZlK;p9^+MuG2?65 zN?ky40Vu!&R9i{kdSweaK)cDQ9?WiQg{QdEh(Q2MuGQxm0C<4j$(BNBw>b^t)_nj_ z2|#%l0Dmj7%p7w7)TpJ>n&zXZ95rcxvh)Bv?mPz?pd;lDJg^#~FIB&t^PJNk@~A} zbKs&6XNXFLxKh?<)5FZr3@LlIM=9bNSL=W(dyMwzkxCtjhVPkQ3z{m#};M0rd9((jf`}#SH+T&aK|M&ojiH_1dyy9vvyd_N8OZ#v7v? z`S$7}n9ID&M2nP(hAeXG^*ViHo0Srq$A4^UZZ1Tlau@foO;`3Zo7NI(W?9W*D*c|9 zZPUZ=Ms~j(;A~a~=(oQ+K)v=H^lJm`;LjvK0QlalY}u{8lUKJ*TQNYj@6$U+Ptiqs z0Dsv(O70O7D^~ki!>#{+^_wmV+WS8aa8YMHdqB^Xzeli~mE~HI^kVe{n{FqOUVrW& zv3x=G2CD+Cm44$;qfJOVrj&!%249?scN@;FHcsqDd49Xp9O9-GMdFx!Dt+s0deyE~ z4@zkYOZ3@|;=UNf*m8~ilrYuPMmaadhA2{iow)yczW9T3_3f1DSV#|Cb>h}6%h=?d z3~|Ahr}6?~fD0kJ%t#No z%(;?THE|W)2jCv}CCx$U8XbqosPXMof#L2lp98pA+);((obrCtk?!~t*_UjOhTrRq zZflkDwaFF!;aa}BuwgFB-XhG>Q^OrEsZ|?r;r(E4YQF1$Xzm)Oqq9wqGk-blqg!+z zU!j;m7aXVu<#r8n^y%JF@7@Dw?{(_3*vW%I@RIQcylmqW0>*o_x;U{{o_OyE!*Lu; z+*W~Y26ZzHv{8t23o9y2@&IFH4~2$$fL0<-YQvto>F4fT_(z4r3y9~*{c>!H- z5R@`p1 z(JGn)IOoZ{2h2szA(~0uxVyiSaCyJ-HJ==)T9{&W${Q8=`<89mt$%Hse*ef Date: Wed, 16 Sep 2026 20:55:34 +0100 Subject: [PATCH 115/128] feat(wuss): add letter/word spacing, colour and background controls to text demo text.c's menu gains a Spacing submenu (letter/word presets), Foreground/Background colourmenus, and a No Background toggle that draws glyphs transparently over the window's content. bmtext_layout/bmtext_draw now take an optional spacing parameter so the word-wrap and draw paths honour the picked preset. Co-Authored-By: Claude Sonnet 5 --- include/text/bmtext.h | 32 ++--- libraries/text/bmtext/draw.c | 19 +-- libraries/text/bmtext/layout.c | 17 +-- libraries/wuss/test/tasks/text.c | 196 ++++++++++++++++++++++++++----- libraries/wuss/test/tasks/text.h | 79 +++++++++---- 5 files changed, 260 insertions(+), 83 deletions(-) diff --git a/include/text/bmtext.h b/include/text/bmtext.h index 8bc6f4dd..bb4cb9a3 100644 --- a/include/text/bmtext.h +++ b/include/text/bmtext.h @@ -49,17 +49,19 @@ bmtext_line_t; * \param[in] string Text to wrap. * \param[in] stringlen Length of \p string in bytes. * \param[in] wrap_width Width to wrap to, in pixels. + * \param[in] spacing Extra letter/word spacing, or NULL for none. * \param[out] lines Filled with up to \p max lines. * \param[in] max Capacity of \p lines. Lines past this are dropped. * * \return Number of lines written to \p lines. */ -int bmtext_layout(bmfont_t *font, - const char *string, - int stringlen, - int wrap_width, - bmtext_line_t *lines, - int max); +int bmtext_layout(bmfont_t *font, + const char *string, + int stringlen, + int wrap_width, + const bmfont_spacing_t *spacing, + bmtext_line_t *lines, + int max); /** * Draw \p nlines pre-laid-out \p lines stacked downward from \p origin, @@ -73,15 +75,17 @@ int bmtext_layout(bmfont_t *font, * \param[in] bg Background colour, for glyph blending. * \param[in] leading Extra pixels between lines. * \param[in] origin Top-left of the first line, in pixels. + * \param[in] spacing Extra letter/word spacing, or NULL for none. */ -void bmtext_draw(bmfont_t *font, - screen_t *scr, - const bmtext_line_t *lines, - int nlines, - colour_t fg, - colour_t bg, - int leading, - point_t origin); +void bmtext_draw(bmfont_t *font, + screen_t *scr, + const bmtext_line_t *lines, + int nlines, + colour_t fg, + colour_t bg, + int leading, + point_t origin, + const bmfont_spacing_t *spacing); #ifdef __cplusplus } diff --git a/libraries/text/bmtext/draw.c b/libraries/text/bmtext/draw.c index f7ee2888..721fe7fb 100644 --- a/libraries/text/bmtext/draw.c +++ b/libraries/text/bmtext/draw.c @@ -8,14 +8,15 @@ #include "text/bmtext.h" -void bmtext_draw(bmfont_t *font, - screen_t *scr, - const bmtext_line_t *lines, - int nlines, - colour_t fg, - colour_t bg, - int leading, - point_t origin) +void bmtext_draw(bmfont_t *font, + screen_t *scr, + const bmtext_line_t *lines, + int nlines, + colour_t fg, + colour_t bg, + int leading, + point_t origin, + const bmfont_spacing_t *spacing) { int font_height, font_ascent; point_t pos; @@ -27,7 +28,7 @@ void bmtext_draw(bmfont_t *font, pos.y += font_ascent; /* origin is top-left; bmfont_draw wants the baseline */ for (i = 0; i < nlines; i++) { - bmfont_draw(font, scr, lines[i].str, lines[i].len, fg, bg, NULL, &pos, + bmfont_draw(font, scr, lines[i].str, lines[i].len, fg, bg, spacing, &pos, NULL); pos.y += font_height + leading; } diff --git a/libraries/text/bmtext/layout.c b/libraries/text/bmtext/layout.c index 226f6852..6f8a8711 100644 --- a/libraries/text/bmtext/layout.c +++ b/libraries/text/bmtext/layout.c @@ -7,12 +7,13 @@ #include "text/bmtext.h" -int bmtext_layout(bmfont_t *font, - const char *string, - int stringlen, - int wrap_width, - bmtext_line_t *lines, - int max) +int bmtext_layout(bmfont_t *font, + const char *string, + int stringlen, + int wrap_width, + const bmfont_spacing_t *spacing, + bmtext_line_t *lines, + int max) { int nlines; @@ -24,8 +25,8 @@ int bmtext_layout(bmfont_t *font, bmfont_width_t width; int friendly_break; - bmfont_measure(font, string, stringlen, NULL, wrap_width, &absolute_break, - &width); + bmfont_measure(font, string, stringlen, spacing, wrap_width, + &absolute_break, &width); friendly_break = absolute_break; if (absolute_break < stringlen) diff --git a/libraries/wuss/test/tasks/text.c b/libraries/wuss/test/tasks/text.c index 2c3263ae..14391b8d 100644 --- a/libraries/wuss/test/tasks/text.c +++ b/libraries/wuss/test/tasks/text.c @@ -38,30 +38,24 @@ text_sample_t; static const text_sample_t text_samples[] = { + { "Lorem Ipsum", + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed eros lacus, imperdiet eget finibus ac, tempus vel risus. Donec scelerisque, elit quis pretium imperdiet, orci magna varius tellus, sit amet sodales ante orci nec nibh. Pellentesque placerat eu diam vitae pharetra. Nunc aliquet ante mi, vulputate commodo dui placerat eu. Morbi velit ex, scelerisque vel mi elementum, tristique viverra enim. Integer a interdum eros, id fringilla nunc. Nunc non felis nisi. Aliquam nec ullamcorper tellus. Maecenas sed aliquam diam. Duis pretium aliquet metus. Suspendisse rhoncus turpis vel dui euismod fringilla. Vivamus efficitur leo vel metus condimentum, tempor bibendum augue vehicula. Nunc eleifend sagittis tortor eget pretium. Fusce interdum tortor eget sapien blandit consectetur. Aliquam vestibulum euismod eros a luctus. Etiam nec nisl et diam imperdiet lobortis. Sed in eros sed tellus commodo bibendum. Sed ipsum velit, sodales a pulvinar non, pharetra ut est. Curabitur eu odio id magna posuere eleifend non id erat. Pellentesque commodo blandit mauris, ac consequat nisi dapibus eget. Mauris sollicitudin molestie urna, sit amet ornare turpis tincidunt bibendum. Vivamus interdum bibendum luctus. Suspendisse in arcu velit. Aenean eget bibendum dolor. Quisque tristique porta purus ornare tincidunt. Etiam hendrerit nunc tellus, et tempus ligula laoreet eu. Quisque pellentesque malesuada tempor. Mauris eu lectus ut neque fringilla hendrerit. Sed scelerisque laoreet felis a eleifend. Fusce sit amet mauris tellus. Sed orci ipsum, consectetur vitae blandit ut, egestas vehicula dolor. Integer ullamcorper, metus a vulputate mattis, elit orci accumsan ligula, eget elementum nibh tortor sit amet mauris. In aliquet nibh at scelerisque suscipit. Sed elit purus, sagittis eu accumsan ultricies, lobortis a odio. Duis libero sem, tempus in fringilla nec, bibendum eu metus. Nulla eget justo metus. In luctus ante massa, pellentesque commodo lorem pretium sed. Cras ultricies est lacus, ut dictum lorem gravida sed. Ut augue mauris, dignissim a pulvinar eget, pharetra ac turpis. Sed ultricies nulla mauris, id dictum ex scelerisque sit amet. Morbi et placerat enim. Phasellus arcu nisl, tempor vitae lacinia et, finibus quis justo. Fusce ipsum mi, porttitor nec faucibus at, fermentum ut massa. Cras faucibus molestie mauris. Sed et metus eget lectus luctus cursus a sit amet eros. Sed enim ligula, gravida eget magna eu, suscipit fermentum lorem. Maecenas vestibulum mollis lacus nec accumsan. Nullam molestie justo eu turpis facilisis tempus quis quis velit. Phasellus gravida mollis condimentum. Nam fringilla mollis dolor, quis posuere quam iaculis ac. Suspendisse ac maximus mi. Pellentesque aliquam ante ante, sed facilisis sapien dictum ac. Nullam pulvinar ante vitae dictum rhoncus. Praesent in pretium justo. Quisque pellentesque at sapien at pulvinar. Aenean a lorem at sapien molestie ullamcorper in dignissim ante. Nunc sagittis mi at dolor accumsan laoreet. Ut id congue elit, ut semper metus. Praesent tellus orci, feugiat suscipit diam sit amet, malesuada efficitur metus. Phasellus condimentum justo ipsum, et lobortis mi ultrices a. Interdum et malesuada fames ac ante ipsum primis in faucibus. Nunc vestibulum volutpat laoreet. Vestibulum convallis lectus at accumsan imperdiet. Aliquam suscipit, justo condimentum sodales iaculis, tellus enim fermentum lacus, quis volutpat sapien tortor quis enim. Vestibulum vehicula turpis eu lorem gravida, nec volutpat massa dapibus. Mauris egestas accumsan mattis. Nullam ex risus, imperdiet ut vestibulum a, malesuada at odio. Praesent congue, nulla a eleifend dignissim, ligula arcu tincidunt tortor, vel vestibulum quam ipsum vitae metus. Nullam lacinia interdum enim id bibendum. Praesent quis elit id turpis cursus auctor. Etiam turpis massa, finibus sed odio quis, dapibus tristique magna. Morbi quis commodo tortor. Pellentesque hendrerit non libero non pretium. Aenean nunc est, aliquet eget tincidunt id, auctor ultrices orci velit." }, { "Quick Brown Fox", "The quick brown fox jumps over the lazy dog." }, { "Pangram (Cwm Fjord)", "Cwm fjord bank glyphs vext quiz." }, { "Pangram (Waltz)", - "Waltz, bad nymph, for quick jigs vex." }, - { "Lorem Ipsum", - "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod " - "tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim " - "veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea " - "commodo consequat. Duis aute irure dolor in reprehenderit in voluptate " - "velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint " - "occaecat cupidatat non proident, sunt in culpa qui officia deserunt " - "mollit anim id est laborum." } + "Waltz, bad nymph, for quick jigs vex." } }; -#define TEXT_DEFAULT_SAMPLE (NELEMS(text_samples) - 1) /* "Lorem Ipsum" */ +#define TEXT_DEFAULT_SAMPLE (0) /* "Lorem Ipsum" */ static const wuss_menu_item_t text_sample_items[] = { + { "Lorem Ipsum", wuss_MENU_ITEM_NONE, NULL }, { "Quick Brown Fox", wuss_MENU_ITEM_NONE, NULL }, { "Pangram (Cwm Fjord)", wuss_MENU_ITEM_NONE, NULL }, - { "Pangram (Waltz)", wuss_MENU_ITEM_NONE, NULL }, - { "Lorem Ipsum", wuss_MENU_ITEM_NONE, NULL } + { "Pangram (Waltz)", wuss_MENU_ITEM_NONE, NULL } }; static const wuss_menu_t text_sample_menu = @@ -69,6 +63,41 @@ static const wuss_menu_t text_sample_menu = "Sample", text_sample_items, NELEMS(text_sample_items) }; +/* one entry per row of the "Spacing" submenu */ +typedef struct text_spacing_preset +{ + const char *name; + int letter_spacing; + int word_spacing; +} +text_spacing_preset_t; + +static const text_spacing_preset_t text_spacing_presets[] = +{ + { "Normal", 0, 0 }, + { "Letter", 1, 0 }, + { "Word", 0, 1 }, + { "Letter+Word", 1, 1 } +}; + +#define TEXT_DEFAULT_SPACING 0 /* "Normal" */ + +static wuss_menu_item_t text_spacing_items[] = +{ + { "Normal", wuss_MENU_ITEM_TICKED, NULL }, + { "Letter", wuss_MENU_ITEM_NONE, NULL }, + { "Word", wuss_MENU_ITEM_NONE, NULL }, + { "Letter+Word", wuss_MENU_ITEM_NONE, NULL } +}; + +static wuss_menu_t text_spacing_menu = +{ + "Spacing", text_spacing_items, NELEMS(text_spacing_items) +}; + +/* index into task->top_items[] of the tickable "No Background" leaf */ +#define TEXT_MENU_NO_BACKGROUND 5 + /* ----------------------------------------------------------------------- */ /* load fonts[idx] if not already in hand; returns it or NULL on failure. @@ -129,11 +158,62 @@ static result_t text_set_sample(text_task_t *task, int idx) return result_OK; } +/* toggle whether the paragraph is drawn with its picked background colour or + * none, so glyphs blend straight onto whatever is already behind the + * window's content */ +static result_t text_toggle_bg(text_task_t *task) +{ + wuss_menu_item_t *item; + + task->bg_transparent = !task->bg_transparent; + + item = &task->top_items[TEXT_MENU_NO_BACKGROUND]; + if (task->bg_transparent) + item->flags |= wuss_MENU_ITEM_TICKED; + else + item->flags &= ~(wuss_menu_item_flags_t) wuss_MENU_ITEM_TICKED; + + wuss_window_invalidate_visible(task->window); + return result_OK; +} + +/* switch the paragraph's foreground to system palette index "picked" */ +static result_t text_set_fg(text_task_t *task, wuss_colour_t picked) +{ + task->fg_index = picked; + wuss_window_invalidate_visible(task->window); + return result_OK; +} + +/* switch the paragraph's background to system palette index "picked" */ +static result_t text_set_bg(text_task_t *task, wuss_colour_t picked) +{ + task->bg_index = picked; + wuss_window_invalidate_visible(task->window); + return result_OK; +} + +/* switch letter/word spacing to text_spacing_presets[idx] */ +static result_t text_set_spacing(text_task_t *task, int idx) +{ + if (idx < 0 || idx >= NELEMS(text_spacing_presets) || idx == task->spacing_idx) + return result_OK; + + task->spacing_idx = idx; + task->spacing.letter_spacing = text_spacing_presets[idx].letter_spacing; + task->spacing.word_spacing = text_spacing_presets[idx].word_spacing; + + wuss_menu_tick_exclusive(&text_spacing_menu, idx); + wuss_menu_tick_exclusive_live(task->menu_handle, &text_spacing_menu, idx); + wuss_window_invalidate_visible(task->window); + return result_OK; +} + static result_t text_open_menu(text_task_t *task) { return wuss_menu_open(task->delegate, &task->top_menu, - wuss_get_pointer(task->wuss), NULL); + wuss_get_pointer(task->wuss), &task->menu_handle); } /* ----------------------------------------------------------------------- */ @@ -158,8 +238,11 @@ result_t text_create(wuss_t *wuss, text_task_t **out) task->current = -1; /* the wuss system font is none of the picker's */ task->sample = TEXT_DEFAULT_SAMPLE; task->text = text_samples[TEXT_DEFAULT_SAMPLE].text; - task->bg = colour_rgb(0xFF, 0xFF, 0xFF); - task->fg = colour_rgb(0x00, 0x00, 0x00); + task->spacing_idx = TEXT_DEFAULT_SPACING; + task->spacing.letter_spacing = text_spacing_presets[TEXT_DEFAULT_SPACING].letter_spacing; + task->spacing.word_spacing = text_spacing_presets[TEXT_DEFAULT_SPACING].word_spacing; + task->fg_index = 0; /* wuss__default_palette: 0 is black */ + task->bg_index = 7; /* wuss__default_palette: 7 is white */ task->frame_count = 0; task->resizing = true; @@ -185,9 +268,29 @@ result_t text_create(wuss_t *wuss, text_task_t **out) return result_OOM; } + rc = wuss_colourmenu_create(&task->fgmenu, wuss, "Foreground"); + if (rc != result_OK) + { + wuss_fontmenu_destroy(task->fontmenu); + free(task->fonts); + free(task); + return rc; + } + + rc = wuss_colourmenu_create(&task->bgmenu, wuss, "Background"); + if (rc != result_OK) + { + wuss_colourmenu_destroy(task->fgmenu); + wuss_fontmenu_destroy(task->fontmenu); + free(task->fonts); + free(task); + return rc; + } + /* top-level menu: "Font" borrows the fontmenu's own live wuss_menu_t (so * ticks and wuss_fontmenu_selected keep working), "Sample" is the static - * text_sample_menu declared above */ + * text_sample_menu declared above, "Foreground"/"Background" each borrow + * their colourmenu's live wuss_menu_t the same way */ task->top_items[0].text = "Font"; task->top_items[0].flags = wuss_MENU_ITEM_NONE; task->top_items[0].submenu = menu; @@ -196,6 +299,22 @@ result_t text_create(wuss_t *wuss, text_task_t **out) task->top_items[1].flags = wuss_MENU_ITEM_NONE; task->top_items[1].submenu = &text_sample_menu; task->top_items[1].window = NULL; + task->top_items[2].text = "Spacing"; + task->top_items[2].flags = wuss_MENU_ITEM_NONE; + task->top_items[2].submenu = &text_spacing_menu; + task->top_items[2].window = NULL; + task->top_items[3].text = "Foreground"; + task->top_items[3].flags = wuss_MENU_ITEM_NONE; + task->top_items[3].submenu = wuss_colourmenu_menu(task->fgmenu); + task->top_items[3].window = NULL; + task->top_items[4].text = "Background"; + task->top_items[4].flags = wuss_MENU_ITEM_NONE; + task->top_items[4].submenu = wuss_colourmenu_menu(task->bgmenu); + task->top_items[4].window = NULL; + task->top_items[5].text = "No Background"; + task->top_items[5].flags = wuss_MENU_ITEM_NONE; + task->top_items[5].submenu = NULL; + task->top_items[5].window = NULL; task->top_menu.title = "Text"; task->top_menu.items = task->top_items; @@ -215,7 +334,7 @@ result_t text_create(wuss_t *wuss, text_task_t **out) wuss_task_set_autoclose(delegate, 1); task->delegate = delegate; - sz = SIZE2D(220, 180); + sz = SIZE2D(220, 220); task->base_width = sz.w; task->base_height = sz.h; @@ -224,7 +343,7 @@ result_t text_create(wuss_t *wuss, text_task_t **out) "Sample Text", wuss_WINDOW_DEFAULT | wuss_WINDOW_NO_RESIZE_BLIT, /* paragraph reflows across the whole window, so a resize must redraw all of it, not just the newly (un)covered edge */ wuss_BACKDROP_COLOUR(wuss_COLOUR_WINDOW), - sz, + SIZE2D(sz.w, 220 * 4), SIZE2D(0, 0), &task->window); if (rc != result_OK) @@ -248,6 +367,8 @@ void text_destroy(text_task_t *task) bmfont_destroy(task->fonts[i]); free(task->fonts); wuss_fontmenu_destroy(task->fontmenu); + wuss_colourmenu_destroy(task->fgmenu); + wuss_colourmenu_destroy(task->bgmenu); free(task); } @@ -257,13 +378,15 @@ void text_destroy(text_task_t *task) static result_t text_redraw(const wuss_event_t *event, void *task_data) { - text_task_t *tcx; - screen_t *scr; - const box_t *bounds; - int sx, sy; - bmtext_line_t lines[MAX_LINES]; - int nlines; - point_t origin; + text_task_t *tcx; + screen_t *scr; + const box_t *bounds; + int sx, sy; + const colour_t *palette; + colour_t fg, bg; + bmtext_line_t lines[MAX_LINES]; + int nlines; + point_t origin; tcx = task_data; @@ -272,17 +395,23 @@ static result_t text_redraw(const wuss_event_t *event, void *task_data) sx = event->data.redraw.scroll.x; sy = event->data.redraw.scroll.y; + palette = wuss_get_palette(tcx->wuss, NULL); + fg = palette[tcx->fg_index]; + bg = tcx->bg_transparent ? colour_rgba(0, 0, 0, 0) : palette[tcx->bg_index]; + nlines = bmtext_layout(tcx->font, tcx->text, (int) strlen(tcx->text), (bounds->x1 - INSET) - (bounds->x0 + INSET), + &tcx->spacing, lines, MAX_LINES); origin.x = bounds->x0 - sx + INSET; origin.y = bounds->y0 - sy + INSET; - bmtext_draw(tcx->font, scr, lines, nlines, tcx->fg, tcx->bg, LEADING, origin); + bmtext_draw(tcx->font, scr, lines, nlines, fg, bg, LEADING, origin, + &tcx->spacing); return result_OK; } @@ -352,13 +481,26 @@ result_t text_handle(wuss_window_t *window, case wuss_EVENT_MENU_SELECT: { - const char *name; + const char *name; + wuss_colour_t picked; + int mine; name = wuss_fontmenu_selected(tcx->fontmenu, event); if (name != NULL) return text_set_font(tcx, event->data.menu_select.index, name); if (event->data.menu_select.menu == &text_sample_menu) return text_set_sample(tcx, event->data.menu_select.index); + if (event->data.menu_select.menu == &text_spacing_menu) + return text_set_spacing(tcx, event->data.menu_select.index); + picked = wuss_colourmenu_selected(tcx->fgmenu, event, &mine); + if (mine) + return text_set_fg(tcx, picked); + picked = wuss_colourmenu_selected(tcx->bgmenu, event, &mine); + if (mine) + return text_set_bg(tcx, picked); + if (event->data.menu_select.menu == &tcx->top_menu && + event->data.menu_select.index == TEXT_MENU_NO_BACKGROUND) + return text_toggle_bg(tcx); } return result_OK; diff --git a/libraries/wuss/test/tasks/text.h b/libraries/wuss/test/tasks/text.h index e9b929b8..dccec334 100644 --- a/libraries/wuss/test/tasks/text.h +++ b/libraries/wuss/test/tasks/text.h @@ -9,6 +9,7 @@ #include "framebuf/bmfont.h" #include "framebuf/colour.h" +#include "wuss/component/colourmenu.h" #include "wuss/component/fontmenu.h" #include "wuss/menu.h" #include "wuss/task.h" @@ -16,33 +17,61 @@ /* window B's task: flows a chosen sample string over its wuss-filled * background, one line per bmfont_draw call. A MENU click on the window opens - * a top-level menu with two submenus: "Font" -- a wuss_fontmenu over - * resources/bmfonts, swapping the paragraph font in place -- and "Sample", - * which swaps the shown string (a choice of pangrams and a lorem ipsum - * paragraph). */ + * a top-level menu with three submenus -- "Font" (a wuss_fontmenu over + * resources/bmfonts, swapping the paragraph font in place), "Sample" (swaps + * the shown string: a choice of pangrams and a lorem ipsum paragraph) and + * "Spacing" (swaps the letter/word spacing), "Foreground" and "Background" + * (each a wuss_colourmenu over the system palette) -- plus a tickable "No + * Background" leaf that unsets the paragraph's background colour so glyphs + * blend straight onto whatever is already behind the window's content. */ typedef struct text_task { - wuss_window_t *window; - wuss_task_t *delegate; /* the wuss task backing this window */ - wuss_t *wuss; /* for wuss_get_pointer when opening the menu */ - wuss_fontmenu_t *fontmenu; /* the font picker; owns the menu and names */ - bmfont_t *font; /* currently shown; fonts[current] or sysfont */ - bmfont_t **fonts; /* one slot per menu item, lazily loaded */ - int nfonts; /* length of fonts[]; == menu item count */ - int current; /* index into fonts[], or -1 for sysfont */ - wuss_menu_item_t top_items[2]; /* "Font" and "Sample", built once the - * fontmenu exists so items[0].submenu can - * borrow its live wuss_menu_t */ - wuss_menu_t top_menu; /* root menu passed to wuss_menu_open */ - int sample; /* index into text_samples[] currently shown */ - const char *text; /* text_samples[sample].text; what text_redraw - * lays out and draws */ - colour_t bg, fg; - int base_width; /* content width when the window was made */ - int base_height; /* content height when the window was made */ - int frame_count; - bool resizing; /* toggled by a content click; text_step only - * resizes the window while this is true */ + wuss_window_t *window; + wuss_task_t *delegate; /* the wuss task backing this window */ + wuss_t *wuss; /* for wuss_get_pointer when opening the + * menu */ + wuss_fontmenu_t *fontmenu; /* the font picker; owns the menu and + * names */ + bmfont_t *font; /* currently shown; fonts[current] or + * sysfont */ + bmfont_t **fonts; /* one slot per menu item, lazily loaded */ + int nfonts; /* length of fonts[]; == menu item count */ + int current; /* index into fonts[], or -1 for sysfont */ + wuss_colourmenu_t *fgmenu; /* the "Foreground" picker; owns its menu */ + wuss_colourmenu_t *bgmenu; /* the "Background" picker; owns its menu */ + wuss_menu_item_t top_items[6]; /* "Font", "Sample", "Spacing", + * "Foreground", "Background" and "No + * Background", built once the fontmenu and + * colourmenus exist so items[0/3/4].submenu + * can borrow their live wuss_menu_t */ + wuss_menu_t top_menu; /* root menu passed to wuss_menu_open */ + wuss_menu_handle_t menu_handle; /* chain handle from the last + * wuss_menu_open, for the _live tick calls + * when an ADJUST pick keeps the chain + * open; NULL if closed */ + int sample; /* index into text_samples[] currently + * shown */ + const char *text; /* text_samples[sample].text; what + * text_redraw lays out and draws */ + int spacing_idx; /* index into text_spacing_presets[] + * currently applied */ + bmfont_spacing_t spacing; /* text_spacing_presets[spacing_idx], + * passed to bmtext_layout/bmtext_draw */ + wuss_colour_t fg_index; /* system palette index picked from + * "Foreground" */ + wuss_colour_t bg_index; /* system palette index picked from + * "Background" */ + bool bg_transparent; /* "No Background" tick; bg drawn + * transparent while set, overriding + * bg_index */ + int base_width; /* content width when the window was + * made */ + int base_height; /* content height when the window was + * made */ + int frame_count; + bool resizing; /* toggled by a content click; text_step + * only resizes the window while this is + * true */ } text_task_t; From d8922bf933cff336e45b7a0807416a1542dc927c Mon Sep 17 00:00:00 2001 From: David Thomas Date: Wed, 16 Sep 2026 21:05:26 +0100 Subject: [PATCH 116/128] docs: update CHANGELOG for work since 311c3ac Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 334a4bcb..ea40dfd1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -352,6 +352,20 @@ _Unreleased_ until one is cut. - `tools/ttf2bmfont.py` — renders a TTF into the bmfont PNG format the loader expects. Three small paletted bitmap faces (`04b_03`, `04b_25`, `Nokia`) added under `resources/bmfonts/`. +- `bmtext_layout()` / `bmtext_draw()` take an optional + `const bmfont_spacing_t *spacing` (NULL for none), threading letter/word + spacing through the paragraph word-wrap and draw paths, not just the + lower-level `bmfont_measure`/`bmfont_draw`. +- The `wuss` text task's menu gains a Spacing submenu (Normal/Letter/Word/ + Letter+Word presets), Foreground/Background colourmenus + (`wuss_colourmenu_t`) and a tickable No Background entry that draws + glyphs with a transparent background instead of the picked colour. +- The `wuss` demo launcher's Launch submenu is split into Games/Tests/ + Utilities/Visuals category submenus hung directly off the top-level task + menu. +- The saturn task's Configure dialogue gains a Default button that resets + `task->config` to `SATURN_CONFIG_DEFAULT`, refills the sliders and + applies it. ### Changed @@ -560,6 +574,10 @@ _Unreleased_ until one is cut. `wuss_window_resize()` before `wuss_window_set_doc()`, which alone only moved the scroll extent) and insets an 8px solid border inside the ninepatch frame. +- The `wuss` demo's global task-list variable is renamed `g` -> `g_tasks`, + avoiding a shadow of the `g`/`b` loop locals in + `tasks_build_screen_palette`; the saturn task now spawns at startup + instead of only via the launcher. ### Fixed @@ -858,3 +876,10 @@ _Unreleased_ until one is cut. started a resize far from the visible handle. It now hands off to a scroll well only when that scrollbar's strip runs the full edge; every other edge pixel resolves to `TITLE` or `CONTENT`. +- The saturn task's stars/ring energy-gate exclusion thresholds were literal + constants tuned for `size==256`; since `p`'s range grows linearly with + size, a larger window shrank the exclusion disc relative to half and let + stars/ring points land inside the planet body. Scaled by + `size/SATURN_SIZE_DEFAULT`. +- `DPT-Digits-Bold` and `MS Sans Serif` bmfonts had an oversized space glyph + width; reduced. From 75d507545dd382872a48089466933e23227d6987 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Wed, 16 Sep 2026 21:24:22 +0100 Subject: [PATCH 117/128] fix(wuss): clamp reversed slider-row snap correctly wuss__slider_row_snap's CLAMP(v, min, max) assumed min <= max, but a reversed slider (documented fill-backwards case, min > max) made every snapped value collapse to max regardless of drag position. Clamp against the ordered [lo, hi] pair instead. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/helpers/icon-spec.c | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/libraries/wuss/helpers/icon-spec.c b/libraries/wuss/helpers/icon-spec.c index 08a0590b..9c60614b 100644 --- a/libraries/wuss/helpers/icon-spec.c +++ b/libraries/wuss/helpers/icon-spec.c @@ -15,12 +15,19 @@ /* value label buffer: ample for any int plus a short unit suffix in fmt */ #define SLIDER_ROW_BUF 32 -/* nearest multiple of step above min, clamped to [min,max] */ +/* nearest multiple of step above min, clamped to [min,max] -- min may be + * greater than max for a reversed (fill-backwards) slider, so clamp against + * the ordered pair rather than assuming min <= max */ static int wuss__slider_row_snap(int v, int min, int max, int step) { + int lo, hi; + v = ((v - min + step / 2) / step) * step + min; - return CLAMP(v, min, max); + lo = MIN(min, max); + hi = MAX(min, max); + + return CLAMP(v, lo, hi); } /* ----------------------------------------------------------------------- */ From 89f35a58b463ebdb071f93e175a501e83698d577 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Wed, 16 Sep 2026 21:24:42 +0100 Subject: [PATCH 118/128] fix(wuss): cap colourmenu swatch generation below the symbolic namespace wuss_colourmenu_create built one row per palette entry with swatch = i and no upper bound, but wuss_colour_t indices >= wuss_COLOUR_SYMBOLIC (128) are reserved for the symbolic/chrome-role namespace, not real palette slots. A palette with more than 128 entries produced rows whose swatch value aliased wuss_COLOUR_BLACK, wuss_COLOUR_GREY, etc, rendering and reporting the wrong colour. Cap the row count at wuss_COLOUR_SYMBOLIC. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/component/colourmenu.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/libraries/wuss/component/colourmenu.c b/libraries/wuss/component/colourmenu.c index faa77554..f90ef265 100644 --- a/libraries/wuss/component/colourmenu.c +++ b/libraries/wuss/component/colourmenu.c @@ -66,6 +66,11 @@ result_t wuss_colourmenu_create(wuss_colourmenu_t **out, a = &wuss->alloc; n = wuss->npalette; + if (n > wuss_COLOUR_SYMBOLIC) + n = wuss_COLOUR_SYMBOLIC; /* wuss_colour_t indices above this are the + * symbolic/chrome-role namespace, not real + * palette slots -- don't hand them out as + * swatches */ cm = a->malloc(sizeof(*cm)); if (cm == NULL) From c4db36c398aeaa8e8ebfc39a40f78df6e23e4471 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Wed, 16 Sep 2026 21:25:09 +0100 Subject: [PATCH 119/128] fix(wuss): let disabled menu rows still collapse an open sibling submenu The DISABLED-item early return in wuss__menu_handle fired before the MOUSE_MOVE child-close logic, so moving the pointer from an open-submenu row onto a disabled sibling left the stale submenu window open and the old row highlighted. Move the disabled check so it only suppresses a disabled row opening its own submenu/pick, not the close-on-move-away path; MOUSE_UP keeps its own disabled guard so a click still can't select a disabled row. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/menu/menu.c | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/libraries/wuss/menu/menu.c b/libraries/wuss/menu/menu.c index 9d73a57c..0a1398f9 100644 --- a/libraries/wuss/menu/menu.c +++ b/libraries/wuss/menu/menu.c @@ -332,16 +332,17 @@ static result_t wuss__menu_handle(wuss_window_t *window, item = &self->menu->items[index]; - if (item->flags & wuss_MENU_ITEM_DISABLED) - return result_OK; - if (event->data.icon.action == wuss_MOUSE_MOVE) { point_t at; int has_child_row; int on_arrow; - has_child_row = (item->submenu != NULL || item->window != NULL); + /* A disabled row still has to run the close-on-move-away logic below -- + * hovering off an open submenu onto a disabled sibling must collapse it + * -- but can never itself open a submenu. */ + has_child_row = !(item->flags & wuss_MENU_ITEM_DISABLED) + && (item->submenu != NULL || item->window != NULL); on_arrow = has_child_row && wuss__pointer_over_row_arrow(self->window, icon); @@ -399,6 +400,9 @@ static result_t wuss__menu_handle(wuss_window_t *window, wuss_task_t *owner; const wuss_menu_t *menu; + if (item->flags & wuss_MENU_ITEM_DISABLED) + return result_OK; + button = event->data.icon.button; if (item->submenu != NULL || item->window != NULL) From 08c1f5079b623e5b4bc795863a5002d9580ced86 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Wed, 16 Sep 2026 21:25:30 +0100 Subject: [PATCH 120/128] fix(wuss): guard wuss_dialogue_handle_icon against NULL dialogue/event Unlike the structurally identical wuss_fontmenu_selected and wuss_colourmenu_selected, wuss_dialogue_handle_icon dereferenced dialogue and event with no NULL check, crashing instead of returning not-mine for a task multiplexing ICON dispatch across an optionally-created dialogue. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/component/dialogue.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libraries/wuss/component/dialogue.c b/libraries/wuss/component/dialogue.c index f4974877..0aeb6cf7 100644 --- a/libraries/wuss/component/dialogue.c +++ b/libraries/wuss/component/dialogue.c @@ -159,6 +159,8 @@ int wuss_dialogue_handle_icon(wuss_dialogue_t *dialogue, { int i; + if (dialogue == NULL || event == NULL) + return 0; if (event->data.icon.action != wuss_MOUSE_UP) return 0; From 6efdcc2b5b4a9b58849a97f74f5b9844996fe0c0 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Wed, 16 Sep 2026 21:26:02 +0100 Subject: [PATCH 121/128] perf(wuss): add n<=1 fast path to wuss__order_pieces wuss__order_pieces always zeroed a full WUSS_MAX_INVALIDATE_PIECES^2 adjacency matrix and ran the O(n^2) intersection loop even for the common single-piece case (no occlusion), on every window move/resize/scroll blit. A single piece can never clobber itself, so skip straight to a trivial order. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/core/window/invalidate.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/libraries/wuss/core/window/invalidate.c b/libraries/wuss/core/window/invalidate.c index 1eb9d7d5..7eb40c28 100644 --- a/libraries/wuss/core/window/invalidate.c +++ b/libraries/wuss/core/window/invalidate.c @@ -173,6 +173,15 @@ int wuss__order_pieces(const box_t *clean, int queue[WUSS_MAX_INVALIDATE_PIECES]; int i, j, head, tail, nout, u; + if (n <= 1) + { + /* a single piece (or none) can't clobber itself: skip the O(n^2) graph + * build, the common case on every plain move/resize/scroll blit */ + if (n == 1) + order[0] = 0; + return 1; + } + for (i = 0; i < n; i++) indeg[i] = 0; for (j = 0; j < n; j++) From 2b0b9992ca43863d2b97d406adfe5e5e416a135c Mon Sep 17 00:00:00 2001 From: David Thomas Date: Wed, 16 Sep 2026 21:31:31 +0100 Subject: [PATCH 122/128] refactor(wuss): de-duplicate clip_to_visible/subtract_boxes carve loops wuss__clip_to_visible and wuss__subtract_boxes each implemented the same ping-pong carve-by-cuts loop with a different cut source. Extract the shared logic into carve_by_cuts, driven by a get_cut callback, with zorder_get_cut and array_get_cut adapters for the two call sites. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/core/window/invalidate.c | 136 ++++++++++++++---------- 1 file changed, 77 insertions(+), 59 deletions(-) diff --git a/libraries/wuss/core/window/invalidate.c b/libraries/wuss/core/window/invalidate.c index 7eb40c28..47591b78 100644 --- a/libraries/wuss/core/window/invalidate.c +++ b/libraries/wuss/core/window/invalidate.c @@ -42,38 +42,41 @@ static void box_subtract_into(const box_t *piece, } } -/* Clip "box" (screen space) down to the parts not already covered by - * windows above "window" in the z-order, writing the surviving pieces to - * "out" (capacity WUSS_MAX_INVALIDATE_PIECES) and returning their count. */ -int wuss__clip_to_visible(wuss_window_t *window, - const box_t *box, - box_t *out) +/* Shared ping-pong carve loop: whittle "box" down by subtracting, in turn, + * each of "ncuts" cut boxes fetched one at a time via "get_cut" (opaque + * "ctx" threaded through), writing the surviving pieces to "out" (capacity + * WUSS_MAX_INVALIDATE_PIECES) and returning their count. A cut for which + * get_cut returns 0 is skipped (used to drop hidden occluders without the + * caller pre-filtering its list). */ +static int carve_by_cuts(const box_t *box, + int ncuts, + int (*get_cut)(void *ctx, int i, box_t *cut), + void *ctx, + box_t *out) { - box_t scratch[WUSS_MAX_INVALIDATE_PIECES]; - box_t *cur, *nxt, *tmp; - int ncur; - list_t *e; + box_t scratch[WUSS_MAX_INVALIDATE_PIECES]; + box_t *cur, *nxt, *tmp; + int ncur, i; cur = out; nxt = scratch; cur[0] = *box; ncur = 1; - for (e = window->wuss->z_order.next; e != &window->link; e = e->next) + for (i = 0; i < ncuts; i++) { - wuss_window_t *occluder; - int nnext, p; + box_t occluder; + int p, nnext; - occluder = wuss__window_from_link(e); - if (occluder->flags & wuss_WINDOW_HIDDEN) - continue; /* a hidden window occludes nothing */ - nnext = 0; + if (!get_cut(ctx, i, &occluder)) + continue; /* e.g. a hidden window occludes nothing */ + nnext = 0; for (p = 0; p < ncur; p++) { box_t cut; - if (box_intersection(&occluder->visible, &cur[p], &cut)) + if (box_intersection(&occluder, &cur[p], &cut)) { if (nnext < WUSS_MAX_INVALIDATE_PIECES) nxt[nnext++] = cur[p]; /* no overlap: piece survives untouched */ @@ -99,57 +102,72 @@ int wuss__clip_to_visible(wuss_window_t *window, return ncur; } -/* Subtract each of "cuts" (an array of "ncuts" boxes) from "whole", writing - * the surviving pieces to "out" (capacity WUSS_MAX_INVALIDATE_PIECES) and - * returning their count. */ -int wuss__subtract_boxes(const box_t *whole, - const box_t *cuts, - int ncuts, - box_t *out) +/* carve_by_cuts callback context/fetcher for wuss__clip_to_visible: walks + * the z-order list occluder-by-occluder, in the same order carve_by_cuts + * counts "i", skipping hidden windows. */ +typedef struct { - box_t scratch[WUSS_MAX_INVALIDATE_PIECES]; - box_t *cur, *nxt, *tmp; - int ncur, i; + list_t *e; +} +zorder_ctx_t; - cur = out; - nxt = scratch; - cur[0] = *whole; - ncur = 1; +static int zorder_get_cut(void *vctx, int i, box_t *cut) +{ + zorder_ctx_t *ctx; + wuss_window_t *occluder; - for (i = 0; i < ncuts; i++) - { - int p, nnext; + (void) i; - nnext = 0; + ctx = vctx; + occluder = wuss__window_from_link(ctx->e); + ctx->e = ctx->e->next; - for (p = 0; p < ncur; p++) - { - box_t cut; + if (occluder->flags & wuss_WINDOW_HIDDEN) + return 0; /* a hidden window occludes nothing */ - if (box_intersection(&cuts[i], &cur[p], &cut)) - { - if (nnext < WUSS_MAX_INVALIDATE_PIECES) - nxt[nnext++] = cur[p]; /* no overlap: piece survives untouched */ - } - else - { - box_subtract_into(&cur[p], &cut, nxt, &nnext); - } - } + *cut = occluder->visible; + return 1; +} - ncur = nnext; - tmp = cur; - cur = nxt; - nxt = tmp; +/* carve_by_cuts callback fetcher for wuss__subtract_boxes: plain indexed + * array lookup, every entry used. */ +static int array_get_cut(void *vctx, int i, box_t *cut) +{ + const box_t *cuts = vctx; - if (ncur == 0) - break; - } + *cut = cuts[i]; + return 1; +} - if (cur != out) - memcpy(out, cur, ncur * sizeof(*out)); +/* Clip "box" (screen space) down to the parts not already covered by + * windows above "window" in the z-order, writing the surviving pieces to + * "out" (capacity WUSS_MAX_INVALIDATE_PIECES) and returning their count. */ +int wuss__clip_to_visible(wuss_window_t *window, + const box_t *box, + box_t *out) +{ + zorder_ctx_t ctx; + list_t *e; + int n; - return ncur; + n = 0; + for (e = window->wuss->z_order.next; e != &window->link; e = e->next) + n++; + + ctx.e = window->wuss->z_order.next; + + return carve_by_cuts(box, n, zorder_get_cut, &ctx, out); +} + +/* Subtract each of "cuts" (an array of "ncuts" boxes) from "whole", writing + * the surviving pieces to "out" (capacity WUSS_MAX_INVALIDATE_PIECES) and + * returning their count. */ +int wuss__subtract_boxes(const box_t *whole, + const box_t *cuts, + int ncuts, + box_t *out) +{ + return carve_by_cuts(whole, ncuts, array_get_cut, (void *) cuts, out); } /* Sequential single-rect blits (each a self-consistent memmove) can still From 2b85bebd63fdb43bb39c27fff8963ef3d33aa8fa Mon Sep 17 00:00:00 2001 From: David Thomas Date: Wed, 16 Sep 2026 21:32:48 +0100 Subject: [PATCH 123/128] refactor(wuss): de-duplicate settled-vs-stale pixel filtering wuss_window_move and wuss_window_resize each filtered their clean (occlusion-clear) pieces against wuss->dirty[] with the same per-piece subtract-and-collect loop. Extract wuss__filter_settled to share it; resize.c keeps its own pre-filter of dirty by intersection with before_content, then hands the result to the shared helper. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/core/impl.h | 11 +++++++++ libraries/wuss/core/window/invalidate.c | 26 +++++++++++++++++++++ libraries/wuss/core/window/move.c | 21 ++--------------- libraries/wuss/core/window/resize.c | 31 ++++++------------------- 4 files changed, 46 insertions(+), 43 deletions(-) diff --git a/libraries/wuss/core/impl.h b/libraries/wuss/core/impl.h index ef677853..9dc37312 100644 --- a/libraries/wuss/core/impl.h +++ b/libraries/wuss/core/impl.h @@ -455,6 +455,17 @@ int wuss__subtract_boxes(const box_t *whole, int ncuts, box_t *out); +/* Filter "clean" (nclean pieces already clipped clear of occluders) down to + * the parts not also covered by "stale" (nstale pending-dirty boxes not yet + * repainted), writing survivors back into "clean" and returning the new + * count. Each piece is subtracted independently, so the result can hold more + * than "nclean" entries; excess beyond WUSS_MAX_INVALIDATE_PIECES is + * dropped. */ +int wuss__filter_settled(box_t *clean, + int nclean, + const box_t *stale, + int nstale); + /* Given "n" single-rect blits, each moving "clean[i]" to "dest[i]", find an * order in which no blit's destination overwrites a still-unread source of a * later blit. Writes the piece indices to "order" (capacity diff --git a/libraries/wuss/core/window/invalidate.c b/libraries/wuss/core/window/invalidate.c index 47591b78..263505df 100644 --- a/libraries/wuss/core/window/invalidate.c +++ b/libraries/wuss/core/window/invalidate.c @@ -170,6 +170,32 @@ int wuss__subtract_boxes(const box_t *whole, return carve_by_cuts(whole, ncuts, array_get_cut, (void *) cuts, out); } +int wuss__filter_settled(box_t *clean, + int nclean, + const box_t *stale, + int nstale) +{ + box_t settled[WUSS_MAX_INVALIDATE_PIECES]; + int nsettled, c; + + if (nstale == 0) + return nclean; + + nsettled = 0; + for (c = 0; c < nclean && nsettled < WUSS_MAX_INVALIDATE_PIECES; c++) + { + box_t piece[WUSS_MAX_INVALIDATE_PIECES]; + int npiece, s; + + npiece = wuss__subtract_boxes(&clean[c], stale, nstale, piece); + for (s = 0; s < npiece && nsettled < WUSS_MAX_INVALIDATE_PIECES; s++) + settled[nsettled++] = piece[s]; + } + + memcpy(clean, settled, (size_t) nsettled * sizeof(*clean)); + return nsettled; +} + /* Sequential single-rect blits (each a self-consistent memmove) can still * corrupt each other when one piece's destination lands on another piece's * still-unread source -- but that only actually matters if no blit order diff --git a/libraries/wuss/core/window/move.c b/libraries/wuss/core/window/move.c index d013c7aa..0da028df 100644 --- a/libraries/wuss/core/window/move.c +++ b/libraries/wuss/core/window/move.c @@ -68,25 +68,8 @@ void wuss_window_move(wuss_window_t *window, point_t p) * would just paste it, untouched, onto the window's new position. Strip * every pending-dirty region out of "clean" first so only pixels already * settled on screen are treated as a valid blit source. */ - if (window->wuss->ndirty > 0) - { - box_t settled[WUSS_MAX_INVALIDATE_PIECES]; - int nsettled, c; - - nsettled = 0; - for (c = 0; c < nclean && nsettled < WUSS_MAX_INVALIDATE_PIECES; c++) - { - box_t piece[WUSS_MAX_INVALIDATE_PIECES]; - int npiece, s; - - npiece = wuss__subtract_boxes(&clean[c], window->wuss->dirty, - window->wuss->ndirty, piece); - for (s = 0; s < npiece && nsettled < WUSS_MAX_INVALIDATE_PIECES; s++) - settled[nsettled++] = piece[s]; - } - nclean = nsettled; - memcpy(clean, settled, (size_t) nclean * sizeof(*clean)); - } + nclean = wuss__filter_settled(clean, nclean, window->wuss->dirty, + window->wuss->ndirty); window->visible.x0 = p.x - outline_px; window->visible.y0 = p.y - outline_px - titlebar_height; diff --git a/libraries/wuss/core/window/resize.c b/libraries/wuss/core/window/resize.c index 38f39ea1..e3da8fce 100644 --- a/libraries/wuss/core/window/resize.c +++ b/libraries/wuss/core/window/resize.c @@ -91,7 +91,6 @@ result_t wuss_window_resize(wuss_window_t *window, size2d_t size) box_t content; box_t stale[WUSS_MAX_DIRTY]; box_t clean[WUSS_MAX_INVALIDATE_PIECES]; - box_t src[WUSS_MAX_INVALIDATE_PIECES]; box_t copied[WUSS_MAX_INVALIDATE_PIECES]; box_t dirty[WUSS_MAX_INVALIDATE_PIECES]; int dx, dy, nstale, nclean, nsrc, ncopied, ndirty, i, overflow; @@ -118,28 +117,12 @@ result_t wuss_window_resize(wuss_window_t *window, size2d_t size) * invalidated either, since the invalidate below only covers "content * minus copied", not "content minus every clean piece found" -- so a * silently-dropped piece would leave genuinely stale pre-resize pixels - * on screen with nothing left to repaint them. */ + * on screen with nothing left to repaint them. wuss__filter_settled caps + * at WUSS_MAX_INVALIDATE_PIECES the same way, so treat a full result as + * a possible overflow too. */ nclean = wuss__clip_to_visible(window, &before_content, clean); - nsrc = 0; - overflow = 0; - for (i = 0; i < nclean; i++) - { - box_t kept[WUSS_MAX_INVALIDATE_PIECES]; - int nkept, k; - - nkept = wuss__subtract_boxes(&clean[i], stale, nstale, kept); - for (k = 0; k < nkept; k++) - { - if (nsrc == WUSS_MAX_INVALIDATE_PIECES) - { - overflow = 1; - break; - } - src[nsrc++] = kept[k]; - } - if (overflow) - break; - } + nsrc = wuss__filter_settled(clean, nclean, stale, nstale); + overflow = (nsrc == WUSS_MAX_INVALIDATE_PIECES); ncopied = 0; if (!overflow) @@ -149,8 +132,8 @@ result_t wuss_window_resize(wuss_window_t *window, size2d_t size) { box_t got; - if (screen_copy_rect(window->wuss->scr, &src[i], - POINT(src[i].x0 - dx, src[i].y0 - dy), + if (screen_copy_rect(window->wuss->scr, &clean[i], + POINT(clean[i].x0 - dx, clean[i].y0 - dy), &got) == result_OK) { /* "got" is already correct on screen -- the blit reused it -- From 48e3334fe5ac2d0e20966eab670ce1743fbb5b2a Mon Sep 17 00:00:00 2001 From: David Thomas Date: Wed, 16 Sep 2026 21:33:12 +0100 Subject: [PATCH 124/128] fix(wuss): enforce WUSS_MIN_SAUSAGE floor when the well is shorter than it CLAMP(sausage_px, WUSS_MIN_SAUSAGE, track_px) silently returns track_px when track_px < WUSS_MIN_SAUSAGE, dropping below the minimum sausage size instead of enforcing it. Clamp the upper bound itself against WUSS_MIN_SAUSAGE so the floor always wins. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/furniture/scroll-box.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/wuss/furniture/scroll-box.c b/libraries/wuss/furniture/scroll-box.c index e15678ad..9bbde462 100644 --- a/libraries/wuss/furniture/scroll-box.c +++ b/libraries/wuss/furniture/scroll-box.c @@ -193,7 +193,7 @@ static void scroll_sausage(const wuss_window_t *window, doc_size = MAX(doc_size, content_size); sausage_px = (doc_size > 0) ? track_px * content_size / doc_size : track_px; - sausage_px = CLAMP(sausage_px, WUSS_MIN_SAUSAGE, track_px); + sausage_px = CLAMP(sausage_px, WUSS_MIN_SAUSAGE, MAX(WUSS_MIN_SAUSAGE, track_px)); scroll = horizontal ? window->scroll.x : window->scroll.y; From 2ba12e3f39f6c6a778e2246dde273acf21276097 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Wed, 16 Sep 2026 21:33:35 +0100 Subject: [PATCH 125/128] fix(wuss): guard wuss_proginfo_set_desc against NULL pi/desc Matches the NULL-guard idiom used elsewhere in this file and in dialogue.c/fontmenu.c/colourmenu.c -- a borrowed const char* dereferenced with no check is a crash waiting for a live caller. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/component/proginfo.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/libraries/wuss/component/proginfo.c b/libraries/wuss/component/proginfo.c index bf064ebe..d16b476c 100644 --- a/libraries/wuss/component/proginfo.c +++ b/libraries/wuss/component/proginfo.c @@ -140,6 +140,9 @@ void wuss_proginfo_destroy(wuss_proginfo_t *doomed) void wuss_proginfo_set_desc(wuss_proginfo_t *pi, const wuss_proginfo_desc_t *desc) { + if (pi == NULL || desc == NULL) + return; + pi->pending = *desc; } From 8210d0052bc6c6ffacee79946dd597c759ac6f9f Mon Sep 17 00:00:00 2001 From: David Thomas Date: Wed, 16 Sep 2026 21:33:56 +0100 Subject: [PATCH 126/128] style(wuss): reorder wuss_window_resize locals by first use old_scroll was declared after before_content/carve despite being assigned first. Co-Authored-By: Claude Sonnet 5 --- libraries/wuss/core/window/resize.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/libraries/wuss/core/window/resize.c b/libraries/wuss/core/window/resize.c index e3da8fce..db71ee63 100644 --- a/libraries/wuss/core/window/resize.c +++ b/libraries/wuss/core/window/resize.c @@ -34,9 +34,10 @@ static void invalidate_grown_or_shrunk(wuss_window_t *window, result_t wuss_window_resize(wuss_window_t *window, size2d_t size) { int outline_px, titlebar_height; - box_t before, before_content; - point_t carve; + box_t before; point_t old_scroll; + box_t before_content; + point_t carve; point_t clamped; if (!wuss__size_ok(size.w, size.h)) From 085c420d5366bb4325d110ec6ec559c0a9ea76b5 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Wed, 16 Sep 2026 21:35:51 +0100 Subject: [PATCH 127/128] docs: update CHANGELOG for code-review fixes Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea40dfd1..e06552d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -578,6 +578,13 @@ _Unreleased_ until one is cut. avoiding a shadow of the `g`/`b` loop locals in `tasks_build_screen_palette`; the saturn task now spawns at startup instead of only via the launcher. +- `wuss__clip_to_visible()` and `wuss__subtract_boxes()`'s near-identical + ping-pong carve loops are factored into a shared `carve_by_cuts()` driven + by a `get_cut` callback, with z-order and plain-array adapters for the two + call sites. +- `wuss_window_move()` and `wuss_window_resize()`'s duplicated "filter clean + pieces against `wuss->dirty[]`" loops are factored into a shared + `wuss__filter_settled()`. ### Fixed @@ -883,3 +890,27 @@ _Unreleased_ until one is cut. `size/SATURN_SIZE_DEFAULT`. - `DPT-Digits-Bold` and `MS Sans Serif` bmfonts had an oversized space glyph width; reduced. +- `wuss__slider_row_snap()`'s `CLAMP(v, min, max)` assumed `min <= max`, so a + reversed (fill-backwards) slider's `CLAMP` always returned `max` regardless + of drag position; clamps against `MIN(min,max)`/`MAX(min,max)` instead. +- `wuss_colourmenu_create()` handed out `wuss_colour_t` swatches up to + `wuss->npalette` with no upper bound, so a palette with more than 128 + entries produced swatches aliasing the symbolic/chrome-role colour + namespace; capped at `wuss_COLOUR_SYMBOLIC`. +- A disabled menu row's `wuss_MOUSE_MOVE` handling was skipped entirely, so + hovering off an open submenu onto a disabled sibling never ran the + close-on-move-away logic, leaving the stale submenu open; the disabled + check now only suppresses opening a submenu, not the move-away close. +- `wuss_dialogue_handle_icon()` lacked the `NULL`/action-kind guard every + other component's icon handler has, risking a crash for a task + multiplexing `ICON` dispatch across an optionally-created dialogue. +- `wuss__order_pieces()` built its full overlap graph even for a single + piece, which by construction cannot clobber itself; added an `n <= 1` fast + path, the common case on every plain move/resize/scroll blit. +- `scroll_sausage()`'s `CLAMP(sausage_px, WUSS_MIN_SAUSAGE, track_px)` + silently dropped below `WUSS_MIN_SAUSAGE` whenever the well was shorter + than the minimum sausage; the upper bound is now clamped against + `WUSS_MIN_SAUSAGE` too so the floor always wins. +- `wuss_proginfo_set_desc()` dereferenced a borrowed `desc` with no `NULL` + check; guarded, matching the idiom used elsewhere in the component + helpers. From 05cfce720d623cbed2390c8696670539366e1dc9 Mon Sep 17 00:00:00 2001 From: David Thomas Date: Wed, 16 Sep 2026 21:42:51 +0100 Subject: [PATCH 128/128] fix(geom): replace stack solver VLAs with a fixed STACK_MAX_ITEMS cap MSVC has no VLA support (C99 6.7.5.2 is optional and unimplemented there), so stack_smallest()'s size2d_t mins[n] and stack__place_container()'s stack__child_t children[n] failed Windows CI with C2057/C2466/C2133. Both are now fixed-size arrays capped at the new STACK_MAX_ITEMS (64), with stack_solve()/stack_smallest() rejecting n > STACK_MAX_ITEMS as result_STACK_BAD_TREE. Co-Authored-By: Claude Sonnet 5 --- include/geom/stack.h | 9 +++++++-- libraries/geom/stack/measure-min.c | 4 +++- libraries/geom/stack/solve.c | 20 ++++++++++++-------- 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/include/geom/stack.h b/include/geom/stack.h index 2073f44e..b6b4485a 100644 --- a/include/geom/stack.h +++ b/include/geom/stack.h @@ -16,6 +16,11 @@ extern "C" #define result_STACK_BAD_TREE (result_BASE_STACK + 0) +/** Largest item-array size stack_solve()/stack_smallest() will accept + * (they use fixed-size internal scratch arrays, not a VLA, for MSVC + * portability). Ample for any hand-written table. */ +#define STACK_MAX_ITEMS 64 + /* ----------------------------------------------------------------------- */ /** The kind of a stack item. */ @@ -129,7 +134,7 @@ stack_item_t; * for `items[i]`, in the same coordinate space as `root`. * \return \ref result_OK on success, result_STACK_BAD_TREE if the parent * indices are not a valid parents-before-children forest rooted at - * 0. + * 0, or if `n` exceeds \ref STACK_MAX_ITEMS. */ result_t stack_solve(const stack_item_t *items, int n, @@ -151,7 +156,7 @@ result_t stack_solve(const stack_item_t *items, * \param[out] out Set to the root item's minimum size. * \return \ref result_OK on success, result_STACK_BAD_TREE if the parent * indices are not a valid parents-before-children forest rooted at - * 0. + * 0, or if `n` exceeds \ref STACK_MAX_ITEMS. */ result_t stack_smallest(const stack_item_t *items, int n, diff --git a/libraries/geom/stack/measure-min.c b/libraries/geom/stack/measure-min.c index 68fd41e0..27f60453 100644 --- a/libraries/geom/stack/measure-min.c +++ b/libraries/geom/stack/measure-min.c @@ -76,8 +76,10 @@ result_t stack_smallest(const stack_item_t *items, int n, size2d_t *out) { - size2d_t mins[n]; + size2d_t mins[STACK_MAX_ITEMS]; + if (n > STACK_MAX_ITEMS) + return result_STACK_BAD_TREE; if (!stack__valid_tree(items, n)) return result_STACK_BAD_TREE; diff --git a/libraries/geom/stack/solve.c b/libraries/geom/stack/solve.c index c8581a30..bd51b7d2 100644 --- a/libraries/geom/stack/solve.c +++ b/libraries/geom/stack/solve.c @@ -127,14 +127,16 @@ static void stack__place_container(const stack_item_t *items, box_t *out) { int horiz; - stack__child_t children[n]; /* upper bound: at most n-1 children */ - int nchildren; - const box_t *box; - int inner_main0, inner_main1; - int cross0, cross1; - int avail; - int pos; - int c; + stack__child_t children[STACK_MAX_ITEMS]; /* upper bound: at most n-1 + * children, n capped by the + * caller (stack_solve) */ + int nchildren; + const box_t *box; + int inner_main0, inner_main1; + int cross0, cross1; + int avail; + int pos; + int c; horiz = (items[index].kind == stack_KIND_HBOX); box = &out[index]; @@ -228,6 +230,8 @@ result_t stack_solve(const stack_item_t *items, const box_t *root, box_t *out) { + if (n > STACK_MAX_ITEMS) + return result_STACK_BAD_TREE; if (!stack__valid_tree(items, n)) return result_STACK_BAD_TREE;