Skip to content

Wuss bagpuss - #9

Merged
dpt merged 290 commits into
masterfrom
wuss-bagpuss
Sep 4, 2026
Merged

dpt merged 290 commits into
masterfrom
wuss-bagpuss

Conversation

@dpt

@dpt dpt commented Sep 4, 2026

Copy link
Copy Markdown
Owner
Screenshot 2026-09-04 at 19 23 57

dpt and others added 30 commits August 28, 2026 11:16
The three line rasterizers seeded their Bresenham/Wu error terms from the
endpoints line_clip() had recomputed on the clip boundary. As wuss splits a
dirty rectangle into up to 32 pieces and redraws the same logical content
under a different clip box for each, the same line took a different pixel
path per piece, leaving visible seams at the piece boundaries.

Use line_clip() against the clip box for rejection only, discarding its
recomputed endpoints, and step from the true endpoints instead, leaving the
per-pixel clip in screen_draw_pixel()/screen_blend_pixel() to do the real
clipping. Bound the work with the screen bounds, which -- unlike the clip
box -- never vary between calls: screen_draw_line() and
screen_draw_line_wu_fix8() clip their endpoints against them, and
screen_draw_line_wu_float() clamps its mid-point loop to them, fast-
forwarding the gradient in closed form.

Adds a screen test asserting a line drawn in one go and drawn once per
piece of a partition of the canvas produce identical pixels, plus a check
that nothing is drawn outside the clip box.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
wuss_mouse_click, wuss_mouse_move and wuss_scroll all add the window's
scroll offset before dispatching, so the point a task receives is already in
virtual content space. The curve, ball and launcher tasks each added the
offset a second time via wuss_window_get_scroll, putting hit-tests, drags,
ball spawns and launcher row indices at twice the scroll offset once a
window was scrolled.

wuss_scroll also computed its event point after stepping the scrollbar, so
the point described the content the wheel had just brought under the pointer
rather than the content the pointer was over when it turned. Compute the
point first.

The wuss_EVENT_MOUSE documentation described the point as window-local
content coordinates, which is what invited the extra addition; say that the
scroll offset is already applied.

Adds a wuss test covering all three dispatch paths.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
wuss_idle operates on the window manager, not a single window, so it
belongs alongside the other wuss_t entry points.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Move the event and task types, wuss_task_start and wuss_task_stop out of
window.h into a new public wuss/task.h, and relocate their sources to
libraries/wuss/task/.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Add a size2d_t (width, height) type alongside point_t, plus box_size to
extract one from a box, and use them for wuss_window_create's document
extent, wuss_window_resize's new size and struct wuss_window's stored
document extent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Replace bitmap_t/screen_t's width and height members with a single
size2d_t, and take a size2d_t in bitmap_init, screen_init and
screen_draw_rect. Most screen_draw_rect call sites now pass box_size()
directly instead of subtracting the box's edges by hand.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The content area's right and bottom edges abutted the scrollbars (or the
resize corner) with no dividing line, leaving the frame's interior looking
unfinished.

Reserve a further pixel on each carved axis in wuss__furniture_carve_for --
so window creation, resize and toggle-size all account for it -- and draw
the rule there, in the same colour as the outline. The requested content
box is unaffected: like the rest of the furniture, the rule is added
outside it, not carved out of it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
wuss__furniture_invalidate_for decided the scrollbar strip widths from
the NO_VSCROLL/NO_HSCROLL flags, but wuss__furniture_carve_for still
reserves a strip on both axes for a window with both scrollbars off and
resize on, to hold the resize icon and the interior rules. Growing such
a window left the old icon and rules painted inside the new content
area. Drive the strips off the same carve the layout uses.

Also:

- Floor the scroll sausage's cross-axis inset at 0 when the well is no
  wider than two insets, which a small titlebar font can produce; the
  box used to invert and the sausage vanish.
- Zero-initialise test_task_t in the scroll-offset test, which only set
  mouse_count while test_handle increments the other counters.
- Correct the screen_copy_rect failure comment in window/move.c and
  document muldiv's domain in geom/line.
- Clamp the chars task's glyph count so no byte above CHAR_MAX reaches
  bmfont_draw, which indexes its glyph table off a plain char.

