Skip to content

Wuss snagglepuss - #13

Merged
dpt merged 128 commits into
developfrom
wuss-snagglepuss
Sep 16, 2026
Merged

dpt merged 128 commits into
developfrom
wuss-snagglepuss

Conversation

@dpt

@dpt dpt commented Sep 16, 2026

Copy link
Copy Markdown
Owner
Screenshot 2026-09-16 at 21 38 52

dpt and others added 30 commits September 10, 2026 11:29
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 <noreply@anthropic.com>
…ypes

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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
Ran tools/wrap_doxygen.py across the headers; two blocks were over 77
cols and reflowed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
…t_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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
… 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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
…ry 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.
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
…lpha

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 <noreply@anthropic.com>
dpt and others added 26 commits September 16, 2026 12:54
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
…asks

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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
…o 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 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
…an 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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
old_scroll was declared after before_content/carve despite being assigned first.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, we are unable to review this pull request

The GitHub API does not allow us to fetch diffs exceeding 20000 lines

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 <noreply@anthropic.com>
@dpt
dpt merged commit 62bc9e0 into develop Sep 16, 2026
11 checks passed
@dpt
dpt deleted the wuss-snagglepuss branch September 16, 2026 20:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant