diff --git a/CHANGELOG.md b/CHANGELOG.md index 7609133..c47943a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -100,7 +100,7 @@ _Unreleased_ until one is cut. a live icon per element. - `wuss_window_invalidate_extent()` — marks a window's whole virtual document extent dirty (`window->doc`), not just the visible content - rectangle `wuss_window_invalidate_all()` covers, so a change touching the + rectangle `wuss_window_invalidate_visible()` covers, so a change touching the whole document repaints correctly at any scroll position. - Clicking in a scrollbar well (not on the sausage) now pages the content one visible extent towards the click, keeping one `WUSS_SCROLL_STEP` of @@ -232,9 +232,42 @@ _Unreleased_ until one is cut. pool. Released areas are not coalesced. - `POINT(x, y)` and `SIZE2D(w, h)` compound-literal macros in `geom/point.h` and `geom/size.h`. +- 1bpp and 2bpp paletted pixel formats (`pixelfmt_p1`, `pixelfmt_p2`), + packed MSB-first (bit 7 / bits 7..6 are the leftmost pixel) to match PNG + and the existing pattern bit order. `span_p1` / `span_p2` (`fill` + + `blendconst`) are registered so `bitmap_init()` hands out a valid span; + `screen_set_pixel` / `screen_blend_pixel` / `screen_fill_pattern` / + `screen_copy_bitmap` gain p1 and p2 cases; `bitmap_convert()` grows + `p1 -> bgrx8888` and `p2 -> bgrx8888` paths for the SDL frontend's + per-frame display conversion. `pixelmap` already handled both generically. +- `bmfont_draw()` renders to a `pixelfmt_p1` or `pixelfmt_p2` screen. A + single `bmfont_p1_plot_row` / `bmfont_p2_plot_row` does the per-pixel + MSB-first bit write for both 1- and 2-byte glyph rows and both opaque + and transparent backgrounds. +- `pixelfmt_paletted_nentries()` — palette entry count for any paletted + format, replacing the `(scr->format == pixelfmt_p4) ? 16 : 0` hack at + every `colour_to_pixel()` call site (which passed a broken `nentries=0` + for any non-p4 paletted screen). +- The `wuss` SDL demo accepts `-d`/`--depth 1` and `--depth 2`, selecting a + `pixelfmt_p1` (stride `(width + 7) / 8`) or `pixelfmt_p2` (stride + `(width + 3) / 4`) framebuffer alongside the existing 4 and 32. +- The `wuss` curve task draws the active control points' convex hull + (Andrew's monotone chain) as a closed light-grey polyline before the + curve and blobs. +- The `wuss` gradient task cycles its ordered-dither matrix through 2x2, + 4x4 and 8x8 on SELECT / ADJUST clicks; each matrix cell maps to a fixed + -8..+8 offset so a larger matrix gives a finer pattern, not a noisier + one (4x4 output unchanged). The current size is drawn top-left. +- `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/`. ### Changed +- **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; + use `wuss_window_invalidate_extent()` for the full document. - The `wuss` demo task launcher is split out of `apps/wuss/main.c` into `apps/wuss/tasks.c` behind `tasks.h`: the 19 `spawn_*` callbacks, every menu table and `task_handle_event` move over, and the shared file-scope @@ -368,6 +401,14 @@ _Unreleased_ until one is cut. leaf now delivers `wuss_EVENT_MENU_SELECT` (with `window == NULL`) to that task — `data.menu_select` carries `{ const struct wuss_menu *menu; int index; wuss_button_t button; }`. `wuss_menu_select_fn_t` is removed. +- `screen_set_pixel_8()` is renamed `screen_set_pixel_p8()`, matching the + `_p1` / `_p2` / `_p4` naming of the other paletted set-pixel helpers. +- The greeble prefab tables (`greeble_prefab_cells` / `greeble_prefab`) are + regenerated from the current sheet; `GREEBLE_NPREFAB` 103 -> 137. +- The `wuss` image task resizes the window frame on load (via + `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. ### Fixed @@ -445,9 +486,9 @@ _Unreleased_ until one is cut. `NDEBUG`). - `screen_fill_hline()` (and so `screen_fill_rect()`, which calls it per row) is a no-op in release builds for a `screen_t` whose pixel format has - no span-registry entry, instead of dereferencing the NULL `scr->span` — - the guarding `assert` was compiled out under `NDEBUG` so this previously - crashed. + no span-registry entry — or a span with no `fill` — instead of + dereferencing the NULL `scr->span` / `scr->span->fill`; the guarding + `assert` was compiled out under `NDEBUG` so this previously crashed. - `wuss_create()`'s font-slot arrays and loop bound in the interactive demo are now sized from one named, compile-time-checked constant (`WUSS_MAIN_NFONTS`, checked against `wuss_MAX_FONTS`) instead of three @@ -484,3 +525,71 @@ _Unreleased_ until one is cut. endpoints. - `screen_draw_ninepatch()` clamps its corner cells so they no longer overlap and double-draw when the destination box is smaller than the source corners. +- `spanregistry_get()`'s one-slot cache seeded `lastformat` to zero, which + equals `pixelfmt_p1` (the first enum member), so the first p1 lookup + short-circuited to a NULL span and tripped the `screen_fill_hline()` + assert. Seeded with `pixelfmt_unknown` and the fast path guarded on a + non-NULL span. +- The `NO_RESIZE_BLIT` branch of `wuss_window_resize()` invalidated the + dirty region but never dropped the cached furniture layout, so + `wuss__furniture_draw` kept painting titlebar/carve/outline rects at the + previous frame's width. It now calls `wuss__chrome_invalidate_layout`, + matching the blit branch's `wuss__chrome_repaint`. +- `bitmap_fill_pattern()` negated the `box_intersection()` result (which is + non-zero when the intersection is *empty*), so any non-NULL `area` that + overlapped the bitmap returned early and painted nothing — only + `area == NULL` worked. Drop the negation to match `screen_fill_pattern()`. +- `bitmap__rle_decode_row()` ran a bare `for (;;)` that only stopped on an + EOL opcode, so a truncated or corrupt RLE blob read opcodes past the + allocation. It now takes an `end` limit (like every other row walker), + stops at it, and has a reserved opcode bail to `end` (release builds + previously did a bare `break`, desynchronising the caller's row cursor). + `bitmap_decompress()` reads `data_len` from the blob header to form + `end`. +- `bmfont_draw()` formed `gid = c - ' '` from a raw signed `char` and + indexed `adw[]` and the glyph bitmaps with no bounds check, so a tab, a + DEL or any byte `>= 0x80` produced a negative or oversized index and an + out-of-bounds read/write. Both draw loops now filter `c < ' '` and + `gid >= totalchars`, matching `bmfont_measure()`. The + fully-vertically-clipped case (`clippedcharheight <= 0`) also gets a + release-build bail rather than entering `drawfn` with a negative height + and looping ~`INT_MAX` times. +- `wuss_destroy()`'s task sweep delivered `wuss_EVENT_QUIT` and freed each + task node without first setting `wuss_TASK__REAPING`. A QUIT handler that + closed its last autoclose window hit `wuss_window_close`'s self-destruct + path, which delivered a second QUIT and freed the node — which the sweep + then freed again. `wuss_TASK__REAPING` is now set before QUIT in the + sweep, and `wuss_task_destroy()` is a no-op when the task is already + being reaped. +- `bitmap_compress()`'s worst-case row estimate assumed one control byte + per 128 pixels, but the encoder emits a 1-byte literal control for every + isolated non-repeatable pixel. A legal y8 image of the form singleton, + pair, singleton, pair, ... needs ~1.33*w bytes/row, overflowing the + allocation and returning `result_BUFFER_OVERFLOW` on valid input. The + budget is now `w * (bpp + 1)` plus the literal-long split control and an + EOL. +- `bmconv_p4_to_bgrx8888()` ran `x < src->size.w / 8` and expanded 8 pixels + per iteration, so the final `w % 8` pixels of each row were never written + — the output had uninitialised garbage columns for any width not a + multiple of 8 — and it ignored row padding in `src->rowbytes`. Switched + to a per-pixel loop honouring both, matching the p1 and p2 converters. +- `wuss_window_set_hidden()` marked a window hidden but left + `wuss->furniture.dragging`, `wuss->pressed_icon` and `wuss->hover_icon` + pointing into it, so hiding a window mid-drag from an event handler meant + the next `wuss_mouse_move()` still drove `wuss_window_move()` on an + off-screen window every pointer report. It now clears the same pointer + state `wuss_window_close()` does. +- `scroll_strip()` always reserved `size + WUSS_DIVIDER_PX` at the far end + for the resize corner, but `scroll_strip_hit()` only reserves it when + something owns that corner. With a horizontal scrollbar and `NO_VSCROLL` + and `NO_RESIZE` the drawn strip stopped short while the hit box ran to + the visible edge: the rightmost band showed stale pixels yet hit-tested + as `HSCROLL_WELL` and started a scrollbar drag over empty chrome. + `scroll_strip()` gets the same far-end rule. +- `wuss__furniture_hit_test()`'s `nearest_edge_region()` fallback claimed + `wuss_FURNITURE_RESIZE` for a whole bottom or right carve band whenever a + resize icon was present, and a scroll well for an edge with no scrollbar + strip. A click on the bare carve band left of the corner icon then + 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`. diff --git a/CLAUDE.md b/CLAUDE.md index 7a781ae..feeb7c9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -82,14 +82,14 @@ New source/header files must be added by hand to the relevant `set(..._SOURCES . **Test structure.** There is a single test binary (`DPTLibTest`, from `apps/test/main.c`) that dispatches to per-module `_test(resources)` functions declared centrally in `include/test/all-tests.h`. Tests return `result_TEST_PASSED`/`result_TEST_FAILED` (`result_BASE_TEST`). -**Notable sub-libraries with their own docs** (`docs/*.md`): `io/stream` (chainable byte-stream sources/transforms, PackBits and move-to-front compression), `framebuf/bmfont` (proportional bitmap fonts loaded from specially-formatted PNGs), `framebuf/composite` (Porter-Duff compositing on RGBA/BGRA bitmaps), `wuss` (minimal window manager: creation, z-ordering, mouse routing, dirty-region redraw, client-delegated content). +**Notable sub-libraries with their own docs** (`docs/*.md`): `io/stream` (chainable byte-stream sources/transforms, PackBits and move-to-front compression), `framebuf/bmfont` (proportional bitmap fonts loaded from specially-formatted PNGs), `framebuf/composite` (Porter-Duff compositing on RGBA/BGRA bitmaps), `framebuf/pixelmap` (cached deep-to-paletted colour conversion tables for blit inner loops), `wuss` (minimal window manager: creation, z-ordering, mouse routing, dirty-region redraw, client-delegated content). **RISC OS.** `TARGET_RISCOS` (set by an external toolchain file) switches CMake to fetch/build zlib+libpng from source, link OSLib from `$GCCSDK_INSTALL_ENV`, and applies `riscos_set_flags()`. `pngusr-ro.dfa`/`pngusr-rw.dfa` select libpng's build config depending on `DPTLIB_IMAGES_READ_ONLY`. ## Code style - C99, `-Wall -Wextra -pedantic`. Allman brace style, 2-space indentation, tabs converted to spaces (see `.astylerc`/`.editorconfig` — astyle is the formatter of record). -- Declare variables at the top of each scope (pre-C99 style is followed throughout, even though the standard is C99), ordered by first use. +- Declare variables at the top of each scope (pre-C99 style is followed throughout, even though the standard is C99), ordered by first use. Exception: a `result_t` status local (`rc`, `err`, `crc`, ...) is always declared first in its block, ahead of the use-ordered locals. - File header comment format: `/* filename.c -- one-line description */`. - Section breaks within files use `/* ----- ... ----- */` rule comments. - Public API docs use Doxygen (`\file`, `\param`, `\return`); a `Doxyfile` exists for generating them. diff --git a/CMakeLists.txt b/CMakeLists.txt index be63626..2c54a5b 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -86,8 +86,11 @@ set(PUBLIC_HEADERS include/framebuf/palettes.h include/framebuf/pattern.h include/framebuf/pixelfmt.h + include/framebuf/pixelmap.h include/framebuf/screen.h include/framebuf/span-bgrx8888.h + include/framebuf/span-p1.h + include/framebuf/span-p2.h include/framebuf/span-p4.h include/framebuf/span-registry.h include/framebuf/span-rgba8888.h @@ -101,6 +104,7 @@ set(PUBLIC_HEADERS include/geom/point.h include/geom/size.h include/io/dirscan.h + include/io/namelist.h include/io/path.h include/io/stream-mem.h include/io/stream-mtfcomp.h @@ -119,6 +123,7 @@ set(PUBLIC_HEADERS include/utils/maths.h include/utils/pack.h include/utils/primes.h + include/utils/rng.h include/wuss/task.h include/wuss/window.h include/wuss/wuss.h) @@ -133,6 +138,8 @@ endif() if(WUSS_COMPONENTS) list(APPEND PUBLIC_HEADERS include/wuss/component/fontmenu.h) 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) endif() # The public headers must be set as properties of the library, not as @@ -245,8 +252,10 @@ set(DATASTRUCT_SOURCES set(FRAMEBUF_SOURCES libraries/framebuf/bitmap/bitmap.c + libraries/framebuf/bitmap/compress.c libraries/framebuf/bitmap/fill-pattern.c libraries/framebuf/bitmap/load.c + libraries/framebuf/bitmap/rle.h libraries/framebuf/bitmap/save.c libraries/framebuf/bmfont/bmfont.c libraries/framebuf/bmfont/enumerate.c @@ -256,7 +265,13 @@ set(FRAMEBUF_SOURCES libraries/framebuf/palettes/palettes.c libraries/framebuf/pattern/pattern.c libraries/framebuf/pixelfmt/log2bpp.c + libraries/framebuf/pixelfmt/paletted-nentries.c + libraries/framebuf/pixelmap/build.c + libraries/framebuf/pixelmap/get.c + libraries/framebuf/pixelmap/impl.h libraries/framebuf/screen/screen.c + libraries/framebuf/screen/screen-copy-bitmap-rle.c + libraries/framebuf/screen/screen-copy-bitmap-rle.h libraries/framebuf/screen/screen-copy-ninepatch.c libraries/framebuf/screen/screen-copy-rect.c libraries/framebuf/screen/screen-draw.c @@ -268,6 +283,8 @@ set(FRAMEBUF_SOURCES libraries/framebuf/span/all8888.c libraries/framebuf/span/all8888.h libraries/framebuf/span/bgrx8888.c + libraries/framebuf/span/p1.c + libraries/framebuf/span/p2.c libraries/framebuf/span/p4.c libraries/framebuf/span/rgbx8888.c libraries/framebuf/span/xbgr8888.c) @@ -295,6 +312,7 @@ set(GEOM_SOURCES set(IO_SOURCES libraries/io/dirscan/dirscan.c + libraries/io/namelist/namelist.c libraries/io/path/path.c libraries/io/stream/stream-mem.c libraries/io/stream/stream-mtfcomp.c @@ -368,6 +386,7 @@ 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/core/task/create.c libraries/wuss/core/task/destroy.c libraries/wuss/core/task/set-autoclose.c @@ -397,6 +416,7 @@ set(WUSS_FURNITURE_SOURCES libraries/wuss/furniture/draw.c libraries/wuss/furniture/hit-test.c libraries/wuss/furniture/invalidate.c + libraries/wuss/furniture/layout.c libraries/wuss/furniture/ops.c libraries/wuss/furniture/resize-box.c libraries/wuss/furniture/scroll-action.c @@ -421,6 +441,7 @@ set(WUSS_ICON_SOURCES libraries/wuss/icon/hit-test.c libraries/wuss/icon/invalidate.c libraries/wuss/icon/plot.c + libraries/wuss/icon/registry.c libraries/wuss/icon/screen-box.c libraries/wuss/icon/set-hidden.c libraries/wuss/icon/set-hover.c @@ -435,7 +456,9 @@ set(WUSS_MENU_SOURCES set(WUSS_COMPONENT_SOURCES libraries/wuss/component/fontmenu.c - libraries/wuss/component/colourmenu.c) + libraries/wuss/component/colourmenu.c + libraries/wuss/component/info.c + libraries/wuss/component/proginfo.c) set(WUSS_SOURCES ${WUSS_CORE_SOURCES}) if(WUSS_FURNITURE) @@ -650,9 +673,11 @@ if(BUILD_TESTS) libraries/datastruct/hash/test/hash-test.c libraries/datastruct/list/test/list-test.c libraries/datastruct/ntree/test/ntree-test.c + libraries/framebuf/bitmap/test/rle-test.c libraries/framebuf/bmfont/test/bmfont-test.c libraries/framebuf/composite/test/composite-test.c libraries/framebuf/curve/test/curve-test.c + libraries/framebuf/pixelmap/test/pixelmap-test.c libraries/framebuf/screen/test/screen-test.c libraries/geom/box/test/box-test.c libraries/geom/layout/test/layout-test.c @@ -731,6 +756,7 @@ if(BUILD_APPS) libraries/wuss/test/tasks/minesweeper.c libraries/wuss/test/tasks/palette.c libraries/wuss/test/tasks/porter-duff.c + libraries/wuss/test/tasks/saturn.c libraries/wuss/test/tasks/sofa.c libraries/wuss/test/tasks/swatches.c libraries/wuss/test/tasks/text.c) diff --git a/apps/test/main.c b/apps/test/main.c index aaf9f21..8438fc2 100644 --- a/apps/test/main.c +++ b/apps/test/main.c @@ -53,7 +53,9 @@ static const test_t tests[] = { "bmfont", bmfont_test }, { "composite", composite_test }, + { "rle", bitmap_rle_test }, { "curve", curve_test }, + { "pixelmap", pixelmap_test }, { "screen", screen_test }, { "box", box_test }, diff --git a/apps/wuss/frontend-riscos.c b/apps/wuss/frontend-riscos.c index 7b225bd..f783daa 100644 --- a/apps/wuss/frontend-riscos.c +++ b/apps/wuss/frontend-riscos.c @@ -88,6 +88,8 @@ result_t wuss_frontend_open(int width, int height, const colour_t *palette, int npalette, + int depth, + int scale, void **pixels, int *rowbytes, pixelfmt_t *fmt, @@ -99,6 +101,11 @@ result_t wuss_frontend_open(int width, _kernel_oserror *err; char msg[160]; /* deferred so it prints after the mode restore */ + /* this backend is locked to the 16-colour Wimp mode with a fixed 1:1 + * window; -depth and -scale only make sense for the SDL backend */ + NOT_USED(depth); + NOT_USED(scale); + msg[0] = '\0'; fe = calloc(1, sizeof(*fe)); diff --git a/apps/wuss/frontend-sdl.c b/apps/wuss/frontend-sdl.c index c6ea89b..77f1d19 100644 --- a/apps/wuss/frontend-sdl.c +++ b/apps/wuss/frontend-sdl.c @@ -22,13 +22,16 @@ #include "frontend.h" -/* Screen pixel format for the demo: 1 = 32bpp pixelfmt_bgrx8888 (feeds SDL - * directly, no per-frame conversion); 0 = pixelfmt_p4 paletted (exercises - * screen_copy_rect's nibble-packed blit path instead). */ -#define WUSS_SDL_32BPP 0 +/* 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. */ /* Integer window zoom: the fixed Wuss screen is drawn at this many device - * pixels per screen pixel. F2 steps it up, Shift-F2 down, clamped to + * pixels per screen pixel. The initial value comes from -scale (0 => use the + * default here); F2 steps it up, Shift-F2 down, clamped to * [WUSS_SDL_MIN_SCALE, WUSS_SDL_MAX_SCALE]. */ #define WUSS_SDL_DEFAULT_SCALE 2 #define WUSS_SDL_MIN_SCALE 1 @@ -44,6 +47,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) */ void *pixels; /* the private framebuffer handed to the caller */ }; @@ -83,6 +87,8 @@ result_t wuss_frontend_open(int width, int height, const colour_t *palette, int npalette, + int depth, + int scale, void **pixels, int *rowbytes, pixelfmt_t *fmt, @@ -94,11 +100,20 @@ result_t wuss_frontend_open(int width, NOT_USED(palette); NOT_USED(npalette); -#if WUSS_SDL_32BPP - stride = width * 4; /* pixelfmt_bgrx8888: 4 bytes/pixel */ -#else - stride = width / 2; /* pixelfmt_p4: 2 pixels/byte */ -#endif + if (depth != 1 && depth != 2 && depth != 4 && depth != 32) + { + fprintf(stderr, "Error: unsupported depth %d (want 1, 2, 4 or 32)\n", depth); + return result_BAD_ARG; + } + + if (scale <= 0) + 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 == 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 */ fe = calloc(1, sizeof(*fe)); if (fe == NULL) @@ -106,7 +121,8 @@ result_t wuss_frontend_open(int width, fe->scr_width = width; fe->scr_height = height; - fe->scale = WUSS_SDL_DEFAULT_SCALE; + fe->scale = scale; + fe->depth = depth; fe->pixels = malloc((size_t) stride * height); if (fe->pixels == NULL) @@ -148,11 +164,10 @@ result_t wuss_frontend_open(int width, /* keep pixels crisp when F2 scales the window up */ SDL_SetTextureScaleMode(fe->texture, SDL_SCALEMODE_NEAREST); -#if WUSS_SDL_32BPP - *fmt = pixelfmt_bgrx8888; -#else - *fmt = pixelfmt_p4; -#endif + *fmt = (depth == 32) ? pixelfmt_bgrx8888 + : (depth == 4) ? pixelfmt_p4 + : (depth == 2) ? pixelfmt_p2 + : pixelfmt_p1; *pixels = fe->pixels; *rowbytes = stride; @@ -274,22 +289,25 @@ bool wuss_frontend_poll(wuss_frontend_t *fe, wuss_input_t *event) void wuss_frontend_present(wuss_frontend_t *fe, const bitmap_t *bm) { -#if WUSS_SDL_32BPP - SDL_UpdateTexture(fe->texture, NULL, bm->base, bm->rowbytes); -#else - bitmap_t *disp; - - /* 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 (fe->depth == 32) { - SDL_UpdateTexture(fe->texture, NULL, disp->base, disp->rowbytes); - free(disp->base); - free(disp); + SDL_UpdateTexture(fe->texture, NULL, bm->base, bm->rowbytes); + } + else + { + bitmap_t *disp; + + /* 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) + { + SDL_UpdateTexture(fe->texture, NULL, disp->base, disp->rowbytes); + free(disp->base); + free(disp); + } } -#endif 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 8729da1..4421d2e 100644 --- a/apps/wuss/frontend.h +++ b/apps/wuss/frontend.h @@ -52,6 +52,16 @@ wuss_input_t; * `palette` / `npalette` are the initial system palette: a backend that owns * the physical palette (RISC OS 16-colour mode) programmes it here. * + * `depth` is the framebuffer's bits per pixel: 32 for a direct-colour + * surface (no per-frame conversion), 4 for a paletted one (exercises the + * nibble-packed blit path). Those are the only values the SDL backend + * accepts today; a backend with a fixed format (RISC OS 16-colour mode) + * ignores it. + * + * `scale` is the initial integer window zoom (device pixels per screen + * pixel) for backends with a resizable window; <= 0 means "backend default". + * The RISC OS backend ignores it. + * * On return `*pixels` points at storage for width*height pixels at * `*rowbytes` stride, and `*fmt` is the pixel format that storage expects. * The caller wraps this in a bitmap_t and hands it to wuss. The backend may @@ -63,6 +73,8 @@ result_t wuss_frontend_open(int width, int height, const colour_t *palette, int npalette, + int depth, + int scale, void **pixels, int *rowbytes, pixelfmt_t *fmt, diff --git a/apps/wuss/main.c b/apps/wuss/main.c index 235f6ba..5a82d8c 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -4,6 +4,12 @@ #include #include +/* RISC OS's GCCSDK newlib has getopt() but not the GNU getopt_long() + * extension, so parse_args() keeps a hand-rolled fallback there. */ +#ifndef __riscos +#include +#endif + #ifdef __EMSCRIPTEN__ #include #endif @@ -33,45 +39,61 @@ /* ----------------------------------------------------------------------- */ -/* Furniture/bevel/accent/backdrop colour indices, one row per palette. Same - * field order as the assignments in run_wuss. */ -static const wuss_colour_t g_chrome[2][15] = +/* Fill config with the chrome colours for the PICO-8 (default) or RISC OS + * 16-colour Wimp palette. */ +static void fill_chrome_config(wuss_config_t *config, int use_wimp16) { - /* PICO-8 */ - { palette_PICO8_DARK_BLUE, palette_PICO8_WHITE, palette_PICO8_GREEN, - palette_PICO8_RED, palette_PICO8_ORANGE, palette_PICO8_LAVENDER, - palette_PICO8_BLUE, palette_PICO8_DARK_BLUE, palette_PICO8_LIGHT_GREY, - palette_PICO8_WHITE, palette_PICO8_DARK_GREY, palette_PICO8_DARK_BLUE, - palette_PICO8_WHITE, palette_PICO8_WHITE, palette_PICO8_LIGHT_GREY }, - /* RISC OS 16-colour Wimp */ - { palette_WIMP16_GREY_75, palette_WIMP16_BLACK, palette_WIMP16_GREEN, - palette_WIMP16_RED, palette_WIMP16_ORANGE, palette_WIMP16_LIGHT_BLUE, - palette_WIMP16_GREY_50, palette_WIMP16_GREY_62, palette_WIMP16_GREY_87, - palette_WIMP16_WHITE, palette_WIMP16_GREY_50, palette_WIMP16_ORANGE, - palette_WIMP16_BLACK, palette_WIMP16_GREY_50, palette_WIMP16_GREY_37 } -}; + config->titlebar_height = 0; + config->backdrop.pattern = screen_PATTERN_DOTS; -static void fill_chrome_config(wuss_config_t *config, int palette_index) -{ - const wuss_colour_t *c = g_chrome[palette_index]; - - config->titlebar_height = 0; - config->furniture.title.bg = c[0]; - config->furniture.title.fg = c[1]; - config->furniture.back = c[2]; - config->furniture.close = c[3]; - config->furniture.toggle = c[4]; - config->furniture.resize = c[5]; - config->furniture.scroll.arrows = c[6]; - config->furniture.scroll.wells = c[7]; - config->furniture.scroll.sausages = c[8]; - config->bevel.light = c[9]; - config->bevel.dark = c[10]; - config->accent.bg = c[11]; - config->accent.fg = c[12]; - config->backdrop.colour = c[13]; - config->backdrop.pattern = screen_PATTERN_DOTS; - config->backdrop.pattern_bg = c[14]; + if (use_wimp16) + { + config->furniture.title.bg = palette_WIMP16_GREY_75; + config->furniture.title.fg = palette_WIMP16_BLACK; + config->furniture.outline = palette_WIMP16_BLACK; + config->furniture.back = palette_WIMP16_GREEN; + config->furniture.close = palette_WIMP16_RED; + config->furniture.toggle = palette_WIMP16_ORANGE; + config->furniture.resize = palette_WIMP16_LIGHT_BLUE; + config->furniture.scroll.arrows = palette_WIMP16_GREY_50; + config->furniture.scroll.wells = palette_WIMP16_GREY_62; + config->furniture.scroll.sausages = palette_WIMP16_GREY_87; + config->bevel.light = palette_WIMP16_WHITE; + config->bevel.dark = palette_WIMP16_GREY_50; + config->bevel.divider = palette_WIMP16_GREY_75; + config->button.bg = palette_WIMP16_GREY_87; + config->button.fg = palette_WIMP16_BLACK; + config->button.pressed = palette_WIMP16_GREY_62; + config->accent.colour = palette_WIMP16_ORANGE; + config->backdrop.colour = palette_WIMP16_GREY_37; + config->backdrop.pattern_bg = palette_WIMP16_GREY_50; + config->body.window = palette_WIMP16_GREY_87; + config->body.menu = palette_WIMP16_WHITE; + } + else + { + config->furniture.title.bg = palette_PICO8_DARK_BLUE; + config->furniture.title.fg = palette_PICO8_WHITE; + config->furniture.outline = palette_PICO8_BLACK; + config->furniture.back = palette_PICO8_GREEN; + config->furniture.close = palette_PICO8_RED; + config->furniture.toggle = palette_PICO8_ORANGE; + config->furniture.resize = palette_PICO8_LAVENDER; + config->furniture.scroll.arrows = palette_PICO8_BLUE; + config->furniture.scroll.wells = palette_PICO8_DARK_BLUE; + config->furniture.scroll.sausages = palette_PICO8_LIGHT_GREY; + config->bevel.light = palette_PICO8_WHITE; + config->bevel.dark = palette_PICO8_DARK_GREY; + config->bevel.divider = palette_PICO8_LAVENDER; + config->button.bg = palette_PICO8_LIGHT_GREY; + config->button.fg = palette_PICO8_BLACK; + config->button.pressed = palette_PICO8_LAVENDER; + config->accent.colour = palette_PICO8_ORANGE; + config->backdrop.colour = palette_PICO8_LAVENDER; + config->backdrop.pattern_bg = palette_PICO8_LIGHT_GREY; + config->body.window = palette_PICO8_LIGHT_GREY; + config->body.menu = palette_PICO8_WHITE; + } } /* Redraw the whole screen one pixel at a time: each wuss_redraw_dirty call is @@ -217,7 +239,10 @@ static void wuss_frame(void *arg) * picker menu swaps the system palette live (wuss_set_palette, picked up by * menu_handle's wuss_EVENT_PALETTE case); the quit input or closing the * window exits */ -static result_t run_wuss(const char *resources) +static result_t run_wuss(const char *resources, + const char *palette_name, + int depth, + int scale) { const int scr_width = 640; const int scr_height = 480; @@ -242,15 +267,11 @@ static result_t run_wuss(const char *resources) int palette_index; { - /* WUSS_PALETTE names a *.hex file under resources/palettes (extension - * stripped, e.g. "RISC-OS"); default is PICO-8. Chrome was never derived - * from *.hex content (see fill_chrome_config), so it stays keyed by - * whether the startup file is "RISC-OS" specifically, not by whatever - * the picker menu later loads. */ - const char *palette_name = getenv("WUSS_PALETTE"); - - if (palette_name == NULL) - palette_name = "PICO-8"; + /* palette_name (from -palette, default "PICO-8") names a *.hex file under + * resources/palettes, extension stripped, e.g. "RISC-OS". Chrome was + * never derived from *.hex content (see fill_chrome_config), so it stays + * keyed by whether the startup file is "RISC-OS" specifically, not by + * whatever the picker menu later loads. */ use_wimp16 = (strcmp(palette_name, "RISC-OS") == 0); palette_index = use_wimp16 ? 1 : 0; @@ -287,7 +308,7 @@ static result_t run_wuss(const char *resources) } rc = wuss_frontend_open(scr_width, scr_height, palette, NELEMS(palette), - &pixels, &rowbytes, &fmt, &frontend); + depth, scale, &pixels, &rowbytes, &fmt, &frontend); logf_info("wuss: wuss_frontend_open -> rc=0x%X (%s)", rc, result_string(rc)); if (rc != result_OK) goto Failure; @@ -308,7 +329,7 @@ static result_t run_wuss(const char *resources) * picker menus -- [2] Symbols is chrome-only, * never a text font choice */ - fill_chrome_config(&config, use_wimp16 ? 1 : 0); + fill_chrome_config(&config, use_wimp16); for (i = 0; i < nfonts; i++) { @@ -382,8 +403,6 @@ static result_t run_wuss(const char *resources) * block. Harmless at process exit. */ wuss_destroy(wuss); /* also sweeps g.menu_task and closes any open chain */ - tasks_teardown(); /* frees the descriptor-built menu, now the chain is gone */ - for (i = 0; i < nfonts; i++) bmfont_destroy(fonts[i]); @@ -401,24 +420,105 @@ static result_t run_wuss(const char *resources) /* ----------------------------------------------------------------------- */ +/* Parsed command-line options. Members are use-ordered to match run_wuss's + * parameter list. */ +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 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"; + +#ifndef __riscos + +/* Desktop: getopt_long. Accepts the short forms and the "--" long forms; the + * historical single-dash long spellings (-resources) are no longer accepted. + * Returns false and prints usage on an unknown option or missing argument. */ +static bool parse_args(int argc, char *argv[], wuss_options_t *opts) +{ + static const struct option longopts[] = + { + { "resources", required_argument, NULL, 'r' }, + { "palette", required_argument, NULL, 'p' }, + { "depth", required_argument, NULL, 'd' }, + { "scale", required_argument, NULL, 's' }, + { NULL, 0, NULL, 0 } + }; + + int c; + + for (;;) + { + c = getopt_long(argc, argv, "r:p:d:s:", longopts, NULL); + if (c == -1) + break; + + switch (c) + { + case 'r': opts->resources = optarg; break; + case 'p': opts->palette_name = optarg; break; + case 'd': opts->depth = atoi(optarg); break; + case 's': opts->scale = atoi(optarg); break; + default: + fputs(wuss_usage, stderr); + return false; + } + } + + return true; +} + +#else /* __riscos */ + +/* RISC OS: no getopt_long. Hand-rolled scan of the same options, single-dash + * long spellings only (matches the pre-getopt behaviour). */ +static bool parse_args(int argc, char *argv[], wuss_options_t *opts) +{ + int i; + + for (i = 1; i < argc; i++) + if (strcmp(argv[i], "-resources") == 0 && i + 1 < argc) + opts->resources = argv[++i]; + else if (strcmp(argv[i], "-palette") == 0 && i + 1 < argc) + opts->palette_name = argv[++i]; + else if (strcmp(argv[i], "-depth") == 0 && i + 1 < argc) + opts->depth = atoi(argv[++i]); + else if (strcmp(argv[i], "-scale") == 0 && i + 1 < argc) + opts->scale = atoi(argv[++i]); + + return true; +} + +#endif /* __riscos */ + int main(int argc, char *argv[]) { /* path_join_filename splices the root and each branch with the platform * separator, so the "here" root differs: "." on Unix, but on RISC OS the * currently-selected directory is "@" ("." there would give "..resources"). */ #ifdef __riscos - const char *resources = "@"; + const char *default_resources = "@"; #else - const char *resources = "."; + const char *default_resources = "."; #endif - int i; - result_t rc; + wuss_options_t opts; + result_t rc; - for (i = 1; i < argc; i++) - if (strcmp(argv[i], "-resources") == 0 && i + 1 < argc) - resources = argv[++i]; + opts.resources = default_resources; + opts.palette_name = "PICO-8"; + opts.depth = 4; + opts.scale = 0; /* 0 = let the frontend pick its default */ + + if (!parse_args(argc, argv, &opts)) + return EXIT_FAILURE; - rc = run_wuss(resources); + rc = run_wuss(opts.resources, opts.palette_name, opts.depth, opts.scale); return rc == result_TEST_PASSED ? EXIT_SUCCESS : EXIT_FAILURE; } diff --git a/apps/wuss/tasks.c b/apps/wuss/tasks.c index 9a6e515..66b9a57 100644 --- a/apps/wuss/tasks.c +++ b/apps/wuss/tasks.c @@ -1,6 +1,5 @@ /* wuss/tasks.c -- Wuss demo task launcher and menu wiring */ -#include #include #include @@ -9,13 +8,11 @@ #include "base/utils.h" #include "framebuf/bitmap.h" #include "framebuf/colour.h" -#include "geom/box.h" #include "io/path.h" #include "wuss/task.h" #include "wuss/wuss.h" #include "wuss/window.h" #include "wuss/menu.h" -#include "wuss/menu-desc.h" #include "frontend.h" #include "tasks.h" @@ -34,6 +31,7 @@ #include "tasks/minesweeper.h" #include "tasks/palette.h" #include "tasks/porter-duff.h" +#include "tasks/saturn.h" #include "tasks/sofa.h" #include "tasks/swatches.h" #include "tasks/text.h" @@ -106,12 +104,12 @@ static result_t spawn_palette(void) 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]; - result_t rc; t = calloc(1, sizeof(*t)); if (t == NULL) return result_OOM; @@ -190,6 +188,17 @@ static result_t spawn_minesweeper(void) return result_OK; } +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); + if (rc != result_OK) return rc; + if (t->window == NULL) { free(t); return rc; } + return result_OK; +} + static result_t spawn_sofa(void) { sofa_task_t *t = calloc(1, sizeof(*t)); @@ -262,108 +271,6 @@ static result_t spawn_porter_duff(void) return result_OK; } -/* A static demo menu tree for the pop-up helper: a submenu, a couple of - * ticked rows and a standalone dashed rule row above the final entry. - * wuss_menu_open never mutates it. */ -static const wuss_menu_item_t g_menu_export_items[] = -{ - { "As PNG", wuss_MENU_ITEM_NONE, NULL }, - { "As JPEG", wuss_MENU_ITEM_NONE, NULL }, - { "As GIF", wuss_MENU_ITEM_DISABLED, NULL } -}; - -static const wuss_menu_t g_menu_export = -{ - "Export", g_menu_export_items, NELEMS(g_menu_export_items) -}; - -/* A caller-owned window wired as a menu item's `.window`: hovering "Details" - * shows it where a submenu would open, and moving off the row (or dismissing - * the menu) hides it again. Created once, lazily, by spawn_menu. */ -static wuss_window_t *g_menu_details_window; - -static wuss_menu_item_t g_menu_items[] = -{ - { "Open", wuss_MENU_ITEM_NONE, NULL, NULL }, - { "Show grid", wuss_MENU_ITEM_TICKED, NULL, NULL }, - { "Wireframe", wuss_MENU_ITEM_TICKED, NULL, NULL }, - { "Export", wuss_MENU_ITEM_NONE, &g_menu_export, NULL }, - { "Details", wuss_MENU_ITEM_NONE, NULL, NULL }, /* .window set in spawn_menu */ - { "Quit", wuss_MENU_ITEM_DASHED, NULL, NULL } -}; - -static const wuss_menu_t g_menu = -{ - "Display", g_menu_items, NELEMS(g_menu_items) -}; - -/* index of the "Details" row in g_menu_items */ -#define G_MENU_DETAILS_INDEX 4 - -static result_t spawn_menu(void) -{ - if (g_menu_details_window == NULL) - { - box_t content; - result_t rc; - - /* a small hidden window; wuss fills its background, no task needed */ - content.x0 = 0; - content.y0 = 0; - content.x1 = 180; - content.y1 = 120; - rc = wuss_window_create(g.menu_task, &content, "Details", - 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_HIDDEN, - wuss_BACKDROP_COLOUR(1), - SIZE2D(180, 120), SIZE2D(0, 0), - &g_menu_details_window); - if (rc != result_OK) - return rc; - g_menu_items[G_MENU_DETAILS_INDEX].window = g_menu_details_window; - } - - return wuss_menu_open(g.menu_task, &g_menu, wuss_get_pointer(g.wuss), NULL); -} - -/* Same menu shape built from a descriptor string, to exercise - * wuss_menu_create_from_desc. The tree must outlive the open chain, so it is - * kept here and rebuilt (previous one freed) on each open. Freed for good in - * tasks_teardown. */ -static wuss_menu_t *g_menu_desc; - -static const wuss_menu_item_t g_menu_desc_export_items[] = -{ - { "As PNG", wuss_MENU_ITEM_NONE, NULL }, - { "As JPEG", wuss_MENU_ITEM_NONE, NULL }, - { "As GIF", wuss_MENU_ITEM_DISABLED, NULL } -}; - -static const wuss_menu_t g_menu_desc_export = -{ - "Export", g_menu_desc_export_items, NELEMS(g_menu_desc_export_items) -}; - -static result_t spawn_menu_desc(void) -{ - wuss_menu_t *m; - result_t rc; - - rc = wuss_menu_create_from_desc(&m, - "Display, Open, !Show grid, !Wireframe, >Export, |Quit", - &g_menu_desc_export); - if (rc != result_OK) - return rc; - - wuss_menu_destroy(g_menu_desc); - g_menu_desc = m; - - return wuss_menu_open(g.menu_task, g_menu_desc, wuss_get_pointer(g.wuss), - NULL); -} - /* The task launcher is a MENU-button pop-up over the backdrop rather than a * window of buttons. Each leaf menu pairs a *_items table with a *_spawn table * in lock-step: picking row i of that menu calls its spawn[i]. */ @@ -386,6 +293,7 @@ static const wuss_menu_item_t g_launch_items[] = { "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 } @@ -396,7 +304,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_sofa, spawn_swatches, spawn_text + spawn_porter_duff, spawn_saturn, spawn_sofa, spawn_swatches, spawn_text }; static const wuss_menu_t g_launch_menu = @@ -410,34 +318,15 @@ static result_t spawn_quit(void) return result_OK; } -/* "Test" submenu: the menu-system exercisers. */ -static const wuss_menu_item_t g_test_items[] = -{ - { "Menu", wuss_MENU_ITEM_NONE, NULL }, - { "Menu (desc)", wuss_MENU_ITEM_NONE, NULL } -}; - -static const task_spawn_fn_t g_test_spawn[] = -{ - spawn_menu, spawn_menu_desc -}; - -static const wuss_menu_t g_test_menu = -{ - "Test", g_test_items, NELEMS(g_test_items) -}; - static const wuss_menu_item_t g_task_items[] = { { "Launch", wuss_MENU_ITEM_NONE, &g_launch_menu, NULL }, - { "Test", wuss_MENU_ITEM_DASHED, &g_test_menu, NULL }, { "Quit Wuss", wuss_MENU_ITEM_DASHED, NULL, NULL } }; static const task_spawn_fn_t g_task_spawn[] = { NULL, /* "Launch" -> submenu g_launch_menu */ - NULL, /* "Test" -> submenu g_test_menu */ spawn_quit }; @@ -446,10 +335,9 @@ static const wuss_menu_t g_task_menu = "Tasks", g_task_items, NELEMS(g_task_items) }; -/* g_task_menu / g_launch_menu / g_test_menu picks are dispatched by index; - * g_menu / g_menu_desc picks just print. 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 / 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. */ result_t task_handle_event(wuss_window_t *window, const wuss_event_t *event, void *task_data) @@ -488,13 +376,6 @@ result_t task_handle_event(wuss_window_t *window, return result_OK; } - if (menu == &g_test_menu) - { - if (index >= 0 && index < (int) NELEMS(g_test_spawn)) - (void) g_test_spawn[index](); - return result_OK; - } - if (menu == &g_task_menu) { if (index >= 0 && index < (int) NELEMS(g_task_spawn) && g_task_spawn[index]) @@ -502,8 +383,6 @@ result_t task_handle_event(wuss_window_t *window, return result_OK; } - printf("menu: picked \"%s\"\n", - menu->items[index].text ? menu->items[index].text : "(sep)"); return result_OK; } @@ -511,10 +390,3 @@ result_t tasks_open_launcher(point_t pos) { return wuss_menu_open(g.menu_task, &g_task_menu, pos, NULL); } - -void tasks_teardown(void) -{ - /* menus are only safe to free once wuss_destroy has torn down any chain - * that was still borrowing them */ - wuss_menu_destroy(g_menu_desc); -} diff --git a/apps/wuss/tasks.h b/apps/wuss/tasks.h index 3907f97..08aa8c5 100644 --- a/apps/wuss/tasks.h +++ b/apps/wuss/tasks.h @@ -22,7 +22,7 @@ extern struct wuss_app_tasks { wuss_t *wuss; - wuss_task_t *menu_task; /* owns the menus and the hidden Details window */ + wuss_task_t *menu_task; /* owns the task launcher menus */ colour_t *palette; int npalette; const char *palette_name; /* startup *.hex leafname, for ticking the @@ -44,12 +44,8 @@ result_t task_handle_event(wuss_window_t *window, const wuss_event_t *event, void *task_data); -/* open the top-level task launcher (Launch / Test / Quit Wuss) at pos; called - * on a MENU click over bare backdrop */ +/* open the top-level task launcher (Launch / Quit Wuss) at pos; called on a + * MENU click over bare backdrop */ result_t tasks_open_launcher(point_t pos); -/* free the descriptor-built menu tree kept alive across opens; call in - * run_wuss's teardown after wuss_destroy */ -void tasks_teardown(void); - #endif /* WUSS_APP_TASKS_H */ diff --git a/docs/framebuf/drawing.md b/docs/framebuf/drawing.md index a30f935..f69a536 100644 --- a/docs/framebuf/drawing.md +++ b/docs/framebuf/drawing.md @@ -53,6 +53,10 @@ the P4 handler address an odd nibble so callers never pre-pack. `screen_fill_hli (and thus `screen_fill_rect` and the circle scanline fill) is a thin wrapper over it. +`copy` ops that blit a deep bitmap onto a paletted screen turn the per-pixel +nearest-palette match into a cached mask-and-lookup via +[pixelmap](pixelmap.md). + ### Axis 2 — ACTION (what kind of mark) | verb | mark | object it takes | @@ -211,3 +215,5 @@ sites updated in the same change: `screen_draw_bitmap` → `screen_copy_bitmap`, - [composite.md](composite.md) — Porter-Duff bitmap compositing - [bmfont.md](bmfont.md) — proportional bitmap fonts +- [pixelmap.md](pixelmap.md) — cached deep-to-paletted conversion tables +- [rle-bitmap.md](rle-bitmap.md) — RLE-compressed bitmaps diff --git a/docs/framebuf/pixelmap.md b/docs/framebuf/pixelmap.md new file mode 100644 index 0000000..ed96a1f --- /dev/null +++ b/docs/framebuf/pixelmap.md @@ -0,0 +1,166 @@ +# [DPTLib](https://github.com/dpt/DPTLib) > framebuf > pixelmap + +`pixelmap` caches lookup tables that convert between a "deep" (32bpp) pixel +format and a paletted (`p1`/`p2`/`p4`/`p8`) one, in either direction. It +replaces a per-pixel `colour_to_pixel` call in a blit inner loop with a table +read. + +## Rationale + +**Deep to paletted.** A bitmap-to-paletted blit inner loop otherwise calls +`colour_to_pixel` per pixel, and for a paletted target that runs a weighted +linear scan of the whole palette (`closest_palette_entry`) every time — +O(pixels x palette-entries) per blit. `pixelmap` does that scan once. The +source pixel's R, G and B channels are quantised to a few bits each, composed +into an index, and used to read a small table. Deep source colours are +deliberately quantised — a 16-colour palette cannot resolve finer than about +4:4:4 anyway — so the table stays resident: 2KB for a 32bpp-to-p4 map. + +**Paletted to deep.** No palette scan is needed — the table is just +`nentries` deep pixels, indexed by the raw palette index. The win here is the +shared, build-once cache across repeated conversions of the same palette, and +one code path for both directions. `bmconv_p4_to_bgrx8888` was the +hand-written version of this (a local 16-entry `map[]`); it now calls +`pixelmap_get`. + +## API + +```C +const pixelmap_t *pixelmap_get(pixelfmt_t srcfmt, + pixelfmt_t destfmt, + const colour_t *palette, + int nentries); +``` + +Exactly one of `srcfmt` / `destfmt` must be paletted and the other a 32bpp +`*8888` format; `palette` is that paletted side's palette. Returns a cached +table, or `NULL` if the pair is unsupported, `palette` is `NULL`, or +allocation failed. The pointer is owned by the module and stays valid until a +later `pixelmap_get` call with a different key evicts it. + +```C +typedef struct pixelmap +{ + pixelfmt_t srcfmt; + pixelfmt_t destfmt; + int dest_log2bpp; /* 0..3, deep->paletted only */ + unsigned char entry_bytes; /* 0 = packed paletted, else deep */ + unsigned char rbits, gbits, bbits; + unsigned char rshift, gshift, bshift; /* into the source pixel */ + unsigned int nentries; + const unsigned char *entries; +} +pixelmap_t; +``` + +**Deep to paletted** (`entry_bytes == 0`). `entries` is packed to the +destination bit width, LSB-first within each byte: `p4` holds two 4-bit +entries per byte, `p2` four, `p1` eight, `p8` one byte per entry. + +**Paletted to deep** (`entry_bytes == 4`). `entries` is `nentries` deep +pixels; the index is the raw palette index. `rbits`/`gbits`/`bbits` and the +shifts are 0. + +## Using it — deep to paletted + +A caller that blits in a loop fetches the table once, before the loop, and +bails if it is unsupported; then does a mask-and-lookup per pixel: + +```C +const pixelmap_t *pm; + +pm = pixelmap_get(pixelfmt_rgba8888, scr->format, scr->palette, 16); +if (pm == NULL) + return result_NOT_SUPPORTED; +... +r = (px >> pm->rshift) & 0xFF; +g = (px >> pm->gshift) & 0xFF; +b = (px >> pm->bshift) & 0xFF; +idx = ((r >> (8 - pm->rbits)) << (pm->gbits + pm->bbits)) + | ((g >> (8 - pm->gbits)) << pm->bbits) + | ( b >> (8 - pm->bbits)); +pxl = (pm->entries[idx >> 1] >> ((idx & 1) << 2)) & 0xF; /* p4 */ +``` + +Alpha is not part of the index. Callers keep their own alpha test and route +only the opaque path through the table. There is no `colour_to_pixel` +fallback: a `NULL` return means the blit cannot proceed and the call sites +propagate `result_NOT_SUPPORTED`. + +Call sites: `screen_copy_bitmap_p4` (`screen-draw.c`) and +`screen_copy_bitmap_rle_p4` (`screen-copy-bitmap-rle.c`). Both treat the source +as RGBA8888 byte order. + +## Using it — paletted to deep + +```C +const pixelmap_t *pm; +const pixelfmt_bgrx8888_t *map; + +pm = pixelmap_get(pixelfmt_p4, pixelfmt_bgrx8888, src->palette, 16); +map = (const pixelfmt_bgrx8888_t *) pm->entries; +... +*outpx++ = map[nibble]; +``` + +Call site: `bmconv_p4_to_bgrx8888` (`bitmap.c`). + +## Policy + +For a deep source, the channel split is chosen by a built-in table keyed on +`destfmt`. There is no caller override in v1. + +| source | dest | bits/channel | entries | table bytes | +|------------------|-------------|--------------|---------|-------------| +| any 32bpp `*8888`| `p1` | R4 G4 B4 | 4096 | 512 | +| any 32bpp `*8888`| `p2` | R4 G4 B4 | 4096 | 1024 | +| any 32bpp `*8888`| `p4` | R4 G4 B4 | 4096 | 2048 | +| any 32bpp `*8888`| `p8` | R5 G6 B5 | 65536 | 65536 | +| `p1`..`p8` | 32bpp `*8888` | *(raw index)* | palette size | 4 x entries | + +`p8` keeps a wider index so 256-colour output stays faithful. A paletted +source has no quantisation — one deep pixel per palette entry. + +## Cache + +A module-global LRU of 4 entries, keyed on `(srcfmt, destfmt, FNV-1a hash of +the palette bytes)`. Hashing the palette contents (rather than its address) +keeps the key correct when a palette is mutated in place or a freed palette's +address is reused. The design mirrors `spanregistry_get`. Single-threaded, like +the rest of `framebuf`. + +## Behaviour change on adoption + +Converting a **deep-to-paletted** call site from `colour_to_pixel` to +`pixelmap` changes its output slightly: the old path matched on the exact +24-bit source colour, the new path quantises to the policy split first. For a +<=16-entry palette this is imperceptible; a palette with two entries inside +one quantisation cell could move a pixel by one index. The converted sites +(`screen_copy_bitmap_p4`, `screen_copy_bitmap_rle_p4`) now return +`result_NOT_SUPPORTED` for a format pair `pixelmap` cannot map, where before +they would have limped along one `colour_to_pixel` call per pixel. + +A **paletted-to-deep** call site is bit-exact — the entries are the same +`colour_to_pixel` results the local `map[]` held. + +## Build cost + +For a deep source, building a table is one `colour_to_pixel` call per index +value, at `get` time only: ~65K calls for a 32bpp-to-p4 map (negligible), +~16M for a `p8` map (a few milliseconds). For a paletted source it is one +call per palette entry (16 or 256). Amortised across every subsequent blit +that hits the cache. + +## Not done + +- No 16bpp (`565`) support on either side — add when a call site needs it. +- Paletted-to-deep only fills 4-byte (`*8888`) entries; 12/15/16bpp deep + destinations are not built. +- No `pixelmap_get_ex` with a caller-supplied channel split. +- `span_p4_blendconst` still re-quantises per pixel; its input is a blend + result, not a source pixel, so it is a separate change. + +## Related + +- [drawing.md](drawing.md) — the `framebuf` drawing taxonomy +- [rle-bitmap.md](rle-bitmap.md) — RLE bitmaps; its p4 blit path uses pixelmap diff --git a/docs/framebuf/rle-bitmap.md b/docs/framebuf/rle-bitmap.md new file mode 100644 index 0000000..f5518e4 --- /dev/null +++ b/docs/framebuf/rle-bitmap.md @@ -0,0 +1,110 @@ +# [DPTLib](https://github.com/dpt/DPTLib) > framebuf > RLE-compressed bitmaps + +An optional **read-only run-length-encoded** representation of a `bitmap_t`. +UI art, icons and nine-patches are typically sparse: long runs of one colour +and large fully-transparent regions. Storing them raw wastes RAM and, at blit +time, memory bandwidth. `bitmap_compress()` squashes a loaded bitmap in place +into a per-scanline ("laterally counted") opcode stream; `screen_copy_bitmap()` +decodes it on the fly, skipping transparent runs by pointer arithmetic. + +## Lifecycle + +```C +bitmap_t bm; +bitmap_load_png(&bm, "icon.png"); /* raw rgba8888 */ +bitmap_compress(&bm); /* now RLE, bm.base is a blob */ + +screen_copy_bitmap(&scr, x, y, &bm);/* blits straight from the blob */ + +/* bitmap_decompress(&bm); to get raw pixels back, or just: */ +free(bm.base); +``` + +`bitmap_compress()` is idempotent. `bitmap_decompress()` restores the raw +`rowbytes`-strided buffer byte-for-byte and is a no-op on an uncompressed +bitmap. + +## The compressed flag + +There is no new `bitmap_t` field. Bit 30 of `bm->format` (`pixelfmt_RLE_FLAG`, +clear of the sign bit) marks a compressed pixel store. The low bits still hold +the real `pixelfmt_t`, so `bm->size`, `bm->palette` and `bm->span` stay valid +while compressed; `bm->rowbytes` is 0. + +- `pixelfmt_is_rle(f)` / `bitmap_is_compressed(bm)` — test the flag. +- `pixelfmt_base(f)` — strip the flag, yielding the decoded pixel format. + +`pixelfmt_log2bpp()` and `spanregistry_get()` are **not** flag-tolerant: a +compressed format reaching them is a missing-guard bug and trips `assert(0)`. +Every other bitmap entry point (`bitmap_clear`, `bitmap_fill_pattern`, +`bitmap_convert`, `bitmap_save_png`, `composite`, `screen_copy_ninepatch`) +guards with `pixelfmt_is_rle()` and declines with `result_NOT_SUPPORTED`. + +## Blob layout + +`bm->base` points at an 8-byte header followed by `bm->size.h` opcode streams, +one per row, concatenated, each EOL-terminated. No magic, no format/size (all +still in the struct), no row-offset index. + +| bytes | field | meaning | +|-------|------------|-----------------------------------------------------| +| 0..3 | `rowbytes` | u32 LE — original stride, restored by decompress | +| 4..7 | `data_len` | u32 LE — encoded byte count that follows | + +To blit from a clipped top row the decoder walks the opcode stream forward +(no writes) counting EOL bytes — `bitmap__rle_skip_row()`. + +## Row opcodes + +Dispatch is by the count of leading zero bits of the first byte (`clz8`, +UTF-8-prefix style). `PIXEL` is 1 byte for an 8bpp base, 4 bytes for 32bpp. +Long forms store `length - shortmax - 1`; a zero length field means `2^bits`. + +``` +1LLLLLLL literal : 1..128 pixels follow inline +01LLLLLL PIXEL repeat : 1..64 copies of one inline pixel +001LLLLL skip : 1..32 zero pixels [alpha only] +0001LLLL LLLLLLLL literal long : 129..4224 pixels follow inline +00001LLL LLLLLLLL PIXEL repeat long : 65..2112 copies +000001LL LLLLLLLL skip long : 33..1056 zero pixels [alpha only] +00000001 EOL +00000000 reserved (decoder rejects) +``` + +A `skip` run drops the pixel bytes entirely, so it is only emitted for a +**wholly zero** pixel (`0x00000000`, the usual PNG transparent value). +Non-zero-but-alpha-0 pixels are encoded as literal/repeat and survive a round +trip. `bitmap_decompress()` writes zeros for skip runs; the blit leaves the +destination untouched. + +## Supported formats + +| base format | compress | skip runs | +|-----------------------------------------------|----------|-----------| +| `pixelfmt_y8` | yes | no | +| `pixelfmt_{bgrx,rgbx,xbgr,xrgb}8888` | yes | no | +| `pixelfmt_{bgra,rgba}8888` | yes | yes | +| `pixelfmt_{abgr,argb}8888` | yes | no | +| `p1`..`p8`, 12/15/16bpp | no (`result_NOT_SUPPORTED`) | — | + +## Screen targets + +- **32bpp screen** — the source must decode to the screen's exact channel + order; the blit is then a byte copy, no per-pixel conversion + (`pixelfmt_base(src->format) == scr->format`, alpha folded out). +- **p4 screen** — each decoded pixel is mapped to the nearest palette index. A + cached [pixelmap](pixelmap.md) table (`pixelmap_get(pixelfmt_rgba8888, + scr->format, scr->palette, 16)`) turns the inner loop into a mask-and-lookup; + if that pair is unsupported the blit declines `result_NOT_SUPPORTED`. +- **Other depths** decline `result_NOT_SUPPORTED`. + +## v1 limitations + +- **Alpha-tested, not alpha-blended.** Transparent runs are skipped, opaque + runs copied. This matches `screen_copy_bitmap`'s p4 path but not its 32bpp + path (which blends per pixel). A blended RLE path would decode alpha runs + into `span->blendarray`. +- No scanline dedup, no row-offset table — pure sequential decode. +- No `composite()` or nine-patch support for compressed bitmaps. +- No colour-key transparency — wholly-zero pixels only. +- Runtime `bitmap_compress()` only — no `.rle` file format or offline tool. diff --git a/docs/windowing/wuss.md b/docs/windowing/wuss.md index 59071ff..6261f54 100644 --- a/docs/windowing/wuss.md +++ b/docs/windowing/wuss.md @@ -29,9 +29,9 @@ A `wuss_backdrop_t` is `{ colour, pattern, pattern_bg }`; the `wuss_BACKDROP_COL A `wuss_colour_t` is a system-palette index. Values `0..127` are raw indices; `wuss_COLOUR_SYMBOLIC` (128) and above are *symbolic* — roles wuss resolves to a concrete index against the live palette (and, for the chrome roles, the live config): - `wuss_COLOUR_BLACK`, `_WHITE`, `_RED`, `_GREEN`, `_BLUE`, `_YELLOW`, `_CYAN`, `_MAGENTA`, `_GREY` — nearest system-palette entry to the named RGB, the same lookup as `wuss_nearest_colour`. -- `wuss_COLOUR_TITLE_BG`, `_TITLE_FG`, `_BUTTON_HILIGHT`, `_BUTTON_SHADOW`, `_ACCENT_BG`, `_ACCENT_FG`, `_BACKDROP` — echo the matching `wuss_config_t` field (`furniture.title.bg`, `bevel.light`, `bevel.dark`, `backdrop.colour`, …). +- `wuss_COLOUR_TITLE_BG`, `_TITLE_FG`, `_BUTTON_HILIGHT`, `_BUTTON_SHADOW`, `_BUTTON_PRESSED`, `_ACCENT_BG`, `_ACCENT_FG`, `_BACKDROP`, `_WINDOW`, `_MENU` — echo the matching `wuss_config_t` field (`furniture.title.bg`, `bevel.light`, `bevel.dark`, `bevel.pressed`, `backdrop.colour`, `body.window`, `body.menu`, …). `body.window` (work-area fill) defaults to `wuss_COLOUR_GREY`, `body.menu` (menu fill) to `wuss_COLOUR_WHITE`, when `config` is NULL. -Pass a symbolic value anywhere a `wuss_colour_t` is taken: config furniture/bevel/accent/backdrop, `wuss_window_create` / `wuss_window_set_background` backgrounds, icon specs. It is resolved once when stored, so drawing never pays for it and reads back a plain index. `wuss_set_palette` and `wuss_set_backdrop` re-resolve. `wuss_NO_BACKGROUND` is not symbolic and always passes through. +Pass a symbolic value anywhere a `wuss_colour_t` is taken: config furniture/bevel/accent/body/backdrop, `wuss_window_create` / `wuss_window_set_background` backgrounds, icon specs. It is resolved once when stored, so drawing never pays for it and reads back a plain index. `wuss_set_palette` and `wuss_set_backdrop` re-resolve. `wuss_NO_BACKGROUND` is not symbolic and always passes through. Destroy with `wuss_destroy`, which also destroys any windows still open on it. @@ -80,8 +80,9 @@ wuss_task_t; - `wuss_WINDOW_NO_HSCROLL` — no horizontal scrollbar on the bottom edge. - `wuss_WINDOW_NO_RESIZE` — no resize button in the bottom-right corner. - `wuss_WINDOW_NO_RESIZE_BLIT` — a resize (drag or toggle-size) always fully redraws the window's content instead of blitting the preserved region; for a task whose rendering depends on window size in ways a partial redraw can't patch (e.g. a layout that spans the whole window). +- `wuss_WINDOW_NO_REDRAW` — no `wuss_EVENT_REDRAW` is delivered for this window: Wuss fills its backdrop and draws its icons, and that is its whole appearance. For a window that is nothing but backdrop and/or icons (a label-only dialogue, say) this saves the task a no-op redraw handler — and, when the window shares a task with a window that does paint, a window-handle check in that handler. -`wuss_WINDOW_NO_CLOSE`/`NO_BACK`/`NO_TOGGLE_SIZE` are ignored if `flags` includes `wuss_WINDOW_NO_TITLEBAR`; `NO_VSCROLL`/`NO_HSCROLL`/`NO_RESIZE`/`NO_RESIZE_BLIT` apply regardless. +`wuss_WINDOW_NO_CLOSE`/`NO_BACK`/`NO_TOGGLE_SIZE` are ignored if `flags` includes `wuss_WINDOW_NO_TITLEBAR`; `NO_VSCROLL`/`NO_HSCROLL`/`NO_RESIZE`/`NO_RESIZE_BLIT`/`NO_REDRAW` apply regardless. All furniture actions (back, toggle-size, resize-drag, scrollbar arrow/thumb) are handled entirely within Wuss via `wuss_mouse_click`/`wuss_mouse_move` — no new client events. @@ -187,7 +188,7 @@ In a redraw callback: start drawing at `bounds.x0 - scroll.x`, `bounds.y0 - scro ## Redrawing - `wuss_redraw` repaints every window, back-to-front, unconditionally, having first painted the configured backdrop colour (see Setup) behind them, if any. -- Within a window, Wuss paints in a fixed order: the window background colour, then its icons (see "Icons" below), then the task's `wuss_EVENT_REDRAW` handler — so a task always draws over the background and any icons, never under them. +- Within a window, Wuss paints in a fixed order: the window background colour, then its icons (see "Icons" below), then the task's `wuss_EVENT_REDRAW` handler — so a task always draws over the background and any icons, never under them. A `wuss_WINDOW_NO_REDRAW` window skips that last step: background and icons are all it gets. - `wuss_invalidate` / `wuss_window_invalidate` mark a screen-space or window-local region dirty; window management calls these automatically for its own changes, but a task must call one of them itself whenever its content changes on its own (e.g. an animation), passing the union of the old and new areas that need repainting. - `wuss_redraw_dirty` repaints only the accumulated dirty region, then clears it, painting the backdrop colour into each dirty region first if one was configured. Without a configured backdrop, Wuss only repaints windows, not the background between/behind them, so a caller whose invalidation can expose background (e.g. after a window move) should clear that region itself first. - `wuss_get_dirty_count`/`wuss_get_dirty(wuss, index, out)` fetch the currently accumulated dirty regions (coalesced as they accumulate, up to a fixed cap after which further regions are merged into the last one) without redrawing. @@ -197,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. 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.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`. The enum is left open for sprite and editable-text types later. Icons are dynamic and owned by their window: -- `wuss_icon_create(window, spec, &icon)` — returns an opaque `wuss_icon_t *`. The spec gives the bounding box, type, text (copied; `NULL` treated as `""`), foreground and background palette indices, and flags. A `wuss_ICON_TYPE_BUTTON` must pass a real `bg`; passing `wuss_NO_BACKGROUND` is rejected with `result_WUSS_BAD_ICON`. An unknown type is also `result_WUSS_BAD_ICON`; an out-of-range `fg`/`bg` is `result_WUSS_BAD_COLOUR`. +- `wuss_icon_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_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`. @@ -215,7 +216,47 @@ An icon's bounding box is in **virtual content space** — the same space as `wu Wuss draws icons in creation order (later icons paint on top); hit-testing scans in reverse, so the topmost icon at a point wins. When the pointer leaves a pressed button its pressed state clears; v1 does not re-press on drag-back-in and does not track which mouse button is held. -The bevel's light (top/left) and dark (bottom/right) edge shades come from `config->bevel.light` / `config->bevel.dark` at `wuss_create` time, validated like the other furniture colours; both default to the titlebar fill colour when `config` is `NULL`. +The bevel's light (top/left) and dark (bottom/right) edge shades come from `config->bevel.light` / `config->bevel.dark` at `wuss_create` time, validated like the other furniture colours; both default to the titlebar fill colour when `config` is `NULL`. A held button also fills its face with `config->bevel.pressed` in place of the icon's own background; pass `wuss_NO_BACKGROUND` (or `NULL` config) to have it follow `bevel.dark`. + +### Loaded icon set + +`wuss_icons_load(wuss, dir)` scans a directory for `*.png`, loads each one and RLE-compresses it in place (`bitmap_compress`), and records its leafname sans `.png`. Entries are indexed in the order the platform's directory scan yields — unspecified, so address them by name: + +- `wuss_icons_lookup(wuss, name)` → 0-based index, or `-1`. +- `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. + +## Components + +Built with the `WUSS_COMPONENTS` CMake option, `libraries/wuss/component/` holds small reusable task helpers layered on the core. Their headers are under `include/wuss/component/`. + +### Program-information dialogue + +`wuss_proginfo` is 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 typically hangs off its Menu-button pop-up. + +```C +typedef struct wuss_proginfo_desc +{ + const char *name; /* row "Name" -- required */ + const char *purpose; /* row "Purpose" */ + const char *author; /* row "Author" */ + const char *version; /* row "Version" */ +} +wuss_proginfo_desc_t; + +result_t wuss_proginfo_create(wuss_proginfo_t **out, wuss_task_t *task, + const wuss_proginfo_desc_t *desc); +void wuss_proginfo_destroy(wuss_proginfo_t *doomed); +wuss_window_t *wuss_proginfo_window(const wuss_proginfo_t *pi); +``` + +`wuss_proginfo_create` builds one hidden window on `task`, sized from that task's font metrics and the text in `desc`, one label row per non-NULL field (the name right-justified in a left column, the value left-justified beside it). Each `desc` field is borrowed and copied. + +The window is created `wuss_WINDOW_NO_CLOSE` and `wuss_WINDOW_NO_REDRAW` — it is labels on a flat backdrop, so it needs no redraw handler — and stays on `task` until `wuss_proginfo_destroy` closes it. So `task` must **not** be an autoclose task: the hidden window would keep its window list from ever emptying. It must also outlive the handle, and — as for any borrowed menu-item window — any menu chain referencing `wuss_proginfo_window()` must be closed before the dialogue is destroyed. + +Point a menu item's `window` field at `wuss_proginfo_window()` to have Wuss show the dialogue where a submenu would open. ## Glossary diff --git a/include/framebuf/bitmap.h b/include/framebuf/bitmap.h index 34d1509..a219b42 100644 --- a/include/framebuf/bitmap.h +++ b/include/framebuf/bitmap.h @@ -139,4 +139,41 @@ result_t bitmap_convert(const bitmap_t *bm, pixelfmt_t newfmt, bitmap_t **newbm); +/** Non-zero if `bm` holds an RLE-compressed (read-only) pixel store. */ +#define bitmap_is_compressed(bm) pixelfmt_is_rle((bm)->format) + +/** + * Compress a bitmap's pixels in place into a read-only run-length-encoded + * blob, replacing `bm->base`. `bm->size`, `bm->palette` and `bm->span` stay + * valid for the decoded format; `bm->format` gains \ref pixelfmt_RLE_FLAG + * and `bm->rowbytes` becomes 0. Idempotent (a second call returns \ref + * result_OK). + * + * Suitable for sparse UI art: long single-colour runs and, for the alpha + * formats, fully-transparent regions collapse to a few bytes, saving RAM and + * blit-time memory bandwidth. Only \ref screen_copy_bitmap can consume the + * result; other bitmap operations decline with \ref result_NOT_SUPPORTED. + * + * Supported source formats: \ref pixelfmt_y8, the 32bpp `*8888` formats + * (`bgrx`/`rgbx`/`xbgr`/`xrgb`) and the 32bpp alpha formats + * (`bgra`/`rgba`/`abgr`/`argb`). Transparent-run compression applies to + * `bgra8888`/`rgba8888` only. + * + * \param[in] bm Bitmap to compress in place. + * \return \ref result_OK on success, \ref result_NOT_SUPPORTED for an + * unsupported format, \ref result_OOM on allocation failure. + */ +result_t bitmap_compress(bitmap_t *bm); + +/** + * Reverse \ref bitmap_compress, restoring a raw `rowbytes`-strided pixel + * buffer byte-for-byte. `bm->format` loses \ref pixelfmt_RLE_FLAG and + * `bm->rowbytes` is restored. A no-op returning \ref result_OK if `bm` is + * not compressed. + * + * \param[in] bm Bitmap to decompress in place. + * \return \ref result_OK on success, \ref result_OOM on allocation failure. + */ +result_t bitmap_decompress(bitmap_t *bm); + #endif /* FRAMEBUF_BITMAP_H */ diff --git a/include/framebuf/pixelfmt.h b/include/framebuf/pixelfmt.h index c8b0f63..ff6d2d9 100644 --- a/include/framebuf/pixelfmt.h +++ b/include/framebuf/pixelfmt.h @@ -95,6 +95,27 @@ typedef unsigned int pixelfmt_argb8888_t; /* 32bpp argb8888 */ typedef unsigned int pixelfmt_xxxa8888_t; /* any 32bpp alpha */ +/* ----------------------------------------------------------------------- */ + +/** + * Bit 30 of a bitmap's `format` word flags an RLE-compressed pixel store: + * its `base` points at an encoded blob rather than a raw `rowbytes`-strided + * buffer. The low bits still hold the real \ref pixelfmt_t of a decoded + * pixel, so `size`, `palette` and `span` stay valid while compressed. Bit 30 + * (not the sign bit) keeps the word non-negative. + * + * \see pixelfmt_base, pixelfmt_is_rle + */ +#define pixelfmt_RLE_FLAG (1 << 30) + +/** Strip any flag bits, yielding the underlying pixel format. */ +#define pixelfmt_base(f) ((pixelfmt_t) ((f) & (pixelfmt_RLE_FLAG - 1))) + +/** Non-zero if `f` names an RLE-compressed pixel store. */ +#define pixelfmt_is_rle(f) (((f) & pixelfmt_RLE_FLAG) != 0) + +/* ----------------------------------------------------------------------- */ + typedef unsigned int pixelfmt_any_t; /* generic/unspecified pixel */ typedef unsigned char pixelfmt_any8_t; /* any 8bpp pixel */ typedef unsigned short pixelfmt_any16_t; /* any 16bpp pixel */ @@ -179,6 +200,17 @@ typedef unsigned int pixelfmt_any32_t; /* any 32bpp pixel */ */ int pixelfmt_log2bpp(pixelfmt_t fmt); +/** + * Number of palette entries a packed paletted format addresses: 2 for p1, 4 + * for p2, 16 for p4, 256 for p8. 0 for any non-paletted format (its + * "palette" is not indexed). Useful as the `nentries` argument to \ref + * colour_to_pixel when the destination is a fixed-size paletted screen. + * + * \param fmt Pixel format. + * \return Entry count, or 0 if `fmt` is not a packed paletted format. + */ +int pixelfmt_paletted_nentries(pixelfmt_t fmt); + /* ----------------------------------------------------------------------- */ #ifdef __cplusplus diff --git a/include/framebuf/pixelmap.h b/include/framebuf/pixelmap.h new file mode 100644 index 0000000..1ba4ceb --- /dev/null +++ b/include/framebuf/pixelmap.h @@ -0,0 +1,87 @@ +/* framebuf/pixelmap.h -- cached bitmap-format conversion tables */ + +#ifndef FRAMEBUF_PIXELMAP_H +#define FRAMEBUF_PIXELMAP_H + +#ifdef __cplusplus +extern "C" +{ +#endif + +#include "framebuf/colour.h" +#include "framebuf/pixelfmt.h" + +/* ----------------------------------------------------------------------- */ + +/** + * A cached lookup table converting between a "deep" (32bpp) pixel format and + * a paletted (p1/p2/p4/p8) one, in either direction. It replaces a per-pixel + * \ref colour_to_pixel call in a blit inner loop with a table read. + * + * **Deep source to paletted destination.** \ref colour_to_pixel would run a + * weighted linear scan of the whole palette per pixel. The pixelmap does + * that once: the source pixel's red, green and blue channels are extracted + * with \ref rshift / \ref gshift / \ref bshift, quantised to \ref rbits / + * \ref gbits / \ref bbits, composed into the index `(r << (gbits + bbits)) | + * (g << bbits) | b`, and used to read \ref entries. The quantisation trades + * a little colour accuracy for a table small enough to stay resident (2KB + * for a 32bpp-to-p4 map). \ref entries is packed to the destination bit + * width, LSB-first within each byte: p4 holds two 4-bit entries per byte, p2 + * four, p1 eight, p8 one byte per entry. \ref entry_bytes is 0. + * + * **Paletted source to deep destination.** The index is the raw palette + * index (0..\ref nentries - 1); there is no quantisation and + * rbits/gbits/bbits and the shifts are 0. \ref entries is an array of \ref + * nentries pixels, each \ref entry_bytes wide (4 for a 32bpp destination), + * holding the destination pixel for that palette index: `dst = ((const + * pixelfmt_any32_t *) pm->entries)[palette_index]`. + * + * The table is owned by the pixelmap module and lives in a small cache. A + * caller that blits in a loop calls \ref pixelmap_get once, before the loop. + */ +typedef struct pixelmap +{ + pixelfmt_t srcfmt; + pixelfmt_t destfmt; + int dest_log2bpp; /* 0..3, deep->paletted only */ + unsigned char entry_bytes; /* 0 = packed paletted, else deep */ + unsigned char rbits, gbits, bbits; + unsigned char rshift, gshift, bshift; /* into the source pixel */ + unsigned int nentries; + const unsigned char *entries; +} +pixelmap_t; + +/* ----------------------------------------------------------------------- */ + +/** + * Return a cached table converting a `srcfmt` pixel to `destfmt` under + * `palette`. + * + * Exactly one of `srcfmt` / `destfmt` must be paletted (p1/p2/p4/p8) and the + * other a 32bpp `*8888` format. `palette` is always the paletted side's + * palette. + * + * The returned pointer is owned by the pixelmap module and stays valid until + * a later \ref pixelmap_get call with a different key evicts it. Callers + * that blit in a loop fetch it once, before the loop. + * + * \param[in] srcfmt Source pixel format. + * \param[in] destfmt Destination pixel format. + * \param[in] palette Palette of the paletted side. + * \param[in] nentries Number of entries in `palette`. + * \return A cached table, or NULL if the format pair is unsupported, + * `palette` is NULL, or allocation failed. + */ +const pixelmap_t *pixelmap_get(pixelfmt_t srcfmt, + pixelfmt_t destfmt, + const colour_t *palette, + int nentries); + +/* ----------------------------------------------------------------------- */ + +#ifdef __cplusplus +} +#endif + +#endif /* FRAMEBUF_PIXELMAP_H */ diff --git a/include/framebuf/screen.h b/include/framebuf/screen.h index aa8087f..6c9b668 100644 --- a/include/framebuf/screen.h +++ b/include/framebuf/screen.h @@ -81,6 +81,23 @@ void screen_fill_rect(screen_t *scr, size2d_t size, colour_t colour); +/** + * Fills several solid rectangles in one call, all in the same colour. + * + * Equivalent to calling `screen_fill_rect` once per box. A non-positive + * `nboxes` draws nothing. Each box is inclusive-exclusive and clipped + * independently to the screen's clip region. + * + * \param[in] scr Screen to draw upon. + * \param[in] boxes Array of boxes to fill. + * \param[in] nboxes Number of boxes in `boxes`. + * \param[in] colour Colour of the rectangles. + */ +void screen_fill_rects(screen_t *scr, + const box_t *boxes, + int nboxes, + colour_t colour); + /** * Special case of `screen_fill_rect`. * @@ -279,6 +296,29 @@ void screen_draw_rect(screen_t *scr, size2d_t size, colour_t colour); +/** + * Draws one 2px-wide bevel ring just inside `box`: colour `a` along the top + * and left edges, colour `b` along the bottom and right. The bottom edge + * owns both bottom corners so the ring closes with no gap or overlap. Only + * the ring is drawn -- fill the interior first if you want one. `box` is + * inclusive-exclusive, as elsewhere. Swap `a` and `b` for a sunken rather + * than a raised look; pass the same colour for both for a plain 2px frame. + * Nest calls on successively inset boxes to build a thicker surround (as the + * wuss action-button border does). + * + * Assumes `box` is at least 4x4; smaller boxes lose their edge shape but + * draw harmlessly. Clipped to the screen's clip region. + * + * \param[in] scr Screen to draw upon. + * \param[in] box Box whose inner 2px border is drawn. + * \param[in] a Colour of the top and left edges. + * \param[in] b Colour of the bottom and right edges. + */ +void screen_draw_bevel_edge(screen_t *scr, + const box_t *box, + colour_t a, + colour_t b); + /** * Draws a one-pixel unfilled circle outline (integer midpoint algorithm, no * anti-aliasing). Clipped to the screen's clip region. A negative radius diff --git a/include/framebuf/span-p1.h b/include/framebuf/span-p1.h new file mode 100644 index 0000000..1bf5c68 --- /dev/null +++ b/include/framebuf/span-p1.h @@ -0,0 +1,10 @@ +/* framebuf/span-p1.h -- P1 (1bpp paletted) format plot methods */ + +#ifndef SPAN_P1_H +#define SPAN_P1_H + +#include "framebuf/span.h" + +extern const span_t span_p1; + +#endif /* SPAN_P1_H */ diff --git a/include/framebuf/span-p2.h b/include/framebuf/span-p2.h new file mode 100644 index 0000000..dec43ea --- /dev/null +++ b/include/framebuf/span-p2.h @@ -0,0 +1,10 @@ +/* framebuf/span-p2.h -- P2 (2bpp paletted) format plot methods */ + +#ifndef SPAN_P2_H +#define SPAN_P2_H + +#include "framebuf/span.h" + +extern const span_t span_p2; + +#endif /* SPAN_P2_H */ diff --git a/include/io/namelist.h b/include/io/namelist.h new file mode 100644 index 0000000..e4b00ed --- /dev/null +++ b/include/io/namelist.h @@ -0,0 +1,42 @@ +/* io/namelist.h -- collect matching dir leafnames into a fixed table */ + +#ifndef DPTLIB_NAMELIST_H +#define DPTLIB_NAMELIST_H + +#include + +#include "base/result.h" + +/** + * Scan a directory (via dirscan_walk) and copy the leafnames that end in \p + * ext into a caller-owned fixed-size table, with the extension stripped. + * + * The table is `cap` slots of `stride` bytes each, laid out contiguously + * (i.e. a `char names[cap][stride]`). An entry is stored only if its name + * with \p ext removed, plus a NUL, fits in \p stride; longer names are + * skipped silently. Scanning stops once \p cap names are stored -- this is + * not an error, `*pcount` simply reports the cap. + * + * \param[in] dir Directory to scan. + * \param[in] ext Extension to match and strip, leading dot included + * (e.g. ".png"). NULL or "" matches every entry and + * strips nothing. + * \param[out] names Base of the `cap`-by-`stride` character table. + * \param[in] stride Bytes per table slot (must be >= 2). + * \param[in] cap Number of slots in the table (must be >= 0). + * \param[in] sorted Non-zero to sort the stored names ascending (strcmp). + * \param[out] pcount Number of names stored. Set on every non-error return. + * \return \ref result_OK (including when the cap was hit), \ref + * result_NULL_ARG if \p dir, \p names or \p pcount is NULL, \ref + * result_BAD_ARG if \p stride < 2 or \p cap < 0, \ref + * result_FILE_NOT_FOUND if \p dir cannot be opened. + */ +result_t namelist_scan(const char *dir, + const char *ext, + char *names, + size_t stride, + int cap, + int sorted, + int *pcount); + +#endif /* DPTLIB_NAMELIST_H */ diff --git a/include/test/all-tests.h b/include/test/all-tests.h index aca2be7..91612cb 100644 --- a/include/test/all-tests.h +++ b/include/test/all-tests.h @@ -23,9 +23,11 @@ extern testfn_t pickle_test, tagdb_test; /* framebuf */ -extern testfn_t bmfont_test, +extern testfn_t bitmap_rle_test, + bmfont_test, composite_test, curve_test, + pixelmap_test, screen_test; /* geom */ diff --git a/include/utils/rng.h b/include/utils/rng.h new file mode 100644 index 0000000..ca701b5 --- /dev/null +++ b/include/utils/rng.h @@ -0,0 +1,113 @@ +/* utils/rng.h -- tiny deterministic pseudo-random number generators */ + +/** + * \file rng.h + * + * Small, seedable, header-only PRNGs for reproducible procedural content + * (dither, scatter, wireframe jitter, test fixtures). + * + * Every generator here is a pure function of its state word, so re-seeding + * with the same value always replays the same sequence -- unlike the C + * library's rand(), which shares one hidden global. State is a single + * uint32_t the caller owns. + * + * Two step functions are provided. rng_xorshift32 is Marsaglia's xorshift32: + * good bit avalanche, period 2^32 - 1, never returns 0 and must not be + * seeded 0. rng_lcg32 is the Numerical Recipes linear congruential + * generator: full period 2^32, but its low bits are weak, so take from the + * top (rng_range below already does). + * + * Prefer rng_xorshift32 unless you specifically need to match an existing + * LCG stream. + */ + +#ifndef UTILS_RNG_H +#define UTILS_RNG_H + +#ifdef __cplusplus +extern "C" +{ +#endif + +#include + +/* ----------------------------------------------------------------------- */ + +/** Generator state. A single 32-bit word the caller stores. */ +typedef uint32_t rng_t; + +/** + * Seed a generator. + * + * A zero seed is remapped to 1 so rng_xorshift32 (which cannot use a zero + * state) is always safe; this leaves rng_lcg32 unable to produce the single + * state value 0 from a fresh seed, which is harmless. + * + * \param[out] s Generator state. + * \param[in] seed Seed value. + */ +static __inline__ void rng_seed(rng_t *s, uint32_t seed) +{ + *s = seed ? seed : 1u; +} + +/** + * Advance the state one xorshift32 step and return it. + * + * \param[in,out] s Generator state (must be non-zero; rng_seed guarantees + * this). + * \return The new state word. + */ +static __inline__ uint32_t rng_xorshift32(rng_t *s) +{ + uint32_t x; + + x = *s; + x ^= x << 13; + x ^= x >> 17; + x ^= x << 5; + *s = x; + + return x; +} + +/** + * Advance the state one LCG step and return it. + * + * Constants are those from Numerical Recipes. The low-order bits cycle + * short; consumers wanting a bounded value should use rng_range, which + * samples the high bits. + * + * \param[in,out] s Generator state. + * \return The new state word. + */ +static __inline__ uint32_t rng_lcg32(rng_t *s) +{ + *s = *s * 1664525u + 1013904223u; + + return *s; +} + +/** + * Return a value in [0, n) using an xorshift32 step. + * + * Uses a plain modulo, so the distribution is very slightly biased for n + * that do not divide 2^32; negligible for the small n typical of grid and + * palette indexing. n must be positive. + * + * \param[in,out] s Generator state. + * \param[in] n Exclusive upper bound (> 0). + * \return A value in [0, n). + */ +static __inline__ int rng_range(rng_t *s, int n) +{ + return (int) (rng_xorshift32(s) % (uint32_t) n); +} + +/* ----------------------------------------------------------------------- */ + +#ifdef __cplusplus +} +#endif + +#endif /* UTILS_RNG_H */ diff --git a/include/wuss/component/info.h b/include/wuss/component/info.h new file mode 100644 index 0000000..bdaa984 --- /dev/null +++ b/include/wuss/component/info.h @@ -0,0 +1,111 @@ +/* wuss/component/info.h -- a label:value grid dialogue */ + +/** + * \file info.h + * + * A shared wuss component: a small fixed window laid out as a grid of + * label:value rows -- the base of the RISC OS "Info" / Toolbox ProgInfo + * dialogue, and of any dialogue that is just a column of named readouts. + * + * A wuss_info owns one window (titlebar caption only -- no close, back, + * toggle, scrollbars or resize), created hidden on a task the caller passes + * in and laid out from an array of wuss_info_row_t: each row is a label + * right-justified in a left column and a value centred in a sunken display + * field beside it. The window is sized from the task's font metrics and the + * widest label and value. + * + * It is meant to be used as a wuss_menu_item_t::window -- point an "Info" + * 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 + * 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 + * (wuss_menu_close) before wuss_info_destroy. + * + * Built only when WUSS_COMPONENTS is defined (which implies WUSS_MENUS). + */ + +#ifndef WUSS_COMPONENT_INFO_H +#define WUSS_COMPONENT_INFO_H + +#ifdef __cplusplus +extern "C" +{ +#endif + +#include "base/result.h" + +#include "wuss/task.h" +#include "wuss/window.h" + +/* ----------------------------------------------------------------------- */ + +/** + * One row of an info dialogue: a label and its value. Both are borrowed and + * copied by wuss_info_create. + */ +typedef struct wuss_info_row +{ + const char *label; /**< Left-column caption, right-justified. */ + const char *value; /**< Right-column readout, shown in a sunken field. */ +} +wuss_info_row_t; + +/** Opaque handle: owns the dialogue window and the strings in it. */ +typedef struct wuss_info wuss_info_t; + +/* ----------------------------------------------------------------------- */ + +/** + * Build a label:value grid dialogue: a hidden window on \p task, one row per + * entry in \p rows. + * + * The window's on-screen size is derived from \p task's font metrics and the + * text in \p rows; 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_info_window()). + * + * \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] title Titlebar caption; borrowed and copied by + * wuss_window_create. + * \param[in] rows Row array; each label and value is borrowed and copied. + * \param[in] nrows Number of rows; must be >= 1. + * \return \ref result_OK, \ref result_OOM, \ref result_NULL_ARG if \p out, + * \p task, \p title or \p rows is NULL, \ref result_BAD_ARG if \p + * nrows is < 1, or a wuss_window_create / wuss_icon_create code. + */ +result_t wuss_info_create(wuss_info_t **out, + wuss_task_t *task, + const char *title, + 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. + * + * \param[in] doomed Handle to free, or NULL. + */ +void wuss_info_destroy(wuss_info_t *doomed); + +/** + * The dialogue's window, for use as a wuss_menu_item_t::window or to show + * and hide directly. Borrowed; valid until wuss_info_destroy. NULL only if + * \p info is NULL. + * + * \param[in] info Handle. + * \return The window, or NULL. + */ +wuss_window_t *wuss_info_window(const wuss_info_t *info); + +#ifdef __cplusplus +} +#endif + +#endif /* WUSS_COMPONENT_INFO_H */ diff --git a/include/wuss/component/proginfo.h b/include/wuss/component/proginfo.h new file mode 100644 index 0000000..d909106 --- /dev/null +++ b/include/wuss/component/proginfo.h @@ -0,0 +1,107 @@ +/* wuss/component/proginfo.h -- a "program information" standard 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. + * + * 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 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. + * + * Built only when WUSS_COMPONENTS is defined (which implies WUSS_MENUS). + */ + +#ifndef WUSS_COMPONENT_PROGINFO_H +#define WUSS_COMPONENT_PROGINFO_H + +#ifdef __cplusplus +extern "C" +{ +#endif + +#include "base/result.h" + +#include "wuss/task.h" +#include "wuss/window.h" + +#include "wuss/component/info.h" + +/* ----------------------------------------------------------------------- */ + +/** + * 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. + */ +typedef struct wuss_proginfo_desc +{ + const char *name; /**< Program name; row "Name". */ + const char *purpose; /**< One-line description; row "Purpose". */ + const char *author; /**< Author / copyright line; row "Author". */ + const char *version; /**< Version string, e.g. "1.23 (14 Feb 2026)"; row + "Version". */ +} +wuss_proginfo_desc_t; + +/** + * Opaque handle: a wuss_info_t. Owns the dialogue window and its strings. + */ +typedef wuss_info_t 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()). + * + * \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. + * \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. + */ +result_t wuss_proginfo_create(wuss_proginfo_t **out, + wuss_task_t *task, + const wuss_proginfo_desc_t *desc); + +/** + * Free a program-information dialogue: closes its window and frees every + * string in it. Safe to pass NULL. An alias for wuss_info_destroy. + * + * \param[in] doomed Handle to free, or NULL. + */ +#define wuss_proginfo_destroy wuss_info_destroy + +/** + * 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. + * + * \param[in] pi Handle. + * \return The window, or NULL. + */ +#define wuss_proginfo_window wuss_info_window + +#ifdef __cplusplus +} +#endif + +#endif /* WUSS_COMPONENT_PROGINFO_H */ diff --git a/include/wuss/icon.h b/include/wuss/icon.h index 94b36f2..57ade2b 100644 --- a/include/wuss/icon.h +++ b/include/wuss/icon.h @@ -105,6 +105,34 @@ typedef enum wuss_icon_type } wuss_icon_type_t; +/** + * The border drawn around a wuss_ICON_TYPE_LABEL, inside its bounding box. + * Ignored by every other icon type. Uses the window manager's bevel shades + * (wuss_config_t::bevel), the same as a button. + */ +typedef enum wuss_icon_border +{ + /** No border; the label fills its box as before. The default. */ + wuss_ICON_BORDER_NONE = 0, + /** A 1px raised bevel: light top/left, dark bottom/right, like a button at + * rest. */ + wuss_ICON_BORDER_RIDGE, + /** A 1px sunken bevel: dark top/left, light bottom/right -- a RISC OS-style + * read-only display field. */ + wuss_ICON_BORDER_GROOVE, + /** A 6px-per-edge "action" surround: a 2px raised outset, a 2px moat filled + * with wuss_COLOUR_ACCENT, then a 2px raised inset -- like a RISC OS + * default-action button. */ + wuss_ICON_BORDER_ACTION, + /** A 4px-per-edge "divider": two 2px bevels, an outer sunken ring wrapping an + * inner raised one. Drawn in a lighter pair of shades than RIDGE/GROOVE -- + * bevel.divider against bevel.light rather than the full light/dark + * contrast -- so it reads as a soft inset panel rather than a hard button + * edge. */ + wuss_ICON_BORDER_DIVIDER +} +wuss_icon_border_t; + /** Icon appearance and behaviour flags, combinable with bitwise OR. */ typedef enum wuss_icon_flags { @@ -120,9 +148,10 @@ 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, in the window - * manager's accent colours (see wuss_config_t::accent) instead of the - * ordinary bevel. Ignored by other icon types. */ + /** wuss_ICON_TYPE_BUTTON: 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. */ wuss_ICON_FLAGS_DEFAULT = 1 << 4, /** wuss_ICON_TYPE_BITMAP: hit-test the icon and raise wuss_EVENT_ICON on a * click, like a button. Without it a bitmap icon is pure decoration and @@ -157,6 +186,13 @@ wuss_icon_flags_t; /** Decode the font slot (0..3) from an icon's flags. */ #define wuss_ICON_FONT_OF(f) (((f) >> 9) & 3) +/** + * 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 + * zero-initialised spec reads as "no icon-set entry". + */ +#define wuss_ICON_SET(i) ((i) + 1) + /** * Description of an icon at creation. Copied by value into the icon; the * caller keeps ownership of \c text, which is copied. @@ -184,8 +220,15 @@ typedef struct wuss_icon_spec 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. */ + * 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 @@ -195,6 +238,10 @@ typedef struct wuss_icon_spec * 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; /** Appearance/behaviour flags. */ wuss_icon_flags_t flags; } @@ -350,6 +397,59 @@ int wuss_icon_get_selected(const wuss_icon_t *icon); */ void wuss_icon_set_selected(wuss_icon_t *icon, int selected); +/* ----------------------------------------------------------------------- */ + +/** + * Scan a directory for PNG images and load them as the window manager's icon + * set. Each ".png" file becomes one indexed entry: its pixels are loaded and + * then RLE-compressed in place (see \ref bitmap_compress), and its leafname + * sans ".png" is recorded so \ref wuss_icons_lookup can resolve it back to + * the index. Non-".png" entries are ignored. Entry indices are assigned in + * the order the platform's directory scan yields, which is unspecified -- + * address entries by name, not by a hardcoded index. + * + * Calling this again replaces the previous set (the old bitmaps and names + * are freed). \ref wuss_destroy frees the set. + * + * \param[in] wuss Window manager. + * \param[in] dir Directory to scan for "*.png". + * \return \ref result_OK on success (including an empty or missing + * directory, which yields a zero-length set), \ref result_OOM on + * allocation failure, or a code propagated from the PNG loader or + * the compressor (the set is left empty on failure). + */ +result_t wuss_icons_load(wuss_t *wuss, const char *dir); + +/** + * Number of entries in the loaded icon set (see \ref wuss_icons_load). Zero + * if none has been loaded. + * + * \param[in] wuss Window manager. + * \return The entry count. + */ +int wuss_icons_count(const wuss_t *wuss); + +/** + * Resolve an icon-set entry's name to its index. + * + * \param[in] wuss Window manager. + * \param[in] name Entry name: a loaded file's leafname without ".png". + * \return The 0-based index, or -1 if no entry has that name. Pass the index + * through \ref wuss_ICON_SET to put it in a spec's \c icon_set. + */ +int wuss_icons_lookup(const wuss_t *wuss, const char *name); + +/** + * Fetch an icon-set entry's (compressed) bitmap by index. + * + * \param[in] wuss Window manager. + * \param[in] index 0-based index, 0..\ref wuss_icons_count -1. + * \return The bitmap, owned by the window manager and valid until the next + * \ref wuss_icons_load or \ref wuss_destroy, or NULL if \p index is + * out of range. + */ +const bitmap_t *wuss_icons_bitmap(const wuss_t *wuss, int index); + #endif /* WUSS_ICONS */ #ifdef __cplusplus diff --git a/include/wuss/menu.h b/include/wuss/menu.h index 5dc7aa3..440fc81 100644 --- a/include/wuss/menu.h +++ b/include/wuss/menu.h @@ -108,6 +108,12 @@ typedef struct wuss__menu *wuss_menu_handle_t; * delivered to \p task's handle (with window == NULL); its data.menu_select * carries the (sub)menu, the item index and the release button. * + * If wuss instead closes the chain itself -- a click outside every menu + * window, or a later wuss_menu_open -- \p task's handle gets a + * wuss_EVENT_MENU_CLOSED (window == NULL, no data). A task that kept \p out + * must drop it there; the chain is freed by the time the event arrives. No + * such event follows a wuss_menu_close the task made. + * * \param[in] task Task opening the menu; receives wuss_EVENT_MENU_SELECT. * The menu windows are wuss-owned, not task's. * \param[in] menu Menu to show; borrowed, must outlive the open chain. diff --git a/include/wuss/task.h b/include/wuss/task.h index 7bed6dc..e3ea2af 100644 --- a/include/wuss/task.h +++ b/include/wuss/task.h @@ -60,7 +60,12 @@ typedef enum wuss_event_kind * vetoes it. Never fired by wuss_window_close. */ wuss_EVENT_PRE_CLOSE, /** A window has closed after a successful wuss_window_try_close. Never - * fired by wuss_window_close. */ + * fired by wuss_window_close. This is not a veto point: PRE_CLOSE has + * already been accepted and wuss frees the window the instant this + * handler returns, so the task must drop any wuss_window_t it holds for + * this window here -- keeping or using it afterwards is a use-after-free. + * Put "keep the window open" logic (e.g. an unsaved-changes prompt) in + * PRE_CLOSE and veto from there. */ wuss_EVENT_CLOSE, /** Wuss has finished its pending work; once per task per wuss_idle. */ wuss_EVENT_IDLE, @@ -72,7 +77,15 @@ typedef enum wuss_event_kind wuss_EVENT_PALETTE, /** A leaf menu item was picked; delivered to the task that opened the * menu. */ - wuss_EVENT_MENU_SELECT + wuss_EVENT_MENU_SELECT, + /** The menu chain the task opened has been closed by wuss itself -- a + * click outside every menu window, or another wuss_menu_open -- rather + * than by a pick or by the task's own wuss_menu_close. Delivered to the + * task that opened it (window == NULL); carries no data. A task that + * 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_kind_t; @@ -92,8 +105,8 @@ typedef wuss_event_kind_t wuss_window_event_kind_t; * no window (window == NULL): the app-wide notifications. * * Members: wuss_EVENT_IDLE, wuss_EVENT_QUIT, wuss_EVENT_PALETTE, - * wuss_EVENT_MENU_SELECT, wuss_EVENT_ICON (reserved for a future shared/dock - * element; nothing emits it yet). + * wuss_EVENT_MENU_SELECT, wuss_EVENT_MENU_CLOSED, wuss_EVENT_ICON (reserved + * for a future shared/dock element; nothing emits it yet). */ typedef wuss_event_kind_t wuss_task_event_kind_t; @@ -187,7 +200,8 @@ typedef struct wuss_event /* wuss_EVENT_OPEN, wuss_EVENT_PRE_SHOW, wuss_EVENT_SHOW, * wuss_EVENT_PRE_CLOSE, wuss_EVENT_CLOSE, wuss_EVENT_IDLE, - * wuss_EVENT_QUIT and wuss_EVENT_PALETTE carry no data. */ + * wuss_EVENT_QUIT, wuss_EVENT_PALETTE and wuss_EVENT_MENU_CLOSED carry + * no data. */ } data; } diff --git a/include/wuss/window.h b/include/wuss/window.h index 0358448..a491483 100644 --- a/include/wuss/window.h +++ b/include/wuss/window.h @@ -49,7 +49,7 @@ extern "C" * wuss_WINDOW_NO_TITLEBAR. * \param[in] flags Appearance flags, e.g. wuss_WINDOW_NO_TITLEBAR / * wuss_WINDOW_NO_OUTLINE, OR'd together, or - * wuss_WINDOW_NONE for the default furniture. + * 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 * wuss_backdrop_t). Set its colour to wuss_NO_BACKGROUND @@ -245,18 +245,21 @@ void wuss_window_get_content_bounds(const wuss_window_t *window, */ void wuss_window_invalidate(wuss_window_t *window, const box_t *local_box); -/** Mark a window's whole content area as dirty. Shorthand for +/** Mark a window's whole visible content rectangle as dirty -- not the + * parts of the document scrolled out of view; use + * wuss_window_invalidate_extent for that. Shorthand for * wuss_window_invalidate(window, NULL). */ -#define wuss_window_invalidate_all(window) \ +#define wuss_window_invalidate_visible(window) \ wuss_window_invalidate((window), NULL) /** * Mark a window's whole virtual document extent as dirty, not just the part - * currently on screen. wuss_window_invalidate_all only covers the visible - * content rectangle, which is enough when the document is no bigger than the - * window; use this when a change touches the whole document (a colour or - * theme swap, say) so every scroll position repaints correctly. The extent - * is clipped to what is actually visible before anything is queued. + * currently on screen. wuss_window_invalidate_visible only covers the + * visible content rectangle, which is enough when the document is no bigger + * than the window; use this when a change touches the whole document (a + * colour or theme swap, say) so every scroll position repaints correctly. + * The extent is clipped to what is actually visible before anything is + * queued. * * \param[in] window Window whose whole document changed. */ diff --git a/include/wuss/wuss.h b/include/wuss/wuss.h index 77bc9ef..836a0f8 100644 --- a/include/wuss/wuss.h +++ b/include/wuss/wuss.h @@ -36,6 +36,8 @@ extern "C" * colour). */ #define result_WUSS_BAD_ICON (result_BASE_WUSS + 2) +/** An icon-set index (see wuss_icons_load) was out of range. */ +#define result_WUSS_BAD_INDEX (result_BASE_WUSS + 3) /* ----------------------------------------------------------------------- */ @@ -119,7 +121,7 @@ typedef unsigned char wuss_colour_t; * the named colours pick the nearest system-palette entry to the RGB the * name implies, the chrome roles echo the matching wuss_config_t field. * Accepted anywhere a wuss_colour_t is: config furniture/bevel/accent/ - * backdrop, window backgrounds (see wuss_window_create), icon specs. + * body/backdrop, window backgrounds (see wuss_window_create), icon specs. * wuss_NO_BACKGROUND is not symbolic and always passes through unchanged. */ #define wuss_COLOUR_SYMBOLIC ((wuss_colour_t) 128) @@ -138,14 +140,24 @@ typedef unsigned char wuss_colour_t; /* Chrome roles: echo the matching wuss_config_t field, resolved to a * concrete index -- e.g. wuss_COLOUR_TITLE_BG is furniture.title.bg, * wuss_COLOUR_BUTTON_HILIGHT is bevel.light, wuss_COLOUR_BUTTON_SHADOW is - * bevel.dark, wuss_COLOUR_BACKDROP is backdrop.colour. */ -#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) -#define wuss_COLOUR_BUTTON_SHADOW (wuss_COLOUR_SYMBOLIC + 19) -#define wuss_COLOUR_ACCENT_BG (wuss_COLOUR_SYMBOLIC + 20) -#define wuss_COLOUR_ACCENT_FG (wuss_COLOUR_SYMBOLIC + 21) -#define wuss_COLOUR_BACKDROP (wuss_COLOUR_SYMBOLIC + 22) + * bevel.dark, wuss_COLOUR_BORDER_DIVIDER is bevel.divider, + * wuss_COLOUR_BUTTON_BG is button.bg, wuss_COLOUR_BUTTON_FG is button.fg, + * 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. */ +#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) +#define wuss_COLOUR_BUTTON_SHADOW (wuss_COLOUR_SYMBOLIC + 19) +#define wuss_COLOUR_ACCENT (wuss_COLOUR_SYMBOLIC + 20) +#define wuss_COLOUR_BUTTON_FG (wuss_COLOUR_SYMBOLIC + 21) +#define wuss_COLOUR_BACKDROP (wuss_COLOUR_SYMBOLIC + 22) +#define wuss_COLOUR_WINDOW (wuss_COLOUR_SYMBOLIC + 23) +#define wuss_COLOUR_MENU (wuss_COLOUR_SYMBOLIC + 24) +#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) /** 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 @@ -160,6 +172,8 @@ typedef struct wuss_furniture_palette wuss_colour_t fg; /**< Titlebar text. */ } title; + wuss_colour_t outline; /**< Window outline. wuss_NO_BACKGROUND means + follow the titlebar fill. */ wuss_colour_t back; /**< Send-to-back icon. */ wuss_colour_t close; /**< Close icon. */ wuss_colour_t toggle; /**< Toggle-size icon. */ @@ -184,7 +198,7 @@ wuss_furniture_palette_t; typedef enum wuss_window_flags { /** Default: every furniture region drawn. */ - wuss_WINDOW_NONE = 0, + wuss_WINDOW_DEFAULT = 0, /** * No titlebar; content fills the full visible area, and no drag handle @@ -239,7 +253,16 @@ typedef enum wuss_window_flags * at runtime, and it is honoured regardless of the WUSS_FURNITURE build * option. */ - wuss_WINDOW_HIDDEN = 1 << 9 + wuss_WINDOW_HIDDEN = 1 << 9, + + /** + * No wuss_EVENT_REDRAW is delivered for this window: wuss fills its + * backdrop and draws its icons unaided, which is the whole of its + * appearance. For a window whose content is entirely backdrop colour + * and/or icons (a label-only dialogue, say) this saves the task a no-op + * redraw handler and a window-handle check in it. + */ + wuss_WINDOW_NO_REDRAW = 1 << 10 } wuss_window_flags_t; @@ -294,9 +317,9 @@ wuss_backdrop_t; * Optional creation-time configuration. * * \note titlebar_height and palette are ignored when the library is built - * with WUSS_FURNITURE off; bevel and accent are ignored when built - * with both WUSS_FURNITURE and WUSS_ICONS off. backdrop is always - * honoured. See the backdrop sub-struct for its own notes. + * with WUSS_FURNITURE off; bevel, button and accent are ignored when + * built with both WUSS_FURNITURE and WUSS_ICONS off. backdrop and body + * are always honoured. See the backdrop sub-struct for its own notes. */ typedef struct wuss_config { @@ -310,33 +333,73 @@ typedef struct wuss_config wuss_furniture_palette_t furniture; /** - * Bevelled work-area button edge shades, as indices into the system - * palette: light on the top/left edges, dark on the bottom/right (swapped - * when the button is pressed). Both default to the titlebar fill colour - * when config is NULL. Ignored when both WUSS_FURNITURE and WUSS_ICONS are - * off. + * Bevel edge shades for work-area buttons and bordered labels, as indices + * into the system palette: light on the top/left edges, dark on the + * bottom/right (swapped when a button is pressed). divider is the lighter + * edge shade for a wuss_ICON_BORDER_DIVIDER label surround, paired with + * light for its sunken and raised rings. light and dark default to the + * titlebar fill colour when config is NULL; divider defaults to light. + * Read back through wuss_COLOUR_BUTTON_HILIGHT / wuss_COLOUR_BUTTON_SHADOW + * / wuss_COLOUR_BORDER_DIVIDER. Ignored when both WUSS_FURNITURE and + * WUSS_ICONS are off. */ struct { - wuss_colour_t light; /**< Top/left bevel edge. */ - wuss_colour_t dark; /**< Bottom/right bevel edge. */ + wuss_colour_t light; /**< Top/left bevel edge. */ + wuss_colour_t dark; /**< Bottom/right bevel edge. */ + wuss_colour_t divider; /**< Lighter edge shade for a DIVIDER border. */ } bevel; /** - * Fill and text colours for a default action button -- a work-area button - * icon created with wuss_ICON_FLAGS_DEFAULT, drawn to stand out from the - * ordinary bevelled buttons around it (RISC OS's "default action button"). - * Both default to the titlebar colours (bg / fg) when config is NULL. - * Ignored when both WUSS_FURNITURE and WUSS_ICONS are off. + * Work-area button colours, as indices into the system palette. bg is the + * button face, used when a wuss_ICON_TYPE_BUTTON 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 + * defaults to the titlebar text colour. Read back through + * wuss_COLOUR_BUTTON_BG / wuss_COLOUR_BUTTON_FG / + * wuss_COLOUR_BUTTON_PRESSED. Ignored when both WUSS_FURNITURE and + * WUSS_ICONS are off. */ struct { - wuss_colour_t bg; /**< Default-button fill. */ - wuss_colour_t fg; /**< Default-button text. */ + wuss_colour_t bg; /**< Button face fill. */ + wuss_colour_t fg; /**< Button label. */ + wuss_colour_t pressed; /**< Button face fill while held down. */ + } + button; + + /** + * Accent colour for a default action button -- a wuss_ICON_TYPE_BUTTON + * 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 + * surround. Its label uses button.fg. Defaults to the titlebar fill colour + * when config is NULL. Read back through wuss_COLOUR_ACCENT. Ignored when + * both WUSS_FURNITURE and WUSS_ICONS are off. + */ + struct + { + wuss_colour_t colour; /**< Default-button accent fill. */ } accent; + /** + * Conventional body fills, as system-palette indices: window is the + * work-area behind a task's content, menu the pop-up menu background. Read + * back through wuss_COLOUR_WINDOW / wuss_COLOUR_MENU. When config is NULL, + * window defaults to wuss_COLOUR_GREY (a light grey) and menu to + * wuss_COLOUR_WHITE. Always honoured, regardless of the WUSS_FURNITURE / + * WUSS_ICONS / WUSS_MENUS options. + */ + struct + { + wuss_colour_t window; /**< Work-area body fill. */ + wuss_colour_t menu; /**< Menu body fill. */ + } + body; + /** Desktop background, painted behind windows on every redraw. */ wuss_backdrop_t backdrop; } diff --git a/libraries/datastruct/cache/test/cache-test.c b/libraries/datastruct/cache/test/cache-test.c index e4fbaaa..3caad0b 100644 --- a/libraries/datastruct/cache/test/cache-test.c +++ b/libraries/datastruct/cache/test/cache-test.c @@ -63,9 +63,9 @@ static result_t cache_test_outer(const cacheconfig_t *config, size_t cachelength, int maxkey) { + result_t err; unsigned char *block; cache_t *cache; - result_t err; block = malloc(cachelength); if (block == NULL) diff --git a/libraries/framebuf/bitmap/bitmap.c b/libraries/framebuf/bitmap/bitmap.c index 139d70a..04c07a3 100644 --- a/libraries/framebuf/bitmap/bitmap.c +++ b/libraries/framebuf/bitmap/bitmap.c @@ -6,6 +6,7 @@ #include #include "framebuf/bitmap.h" +#include "framebuf/pixelmap.h" #include "framebuf/span-registry.h" result_t bitmap_init(bitmap_t *bm, @@ -72,6 +73,12 @@ void bitmap_clear(bitmap_t *bm, colour_t colour) assert(bm); + if (pixelfmt_is_rle(bm->format)) + { + assert(!"bitmap_clear on compressed bitmap"); + return; + } + log2bpp = pixelfmt_log2bpp(bm->format); px = colour_to_pixel(bm->palette, bm->palette ? 1 << (1 << log2bpp) : 0, @@ -126,19 +133,23 @@ void bitmap_clear(bitmap_t *bm, colour_t colour) static result_t bmconv_p4_to_bgrx8888(const bitmap_t *src, bitmap_t **pdst) { - result_t rc; - bitmap_t *dst; - pixelfmt_bgrx8888_t *outpixels; - pixelfmt_bgrx8888_t map[16]; - int i; - pixelfmt_p4_t *inpixels; - int x,y; + 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); - for (i = 0; i < 16; i++) - map[i] = colour_to_pixel(src->palette, 16, src->palette[i], pixelfmt_bgrx8888); + /* cached p4-palette -> bgrx8888 expansion table (built once, shared across + * conversions of the same palette) */ + pm = pixelmap_get(pixelfmt_p4, pixelfmt_bgrx8888, src->palette, 16); + 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); // rowbytes rounding needed? if (outpixels == NULL) @@ -160,22 +171,120 @@ static result_t bmconv_p4_to_bgrx8888(const bitmap_t *src, bitmap_t **pdst) if (rc) return rc; - inpixels = src->base; + inrow = src->base; for (y = 0; y < src->size.h; y++) { - for (x = 0; x < src->size.w / 8; x++) - { - pixelfmt_p4_t in = *inpixels++; // fetches 8 pixels - // 0xABCDEFGH is 8 4bpp pixels shown H,G,F,E,D,C,B,A - *outpixels++ = map[(in >> 0) & 0xF]; - *outpixels++ = map[(in >> 4) & 0xF]; - *outpixels++ = map[(in >> 8) & 0xF]; - *outpixels++ = map[(in >> 12) & 0xF]; - *outpixels++ = map[(in >> 16) & 0xF]; - *outpixels++ = map[(in >> 20) & 0xF]; - *outpixels++ = map[(in >> 24) & 0xF]; - *outpixels++ = map[(in >> 28) & 0xF]; - } + /* per-pixel, so widths that aren't a multiple of 8 keep their last + * w % 8 pixels, and row padding in src->rowbytes is respected */ + for (x = 0; x < src->size.w; x++) + *outpixels++ = map[(inrow[x >> 1] >> ((x & 1) << 2)) & 0xF]; + 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) +{ + 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_p1, pixelfmt_bgrx8888, src->palette, 2); + 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 >> 3] >> (7 - (x & 7))) & 1]; + inrow += src->rowbytes; + } + + *pdst = dst; + + return rc; +} + +/* As bmconv_p1_to_bgrx8888 but for 2bpp paletted, packed MSB-first (bits 7..6 + * are the leftmost pixel) to match the screen p2 plot helpers. */ +static result_t bmconv_p2_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_p2, pixelfmt_bgrx8888, src->palette, 4); + 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 >> 2] >> (6 - ((x & 3) << 1))) & 3]; + inrow += src->rowbytes; } *pdst = dst; @@ -189,8 +298,33 @@ result_t bitmap_convert(const bitmap_t *src, { *dst = NULL; + if (pixelfmt_is_rle(src->format)) + return result_NOT_SUPPORTED; + switch (src->format) { + case pixelfmt_p1: + switch (newfmt) + { + case pixelfmt_bgrx8888: + return bmconv_p1_to_bgrx8888(src, dst); + + default: + return result_NOT_SUPPORTED; + } + break; + + case pixelfmt_p2: + switch (newfmt) + { + case pixelfmt_bgrx8888: + return bmconv_p2_to_bgrx8888(src, dst); + + default: + return result_NOT_SUPPORTED; + } + break; + case pixelfmt_p4: switch (newfmt) { diff --git a/libraries/framebuf/bitmap/compress.c b/libraries/framebuf/bitmap/compress.c new file mode 100644 index 0000000..abd25a4 --- /dev/null +++ b/libraries/framebuf/bitmap/compress.c @@ -0,0 +1,643 @@ +/* framebuf/bitmap/compress.c -- RLE compress/decompress a read-only bitmap */ + +#include +#include +#include +#include +#include + +#include "base/debug.h" +#include "base/result.h" +#include "base/utils.h" +#include "utils/barith.h" + +#include "framebuf/pixelfmt.h" + +#include "framebuf/bitmap.h" + +#include "rle.h" + +/* ----------------------------------------------------------------------- */ + +void bitmap__rle_put32(uint8_t *p, uint32_t v) +{ + p[0] = (uint8_t) (v ); + p[1] = (uint8_t) (v >> 8); + p[2] = (uint8_t) (v >> 16); + p[3] = (uint8_t) (v >> 24); +} + +uint32_t bitmap__rle_get32(const uint8_t *p) +{ + return (uint32_t) p[0] + | (uint32_t) p[1] << 8 + | (uint32_t) p[2] << 16 + | (uint32_t) p[3] << 24; +} + +/* ----------------------------------------------------------------------- */ + +/* Read one pixel from `p` (1 or 4 bytes) as a host value. */ +static pixelfmt_any_t rle__load_pixel(const uint8_t *p, int log2bpp) +{ + if (log2bpp == 3) + return p[0]; + + return (pixelfmt_any_t) p[0] + | (pixelfmt_any_t) p[1] << 8 + | (pixelfmt_any_t) p[2] << 16 + | (pixelfmt_any_t) p[3] << 24; +} + +/* ----------------------------------------------------------------------- */ + +/* + * Encoder cursor. `p` walks the output, `end` is one past the buffer; + * `overflow` latches once a write would pass `end`. + */ +typedef struct rle__enc +{ + uint8_t *p; + uint8_t *end; + int overflow; +} +rle__enc_t; + +static void rle__put(rle__enc_t *e, uint8_t b) +{ + if (e->p >= e->end) + { + e->overflow = 1; + return; + } + *e->p++ = b; +} + +static void rle__put_pixel(rle__enc_t *e, pixelfmt_any_t px, int bpp) +{ + int i; + + for (i = 0; i < bpp; i++) + rle__put(e, (uint8_t) (px >> (8 * i))); +} + +/* ZEROIS2N: a length equal to 2^bits is stored as a zero field. */ +static unsigned int rle__lenfield(int length, int bits) +{ + if (length == (1 << bits)) + return 0; + + return (unsigned int) length; +} + +/* Emit a literal run: `length` pixels starting at `src` (already at the run). */ +static void rle__emit_literal(rle__enc_t *e, + const uint8_t *src, + int length, + int bpp) +{ + int chunk; + int i; + + while (length > 0) + { + if (length <= bitmap__RLE_LITERAL_LEN_MAX) + { + chunk = length; + rle__put(e, (uint8_t) (bitmap__RLE_VAL(bitmap__RLE_LITERAL_ID) + | rle__lenfield(chunk, bitmap__RLE_LITERAL_LEN_BITS))); + } + else + { + unsigned int store; + + chunk = MIN(length, bitmap__RLE_LITERAL_LONG_LEN_MAX); + store = (unsigned int) (chunk - bitmap__RLE_LITERAL_LONG_LEN_MIN); + rle__put(e, (uint8_t) (bitmap__RLE_VAL(bitmap__RLE_LITERAL_LONG_ID) + | (store >> 8))); + rle__put(e, (uint8_t) store); + } + + for (i = 0; i < chunk * bpp; i++) + rle__put(e, src[i]); + + src += (size_t) chunk * bpp; + length -= chunk; + } +} + +/* Emit a repeat run: `length` copies of pixel `px`. */ +static void rle__emit_repeat(rle__enc_t *e, + pixelfmt_any_t px, + int length, + int bpp) +{ + int chunk; + + while (length > 0) + { + if (length <= bitmap__RLE_REPEAT_LEN_MAX) + { + chunk = length; + rle__put(e, (uint8_t) (bitmap__RLE_VAL(bitmap__RLE_REPEAT_ID) + | rle__lenfield(chunk, bitmap__RLE_REPEAT_LEN_BITS))); + } + else + { + unsigned int store; + + chunk = MIN(length, bitmap__RLE_REPEAT_LONG_LEN_MAX); + store = (unsigned int) (chunk - bitmap__RLE_REPEAT_LONG_LEN_MIN); + rle__put(e, (uint8_t) (bitmap__RLE_VAL(bitmap__RLE_REPEAT_LONG_ID) + | (store >> 8))); + rle__put(e, (uint8_t) store); + } + + rle__put_pixel(e, px, bpp); + length -= chunk; + } +} + +/* Emit a skip run: `length` transparent pixels. */ +static void rle__emit_skip(rle__enc_t *e, int length) +{ + int chunk; + + while (length > 0) + { + if (length <= bitmap__RLE_SKIP_LEN_MAX) + { + chunk = length; + rle__put(e, (uint8_t) (bitmap__RLE_VAL(bitmap__RLE_SKIP_ID) + | rle__lenfield(chunk, bitmap__RLE_SKIP_LEN_BITS))); + } + else + { + unsigned int store; + + chunk = MIN(length, bitmap__RLE_SKIP_LONG_LEN_MAX); + store = (unsigned int) (chunk - bitmap__RLE_SKIP_LONG_LEN_MIN); + rle__put(e, (uint8_t) (bitmap__RLE_VAL(bitmap__RLE_SKIP_LONG_ID) + | (store >> 8))); + rle__put(e, (uint8_t) store); + } + + length -= chunk; + } +} + +/* ----------------------------------------------------------------------- */ + +result_t bitmap__rle_encode_row(const void *srcrow, + int npix, + int log2bpp, + int has_alpha, + uint8_t *dst, + size_t dstcap, + size_t *dstused) +{ + rle__enc_t e; + const uint8_t *src; + int bpp; + int i; + int runstart; + + assert(srcrow); + assert(dst); + assert(log2bpp == 3 || log2bpp == 5); + + e.p = dst; + e.end = dst + dstcap; + e.overflow = 0; + + src = srcrow; + bpp = 1 << (log2bpp - 3); + + i = 0; + while (i < npix) + { + pixelfmt_any_t px0; + int run; + int transparent; + + px0 = rle__load_pixel(src + (size_t) i * bpp, log2bpp); + + /* Maximal identical run from i. */ + run = 1; + while (i + run < npix + && rle__load_pixel(src + (size_t) (i + run) * bpp, log2bpp) == px0) + run++; + + /* A skip run drops the pixel bytes entirely, so it must be reversible: + * only wholly-zero pixels qualify (the common PNG transparent value). + * Non-zero-but-alpha-0 pixels go through as literal/repeat. */ + transparent = has_alpha && (px0 == 0); + + if (run >= 2 || transparent) + { + if (transparent) + rle__emit_skip(&e, run); + else + rle__emit_repeat(&e, px0, run, bpp); + i += run; + continue; + } + + /* Not worth a repeat: accumulate a literal run until the next repeatable + * pair (or a transparent pixel for alpha formats). */ + runstart = i; + i++; + while (i < npix) + { + pixelfmt_any_t a; + + a = rle__load_pixel(src + (size_t) i * bpp, log2bpp); + + if (has_alpha && a == 0) + break; + if (i + 1 < npix + && rle__load_pixel(src + (size_t) (i + 1) * bpp, log2bpp) == a) + break; + i++; + } + rle__emit_literal(&e, src + (size_t) runstart * bpp, i - runstart, bpp); + } + + rle__put(&e, bitmap__RLE_EOL_BYTE); + + if (e.overflow) + return result_BUFFER_OVERFLOW; + + *dstused = (size_t) (e.p - dst); + return result_OK; +} + +/* ----------------------------------------------------------------------- */ + +const uint8_t *bitmap__rle_decode_row(const uint8_t *p, + const uint8_t *end, + int log2bpp, + void *dstrow, + int skip, + int plot, + int zero_skip) +{ + uint8_t *dst; + int bpp; + + assert(p); + assert(end); + assert(log2bpp == 3 || log2bpp == 5); + + dst = dstrow; + bpp = 1 << (log2bpp - 3); + + while (p < end) + { + uint8_t op; + int id; + int length; + int take; /* pixels of this run actually written */ + int drop; /* pixels of this run swallowed by `skip` */ + + op = *p++; + id = clz8(op); + + if (id == bitmap__RLE_EOL_ID) + return p; + + /* Decode opcode + length; `srcpx` (literal) / `px` (repeat) set below. */ + if (id == bitmap__RLE_LITERAL_ID) + { + length = op & (bitmap__RLE_LITERAL_LEN_MAX - 1); + if (length == 0) + length = bitmap__RLE_LITERAL_LEN_MAX; + } + else if (id == bitmap__RLE_REPEAT_ID) + { + length = op & (bitmap__RLE_REPEAT_LEN_MAX - 1); + if (length == 0) + length = bitmap__RLE_REPEAT_LEN_MAX; + } + else if (id == bitmap__RLE_SKIP_ID) + { + length = op & (bitmap__RLE_SKIP_LEN_MAX - 1); + if (length == 0) + length = bitmap__RLE_SKIP_LEN_MAX; + } + else if (id == bitmap__RLE_LITERAL_LONG_ID) + { + length = ((op & (bitmap__RLE_VAL(bitmap__RLE_LITERAL_LONG_ID) - 1)) << 8) + | *p++; + length += bitmap__RLE_LITERAL_LONG_LEN_MIN; + } + else if (id == bitmap__RLE_REPEAT_LONG_ID) + { + length = ((op & (bitmap__RLE_VAL(bitmap__RLE_REPEAT_LONG_ID) - 1)) << 8) + | *p++; + length += bitmap__RLE_REPEAT_LONG_LEN_MIN; + } + else if (id == bitmap__RLE_SKIP_LONG_ID) + { + length = ((op & (bitmap__RLE_VAL(bitmap__RLE_SKIP_LONG_ID) - 1)) << 8) + | *p++; + length += bitmap__RLE_SKIP_LONG_LEN_MIN; + } + else + { + /* Reserved / forward-incompatible opcode, or a corrupt stream. Bail to + * `end` so this row and every later row decode to nothing rather than + * garbage from a desynchronised cursor. */ + assert(!"bitmap__rle_decode_row: reserved opcode"); + return end; + } + + /* Clip this run against the remaining left-skip and plot budget. The + * stream is still walked to EOL after `plot` hits 0 (take clamps to 0), + * so the returned cursor always lands on the next row. */ + drop = MIN(skip, length); + skip -= drop; + take = length - drop; + if (take > plot) + take = plot; + if (take < 0) + take = 0; + + switch (id) + { + case bitmap__RLE_LITERAL_ID: + case bitmap__RLE_LITERAL_LONG_ID: + if (take > 0) + memcpy(dst, p + (size_t) drop * bpp, (size_t) take * bpp); + p += (size_t) length * bpp; + dst += (size_t) take * bpp; + plot -= take; + break; + + case bitmap__RLE_REPEAT_ID: + case bitmap__RLE_REPEAT_LONG_ID: + if (bpp == 1) + { + if (take > 0) + memset(dst, p[0], (size_t) take); + } + else + { + int k; + + for (k = 0; k < take; k++) + memcpy(dst + (size_t) k * bpp, p, (size_t) bpp); + } + p += bpp; + dst += (size_t) take * bpp; + plot -= take; + break; + + default: /* skip */ + if (zero_skip && take > 0) + memset(dst, 0, (size_t) take * bpp); + dst += (size_t) take * bpp; + plot -= take; + break; + } + } + + return p; +} + +/* ----------------------------------------------------------------------- */ + +const uint8_t *bitmap__rle_skip_row(const uint8_t *p, + const uint8_t *end, + int log2bpp) +{ + int bpp; + + assert(log2bpp == 3 || log2bpp == 5); + bpp = 1 << (log2bpp - 3); + + while (p < end) + { + uint8_t op; + int id; + int length; + + op = *p++; + id = clz8(op); + + if (id == bitmap__RLE_EOL_ID) + break; + + switch (id) + { + case bitmap__RLE_LITERAL_ID: + length = op & (bitmap__RLE_LITERAL_LEN_MAX - 1); + if (length == 0) + length = bitmap__RLE_LITERAL_LEN_MAX; + p += (size_t) length * bpp; + break; + + case bitmap__RLE_REPEAT_ID: + p += bpp; + break; + + case bitmap__RLE_SKIP_ID: + break; + + case bitmap__RLE_LITERAL_LONG_ID: + length = ((op & (bitmap__RLE_VAL(bitmap__RLE_LITERAL_LONG_ID) - 1)) << 8) + | *p++; + length += bitmap__RLE_LITERAL_LONG_LEN_MIN; + p += (size_t) length * bpp; + break; + + case bitmap__RLE_REPEAT_LONG_ID: + p += 1; /* low length byte */ + p += bpp; /* inline pixel */ + break; + + case bitmap__RLE_SKIP_LONG_ID: + p += 1; /* low length byte */ + break; + + default: + assert(!"bitmap__rle_skip_row: reserved opcode"); + return end; + } + } + + return p; +} + +/* ----------------------------------------------------------------------- */ + +/* Is `fmt` a format the RLE codec accepts? Sets *has_alpha for the alpha + * formats whose alpha lives in the top byte (bgra/rgba), which are the ones + * that get `skip` runs. + * ponytail: abgr8888/argb8888 carry alpha in the low byte; they still + * compress but with no skip runs. Add a per-format alpha shift if sparse + * abgr/argb art shows up. */ +static int rle__format_ok(pixelfmt_t fmt, int *has_alpha) +{ + *has_alpha = 0; + + switch (fmt) + { + case pixelfmt_bgra8888: + case pixelfmt_rgba8888: + *has_alpha = 1; + return 1; + + case pixelfmt_y8: + case pixelfmt_bgrx8888: + case pixelfmt_rgbx8888: + case pixelfmt_xbgr8888: + case pixelfmt_xrgb8888: + case pixelfmt_abgr8888: + case pixelfmt_argb8888: + return 1; + + default: + return 0; + } +} + +/* ----------------------------------------------------------------------- */ + +result_t bitmap_compress(bitmap_t *bm) +{ + result_t rc; + pixelfmt_t base; + int has_alpha; + int log2bpp; + int bpp; + int w, h; + size_t worst; + size_t rowworst; + uint8_t *blob; + uint8_t *cursor; + const uint8_t *srcrow; + uint32_t data_len; + uint8_t *shrunk; + int y; + + assert(bm); + + if (pixelfmt_is_rle(bm->format)) + return result_OK; /* already compressed */ + + base = pixelfmt_base(bm->format); + if (!rle__format_ok(base, &has_alpha)) + return result_NOT_SUPPORTED; + + log2bpp = pixelfmt_log2bpp(base); + bpp = 1 << (log2bpp - 3); + w = bm->size.w; + h = bm->size.h; + + /* Worst case per row: the encoder can be driven to a one-byte control per + * pixel (e.g. singleton, pair, singleton, pair ... forces a literal-of-1 + * then a repeat-of-2 for every three source pixels), so budget one control + * byte plus the pixel bytes for every pixel, one extra control per 128 px + * for the literal-long split, plus an EOL. For bpp >= 4 the w*bpp term + * dominates and this is barely above the old estimate; for y8 it is the + * difference between fitting and a spurious result_BUFFER_OVERFLOW. */ + rowworst = (size_t) w * (bpp + 1) + + ((size_t) w / bitmap__RLE_LITERAL_LEN_MAX + 1) + + 1; + worst = bitmap__RLE_HEADER_SIZE + rowworst * (size_t) h; + + blob = malloc(worst); + if (blob == NULL) + return result_OOM; + + cursor = blob + bitmap__RLE_HEADER_SIZE; + srcrow = bm->base; + for (y = 0; y < h; y++) + { + size_t used; + + rc = bitmap__rle_encode_row(srcrow, w, log2bpp, has_alpha, + cursor, worst - (size_t) (cursor - blob), + &used); + if (rc != result_OK) + { + free(blob); + return rc; + } + + cursor += used; + srcrow += bm->rowbytes; + } + + data_len = (uint32_t) (cursor - blob - bitmap__RLE_HEADER_SIZE); + + if ((size_t) bitmap__RLE_HEADER_SIZE + data_len >= (size_t) bm->rowbytes * h) + logf_warning("bitmap_compress: %ux%u RLE grew to %u bytes from %zu", + (unsigned) w, (unsigned) h, + (unsigned) (bitmap__RLE_HEADER_SIZE + data_len), + (size_t) bm->rowbytes * h); + + bitmap__rle_put32(blob + 0, (uint32_t) bm->rowbytes); + bitmap__rle_put32(blob + 4, data_len); + + shrunk = realloc(blob, bitmap__RLE_HEADER_SIZE + data_len); + if (shrunk != NULL) + blob = shrunk; + + free(bm->base); + bm->base = blob; + bm->format = base | pixelfmt_RLE_FLAG; + bm->rowbytes = 0; + /* bm->span and bm->palette stay valid for the decoded (base) format. */ + + return result_OK; +} + +/* ----------------------------------------------------------------------- */ + +result_t bitmap_decompress(bitmap_t *bm) +{ + pixelfmt_t base; + int log2bpp; + uint32_t rowbytes; + const uint8_t *blob; + const uint8_t *p; + const uint8_t *end; + uint8_t *raw; + uint8_t *dstrow; + int y; + + assert(bm); + + if (!pixelfmt_is_rle(bm->format)) + return result_OK; /* not compressed */ + + assert(bm->base != NULL); + + base = pixelfmt_base(bm->format); + log2bpp = pixelfmt_log2bpp(base); + + blob = bm->base; + rowbytes = bitmap__rle_get32(blob + 0); + p = blob + bitmap__RLE_HEADER_SIZE; + end = p + bitmap__rle_get32(blob + 4); + + raw = malloc((size_t) rowbytes * bm->size.h); + if (raw == NULL) + return result_OOM; + + dstrow = raw; + for (y = 0; y < bm->size.h; y++) + { + p = bitmap__rle_decode_row(p, end, log2bpp, dstrow, 0, bm->size.w, 1); + dstrow += rowbytes; + } + + free(bm->base); + bm->base = raw; + bm->format = base; + bm->rowbytes = (int) rowbytes; + + return result_OK; +} diff --git a/libraries/framebuf/bitmap/fill-pattern.c b/libraries/framebuf/bitmap/fill-pattern.c index f30ea2e..57d4f0d 100644 --- a/libraries/framebuf/bitmap/fill-pattern.c +++ b/libraries/framebuf/bitmap/fill-pattern.c @@ -26,6 +26,9 @@ result_t bitmap_fill_pattern(bitmap_t *bm, assert(bm); assert(pattern); + if (pixelfmt_is_rle(bm->format)) + return result_NOT_SUPPORTED; + log2bpp = pixelfmt_log2bpp(bm->format); if (log2bpp != 3 && log2bpp != 5) return result_NOT_SUPPORTED; @@ -39,7 +42,7 @@ result_t bitmap_fill_pattern(bitmap_t *bm, { clip = full; } - else if (!box_intersection(area, &full, &clip)) + else if (box_intersection(area, &full, &clip)) { return result_OK; /* nothing to fill */ } diff --git a/libraries/framebuf/bitmap/rle.h b/libraries/framebuf/bitmap/rle.h new file mode 100644 index 0000000..4fee9f0 --- /dev/null +++ b/libraries/framebuf/bitmap/rle.h @@ -0,0 +1,152 @@ +/* framebuf/bitmap/rle.h -- private RLE bitmap encode/decode shared internals */ + +#ifndef FRAMEBUF_BITMAP_RLE_H +#define FRAMEBUF_BITMAP_RLE_H + +#include +#include + +#include "base/result.h" + +/* ----------------------------------------------------------------------- */ + +/** + * Blob header prefixing a compressed bitmap's `base`. Two little-endian + * `uint32_t`s, written and read byte-wise (see bitmap__rle_put32 / + * bitmap__rle_get32) so the on-blob layout is host-endian independent. + */ +#define bitmap__RLE_HEADER_SIZE 8 + +/* ----------------------------------------------------------------------- */ + +/* + * Row opcode stream. The opcode of the first byte of each command is its + * count of leading zero bits (clz8), UTF-8-prefix style. Lengths are pixel + * counts. A zero length field means 2^(field width). Long forms store + * `length - shortmax - 1` so their range begins where the short form ends. + * + * 1LLLLLLL literal : 1..128 px, then len px inline + * 01LLLLLL repeat : 1..64 copies of one inline px + * 001LLLLL skip : 1..32 transparent px [alpha only] + * 0001LLLL LLLLLLLL literal long : 129..4224 px, then len px inline + * 00001LLL LLLLLLLL repeat long : 65..2112 copies + * 000001LL LLLLLLLL skip long : 33..1056 px [alpha only] + * 00000001 EOL + * 00000000 reserved (decoder rejects) + */ + +#define bitmap__RLE_LITERAL_ID 0 +#define bitmap__RLE_REPEAT_ID 1 +#define bitmap__RLE_SKIP_ID 2 +#define bitmap__RLE_LITERAL_LONG_ID 3 +#define bitmap__RLE_REPEAT_LONG_ID 4 +#define bitmap__RLE_SKIP_LONG_ID 5 +/* id 6 reserved */ +#define bitmap__RLE_EOL_ID 7 + +#define bitmap__RLE_LITERAL_LEN_BITS 7 +#define bitmap__RLE_REPEAT_LEN_BITS 6 +#define bitmap__RLE_SKIP_LEN_BITS 5 +#define bitmap__RLE_LITERAL_LONG_LEN_BITS 12 +#define bitmap__RLE_REPEAT_LONG_LEN_BITS 11 +#define bitmap__RLE_SKIP_LONG_LEN_BITS 10 + +#define bitmap__RLE_LITERAL_LEN_MAX (1 << bitmap__RLE_LITERAL_LEN_BITS) /* 128 */ +#define bitmap__RLE_REPEAT_LEN_MAX (1 << bitmap__RLE_REPEAT_LEN_BITS) /* 64 */ +#define bitmap__RLE_SKIP_LEN_MAX (1 << bitmap__RLE_SKIP_LEN_BITS) /* 32 */ + +#define bitmap__RLE_LITERAL_LONG_LEN_MIN (bitmap__RLE_LITERAL_LEN_MAX + 1) +#define bitmap__RLE_REPEAT_LONG_LEN_MIN (bitmap__RLE_REPEAT_LEN_MAX + 1) +#define bitmap__RLE_SKIP_LONG_LEN_MIN (bitmap__RLE_SKIP_LEN_MAX + 1) + +#define bitmap__RLE_LITERAL_LONG_LEN_MAX \ + ((1 << bitmap__RLE_LITERAL_LONG_LEN_BITS) - 1 + bitmap__RLE_LITERAL_LONG_LEN_MIN) +#define bitmap__RLE_REPEAT_LONG_LEN_MAX \ + ((1 << bitmap__RLE_REPEAT_LONG_LEN_BITS) - 1 + bitmap__RLE_REPEAT_LONG_LEN_MIN) +#define bitmap__RLE_SKIP_LONG_LEN_MAX \ + ((1 << bitmap__RLE_SKIP_LONG_LEN_BITS) - 1 + bitmap__RLE_SKIP_LONG_LEN_MIN) + +#define bitmap__RLE_EOL_BYTE 0x01u + +/* First byte of each short opcode: a single set bit at position (7 - id). */ +#define bitmap__RLE_VAL(id) ((0x80u >> (id)) & 0xFFu) + +/* ----------------------------------------------------------------------- */ + +/** Write `v` as four little-endian bytes to `p`. */ +void bitmap__rle_put32(uint8_t *p, uint32_t v); + +/** Read four little-endian bytes from `p`. */ +uint32_t bitmap__rle_get32(const uint8_t *p); + +/* ----------------------------------------------------------------------- */ + +/** + * Encode one source row into the opcode stream. + * + * \param[in] srcrow Row of `npix` pixels, `1 << (log2bpp - 3)` bytes + * each. + * \param[in] npix Pixels in the row. + * \param[in] log2bpp 3 (1-byte pixels) or 5 (4-byte pixels). + * \param[in] has_alpha Non-zero to emit `skip` for runs of alpha-0 pixels. + * \param[out] dst Output cursor. + * \param[in] dstcap Bytes available at `dst`. + * \param[out] dstused Bytes written (including the EOL). + * \return \ref result_OK, or \ref result_BUFFER_OVERFLOW if `dstcap` is + * short. + */ +result_t bitmap__rle_encode_row(const void *srcrow, + int npix, + int log2bpp, + int has_alpha, + uint8_t *dst, + size_t dstcap, + size_t *dstused); + +/** + * Decode one row of the opcode stream into `dstrow`, honouring a left-skip + * and a plot count for clipping. + * + * Literal runs are `memcpy`d, repeat runs splatted, `skip` runs advance the + * destination pointer only (whole-byte formats, so the copy is + * format-agnostic given `bpp`). `dstrow` points at the first pixel to write + * (already offset to the clip's left edge); `skip` pixels of the row are + * consumed before writing begins and at most `plot` pixels are written. + * + * \param[in] p Cursor at the row's first opcode. + * \param[in] end One past the last valid stream byte. Decoding stops + * here even if no EOL is seen; a reserved opcode also + * bails to `end`, so a truncated or corrupt blob + * decodes to nothing rather than reading past the + * buffer. + * \param[in] log2bpp 3 or 5. + * \param[in] dstrow Destination, at the first pixel to be written. + * \param[in] skip Row pixels to consume before writing. + * \param[in] plot Maximum pixels to write. + * \param[in] zero_skip Non-zero to write zero pixels for `skip` runs + * (lossless decode); zero to leave them untouched + * (alpha-tested blit). + * \return Cursor just past this row's EOL (clamped to `end`). + */ +const uint8_t *bitmap__rle_decode_row(const uint8_t *p, + const uint8_t *end, + int log2bpp, + void *dstrow, + int skip, + int plot, + int zero_skip); + +/** + * Advance past one row's opcodes without decoding, for reaching a clipped + * top row. + * + * \param[in] p Cursor at the row's first opcode. + * \param[in] end One past the last valid stream byte. + * \param[in] log2bpp 3 or 5 (sizes inline literal/repeat pixels). + * \return Cursor just past this row's EOL (clamped to `end`). + */ +const uint8_t *bitmap__rle_skip_row(const uint8_t *p, + const uint8_t *end, + int log2bpp); + +#endif /* FRAMEBUF_BITMAP_RLE_H */ diff --git a/libraries/framebuf/bitmap/test/rle-test.c b/libraries/framebuf/bitmap/test/rle-test.c new file mode 100644 index 0000000..a70e95a --- /dev/null +++ b/libraries/framebuf/bitmap/test/rle-test.c @@ -0,0 +1,382 @@ +/* framebuf/bitmap/test/rle-test.c -- RLE-compressed bitmap tests */ + +#include +#include +#include +#include +#include + +#include "base/result.h" +#include "io/path.h" + +#include "framebuf/pixelfmt.h" + +#include "framebuf/bitmap.h" +#include "framebuf/screen.h" + +#include "../rle.h" + +/* ----------------------------------------------------------------------- */ + +#define W 256 +#define H 200 + +/* ----------------------------------------------------------------------- */ + +/* Encode one row then decode it unclipped, compare byte-for-byte. */ +static int roundtrip_row(const void *row, + int npix, + int log2bpp, + int has_alpha) +{ + uint8_t enc[4 * W + W / 64 + 16]; + uint8_t dec[4 * W]; + size_t used; + result_t rc; + int bpp; + + bpp = 1 << (log2bpp - 3); + + memset(dec, 0xAB, sizeof dec); + + rc = bitmap__rle_encode_row(row, npix, log2bpp, has_alpha, + enc, sizeof enc, &used); + if (rc != result_OK) + { + fprintf(stderr, "roundtrip_row: encode rc=&%x\n", rc); + return 0; + } + + (void) bitmap__rle_decode_row(enc, enc + used, log2bpp, dec, 0, npix, 1); + + if (memcmp(dec, row, (size_t) npix * bpp) != 0) + { + fprintf(stderr, "roundtrip_row: mismatch (npix=%d log2bpp=%d)\n", + npix, log2bpp); + return 0; + } + return 1; +} + +/* ----------------------------------------------------------------------- */ + +static int test_synthetic(void) +{ + uint32_t r32[W]; + uint8_t r8[W]; + int i; + int ok = 1; + + /* flat */ + for (i = 0; i < W; i++) { r32[i] = 0xFF204060u; r8[i] = 0x7F; } + ok &= roundtrip_row(r32, W, 5, 1); + ok &= roundtrip_row(r8, W, 3, 0); + + /* all transparent (fully zero: exercises skip runs) */ + for (i = 0; i < W; i++) r32[i] = 0x00000000u; + ok &= roundtrip_row(r32, W, 5, 1); + + /* non-zero but alpha-0: must survive as literal/repeat, not skip */ + for (i = 0; i < W; i++) r32[i] = 0x00123456u; + ok &= roundtrip_row(r32, W, 5, 1); + + /* zero and non-zero-transparent interleaved with opaque */ + for (i = 0; i < W; i++) + r32[i] = (i % 5 == 0) ? 0x00000000u + : (i % 5 == 1) ? 0x00AABBCCu + : 0xFF000000u | (uint32_t) i; + ok &= roundtrip_row(r32, W, 5, 1); + + /* high-noise */ + for (i = 0; i < W; i++) { r32[i] = 0xFF000000u | (uint32_t) (i * 2654435761u); + r8[i] = (uint8_t) (i * 37 + 1); } + ok &= roundtrip_row(r32, W, 5, 1); + ok &= roundtrip_row(r8, W, 3, 0); + + /* alternating pairs */ + for (i = 0; i < W; i++) { r32[i] = (i & 1) ? 0xFF111111u : 0xFFEEEEEEu; + r8[i] = (i & 1) ? 0x10 : 0xE0; } + ok &= roundtrip_row(r32, W, 5, 1); + ok &= roundtrip_row(r8, W, 3, 0); + + /* single pixel */ + r32[0] = 0xFF010203u; r8[0] = 0x42; + ok &= roundtrip_row(r32, 1, 5, 1); + ok &= roundtrip_row(r8, 1, 3, 0); + + /* run lengths straddling the short/long boundary */ + for (i = 0; i < W; i++) { r32[i] = 0xFF777777u; r8[i] = 0x55; } + ok &= roundtrip_row(r32, 128, 5, 1); /* exactly LITERAL_LEN_MAX repeats */ + ok &= roundtrip_row(r32, 129, 5, 1); + ok &= roundtrip_row(r8, 64, 3, 0); /* REPEAT_LEN_MAX */ + ok &= roundtrip_row(r8, 65, 3, 0); + + /* literal run straddling the boundary: no two adjacent pixels equal */ + for (i = 0; i < W; i++) r8[i] = (uint8_t) ((i * 131 + 7) | 1); + for (i = 1; i < W; i++) if (r8[i] == r8[i - 1]) r8[i] ^= 2; + ok &= roundtrip_row(r8, 128, 3, 0); + ok &= roundtrip_row(r8, 129, 3, 0); + + return ok; +} + +/* ----------------------------------------------------------------------- */ + +/* Reference blit matching the RLE decoder: copy every pixel except a wholly + * zero one (which the encoder turns into a skip run). */ +static void ref_blit(uint32_t *dst, + int dst_rowwords, + const uint32_t *src, + int src_rowwords, + int w, + int h) +{ + int x, y; + + for (y = 0; y < h; y++) + for (x = 0; x < w; x++) + { + uint32_t px = src[y * src_rowwords + x]; + if (px != 0) + dst[y * dst_rowwords + x] = px; + } +} + +static int test_blit(const char *resources) +{ + result_t rc; + bitmap_t bm; + bitmap_t cbm; + uint32_t *rawpix; + const char *filename; + int w, h; + int rowwords; + uint32_t *scr_ref; + uint32_t *scr_rle; + screen_t s; + box_t clip; + size_t npix; + int ok = 1; + + filename = path_join_filename(resources, 3, "resources", "composite", "A.png"); + rc = bitmap_load_png(&bm, filename); + if (rc) + { + fprintf(stderr, "test_blit: load rc=&%x\n", rc); + return 0; + } + /* bitmap_load_png yields rgba8888 (R,G,B,A byte order). */ + + w = bm.size.w; + h = bm.size.h; + npix = (size_t) w * h; + + /* Keep an untouched copy of the raw pixels. */ + rawpix = malloc(npix * 4); + memcpy(rawpix, bm.base, npix * 4); + + /* --- round trip --- */ + cbm = bm; + cbm.base = malloc(npix * 4); + memcpy(cbm.base, bm.base, npix * 4); + bitmap_init(&cbm, bm.size, bm.format, bm.rowbytes, NULL, cbm.base); + + rc = bitmap_compress(&cbm); + if (rc) { fprintf(stderr, "compress rc=&%x\n", rc); ok = 0; goto done; } + + if (!bitmap_is_compressed(&cbm) + || pixelfmt_base(cbm.format) != bm.format) + { + fprintf(stderr, "compress: flag/format wrong\n"); + ok = 0; goto done; + } + { + uint32_t data_len = bitmap__rle_get32((const uint8_t *) cbm.base + 4); + fprintf(stdout, "rle: A.png %zu raw -> %u encoded (%.2f)\n", + npix * 4, (unsigned) data_len, (double) data_len / (double) (npix * 4)); + if ((size_t) data_len >= npix * 4) + fprintf(stderr, "rle: warning: no size win on A.png\n"); + } + + rc = bitmap_decompress(&cbm); + if (rc) { fprintf(stderr, "decompress rc=&%x\n", rc); ok = 0; goto done; } + + if (cbm.rowbytes != bm.rowbytes || memcmp(cbm.base, rawpix, npix * 4) != 0) + { + fprintf(stderr, "round trip: pixels or rowbytes differ\n"); + ok = 0; goto done; + } + + /* --- blit equivalence, unclipped then clipped --- */ + rowwords = w; + scr_ref = malloc(npix * 4); + scr_rle = malloc(npix * 4); + + /* recompress for the blit */ + rc = bitmap_compress(&cbm); + if (rc) { fprintf(stderr, "compress2 rc=&%x\n", rc); ok = 0; goto done2; } + + /* unclipped */ + memset(scr_ref, 0, npix * 4); + memset(scr_rle, 0, npix * 4); + ref_blit(scr_ref, rowwords, rawpix, w, w, h); + + screen_init(&s, bm.size, pixelfmt_rgba8888, w * 4, NULL, scr_rle); + rc = screen_copy_bitmap(&s, 0, 0, &cbm); + if (rc) { fprintf(stderr, "blit rc=&%x\n", rc); ok = 0; goto done2; } + if (memcmp(scr_ref, scr_rle, npix * 4) != 0) + { + fprintf(stderr, "blit: unclipped mismatch\n"); + ok = 0; goto done2; + } + + /* clipped: crop top and left */ + memset(scr_ref, 0, npix * 4); + memset(scr_rle, 0, npix * 4); + { + int cx = w / 3, cy = h / 4; + int y; + + for (y = cy; y < h; y++) + ref_blit(scr_ref + y * rowwords + cx, rowwords, + rawpix + y * w + cx, w, w - cx, 1); + + clip.x0 = cx; clip.y0 = cy; clip.x1 = w; clip.y1 = h; + s.clip = clip; + rc = screen_copy_bitmap(&s, 0, 0, &cbm); + if (rc) { fprintf(stderr, "blit clipped rc=&%x\n", rc); ok = 0; goto done2; } + if (memcmp(scr_ref, scr_rle, npix * 4) != 0) + { + fprintf(stderr, "blit: clipped mismatch\n"); + ok = 0; goto done2; + } + } + + /* --- 4bpp screen: RLE path must match the raw p4 blit --- */ + { + colour_t pal[16]; + uint8_t *p4_ref; + uint8_t *p4_rle; + int p4_rowbytes; + int i; + + /* A spread of greys is enough for colour_to_pixel's nearest match to + * exercise more than one entry. */ + for (i = 0; i < 16; i++) + { + unsigned int g = (unsigned int) (i * 17); + pal[i].primary = 0xFF000000u | (g << 16) | (g << 8) | g; + } + + p4_rowbytes = (w + 1) / 2; + p4_ref = malloc((size_t) p4_rowbytes * h); + p4_rle = malloc((size_t) p4_rowbytes * h); + memset(p4_ref, 0, (size_t) p4_rowbytes * h); + memset(p4_rle, 0, (size_t) p4_rowbytes * h); + + rc = bitmap_decompress(&cbm); + if (rc) { fprintf(stderr, "p4 decompress rc=&%x\n", rc); ok = 0; } + + screen_init(&s, bm.size, pixelfmt_p4, p4_rowbytes, pal, p4_ref); + rc = screen_copy_bitmap(&s, 0, 0, &cbm); + if (rc) { fprintf(stderr, "p4 raw blit rc=&%x\n", rc); ok = 0; } + + rc = bitmap_compress(&cbm); + if (rc) { fprintf(stderr, "p4 compress rc=&%x\n", rc); ok = 0; } + + screen_init(&s, bm.size, pixelfmt_p4, p4_rowbytes, pal, p4_rle); + rc = screen_copy_bitmap(&s, 0, 0, &cbm); + if (rc) { fprintf(stderr, "p4 rle blit rc=&%x\n", rc); ok = 0; } + + if (ok && memcmp(p4_ref, p4_rle, (size_t) p4_rowbytes * h) != 0) + { + fprintf(stderr, "blit: 4bpp RLE vs raw mismatch\n"); + ok = 0; + } + + free(p4_ref); + free(p4_rle); + } + +done2: + free(scr_ref); + free(scr_rle); +done: + free(cbm.base); + free(rawpix); + free(bm.base); + return ok; +} + +/* ----------------------------------------------------------------------- */ + +static int test_y8(void) +{ + bitmap_t bm; + uint8_t *pix; + uint8_t *copy; + int i; + result_t rc; + int ok = 1; + size_t n = (size_t) W * H; + + pix = malloc(n); + copy = malloc(n); + for (i = 0; i < (int) n; i++) + pix[i] = (uint8_t) ((i / 17) ^ (i * 13)); + memcpy(copy, pix, n); + + bitmap_init(&bm, SIZE2D(W, H), pixelfmt_y8, W, NULL, pix); + + rc = bitmap_compress(&bm); + if (rc) { fprintf(stderr, "y8 compress rc=&%x\n", rc); ok = 0; goto done; } + rc = bitmap_decompress(&bm); + if (rc) { fprintf(stderr, "y8 decompress rc=&%x\n", rc); ok = 0; goto done; } + + if (bm.rowbytes != W || memcmp(bm.base, copy, n) != 0) + { + fprintf(stderr, "y8: round trip differs\n"); + ok = 0; + } + + /* Pathological expansion: singleton, pair, singleton, pair, ... forces a + * literal-of-1 then a repeat-of-2 per three pixels, ~1.33*w bytes/row. The + * worst-case allocation must cover it rather than returning + * result_BUFFER_OVERFLOW on this legal input. (bm.base was freed and + * replaced by the round trip above, so allocate afresh.) */ + { + uint8_t *p2 = malloc(n); + + for (i = 0; i < (int) n; i++) + p2[i] = (uint8_t) (i % 3 == 0 ? (i / 3 + 1) : 0x80); + memcpy(copy, p2, n); + bitmap_init(&bm, SIZE2D(W, H), pixelfmt_y8, W, NULL, p2); + rc = bitmap_compress(&bm); + if (rc) { fprintf(stderr, "y8 pathological compress rc=&%x\n", rc); ok = 0; goto done; } + rc = bitmap_decompress(&bm); + if (rc) { fprintf(stderr, "y8 pathological decompress rc=&%x\n", rc); ok = 0; goto done; } + if (memcmp(bm.base, copy, n) != 0) + { + fprintf(stderr, "y8 pathological: round trip differs\n"); + ok = 0; + } + } + +done: + free(bm.base); + free(copy); + return ok; +} + +/* ----------------------------------------------------------------------- */ + +result_t bitmap_rle_test(const char *resources) +{ + int ok = 1; + + ok &= test_synthetic(); + ok &= test_y8(); + 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 8157235..aae597d 100644 --- a/libraries/framebuf/bmfont/bmfont.c +++ b/libraries/framebuf/bmfont/bmfont.c @@ -986,6 +986,323 @@ static void bmfont_drawchar_p4_2w_t(void *vscreen, /* -------------------------------------------------------------------------- */ +/* Plot one glyph row onto a p1 (1bpp) screen row. "row" is the glyph bits + * already shifted down by right_skip, so bit (cw - 1) is the leftmost visible + * pixel. "scr" points at the byte holding pixel column 0; "shift" (0..7) is + * that pixel's bit offset from the byte's MSB, matching the MSB-first 1bpp + * packing used elsewhere. When "opaque" is zero, clear glyph bits are left + * untouched. */ +static void bmfont_p1_plot_row(unsigned char *scr, + unsigned int row, + int cw, + int shift, + pixelfmt_any_t fg, + pixelfmt_any_t bg, + int opaque) +{ + int i; + + for (i = 0; i < cw; i++) + { + int set; + int bitpos; + unsigned char *p; + int b; + + set = (row >> (cw - 1 - i)) & 1; + if (!set && !opaque) + continue; + + bitpos = shift + i; + p = scr + (bitpos >> 3); + b = 7 - (bitpos & 7); + + *p = (unsigned char) ((*p & ~(1 << b)) + | (((set ? fg : bg) & 1) << b)); + } +} + +/* As bmfont_p1_plot_row but for a p2 (2bpp) screen row: two bits per pixel, + * "shift" is a bit offset (0, 2, 4 or 6) and pixel column i lands at bit-pair + * (shift + 2 * i), MSB-first within the byte. */ +static void bmfont_p2_plot_row(unsigned char *scr, + unsigned int row, + int cw, + int shift, + pixelfmt_any_t fg, + pixelfmt_any_t bg, + int opaque) +{ + int i; + + for (i = 0; i < cw; i++) + { + int set; + int bitpos; + unsigned char *p; + int b; + + set = (row >> (cw - 1 - i)) & 1; + if (!set && !opaque) + continue; + + bitpos = shift + (i << 1); + p = scr + (bitpos >> 3); + b = 6 - (bitpos & 7); + + *p = (unsigned char) ((*p & ~(3 << b)) + | (((set ? fg : bg) & 3) << b)); + } +} + +/* Draw a character to a p1 screen. One helper covers 1- and 2-byte glyph rows + * and both opaque and transparent backgrounds -- the p1 inner loop is a plain + * per-pixel bit write, so the LUT-driven width unrolling the p4 path needs + * buys nothing here. */ +static void bmfont_drawchar_p1_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; + + assert(charwidth > 0); + assert(charheight > 0); + + gly += top_skip; + stride = rowbytes; + + while (charheight--) + { + unsigned int row = (unsigned int) *gly++ >> right_skip; + + bmfont_p1_plot_row(scr, row, charwidth, shift, fg, bg, 1); + scr += stride; + } +} + +static void bmfont_drawchar_p1_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; + + assert(charwidth > 0); + assert(charheight > 0); + + gly += top_skip; + stride = rowbytes; + + while (charheight--) + { + unsigned int row = (unsigned int) *gly++ >> right_skip; + + bmfont_p1_plot_row(scr, row, charwidth, shift, fg, bg, 0); + scr += stride; + } +} + +static void bmfont_drawchar_p1_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; + + assert(charwidth > 0); + assert(charheight > 0); + + gly += top_skip; + stride = rowbytes; + + while (charheight--) + { + unsigned int row = (unsigned int) *gly++ >> right_skip; + + bmfont_p1_plot_row(scr, row, charwidth, shift, fg, bg, 1); + scr += stride; + } +} + +static void bmfont_drawchar_p1_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; + + assert(charwidth > 0); + assert(charheight > 0); + + gly += top_skip; + stride = rowbytes; + + while (charheight--) + { + unsigned int row = (unsigned int) *gly++ >> right_skip; + + bmfont_p1_plot_row(scr, row, charwidth, shift, fg, bg, 0); + scr += stride; + } +} + +/* -------------------------------------------------------------------------- */ + +static void bmfont_drawchar_p2_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; + + assert(charwidth > 0); + assert(charheight > 0); + + gly += top_skip; + stride = rowbytes; + + while (charheight--) + { + unsigned int row = (unsigned int) *gly++ >> right_skip; + + bmfont_p2_plot_row(scr, row, charwidth, shift, fg, bg, 1); + scr += stride; + } +} + +static void bmfont_drawchar_p2_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; + + assert(charwidth > 0); + assert(charheight > 0); + + gly += top_skip; + stride = rowbytes; + + while (charheight--) + { + unsigned int row = (unsigned int) *gly++ >> right_skip; + + bmfont_p2_plot_row(scr, row, charwidth, shift, fg, bg, 0); + scr += stride; + } +} + +static void bmfont_drawchar_p2_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; + + assert(charwidth > 0); + assert(charheight > 0); + + gly += top_skip; + stride = rowbytes; + + while (charheight--) + { + unsigned int row = (unsigned int) *gly++ >> right_skip; + + bmfont_p2_plot_row(scr, row, charwidth, shift, fg, bg, 1); + scr += stride; + } +} + +static void bmfont_drawchar_p2_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; + + assert(charwidth > 0); + assert(charheight > 0); + + gly += top_skip; + stride = rowbytes; + + while (charheight--) + { + unsigned int row = (unsigned int) *gly++ >> right_skip; + + bmfont_p2_plot_row(scr, row, charwidth, shift, fg, bg, 0); + scr += stride; + } +} + +/* -------------------------------------------------------------------------- */ + /* Draw a character, a maximum of one byte wide, to any 8888 screen using an * opaque background. */ static void bmfont_drawchar_any8888_1w_o(void *vscreen, @@ -1198,6 +1515,24 @@ result_t bmfont_draw(bmfont_t *bmfont, switch (scr->format) { + case pixelfmt_p1: + switch (bmfont->glyphrowbytes) + { + case 1: drawfn = (bgalpha < 255) ? bmfont_drawchar_p1_1w_t : bmfont_drawchar_p1_1w_o; break; + case 2: drawfn = (bgalpha < 255) ? bmfont_drawchar_p1_2w_t : bmfont_drawchar_p1_2w_o; break; + default: assert(0); return result_NOT_SUPPORTED; + } + break; + + case pixelfmt_p2: + switch (bmfont->glyphrowbytes) + { + case 1: drawfn = (bgalpha < 255) ? bmfont_drawchar_p2_1w_t : bmfont_drawchar_p2_1w_o; break; + case 2: drawfn = (bgalpha < 255) ? bmfont_drawchar_p2_2w_t : bmfont_drawchar_p2_2w_o; break; + default: assert(0); return result_NOT_SUPPORTED; + } + break; + case pixelfmt_p4: switch (bmfont->glyphrowbytes) { @@ -1261,7 +1596,16 @@ result_t bmfont_draw(bmfont_t *bmfont, int x = pos->x; + /* box_intersects said something is visible, but a glyph can still be fully + * clipped vertically at a boundary. drawfn's `while (charheight--)` would + * then run ~INT_MAX times, so bail rather than trust the assert alone. */ assert(clippedcharheight > 0); + if (clippedcharheight <= 0) + { + if (end_pos) + *end_pos = POINT(pos->x, pos->y); + return result_OK; + } while (len--) { @@ -1271,7 +1615,15 @@ result_t bmfont_draw(bmfont_t *bmfont, const void *glyph; c = *text++; - gid = c - ' '; + + /* control characters and anything outside the glyph table draw nothing + * and advance nothing -- matching bmfont_measure. */ + if ((unsigned char) c < ' ') + continue; + gid = (unsigned char) c - ' '; + if (gid >= bmfont->totalchars) + continue; + advance = bmfont_advance_for(bmfont, gid) + tracking; x += advance; @@ -1334,8 +1686,13 @@ result_t bmfont_draw(bmfont_t *bmfont, int gid; int advance; - c = *text++; - gid = c - ' '; + c = *text++; + if ((unsigned char) c < ' ') + continue; + gid = (unsigned char) c - ' '; + if (gid >= bmfont->totalchars) + continue; + advance = bmfont_advance_for(bmfont, gid) + tracking; x += advance; @@ -1360,9 +1717,9 @@ result_t bmfont_draw_relief(bmfont_t *bmfont, const point_t *offset, point_t *end_pos) { + result_t rc; colour_t transparent; point_t shadowpos; - result_t rc; transparent = colour_rgba(0, 0, 0, 0); shadowpos = POINT(pos->x + offset->x, pos->y + offset->y); diff --git a/libraries/framebuf/bmfont/test/bmfont-test.c b/libraries/framebuf/bmfont/test/bmfont-test.c index 7633d6b..11c7f25 100644 --- a/libraries/framebuf/bmfont/test/bmfont-test.c +++ b/libraries/framebuf/bmfont/test/bmfont-test.c @@ -197,14 +197,19 @@ static bmtestfont_t bmfonts[MAXFONTS] = { "Digits-Bold", NULL } }; -/* Symbols.png isn't Latin text, so it's excluded from bmfonts[] above (used - * to draw lorem_ipsum in the clipping/layout tests) but must still show up - * in the enumerate test, which just walks the fixture directory. */ -#define MAXFONTS_ENUM 9 +/* Fixture PNGs not in bmfonts[] above (which is the Latin-text set the + * clipping/layout tests draw lorem_ipsum with) but which the enumerate test + * must still see, since it just walks the fixture directory: Symbols.png is + * not Latin text; 04b_03, 04b_25 and Nokia are tiny pixel faces added for + * the wuss tasks. */ +#define MAXFONTS_ENUM 12 static const char *bmfonts_enum_extra[MAXFONTS_ENUM - MAXFONTS] = { - "Symbols" + "Symbols", + "04b_03", + "04b_25", + "Nokia" }; /* ----------------------------------------------------------------------- */ @@ -767,9 +772,9 @@ static result_t bmfont_enum_cb(const char *name, static result_t bmfont_enumerate_test(const char *resources) { + result_t rc; const char *dir; bmfont_enum_check_t chk; - result_t rc; int i; dir = path_join_filename(resources, 2, "resources", "bmfonts"); @@ -1027,6 +1032,8 @@ result_t bmfont_test(const char *resources) } tab[] = { + { 800, 600, pixelfmt_p1 }, + { 800, 600, pixelfmt_p2 }, { 800, 600, pixelfmt_p4 }, { 800, 600, pixelfmt_bgrx8888 } }; diff --git a/libraries/framebuf/composite/composite.c b/libraries/framebuf/composite/composite.c index b2859cd..b6b66c9 100644 --- a/libraries/framebuf/composite/composite.c +++ b/libraries/framebuf/composite/composite.c @@ -964,6 +964,9 @@ result_t composite(composite_rule_t rule, src->format != dst->format) return result_BAD_ARG; + if (pixelfmt_is_rle(src->format) || pixelfmt_is_rle(dst->format)) + return result_NOT_SUPPORTED; + switch (src->format) { case pixelfmt_rgba8888: diff --git a/libraries/framebuf/pixelfmt/paletted-nentries.c b/libraries/framebuf/pixelfmt/paletted-nentries.c new file mode 100644 index 0000000..d7f4682 --- /dev/null +++ b/libraries/framebuf/pixelfmt/paletted-nentries.c @@ -0,0 +1,21 @@ +/* framebuf/pixelfmt/paletted-nentries.c -- palette entry count for a packed paletted format */ + +#include "framebuf/pixelfmt.h" + +int pixelfmt_paletted_nentries(pixelfmt_t fmt) +{ + switch (fmt) + { + case pixelfmt_p1: + return 2; + case pixelfmt_p2: + return 4; + case pixelfmt_p4: + return 16; + case pixelfmt_p8: + return 256; + + default: + return 0; + } +} diff --git a/libraries/framebuf/pixelmap/build.c b/libraries/framebuf/pixelmap/build.c new file mode 100644 index 0000000..373ab75 --- /dev/null +++ b/libraries/framebuf/pixelmap/build.c @@ -0,0 +1,109 @@ +/* framebuf/pixelmap/build.c -- fill a pixelmap's entry table */ + +#include +#include +#include + +#include "framebuf/colour.h" +#include "framebuf/pixelfmt.h" + +#include "impl.h" + +/* paletted -> deep: one destination pixel per palette entry, index = the raw + * palette index. */ +static int pixelmap__build_paletted_to_deep(pixelmap_t *pm, + const colour_t *palette, + int nentries) +{ + uint32_t *entries; + int i; + + entries = calloc(pm->nentries, sizeof(*entries)); + if (entries == NULL) + return -1; + + for (i = 0; i < nentries; i++) + entries[i] = (uint32_t) colour_to_pixel(palette, nentries, + palette[i], pm->destfmt); + + pm->entries = (const unsigned char *) entries; + + return 0; +} + +/* Replicate an n-bit value up to 8 bits (e.g. 4-bit 0xA -> 0xAA), so a + * quantised channel spans the full 0..255 range before the nearest-colour + * match. */ +static unsigned int pixelmap__expand(unsigned int v, unsigned int nbits) +{ + unsigned int out; + unsigned int filled; + + out = 0; + filled = 0; + + while (filled < 8) + { + out = (out << nbits) | v; + filled += nbits; + } + + return (out >> (filled - 8)) & 0xFF; +} + +int pixelmap__build_table(pixelmap_t *pm, + const colour_t *palette, + int nentries) +{ + unsigned char *entries; + size_t nbytes; + unsigned int per_byte; + unsigned int rbits, gbits, bbits; + unsigned int r, g, b; + unsigned int idx; + unsigned int pal; + colour_t c; + + if (pm->entry_bytes != 0) + return pixelmap__build_paletted_to_deep(pm, palette, nentries); + + rbits = pm->rbits; + gbits = pm->gbits; + bbits = pm->bbits; + + per_byte = 8u >> pm->dest_log2bpp; /* p1:8 p2:4 p4:2 p8:1 */ + nbytes = (pm->nentries + per_byte - 1) / per_byte; + + entries = calloc(nbytes, 1); + if (entries == NULL) + return -1; + + for (r = 0; r < (1u << rbits); r++) + for (g = 0; g < (1u << gbits); g++) + for (b = 0; b < (1u << bbits); b++) + { + idx = (r << (gbits + bbits)) | (g << bbits) | b; + + c = colour_rgb((int) pixelmap__expand(r, rbits), + (int) pixelmap__expand(g, gbits), + (int) pixelmap__expand(b, bbits)); + + pal = colour_to_pixel(palette, nentries, c, pm->destfmt) + & ((1u << (1u << pm->dest_log2bpp)) - 1); + + /* pack LSB-first: p8 is a plain byte store */ + if (pm->dest_log2bpp == 3) + entries[idx] = (unsigned char) pal; + else + { + unsigned int bit; + + bit = (idx % per_byte) << pm->dest_log2bpp; + entries[idx / per_byte] |= (unsigned char) (pal << bit); + } + } + + pm->entries = entries; + + return 0; +} diff --git a/libraries/framebuf/pixelmap/get.c b/libraries/framebuf/pixelmap/get.c new file mode 100644 index 0000000..720a986 --- /dev/null +++ b/libraries/framebuf/pixelmap/get.c @@ -0,0 +1,241 @@ +/* framebuf/pixelmap/get.c -- pixelmap lookup and cache */ + +#include +#include + +#include "framebuf/colour.h" +#include "framebuf/pixelfmt.h" + +#include "framebuf/pixelmap.h" + +#include "impl.h" + +/* ----------------------------------------------------------------------- */ + +static int pixelmap__is_deep(pixelfmt_t fmt) +{ + switch (fmt) + { + case pixelfmt_bgrx8888: + case pixelfmt_rgbx8888: + case pixelfmt_xbgr8888: + case pixelfmt_xrgb8888: + case pixelfmt_bgra8888: + case pixelfmt_rgba8888: + case pixelfmt_abgr8888: + case pixelfmt_argb8888: + return 1; + default: + return 0; + } +} + +static int pixelmap__paletted_log2bpp(pixelfmt_t fmt) +{ + switch (fmt) + { + case pixelfmt_p1: return 0; + case pixelfmt_p2: return 1; + case pixelfmt_p4: return 2; + case pixelfmt_p8: return 3; + default: return -1; + } +} + +/* deep -> paletted: fill the channel quantisation layout. p1/p2/p4 quantise to + * 4:4:4 (2KB max); p8 to 5:6:5 to keep 256-colour output faithful. */ +static int pixelmap__layout_deep_to_paletted(pixelfmt_t srcfmt, + int dest_log2bpp, + pixelmap_t *out) +{ + out->entry_bytes = 0; + + if (dest_log2bpp == 3) + { + out->rbits = 5; out->gbits = 6; out->bbits = 5; + } + else + { + out->rbits = 4; out->gbits = 4; out->bbits = 4; + } + + /* 8-bit channel extract from the source pixel */ + switch (srcfmt) + { + case pixelfmt_bgrx8888: + case pixelfmt_bgra8888: + out->rshift = 16; out->gshift = 8; out->bshift = 0; + break; + case pixelfmt_rgbx8888: + case pixelfmt_rgba8888: + out->rshift = 0; out->gshift = 8; out->bshift = 16; + break; + case pixelfmt_xbgr8888: + case pixelfmt_abgr8888: + out->rshift = 24; out->gshift = 16; out->bshift = 8; + break; + default: /* xrgb8888, argb8888 */ + out->rshift = 8; out->gshift = 16; out->bshift = 24; + break; + } + + out->nentries = 1u << (out->rbits + out->gbits + out->bbits); + + return 0; +} + +/* paletted -> deep: the index is the raw palette index, one deep pixel per + * entry. nentries comes from the palette, filled in by the caller. */ +static int pixelmap__layout_paletted_to_deep(int nentries, pixelmap_t *out) +{ + out->entry_bytes = 4; + out->rbits = out->gbits = out->bbits = 0; + out->rshift = out->gshift = out->bshift = 0; + out->nentries = (unsigned int) nentries; + + return 0; +} + +int pixelmap__layout_for(pixelfmt_t srcfmt, + pixelfmt_t destfmt, + pixelmap_t *out) +{ + int src_deep; + int dst_deep; + int dest_log2bpp; + + src_deep = pixelmap__is_deep(srcfmt); + dst_deep = pixelmap__is_deep(destfmt); + + out->srcfmt = srcfmt; + out->destfmt = destfmt; + out->dest_log2bpp = -1; + out->entries = NULL; + + if (src_deep && !dst_deep) + { + dest_log2bpp = pixelmap__paletted_log2bpp(destfmt); + if (dest_log2bpp < 0) + return -1; + out->dest_log2bpp = dest_log2bpp; + return pixelmap__layout_deep_to_paletted(srcfmt, dest_log2bpp, out); + } + + if (!src_deep && dst_deep) + { + if (pixelmap__paletted_log2bpp(srcfmt) < 0) + return -1; + /* nentries is set from the palette count by the caller-side layout call */ + return pixelmap__layout_paletted_to_deep(0, out); + } + + return -1; /* deep<->deep and paletted<->paletted are not pixelmap's job */ +} + +/* ----------------------------------------------------------------------- */ + +/* FNV-1a over the palette bytes. */ +static unsigned int pixelmap__hash(const colour_t *palette, int nentries) +{ + const unsigned char *p; + size_t n; + unsigned int h; + size_t i; + + p = (const unsigned char *) palette; + n = (size_t) nentries * sizeof(*palette); + h = 2166136261u; + + for (i = 0; i < n; i++) + { + h ^= p[i]; + h *= 16777619u; + } + + return h; +} + +/* ----------------------------------------------------------------------- */ + +#define PIXELMAP_CACHE_SIZE 4 + +struct pixelmap__cacheent +{ + int used; + pixelfmt_t srcfmt; + pixelfmt_t destfmt; + unsigned int palhash; + unsigned int lru; /* higher = more recently used */ + pixelmap_t map; +}; + +const pixelmap_t *pixelmap_get(pixelfmt_t srcfmt, + pixelfmt_t destfmt, + const colour_t *palette, + int nentries) +{ + static struct pixelmap__cacheent cache[PIXELMAP_CACHE_SIZE]; + static unsigned int clock; + + unsigned int palhash; + int victim; + unsigned int oldest; + int i; + + if (palette == NULL) + return NULL; + + palhash = pixelmap__hash(palette, nentries); + + for (i = 0; i < PIXELMAP_CACHE_SIZE; i++) + if (cache[i].used && + cache[i].srcfmt == srcfmt && + cache[i].destfmt == destfmt && + cache[i].palhash == palhash) + { + cache[i].lru = ++clock; + return &cache[i].map; + } + + /* miss: pick a victim (unused, else least-recently-used) */ + victim = 0; + oldest = cache[0].used ? cache[0].lru : 0; + for (i = 0; i < PIXELMAP_CACHE_SIZE; i++) + { + if (!cache[i].used) + { + victim = i; + break; + } + if (cache[i].lru < oldest) + { + oldest = cache[i].lru; + victim = i; + } + } + + if (cache[victim].used) + { + free((void *) cache[victim].map.entries); + cache[victim].used = 0; + } + + if (pixelmap__layout_for(srcfmt, destfmt, &cache[victim].map) != 0) + return NULL; + + /* paletted -> deep: the entry count is the palette size, not known to + * pixelmap__layout_for */ + if (cache[victim].map.entry_bytes != 0) + cache[victim].map.nentries = (unsigned int) nentries; + + if (pixelmap__build_table(&cache[victim].map, palette, nentries) != 0) + return NULL; + + cache[victim].used = 1; + cache[victim].srcfmt = srcfmt; + cache[victim].destfmt = destfmt; + cache[victim].palhash = palhash; + cache[victim].lru = ++clock; + + return &cache[victim].map; +} diff --git a/libraries/framebuf/pixelmap/impl.h b/libraries/framebuf/pixelmap/impl.h new file mode 100644 index 0000000..4ed94fb --- /dev/null +++ b/libraries/framebuf/pixelmap/impl.h @@ -0,0 +1,45 @@ +/* framebuf/pixelmap/impl.h -- pixelmap internals */ + +#ifndef PIXELMAP_IMPL_H +#define PIXELMAP_IMPL_H + +#include "framebuf/colour.h" +#include "framebuf/pixelfmt.h" + +#include "framebuf/pixelmap.h" + +/* ----------------------------------------------------------------------- */ + +/** + * 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). + * + * \param[in] srcfmt Source pixel format. + * \param[in] destfmt Destination format. + * \param[out] out Partly-filled pixelmap (entries not touched). + * \return 0 on success, -1 if the pair is unsupported. + */ +int pixelmap__layout_for(pixelfmt_t srcfmt, + pixelfmt_t destfmt, + pixelmap_t *out); + +/** + * Allocate and fill `pm->entries` from `palette`, using the channel layout + * already set in `pm` by \ref pixelmap__layout_for. + * + * \param[in,out] pm Pixelmap with its layout filled in. + * \param[in] palette Destination palette. + * \param[in] nentries Number of entries in `palette`. + * \return 0 on success, -1 on allocation failure. + */ +int pixelmap__build_table(pixelmap_t *pm, + const colour_t *palette, + int nentries); + +/* ----------------------------------------------------------------------- */ + +#endif /* PIXELMAP_IMPL_H */ diff --git a/libraries/framebuf/pixelmap/test/pixelmap-test.c b/libraries/framebuf/pixelmap/test/pixelmap-test.c new file mode 100644 index 0000000..5ce2813 --- /dev/null +++ b/libraries/framebuf/pixelmap/test/pixelmap-test.c @@ -0,0 +1,238 @@ +/* framebuf/pixelmap/test/pixelmap-test.c */ + +#include +#include + +#include "base/result.h" + +#include "framebuf/colour.h" +#include "framebuf/palettes.h" +#include "framebuf/pixelfmt.h" +#include "framebuf/pixelmap.h" + +#include "test/all-tests.h" + +/* compose the packed index for an rgba8888 source pixel */ +static unsigned int pm_index(const pixelmap_t *pm, pixelfmt_rgba8888_t px) +{ + unsigned int r, g, b; + + r = (px >> pm->rshift) & 0xFF; + g = (px >> pm->gshift) & 0xFF; + b = (px >> pm->bshift) & 0xFF; + + return ((r >> (8 - pm->rbits)) << (pm->gbits + pm->bbits)) + | ((g >> (8 - pm->gbits)) << pm->bbits) + | ( b >> (8 - pm->bbits)); +} + +static unsigned int pm_lookup(const pixelmap_t *pm, unsigned int idx) +{ + unsigned int per_byte; + unsigned int bit; + + if (pm->dest_log2bpp == 3) + return pm->entries[idx]; + + per_byte = 8u >> pm->dest_log2bpp; + bit = (idx % per_byte) << pm->dest_log2bpp; + + return (pm->entries[idx / per_byte] >> bit) & ((1u << (1u << pm->dest_log2bpp)) - 1); +} + +static int chan_dist(pixelfmt_rgba8888_t a, pixelfmt_rgba8888_t b) +{ + int dr, dg, db; + + dr = (int) PIXELFMT_Rxxx8888(a) - (int) PIXELFMT_Rxxx8888(b); + dg = (int) PIXELFMT_xGxx8888(a) - (int) PIXELFMT_xGxx8888(b); + db = (int) PIXELFMT_xxBx8888(a) - (int) PIXELFMT_xxBx8888(b); + + return dr * dr + dg * dg + db * db; +} + +result_t pixelmap_test(const char *resources) +{ + colour_t pal[palette_PICO8__LENGTH]; + const pixelmap_t *pm, *pm2, *pm8; + unsigned int idx; + int i; + + (void) resources; + + define_pico8_palette(pal); + + /* 1. rgba8888 -> p4: known colours land on the expected index */ + pm = pixelmap_get(pixelfmt_rgba8888, pixelfmt_p4, pal, palette_PICO8__LENGTH); + if (pm == NULL) + { + printf("pixelmap: get rgba8888->p4 returned NULL\n"); + return result_TEST_FAILED; + } + + for (i = 0; i < palette_PICO8__LENGTH; i++) + { + unsigned int got; + + idx = pm_index(pm, pal[i].primary); + got = pm_lookup(pm, idx); + + /* the exact palette colour, quantised to 4:4:4, should match itself or a + * near neighbour -- assert the resolved colour is close, not that the + * index is identical */ + if (chan_dist(pal[i].primary, pal[got].primary) > chan_dist(pal[i].primary, pal[i].primary) + 3 * 16 * 16) + { + printf("pixelmap: entry %d resolves too far (idx %u -> pal %u)\n", i, idx, got); + return result_TEST_FAILED; + } + } + + /* 2. same args -> same cached pointer */ + pm2 = pixelmap_get(pixelfmt_rgba8888, pixelfmt_p4, pal, palette_PICO8__LENGTH); + if (pm2 != pm) + { + printf("pixelmap: cache miss on identical request\n"); + return result_TEST_FAILED; + } + + /* 3. unsupported pair -> NULL */ + if (pixelmap_get(pixelfmt_rgba8888, pixelfmt_rgba8888, pal, palette_PICO8__LENGTH) != NULL) + { + printf("pixelmap: unsupported destfmt did not return NULL\n"); + return result_TEST_FAILED; + } + + /* 4. rgba8888 -> p8 (64KB table): every palette entry resolves close */ + pm8 = pixelmap_get(pixelfmt_rgba8888, pixelfmt_p8, pal, palette_PICO8__LENGTH); + if (pm8 == NULL) + { + printf("pixelmap: get rgba8888->p8 returned NULL\n"); + return result_TEST_FAILED; + } + if (pm8->nentries != 65536) + { + printf("pixelmap: p8 table has %u entries, expected 65536\n", pm8->nentries); + return result_TEST_FAILED; + } + + for (i = 0; i < palette_PICO8__LENGTH; i++) + { + unsigned int got; + + idx = pm_index(pm8, pal[i].primary); + got = pm_lookup(pm8, idx); + + /* 5:6:5 quantisation: within ~8 levels per channel */ + if (chan_dist(pal[i].primary, pal[got].primary) > 3 * 8 * 8) + { + printf("pixelmap: p8 entry %d resolves too far (pal %u)\n", i, got); + return result_TEST_FAILED; + } + } + + /* 4b. rgba8888 -> p1 (2KB 4:4:4 table): a 2-entry black/white palette + * resolves dark colours to index 0 and light ones to index 1 */ + { + const pixelmap_t *pm1; + colour_t bw[2]; + + bw[0] = colour_rgb(0x00, 0x00, 0x00); + bw[1] = colour_rgb(0xFF, 0xFF, 0xFF); + + pm1 = pixelmap_get(pixelfmt_rgba8888, pixelfmt_p1, bw, 2); + if (pm1 == NULL) + { + printf("pixelmap: get rgba8888->p1 returned NULL\n"); + return result_TEST_FAILED; + } + if (pm1->dest_log2bpp != 0 || pm1->nentries != 4096) + { + printf("pixelmap: p1 layout wrong (dest_log2bpp %d, nentries %u)\n", + pm1->dest_log2bpp, pm1->nentries); + return result_TEST_FAILED; + } + + if (pm_lookup(pm1, pm_index(pm1, colour_rgb(0x10, 0x10, 0x10).primary)) != 0) + { + printf("pixelmap: p1 dark colour did not resolve to index 0\n"); + return result_TEST_FAILED; + } + if (pm_lookup(pm1, pm_index(pm1, colour_rgb(0xF0, 0xF0, 0xF0).primary)) != 1) + { + printf("pixelmap: p1 light colour did not resolve to index 1\n"); + return result_TEST_FAILED; + } + } + + /* 4c. rgba8888 -> p2 (4:4:4 table): a 4-entry greyscale palette resolves + * each grey band to its nearest index */ + { + const pixelmap_t *pm2; + colour_t grey[4]; + + grey[0] = colour_rgb(0x00, 0x00, 0x00); + grey[1] = colour_rgb(0x55, 0x55, 0x55); + grey[2] = colour_rgb(0xAA, 0xAA, 0xAA); + grey[3] = colour_rgb(0xFF, 0xFF, 0xFF); + + pm2 = pixelmap_get(pixelfmt_rgba8888, pixelfmt_p2, grey, 4); + if (pm2 == NULL) + { + printf("pixelmap: get rgba8888->p2 returned NULL\n"); + return result_TEST_FAILED; + } + if (pm2->dest_log2bpp != 1 || pm2->nentries != 4096) + { + printf("pixelmap: p2 layout wrong (dest_log2bpp %d, nentries %u)\n", + pm2->dest_log2bpp, pm2->nentries); + return result_TEST_FAILED; + } + + if (pm_lookup(pm2, pm_index(pm2, colour_rgb(0x08, 0x08, 0x08).primary)) != 0) + { + printf("pixelmap: p2 black did not resolve to index 0\n"); + return result_TEST_FAILED; + } + if (pm_lookup(pm2, pm_index(pm2, colour_rgb(0xF8, 0xF8, 0xF8).primary)) != 3) + { + printf("pixelmap: p2 white did not resolve to index 3\n"); + return result_TEST_FAILED; + } + } + + /* 5. paletted -> deep: p4 -> bgrx8888, one deep pixel per palette index */ + { + const pixelmap_t *pmd; + const pixelfmt_bgrx8888_t *map; + + pmd = pixelmap_get(pixelfmt_p4, pixelfmt_bgrx8888, pal, palette_PICO8__LENGTH); + if (pmd == NULL) + { + printf("pixelmap: get p4->bgrx8888 returned NULL\n"); + return result_TEST_FAILED; + } + if (pmd->entry_bytes != 4 || pmd->nentries != palette_PICO8__LENGTH) + { + printf("pixelmap: p4->bgrx8888 layout wrong (entry_bytes %u, nentries %u)\n", + pmd->entry_bytes, pmd->nentries); + return result_TEST_FAILED; + } + + map = (const pixelfmt_bgrx8888_t *) pmd->entries; + for (i = 0; i < palette_PICO8__LENGTH; i++) + { + pixelfmt_bgrx8888_t want; + + want = (pixelfmt_bgrx8888_t) colour_to_pixel(pal, palette_PICO8__LENGTH, + pal[i], pixelfmt_bgrx8888); + if (map[i] != want) + { + printf("pixelmap: p4->bgrx8888 entry %d = 0x%08X, want 0x%08X\n", + i, map[i], want); + return result_TEST_FAILED; + } + } + } + + return result_TEST_PASSED; +} diff --git a/libraries/framebuf/screen/screen-copy-bitmap-rle.c b/libraries/framebuf/screen/screen-copy-bitmap-rle.c new file mode 100644 index 0000000..3f91735 --- /dev/null +++ b/libraries/framebuf/screen/screen-copy-bitmap-rle.c @@ -0,0 +1,208 @@ +/* framebuf/screen/screen-copy-bitmap-rle.c -- RLE bitmap blit */ + +#include +#include +#include +#include + +#include "base/result.h" + +#include "geom/box.h" + +#include "framebuf/colour.h" +#include "framebuf/pixelfmt.h" +#include "framebuf/pixelmap.h" + +#include "framebuf/bitmap.h" +#include "framebuf/screen.h" + +#include "../bitmap/rle.h" + +#include "screen-copy-bitmap-rle.h" + +/* Map a 32bpp 8888 format to its no-alpha ("x") sibling: bgra->bgrx, + * rgba->rgbx, etc. The RLE blit writes whole 4-byte pixels and honours skip + * runs for transparency, so an alpha source and its matching x screen (or the + * reverse) decode identically -- only the channel order has to agree. Passes + * non-8888 formats through unchanged. */ +static pixelfmt_t rle__dealpha(pixelfmt_t f) +{ + switch (f) + { + case pixelfmt_bgra8888: return pixelfmt_bgrx8888; + case pixelfmt_rgba8888: return pixelfmt_rgbx8888; + case pixelfmt_abgr8888: return pixelfmt_xbgr8888; + case pixelfmt_argb8888: return pixelfmt_xrgb8888; + default: return f; + } +} + +/* Blit a 32bpp RLE source onto a 4bpp paletted screen. The row codec only + * emits whole 4-byte pixels, so each visible row is decoded (skip runs + * zero-filled) into a scratch buffer, then converted pixel-by-pixel to the + * nearest palette index -- matching screen_copy_bitmap_p4's alpha-tested + * transfer. Zero-alpha pixels (skip runs, and genuinely transparent source + * pixels) leave the background alone. + * + * ponytail: an opaque source with a 0x00000000 pixel (only possible for the + * no-alpha *x8888 bases) loses that pixel, same trade-off as the raw p4 + * blit. Carry an explicit mask if opaque-black art shows up. */ +static result_t screen_copy_bitmap_rle_p4(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; + + /* The decode buffer is RGBA8888 byte order. The cached RGB->index table + * turns the per-pixel nearest-palette match into a mask-and-lookup. */ + pm = pixelmap_get(pixelfmt_rgba8888, scr->format, scr->palette, 16); + 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; + uint8_t *scrp; + int shift; + pixelfmt_any_t pxl; + + c.primary = scratch[xx]; + if (colour_get_alpha(&c) == 0) + continue; + + dstx = draw_box->x0 + xx; + scrp = rowp + (dstx >> 1); + shift = (dstx & 1) * 4; + + 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)); + pxl = (pm->entries[idx >> 1] >> ((idx & 1) << 2)) & 0xF; + + *scrp = (uint8_t) ((*scrp & ~(0xF << shift)) | ((pxl & 0xF) << shift)); + } + } + + free(scratch); + + return result_OK; +} + +result_t screen_copy_bitmap_rle(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; + int firstrow; + int skip, plot; + int rows; + int bpp; + int i; + + assert(scr); + assert(src); + assert(pixelfmt_is_rle(src->format)); + + if (pixelfmt_log2bpp(scr->format) == 2) + return screen_copy_bitmap_rle_p4(scr, x, y, src, draw_box); + + base = pixelfmt_base(src->format); + + /* v1: 32bpp screen, source decodes to the screen's channel order. An alpha + * source onto its matching x screen (or vice versa) is fine -- the decode + * is byte-identical -- so compare with alpha folded out. */ + if (pixelfmt_log2bpp(scr->format) != 5) + return result_NOT_SUPPORTED; + if (rle__dealpha(base) != rle__dealpha(scr->format)) + return result_NOT_SUPPORTED; + + log2bpp = pixelfmt_log2bpp(base); + bpp = 1 << (log2bpp - 3); + + blob = src->base; + p = blob + bitmap__RLE_HEADER_SIZE; + end = p + bitmap__rle_get32(blob + 4); + + /* Walk past rows clipped off the top. */ + 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; + + dstbase = scr->base; + for (i = 0; i < rows; i++) + { + uint8_t *dstrow; + + dstrow = dstbase + + (size_t) (draw_box->y0 + i) * scr->rowbytes + + (size_t) draw_box->x0 * bpp; + + p = bitmap__rle_decode_row(p, end, log2bpp, dstrow, skip, plot, 0); + } + + return result_OK; +} diff --git a/libraries/framebuf/screen/screen-copy-bitmap-rle.h b/libraries/framebuf/screen/screen-copy-bitmap-rle.h new file mode 100644 index 0000000..c97d264 --- /dev/null +++ b/libraries/framebuf/screen/screen-copy-bitmap-rle.h @@ -0,0 +1,36 @@ +/* framebuf/screen/screen-copy-bitmap-rle.h -- RLE bitmap blit (private) */ + +#ifndef FRAMEBUF_SCREEN_COPY_BITMAP_RLE_H +#define FRAMEBUF_SCREEN_COPY_BITMAP_RLE_H + +#include "base/result.h" + +#include "geom/box.h" + +#include "framebuf/bitmap.h" +#include "framebuf/screen.h" + +/** + * Blit an RLE-compressed bitmap onto a screen, top-left at (x, y), clipped + * to `draw_box` (already intersected with the screen clip). + * + * Constraints, else \ref result_NOT_SUPPORTED: - screen must be 32bpp or + * 4bpp paletted; - source must decode to 32bpp; - on a 32bpp screen + * `pixelfmt_base(src->format)` must match the screen's channel order (byte + * copy, no per-pixel conversion), on a 4bpp screen each pixel is converted + * to the nearest palette entry. The blit is alpha-tested (transparent runs + * skipped, opaque runs written), not alpha-blended. + * + * \param[in] scr Destination screen. + * \param[in] x,y Where the bitmap's top-left lands, screen pixels. + * \param[in] src Compressed source bitmap. + * \param[in] draw_box Visible rectangle in screen pixels. + * \return \ref result_OK, or \ref result_NOT_SUPPORTED. + */ +result_t screen_copy_bitmap_rle(screen_t *scr, + int x, + int y, + const bitmap_t *src, + const box_t *draw_box); + +#endif /* FRAMEBUF_SCREEN_COPY_BITMAP_RLE_H */ diff --git a/libraries/framebuf/screen/screen-copy-ninepatch.c b/libraries/framebuf/screen/screen-copy-ninepatch.c index be1c6b7..668e432 100644 --- a/libraries/framebuf/screen/screen-copy-ninepatch.c +++ b/libraries/framebuf/screen/screen-copy-ninepatch.c @@ -91,6 +91,9 @@ result_t screen_copy_ninepatch(screen_t *scr, assert(src->size.w > 0 && src->size.w % 3 == 0); assert(src->size.h > 0 && src->size.h % 3 == 0); + if (pixelfmt_is_rle(src->format)) + return result_NOT_SUPPORTED; + if (box_is_empty(dst)) return result_OK; diff --git a/libraries/framebuf/screen/screen-draw.c b/libraries/framebuf/screen/screen-draw.c index af8548a..877d010 100644 --- a/libraries/framebuf/screen/screen-draw.c +++ b/libraries/framebuf/screen/screen-draw.c @@ -9,12 +9,15 @@ #include "base/utils.h" #include "framebuf/colour.h" +#include "framebuf/pixelmap.h" #include "framebuf/span-registry.h" #include "geom/line.h" #include "utils/fxp.h" #include "framebuf/screen.h" +#include "screen-copy-bitmap-rle.h" + /* Number of pixels converted per span call in screen_copy_bitmap(). Bounds * the size of its stack scratch buffers so arbitrarily wide bitmaps don't * blow the stack (relevant on RISC OS). */ @@ -23,6 +26,34 @@ /* Each helper writes the single pixel (x, y), already known to be inside the * clip, with the colour previously resolved to "pxl". */ +static void screen_set_pixel_p1(screen_t *scr, + int x, + int y, + pixelfmt_any_t pxl) +{ + unsigned char *scrp; + int shift; + + scrp = (unsigned char *) scr->base + y * scr->rowbytes + (x >> 3); + shift = 7 - (x & 7); /* bit 7 is the leftmost pixel */ + + *scrp = (unsigned char) ((*scrp & ~(1 << shift)) | ((pxl & 1) << shift)); +} + +static void screen_set_pixel_p2(screen_t *scr, + int x, + int y, + pixelfmt_any_t pxl) +{ + unsigned char *scrp; + int shift; + + scrp = (unsigned char *) scr->base + y * scr->rowbytes + (x >> 2); + shift = 6 - ((x & 3) << 1); /* bits 7..6 are the leftmost pixel */ + + *scrp = (unsigned char) ((*scrp & ~(3 << shift)) | ((pxl & 3) << shift)); +} + static void screen_set_pixel_p4(screen_t *scr, int x, int y, @@ -37,10 +68,10 @@ static void screen_set_pixel_p4(screen_t *scr, *scrp = (unsigned char) ((*scrp & ~(0xF << shift)) | ((pxl & 0xF) << shift)); } -static void screen_set_pixel_8(screen_t *scr, - int x, - int y, - pixelfmt_any_t pxl) +static void screen_set_pixel_p8(screen_t *scr, + int x, + int y, + pixelfmt_any_t pxl) { pixelfmt_any8_t *scrp; @@ -85,12 +116,14 @@ void screen_set_pixel(screen_t *scr, int x, int y, colour_t colour) return; pxl = colour_to_pixel(scr->palette, - (scr->format == pixelfmt_p4) ? 16 : 0, + pixelfmt_paletted_nentries(scr->format), colour, scr->format); switch (pixelfmt_log2bpp(scr->format)) { + case 0: screen_set_pixel_p1(scr, x, y, pxl); break; + case 1: screen_set_pixel_p2(scr, x, y, pxl); break; case 2: screen_set_pixel_p4(scr, x, y, pxl); break; - case 3: screen_set_pixel_8(scr, x, y, pxl); break; + case 3: screen_set_pixel_p8(scr, x, y, pxl); break; case 4: screen_set_pixel_16(scr, x, y, pxl); break; case 5: screen_set_pixel_32(scr, x, y, pxl); break; @@ -103,6 +136,44 @@ void screen_set_pixel(screen_t *scr, int x, int y, colour_t colour) /* Each helper alpha-blends "colour" at "alpha" into the single pixel (x, y), * already known to be inside the clip. */ +static void screen_blend_pixel_p1(screen_t *scr, + int x, + int y, + colour_t colour, + int alpha) +{ + unsigned char *scrp; + int shift; + unsigned char idx, out; + + scrp = (unsigned char *) scr->base + y * scr->rowbytes + (x >> 3); + shift = 7 - (x & 7); + idx = (*scrp >> shift) & 1; + + scr->span->blendconst(&out, &idx, &colour, 1, alpha, scr->palette); + + *scrp = (unsigned char) ((*scrp & ~(1 << shift)) | ((out & 1) << shift)); +} + +static void screen_blend_pixel_p2(screen_t *scr, + int x, + int y, + colour_t colour, + int alpha) +{ + unsigned char *scrp; + int shift; + unsigned char idx, out; + + scrp = (unsigned char *) scr->base + y * scr->rowbytes + (x >> 2); + shift = 6 - ((x & 3) << 1); + idx = (*scrp >> shift) & 3; + + scr->span->blendconst(&out, &idx, &colour, 1, alpha, scr->palette); + + *scrp = (unsigned char) ((*scrp & ~(3 << shift)) | ((out & 3) << shift)); +} + static void screen_blend_pixel_p4(screen_t *scr, int x, int y, @@ -155,6 +226,8 @@ static void screen_blend_pixel(screen_t *scr, switch (pixelfmt_log2bpp(scr->format)) { + 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 5: screen_blend_pixel_32(scr, x, y, colour, alpha); break; @@ -193,6 +266,21 @@ void screen_fill_rect(screen_t *scr, screen_fill_hline(scr, draw_box.x0, yy, draw_box.x1 - draw_box.x0, colour); } +void screen_fill_rects(screen_t *scr, + const box_t *boxes, + int nboxes, + colour_t colour) +{ + int i; + + for (i = 0; i < nboxes; i++) + screen_fill_rect(scr, + boxes[i].x0, + boxes[i].y0, + box_size(&boxes[i]), + colour); +} + void screen_fill_square(screen_t *scr, int x, int y, @@ -205,18 +293,21 @@ void screen_fill_square(screen_t *scr, /* ----------------------------------------------------------------------- */ /* Blit "src" onto the paletted screen, its top-left at (x, y), clipped to - * "draw_box". No linear channel bits to blend, so this falls back to - * alpha-tested transfer (skip fully transparent, else nearest palette match) - * rather than true blending, matching screen_set_pixel's case 2. */ -static void screen_copy_bitmap_p4(screen_t *scr, - int x, - int y, - const bitmap_t *src, - const box_t *draw_box, - int has_alpha) + * "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. */ +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) { const unsigned char *srcrow; unsigned char *dstbase; + const pixelmap_t *pm; int clipped_width, clipped_height; int yy; @@ -226,6 +317,12 @@ static void screen_copy_bitmap_p4(screen_t *scr, 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. */ + pm = pixelmap_get(pixelfmt_rgba8888, scr->format, scr->palette, 16); + if (pm == NULL) + return result_NOT_SUPPORTED; + for (yy = 0; yy < clipped_height; yy++) { const pixelfmt_rgba8888_t *srcpx; @@ -238,6 +335,7 @@ static void screen_copy_bitmap_p4(screen_t *scr, for (xx = 0; xx < clipped_width; xx++) { colour_t c; + unsigned int r, g, b, idx; int dstx; unsigned char *scrp; int shift; @@ -250,13 +348,165 @@ static void screen_copy_bitmap_p4(screen_t *scr, dstx = draw_box->x0 + xx; scrp = rowp + (dstx >> 1); shift = (dstx & 1) * 4; - pxl = colour_to_pixel(scr->palette, 16, c, scr->format); + + 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)); + pxl = (pm->entries[idx >> 1] >> ((idx & 1) << 2)) & 0xF; *scrp = (unsigned char) ((*scrp & ~(0xF << shift)) | ((pxl & 0xF) << shift)); } 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 + * 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. */ +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) +{ + const unsigned char *srcrow; + unsigned char *dstbase; + const pixelmap_t *pm; + 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; + dstbase = scr->base; + + pm = pixelmap_get(pixelfmt_rgba8888, scr->format, scr->palette, 2); + 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, shift; + unsigned char *scrp; + pixelfmt_any_t pxl; + + c.primary = srcpx[xx]; + if (has_alpha && colour_get_alpha(&c) == 0) + continue; /* fully transparent: leave background alone */ + + dstx = draw_box->x0 + xx; + scrp = rowp + (dstx >> 3); + shift = 7 - (dstx & 7); + + 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)); + pxl = (pm->entries[idx >> 3] >> (idx & 7)) & 1; + + *scrp = (unsigned char) ((*scrp & ~(1 << shift)) | ((pxl & 1) << shift)); + } + + srcrow += src->rowbytes; + } + + return result_OK; +} + +/* Blit "src" onto the 2bpp screen, its top-left at (x, y), clipped to + * "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. */ +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) +{ + const unsigned char *srcrow; + unsigned char *dstbase; + const pixelmap_t *pm; + 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; + dstbase = scr->base; + + pm = pixelmap_get(pixelfmt_rgba8888, scr->format, scr->palette, 4); + 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, shift; + unsigned char *scrp; + pixelfmt_any_t pxl; + + c.primary = srcpx[xx]; + if (has_alpha && colour_get_alpha(&c) == 0) + continue; /* fully transparent: leave background alone */ + + dstx = draw_box->x0 + xx; + scrp = rowp + (dstx >> 2); + shift = 6 - ((dstx & 3) << 1); + + 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)); + pxl = (pm->entries[idx >> 2] >> ((idx & 3) << 1)) & 3; + + *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 @@ -336,6 +586,9 @@ result_t screen_copy_bitmap(screen_t *scr, int x, int y, const bitmap_t *src) if (box_intersection(&clip_box, &src_box, &draw_box)) return result_OK; /* nothing visible */ + if (pixelfmt_is_rle(src->format)) + return screen_copy_bitmap_rle(scr, x, y, src, &draw_box); + /* 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. */ @@ -343,7 +596,9 @@ result_t screen_copy_bitmap(screen_t *scr, int x, int y, const bitmap_t *src) switch (pixelfmt_log2bpp(scr->format)) { - case 2: screen_copy_bitmap_p4(scr, x, y, src, &draw_box, has_alpha); break; + 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 5: screen_copy_bitmap_32(scr, x, y, src, &draw_box, has_alpha); break; default: @@ -480,6 +735,50 @@ void screen_draw_rect(screen_t *scr, screen_draw_lines(scr, p, 5, colour); } +void screen_draw_bevel_edge(screen_t *scr, + const box_t *box, + colour_t a, + colour_t b) +{ + int x0, y0, x1, y1; + int w, h; + + x0 = box->x0; + y0 = box->y0; + x1 = box->x1; + y1 = box->y1; + w = x1 - x0; + h = y1 - y0; + + /* For a 5x5 box draw like so using colours A and B: + * + * AAAAB + * AAABB + * AA BB + * AABBB + * ABBBB + * + * Using rects for the bulk of the filling, then single pixel fixups in the + * following order: + * + * 00037 + * 00044 + * 11 44 + * 11555 + * 26555 + */ + + screen_fill_rect(scr, x0, y0, SIZE2D(w - 2, 2), a); + screen_fill_rect(scr, x0, y0 + 2, SIZE2D(2, h - 3), a); + screen_set_pixel(scr, x0, y1 - 1, a); + screen_set_pixel(scr, x1 - 2, y0, a); + + screen_fill_rect(scr, x1 - 2, y0 + 1, SIZE2D(2, h - 1), b); + screen_fill_rect(scr, x0 + 2, y1 - 2, SIZE2D(w - 2, 2), b); + screen_set_pixel(scr, x0 + 1, y1 - 1, b); + screen_set_pixel(scr, x1 - 1, y0, b); +} + void screen_draw_dashed_line(screen_t *scr, int x0, int y0, diff --git a/libraries/framebuf/screen/screen-fill-hline.c b/libraries/framebuf/screen/screen-fill-hline.c index 45403da..6880aef 100644 --- a/libraries/framebuf/screen/screen-fill-hline.c +++ b/libraries/framebuf/screen/screen-fill-hline.c @@ -34,7 +34,7 @@ void screen_fill_hline(screen_t *scr, int x, int y, int w, colour_t colour) clipped_width = draw_box.x1 - draw_box.x0; fmt = colour_to_pixel(scr->palette, - (scr->format == pixelfmt_p4) ? 16 : 0, + pixelfmt_paletted_nentries(scr->format), colour, scr->format); /* the per-format run fill lives in the span table; "first" lets it address @@ -42,7 +42,7 @@ void screen_fill_hline(screen_t *scr, int x, int y, int w, colour_t colour) * with no span-registry entry -- the assert catches that in debug builds, * but a release build must still no-op rather than dereference NULL. */ assert(scr->span && scr->span->fill); - if (scr->span == NULL) + if (scr->span == NULL || scr->span->fill == NULL) return; rowp = (unsigned char *) scr->base + draw_box.y0 * scr->rowbytes; diff --git a/libraries/framebuf/screen/screen-fill-pattern.c b/libraries/framebuf/screen/screen-fill-pattern.c index 9751db2..16a4e69 100644 --- a/libraries/framebuf/screen/screen-fill-pattern.c +++ b/libraries/framebuf/screen/screen-fill-pattern.c @@ -17,6 +17,84 @@ * bit per pixel rather than using the run. */ typedef pixelfmt_any_t pattern_runs_t[8][8]; +static void screen_fill_pattern_p1(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)))) + { + unsigned char *scrp; + int shift; + + scrp = rowp + (x >> 3); + shift = 7 - (x & 7); + + *scrp = (unsigned char) ((*scrp & ~(1 << shift)) | + ((run[col] & 1) << shift)); + } + col = (col + 1) & 7; + } + rowp += scr->rowbytes; + } +} + +static void screen_fill_pattern_p2(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)))) + { + unsigned char *scrp; + int shift; + + scrp = rowp + (x >> 2); + shift = 6 - ((x & 3) << 1); + + *scrp = (unsigned char) ((*scrp & ~(3 << shift)) | + ((run[col] & 3) << shift)); + } + col = (col + 1) & 7; + } + rowp += scr->rowbytes; + } +} + static void screen_fill_pattern_p4(screen_t *scr, const pattern_t *pattern, const box_t *draw_box, @@ -131,10 +209,10 @@ void screen_fill_pattern(screen_t *scr, stencil = (pattern->flags & pattern_FLAG_STENCIL) != 0; fg_fmt = colour_to_pixel(scr->palette, - (scr->format == pixelfmt_p4) ? 16 : 0, + pixelfmt_paletted_nentries(scr->format), pattern->fg, scr->format); bg_fmt = colour_to_pixel(scr->palette, - (scr->format == pixelfmt_p4) ? 16 : 0, + pixelfmt_paletted_nentries(scr->format), pattern->bg, scr->format); xphase = ((draw_box.x0 - pattern->origin.x) & 7); @@ -150,6 +228,14 @@ void screen_fill_pattern(screen_t *scr, switch (pixelfmt_log2bpp(scr->format)) { + case 0: + screen_fill_pattern_p1(scr, pattern, &draw_box, stencil, runs); + break; + + case 1: + screen_fill_pattern_p2(scr, pattern, &draw_box, stencil, runs); + break; + case 2: screen_fill_pattern_p4(scr, pattern, &draw_box, stencil, runs); break; diff --git a/libraries/framebuf/screen/test/screen-test.c b/libraries/framebuf/screen/test/screen-test.c index 7fec322..8bacb42 100644 --- a/libraries/framebuf/screen/test/screen-test.c +++ b/libraries/framebuf/screen/test/screen-test.c @@ -740,6 +740,226 @@ static result_t test_fill_circle(void) /* ----------------------------------------------------------------------- */ +/* Blit an rgba8888 source onto a 1bpp screen: each pixel must quantise to + * the nearer of the two palette entries and pack MSB-first, bit 7 leftmost. */ +static result_t test_copy_bitmap_p1(void) +{ +#define P1_ROWBYTES (WIDTH / 8) + static unsigned char p1pixels[P1_ROWBYTES * HEIGHT]; + static pixelfmt_rgba8888_t srcbuf[16 * 4]; + + screen_t scr; + bitmap_t src; + colour_t pal[2]; + int x, y; + + pal[0] = colour_rgb(0x00, 0x00, 0x00); + pal[1] = colour_rgb(0xFF, 0xFF, 0xFF); + + /* 16x4 source: left half dark (-> index 0), right half light (-> index 1) */ + 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(p1pixels, 0, sizeof(p1pixels)); + screen_init(&scr, SIZE2D(WIDTH, HEIGHT), pixelfmt_p1, P1_ROWBYTES, pal, + p1pixels); + + if (screen_copy_bitmap(&scr, 0, 0, &src) != result_OK) + { + printf("screen: copy_bitmap to p1 screen failed\n"); + return result_TEST_FAILED; + } + + /* row 0: byte 0 = eight dark pixels = 0x00; byte 1 = eight light = 0xFF */ + if (p1pixels[0] != 0x00 || p1pixels[1] != 0xFF) + { + printf("screen: p1 blit packed wrong (0x%02X 0x%02X, want 0x00 0xFF)\n", + p1pixels[0], p1pixels[1]); + return result_TEST_FAILED; + } + + /* an untouched row past the source stays clear */ + if (p1pixels[4 * P1_ROWBYTES] != 0x00) + { + printf("screen: p1 blit spilled past the source\n"); + return result_TEST_FAILED; + } + + /* odd x offset: the first light pixel must land in bit 7-(3&7) of byte 0 */ + memset(p1pixels, 0, sizeof(p1pixels)); + { + pixelfmt_rgba8888_t one = colour_rgb(0xF0, 0xF0, 0xF0).primary; + bitmap_t dot; + + bitmap_init(&dot, SIZE2D(1, 1), pixelfmt_rgba8888, + (int) sizeof(one), NULL, &one); + screen_copy_bitmap(&scr, 3, 0, &dot); + if (p1pixels[0] != (1 << (7 - 3))) + { + printf("screen: p1 blit odd-x packed wrong (0x%02X)\n", p1pixels[0]); + return result_TEST_FAILED; + } + } + + /* screen_set_pixel: white pixel at x=2 sets bit 7-2 of byte 0 */ + memset(p1pixels, 0, sizeof(p1pixels)); + screen_set_pixel(&scr, 2, 0, pal[1]); + if (p1pixels[0] != (1 << (7 - 2))) + { + printf("screen: p1 set_pixel packed wrong (0x%02X)\n", p1pixels[0]); + return result_TEST_FAILED; + } + + /* screen_fill_hline: 8 white pixels from x=0 fill byte 0 */ + memset(p1pixels, 0, sizeof(p1pixels)); + screen_fill_hline(&scr, 0, 1, 8, pal[1]); + if (p1pixels[P1_ROWBYTES] != 0xFF) + { + printf("screen: p1 fill_hline packed wrong (0x%02X)\n", + p1pixels[P1_ROWBYTES]); + return result_TEST_FAILED; + } + + /* screen_fill_pattern: solid all-bits pattern in fg colour = 0xFF row */ + memset(p1pixels, 0, sizeof(p1pixels)); + { + pattern_t pat; + box_t b; + + memset(&pat, 0, sizeof(pat)); + memset(pat.bits, 0xFF, sizeof(pat.bits)); + pat.fg = pal[1]; + pat.bg = pal[0]; + b.x0 = 0; b.y0 = 2; b.x1 = 8; b.y1 = 3; + screen_fill_pattern(&scr, &b, &pat); + if (p1pixels[2 * P1_ROWBYTES] != 0xFF) + { + printf("screen: p1 fill_pattern packed wrong (0x%02X)\n", + p1pixels[2 * P1_ROWBYTES]); + return result_TEST_FAILED; + } + } + + return result_TEST_PASSED; +} + +static result_t test_copy_bitmap_p2(void) +{ +#define P2_ROWBYTES (WIDTH / 4) + static unsigned char p2pixels[P2_ROWBYTES * HEIGHT]; + static pixelfmt_rgba8888_t srcbuf[16 * 4]; + + screen_t scr; + bitmap_t src; + colour_t pal[4]; + 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); + + /* 16x4 source: left half black (-> index 0), right half white (-> index 3) */ + for (y = 0; y < 4; y++) + for (x = 0; x < 16; x++) + srcbuf[y * 16 + x] = (x < 8 ? colour_rgb(0x08, 0x08, 0x08) + : colour_rgb(0xF8, 0xF8, 0xF8)).primary; + + bitmap_init(&src, SIZE2D(16, 4), pixelfmt_rgba8888, + 16 * (int) sizeof(srcbuf[0]), NULL, srcbuf); + + memset(p2pixels, 0, sizeof(p2pixels)); + screen_init(&scr, SIZE2D(WIDTH, HEIGHT), pixelfmt_p2, P2_ROWBYTES, pal, + p2pixels); + + if (screen_copy_bitmap(&scr, 0, 0, &src) != result_OK) + { + printf("screen: copy_bitmap to p2 screen failed\n"); + return result_TEST_FAILED; + } + + /* row 0: bytes 0..1 = four black pixels each = 0x00; bytes 2..3 = four + * white (index 3 = 0b11) each = 0xFF */ + if (p2pixels[0] != 0x00 || p2pixels[1] != 0x00 || + p2pixels[2] != 0xFF || p2pixels[3] != 0xFF) + { + printf("screen: p2 blit packed wrong (0x%02X 0x%02X 0x%02X 0x%02X)\n", + p2pixels[0], p2pixels[1], p2pixels[2], p2pixels[3]); + return result_TEST_FAILED; + } + + /* an untouched row past the source stays clear */ + if (p2pixels[4 * P2_ROWBYTES] != 0x00) + { + printf("screen: p2 blit spilled past the source\n"); + return result_TEST_FAILED; + } + + /* odd x offset: first white pixel lands in bits 6-((3&3)<<1) = 0 of byte 0 */ + memset(p2pixels, 0, sizeof(p2pixels)); + { + pixelfmt_rgba8888_t one = colour_rgb(0xF8, 0xF8, 0xF8).primary; + bitmap_t dot; + + bitmap_init(&dot, SIZE2D(1, 1), pixelfmt_rgba8888, + (int) sizeof(one), NULL, &one); + screen_copy_bitmap(&scr, 3, 0, &dot); + if (p2pixels[0] != (3 << 0)) + { + printf("screen: p2 blit odd-x packed wrong (0x%02X)\n", p2pixels[0]); + return result_TEST_FAILED; + } + } + + /* screen_set_pixel: white pixel at x=1 sets bits 6-((1&3)<<1) = 4 of byte 0 */ + memset(p2pixels, 0, sizeof(p2pixels)); + screen_set_pixel(&scr, 1, 0, pal[3]); + if (p2pixels[0] != (3 << 4)) + { + printf("screen: p2 set_pixel packed wrong (0x%02X)\n", p2pixels[0]); + return result_TEST_FAILED; + } + + /* screen_fill_hline: 4 white pixels from x=0 fill byte 0 */ + memset(p2pixels, 0, sizeof(p2pixels)); + screen_fill_hline(&scr, 0, 1, 4, pal[3]); + if (p2pixels[P2_ROWBYTES] != 0xFF) + { + printf("screen: p2 fill_hline packed wrong (0x%02X)\n", + p2pixels[P2_ROWBYTES]); + return result_TEST_FAILED; + } + + /* screen_fill_pattern: solid all-bits pattern in fg colour = 0xFF row */ + memset(p2pixels, 0, sizeof(p2pixels)); + { + pattern_t pat; + box_t b; + + memset(&pat, 0, sizeof(pat)); + memset(pat.bits, 0xFF, sizeof(pat.bits)); + pat.fg = pal[3]; + pat.bg = pal[0]; + b.x0 = 0; b.y0 = 2; b.x1 = 4; b.y1 = 3; + screen_fill_pattern(&scr, &b, &pat); + if (p2pixels[2 * P2_ROWBYTES] != 0xFF) + { + printf("screen: p2 fill_pattern packed wrong (0x%02X)\n", + p2pixels[2 * P2_ROWBYTES]); + return result_TEST_FAILED; + } + } + + return result_TEST_PASSED; +} + +/* ----------------------------------------------------------------------- */ + result_t screen_test(const char *resources) { typedef result_t (*screentestfn)(void); @@ -755,7 +975,9 @@ result_t screen_test(const char *resources) test_draw_lines, test_draw_rect, test_draw_circle, - test_fill_circle + test_fill_circle, + test_copy_bitmap_p1, + test_copy_bitmap_p2 }; result_t rc; diff --git a/libraries/framebuf/span-registry/get.c b/libraries/framebuf/span-registry/get.c index 52f99df..eea5cef 100644 --- a/libraries/framebuf/span-registry/get.c +++ b/libraries/framebuf/span-registry/get.c @@ -12,12 +12,15 @@ const span_t *spanregistry_get(pixelfmt_t format) { - static pixelfmt_t lastformat; + /* seed the cache with a value no real pixelfmt_t takes, so the first call + * for pixelfmt_p1 (== 0) still does the lookup rather than returning the + * zero-initialised lastspan (NULL) */ + static pixelfmt_t lastformat = pixelfmt_unknown; static const span_t *lastspan; int i; /* avoid full lookup where possible */ - if (lastformat == format) + if (lastformat == format && lastspan != NULL) return lastspan; for (i = 0; i < nspans; i++) diff --git a/libraries/framebuf/span-registry/regdata.h b/libraries/framebuf/span-registry/regdata.h index 37b3fe1..546e6ae 100644 --- a/libraries/framebuf/span-registry/regdata.h +++ b/libraries/framebuf/span-registry/regdata.h @@ -6,6 +6,8 @@ #include "base/utils.h" #include "framebuf/span-bgrx8888.h" +#include "framebuf/span-p1.h" +#include "framebuf/span-p2.h" #include "framebuf/span-p4.h" #include "framebuf/span-rgbx8888.h" #include "framebuf/span-xbgr8888.h" @@ -15,6 +17,8 @@ static const span_t *spans[] = { &span_bgrx8888, + &span_p1, + &span_p2, &span_p4, &span_rgbx8888, &span_xbgr8888, diff --git a/libraries/framebuf/span/p1.c b/libraries/framebuf/span/p1.c new file mode 100644 index 0000000..b1e32d5 --- /dev/null +++ b/libraries/framebuf/span/p1.c @@ -0,0 +1,105 @@ +/* framebuf/span/p1.c -- P1 (1bpp paletted) format plot methods */ + +#include + +#include "framebuf/colour.h" +#include "framebuf/pixelfmt.h" + +#include "framebuf/span.h" + +#include "framebuf/span-p1.h" + +#define SPAN_P1_NENTRIES 2 + +/* pixel values here are unpacked palette indices, one per byte (not the + * eight-bits-per-byte layout used in screen memory); packing/unpacking + * into the actual screen bytes is the caller's job, as with + * screen_set_pixel's other pixel formats. src2 is an array of colour_t, + * not pre-quantised pixels, so the blend happens in full RGB precision + * before re-quantising to the nearest palette entry. */ +static void span_p1_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 & 1]; + 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_P1_NENTRIES, blended, pixelfmt_p1); + + pdst++; + psrc1++; + psrc2++; + } +} + +/* unlike span_p1_blendconst above, this works directly on packed screen + * bytes: "dst" is the row base, "first" the bit (pixel) index into it, so + * odd start columns and odd lengths are handled without the caller packing. + * "pixel" is a palette index in its low bit. Bit 7 of a byte is the + * leftmost pixel, matching PNG's MSB-first 1bpp packing. */ +static void span_p1_fill(void *vdst, + int first, + pixelfmt_any_t pixel, + int length) +{ + unsigned char *base; + unsigned char bit; + int x; + + base = vdst; + bit = (unsigned char) (pixel & 1); + + for (x = first; x < first + length; x++) + { + unsigned char *p; + int shift; + + p = base + (x >> 3); + shift = 7 - (x & 7); + *p = (unsigned char) ((*p & ~(1 << shift)) | (bit << shift)); + } +} + +const span_t span_p1 = +{ + pixelfmt_p1, + NULL, /* copy: unneeded so far (bit packing makes a generic array copy awkward) */ + span_p1_fill, + span_p1_blendconst, + NULL, /* blendarray: unneeded so far */ +}; diff --git a/libraries/framebuf/span/p2.c b/libraries/framebuf/span/p2.c new file mode 100644 index 0000000..61dbef8 --- /dev/null +++ b/libraries/framebuf/span/p2.c @@ -0,0 +1,108 @@ +/* framebuf/span/p2.c -- P2 (2bpp paletted) format plot methods */ + +#include + +#include "framebuf/colour.h" +#include "framebuf/pixelfmt.h" + +#include "framebuf/span.h" + +#include "framebuf/span-p2.h" + +#define SPAN_P2_NENTRIES 4 +#define SPAN_P2_MASK 3 + +/* pixel values here are unpacked palette indices, one per byte (not the + * four-pixels-per-byte layout used in screen memory); packing/unpacking + * into the actual screen bytes is the caller's job, as with + * screen_set_pixel's other pixel formats. src2 is an array of colour_t, + * not pre-quantised pixels, so the blend happens in full RGB precision + * before re-quantising to the nearest palette entry. */ +static void span_p2_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 & SPAN_P2_MASK]; + 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_P2_NENTRIES, + blended, pixelfmt_p2); + + pdst++; + psrc1++; + psrc2++; + } +} + +/* unlike span_p2_blendconst above, this works directly on packed screen + * bytes: "dst" is the row base, "first" the pixel index into it, so odd + * start columns and odd lengths are handled without the caller packing. + * "pixel" is a palette index in its low two bits. Bits 7..6 of a byte are + * the leftmost pixel, matching the MSB-first sub-byte packing used + * elsewhere in framebuf. */ +static void span_p2_fill(void *vdst, + int first, + pixelfmt_any_t pixel, + int length) +{ + unsigned char *base; + unsigned char idx; + int x; + + base = vdst; + idx = (unsigned char) (pixel & SPAN_P2_MASK); + + for (x = first; x < first + length; x++) + { + unsigned char *p; + int shift; + + p = base + (x >> 2); + shift = 6 - ((x & 3) << 1); + *p = (unsigned char) ((*p & ~(SPAN_P2_MASK << shift)) | (idx << shift)); + } +} + +const span_t span_p2 = +{ + pixelfmt_p2, + NULL, /* copy: unneeded so far (bit packing makes a generic array copy awkward) */ + span_p2_fill, + span_p2_blendconst, + NULL, /* blendarray: unneeded so far */ +}; diff --git a/libraries/io/namelist/namelist.c b/libraries/io/namelist/namelist.c new file mode 100644 index 0000000..43d3964 --- /dev/null +++ b/libraries/io/namelist/namelist.c @@ -0,0 +1,97 @@ +/* io/namelist.c -- collect matching dir leafnames into a fixed table */ + +#include +#include +#include + +#include "base/result.h" +#include "io/dirscan.h" + +#include "io/namelist.h" + +/* ----------------------------------------------------------------------- */ + +typedef struct namelist_ctx +{ + char *names; /* base of the cap-by-stride table */ + size_t stride; + int cap; + const char *ext; /* NULL or "" for "match everything" */ + size_t extlen; + int count; +} +namelist_ctx_t; + +static result_t namelist_entry(const char *leaf, void *opaque) +{ + namelist_ctx_t *ctx; + size_t leaflen; + size_t namelen; + + ctx = opaque; + leaflen = strlen(leaf); + + if (ctx->count >= ctx->cap) + return result_STOP_WALK; + + if (ctx->extlen > 0) + { + if (leaflen <= ctx->extlen || + strcmp(leaf + leaflen - ctx->extlen, ctx->ext) != 0) + return result_OK; + namelen = leaflen - ctx->extlen; + } + else + { + namelen = leaflen; + } + + if (namelen + 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'; + ctx->count++; + + return result_OK; +} + +static int namelist_cmp(const void *a, const void *b) +{ + return strcmp(a, b); +} + +result_t namelist_scan(const char *dir, + const char *ext, + char *names, + size_t stride, + int cap, + int sorted, + int *pcount) +{ + result_t rc; + namelist_ctx_t ctx; + + if (dir == NULL || names == NULL || pcount == NULL) + return result_NULL_ARG; + if (stride < 2 || cap < 0) + return result_BAD_ARG; + + ctx.names = names; + 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); + if (rc != result_OK) + return rc; + + if (sorted && ctx.count > 1) + qsort(names, (size_t) ctx.count, stride, namelist_cmp); + + *pcount = ctx.count; + + return result_OK; +} diff --git a/libraries/wuss/TODO.txt b/libraries/wuss/TODO.txt new file mode 100644 index 0000000..d39e91c --- /dev/null +++ b/libraries/wuss/TODO.txt @@ -0,0 +1,101 @@ +Wuss + +Ordered by dependency. Each layer builds on the ones above it. +Items marked (blocks: X) are prerequisites for X. + + +LAYER 0 - BUGS (fix before building on top) + +- Various window size toggle ops need checking out +- 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) + +- Masks: how to get (alpha) masks into the system? Another channel, + fixed value, or a second bitmap? Decide the model first. + 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 + + +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 + + +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 + + +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 +- Scroll: auto repeat on scroll buttons +- Snap to edges (outline-click bug fixed; unblocked) +- Dragging against window edge shuffles window in opposite direction + (outline-click bug fixed; unblocked) +- Input focus, text entry, furniture tinting +- Icon layout +- Furniture colour selection needs improvements (e.g. losing menu readability when palette changed) + + +LAYER 5 - DRAG & TEMPLATES (needs sprites, config-after-the-fact) + +- Dragbox concepts (mouse clamping, marching ants) + blocks: Drag & Drop +- Window and menu templates (text format?) +- Drag & Drop + + +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? +- Test submenu dialogues +- Dialogue abstraction from app engine? + + +LAYER 7 - APPS & POLISH (needs most of the above) + +- Have a Paint app as a target +- Desktop cohort: Filer, Task Manager, Palette Util +- Colour Picker component +- Proper desktop font +- Support palette images +- Greebling symmetry +- 8x8 icon libraries - share the greeble code with other parts of wuss +- Tiled fills (tile_1) +- Rounded corner windows + + +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) +- Nested wimp type nesting +- Automatic pane handling? +- Stress test: moving windows in a circle a-la nested Wimp diff --git a/libraries/wuss/component/fontmenu.c b/libraries/wuss/component/fontmenu.c index 8bf3f14..fac6cf2 100644 --- a/libraries/wuss/component/fontmenu.c +++ b/libraries/wuss/component/fontmenu.c @@ -200,9 +200,9 @@ result_t wuss_fontmenu_create(wuss_fontmenu_t **out, const wuss_t *wuss, const wuss_alloc_t *alloc) { + result_t rc; namelist_t nl; wuss_fontmenu_t *fm; - result_t rc; if (out == NULL || dir == NULL) return result_NULL_ARG; diff --git a/libraries/wuss/component/info.c b/libraries/wuss/component/info.c new file mode 100644 index 0000000..7bd7aee --- /dev/null +++ b/libraries/wuss/component/info.c @@ -0,0 +1,203 @@ +/* wuss/component/info.c -- a label:value grid dialogue */ + +#include +#include +#include + +#ifdef FORTIFY +#include "fortify/fortify.h" +#endif + +#include "base/result.h" +#include "base/utils.h" +#include "framebuf/bmfont.h" +#include "geom/box.h" +#include "geom/size.h" + +#include "wuss/icon.h" +#include "wuss/task.h" +#include "wuss/window.h" + +#include "wuss/component/info.h" + +#include "../core/impl.h" + +/* ----------------------------------------------------------------------- */ + +/* ponytail: a flat grid of LABEL icons, two per row -- no action buttons, no + * file-type sprite icon (the RISC OS Info dialogue's first field). Add either + * through here if a component ever needs one. The spec array is on the stack, + * so the row count is capped; INFO_MAX_ROWS is well past any real dialogue. */ + +#define INFO_MARGIN 4 /* px border around the row block */ +#define INFO_GAP 4 /* px between the label column and the value column */ +#define INFO_ROW_PAD 12 /* px added to the font height for the row pitch */ +#define INFO_ROW_LEADING 4 /* px leading between rows */ +#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 + * 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. */ +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 */ +}; + +/* ----------------------------------------------------------------------- */ + +/* Widest of "s" (whole string, no wrap) and "cur", in pixels; "cur" + * unchanged when the font or string is absent. bmfont_measure with no target + * width returns the whole string's width in its last out parameter. */ +static int info_widen(bmfont_t *font, const char *s, int cur) +{ + bmfont_width_t w; + + if (font == NULL || s == NULL || *s == '\0') + return cur; + + wuss__text_measure(font, s, (int) strlen(s), INT_MAX, NULL, &w); + 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) +{ + 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; + + wuss = task->wuss; + font = wuss_get_font(wuss); + + /* 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 + * nothing measured to size them. */ + labelw = 0; + valuew = 0; + for (i = 0; i < nrows; i++) + { + labelw = info_widen(font, rows[i].label, labelw); + valuew = info_widen(font, rows[i].value, valuew); + } + + fonth = 0; + if (font != NULL) + bmfont_get_info(font, NULL, &fonth); + rowh = MAX(fonth + INFO_ROW_PAD, 12) + INFO_ROW_LEADING; + fieldh = rowh - INFO_ROW_LEADING; /* rowh is the pitch; leave a gap */ + + labelw = MAX(labelw, 1); + valuew = MAX(valuew, 1) + INFO_FIELD_PAD * 2; /* groove border + inset */ + + 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; + + content = (box_t) BOX_POS_SIZE(0, 0, w, h); + 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_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; + } + + /* 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)); + for (i = 0; i < nrows; i++) + { + wuss_icon_spec_t *label; + wuss_icon_spec_t *value; + int y; + + label = &specs[i * 2]; + value = &specs[i * 2 + 1]; + y = INFO_MARGIN + rowh * i; + + label->bbox = (box_t) BOX_POS_SIZE(INFO_MARGIN, y, labelw, fieldh); + label->type = wuss_ICON_TYPE_LABEL; + label->text = rows[i].label; + label->fg = wuss_COLOUR_BLACK; + label->bg = wuss_NO_BACKGROUND; + label->flags = wuss_ICON_FLAGS_JUSTIFY_RIGHT; + + value->bbox = (box_t) BOX_POS_SIZE(INFO_MARGIN + labelw + INFO_GAP, + y, valuew, fieldh); + value->type = wuss_ICON_TYPE_LABEL; + 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->flags = wuss_ICON_FLAGS_JUSTIFY_CENTRE; + } + + rc = wuss_icon_create_array(info->window, specs, nrows * 2, NULL); + if (rc != result_OK) + { + wuss_window_close(info->window); /* frees the window and any icons on it */ + info->alloc.free(info); + return rc; + } + + *out = info; + return result_OK; +} + +void wuss_info_destroy(wuss_info_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); +} + +wuss_window_t *wuss_info_window(const wuss_info_t *info) +{ + return info ? info->window : NULL; +} diff --git a/libraries/wuss/component/proginfo.c b/libraries/wuss/component/proginfo.c new file mode 100644 index 0000000..991865d --- /dev/null +++ b/libraries/wuss/component/proginfo.c @@ -0,0 +1,58 @@ +/* wuss/component/proginfo.c -- a "program information" standard dialogue */ + +#include + +#ifdef FORTIFY +#include "fortify/fortify.h" +#endif + +#include "base/result.h" + +#include "wuss/task.h" + +#include "wuss/component/info.h" +#include "wuss/component/proginfo.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) +{ + wuss_info_row_t rows[4]; + int nrows; + + if (out == NULL || task == NULL || desc == NULL || desc->name == NULL) + return result_NULL_ARG; + + nrows = 0; + if (desc->name != NULL) + { + rows[nrows].label = "Name"; + rows[nrows].value = desc->name; + nrows++; + } + if (desc->purpose != NULL) + { + rows[nrows].label = "Purpose"; + rows[nrows].value = desc->purpose; + nrows++; + } + if (desc->author != NULL) + { + rows[nrows].label = "Author"; + rows[nrows].value = desc->author; + nrows++; + } + if (desc->version != NULL) + { + rows[nrows].label = "Version"; + rows[nrows].value = desc->version; + nrows++; + } + + return wuss_info_create(out, task, "About this program", rows, nrows); +} diff --git a/libraries/wuss/core/create.c b/libraries/wuss/core/create.c index dc47067..b75fd87 100644 --- a/libraries/wuss/core/create.c +++ b/libraries/wuss/core/create.c @@ -34,13 +34,19 @@ static const colour_t wuss__default_palette[] = static result_t validate_bevel_backdrop(const wuss_t *w, wuss_colour_t blight, wuss_colour_t bdark, - wuss_colour_t abg, - wuss_colour_t afg) + wuss_colour_t bdivider, + wuss_colour_t btnbg, + wuss_colour_t btnfg, + wuss_colour_t btnpressed, + wuss_colour_t accent) { - if (blight >= w->npalette || - bdark >= w->npalette || - abg >= w->npalette || - afg >= w->npalette || + if (blight >= w->npalette || + bdark >= w->npalette || + bdivider >= w->npalette || + btnbg >= w->npalette || + btnfg >= w->npalette || + btnpressed >= w->npalette || + accent >= w->npalette || wuss__validate_backdrop(w, &w->backdrop) != result_OK) return result_WUSS_BAD_COLOUR; @@ -65,8 +71,8 @@ result_t wuss_create(screen_t *scr, wuss_colour_t bg, fg; #endif #if defined(WUSS_FURNITURE) || defined(WUSS_ICONS) - wuss_colour_t blight, bdark; - wuss_colour_t abg, afg; + wuss_colour_t blight, bdark, bdivider; + wuss_colour_t btnbg, btnfg, btnpressed, accent; #endif #ifdef WUSS_FURNITURE int font_height; @@ -118,12 +124,23 @@ result_t wuss_create(screen_t *scr, w->backdrop = config->backdrop; w->backdrop.colour = wuss__resolve_colour(w, w->backdrop.colour); w->backdrop.pattern_bg = wuss__resolve_colour(w, w->backdrop.pattern_bg); + w->window_bg = wuss__resolve_colour(w, config->body.window); + w->menu_bg = wuss__resolve_colour(w, config->body.menu); } else { w->backdrop.colour = wuss_NO_BACKGROUND; w->backdrop.pattern = screen_PATTERN_SOLID; w->backdrop.pattern_bg = wuss_NO_BACKGROUND; + w->window_bg = wuss__resolve_colour(w, wuss_COLOUR_GREY); + w->menu_bg = wuss__resolve_colour(w, wuss_COLOUR_WHITE); + } + + if (w->window_bg >= w->npalette || w->menu_bg >= w->npalette) + { + wuss__free(w, w->palette); + wuss__free(w, w); + return result_WUSS_BAD_COLOUR; } #ifdef WUSS_FURNITURE @@ -132,6 +149,9 @@ result_t wuss_create(screen_t *scr, pal = config->furniture; pal.title.bg = wuss__resolve_colour(w, pal.title.bg); pal.title.fg = wuss__resolve_colour(w, pal.title.fg); + pal.outline = (pal.outline == wuss_NO_BACKGROUND) + ? pal.title.bg + : wuss__resolve_colour(w, pal.outline); pal.back = wuss__resolve_colour(w, pal.back); pal.close = wuss__resolve_colour(w, pal.close); pal.toggle = wuss__resolve_colour(w, pal.toggle); @@ -139,10 +159,19 @@ result_t wuss_create(screen_t *scr, pal.scroll.arrows = wuss__resolve_colour(w, pal.scroll.arrows); pal.scroll.wells = wuss__resolve_colour(w, pal.scroll.wells); pal.scroll.sausages = wuss__resolve_colour(w, pal.scroll.sausages); - blight = wuss__resolve_colour(w, config->bevel.light); - bdark = wuss__resolve_colour(w, config->bevel.dark); - abg = wuss__resolve_colour(w, config->accent.bg); - afg = wuss__resolve_colour(w, config->accent.fg); + blight = wuss__resolve_colour(w, config->bevel.light); + bdark = wuss__resolve_colour(w, config->bevel.dark); + bdivider = (config->bevel.divider == wuss_NO_BACKGROUND) + ? blight + : wuss__resolve_colour(w, config->bevel.divider); + btnbg = (config->button.bg == wuss_NO_BACKGROUND) + ? blight + : wuss__resolve_colour(w, config->button.bg); + btnfg = wuss__resolve_colour(w, config->button.fg); + btnpressed = (config->button.pressed == wuss_NO_BACKGROUND) + ? bdark + : wuss__resolve_colour(w, config->button.pressed); + accent = wuss__resolve_colour(w, config->accent.colour); } else { @@ -151,6 +180,7 @@ result_t wuss_create(screen_t *scr, pal.title.bg = bg; pal.title.fg = fg; + pal.outline = bg; pal.back = fg; pal.close = fg; pal.toggle = fg; @@ -159,14 +189,18 @@ result_t wuss_create(screen_t *scr, pal.scroll.wells = bg; pal.scroll.sausages = fg; - blight = 0; - bdark = 0; - abg = bg; /* default action button: the titlebar colours */ - afg = fg; + blight = 0; + bdark = 0; + bdivider = 0; + btnbg = 0; + btnfg = fg; + btnpressed = 0; + accent = bg; /* default action button: the titlebar fill */ } if (pal.title.bg >= w->npalette || pal.title.fg >= w->npalette || + pal.outline >= w->npalette || pal.back >= w->npalette || pal.close >= w->npalette || pal.toggle >= w->npalette || @@ -174,7 +208,8 @@ result_t wuss_create(screen_t *scr, pal.scroll.arrows >= w->npalette || pal.scroll.wells >= w->npalette || pal.scroll.sausages >= w->npalette || - validate_bevel_backdrop(w, blight, bdark, abg, afg) != result_OK) + validate_bevel_backdrop(w, blight, bdark, bdivider, + btnbg, btnfg, btnpressed, accent) != result_OK) { wuss__free(w, w->palette); wuss__free(w, w); @@ -184,8 +219,11 @@ result_t wuss_create(screen_t *scr, w->furniture_colours = pal; w->bevel_light = blight; w->bevel_dark = bdark; - w->accent_bg = abg; - w->accent_fg = afg; + w->bevel_divider = bdivider; + w->button_bg = btnbg; + w->button_fg = btnfg; + w->button_pressed = btnpressed; + w->accent = accent; if (config != NULL && config->titlebar_height > 0) { @@ -213,28 +251,44 @@ result_t wuss_create(screen_t *scr, #ifdef WUSS_ICONS if (config != NULL) { - blight = wuss__resolve_colour(w, config->bevel.light); - bdark = wuss__resolve_colour(w, config->bevel.dark); - abg = wuss__resolve_colour(w, config->accent.bg); - afg = wuss__resolve_colour(w, config->accent.fg); + blight = wuss__resolve_colour(w, config->bevel.light); + bdark = wuss__resolve_colour(w, config->bevel.dark); + bdivider = (config->bevel.divider == wuss_NO_BACKGROUND) + ? blight + : wuss__resolve_colour(w, config->bevel.divider); + btnbg = (config->button.bg == wuss_NO_BACKGROUND) + ? blight + : wuss__resolve_colour(w, config->button.bg); + btnfg = wuss__resolve_colour(w, config->button.fg); + btnpressed = (config->button.pressed == wuss_NO_BACKGROUND) + ? bdark + : wuss__resolve_colour(w, config->button.pressed); + accent = wuss__resolve_colour(w, config->accent.colour); } else { - blight = 0; - bdark = 0; - abg = 0; - afg = (w->npalette > 1) ? 1 : 0; + blight = 0; + bdark = 0; + bdivider = 0; + btnbg = 0; + btnfg = (w->npalette > 1) ? 1 : 0; + btnpressed = 0; + accent = 0; } - if (validate_bevel_backdrop(w, blight, bdark, abg, afg) != result_OK) + if (validate_bevel_backdrop(w, blight, bdark, bdivider, + btnbg, btnfg, btnpressed, accent) != result_OK) { wuss__free(w, w->palette); wuss__free(w, w); return result_WUSS_BAD_COLOUR; } - w->bevel_light = blight; - w->bevel_dark = bdark; - w->accent_bg = abg; - w->accent_fg = afg; + w->bevel_light = blight; + w->bevel_dark = bdark; + w->bevel_divider = bdivider; + w->button_bg = btnbg; + w->button_fg = btnfg; + w->button_pressed = btnpressed; + w->accent = accent; #else if (wuss__validate_backdrop(w, &w->backdrop) != result_OK) { @@ -276,6 +330,10 @@ result_t wuss_create(screen_t *scr, #ifdef WUSS_ICONS w->pressed_icon = NULL; w->hover_icon = NULL; + w->icon.names = NULL; + w->icon.atoms = NULL; + w->icon.bitmaps = NULL; + w->icon.nbitmaps = 0; #endif #ifdef WUSS_MENUS w->menu_chain = NULL; diff --git a/libraries/wuss/core/destroy.c b/libraries/wuss/core/destroy.c index b8582de..6734e7b 100644 --- a/libraries/wuss/core/destroy.c +++ b/libraries/wuss/core/destroy.c @@ -10,46 +10,98 @@ void wuss_destroy(wuss_t *doomed) { - list_t *e; + list_t *e; + wuss_task_t *menu_task; if (doomed == NULL) return; #ifdef WUSS_MENUS - /* Drop any open menu chain first: its windows are freed by the z_order - * sweep below, but the chain nodes and their icon-handle arrays are not. */ - wuss_menu_close(doomed->menu_chain); + menu_task = doomed->menu_task; +#else + menu_task = NULL; #endif - e = doomed->z_order.next; + /* QUIT then free any tasks the caller left registered, so a task's + * client-owned task_data (freed only from its QUIT handler, per the + * task_data-ownership contract) is not leaked. Done before the menu-chain + * and window teardown below so a QUIT handler can still close its own + * windows and its own menu chain (via a wuss_menu_handle_t it still holds) + * against live objects -- the leftover-chain close and the z_order sweep + * then just find less to do. + * + * The internal wuss__menu_task is skipped here and torn down last: a client + * task's QUIT handler may wuss_menu_close a chain it still holds, which + * closes menu windows off wuss__menu_task's window list -- so that task + * must outlive every client QUIT, regardless of registration order (it is + * created lazily on the first wuss_menu_open, so it can sit anywhere). */ + e = doomed->tasks.next; while (e != NULL) { - list_t *next; + list_t *next; + wuss_event_t event; next = e->next; -#ifdef WUSS_ICONS - wuss__icons_free(wuss__window_from_link(e)); -#endif - wuss__free(doomed, e); + if (wuss__task_from_link(e) != menu_task) + { + wuss_task_t *task = wuss__task_from_link(e); + + /* Mark the teardown before QUIT so a handler that closes its last + * autoclose window -- or calls wuss_window_close / wuss_task_destroy + * on itself -- doesn't also fire QUIT and free this node from under + * the sweep, which would then wuss__free it a second time. Same guard + * wuss_task_destroy sets for its own internal QUIT. */ + task->flags |= wuss_TASK__REAPING; + + event.kind = wuss_EVENT_QUIT; + (void) wuss__deliver(task, NULL, &event); + wuss__free(doomed, e); + } e = next; } - /* Windows are gone; QUIT then free any tasks the caller left registered, - * so a task's client-owned task_data (freed only from its QUIT handler, - * per the task_data-ownership contract) is not leaked. */ - e = doomed->tasks.next; - while (e != NULL) +#ifdef WUSS_MENUS + /* Drop any menu chain a QUIT handler left open: its windows are freed by + * the z_order sweep below, but the chain nodes and their icon-handle + * arrays are not. NULL (the common case now) is a no-op. Still runs + * against a live wuss__menu_task. + * + * ponytail: a bare wuss_menu_close, not wuss__menu_abandon -- every client + * task is already freed by the sweep above, so the chain's owner (and any + * flashing pick's owner) is dangling and must not be delivered to. No path + * leaves a mid-flash pick standing at wuss_destroy today; track the flash + * owner per task if one ever does. */ + wuss_menu_close(doomed->menu_chain); + + /* Now the menu task has nothing left to own -- QUIT and free it. */ + if (menu_task != NULL) { - list_t *next; wuss_event_t event; - next = e->next; event.kind = wuss_EVENT_QUIT; - (void) wuss__deliver(wuss__task_from_link(e), NULL, &event); + (void) wuss__deliver(menu_task, NULL, &event); + wuss__free(doomed, &menu_task->link); + doomed->menu_task = NULL; + } +#endif + + e = doomed->z_order.next; + while (e != NULL) + { + list_t *next; + + next = e->next; +#ifdef WUSS_ICONS + wuss__icons_free(wuss__window_from_link(e)); +#endif wuss__free(doomed, e); e = next; } +#ifdef WUSS_ICONS + wuss__icons_registry_free(doomed); +#endif + packer_destroy(doomed->layout); wuss__free(doomed, doomed->palette); wuss__free(doomed, doomed); /* reads doomed->alloc.free before freeing */ diff --git a/libraries/wuss/core/idle.c b/libraries/wuss/core/idle.c index a27b1ef..d7f12b0 100644 --- a/libraries/wuss/core/idle.c +++ b/libraries/wuss/core/idle.c @@ -4,8 +4,8 @@ result_t wuss_idle(wuss_t *wuss) { - wuss_event_t event; result_t rc; + wuss_event_t event; list_t *e; event.kind = wuss_EVENT_IDLE; @@ -13,9 +13,9 @@ result_t wuss_idle(wuss_t *wuss) rc = result_OK; for (e = wuss->tasks.next; e != NULL; ) { + result_t crc; wuss_task_t *task; list_t *next; - result_t crc; task = (wuss_task_t *) e; next = e->next; /* an autoclose task may free its own node in the handler */ diff --git a/libraries/wuss/core/impl.h b/libraries/wuss/core/impl.h index 33ea210..30ebc4e 100644 --- a/libraries/wuss/core/impl.h +++ b/libraries/wuss/core/impl.h @@ -123,11 +123,16 @@ struct wuss wuss_furniture_palette_t furniture_colours; #endif #if defined(WUSS_FURNITURE) || defined(WUSS_ICONS) - wuss_colour_t bevel_light; /* work-area button top/left edge */ - wuss_colour_t bevel_dark; /* work-area button bottom/right edge */ - wuss_colour_t accent_bg; /* default action button fill */ - wuss_colour_t accent_fg; /* default action button text */ + wuss_colour_t bevel_light; /* work-area button top/left edge */ + wuss_colour_t bevel_dark; /* work-area button bottom/right edge */ + wuss_colour_t bevel_divider; /* lighter edge for a DIVIDER border */ + wuss_colour_t button_bg; /* button face when spec bg is NONE */ + 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 + wuss_colour_t window_bg; /* work-area body fill; wuss_COLOUR_WINDOW */ + wuss_colour_t menu_bg; /* menu body fill; wuss_COLOUR_MENU */ wuss_backdrop_t backdrop; /* colour==wuss_NO_BACKGROUND: none */ #ifdef WUSS_FURNITURE int titlebar_height; @@ -159,6 +164,20 @@ struct wuss * over, NULL when none; drives * hover-highlight repaint of * menu-entry icons */ + /* 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 + * in it (atom values are not array indices, so the position is tracked + * explicitly); bitmaps[i] is the matching RLE-compressed bitmap. All + * owned; freed by wuss__icons_registry_free. NULL / 0 until first load. */ + struct + { + struct atom_set *names; + int *atoms; + bitmap_t *bitmaps; + int nbitmaps; + } + icon; #endif #ifdef WUSS_MENUS struct wuss__menu *menu_chain; /* head (root) of the open pop-up @@ -204,6 +223,7 @@ struct wuss_window #ifdef WUSS_FURNITURE box_t pre_toggle; /* visible bounds to restore on the next toggle */ char title[WUSS_TITLE_MAX + 1]; + wuss__furniture_layout_t furniture_layout; /* lazily built; see furniture.h */ #endif #ifdef WUSS_ICONS wuss_icon_t **icons; /* owned; array of owned icon pointers */ @@ -237,6 +257,14 @@ static inline void wuss__chrome_repaint_for(wuss_window_t *window, { window->wuss->furniture_ops->invalidate_for(window, visible); } + +/* Drop just the cached furniture layout, without queuing any dirty region -- + * for a window move, where the caller already handles the repaint but the + * cache (absolute coords) must be rebuilt for the new position. */ +static inline void wuss__chrome_invalidate_layout(wuss_window_t *window) +{ + window->furniture_layout.valid = 0; +} #else static inline void wuss__chrome_draw(wuss_t *wuss, wuss_window_t *window, @@ -258,10 +286,34 @@ static inline void wuss__chrome_repaint_for(wuss_window_t *window, (void) window; (void) visible; } + +static inline void wuss__chrome_invalidate_layout(wuss_window_t *window) +{ + (void) window; +} #endif 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 @@ -371,7 +423,9 @@ void wuss__scroll_step(wuss_window_t *window, point_t delta); #ifdef WUSS_FURNITURE void wuss__titlebar_box(const wuss_window_t *window, box_t *out); +void wuss__title_hit_box(const wuss_window_t *window, box_t *out); void wuss__close_box(const wuss_window_t *window, box_t *out); +void wuss__close_hit_box(const wuss_window_t *window, box_t *out); void wuss__content_box(const wuss_window_t *window, box_t *out); #else /* No furniture: the content area is the whole visible footprint. */ @@ -458,6 +512,14 @@ result_t wuss__deliver(wuss_task_t *task, wuss_window_t *win_or_null, const wuss_event_t *ev); +#ifdef WUSS_MENUS +/* Tear down the whole open menu chain because wuss decided to (not the + * client): unlinks wuss->menu_chain, delivers wuss_EVENT_MENU_CLOSED to its + * owner so a stored handle is dropped, then frees the nodes. No-op if no + * chain is open. Defined in menu/menu.c. */ +void wuss__menu_abandon(wuss_t *wuss); +#endif + /* Notify a window's task that it has been moved or resized, via * wuss_EVENT_OPEN; the return value is discarded, matching how furniture * drawing and other in-line notifications are treated. */ diff --git a/libraries/wuss/core/rebuild-palettecache.c b/libraries/wuss/core/rebuild-palettecache.c index a2350fa..7b904b4 100644 --- a/libraries/wuss/core/rebuild-palettecache.c +++ b/libraries/wuss/core/rebuild-palettecache.c @@ -50,9 +50,15 @@ void wuss__rebuild_palettecache(wuss_t *wuss) #if defined(WUSS_FURNITURE) || defined(WUSS_ICONS) cache[wuss_COLOUR_BUTTON_HILIGHT - wuss_COLOUR_SYMBOLIC] = wuss->bevel_light; cache[wuss_COLOUR_BUTTON_SHADOW - wuss_COLOUR_SYMBOLIC] = wuss->bevel_dark; - cache[wuss_COLOUR_ACCENT_BG - wuss_COLOUR_SYMBOLIC] = wuss->accent_bg; - cache[wuss_COLOUR_ACCENT_FG - wuss_COLOUR_SYMBOLIC] = wuss->accent_fg; + cache[wuss_COLOUR_BORDER_DIVIDER - wuss_COLOUR_SYMBOLIC] = wuss->bevel_divider; + cache[wuss_COLOUR_BUTTON_BG - wuss_COLOUR_SYMBOLIC] = wuss->button_bg; + 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 if (wuss->backdrop.colour != wuss_NO_BACKGROUND) cache[wuss_COLOUR_BACKDROP - wuss_COLOUR_SYMBOLIC] = wuss->backdrop.colour; + + cache[wuss_COLOUR_WINDOW - wuss_COLOUR_SYMBOLIC] = wuss->window_bg; + cache[wuss_COLOUR_MENU - wuss_COLOUR_SYMBOLIC] = wuss->menu_bg; } diff --git a/libraries/wuss/core/redraw.c b/libraries/wuss/core/redraw.c index fe6d31a..d06481a 100644 --- a/libraries/wuss/core/redraw.c +++ b/libraries/wuss/core/redraw.c @@ -3,10 +3,11 @@ #include "impl.h" /* Fill "area" with the desktop backdrop, except for whatever part of it is - * covered by a wuss_NO_BACKGROUND window's content -- that task owns those - * pixels outright, so a desktop-coloured pre-fill there would flash (or, - * for a task that doesn't repaint every pixel every time, permanently - * show) backdrop instead of whatever was already on screen. */ + * covered by a window's visible box -- every non-hidden window opaquely + * repaints its whole visible box each redraw (furniture over the chrome + * band, task/backdrop over the content), so a desktop pre-fill underneath + * is wasted work and, for a wuss_NO_BACKGROUND task that doesn't repaint + * every pixel, a flash of backdrop over what was there. */ static void fill_backdrop_excluding_content(wuss_t *wuss, const box_t *area) { box_t cuts[WUSS_MAX_INVALIDATE_PIECES]; @@ -20,16 +21,13 @@ static void fill_backdrop_excluding_content(wuss_t *wuss, const box_t *area) e = e->next) { wuss_window_t *win; - box_t content, clipped; + box_t clipped; win = wuss__window_from_link(e); if (win->flags & wuss_WINDOW_HIDDEN) continue; - if (win->bg.colour != wuss_NO_BACKGROUND) - continue; - wuss__content_box(win, &content); - if (box_intersection(&content, area, &clipped)) + if (box_intersection(&win->visible, area, &clipped)) continue; /* no overlap with the area being filled */ cuts[ncuts++] = clipped; @@ -90,9 +88,12 @@ static void redraw_window(wuss_t *wuss, content.x0 - win->scroll.x, content.y0 - win->scroll.y); + /* NO_REDRAW: the backdrop fill above plus the icons below are the + * window's whole appearance, so skip the client redraw entirely */ + if (!(win->flags & wuss_WINDOW_NO_REDRAW)) { - wuss_event_t event; result_t crc; + wuss_event_t event; event.kind = wuss_EVENT_REDRAW; event.data.redraw.scr = wuss->scr; @@ -147,8 +148,8 @@ static void redraw_from(wuss_t *wuss, result_t wuss_redraw(wuss_t *wuss) { - box_t full; result_t rc; + box_t full; full.x0 = 0; full.y0 = 0; diff --git a/libraries/wuss/core/set-backdrop.c b/libraries/wuss/core/set-backdrop.c index d098467..48a9f14 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) { - box_t screen; result_t rc; + box_t screen; wuss_backdrop_t resolved; assert(wuss != NULL); diff --git a/libraries/wuss/core/set-palette.c b/libraries/wuss/core/set-palette.c index e459990..3bda667 100644 --- a/libraries/wuss/core/set-palette.c +++ b/libraries/wuss/core/set-palette.c @@ -11,9 +11,9 @@ result_t wuss_set_palette(wuss_t *wuss, const colour_t *palette, int npalette) { + result_t rc; wuss_event_t event; box_t screen; - result_t rc; list_t *e; assert(wuss != NULL); @@ -33,9 +33,9 @@ result_t wuss_set_palette(wuss_t *wuss, event.kind = wuss_EVENT_PALETTE; for (e = wuss->tasks.next; e != NULL; ) { + result_t crc; wuss_task_t *task; list_t *next; - result_t crc; task = (wuss_task_t *) e; next = e->next; /* an autoclose task may free its own node in the handler */ diff --git a/libraries/wuss/core/task/deliver.c b/libraries/wuss/core/task/deliver.c index 6be99e2..02d8062 100644 --- a/libraries/wuss/core/task/deliver.c +++ b/libraries/wuss/core/task/deliver.c @@ -29,6 +29,7 @@ static int wuss__kind_ok_for(wuss_event_kind_t kind, int have_window) case wuss_EVENT_QUIT: case wuss_EVENT_PALETTE: case wuss_EVENT_MENU_SELECT: + case wuss_EVENT_MENU_CLOSED: return !have_window; case wuss_EVENT_ICON: diff --git a/libraries/wuss/core/task/destroy.c b/libraries/wuss/core/task/destroy.c index 74c7760..2268c05 100644 --- a/libraries/wuss/core/task/destroy.c +++ b/libraries/wuss/core/task/destroy.c @@ -19,6 +19,12 @@ void wuss_task_destroy(wuss_task_t *doomed) if (doomed == NULL) return; + /* Already being reaped -- by wuss_destroy's sweep, or a re-entrant call + * from this task's own QUIT handler. The outer teardown owns the unlink + * and free; doing it here too would double-free the node. */ + if (doomed->flags & wuss_TASK__REAPING) + return; + wuss = doomed->wuss; /* Mark the teardown so wuss_window_close's autoclose path doesn't also @@ -26,12 +32,14 @@ void wuss_task_destroy(wuss_task_t *doomed) doomed->flags |= wuss_TASK__REAPING; #ifdef WUSS_MENUS - /* If this task opened the live menu chain, close it now: MENU_SELECT is - * delivered to the chain's owner, and leaving the chain open would leave - * its owner (and any pending pick flash's owner) pointing at `doomed` - * after it is freed below. */ + /* If this task opened the live menu chain, tear it down now: leaving it + * open would leave its owner (and any pending pick flash's owner) pointing + * at `doomed` after it is freed below. wuss__menu_abandon also delivers + * wuss_EVENT_MENU_CLOSED to `doomed`, so a wuss_menu_handle_t it still + * holds is nulled before the QUIT below -- otherwise a QUIT handler that + * calls wuss_menu_close on that stale handle double-frees the chain. */ if (wuss->menu_chain != NULL && wuss->menu_chain->owner == doomed) - wuss_menu_close(wuss->menu_chain); + wuss__menu_abandon(wuss); #endif /* One QUIT while every window is still alive. */ diff --git a/libraries/wuss/core/text.c b/libraries/wuss/core/text.c new file mode 100644 index 0000000..6d04abe --- /dev/null +++ b/libraries/wuss/core/text.c @@ -0,0 +1,43 @@ +/* text.c -- wuss centralised bitmap-font text rendering */ + +#include "geom/point.h" +#include "framebuf/bmfont.h" +#include "framebuf/screen.h" + +#include "impl.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, + const char *text, + int len, + bmfont_width_t target_width, + int *split_point, + bmfont_width_t *actual_width) +{ + return bmfont_measure(font, text, len, target_width, split_point, + 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) +{ + 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); +} diff --git a/libraries/wuss/core/window/create-placed.c b/libraries/wuss/core/window/create-placed.c index f348417..7c1b739 100644 --- a/libraries/wuss/core/window/create-placed.c +++ b/libraries/wuss/core/window/create-placed.c @@ -83,8 +83,8 @@ result_t wuss_window_create_placed(wuss_task_t *task, size2d_t min_doc, wuss_window_t **window) { - wuss_t *wuss; result_t rc; + wuss_t *wuss; int left, top, right, bottom; int fw, fh; box_t screen, content, consumed; diff --git a/libraries/wuss/core/window/create.c b/libraries/wuss/core/window/create.c index a48ed7b..941807f 100644 --- a/libraries/wuss/core/window/create.c +++ b/libraries/wuss/core/window/create.c @@ -97,6 +97,7 @@ result_t wuss_window_create(wuss_task_t *task, win->min_doc = min_doc; #ifdef WUSS_FURNITURE win->state = wuss_WINDOW_STATE_NONE; + win->furniture_layout.valid = 0; /* built lazily on first paint */ #endif #ifdef WUSS_ICONS win->icons = NULL; diff --git a/libraries/wuss/core/window/move.c b/libraries/wuss/core/window/move.c index 9c96a85..6d1357d 100644 --- a/libraries/wuss/core/window/move.c +++ b/libraries/wuss/core/window/move.c @@ -18,6 +18,10 @@ 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); diff --git a/libraries/wuss/core/window/resize.c b/libraries/wuss/core/window/resize.c index a3d26e1..c876422 100644 --- a/libraries/wuss/core/window/resize.c +++ b/libraries/wuss/core/window/resize.c @@ -135,6 +135,12 @@ result_t wuss_window_resize(wuss_window_t *window, size2d_t size) box_union(&before, &window->visible, &dirty); wuss__invalidate_clipped(window, &dirty); + + /* the dirty box above covers every furniture strip, but nothing here + * has dropped the cached furniture layout -- it still holds the old + * width's rects. Force a rebuild so the redraw paints furniture at the + * new geometry (the else branch gets this via wuss__chrome_repaint). */ + wuss__chrome_invalidate_layout(window); } else { diff --git a/libraries/wuss/core/window/set-hidden.c b/libraries/wuss/core/window/set-hidden.c index 373c247..f1b9467 100644 --- a/libraries/wuss/core/window/set-hidden.c +++ b/libraries/wuss/core/window/set-hidden.c @@ -8,8 +8,8 @@ result_t wuss_window_set_hidden(wuss_window_t *window, int hidden) { - wuss_event_t event; result_t rc; + wuss_event_t event; int was_hidden; was_hidden = (window->flags & wuss_WINDOW_HIDDEN) != 0; @@ -18,8 +18,27 @@ result_t wuss_window_set_hidden(wuss_window_t *window, int hidden) if (hidden) { + wuss_t *wuss = window->wuss; + + /* Drop any pointer state that targets this window: an off-screen window + * must not keep being driven by the mouse. Same set wuss_window_close + * clears. */ +#ifdef WUSS_FURNITURE + if (wuss->furniture.dragging == window) + { + wuss->furniture.dragging = NULL; + wuss->furniture.drag_kind = wuss_FURNITURE_DRAG_NONE; + } +#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; +#endif + /* still on screen: repaint its footprint now, then mark it gone */ - wuss_invalidate(window->wuss, &window->visible); + wuss_invalidate(wuss, &window->visible); window->flags |= wuss_WINDOW_HIDDEN; return result_OK; } diff --git a/libraries/wuss/core/window/try-close.c b/libraries/wuss/core/window/try-close.c index d8a14a4..882c978 100644 --- a/libraries/wuss/core/window/try-close.c +++ b/libraries/wuss/core/window/try-close.c @@ -8,8 +8,8 @@ result_t wuss_window_try_close(wuss_window_t *window) { - wuss_event_t event; result_t rc; + wuss_event_t event; if (window == NULL) return result_OK; diff --git a/libraries/wuss/furniture.h b/libraries/wuss/furniture.h index 0bf378b..052e4fe 100644 --- a/libraries/wuss/furniture.h +++ b/libraries/wuss/furniture.h @@ -12,6 +12,57 @@ #define WUSS_SCROLL_END_GAP 2 /* sausage along-axis margin from its well's ends, purely cosmetic */ #define WUSS_SCROLL_STEP 20 /* pixels stepped per scrollbar arrow click */ +/* Cached furniture layout ------------------------------------------------- */ + +/* Which chrome colour a cached furniture rect is painted in. The layout + * stores this class rather than a resolved colour so a palette change needs + * no layout rebuild: wuss__furniture_draw resolves it through wuss->palette + * at paint time. */ +typedef enum wuss__furniture_paint_class +{ + wuss__FURNITURE_PAINT_TITLE_BG, /* titlebar fill, resize-carve bands */ + wuss__FURNITURE_PAINT_CLOSE, + wuss__FURNITURE_PAINT_BACK, + wuss__FURNITURE_PAINT_TOGGLE, + wuss__FURNITURE_PAINT_RESIZE, + wuss__FURNITURE_PAINT_SCROLL_ARROWS, + wuss__FURNITURE_PAINT_SCROLL_WELLS, + wuss__FURNITURE_PAINT_OUTLINE +} +wuss__furniture_paint_class_t; + +/* One filled rectangle in the cached layout. */ +typedef struct wuss__furniture_piece +{ + box_t rect; + wuss__furniture_paint_class_t paint; +} +wuss__furniture_piece_t; + +/* Upper bound on pieces a single window's furniture can contribute: + * titlebar(1) + close/back/toggle(3) + no-scroll resize bands(2) + + * resize icon + its two seams(3) + v/h scroll arrows+well(3+3) + + * two interior rules(2) + four outline edges(4) = 21. Round up. */ +#define WUSS__FURNITURE_MAX_PIECES 24 + +/* Per-window cache of the furniture layout: every filled rect except the two + * scrollbar sausages (which move with window->scroll and are recomputed each + * paint) and the title text (font-dependent, drawn live). Rebuilt lazily by + * wuss__furniture_draw when "valid" is 0; wuss__furniture_invalidate* clear + * it on any geometry change. */ +typedef struct wuss__furniture_layout +{ + int valid; + int npieces; + wuss__furniture_piece_t pieces[WUSS__FURNITURE_MAX_PIECES]; + box_t titlebar; /* for the live title-text pass; empty if no titlebar */ + int has_titlebar; +} +wuss__furniture_layout_t; + +/* Populate window->furniture_layout from the current geometry and flags. */ +void wuss__furniture_layout_build(wuss_window_t *window); + /* Which region of a window's border (or its content) a point falls in. */ typedef enum wuss_furniture_region { @@ -110,22 +161,33 @@ wuss__furniture_ops_t; extern const wuss__furniture_ops_t wuss__furniture_default_ops; -/* geometry: titlebar icons */ +/* geometry: titlebar icons. The _box helpers are the drawn rectangles; the + * _hit_box helpers are the same rectangles grown outward to tile the outline + * band and the window corners for hit testing (see furniture/hit-test.c). */ void wuss__back_box(const wuss_window_t *window, box_t *out); +void wuss__back_hit_box(const wuss_window_t *window, box_t *out); void wuss__toggle_box(const wuss_window_t *window, box_t *out); +void wuss__toggle_hit_box(const wuss_window_t *window, box_t *out); void wuss__resize_box(const wuss_window_t *window, box_t *out); +void wuss__resize_hit_box(const wuss_window_t *window, box_t *out); /* geometry: vertical scrollbar */ void wuss__vscroll_up_box(const wuss_window_t *window, box_t *out); +void wuss__vscroll_up_hit_box(const wuss_window_t *window, box_t *out); void wuss__vscroll_down_box(const wuss_window_t *window, box_t *out); +void wuss__vscroll_down_hit_box(const wuss_window_t *window, box_t *out); void wuss__vscroll_well_box(const wuss_window_t *window, box_t *out); +void wuss__vscroll_well_hit_box(const wuss_window_t *window, box_t *out); void wuss__vscroll_sausage_box(const wuss_window_t *window, box_t *out); int wuss__vscroll_well_px(const wuss_window_t *window); /* geometry: horizontal scrollbar */ void wuss__hscroll_left_box(const wuss_window_t *window, box_t *out); +void wuss__hscroll_left_hit_box(const wuss_window_t *window, box_t *out); void wuss__hscroll_right_box(const wuss_window_t *window, box_t *out); +void wuss__hscroll_right_hit_box(const wuss_window_t *window, box_t *out); void wuss__hscroll_well_box(const wuss_window_t *window, box_t *out); +void wuss__hscroll_well_hit_box(const wuss_window_t *window, box_t *out); void wuss__hscroll_sausage_box(const wuss_window_t *window, box_t *out); int wuss__hscroll_well_px(const wuss_window_t *window); diff --git a/libraries/wuss/furniture/back-box.c b/libraries/wuss/furniture/back-box.c index 3998a78..4a53450 100644 --- a/libraries/wuss/furniture/back-box.c +++ b/libraries/wuss/furniture/back-box.c @@ -17,3 +17,19 @@ void wuss__back_box(const wuss_window_t *window, box_t *out) out->x1 = out->x0 + size; out->y1 = out->y0 + size; } + +/* BACK is the leftmost titlebar icon, so its hit box owns the top-left corner: + * it grows left and up to the window edge, swallowing the outline pixel and the + * inset strip between the outline and the drawn icon. Its right and bottom + * edges (facing CLOSE and the titlebar) are unchanged. */ +void wuss__back_hit_box(const wuss_window_t *window, box_t *out) +{ + box_t drawn; + + wuss__back_box(window, &drawn); + + out->x0 = window->visible.x0; + out->y0 = window->visible.y0; + out->x1 = drawn.x1; + out->y1 = drawn.y1; +} diff --git a/libraries/wuss/furniture/close-box.c b/libraries/wuss/furniture/close-box.c index e7c19c8..3de8147 100644 --- a/libraries/wuss/furniture/close-box.c +++ b/libraries/wuss/furniture/close-box.c @@ -19,3 +19,19 @@ void wuss__close_box(const wuss_window_t *window, box_t *out) out->x1 = out->x0 + size; out->y1 = out->y0 + size; } + +/* CLOSE always grows up over the top outline. It only owns the top-left corner + * when there is no BACK icon to its left; with BACK present its left edge is + * unchanged and butts BACK's right edge, so the two hit boxes tile with no + * seam. */ +void wuss__close_hit_box(const wuss_window_t *window, box_t *out) +{ + box_t drawn; + + wuss__close_box(window, &drawn); + + out->x0 = (window->flags & wuss_WINDOW_NO_BACK) ? window->visible.x0 : drawn.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 38d73fe..a9b9405 100644 --- a/libraries/wuss/furniture/draw.c +++ b/libraries/wuss/furniture/draw.c @@ -1,5 +1,13 @@ /* wuss/furniture/draw.c -- wuss - minimal window manager */ +/* Two-phase furniture paint. Phase 1 (wuss__furniture_layout_build, cached) + * computes every filled rect from the window geometry. Phase 2 (here) walks + * the cache clipping each rect to the redraw region "full" and filling it. + * The two scrollbar sausages track window->scroll so they are recomputed + * each paint rather than cached; the title string is font-dependent and + * drawn live. */ + +#include #include #include "base/utils.h" @@ -25,140 +33,131 @@ static void fill_furniture_rect(wuss_t *wuss, screen_fill_rect(wuss->scr, b->x0, b->y0, box_size(b), colour); } -void wuss__furniture_draw(wuss_t *wuss, - wuss_window_t *window, - const box_t *full) +/* Resolve a cached piece's paint class to a concrete palette colour. */ +static colour_t paint_colour(const wuss_t *wuss, + wuss__furniture_paint_class_t paint) { - box_t visible_clipped; - box_t clipped; - box_t titlebar; - bmfont_t *titlefont; + const wuss_furniture_palette_t *fc; + + fc = &wuss->furniture_colours; + + switch (paint) + { + case wuss__FURNITURE_PAINT_TITLE_BG: return wuss->palette[fc->title.bg]; + case wuss__FURNITURE_PAINT_CLOSE: return wuss->palette[fc->close]; + case wuss__FURNITURE_PAINT_BACK: return wuss->palette[fc->back]; + case wuss__FURNITURE_PAINT_TOGGLE: return wuss->palette[fc->toggle]; + case wuss__FURNITURE_PAINT_RESIZE: return wuss->palette[fc->resize]; + case wuss__FURNITURE_PAINT_SCROLL_ARROWS: return wuss->palette[fc->scroll.arrows]; + case wuss__FURNITURE_PAINT_SCROLL_WELLS: return wuss->palette[fc->scroll.wells]; + case wuss__FURNITURE_PAINT_OUTLINE: return wuss->palette[fc->outline]; + } + + assert(!"unhandled furniture paint class"); + return wuss->palette[fc->title.bg]; +} + +/* The title string, drawn into its titlebar slot. Split out of the main + * routine only to keep the phase-2 loop readable; still runs every paint + * because measurement depends on the current font. */ +static void draw_title(wuss_t *wuss, + wuss_window_t *window, + const box_t *titlebar, + const box_t *titlebar_clip) +{ + bmfont_t *titlefont; + point_t pos; + int text_x0, text_x1, titlelen, split_point; + bmfont_width_t width; + box_t text_box, text_clip; + + if (window->title[0] == '\0') + return; /* 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]; + if (titlefont == NULL) + return; - if (box_intersection(&window->visible, full, &visible_clipped)) - return; /* offscreen */ + text_x0 = titlebar->x0 + 2; + if (!(window->flags & wuss_WINDOW_NO_CLOSE)) + { + box_t close; - wuss__titlebar_box(window, &titlebar); - if (!box_intersection(&titlebar, full, &clipped)) + wuss__close_box(window, &close); + text_x0 = close.x1 + 2; + } + else if (!(window->flags & wuss_WINDOW_NO_BACK)) { - wuss->scr->clip = clipped; - screen_fill_rect(wuss->scr, - titlebar.x0, titlebar.y0, box_size(&titlebar), - wuss->palette[wuss->furniture_colours.title.bg]); - - if (titlefont != NULL && window->title[0] != '\0') - { - point_t pos; - int text_x0, text_x1, titlelen, split_point; - bmfont_width_t width; - box_t text_box, text_clip; - - text_x0 = titlebar.x0 + 2; - if (!(window->flags & wuss_WINDOW_NO_CLOSE)) - { - box_t close; - - wuss__close_box(window, &close); - text_x0 = close.x1 + 2; - } - else if (!(window->flags & wuss_WINDOW_NO_BACK)) - { - box_t back; - - wuss__back_box(window, &back); - text_x0 = back.x1 + 2; - } - - text_x1 = titlebar.x1 - 2; - if (!(window->flags & wuss_WINDOW_NO_TOGGLE_SIZE)) - { - box_t toggle; - - wuss__toggle_box(window, &toggle); - text_x1 = toggle.x0 - 2; - } - - /* A title too wide for its slot mustn't bleed into a neighbouring - * icon: clip drawing to the slot itself, not just the whole titlebar, - * so a too-long title is cut off cleanly rather than overdrawing - * whatever furniture the current redraw didn't happen to touch. */ - text_box.x0 = text_x0; - text_box.y0 = titlebar.y0; - text_box.x1 = text_x1; - text_box.y1 = titlebar.y1; - if (text_x1 > text_x0 && !box_intersection(&text_box, &clipped, &text_clip)) - { - titlelen = (int) strlen(window->title); - bmfont_measure(titlefont, window->title, titlelen, text_x1 - text_x0, &split_point, &width); - - pos.x = (split_point < titlelen) ? text_x0 : text_x0 + MAX(0, ((text_x1 - text_x0) - width) / 2); - pos.y = titlebar.y0 + 2; - wuss->scr->clip = text_clip; - bmfont_draw(titlefont, wuss->scr, window->title, titlelen, - wuss->palette[wuss->furniture_colours.title.fg], wuss->palette[wuss->furniture_colours.title.bg], - &pos, NULL); - wuss->scr->clip = clipped; - } - } - - if (!(window->flags & wuss_WINDOW_NO_CLOSE)) - { - box_t close; - - wuss__close_box(window, &close); - fill_furniture_rect(wuss, &close, full, - wuss->palette[wuss->furniture_colours.close]); - } - - if (!(window->flags & wuss_WINDOW_NO_BACK)) - { - box_t back; - - wuss__back_box(window, &back); - fill_furniture_rect(wuss, &back, full, - wuss->palette[wuss->furniture_colours.back]); - } - - if (!(window->flags & wuss_WINDOW_NO_TOGGLE_SIZE)) - { - box_t toggle; - - wuss__toggle_box(window, &toggle); - fill_furniture_rect(wuss, &toggle, full, - wuss->palette[wuss->furniture_colours.toggle]); - } + box_t back; + + wuss__back_box(window, &back); + text_x0 = back.x1 + 2; } - if (!(window->flags & wuss_WINDOW_NO_RESIZE)) + text_x1 = titlebar->x1 - 2; + if (!(window->flags & wuss_WINDOW_NO_TOGGLE_SIZE)) { - box_t resize; + box_t toggle; - wuss__resize_box(window, &resize); - fill_furniture_rect(wuss, &resize, full, - wuss->palette[wuss->furniture_colours.resize]); + wuss__toggle_box(window, &toggle); + text_x1 = toggle.x0 - 2; } - if (!(window->flags & wuss_WINDOW_NO_VSCROLL)) - { - box_t up, down, well, sausage; + /* A title too wide for its slot mustn't bleed into a neighbouring icon: + * clip drawing to the slot itself, not just the whole titlebar, so a + * too-long title is cut off cleanly rather than overdrawing whatever + * furniture the current redraw didn't happen to touch. */ + text_box.x0 = text_x0; + text_box.y0 = titlebar->y0; + text_box.x1 = text_x1; + text_box.y1 = titlebar->y1; + if (text_x1 <= text_x0 || box_intersection(&text_box, titlebar_clip, &text_clip)) + return; + + titlelen = (int) strlen(window->title); + wuss__text_measure(titlefont, window->title, titlelen, text_x1 - text_x0, &split_point, &width); + + pos.x = (split_point < titlelen) ? text_x0 : text_x0 + MAX(0, ((text_x1 - text_x0) - width) / 2); + pos.y = titlebar->y0 + 2; + wuss->scr->clip = text_clip; + wuss__text_draw(titlefont, wuss->scr, window->title, titlelen, + wuss->palette[wuss->furniture_colours.title.fg], + wuss->palette[wuss->furniture_colours.title.bg], + &pos, NULL); +} + +void wuss__furniture_draw(wuss_t *wuss, + wuss_window_t *window, + const box_t *full) +{ + box_t visible_clipped; + box_t sausage; + int i; - wuss__vscroll_up_box(window, &up); - fill_furniture_rect(wuss, &up, full, - wuss->palette[wuss->furniture_colours.scroll.arrows]); + if (box_intersection(&window->visible, full, &visible_clipped)) + return; /* offscreen */ - wuss__vscroll_down_box(window, &down); - fill_furniture_rect(wuss, &down, full, - wuss->palette[wuss->furniture_colours.scroll.arrows]); + if (!window->furniture_layout.valid) + wuss__furniture_layout_build(window); - wuss__vscroll_well_box(window, &well); - fill_furniture_rect(wuss, &well, full, - wuss->palette[wuss->furniture_colours.scroll.wells]); + /* phase 2: clip-and-fill every cached rect */ + for (i = 0; i < window->furniture_layout.npieces; i++) + { + const wuss__furniture_piece_t *piece; + piece = &window->furniture_layout.pieces[i]; + fill_furniture_rect(wuss, &piece->rect, full, + paint_colour(wuss, piece->paint)); + } + + /* the two scrollbar sausages: geometry depends on window->scroll, so they + * are computed live rather than cached */ + if (!(window->flags & wuss_WINDOW_NO_VSCROLL)) + { wuss__vscroll_sausage_box(window, &sausage); fill_furniture_rect(wuss, &sausage, full, wuss->palette[wuss->furniture_colours.scroll.sausages]); @@ -166,68 +165,17 @@ void wuss__furniture_draw(wuss_t *wuss, if (!(window->flags & wuss_WINDOW_NO_HSCROLL)) { - box_t left, right, well, sausage; - - wuss__hscroll_left_box(window, &left); - fill_furniture_rect(wuss, &left, full, - wuss->palette[wuss->furniture_colours.scroll.arrows]); - - wuss__hscroll_right_box(window, &right); - fill_furniture_rect(wuss, &right, full, - wuss->palette[wuss->furniture_colours.scroll.arrows]); - - wuss__hscroll_well_box(window, &well); - fill_furniture_rect(wuss, &well, full, - wuss->palette[wuss->furniture_colours.scroll.wells]); - wuss__hscroll_sausage_box(window, &sausage); fill_furniture_rect(wuss, &sausage, full, wuss->palette[wuss->furniture_colours.scroll.sausages]); } + /* the title string, drawn live over its (already-filled) titlebar slot */ + if (window->furniture_layout.has_titlebar) { - box_t content, rule; - point_t carve; - - /* Interior rules: where furniture is carved off the content area's right - * or bottom edge, the last pixel of the carve is a dividing line. */ - wuss__content_box(window, &content); - wuss__furniture_carve_for(window->flags, wuss__button_size(window), &carve); - - if (carve.x > 0) - { - rule.x0 = content.x1; - rule.x1 = content.x1 + WUSS_DIVIDER_PX; - rule.y0 = content.y0; - rule.y1 = content.y1; - fill_furniture_rect(wuss, &rule, full, - wuss->palette[wuss->furniture_colours.title.bg]); - } - - if (carve.y > 0) - { - rule.x0 = content.x0; - rule.x1 = content.x1 + ((carve.x > 0) ? WUSS_DIVIDER_PX : 0); /* meet the vertical rule at the corner */ - rule.y0 = content.y1; - rule.y1 = content.y1 + WUSS_DIVIDER_PX; - fill_furniture_rect(wuss, &rule, full, - wuss->palette[wuss->furniture_colours.title.bg]); - } - } + box_t titlebar_clip; - if (!(window->flags & wuss_WINDOW_NO_OUTLINE)) - { - int width, height; - colour_t border; - - width = window->visible.x1 - window->visible.x0; - height = window->visible.y1 - window->visible.y0; - border = wuss->palette[wuss->furniture_colours.title.bg]; /* no dedicated outline class; matches titlebar chrome */ - - wuss->scr->clip = visible_clipped; - screen_fill_rect(wuss->scr, window->visible.x0, window->visible.y0, SIZE2D(width, 1), border); - screen_fill_rect(wuss->scr, window->visible.x0, window->visible.y1 - 1, SIZE2D(width, 1), border); - screen_fill_rect(wuss->scr, window->visible.x0, window->visible.y0, SIZE2D(1, height), border); - screen_fill_rect(wuss->scr, window->visible.x1 - 1, window->visible.y0, SIZE2D(1, height), border); + if (!box_intersection(&window->furniture_layout.titlebar, full, &titlebar_clip)) + draw_title(wuss, window, &window->furniture_layout.titlebar, &titlebar_clip); } } diff --git a/libraries/wuss/furniture/hit-test.c b/libraries/wuss/furniture/hit-test.c index b243900..04bc36c 100644 --- a/libraries/wuss/furniture/hit-test.c +++ b/libraries/wuss/furniture/hit-test.c @@ -2,6 +2,58 @@ #include "../core/impl.h" +/* Attribute a point that is inside window->visible but outside the content box + * and outside every furniture hit box above to the nearest furniture. Such a + * point is a bare outline/carve pixel: an edge that has a scrollbar strip + * running its full length hands off to that strip's well; every other edge + * pixel (including the carve band left over when only a lone resize corner is + * present -- the corner itself already has its own hit box) is frame, not a + * widget, so it resolves to TITLE or CONTENT rather than starting a resize or + * scrollbar drag from somewhere the user cannot see the handle. */ +static wuss_furniture_region_t nearest_edge_region(const wuss_window_t *window, + point_t p) +{ + box_t content; + int has_v, has_h, has_title; + + 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); + + /* bottom edge: the hscroll strip runs its full width */ + if (p.y >= content.y1) + return has_h ? wuss_FURNITURE_HSCROLL_WELL + : has_title ? wuss_FURNITURE_TITLE + : wuss_FURNITURE_CONTENT; + + /* right edge: the vscroll strip runs its full height */ + if (p.x >= content.x1) + return has_v ? wuss_FURNITURE_VSCROLL_WELL + : has_title ? wuss_FURNITURE_TITLE + : wuss_FURNITURE_CONTENT; + + if (p.y < content.y0) + return has_title ? wuss_FURNITURE_TITLE : wuss_FURNITURE_CONTENT; + + /* left outline column, level with the content: only the hscroll strip's + * grown hit box reaches here, and only if there is one */ + return has_h ? wuss_FURNITURE_HSCROLL_WELL + : has_title ? wuss_FURNITURE_TITLE + : wuss_FURNITURE_CONTENT; +} + +/* The furniture hit boxes are grown outward from the content box to tile every + * pixel of window->visible -- the drawn boxes are the un-suffixed wuss__*_box + * helpers, these _hit_box variants swallow the outline band, the four corners + * and the interior divider seam. Grown boxes OVERLAP where they meet, so the + * test order below is load-bearing: + * - icons before TITLE: the titlebar corners belong to BACK / CLOSE / TOGGLE; + * - RESIZE before the scroll strips: the bottom-right corner is the resize + * icon when there is one; + * - VSCROLL before HSCROLL: with no resize icon the bottom-right corner + * resolves to VSCROLL_DOWN. */ wuss_furniture_region_t wuss__furniture_hit_test(const wuss_window_t *window, point_t p) { @@ -11,66 +63,74 @@ wuss_furniture_region_t wuss__furniture_hit_test(const wuss_window_t *window, { if (!(window->flags & wuss_WINDOW_NO_BACK)) { - wuss__back_box(window, &box); + 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)) { - wuss__close_box(window, &box); + 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)) { - wuss__toggle_box(window, &box); + wuss__toggle_hit_box(window, &box); if (box_contains_point(&box, p.x, p.y)) return wuss_FURNITURE_TOGGLE_SIZE; } - wuss__titlebar_box(window, &box); + wuss__title_hit_box(window, &box); if (box_contains_point(&box, p.x, p.y)) return wuss_FURNITURE_TITLE; } if (!(window->flags & wuss_WINDOW_NO_RESIZE)) { - wuss__resize_box(window, &box); + 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)) { - wuss__vscroll_up_box(window, &box); + wuss__vscroll_up_hit_box(window, &box); if (box_contains_point(&box, p.x, p.y)) return wuss_FURNITURE_VSCROLL_UP; - wuss__vscroll_down_box(window, &box); + wuss__vscroll_down_hit_box(window, &box); if (box_contains_point(&box, p.x, p.y)) return wuss_FURNITURE_VSCROLL_DOWN; - wuss__vscroll_well_box(window, &box); + wuss__vscroll_well_hit_box(window, &box); if (box_contains_point(&box, p.x, p.y)) return wuss_FURNITURE_VSCROLL_WELL; } if (!(window->flags & wuss_WINDOW_NO_HSCROLL)) { - wuss__hscroll_left_box(window, &box); + wuss__hscroll_left_hit_box(window, &box); if (box_contains_point(&box, p.x, p.y)) return wuss_FURNITURE_HSCROLL_LEFT; - wuss__hscroll_right_box(window, &box); + wuss__hscroll_right_hit_box(window, &box); if (box_contains_point(&box, p.x, p.y)) return wuss_FURNITURE_HSCROLL_RIGHT; - wuss__hscroll_well_box(window, &box); + wuss__hscroll_well_hit_box(window, &box); if (box_contains_point(&box, p.x, p.y)) return wuss_FURNITURE_HSCROLL_WELL; } + /* The grown boxes cover every chrome pixel except the outline band on an edge + * with no scrollbar and no resize icon. Attribute that to the nearest + * furniture rather than letting it fall through to the workarea. */ + wuss__content_box(window, &box); + if (box_contains_point(&window->visible, p.x, p.y) && + !box_contains_point(&box, p.x, p.y)) + return nearest_edge_region(window, p); + return wuss_FURNITURE_CONTENT; } diff --git a/libraries/wuss/furniture/invalidate.c b/libraries/wuss/furniture/invalidate.c index 51c03d5..7b668b3 100644 --- a/libraries/wuss/furniture/invalidate.c +++ b/libraries/wuss/furniture/invalidate.c @@ -7,12 +7,20 @@ * (toggle-size) also mark the *old* furniture strips dirty, since a blit * that reused the old pixels leaves stale titlebar/scrollbar/outline * pixels sitting wherever those strips used to be. */ +/* The dirty rects marked here -- the titlebar strip, the right carve column + * (full height), the bottom carve row (full width) and the four outline edges + * -- already union to every pixel of "visible", so they also cover the grown + * furniture hit boxes (furniture/hit-test.c): nothing extra to invalidate. */ void wuss__furniture_invalidate_for(wuss_window_t *window, const box_t *visible) { int outline_px; point_t carve; + /* Every geometry change routes through here, so it is also the one place + * the cached furniture layout is dropped. */ + window->furniture_layout.valid = 0; + outline_px = wuss__outline_px(window); /* Ask for the same carve the layout uses rather than reading the * scrollbar flags directly: a window with both scrollbars off but resize diff --git a/libraries/wuss/furniture/layout.c b/libraries/wuss/furniture/layout.c new file mode 100644 index 0000000..9888b82 --- /dev/null +++ b/libraries/wuss/furniture/layout.c @@ -0,0 +1,239 @@ +/* wuss/furniture/layout.c -- wuss - minimal window manager */ + +/* Precompute the fixed part of a window's furniture layout -- every filled + * rectangle bar the two scrollbar sausages (which track window->scroll and + * are cheap to recompute each paint) and the title string (font-dependent). + * wuss__furniture_draw calls this once when the cache is stale, then does a + * pure clip-and-fill loop over the result. The cache is invalidated on every + * geometry change through wuss__furniture_invalidate*. */ + +#include + +#include "../core/impl.h" + +/* Append one rect/paint-class pair, unless it would overflow the fixed store + * (WUSS__FURNITURE_MAX_PIECES is sized to the worst case, so this is a guard + * against a future furniture addition, not an expected path). */ +static void push_piece(wuss__furniture_layout_t *layout, + const box_t *rect, + wuss__furniture_paint_class_t paint) +{ + wuss__furniture_piece_t *piece; + + if (layout->npieces >= WUSS__FURNITURE_MAX_PIECES) + { + assert(!"furniture layout piece overflow"); + return; + } + + piece = &layout->pieces[layout->npieces++]; + piece->rect = *rect; + piece->paint = paint; +} + +void wuss__furniture_layout_build(wuss_window_t *window) +{ + wuss__furniture_layout_t *layout; + box_t titlebar; + box_t content; + point_t carve; + int outline_px; + + layout = &window->furniture_layout; + layout->npieces = 0; + layout->has_titlebar = 0; + + outline_px = wuss__outline_px(window); + wuss__content_box(window, &content); + wuss__furniture_carve_for(window->flags, wuss__button_size(window), &carve); + + /* titlebar fill + its icons ------------------------------------------- */ + wuss__titlebar_box(window, &titlebar); + if (!(window->flags & wuss_WINDOW_NO_TITLEBAR)) + { + layout->titlebar = titlebar; + layout->has_titlebar = 1; + + push_piece(layout, &titlebar, wuss__FURNITURE_PAINT_TITLE_BG); + + /* dividing rule between the titlebar and the content, full width between + * the side outlines. Drawn as the titlebar's own bottom pixel row (no + * geometry moves), over the fill just pushed. */ + { + box_t rule; + + rule.x0 = titlebar.x0; + rule.x1 = titlebar.x1; + rule.y0 = titlebar.y1 - WUSS_DIVIDER_PX; + rule.y1 = titlebar.y1; + push_piece(layout, &rule, wuss__FURNITURE_PAINT_OUTLINE); + } + + if (!(window->flags & wuss_WINDOW_NO_CLOSE)) + { + box_t close; + + wuss__close_box(window, &close); + push_piece(layout, &close, wuss__FURNITURE_PAINT_CLOSE); + } + + if (!(window->flags & wuss_WINDOW_NO_BACK)) + { + box_t back; + + wuss__back_box(window, &back); + push_piece(layout, &back, wuss__FURNITURE_PAINT_BACK); + } + + if (!(window->flags & wuss_WINDOW_NO_TOGGLE_SIZE)) + { + box_t toggle; + + wuss__toggle_box(window, &toggle); + push_piece(layout, &toggle, wuss__FURNITURE_PAINT_TOGGLE); + } + } + + /* resize-only carve bands ------------------------------------------------ + * 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)) + { + box_t band; + + if (carve.x > 0) + { + band.x0 = content.x1; + band.x1 = window->visible.x1 - outline_px; + band.y0 = window->visible.y0 + outline_px + + wuss__titlebar_height(window); + band.y1 = window->visible.y1 - outline_px; + push_piece(layout, &band, wuss__FURNITURE_PAINT_TITLE_BG); + } + + if (carve.y > 0) + { + band.x0 = window->visible.x0 + outline_px; + band.x1 = content.x1; + band.y0 = content.y1; + band.y1 = window->visible.y1 - outline_px; + push_piece(layout, &band, wuss__FURNITURE_PAINT_TITLE_BG); + } + } + + /* resize icon + its dividing seams ---------------------------------- */ + if (!(window->flags & wuss_WINDOW_NO_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; + + push_piece(layout, &resize, wuss__FURNITURE_PAINT_RESIZE); + + if (top_seam > 0) + { + rule.x0 = resize.x0 - left_seam; + rule.x1 = resize.x1; + rule.y0 = resize.y0 - top_seam; + rule.y1 = resize.y0; + push_piece(layout, &rule, wuss__FURNITURE_PAINT_OUTLINE); + } + + if (left_seam > 0) + { + rule.x0 = resize.x0 - left_seam; + rule.x1 = resize.x0; + rule.y0 = resize.y0 - top_seam; + rule.y1 = resize.y1; + push_piece(layout, &rule, wuss__FURNITURE_PAINT_OUTLINE); + } + } + + /* vertical scrollbar: arrows + well (sausage is drawn live) --------- */ + if (!(window->flags & wuss_WINDOW_NO_VSCROLL)) + { + box_t up, down, well; + + wuss__vscroll_up_box(window, &up); + push_piece(layout, &up, wuss__FURNITURE_PAINT_SCROLL_ARROWS); + + wuss__vscroll_down_box(window, &down); + push_piece(layout, &down, wuss__FURNITURE_PAINT_SCROLL_ARROWS); + + wuss__vscroll_well_box(window, &well); + push_piece(layout, &well, wuss__FURNITURE_PAINT_SCROLL_WELLS); + } + + /* horizontal scrollbar: arrows + well ----------------------------- */ + if (!(window->flags & wuss_WINDOW_NO_HSCROLL)) + { + box_t left, right, well; + + wuss__hscroll_left_box(window, &left); + push_piece(layout, &left, wuss__FURNITURE_PAINT_SCROLL_ARROWS); + + wuss__hscroll_right_box(window, &right); + push_piece(layout, &right, wuss__FURNITURE_PAINT_SCROLL_ARROWS); + + wuss__hscroll_well_box(window, &well); + push_piece(layout, &well, wuss__FURNITURE_PAINT_SCROLL_WELLS); + } + + /* interior rules where furniture is carved off the content edges --- */ + if (carve.x > 0) + { + box_t rule; + + rule.x0 = content.x1; + rule.x1 = content.x1 + WUSS_DIVIDER_PX; + rule.y0 = content.y0; + rule.y1 = content.y1; + push_piece(layout, &rule, wuss__FURNITURE_PAINT_OUTLINE); + } + + if (carve.y > 0) + { + box_t rule; + + rule.x0 = content.x0; + rule.x1 = content.x1 + ((carve.x > 0) ? WUSS_DIVIDER_PX : 0); /* meet the vertical rule at the corner */ + rule.y0 = content.y1; + rule.y1 = content.y1 + WUSS_DIVIDER_PX; + push_piece(layout, &rule, wuss__FURNITURE_PAINT_OUTLINE); + } + + /* window outline: four one-pixel edges ---------------------------- */ + if (!(window->flags & wuss_WINDOW_NO_OUTLINE)) + { + box_t edge; + + edge.x0 = window->visible.x0; + edge.y0 = window->visible.y0; + edge.x1 = window->visible.x1; + edge.y1 = window->visible.y0 + 1; + push_piece(layout, &edge, wuss__FURNITURE_PAINT_OUTLINE); /* top */ + + edge.y0 = window->visible.y1 - 1; + edge.y1 = window->visible.y1; + push_piece(layout, &edge, wuss__FURNITURE_PAINT_OUTLINE); /* bottom */ + + edge.x0 = window->visible.x0; + edge.y0 = window->visible.y0; + edge.x1 = window->visible.x0 + 1; + edge.y1 = window->visible.y1; + push_piece(layout, &edge, wuss__FURNITURE_PAINT_OUTLINE); /* left */ + + edge.x0 = window->visible.x1 - 1; + edge.x1 = window->visible.x1; + push_piece(layout, &edge, wuss__FURNITURE_PAINT_OUTLINE); /* right */ + } + + layout->valid = 1; +} diff --git a/libraries/wuss/furniture/resize-box.c b/libraries/wuss/furniture/resize-box.c index a55aba8..c13b27d 100644 --- a/libraries/wuss/furniture/resize-box.c +++ b/libraries/wuss/furniture/resize-box.c @@ -14,3 +14,25 @@ void wuss__resize_box(const wuss_window_t *window, box_t *out) out->y1 = window->visible.y1 - outline_px; out->y0 = out->y1 - size; } + +/* The resize hit box owns the bottom-right corner: the button-sized drawn + * icon square, grown out over the outline band so the extreme corner still + * hits. Independent of which scrollbars are present -- a strip butts the + * icon at the same edge the icon would sit at with no strip there. */ +void wuss__resize_hit_box(const wuss_window_t *window, box_t *out) +{ + int outline_px, size; + + outline_px = wuss__outline_px(window); + size = wuss__button_size(window); + + /* The drawn icon (wuss__resize_box) is always the button-sized square at + * the bottom-right, inside the outline. Match it exactly, then grow out + * over the outline band so the very corner still counts as a hit. Whether + * a scroll strip butts the icon or (no strip) nothing paints that margin, + * the icon's near edges are the same, so no per-axis branch is needed. */ + out->x0 = window->visible.x1 - outline_px - size; + out->y0 = window->visible.y1 - outline_px - size; + out->x1 = window->visible.x1; + out->y1 = window->visible.y1; +} diff --git a/libraries/wuss/furniture/scroll-box.c b/libraries/wuss/furniture/scroll-box.c index 42aaf39..c56d69d 100644 --- a/libraries/wuss/furniture/scroll-box.c +++ b/libraries/wuss/furniture/scroll-box.c @@ -11,23 +11,34 @@ /* The scrollbar strip: full breadth, running between the titlebar (v) or left * outline (h) and the resize corner. "long" axis is y when vertical, x when - * horizontal. */ + * horizontal. + * + * The far end stops short of the bottom-right corner (a seam then a + * size x size square) only when something actually owns that corner: a + * resize icon, or -- for the horizontal strip -- the vertical scrollbar it + * would otherwise draw over. With neither present the strip runs to the + * outline. This must match scroll_strip_hit's far-end rule so the drawn + * strip and its hit region agree. */ static void scroll_strip(const wuss_window_t *window, int horizontal, box_t *out) { box_t titlebar; - int outline_px, size; + int outline_px, size, has_resize, has_vscroll; - outline_px = wuss__outline_px(window); - size = wuss__button_size(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); if (horizontal) { out->y1 = window->visible.y1 - outline_px; out->y0 = out->y1 - size; out->x0 = window->visible.x0 + outline_px; - out->x1 = window->visible.x1 - outline_px - size; /* clear of resize corner */ + out->x1 = (has_resize || has_vscroll) + ? window->visible.x1 - outline_px - size - WUSS_DIVIDER_PX /* seam then resize corner */ + : window->visible.x1 - outline_px; } else { @@ -35,7 +46,56 @@ static void scroll_strip(const wuss_window_t *window, out->x0 = out->x1 - size; wuss__titlebar_box(window, &titlebar); out->y0 = titlebar.y1; - out->y1 = window->visible.y1 - outline_px - size; /* clear of resize corner */ + out->y1 = has_resize + ? window->visible.y1 - outline_px - size - WUSS_DIVIDER_PX /* seam then resize corner */ + : window->visible.y1 - outline_px; + } +} + +/* The scrollbar strip's HIT box: as scroll_strip, but grown so it tiles the + * outline band and the divider seam that the drawn strip leaves for the + * outline / interior rule. + * + * - breadth (cross axis) grows out to the visible edge it hugs; + * - the content-facing inner edge moves in by WUSS_DIVIDER_PX to the content + * edge, swallowing the seam; + * - the far (resize-corner) end: the VERTICAL strip owns the bottom-right + * corner whenever there is no resize icon; the HORIZONTAL strip stops short + * of the corner whenever either a resize icon OR a vertical scrollbar is + * present, so the drawn-over-nothing overlap at the corner is avoided and + * the tiling stays gap-free. */ +static void scroll_strip_hit(const wuss_window_t *window, + int horizontal, + box_t *out) +{ + point_t carve; + box_t titlebar; + int outline_px, size, has_resize, has_vscroll; + + 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); + wuss__furniture_carve_for(window->flags, size, &carve); + + if (horizontal) + { + out->y1 = window->visible.y1; + out->y0 = window->visible.y1 - outline_px - carve.y; /* = content bottom edge */ + out->x0 = window->visible.x0; + out->x1 = (has_resize || has_vscroll) + ? window->visible.x1 - outline_px - size + : window->visible.x1; + } + else + { + out->x1 = window->visible.x1; + out->x0 = window->visible.x1 - outline_px - carve.x; /* = content right edge */ + wuss__titlebar_box(window, &titlebar); + out->y0 = titlebar.y1; + out->y1 = has_resize + ? window->visible.y1 - outline_px - size + : window->visible.y1; } } @@ -200,6 +260,30 @@ void wuss__vscroll_sausage_box(const wuss_window_t *window, box_t *out) scroll_sausage(window, 0, out); } +void wuss__vscroll_up_hit_box(const wuss_window_t *window, box_t *out) +{ + box_t strip; + + scroll_strip_hit(window, 0, &strip); + scroll_end_slice(&strip, 0, 0, wuss__button_size(window), out); +} + +void wuss__vscroll_down_hit_box(const wuss_window_t *window, box_t *out) +{ + box_t strip; + + scroll_strip_hit(window, 0, &strip); + scroll_end_slice(&strip, 0, 1, wuss__button_size(window), out); +} + +void wuss__vscroll_well_hit_box(const wuss_window_t *window, box_t *out) +{ + box_t strip; + + scroll_strip_hit(window, 0, &strip); + scroll_middle_slice(&strip, 0, wuss__button_size(window), out); +} + /* ----- horizontal scrollbar -------------------------------------------- */ void wuss__hscroll_left_box(const wuss_window_t *window, box_t *out) @@ -236,3 +320,27 @@ void wuss__hscroll_sausage_box(const wuss_window_t *window, box_t *out) { scroll_sausage(window, 1, out); } + +void wuss__hscroll_left_hit_box(const wuss_window_t *window, box_t *out) +{ + box_t strip; + + scroll_strip_hit(window, 1, &strip); + scroll_end_slice(&strip, 1, 0, wuss__button_size(window), out); +} + +void wuss__hscroll_right_hit_box(const wuss_window_t *window, box_t *out) +{ + box_t strip; + + scroll_strip_hit(window, 1, &strip); + scroll_end_slice(&strip, 1, 1, wuss__button_size(window), out); +} + +void wuss__hscroll_well_hit_box(const wuss_window_t *window, box_t *out) +{ + box_t strip; + + scroll_strip_hit(window, 1, &strip); + scroll_middle_slice(&strip, 1, wuss__button_size(window), out); +} diff --git a/libraries/wuss/furniture/titlebar-box.c b/libraries/wuss/furniture/titlebar-box.c index 6a620b6..b2153c9 100644 --- a/libraries/wuss/furniture/titlebar-box.c +++ b/libraries/wuss/furniture/titlebar-box.c @@ -13,3 +13,19 @@ void wuss__titlebar_box(const wuss_window_t *window, box_t *out) out->x1 = window->visible.x1 - outline_px; out->y1 = out->y0 + wuss__titlebar_height(window); } + +/* The titlebar's hit box grows over the top, left and right outline so a click + * on the outline band there routes to TITLE rather than falling through to the + * workarea. The bottom edge is unchanged -- it butts the content top, which + * does not move. */ +void wuss__title_hit_box(const wuss_window_t *window, box_t *out) +{ + int outline_px; + + outline_px = wuss__outline_px(window); + + out->x0 = window->visible.x0; + out->y0 = window->visible.y0; + out->x1 = window->visible.x1; + out->y1 = window->visible.y0 + outline_px + wuss__titlebar_height(window); +} diff --git a/libraries/wuss/furniture/toggle-action.c b/libraries/wuss/furniture/toggle-action.c index 69b5c6b..18c0246 100644 --- a/libraries/wuss/furniture/toggle-action.c +++ b/libraries/wuss/furniture/toggle-action.c @@ -148,12 +148,16 @@ void wuss__furniture_toggle_size(wuss_window_t *window) else { /* No blit: the window is not topmost, its content self-lays-out - * (NO_RESIZE_BLIT), the old and new content boxes do not overlap, or - * screen_copy_rect declined. Repaint the whole union of old and new - * footprint. */ + * (NO_RESIZE_BLIT), the old and new content boxes do not overlap, + * screen_copy_rect declined, or the toggle re-clamped scroll. Repaint + * the whole union of old and new footprint, and drop the cached + * furniture layout so it rebuilds at the new geometry -- the blit path + * above gets this via wuss__furniture_invalidate, this path otherwise + * would not. */ logf_info("%s", "wuss furniture toggle: no blit fast path, repainting the " "whole old+new footprint"); box_union(&before, &window->visible, &dirty); wuss__invalidate_clipped(window, &dirty); + wuss__furniture_invalidate(window); } } diff --git a/libraries/wuss/furniture/toggle-box.c b/libraries/wuss/furniture/toggle-box.c index 326ecf6..ebae725 100644 --- a/libraries/wuss/furniture/toggle-box.c +++ b/libraries/wuss/furniture/toggle-box.c @@ -17,3 +17,18 @@ void wuss__toggle_box(const wuss_window_t *window, box_t *out) out->y0 = titlebar.y0 + inset; out->y1 = out->y0 + size; } + +/* TOGGLE_SIZE is the rightmost titlebar icon, so its hit box owns the top-right + * corner: it grows right and up to the window edge. Its left edge (facing the + * title text) is unchanged. */ +void wuss__toggle_hit_box(const wuss_window_t *window, box_t *out) +{ + box_t drawn; + + wuss__toggle_box(window, &drawn); + + out->x0 = drawn.x0; + out->y0 = window->visible.y0; + out->x1 = window->visible.x1; + out->y1 = drawn.y1; +} diff --git a/libraries/wuss/helpers/create-from-desc.c b/libraries/wuss/helpers/create-from-desc.c index 85ac41d..baf3dc3 100644 --- a/libraries/wuss/helpers/create-from-desc.c +++ b/libraries/wuss/helpers/create-from-desc.c @@ -135,10 +135,10 @@ void wuss_menu_destroy(wuss_menu_t *menu) * freed with, the result of wuss_menu_create_from_desc. */ static result_t menu_deep_copy(const wuss_menu_t *src, wuss_menu_t **out) { + result_t rc; wuss_menu_t *m; wuss_menu_item_t *items; int i; - result_t rc; m = malloc(sizeof(*m)); if (m == NULL) @@ -302,13 +302,13 @@ static void build_discard(Building *b) result_t wuss_menu_create_from_desc(wuss_menu_t **out, const char *desc, ...) { + result_t rc; Parser parser; Building stack[WUSS_MENU_DESC_DEPTH]; int need_title[WUSS_MENU_DESC_DEPTH]; char *titles[WUSS_MENU_DESC_DEPTH]; int sp; va_list ap; - result_t rc; char text[WUSS_MENU_DESC_TEXT]; int i; diff --git a/libraries/wuss/icon.h b/libraries/wuss/icon.h index ff67acc..b553eb3 100644 --- a/libraries/wuss/icon.h +++ b/libraries/wuss/icon.h @@ -36,6 +36,7 @@ struct wuss_icon 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; }; @@ -113,4 +114,9 @@ wuss_icon_t *wuss__icon_hit_test(wuss_window_t *window, point_t doc_point); * not invalidate or swap-remove. */ void wuss__icons_free(wuss_window_t *window); +/* Free the wuss-wide icon set loaded by wuss_icons_load (the atom_set and the + * compressed bitmaps), leaving the fields NULL / 0. Safe on an unloaded set. + * Called by wuss_icons_load before a reload and by wuss_destroy. */ +void wuss__icons_registry_free(wuss_t *wuss); + #endif /* WUSS_ICON_IMPL_H */ diff --git a/libraries/wuss/icon/create.c b/libraries/wuss/icon/create.c index aeac0f5..be9ef71 100644 --- a/libraries/wuss/icon/create.c +++ b/libraries/wuss/icon/create.c @@ -14,6 +14,7 @@ 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; @@ -21,7 +22,6 @@ result_t wuss_icon_create(wuss_window_t *window, const char *src; size_t len; int newcap; - result_t rc; assert(window != NULL); assert(spec != NULL); diff --git a/libraries/wuss/icon/draw.c b/libraries/wuss/icon/draw.c index b058a7e..954e77a 100644 --- a/libraries/wuss/icon/draw.c +++ b/libraries/wuss/icon/draw.c @@ -1,5 +1,6 @@ /* wuss/icon/draw.c -- draw a work-area icon */ +#include #include #include "base/utils.h" @@ -33,19 +34,61 @@ icon_draw_ctx_t; /* ----------------------------------------------------------------------- */ -static void icon_bevel(screen_t *scr, - const box_t *b, - colour_t fill, - colour_t light, - colour_t dark) +static void icon_fill_bevel(screen_t *scr, + const box_t *b, + colour_t fill, + 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_draw_bevel_edge(scr, b, light, dark); +} + +/* The 6px-per-edge "action" surround shared by wuss_ICON_BORDER_ACTION and a + * default button: a 2px sunken outset (dark top/left), a 2px accent moat, then + * a 2px raised inset. Only the inset tracks the pressed state -- it flips to + * sunken -- so a pressed action button reads as pushed in without the whole + * surround inverting. */ +static void icon_draw_action_border(screen_t *scr, + const box_t *b, + colour_t light, + colour_t dark, + colour_t accent, + int pressed) +{ + box_t ring; + + 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); + 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); + screen_draw_bevel_edge(scr, &ring, pressed ? dark : light, + pressed ? light : dark); +} - screen_draw_line(scr, b->x0, b->y0, b->x1 - 1, b->y0, light); - screen_draw_line(scr, b->x0, b->y0, b->x0, b->y1 - 1, light); - screen_draw_line(scr, b->x0, b->y1 - 1, b->x1 - 1, b->y1 - 1, dark); - screen_draw_line(scr, b->x1 - 1, b->y0, b->x1 - 1, b->y1 - 1, dark); +/* The wuss_ICON_BORDER_DIVIDER surround: two 2px bevels in a lighter pair + * than RIDGE/GROOVE -- bevel_divider against bevel_light rather than the full + * light/dark contrast. Outer ring sunken, inner ring raised. Shared by the + * bordered label and the grouping frame. */ +static void icon_draw_divider_border(screen_t *scr, + const box_t *b, + colour_t light, + colour_t divider) +{ + box_t ring; + + 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); + screen_draw_bevel_edge(scr, &ring, light, divider); } /* Resolve the ground an icon's text/glyph blends against: an explicit icon bg, @@ -131,7 +174,6 @@ static void wuss__icon_draw_label(const icon_draw_ctx_t *c) const box_t *b = &c->b; colour_t bg; point_t pos; - int interior_w, split_point; bmfont_width_t width; if (icon->bg != wuss_NO_BACKGROUND) @@ -145,13 +187,43 @@ 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) + { + colour_t light, dark, accent, divider; + + light = c->wuss->palette[c->wuss->bevel_light]; + dark = c->wuss->palette[c->wuss->bevel_dark]; + 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) + { + icon_draw_action_border(c->scr, b, light, dark, accent, 0); + } + else if (icon->border == wuss_ICON_BORDER_DIVIDER) + { + icon_draw_divider_border(c->scr, b, light, divider); + } + else + { + /* RIDGE reads raised (light top/left); GROOVE reads sunken. One 2px + * bevel ring. */ + box_t ring = *b; + + if (icon->border == wuss_ICON_BORDER_RIDGE) + screen_draw_bevel_edge(c->scr, &ring, light, dark); + else + screen_draw_bevel_edge(c->scr, &ring, dark, light); + } + } + if (!c->have_font) return; - interior_w = MAX((b->x1 - b->x0) - 2, 1); - - bmfont_measure(c->font, icon->text, (int) strlen(icon->text), - interior_w, &split_point, &width); + /* Measure the whole string, not the box interior: a clipped measurement + * left the widest label of a right-justified set overhanging the rest. */ + wuss__text_measure(c->font, icon->text, (int) strlen(icon->text), + INT_MAX, NULL, &width); if (icon->flags & wuss_ICON_FLAGS_JUSTIFY_CENTRE) pos.x = b->x0 + ((b->x1 - b->x0) - width) / 2; @@ -159,10 +231,11 @@ static void wuss__icon_draw_label(const icon_draw_ctx_t *c) pos.x = b->x1 - 1 - width; else pos.x = b->x0 + 1; + pos.y = b->y0 + (b->y1 - b->y0 - c->font_height) / 2; - bmfont_draw(c->font, c->scr, icon->text, (int) strlen(icon->text), - c->fg, bg, &pos, NULL); + wuss__text_draw(c->font, c->scr, icon->text, (int) strlen(icon->text), + c->fg, bg, &pos, NULL); } /* ----------------------------------------------------------------------- */ @@ -171,10 +244,12 @@ 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; - int cap_w, cap_x, gap_x0, gap_x1, mid_y; + colour_t bg, light, divider; + int cap_w, cap_x, gap_x0, gap_x1; - bg = icon_blend_ground(c, c->fg); + bg = icon_blend_ground(c, c->fg); + light = c->wuss->palette[c->wuss->bevel_light]; + divider = c->wuss->palette[c->wuss->bevel_divider]; cap_w = 0; if (c->have_font) @@ -182,29 +257,26 @@ static void wuss__icon_draw_frame(const icon_draw_ctx_t *c) int split_point; bmfont_width_t width; - bmfont_measure(c->font, icon->text, (int) strlen(icon->text), - (b->x1 - b->x0) - WUSS_FRAME_CAPTION_INSET * 2, - &split_point, &width); + wuss__text_measure(c->font, icon->text, (int) strlen(icon->text), + (b->x1 - b->x0) - WUSS_FRAME_CAPTION_INSET * 2, + &split_point, &width); cap_w = width; } - mid_y = b->y0 + c->font_height / 2; + /* the whole surround is a wuss_ICON_BORDER_DIVIDER ring... */ + icon_draw_divider_border(c->scr, b, light, divider); - /* left, right and bottom edges are unbroken */ - screen_draw_line(c->scr, b->x0, mid_y, b->x0, b->y1 - 1, c->fg); - screen_draw_line(c->scr, b->x1 - 1, mid_y, b->x1 - 1, b->y1 - 1, c->fg); - screen_draw_line(c->scr, b->x0, b->y1 - 1, b->x1 - 1, b->y1 - 1, c->fg); - - /* the top edge is broken around the caption. INSET (8) always exceeds PAD - * (2), so gap_x0 sits a few pixels right of b->x0 and the left stub is - * always drawn; only the right stub can vanish, when a wide caption pushes - * gap_x1 past the frame's right edge. */ + /* ...with the top edge broken around the caption: overpaint the caption + * slot (the ring is 2px, plus a PAD margin either side) back to the frame + * ground. INSET (8) always exceeds PAD (2), so gap_x0 sits a few pixels + * right of b->x0 and the left stub always survives; only the right stub + * can vanish, when a wide caption pushes gap_x1 past the frame edge. */ cap_x = b->x0 + WUSS_FRAME_CAPTION_INSET; gap_x0 = cap_x - WUSS_FRAME_CAPTION_PAD; gap_x1 = cap_x + cap_w + WUSS_FRAME_CAPTION_PAD; - screen_draw_line(c->scr, b->x0, mid_y, gap_x0, mid_y, c->fg); - if (gap_x1 < b->x1 - 1) - screen_draw_line(c->scr, gap_x1, mid_y, b->x1 - 1, mid_y, c->fg); + if (gap_x1 > gap_x0) + screen_fill_rect(c->scr, gap_x0, b->y0, + SIZE2D(MIN(gap_x1, b->x1) - gap_x0, 2), bg); if (c->have_font && cap_w > 0) { @@ -212,8 +284,8 @@ static void wuss__icon_draw_frame(const icon_draw_ctx_t *c) pos.x = cap_x; pos.y = b->y0; - bmfont_draw(c->font, c->scr, icon->text, (int) strlen(icon->text), - c->fg, bg, &pos, NULL); + wuss__text_draw(c->font, c->scr, icon->text, (int) strlen(icon->text), + c->fg, bg, &pos, NULL); } } @@ -229,32 +301,41 @@ static void wuss__icon_draw_button(const icon_draw_ctx_t *c) pressed = wuss__icon_pressed(icon); is_default = (icon->flags & wuss_ICON_FLAGS_DEFAULT) != 0; + /* a plain button whose spec left bg as wuss_NO_BACKGROUND takes the config + * button face */ + base = (icon->bg != wuss_NO_BACKGROUND) + ? c->wuss->palette[icon->bg] + : c->wuss->palette[c->wuss->button_bg]; + light = c->wuss->palette[c->wuss->bevel_light]; + dark = c->wuss->palette[c->wuss->bevel_dark]; + /* a default action button labels in button.fg to read against its accent + * fill; a plain button uses the icon's own fg */ + label = is_default ? c->wuss->palette[c->wuss->button_fg] : c->fg; + + /* a held button fills with the pressed shade regardless of type */ + if (pressed) + base = c->wuss->palette[c->wuss->button_pressed]; + + if (icon->flags & wuss_ICON_FLAGS_DISABLED) + label = c->wuss->palette[c->wuss->bevel_dark]; /* greyed: sink toward dark */ + if (is_default) { - /* default action button: a flat accent-filled rectangle inside a - * one-pixel accent-text border, distinct from the bevelled ordinary - * buttons around it */ - base = c->wuss->palette[c->wuss->accent_bg]; - light = c->wuss->palette[c->wuss->accent_fg]; - dark = light; - label = c->wuss->palette[c->wuss->accent_fg]; + /* default action button: an accent-filled rectangle inside the same 6px + * "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); + icon_draw_action_border(c->scr, b, light, dark, + c->wuss->palette[c->wuss->accent], pressed); } else { - base = c->wuss->palette[icon->bg]; - light = c->wuss->palette[c->wuss->bevel_light]; - dark = c->wuss->palette[c->wuss->bevel_dark]; - label = c->fg; + icon_fill_bevel(c->scr, b, base, pressed ? dark : light, + pressed ? light : dark); } - if (icon->flags & wuss_ICON_FLAGS_DISABLED) - label = c->wuss->palette[c->wuss->bevel_dark]; /* greyed: sink toward dark */ - - if (pressed) - icon_bevel(c->scr, b, base, dark, light); - else - icon_bevel(c->scr, b, base, light, dark); - if (c->have_font) { point_t pos; @@ -263,8 +344,8 @@ static void wuss__icon_draw_button(const icon_draw_ctx_t *c) interior_w = MAX((b->x1 - b->x0) - 2, 1); - bmfont_measure(c->font, icon->text, (int) strlen(icon->text), - interior_w, &split_point, &width); + wuss__text_measure(c->font, icon->text, (int) strlen(icon->text), + interior_w, &split_point, &width); pos.x = b->x0 + ((b->x1 - b->x0) - width) / 2; pos.y = b->y0 + (b->y1 - b->y0 - c->font_height) / 2; @@ -274,17 +355,54 @@ static void wuss__icon_draw_button(const icon_draw_ctx_t *c) pos.y += 1; } - bmfont_draw(c->font, c->scr, icon->text, (int) strlen(icon->text), - label, base, &pos, NULL); + wuss__text_draw(c->font, c->scr, icon->text, (int) strlen(icon->text), + label, base, &pos, NULL); } } /* ----------------------------------------------------------------------- */ +/* 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) +{ + const char *name; + const bitmap_t *bm; + screen_t clipped; + int idx, bx, by; + + if (c->icon->type == wuss_ICON_TYPE_RADIO) + name = wuss__icon_selected(c->icon) ? "radon" : "radoff"; + else + name = wuss__icon_selected(c->icon) ? "opton" : "optoff"; + + 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; + + 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; +} + /* 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. RADIO draws a - * square ring with a solid centre when selected; OPTION draws a box with a tick - * when selected. */ + * 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. */ static void wuss__icon_draw_radio_option(const icon_draw_ctx_t *c) { const wuss_icon_t *icon = c->icon; @@ -311,7 +429,11 @@ 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 (icon->type == wuss_ICON_TYPE_RADIO) + if (wuss__icon_blit_radio_option(c, &g)) + { + /* bitmap drawn -- fall through to the label */ + } + else if (icon->type == wuss_ICON_TYPE_RADIO) { /* a square ring (no circle primitive); a solid centre when selected */ screen_draw_line(c->scr, g.x0 + 2, g.y0, g.x1 - 3, g.y0, glyph); @@ -345,14 +467,14 @@ static void wuss__icon_draw_radio_option(const icon_draw_ctx_t *c) tx = g.x1 + 4; interior_w = MAX((b->x1 - tx) - 1, 1); - bmfont_measure(c->font, icon->text, (int) strlen(icon->text), - interior_w, &split_point, &width); + wuss__text_measure(c->font, icon->text, (int) strlen(icon->text), + interior_w, &split_point, &width); NOT_USED(width); pos.x = tx; pos.y = b->y0 + (b->y1 - b->y0 - c->font_height) / 2; - bmfont_draw(c->font, c->scr, icon->text, (int) strlen(icon->text), - glyph, bg, &pos, NULL); + wuss__text_draw(c->font, c->scr, icon->text, (int) strlen(icon->text), + glyph, bg, &pos, NULL); } } @@ -477,12 +599,12 @@ static void wuss__icon_draw_menu_entry(const icon_draw_ctx_t *c) * the same width is left spare at text_x1 too since the fill already * spans the full column */ space_w = 0; - bmfont_measure(c->font, " ", 1, INT_MAX, NULL, &space_w); + 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; - bmfont_draw(c->font, c->scr, icon->text, (int) strlen(icon->text), - text_ink, text_ground, &pos, NULL); + wuss__text_draw(c->font, c->scr, icon->text, (int) strlen(icon->text), + text_ink, text_ground, &pos, NULL); } } diff --git a/libraries/wuss/icon/from-spec.c b/libraries/wuss/icon/from-spec.c index fa0883c..e1e0852 100644 --- a/libraries/wuss/icon/from-spec.c +++ b/libraries/wuss/icon/from-spec.c @@ -12,8 +12,9 @@ result_t wuss__icon_from_spec(const wuss_t *w, const wuss_icon_spec_t *spec, wuss_icon_t *out) { - wuss_colour_t fg, bg, swatch; - int has_swatch; + wuss_colour_t fg, bg, swatch; + int has_swatch; + const bitmap_t *bitmap; assert(w != NULL); assert(spec != NULL); @@ -44,18 +45,33 @@ result_t wuss__icon_from_spec(const wuss_t *w, return result_WUSS_BAD_ICON; } - if ((spec->type == wuss_ICON_TYPE_BUTTON || - spec->type == wuss_ICON_TYPE_PATTERN) && - bg == wuss_NO_BACKGROUND) + /* a BUTTON 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; - if (spec->type == wuss_ICON_TYPE_BITMAP && spec->bitmap == NULL) + /* 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) + { + bitmap = wuss_icons_bitmap(w, spec->icon_set - 1); + if (bitmap == NULL) + return result_WUSS_BAD_INDEX; + } + + if (spec->type == wuss_ICON_TYPE_BITMAP && bitmap == NULL) return result_WUSS_BAD_ICON; if (spec->type == wuss_ICON_TYPE_PATTERN && spec->pattern >= screen_PATTERN__LIMIT) return result_WUSS_BAD_ICON; + if (spec->type == wuss_ICON_TYPE_LABEL && + spec->border > wuss_ICON_BORDER_DIVIDER) + return result_WUSS_BAD_ICON; + if (fg >= w->npalette) return result_WUSS_BAD_COLOUR; @@ -73,9 +89,11 @@ result_t wuss__icon_from_spec(const wuss_t *w, out->bg = bg; out->pattern = (spec->type == wuss_ICON_TYPE_PATTERN) ? spec->pattern : screen_PATTERN_SOLID; - out->bitmap = (spec->type == wuss_ICON_TYPE_BITMAP) ? spec->bitmap : NULL; + 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; out->flags = spec->flags; out->state = wuss_ICON_STATE_NONE; diff --git a/libraries/wuss/icon/plot.c b/libraries/wuss/icon/plot.c index a305e96..caa1c4b 100644 --- a/libraries/wuss/icon/plot.c +++ b/libraries/wuss/icon/plot.c @@ -13,8 +13,8 @@ result_t wuss_icon_plot(wuss_window_t *window, const box_t *content, point_t scroll) { - wuss_icon_t scratch; result_t rc; + wuss_icon_t scratch; assert(window != NULL); assert(spec != NULL); diff --git a/libraries/wuss/icon/registry.c b/libraries/wuss/icon/registry.c new file mode 100644 index 0000000..341d9f8 --- /dev/null +++ b/libraries/wuss/icon/registry.c @@ -0,0 +1,306 @@ +/* wuss/icon/registry.c -- wuss-wide icon set loaded from a directory of PNGs */ + +#include +#include +#include +#include + +#ifdef FORTIFY +#include "fortify/fortify.h" +#endif + +#include "base/result.h" +#include "datastruct/atom.h" +#include "framebuf/bitmap.h" +#include "framebuf/pixelfmt.h" +#include "io/dirscan.h" +#include "io/path.h" + +/* path_join_filename returns a single static buffer, so the per-entry join in + * the callback would clobber a caller's dir pointer if we held it directly -- + * keep our own copy. Bounded by that buffer's own DPTLIB_MAXPATH. */ +#define ICONS_DIR_MAX 256 + +#include "../core/impl.h" + +#ifdef WUSS_ICONS + +/* State threaded through the dirscan callback while a set is being built. The + * three arrays grow in lockstep via wuss__array_grow. */ +typedef struct icons_load +{ + wuss_t *wuss; + char dir[ICONS_DIR_MAX]; /* own copy; see ICONS_DIR_MAX */ + int *atoms; /* owned; atoms[i] is entry i's atom in wuss->icon.names */ + bitmap_t *bitmaps; /* owned */ + int n; + int cap; /* shared allocation length of atoms[] and bitmaps[] */ + result_t rc; /* first failure; stops the walk */ +} +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 + * is usually bgr(x|a)8888. Swap R and B in place (byte 0 <-> byte 2; the + * alpha/pad byte is untouched, so this maps rgbx<->bgrx and rgba<->bgra) and + * relabel the format. Bitmap must be uncompressed and 32bpp. + * ponytail: lives here until a second caller needs rgb<->bgr; then fold into + * bitmap_convert. */ +static void icons_swap_rb(bitmap_t *bm) +{ + unsigned char *row; + int x, y; + + assert(!pixelfmt_is_rle(bm->format)); + assert(pixelfmt_log2bpp(bm->format) == 5); + + for (y = 0; y < bm->size.h; y++) + { + row = (unsigned char *) bm->base + (size_t) y * bm->rowbytes; + for (x = 0; x < bm->size.w; x++) + { + unsigned char t; + + t = row[x * 4 + 0]; + row[x * 4 + 0] = row[x * 4 + 2]; + row[x * 4 + 2] = t; + } + } + + switch (bm->format) + { + case pixelfmt_rgbx8888: bm->format = pixelfmt_bgrx8888; break; + case pixelfmt_rgba8888: bm->format = pixelfmt_bgra8888; break; + case pixelfmt_bgrx8888: bm->format = pixelfmt_rgbx8888; break; + case pixelfmt_bgra8888: bm->format = pixelfmt_rgba8888; break; + default: break; + } +} + +/* True when "a" and "b" are 32bpp RGB(A/X) formats of opposite R/B order. */ +static int icons_rb_swapped(pixelfmt_t a, pixelfmt_t b) +{ + return (a == pixelfmt_rgbx8888 && b == pixelfmt_bgrx8888) || + (a == pixelfmt_bgrx8888 && b == pixelfmt_rgbx8888) || + (a == pixelfmt_rgba8888 && b == pixelfmt_bgra8888) || + (a == pixelfmt_bgra8888 && b == pixelfmt_rgba8888) || + (a == pixelfmt_rgbx8888 && b == pixelfmt_bgra8888) || + (a == pixelfmt_rgba8888 && b == pixelfmt_bgrx8888); +} + +static result_t icons_load_entry(const char *leaf, void *opaque) +{ + result_t rc; + icons_load_t *st = opaque; + char name[128]; + const char *path; + bitmap_t bm; + atom_t atom; + int oldcap; + + if (!png_leaf(leaf, name, sizeof(name))) + return result_OK; /* not a ".png", or name too long -- skip */ + + path = path_join_filename(st->dir, 1, leaf); + + rc = bitmap_load_png(&bm, path); + if (rc != result_OK) + { + st->rc = rc; + return result_STOP_WALK; + } + + /* bring the pixels into screen byte order before compressing -- see + * icons_swap_rb. Only the R/B-swapped 32bpp case is handled; a matching + * format needs nothing, anything else is left for the blit to reject. */ + if (pixelfmt_log2bpp(bm.format) == 5 && + icons_rb_swapped(bm.format, st->wuss->scr->format)) + icons_swap_rb(&bm); + + rc = bitmap_compress(&bm); + if (rc != result_OK) + { + free(bm.base); + st->rc = rc; + return result_STOP_WALK; + } + + /* Intern the name. atom_new returns result_ATOM_NAME_EXISTS (with the + * existing atom) for a repeat -- a directory scan can't repeat a leaf, so + * treat only a hard error as failure. */ + rc = atom_new(st->wuss->icon.names, + (const unsigned char *) name, strlen(name) + 1, &atom); + if (rc != result_OK && rc != result_ATOM_NAME_EXISTS) + { + free(bm.base); + st->rc = rc; + return result_STOP_WALK; + } + + oldcap = st->cap; + if (wuss__array_grow(&st->wuss->alloc, (void **) &st->bitmaps, + sizeof(*st->bitmaps), st->n, &st->cap, 1, 8) != 0) + { + free(bm.base); + st->rc = result_OOM; + return result_STOP_WALK; + } + if (st->cap != oldcap) /* bitmaps[] grew; bring atoms[] to the same length */ + { + int *grown; + + grown = wuss__realloc(st->wuss, st->atoms, + (size_t) st->cap * sizeof(*st->atoms)); + if (grown == NULL) + { + free(bm.base); + st->rc = result_OOM; + return result_STOP_WALK; + } + st->atoms = grown; + } + + st->atoms[st->n] = (int) atom; + st->bitmaps[st->n++] = bm; + return result_OK; +} + +void wuss__icons_registry_free(wuss_t *wuss) +{ + int i; + + assert(wuss != NULL); + + for (i = 0; i < wuss->icon.nbitmaps; i++) + free(wuss->icon.bitmaps[i].base); + + wuss__free(wuss, wuss->icon.bitmaps); + wuss__free(wuss, wuss->icon.atoms); + wuss->icon.bitmaps = NULL; + wuss->icon.atoms = NULL; + wuss->icon.nbitmaps = 0; + + if (wuss->icon.names != NULL) + { + atom_destroy(wuss->icon.names); + wuss->icon.names = NULL; + } +} + +result_t wuss_icons_load(wuss_t *wuss, const char *dir) +{ + result_t rc; + icons_load_t st; + int i; + + assert(wuss != NULL); + + if (dir == NULL) + return result_NULL_ARG; + + wuss__icons_registry_free(wuss); + + wuss->icon.names = atom_create(); + if (wuss->icon.names == NULL) + return result_OOM; + + if (strlen(dir) >= sizeof(st.dir)) + { + atom_destroy(wuss->icon.names); + wuss->icon.names = NULL; + return result_BAD_ARG; + } + + st.wuss = wuss; + strcpy(st.dir, dir); + st.atoms = NULL; + st.bitmaps = NULL; + st.n = 0; + st.cap = 0; + st.rc = result_OK; + + rc = dirscan_walk(dir, icons_load_entry, &st); + + /* A missing directory just yields an empty set. */ + if (rc == result_FILE_NOT_FOUND) + rc = result_OK; + + if (rc == result_OK && st.rc != result_OK) + rc = st.rc; + + if (rc != result_OK) + { + for (i = 0; i < st.n; i++) + free(st.bitmaps[i].base); + wuss__free(wuss, st.bitmaps); + wuss__free(wuss, st.atoms); + atom_destroy(wuss->icon.names); + wuss->icon.names = NULL; + return rc; + } + + wuss->icon.atoms = st.atoms; + wuss->icon.bitmaps = st.bitmaps; + wuss->icon.nbitmaps = st.n; + return result_OK; +} + +int wuss_icons_count(const wuss_t *wuss) +{ + assert(wuss != NULL); + return wuss->icon.nbitmaps; +} + +int wuss_icons_lookup(const wuss_t *wuss, const char *name) +{ + atom_t atom; + int i; + + assert(wuss != NULL); + + if (name == NULL || wuss->icon.names == NULL) + return -1; + + atom = atom_for_block((atom_set_t *) wuss->icon.names, + (const unsigned char *) name, strlen(name) + 1); + if (atom == atom_NOT_FOUND) + return -1; + + /* atom values are not array indices -- find which entry holds it. A handful + * of icons, so a linear scan is cheaper than a second index structure. */ + for (i = 0; i < wuss->icon.nbitmaps; i++) + if (wuss->icon.atoms[i] == (int) atom) + return i; + + return -1; +} + +const bitmap_t *wuss_icons_bitmap(const wuss_t *wuss, int index) +{ + assert(wuss != NULL); + + if (index < 0 || index >= wuss->icon.nbitmaps) + return NULL; + + return &wuss->icon.bitmaps[index]; +} + +#endif /* WUSS_ICONS */ diff --git a/libraries/wuss/icon/set-hover.c b/libraries/wuss/icon/set-hover.c index 1c55d1f..090c91d 100644 --- a/libraries/wuss/icon/set-hover.c +++ b/libraries/wuss/icon/set-hover.c @@ -20,7 +20,14 @@ void wuss__icon_set_hover(wuss_t *wuss, wuss_icon_t *icon) if (prev == icon) return; + /* Every ancestor row on an open menu chain's open-submenu path keeps its + * highlight while the pointer is down in a deeper level (RISC OS feel), so + * don't drop `prev`'s highlight if it is one of those rows. */ +#ifdef WUSS_MENUS + if (prev != NULL && !wuss__menu_row_pinned(wuss, prev)) +#else if (prev != NULL) +#endif { wuss__icon_set_state(prev, wuss_ICON_STATE_HOVERED, 0); if (wuss__icon_hover_visible(prev)) diff --git a/libraries/wuss/menu.h b/libraries/wuss/menu.h index 482b392..2c0e451 100644 --- a/libraries/wuss/menu.h +++ b/libraries/wuss/menu.h @@ -57,4 +57,10 @@ struct wuss__menu * and return 1 (the click is spent on dismissal). Returns 0 otherwise. */ int wuss__menu_click_outside(wuss_t *wuss, const wuss_window_t *hit); +/* True if `icon` is a parent row on an open menu chain whose submenu is open -- + * i.e. some chain level's `open_index` row. Such a row keeps its hover + * highlight while the pointer is in a deeper level; wuss__icon_set_hover asks + * before dropping a row's highlight. */ +int wuss__menu_row_pinned(const wuss_t *wuss, const wuss_icon_t *icon); + #endif /* WUSS_MENU_IMPL_H */ diff --git a/libraries/wuss/menu/menu.c b/libraries/wuss/menu/menu.c index ea245eb..6124b6c 100644 --- a/libraries/wuss/menu/menu.c +++ b/libraries/wuss/menu/menu.c @@ -133,6 +133,37 @@ static void wuss__menu_close_from(struct wuss__menu *node) } } +/* Tear the whole open chain down because wuss decided to, not the client: a + * click outside every menu window, or another wuss_menu_open. The task that + * opened it still holds the handle wuss_menu_open handed back, so tell it the + * chain is gone (wuss_EVENT_MENU_CLOSED) before the nodes are freed -- a pick + * has wuss_EVENT_MENU_SELECT for that, but these paths have nothing. Unlinks + * wuss->menu_chain first so a re-entrant wuss_menu_close from the handler is a + * no-op. Also used by wuss_task_destroy when the chain's owner is the task + * going away, so declared in core impl.h. */ +void wuss__menu_abandon(wuss_t *wuss) +{ + struct wuss__menu *root; + wuss_task_t *owner; + + root = wuss->menu_chain; + if (root == NULL) + return; + + owner = root->owner; + wuss->menu_chain = NULL; + + if (owner != NULL) + { + wuss_event_t ev; + + ev.kind = wuss_EVENT_MENU_CLOSED; + (void) wuss__deliver(owner, NULL, &ev); + } + + wuss__menu_close_from(root); +} + /* Compute where a submenu (or a borrowed window standing in for one) opens * off the row `icon` in parent level `self`: content top-left in screen * space, the child's own titlebar then sitting above it so the two rows line @@ -268,9 +299,23 @@ static result_t wuss__menu_handle(wuss_window_t *window, * this row's text off the arrow -- closes the child it opened. */ if (self->child != NULL && !(self->open_index == index && on_arrow)) { + wuss_icon_t *was_parent; + + was_parent = (self->open_index >= 0) ? self->icons[self->open_index] + : NULL; + wuss__menu_close_from(self->child); self->child = NULL; self->open_index = -1; + + /* The ex-parent row was held highlit while its submenu was open (see + * wuss__menu_row_pinned). Now the submenu is gone, drop that highlight + * unless the pointer has landed back on that very row. */ + if (was_parent != NULL && was_parent != icon) + { + wuss__icon_set_state(was_parent, wuss_ICON_STATE_HOVERED, 0); + wuss__icon_invalidate(was_parent); + } } if (self->child != NULL) @@ -478,11 +523,11 @@ static result_t wuss__menu_spawn(wuss_t *wuss, struct wuss__menu *parent, struct wuss__menu **out) { + result_t rc; struct wuss__menu *node; wuss_task_t *menu_task; wuss_icon_spec_t *specs; wuss_icon_t **made; - result_t rc; int fh; int pitch; int sep_h; @@ -544,8 +589,8 @@ 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 (bmfont_measure(wuss->fonts[0], text, len, - INT_MAX, &split, &w) == result_OK && (int) w > widest) + if (wuss__text_measure(wuss->fonts[0], text, len, + INT_MAX, &split, &w) == result_OK && (int) w > widest) widest = (int) w; } @@ -555,7 +600,7 @@ static result_t wuss__menu_spawn(wuss_t *wuss, { bmfont_width_t space_w = 0; - bmfont_measure(wuss->fonts[0], " ", 1, INT_MAX, NULL, &space_w); + wuss__text_measure(wuss->fonts[0], " ", 1, INT_MAX, NULL, &space_w); widest += 2 * (int) space_w; } @@ -575,8 +620,8 @@ static result_t wuss__menu_spawn(wuss_t *wuss, titlefont = (wuss->nfonts > 1 && wuss->fonts[1] != NULL) ? wuss->fonts[1] : wuss->fonts[0]; titlelen = (int) strlen(menu->title); - if (bmfont_measure(titlefont, menu->title, titlelen, INT_MAX, &split, - &title_w) == result_OK && + if (wuss__text_measure(titlefont, menu->title, titlelen, INT_MAX, &split, + &title_w) == result_OK && (int) title_w + 2 * WUSS_MENU_TITLE_PAD > width) width = (int) title_w + 2 * WUSS_MENU_TITLE_PAD; } @@ -716,7 +761,7 @@ static result_t wuss__menu_spawn(wuss_t *wuss, rc = wuss_window_create(menu_task, &content, menu->title ? menu->title : "", menu_flags, - wuss_BACKDROP_COLOUR(wuss_COLOUR_WHITE), + wuss_BACKDROP_COLOUR(wuss_COLOUR_MENU), doc, min_doc, &node->window); if (rc != result_OK) { @@ -765,9 +810,9 @@ result_t wuss_menu_open(wuss_task_t *task, point_t at, wuss_menu_handle_t *out) { + result_t rc; struct wuss__menu *root; wuss_t *wuss; - result_t rc; assert(task != NULL); assert(menu != NULL); @@ -775,10 +820,7 @@ result_t wuss_menu_open(wuss_task_t *task, wuss = task->wuss; if (wuss->menu_chain != NULL) - { - wuss__menu_close_from(wuss->menu_chain); - wuss->menu_chain = NULL; - } + wuss__menu_abandon(wuss); /* RISC OS convention: the pointer opens the menu sitting a little inside its * first item, not on the top-left corner. Shift the content top-left up and @@ -926,6 +968,21 @@ void wuss_menu_set_item_ticked(wuss_menu_handle_t handle, /* ----------------------------------------------------------------------- */ +int wuss__menu_row_pinned(const wuss_t *wuss, const wuss_icon_t *icon) +{ + const struct wuss__menu *node; + + for (node = wuss->menu_chain; node != NULL; node = node->child) + { + if (node->borrowed || node->child == NULL || node->open_index < 0) + continue; + if (node->icons[node->open_index] == icon) + return 1; + } + + return 0; +} + int wuss__menu_click_outside(wuss_t *wuss, const wuss_window_t *hit) { struct wuss__menu *node; @@ -939,7 +996,6 @@ int wuss__menu_click_outside(wuss_t *wuss, const wuss_window_t *hit) return 0; } - wuss__menu_close_from(wuss->menu_chain); - wuss->menu_chain = NULL; + wuss__menu_abandon(wuss); return 1; } diff --git a/libraries/wuss/test/tasks/ball.c b/libraries/wuss/test/tasks/ball.c index ce73ddd..353d60e 100644 --- a/libraries/wuss/test/tasks/ball.c +++ b/libraries/wuss/test/tasks/ball.c @@ -53,9 +53,9 @@ static box_t ball_local_box(int vx0, int vy0, int vx1, int vy1, int radius) result_t ball_create(wuss_t *wuss, ball_task_t *task) { + result_t rc; wuss_task_t *delegate; wuss_task_desc_t delegate_desc; - result_t rc; task->bg = colour_rgb(0xFF, 0x00, 0x00); task->nballs = 1; @@ -82,7 +82,7 @@ result_t ball_create(wuss_t *wuss, ball_task_t *task) rc = wuss_window_create_placed(delegate, SIZE2D(200, 160), "Bouncing Ball", - wuss_WINDOW_NONE, + wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), SIZE2D(200, 160), SIZE2D(0, 0), diff --git a/libraries/wuss/test/tasks/blank.c b/libraries/wuss/test/tasks/blank.c index 9986416..f1ef9b6 100644 --- a/libraries/wuss/test/tasks/blank.c +++ b/libraries/wuss/test/tasks/blank.c @@ -19,9 +19,9 @@ result_t blank_create(wuss_t *wuss, blank_task_t *task) { + result_t rc; wuss_task_t *delegate; wuss_task_desc_t delegate_desc; - result_t rc; task->npalette = 16; // TODO: Read max palette index from wuss task->index = 0; @@ -55,8 +55,8 @@ result_t blank_create(wuss_t *wuss, blank_task_t *task) static result_t blank_idle(void *task_data) { - blank_task_t *bc; result_t rc; + blank_task_t *bc; bc = task_data; diff --git a/libraries/wuss/test/tasks/chars.c b/libraries/wuss/test/tasks/chars.c index bc8e2ca..4156fcd 100644 --- a/libraries/wuss/test/tasks/chars.c +++ b/libraries/wuss/test/tasks/chars.c @@ -51,10 +51,10 @@ static bmfont_t *chars_load_font(chars_task_t *task, int idx, const char *name) { + result_t rc; const char *leaf; const char *filename; bmfont_t *font; - result_t rc; if (task->fonts[idx] != NULL) return task->fonts[idx]; @@ -87,7 +87,7 @@ static result_t chars_set_font(chars_task_t *task, int idx, const char *name) task->current = idx; wuss_window_resize(task->window, chars_window_size(task, font)); - wuss_window_invalidate_all(task->window); + wuss_window_invalidate_visible(task->window); return result_OK; } @@ -107,9 +107,9 @@ result_t chars_create(wuss_t *wuss, const char *resources, chars_task_t *task) { + result_t rc; wuss_task_t *delegate; wuss_task_desc_t delegate_desc; - result_t rc; bmfont_t *font; const char *bmfonts_dir; const wuss_menu_t *menu; @@ -280,8 +280,8 @@ result_t chars_handle(wuss_window_t *window, case wuss_EVENT_MENU_SELECT: { - const char *name; result_t rc; + const char *name; name = wuss_fontmenu_selected(cc->fontmenu, event); if (name == NULL) @@ -303,6 +303,10 @@ result_t chars_handle(wuss_window_t *window, return rc; } + case wuss_EVENT_MENU_CLOSED: + cc->menu_handle = NULL; /* wuss closed the chain under us */ + return result_OK; + case wuss_EVENT_REDRAW: return chars_redraw(event, task_data); diff --git a/libraries/wuss/test/tasks/checker.c b/libraries/wuss/test/tasks/checker.c index 2d284fd..a9c55eb 100644 --- a/libraries/wuss/test/tasks/checker.c +++ b/libraries/wuss/test/tasks/checker.c @@ -20,9 +20,9 @@ result_t checker_create(wuss_t*wuss, checker_task_t *task) { + result_t rc; wuss_task_t *delegate; wuss_task_desc_t delegate_desc; - result_t rc; task->black = colour_rgb(0x00, 0x00, 0x00); task->white = colour_rgb(0xFF, 0xFF, 0xFF); @@ -45,7 +45,7 @@ result_t checker_create(wuss_t*wuss, checker_task_t *task) rc = wuss_window_create_placed(delegate, SIZE2D(160, 160), "Checker 1", - wuss_WINDOW_NONE, + wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), SIZE2D(160, 160), SIZE2D(0, 0), @@ -59,7 +59,7 @@ result_t checker_create(wuss_t*wuss, checker_task_t *task) rc = wuss_window_create_placed(delegate, SIZE2D(160, 160), "Checker 2", - wuss_WINDOW_NONE, + wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), SIZE2D(160, 160), SIZE2D(0, 0), @@ -130,7 +130,7 @@ static result_t checker_mouse(wuss_window_t *window, void *task_data) pattern = (window == cc->window2) ? &cc->pattern2 : &cc->pattern; *pattern = (*pattern + 1) % checker_PATTERN__COUNT; - wuss_window_invalidate_all(window); + wuss_window_invalidate_visible(window); return result_OK; } @@ -148,7 +148,7 @@ static result_t checker_scroll(wuss_window_t *window, *band += delta; *band = CLAMP(*band, CHECKER_BAND_MIN, CHECKER_BAND_MAX); - wuss_window_invalidate_all(window); + wuss_window_invalidate_visible(window); return result_OK; } diff --git a/libraries/wuss/test/tasks/clock.c b/libraries/wuss/test/tasks/clock.c index bf8f475..5c2e99c 100644 --- a/libraries/wuss/test/tasks/clock.c +++ b/libraries/wuss/test/tasks/clock.c @@ -93,7 +93,7 @@ static result_t clock_create_window(wuss_t *wuss, return wuss_window_create_placed(delegate, SIZE2D(160, 160), "Clock", - wuss_WINDOW_NONE, + wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), SIZE2D(160, 160), SIZE2D(0, 0), @@ -102,9 +102,9 @@ static result_t clock_create_window(wuss_t *wuss, result_t clock_create(wuss_t *wuss, bmfont_t *font, clock_task_t *task) { + result_t rc; wuss_task_t *delegate; wuss_task_desc_t delegate_desc; - result_t rc; task->font = font; task->bg = colour_rgb(0x1D, 0x2B, 0x53); @@ -214,7 +214,7 @@ static result_t clock_mouse(clock_task_t *cc, wuss_button_t button) if (button & wuss_BUTTON_SELECT) { cc->show_second = !cc->show_second; - wuss_window_invalidate_all(cc->window); + wuss_window_invalidate_visible(cc->window); } return result_OK; @@ -241,7 +241,7 @@ result_t clock_handle(wuss_window_t *window, return clock_mouse(cc, event->data.mouse.button); case wuss_EVENT_IDLE: - wuss_window_invalidate_all(cc->window); + wuss_window_invalidate_visible(cc->window); return result_OK; case wuss_EVENT_QUIT: diff --git a/libraries/wuss/test/tasks/curve.c b/libraries/wuss/test/tasks/curve.c index 38e7b34..9e39148 100644 --- a/libraries/wuss/test/tasks/curve.c +++ b/libraries/wuss/test/tasks/curve.c @@ -10,11 +10,15 @@ #include "fortify/fortify.h" #endif +#include + #include "base/utils.h" +#include "framebuf/bmfont.h" #include "framebuf/curve.h" #include "framebuf/palettes.h" #include "geom/box.h" #include "utils/fxp.h" +#include "wuss/wuss.h" #include "curve.h" @@ -23,22 +27,78 @@ #define CURVE_SEGMENTS_MIN 4 #define CURVE_SEGMENTS_MAX 128 +/* 2D cross product of (b - a) and (c - a); > 0 for a left (counter-clockwise) + * turn, < 0 for a right turn, 0 if collinear. long to keep the products of + * two point coordinates from overflowing an int. */ +static long curve_cross(point_t a, point_t b, point_t c) +{ + return (long) (b.x - a.x) * (c.y - a.y) - (long) (b.y - a.y) * (c.x - a.x); +} + +static int curve_point_cmp(const void *va, const void *vb) +{ + const point_t *a = va; + const point_t *b = vb; + + if (a->x != b->x) + return a->x - b->x; + return a->y - b->y; +} + +/* Andrew's monotone chain: fill "hull" (capacity 2 * n + 1) with the convex + * hull of the first n of "src" as a closed polyline (first point repeated at + * the end) and return its point count. n < 3, or all points collinear, + * degenerates to the sorted span, still returned closed. */ +static int curve_convex_hull(const point_t *src, int n, point_t *hull) +{ + point_t pts[CURVE_MAXCONTROLPTS]; + int k, i; + + memcpy(pts, src, (size_t) n * sizeof(*pts)); + qsort(pts, (size_t) n, sizeof(*pts), curve_point_cmp); + + k = 0; + for (i = 0; i < n; i++) /* lower hull */ + { + while (k >= 2 && curve_cross(hull[k - 2], hull[k - 1], pts[i]) <= 0) + k--; + hull[k++] = pts[i]; + } + { + int lower, j; + + lower = k + 1; + for (j = n - 2; j >= 0; j--) /* upper hull */ + { + while (k >= lower && curve_cross(hull[k - 2], hull[k - 1], pts[j]) <= 0) + k--; + hull[k++] = pts[j]; + } + } + + return k; /* hull[0] == hull[k - 1], a closed loop */ +} + result_t curve_create(wuss_t *wuss, curve_task_t *task) { + result_t rc; wuss_task_t *delegate; wuss_task_desc_t delegate_desc; - result_t rc; task->bg = colour_rgb(0xFF, 0xFF, 0xFF); task->line = colour_rgb(0x00, 0x00, 0x00); task->blob = colour_rgb(0xFF, 0x00, 0x00); + task->wuss = wuss; task->nsegments = CURVE_SEGMENTS_DEFAULT; + task->npoints = 4; /* cubic, matching the original task */ task->dragging = -1; task->points[0] = POINT(10, 10); task->points[1] = POINT(10, 140); task->points[2] = POINT(210, 10); task->points[3] = POINT(210, 140); + task->points[4] = POINT(110, 10); + task->points[5] = POINT(110, 140); /* curve_redraw paints its own background */ delegate_desc.handle = curve_handle; @@ -55,7 +115,7 @@ result_t curve_create(wuss_t *wuss, curve_task_t *task) rc = wuss_window_create_placed(delegate, SIZE2D(220, 160), "Curve", - wuss_WINDOW_NONE, + wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), SIZE2D(220, 160), SIZE2D(0, 0), @@ -74,6 +134,56 @@ static int blob_hit(const point_t *p, int x, int y) y >= p->y - half && y < p->y + half; } +/* Marker colour for control point i of a curve with npoints points: the two + * end points (0 and npoints - 1) draw in task->blob (red); the interior + * control points cycle through a set of other bright hues so each is + * distinct. */ +static colour_t blob_colour(const curve_task_t *task, int i) +{ + colour_t control[4]; + + if (i == 0 || i == task->npoints - 1) + return task->blob; + + control[0] = colour_rgb(0x00, 0xC0, 0x00); /* green */ + control[1] = colour_rgb(0x00, 0x80, 0xFF); /* blue */ + control[2] = colour_rgb(0xFF, 0xA0, 0x00); /* orange */ + control[3] = colour_rgb(0xC0, 0x00, 0xFF); /* purple */ + + return control[(i - 1) % NELEMS(control)]; +} + +/* Curve-type name for each valid task->npoints, indexed by + * npoints - CURVE_MINCONTROLPTS. */ +static const char *curve_kind_name(int npoints) +{ + static const char *const names[] = + { + "Line", "Quadratic", "Cubic", "Quartic", "Quintic" + }; + + return names[npoints - CURVE_MINCONTROLPTS]; +} + +/* The point at time t on the curve through the first task->npoints points, + * dispatching on the count: 2 is a straight line, 3..6 the quadratic through + * quintic Beziers. */ +static point_t curve_point(const curve_task_t *task, fix16_t t) +{ + const point_t *p = task->points; + + switch (task->npoints) + { + case 2: return curve_point_on_line(p[0], p[1], t); + case 3: return curve_bezier_point_on_quad(p[0], p[1], p[2], t); + case 4: return curve_bezier_point_on_cubic(p[0], p[1], p[2], p[3], t); + case 5: return curve_bezier_point_on_quartic(p[0], p[1], p[2], p[3], + p[4], t); + default: return curve_bezier_point_on_quintic(p[0], p[1], p[2], p[3], + p[4], p[5], t); + } +} + static result_t curve_redraw(const wuss_event_t *event, curve_task_t *task) { screen_t *scr; @@ -91,14 +201,28 @@ static result_t curve_redraw(const wuss_event_t *event, curve_task_t *task) screen_fill_rect(scr, content->x0, content->y0, box_size(content), task->bg); + /* control polygon's convex hull, drawn first so the curve and the blobs + * sit on top of it */ + { + point_t hull[2 * CURVE_MAXCONTROLPTS + 1]; + int nhull, k; + + nhull = curve_convex_hull(task->points, task->npoints, hull); + for (k = 0; k < nhull; k++) + { + hull[k].x += bounds->x0 - sx; + hull[k].y += bounds->y0 - sy; + } + screen_draw_lines(scr, hull, nhull, colour_rgb(0xC0, 0xC0, 0xC0)); + } + prev = task->points[0]; prev.x += bounds->x0 - sx; prev.y += bounds->y0 - sy; for (i = 1; i <= task->nsegments; i++) { t = i * FIX16_ONE / task->nsegments; - cur = curve_bezier_point_on_cubic(task->points[0], task->points[1], - task->points[2], task->points[3], t); + cur = curve_point(task, t); cur.x += bounds->x0 - sx; cur.y += bounds->y0 - sy; screen_draw_line(scr, prev.x, prev.y, cur.x, cur.y, task->line); @@ -107,11 +231,25 @@ static result_t curve_redraw(const wuss_event_t *event, curve_task_t *task) } half = CURVE_BLOBSZ / 2; - for (i = 0; i < CURVE_NCONTROLPTS; i++) + for (i = 0; i < task->npoints; i++) { cur = task->points[i]; cur.x += bounds->x0 - sx; cur.y += bounds->y0 - sy; - screen_fill_square(scr, cur.x - half, cur.y - half, CURVE_BLOBSZ, task->blob); + screen_fill_square(scr, cur.x - half, cur.y - half, CURVE_BLOBSZ, + 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 */ + { + 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); + + if (font != NULL) + bmfont_draw(font, scr, name, (int) strlen(name), + task->line, task->bg, &pos, NULL); } return result_OK; @@ -132,9 +270,18 @@ static result_t curve_mouse(curve_task_t *task, switch (action) { case wuss_MOUSE_DOWN: + if (button & wuss_BUTTON_ADJUST) + { + /* cycle line -> quad -> cubic -> quartic -> quintic -> line */ + task->npoints++; + if (task->npoints > CURVE_MAXCONTROLPTS) + task->npoints = CURVE_MINCONTROLPTS; + wuss_window_invalidate_visible(window); + break; + } if (!(button & wuss_BUTTON_SELECT)) break; - for (i = 0; i < CURVE_NCONTROLPTS; i++) + for (i = 0; i < task->npoints; i++) { if (blob_hit(&task->points[i], x, y)) { @@ -149,7 +296,7 @@ static result_t curve_mouse(curve_task_t *task, break; task->points[task->dragging].x = x; task->points[task->dragging].y = y; - wuss_window_invalidate_all(window); + wuss_window_invalidate_visible(window); break; case wuss_MOUSE_UP: @@ -167,7 +314,7 @@ static result_t curve_scroll(curve_task_t *task, task->nsegments += delta; task->nsegments = CLAMP(task->nsegments, CURVE_SEGMENTS_MIN, CURVE_SEGMENTS_MAX); - wuss_window_invalidate_all(window); + wuss_window_invalidate_visible(window); return result_OK; } diff --git a/libraries/wuss/test/tasks/curve.h b/libraries/wuss/test/tasks/curve.h index 92e842a..8c32415 100644 --- a/libraries/wuss/test/tasks/curve.h +++ b/libraries/wuss/test/tasks/curve.h @@ -7,17 +7,23 @@ #include "framebuf/colour.h" #include "geom/point.h" +#include "wuss/wuss.h" #include "wuss/window.h" -#define CURVE_NCONTROLPTS 4 /* cubic Bezier: start, 2 control points, end */ +#define CURVE_MINCONTROLPTS 2 /* a straight line */ +#define CURVE_MAXCONTROLPTS 6 /* a quintic Bezier */ -/* a single cubic Bezier curve with draggable control points, redrawn as - * nsegments straight line segments; the mouse wheel adjusts nsegments */ +/* a single Bezier curve with draggable control points, redrawn as nsegments + * straight line segments. The mouse wheel adjusts nsegments; an Adjust click + * cycles the curve type (line, quadratic, cubic, quartic, quintic) by + * stepping npoints, the count of points[] actually in play. */ typedef struct curve_task { + wuss_t *wuss; /* borrowed; for wuss_get_font in the redraw */ wuss_window_t *window; colour_t bg, line, blob; - point_t points[CURVE_NCONTROLPTS]; + point_t points[CURVE_MAXCONTROLPTS]; + int npoints; /* CURVE_MINCONTROLPTS..CURVE_MAXCONTROLPTS */ int nsegments; int dragging; /* index into points, or -1 if not dragging */ } diff --git a/libraries/wuss/test/tasks/gradient.c b/libraries/wuss/test/tasks/gradient.c index 4dd1e52..1a21c87 100644 --- a/libraries/wuss/test/tasks/gradient.c +++ b/libraries/wuss/test/tasks/gradient.c @@ -8,7 +8,11 @@ #include "fortify/fortify.h" #endif +#include +#include + #include "base/utils.h" +#include "framebuf/bmfont.h" #include "framebuf/colour.h" #include "geom/box.h" @@ -19,25 +23,69 @@ #define GRADIENT_OPEN_WIDTH 100 #define GRADIENT_OPEN_HEIGHT 100 -/* 4x4 ordered (Bayer) dither matrix, values 0..15 */ -static const int bayer4x4[4][4] = +/* ordered (Bayer) dither matrices, each holding values 0 .. dim*dim-1. + * SELECT/ADJUST clicks cycle task->dither_index forward/backward through + * this table. */ +static const struct +{ + int dim; + int cell[64]; /* row-major, first dim*dim entries used */ +} +gradient_dithers[] = { - { 0, 8, 2, 10 }, - { 12, 4, 14, 6 }, - { 3, 11, 1, 9 }, - { 15, 7, 13, 5 }, + { + 2, + { + 0, 2, + 3, 1, + } + }, + { + 4, + { + 0, 8, 2, 10, + 12, 4, 14, 6, + 3, 11, 1, 9, + 15, 7, 13, 5, + } + }, + { + 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, + } + }, }; -static int dither(int v, int x, int y) +/* offset "v" by the dither cell for (x, y), mapped to a fixed -8..+8 swing + * (matching the original 4x4 code) whatever the matrix size, so a larger + * matrix just gives a finer pattern rather than a louder one */ +static int dither(int index, int v, int x, int y) { - return CLAMP(v + bayer4x4[y & 3][x & 3] - 8, 0, 255); + int dim, n, m; + + dim = gradient_dithers[index].dim; + n = dim * dim; + m = gradient_dithers[index].cell[(y % dim) * dim + (x % dim)]; + + return CLAMP(v + (m * 16 / (n - 1)) - 8, 0, 255); } result_t gradient_create(wuss_t *wuss, gradient_task_t *task) { + result_t rc; wuss_task_t *delegate; wuss_task_desc_t delegate_desc; - result_t rc; + + task->wuss = wuss; + task->dither_index = 1; /* 4x4, matching the original */ /* gradient_redraw paints every pixel itself */ delegate_desc.handle = gradient_handle; @@ -54,7 +102,7 @@ result_t gradient_create(wuss_t *wuss, gradient_task_t *task) rc = wuss_window_create_placed(delegate, SIZE2D(GRADIENT_OPEN_WIDTH, GRADIENT_OPEN_HEIGHT), "Gradient", - wuss_WINDOW_NONE, + wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), SIZE2D(GRADIENT_DOC_WIDTH, GRADIENT_DOC_HEIGHT), SIZE2D(0, 0), @@ -67,11 +115,13 @@ result_t gradient_create(wuss_t *wuss, gradient_task_t *task) static result_t gradient_redraw(const wuss_event_t *event, void *task_data) { - screen_t *scr; - const box_t *content, *bounds; - int sx, sy, x, y, lx, ly; + gradient_task_t *gc; + screen_t *scr; + const box_t *content, *bounds; + int di, sx, sy, x, y, lx, ly; - NOT_USED(task_data); + gc = task_data; + di = gc->dither_index; scr = event->data.redraw.scr; content = event->data.redraw.content; @@ -87,12 +137,55 @@ static result_t gradient_redraw(const wuss_event_t *event, void *task_data) ly = y - bounds->y0 + sy; screen_set_pixel(scr, x, y, - colour_rgb(dither(lx * 255 / GRADIENT_DOC_WIDTH, lx, ly), - dither(ly * 255 / GRADIENT_DOC_HEIGHT, lx, ly), - dither(255 - (lx + ly) * 255 / (GRADIENT_DOC_WIDTH + GRADIENT_DOC_HEIGHT), lx, ly))); + colour_rgb(dither(di, lx * 255 / GRADIENT_DOC_WIDTH, lx, ly), + dither(di, ly * 255 / GRADIENT_DOC_HEIGHT, lx, ly), + dither(di, 255 - (lx + ly) * 255 / (GRADIENT_DOC_WIDTH + GRADIENT_DOC_HEIGHT), lx, ly))); } } + /* 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 */ + { + 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); + 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) + bmfont_draw(font, scr, label, (int) strlen(label), ink, bg, &pos, NULL); + } + + return result_OK; +} + +static result_t gradient_mouse(const wuss_event_t *event, void *task_data) +{ + gradient_task_t *gc; + wuss_button_t button; + int n; + + gc = task_data; + + if (event->data.mouse.action != wuss_MOUSE_DOWN) + return result_OK; + + button = event->data.mouse.button; + n = (int) NELEMS(gradient_dithers); + + if (button & wuss_BUTTON_SELECT) + gc->dither_index = (gc->dither_index + 1) % n; + else if (button & wuss_BUTTON_ADJUST) + gc->dither_index = (gc->dither_index + n - 1) % n; + else + return result_OK; + + wuss_window_invalidate_visible(gc->window); /* whole fill changes */ + return result_OK; } @@ -102,6 +195,8 @@ result_t gradient_handle(wuss_window_t *window, { gradient_task_t *gc; + NOT_USED(window); + gc = task_data; switch (event->kind) @@ -109,6 +204,9 @@ result_t gradient_handle(wuss_window_t *window, case wuss_EVENT_REDRAW: return gradient_redraw(event, task_data); + case wuss_EVENT_MOUSE: + return gradient_mouse(event, task_data); + case wuss_EVENT_QUIT: free(gc); /* task_data was calloc'd per instance by the spawner */ return result_OK; diff --git a/libraries/wuss/test/tasks/gradient.h b/libraries/wuss/test/tasks/gradient.h index 0ad2c65..e6ce817 100644 --- a/libraries/wuss/test/tasks/gradient.h +++ b/libraries/wuss/test/tasks/gradient.h @@ -9,10 +9,13 @@ /* fills its whole content area with a two-axis colour gradient; opens * small (100x100) against a large (400x400) document, so scrollbars - * appear and the fill can be scrolled around */ + * appear and the fill can be scrolled around. SELECT/ADJUST clicks cycle + * the ordered-dither matrix forward/backward through 2x2, 4x4 and 8x8. */ typedef struct gradient_task { + wuss_t *wuss; /* borrowed; for wuss_get_font in the redraw */ wuss_window_t *window; + int dither_index; } gradient_task_t; diff --git a/libraries/wuss/test/tasks/greeble-tiles.h b/libraries/wuss/test/tasks/greeble-tiles.h index 914a77a..db86382 100644 --- a/libraries/wuss/test/tasks/greeble-tiles.h +++ b/libraries/wuss/test/tasks/greeble-tiles.h @@ -223,11 +223,25 @@ static const unsigned short greeble_tiles[GREEBLE_NTILES][GREEBLE_TILE_PX] = * grid of stamp indices in greeble_prefab_cells[], 0xFF marking a * cell the block's bounding box does not fill. 1x1 clusters are * dropped. GREEBLE_NPREFAB is 0 when the sheet was not supplied. */ -#define GREEBLE_NPREFAB 103 +#define GREEBLE_NPREFAB 137 #define GREEBLE_PREFAB_HOLE 0xFF static const unsigned char greeble_prefab_cells[] = { + 0x79, 0x7E, + 0x89, 0x8A, + 0x79, 0x7A, 0x7E, + 0x7C, 0xFF, 0x7C, + 0x89, 0x7A, 0x8A, + 0x79, 0x7A, 0x7A, 0x7E, + 0x7C, 0xFF, 0xFF, 0x7C, + 0x7C, 0xFF, 0xFF, 0x7C, + 0x89, 0x7A, 0x7A, 0x8A, + 0x79, 0x7A, 0x7A, 0x7A, 0x7E, + 0x7C, 0xFF, 0xFF, 0xFF, 0x7C, + 0x7C, 0xFF, 0xFF, 0xFF, 0x7C, + 0x7C, 0xFF, 0xFF, 0xFF, 0x7C, + 0x89, 0x7A, 0x7A, 0x7A, 0x8A, 0x86, 0x80, 0x86, @@ -253,21 +267,29 @@ static const unsigned char greeble_prefab_cells[] = 0x7D, 0x7A, 0x7E, 0xFF, 0xFF, 0x7C, 0xFF, 0xFF, 0x80, - 0x79, 0x7E, - 0x89, 0x8A, - 0x79, 0x7A, 0x7E, - 0x7C, 0xFF, 0x7C, - 0x89, 0x7A, 0x8A, - 0x79, 0x7A, 0x7A, 0x7E, - 0x7C, 0xFF, 0xFF, 0x7C, - 0x7C, 0xFF, 0xFF, 0x7C, - 0x89, 0x7A, 0x7A, 0x8A, - 0x79, 0x7A, 0x7A, 0x7A, 0x7E, - 0x7C, 0xFF, 0xFF, 0xFF, 0x7C, - 0x7C, 0xFF, 0xFF, 0xFF, 0x7C, - 0x7C, 0xFF, 0xFF, 0xFF, 0x7C, - 0x89, 0x7A, 0x7A, 0x7A, 0x8A, + 0x79, 0x7A, 0x7A, 0x7B, + 0x7C, 0xFF, 0xFF, 0xFF, + 0x7C, 0xFF, 0xFF, 0xFF, + 0x80, 0xFF, 0xFF, 0xFF, + 0x7D, 0x7A, 0x7A, 0x7E, + 0xFF, 0xFF, 0xFF, 0x7C, + 0xFF, 0xFF, 0xFF, 0x7C, + 0xFF, 0xFF, 0xFF, 0x80, 0x7D, 0x7A, 0x84, + 0x4C, 0x0E, + 0x10, 0x57, + 0x4C, 0x4D, 0x0E, + 0x50, 0x0F, 0x51, + 0x10, 0x56, 0x57, + 0x4C, 0x4D, 0x4D, 0x0E, + 0x50, 0x0F, 0x0F, 0x51, + 0x50, 0x0F, 0x0F, 0x51, + 0x10, 0x56, 0x56, 0x57, + 0x4C, 0x4D, 0x4D, 0x4D, 0x0E, + 0x50, 0x0F, 0x0F, 0x0F, 0x51, + 0x50, 0x0F, 0xFF, 0x0F, 0x51, + 0x50, 0x0F, 0x0F, 0x0F, 0x51, + 0x10, 0x56, 0x56, 0x56, 0x57, 0x86, 0xFF, 0x89, 0x84, 0xFF, 0x86, @@ -279,7 +301,29 @@ static const unsigned char greeble_prefab_cells[] = 0xFF, 0xFF, 0x86, 0xFF, 0xFF, 0x7C, 0x7D, 0x7A, 0x8A, + 0x86, 0xFF, 0xFF, 0xFF, + 0x7C, 0xFF, 0xFF, 0xFF, + 0x7C, 0xFF, 0xFF, 0xFF, + 0x89, 0x7A, 0x7A, 0x84, + 0xFF, 0xFF, 0xFF, 0x86, + 0xFF, 0xFF, 0xFF, 0x7C, + 0xFF, 0xFF, 0xFF, 0x7C, + 0x7D, 0x7A, 0x7A, 0x8A, 0x7D, 0x7A, 0x7A, 0x7A, 0x84, + 0x1C, 0x1D, + 0x27, 0x28, + 0x1C, 0x4E, 0x1D, + 0x52, 0x0F, 0x53, + 0x27, 0x58, 0x28, + 0x1C, 0x4E, 0x4E, 0x1D, + 0x52, 0x0F, 0x0F, 0x53, + 0x52, 0x0F, 0x0F, 0x53, + 0x27, 0x58, 0x58, 0x28, + 0x1C, 0x4E, 0x4E, 0x4E, 0x1D, + 0x52, 0x0F, 0x0F, 0x0F, 0x53, + 0x52, 0x0F, 0xFF, 0x0F, 0x53, + 0x52, 0x0F, 0x0F, 0x0F, 0x53, + 0x27, 0x58, 0x58, 0x58, 0x28, 0x17, 0x22, 0x17, @@ -308,6 +352,10 @@ static const unsigned char greeble_prefab_cells[] = 0xFF, 0x17, 0x17, 0xFF, 0xFF, 0x05, 0x05, 0xFF, 0x2E, 0xAF, 0xB0, 0x2F, + 0x98, 0x99, 0x9A, + 0x9B, 0x9C, 0x9D, + 0x9E, 0x9F, 0xA0, + 0xA2, 0xA3, 0xA4, 0x2E, 0x06, 0x2F, 0x2E, 0xA6, 0xFF, 0x22, @@ -320,7 +368,31 @@ static const unsigned char greeble_prefab_cells[] = 0x64, 0x06, 0x65, 0x05, 0xFF, 0xFF, 0x22, 0xFF, 0xFF, + 0x1E, 0x1F, + 0x29, 0x2A, + 0x1E, 0x4F, 0x1F, + 0x54, 0x0F, 0x55, + 0x29, 0x59, 0x2A, + 0x1E, 0x4F, 0x4F, 0x1F, + 0x54, 0x0F, 0x0F, 0x55, + 0x54, 0x0F, 0x0F, 0x55, + 0x29, 0x59, 0x59, 0x2A, + 0x1E, 0x4F, 0x4F, 0x4F, 0x1F, + 0x54, 0x0F, 0x0F, 0x0F, 0x55, + 0x54, 0x0F, 0xFF, 0x0F, 0x55, + 0x54, 0x0F, 0x0F, 0x0F, 0x55, + 0x29, 0x59, 0x59, 0x59, 0x2A, + 0x98, 0x99, 0x9A, 0x9B, 0x9C, 0x9D, 0x9E, 0x9F, 0xA0, 0x2E, 0x06, 0x06, 0x06, 0x2F, + 0x98, + 0x99, + 0x9A, + 0x9B, + 0x9C, + 0x9D, + 0x9E, + 0x9F, + 0xA0, 0x97, 0xA1, 0x97, @@ -350,6 +422,20 @@ static const unsigned char greeble_prefab_cells[] = 0xFF, 0x05, 0x05, 0xFF, 0x95, 0xAF, 0xB0, 0x96, 0x95, 0x06, 0x96, + 0x1E, 0x1F, + 0x29, 0x2A, + 0x1E, 0x4F, 0x1F, + 0x54, 0x44, 0x55, + 0x29, 0x59, 0x2A, + 0x1E, 0x4F, 0x4F, 0x1F, + 0x54, 0x44, 0x44, 0x55, + 0x54, 0x44, 0x44, 0x55, + 0x29, 0x59, 0x59, 0x2A, + 0x1E, 0x4F, 0x4F, 0x4F, 0x1F, + 0x54, 0x44, 0x44, 0x44, 0x55, + 0x54, 0x44, 0x44, 0x44, 0x55, + 0x54, 0x44, 0x44, 0x44, 0x55, + 0x29, 0x59, 0x59, 0x59, 0x2A, 0x95, 0xA6, 0xFF, 0xA1, 0x64, 0x96, @@ -406,26 +492,46 @@ static const unsigned char greeble_prefab_cells[] = 0x3B, 0x3B, 0x43, - 0xAC, - 0xAE, + 0xA8, 0x1F, + 0x29, 0xAA, + 0xA8, 0x4F, 0x1F, + 0x54, 0xA9, 0x55, + 0x29, 0x59, 0xAA, + 0xA8, 0x4F, 0x4F, 0x1F, + 0x54, 0xA9, 0xA9, 0x55, + 0x54, 0xA9, 0xA9, 0x55, + 0x29, 0x59, 0x59, 0xAA, + 0xA8, 0x4F, 0x4F, 0x4F, 0x1F, + 0x54, 0xA9, 0xA9, 0xA9, 0x55, + 0x54, 0xA9, 0xA9, 0xA9, 0x55, + 0x54, 0xA9, 0xA9, 0xA9, 0x55, + 0x29, 0x59, 0x59, 0x59, 0xAA, 0x0D, 0x0A, 0x0B, 0x2C, 0xA5, 0x2D, 0x0D, 0x0A, 0x0A, 0x0B, 0x2C, 0xA5, 0xA5, 0x2D, 0x0D, 0x0A, 0x0A, 0x0A, 0x0B, 0x2C, 0xA5, 0xA5, 0xA5, 0x2D, + 0x1A, 0x1B, + 0x25, 0x26, + 0x1A, 0x30, 0x1B, + 0x35, 0x36, 0x37, + 0x25, 0x3C, 0x26, + 0x1A, 0x30, 0x30, 0x1B, + 0x35, 0x36, 0x36, 0x37, + 0x35, 0x36, 0x36, 0x37, + 0x25, 0x3C, 0x3C, 0x26, + 0x1A, 0x30, 0x30, 0x30, 0x1B, + 0x35, 0x36, 0x36, 0x36, 0x37, + 0x35, 0x36, 0x36, 0x36, 0x37, + 0x35, 0x36, 0x36, 0x36, 0x37, + 0x25, 0x3C, 0x3C, 0x3C, 0x26, 0x18, 0x19, 0x23, 0x24, 0x1A, 0x1B, 0x25, 0x26, 0x3D, 0x3E, 0x45, 0x46, - 0x1C, 0x1D, - 0x27, 0x28, - 0x1E, 0x1F, - 0x29, 0x2A, - 0x20, 0x1F, - 0x29, 0x2B, 0x3F, 0x40, 0x47, 0x48, 0x41, 0x42, @@ -436,41 +542,8 @@ static const unsigned char greeble_prefab_cells[] = 0x38, 0x39, 0xBE, 0xBF, 0xC0, 0xC9, - 0x98, 0x99, 0x9A, 0x9B, 0x9C, 0x9D, 0x9E, 0x9F, 0xA0, - 0x98, 0x99, 0x9A, - 0x9B, 0x9C, 0x9D, - 0x9E, 0x9F, 0xA0, - 0xA2, 0xA3, 0xA4, - 0x98, - 0x99, - 0x9A, - 0x9B, - 0x9C, - 0x9D, - 0x9E, - 0x9F, - 0xA0, - 0x4C, 0x4D, 0x0E, - 0x50, 0x0F, 0x51, - 0x10, 0x56, 0x57, - 0x1C, 0x4E, 0x1D, - 0x52, 0x0F, 0x53, - 0x27, 0x58, 0x28, - 0x1E, 0x4F, 0x1F, - 0x54, 0x0F, 0x55, - 0x29, 0x59, 0x2A, - 0x1E, 0x4F, 0x1F, - 0x54, 0x44, 0x55, - 0x29, 0x59, 0x2A, - 0xA8, 0x4F, 0x1F, - 0x54, 0xA9, 0x55, - 0x29, 0x59, 0xAA, - 0x1A, 0x30, 0x1B, - 0x35, 0x36, 0x37, - 0x25, 0x3C, 0x26, - 0xC2, 0xC3, 0xC4, - 0xC5, 0x0F, 0xC6, - 0xC7, 0xC8, 0xC1, + 0xAC, + 0xAE, 0x8B, 0x0A, 0x0B, 0x8C, 0x0D, 0x0A, 0x0E, 0x0C, 0x01, 0x09, 0x8D, 0x09, 0x01, 0x0C, 0x03, 0x08, 0xFF, 0xFF, 0xFF, 0x08, 0x03, @@ -478,132 +551,224 @@ static const unsigned char greeble_prefab_cells[] = 0x02, 0x08, 0xFF, 0xFF, 0xFF, 0x08, 0x02, 0x0C, 0x01, 0x09, 0x92, 0x09, 0x01, 0x0C, 0x10, 0x0A, 0x0B, 0x93, 0x0D, 0x0A, 0x94, + 0xC2, 0xC3, 0xC4, + 0xC5, 0x0F, 0xC6, + 0xC7, 0xC8, 0xC1, + 0x11, 0x12, + 0x13, 0x14, + 0x11, 0x8D, 0x12, + 0x8F, 0x07, 0x90, + 0x13, 0x92, 0x14, + 0x11, 0x8D, 0x8D, 0x12, + 0x8F, 0x07, 0x07, 0x90, + 0x8F, 0x07, 0x07, 0x90, + 0x13, 0x92, 0x92, 0x14, + 0x11, 0x8D, 0x8D, 0x8D, 0x12, + 0x8F, 0x07, 0x07, 0x07, 0x90, + 0x8F, 0x07, 0x07, 0x07, 0x90, + 0x8F, 0x07, 0x07, 0x07, 0x90, + 0x13, 0x92, 0x92, 0x92, 0x14, + 0x5A, 0x5C, + 0x6E, 0x70, 0x5A, 0x5B, 0x5C, 0x66, 0x63, 0x67, 0x6E, 0x6F, 0x70, + 0x5A, 0x5B, 0x5B, 0x5C, + 0x66, 0x63, 0x63, 0x67, + 0x66, 0x63, 0x63, 0x67, + 0x6E, 0x6F, 0x6F, 0x70, + 0x5A, 0x5B, 0x5B, 0x5B, 0x5C, + 0x66, 0x63, 0x63, 0x63, 0x67, + 0x66, 0x63, 0x63, 0x63, 0x67, + 0x66, 0x63, 0x63, 0x63, 0x67, + 0x6E, 0x6F, 0x6F, 0x6F, 0x70, + 0x81, 0x83, + 0x87, 0x88, + 0x81, 0x82, 0x83, + 0x85, 0x16, 0x85, + 0x87, 0x82, 0x88, + 0x81, 0x82, 0x82, 0x83, + 0x85, 0x16, 0x16, 0x85, + 0x85, 0x16, 0x16, 0x85, + 0x87, 0x82, 0x82, 0x88, + 0x81, 0x82, 0x82, 0x82, 0x83, + 0x85, 0x16, 0x16, 0x16, 0x85, + 0x85, 0x16, 0x16, 0x16, 0x85, + 0x85, 0x16, 0x16, 0x16, 0x85, + 0x87, 0x82, 0x82, 0x82, 0x88, + 0x5D, 0x5F, + 0x71, 0x73, 0x5D, 0x5E, 0x5F, 0x68, 0x63, 0x69, 0x71, 0x72, 0x73, + 0x5D, 0x5E, 0x5E, 0x5F, + 0x68, 0x63, 0x63, 0x69, + 0x68, 0x63, 0x63, 0x69, + 0x71, 0x72, 0x72, 0x73, + 0x5D, 0x5E, 0x5E, 0x5E, 0x5F, + 0x68, 0x63, 0x63, 0x63, 0x69, + 0x68, 0x63, 0x63, 0x63, 0x69, + 0x68, 0x63, 0x63, 0x63, 0x69, + 0x71, 0x72, 0x72, 0x72, 0x73, 0x60, 0x61, 0x62, 0x6A, 0x6B, 0x0F, 0x74, 0x75, 0x76, + 0x60, 0x62, + 0x74, 0x76, 0x60, 0x63, 0x62, 0x63, 0x6C, 0x0F, 0x74, 0x75, 0x76, - 0x81, 0x82, 0x83, - 0x85, 0x16, 0x85, - 0x87, 0x82, 0x88, - 0x11, 0x8D, 0x12, - 0x8F, 0x07, 0x90, - 0x13, 0x92, 0x14, + 0x60, 0x63, 0x63, 0x62, + 0x63, 0x6C, 0x6C, 0x0F, + 0x63, 0x6C, 0x6C, 0x0F, + 0x74, 0x75, 0x75, 0x76, + 0x60, 0x63, 0x63, 0x63, 0x62, + 0x63, 0x6C, 0x6C, 0x6C, 0x0F, + 0x63, 0x6C, 0x6C, 0x6C, 0x0F, + 0x63, 0x6C, 0x6C, 0x6C, 0x0F, + 0x74, 0x75, 0x75, 0x75, 0x76, }; static const struct { unsigned char w, h; unsigned short off; } greeble_prefab[GREEBLE_NPREFAB] = { - { 1, 2, 0 }, /* 0 */ - { 1, 3, 2 }, /* 1 */ - { 1, 4, 5 }, /* 2 */ - { 1, 5, 9 }, /* 3 */ - { 2, 1, 14 }, /* 4 */ - { 2, 2, 16 }, /* 5 */ - { 2, 2, 20 }, /* 6 */ - { 3, 3, 24 }, /* 7 */ - { 3, 3, 33 }, /* 8 */ - { 2, 2, 42 }, /* 9 */ - { 3, 3, 46 }, /* 10 */ - { 4, 4, 55 }, /* 11 */ - { 5, 5, 71 }, /* 12 */ - { 3, 1, 96 }, /* 13 */ - { 2, 2, 99 }, /* 14 */ - { 2, 2, 103 }, /* 15 */ - { 4, 1, 107 }, /* 16 */ - { 3, 3, 111 }, /* 17 */ - { 3, 3, 120 }, /* 18 */ - { 5, 1, 129 }, /* 19 */ - { 1, 2, 134 }, /* 20 */ - { 1, 3, 136 }, /* 21 */ - { 1, 4, 139 }, /* 22 */ - { 1, 5, 143 }, /* 23 */ - { 2, 1, 148 }, /* 24 */ - { 2, 2, 150 }, /* 25 */ - { 2, 2, 154 }, /* 26 */ - { 3, 3, 158 }, /* 27 */ - { 3, 3, 167 }, /* 28 */ - { 4, 3, 176 }, /* 29 */ - { 3, 1, 188 }, /* 30 */ - { 2, 2, 191 }, /* 31 */ - { 2, 2, 195 }, /* 32 */ - { 4, 1, 199 }, /* 33 */ - { 3, 3, 203 }, /* 34 */ - { 3, 3, 212 }, /* 35 */ - { 5, 1, 221 }, /* 36 */ - { 1, 2, 226 }, /* 37 */ - { 1, 3, 228 }, /* 38 */ - { 1, 4, 231 }, /* 39 */ - { 1, 5, 235 }, /* 40 */ - { 2, 1, 240 }, /* 41 */ - { 2, 2, 242 }, /* 42 */ - { 2, 2, 246 }, /* 43 */ - { 3, 3, 250 }, /* 44 */ - { 3, 3, 259 }, /* 45 */ - { 4, 3, 268 }, /* 46 */ - { 3, 1, 280 }, /* 47 */ - { 2, 2, 283 }, /* 48 */ - { 2, 2, 287 }, /* 49 */ - { 4, 1, 291 }, /* 50 */ - { 3, 3, 295 }, /* 51 */ - { 3, 3, 304 }, /* 52 */ - { 5, 1, 313 }, /* 53 */ - { 1, 2, 318 }, /* 54 */ - { 1, 3, 320 }, /* 55 */ - { 1, 4, 323 }, /* 56 */ - { 1, 5, 327 }, /* 57 */ - { 2, 1, 332 }, /* 58 */ - { 1, 2, 334 }, /* 59 */ - { 1, 3, 336 }, /* 60 */ - { 1, 4, 339 }, /* 61 */ - { 1, 5, 343 }, /* 62 */ - { 2, 1, 348 }, /* 63 */ - { 1, 2, 350 }, /* 64 */ - { 1, 3, 352 }, /* 65 */ - { 1, 4, 355 }, /* 66 */ - { 1, 5, 359 }, /* 67 */ - { 1, 2, 364 }, /* 68 */ - { 3, 1, 366 }, /* 69 */ - { 3, 1, 369 }, /* 70 */ - { 4, 1, 372 }, /* 71 */ - { 4, 1, 376 }, /* 72 */ - { 5, 1, 380 }, /* 73 */ - { 5, 1, 385 }, /* 74 */ - { 2, 2, 390 }, /* 75 */ - { 2, 2, 394 }, /* 76 */ - { 2, 2, 398 }, /* 77 */ - { 2, 2, 402 }, /* 78 */ - { 2, 2, 406 }, /* 79 */ - { 2, 2, 410 }, /* 80 */ - { 2, 2, 414 }, /* 81 */ - { 2, 2, 418 }, /* 82 */ - { 2, 2, 422 }, /* 83 */ - { 2, 2, 426 }, /* 84 */ - { 2, 2, 430 }, /* 85 */ - { 9, 1, 434 }, /* 86 */ - { 3, 4, 443 }, /* 87 */ - { 1, 9, 455 }, /* 88 */ - { 3, 3, 464 }, /* 89 */ - { 3, 3, 473 }, /* 90 */ - { 3, 3, 482 }, /* 91 */ - { 3, 3, 491 }, /* 92 */ - { 3, 3, 500 }, /* 93 */ - { 3, 3, 509 }, /* 94 */ - { 3, 3, 518 }, /* 95 */ - { 7, 7, 527 }, /* 96 */ - { 3, 3, 576 }, /* 97 */ - { 3, 3, 585 }, /* 98 */ - { 3, 3, 594 }, /* 99 */ - { 3, 3, 603 }, /* 100 */ - { 3, 3, 612 }, /* 101 */ - { 3, 3, 621 }, /* 102 */ + { 2, 2, 0 }, /* 0 */ + { 3, 3, 4 }, /* 1 */ + { 4, 4, 13 }, /* 2 */ + { 5, 5, 29 }, /* 3 */ + { 1, 2, 54 }, /* 4 */ + { 1, 3, 56 }, /* 5 */ + { 1, 4, 59 }, /* 6 */ + { 1, 5, 63 }, /* 7 */ + { 2, 1, 68 }, /* 8 */ + { 2, 2, 70 }, /* 9 */ + { 2, 2, 74 }, /* 10 */ + { 3, 3, 78 }, /* 11 */ + { 3, 3, 87 }, /* 12 */ + { 4, 4, 96 }, /* 13 */ + { 4, 4, 112 }, /* 14 */ + { 3, 1, 128 }, /* 15 */ + { 2, 2, 131 }, /* 16 */ + { 3, 3, 135 }, /* 17 */ + { 4, 4, 144 }, /* 18 */ + { 5, 5, 160 }, /* 19 */ + { 2, 2, 185 }, /* 20 */ + { 2, 2, 189 }, /* 21 */ + { 4, 1, 193 }, /* 22 */ + { 3, 3, 197 }, /* 23 */ + { 3, 3, 206 }, /* 24 */ + { 4, 4, 215 }, /* 25 */ + { 4, 4, 231 }, /* 26 */ + { 5, 1, 247 }, /* 27 */ + { 2, 2, 252 }, /* 28 */ + { 3, 3, 256 }, /* 29 */ + { 4, 4, 265 }, /* 30 */ + { 5, 5, 281 }, /* 31 */ + { 1, 2, 306 }, /* 32 */ + { 1, 3, 308 }, /* 33 */ + { 1, 4, 311 }, /* 34 */ + { 1, 5, 315 }, /* 35 */ + { 2, 1, 320 }, /* 36 */ + { 2, 2, 322 }, /* 37 */ + { 2, 2, 326 }, /* 38 */ + { 3, 3, 330 }, /* 39 */ + { 3, 3, 339 }, /* 40 */ + { 4, 3, 348 }, /* 41 */ + { 3, 4, 360 }, /* 42 */ + { 3, 1, 372 }, /* 43 */ + { 2, 2, 375 }, /* 44 */ + { 2, 2, 379 }, /* 45 */ + { 4, 1, 383 }, /* 46 */ + { 3, 3, 387 }, /* 47 */ + { 3, 3, 396 }, /* 48 */ + { 2, 2, 405 }, /* 49 */ + { 3, 3, 409 }, /* 50 */ + { 4, 4, 418 }, /* 51 */ + { 5, 5, 434 }, /* 52 */ + { 9, 1, 459 }, /* 53 */ + { 5, 1, 468 }, /* 54 */ + { 1, 9, 473 }, /* 55 */ + { 1, 2, 482 }, /* 56 */ + { 1, 3, 484 }, /* 57 */ + { 1, 4, 487 }, /* 58 */ + { 1, 5, 491 }, /* 59 */ + { 2, 1, 496 }, /* 60 */ + { 2, 2, 498 }, /* 61 */ + { 2, 2, 502 }, /* 62 */ + { 3, 3, 506 }, /* 63 */ + { 3, 3, 515 }, /* 64 */ + { 4, 3, 524 }, /* 65 */ + { 3, 1, 536 }, /* 66 */ + { 2, 2, 539 }, /* 67 */ + { 3, 3, 543 }, /* 68 */ + { 4, 4, 552 }, /* 69 */ + { 5, 5, 568 }, /* 70 */ + { 2, 2, 593 }, /* 71 */ + { 2, 2, 597 }, /* 72 */ + { 4, 1, 601 }, /* 73 */ + { 3, 3, 605 }, /* 74 */ + { 3, 3, 614 }, /* 75 */ + { 5, 1, 623 }, /* 76 */ + { 1, 2, 628 }, /* 77 */ + { 1, 3, 630 }, /* 78 */ + { 1, 4, 633 }, /* 79 */ + { 1, 5, 637 }, /* 80 */ + { 2, 1, 642 }, /* 81 */ + { 1, 2, 644 }, /* 82 */ + { 1, 3, 646 }, /* 83 */ + { 1, 4, 649 }, /* 84 */ + { 1, 5, 653 }, /* 85 */ + { 2, 1, 658 }, /* 86 */ + { 1, 2, 660 }, /* 87 */ + { 1, 3, 662 }, /* 88 */ + { 1, 4, 665 }, /* 89 */ + { 1, 5, 669 }, /* 90 */ + { 2, 2, 674 }, /* 91 */ + { 3, 3, 678 }, /* 92 */ + { 4, 4, 687 }, /* 93 */ + { 5, 5, 703 }, /* 94 */ + { 3, 1, 728 }, /* 95 */ + { 3, 1, 731 }, /* 96 */ + { 4, 1, 734 }, /* 97 */ + { 4, 1, 738 }, /* 98 */ + { 5, 1, 742 }, /* 99 */ + { 5, 1, 747 }, /* 100 */ + { 2, 2, 752 }, /* 101 */ + { 3, 3, 756 }, /* 102 */ + { 4, 4, 765 }, /* 103 */ + { 5, 5, 781 }, /* 104 */ + { 2, 2, 806 }, /* 105 */ + { 2, 2, 810 }, /* 106 */ + { 2, 2, 814 }, /* 107 */ + { 2, 2, 818 }, /* 108 */ + { 2, 2, 822 }, /* 109 */ + { 2, 2, 826 }, /* 110 */ + { 2, 2, 830 }, /* 111 */ + { 2, 2, 834 }, /* 112 */ + { 1, 2, 838 }, /* 113 */ + { 7, 7, 840 }, /* 114 */ + { 3, 3, 889 }, /* 115 */ + { 2, 2, 898 }, /* 116 */ + { 3, 3, 902 }, /* 117 */ + { 4, 4, 911 }, /* 118 */ + { 5, 5, 927 }, /* 119 */ + { 2, 2, 952 }, /* 120 */ + { 3, 3, 956 }, /* 121 */ + { 4, 4, 965 }, /* 122 */ + { 5, 5, 981 }, /* 123 */ + { 2, 2, 1006 }, /* 124 */ + { 3, 3, 1010 }, /* 125 */ + { 4, 4, 1019 }, /* 126 */ + { 5, 5, 1035 }, /* 127 */ + { 2, 2, 1060 }, /* 128 */ + { 3, 3, 1064 }, /* 129 */ + { 4, 4, 1073 }, /* 130 */ + { 5, 5, 1089 }, /* 131 */ + { 3, 3, 1114 }, /* 132 */ + { 2, 2, 1123 }, /* 133 */ + { 3, 3, 1127 }, /* 134 */ + { 4, 4, 1136 }, /* 135 */ + { 5, 5, 1152 }, /* 136 */ }; /* stamps the generator scatters into cells no prefab claimed: the diff --git a/libraries/wuss/test/tasks/greeble.c b/libraries/wuss/test/tasks/greeble.c index d5e9f30..c2f9755 100644 --- a/libraries/wuss/test/tasks/greeble.c +++ b/libraries/wuss/test/tasks/greeble.c @@ -12,6 +12,7 @@ #include "base/utils.h" #include "framebuf/colour.h" #include "geom/box.h" +#include "utils/rng.h" #include "wuss/menu.h" #include "greeble.h" @@ -40,11 +41,6 @@ static wuss_menu_t g_greeble_menu = * is free. greeble_redraw skips these. */ #define GREEBLE_EMPTY 0xFF -/* one xorshift32 step on lvalue s; kept local to greeble_generate so the same - * seed always yields the same grid */ -#define GREEBLE_XORSHIFT(s) \ - ((s) ^= (s) << 13, (s) ^= (s) >> 17, (s) ^= (s) << 5) - /* try to drop prefab p with its top-left at (row,col): succeeds only if every * non-hole cell of the prefab lands on an in-bounds, still-empty grid cell, so * blocks never overlap. pal is the greeble_palettes[] row to tag every cell @@ -100,13 +96,13 @@ static int greeble_try_prefab(greeble_task_t *task, static void greeble_generate(greeble_task_t *task) { - unsigned int s; + rng_t s; int i; unsigned char order[GREEBLE_NPREFAB]; /* prefab indices, largest area first */ int tries_each; int r, c; - s = task->seed; + rng_seed(&s, task->seed); /* insertion-sort a fresh index list by w*h descending; NPREFAB is small and * this runs once per regenerate */ @@ -151,17 +147,12 @@ static void greeble_generate(greeble_task_t *task) int row, col; unsigned char pal; - GREEBLE_XORSHIFT(s); - row = (int) (s % (unsigned int) task->rows); - GREEBLE_XORSHIFT(s); - col = (int) (s % (unsigned int) task->cols); + row = rng_range(&s, task->rows); + col = rng_range(&s, task->cols); pal = task->palette; if (task->random_prefab_palettes) - { - GREEBLE_XORSHIFT(s); - pal = (unsigned char) (s % (unsigned int) GREEBLE_NPALETTE); - } + pal = (unsigned char) rng_range(&s, GREEBLE_NPALETTE); greeble_try_prefab(task, p, row, col, pal); } @@ -170,11 +161,8 @@ static void greeble_generate(greeble_task_t *task) for (r = 0; r < task->rows; r++) for (c = 0; c < task->cols; c++) if (task->grid[r][c] == GREEBLE_EMPTY) - { - GREEBLE_XORSHIFT(s); task->grid[r][c] = - greeble_filler[s % (unsigned int) GREEBLE_NFILLER]; - } + greeble_filler[rng_range(&s, GREEBLE_NFILLER)]; } /* recompute the grid extent for the window's content box, then regenerate */ @@ -274,14 +262,15 @@ static result_t greeble_redraw(const wuss_event_t *event, /* Select: advance to a fresh pattern (new seed, regenerate the grid). */ static result_t greeble_select(greeble_task_t *task, wuss_window_t *window) { - /* LCG step; any nonzero state keeps the pattern xorshift off its fixed - * point */ - task->seed = task->seed * 1664525u + 1013904223u; + /* LCG-hop to the next start seed; rng_range's xorshift then walks that. + * Remap the one zero state so the xorshift never lands on its fixed + * point. */ + rng_lcg32(&task->seed); if (task->seed == 0) task->seed = 1; greeble_generate(task); - wuss_window_invalidate_all(window); + wuss_window_invalidate_extent(window); return result_OK; } @@ -296,7 +285,7 @@ static result_t greeble_toggle_randpal(greeble_task_t *task, { task->random_prefab_palettes = !task->random_prefab_palettes; greeble_generate(task); - wuss_window_invalidate_all(window); + wuss_window_invalidate_extent(window); return result_OK; } @@ -308,7 +297,7 @@ static result_t greeble_adjust(greeble_task_t *task, wuss_window_t *window) { task->palette = (unsigned char) ((task->palette + 1) % GREEBLE_NPALETTE); greeble_generate(task); - wuss_window_invalidate_all(window); + wuss_window_invalidate_extent(window); return result_OK; } @@ -376,6 +365,10 @@ result_t greeble_handle(wuss_window_t *window, case wuss_EVENT_MENU_SELECT: return greeble_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: free(task); /* task_data was calloc'd per instance by the spawner */ return result_OK; @@ -387,11 +380,11 @@ result_t greeble_handle(wuss_window_t *window, result_t greeble_create(wuss_t *wuss, greeble_task_t *task) { + result_t rc; wuss_task_t *delegate; wuss_task_desc_t delegate_desc; size2d_t grid_px; box_t content; - result_t rc; /* the full generator grid in pixels: content sized so cols/rows land * exactly on the GREEBLE_MAX_* caps in 8-pixel tiles */ @@ -420,7 +413,7 @@ result_t greeble_create(wuss_t *wuss, greeble_task_t *task) rc = wuss_window_create_placed(delegate, grid_px, "Greeble", - wuss_WINDOW_NONE, + wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), grid_px, SIZE2D(0, 0), diff --git a/libraries/wuss/test/tasks/greeble.h b/libraries/wuss/test/tasks/greeble.h index e665e42..e44a0bd 100644 --- a/libraries/wuss/test/tasks/greeble.h +++ b/libraries/wuss/test/tasks/greeble.h @@ -5,6 +5,7 @@ #ifdef WUSS_APP +#include "utils/rng.h" #include "wuss/menu.h" #include "wuss/window.h" @@ -30,7 +31,7 @@ typedef struct greeble_task wuss_task_t *delegate; /* the task that owns the menu */ wuss_window_t *window; wuss_menu_handle_t menu_handle; /* live only between open and a SELECT pick */ - unsigned int seed; /* current pattern seed; advanced on click */ + rng_t seed; /* current pattern seed; advanced on click */ int cols, rows; /* live grid extent, <= the MAX_* caps */ unsigned char palette; /* greeble_palettes[] row for this pattern */ int random_prefab_palettes; /* nonzero: each scattered prefab diff --git a/libraries/wuss/test/tasks/icons.c b/libraries/wuss/test/tasks/icons.c index 149eb00..44cb800 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 900 /* taller than the window, so scrolling is exercised */ +#define ICONS_DOC_H 1160 /* 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 */ @@ -34,13 +34,16 @@ enum { ICONS_N_INTRO = 4, /* heading, counter button, counter label, scrolled-away button */ - ICONS_N_BUTTONS = 4, /* frame + normal + default + disabled button */ + ICONS_N_BUTTONS = 5, /* frame + 2x2 grid: default/normal x plain/disabled */ ICONS_N_RADIOS = 8, /* frame + 3 radios + option + state label + 2 justified labels */ ICONS_N_BITMAPS = 3, /* frame + decorative + interactive bitmap */ + ICONS_N_ICONSET = 5, /* frame + opton + optoff + radon + radoff from the loaded set */ ICONS_N_PATTERN = 2, /* frame + one PATTERN swatch */ - ICONS_N_MENU = 7, /* plain, ticked, swatch, submenu, disabled, rule, separator entry */ + ICONS_N_BORDERS = 6, /* frame + GROOVE + RIDGE + ACTION + DIVIDER labels */ + 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_PATTERN + ICONS_N_MENU + ICONS_N_BITMAPS + ICONS_N_ICONSET + ICONS_N_PATTERN + + ICONS_N_BORDERS + ICONS_N_MENU }; /* Running state threaded through the icons_add_* helpers: where to write the @@ -51,9 +54,8 @@ typedef struct icons_layout int n; /* specs[0..n) are filled in */ int y; /* document y of the next group */ wuss_colour_t black; - wuss_colour_t grey5; - wuss_colour_t grey6; - wuss_colour_t red; /* menu swatch demo colour */ + wuss_colour_t window; /* wuss_COLOUR_WINDOW: the standard work-area fill */ + wuss_colour_t red; /* menu swatch demo colour */ } icons_layout_t; @@ -79,11 +81,13 @@ static void icons_add_intro(icons_layout_t *lay, int *button, int *counter) *button = lay->n; s = &lay->specs[lay->n]; - s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN, lay->y, 80, 22); + /* 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->text = "Press me"; s->fg = lay->black; - s->bg = lay->grey6; + s->bg = lay->window; s->flags = wuss_ICON_FLAGS_DEFAULT; lay->n++; @@ -98,57 +102,69 @@ static void icons_add_intro(icons_layout_t *lay, int *button, int *counter) lay->y += 46; s = &lay->specs[lay->n]; - s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN, 780, 90, 52); + s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN, 980, 90, 52); s->type = wuss_ICON_TYPE_BUTTON; s->text = "Scrolled"; s->fg = lay->black; - s->bg = lay->grey6; + s->bg = lay->window; lay->n++; } -/* A grouping frame captioned "Buttons", with a normal, a default (accent) and - * a disabled button side by side inside it. */ +/* A grouping frame captioned "Buttons", holding a 2x2 grid: column 0 is the + * DEFAULT (accent) button, column 1 the plain one; the bottom row adds + * DISABLED. The DEFAULT column is inset 4px on every side to seat the accent + * icon's 6px action surround. */ static void icons_add_buttons(icons_layout_t *lay) { wuss_icon_spec_t *s; int top; + int col; + int row; + int x; + int y; top = lay->y; s = &lay->specs[lay->n]; - s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN, top, 200, 56); + s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN, top, 200, 90); s->type = wuss_ICON_TYPE_FRAME; s->text = "Buttons"; s->fg = lay->black; s->bg = wuss_NO_BACKGROUND; lay->n++; - s = &lay->specs[lay->n]; - s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN + 10, top + 22, 56, 22); - s->type = wuss_ICON_TYPE_BUTTON; - s->text = "Normal"; - s->fg = lay->black; - s->bg = lay->grey6; - lay->n++; - - s = &lay->specs[lay->n]; - s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN + 74, top + 22, 56, 22); - s->type = wuss_ICON_TYPE_BUTTON; - s->text = "Default"; - s->fg = lay->black; - s->bg = lay->grey6; - s->flags = wuss_ICON_FLAGS_DEFAULT; - lay->n++; - - s = &lay->specs[lay->n]; - s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN + 138, top + 22, 56, 22); - s->type = wuss_ICON_TYPE_BUTTON; - s->text = "Disabled"; - s->fg = lay->black; - s->bg = lay->grey6; - s->flags = wuss_ICON_FLAGS_DISABLED; - lay->n++; + for (row = 0; row < 2; row++) + { + for (col = 0; col < 2; col++) + { + x = ICONS_MARGIN + 10 + col * 100; + y = top + 22 + row * 34; + + s = &lay->specs[lay->n]; + s->fg = lay->black; + s->bg = lay->window; + s->type = wuss_ICON_TYPE_BUTTON; + + if (col == 0) + { + s->bbox = (box_t) BOX_POS_SIZE(x - 4, y - 4, 64, 30); + s->text = "Default"; + s->flags = wuss_ICON_FLAGS_DEFAULT; + } + else + { + s->bbox = (box_t) BOX_POS_SIZE(x, y, 56, 22); + s->text = "Normal"; + s->flags = 0; + } + + if (row == 1) + s->flags |= wuss_ICON_FLAGS_DISABLED; + + lay->n++; + } + } - lay->y = top + 70; + lay->y = top + 104; } /* A grouping frame captioned "Radios & options", with two justified labels, @@ -264,6 +280,54 @@ static void icons_add_bitmaps(icons_layout_t *lay, lay->y = top + sprite->size.h + 50; } +/* A grouping frame captioned "Icon set", holding the four fixtures loaded by + * wuss_icons_load -- opton/optoff/radon/radoff -- each drawn as a BITMAP icon + * that references the loaded set by index via wuss_ICON_SET. Laid out only + * when the set loaded (all four looked up); skipped otherwise. */ +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; + + for (i = 0; i < 4; i++) + { + idx[i] = wuss_icons_lookup(wuss, names[i]); + if (idx[i] < 0) + return; + } + + top = lay->y; + s = &lay->specs[lay->n]; + s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN, top, 200, 44); + s->type = wuss_ICON_TYPE_FRAME; + s->text = "Icon set"; + s->fg = lay->black; + s->bg = wuss_NO_BACKGROUND; + lay->n++; + + for (i = 0; i < 4; i++) + { + const bitmap_t *bm; + int w, h; + + bm = wuss_icons_bitmap(wuss, idx[i]); + w = bm ? bm->size.w : 16; + h = bm ? bm->size.h : 16; + + s = &lay->specs[lay->n]; + 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]); + lay->n++; + } + + lay->y = top + 60; +} + /* A grouping frame captioned "Pattern", holding one PATTERN-filled swatch. */ static void icons_add_pattern(icons_layout_t *lay) { @@ -283,25 +347,98 @@ static void icons_add_pattern(icons_layout_t *lay) s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN + 10, top + 20, 180, 26); s->type = wuss_ICON_TYPE_PATTERN; s->fg = lay->black; - s->bg = lay->grey6; + s->bg = lay->window; s->pattern = screen_PATTERN_DIAGONAL; lay->n++; lay->y = top + 76; } -/* 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 entry to see the highlight track; the rule - * stays inert. Returns the "Show grid" index (started ticked) via *ticked. */ +/* A grouping frame captioned "Borders", holding a GROOVE-bordered label (a + * sunken RISC OS display field), a RIDGE-bordered one (raised), an + * ACTION-bordered one -- a 6px surround: raised outset, accent moat, raised + * inset, like a default-action button -- then a DIVIDER-bordered one: a 4px + * surround, outer sunken ring around inner raised, in lighter shades. */ +static void icons_add_borders(icons_layout_t *lay) +{ + 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, 150); + s->type = wuss_ICON_TYPE_FRAME; + s->text = "Borders"; + s->fg = lay->black; + s->bg = wuss_NO_BACKGROUND; + lay->n++; + + s = &lay->specs[lay->n]; + s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN + 10, top + 20, 180, 22); + s->type = wuss_ICON_TYPE_LABEL; + s->text = "Groove"; + s->fg = lay->black; + s->bg = lay->window; + s->border = wuss_ICON_BORDER_GROOVE; + s->flags = wuss_ICON_FLAGS_JUSTIFY_CENTRE; + lay->n++; + + s = &lay->specs[lay->n]; + s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN + 10, top + 46, 180, 22); + s->type = wuss_ICON_TYPE_LABEL; + s->text = "Ridge"; + s->fg = lay->black; + s->bg = lay->window; + s->border = wuss_ICON_BORDER_RIDGE; + s->flags = wuss_ICON_FLAGS_JUSTIFY_CENTRE; + lay->n++; + + s = &lay->specs[lay->n]; + s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN + 10, top + 72, 180, 34); + s->type = wuss_ICON_TYPE_LABEL; + s->text = "Action"; + s->fg = lay->black; + s->bg = lay->window; + s->border = wuss_ICON_BORDER_ACTION; + s->flags = wuss_ICON_FLAGS_JUSTIFY_CENTRE; + lay->n++; + + s = &lay->specs[lay->n]; + s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN + 10, top + 110, 180, 30); + s->type = wuss_ICON_TYPE_LABEL; + s->text = "Divider"; + s->fg = lay->black; + s->bg = lay->window; + s->border = wuss_ICON_BORDER_DIVIDER; + s->flags = wuss_ICON_FLAGS_JUSTIFY_CENTRE; + lay->n++; + + lay->y = top + 166; +} + +/* 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 + * entry to see the highlight track; the rule stays inert. Returns the "Show + * grid" index (started ticked) via *ticked. */ static void icons_add_menu(icons_layout_t *lay, int *ticked) { wuss_icon_spec_t *s; int top; + int row; top = lay->y; s = &lay->specs[lay->n]; - s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN, top, 180, 16); + s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN, top, 200, ICONS_ROW * 7 + 30); + s->type = wuss_ICON_TYPE_FRAME; + s->text = "Menu"; + s->fg = lay->black; + s->bg = wuss_NO_BACKGROUND; + lay->n++; + + row = top + 20; + s = &lay->specs[lay->n]; + s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN + 10, row, 180, 16); s->type = wuss_ICON_TYPE_MENU_ENTRY; s->text = "Open"; s->fg = lay->black; @@ -309,7 +446,7 @@ static void icons_add_menu(icons_layout_t *lay, int *ticked) lay->n++; s = &lay->specs[lay->n]; - s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN, top + ICONS_ROW, 180, 16); + s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN + 10, row + ICONS_ROW, 180, 16); s->type = wuss_ICON_TYPE_MENU_ENTRY; s->text = "Show grid"; s->fg = lay->black; @@ -318,7 +455,7 @@ static void icons_add_menu(icons_layout_t *lay, int *ticked) lay->n++; s = &lay->specs[lay->n]; - s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN, top + ICONS_ROW * 2, 180, 16); + s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN + 10, row + ICONS_ROW * 2, 180, 16); s->type = wuss_ICON_TYPE_MENU_ENTRY; s->text = "Layer colour"; s->fg = lay->black; @@ -328,7 +465,7 @@ static void icons_add_menu(icons_layout_t *lay, int *ticked) lay->n++; s = &lay->specs[lay->n]; - s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN, top + ICONS_ROW * 3, 180, 16); + s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN + 10, row + ICONS_ROW * 3, 180, 16); s->type = wuss_ICON_TYPE_MENU_ENTRY; s->text = "Export"; s->fg = lay->black; @@ -337,7 +474,7 @@ static void icons_add_menu(icons_layout_t *lay, int *ticked) lay->n++; s = &lay->specs[lay->n]; - s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN, top + ICONS_ROW * 4, 180, 16); + s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN + 10, row + ICONS_ROW * 4, 180, 16); s->type = wuss_ICON_TYPE_MENU_ENTRY; s->text = "Disabled"; s->fg = lay->black; @@ -346,14 +483,14 @@ static void icons_add_menu(icons_layout_t *lay, int *ticked) lay->n++; s = &lay->specs[lay->n]; - s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN, top + ICONS_ROW * 5, 180, 10); + s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN + 10, row + ICONS_ROW * 5, 180, 10); s->type = wuss_ICON_TYPE_RULE; s->fg = lay->black; s->bg = wuss_NO_BACKGROUND; lay->n++; s = &lay->specs[lay->n]; - s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN, top + ICONS_ROW * 6, 180, 16); + s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN + 10, row + ICONS_ROW * 6, 180, 16); s->type = wuss_ICON_TYPE_MENU_ENTRY; s->text = "Quit"; s->fg = lay->black; @@ -361,7 +498,7 @@ static void icons_add_menu(icons_layout_t *lay, int *ticked) s->flags = wuss_ICON_FLAGS_SEPARATOR; lay->n++; - lay->y = top + ICONS_ROW * 7 + 10; + lay->y = top + ICONS_ROW * 7 + 46; } /* ----------------------------------------------------------------------- */ @@ -382,7 +519,10 @@ result_t icons_create(wuss_t *wuss, task->font = font; task->label = colour_rgb(0x00, 0x00, 0x00); - task->paper = colour_rgb(0xDD, 0xDD, 0xDD); /* the window bg, below */ + /* only the ruler-text glyph blend; approximates the wuss_COLOUR_WINDOW / + * wuss_COLOUR_BACKDROP crosshatch the rulers sit on -- no public call + * resolves a symbolic wuss_colour_t to a concrete colour_t here */ + task->paper = colour_rgb(0xDD, 0xDD, 0xDD); task->window = NULL; task->button = NULL; task->counter = NULL; @@ -397,6 +537,18 @@ result_t icons_create(wuss_t *wuss, if (bitmap_load_png(&task->sprite, sprite_path) == result_OK) task->has_sprite = 1; + /* load the wuss-wide icon set; path_join_filename hands back one shared + * static buffer and wuss_icons_load re-joins per file, so copy it first */ + { + char icons_dir[256]; + + strncpy(icons_dir, + path_join_filename(resources, 3, "resources", "wuss", "icons"), + sizeof(icons_dir) - 1); + icons_dir[sizeof(icons_dir) - 1] = '\0'; + (void) wuss_icons_load(wuss, icons_dir); /* absent set just skips the group */ + } + delegate_desc.handle = icons_handle; delegate_desc.task_data = task; delegate_desc.name = "icons"; @@ -410,18 +562,19 @@ result_t icons_create(wuss_t *wuss, } memset(&lay, 0, sizeof(lay)); - lay.black = wuss_nearest_colour(wuss, 0x00, 0x00, 0x00); - lay.grey5 = wuss_nearest_colour(wuss, 0xBB, 0xBB, 0xBB); - lay.grey6 = wuss_nearest_colour(wuss, 0xDD, 0xDD, 0xDD); - lay.red = wuss_nearest_colour(wuss, 0xCC, 0x33, 0x33); + lay.black = wuss_COLOUR_BLACK; + lay.window = wuss_COLOUR_WINDOW; /* the standard work-area fill */ + lay.red = wuss_nearest_colour(wuss, 0xCC, 0x33, 0x33); /* off-primary; no symbol */ + /* crosshatch the standard window colour over the standard backdrop colour, + * so the texture tracks the chrome config rather than fixed greys */ rc = wuss_window_create_placed(delegate, SIZE2D(ICONS_DOC_W, 200), "Icons", - wuss_WINDOW_NONE, - wuss_BACKDROP_PATTERN(lay.grey5, + wuss_WINDOW_DEFAULT, + wuss_BACKDROP_PATTERN(wuss_COLOUR_GREY, screen_PATTERN_CROSSHATCH, - lay.grey6), + wuss_COLOUR_WINDOW), SIZE2D(ICONS_DOC_W, ICONS_DOC_H), SIZE2D(0, 0), &task->window); @@ -444,7 +597,9 @@ result_t icons_create(wuss_t *wuss, icons_add_buttons(&lay); icons_add_radios(&lay, &i_opt, &i_state); icons_add_bitmaps(&lay, task->has_sprite ? &task->sprite : NULL, &i_hotspot); + icons_add_iconset(&lay, wuss); icons_add_pattern(&lay); + icons_add_borders(&lay); icons_add_menu(&lay, &i_ticked); rc = wuss_icon_create_array(task->window, specs, lay.n, made); diff --git a/libraries/wuss/test/tasks/image.c b/libraries/wuss/test/tasks/image.c index 2f80cda..f4f56c3 100644 --- a/libraries/wuss/test/tasks/image.c +++ b/libraries/wuss/test/tasks/image.c @@ -2,6 +2,7 @@ #ifdef WUSS_APP +#include #include #include @@ -13,37 +14,17 @@ #include "base/utils.h" #include "framebuf/palettes.h" #include "geom/box.h" -#include "io/dirscan.h" +#include "io/namelist.h" #include "io/path.h" +#include "wuss/menu.h" +#include "wuss/menu-desc.h" #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_LEN 4 - -static result_t image__scan_entry(const char *leaf, void *opaque) -{ - image_task_t *ic; - size_t leaflen; - - ic = opaque; - leaflen = strlen(leaf); - - if (leaflen <= IMAGE_EXT_LEN || - leaflen - IMAGE_EXT_LEN >= IMAGE_MAX_NAME_LEN) - return result_OK; - if (strcmp(leaf + leaflen - IMAGE_EXT_LEN, IMAGE_EXT) != 0) - return result_OK; - if (ic->nnames >= IMAGE_MAX_NAMES) - return result_STOP_WALK; - - memcpy(ic->names[ic->nnames], leaf, leaflen - IMAGE_EXT_LEN); - ic->names[ic->nnames][leaflen - IMAGE_EXT_LEN] = '\0'; - ic->nnames++; - - return result_OK; -} result_t image_create(wuss_t *wuss, const char *resources, @@ -51,18 +32,25 @@ result_t image_create(wuss_t *wuss, const char *background_path, image_task_t *task) { + result_t rc; wuss_task_t *delegate; wuss_task_desc_t delegate_desc; const char *images_dir; - result_t rc; - size2d_t sz; + size2d_t sz; - task->resources = resources; - task->index = 0; - task->nnames = 0; + task->wuss = wuss; + task->delegate = NULL; + task->resources = resources; + task->index = 0; + task->nnames = 0; + task->menu = NULL; + task->proginfo = NULL; + task->menu_handle = NULL; images_dir = path_join_filename(resources, 2, "resources", "images"); - rc = dirscan_walk(images_dir, image__scan_entry, task); + rc = namelist_scan(images_dir, IMAGE_EXT, task->names[0], + sizeof(task->names[0]), IMAGE_MAX_NAMES, 0 /* unsorted */, + &task->nnames); if (rc != result_OK) { free(task); /* nothing registered yet; the spawner will not free it */ @@ -96,24 +84,46 @@ result_t image_create(wuss_t *wuss, free(task); /* nothing registered yet; the spawner will not free it */ return rc; } - wuss_task_set_autoclose(delegate, 1); + task->delegate = delegate; + /* No autoclose: the task also owns the hidden proginfo window below, so its + * window list never empties while the main window is up. QUIT is delivered + * at wuss_destroy instead. Closing the main window early leaks this block + * until then -- fine for a demo. */ - sz.w = task->bitmap.size.w + NINEPATCHSZ * 2; - sz.h = task->bitmap.size.h + NINEPATCHSZ * 2; + sz.w = task->bitmap.size.w + IMAGE_MARGINSZ * 2; + sz.h = task->bitmap.size.h + IMAGE_MARGINSZ * 2; rc = wuss_window_create_placed(delegate, - /* shorter than the bitmap so there's something to scroll through */ - SIZE2D(sz.w, sz.h * 2 / 3), + SIZE2D(sz.w, sz.h), "Image", - wuss_WINDOW_NONE, + wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(palette_PICO8_PINK), sz, SIZE2D(32, 32), &task->window); if (rc != result_OK) + { wuss_task_destroy(delegate); /* QUIT frees the two bitmaps and the block */ + return rc; + } - return rc; + /* The "Info" menu row's standard dialogue. Hung off the descriptor menu as + * a wuss_menu_item_t.window in image_open_menu. A create failure is + * non-fatal -- the task just runs without an Info dialogue. */ + { + static const wuss_proginfo_desc_t desc = + { + "Image", + "Cycle the PNGs under resources/images", + "(c) DPTLib contributors", + "1.0 (" __DATE__ ")" + }; + + if (wuss_proginfo_create(&task->proginfo, delegate, &desc) != result_OK) + task->proginfo = NULL; + } + + return result_OK; } static result_t image_redraw(const wuss_event_t *event, void *task_data) @@ -124,6 +134,7 @@ static result_t image_redraw(const wuss_event_t *event, void *task_data) int sx, sy; int bx, by; box_t behind; + box_t band[4]; ic = task_data; @@ -131,15 +142,34 @@ static result_t image_redraw(const wuss_event_t *event, void *task_data) bounds = event->data.redraw.bounds; sx = event->data.redraw.scroll.x; sy = event->data.redraw.scroll.y; - bx = bounds->x0 - sx + NINEPATCHSZ; - by = bounds->y0 - sy + NINEPATCHSZ; + bx = bounds->x0 - sx + IMAGE_MARGINSZ; + by = bounds->y0 - sy + IMAGE_MARGINSZ; - behind.x0 = bx - NINEPATCHSZ; - behind.y0 = by - NINEPATCHSZ; - behind.x1 = behind.x0 + ic->bitmap.size.w + NINEPATCHSZ * 2; - behind.y1 = behind.y0 + ic->bitmap.size.h + NINEPATCHSZ * 2; + 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); + /* 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 */ + screen_copy_bitmap(scr, bx, by, &ic->bitmap); return result_OK; @@ -149,12 +179,12 @@ static result_t image_click(wuss_window_t *window, image_task_t *ic, int step) { + result_t rc; const char *leafname; const char *filename; char buf[DPTLIB_MAXPATH]; bitmap_t next; size2d_t sz; - result_t rc; if (ic->nnames == 0) return result_OK; /* nothing to cycle to */ @@ -176,16 +206,70 @@ static result_t image_click(wuss_window_t *window, free(ic->bitmap.base); ic->bitmap = next; - sz.w = ic->bitmap.size.w + NINEPATCHSZ * 2; - sz.h = ic->bitmap.size.h + NINEPATCHSZ * 2; + sz.w = ic->bitmap.size.w + IMAGE_MARGINSZ * 2; + sz.h = ic->bitmap.size.h + IMAGE_MARGINSZ * 2; + + rc = wuss_window_resize(window, sz); + if (rc != result_OK) + return rc; return wuss_window_set_doc(window, sz); } +/* Same menu shape built from a descriptor string, to exercise + * wuss_menu_create_from_desc. The tree must outlive the open chain, so it is + * kept on the task and rebuilt (previous one freed) on each open. Freed for + * good in the QUIT handler. */ +static const wuss_menu_item_t image_menu_export_items[] = +{ + { "As PNG", wuss_MENU_ITEM_NONE, NULL }, + { "As JPEG", wuss_MENU_ITEM_NONE, NULL }, + { "As GIF", wuss_MENU_ITEM_DISABLED, NULL } +}; + +static const wuss_menu_t image_menu_export = +{ + "Export", image_menu_export_items, NELEMS(image_menu_export_items) +}; + +static result_t image_open_menu(image_task_t *ic) +{ + result_t rc; + wuss_menu_t *m; + int i; + + rc = wuss_menu_create_from_desc(&m, + "Image, Info, New..., Open, !Show grid, !Wireframe, >Export, |Quit", + &image_menu_export); + if (rc != result_OK) + return rc; + + /* The descriptor syntax has no "open this window on hover" mark, so point + * the "Info" row at the proginfo dialogue by hand: wuss treats a + * wuss_menu_item_t.window exactly like a submenu, showing it where one would + * open. */ + if (ic->proginfo != NULL) + for (i = 0; i < m->nitems; i++) + if (m->items[i].text != NULL && strcmp(m->items[i].text, "Info") == 0) + { + ((wuss_menu_item_t *) m->items)[i].window = + wuss_proginfo_window(ic->proginfo); + break; + } + + wuss_menu_destroy(ic->menu); + ic->menu = m; + + return wuss_menu_open(ic->delegate, ic->menu, wuss_get_pointer(ic->wuss), + &ic->menu_handle); +} + result_t image_handle(wuss_window_t *window, const wuss_event_t *event, void *task_data) { - image_task_t *ic; + image_task_t *ic; + const wuss_menu_t *menu; + int index; ic = task_data; @@ -195,15 +279,38 @@ result_t image_handle(wuss_window_t *window, return image_redraw(event, task_data); case wuss_EVENT_MOUSE: + if (window != ic->window) + return result_OK; /* not the image window (e.g. the proginfo dialogue) */ if (event->data.mouse.action != wuss_MOUSE_DOWN) return result_OK; if (event->data.mouse.button & wuss_BUTTON_SELECT) return image_click(window, ic, 1); if (event->data.mouse.button & wuss_BUTTON_ADJUST) return image_click(window, ic, -1); + if (event->data.mouse.button & wuss_BUTTON_MENU) + return image_open_menu(ic); + return result_OK; + + case wuss_EVENT_MENU_SELECT: + menu = event->data.menu_select.menu; + index = event->data.menu_select.index; + printf("image menu: picked \"%s\"\n", + menu->items[index].text ? menu->items[index].text : "(sep)"); + if (!wuss_menu_should_keep_open(event)) + ic->menu_handle = NULL; /* SELECT pick already freed the chain */ + return result_OK; + + case wuss_EVENT_MENU_CLOSED: + ic->menu_handle = NULL; /* wuss closed the chain under us */ 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 */ 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 cfba7e3..6c4f3ab 100644 --- a/libraries/wuss/test/tasks/image.h +++ b/libraries/wuss/test/tasks/image.h @@ -6,6 +6,8 @@ #ifdef WUSS_APP #include "framebuf/bitmap.h" +#include "wuss/component/proginfo.h" +#include "wuss/menu.h" #include "wuss/window.h" #define IMAGE_MAX_NAMES 64 @@ -17,6 +19,8 @@ typedef struct image_task { wuss_window_t *window; + wuss_t *wuss; /* for wuss_get_pointer when opening the menu */ + 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 */ @@ -25,6 +29,14 @@ typedef struct image_task * leafnames (extension stripped) found under * resources/images at spawn time */ int nnames; + wuss_menu_t *menu; /* owned: MENU-button demo tree, built from a + * descriptor string; rebuilt on each open, + * freed at QUIT */ + 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_menu_handle_t menu_handle; /* the open chain, if any -- closed before the + * proginfo it borrows is destroyed at QUIT */ } image_task_t; diff --git a/libraries/wuss/test/tasks/lissajous.c b/libraries/wuss/test/tasks/lissajous.c index ee5ab11..69a6c04 100644 --- a/libraries/wuss/test/tasks/lissajous.c +++ b/libraries/wuss/test/tasks/lissajous.c @@ -23,9 +23,9 @@ static const int lissajous_freqs[][2] = result_t lissajous_create(wuss_t *wuss, lissajous_task_t *task) { + result_t rc; wuss_task_t *delegate; wuss_task_desc_t delegate_desc; - result_t rc; task->bg = colour_rgb(0x00, 0x00, 0x00); task->fg = colour_rgb(0x00, 0xFF, 0x00); @@ -50,7 +50,7 @@ result_t lissajous_create(wuss_t *wuss, lissajous_task_t *task) rc = wuss_window_create_placed(delegate, SIZE2D(220, 220), "Lissajous", - wuss_WINDOW_NONE, + wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), SIZE2D(220, 220), SIZE2D(0, 0), @@ -121,7 +121,7 @@ static result_t lissajous_mouse(wuss_window_t *window, lc->freq_index = (lc->freq_index + 1) % (int) NELEMS(lissajous_freqs); lc->a = lissajous_freqs[lc->freq_index][0]; lc->b = lissajous_freqs[lc->freq_index][1]; - wuss_window_invalidate_all(lc->window); /* whole figure changes */ + wuss_window_invalidate_visible(lc->window); /* whole figure changes */ } else if (button & wuss_BUTTON_ADJUST) { @@ -143,7 +143,7 @@ static result_t lissajous_idle(void *task_data) else if (lc->phase < 0.0) lc->phase += 2.0 * M_PI; - wuss_window_invalidate_all(lc->window); /* ponytail: repaint whole content; figure fills it and is cheap */ + wuss_window_invalidate_visible(lc->window); /* ponytail: repaint whole content; figure fills it and is cheap */ return result_OK; } diff --git a/libraries/wuss/test/tasks/minesweeper.c b/libraries/wuss/test/tasks/minesweeper.c index 87f66b5..ceeeea2 100644 --- a/libraries/wuss/test/tasks/minesweeper.c +++ b/libraries/wuss/test/tasks/minesweeper.c @@ -180,9 +180,9 @@ result_t minesweeper_create(wuss_t *wuss, bmfont_t *font, minesweeper_task_t *task) { + result_t rc; wuss_task_t *delegate; wuss_task_desc_t delegate_desc; - result_t rc; task->wuss = wuss; task->font = font; @@ -203,7 +203,7 @@ result_t minesweeper_create(wuss_t *wuss, rc = wuss_window_create_placed(delegate, SIZE2D(MS_WIDTH, MS_HEIGHT), "Minesweeper", - wuss_WINDOW_NONE, + wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), SIZE2D(MS_WIDTH, MS_HEIGHT), SIZE2D(0, 0), @@ -397,7 +397,7 @@ static result_t minesweeper_mouse(minesweeper_task_t *ms, if (button & (wuss_BUTTON_SELECT | wuss_BUTTON_ADJUST)) { minesweeper_reset(ms); - wuss_window_invalidate_all(ms->window); + wuss_window_invalidate_visible(ms->window); } return result_OK; } @@ -442,7 +442,7 @@ static result_t minesweeper_mouse(minesweeper_task_t *ms, return result_OK; } - wuss_window_invalidate_all(ms->window); + wuss_window_invalidate_visible(ms->window); return result_OK; } @@ -487,7 +487,7 @@ result_t minesweeper_handle(wuss_window_t *window, case wuss_EVENT_MENU_SELECT: /* only item is "New Game" */ minesweeper_reset(ms); - wuss_window_invalidate_all(ms->window); + wuss_window_invalidate_visible(ms->window); return result_OK; case wuss_EVENT_QUIT: diff --git a/libraries/wuss/test/tasks/palette.c b/libraries/wuss/test/tasks/palette.c index e90b196..105dc64 100644 --- a/libraries/wuss/test/tasks/palette.c +++ b/libraries/wuss/test/tasks/palette.c @@ -13,14 +13,13 @@ #include "base/utils.h" #include "framebuf/palettes.h" #include "geom/box.h" -#include "io/dirscan.h" +#include "io/namelist.h" #include "io/path.h" #include "wuss/menu.h" #include "palette.h" #define PALETTE_HEX_EXT ".hex" -#define PALETTE_HEX_EXT_LEN 4 #define PALETTE_NCOLOURS 16 /* one colour_t[] row per *.hex file; the * system palette wuss_create was given is * fixed at this length */ @@ -29,42 +28,6 @@ * file and the dashed rule above it */ #define PALETTE_MENU_INVERT_INDEX(pc) ((pc)->nnames) -/* ----------------------------------------------------------------------- */ -/* Scan resources/palettes for *.hex files, collecting each leafname (ext - * stripped) into task->names, sorted, capped at PALETTE_MAX_FILES. */ - -static int name_cmp(const void *a, const void *b) -{ - return strcmp(a, b); -} - -static result_t add_name(const char *leaf, void *opaque) -{ - palette_task_t *pc; - size_t leaflen; - size_t namelen; - - pc = opaque; - leaflen = strlen(leaf); - - if (pc->nnames >= PALETTE_MAX_FILES) - return result_STOP_WALK; - if (leaflen <= PALETTE_HEX_EXT_LEN) - return result_OK; - if (strcmp(leaf + leaflen - PALETTE_HEX_EXT_LEN, PALETTE_HEX_EXT) != 0) - return result_OK; - - namelen = leaflen - PALETTE_HEX_EXT_LEN; - if (namelen >= sizeof(pc->names[0])) - return result_OK; - - memcpy(pc->names[pc->nnames], leaf, namelen); - pc->names[pc->nnames][namelen] = '\0'; - pc->nnames++; - - return result_OK; -} - /* ----------------------------------------------------------------------- */ /* Parse a *.hex file: one "rrggbb" line per colour, no leading '#'. Fails * (leaving *out untouched) unless exactly PALETTE_NCOLOURS well-formed lines @@ -113,11 +76,11 @@ result_t palette_create(wuss_t *wuss, const char *startup_name, palette_task_t *task) { + result_t rc; wuss_task_desc_t delegate_desc; const char *dir; char dirbuf[DPTLIB_MAXPATH]; int i; - result_t rc; task->wuss = wuss; task->resources = resources; @@ -127,10 +90,9 @@ result_t palette_create(wuss_t *wuss, dir = path_join_filename(resources, 2, "resources", "palettes"); strcpy(dirbuf, dir); /* path_join_filename's buffer is reused by the scan */ - dirscan_walk(dirbuf, add_name, task); - if (task->nnames > 1) - qsort(task->names, (size_t) task->nnames, sizeof(task->names[0]), - name_cmp); + namelist_scan(dirbuf, PALETTE_HEX_EXT, task->names[0], + sizeof(task->names[0]), PALETTE_MAX_FILES, 1 /* sorted */, + &task->nnames); if (startup_name != NULL) for (i = 0; i < task->nnames; i++) @@ -267,10 +229,10 @@ static result_t palette_click(palette_task_t *pc, const wuss_event_t *event) static result_t palette_menu_select(palette_task_t *pc, const wuss_event_t *event) { + result_t rc; int index; int old; colour_t loaded[PALETTE_NCOLOURS]; - result_t rc; int i; if (event->data.menu_select.menu != &pc->menu) @@ -325,8 +287,12 @@ result_t palette_handle(wuss_window_t *window, const wuss_event_t *event, void *task_data) { + palette_task_t *pc; + NOT_USED(window); + pc = task_data; + switch (event->kind) { case wuss_EVENT_REDRAW: @@ -338,6 +304,10 @@ result_t palette_handle(wuss_window_t *window, case wuss_EVENT_MENU_SELECT: return palette_menu_select(task_data, event); + case wuss_EVENT_MENU_CLOSED: + pc->menu_handle = NULL; /* wuss closed the chain under us */ + return result_OK; + case wuss_EVENT_QUIT: free(task_data); /* calloc'd per instance by the spawner */ return result_OK; diff --git a/libraries/wuss/test/tasks/porter-duff.c b/libraries/wuss/test/tasks/porter-duff.c index 6d97058..24fadba 100644 --- a/libraries/wuss/test/tasks/porter-duff.c +++ b/libraries/wuss/test/tasks/porter-duff.c @@ -133,9 +133,9 @@ static result_t load_demo_png(bitmap_t *bm, const char *resources, const char *leafname) { + result_t rc; const char *leafname_ext; const char *filename; - result_t rc; leafname_ext = path_join_leafname(leafname, "png"); filename = path_join_filename(resources, 3, @@ -163,9 +163,9 @@ result_t porter_duff_create(wuss_t *wuss, const char *resources, porter_duff_task_t *task) { + result_t rc; wuss_task_t *delegate; wuss_task_desc_t delegate_desc; - result_t rc; task->font = font; task->rule = composite_RULE_CLEAR; @@ -204,7 +204,7 @@ result_t porter_duff_create(wuss_t *wuss, rc = wuss_window_create_placed(delegate, SIZE2D(PD_SIZE, PD_SIZE + PD_LABEL_HEIGHT), "Porter-Duff", - wuss_WINDOW_NONE, + wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), SIZE2D(PD_SIZE, PD_SIZE + PD_LABEL_HEIGHT), SIZE2D(0, 0), @@ -293,12 +293,12 @@ static void porter_duff_draw_checkerboard(const porter_duff_task_t *pd, static result_t porter_duff_redraw(const wuss_event_t *event, void *task_data) { + result_t rc; porter_duff_task_t *pd; screen_t *scr; const box_t *content, *bounds; const char *name; point_t pos; - result_t rc; pd = task_data; @@ -345,7 +345,7 @@ static result_t porter_duff_idle(void *task_data) } /* the ramp changes every frame, so the whole pane is stale every frame */ - wuss_window_invalidate_all(pd->window); + wuss_window_invalidate_visible(pd->window); return result_OK; } @@ -359,7 +359,7 @@ static result_t porter_duff_mouse(wuss_window_t *window, void *task_data) pd->rule = (pd->rule + 1) % composite_RULE__LIMIT; pd->frame = 0; - wuss_window_invalidate_all(window); + wuss_window_invalidate_visible(window); return result_OK; } @@ -377,7 +377,7 @@ static result_t porter_duff_scroll(wuss_window_t *window, PD_FRAMES_MIN, PD_FRAMES_MAX); pd->frame = MIN(pd->frame, pd->frames_per_rule); - wuss_window_invalidate_all(window); + wuss_window_invalidate_visible(window); return result_OK; } diff --git a/libraries/wuss/test/tasks/saturn.c b/libraries/wuss/test/tasks/saturn.c new file mode 100644 index 0000000..c28c4ea --- /dev/null +++ b/libraries/wuss/test/tasks/saturn.c @@ -0,0 +1,245 @@ +/* wuss/test/tasks/saturn.c -- Elite loading-screen planet, recreated */ + +#ifdef WUSS_APP + +#include +#include + +#ifdef FORTIFY +#include "fortify/fortify.h" +#endif + +#include "base/utils.h" +#include "framebuf/palettes.h" +#include "geom/box.h" +#include "utils/rng.h" + +#include "saturn.h" + +/* A recreation of the ringed planet from the loading screen of Elite (Ian + * Bell and David Braben, Acornsoft, 1984), the stippled Saturn-like world + * the BBC Micro drew while the game loaded from tape. + * + * The source is one line of BBC BASIC (MODE 4, VDU5) that seeds the RNG from + * TIME then runs three FOR loops, each rejection-sampling random points and + * PLOT 69 (plot single point) at the survivors. In MODE 4 the plot area is + * 1280x1024 OS units, so the original scales its -128..127 sample space by 4 + * and plots each point at y = (255 - (v + 128)). RISC OS screen coords run + * 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. + * + * loop 1: X%,Y% in -127..127; keep if (X*X+Y*Y)/256 > 17 -> the ring + * 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 + * loop 3: R1,R2 random; keep if R1*R1+R2*R2 < 16384; + * x = sqrt(16384-P)/2 -> planet body + */ + +#define SATURN_SIZE 256 /* window is SATURN_SIZE x SATURN_SIZE */ + +/* 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_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 */ + +/* 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 + * Select click can re-seed for a fresh sketch. */ +static rng_t saturn_rnd_state; + +static void saturn_rnd_seed(unsigned long seed) +{ + rng_seed(&saturn_rnd_state, (uint32_t) seed); +} + +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) + +result_t saturn_create(wuss_t *wuss, saturn_task_t *task) +{ + result_t rc; + wuss_task_t *delegate; + wuss_task_desc_t delegate_desc; + + task->bg = colour_rgb(0x00, 0x00, 0x00); + task->fg = colour_rgb(0xFF, 0xFF, 0xFF); + task->seed = 1; + + /* saturn_redraw paints its own background */ + delegate_desc.handle = saturn_handle; + delegate_desc.task_data = task; + delegate_desc.name = "saturn"; + 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; + } + wuss_task_set_autoclose(delegate, 1); + + rc = wuss_window_create_placed(delegate, + SIZE2D(SATURN_SIZE, SATURN_SIZE), + "Saturn", + wuss_WINDOW_DEFAULT, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + SIZE2D(SATURN_SIZE, SATURN_SIZE), + SIZE2D(0, 0), + &task->window); + if (rc != result_OK) + wuss_task_destroy(delegate); /* unregister; its QUIT frees the task block */ + + return rc; +} + +/* 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). */ +static void saturn_plot(screen_t *scr, + int ox, + int oy, + int x, + int y, + colour_t c) +{ + if (x < 0 || x >= SATURN_SIZE || y < 0 || y >= SATURN_SIZE) + return; + + screen_set_pixel(scr, ox + x, oy + y, c); +} + +static result_t saturn_redraw(const wuss_event_t *event, saturn_task_t *task) +{ + screen_t *scr; + const box_t *content, *bounds; + int ox, oy; + int i; + int x, y, p; + + scr = event->data.redraw.scr; + content = event->data.redraw.content; + bounds = event->data.redraw.bounds; + + 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 */ + ox = bounds->x0 - event->data.redraw.scroll.x; + oy = bounds->y0 - event->data.redraw.scroll.y; + + saturn_rnd_seed(task->seed); + + /* loop 1 - the ring: keep points outside the inner disc */ + for (i = 0; i <= SATURN_RING_ITERS; i++) + { + x = SATURN_SAMPLE(); + y = SATURN_SAMPLE(); + p = (x * x + y * y) / SATURN_ENERGY_SHIFT; + if (p > 17) + saturn_plot(scr, ox, oy, x + SATURN_HALF, SATURN_FLIP - (y + SATURN_HALF), + task->fg); + } + + /* loop 2 - ring shadow band: sheared sample with a banded energy gate */ + for (i = 0; i <= SATURN_BAND_ITERS; i++) + { + int r5, r6, r7, e; + + r5 = SATURN_SAMPLE(); + r6 = SATURN_SAMPLE(); + 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; + if (e >= 32 && e < 80 && (r5 < 0 || p > 16)) + saturn_plot(scr, ox, oy, x + SATURN_HALF, y + SATURN_HALF, task->fg); + } + + /* loop 3 - planet body: filled half-disc offset right */ + for (i = 0; i <= SATURN_BODY_ITERS; i++) + { + int r1, r2; + + r1 = SATURN_SAMPLE(); + r2 = SATURN_SAMPLE(); + p = r1 * r1 + r2 * r2; + if (p < SATURN_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); + } + } + + 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, + wuss_window_t *window) +{ + if (action != wuss_MOUSE_DOWN) + return result_OK; + + if (button & wuss_BUTTON_SELECT) + { + task->seed += 0x9E3779B9UL; /* fresh sketch */ + wuss_window_invalidate_visible(window); + } + + return result_OK; +} + +result_t saturn_handle(wuss_window_t *window, + const wuss_event_t *event, + void *task_data) +{ + saturn_task_t *task; + + task = task_data; + + switch (event->kind) + { + case wuss_EVENT_REDRAW: + 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_IDLE: + return saturn_idle(task); + + case wuss_EVENT_QUIT: + free(task); /* task_data was calloc'd per instance by the spawner */ + return result_OK; + + default: + return result_OK; + } +} + +#endif /* WUSS_APP */ diff --git a/libraries/wuss/test/tasks/saturn.h b/libraries/wuss/test/tasks/saturn.h new file mode 100644 index 0000000..d9a6630 --- /dev/null +++ b/libraries/wuss/test/tasks/saturn.h @@ -0,0 +1,34 @@ +/* wuss/test/tasks/saturn.h -- Elite loading-screen planet, recreated */ + +#ifndef TASKS_SATURN_H +#define TASKS_SATURN_H + +#ifdef WUSS_APP + +#include "framebuf/colour.h" +#include "wuss/window.h" + +/* window task: recreates the ringed planet from the loading screen of + * Elite (Ian Bell and David Braben, Acornsoft, 1984). A cryptic BBC BASIC + * one-liner stipples it out of three rejection-sampled point clouds - a + * ring of dots, a sheared streak across it and a filled disc for the + * 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. */ +typedef struct saturn_task +{ + wuss_window_t *window; + colour_t bg, fg; + unsigned long seed; /* RNG state; a Select click bumps it */ +} +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); + +#endif /* WUSS_APP */ + +#endif /* TASKS_SATURN_H */ diff --git a/libraries/wuss/test/tasks/sofa.c b/libraries/wuss/test/tasks/sofa.c index b827ca2..fce7065 100644 --- a/libraries/wuss/test/tasks/sofa.c +++ b/libraries/wuss/test/tasks/sofa.c @@ -356,9 +356,9 @@ static void draw_vertex_dots(screen_t *scr, result_t sofa_create(wuss_t*wuss, sofa_task_t*task) { + result_t rc; wuss_task_t *delegate; wuss_task_desc_t delegate_desc; - result_t rc; task->bg = colour_rgb(0x7E, 0x25, 0x53); task->line = colour_rgb(0xFF, 0xA3, 0x00); @@ -384,7 +384,7 @@ result_t sofa_create(wuss_t*wuss, sofa_task_t*task) rc = wuss_window_create_placed(delegate, SIZE2D(180, 160), "Sofa", - wuss_WINDOW_NONE, + wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), SIZE2D(180, 160), SIZE2D(0, 0), @@ -521,7 +521,7 @@ static result_t sofa_mouse(wuss_window_t *window, { sc->shape = (sc->shape + 1) % sofa_SHAPE__LIMIT; sc->turns = 0; - wuss_window_invalidate_all(window); + wuss_window_invalidate_visible(window); } else if (button & wuss_BUTTON_SELECT) { @@ -542,7 +542,7 @@ static result_t sofa_scroll(wuss_window_t *window, sc->zoom += delta * SOFA_ZOOM_PER_NOTCH; sc->zoom = CLAMP(sc->zoom, SOFA_ZOOM_MIN, SOFA_ZOOM_MAX); - wuss_window_invalidate_all(window); + wuss_window_invalidate_visible(window); return result_OK; } @@ -567,7 +567,7 @@ static result_t sofa_idle(void *task_data) } } - wuss_window_invalidate_all(task->window); + wuss_window_invalidate_visible(task->window); return result_OK; } diff --git a/libraries/wuss/test/tasks/swatches.c b/libraries/wuss/test/tasks/swatches.c index cefd1b1..deb23d9 100644 --- a/libraries/wuss/test/tasks/swatches.c +++ b/libraries/wuss/test/tasks/swatches.c @@ -72,9 +72,9 @@ static result_t swatches_redraw(swatches_task_t *task, result_t swatches_create(wuss_t *wuss, swatches_task_t *task) { + result_t rc; wuss_task_t *delegate; wuss_task_desc_t delegate_desc; - result_t rc; task->wuss = wuss; task->window = NULL; @@ -104,7 +104,7 @@ result_t swatches_create(wuss_t *wuss, swatches_task_t *task) SIZE2D(SWATCHES_DOC_W, 140), "Swatches", wuss_WINDOW_NO_RESIZE_BLIT, /* grid spans the whole window; a resize redraws all of it */ - wuss_BACKDROP_COLOUR(wuss_nearest_colour(wuss, 0xDD, 0xDD, 0xDD)), + wuss_BACKDROP_COLOUR(wuss_COLOUR_WINDOW), SIZE2D(SWATCHES_DOC_W, SWATCHES_DOC_H), SIZE2D(0, 0), &task->window); @@ -174,7 +174,7 @@ static result_t swatches_menu_select(swatches_task_t *task, task->paper = picked; /* every cell shares the new paper: dirty the whole document, not just the - * visible rectangle wuss_window_invalidate_all covers */ + * visible rectangle wuss_window_invalidate_visible covers */ wuss_window_invalidate_extent(task->window); return result_OK; } diff --git a/libraries/wuss/test/tasks/text.c b/libraries/wuss/test/tasks/text.c index 695c2ba..f2c70d9 100644 --- a/libraries/wuss/test/tasks/text.c +++ b/libraries/wuss/test/tasks/text.c @@ -78,10 +78,10 @@ static bmfont_t *text_load_font(text_task_t *task, int idx, const char *name) { + result_t rc; const char *leaf; const char *filename; bmfont_t *font; - result_t rc; if (task->fonts[idx] != NULL) return task->fonts[idx]; @@ -112,7 +112,7 @@ static result_t text_set_font(text_task_t *task, int idx, const char *name) task->font = font; task->current = idx; - wuss_window_invalidate_all(task->window); + wuss_window_invalidate_visible(task->window); return result_OK; } @@ -125,7 +125,7 @@ static result_t text_set_sample(text_task_t *task, int idx) task->sample = idx; task->text = text_samples[idx].text; - wuss_window_invalidate_all(task->window); + wuss_window_invalidate_visible(task->window); return result_OK; } @@ -142,12 +142,12 @@ result_t text_create(wuss_t *wuss, const char *resources, text_task_t *task) { + result_t rc; wuss_task_t *delegate; wuss_task_desc_t delegate_desc; const char *bmfonts_dir; const wuss_menu_t *menu; size2d_t sz; - result_t rc; task->wuss = wuss; task->font = wuss_get_font(wuss); @@ -220,7 +220,7 @@ result_t text_create(wuss_t *wuss, 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_BACKDROP_COLOUR(wuss_nearest_colour(wuss, 0xFF, 0xFF, 0xFF)), + wuss_BACKDROP_COLOUR(wuss_COLOUR_WINDOW), sz, SIZE2D(0, 0), &task->window); diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index c56d158..4667779 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -32,9 +32,13 @@ #endif /* white-box: the menu-flash test drives picks through the icon layer and - * reads back struct wuss__menu / struct wuss_icon state directly */ -#if defined(WUSS_MENUS) && defined(WUSS_ICONS) + * reads back struct wuss__menu / struct wuss_icon state directly; the + * furniture hit-test sweep calls wuss__furniture_hit_test and the box + * helpers from impl.h / furniture.h directly */ +#if defined(WUSS_FURNITURE) && defined(WUSS_ICONS) #include "../core/impl.h" +#endif +#if defined(WUSS_MENUS) && defined(WUSS_ICONS) #include "../icon.h" #endif @@ -273,6 +277,14 @@ static void reap_test_tasks(void) mk_task_count = 0; } +/* Drop the registry without destroying anything -- for a block that hands + * its tasks straight to wuss_destroy and wants that teardown path exercised + * rather than wuss_task_destroy's. */ +static void forget_test_tasks(void) +{ + mk_task_count = 0; +} + /* ----------------------------------------------------------------------- */ #if defined(WUSS_MENUS) && defined(WUSS_ICONS) @@ -302,14 +314,44 @@ static void flash_pick_row(wuss_t *wuss, wuss_mouse_click(wuss, at, button, wuss_MOUSE_UP, NULL); } +/* Move the pointer over row `row` of menu level `level`: to the text column + * (`over_arrow` == 0) or into the submenu-arrow gutter at the row's right edge + * (`over_arrow` == 1). Used to drive submenu open/close from the tests. */ +static void menu_move_over_row(wuss_t *wuss, + struct wuss__menu *level, + int row, + int over_arrow) +{ + box_t content; + box_t bbox; + box_t screen_box; + point_t scroll; + point_t at; + + wuss_window_get_content_bounds(level->window, &content); + wuss_window_get_scroll(level->window, &scroll); + wuss_icon_get_bbox(level->icons[row], &bbox); + wuss__icon_box_to_screen(&content, scroll, &bbox, &screen_box); + + at.x = over_arrow ? screen_box.x1 - 4 + : screen_box.x0 + 4; + at.y = (screen_box.y0 + screen_box.y1) / 2; + + wuss_mouse_move(wuss, at, NULL); +} + /* A task that opens its own menu on a MENU press over its window, the way a * real content task (e.g. greeble) does. task_data points to a menu_task_t * so the test can see the press arrived and which menu it raised. */ typedef struct menu_task { - wuss_task_t *self; + wuss_task_t *self; const wuss_menu_t *menu; - int menu_press_count; + int menu_press_count; + wuss_menu_handle_t menu_handle; /* last chain this task opened */ + int menu_closed_count; /* wuss_EVENT_MENU_CLOSED deliveries */ + int close_chain_on_quit; /* mimic image.c: close a still-open + * chain from the QUIT handler */ } menu_task_t; @@ -329,9 +371,18 @@ static result_t menu_open_handle(wuss_window_t *window, { mt->menu_press_count++; return wuss_menu_open(mt->self, mt->menu, - event->data.mouse.point, NULL); + event->data.mouse.point, &mt->menu_handle); + } + + if (event->kind == wuss_EVENT_MENU_CLOSED) + { + mt->menu_closed_count++; + mt->menu_handle = NULL; /* chain freed under us; handle now stale */ } + if (event->kind == wuss_EVENT_QUIT && mt->close_chain_on_quit) + wuss_menu_close(mt->menu_handle); /* against the menu task, still live */ + return result_OK; } #endif @@ -372,6 +423,57 @@ static int dirty_union_area(wuss_t *wuss, const box_t *bounds) #if defined(WUSS_FURNITURE) && defined(WUSS_ICONS) +/* Sweep every pixel of a window's visible box through wuss__furniture_hit_test + * and check the tiling invariant: no pixel is wuss_FURNITURE_NONE, and a pixel + * reports wuss_FURNITURE_CONTENT if and only if it lies inside the drawn + * content box -- with one documented exception, the bare outline band of a + * fully chromeless window (no titlebar, no scrollbars, no resize), where + * content_may_leak is passed non-zero to allow CONTENT on 1px-frame pixels + * outside the content box too. Returns 1 on pass, 0 on the first breach + * (printing the offending pixel). */ +static int furniture_hit_sweep(const wuss_window_t *window, + int content_may_leak) +{ + box_t visible, content; + int x, y, inside; + + wuss_window_get_visible_bounds((wuss_window_t *) window, &visible); + wuss__content_box(window, &content); + + for (y = visible.y0; y < visible.y1; y++) + { + for (x = visible.x0; x < visible.x1; x++) + { + wuss_furniture_region_t region; + + region = wuss__furniture_hit_test(window, POINT(x, y)); + inside = box_contains_point(&content, x, y); + + if (region == wuss_FURNITURE_NONE) + { + printf("wuss_test: hit sweep: (%d,%d) is FURNITURE_NONE\n", x, y); + return 0; + } + + if (inside && region != wuss_FURNITURE_CONTENT) + { + printf("wuss_test: hit sweep: content pixel (%d,%d) reported %d\n", + x, y, (int) region); + return 0; + } + + if (!inside && region == wuss_FURNITURE_CONTENT && !content_may_leak) + { + printf("wuss_test: hit sweep: chrome pixel (%d,%d) reported CONTENT\n", + x, y); + return 0; + } + } + } + + return 1; +} + result_t wuss_test(const char *resources) { result_t rc; @@ -412,6 +514,7 @@ result_t wuss_test(const char *resources) bad_config.titlebar_height = 0; bad_config.furniture.title.bg = 100; /* in range as a byte, but well past any test palette and below wuss_COLOUR_SYMBOLIC */ bad_config.furniture.title.fg = 0; + bad_config.furniture.outline = 0; bad_config.furniture.back = 0; bad_config.furniture.close = 0; bad_config.furniture.toggle = 0; @@ -419,6 +522,13 @@ result_t wuss_test(const char *resources) bad_config.furniture.scroll.arrows = 0; bad_config.furniture.scroll.wells = 0; bad_config.furniture.scroll.sausages = 0; + bad_config.bevel.light = 0; + bad_config.bevel.dark = 0; + bad_config.bevel.divider = 0; + bad_config.button.bg = 0; + 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); if (rc != result_WUSS_BAD_COLOUR) goto Failure; @@ -461,6 +571,8 @@ result_t wuss_test(const char *resources) symcfg.furniture.title.bg = wuss_COLOUR_BLUE; /* -> index 2 */ symcfg.furniture.title.fg = wuss_COLOUR_WHITE; /* -> index 3 */ symcfg.backdrop = wuss_BACKDROP_COLOUR(wuss_COLOUR_GREEN); /* -> 1 */ + 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); if (rc != result_OK) @@ -478,7 +590,9 @@ result_t wuss_test(const char *resources) wuss__resolve_colour(symw, wuss_NO_BACKGROUND) != wuss_NO_BACKGROUND || wuss__resolve_colour(symw, wuss_COLOUR_TITLE_BG) != 2 || wuss__resolve_colour(symw, wuss_COLOUR_TITLE_FG) != 3 || - wuss__resolve_colour(symw, wuss_COLOUR_BACKDROP) != 1) + wuss__resolve_colour(symw, wuss_COLOUR_BACKDROP) != 1 || + wuss__resolve_colour(symw, wuss_COLOUR_WINDOW) != 0 || + wuss__resolve_colour(symw, wuss_COLOUR_MENU) != 3) { wuss_destroy(symw); goto Failure; @@ -489,7 +603,7 @@ result_t wuss_test(const char *resources) symdel = mk_task(symw, NULL, NULL); if (symdel == NULL) { wuss_destroy(symw); goto Failure; } symbox.x0 = 0; symbox.y0 = 0; symbox.x1 = 80; symbox.y1 = 80; - rc = wuss_window_create(symdel, &symbox, "sym", wuss_WINDOW_NONE, + rc = wuss_window_create(symdel, &symbox, "sym", wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_COLOUR_RED), SIZE2D(80, 80), SIZE2D(80, 80), &symwin); if (rc != result_OK) { wuss_destroy(symw); goto Failure; } @@ -512,7 +626,7 @@ result_t wuss_test(const char *resources) rc = wuss_window_create(mk_task(wuss, NULL, NULL), &box_a, "toosmall", - wuss_WINDOW_NONE, + wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), box_size(&box_a), SIZE2D(0, 0), @@ -1239,7 +1353,7 @@ result_t wuss_test(const char *resources) rc = wuss_window_create(delegate_h, &box_h, "H", - wuss_WINDOW_NONE, + wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), box_size(&box_h), SIZE2D(0, 0), @@ -1376,7 +1490,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_NONE, + rc = wuss_window_create(delegate_m, &box_m, "M", wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), SIZE2D(200, 200), SIZE2D(80, 60), &win_m); @@ -1426,7 +1540,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_NONE, + rc = wuss_window_create(delegate_t, &box_t_win, "T", wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), SIZE2D(150, 150), SIZE2D(0, 0), &win_t); if (rc != result_OK) @@ -1593,7 +1707,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_NONE, + rc = wuss_window_create(delegate_r, &box_r, "R", wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), SIZE2D(70, 70), SIZE2D(0, 0), &win_r); if (rc != result_OK) @@ -1698,7 +1812,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_NONE, + rc = wuss_window_create(delegate_d, &box_d, "D", wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), SIZE2D(80, 140), SIZE2D(0, 0), &win_d); if (rc != result_OK) @@ -1803,7 +1917,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_NONE, + rc = wuss_window_create(delegate_d, &box_d, "D", wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), SIZE2D(100, 140), SIZE2D(0, 0), &win_d); if (rc != result_OK) @@ -1815,7 +1929,7 @@ result_t wuss_test(const char *resources) /* occluder covering the right half of D's content box and beyond */ 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_NONE, + rc = wuss_window_create(delegate_o, &box_o, "O", wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), SIZE2D(box_o.x1 - box_o.x0, box_o.y1 - box_o.y0), SIZE2D(0, 0), &win_o); @@ -1915,7 +2029,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_NONE, + rc = wuss_window_create(delegate_s, &box_s, "S", wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), SIZE2D(100, 200), SIZE2D(0, 0), &win_s); if (rc != result_OK) @@ -1926,7 +2040,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_NONE, + rc = wuss_window_create(delegate_o, &box_o, "O", wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), SIZE2D(box_o.x1 - box_o.x0, box_o.y1 - box_o.y0), SIZE2D(0, 0), &win_o); @@ -2018,7 +2132,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_NONE, + rc = wuss_window_create(delegate_m, &box_m, "M", wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), SIZE2D(100, 300), SIZE2D(0, 0), &win_m); if (rc != result_OK) @@ -2033,7 +2147,7 @@ result_t wuss_test(const char *resources) * repaint region touches O, so without the fix O stays overpainted. */ 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_NONE, + rc = wuss_window_create(delegate_o, &box_o, "O", wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), SIZE2D(box_o.x1 - box_o.x0, box_o.y1 - box_o.y0), SIZE2D(0, 0), &win_o); @@ -2099,7 +2213,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_NONE, + rc = wuss_window_create(delegate_m, &box_m, "M", wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), SIZE2D(100, 300), SIZE2D(0, 0), &win_m); if (rc != result_OK) @@ -2110,7 +2224,7 @@ result_t wuss_test(const char *resources) /* O strictly inside M's content, gap on every side */ 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_NONE, + rc = wuss_window_create(delegate_o, &box_o, "O", wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), SIZE2D(box_o.x1 - box_o.x0, box_o.y1 - box_o.y0), SIZE2D(0, 0), &win_o); @@ -2250,7 +2364,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_NONE, + rc = wuss_window_create(mk_task(wuss, NULL, NULL), &box_u, "U", wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), 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) @@ -2360,7 +2474,7 @@ result_t wuss_test(const char *resources) * unrelated to toggle-size specifically). * Doc big enough that maximize is * screen-limited, not doc-limited. */ - rc = wuss_window_create(delegate_v, &box_v, "V", wuss_WINDOW_NONE, + rc = wuss_window_create(delegate_v, &box_v, "V", wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), SIZE2D(200, 200), SIZE2D(0, 0), &win_v); if (rc != result_OK) @@ -2475,7 +2589,7 @@ result_t wuss_test(const char *resources) * have a real interior the blit can keep; * doc big enough that maximize is * screen-limited so the window must move */ - rc = wuss_window_create(delegate_p, &box_p, "P", wuss_WINDOW_NONE, + rc = wuss_window_create(delegate_p, &box_p, "P", wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), SIZE2D(400, 400), SIZE2D(0, 0), &win_p); if (rc != result_OK) @@ -2612,7 +2726,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_NONE, + rc = wuss_window_create(delegate_z, &box_z, "Z", wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), axis == 0 ? SIZE2D(0, 0) : SIZE2D(60, 0), SIZE2D(0, 0), &win_z); @@ -2916,6 +3030,69 @@ result_t wuss_test(const char *resources) wuss_window_close(win_nb2); } + printf("test: resizing a wuss_WINDOW_NO_RESIZE_BLIT window drops its cached " + "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; + + tc_fl.redraw_count = 0; + tc_fl.mouse_count = 0; + delegate_fl = mk_task(wuss, test_handle, &tc_fl); + if (delegate_fl == NULL) goto Failure; + + 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), + box_size(&box_fl), + SIZE2D(0, 0), + &win_fl); + if (rc != result_OK) + goto Failure; + + /* build and cache the furniture layout at the creation width */ + wuss__furniture_layout_build(win_fl); + if (!win_fl->furniture_layout.valid) + goto Failure; + old_titlebar_x1 = win_fl->furniture_layout.titlebar.x1; + + rc = wuss_window_resize(win_fl, SIZE2D(140, 60)); /* grow the width */ + if (rc != result_OK) + goto Failure; + + /* the resize must have invalidated the cache: nothing else here rebuilds + * it, so a stale valid==1 means furniture would paint at the old width */ + if (win_fl->furniture_layout.valid) + goto Failure; + + /* rebuilding now must track the new, wider window */ + wuss__furniture_layout_build(win_fl); + wuss_window_get_visible_bounds(win_fl, &after_fl); + if (win_fl->furniture_layout.titlebar.x1 <= old_titlebar_x1) + goto Failure; /* titlebar still at the old width */ + + /* the full-width divider rule must span to the new right edge too */ + found = 0; + for (i = 0; i < win_fl->furniture_layout.npieces; i++) + { + const wuss__furniture_piece_t *p = &win_fl->furniture_layout.pieces[i]; + + if (p->paint == wuss__FURNITURE_PAINT_OUTLINE && + p->rect.y1 == win_fl->furniture_layout.titlebar.y1 && + p->rect.x1 == win_fl->furniture_layout.titlebar.x1) + found = 1; + } + if (!found) + goto Failure; + + wuss_window_close(win_fl); + } + printf("test: dragging a clear window onto an occluder leaves the occluder untouched\n"); { @@ -3455,7 +3632,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_NONE, + rc = wuss_window_create(delegate_s, &box_s, "S", wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), SIZE2D(200, 200), SIZE2D(0, 0), &win_s); if (rc != result_OK) @@ -3530,7 +3707,7 @@ result_t wuss_test(const char *resources) rc = wuss_window_create(delegate_r, &box_r, "rules", - wuss_WINDOW_NONE, /* all furniture present */ + wuss_WINDOW_DEFAULT, /* all furniture present */ wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), SIZE2D(400, 400), SIZE2D(0, 0), @@ -3574,7 +3751,7 @@ result_t wuss_test(const char *resources) rc = wuss_window_create_placed(delegate_p, SIZE2D(40, 30), "P", - wuss_WINDOW_NONE, + wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), SIZE2D(40, 30), SIZE2D(0, 0), @@ -3597,7 +3774,7 @@ result_t wuss_test(const char *resources) rc = wuss_window_create_placed(delegate_p, SIZE2D(40, 30), "P", - wuss_WINDOW_NONE, + wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), SIZE2D(40, 30), SIZE2D(0, 0), @@ -3680,7 +3857,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_NONE, + rc = wuss_window_create(delegate_ps, &box_ps, "PS", wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), box_size(&box_ps), SIZE2D(0, 0), &win_ps); if (rc != result_OK) @@ -3744,12 +3921,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_NONE, + rc = wuss_window_create(delegate_q, &box_q, "Q1", wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), 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_NONE, + rc = wuss_window_create(delegate_q, &box_q, "Q2", wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), box_size(&box_q), SIZE2D(0, 0), &win_q2); if (rc != result_OK) @@ -3779,12 +3956,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_NONE, + rc = wuss_window_create(delegate_ac, &box_ac, "AC1", wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), 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_NONE, + rc = wuss_window_create(delegate_ac, &box_ac, "AC2", wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), box_size(&box_ac), SIZE2D(0, 0), &win_ac2); if (rc != result_OK) @@ -3835,7 +4012,7 @@ result_t wuss_test(const char *resources) delegate_w1 = mk_task(wuss, test_handle, &tc_w1); if (delegate_w1 == NULL) goto Failure; wuss_task_set_autoclose(delegate_w1, 1); - rc = wuss_window_create(delegate_w1, &box_w, "W1", wuss_WINDOW_NONE, + rc = wuss_window_create(delegate_w1, &box_w, "W1", wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), box_size(&box_w), SIZE2D(0, 0), &win_w1); if (rc != result_OK) @@ -3844,7 +4021,7 @@ result_t wuss_test(const char *resources) delegate_w2 = mk_task(wuss, close_on_idle_handle, &tc_w2); if (delegate_w2 == NULL) goto Failure; wuss_task_set_autoclose(delegate_w2, 1); - rc = wuss_window_create(delegate_w2, &box_w, "W2", wuss_WINDOW_NONE, + rc = wuss_window_create(delegate_w2, &box_w, "W2", wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), box_size(&box_w), SIZE2D(0, 0), &g_close_on_idle_win); @@ -3854,7 +4031,7 @@ result_t wuss_test(const char *resources) delegate_w3 = mk_task(wuss, test_handle, &tc_w3); if (delegate_w3 == NULL) goto Failure; wuss_task_set_autoclose(delegate_w3, 1); - rc = wuss_window_create(delegate_w3, &box_w, "W3", wuss_WINDOW_NONE, + rc = wuss_window_create(delegate_w3, &box_w, "W3", wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), box_size(&box_w), SIZE2D(0, 0), &win_w3); if (rc != result_OK) @@ -4251,6 +4428,136 @@ ColourMenuOK: ; return result_TEST_FAILED; } + printf("test: a parent menu row keeps its highlight while its submenu is " + "open, and loses it once the submenu closes\n"); + { + static const wuss_menu_item_t sm_sub_items[] = + { + { "Sub-A", wuss_MENU_ITEM_NONE, NULL }, + { "Sub-B", wuss_MENU_ITEM_NONE, NULL } + }; + static const wuss_menu_t sm_sub = + { + "Sub", sm_sub_items, NELEMS(sm_sub_items) + }; + static const wuss_menu_item_t sm_items[] = + { + { "Plain", wuss_MENU_ITEM_NONE, NULL }, + { "More", wuss_MENU_ITEM_NONE, &sm_sub }, /* row 1 owns the submenu */ + { "Bottom", wuss_MENU_ITEM_NONE, NULL } + }; + static const wuss_menu_t sm_menu = + { + "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; + struct wuss__menu *root; + struct wuss__menu *sub; + + fontfile = path_join_filename(resources, 3, "resources", "bmfonts", + path_join_leafname("Tiny", "png")); + rc = bmfont_create(fontfile, &font); + if (rc != result_OK) + { + printf("wuss_test: submenu-highlight test could not load %s\n", fontfile); + goto Failure; + } + + spixels = malloc((size_t) rowbytes * 200); + if (spixels == NULL) { rc = result_OOM; goto SubHiFail; } + rc = bitmap_init(&sbm, SIZE2D(200, 200), pixelfmt_bgrx8888, rowbytes, + NULL, spixels); + if (rc != result_OK) goto SubHiFailFree; + screen_for_bitmap(&sscr, &sbm); + + fdesc.font = font; + fdesc.font_class = wuss_FONT_CLASS_NONE; + fdesc.name = NULL; + rc = wuss_create(&sscr, &fdesc, 1, NULL, 0, NULL, NULL, &swuss); + if (rc != result_OK) goto SubHiFailFree; + + memset(&stc, 0, sizeof(stc)); + sowner = mk_task(swuss, test_handle, &stc); + if (sowner == NULL) { rc = result_OOM; goto SubHiDestroy; } + + rc = wuss_menu_open(sowner, &sm_menu, POINT(40, 40), NULL); + if (rc != result_OK) goto SubHiDestroy; + + root = swuss->menu_chain; + if (root == NULL || root->menu != &sm_menu) goto SubHiCheckFail; + + /* pointer onto row 1's arrow gutter: its submenu opens */ + menu_move_over_row(swuss, root, 1, 1); + if (root->child == NULL) goto SubHiCheckFail; + if (root->open_index != 1) goto SubHiCheckFail; + sub = root->child; + if (sub->menu != &sm_sub) goto SubHiCheckFail; + if (!wuss__icon_hovered(root->icons[1])) goto SubHiCheckFail; /* parent lit */ + + /* pointer travels into the submenu, onto its row 0. The parent row that + * spawned it must keep its highlight; the submenu row gets one too. */ + menu_move_over_row(swuss, sub, 0, 0); + if (root->child != sub) goto SubHiCheckFail; /* still open */ + if (!wuss__icon_hovered(root->icons[1])) goto SubHiCheckFail; /* retained */ + if (!wuss__icon_hovered(sub->icons[0])) goto SubHiCheckFail; + + /* and while parked over the submenu's own title strip (no row under the + * pointer, so no ICON event) the parent highlight still holds */ + { + box_t cb; + point_t p; + + wuss_window_get_content_bounds(sub->window, &cb); + p.x = (cb.x0 + cb.x1) / 2; + p.y = cb.y0 - 3; /* just above the content: the titlebar */ + wuss_mouse_move(swuss, p, NULL); + if (!wuss__icon_hovered(root->icons[1])) goto SubHiCheckFail; + } + + /* pointer re-enters the parent on a different row (row 2, off the arrow): + * the submenu closes and row 1 must go dark, row 2 lit */ + menu_move_over_row(swuss, root, 2, 0); + if (root->child != NULL) goto SubHiCheckFail; /* closed */ + if (root->open_index != -1) goto SubHiCheckFail; + if (wuss__icon_hovered(root->icons[1])) goto SubHiCheckFail; /* dropped */ + if (!wuss__icon_hovered(root->icons[2])) goto SubHiCheckFail; + + /* re-open, then re-enter the parent on the *same* row's text (off the + * arrow): submenu closes, and row 1 stays lit because the pointer is on it */ + menu_move_over_row(swuss, root, 1, 1); + if (root->child == NULL) goto SubHiCheckFail; + menu_move_over_row(swuss, root, 1, 0); + if (root->child != NULL) goto SubHiCheckFail; + if (!wuss__icon_hovered(root->icons[1])) goto SubHiCheckFail; + + wuss_menu_close(root); + rc = result_OK; + goto SubHiDestroy; + +SubHiCheckFail: + printf("wuss_test: submenu-highlight check failed\n"); + rc = result_TEST_FAILED; + +SubHiDestroy: + reap_test_tasks(); + wuss_destroy(swuss); +SubHiFailFree: + free(spixels); +SubHiFail: + bmfont_destroy(font); + if (rc != result_OK) + return result_TEST_FAILED; + } + printf("test: a MENU press over another window closes the open menu and " "reaches that window's task so it opens its own menu\n"); { @@ -4357,11 +4664,24 @@ ColourMenuOK: ; if (mwuss->menu_chain == NULL || mwuss->menu_chain->menu != &menu_b) goto MoveCheckFail; + /* replacing menu A's chain told task A its handle was gone */ + if (mta.menu_closed_count != 1) goto MoveCheckFail; + if (mta.menu_handle != NULL) goto MoveCheckFail; + /* a plain SELECT press on bare backdrop still just dismisses */ wuss_mouse_click(mwuss, POINT(4, 196), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, NULL); if (mwuss->menu_chain != NULL) goto MoveCheckFail; + /* the click-outside dismissal told task B too. Regression: before the + * MENU_CLOSED fix wuss freed the chain here without telling task B, so + * mtb.menu_handle dangled and this wuss_menu_close was a use-after-free + * (ASan: heap-use-after-free in wuss_menu_close). */ + if (mtb.menu_closed_count != 1) goto MoveCheckFail; + if (mtb.menu_handle != NULL) goto MoveCheckFail; + wuss_menu_close(mtb.menu_handle); /* NULL now: safe no-op */ + if (mwuss->menu_chain != NULL) goto MoveCheckFail; + rc = result_OK; goto MoveDestroy; @@ -4382,6 +4702,140 @@ ColourMenuOK: ; if (rc != result_OK) return result_TEST_FAILED; } + + printf("test: a task teardown that leaves a menu chain open is safe -- " + "wuss_task_destroy abandons it before QUIT, and wuss_destroy frees " + "the internal menu task after every client QUIT\n"); + { + static const wuss_menu_item_t q_items[] = + { + { "Q-one", wuss_MENU_ITEM_NONE, NULL } + }; + static const wuss_menu_t q_menu = { "Q", q_items, NELEMS(q_items) }; + + const char *fontfile; + bmfont_t *font = NULL; + wuss_font_desc_t fdesc; + screen_t qscr; + bitmap_t qbm; + void *qpixels; + wuss_t *qwuss; + menu_task_t qmt; + wuss_task_t *task_q; + wuss_window_t *wq; + box_t bq; + + fontfile = path_join_filename(resources, 3, "resources", "bmfonts", + path_join_leafname("Tiny", "png")); + rc = bmfont_create(fontfile, &font); + if (rc != result_OK) + { + printf("wuss_test: menu-quit test could not load %s\n", fontfile); + goto Failure; + } + + qwuss = NULL; + qpixels = malloc((size_t) rowbytes * 200); + if (qpixels == NULL) { rc = result_OOM; goto QuitFail; } + rc = bitmap_init(&qbm, SIZE2D(200, 200), pixelfmt_bgrx8888, rowbytes, + NULL, qpixels); + if (rc != result_OK) goto QuitFreeOnly; + screen_for_bitmap(&qscr, &qbm); + + fdesc.font = font; + fdesc.font_class = wuss_FONT_CLASS_NONE; + fdesc.name = NULL; + rc = wuss_create(&qscr, &fdesc, 1, NULL, 0, NULL, NULL, &qwuss); + if (rc != result_OK) goto QuitFreeOnly; + + memset(&qmt, 0, sizeof(qmt)); + qmt.menu = &q_menu; + qmt.close_chain_on_quit = 1; + + /* the client task is registered first; the internal menu task is created + * only on the wuss_menu_open below, so it lands *after* the client in + * wuss::tasks. wuss_destroy's task sweep therefore reaches the client's + * QUIT while the menu task node is still, by list order, pending. */ + task_q = mk_task(qwuss, menu_open_handle, &qmt); + if (task_q == NULL) { rc = result_OOM; goto QuitDestroy; } + qmt.self = task_q; + + 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, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + box_size(&bq), SIZE2D(0, 0), &wq); + if (rc != result_OK) goto QuitDestroy; + NOT_USED(wq); + + wuss_mouse_click(qwuss, POINT(20, 20), wuss_BUTTON_MENU, + wuss_MOUSE_DOWN, NULL); + wuss_mouse_click(qwuss, POINT(20, 20), wuss_BUTTON_MENU, + wuss_MOUSE_UP, NULL); + if (qwuss->menu_chain == NULL) goto QuitCheckFail; + if (qmt.menu_handle == NULL) goto QuitCheckFail; + + /* Phase 1: wuss_task_destroy on the chain's owner. It abandons the chain + * (wuss_EVENT_MENU_CLOSED), which nulls qmt.menu_handle, *before* the + * QUIT it then delivers -- so the close_chain_on_quit handler's + * wuss_menu_close is a NULL no-op, not a double-free of the just-freed + * chain (ASan: heap-use-after-free in wuss_menu_close). */ + reap_test_tasks(); + if (qmt.menu_handle != NULL) goto QuitCheckFail; + if (qwuss->menu_chain != NULL) goto QuitCheckFail; + + /* Phase 2: same again but torn down by wuss_destroy's own task sweep, + * with task_q left registered. The internal menu task used to be freed by + * that sweep before task_q's QUIT, so the QUIT handler's wuss_menu_close + * walked a freed task's window list (ASan: heap-use-after-free in + * list_remove via wuss_window_close). */ + memset(&qmt, 0, sizeof(qmt)); + qmt.menu = &q_menu; + qmt.close_chain_on_quit = 1; + task_q = mk_task(qwuss, menu_open_handle, &qmt); + 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, + wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + box_size(&bq), SIZE2D(0, 0), &wq); + if (rc != result_OK) goto QuitDestroy; + wuss_mouse_click(qwuss, POINT(20, 20), wuss_BUTTON_MENU, + wuss_MOUSE_DOWN, NULL); + wuss_mouse_click(qwuss, POINT(20, 20), wuss_BUTTON_MENU, + wuss_MOUSE_UP, NULL); + if (qwuss->menu_chain == NULL) goto QuitCheckFail; + + forget_test_tasks(); /* drop task_q from the registry; wuss_destroy frees it */ + wuss_destroy(qwuss); /* QUIT -> task_q closes the chain; must not fault */ + qwuss = NULL; + + rc = result_OK; + goto QuitFreeOnly; + +QuitCheckFail: + printf("wuss_test: menu-quit check failed (chain=%p handle=%p)\n", + (void *) qwuss->menu_chain, (void *) qmt.menu_handle); + rc = result_TEST_FAILED; + goto QuitDestroy; + +QuitDestroy: + reap_test_tasks(); + if (qwuss != NULL) + wuss_destroy(qwuss); +QuitFreeOnly: + free(qpixels); +QuitFail: + bmfont_destroy(font); + if (rc != result_OK) + return result_TEST_FAILED; + } #endif /* WUSS_ICONS */ #endif /* WUSS_MENUS */ @@ -4420,6 +4874,317 @@ ColourMenuOK: ; goto Failure; } + printf("test: furniture hit boxes tile the visible box with no leaks\n"); + + { + static test_task_t tc_hs; + wuss_task_t *delegate_hs; + box_t box_hs, visible, content; + box_t back, close, toggle, resize; + box_t vup, vdown, vwell, hleft, hright, hwell; + wuss_window_t *win_hs; + int midx, midy; + + tc_hs.redraw_count = 0; + tc_hs.mouse_count = 0; + delegate_hs = mk_task(wuss, test_handle, &tc_hs); + if (delegate_hs == NULL) goto Failure; + + /* 120x80 content: wide enough that BACK, CLOSE and TOGGLE_SIZE do not + * overlap in the titlebar. */ + 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), + SIZE2D(400, 400), SIZE2D(0, 0), &win_hs); + if (rc != result_OK) + goto Failure; + + wuss_window_get_visible_bounds(win_hs, &visible); + wuss_window_get_content_bounds(win_hs, &content); + wuss__back_box(win_hs, &back); + wuss__close_box(win_hs, &close); + wuss__toggle_box(win_hs, &toggle); + wuss__resize_box(win_hs, &resize); + wuss__vscroll_up_box(win_hs, &vup); + wuss__vscroll_down_box(win_hs, &vdown); + wuss__vscroll_well_box(win_hs, &vwell); + wuss__hscroll_left_box(win_hs, &hleft); + wuss__hscroll_right_box(win_hs, &hright); + wuss__hscroll_well_box(win_hs, &hwell); + + midx = (content.x0 + content.x1) / 2; + midy = (content.y0 + content.y1) / 2; + + /* outline band, each edge */ + if (wuss__furniture_hit_test(win_hs, POINT(midx, visible.y0)) != wuss_FURNITURE_TITLE) + goto Failure; + if (wuss__furniture_hit_test(win_hs, POINT(visible.x0, midy)) != wuss_FURNITURE_HSCROLL_WELL) + goto Failure; /* left outline column, level with the content: nearest chrome is the hscroll strip */ + if (wuss__furniture_hit_test(win_hs, POINT(visible.x1 - 1, midy)) != wuss_FURNITURE_VSCROLL_WELL) + goto Failure; + if (wuss__furniture_hit_test(win_hs, POINT(midx, visible.y1 - 1)) != wuss_FURNITURE_HSCROLL_WELL) + goto Failure; + + /* the four window corners belong to the adjacent furniture */ + if (wuss__furniture_hit_test(win_hs, POINT(visible.x0, visible.y0)) != wuss_FURNITURE_BACK) + goto Failure; + if (wuss__furniture_hit_test(win_hs, POINT(visible.x1 - 1, visible.y0)) != wuss_FURNITURE_TOGGLE_SIZE) + goto Failure; + if (wuss__furniture_hit_test(win_hs, POINT(visible.x1 - 1, visible.y1 - 1)) != wuss_FURNITURE_RESIZE) + goto Failure; + if (wuss__furniture_hit_test(win_hs, POINT(visible.x0, visible.y1 - 1)) != wuss_FURNITURE_HSCROLL_LEFT) + goto Failure; + + /* icon centres */ + if (wuss__furniture_hit_test(win_hs, POINT((back.x0 + back.x1) / 2, (back.y0 + back.y1) / 2)) != wuss_FURNITURE_BACK) + goto Failure; + if (wuss__furniture_hit_test(win_hs, POINT((close.x0 + close.x1) / 2, (close.y0 + close.y1) / 2)) != wuss_FURNITURE_CLOSE) + goto Failure; + if (wuss__furniture_hit_test(win_hs, POINT((toggle.x0 + toggle.x1) / 2, (toggle.y0 + toggle.y1) / 2)) != wuss_FURNITURE_TOGGLE_SIZE) + goto Failure; + + /* scroll-part centres */ + if (wuss__furniture_hit_test(win_hs, POINT((vup.x0 + vup.x1) / 2, (vup.y0 + vup.y1) / 2)) != wuss_FURNITURE_VSCROLL_UP) + goto Failure; + if (wuss__furniture_hit_test(win_hs, POINT((vdown.x0 + vdown.x1) / 2, (vdown.y0 + vdown.y1) / 2)) != wuss_FURNITURE_VSCROLL_DOWN) + goto Failure; + if (wuss__furniture_hit_test(win_hs, POINT((vwell.x0 + vwell.x1) / 2, (vwell.y0 + vwell.y1) / 2)) != wuss_FURNITURE_VSCROLL_WELL) + goto Failure; + if (wuss__furniture_hit_test(win_hs, POINT((hleft.x0 + hleft.x1) / 2, (hleft.y0 + hleft.y1) / 2)) != wuss_FURNITURE_HSCROLL_LEFT) + goto Failure; + if (wuss__furniture_hit_test(win_hs, POINT((hright.x0 + hright.x1) / 2, (hright.y0 + hright.y1) / 2)) != wuss_FURNITURE_HSCROLL_RIGHT) + goto Failure; + if (wuss__furniture_hit_test(win_hs, POINT((hwell.x0 + hwell.x1) / 2, (hwell.y0 + hwell.y1) / 2)) != wuss_FURNITURE_HSCROLL_WELL) + goto Failure; + + /* divider seam between the content and the scroll strips */ + if (wuss__furniture_hit_test(win_hs, POINT(content.x1, midy)) != wuss_FURNITURE_VSCROLL_WELL) + goto Failure; + if (wuss__furniture_hit_test(win_hs, POINT(midx, content.y1)) != wuss_FURNITURE_HSCROLL_WELL) + goto Failure; + if (wuss__furniture_hit_test(win_hs, POINT(content.x1, content.y1 - 1)) == wuss_FURNITURE_CONTENT) + goto Failure; + if (wuss__furniture_hit_test(win_hs, POINT(content.x1 - 1, content.y1)) == wuss_FURNITURE_CONTENT) + goto Failure; + + /* genuine content */ + if (wuss__furniture_hit_test(win_hs, POINT(content.x0 + 5, content.y0 + 5)) != wuss_FURNITURE_CONTENT) + goto Failure; + if (wuss__furniture_hit_test(win_hs, POINT(content.x1 - 1, content.y1 - 1)) != wuss_FURNITURE_CONTENT) + goto Failure; + + if (!furniture_hit_sweep(win_hs, 0)) + goto Failure; + + wuss_window_close(win_hs); + } + + printf("test: furniture hit tiling holds for every furniture-flag combo\n"); + + { + 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 + }; + static test_task_t tc_cb; + wuss_task_t *delegate_cb; + box_t box_cb, visible; + wuss_window_t *win_cb; + unsigned int i; + + for (i = 0; i < sizeof combos / sizeof combos[0]; i++) + { + int leak; + + tc_cb.redraw_count = 0; + tc_cb.mouse_count = 0; + delegate_cb = mk_task(wuss, test_handle, &tc_cb); + if (delegate_cb == NULL) goto Failure; + + 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), + SIZE2D(400, 400), SIZE2D(0, 0), &win_cb); + if (rc != result_OK) + goto Failure; + + /* 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); + + if (!furniture_hit_sweep(win_cb, leak)) + { + printf("wuss_test: combo index %u (flags 0x%X) failed the hit sweep\n", + i, (unsigned int) combos[i]); + goto Failure; + } + + /* 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)) + { + box_t resize; + + wuss__resize_box(win_cb, &resize); + if (wuss__furniture_hit_test(win_cb, + POINT((resize.x0 + resize.x1) / 2, + (resize.y0 + resize.y1) / 2)) != wuss_FURNITURE_RESIZE) + { + printf("wuss_test: combo index %u (flags 0x%X): resize icon centre " + "does not hit RESIZE\n", i, (unsigned int) combos[i]); + goto Failure; + } + } + + if ((combos[i] & wuss_WINDOW_NO_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); + if (wuss__furniture_hit_test(win_cb, POINT(visible.x0, visible.y0)) != wuss_FURNITURE_CLOSE) + goto Failure; + } + + wuss_window_close(win_cb); + } + } + + printf("test: a titlebar carries a full-width divider rule at its foot\n"); + + { + static test_task_t tc_dv; + wuss_task_t *delegate_dv; + box_t box_dv; + wuss_window_t *win_dv; + int i, found; + + tc_dv.redraw_count = 0; + tc_dv.mouse_count = 0; + delegate_dv = mk_task(wuss, test_handle, &tc_dv); + if (delegate_dv == NULL) goto Failure; + + 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), + SIZE2D(400, 400), SIZE2D(0, 0), &win_dv); + if (rc != result_OK) + goto Failure; + + wuss__furniture_layout_build(win_dv); + + /* an OUTLINE piece exactly one pixel tall, sitting on the titlebar's + * bottom edge and spanning its full width */ + found = 0; + for (i = 0; i < win_dv->furniture_layout.npieces; i++) + { + const wuss__furniture_piece_t *p = &win_dv->furniture_layout.pieces[i]; + + if (p->paint == wuss__FURNITURE_PAINT_OUTLINE && + p->rect.y1 == win_dv->furniture_layout.titlebar.y1 && + p->rect.y1 - p->rect.y0 == WUSS_DIVIDER_PX && + p->rect.x0 == win_dv->furniture_layout.titlebar.x0 && + p->rect.x1 == win_dv->furniture_layout.titlebar.x1) + found = 1; + } + if (!found) + goto Failure; + + wuss_window_close(win_dv); + } + + printf("test: wuss_icons_load scans resources/wuss/icons and compresses\n"); + + { + char icons_dir[256]; + const bitmap_t *icon_bm; + int idx, opton; + + /* copy: path_join_filename hands back one shared static buffer, and + * wuss_icons_load's own per-file joins would clobber it mid-call */ + strncpy(icons_dir, + path_join_filename(resources, 3, "resources", "wuss", "icons"), + sizeof(icons_dir) - 1); + icons_dir[sizeof(icons_dir) - 1] = '\0'; + + rc = wuss_icons_load(wuss, icons_dir); + if (rc != result_OK) + goto Failure; + + /* the four fixtures: optoff/opton/radoff/radon */ + if (wuss_icons_count(wuss) != 4) + goto Failure; + + opton = wuss_icons_lookup(wuss, "opton"); + if (opton < 0 || wuss_icons_lookup(wuss, "radoff") < 0) + goto Failure; + if (wuss_icons_lookup(wuss, "nonesuch") != -1) + goto Failure; + + icon_bm = wuss_icons_bitmap(wuss, opton); + if (icon_bm == NULL || !bitmap_is_compressed(icon_bm)) + goto Failure; + if (wuss_icons_bitmap(wuss, 4) != NULL) + goto Failure; + + /* an icon spec picks it up by index via wuss_ICON_SET */ + { + static test_task_t tc_ic; + wuss_task_t *delegate_ic; + wuss_window_t *win_ic; + wuss_icon_t *icon; + wuss_icon_spec_t spec; + box_t box_ic; + + memset(&tc_ic, 0, sizeof(tc_ic)); + delegate_ic = mk_task(wuss, test_handle, &tc_ic); + if (delegate_ic == NULL) goto Failure; + + 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), + SIZE2D(400, 400), SIZE2D(0, 0), &win_ic); + if (rc != result_OK) + goto Failure; + + 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); + 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); + if (wuss_icon_create(win_ic, &spec, &icon) != result_WUSS_BAD_INDEX) + goto Failure; + rc = result_OK; + + wuss_window_close(win_ic); + } + + /* a reload replaces the set cleanly (no leak, ASan would catch it) */ + rc = wuss_icons_load(wuss, icons_dir); + if (rc != result_OK || wuss_icons_count(wuss) != 4) + goto Failure; + + idx = wuss_icons_lookup(wuss, "optoff"); + if (idx < 0) + goto Failure; + } + wuss_destroy(wuss); free(pixels); @@ -4483,7 +5248,7 @@ result_t wuss_test(const char *resources) printf("test: window_create too small\n"); box_a.x0 = 0; box_a.y0 = 0; box_a.x1 = 100; box_a.y1 = 0; - rc = wuss_window_create(mk_task(wuss, NULL, NULL), &box_a, "toosmall", wuss_WINDOW_NONE, + rc = wuss_window_create(mk_task(wuss, NULL, NULL), &box_a, "toosmall", wuss_WINDOW_DEFAULT, wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), box_size(&box_a), SIZE2D(0, 0), &win_a); diff --git a/resources/bmfonts/04b_03.png b/resources/bmfonts/04b_03.png new file mode 100644 index 0000000..491fd8a Binary files /dev/null and b/resources/bmfonts/04b_03.png differ diff --git a/resources/bmfonts/04b_25.png b/resources/bmfonts/04b_25.png new file mode 100644 index 0000000..d808fa2 Binary files /dev/null and b/resources/bmfonts/04b_25.png differ diff --git a/resources/bmfonts/Digits-Bold.png b/resources/bmfonts/Digits-Bold.png index 6b798bb..a884102 100644 Binary files a/resources/bmfonts/Digits-Bold.png and b/resources/bmfonts/Digits-Bold.png differ diff --git a/resources/bmfonts/Digits-Regular.png b/resources/bmfonts/Digits-Regular.png index 6219f9f..de4cf73 100755 Binary files a/resources/bmfonts/Digits-Regular.png and b/resources/bmfonts/Digits-Regular.png differ diff --git a/resources/bmfonts/Nokia.png b/resources/bmfonts/Nokia.png new file mode 100644 index 0000000..90bdfff Binary files /dev/null and b/resources/bmfonts/Nokia.png differ diff --git a/resources/images/smred.png b/resources/images/smred.png new file mode 100644 index 0000000..a7681c0 Binary files /dev/null and b/resources/images/smred.png differ diff --git a/resources/wuss/icons/optoff.png b/resources/wuss/icons/optoff.png new file mode 100755 index 0000000..e1768e5 Binary files /dev/null and b/resources/wuss/icons/optoff.png differ diff --git a/resources/wuss/icons/opton.png b/resources/wuss/icons/opton.png new file mode 100755 index 0000000..373b9e6 Binary files /dev/null and b/resources/wuss/icons/opton.png differ diff --git a/resources/wuss/icons/radoff.png b/resources/wuss/icons/radoff.png new file mode 100755 index 0000000..e3f4433 Binary files /dev/null and b/resources/wuss/icons/radoff.png differ diff --git a/resources/wuss/icons/radon.png b/resources/wuss/icons/radon.png new file mode 100755 index 0000000..062b118 Binary files /dev/null and b/resources/wuss/icons/radon.png differ diff --git a/resources/wuss/ninepatch.aseprite b/resources/wuss/source/ninepatch.aseprite similarity index 100% rename from resources/wuss/ninepatch.aseprite rename to resources/wuss/source/ninepatch.aseprite diff --git a/tools/gen_greeble_tiles.py b/tools/gen_greeble_tiles.py old mode 100644 new mode 100755 diff --git a/tools/test_wrap_doxygen.py b/tools/test_wrap_doxygen.py old mode 100644 new mode 100755 diff --git a/tools/test_wrap_protos.py b/tools/test_wrap_protos.py old mode 100644 new mode 100755 diff --git a/tools/ttf2bmfont.py b/tools/ttf2bmfont.py new file mode 100755 index 0000000..e9e7bb0 --- /dev/null +++ b/tools/ttf2bmfont.py @@ -0,0 +1,285 @@ +#!/usr/bin/env python3 +"""ttf2bmfont.py -- render a pixel-design TTF into a DPTLib bmfont PNG. + +DPTLib's framebuf/bmfont loader (libraries/framebuf/bmfont/bmfont.c) reads +proportional bitmap fonts from a very specific 2bpp paletted PNG: + + * 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). + * 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` + index-2 pixels (left-justified here) and no index-1 pixels. The loader + counts index-2 pixels in the cell to get the advance width. + * 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. + +Requires freetype-py ( pip install freetype-py ). PNG is written by hand so +Pillow is not needed. + +Usage: + python3 tools/ttf2bmfont.py FONT.ttf OUT.png --size PX [options] +""" + +import argparse +import struct +import sys +import zlib + +import freetype + +# Palette matches resources/bmfonts/Nokia.png and Digits-Bold.png. +PALETTE = [(255, 255, 255), (0, 0, 0), (0, 0, 255), (192, 192, 192)] + +IDX_BG = 0 +IDX_INK = 1 +IDX_ADW = 2 +IDX_GRID = 3 + +CHARS_PER_ROW = 32 +MAX_CELL_W = 16 + + +def load_glyphs(path, size, first, last): + """Return (list of glyph dicts, face) for codepoints first..last inclusive. + + Each glyph dict has: cp, bits (list of (x, y) set pixels, origin at the + pen position, y down), left, top, width, rows, advance. + """ + face = freetype.Face(path) + face.set_pixel_sizes(0, size) + + glyphs = [] + for cp in range(first, last + 1): + face.load_char(chr(cp), + freetype.FT_LOAD_RENDER | freetype.FT_LOAD_TARGET_MONO) + slot = face.glyph + bm = slot.bitmap + bits = [] + for by in range(bm.rows): + rowbase = by * bm.pitch + for bx in range(bm.width): + byte = bm.buffer[rowbase + (bx >> 3)] + if byte & (0x80 >> (bx & 7)): + bits.append((bx, by)) + glyphs.append(dict(cp=cp, + bits=bits, + left=slot.bitmap_left, + top=slot.bitmap_top, + width=bm.width, + rows=bm.rows, + advance=slot.advance.x >> 6)) + return glyphs, face + + +def measure_cell(glyphs, pad, advance_source): + """Work out the common cell geometry from the rendered glyphs. + + Returns (cellW, cellH, charH, ascent, minLeft). + """ + ascent = 0 + descent = 0 + ink_right = 0 + min_left = 0 + for g in glyphs: + if g["bits"]: + ascent = max(ascent, g["top"]) + descent = max(descent, g["rows"] - g["top"]) + ink_right = max(ink_right, g["left"] + g["width"]) + min_left = min(min_left, g["left"]) + + max_advance = 0 + for g in glyphs: + adv = g["advance"] if advance_source == "ttf" else _ink_advance(g) + max_advance = max(max_advance, adv) + + cell_w = max(ink_right - min_left, max_advance) + pad + char_h = ascent + descent + pad + cell_h = char_h + 1 # + strip row + + if cell_w < 1: + cell_w = 1 + if cell_w > MAX_CELL_W: + sys.exit("ttf2bmfont: cell width %d exceeds the loader limit of %d; " + "use a smaller --size" % (cell_w, MAX_CELL_W)) + + return cell_w, cell_h, char_h, ascent, min_left + + +def _ink_advance(g): + if not g["bits"]: + return 0 + return g["left"] + g["width"] + 1 + + +def advance_for(g, cell_w, advance_source): + """Clamp the chosen advance into [1, cell_w]. + + At least 1 so every strip row carries an index-2 pixel (the loader's + height detector requires it). + """ + adv = g["advance"] if advance_source == "ttf" else _ink_advance(g) + if adv < 1: + adv = 1 + if adv > cell_w: + adv = cell_w + return adv + + +def build_indices(glyphs, cell_w, cell_h, char_h, ascent, min_left, + advance_source, draw_grid): + """Render every glyph into an H*W buffer of palette indices.""" + nrows = (len(glyphs) + CHARS_PER_ROW - 1) // CHARS_PER_ROW + w = CHARS_PER_ROW * cell_w + h = nrows * cell_h + buf = bytearray(w * h) # zero == IDX_BG + + def put(x, y, idx): + if 0 <= x < w and 0 <= y < h: + buf[y * w + x] = idx + + baseline = ascent # body-row index of the baseline + + if draw_grid: + for r in range(nrows): + body0 = r * cell_h + 1 + for c in range(CHARS_PER_ROW): + x0 = c * cell_w + for dy in range(char_h): # left sidebearing line + put(x0, body0 + dy, IDX_GRID) + for dx in range(cell_w): # baseline and cell-bottom lines + put(x0 + dx, body0 + baseline, IDX_GRID) + put(x0 + dx, body0 + char_h - 1, IDX_GRID) + + warned = [False] + advances = [] + for i, g in enumerate(glyphs): + r, c = divmod(i, CHARS_PER_ROW) + x0 = c * cell_w + y_strip = r * cell_h + body0 = y_strip + 1 + + adv = advance_for(g, cell_w, advance_source) + advances.append(adv) + for dx in range(adv): # advance strip + put(x0 + dx, y_strip, IDX_ADW) + + for bx, by in g["bits"]: # glyph ink (wins over any grid pixel) + gx = x0 + (g["left"] - min_left) + bx + gy = body0 + (ascent - g["top"]) + by + if not (x0 <= gx < x0 + cell_w and body0 <= gy < body0 + char_h): + if not warned[0]: + sys.stderr.write("ttf2bmfont: warning: glyph U+%04X " + "overflows its cell; clipping\n" % g["cp"]) + warned[0] = True + continue + put(gx, gy, IDX_INK) + + return buf, w, h, advances + + +def write_png(path, buf, w, h): + """Write a 2bpp, colour-type-3, non-interlaced PNG.""" + def chunk(tag, data): + return (struct.pack(">I", len(data)) + tag + data + + struct.pack(">I", zlib.crc32(tag + data) & 0xFFFFFFFF)) + + sig = b"\x89PNG\r\n\x1a\n" + ihdr = struct.pack(">IIBBBBB", w, h, 2, 3, 0, 0, 0) + + plte = b"".join(struct.pack("BBB", *rgb) for rgb in PALETTE) + + raw = bytearray() + rowbytes = (w * 2 + 7) // 8 + for y in range(h): + raw.append(0) # filter type 0 (none) + row = bytearray(rowbytes) + base = y * w + for x in range(w): + row[x >> 2] |= (buf[base + x] & 3) << (6 - 2 * (x & 3)) + raw.extend(row) + + with open(path, "wb") as f: + f.write(sig) + f.write(chunk(b"IHDR", ihdr)) + f.write(chunk(b"PLTE", plte)) + f.write(chunk(b"IDAT", zlib.compress(bytes(raw), 9))) + f.write(chunk(b"IEND", b"")) + + +def selfcheck(buf, w, h, cell_w, cell_h, advances): + """Re-parse the buffer with the loader's own rules and assert agreement.""" + assert w % CHARS_PER_ROW == 0, "width not a multiple of 32" + assert w // CHARS_PER_ROW == cell_w <= MAX_CELL_W, "bad cell width" + assert h % cell_h == 0, "height not a multiple of the cell height" + + def has(idx, y): + base = y * w + return any(buf[base + x] == idx for x in range(w)) + + for y in range(h): + if y % cell_h == 0: + assert has(IDX_ADW, y), "strip row %d has no advance pixel" % y + assert not has(IDX_INK, y), "strip row %d has ink" % y + else: + assert not has(IDX_ADW, y), "body row %d has an advance pixel" % y + + # round-trip the advance counts + for i, want in enumerate(advances): + r, c = divmod(i, CHARS_PER_ROW) + y = r * cell_h + x0 = c * cell_w + got = sum(1 for dx in range(cell_w) if buf[y * w + x0 + dx] == IDX_ADW) + assert got == want, "glyph %d advance %d != %d" % (i, got, want) + + print("selfcheck OK: %dx%d, cell %dx%d, %d glyphs" % + (w, h, cell_w, cell_h, len(advances))) + + +def main(argv): + p = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + p.add_argument("font") + p.add_argument("out") + p.add_argument("--size", type=int, required=True, + help="pixel em size passed to FreeType") + 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") + 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) + p.add_argument("--last", type=lambda s: int(s, 0), default=0x7E) + p.add_argument("--selfcheck", action="store_true", + help="re-parse the result and assert it is well formed") + args = p.parse_args(argv) + + glyphs, _ = load_glyphs(args.font, args.size, args.first, args.last) + + # pad the cell count up to a multiple of 32 with blank glyphs + while len(glyphs) % CHARS_PER_ROW: + glyphs.append(dict(cp=0, bits=[], left=0, top=0, width=0, rows=0, + advance=1)) + + cell_w, cell_h, char_h, ascent, min_left = measure_cell( + glyphs, args.pad, args.advance_source) + + buf, w, h, advances = build_indices(glyphs, cell_w, cell_h, char_h, + ascent, min_left, args.advance_source, + not args.no_grid) + + write_png(args.out, buf, w, h) + print("wrote %s (%dx%d, cell %dx%d, charheight %d, %d glyphs)" % + (args.out, w, h, cell_w, cell_h, cell_h - 1, len(glyphs))) + + if args.selfcheck: + selfcheck(buf, w, h, cell_w, cell_h, advances) + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/tools/wrap_doxygen.py b/tools/wrap_doxygen.py old mode 100644 new mode 100755 diff --git a/tools/wrap_protos.py b/tools/wrap_protos.py old mode 100644 new mode 100755