The resize-sliver test summed per-region dirty areas, double-counting
overlaps; it passed by 257 pixels and the extra strips tipped it over.
Measure the true union instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds an eleventh launcher task to the interactive wuss test: the two
bundled composite demo images (resources/composite/A.png and B.png)
composited under a Porter-Duff rule that cycles through all twelve
operators, over an alpha checkerboard so transparency reads as
transparent rather than white.

composite() takes no offset or alpha parameter, so the animation is a
per-frame edit on scratch copies: the destination is restored from a
pristine B and the source's alpha channel is scaled by a triangle ramp
(0 to 255 and back across each rule's turn). Alpha is non-premultiplied,
so only the alpha byte is touched. Pacing is a per-idle frame counter,
as in ball.c; clicking advances the rule and the wheel adjusts the
cycle speed.

The bitmap clone/convert helpers are copied from composite-test.c,
where they are statics rather than library functions.

Widens the launcher window to fit the longer entry name.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adjust-clicking a scroll arrow now steps against the direction the arrow
points, so a single arrow can be worked both ways without moving the
pointer. Toggle-size remains Select-only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ction

Reflows the Doxygen parameter blocks in window.h and wuss.h, and renames
the README's "Wuss" section to "Windowing".
Select/Menu/Adjust were consecutive enumerated values, so a chord such as
Select+Adjust could not be expressed at all. They become flags -- Select
4, Menu 2, Adjust 1, matching the RISC OS button order -- with a
wuss_BUTTON_NONE of zero, and every internal comparison switches from
equality to a bit test.

Where a chord is ambiguous Select wins: a Select+Adjust click on a scroll
arrow scrolls the way the arrow points rather than backwards, and on the
back icon sends the window to the back.

BREAKING CHANGE: wuss_button_t's values have changed, and client code
comparing the reported button for equality must now test with '&' or it
will fail to match a chord.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Matches the per-area layout of the other docs (databases, datastruct,
framebuf, geom, io).
Names the terms the document already leans on: the three coordinate
spaces (screen, window-local, virtual content), content area versus
visible bounds, furniture and its parts, and the RISC OS button
conventions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
doc already caps how large a resize-drag or toggle-size can make a
window; the floor was the fixed WUSS_MIN_CONTENT, so a task whose
content stops making sense below some size had no way to say so.

wuss_window_create gains a min_doc alongside doc, set once at creation.
wuss__min_content clamps it up to WUSS_MIN_CONTENT, so a window can
never be squeezed too small to grab, and down to doc, so a minimum can
never demand a window larger than the document it shows. Pass (0,0) for
the built-in floor.

BREAKING CHANGE: wuss_window_create takes a min_doc argument between doc
and window.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The launcher entry table is now const, so the per-row "running" flag can no
longer live in it. Move it into launcher_task as a fixed-size bool array
indexed by entry, capped at LAUNCHER_MAX_ENTRIES.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Replace inline (point_t) { x, y } and (size2d_t) { w, h } compound
literals across the tree with POINT(x, y) / SIZE2D(w, h) helpers defined
alongside their types.

Also folds in a pre-existing null-pointer guard in bmfont_get_info and
minor alignment tidy-ups that were already in the working tree.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Resize-drag recomputed width/height as pointer minus content origin on
every move, discarding where within the resize icon the mouse-down
click landed. The window's corner snapped to the raw pointer position
on the first move, jumping if the click wasn't at the icon's exact
corner pixel.

Store the pointer's offset from the content box's bottom-right corner
at mouse-down (mirroring how a titlebar drag already stores its
content-relative offset), and subtract it on each move, so the grabbed
point stays under the pointer through the drag.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TWwV3bFEi9z5MZc5wqiJzZ
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Add a work-area icon subsystem: static labels and clickable bevelled
buttons that Wuss draws inside a window's content area and hit-tests
before the content task sees a click. Icon boxes are in virtual document
space so they scroll with the content; button clicks and hovers reach the
task as wuss_EVENT_ICON, while labels and hidden or disabled icons fall
through as wuss_EVENT_MOUSE.

Includes the public wuss/icon.h API, per-window icon storage, drawing,
mouse routing integration, and an icons test task.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The icons task drew its backdrop grid and x/y axis rulers pinned to the
window edge, so Wuss's scroll blit displaced the painted pixels without
repainting them, smearing the labels. Anchor all task drawing to document
space so it scrolls rigidly with the content, as the blit assumes; add a
bmfont-drawn coordinate ruler along the document x=0 and y=0 lines.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Loops wuss_icon_create over a spec array with all-or-nothing rollback:
on the first failure any icons already created by the call are destroyed
and no handles are written. Converts the icons test task to use it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds a BORDER margin around the bitmap, a min-doc resize floor, and a
pink background so the border and transparent pixels are visible.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
image_create took the repo root and built the PNG path itself; it now
takes the full filename, with path assembly moved to the spawn_image
caller in wuss-test.c.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
New entry point taking a content size instead of a box: wuss packs the
window (furniture included) into the first free screen region via a
packer_t it owns, top-left order, tracking occupied area across calls so
successive auto-placed windows tile. Cascades from the previous
placement when no region fits.

The slot is released back to the pool on close, and on the first
wuss_window_move / wuss_window_resize (a titlebar drag counts as a
move), after which wuss stops tracking the window's position.

Adds packer_release() to geom/packer as the inverse of packer_place_*;
released areas are not coalesced, which is sufficient for whole-window
placement.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Switch every test task from a hardcoded BOX_POS_SIZE origin to
wuss_window_create_placed, passing just the content size. Windows now
tile from the top-left instead of landing at fixed coordinates.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
packer_place_by now searches for the box plus a configurable gutter strip
along its two inner edges, so boxes placed by location never end up flush.
The gutter defaults to 0, leaving every existing caller unchanged. The
returned position is still the un-inflated box.

wuss_window_create_placed sets a fixed WUSS_PLACE_GUTTER and stores the
gutter-inflated region in wuss_window::packed so the whole reservation,
not just the footprint, is handed back on close/move.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Each wireframe model in the sofa test task now draws a small white square
at every projected vertex, on top of the edges. Also adds a Cobra Mk III
model alongside the existing ship.

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

The anti-aliased fixed-point line rasteriser had three undefined behaviours,
all reachable with long or off-screen endpoints:

- FIX16_ONE * dy_f8 (and grad_f16 * dx) overflowed 32-bit int once a line
  exceeded ~32k fixed-point units; compute those in long long.
- INT_TO_FIX8(iy) left-shifted pixel Y coordinates that can be negative;
  use a multiply instead.
- the yf recurrence left-shifted a possibly-negative fix8 value before
  shifting back; form the fix16 sum by multiply, then arithmetic-shift down.

Adds test_wu_fix8_extreme_coords covering large and off-screen endpoints,
and fixes the test draw() helper's own negative left shift.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
dpt and others added 21 commits September 4, 2026 14:47
An ADJUST pick on a menu row delivers MENU_SELECT but, RISC OS style,
leaves the chain open rather than tearing it down. A task's tick,
however, is baked into the row's icon selected-state once at
wuss_menu_open time (wuss_menu_item_t.flags is read only when the chain
is (re)built) -- so a task that only edits its own item array on
MENU_SELECT never sees the change while that same chain stays open.

Add wuss_menu_set_ticked(handle, menu, index): finds the open level
whose menu matches, ticks index and unticks every other row's icon
directly, invalidating the ones that changed.

chars.c now keeps the handle from wuss_menu_open and calls this after
an ADJUST re-pick switches font, so the picker's tick tracks the
current font even with the menu held open.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
wuss_create's fonts[] is now an array of wuss_font_desc_t (font handle,
wuss_font_class_t, and a borrowed leafname), replacing the old bare
bmfont_t* array. wuss_FONT_CLASS_SYSTEM marks a slot as chrome/decoration
only (e.g. the symbol font menu ticks and submenu arrows are drawn from),
as opposed to a normal user-selectable text font.

