Skip to content

Wuss sourpuss - #11

Merged
dpt merged 47 commits into
developfrom
wuss-sourpuss
Sep 6, 2026
Merged

dpt merged 47 commits into
developfrom
wuss-sourpuss

Conversation

@dpt

@dpt dpt commented Sep 6, 2026

Copy link
Copy Markdown
Owner
Screenshot 2026-09-06 at 11 13 28

dpt and others added 30 commits September 4, 2026 21:48
wuss_window_move's fast path slid occlusion-clean pieces of a window's
old footprint straight onto its new position, but occlusion-clean isn't
the same as settled: a piece can still have part of it queued in
wuss->dirty[] from an earlier invalidation this frame that
wuss_redraw_dirty hasn't repainted yet (e.g. a screen-edge clip from a
prior move in the same input batch). Sliding that stale ground pasted
it, untouched, onto the window's new position.

Emscripten's browser event queue commonly hands SDL several
MOUSE_MOTION events per animation frame before the demo's single
wuss_redraw_dirty() call, so batched drags near a screen edge hit this
every time; native SDL rarely batches more than one motion event per
frame, which masked it on macOS.

Strip pending wuss->dirty[] regions out of the clean source set before
sliding, so only pixels already settled on screen are treated as a
valid blit source.
screen_draw_dashed_line clipped x0,y0 to the screen bounds but always
reset the dash phase counter to 0 at the new, clipped start point. When
a dashed rule (e.g. a menu divider) is scrolled partially off-screen
and back, the amount clipped off the start varies per frame, so the
dash pattern drifted/smeared relative to its unclipped appearance.

Capture the pre-clip start point, compute how many pixels were
skipped by the bounds clip, and seed phase with that count mod the
dash period so the pattern stays anchored to the original line.
Classic minesweeper on a 12x12/20-mine board: Select reveals (flood-filling
zero-neighbour cells), Adjust flags, a MENU click pops a New Game entry, and
either click retries a finished board. A one-cell border frames the grid;
neighbour counts draw in the bold digit font, centred via bmfont_measure.

Wired into apps/wuss/main.c's Launch menu alongside the other demo tasks.
wuss__menu_spawn sized the menu window's content width purely from the
widest item label; a title longer than every item (e.g. a one-item menu)
got clipped by the titlebar instead of widening the row to fit. Measure
the title too (same bold-font-with-fallback rule wuss__titlebar_draw
uses) and take the wider of the two.
HUD strip above the grid shows mines-remaining (mines minus flags) on
the left and elapsed seconds on the right. Timer starts on the first
reveal and freezes once the game ends; a wuss_EVENT_IDLE handler
invalidates the window once per elapsed second to keep it live.
The row highlight previously swapped fg/bg across the full item bbox,
darkening the tick and submenu-arrow gutters too. Confine the
highlight fill and text draw to the column between the two gutters,
reserving the arrow gutter even on rows with no arrow so every row's
highlight lines up at the same right edge.

The text itself is drawn as if a space padded it either side (a
measured space-width inset), without touching the item's actual
string, and the menu-width calculation budgets two of those
space-widths so the window is sized to match. Rename
WUSS_MENU_TICK_W/WUSS_MENU_ARROW_W to WUSS_MENU_GUTTER_LEFT/
WUSS_MENU_GUTTER_RIGHT and drop the now-redundant fixed
WUSS_MENU_TEXT_PAD gap.
…der menu