wuss_fontmenu_create takes the owning wuss_t and skips any SYSTEM-class
slot whose name matches a scanned leafname, so a font picker no longer
offers chrome fonts as text choices. Matching is by name, not by loading
the bmfont_t, preserving the menu's lazy load-on-select design.

Adds wuss_get_font_class_n/wuss_get_font_name_n getters and updates all
wuss_create/wuss_fontmenu_create call sites (apps/wuss demo, chars/text
test tasks, wuss-test.c) accordingly, plus a test covering SYSTEM-class
exclusion.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Byproduct of recent font-slot tagging/picker work on wuss.
redraw_window drew a window's chrome (titlebar, scrollbars, outline) against
the raw dirty/full rect with no occlusion clipping at all -- only its content
went through wuss__clip_to_visible. A window redrawing its own dirty region
while partly covered by a higher window would repaint its furniture straight
back over that higher window's pixels.

Also exclude wuss_NO_BACKGROUND windows' content boxes from the desktop
backdrop's pre-fill, so a task that skips or only partially repaints its
content sees whatever was already on screen instead of a flash of backdrop
colour.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Replace hand-picked spec index arithmetic (g, m, g+N offsets into one flat
array) with a layout cursor threaded through per-group helper functions
(icons_add_intro/buttons/radios/bitmaps/pattern/menu), each appending its own
specs and advancing a document y. A named enum reserves the exact spec count
per group so the total can't silently drift from the array bound.

Expand the demo to cover every wuss_icon_type_t: bordered FRAME groups for
buttons (normal/default/disabled), radios+option, bitmaps, and a new PATTERN
swatch group, plus a SWATCH and a DISABLED entry added to the menu strip.
Window and document grown to fit; the scrolled-away proof button is kept.

Fix two -Wcomment warnings from *ptr inside doc comments.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
create.c, icon/draw.c and bmtext/draw.c queried font_width via
bmfont_get_info but never used it, needing a NOT_USED() escape hatch.
Pass NULL for outputs callers don't need instead.
Replace open-coded floor/ceiling ifs and min/max ternaries across
wuss, framebuf, geom, datastruct, text and utils with the MIN/MAX/CLAMP
macros from base/utils.h. Adds the include where a file didn't already
pull it in. Left the vendored libraries/fortify/fortify.c untouched.
16x16 grid instead of 32x8, cell shows index in system font stacked
above the glyph. Window now resizable/scrollable and placed with a
64,64 offset instead of pinned at origin.
…hain

wuss__menu_flash_finish's MENU_SELECT delivery can re-enter and close
the whole chain even for a keep_open (ADJUST) pick, e.g. when the
owner's handler calls wuss_menu_close(). The old guard only bailed out
for the non-keep_open case, which frees the chain itself; the
re-entrant client-triggered free was not covered, so a fast ADJUST
re-pick could dereference the freed self->icons[index] afterwards.

Replace the prev_keep_open check with a liveness check against the
live chain (wuss__menu_node_for), matching the existing pattern used
by the IDLE flash-step handler.
wuss_task_destroy freed the task without checking whether the live
menu chain (or an in-flight pick flash) still recorded it as owner.
A later row pick or flash completion would then call wuss__deliver on
the freed task through that dangling owner pointer.

wuss_destroy already closes menu_chain before freeing tasks during
whole-manager teardown; add the equivalent check to the single-task
path, closing the chain first when this task is its owner.
…own early

wuss__menu_close_from froze every chain node unconditionally without
finishing an in-flight pick flash first, so a click outside the menu
(or another wuss_menu_open/close) landing during the highlight flash
silently dropped the MENU_SELECT the flash was standing in for.

Deliver the flash's captured MENU_SELECT directly here instead of
going through wuss__menu_flash_finish, whose own chain teardown would
race the caller's (the chain is already unlinked from wuss->menu_chain
by the time close_from runs).
…a menu row

wuss__menu_open_window moved the window to the submenu anchor before
checking wuss_window_set_hidden's PRE_SHOW veto result. On veto only
the menu node was freed; the window itself was left permanently
repositioned at the anchor even though it never became visible, so
its next legitimate show rendered in the wrong place.

Capture the window's prior position first and restore it on veto.
wuss_destroy freed each leftover task's block directly, skipping the
QUIT event. Per the task_data-ownership contract in include/wuss/task.h
a task's client-owned allocations are freed only from its QUIT
handler, so a still-registered task (e.g. a menu-spawned demo window
not yet closed) leaked its task_data on whole-manager teardown.

Deliver QUIT before freeing each task block; windows are already gone
by this point in the sweep, so QUIT handlers see the same view they
would from wuss_task_destroy. Add wuss__task_from_link alongside the
existing wuss__window_from_link, naming the cast rather than doing it
inline.
…happen