Top-level MENU click on the text task now opens a root menu with two
submenus: "Font" (unchanged fontmenu picker) and "Sample" (Quick Brown
Fox, two more pangrams, and Lorem Ipsum, the prior fixed paragraph and
still the default). text_redraw draws whichever text_samples[] entry is
currently selected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
fmt, bytespp, info_ptr and outrow are set before the setjmp and read
after a longjmp back into it, so the compiler can't prove they survive
the jump. Mark them volatile.
The RISC OS cross-compiler warned that callers pass a non-const
pattern_runs_t where the callees declared a const-qualified one, even
though the conversion is legal. The params are read-only by convention
already; drop the const to match what callers actually pass.
wuss_colour_t is unsigned char and screen_pattern_t is an unsigned
enum, so the "< 0" half of each range check is always false and
warns under -Wtype-limits. The ">=" upper-bound check already covers
the real validation.
Same -Wtype-limits issue as backdrop.c: wuss_colour_t is unsigned
char, so "< 0" is always false. Applies to validate_bevel_backdrop
and the furniture-palette range check in wuss_create.
Same -Wtype-limits issue: spec->pattern, fg, bg and swatch are all
unsigned, so the "< 0" half of each range check is always false.
Replace the hardcoded PICO-8/WIMP16 palette tables with a runtime scan
of resources/palettes/*.hex, so new palettes drop in without a rebuild.
Add wuss_menu_set_item_ticked to the core menu API so a menu tracking
more than one independent tick (selected file plus an Invert toggle)
can update a single row in place on an ADJUST pick, without reopening
or repositioning the chain. Guard both tick calls against a stale
handle after a SELECT pick, which closes and frees the chain before
delivering the event. Tick the menu row matching the actual startup
palette (WUSS_PALETTE env var or PICO-8 default) on first open, and
reset Invert whenever a new palette file is picked.

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

The palette picker task previously handed its loaded/inverted array to
an app-supplied on_select callback, which then had to know about the
framebuffer bitmap and physical frontend to push it there. Replace
that with the event wuss already broadcasts: palette_create no longer
takes a palette/npalette/on_select/user_data at all, palette_menu_select
installs a pick with wuss_set_palette directly, and the swatch grid
reads the live array back with the new wuss_get_palette accessor on
every redraw instead of caching a caller-owned pointer. main.c's
menu_task now does the bitmap/frontend push itself from a
wuss_EVENT_PALETTE case, reading the same accessor, so the palette task
has no reason to know the frontend exists.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Q collided with normal typing in text-entry tasks; F4 was free since
the palette-cycle key that used to live there was removed. Escape still
quits either way.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
bmfont_enumerate and the wuss palette-picker task each carried their own copy of the RISC OS OS_GBPB / Win32 FindFirstFile / dirent flat-directory walk. Move the walk into a new io/dirscan module; both callers now just filter/collect from dirscan_walk's per-leafname callback.
Replace the default emscripten boilerplate page with a wuss-specific shell (title, control key legend, about blurb), wired via --shell-file.
…ling

Add wuss_window_set_doc so a window's virtual document extent can change
after creation (used when cycling to a differently-sized image), replace
image.c's hardcoded name list with a dirscan_walk() scan of resources/images,
and add new sample images for it to cycle through.

Add a wuss-test.c case for a dirty-rect containment bug hit while working
on this: wuss_invalidate() must not shrink an existing dirty rect down to
a newly-invalidated box that is fully contained within it.
…buffer

path_join_filename returns a shared static buffer. spawn_image held the
ninepatch pointer into it, then image_create called path_join_filename
again for its resources/images dirscan, overwriting that buffer with a
directory path. bitmap_load_png then fopen'd the directory, file_is_png
failed, and image_create returned result_BAD_ARG. Copy the ninepatch
path into its own buffer before the call.

Also make image_click swallow a failed reload (warn, keep the current
image) rather than propagating rc, since some sample PNGs are sub-8-bit
and bitmap_load_png returns result_INCOMPATIBLE for them.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1/2/4-bpp paletted PNGs were rejected with result_INCOMPATIBLE by the
blanket pngbitdepth != 8 check. Restrict that check to non-palette
colour types and call png_set_packing() before png_set_palette_to_rgb()
so libpng unpacks the sub-byte indices to one per byte; the existing
palette-to-RGB expansion then handles the rest unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… window off-edge

wuss_mouse_move now pins the incoming point to [0, scr size - 1] before it
feeds the drag-move/resize/scrollbar paths and the stored wuss->pointer, so a
pointer report from outside the frame can no longer carry a window off the
desktop. Click hit-routing is left unclamped.

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

Recasts the circuit-greeble look as a Wang-style tile problem: 19 code-drawn
"pipe" tiles (blank, stubs, straights, decorated straights, elbows, tees,
cross, pip) each carrying a 4-bit edge mask. A greedy row-major scan fills the
grid, at each cell picking uniformly from bag tiles whose west/north edges
match the already-placed neighbours; blank's all-closed mask keeps the
candidate set non-empty so no backtracking is needed, and the outer ring is
forced closed outward so wires stay in-window.

Tiles are stamped from code as 2px strokes centre-to-edge, drawn once in a
shadow colour at +1,+1 then in the foreground. Click reseeds the pattern
(deterministic per seed); scroll changes tile size 6-16px and relayouts.

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

Replaces the 19 code-drawn pipe tiles with the real Xenon 2 greebling sheet:
205 hand-drawn 8x8 PICO-8 stamps baked to greeble-tiles.h by the new
tools/gen_greeble_tiles.py, blitted 1:1 with their four palette slots mapped
to the active Wuss palette so the pattern recolours with the theme.

Edge-matched WFC was tried first (the generator also bakes a per-edge
colour-set class table) but the sheet is a scrapbook of decorative fragments,
not a connective tile set, so matching only ever collapsed the plane into
noise. greeble_generate now just draws a uniform-random stamp per cell, which
reads as the intended greeble texture; greeble_tile_edge[] is left in the
header, unused, for anyone who wants to revisit adjacency.

Drops the tile-size scroll control (stamps are fixed 8px).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
gen_greeble_tiles.py takes an optional second image -- the sheet where the
artist laid the stamps out in their intended shapes. Its 4-connected
non-empty clusters become prefab blocks (greeble_prefab / greeble_prefab_cells,
0xFF marking an uncovered cell); the lone 1x1 clusters become greeble_filler[],
the shortlist of stamps that read well standing alone. Without the second
image, filler is every tile and no prefabs are emitted -- the old flat scatter.

greeble_generate now clears the grid, scatters prefab blocks without overlap,
then fills every unclaimed cell from greeble_filler[]. The repeated xorshift32
step is now the GREEBLE_XORSHIFT macro. greeble_tile_edge[] and the edge-class
machinery are gone from both the generator and the header.
gen_greeble_tiles.py takes an optional third image -- a 4xN sheet, one
4-colour palette per row -- and bakes it as greeble_palettes[NPALETTE][4]
(packed 0xAABBGGRR). The sheet runs light-to-dark; stamp slots run
dark-to-light, so each row is reversed on read. Without the sheet, one
PICO-8 row is emitted.

greeble_generate picks a palette row from the seed alongside the pattern;
greeble_redraw expands that row into colour_t once via colour_rgba and
greeble_stamp indexes it by slot. Drops the old greeble_palette[] table and
the framebuf/palettes.h / scr->palette indirection.
…ette

greeble_stamp walked each 8x8 row a pixel at a time -- 64 screen_set_pixel
calls per stamp, each re-clipping and re-resolving the colour. It now walks
same-slot runs and draws them with screen_fill_hline: 1-4 contiguous-word
run fills per row.

Select and Adjust are now split. Select (greeble_select) reseeds the pattern
as before; Adjust (greeble_adjust) steps task->palette to the next
greeble_palettes[] row without regenerating the grid. greeble_generate no
longer picks a palette; greeble_create seeds task->palette to 0.
MENU over the content opens a Greeble menu: the first rows pick a
symmetry mode (Scatter/Mirror/Quad) that folds the generated grid onto
itself from the same seed, and a trailing toggle gives each scattered
prefab its own random palette, tracked per cell in cellpal[][].

Adjust now regenerates after stepping the base palette so filler cells
pick up the new row. Prefab sheet rebuilt to 62 blocks. memset clears
the grid in one pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
dpt and others added 17 commits September 6, 2026 11:21
…and the menu

The symmetry modes and their menu rows are removed for now. The Greeble
menu is a single "Random palettes" toggle, and an Adjust click flips the
same state via a shared greeble_toggle_randpal that keeps the menu row's
tick in step. Adjust no longer cycles the base palette.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A MENU press outside the open menu chain was spent by wuss__menu_click_outside
closing the chain, so the target window's delegate never got the MOUSE_DOWN
and could not open its own menu. Only the app's bare-backdrop path worked,
because it calls wuss_menu_open itself after wuss_mouse_click returns.

Now a MENU press that lands on a window is not consumed by the dismissal:
the chain still closes, but the press falls through to that window's task,
matching the RISC OS "MENU moves the menu" behaviour and the backdrop.

Adds a wuss test covering the two-window case.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The greeble menu struct is file-scope, shared by every greeble window. The
single row's tick showed whichever window toggled last, and an ADJUST pick
of the row toggled task state but never redrew the still-open menu.

Sync the shared row's flag from task state at open time, capture the chain
handle, and on an ADJUST menu pick retick the live level via
wuss_menu_set_item_ticked. SELECT nulls the now-stale handle.
main.c kept run_wuss, the frame loop, pixel_stress and the chrome
config; the 19 spawn_* callbacks, every menu table and task_handle_event
move to a new apps/wuss/tasks.c behind tasks.h. The shared context struct
(formerly the anonymous file-scope `g`) becomes struct wuss_app_tasks,
declared in the header and defined in tasks.c; run_wuss fills it as before.
The backdrop MENU click now calls tasks_open_launcher and teardown calls
tasks_teardown.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The SDL window now tracks an integer scale (device pixels per screen
pixel) in struct wuss_frontend, starting at 2x. F2 steps it up, Shift-F2
down, clamped to [1, 4], and the window is resized from scr_width/height
* scale so repeated halving can no longer drift the size off the grid.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
6009781 moved Adjust onto the random-palettes toggle. Put base-palette
cycling back on an Adjust click, regenerating from the same seed so the
layout is unchanged. The menu's "Random palettes" row still owns the
toggle.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The generator picked a random prefab on each placement attempt. By the time
a large block was tried, small blocks and filler had already fragmented the
grid, so its bounding box rarely landed wholly empty and the big circuit
shapes were almost never placed.

Sort the prefab indices by footprint (w*h) descending once per regenerate
and split the attempt budget evenly across prefabs, walking them largest
first. Each prefab gets its share of random (row,col) tries while the grid
is still open. The xorshift stream still drives every position, so a given
seed yields the same grid.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The window opened at a hardcoded 160x320 content area, smaller than the
grid the generator can fill. Size it from GREEBLE_MAX_COLS/ROWS * the tile
pixel size instead, so it opens showing the whole grid (256x384) with cols
and rows landing exactly on the caps. Same value is passed as the minimum
content size.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
New prefab/filler sheet: 94 prefab blocks (was 62), 30 standalone filler
stamps (was 27). GREEBLE_MAX_COLS goes 32 -> 48 to match GREEBLE_MAX_ROWS,
so the generator can fill a square grid.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
bmfont_set_flags() with bmfont_FLAG_MONOSPACE forces every glyph to advance
by the font's widest advance width, giving a fixed-pitch layout. Glyph
bitmaps are unchanged: proportional ink sits left-aligned in the wider
fixed cell.

The widest advance (maxadw) is computed once at load. bmfont_measure and
both bmfont_draw advance lookups route through a new bmfont_advance_for()
helper that returns maxadw when the flag is set. maxadw <= gridwidth
always holds, so the glyph blit's right_skip stays non-negative.

bmfont_monospace_test loads a proportional font, sets the flag, and
asserts an N-char string measures N times the single-char width with a
constant per-char step.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Tasks that open a wuss_menu all hand-rolled the same two idioms: a
pre-open loop setting each row's TICKED flag from task state, and a
post-pick check of the ADJUST button to decide whether the stored chain
handle is still live.

Add two helpers to wuss/menu.h:

- wuss_menu_should_keep_open(ev) -- names the "ADJUST keeps the chain
  open, SELECT has already freed it" predicate.
- wuss_menu_open_ticked(task, menu, ticks[], at, out) -- sets each row's
  TICKED bit from a bool array, then opens; the one call that writes back
  to the caller's item array.

Convert greeble, palette and chars to use them. palette's permanent
DASHED flag on the Invert row moves to create time, since
wuss_menu_open_ticked only touches TICKED. minesweeper, text and
swatches keep no handle and carry no ticks, so they are left as-is.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The idle-tick handler invalidated the whole window every second, forcing a
full 480-cell repaint plus per-digit bmfont work just to advance the HUD
timer. Add an MS_TIMER_BOX covering the timer's 3-digit field and invalidate
only that on a tick.

minesweeper_redraw now always paints the cheap HUD strip (its own backdrop,
since the window is wuss_NO_BACKGROUND, plus the counters) and runs the frame
fill, cell loop and win/lose banner only when the dirty region reaches below
the strip.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Draws the string twice with a transparent background: a shadow pass in a
given colour at pos+offset, then the main pass at pos. Factors the relief
effect that the clipping test previously hand-rolled as two bmfont_draw
calls out into the library.

The clipping test's transparent pass now uses it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The help/controls list was a fixed-position column on the left of the
canvas. Move it below the canvas as a centred wrap-flow row, drop the
associated fixed-position and responsive-reflow CSS, and refresh the
about/controls copy.

Add LINK_DEPENDS on shell.html so editing it relinks the Emscripten
target instead of silently reusing the stale shell.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Folds the 44 commits since 9172482 into the Unreleased section: the
bmfont relief/monospace additions, io/dirscan extraction, the wuss demo
task-launcher split and palette-picker rework, the Minesweeper and
greeble tasks, and the window-move/menu-routing fixes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The compact wuss_test used when WUSS_FURNITURE/WUSS_ICONS are off had
not compiled since the backdrop API moved from wuss_colour_t to
wuss_backdrop_t; only the furniture build exercises the other body, so
the breakage went unnoticed.

- wrap the four wuss_window_create bg arguments in wuss_BACKDROP_COLOUR(),
  matching every other call site
- the wheel-scroll case expected the offset to move, but the test's
  chromeless flag set includes wuss_WINDOW_NO_VSCROLL, which
  wuss__scroll_step deliberately suppresses; assert the offset is
  unchanged and say why

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
toggle-action.c and bmfont.c each call a logf_* macro with a fixed
message and no substitutions. -std=c99 -pedantic then warns "ISO C99
requires rest arguments to be used" because the GNU ",##__VA_ARGS__"
comma-swallow is an extension. Pass "%s" plus the string so a variadic
argument is present, and note the constraint in debug.h.

icon/draw.c computed gap_x0 = b->x0 + INSET - PAD and tested
gap_x0 > b->x0, which GCC's -Wstrict-overflow flags as an x+c > x
pattern. INSET (8) always exceeds PAD (2), so the guard was constant
true; drop it and keep only the right-stub check, with a comment on why
that is the only edge that can vanish.

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 @dpt, your pull request is larger than the review limit of 150,000 diff characters

@dpt
dpt merged commit 9838658 into develop Sep 6, 2026
11 checks passed
@dpt
dpt deleted the wuss-sourpuss branch September 6, 2026 16:11
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