extract_advance_widths's end-of-row bounds check ran after the row's
pixel reads, including the per-character refill inside the inner loop
still marked with a TODO. A font PNG whose width isn't an exact
multiple of CHARS_PER_ROW produces a truncating gridwidth, so the
inner loop can iterate past the row's -- or the buffer's -- last valid
word before any check catches it.

Add an explicit end-of-buffer pointer and check it at both read sites,
before the read rather than after.
… in release

scr->span is NULL when screen_init's pixelfmt_t has no span-registry
entry; the assert guarding the fill dispatch is compiled out under
NDEBUG, so a release build crashed on the NULL dereference instead of
getting the old hand-rolled switch's safe do-nothing default.

screen_fill_rect calls screen_fill_hline per row, so this covers it
too.
fonts[3]/descs[3] and the font-loading loop bound were three separate
hardcoded literal 3s, unrelated to wuss_MAX_FONTS (4) and to each
other; wuss_create's nfonts-vs-wuss_MAX_FONTS validation runs only
after both arrays are already fully populated in the caller, so it
can't protect against them.

Introduce WUSS_MAIN_NFONTS, used for both array sizes and the loop
bound, with a compile-time check against wuss_MAX_FONTS.
resources/bmfonts/digits.png was renamed/split into Digits-Regular.png
and Digits-Bold.png; the doc's example image still pointed at the old
deleted path.
The header comment still named the old libraries/wuss/get-font.c path
with a generic blurb; other files moved into core/ in the same diff
got their headers updated, this one didn't.
Pages workflow only triggered on master; add develop so the emscripten demo deploys on develop branch pushes as well.
Covers the Emscripten/wasm build and Pages deploy, shared fontmenu/colourmenu components, menu/icon polish, and the wuss_create font-array and fontmenu allocator breaking changes, plus the QUIT-delivery, menu UAF, bmfont bounds and screen_fill_hline fixes landed since.

@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 300 files, and this pull request has 446

dpt and others added 6 commits September 4, 2026 19:42
SharedCLibrary (-mlibscl) has no opendir/readdir/closedir, so bmfont_enumerate() failed to link on RISC OS. Add a TARGET_RISCOS branch using OSLib's xosgbpb_dir_entries instead, and actually define TARGET_RISCOS as a compiler macro (it was only a CMake-scope variable before, so the #ifdef was dead code).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The fixture dir gained Symbols.png but the test's hardcoded MAXFONTS
stayed at 8, so bmfont_enumerate_test failed. Symbols can't just join
bmfonts[] though: the clipping/layout tests draw Latin lorem_ipsum
through every font in that list, and Symbols' small glyph table
overflows on ASCII input. Split into bmfonts[] (8, Latin-drawing
tests) and a separate bmfonts_enum_extra[] checked only by the
enumerate test.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
git's index had daydream/gliderrider/henry/tiny.png in lowercase while
the working files on disk are Daydream/GliderRider/Henry/Tiny.png.
macOS's case-insensitive filesystem hid the mismatch locally, but a
case-sensitive Linux checkout (e.g. the Ubuntu release build) sees
the lowercase names, bmfont_enumerate's names don't match wuss-test's
expected font names, and the fontmenu check fails.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
wuss_test still referenced the fixture as "tiny.png"; after the
bmfonts case-fix commit the file is Tiny.png, so on case-sensitive
filesystems bmfont_create fails and left 'font' uninitialised. The
FlashFail unwind path then called bmfont_destroy on that garbage
pointer -- segfault on Linux CI, masked on macOS's case-insensitive
filesystem. Also init font = NULL at declaration so any future
load failure hits a safe no-op free instead of relying on every
call site remembering to set it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
MSVC has no dirent.h. Enumerate via FindFirstFileA/FindNextFileA instead of opendir/readdir on that toolchain; POSIX and RISC OS paths unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
wuss_BACKDROP_COLOUR already casts its compound literal to wuss_backdrop_t; casting the macro result again gave MSVC C2440 (cannot convert type to itself). GCC/Clang tolerated it, MSVC did not.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@dpt
dpt merged commit 41734c7 into master Sep 4, 2026
11 checks passed
@dpt
dpt deleted the wuss-bagpuss branch September 4, 2026 19: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