diff --git a/CHANGELOG.md b/CHANGELOG.md index c47943a..e06552d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,97 @@ _Unreleased_ until one is cut. ### Added +- `wuss_ICON_TYPE_SLIDER` — horizontal/vertical slider icon: bevelled surround + with a sunken groove inset by a fixed gap, drag/click value mapping that + rounds to the nearest value rather than truncating, and a min > max + reversed-fill mode. `wuss_icon_get_value()` / `wuss_icon_set_value()` read + and write it. +- `geom/stack` — a nested HBox/VBox box-stack layout solver with flex + weights, cross-axis alignment, per-item min/max clamps and container + padding/gap (`docs/windowing/stack-sketch.md`); `STACK_VBOX`/`STACK_HBOX`/ + `STACK_LEAF`/`STACK_SPACER` (plus `_EX` variants) compact a static + `stack_item_t` table to one designated-init literal per row. The saturn + Size dialogue is laid out through it. +- `wuss/icon-spec.h` — `wuss_icon_spec_label()` / `_slider()` / `_action()` + fill a `wuss_icon_spec_t` for the common cases, replacing manual + field-by-field assignment (following the `create-from-desc.c` helper + precedent). +- `wuss_STD_GAP` / `wuss_STD_INSET` / `wuss_STD_SLIDER_HEIGHT` / + `wuss_STD_SECONDARY_BUTTON_HEIGHT` / `wuss_STD_PRIMARY_BUTTON_HEIGHT` — + standard sibling-gap, edge-inset and icon-height constants in + `wuss/icon.h`, generalised out of what was local to `saturn.c`. +- The saturn task gets a Colours menu (Foreground/Background via + `wuss_colourmenu_t`) and a Size... submenu — a hover-opened dialogue + (`wuss_menu_item_t::window`) with a slider for the sketch size and a + second slider for the stars-loop iteration count, Apply/Cancel/ + Adjust-Apply/Adjust-Cancel following RISC OS convention. +- The `wuss` chars task draws a dotted baseline rule and a light-blue + advance-width rule under each glyph (useful for eyeballing advance-width + drift), trims contiguous leading/trailing blank grid rows, and gains a + `wuss_STD_INSET` margin. +- `pixelfmt_p8` — 8bpp paletted screen support across the + framebuf/wuss/image-io stack: `span_p8`, screen blend/copy/fill-pattern/ + RLE-blit p8 branches, `bmfont` p8 glyph drawing, `bitmap_convert()` + p8→bgrx8888/rgbx8888, PNG load/save keeping a palette-type PNG as + `pixelfmt_p8` instead of expanding it, and `--depth 8` on the `wuss` SDL + frontend. The palette task gains a second "Screen" window showing the + physical screen bitmap's own palette; the p8 tail beyond the 16 UI colours + is filled with the web-safe 216-colour cube. +- `screen_copy_bitmap_dithered()` — blits onto a paletted screen with + per-pixel 8x8 Bayer ordered dithering before the nearest-palette lookup, + phased to screen coordinates so it stays put across redraws; + `pattern_bayer_threshold()` exposes the underlying matrix. The `wuss` + image task gains a Dithering menu toggle. +- `screen_copy_bitmap()` now blits a paletted (p1/p2/p4/p8) source bitmap + onto a screen of any format via a shared `src_fetch_rgba()` decode + helper, reusing the existing paletted→deep `pixelmap_get()` table with no + extra allocation or pass; a paletted source's tRNS-derived alpha is + honoured. +- `screen_copy_rect()` now supports 1bpp and 2bpp screens (generalised from + the existing 4bpp packed-pixel path), so window move/scroll blit fast + paths work on low-bpp screens instead of falling back to a full repaint. +- `wuss_EVENT_POINTER_ENTER` / `wuss_EVENT_POINTER_EXIT` — window-scoped + events fired when the pointer crosses onto or off a window's whole + on-screen footprint (content or furniture alike); exactly one ENTER is + outstanding per window, always balanced by a later EXIT. +- `wuss_text_draw()` / `wuss_text_measure()` — public wrappers exposing the + existing internal `bmfont_draw`/`bmfont_measure` pass-throughs, so a + client task can draw/measure text in a wuss system font without including + `framebuf/bmfont.h`. +- The minesweeper task gains a Grid Size submenu (24x24 down to 12x12, + radio-ticked to the current size); board dimensions move from + compile-time defines to runtime task fields. +- The image task gains a Background colour menu (`wuss_colourmenu_t`) for + the fill behind the 9-patch border band; `wuss_MENU_ITEM_BORROWED_SUBMENU` + marks a submenu `wuss_menu_destroy()` should not recurse into, and + `wuss_NO_BACKDROP` replaces the `wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND)` + idiom. +- `wuss_menu_create_from_desc()`'s `!`/`~` (tick/shade) prefixes now + optionally pull a bool vararg at the point they're scanned instead of + always applying, so a descriptor can encode live state (e.g. a toggle's + current value) directly. +- `wuss_menu_tick_exclusive()` / `wuss_menu_tick_item()` / + `wuss_menu_tick_set_live()` — data-level tick setters (for editing menu + items before a (re)open) completing the existing live-chain setter family + (`wuss_menu_tick_exclusive_live`, `wuss_menu_tick_item_live`, renamed from + `wuss_menu_set_ticked`/`wuss_menu_set_item_ticked`). +- Shift-F1 on the RISC OS frontend now forces `wuss_INPUT_GARBAGE` + (single-frame screen corruption), matching the existing SDL frontend and + `frontend.h`'s documented F1/Shift-F1 intent. +- `path_leaf_strip_ext()` — host-aware `.ext`-stripping helper; on RISC OS a + leafname carries no dotted extension at all (file type is separate + metadata), so the three call sites hand-rolling `strcmp`-based stripping + (namelist, bmfont enumerate, wuss icon registry) previously matched + nothing there. +- `bmfont` glyphs are now drawn at the text baseline rather than the + glyph-cell top-left: `bmfont_create()` derives ascent/descent at load + time from the space glyph's grid rows, `bmfont_get_info()` gains ascent/ + descent out-params, and every caller doing manual top-left positioning is + updated to add ascent. +- Six reserved `wuss_ICON_TYPE_*` constants (`DISPLAY`, `WRITABLE`, + `NUMBER`, `STRING_SET`, `SLIDER`, `DRAGGABLE`) after `RULE`, validated + from a spec but (bar SLIDER, now implemented) not yet drawn/hit-tested/ + routed. - `bmfont_draw_relief()` — draws a string twice with a transparent background, a shadow pass in a given colour at `pos + offset` then the main pass at `pos`, factoring the hand-rolled two-call drop-shadow idiom @@ -261,9 +352,83 @@ _Unreleased_ until one is cut. - `tools/ttf2bmfont.py` — renders a TTF into the bmfont PNG format the loader expects. Three small paletted bitmap faces (`04b_03`, `04b_25`, `Nokia`) added under `resources/bmfonts/`. +- `bmtext_layout()` / `bmtext_draw()` take an optional + `const bmfont_spacing_t *spacing` (NULL for none), threading letter/word + spacing through the paragraph word-wrap and draw paths, not just the + lower-level `bmfont_measure`/`bmfont_draw`. +- The `wuss` text task's menu gains a Spacing submenu (Normal/Letter/Word/ + Letter+Word presets), Foreground/Background colourmenus + (`wuss_colourmenu_t`) and a tickable No Background entry that draws + glyphs with a transparent background instead of the picked colour. +- The `wuss` demo launcher's Launch submenu is split into Games/Tests/ + Utilities/Visuals category submenus hung directly off the top-level task + menu. +- The saturn task's Configure dialogue gains a Default button that resets + `task->config` to `SATURN_CONFIG_DEFAULT`, refills the sliders and + applies it. ### Changed +- **Breaking:** furniture window flags flip to opt-in: + `wuss_WINDOW_NO_TITLEBAR`/`_OUTLINE`/`_CLOSE`/`_BACK`/`_TOGGLE_SIZE`/ + `_VSCROLL`/`_HSCROLL`/`_RESIZE` become `wuss_WINDOW_TITLEBAR`/`_OUTLINE`/ + `_CLOSE`/`_BACK`/`_TOGGLE_SIZE`/`_VSCROLL`/`_HSCROLL`/`_RESIZE` (set to + show, not set to hide); `wuss_WINDOW_DEFAULT` is the OR of all eight, so a + bare-`DEFAULT` caller needs no change, but any caller that combined + `DEFAULT` with a single furniture flag (e.g. `wuss_WINDOW_NO_RESIZE_BLIT` + alone) must now OR `DEFAULT` in explicitly to keep its chrome. +- **Breaking:** menu tick setters are renamed to separate data-level and + live-chain variants: `wuss_menu_set_ticked` → `wuss_menu_tick_exclusive_live`, + `wuss_menu_set_item_ticked` → `wuss_menu_tick_item_live`; + `wuss_menu_open_ticked` and the new `wuss_menu_tick_set` take a bit-vector + (`unsigned int`) instead of an `int` array. +- struct `wuss_icon`'s mutually-exclusive per-type fields (label border, + pattern tile, bitmap image, radio group, menu-entry swatch) move into a + per-type union, mirrored on `wuss_icon_spec_t`, then the spec is nested + directly inside `struct wuss_icon` as `{ spec; state; }` — + `icon->FIELD` becomes `icon->spec.FIELD`. `wuss_icon_get_window()` and the + icon→window back-pointer are removed; the four mutators that needed it + (delete, set_text, set_hidden, set_selected) now take the window as an + explicit argument. +- `wuss_ICON_TYPE_BUTTON` is renamed `wuss_ICON_TYPE_ACTION` throughout the + icon API, implementation, tests and docs. +- Fonts move out of loose `struct wuss` fields into a core-owned + `libraries/wuss/font/` module (`struct wuss_fontset`, + `wuss__fontset_init/_height`, the `wuss_get_font*` accessors, the + centralised text renderer); `wuss`'s four loose font fields collapse to + one embedded fontset. +- The `wuss` demo's SDL present path converts and uploads only dirty rows + instead of the whole screen every frame (`wuss_frontend_present` now + takes a dirty box); a new `wuss->touched` region list (distinct from + `wuss->dirty`) tracks pixels written directly by `wuss__blit_pieces`/ + resize/toggle-size so the frontend still re-uploads blitted-but-not- + repainted destinations. +- `wuss_window_move()` translates the cached furniture-layout rects by the + move delta instead of invalidating and rebuilding the whole per-window + layout cache on every drag tick; `wuss_window_move()` and the resize path + both now early-return once the target box is found equal to the window's + current one, skipping the blit/invalidate machinery for a no-op drag tick. +- Toggle-size now grows a window to fill the whole screen (capped per axis + at the document extent, or uncapped when that's 0), nudging the top-left + toward the origin by the minimum needed to fit, rather than pinning the + top-left and only growing the bottom-right; restore returns the exact + pre-toggle box. +- Adjust-clicking a scroll well now pages away from the click point + (mirroring Select's page-towards), matching RISC OS's usual Select/Adjust + reversal elsewhere in the furniture. +- `screen_set_pixel_8()` is renamed `screen_set_pixel_p8()`, matching the + `_p1`/`_p2`/`_p4` naming of the other paletted set-pixel helpers. +- Advance widths encoded in font PNGs are now 1px shorter than the true pen + advance (`bmfont_advance_for()` adds the missing pixel back in as letter + spacing at every use site); the bundled bitmap fonts (`04b_03`, `04b_25`, + the `DPT-*` faces, `Nokia`, `Symbols`, `Tiny`) are regenerated to match, + and `tools/ttf2bmfont.py`'s ink-advance mode drops a spurious extra pixel + it previously added. +- The bundled bitmap fonts are renamed with a provenance prefix (`DPT-` for + the in-house faces, `ZX-` for GliderRider). +- `screen_copy_bitmap_i`'s has-alpha-channel check is hoisted into a general + `pixelfmt_has_alpha()` macro alongside `pixelfmt_is_rle`/ + `pixelfmt_paletted_nentries`. - **Breaking:** `wuss_window_invalidate_all()` is renamed `wuss_window_invalidate_visible()` -- "all" misread as the whole document when it only ever covered the visible content rectangle. Same behaviour; @@ -409,9 +574,134 @@ _Unreleased_ until one is cut. `wuss_window_resize()` before `wuss_window_set_doc()`, which alone only moved the scroll extent) and insets an 8px solid border inside the ninepatch frame. +- The `wuss` demo's global task-list variable is renamed `g` -> `g_tasks`, + avoiding a shadow of the `g`/`b` loop locals in + `tasks_build_screen_palette`; the saturn task now spawns at startup + instead of only via the launcher. +- `wuss__clip_to_visible()` and `wuss__subtract_boxes()`'s near-identical + ping-pong carve loops are factored into a shared `carve_by_cuts()` driven + by a `get_cut` callback, with z-order and plain-array adapters for the two + call sites. +- `wuss_window_move()` and `wuss_window_resize()`'s duplicated "filter clean + pieces against `wuss->dirty[]`" loops are factored into a shared + `wuss__filter_settled()`. ### Fixed +- `bmfont_draw()` clipped full-width glyphs (M, W, ...) out of existence: + `bmfont_advance_for()`'s advance (cell width + the new 1px letter + spacing) was used directly as the drawable pixel width, pushing + `clippedcharwidth` past `charwidth` and driving `right_skip` negative. + Each glyph's draw now splits into the cell itself (clamped to + `charwidth`) and any leftover advance, drawn from a zero-filled glyph + buffer. +- Several furniture task windows (swatches, text, palette) passed + `wuss_WINDOW_NO_RESIZE_BLIT` alone as their window flags, zeroing every + furniture bit under the new opt-in flag polarity instead of just opting + out of blit-based resize; now OR'd with `wuss_WINDOW_DEFAULT`. +- The saturn Size dialogue's Cancel/Apply buttons now act on Select release + (`wuss_MOUSE_UP`) instead of press, so a press dragged off the button no + longer commits and an Adjust click leaves the dialogue open per RISC OS + convention; the slider groove now derives from the surround box rather + than the bevel box. +- Icon text was vertically centred using ascender+descender (`font_height`), + pulling label/button/radio-option/menu-entry text visually high since + most glyphs never touch the descender band; centred by `font_ascent` + instead, factored into a shared `icon_text_baseline_y()` helper. +- The chars grid's document extent wasn't updated on a font switch, so the + scroll range stayed sized to the original font — a larger font put grid + cells out of scroll reach, a smaller one left dead scroll space. +- A borrowed-window menu item (`wuss_menu_item_t::window`) could open + off-screen near the parent menu's right edge, since `wuss_window_move` + deliberately doesn't clamp; extracted `wuss_window_create()`'s on-screen + nudge into a shared helper and applied it after the anchor move. +- The RADIO/OPTION icon glyph square was always font-height, truncating a + larger state-sprite blit and ignoring its real width for the label + offset; now sized from the state bitmap when present. Its box now also + grows to fit the larger of its two state bitmaps so a select-time + invalidate covers the whole overhanging sprite, and the box is now + cleared before redraw when the icon has no explicit background + (previously a shrinking glyph swap left stale pixels). +- A submenu dragged back over its own parent's titlebar was closed + mid-drag, because the IDLE-tick "pointer over parent titlebar" check + didn't verify the parent was actually frontmost there; now requires + `wuss__window_at` to resolve to the parent itself. Separately, a menu + chain's submenu now only opens while the pointer is in the row's arrow + gutter (not anywhere on the row) and closes on re-entry; and the parent + menu's title bar being hovered now closes an open child chain (previously + `wuss_mouse_move`'s early-return over furniture meant nothing heard about + it). +- `screen_copy_bitmap()` read a paletted source bitmap (e.g. a PLTE-chunk + PNG) as if it were 32bpp RGBA, running past the buffer end; first + hardened to reject non-32bpp sources, then given real paletted-source + support via a shared `src_fetch_rgba()` decode path. +- The ordered-dither bias table was keyed on raw palette entry count + instead of the true per-channel quantisation level (4:4:4 for p1/p2/p4, + 5:6:5 for p8), collapsing the p8 Bayer bias to `{-1,0}` and making + dithering invisible; and the dither cell was keyed on destination screen + coordinates rather than sprite-local ones, so the pattern crawled across + a moving sprite instead of staying fixed to it. +- The generalised packed-copy path (`screen_copy_rect_packed`, covering + p1/p2/p4) used LSB-first within-byte indexing for every depth, but p1/p2 + are packed MSB-first — a non-byte-aligned move on a 2bpp screen picked + the wrong pixel and shuffled the row. +- The screen palette wasn't repadded on a live palette change, only at + startup: picking a palette entry while running p8 fed wuss's bare + 16-entry UI palette straight to `bitmap_set_palette()`, which reads 256 + entries for p8 — a heap-buffer-overflow. The pad-to-screen-size logic is + now shared between startup and the live-change handler. +- A p8-loaded PNG converted to `bgrx8888` before reaching + `screen_copy_bitmap`/`screen_copy_ninepatch` produced dull/wrong-hue + composites, since that blit family assumes `rgba8888` byte order; + switched to a matching `bmconv_p8_to_rgbx8888`. +- `wuss_window_invalidate(window, NULL)` built its whole-visible-rect + placeholder already in screen space then ran it through the doc-space- + to-screen translation a second time, double-applying scroll once a + window was shrunk below its document and scrolled, sliding queued dirty + rects off to one side; the placeholder is now built in doc space. +- A client-supplied `wuss_window_invalidate()` box was never clipped + against the window's own furniture (only against occluding windows above + it), so an edge-hugging client could dirty the titlebar/scrollbars/ + outline around its content; now intersected with the content box first. +- The ball task's edge-clamp was off-by-one on the right/bottom (content + being a half-open box), poking the invalidation 1px into the furniture + strip. +- A resize drag's scroll-reclamp fast-path blit read its source without + checking `wuss->dirty[]` (unlike move/set-scroll), letting an earlier + unpainted dirty region in the same frame get slid forward as settled + content; a follow-up guarded the same path against + `WUSS_MAX_INVALIDATE_PIECES` overflow (falling back to a full repaint + instead of silently dropping survivor pieces), and a duplicate + furniture-repaint call on the same reclamp path was removed. +- `wuss__blit_pieces` translated clean source pieces by the scroll delta + and clipped only against occluding windows, never against the content + box itself — a fast scroll (especially after a shrink) could blit and + then invalidate screen area outside the window, painted with stale/ + garbage content on the next redraw; now intersected against the content + box first. +- A scroll-well click's page-and-hold also armed a sausage drag, turning a + held button into a live scroll; then, once guarded off by region, that + guard also blocked a *direct* sausage hit from arming a drag at all — + now gated on whether the click actually paged, not on which region it + hit. +- Interactive resize-drag could be dragged past the screen's own + width/height when the window's top-left was off-screen (negative x0/y0), + since the clamp measured room left from the window's current position + rather than the screen's total size. +- `path_join_leafname()` on RISC OS produced invalid pathnames like + `Nokia/png` (`/` is a literal leafname character there, not a separator, + and file type isn't part of the string at all). +- `mkstemps` (a GNU/BSD extension absent from the RISC OS GCCSDK libc) in + `rle-test.c` broke the RISC OS build; replaced with a fixed leafname via + `path_join_leafname`. +- Minesweeper repainted the whole HUD strip and every board cell on any + click, even a single flag toggle; reveal now accumulates just the + board-cell range its flood fill touched and redraw skips the HUD/ + off-dirty cells accordingly, cutting the flicker. +- curve/porter-duff/gradient task content (a type label, a checkerboard, a + composited bitmap) was pinned to the window corner instead of tracking + the document scroll offset, so it stayed visually fixed while the rest + of the content scrolled underneath it. - `wuss_window_move()`'s fast path no longer slides pixels still queued in `wuss->dirty[]` from an earlier invalidation this frame onto the window's new position: pending dirty regions are stripped from the @@ -593,3 +883,34 @@ _Unreleased_ until one is cut. started a resize far from the visible handle. It now hands off to a scroll well only when that scrollbar's strip runs the full edge; every other edge pixel resolves to `TITLE` or `CONTENT`. +- The saturn task's stars/ring energy-gate exclusion thresholds were literal + constants tuned for `size==256`; since `p`'s range grows linearly with + size, a larger window shrank the exclusion disc relative to half and let + stars/ring points land inside the planet body. Scaled by + `size/SATURN_SIZE_DEFAULT`. +- `DPT-Digits-Bold` and `MS Sans Serif` bmfonts had an oversized space glyph + width; reduced. +- `wuss__slider_row_snap()`'s `CLAMP(v, min, max)` assumed `min <= max`, so a + reversed (fill-backwards) slider's `CLAMP` always returned `max` regardless + of drag position; clamps against `MIN(min,max)`/`MAX(min,max)` instead. +- `wuss_colourmenu_create()` handed out `wuss_colour_t` swatches up to + `wuss->npalette` with no upper bound, so a palette with more than 128 + entries produced swatches aliasing the symbolic/chrome-role colour + namespace; capped at `wuss_COLOUR_SYMBOLIC`. +- A disabled menu row's `wuss_MOUSE_MOVE` handling was skipped entirely, so + hovering off an open submenu onto a disabled sibling never ran the + close-on-move-away logic, leaving the stale submenu open; the disabled + check now only suppresses opening a submenu, not the move-away close. +- `wuss_dialogue_handle_icon()` lacked the `NULL`/action-kind guard every + other component's icon handler has, risking a crash for a task + multiplexing `ICON` dispatch across an optionally-created dialogue. +- `wuss__order_pieces()` built its full overlap graph even for a single + piece, which by construction cannot clobber itself; added an `n <= 1` fast + path, the common case on every plain move/resize/scroll blit. +- `scroll_sausage()`'s `CLAMP(sausage_px, WUSS_MIN_SAUSAGE, track_px)` + silently dropped below `WUSS_MIN_SAUSAGE` whenever the well was shorter + than the minimum sausage; the upper bound is now clamped against + `WUSS_MIN_SAUSAGE` too so the floor always wins. +- `wuss_proginfo_set_desc()` dereferenced a borrowed `desc` with no `NULL` + check; guarded, matching the idiom used elsewhere in the component + helpers. diff --git a/CLAUDE.md b/CLAUDE.md index feeb7c9..73681d9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -94,7 +94,8 @@ New source/header files must be added by hand to the relevant `set(..._SOURCES . - Section breaks within files use `/* ----- ... ----- */` rule comments. - Public API docs use Doxygen (`\file`, `\param`, `\return`); a `Doxyfile` exists for generating them. - Edit `.c`/`.h` files with the Edit tool, never `sed -i` line-range splices — they corrupt the Allman/2-space layout and can't be verified without re-reading. To inspect exact bytes or indentation, Read the file; don't shell out to `cat -A`/`cat -v`. -- After editing any `.c`/`.h` function prototype or definition, run `python3 tools/wrap_protos.py `; after editing a header's Doxygen, run `python3 tools/wrap_doxygen.py ` (skip vendored headers). +- After editing any `.c`/`.h` function prototype or definition, run `python3 tools/fmt/wrap_protos.py `; after editing a header's Doxygen, run `python3 tools/fmt/wrap_doxygen.py ` (skip vendored headers). +- Before committing any `.c`/`.h` changes, run `python3 tools/fmt/realign_decls.py ` (declaration-block alignment) alongside `wrap_protos.py`/`wrap_doxygen.py` on the same files. ## Commit messages diff --git a/CMakeLists.txt b/CMakeLists.txt index 2c54a5b..a63f486 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -92,6 +92,7 @@ set(PUBLIC_HEADERS include/framebuf/span-p1.h include/framebuf/span-p2.h include/framebuf/span-p4.h + include/framebuf/span-p8.h include/framebuf/span-registry.h include/framebuf/span-rgba8888.h include/framebuf/span-rgbx8888.h @@ -103,6 +104,7 @@ set(PUBLIC_HEADERS include/geom/packer.h include/geom/point.h include/geom/size.h + include/geom/stack.h include/io/dirscan.h include/io/namelist.h include/io/path.h @@ -130,6 +132,7 @@ set(PUBLIC_HEADERS if(WUSS_ICONS) list(APPEND PUBLIC_HEADERS include/wuss/icon.h) + list(APPEND PUBLIC_HEADERS include/wuss/icon-spec.h) endif() if(WUSS_MENUS) list(APPEND PUBLIC_HEADERS include/wuss/menu.h) @@ -140,6 +143,7 @@ if(WUSS_COMPONENTS) list(APPEND PUBLIC_HEADERS include/wuss/component/colourmenu.h) list(APPEND PUBLIC_HEADERS include/wuss/component/info.h) list(APPEND PUBLIC_HEADERS include/wuss/component/proginfo.h) + list(APPEND PUBLIC_HEADERS include/wuss/component/dialogue.h) endif() # The public headers must be set as properties of the library, not as @@ -286,6 +290,7 @@ set(FRAMEBUF_SOURCES libraries/framebuf/span/p1.c libraries/framebuf/span/p2.c libraries/framebuf/span/p4.c + libraries/framebuf/span/p8.c libraries/framebuf/span/rgbx8888.c libraries/framebuf/span/xbgr8888.c) @@ -308,7 +313,11 @@ set(GEOM_SOURCES libraries/geom/layout/layout.c libraries/geom/line/line.c libraries/geom/packer/impl.h - libraries/geom/packer/packer.c) + libraries/geom/packer/packer.c + libraries/geom/stack/impl.h + libraries/geom/stack/measure-min.c + libraries/geom/stack/solve.c + libraries/geom/stack/valid-tree.c) set(IO_SOURCES libraries/io/dirscan/dirscan.c @@ -371,9 +380,9 @@ set(WUSS_CORE_SOURCES libraries/wuss/core/backdrop.c libraries/wuss/core/create.c libraries/wuss/core/destroy.c - libraries/wuss/core/get-font.c libraries/wuss/core/get-palette.c libraries/wuss/core/get-pointer.c + libraries/wuss/core/get-resources.c libraries/wuss/core/idle.c libraries/wuss/core/impl.h libraries/wuss/core/invalidate.c @@ -386,7 +395,9 @@ set(WUSS_CORE_SOURCES libraries/wuss/core/scroll-step.c libraries/wuss/core/set-backdrop.c libraries/wuss/core/set-palette.c - libraries/wuss/core/text.c + libraries/wuss/font/font.h + libraries/wuss/font/registry.c + libraries/wuss/font/text.c libraries/wuss/core/task/create.c libraries/wuss/core/task/destroy.c libraries/wuss/core/task/set-autoclose.c @@ -433,11 +444,12 @@ set(WUSS_ICON_SOURCES libraries/wuss/icon/draw.c libraries/wuss/icon/free.c libraries/wuss/icon/from-spec.c + libraries/wuss/helpers/icon-spec.c libraries/wuss/icon/get-bbox.c libraries/wuss/icon/get-selected.c libraries/wuss/icon/get-text.c libraries/wuss/icon/get-type.c - libraries/wuss/icon/get-window.c + libraries/wuss/icon/get-value.c libraries/wuss/icon/hit-test.c libraries/wuss/icon/invalidate.c libraries/wuss/icon/plot.c @@ -447,6 +459,8 @@ set(WUSS_ICON_SOURCES libraries/wuss/icon/set-hover.c libraries/wuss/icon/set-selected.c libraries/wuss/icon/set-text.c + libraries/wuss/icon/set-value.c + libraries/wuss/icon/slider-box.c libraries/wuss/icon.h) set(WUSS_MENU_SOURCES @@ -458,7 +472,8 @@ set(WUSS_COMPONENT_SOURCES libraries/wuss/component/fontmenu.c libraries/wuss/component/colourmenu.c libraries/wuss/component/info.c - libraries/wuss/component/proginfo.c) + libraries/wuss/component/proginfo.c + libraries/wuss/component/dialogue.c) set(WUSS_SOURCES ${WUSS_CORE_SOURCES}) if(WUSS_FURNITURE) @@ -682,6 +697,7 @@ if(BUILD_TESTS) libraries/geom/box/test/box-test.c libraries/geom/layout/test/layout-test.c libraries/geom/packer/test/packer-test.c + libraries/geom/stack/test/stack-test.c libraries/io/stream/test/stream-test.c libraries/text/txtfmt/test/txtfmt-test.c libraries/utils/array/test/array-test.c diff --git a/apps/test/main.c b/apps/test/main.c index 8438fc2..babfe8e 100644 --- a/apps/test/main.c +++ b/apps/test/main.c @@ -61,6 +61,7 @@ static const test_t tests[] = { "box", box_test }, { "layout", layout_test }, { "packer", packer_test }, + { "stack", stack_test }, { "stream", stream_test }, diff --git a/apps/wuss/frontend-riscos.c b/apps/wuss/frontend-riscos.c index f783daa..be0dd17 100644 --- a/apps/wuss/frontend-riscos.c +++ b/apps/wuss/frontend-riscos.c @@ -213,8 +213,14 @@ static wuss_button_t mouse_buttons_to_wuss(int buttons) return b; } +/* INKEY scan codes for keys tested individually, outside g_keys[] below: F1 + * (checked with Shift to pick REDRAW_ALL vs GARBAGE) and Shift itself. */ +#define KEY_SCAN_F1 (-114) +#define KEY_SCAN_SHIFT (-1) + /* Keys the demo reacts to: negative INKEY scan code -> input kind. F4 and - * Escape both quit. */ + * Escape both quit. F1 is handled separately in wuss_frontend_poll, not in + * this table, so it can vary its kind with Shift. */ static const struct { int scan; @@ -223,7 +229,6 @@ static const struct g_keys[] = { { -113, wuss_INPUT_QUIT }, /* Escape */ - { -114, wuss_INPUT_REDRAW_ALL }, /* F1 */ { -116, wuss_INPUT_PIXEL_STRESS }, /* F3 */ { -117, wuss_INPUT_QUIT } /* F4 */ }; @@ -242,6 +247,7 @@ bool wuss_frontend_poll(wuss_frontend_t *fe, wuss_input_t *event) static unsigned int key_was; /* bit i = g_keys[i] was down last poll */ unsigned int key_now = 0; + unsigned int f1_bit = 1u << NELEMS(g_keys); /* one bit past the table */ int mx, my, buttons, t; int px, py; size_t i; @@ -251,6 +257,18 @@ bool wuss_frontend_poll(wuss_frontend_t *fe, wuss_input_t *event) for (i = 0; i < NELEMS(g_keys); i++) if (key_down(g_keys[i].scan)) key_now |= 1u << i; + if (key_down(KEY_SCAN_F1)) + key_now |= f1_bit; + + /* F1 is handled here rather than in g_keys[]: its kind depends on Shift, + * which a static table entry can't express. */ + if ((key_now & f1_bit) && !(key_was & f1_bit)) + { + key_was = key_now; + event->kind = key_down(KEY_SCAN_SHIFT) ? wuss_INPUT_GARBAGE + : wuss_INPUT_REDRAW_ALL; + return true; + } for (i = 0; i < NELEMS(g_keys); i++) { @@ -313,10 +331,13 @@ bool wuss_frontend_poll(wuss_frontend_t *fe, wuss_input_t *event) return false; } -void wuss_frontend_present(wuss_frontend_t *fe, const bitmap_t *bm) +void wuss_frontend_present(wuss_frontend_t *fe, + const bitmap_t *bm, + const box_t *dirty) { NOT_USED(fe); NOT_USED(bm); + NOT_USED(dirty); /* wuss drew straight into screen memory; just pace to the frame rate */ _swix(OS_Byte, _IN(0), 19); diff --git a/apps/wuss/frontend-sdl.c b/apps/wuss/frontend-sdl.c index 77f1d19..d876700 100644 --- a/apps/wuss/frontend-sdl.c +++ b/apps/wuss/frontend-sdl.c @@ -23,11 +23,12 @@ #include "frontend.h" /* Screen pixel format for the demo, chosen at run time via --depth: 32 = - * pixelfmt_bgrx8888 (feeds SDL directly, no per-frame conversion); 4 (the - * default) = pixelfmt_p4 paletted (exercises screen_copy_rect's nibble-packed - * blit path instead); 2 = pixelfmt_p2; 1 = pixelfmt_p1 monochrome. Paletted - * depths are converted to bgrx8888 per frame via bitmap_convert. Stashed in - * struct wuss_frontend so present() can branch on it. */ + * pixelfmt_bgrx8888 (feeds SDL directly, no per-frame conversion); 8 = + * pixelfmt_p8; 4 (the default) = pixelfmt_p4 paletted (exercises + * screen_copy_rect's nibble-packed blit path instead); 2 = pixelfmt_p2; 1 = + * pixelfmt_p1 monochrome. Paletted depths are converted to bgrx8888 per + * frame via bitmap_convert. Stashed in struct wuss_frontend so present() can + * branch on it. */ /* Integer window zoom: the fixed Wuss screen is drawn at this many device * pixels per screen pixel. The initial value comes from -scale (0 => use the @@ -47,7 +48,7 @@ struct wuss_frontend int scr_width; int scr_height; int scale; /* device pixels per screen pixel; see WUSS_SDL_*_SCALE */ - int depth; /* framebuffer bits per pixel: 32 (bgrx8888), 4 (p4), 2 (p2) or 1 (p1) */ + int depth; /* framebuffer bits per pixel: 32 (bgrx8888), 8 (p8), 4 (p4), 2 (p2) or 1 (p1) */ void *pixels; /* the private framebuffer handed to the caller */ }; @@ -100,9 +101,10 @@ result_t wuss_frontend_open(int width, NOT_USED(palette); NOT_USED(npalette); - if (depth != 1 && depth != 2 && depth != 4 && depth != 32) + if (depth != 1 && depth != 2 && depth != 4 && depth != 8 && depth != 32) { - fprintf(stderr, "Error: unsupported depth %d (want 1, 2, 4 or 32)\n", depth); + fprintf(stderr, + "Error: unsupported depth %d (want 1, 2, 4, 8 or 32)\n", depth); return result_BAD_ARG; } @@ -110,10 +112,7 @@ result_t wuss_frontend_open(int width, scale = WUSS_SDL_DEFAULT_SCALE; scale = CLAMP(scale, WUSS_SDL_MIN_SCALE, WUSS_SDL_MAX_SCALE); - stride = (depth == 32) ? width * 4 /* pixelfmt_bgrx8888: 4 bytes/pixel */ - : (depth == 4) ? width / 2 /* pixelfmt_p4: 2 pixels/byte */ - : (depth == 2) ? (width + 3) / 4 /* pixelfmt_p2: 4 pixels/byte */ - : (width + 7) / 8; /* pixelfmt_p1: 8 pixels/byte */ + stride = (width * depth + 7) >> 3; fe = calloc(1, sizeof(*fe)); if (fe == NULL) @@ -165,6 +164,7 @@ result_t wuss_frontend_open(int width, SDL_SetTextureScaleMode(fe->texture, SDL_SCALEMODE_NEAREST); *fmt = (depth == 32) ? pixelfmt_bgrx8888 + : (depth == 8) ? pixelfmt_p8 : (depth == 4) ? pixelfmt_p4 : (depth == 2) ? pixelfmt_p2 : pixelfmt_p1; @@ -202,36 +202,42 @@ bool wuss_frontend_poll(wuss_frontend_t *fe, wuss_input_t *event) return true; case SDL_EVENT_KEY_UP: - if (ev.key.key == SDLK_F4) - event->kind = wuss_INPUT_QUIT; - else if (ev.key.key == SDLK_F1 && (ev.key.mod & SDL_KMOD_SHIFT)) - event->kind = wuss_INPUT_GARBAGE; - else if (ev.key.key == SDLK_F1) - event->kind = wuss_INPUT_REDRAW_ALL; - else if (ev.key.key == SDLK_F3) - event->kind = wuss_INPUT_PIXEL_STRESS; - else if (ev.key.key == SDLK_F2) + switch (ev.key.key) { - int scale; - - /* F2 steps the SDL window zoom up, Shift-F2 down, clamped to - * [WUSS_SDL_MIN_SCALE, WUSS_SDL_MAX_SCALE]: a backend-local zoom the - * demo loop never sees. */ - scale = fe->scale + ((ev.key.mod & SDL_KMOD_SHIFT) ? -1 : 1); - if (scale < WUSS_SDL_MIN_SCALE) - scale = WUSS_SDL_MIN_SCALE; - else if (scale > WUSS_SDL_MAX_SCALE) - scale = WUSS_SDL_MAX_SCALE; - if (scale != fe->scale) + case SDLK_F1: + event->kind = (ev.key.mod & SDL_KMOD_SHIFT) + ? wuss_INPUT_GARBAGE + : wuss_INPUT_REDRAW_ALL; + break; + case SDLK_F2: { - fe->scale = scale; - SDL_SetWindowSize(fe->window, fe->scr_width * scale, - fe->scr_height * scale); + int scale; + + /* F2 steps the SDL window zoom up, Shift-F2 down, clamped to + * [WUSS_SDL_MIN_SCALE, WUSS_SDL_MAX_SCALE]: a backend-local zoom the + * demo loop never sees. */ + scale = fe->scale + ((ev.key.mod & SDL_KMOD_SHIFT) ? -1 : 1); + if (scale < WUSS_SDL_MIN_SCALE) + scale = WUSS_SDL_MIN_SCALE; + else if (scale > WUSS_SDL_MAX_SCALE) + scale = WUSS_SDL_MAX_SCALE; + if (scale != fe->scale) + { + fe->scale = scale; + SDL_SetWindowSize(fe->window, fe->scr_width * scale, + fe->scr_height * scale); + } + continue; } + case SDLK_F3: + event->kind = wuss_INPUT_PIXEL_STRESS; + break; + case SDLK_F4: + event->kind = wuss_INPUT_QUIT; + break; + default: continue; } - else - continue; return true; case SDL_EVENT_MOUSE_BUTTON_DOWN: @@ -287,7 +293,9 @@ bool wuss_frontend_poll(wuss_frontend_t *fe, wuss_input_t *event) } } -void wuss_frontend_present(wuss_frontend_t *fe, const bitmap_t *bm) +void wuss_frontend_present(wuss_frontend_t *fe, + const bitmap_t *bm, + const box_t *dirty) { if (fe->depth == 32) { @@ -295,20 +303,46 @@ void wuss_frontend_present(wuss_frontend_t *fe, const bitmap_t *bm) } else { - bitmap_t *disp; + result_t rc; + bitmap_t rows, *disp; + int y0, y1; + SDL_Rect rect; + + /* Sub-byte formats (p1/p2/p4) pack several pixels per byte, so only a + * whole-row crop is safe without redoing their bit-unpacking maths for an + * arbitrary x0; a dirty rect just narrows which rows get converted. */ + y0 = (dirty != NULL) ? CLAMP(dirty->y0, 0, bm->size.h) : 0; + y1 = (dirty != NULL) ? CLAMP(dirty->y1, 0, bm->size.h) : bm->size.h; + if (y1 <= y0) + goto present; + + rc = bitmap_init(&rows, + SIZE2D(bm->size.w, y1 - y0), + bm->format, + bm->rowbytes, + bm->palette, + (unsigned char *) bm->base + y0 * bm->rowbytes); + if (rc != result_OK) + goto present; /* wuss draws into a paletted bitmap; SDL wants bgrx. bitmap_convert reads * the palette straight off `bm`, which the caller updates when the palette * task's picker menu changes it, so a live palette change just shows up in * the next converted frame. */ - if (bitmap_convert(bm, pixelfmt_bgrx8888, &disp) == result_OK) + if (bitmap_convert(&rows, pixelfmt_bgrx8888, &disp) == result_OK) { - SDL_UpdateTexture(fe->texture, NULL, disp->base, disp->rowbytes); + rect.x = 0; + rect.y = y0; + rect.w = bm->size.w; + rect.h = y1 - y0; + + SDL_UpdateTexture(fe->texture, &rect, disp->base, disp->rowbytes); free(disp->base); free(disp); } } +present: SDL_RenderTexture(fe->renderer, fe->texture, NULL, NULL); SDL_RenderPresent(fe->renderer); diff --git a/apps/wuss/frontend.h b/apps/wuss/frontend.h index 4421d2e..52aa46a 100644 --- a/apps/wuss/frontend.h +++ b/apps/wuss/frontend.h @@ -11,6 +11,7 @@ #include "framebuf/bitmap.h" #include "framebuf/colour.h" #include "framebuf/pixelfmt.h" +#include "geom/box.h" #include "geom/point.h" #include "wuss/wuss.h" @@ -85,8 +86,15 @@ result_t wuss_frontend_open(int width, bool wuss_frontend_poll(wuss_frontend_t *frontend, wuss_input_t *event); /* Push the current framebuffer contents to the screen and pace the frame. A - * backend rendering straight into screen memory only waits for vsync here. */ -void wuss_frontend_present(wuss_frontend_t *frontend, const bitmap_t *bm); + * backend rendering straight into screen memory only waits for vsync here. + * + * `dirty`, if non-NULL, bounds the region that actually changed since the + * last present; a backend that has to convert `bm` (paletted -> display + * format) may use it to convert only that rect instead of the whole + * bitmap. NULL means "assume the whole bitmap changed". */ +void wuss_frontend_present(wuss_frontend_t *frontend, + const bitmap_t *bm, + const box_t *dirty); /* Push a new system palette to the physical palette, if the backend owns one. * Called after the palette task's picker menu changes the system palette. diff --git a/apps/wuss/main.c b/apps/wuss/main.c index 5a82d8c..316ef6d 100644 --- a/apps/wuss/main.c +++ b/apps/wuss/main.c @@ -36,6 +36,7 @@ #include "tasks.h" #include "tasks/palette.h" /* palette_load_hex for the startup *.hex */ +#include "tasks/saturn.h" /* saturn_create at startup */ /* ----------------------------------------------------------------------- */ @@ -69,6 +70,9 @@ static void fill_chrome_config(wuss_config_t *config, int use_wimp16) config->backdrop.pattern_bg = palette_WIMP16_GREY_50; config->body.window = palette_WIMP16_GREY_87; config->body.menu = palette_WIMP16_WHITE; + config->slider.track = palette_WIMP16_WHITE; + config->slider.value = palette_WIMP16_GREY_50; + config->slider.surround = wuss_NO_BACKGROUND; } else { @@ -93,6 +97,9 @@ static void fill_chrome_config(wuss_config_t *config, int use_wimp16) config->backdrop.pattern_bg = palette_PICO8_LIGHT_GREY; config->body.window = palette_PICO8_LIGHT_GREY; config->body.menu = palette_PICO8_WHITE; + config->slider.track = palette_PICO8_WHITE; + config->slider.value = palette_PICO8_DARK_GREY; + config->slider.surround = wuss_NO_BACKGROUND; } } @@ -138,16 +145,16 @@ struct wuss_frame_ctx static void wuss_frame(void *arg) { struct wuss_frame_ctx *c = arg; - wuss_input_t ev; - bool pixel_stress_pending = false; - bool garbage_pending = false; + wuss_input_t ev; + bool pixel_stress_pending = false; + bool garbage_pending = false; while (wuss_frontend_poll(c->frontend, &ev)) { switch (ev.kind) { case wuss_INPUT_QUIT: - g.quit = true; + g_tasks.quit = true; break; case wuss_INPUT_REDRAW_ALL: @@ -207,16 +214,39 @@ static void wuss_frame(void *arg) for (i = 0; i < n; i++) p[i] = (unsigned char) rand(); - wuss_frontend_present(c->frontend, c->bm); + wuss_frontend_present(c->frontend, c->bm, NULL); + } + else if (pixel_stress_pending) + { + pixel_stress(c->wuss, c->scr_width, c->scr_height); + wuss_frontend_present(c->frontend, c->bm, NULL); } else { - if (pixel_stress_pending) - pixel_stress(c->wuss, c->scr_width, c->scr_height); - else - wuss_redraw_dirty(c->wuss); + box_t dirty, region, touched; + int ndirty, i, have_touched, have_any; + + ndirty = wuss_get_dirty_count(c->wuss); + dirty = (box_t) BOX_INIT; + for (i = 0; i < ndirty; i++) + { + wuss_get_dirty(c->wuss, i, ®ion); + box_union(&dirty, ®ion, &dirty); + } - wuss_frontend_present(c->frontend, c->bm); + /* Pixels a fast blit path (window move/resize, scroll) slid to a new + * position without a repaint: wuss_redraw_dirty won't touch them, but + * present() still has to re-upload them -- fold them into the same + * bounding box before wuss_clear_touched drops them. */ + have_touched = wuss_get_touched_extent(c->wuss, &touched); + have_any = (ndirty > 0) || have_touched; + if (have_touched) + box_union(&dirty, &touched, &dirty); + + wuss_redraw_dirty(c->wuss); + wuss_clear_touched(c->wuss); + + wuss_frontend_present(c->frontend, c->bm, have_any ? &dirty : NULL); } #ifdef __EMSCRIPTEN__ @@ -244,27 +274,30 @@ static result_t run_wuss(const char *resources, int depth, int scale) { - const int scr_width = 640; - const int scr_height = 480; - - result_t rc; - const char *leafname; - const char *filename; - bmfont_t *fonts[WUSS_MAIN_NFONTS]; static const char *const names[WUSS_MAIN_NFONTS] = - { "Digits-Regular", "Digits-Bold", "Symbols" }; - int nfonts; - int i; - void *pixels; - int rowbytes; - pixelfmt_t fmt; - bitmap_t bm; - screen_t scr; - colour_t palette[16]; - wuss_t *wuss; - wuss_frontend_t *frontend; - bool use_wimp16; - int palette_index; + { "DPT-Digits-Regular", "DPT-Digits-Bold", "Symbols" }; + + const int scr_width = 640; + const int scr_height = 480; + result_t rc; + const char *leafname; + const char *filename; + bmfont_t *fonts[WUSS_MAIN_NFONTS]; + int nfonts; + int i; + void *pixels; + int rowbytes; + pixelfmt_t fmt; + bitmap_t bm; + screen_t scr; + colour_t palette[16]; /* the fixed-size UI palette */ + colour_t scr_palette[256]; /* palette[] padded out to whatever + * count the chosen depth's bitmap + * needs (p8 reads all 256) */ + wuss_t *wuss; + wuss_frontend_t *frontend; + bool use_wimp16; + int palette_index; { /* palette_name (from -palette, default "PICO-8") names a *.hex file under @@ -282,8 +315,6 @@ static result_t run_wuss(const char *resources, palette_name, rc, result_string(rc)); goto Failure; } - - g.palette_name = palette_name; } logf_info("wuss: resources root = \"%s\"", resources); @@ -313,8 +344,16 @@ static result_t run_wuss(const char *resources, if (rc != result_OK) goto Failure; - rc = bitmap_init(&bm, SIZE2D(scr_width, scr_height), fmt, rowbytes, palette, - pixels); + /* bitmap_set_palette reads every entry a paletted format's bitmap_init + * needs (up to 256 for p8); pad scr_palette with palette's 16 UI colours, + * then the web-safe 216 (so a p8 screen has real range beyond the UI + * colours for nearest-match), then black for what's left. Also done on + * a live palette change; see task_handle_event. */ + tasks_build_screen_palette(scr_palette, NELEMS(scr_palette), + palette, NELEMS(palette)); + + rc = bitmap_init(&bm, SIZE2D(scr_width, scr_height), fmt, rowbytes, + scr_palette, pixels); logf_info("wuss: bitmap_init -> rc=0x%X (%s)", rc, result_string(rc)); if (rc != result_OK) goto Failure; @@ -340,20 +379,15 @@ static result_t run_wuss(const char *resources, } rc = wuss_create(&scr, descs, nfonts, palette, NELEMS(palette), &config, - NULL, &wuss); + NULL, resources, &wuss); logf_info("wuss: wuss_create -> rc=0x%X (%s)", rc, result_string(rc)); if (rc != result_OK) goto Failure; } - g.wuss = wuss; - g.palette = palette; - g.npalette = NELEMS(palette); - g.resources = resources; - g.daydream_font = fonts[0]; /* tasks draw with the regular weight */ - g.bold_font = fonts[1]; - g.frontend = frontend; - g.bm = &bm; + g_tasks.wuss = wuss; + g_tasks.frontend = frontend; + g_tasks.bm = &bm; { wuss_task_desc_t desc; @@ -361,14 +395,35 @@ static result_t run_wuss(const char *resources, desc.handle = task_handle_event; desc.task_data = NULL; desc.name = "menu"; - rc = wuss_task_create(wuss, &desc, &g.menu_task); + rc = wuss_task_create(wuss, &desc, &g_tasks.menu_task); logf_info("wuss: wuss_task_create(menu) -> rc=0x%X (%s)", rc, result_string(rc)); if (rc != result_OK) goto Failure; } - g.quit = false; + { + /* The "Info" task-menu row's shared dialogue. Hung off g_task_menu as a + * wuss_menu_item_t.window in tasks.c. A create failure is non-fatal -- + * the row just does nothing. */ + static const wuss_proginfo_desc_t desc = + { + "Wuss demo", + "Window manager test environment", + "(c) DPTLib contributors", + "1.0 (" __DATE__ ")" + }; + + if (wuss_proginfo_create(&g_tasks.proginfo, g_tasks.menu_task, &desc) != result_OK) + g_tasks.proginfo = NULL; + } + + g_tasks.quit = false; + + rc = saturn_create(wuss, NULL); + logf_info("wuss: saturn_create -> rc=0x%X (%s)", rc, result_string(rc)); + if (rc != result_OK) + goto Failure; wuss_redraw(wuss); @@ -392,7 +447,7 @@ static result_t run_wuss(const char *resources, * for requestAnimationFrame pacing. */ emscripten_set_main_loop_arg(wuss_frame, &ctx, 0, 1); #else - while (!g.quit) + while (!g_tasks.quit) wuss_frame(&ctx); #endif } @@ -401,6 +456,7 @@ static result_t run_wuss(const char *resources, * frees every registered task node, but not the per-instance task_data * block a spawn_* calloc'd, so any task window left open at quit leaks that * block. Harmless at process exit. */ + wuss_proginfo_destroy(g_tasks.proginfo); /* closes its dialogue window */ wuss_destroy(wuss); /* also sweeps g.menu_task and closes any open chain */ for (i = 0; i < nfonts; i++) @@ -426,14 +482,14 @@ typedef struct wuss_options { const char *resources; /* -r/--resources: fixture root */ const char *palette_name; /* -p/--palette: startup *.hex leafname */ - int depth; /* -d/--depth: framebuffer bpp (1, 2, 4 or 32) */ + int depth; /* -d/--depth: framebuffer bpp (1, 2, 4, 8 or 32) */ int scale; /* -s/--scale: initial window zoom, 0 = default */ } wuss_options_t; static const char wuss_usage[] = "usage: wuss [-r|--resources DIR] [-p|--palette NAME] " - "[-d|--depth 1|2|4|32] [-s|--scale N]\n"; + "[-d|--depth 1|2|4|8|32] [-s|--scale N]\n"; #ifndef __riscos diff --git a/apps/wuss/tasks.c b/apps/wuss/tasks.c index 66b9a57..414794c 100644 --- a/apps/wuss/tasks.c +++ b/apps/wuss/tasks.c @@ -8,7 +8,7 @@ #include "base/utils.h" #include "framebuf/bitmap.h" #include "framebuf/colour.h" -#include "io/path.h" +#include "framebuf/pixelfmt.h" #include "wuss/task.h" #include "wuss/wuss.h" #include "wuss/window.h" @@ -38,306 +38,196 @@ /* ----------------------------------------------------------------------- */ -struct wuss_app_tasks g; +struct wuss_app_tasks g_tasks; -/* Each spawn allocates a fresh per-instance task block so a task may run in - * several windows at once; the block is owned by its window and freed by the - * task's wuss_EVENT_QUIT handler. On any create failure X_create has already - * torn down whatever it built and freed the block itself, so the only block - * the spawner frees is the font-less chars case: create returns OK but opens - * no window, leaving the block with no owner. */ - -static result_t spawn_ball(void) +void tasks_build_screen_palette(colour_t *out, + int nout, + const colour_t *ui, + int nui) { - ball_task_t *t = calloc(1, sizeof(*t)); - result_t rc; - if (t == NULL) return result_OOM; - rc = ball_create(g.wuss, t); - if (rc != result_OK) return rc; - if (t->window == NULL) { free(t); return rc; } - return result_OK; -} + int r, g, b, n; -static result_t spawn_text(void) -{ - text_task_t *t = calloc(1, sizeof(*t)); - result_t rc; - if (t == NULL) return result_OOM; - rc = text_create(g.wuss, g.resources, t); - if (rc != result_OK) return rc; - if (t->window == NULL) { free(t); return rc; } - return result_OK; -} + memset(out, 0, nout * sizeof(*out)); -static result_t spawn_blank(void) -{ - blank_task_t *t = calloc(1, sizeof(*t)); - result_t rc; - if (t == NULL) return result_OOM; - rc = blank_create(g.wuss, t); - if (rc != result_OK) return rc; - if (t->window == NULL) { free(t); return rc; } - return result_OK; -} + n = MIN(nui, nout); + memcpy(out, ui, n * sizeof(*out)); -static result_t spawn_chars(void) -{ - chars_task_t *t = calloc(1, sizeof(*t)); - result_t rc; - if (t == NULL) return result_OOM; - rc = chars_create(g.wuss, g.resources, t); - if (rc != result_OK) return rc; - if (t->window == NULL) { free(t); return rc; } - return result_OK; + for (r = 0; r < 6 && n < nout; r++) + for (g = 0; g < 6 && n < nout; g++) + for (b = 0; b < 6 && n < nout; b++) + out[n++] = colour_rgb(r * 0x33, g * 0x33, b * 0x33); } -static result_t spawn_palette(void) -{ - palette_task_t *t = calloc(1, sizeof(*t)); - result_t rc; - if (t == NULL) return result_OOM; - rc = palette_create(g.wuss, g.resources, g.palette_name, t); - if (rc != result_OK) return rc; - if (t->window == NULL) { free(t); return rc; } - return result_OK; -} +/* Every demo task's X_create now shares one shape: (wuss_t *, X_task_t **). + * The task block it allocates is owned by its window and freed by + * X_destroy, called from the task's wuss_EVENT_QUIT handler; callers here + * never need the block back, so every X_create is invoked with NULL for it, + * which is type-compatible however the second parameter is spelt. That lets + * a single generic spawn walk a table of {name, X_create} instead of one + * hand-written wrapper per task. */ +typedef result_t (*task_create_fn_t)(wuss_t *wuss, void **out); -static result_t spawn_image(void) +static result_t spawn_task(const char *name, task_create_fn_t create) { - result_t rc; - image_task_t *t; - const char *leafname; - const char *filename; - char buf[DPTLIB_MAXPATH]; - char ninepatch[DPTLIB_MAXPATH]; - - t = calloc(1, sizeof(*t)); - if (t == NULL) return result_OOM; - - leafname = path_join_leafname("jessica", "png"); - filename = path_join_filename(g.resources, 3, "resources", "images", leafname); - strcpy(buf, filename); - /* path_join_filename returns a shared static buffer; copy before the next - * call (image_create's own dirscan join) clobbers it */ - filename = path_join_filename(g.resources, 3, "resources", "wuss", - path_join_leafname("ninepatch", "png")); - strcpy(ninepatch, filename); - - logf_info("wuss: image task loading \"%s\" + \"%s\"", buf, ninepatch); - rc = image_create(g.wuss, g.resources, buf, ninepatch, t); + result_t rc; + + rc = create(g_tasks.wuss, NULL); if (rc != result_OK) - logf_error("wuss: image_create(\"%s\") failed, rc=0x%X (%s)", buf, rc, + logf_error("wuss: %s_create failed, rc=0x%X (%s)", name, rc, result_string(rc)); - if (rc != result_OK) return rc; - if (t->window == NULL) { free(t); return rc; } - return result_OK; -} - -static result_t spawn_checker(void) -{ - checker_task_t *t = calloc(1, sizeof(*t)); - result_t rc; - if (t == NULL) return result_OOM; - rc = checker_create(g.wuss, t); - if (rc != result_OK) return rc; - if (t->window == NULL) { free(t); return rc; } - return result_OK; -} - -static result_t spawn_clock(void) -{ - clock_task_t *t = calloc(1, sizeof(*t)); - result_t rc; - if (t == NULL) return result_OOM; - rc = clock_create(g.wuss, g.daydream_font, t); - if (rc != result_OK) return rc; - if (t->window == NULL) { free(t); return rc; } - return result_OK; -} - -static result_t spawn_curve(void) -{ - curve_task_t *t = calloc(1, sizeof(*t)); - result_t rc; - if (t == NULL) return result_OOM; - rc = curve_create(g.wuss, t); - if (rc != result_OK) return rc; - if (t->window == NULL) { free(t); return rc; } - return result_OK; + return rc; } -static result_t spawn_lissajous(void) -{ - lissajous_task_t *t = calloc(1, sizeof(*t)); - result_t rc; - if (t == NULL) return result_OOM; - rc = lissajous_create(g.wuss, t); - if (rc != result_OK) return rc; - if (t->window == NULL) { free(t); return rc; } - return result_OK; -} +/* 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]. */ +typedef result_t (*task_spawn_fn_t)(void); -static result_t spawn_minesweeper(void) +/* Category submenus, hung directly off the top-level task menu (see + * g_task_items below): g_*_items[i] and g_*_tasks[i] are picked by the same + * menu row index i -- keep each pair's tables in that order. Each category + * menu is dispatched in task_handle_event by matching + * event->data.menu_select.menu against the category's g_*_menu address. */ +static const struct { - minesweeper_task_t *t = calloc(1, sizeof(*t)); - result_t rc; - if (t == NULL) return result_OOM; - rc = minesweeper_create(g.wuss, g.bold_font, t); - if (rc != result_OK) return rc; - if (t->window == NULL) { free(t); return rc; } - return result_OK; + const char *name; + task_create_fn_t create; } - -static result_t spawn_saturn(void) +g_games_tasks[] = { - 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) + { "Ball", (task_create_fn_t) ball_create }, + { "Minesweeper", (task_create_fn_t) minesweeper_create } +}, +g_tests_tasks[] = { - sofa_task_t *t = calloc(1, sizeof(*t)); - result_t rc; - if (t == NULL) return result_OOM; - rc = sofa_create(g.wuss, t); - if (rc != result_OK) return rc; - if (t->window == NULL) { free(t); return rc; } - return result_OK; -} - -static result_t spawn_gradient(void) + { "Checker", (task_create_fn_t) checker_create }, + { "Curve", (task_create_fn_t) curve_create }, + { "Greeble", (task_create_fn_t) greeble_create }, + { "Icons", (task_create_fn_t) icons_create }, + { "Porter-Duff", (task_create_fn_t) porter_duff_create }, + { "Sofa", (task_create_fn_t) sofa_create }, + { "Text", (task_create_fn_t) text_create } +}, +g_utilities_tasks[] = { - gradient_task_t *t = calloc(1, sizeof(*t)); - result_t rc; - if (t == NULL) return result_OOM; - rc = gradient_create(g.wuss, t); - if (rc != result_OK) return rc; - if (t->window == NULL) { free(t); return rc; } - return result_OK; -} - -static result_t spawn_greeble(void) + { "Chars", (task_create_fn_t) chars_create }, + { "Clock", (task_create_fn_t) clock_create }, + { "Palette", (task_create_fn_t) palette_create }, + { "Swatches", (task_create_fn_t) swatches_create } +}, +g_visuals_tasks[] = { - greeble_task_t *t = calloc(1, sizeof(*t)); - result_t rc; - if (t == NULL) return result_OOM; - rc = greeble_create(g.wuss, t); - if (rc != result_OK) return rc; - if (t->window == NULL) { free(t); return rc; } - return result_OK; -} + { "Blank", (task_create_fn_t) blank_create }, + { "Gradient", (task_create_fn_t) gradient_create }, + { "Image", (task_create_fn_t) image_create }, + { "Lissajous", (task_create_fn_t) lissajous_create }, + { "Saturn", (task_create_fn_t) saturn_create } +}; -static result_t spawn_icons(void) +static const wuss_menu_item_t g_games_items[] = { - icons_task_t *t = calloc(1, sizeof(*t)); - result_t rc; - if (t == NULL) return result_OOM; - rc = icons_create(g.wuss, g.daydream_font, g.resources, t); - if (rc != result_OK) - logf_error("wuss: icons_create (resources \"%s\") failed, rc=0x%X (%s)", - g.resources, rc, result_string(rc)); - if (rc != result_OK) return rc; - if (t->window == NULL) { free(t); return rc; } - return result_OK; -} + { "Ball", wuss_MENU_ITEM_NONE, NULL }, + { "Minesweeper", wuss_MENU_ITEM_NONE, NULL } +}; -static result_t spawn_swatches(void) +static const wuss_menu_item_t g_tests_items[] = { - swatches_task_t *t = calloc(1, sizeof(*t)); - result_t rc; - if (t == NULL) return result_OOM; - rc = swatches_create(g.wuss, t); - if (rc != result_OK) return rc; - if (t->window == NULL) { free(t); return rc; } - return result_OK; -} + { "Checker", wuss_MENU_ITEM_NONE, NULL }, + { "Curve", wuss_MENU_ITEM_NONE, NULL }, + { "Greeble", wuss_MENU_ITEM_NONE, NULL }, + { "Icons", wuss_MENU_ITEM_NONE, NULL }, + { "Porter-Duff", wuss_MENU_ITEM_NONE, NULL }, + { "Sofa", wuss_MENU_ITEM_NONE, NULL }, + { "Text", wuss_MENU_ITEM_NONE, NULL } +}; -static result_t spawn_porter_duff(void) +static const wuss_menu_item_t g_utilities_items[] = { - porter_duff_task_t *t = calloc(1, sizeof(*t)); - result_t rc; - if (t == NULL) return result_OOM; - rc = porter_duff_create(g.wuss, g.palette, g.daydream_font, g.resources, t); - if (rc != result_OK) - logf_error("wuss: porter_duff_create (resources \"%s\") failed, " - "rc=0x%X (%s)", g.resources, rc, result_string(rc)); - if (rc != result_OK) return rc; - if (t->window == NULL) { free(t); return rc; } - return result_OK; -} - -/* 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]. */ -typedef result_t (*task_spawn_fn_t)(void); + { "Chars", wuss_MENU_ITEM_NONE, NULL }, + { "Clock", wuss_MENU_ITEM_NONE, NULL }, + { "Palette", wuss_MENU_ITEM_NONE, NULL }, + { "Swatches", wuss_MENU_ITEM_NONE, NULL } +}; -/* "Launch" submenu: the demo tasks. */ -static const wuss_menu_item_t g_launch_items[] = +static const wuss_menu_item_t g_visuals_items[] = { - { "Ball", wuss_MENU_ITEM_NONE, NULL }, { "Blank", wuss_MENU_ITEM_NONE, NULL }, - { "Chars", wuss_MENU_ITEM_NONE, NULL }, - { "Checker", wuss_MENU_ITEM_NONE, NULL }, - { "Clock", wuss_MENU_ITEM_NONE, NULL }, - { "Curve", wuss_MENU_ITEM_NONE, NULL }, { "Gradient", wuss_MENU_ITEM_NONE, NULL }, - { "Greeble", wuss_MENU_ITEM_NONE, NULL }, - { "Icons", wuss_MENU_ITEM_NONE, NULL }, { "Image", wuss_MENU_ITEM_NONE, NULL }, { "Lissajous", wuss_MENU_ITEM_NONE, NULL }, - { "Minesweeper", wuss_MENU_ITEM_NONE, NULL }, - { "Palette", wuss_MENU_ITEM_NONE, NULL }, - { "Porter-Duff", wuss_MENU_ITEM_NONE, NULL }, - { "Saturn", wuss_MENU_ITEM_NONE, NULL }, - { "Sofa", wuss_MENU_ITEM_NONE, NULL }, - { "Swatches", wuss_MENU_ITEM_NONE, NULL }, - { "Text", wuss_MENU_ITEM_NONE, NULL } + { "Saturn", wuss_MENU_ITEM_NONE, NULL } +}; + +static const wuss_menu_t g_games_menu = +{ + "Games", g_games_items, NELEMS(g_games_items) +}; + +static const wuss_menu_t g_tests_menu = +{ + "Tests", g_tests_items, NELEMS(g_tests_items) }; -static const task_spawn_fn_t g_launch_spawn[] = +static const wuss_menu_t g_utilities_menu = { - spawn_ball, spawn_blank, spawn_chars, spawn_checker, spawn_clock, spawn_curve, - spawn_gradient, spawn_greeble, spawn_icons, spawn_image, spawn_lissajous, - spawn_minesweeper, spawn_palette, - spawn_porter_duff, spawn_saturn, spawn_sofa, spawn_swatches, spawn_text + "Utilities", g_utilities_items, NELEMS(g_utilities_items) }; -static const wuss_menu_t g_launch_menu = +static const wuss_menu_t g_visuals_menu = { - "Launch", g_launch_items, NELEMS(g_launch_items) + "Visuals", g_visuals_items, NELEMS(g_visuals_items) }; static result_t spawn_quit(void) { - g.quit = true; + g_tasks.quit = true; return result_OK; } -static const wuss_menu_item_t g_task_items[] = +/* Indices into g_task_items / g_task_spawn -- keep both tables in this + * order. */ +enum { - { "Launch", wuss_MENU_ITEM_NONE, &g_launch_menu, NULL }, - { "Quit Wuss", wuss_MENU_ITEM_DASHED, NULL, NULL } + TASK_ITEM_INFO, + TASK_ITEM_GAMES, + TASK_ITEM_TESTS, + TASK_ITEM_UTILITIES, + TASK_ITEM_VISUALS, + TASK_ITEM_QUIT +}; + +/* g_task_items' "Info" row's .window is filled in by tasks_open_launcher + * (built from g.proginfo, which does not exist until run_wuss creates it) -- + * the table itself cannot name it at compile time. */ +static wuss_menu_item_t g_task_items[] = +{ + { "Info", wuss_MENU_ITEM_NONE, NULL, NULL }, + { "Games", wuss_MENU_ITEM_NONE, &g_games_menu, NULL }, + { "Tests", wuss_MENU_ITEM_NONE, &g_tests_menu, NULL }, + { "Utilities", wuss_MENU_ITEM_NONE, &g_utilities_menu, NULL }, + { "Visuals", wuss_MENU_ITEM_NONE, &g_visuals_menu, NULL }, + { "Quit Wuss", wuss_MENU_ITEM_NONE, NULL, NULL } }; static const task_spawn_fn_t g_task_spawn[] = { - NULL, /* "Launch" -> submenu g_launch_menu */ + NULL, /* "Info" -> wuss_menu_item_t.window leaf, no spawn */ + NULL, /* "Games" -> submenu g_games_menu */ + NULL, /* "Tests" -> submenu g_tests_menu */ + NULL, /* "Utilities" -> submenu g_utilities_menu */ + NULL, /* "Visuals" -> submenu g_visuals_menu */ spawn_quit }; static const wuss_menu_t g_task_menu = { - "Tasks", g_task_items, NELEMS(g_task_items) + "Wuss", g_task_items, NELEMS(g_task_items) }; -/* g_task_menu / g_launch_menu picks are dispatched by index. Every menu is - * opened by g.menu_task, so one handler sees every wuss_EVENT_MENU_SELECT and - * tells them apart by data.menu_select.menu. */ +/* g_task_menu picks are dispatched by index; g_games_menu/g_tests_menu/ + * g_utilities_menu/g_visuals_menu picks by pointer identity below. Every + * menu is opened by g.menu_task, so one handler sees every + * wuss_EVENT_MENU_SELECT and tells them apart by data.menu_select.menu. */ result_t task_handle_event(wuss_window_t *window, const wuss_event_t *event, void *task_data) @@ -345,7 +235,6 @@ result_t task_handle_event(wuss_window_t *window, const wuss_menu_t *menu; int index; - NOT_USED(window); NOT_USED(task_data); if (event->kind == wuss_EVENT_PALETTE) @@ -353,13 +242,30 @@ result_t task_handle_event(wuss_window_t *window, /* wuss_set_palette already updated wuss's own copy and re-cached * furniture; read the new array back and push it on to the framebuffer * bitmap and any physical palette, so callers (e.g. the palette picker) - * never need to know the frontend exists */ + * never need to know the frontend exists. bitmap_set_palette reads + * every entry g.bm's own format needs (up to 256 for p8), not just + * wuss's fixed-size UI palette, so pad out to match -- same as the + * startup array run_wuss builds. */ const colour_t *palette; - int npalette; + int npalette; + colour_t scr_palette[256]; + int scr_nentries; + + palette = wuss_get_palette(g_tasks.wuss, &npalette); + scr_nentries = pixelfmt_paletted_nentries(g_tasks.bm->format); + if (scr_nentries > 0) + { + tasks_build_screen_palette(scr_palette, scr_nentries, palette, npalette); + bitmap_set_palette(g_tasks.bm, scr_palette); + } + wuss_frontend_set_palette(g_tasks.frontend, palette, npalette); + return result_OK; + } - palette = wuss_get_palette(g.wuss, &npalette); - bitmap_set_palette(g.bm, palette); - wuss_frontend_set_palette(g.frontend, palette, npalette); + if (event->kind == wuss_EVENT_PRE_SHOW) + { + if (window == wuss_proginfo_window(g_tasks.proginfo)) + return wuss_proginfo_handle_pre_show(g_tasks.proginfo); return result_OK; } @@ -369,10 +275,33 @@ result_t task_handle_event(wuss_window_t *window, menu = event->data.menu_select.menu; index = event->data.menu_select.index; - if (menu == &g_launch_menu) + if (menu == &g_games_menu) + { + if (index >= 0 && index < (int) NELEMS(g_games_tasks)) + (void) spawn_task(g_games_tasks[index].name, g_games_tasks[index].create); + return result_OK; + } + + if (menu == &g_tests_menu) + { + if (index >= 0 && index < (int) NELEMS(g_tests_tasks)) + (void) spawn_task(g_tests_tasks[index].name, g_tests_tasks[index].create); + return result_OK; + } + + if (menu == &g_utilities_menu) { - if (index >= 0 && index < (int) NELEMS(g_launch_spawn)) - (void) g_launch_spawn[index](); + if (index >= 0 && index < (int) NELEMS(g_utilities_tasks)) + (void) spawn_task(g_utilities_tasks[index].name, + g_utilities_tasks[index].create); + return result_OK; + } + + if (menu == &g_visuals_menu) + { + if (index >= 0 && index < (int) NELEMS(g_visuals_tasks)) + (void) spawn_task(g_visuals_tasks[index].name, + g_visuals_tasks[index].create); return result_OK; } @@ -388,5 +317,7 @@ result_t task_handle_event(wuss_window_t *window, result_t tasks_open_launcher(point_t pos) { - return wuss_menu_open(g.menu_task, &g_task_menu, pos, NULL); + g_task_items[TASK_ITEM_INFO].window = wuss_proginfo_window(g_tasks.proginfo); + + return wuss_menu_open(g_tasks.menu_task, &g_task_menu, pos, NULL); } diff --git a/apps/wuss/tasks.h b/apps/wuss/tasks.h index 08aa8c5..436524e 100644 --- a/apps/wuss/tasks.h +++ b/apps/wuss/tasks.h @@ -6,11 +6,11 @@ #include #include "framebuf/bitmap.h" -#include "framebuf/bmfont.h" #include "framebuf/colour.h" #include "geom/point.h" #include "wuss/task.h" #include "wuss/wuss.h" +#include "wuss/component/proginfo.h" #include "frontend.h" @@ -23,19 +23,14 @@ extern struct wuss_app_tasks { wuss_t *wuss; 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 - * picker menu's initial selection */ - const char *resources; - bmfont_t *daydream_font; - bmfont_t *bold_font; + wuss_proginfo_t *proginfo; /* shared "Info" dialogue on menu_task; see + * tasks_open_launcher's "Info" row */ bool quit; /* set by the "Quit Wuss" task-menu entry */ wuss_frontend_t *frontend; /* pushed to on wuss_EVENT_PALETTE; see * task_handle_event */ bitmap_t *bm; /* framebuffer bitmap, likewise */ } -g; +g_tasks; /* the menu-task event handler: dispatches every wuss_EVENT_MENU_SELECT and * relays wuss_EVENT_PALETTE to the frontend. Passed as wuss_task_desc.handle @@ -48,4 +43,14 @@ result_t task_handle_event(wuss_window_t *window, * MENU click over bare backdrop */ result_t tasks_open_launcher(point_t pos); +/* Fill out[0..nout-1]: ui[0..nui-1] copied in, then as much of the web-safe + * 216 (6x6x6 cube, steps of 0x33) as fits, then black for whatever is left. + * Used to build a paletted screen's full-size palette (nout = whatever + * bitmap_set_palette will read for the screen's format) from wuss's + * fixed-size UI palette, both at startup and on a live wuss_EVENT_PALETTE. */ +void tasks_build_screen_palette(colour_t *out, + int nout, + const colour_t *ui, + int nui); + #endif /* WUSS_APP_TASKS_H */ diff --git a/docs/framebuf/bmfont.md b/docs/framebuf/bmfont.md index 025686f..b595f8e 100644 --- a/docs/framebuf/bmfont.md +++ b/docs/framebuf/bmfont.md @@ -2,11 +2,11 @@ "bmfont" is a sub-library of DPTLib for drawing proportionally spaced bitmap fonts. It reads font definitions from PNG files like this: -![Henry Font](../resources/bmfonts/henry.png) +![DPT-Henry Font](../resources/bmfonts/DPT-Henry.png) or this: -![Digits Font](../resources/bmfonts/Digits-Regular.png) +![DPT-Digits Font](../resources/bmfonts/DPT-Digits-Regular.png) or even this: @@ -20,9 +20,16 @@ or even this: ## PNG Font Format -The PNG should be 32 characters wide: bmfont works out the character dimensions from the total PNG size. +The PNG should be 32 characters wide: bmfont works out the character dimensions from the total PNG size. It should be a paletted (colour type 3), 2 bits-per-pixel image with a four-entry palette: -It should be a four colour PNG where pixels of value 1 are font definitions and pixels of value 2 are advance widths. Note: An 8bpp 4-palette-entry image won't do, it must be 4bpp. +- 0 -- background +- 1 -- glyph ink +- 2 -- advance-width marker (row 0 of each glyph cell: one pixel per column, present wherever that column's glyph should draw, sets the advance width) +- 3 -- grid/baseline (see below) + +Pixel value 3 draws the cell's left sidebearing line (cosmetic only) plus two full-width rows that `bmfont_create` reads back to work out `ascent` and `descent`: one at the font's baseline, one at the cell bottom. It scans the space glyph's cell (grid column 0, which carries no ink to interfere) for the first two full-width rows of value-3 pixels within the cell body; the first is the baseline (its offset from the top of the cell is the ascent) and the second is the cell bottom (its offset from the baseline is the descent). A font predating this convention (no such rows found) falls back to treating the whole cell as ascent, with zero descent. + +`tools/ttf2bmfont.py` generates conforming PNGs from a TTF automatically. `--no-grid` omits all the value-3 pixels, baseline row included -- don't pass it, or `bmfont_create` will fall back to the no-descender default for that font. ## Setup @@ -69,7 +76,17 @@ result_t bmfont_draw(bmfont_t *bmfont, It requires a font handle, the screen to draw to, a pointer to some text, the number of characters to consider, foreground and background colours, and a start position. It returns an end position (optional - pass `NULL` if not required). -The screen origin is at the top left. +The screen origin is at the top left. `pos` and `end_pos` are baseline positions, not the top-left of the glyph cells -- use `bmfont_get_info()`'s `ascent` out-param to convert from a top-left layout position (`baseline_y = top_y + ascent`). + +```C +void bmfont_get_info(bmfont_t *bmfont, + int *width, + int *height, + int *ascent, + int *descent); +``` + +Returns the font's cell width, cell height, ascent and descent (any may be `NULL` if not required). ## Limitations diff --git a/docs/windowing/stack-sketch.md b/docs/windowing/stack-sketch.md new file mode 100644 index 0000000..a66e038 --- /dev/null +++ b/docs/windowing/stack-sketch.md @@ -0,0 +1,294 @@ +# Box-stack layout (design sketch) + +Design notes for the `stack` module. **Implemented** as `include/geom/stack.h` +/ `libraries/geom/stack/`; kept here as the design record. + +## Motivation + +Laying out the *contents* of a window: icons, labels, buttons and fields +arranged into a dialog. This is client-side layout — the stuff a window's +owner draws inside the work area — not window furniture. `wuss`'s own +furniture (titlebar, close and toggle buttons, scrollbars) stays as the +per-item formulas in `libraries/wuss/furniture/*.c` and is out of scope for +this module. + +Without something like this, every dialog hand-codes an inset chain per icon: +readable for three icons, a maintenance sink for thirty, and impossible to +reflow when the window resizes or the font metrics change. A box stack turns a +dialog into a nested description that the solver turns into boxes. + +Not built yet. Build it when the first real dialog needs it. + +## Relationship to `geom/layout` + +There is already a `geom/layout.h` (`layout_place`). It is a **line-flow** +layout: an element list of boxes and `NEWLINE`s, placed by `geom/packer`, +wrapping like text. One `spacing` and one `leading` scalar for the whole spec, +no nesting, no flex. + +This sketch is a different tool — a **box-stack solver**: nested HBox/VBox, +flex weights, cross-axis alignment, per-edge padding. The Monitor dialog below +(aligned label column, nested group box, spread button row) is not expressible +as line-flow. Hence a separate module with a separate name; `layout_place` +stays as-is for flowing text runs. + +## Scope + +In: + +- Nested horizontal and vertical stacks. +- Per-item fixed size, or a flex weight for sharing leftover space. +- Per-item main-axis `min` / `max` clamps. +- Cross-axis alignment (start, centre, end, fill). +- Spacers (a flex item with no output consumer). +- Container inner padding (per edge) and inter-child gap. +- Integer geometry throughout, remainder pixels distributed so child extents + sum exactly to the parent. + +Out (add only when a real layout needs it, each its own follow-up): + +- Shared column widths across independent rows (a label column where every + field starts at the same x). Nested stacks solve each row alone, so the + caller pre-measures: loop over the labels, find the widest, set every label + leaf to that fixed `size`. It is a handful of lines at the call site and + needs nothing from the module. If callers keep writing that loop, the clean + fix is a `GRID` kind with a max-content track — real code, deferred until a + caller exists. +- Grid with row/column spans and per-track sizing. Uniform grids are + expressible as a stack of stacks; true 2D alignment across rows is not, and + that is the trigger to add a real `GRID` kind. +- Linear constraints (`a.right == b.left + 8` solved by Cassowary-style + simplex). Only if a layout genuinely cannot be nested into stacks. +- A retained node tree the caller mutates and re-solves. `wuss` already owns + window state and dirty-region redraw; a second tree to keep in sync is not + worth it. +- A `GROUP` kind that draws its own border and caption. A group box is a + `VBOX` with a wider top `pad` for the caption; the border and caption text + are draw-time work for the caller. The module computes rectangles, not + pixels — no strings, no drawing. +- Chained cross-axis flex (a leaf's cross size driving a sibling's). + +## Model + +One-shot. The caller fills a flat array describing the tree, calls the solver, +and reads a parallel array of boxes. No allocation, no tree pointers, no +retained state. Rebuild the array each resize (or each frame) and solve again; +the solve is cheap. + +The tree is expressed by parent index. `items[0]` is the root. Every other +item names its parent, which must appear earlier in the array (parents before +children). A container's children are every item naming it as parent, in array +order — that order is the stacking order. + +```c +typedef enum stack_kind +{ + stack_KIND_HBOX, /* stack children left to right */ + stack_KIND_VBOX, /* stack children top to bottom */ + stack_KIND_SPACER, /* flexible gap; no output box read by anyone */ + stack_KIND_LEAF /* a thing the caller will position */ +} +stack_kind_t; + +typedef enum stack_align +{ + stack_ALIGN_START, /* against the low cross-axis edge */ + stack_ALIGN_CENTRE, /* centred on the cross axis */ + stack_ALIGN_END, /* against the high cross-axis edge */ + stack_ALIGN_FILL /* span the container's cross extent */ +} +stack_align_t; + +typedef struct stack_item +{ + stack_kind_t kind; + int parent; /* index of the containing item; -1 for the root */ + + int size; /* fixed main-axis extent in px; 0 means "use flex" */ + int flex; /* weight for sharing leftover main-axis space; 0 means "do not grow" */ + int min; /* main-axis lower clamp in px; 0 means none */ + int max; /* main-axis upper clamp in px; 0 means unbounded */ + + stack_align_t align; /* cross-axis placement of this item within its container */ + + int gap; /* containers only: px between adjacent children */ + int pad_l; /* containers only: inner inset, left edge */ + int pad_t; /* containers only: inner inset, top edge */ + int pad_r; /* containers only: inner inset, right edge */ + int pad_b; /* containers only: inner inset, bottom edge */ +} +stack_item_t; + +/* Solve `items[0..n)` into `out[0..n)`. `out[i]` is the computed box for + * `items[i]`, in the same coordinate space as `root`. `root` is the area the + * root item is laid into. Returns result_OK, or result_STACK_BAD_TREE if the + * parent indices are not a valid parents-before-children forest rooted at 0. */ +result_t stack_solve(const stack_item_t *items, + int n, + const box_t *root, + box_t *out); +``` + +`SPACER` gets an `out` box like any item; nothing is expected to read it. It +exists so "push the rest to the right" is `{ .kind = stack_KIND_SPACER, +.flex = 1 }` rather than a special case in the solver. + +## Algorithm + +Recurse over containers, root first. For each container, lay out its direct +children along its main axis; the container's own box is already known (the +root's is `root` inset by its four `pad_*`; a child container's was set when +its parent was processed). + +Let `avail` be the container's main-axis extent, minus the two `pad_*` on that +axis, minus `gap * (childcount - 1)`. + +**Pass 1 — fixed and minimum.** For each child, take its base main extent: +`size` if non-zero, else `min` if non-zero, else 0. Sum these into `used`. + +**Pass 2 — distribute the remainder.** `slack = avail - used`. + +- If `slack > 0`: share it across children with `flex > 0` in proportion to + their weights. Integer division leaves a remainder of at most + `flexchildren - 1` px; hand those out one per child, earliest first, so the + children's extents sum to exactly `avail`. Apply each child's `max` clamp; px + removed by a clamp go back into `slack` for another distribution pass over + the still-unclamped flex children (at most a few passes; stop when no child + clamps). +- If `slack < 0` (children's minimums overflow the container): the container + is smaller than its contents. Do not shrink below `min`. Let children + overflow the high edge. `min` is a hard floor; a caller that wants + shrink-to-fit sets `min` to 0. +- If `slack == 0`: nothing to distribute. + +**Placement.** Walk the children along the main axis from the container's low +edge plus the leading `pad_*` for that axis, advancing by each child's resolved +extent plus `gap`. On the cross axis, place each child within the container +inset by the two cross-axis `pad_*`, per its `align`: `START` at the low edge, +`END` at the high edge, `CENTRE` at `(container_cross - child_cross) / 2` +rounding down, `FILL` sets the child's cross extent to the whole span. A +non-`FILL` child's cross extent is its `size` if the cross axis has one to give +— otherwise `FILL` is the sensible default and a caller wanting an intrinsic +cross size supplies it out of band. + +Write each child's `out` box, then recurse into it if it is a container. + +## Worked example — the Monitor configuration dialog + +The RISC OS dialog it is modelled on: + +``` ++------------------------------------------------+ +| Driver [ Vpod ][v] | +| Monitor [ Samsung 23" (SAM0473) ][v] | +| | +| +- Monitor options --------------------------+ | +| | [ ] Use mode definition file | | +| | Monitor type [ Samsung SyncMaster ] | | +| | Colours [ 64 thousand ][v] | | +| | Resolution [ 1680 x 1050 ][v] | | +| | DPMS level [ Monitor standby ][v] | | +| +--------------------------------------------+ | +| | +| [ Refresh ] [ Cancel ] [ Set ] | ++------------------------------------------------+ +``` + +Structure: + +- The root is a `VBOX`: two label/field rows, the group box, then the button + row. +- **Aligned label column.** "Driver", "Monitor", "Monitor type", "Colours", + "Resolution", "DPMS level" all right-align to one column so the fields start + at a common x. The caller measures the six label strings, takes the widest as + `LW`, and sets every label leaf to `.size = LW, .align = stack_ALIGN_END`. + Rows solved independently still line up because the caller fed them one width. +- **Each row** is an `HBOX`: label leaf (`size = LW`), field leaf (`flex = 1`), + and — only on rows that have one — a fixed-size pop-up icon. A row without the + pop-up just lets the field run to the edge; alignment holds either way. +- **Group box** is a `VBOX` with `.pad_t` big enough for the caption and + border, `.pad_l/r/b` the border inset. The caller draws the frame and the + "Monitor options" text; the module only insets the children. +- **Button row** is an `HBOX`: `Refresh`, a `SPACER`, `Cancel`, a `SPACER`, + `Set`. Two spacers spread the three buttons the way the dialog does. + +Sketched as an array (row children elided after the first for length; `RH` row +height, `PU` pop-up width, `BW`/`BH` button size, `M`/`G` margin and gap, +`CAP` caption height): + +```c +enum +{ + ROOT, + DRV_ROW, DRV_LBL, DRV_FLD, DRV_PU, + MON_ROW, MON_LBL, MON_FLD, MON_PU, + GROUP, + USE_ROW, USE_OPT, /* option (check) icon + its label */ + TYP_ROW, TYP_LBL, TYP_FLD, /* no pop-up on this row */ + COL_ROW, COL_LBL, COL_FLD, COL_PU, + RES_ROW, RES_LBL, RES_FLD, RES_PU, + DPM_ROW, DPM_LBL, DPM_FLD, DPM_PU, + BTN_ROW, B_REFRESH, GAP1, B_CANCEL, GAP2, B_SET, + N_ITEMS +}; + +stack_item_t items[N_ITEMS] = +{ + [ROOT] = { .kind = stack_KIND_VBOX, .parent = -1, + .pad_l = M, .pad_t = M, .pad_r = M, .pad_b = M, .gap = G }, + + /* --- Driver row --- */ + [DRV_ROW] = { .kind = stack_KIND_HBOX, .parent = ROOT, .size = RH, .gap = G, .align = stack_ALIGN_FILL }, + [DRV_LBL] = { .kind = stack_KIND_LEAF, .parent = DRV_ROW, .size = LW, .align = stack_ALIGN_END }, + [DRV_FLD] = { .kind = stack_KIND_LEAF, .parent = DRV_ROW, .flex = 1, .align = stack_ALIGN_FILL }, + [DRV_PU] = { .kind = stack_KIND_LEAF, .parent = DRV_ROW, .size = PU, .align = stack_ALIGN_CENTRE }, + /* Monitor row: same shape, parent = ROOT */ + + /* --- Monitor options group --- */ + [GROUP] = { .kind = stack_KIND_VBOX, .parent = ROOT, .flex = 1, + .pad_l = M, .pad_t = CAP, .pad_r = M, .pad_b = M, .gap = G, .align = stack_ALIGN_FILL }, + /* USE_ROW .. DPM_ROW: HBOX rows, parent = GROUP. + * TYP_ROW carries no *_PU child; its field runs to the group's right pad. */ + + /* --- Button row --- */ + [BTN_ROW] = { .kind = stack_KIND_HBOX, .parent = ROOT, .size = BH, .gap = G, .align = stack_ALIGN_FILL }, + [B_REFRESH]= { .kind = stack_KIND_LEAF, .parent = BTN_ROW, .size = BW, .align = stack_ALIGN_FILL }, + [GAP1] = { .kind = stack_KIND_SPACER, .parent = BTN_ROW, .flex = 1 }, + [B_CANCEL] = { .kind = stack_KIND_LEAF, .parent = BTN_ROW, .size = BW, .align = stack_ALIGN_FILL }, + [GAP2] = { .kind = stack_KIND_SPACER, .parent = BTN_ROW, .flex = 1 }, + [B_SET] = { .kind = stack_KIND_LEAF, .parent = BTN_ROW, .size = BW, .align = stack_ALIGN_FILL }, +}; + +box_t out[N_ITEMS]; + +stack_solve(items, N_ITEMS, &work, out); +/* Every *_FLD box starts at work.x0 + M + LW + G, so the fields line up down + * the whole dialog including the group. Resize the window and the fields + * stretch; the labels, pop-ups and buttons keep their fixed widths. */ +``` + +The hand-coded equivalent recomputes an inset chain per icon and repeats the +label-column arithmetic in every row. The array form measures the labels once +and reflows the rest for free. + +## Notes for the implementer + +- Follows the module split: `include/geom/stack.h` for the API, + `libraries/geom/stack/` for the solver, one function per file where it reads + naturally. Add sources by hand to `CMakeLists.txt`. Sits next to + `geom/layout` (line-flow) and `geom/packer`. +- No dependency on `wuss` or SDL. `geom` (`box_t`, `point_t`, `size2d_t`) and + `base/result.h` only. Testable as a plain core test: build an array, solve, + assert boxes. +- Reserve a `result_BASE_STACK` block in `include/base/result.h` (0x0C00 is the + next free base after `result_BASE_WUSS`); the only code needed at first is + `result_STACK_BAD_TREE` (parent indices not a valid forest). +- Integer only. The rounding rule (remainder px to earliest children) is the + one behaviour a test must pin: three flex-1 children in a 100px box get + 34 / 33 / 33, and the boxes abut with no gap or overlap. A second test pins + the aligned-label pattern: two rows, label leaves both `size = LW`, assert + the two field boxes share an `x0`. +- `wuss` need not depend on this. A window's owner calls `stack_solve` in its + own redraw code and positions its icons from `out[]`; the module sits + alongside `wuss`, not inside it. +``` diff --git a/docs/windowing/wuss.md b/docs/windowing/wuss.md index 6261f54..fd4f73a 100644 --- a/docs/windowing/wuss.md +++ b/docs/windowing/wuss.md @@ -9,18 +9,48 @@ ## Setup -Create a window manager onto a `screen_t`, with an optional font for titlebar labels and an optional system palette: +Create a window manager onto a `screen_t`, with an array of font slots, an optional system palette, optional creation config and optional allocator hooks: ```C -result_t wuss_create(screen_t *scr, - bmfont_t *font, - const colour_t *palette, - int npalette, - const wuss_config_t *config, - wuss_t **wuss); +result_t wuss_create(screen_t *scr, + const wuss_font_desc_t *fonts, + int nfonts, + const colour_t *palette, + int npalette, + const wuss_config_t *config, + const wuss_alloc_t *alloc, + wuss_t **wuss); ``` -`font` and `palette` may both be NULL, for unlabelled titlebars and a built-in default palette respectively. `config` may be NULL for default titlebar height/colours. `config->backdrop` is a `wuss_backdrop_t` — a flat colour, or an 8x8 fill pattern — painted behind windows on every redraw; set its `colour` to `wuss_NO_BACKGROUND` (the default when `config` is NULL) to leave the background untouched and require the caller to repaint it itself. A non-`screen_PATTERN_SOLID` `pattern` tiles that pattern in `colour` over `pattern_bg`, phased to a fixed screen origin so it does not crawl between full and dirty-region redraws. `wuss_get_font` reads back the font passed in (or NULL), for a task that wants to draw its own content in the same face as titlebars. +`fonts` (or `nfonts` 0) may be NULL, for unlabelled titlebars; `palette` may be NULL for a built-in default palette; `alloc` may be NULL for plain stdlib `malloc`/`realloc`/`free`. `config` may be NULL for default titlebar height/colours. `config->backdrop` is a `wuss_backdrop_t` — a flat colour, or an 8x8 fill pattern — painted behind windows on every redraw; set its `colour` to `wuss_NO_BACKGROUND` (the default when `config` is NULL) to leave the background untouched and require the caller to repaint it itself. A non-`screen_PATTERN_SOLID` `pattern` tiles that pattern in `colour` over `pattern_bg`, phased to a fixed screen origin so it does not crawl between full and dirty-region redraws. + +### Fonts + +Text drawing lives in its own module, `libraries/wuss/font/`: the region core delegates it the same way it delegates furniture, icons and menus. It owns the font slots a `wuss_t` was created with and the one text-rendering primitive furniture, menus and icons all call, so region handling can be used without pulling any font code in beyond the slot storage. + +`wuss_create` takes up to `wuss_MAX_FONTS` (4) slots as an array of `wuss_font_desc_t`: + +```C +typedef struct wuss_font_desc +{ + bmfont_t *font; /* not owned; must outlive the wuss_t */ + wuss_font_class_t font_class; /* wuss_FONT_CLASS_NONE (a text face) or + * wuss_FONT_CLASS_SYSTEM (chrome only) */ + const char *name; /* borrowed; the font's leafname sans ".png", + * for a picker to match against; may be NULL */ +} +wuss_font_desc_t; +``` + +The descriptors are copied in; the fonts are not owned. Slot use: + +- **slot 0** — the system font: titlebar labels, menu rows, and any icon that does not select another weight. Also what `wuss_get_font` reads back, for a task that wants to draw its own content in the same face. +- **slot 1** — the bold weight, if supplied: window and menu titles draw from it, falling back to slot 0. +- **slot `WUSS_SYMBOL_FONT` (2)** — an optional symbol font the menu tick and submenu arrow are drawn from as glyphs; without it they are drawn as vector strokes. + +An empty slot is NULL and callers degrade to slot 0 or a stroke fallback. When no `config->titlebar_height` is set, the titlebar is sized to the taller of the slot-0 and slot-1 faces, or `WUSS_DEFAULT_TITLEBAR_HEIGHT` if both are empty. + +Read the slots back with `wuss_get_font` (slot 0), `wuss_get_font_n(wuss, i)`, `wuss_get_font_class_n(wuss, i)` and `wuss_get_font_name_n(wuss, i)` — the last two let a picker such as `wuss_fontmenu` tell a chrome font apart from a selectable text face. All text wuss itself draws goes through one internal primitive so a uniform optical baseline nudge is applied everywhere. A `wuss_backdrop_t` is `{ colour, pattern, pattern_bg }`; the `wuss_BACKDROP_COLOUR(c)` and `wuss_BACKDROP_PATTERN(c, p, b)` macros build one as a compound literal. @@ -124,7 +154,7 @@ For `wuss_EVENT_REDRAW`, `event->data.redraw.scr` is called with `scr->clip` alr For `wuss_EVENT_MOUSE` and `wuss_EVENT_SCROLL`, `event->data.mouse.point` and `event->data.scroll.point` are window-local content coordinates: the content area's top-left is `(0,0)` plus the window's current scroll offset (see "Scrolling" below). `event->data.mouse.action` is `wuss_MOUSE_DOWN`/`wuss_MOUSE_UP`/`wuss_MOUSE_MOVE`. A titlebar click never reaches a task's handle callback: it starts a drag (and, for `wuss_BUTTON_SELECT`, brings the window to front) instead. A content click, even on a `wuss_WINDOW_NO_TITLEBAR` window with no drag handle, never changes z-order — only a titlebar click raises a window — so tasks are free to use content clicks for their own purposes without Wuss reordering windows underneath them. -`wuss_EVENT_ICON` is delivered instead of `wuss_EVENT_MOUSE` whenever the pointer is inside a `wuss_ICON_TYPE_BUTTON` icon's bounding box (see "Icons" below): `event->data.icon.icon` names the icon, `action` and `button` carry the same values a `wuss_EVENT_MOUSE` would. `wuss_ICON_TYPE_LABEL` icons, and hidden or disabled icons, never raise it — clicks over them fall through as `wuss_EVENT_MOUSE`. +`wuss_EVENT_ICON` is delivered instead of `wuss_EVENT_MOUSE` whenever the pointer is inside a `wuss_ICON_TYPE_ACTION` icon's bounding box (see "Icons" below): `event->data.icon.icon` names the icon, `action` and `button` carry the same values a `wuss_EVENT_MOUSE` would. `wuss_ICON_TYPE_LABEL` icons, and hidden or disabled icons, never raise it — clicks over them fall through as `wuss_EVENT_MOUSE`. ## Mouse and scroll routing @@ -198,17 +228,17 @@ In a redraw callback: start drawing at `bounds.x0 - scroll.x`, `bounds.y0 - scro Taking inspiration from RISC OS, a window can carry **icons**: rectangular UI elements Wuss draws and hit-tests inside the content area. v1 ships two types: -- `wuss_ICON_TYPE_LABEL` — static text, optionally in a 1px raised or sunken border (`spec.border` — `wuss_ICON_BORDER_NONE` / `_RIDGE` / `_GROOVE`, the last a RISC OS-style read-only display field). Clicks fall through to the task as `wuss_EVENT_MOUSE`. -- `wuss_ICON_TYPE_BUTTON` — a bevelled rectangle with a centred label and pressed-state feedback (the bevel inverts and the label shifts one pixel down-right while held). Clicks and hovers arrive as `wuss_EVENT_ICON`. +- `wuss_ICON_TYPE_LABEL` — static text, optionally in a 1px raised or sunken border (`spec.u.label.border` — `wuss_ICON_BORDER_NONE` / `_RIDGE` / `_GROOVE`, the last a RISC OS-style read-only display field). Clicks fall through to the task as `wuss_EVENT_MOUSE`. +- `wuss_ICON_TYPE_ACTION` — a bevelled rectangle with a centred label and pressed-state feedback (the bevel inverts and the label shifts one pixel down-right while held). Clicks and hovers arrive as `wuss_EVENT_ICON`. The enum is left open for sprite and editable-text types later. Icons are dynamic and owned by their window: -- `wuss_icon_create(window, spec, &icon)` — returns an opaque `wuss_icon_t *`. The spec gives the bounding box, type, text (copied; `NULL` treated as `""`), foreground and background palette indices, a `border` (label only), and flags. A `wuss_ICON_TYPE_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`. +- `wuss_icon_create(window, spec, &icon)` — returns an opaque `wuss_icon_t *`. The spec gives the bounding box, type, text (copied; `NULL` treated as `""`), foreground and background palette indices, a per-type payload in the `u` union (`u.label.border`, `u.pattern.tile`, `u.bitmap.image`/`u.bitmap.set`, `u.radio.group`, `u.menu_entry.swatch`), and flags. A `wuss_ICON_TYPE_ACTION` must pass a real `bg`; passing `wuss_NO_BACKGROUND` is rejected with `result_WUSS_BAD_ICON`. An unknown type is also `result_WUSS_BAD_ICON`; an out-of-range `fg`/`bg` is `result_WUSS_BAD_COLOUR`. +- `wuss_icon_delete(window, icon)` — NULL-safe. +- `wuss_icon_set_text(window, icon, text)`, `wuss_icon_set_hidden(window, icon, hidden)`, `wuss_icon_set_selected(window, icon, selected)`. +- Getters: `wuss_icon_get_bbox`, `wuss_icon_get_type`, `wuss_icon_get_text` (never `NULL`), `wuss_icon_get_selected`. Flags: `wuss_ICON_FLAGS_HIDDEN` (not drawn, not hit-tested) and `wuss_ICON_FLAGS_DISABLED` (drawn greyed; clicks fall through as `wuss_EVENT_MOUSE`). @@ -226,7 +256,7 @@ The bevel's light (top/left) and dark (bottom/right) edge shades come from `conf - `wuss_icons_bitmap(wuss, index)` → the compressed `bitmap_t *` (window-manager-owned), or `NULL` out of range. - `wuss_icons_count(wuss)`. -A `wuss_ICON_TYPE_BITMAP` spec with `bitmap == NULL` and `icon_set = wuss_ICON_SET(idx)` draws the loaded entry at `idx` (the `wuss_ICON_SET` macro offsets by one so a zero-initialised spec means "no entry"). An out-of-range index is `result_WUSS_BAD_INDEX`. Calling `wuss_icons_load` again replaces the set; `wuss_destroy` frees it. A missing directory is not an error — it yields a zero-length set. `dir` is copied internally, so a `path_join_filename` result is safe to pass. +A `wuss_ICON_TYPE_BITMAP` spec with `u.bitmap.image == NULL` and `u.bitmap.set = wuss_ICON_SET(idx)` draws the loaded entry at `idx` (the `wuss_ICON_SET` macro offsets by one so a zero-initialised spec means "no entry"). An out-of-range index is `result_WUSS_BAD_INDEX`. Calling `wuss_icons_load` again replaces the set; `wuss_destroy` frees it. A missing directory is not an error — it yields a zero-length set. `dir` is copied internally, so a `path_join_filename` result is safe to pass. ## Components @@ -268,12 +298,13 @@ Terms as this document and the API use them. Several are RISC OS conventions, wh - **Chord** — two or more mouse buttons held together, e.g. Select+Adjust. Wuss's own furniture handling resolves an ambiguous chord in Select's favour. - **Content area** — the part of a window belonging to its task. Its bounds are exactly what was passed to `wuss_window_create`, furniture being added outside it; read back with `wuss_window_get_content_bounds`. - **Dirty region** — the accumulated set of screen-space boxes needing repaint, coalesced as they accumulate. `wuss_redraw_dirty` repaints and clears it. +- **Font slot** — one of up to `wuss_MAX_FONTS` `wuss_font_desc_t` entries passed to `wuss_create` and owned by the `libraries/wuss/font/` module. Slot 0 is the system font, slot 1 the bold weight, slot 2 (`WUSS_SYMBOL_FONT`) an optional glyph font for menu decoration. Read back with `wuss_get_font` / `wuss_get_font_n`. See "Fonts". - **Document extent** — `doc`, the size of a task's virtual content space, fixed at window creation. Sets how far a window can scroll, the scrollbar sausages' proportions, and the size a resize-drag or toggle-size can grow the content area to. - **Minimum extent** — `min_doc`, the smallest content size a resize-drag or toggle-size will leave a window at, fixed at window creation. `(0, 0)` means the built-in floor. - **Furniture** — everything Wuss draws around a window's content: outline, titlebar and its buttons, scrollbars, resize button. Drawn outside the content area, never carved out of it. Furniture clicks are handled entirely within Wuss and never reach the task. - **Furniture button** — a clickable furniture region in the titlebar or window corner: close, back, toggle-size, resize. (Called an "icon" in earlier revisions; that name now means the work-area element below.) - **Handle callback** — a task's single `wuss_event_fn_t`, receiving every event kind and dispatching on `event->kind`. A window whose task has no handle receives no events at all. -- **Icon** — a rectangular UI element Wuss draws and hit-tests inside a window's content area: a static `wuss_ICON_TYPE_LABEL`, or a clickable bevelled `wuss_ICON_TYPE_BUTTON`. Created with `wuss_icon_create` and owned by its window. Its bounding box is in virtual content space, so it scrolls with the content; its screen position is `content-top-left - scroll + bbox`. See "Icons". +- **Icon** — a rectangular UI element Wuss draws and hit-tests inside a window's content area: a static `wuss_ICON_TYPE_LABEL`, or a clickable bevelled `wuss_ICON_TYPE_ACTION`. Created with `wuss_icon_create` and owned by its window. Its bounding box is in virtual content space, so it scrolls with the content; its screen position is `content-top-left - scroll + bbox`. See "Icons". - **Invalidate** — mark a region dirty for the next `wuss_redraw_dirty`. Window management does this for its own changes; a task must do it for its own content changes. - **Menu** — the middle mouse button, `wuss_BUTTON_MENU`. Routed like any other button; Wuss provides no menu widget of its own. - **Outline** — the 1px border drawn around a window, suppressed by `wuss_WINDOW_NO_OUTLINE`. diff --git a/include/base/result.h b/include/base/result.h index 6515b49..5fc96d0 100644 --- a/include/base/result.h +++ b/include/base/result.h @@ -23,6 +23,7 @@ typedef int result_t; #define result_BASE_LAYOUT 0x0900 #define result_BASE_BITFIFO 0x0A00 #define result_BASE_WUSS 0x0B00 +#define result_BASE_STACK 0x0C00 /* Non-DPTLib bases */ #define result_BASE_MMPLAYER 0x4000 diff --git a/include/framebuf/bitmap.h b/include/framebuf/bitmap.h index a219b42..4fad5cf 100644 --- a/include/framebuf/bitmap.h +++ b/include/framebuf/bitmap.h @@ -111,6 +111,10 @@ result_t bitmap_fill_pattern(bitmap_t *bm, /** * Load a PNG image into the given bitmap. * + * A palette-type PNG is kept as \ref pixelfmt_p8 with its PLTE (and any + * tRNS) copied into `bm->palette`; RGB and greyscale PNGs are expanded to a + * 32bpp `rgbx8888` / `rgba8888` bitmap with no palette. + * * \param[in] bm Bitmap to load the image into. * \param[in] filename Filename of the image to load. * \return \ref result_OK on success, or appropriate result code otherwise. @@ -120,6 +124,12 @@ result_t bitmap_load_png(bitmap_t *bm, const char *filename); /** * Save the given bitmap as a PNG image. * + * Supported formats: `bgrx8888` (written as RGB), `bgra8888` (RGBA), and + * \ref pixelfmt_p8 (written as a palette-type PNG, its `bm->palette` + * becoming the PLTE, plus a tRNS chunk if any palette entry is non-opaque). + * Other formats return \ref result_NOT_SUPPORTED, as does a \ref pixelfmt_p8 + * bitmap with no palette. + * * \param[in] bm Bitmap to save. * \param[in] filename Filename to save the image to. * \return \ref result_OK on success, or appropriate result code otherwise. diff --git a/include/framebuf/bmfont.h b/include/framebuf/bmfont.h index c466167..db61339 100644 --- a/include/framebuf/bmfont.h +++ b/include/framebuf/bmfont.h @@ -25,6 +25,22 @@ enum /** A set of bmfont_FLAG_* values. */ typedef unsigned int bmfont_flags_t; +/** + * Extra spacing to apply on top of a font's own advance widths, passed to + * bmfont_measure(), bmfont_draw() and bmfont_draw_relief(). A NULL pointer + * means no extra spacing (as if both fields were zero). + */ +typedef struct bmfont_spacing +{ + int letter_spacing; /**< Added to every glyph's advance width, in pixels. + * May be negative to tighten; a large negative + * value can make glyphs overlap or run + * backwards. */ + int word_spacing; /**< Added to space (' ') glyphs, on top of + * letter_spacing, in pixels. */ +} +bmfont_spacing_t; + /** * Create a new bitmap font from a PNG format font file. * @@ -83,13 +99,21 @@ void bmfont_destroy(bmfont_t *bmfont); void bmfont_set_flags(bmfont_t *bmfont, bmfont_flags_t flags); /** - * Read the width and height of the specified bitmap font. + * Read the width, height, ascent and descent of the specified bitmap font. * * \param[in] bmfont Bitmap font to query. * \param[out] width Width of the font in pixels, or NULL if not wanted. * \param[out] height Height of the font in pixels, or NULL if not wanted. + * \param[out] ascent Baseline offset from the top of a glyph cell, in + * pixels, or NULL if not wanted. + * \param[out] descent Offset from the baseline to the bottom of a glyph + * cell, in pixels, or NULL if not wanted. */ -void bmfont_get_info(bmfont_t *bmfont, int *width, int *height); +void bmfont_get_info(bmfont_t *bmfont, + int *width, + int *height, + int *ascent, + int *descent); /** * Read the number of glyphs in the specified bitmap font. Glyphs are laid @@ -107,17 +131,19 @@ int bmfont_get_count(bmfont_t *bmfont); * \param[in] bmfont Bitmap font to measure. * \param[in] text String to measure. * \param[in] len Length of the string. + * \param[in] spacing Extra letter/word spacing, or NULL for none. * \param[in] target_width Target width in pixels. * \param[out] split_point Split point in pixels. * \param[out] actual_width Actual width of the split string in pixels. * \return \ref result_OK on success, or appropriate result code otherwise. */ -result_t bmfont_measure(bmfont_t *bmfont, - const char *text, - int len, - bmfont_width_t target_width, - int *split_point, - bmfont_width_t *actual_width); +result_t bmfont_measure(bmfont_t *bmfont, + const char *text, + int len, + const bmfont_spacing_t *spacing, + bmfont_width_t target_width, + int *split_point, + bmfont_width_t *actual_width); /** * Draw the given string using the specified font, position and colours. @@ -128,18 +154,20 @@ result_t bmfont_measure(bmfont_t *bmfont, * \param[in] len Length of the string. * \param[in] fg Foreground colour. * \param[in] bg Background colour. - * \param[in] pos Position of the string in pixels. - * \param[out] end_pos End position of the string in pixels. + * \param[in] spacing Extra letter/word spacing, or NULL for none. + * \param[in] pos Baseline start position of the string in pixels. + * \param[out] end_pos Baseline end position of the string in pixels. * \return \ref result_OK on success, or appropriate result code otherwise. */ -result_t bmfont_draw(bmfont_t *bmfont, - screen_t *scr, - const char *text, - int len, - colour_t fg, - colour_t bg, - const point_t *pos, - point_t *end_pos); +result_t bmfont_draw(bmfont_t *bmfont, + screen_t *scr, + const char *text, + int len, + colour_t fg, + colour_t bg, + const bmfont_spacing_t *spacing, + const point_t *pos, + point_t *end_pos); /** * Draw the given string twice to produce a relief (drop-shadow) effect: a @@ -156,19 +184,21 @@ result_t bmfont_draw(bmfont_t *bmfont, * \param[in] len Length of the string. * \param[in] fg Foreground colour, used for the main pass. * \param[in] shadow Shadow colour, used for the offset pass. - * \param[in] pos Position of the main pass in pixels. + * \param[in] spacing Extra letter/word spacing, or NULL for none. + * \param[in] pos Baseline position of the main pass in pixels. * \param[in] offset Shadow displacement in pixels, e.g. { 1, 1 }. - * \param[out] end_pos End position of the string in pixels. + * \param[out] end_pos Baseline end position of the string in pixels. * \return \ref result_OK on success, or appropriate result code otherwise. */ -result_t bmfont_draw_relief(bmfont_t *bmfont, - screen_t *scr, - const char *text, - int len, - colour_t fg, - colour_t shadow, - const point_t *pos, - const point_t *offset, - point_t *end_pos); +result_t bmfont_draw_relief(bmfont_t *bmfont, + screen_t *scr, + const char *text, + int len, + colour_t fg, + colour_t shadow, + const bmfont_spacing_t *spacing, + const point_t *pos, + const point_t *offset, + point_t *end_pos); #endif /* DPTLIB_BMFONT_H */ diff --git a/include/framebuf/pattern.h b/include/framebuf/pattern.h index 67c783e..641be86 100644 --- a/include/framebuf/pattern.h +++ b/include/framebuf/pattern.h @@ -105,4 +105,17 @@ pattern_t pattern_from_preset(screen_pattern_t preset, */ pattern_t pattern_from_mask(const uint8_t mask[8], colour_t colour); +/** + * The 8x8 ordered (Bayer) dither threshold for screen pixel (`x`, `y`), a + * value in the range 0 to 63. This is the recursively-built matrix the + * `screen_PATTERN_BAYER*` tiles are derived from, exposed for callers that + * dither a continuous value per pixel rather than fill with a fixed-coverage + * tile (e.g. `screen_copy_bitmap_dithered`). `x` and `y` may be negative. + * + * \param[in] x Screen X coordinate. + * \param[in] y Screen Y coordinate. + * \return The threshold, 0 to 63. + */ +int pattern_bayer_threshold(int x, int y); + #endif /* FRAMEBUF_PATTERN_H */ diff --git a/include/framebuf/pixelfmt.h b/include/framebuf/pixelfmt.h index ff6d2d9..b42d4a0 100644 --- a/include/framebuf/pixelfmt.h +++ b/include/framebuf/pixelfmt.h @@ -114,6 +114,13 @@ typedef unsigned int pixelfmt_xxxa8888_t; /* any 32bpp alpha */ /** Non-zero if `f` names an RLE-compressed pixel store. */ #define pixelfmt_is_rle(f) (((f) & pixelfmt_RLE_FLAG) != 0) +/** Non-zero if `f` is one of the four 32bpp formats with a real alpha + * channel (`bgra8888`/`rgba8888`/`abgr8888`/`argb8888`), as opposed to a + * 32bpp format with a padding byte (`*x8888`) or a paletted/other format. */ +#define pixelfmt_has_alpha(f) \ + ((f) == pixelfmt_bgra8888 || (f) == pixelfmt_rgba8888 || \ + (f) == pixelfmt_abgr8888 || (f) == pixelfmt_argb8888) + /* ----------------------------------------------------------------------- */ typedef unsigned int pixelfmt_any_t; /* generic/unspecified pixel */ diff --git a/include/framebuf/screen.h b/include/framebuf/screen.h index 6c9b668..58b0b63 100644 --- a/include/framebuf/screen.h +++ b/include/framebuf/screen.h @@ -169,6 +169,31 @@ result_t screen_copy_bitmap(screen_t *scr, int y, const bitmap_t *src); +/** + * As `screen_copy_bitmap`, but on a paletted screen the source colour is + * ordered-dithered (8x8 Bayer) per pixel before the nearest-palette-entry + * lookup, so a smooth gradient stipples between the available entries + * instead of banding at each quantisation step. The dither is phased to + * screen coordinates, so it stays put when the same content is redrawn at + * the same place. + * + * On a 32bpp screen, and for an RLE-compressed source, this is identical to + * `screen_copy_bitmap` (a deep screen has no banding to break up; an RLE + * source is already quantised UI art). Alpha handling, clipping and the lack + * of scaling are all as `screen_copy_bitmap`. + * + * \param[in] scr Screen to draw upon. + * \param[in] x X coordinate of leftmost point to draw bitmap at. + * \param[in] y Y coordinate of topmost point to draw bitmap at. + * \param[in] src Bitmap to copy. + * \return \ref result_OK on success, \ref result_NOT_SUPPORTED if the + * screen's pixel format has no blit path. + */ +result_t screen_copy_bitmap_dithered(screen_t *scr, + int x, + int y, + const bitmap_t *src); + /** Flags for `screen_copy_ninepatch`. */ enum { diff --git a/include/framebuf/span-p8.h b/include/framebuf/span-p8.h new file mode 100644 index 0000000..ffea478 --- /dev/null +++ b/include/framebuf/span-p8.h @@ -0,0 +1,10 @@ +/* framebuf/span-p8.h -- P8 (8bpp paletted) format plot methods */ + +#ifndef SPAN_P8_H +#define SPAN_P8_H + +#include "framebuf/span.h" + +extern const span_t span_p8; + +#endif /* SPAN_P8_H */ diff --git a/include/geom/box.h b/include/geom/box.h index da7751c..c4c5555 100644 --- a/include/geom/box.h +++ b/include/geom/box.h @@ -138,6 +138,15 @@ void box_scalelog2(box_t *b, int log2scale); */ void box_grow(box_t *b, int change); +/** + * Populates "result" with a copy of "b" grown outwards by "change". + * + * \param[in] b The source box. + * \param[in] change Amount to grow (negative to shrink). + * \return The resulting box. + */ +box_t box_grown(const box_t *b, int change); + /** * Rounds a box's coordinates so that they're a multiple of log2 x,y. * diff --git a/include/geom/stack.h b/include/geom/stack.h new file mode 100644 index 0000000..b6b4485 --- /dev/null +++ b/include/geom/stack.h @@ -0,0 +1,169 @@ +/* geom/stack.h -- box-stack layout solver */ + +#ifndef GEOM_STACK_H +#define GEOM_STACK_H + +#ifdef __cplusplus +extern "C" +{ +#endif + +#include "base/result.h" +#include "geom/box.h" +#include "geom/size.h" + +/* ----------------------------------------------------------------------- */ + +#define result_STACK_BAD_TREE (result_BASE_STACK + 0) + +/** Largest item-array size stack_solve()/stack_smallest() will accept + * (they use fixed-size internal scratch arrays, not a VLA, for MSVC + * portability). Ample for any hand-written table. */ +#define STACK_MAX_ITEMS 64 + +/* ----------------------------------------------------------------------- */ + +/** The kind of a stack item. */ +typedef enum stack_kind +{ + stack_KIND_HBOX, /**< Stack children left to right. */ + stack_KIND_VBOX, /**< Stack children top to bottom. */ + stack_KIND_SPACER, /**< Flexible gap; no output box read by anyone. */ + stack_KIND_LEAF /**< A thing the caller will position. */ +} +stack_kind_t; + +/** Cross-axis alignment of a stack item within its container. */ +typedef enum stack_align +{ + stack_ALIGN_START, /**< Against the low cross-axis edge. */ + stack_ALIGN_CENTRE, /**< Centred on the cross axis. */ + stack_ALIGN_END, /**< Against the high cross-axis edge. */ + stack_ALIGN_FILL /**< Span the container's cross extent. */ +} +stack_align_t; + +/** One node in a box-stack tree. */ +typedef struct stack_item +{ + stack_kind_t kind; + int parent; /**< Index of the containing item; -1 for the + root. */ + + int axis_size; /**< Fixed main-axis extent in px; 0 means "use + flex". */ + int flex; /**< Weight for sharing leftover main-axis + space; 0 means "do not grow". */ + int min; /**< Main-axis lower clamp in px; 0 means + none. */ + int max; /**< Main-axis upper clamp in px; 0 means + unbounded. */ + + stack_align_t align; /**< Cross-axis placement of this item within + its container. */ + int cross_size; /**< Cross-axis extent in px for + stack_ALIGN_CENTRE/stack_ALIGN_END; 0 means + span the container's full cross extent. + Unused for stack_ALIGN_START/FILL. */ + + int gap; /**< Containers only: px between adjacent + children. */ + int pad_l; /**< Containers only: inner inset, left + edge. */ + int pad_t; /**< Containers only: inner inset, top edge. */ + int pad_r; /**< Containers only: inner inset, right + edge. */ + int pad_b; /**< Containers only: inner inset, bottom + edge. */ +} +stack_item_t; + +/* ----------------------------------------------------------------------- */ + +/* Compact initialisers for a static stack_item_t[] table, covering the + * fields each kind needs in the common case. Parent is still given + * explicitly -- a flat initializer list has no nesting to infer it from -- + * but the field names and .kind are no longer spelled out per item. The + * _EX variants add the fields the plain macro omits, for the occasional + * item that needs them (e.g. root padding, or a leaf that both flexes and + * clamps). There is no non-EX/EX split for stack_KIND_SPACER: flex is its + * only field. */ + +#define STACK_VBOX(p_, axis_, gap_) \ + { .kind = stack_KIND_VBOX, .parent = (p_), \ + .axis_size = (axis_), .gap = (gap_) } + +#define STACK_VBOX_EX(p_, axis_, gap_, padl_, padt_, padr_, padb_) \ + { .kind = stack_KIND_VBOX, .parent = (p_), \ + .axis_size = (axis_), .gap = (gap_), \ + .pad_l = (padl_), .pad_t = (padt_), .pad_r = (padr_), .pad_b = (padb_) } + +#define STACK_HBOX(p_, axis_, gap_, align_) \ + { .kind = stack_KIND_HBOX, .parent = (p_), \ + .axis_size = (axis_), .gap = (gap_), .align = (align_) } + +#define STACK_HBOX_EX(p_, axis_, gap_, align_, padl_, padt_, padr_, padb_) \ + { .kind = stack_KIND_HBOX, .parent = (p_), \ + .axis_size = (axis_), .gap = (gap_), .align = (align_), \ + .pad_l = (padl_), .pad_t = (padt_), .pad_r = (padr_), .pad_b = (padb_) } + +#define STACK_LEAF(p_, axis_, cross_, align_) \ + { .kind = stack_KIND_LEAF, .parent = (p_), \ + .axis_size = (axis_), .cross_size = (cross_), .align = (align_) } + +#define STACK_LEAF_EX(p_, axis_, cross_, align_, flex_, min_, max_) \ + { .kind = stack_KIND_LEAF, .parent = (p_), \ + .axis_size = (axis_), .cross_size = (cross_), .align = (align_), \ + .flex = (flex_), .min = (min_), .max = (max_) } + +#define STACK_SPACER(p_, flex_) \ + { .kind = stack_KIND_SPACER, .parent = (p_), .flex = (flex_) } + +/** + * Solve a box-stack tree into boxes. + * + * `items[0]` is the root. Every other item names its parent by index, which + * must appear earlier in the array (parents before children). A container's + * children are every item naming it as parent, in array order; that order is + * the stacking order. + * + * \param[in] items Array of stack items describing the tree. + * \param[in] n Number of items in the array. + * \param[in] root The area the root item is laid into. + * \param[out] out Array of `n` boxes; `out[i]` receives the computed box + * for `items[i]`, in the same coordinate space as `root`. + * \return \ref result_OK on success, result_STACK_BAD_TREE if the parent + * indices are not a valid parents-before-children forest rooted at + * 0, or if `n` exceeds \ref STACK_MAX_ITEMS. + */ +result_t stack_solve(const stack_item_t *items, + int n, + const box_t *root, + box_t *out); + +/** + * Measure the smallest root box the tree can be solved into without any + * flexible item shrinking below its `min`/`axis_size` -- the size to pass + * `stack_solve` (and, typically, a window's create call) so every item sits + * at its minimum rather than being handed extra slack. Each container's + * minimum main-axis extent is the sum of its children's minima plus gaps and + * insets; its minimum cross-axis extent is the largest of its children's (a + * child contributes its `cross_size`, or 0 if unset, since an unset cross + * size means "span the parent" rather than "needs this much"). + * + * \param[in] items Array of stack items describing the tree. + * \param[in] n Number of items in the array. + * \param[out] out Set to the root item's minimum size. + * \return \ref result_OK on success, result_STACK_BAD_TREE if the parent + * indices are not a valid parents-before-children forest rooted at + * 0, or if `n` exceeds \ref STACK_MAX_ITEMS. + */ +result_t stack_smallest(const stack_item_t *items, + int n, + size2d_t *out); + +#ifdef __cplusplus +} +#endif + +#endif /* GEOM_STACK_H */ diff --git a/include/io/path.h b/include/io/path.h index 0cb354f..205edc4 100644 --- a/include/io/path.h +++ b/include/io/path.h @@ -3,6 +3,8 @@ #ifndef DPTLIB_PATH #define DPTLIB_PATH +#include + #define DPTLIB_MAXPATH 256 /* not ideal */ /** @@ -22,4 +24,22 @@ const char *path_join_leafname(const char *leaf, const char *ext); */ const char *path_join_filename(const char *root, int nbranches, ...); +/** + * Tests whether a directory leafname (as returned by dirscan_walk) has the + * dotted extension 'ext', e.g. ".png", under the host convention. On a + * match, writes the leaf with that extension stripped into 'name' (capacity + * 'cap') and returns nonzero. + * + * On RISC OS a leafname carries no extension in the string at all -- file + * type is separate filesystem metadata, not a suffix -- so every leaf + * matches unchanged (a straight copy into 'name'). + * + * \return Nonzero on a match, zero if "leaf" lacks 'ext' or does not fit + * 'cap'. + */ +int path_leaf_strip_ext(const char *leaf, + const char *ext, + char *name, + size_t cap); + #endif /* DPTLIB_PATH */ diff --git a/include/test/all-tests.h b/include/test/all-tests.h index 91612cb..a5aefdd 100644 --- a/include/test/all-tests.h +++ b/include/test/all-tests.h @@ -33,7 +33,8 @@ extern testfn_t bitmap_rle_test, /* geom */ extern testfn_t box_test, layout_test, - packer_test; + packer_test, + stack_test; /* io */ extern testfn_t stream_test; diff --git a/include/text/bmtext.h b/include/text/bmtext.h index 8bc6f4d..bb4cb9a 100644 --- a/include/text/bmtext.h +++ b/include/text/bmtext.h @@ -49,17 +49,19 @@ bmtext_line_t; * \param[in] string Text to wrap. * \param[in] stringlen Length of \p string in bytes. * \param[in] wrap_width Width to wrap to, in pixels. + * \param[in] spacing Extra letter/word spacing, or NULL for none. * \param[out] lines Filled with up to \p max lines. * \param[in] max Capacity of \p lines. Lines past this are dropped. * * \return Number of lines written to \p lines. */ -int bmtext_layout(bmfont_t *font, - const char *string, - int stringlen, - int wrap_width, - bmtext_line_t *lines, - int max); +int bmtext_layout(bmfont_t *font, + const char *string, + int stringlen, + int wrap_width, + const bmfont_spacing_t *spacing, + bmtext_line_t *lines, + int max); /** * Draw \p nlines pre-laid-out \p lines stacked downward from \p origin, @@ -73,15 +75,17 @@ int bmtext_layout(bmfont_t *font, * \param[in] bg Background colour, for glyph blending. * \param[in] leading Extra pixels between lines. * \param[in] origin Top-left of the first line, in pixels. + * \param[in] spacing Extra letter/word spacing, or NULL for none. */ -void bmtext_draw(bmfont_t *font, - screen_t *scr, - const bmtext_line_t *lines, - int nlines, - colour_t fg, - colour_t bg, - int leading, - point_t origin); +void bmtext_draw(bmfont_t *font, + screen_t *scr, + const bmtext_line_t *lines, + int nlines, + colour_t fg, + colour_t bg, + int leading, + point_t origin, + const bmfont_spacing_t *spacing); #ifdef __cplusplus } diff --git a/include/wuss/component/dialogue.h b/include/wuss/component/dialogue.h new file mode 100644 index 0000000..a803315 --- /dev/null +++ b/include/wuss/component/dialogue.h @@ -0,0 +1,238 @@ +/* wuss/component/dialogue.h -- interactive dialogue base class */ + +/** + * \file dialogue.h + * + * A shared wuss component: the window-and-button shell behind an interactive + * dialogue (a Configure box, a form with OK/Cancel), so a task need not + * hand-roll its own window creation, fillout-on-show wiring and per-button + * event dispatch. + * + * A wuss_dialogue owns one hidden window on a task the caller passes in. The + * caller builds its own content (icons, layout) on that window after + * wuss_dialogue_create returns, then registers a small table of actions -- + * an icon and a callback -- with wuss_dialogue_set_actions. From the task's + * own wuss_EVENT_ICON handler, a click on any registered icon is dispatched + * to that action's callback via wuss_dialogue_handle_icon; every other icon + * event (e.g. a slider drag) is left to the caller to handle first, as + * wuss_dialogue_handle_icon only fires on wuss_MOUSE_UP against a registered + * icon and returns 0 (not consumed) otherwise. From the task's + * wuss_EVENT_PRE_SHOW handler, wuss_dialogue_handle_pre_show calls the + * fillout callback (if any) to resync the dialogue's icons to live state + * before it is revealed. + * + * This component does not know about menus: a dialogue shown as a + * wuss_menu_item_t::window leaf is dismissed by the task's own action + * callback calling wuss_menu_close, exactly as it already must decide + * whether a Select or Adjust click on Cancel/Apply/OK dismisses at all (the + * RISC OS convention: Select dismisses, Adjust applies/resets and leaves the + * dialogue open). A standalone dialogue dismisses itself with + * wuss_dialogue_hide. + * + * Built only when WUSS_COMPONENTS is defined (which implies WUSS_MENUS). + */ + +#ifndef WUSS_COMPONENT_DIALOGUE_H +#define WUSS_COMPONENT_DIALOGUE_H + +#ifdef __cplusplus +extern "C" +{ +#endif + +#include "base/result.h" +#include "geom/size.h" + +#include "wuss/icon.h" +#include "wuss/task.h" +#include "wuss/window.h" +#include "wuss/wuss.h" + +/* ----------------------------------------------------------------------- */ + +/** Opaque handle: owns the dialogue window and its action table. */ +typedef struct wuss_dialogue wuss_dialogue_t; + +/** + * Fillout callback: resync the dialogue's icons to live state. Called by + * wuss_dialogue_handle_pre_show, and so on every reveal of the dialogue. + * + * \param[in] opaque As passed to wuss_dialogue_create. + * \return \ref result_OK to allow the reveal, else a code that vetoes it + * (see wuss_EVENT_PRE_SHOW). + */ +typedef result_t (wuss_dialogue_fillout_fn_t)(void *opaque); + +/** + * Action callback: a registered icon was clicked. Called by + * wuss_dialogue_handle_icon. + * + * \param[in] opaque As passed to wuss_dialogue_create. + * \param[in] button Button(s) released, as wuss_event_t::data::icon::button. + * Test with '&': RISC OS convention is Select dismisses, + * Adjust applies/resets without dismissing. + * \return \ref result_OK, or an appropriate result code; propagated back + * through wuss_dialogue_handle_icon to the task's own handler. + */ +typedef result_t (wuss_dialogue_action_fn_t)(void *opaque, + wuss_button_t button); + +/** One entry in a dialogue's action table: an icon and the callback a click + * on it dispatches to. */ +typedef struct wuss_dialogue_action +{ + wuss_icon_t *icon; /**< Icon this action fires for. */ + wuss_dialogue_action_fn_t *fn; /**< Callback; never NULL in a + * registered entry. */ +} +wuss_dialogue_action_t; + +/** + * Upper bound on the number of entries wuss_dialogue_set_actions accepts. + */ +#define WUSS_DIALOGUE_MAX_ACTIONS 16 + +/* ----------------------------------------------------------------------- */ + +/** + * Create a dialogue: a hidden window on \p task, sized \p size, titled \p + * title. The caller builds its own icons on the returned window's + * wuss_dialogue_window afterwards. + * + * \param[out] out Filled with the new handle on success, untouched on + * failure. + * \param[in] task Task the dialogue window is created on. Must not be an + * autoclose task and must outlive the handle. + * \param[in] size Content size; also used as both \c doc and \c min_doc, + * so the window never scrolls or resizes on its own. + * \param[in] title Titlebar caption, or NULL for none; borrowed and + * copied by wuss_window_create. + * \param[in] fillout Called by wuss_dialogue_handle_pre_show on every + * reveal, or NULL for none. + * \param[in] opaque Passed back to \p fillout and to every action + * callback. + * \return \ref result_OK on success, \ref result_OOM, \ref result_NULL_ARG + * if \p out or \p task is NULL, or a wuss_window_create code. + */ +result_t wuss_dialogue_create(wuss_dialogue_t **out, + wuss_task_t *task, + size2d_t size, + const char *title, + wuss_dialogue_fillout_fn_t *fillout, + void *opaque); + +/** + * Create a dialogue wrapping an already-created window, e.g. one owned and + * laid out by another component such as wuss_info_t, instead of creating its + * own. wuss_dialogue_destroy then leaves \p window alone -- the caller (or + * the component that owns it) is responsible for closing it. + * + * Use this to give an existing window fillout/action-table dispatch without + * duplicating its layout logic; see wuss_proginfo_t for the motivating case + * of one dialogue shared and refilled by several callers. + * + * \param[out] out Filled with the new handle on success, untouched on + * failure. + * \param[in] wuss The window's owning wuss instance. + * \param[in] window Existing window; borrowed, not closed by + * wuss_dialogue_destroy. + * \param[in] fillout Called by wuss_dialogue_handle_pre_show on every + * reveal, or NULL for none. + * \param[in] opaque Passed back to \p fillout and to every action + * callback; see also wuss_dialogue_set_opaque. + * \return \ref result_OK on success, \ref result_OOM, \ref result_NULL_ARG + * if \p out, \p wuss or \p window is NULL. + */ +result_t wuss_dialogue_create_on_window(wuss_dialogue_t **out, + wuss_t *wuss, + wuss_window_t *window, + wuss_dialogue_fillout_fn_t *fillout, + void *opaque); + +/** + * Free a dialogue: closes its window (and so the icons on it), unless it was + * built with wuss_dialogue_create_on_window, in which case the window is + * left for its owning component to close. Safe to pass NULL. + * + * \param[in] doomed Handle to free, or NULL. + */ +void wuss_dialogue_destroy(wuss_dialogue_t *doomed); + +/** + * Change the opaque pointer passed to the fillout and action callbacks, + * replacing the one given at creation. For a dialogue shared by several + * callers (e.g. one wuss_proginfo_t reused across tasks), call this to + * retarget it at the caller about to show it, before revealing it. + * + * \param[in] dialogue Handle. + * \param[in] opaque New opaque pointer. + */ +void wuss_dialogue_set_opaque(wuss_dialogue_t *dialogue, void *opaque); + +/** + * Register the dialogue's action table, replacing any previous one. Does not + * take ownership of the icons named in it -- they belong to the dialogue's + * window, as created by the caller. + * + * \param[in] dialogue Handle. + * \param[in] actions Action table; copied. + * \param[in] nactions Number of entries, 0..\ref WUSS_DIALOGUE_MAX_ACTIONS. + * \return \ref result_OK on success, \ref result_BAD_ARG if \p nactions is + * out of range. + */ +result_t wuss_dialogue_set_actions(wuss_dialogue_t *dialogue, + const wuss_dialogue_action_t *actions, + int nactions); + +/** + * Dispatch a wuss_EVENT_ICON from the task's own handler: if \p event is a + * wuss_MOUSE_UP on an icon registered via wuss_dialogue_set_actions, calls + * that action's callback and returns 1 (with *out_result set to its return + * value). Otherwise returns 0 and leaves *out_result untouched, for the + * caller to handle the event itself (e.g. a slider drag) or ignore it. + * + * \param[in] dialogue Handle. + * \param[in] event The wuss_EVENT_ICON event. + * \param[out] out_result Filled with the action callback's return value if + * (and only if) this call returns 1. + * \return 1 if the event was dispatched to a registered action, else 0. + */ +int wuss_dialogue_handle_icon(wuss_dialogue_t *dialogue, + const wuss_event_t *event, + result_t *out_result); + +/** + * Handle a wuss_EVENT_PRE_SHOW from the task's own handler: calls the + * fillout callback passed to wuss_dialogue_create, if any. + * + * \param[in] dialogue Handle. + * \return \ref result_OK if there is no fillout callback, else its return + * value. + */ +result_t wuss_dialogue_handle_pre_show(wuss_dialogue_t *dialogue); + +/** + * Hide the dialogue without destroying it, as wuss_window_set_hidden(window, + * 1). For a standalone dialogue (not a wuss_menu_item_t::window leaf, which + * dismisses via wuss_menu_close instead). + * + * \param[in] dialogue Handle. + * \return \ref result_OK. + */ +result_t wuss_dialogue_hide(wuss_dialogue_t *dialogue); + +/** + * The dialogue's window, for building content on it, using it as a + * wuss_menu_item_t::window, or showing/hiding directly. Borrowed; valid + * until wuss_dialogue_destroy. + * + * \param[in] dialogue Handle. + * \return The window, or NULL if \p dialogue is NULL. + */ +wuss_window_t *wuss_dialogue_window(const wuss_dialogue_t *dialogue); + +#ifdef __cplusplus +} +#endif + +#endif /* WUSS_COMPONENT_DIALOGUE_H */ diff --git a/include/wuss/component/info.h b/include/wuss/component/info.h index bdaa984..5c4a11e 100644 --- a/include/wuss/component/info.h +++ b/include/wuss/component/info.h @@ -18,8 +18,8 @@ * menu row at wuss_info_window() and wuss shows it where a submenu would * open. A task can equally wuss_window_set_hidden() it directly. * - * The window is created wuss_WINDOW_NO_CLOSE and stays on the caller's task - * until wuss_info_destroy closes it. That task must therefore not be an + * The window is created without wuss_WINDOW_CLOSE and stays on the caller's + * task until wuss_info_destroy closes it. That task must therefore not be an * autoclose task -- its window list would never empty -- and must outlive * the handle. As for any borrowed wuss_menu_item_t::window, the dialogue * must also outlive every menu chain that references it: close the chain @@ -86,6 +86,26 @@ result_t wuss_info_create(wuss_info_t **out, const wuss_info_row_t *rows, int nrows); +/** + * Replace an info dialogue's rows in place: recomputes column widths and row + * pitch for \p rows, resizes the window to fit, and rebuilds its label:value + * icons. For a dialogue shared by several callers (e.g. one wuss_proginfo_t + * reused by every task), call this from a wuss_dialogue_fillout_fn_t just + * before each reveal so the window is always sized for whichever caller is + * about to show it. + * + * \param[in] info Handle. + * \param[in] rows Row array; each label and value is borrowed and copied. + * \param[in] nrows Number of rows; must be >= 1. Need not match the count \p + * info was created with. + * \return \ref result_OK, \ref result_OOM, \ref result_NULL_ARG if \p info + * or \p rows is NULL, \ref result_BAD_ARG if \p nrows is < 1, or a + * wuss_window_resize / wuss_icon_create_array code. + */ +result_t wuss_info_set_rows(wuss_info_t *info, + const wuss_info_row_t *rows, + int nrows); + /** * Free an info dialogue: closes its window and frees every string in it. * Safe to pass NULL. diff --git a/include/wuss/component/proginfo.h b/include/wuss/component/proginfo.h index d909106..498d901 100644 --- a/include/wuss/component/proginfo.h +++ b/include/wuss/component/proginfo.h @@ -1,20 +1,33 @@ -/* wuss/component/proginfo.h -- a "program information" standard dialogue */ +/* wuss/component/proginfo.h -- a shared "program information" dialogue */ /** * \file proginfo.h * * A shared wuss component: the RISC OS "Info" / Toolbox ProgInfo dialogue in - * miniature -- a small fixed window of Name / Purpose / Author / Version - * rows a task fills in once, then hangs off its Menu-button pop-up. + * miniature -- a small window of Name / Purpose / Author / Version rows, + * built once and reusable by every task that wants an "Info" row on its + * Menu-button pop-up, rather than each task carrying its own copy. * - * This is a thin cap over \ref info.h: wuss_proginfo_create maps the - * non-NULL fields of a wuss_proginfo_desc_t to wuss_info_row_t rows and - * calls wuss_info_create. wuss_proginfo_t is wuss_info_t, and the destroy / - * window calls forward straight through, so all of info.h's lifetime rules - * apply verbatim: the window is 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. + * wuss_proginfo_t composes a wuss_info_t (the label:value grid and its + * window) with a wuss_dialogue_t (fillout-on-show dispatch, via + * wuss_dialogue_create_on_window). A caller that wants to show it points its + * "Info" wuss_menu_item_t::window at wuss_proginfo_window(), then calls + * wuss_proginfo_set_desc() with its own field values before opening the menu + * chain. The stored desc is only applied -- resizing the window and + * rewriting its rows via wuss_info_set_rows -- when the dialogue is actually + * revealed, from the home task's wuss_EVENT_PRE_SHOW handler via + * wuss_dialogue_handle_pre_show(); see wuss_proginfo_handle_pre_show(), a + * thin cap over that call. This way the same window is correctly sized for + * whichever caller most recently asked to show it, even though its text + * differs per caller. + * + * The dialogue lives on one "home" task -- normally whichever task's caller + * created it first, or a dedicated task if several unrelated tasks share it + * -- and every wuss_EVENT_PRE_SHOW for wuss_proginfo_window() must reach + * that task's handler and be forwarded to wuss_proginfo_handle_pre_show(). + * As for wuss_info_t, that task must not be an autoclose task and must + * outlive the handle, and the dialogue must outlive every menu chain that + * borrows it as a wuss_menu_item_t::window. * * Built only when WUSS_COMPONENTS is defined (which implies WUSS_MENUS). */ @@ -38,8 +51,8 @@ extern "C" /** * The rows of a program-information dialogue. Every field is borrowed and - * copied; a NULL field is left out entirely, so a zero-initialised desc with - * only \c name set yields a one-row dialogue. + * copied; a NULL field is left out entirely, so a desc with only \c name set + * yields a one-row dialogue. */ typedef struct wuss_proginfo_desc { @@ -52,31 +65,30 @@ typedef struct wuss_proginfo_desc wuss_proginfo_desc_t; /** - * Opaque handle: a wuss_info_t. Owns the dialogue window and its strings. + * Opaque handle: owns the dialogue's window, wuss_info_t and + * wuss_dialogue_t. */ -typedef wuss_info_t wuss_proginfo_t; +typedef struct wuss_proginfo wuss_proginfo_t; /* ----------------------------------------------------------------------- */ /** * Build a program-information dialogue: a hidden window on \p task, laid out * with one label row per non-NULL field of \p desc, in the order Name, - * Purpose, Author, Version. - * - * The window's on-screen size is derived from \p task's font metrics and the - * text in \p desc; it is created with wuss_WINDOW_HIDDEN, so nothing shows - * until the caller reveals it (directly, or via a wuss_menu_item_t::window - * that names wuss_proginfo_window()). + * Purpose, Author, Version. \p desc need only be some starting size -- e.g. + * the first caller's own values -- since wuss_proginfo_set_desc relays out + * and resizes the window for whoever is about to show it next. * * \param[out] out Filled with the new handle on success, untouched on * failure. - * \param[in] task Task the dialogue window is created on. Must not be an - * autoclose task and must outlive the handle. - * \param[in] desc Field values; each borrowed and copied. At least \c name - * must be non-NULL. + * \param[in] task Task the dialogue window is created on ("home task"; see + * \ref proginfo.h). Must not be an autoclose task and must + * outlive the handle. + * \param[in] desc Initial field values; each borrowed and copied. At least + * \c name must be non-NULL. * \return \ref result_OK, \ref result_OOM, \ref result_NULL_ARG if \p out, - * \p task, \p desc or \c desc->name is NULL, or a wuss_info_create - * code. + * \p task, \p desc or \c desc->name is NULL, or a wuss_info_create / + * wuss_dialogue_create_on_window code. */ result_t wuss_proginfo_create(wuss_proginfo_t **out, wuss_task_t *task, @@ -84,21 +96,43 @@ result_t wuss_proginfo_create(wuss_proginfo_t **out, /** * Free a program-information dialogue: closes its window and frees every - * string in it. Safe to pass NULL. An alias for wuss_info_destroy. + * string in it. Safe to pass NULL. * * \param[in] doomed Handle to free, or NULL. */ -#define wuss_proginfo_destroy wuss_info_destroy +void wuss_proginfo_destroy(wuss_proginfo_t *doomed); + +/** + * Point the dialogue at \p desc's field values, to be applied -- resizing + * the window and rewriting its rows -- the next time it is revealed. Call + * this before opening the menu chain that shows wuss_proginfo_window(), so + * whichever caller last asked gets its own values. + * + * \param[in] pi Handle. + * \param[in] desc Field values; each borrowed and copied when applied. At + * least \c name must be non-NULL. + */ +void wuss_proginfo_set_desc(wuss_proginfo_t *pi, + const wuss_proginfo_desc_t *desc); + +/** + * Handle a wuss_EVENT_PRE_SHOW for wuss_proginfo_window() from the home + * task's own handler: applies the desc last passed to + * wuss_proginfo_set_desc, as wuss_dialogue_handle_pre_show. + * + * \param[in] pi Handle. + * \return \ref result_OK, or a wuss_info_set_rows code. + */ +result_t wuss_proginfo_handle_pre_show(wuss_proginfo_t *pi); /** - * The dialogue's window, for use as a wuss_menu_item_t::window or to show - * and hide directly. Borrowed; valid until wuss_proginfo_destroy. NULL only - * if \p pi is NULL. An alias for wuss_info_window. + * The dialogue's window, for use as a wuss_menu_item_t::window. Borrowed; + * valid until wuss_proginfo_destroy. NULL only if \p pi is NULL. * * \param[in] pi Handle. * \return The window, or NULL. */ -#define wuss_proginfo_window wuss_info_window +wuss_window_t *wuss_proginfo_window(const wuss_proginfo_t *pi); #ifdef __cplusplus } diff --git a/include/wuss/icon-spec.h b/include/wuss/icon-spec.h new file mode 100644 index 0000000..cec884d --- /dev/null +++ b/include/wuss/icon-spec.h @@ -0,0 +1,223 @@ +/* wuss/icon-spec.h -- fill helpers for common wuss_icon_spec_t shapes */ + +/** + * \file icon-spec.h + * + * A task laying out a dialogue with wuss_icon_create_array typically builds + * several wuss_icon_spec_t entries that only differ in bbox, text and a + * couple of type-specific fields. These helpers fill one spec in place for + * the three shapes that recur across the test tasks: a plain or + * right-justified label, a horizontal or vertical slider, and an action + * button (optionally the dialogue's default action). Each takes the spec to + * fill plus its distinguishing fields; the caller is still responsible for + * memset(specs, 0, sizeof(specs)) up front and for wuss_icon_create_array. + */ + +#ifndef WUSS_ICON_SPEC_H +#define WUSS_ICON_SPEC_H + +#ifdef __cplusplus +extern "C" +{ +#endif + +#include "base/result.h" +#include "geom/box.h" + +#include "wuss/icon.h" +#include "wuss/window.h" + +#ifdef WUSS_ICONS + +/* ----------------------------------------------------------------------- */ + +/** + * A slider paired with a label that echoes its current value, the recurring + * "Label: [slider] value" row seen in dialogues across the test tasks (see + * saturn.c's size dialogue and icons.c's Sliders group). The value label's + * text is formatted by \c fmt (a single printf-style "%d" conversion; NULL + * defaults to "%d") every time \ref wuss_slider_row_set or \ref + * wuss_slider_row_event changes the value, so callers never format it by + * hand. If \c step is non-zero, both functions also snap the value to the + * nearest multiple of \c step above \c min, so callers never hand-roll that + * either. + */ +typedef struct wuss_slider_row +{ + /** The slider icon. Not owned; destroyed with its window as normal. */ + wuss_icon_t *slider; + /** The label icon echoing the slider's value. Not owned. */ + wuss_icon_t *value; + /** printf-style format for the value label, one "%d" conversion. + * Borrowed; must outlive the row (a string literal in practice). NULL + * means "%d". */ + const char *fmt; + /** Value at the slider's groove start and end; snapping rounds to a + * multiple of \c step away from \c min and clamps to [min,max]. Match + * the \c min/\c max passed to \ref wuss_icon_spec_slider_row. */ + int min, max; + /** Snapping step; 0 means "no snapping" (any value is kept as-is). */ + int step; +} +wuss_slider_row_t; + +/** + * Fill \p slider_spec and \p value_spec as a wuss_ICON_TYPE_SLIDER and its + * paired wuss_ICON_TYPE_LABEL value echo, ready for \ref + * wuss_icon_create_array alongside the row's own text label (built + * separately with \ref wuss_icon_spec_label). Call \ref wuss_slider_row_bind + * on the two created icons afterwards to get a usable \ref + * wuss_slider_row_t. + * + * \param[out] slider_spec Spec to fill as the slider; overwritten. + * \param[out] value_spec Spec to fill as the value label; overwritten. + * \param[in] slider_bbox Slider's bounding box. + * \param[in] value_bbox Value label's bounding box. + * \param[in] fg Groove/fill and text colour. + * \param[in] orientation Groove direction. + * \param[in] min Value at the groove's start. + * \param[in] max Value at the groove's end. + * \param[in] default_value Initial value, clamped to [min,max] and (if \p + * step is non-zero) snapped to a multiple of it. + * \param[in] fmt printf-style format for the value label, one + * "%d" conversion; NULL for "%d". Borrowed; must + * outlive the row. + * \param[in] step Snapping step passed through to \ref + * wuss_slider_row_bind; 0 for none. + */ +void wuss_icon_spec_slider_row(wuss_icon_spec_t *slider_spec, + wuss_icon_spec_t *value_spec, + box_t slider_bbox, + box_t value_bbox, + wuss_colour_t fg, + wuss_slider_orientation_t orientation, + int min, + int max, + int default_value, + const char *fmt, + int step); + +/** + * Bind a \ref wuss_slider_row_t to the icons \ref wuss_icon_create_array + * made from a pair of specs filled by \ref wuss_icon_spec_slider_row. + * + * \param[out] row Row to fill. + * \param[in] slider The created slider icon. + * \param[in] value The created value-label icon. + * \param[in] fmt Same format string passed to \ref + * wuss_icon_spec_slider_row; borrowed, must outlive the + * row. NULL for "%d". + * \param[in] min Same \c min passed to \ref wuss_icon_spec_slider_row. + * \param[in] max Same \c max passed to \ref wuss_icon_spec_slider_row. + * \param[in] step Same \c step passed to \ref wuss_icon_spec_slider_row; + * 0 for no snapping. + */ +void wuss_slider_row_bind(wuss_slider_row_t *row, + wuss_icon_t *slider, + wuss_icon_t *value, + const char *fmt, + int min, + int max, + int step); + +/** + * Set a slider row's value programmatically -- the slider fill and its + * formatted value label both update, invalidating each so the next redraw + * repaints them. No task event is delivered, matching \ref + * wuss_icon_set_value. + * + * \param[in] window Window the row's icons belong to. + * \param[in] row Row to change. + * \param[in] value New value; snapped to \c row->step (if non-zero) and + * clamped to [row->min,row->max]. + * \return \ref result_OK, or \ref result_OOM from formatting the label. + */ +result_t wuss_slider_row_set(wuss_window_t *window, + const wuss_slider_row_t *row, + int value); + +/** + * Handle a wuss_EVENT_ICON delivered to the row's window: if it names this + * row's slider and is a drag (MOUSE_DOWN or MOUSE_MOVE), snaps the value to + * \c row->step (if non-zero, re-setting the slider itself so it visibly + * jumps to the snapped position), reformats the value label to match, and + * reports the new value. Any other icon or action is left untouched. + * + * \param[in] window Window the row's icons belong to. + * \param[in] row Row to check against. + * \param[in] event The wuss_EVENT_ICON event. + * \param[out] value Set to the new (already-snapped) value when handled. + * May be NULL. + * \return Non-zero if \p event was a drag on this row's slider (and so was + * handled), zero otherwise. + */ +int wuss_slider_row_event(wuss_window_t *window, + const wuss_slider_row_t *row, + const wuss_event_t *event, + int *value); + +/** + * Fill \p spec as a wuss_ICON_TYPE_LABEL. + * + * \param[out] spec Spec to fill; any prior contents are overwritten. + * \param[in] bbox Bounding box. + * \param[in] text Label text, borrowed until + * wuss_icon_create(_array) copies it. + * \param[in] fg Text colour. + * \param[in] justify_right Non-zero to right-justify + * (wuss_ICON_FLAGS_JUSTIFY_RIGHT) instead of the + * default left justification. + */ +void wuss_icon_spec_label(wuss_icon_spec_t *spec, + box_t bbox, + const char *text, + wuss_colour_t fg, + int justify_right); + +/** + * Fill \p spec as a wuss_ICON_TYPE_SLIDER. + * + * \param[out] spec Spec to fill; any prior contents are + * overwritten. + * \param[in] bbox Bounding box. + * \param[in] fg Groove/fill colour. + * \param[in] orientation Groove direction. + * \param[in] min Value at the groove's start. + * \param[in] max Value at the groove's end. + * \param[in] default_value Initial value, clamped to [min,max]. + */ +void wuss_icon_spec_slider(wuss_icon_spec_t *spec, + box_t bbox, + wuss_colour_t fg, + wuss_slider_orientation_t orientation, + int min, + int max, + int default_value); + +/** + * Fill \p spec as a wuss_ICON_TYPE_ACTION button. + * + * \param[out] spec Spec to fill; any prior contents are overwritten. + * \param[in] bbox Bounding box. + * \param[in] text Button text, borrowed until wuss_icon_create(_array) + * copies it. + * \param[in] fg Text colour. + * \param[in] bg Fill colour. + * \param[in] is_default Non-zero to draw as the dialogue's default action + * (wuss_ICON_FLAGS_DEFAULT) instead of an ordinary + * button. + */ +void wuss_icon_spec_action(wuss_icon_spec_t *spec, + box_t bbox, + const char *text, + wuss_colour_t fg, + wuss_colour_t bg, + int is_default); + +#endif /* WUSS_ICONS */ + +#ifdef __cplusplus +} +#endif + +#endif /* WUSS_ICON_SPEC_H */ diff --git a/include/wuss/icon.h b/include/wuss/icon.h index 57ade2b..e1a5c63 100644 --- a/include/wuss/icon.h +++ b/include/wuss/icon.h @@ -15,7 +15,7 @@ * * Wuss fills a window's background, draws its icons, then delivers * wuss_EVENT_REDRAW, so a task is free to paint over or around icon pixels. - * A click on a wuss_ICON_TYPE_BUTTON reaches the task as wuss_EVENT_ICON; + * A click on a wuss_ICON_TYPE_ACTION reaches the task as wuss_EVENT_ICON; * clicks on a label, or on a hidden or disabled icon, fall through as * wuss_EVENT_MOUSE. */ @@ -58,13 +58,13 @@ typedef enum wuss_icon_type /** Bevelled rectangle with a centred text label and pressed-state visual * feedback; clicks and hovers are delivered to the task as * wuss_EVENT_ICON. */ - wuss_ICON_TYPE_BUTTON, - /** Bounding box filled with a repeating two-colour 8x8 tile (spec.pattern) in - * fg/bg, phased to document space so it scrolls rigidly with content. Not - * interactive: clicks fall through to the task as wuss_EVENT_MOUSE. text is - * ignored. */ + wuss_ICON_TYPE_ACTION, + /** Bounding box filled with a repeating two-colour 8x8 tile + * (spec.u.pattern.tile) in fg/bg, phased to document space so it scrolls + * rigidly with content. Not interactive: clicks fall through to the task as + * wuss_EVENT_MOUSE. text is ignored. */ wuss_ICON_TYPE_PATTERN, - /** A grouping box: a one-pixel rectangle in fg around the bounding box, + /** A grouping box: a bevelled rectangle around the bounding box, * broken at the top-left for an optional caption (text) drawn over the * window background. Not interactive: clicks fall through to the task as * wuss_EVENT_MOUSE. */ @@ -79,10 +79,10 @@ typedef enum wuss_icon_type * its own selected state (group is ignored), then the task is told via * wuss_EVENT_ICON. */ wuss_ICON_TYPE_OPTION, - /** A caller-owned bitmap (spec.bitmap) drawn at the top-left of the bounding - * box, alpha-blended against what is already there, clipped to the box; no - * scaling. The bitmap is borrowed, not copied, and must outlive the icon - * (unlike text). fg, bg, text and pattern are ignored. Not interactive + /** A caller-owned bitmap (spec.u.bitmap.image) drawn at the top-left of the + * bounding box, alpha-blended against what is already there, clipped to the + * box; no scaling. The bitmap is borrowed, not copied, and must outlive the + * icon (unlike text). fg, bg, text and pattern are ignored. Not interactive * unless wuss_ICON_FLAGS_INTERACTIVE is set, in which case clicks raise * wuss_EVENT_ICON like a button. */ wuss_ICON_TYPE_BITMAP, @@ -101,7 +101,34 @@ typedef enum wuss_icon_type * window background. Purely decorative: never hit-tested, never highlights. * text, bg and pattern are ignored. Used between menu rows to render the * line a wuss_ICON_FLAGS_SEPARATOR entry sits below. */ - wuss_ICON_TYPE_RULE + wuss_ICON_TYPE_RULE, + /** A slider: the bounding box is filled with slider.surround, with a fixed + * 4px gap (also slider.surround) around an inner sunken groove + * (spec.u.slider) that fills the remaining space on both axes; its fill + * marks a value between min and max. A click or drag anywhere in the + * inner rect -- fill or bare groove, but not the surround or gap -- jumps + * the value straight to the pointer position; the task is told via + * wuss_EVENT_ICON, continuously while dragging. */ + wuss_ICON_TYPE_SLIDER, + + /* The following types are reserved: the enum values and validation exist but + * no rendering, hit-testing or event routing is wired up yet. A spec using + * one is accepted and currently draws as a plain wuss_ICON_TYPE_LABEL. */ + + /** A read-only value field: a bevelled well showing text the task updates but + * the user cannot edit. Not yet implemented. */ + wuss_ICON_TYPE_DISPLAY, + /** An editable single-line text field. Not yet implemented. */ + wuss_ICON_TYPE_WRITABLE, + /** An editable numeric field, optionally with up/down adjusters. Not yet + * implemented. */ + wuss_ICON_TYPE_NUMBER, + /** A field cycling through a fixed set of string values. Not yet + * implemented. */ + wuss_ICON_TYPE_STRING_SET, + /** A free-drag handle: reports pointer motion to the task while dragged. Not + * yet implemented. */ + wuss_ICON_TYPE_DRAGGABLE } wuss_icon_type_t; @@ -148,7 +175,7 @@ typedef enum wuss_icon_flags /** wuss_ICON_TYPE_LABEL: centre the text in the bounding box. Takes * precedence over wuss_ICON_FLAGS_JUSTIFY_RIGHT. */ wuss_ICON_FLAGS_JUSTIFY_CENTRE = 1 << 3, - /** wuss_ICON_TYPE_BUTTON: draw as a default action button -- an accent fill + /** wuss_ICON_TYPE_ACTION: draw as a default action button -- an accent fill * (see wuss_config_t::accent) inside the same 6px "action" surround as * wuss_ICON_BORDER_ACTION, instead of the ordinary 1px bevel. Ignored by * other icon types. */ @@ -166,7 +193,7 @@ typedef enum wuss_icon_flags * laid out and drawn as a separate wuss_ICON_TYPE_RULE icon. Ignored by * other types. */ wuss_ICON_FLAGS_SEPARATOR = 1 << 7, - /** wuss_ICON_TYPE_MENU_ENTRY: draw a small colour chip (spec.swatch) in the + /** wuss_ICON_TYPE_MENU_ENTRY: draw a small colour chip (spec.u.menu_entry.swatch) in the * row's left gutter, where the tick would sit. Mutually exclusive with a * selected tick -- the chip wins. Ignored by other types. */ wuss_ICON_FLAGS_SWATCH = 1 << 8, @@ -188,11 +215,112 @@ wuss_icon_flags_t; /** * Encode a 0-based icon-set index (from \ref wuss_icons_lookup) for - * wuss_icon_spec::icon_set. The stored value is offset by one so a + * wuss_icon_spec_data::bitmap::set. The stored value is offset by one so a * zero-initialised spec reads as "no icon-set entry". */ #define wuss_ICON_SET(i) ((i) + 1) +/** + * A wuss_ICON_TYPE_SLIDER's axis: which way its groove runs and, so, which + * end of the groove (inset from the bounding box by the fixed 4px gap on all + * four sides, filling the rest) is pixel 0 of the value fill. + */ +typedef enum wuss_slider_orientation +{ + /** Groove runs left-to-right; value grows rightward, pixel 0 at the + * groove's left. */ + wuss_SLIDER_HORIZONTAL = 0, + /** Groove runs top-to-bottom; value grows upward (low at the bottom, high + * at the top), matching a volume-slider convention -- pixel 0 at the + * groove's bottom. */ + wuss_SLIDER_VERTICAL +} +wuss_slider_orientation_t; + +/** + * Per-type payload in a wuss_icon_spec. Exactly one arm applies, selected by + * wuss_icon_spec::type; the arms mirror the icon's internal storage. Types + * with no type-specific data (ACTION, FRAME, OPTION, RULE, and the reserved + * types) touch no arm, so a zero-initialised spec is valid for them. Each + * arm is its own struct so a type can gain fields without disturbing the + * others. + */ +typedef union wuss_icon_spec_data +{ + /** wuss_ICON_TYPE_LABEL */ + struct + { + /** Border drawn inside the bounding box. Zero (wuss_ICON_BORDER_NONE) is + * the default for a zero-initialised spec. */ + wuss_icon_border_t border; + } + label; + + /** wuss_ICON_TYPE_PATTERN */ + struct + { + /** Repeating two-colour 8x8 tile. Zero (screen_PATTERN_SOLID) is a safe + * default for a zero-initialised spec. */ + screen_pattern_t tile; + } + pattern; + + /** wuss_ICON_TYPE_BITMAP */ + struct + { + /** The image to draw. Borrowed, not copied; must outlive the icon. NULL + * (the default) falls back to \c set. */ + const bitmap_t *image; + /** When \c image is NULL, draw an entry from the window manager's loaded + * icon set (see \ref wuss_icons_load). Zero (the default for a + * zero-initialised spec) means "no icon-set entry"; encode a 0-based + * index from \ref wuss_icons_lookup with \ref wuss_ICON_SET. Ignored + * when \c image is set. */ + int set; + } + bitmap; + + /** wuss_ICON_TYPE_RADIO */ + struct + { + /** Exclusive-selection group. Selecting a radio clears every other + * selected radio on the same window with the same group. Zero (the + * default) means "no group": such a radio still toggles but never clears + * another. */ + int group; + } + radio; + + /** wuss_ICON_TYPE_MENU_ENTRY */ + struct + { + /** With wuss_ICON_FLAGS_SWATCH: the colour chip to draw in the left + * gutter, as an index into the system palette. Ignored unless that flag + * is set. */ + wuss_colour_t swatch; + } + menu_entry; + + /** wuss_ICON_TYPE_SLIDER */ + struct + { + /** Which way the groove runs. wuss_SLIDER_HORIZONTAL (0) is the default + * for a zero-initialised spec. */ + wuss_slider_orientation_t orientation; + /** Value at the groove's start (left for wuss_SLIDER_HORIZONTAL, bottom + * for VERTICAL). May be greater than max to run the fill backwards. */ + int min; + /** + * Value at the groove's end (right for HORIZONTAL, top for VERTICAL). + */ + int max; + /** Initial value, clamped to [min,max] (or [max,min] if min > max). */ + int default_value; + } + slider; +} +wuss_icon_spec_data_t; + /** * Description of an icon at creation. Copied by value into the icon; the * caller keeps ownership of \c text, which is copied. @@ -204,51 +332,41 @@ wuss_icon_flags_t; typedef struct wuss_icon_spec { /** Bounding box, virtual document space, inclusive-exclusive. */ - box_t bbox; + box_t bbox; /** Icon type. */ - wuss_icon_type_t type; + wuss_icon_type_t type; /** NUL-terminated label; copied. NULL means "". */ - const char *text; + const char *text; /** Text colour, as an index into the system palette. */ - wuss_colour_t fg; + wuss_colour_t fg; /** Fill/bevel base colour, as an index into the system palette. A label, * frame, radio or option icon may pass wuss_NO_BACKGROUND for no fill behind * its text/glyph; a button or pattern icon must pass a real index. */ - wuss_colour_t bg; - /** Tile for wuss_ICON_TYPE_PATTERN; ignored by other types. Zero - * (screen_PATTERN_SOLID) is a safe default for zero-initialised specs. */ - screen_pattern_t pattern; - /** wuss_ICON_TYPE_BITMAP: the image to draw. Borrowed, not copied; must - * outlive the icon. Ignored by other types; NULL (the default) is only valid - * when type is not wuss_ICON_TYPE_BITMAP, or when \c icon_set selects a - * bitmap from the window manager's loaded icon set instead. */ - const bitmap_t *bitmap; - /** wuss_ICON_TYPE_BITMAP: when \c bitmap is NULL, draw an entry from the - * window manager's loaded icon set (see \ref wuss_icons_load). Zero (the - * default for a zero-initialised spec) means "no icon-set entry"; encode a - * 0-based index from \ref wuss_icons_lookup with \ref wuss_ICON_SET. - * Ignored by other types and when \c bitmap is set. */ - int icon_set; - /** wuss_ICON_TYPE_RADIO: exclusive-selection group. Selecting a radio clears - * every other selected radio on the same window with the same group. Zero - * (the default) means "no group": such a radio still toggles but never - * clears another. Ignored by all other icon types. */ - int group; - /** wuss_ICON_TYPE_MENU_ENTRY with wuss_ICON_FLAGS_SWATCH: the colour chip to - * draw in the left gutter, as an index into the system palette. Ignored - * unless that flag is set; ignored by all other icon types. */ - wuss_colour_t swatch; - /** wuss_ICON_TYPE_LABEL: border drawn inside the bounding box. Zero - * (wuss_ICON_BORDER_NONE) is the default for zero-initialised specs. - * Ignored by all other icon types. */ - wuss_icon_border_t border; + wuss_colour_t bg; /** Appearance/behaviour flags. */ - wuss_icon_flags_t flags; + wuss_icon_flags_t flags; + /** Per-type payload, selected by \c type. */ + wuss_icon_spec_data_t u; } wuss_icon_spec_t; /* ----------------------------------------------------------------------- */ +/** Standard main-axis size (px) for a wuss_ICON_TYPE_SLIDER. */ +#define wuss_STD_SLIDER_HEIGHT 18 +/** Standard main-axis size (px) for a non-default wuss_ICON_TYPE_ACTION + * button, e.g. Cancel. */ +#define wuss_STD_SECONDARY_BUTTON_HEIGHT 26 +/** Standard main-axis size (px) for a wuss_ICON_FLAGS_DEFAULT + * wuss_ICON_TYPE_ACTION button, e.g. OK/Apply. */ +#define wuss_STD_PRIMARY_BUTTON_HEIGHT 34 +/** Standard gap (px) between two sibling icons/components in a layout. */ +#define wuss_STD_GAP 4 +/** Standard gap (px) between an icon/component and the window edge. */ +#define wuss_STD_INSET 4 + +/* ----------------------------------------------------------------------- */ + /** * Create an icon on a window. The icon is owned by the window and freed when * the window is closed (or the window manager destroyed). Its bounding box @@ -316,31 +434,38 @@ result_t wuss_icon_plot(wuss_window_t *window, /** * Destroy an icon, unlinking it from its window and invalidating its - * bounding box so the next redraw clears it. Safe to pass NULL. + * bounding box so the next redraw clears it. Safe to pass NULL for \p icon. * - * \param[in] icon Icon to destroy, or NULL. + * \param[in] window Window the icon belongs to. + * \param[in] icon Icon to destroy, or NULL. */ -void wuss_icon_delete(wuss_icon_t *icon); +void wuss_icon_delete(wuss_window_t *window, wuss_icon_t *icon); /** * Replace an icon's label text. The new text is copied. Invalidates the * icon's bounding box. * - * \param[in] icon Icon to change. - * \param[in] text New NUL-terminated label; copied. NULL means "". + * \param[in] window Window the icon belongs to. + * \param[in] icon Icon to change. + * \param[in] text New NUL-terminated label; copied. NULL means "". * \return \ref result_OK on success, \ref result_OOM on allocation failure * (the icon keeps its old text). */ -result_t wuss_icon_set_text(wuss_icon_t *icon, const char *text); +result_t wuss_icon_set_text(wuss_window_t *window, + wuss_icon_t *icon, + const char *text); /** * Show or hide an icon, toggling wuss_ICON_FLAGS_HIDDEN. Invalidates the * icon's bounding box. * + * \param[in] window Window the icon belongs to. * \param[in] icon Icon to change. * \param[in] hidden Non-zero to hide the icon, zero to show it. */ -void wuss_icon_set_hidden(wuss_icon_t *icon, int hidden); +void wuss_icon_set_hidden(wuss_window_t *window, + wuss_icon_t *icon, + int hidden); /** * Fetch an icon's bounding box, in virtual document space. @@ -367,14 +492,6 @@ wuss_icon_type_t wuss_icon_get_type(const wuss_icon_t *icon); */ const char *wuss_icon_get_text(const wuss_icon_t *icon); -/** - * Fetch the window an icon belongs to. - * - * \param[in] icon Icon to query. - * \return The owning window. - */ -wuss_window_t *wuss_icon_get_window(const wuss_icon_t *icon); - /** * Fetch a radio or option icon's selected (latched) state. * @@ -392,10 +509,36 @@ int wuss_icon_get_selected(const wuss_icon_t *icon); * programmatic path, distinct from a user click. A no-op for icon types with * no latched state. * + * \param[in] window Window the icon belongs to. * \param[in] icon Icon to change. * \param[in] selected Non-zero to select, zero to deselect. */ -void wuss_icon_set_selected(wuss_icon_t *icon, int selected); +void wuss_icon_set_selected(wuss_window_t *window, + wuss_icon_t *icon, + int selected); + +/** + * Fetch a slider icon's current value. + * + * \param[in] icon Icon to query. + * \return The value, in [min,max] (or [max,min]) as given at creation. + * Always 0 for icon types with no value. + */ +int wuss_icon_get_value(const wuss_icon_t *icon); + +/** + * Set a slider icon's value programmatically, invalidating it so the next + * redraw repaints it. No task event is delivered -- this is the programmatic + * path, distinct from a user click or drag. A no-op for icon types with no + * value. + * + * \param[in] window Window the icon belongs to. + * \param[in] icon Icon to change. + * \param[in] value New value; clamped to the icon's [min,max] range. + */ +void wuss_icon_set_value(wuss_window_t *window, + wuss_icon_t *icon, + int value); /* ----------------------------------------------------------------------- */ diff --git a/include/wuss/menu-desc.h b/include/wuss/menu-desc.h index d15c9a1..0eb6c3a 100644 --- a/include/wuss/menu-desc.h +++ b/include/wuss/menu-desc.h @@ -6,7 +6,8 @@ * A convenience helper on top of wuss/menu.h: parse a compact * PrivateEye-style descriptor string into a heap wuss_menu_t tree the caller * owns, and free it again. Nothing in the core menu helper depends on this; - * a task that hand-assembles its wuss_menu_t arrays never needs to include it. + * a task that hand-assembles its wuss_menu_t arrays never needs to include + * it. * * Built only when WUSS_MENUS is defined. */ @@ -35,15 +36,20 @@ extern "C" * leading '|' on an item draws a dashed rule above it, marking a group * boundary; the item itself stays an ordinary interactive row. A '{ ... }' * group after an item is that item's submenu. A per-token prefix '!' ticks - * the item, '~' shades (disables) it, and '>' attaches a submenu pulled as a - * const wuss_menu_t * from the varargs rather than from a following - * '{ }' block. A "%s" in a token substitutes the next const char * - * vararg. The '>' and "%s" varargs are consumed in the order they are - * encountered scanning left to right. + * the item and '~' shades (disables) it, each pulling the next int + * vararg -- must be 0 or 1, applying the flag iff 1 -- rather than applying + * unconditionally, so a task's live state drives the tick/shade directly + * instead of building the menu ticked/shaded throughout and mutating it + * afterwards. '>' attaches a submenu pulled as a const wuss_menu_t + * * from the varargs rather than from a following '{ }' block. A "%s" + * in a token substitutes the next const char * vararg. All of '!', + * '~', '>' and "%s" pull their vararg in the order encountered scanning left + * to right. * * Example: wuss_menu_create_from_desc(&m, "Display, Open, !Grid, - * ~Export, |Quit") -- "Display" is the caption; the menu has four - * items, with a dashed rule above "Quit". + * ~Export, |Quit", grid_on, !can_export) -- "Display" is the caption; + * the menu has four items, with a dashed rule above "Quit", "Grid" ticked + * iff \c grid_on and "Export" shaded iff \c can_export is false. * * The whole tree, including copied label text, is one heap allocation graph * owned by the caller; free it with wuss_menu_destroy. wuss_menu_open treats @@ -53,7 +59,8 @@ extern "C" * failure. * \param[in] desc Descriptor string; its first token is the root caption. * \return \ref result_OK, \ref result_OOM, or \ref result_BAD_ARG for a - * malformed descriptor (unbalanced braces, empty token, too deep). + * malformed descriptor (unbalanced braces, empty token, too deep, or + * a '!'/'~' vararg that is not 0 or 1). */ result_t wuss_menu_create_from_desc(wuss_menu_t **out, const char *desc, ...); diff --git a/include/wuss/menu.h b/include/wuss/menu.h index 440fc81..8ce6e16 100644 --- a/include/wuss/menu.h +++ b/include/wuss/menu.h @@ -52,7 +52,13 @@ typedef enum wuss_menu_item_flags /** Draw a colour chip of \c swatch at the item's left edge, in place of * a tick */ - wuss_MENU_ITEM_SWATCH = 1 << 3 + wuss_MENU_ITEM_SWATCH = 1 << 3, + + /** \c submenu is borrowed (e.g. patched in after the menu was built) and + * must outlive the tree, not be freed with it: wuss_menu_destroy leaves + * it alone instead of recursing into it. Ignored on an item with no + * \c submenu. */ + wuss_MENU_ITEM_BORROWED_SUBMENU = 1 << 4 } wuss_menu_item_flags_t; @@ -134,13 +140,90 @@ void wuss_menu_close(wuss_menu_handle_t handle); int wuss_menu_is_open(wuss_menu_handle_t handle); /** - * Re-tick a currently open menu level in place, for a task that keeps an + * True when \p ev is a wuss_EVENT_MENU_SELECT whose pick keeps the chain + * open -- an ADJUST-button release (see wuss_menu_open). A SELECT-button + * pick has already closed and freed the chain by the time the event arrives, + * so a task that stored the wuss_menu_open handle must drop it in that case: + * + * \code if (!wuss_menu_should_keep_open(ev)) task->menu_handle = NULL; + * \endcode + * + * \param[in] ev The event passed to the task's handle callback. + * \return Non-zero if \p ev is a MENU_SELECT that leaves the chain open. + */ +int wuss_menu_should_keep_open(const wuss_event_t *ev); + +/* ----------------------------------------------------------------------- */ + +/* Setting ticks on menu data, whether or not it is currently open -- a task + * that edits the item array before a (re)opening wuss_menu_open, rather than + * re-ticking an already-open chain. See below for the _live equivalents that + * act on an open chain and redraw the changed rows in place. */ + +/** + * Set every row's tick from \p ticked (row i ticked iff bit i is set) on a + * menu that need not be open. \p menu is mutated (its items' + * wuss_MENU_ITEM_TICKED bit only); every other flag on each item is left as + * the caller set it. \c menu->nitems must not exceed the bit width of + * unsigned int. + * + * \param[in,out] menu Menu whose items' ticks are set. + * \param[in] ticked Bit i ticks row i; 0 unticks every row. + */ +void wuss_menu_tick_set(wuss_menu_t *menu, unsigned int ticked); + +/** + * Tick \p index and untick every other row of \p menu, whether or not it is + * open. Data-level equivalent of wuss_menu_tick_exclusive_live, for a task + * that edits the item array before a fresh wuss_menu_open rather than + * re-ticking an already-open chain. + * + * \param[in,out] menu Menu whose items' ticks are set. + * \param[in] index Row to tick, or -1 to untick every row. + */ +void wuss_menu_tick_exclusive(wuss_menu_t *menu, int index); + +/** + * Set or clear \p index's tick in \p menu, leaving every other item's tick + * untouched, whether or not the menu is open. Data-level equivalent of + * wuss_menu_tick_item_live, for a menu that tracks more than one independent + * tick (see wuss_menu_tick_item_live for why). + * + * \param[in,out] menu Menu whose item's tick is set. + * \param[in] index Row to update. A no-op if out of range. + * \param[in] ticked Non-zero to tick the row, zero to untick it. + */ +void wuss_menu_tick_item(wuss_menu_t *menu, int index, int ticked); + +/** + * Set every row's tick from \p ticked (as wuss_menu_tick_set), then open \p + * menu -- the tick-sync-then-open a task otherwise hand-rolls before each + * wuss_menu_open. + * + * \param[in] task As wuss_menu_open. + * \param[in] menu As wuss_menu_open, but non-const: its ticks are set. + * \param[in] ticked Bit i ticks row i; 0 unticks every row. + * \param[in] at As wuss_menu_open. + * \param[out] out As wuss_menu_open. + * \return As wuss_menu_open. + */ +result_t wuss_menu_open_ticked(wuss_task_t *task, + wuss_menu_t *menu, + unsigned int ticked, + point_t at, + wuss_menu_handle_t *out); + +/* ----------------------------------------------------------------------- */ + +/* Re-ticking a currently open menu level in place, for a task that keeps an * ADJUST-picked menu open (see wuss_menu_open) and wants its own tick to * change without rebuilding the chain: an ADJUST pick delivers * wuss_EVENT_MENU_SELECT but does not close or redraw the menu, so a task * that only edits its wuss_menu_item_t.flags array never sees it take effect - * on screen. - * + * on screen. These invalidate the changed rows; the data-level functions + * above do not. */ + +/** * Ticks item \p index and unticks every other item of the open level whose * \c menu is \p menu (searched from \p handle's chain), then invalidates the * changed rows. A no-op if \p handle is stale/closed, \p menu is not an open @@ -153,62 +236,46 @@ int wuss_menu_is_open(wuss_menu_handle_t handle); * reached via a wuss_menu_item_t.submenu. * \param[in] index Row to tick, or -1 to untick every row. */ -void wuss_menu_set_ticked(wuss_menu_handle_t handle, - const wuss_menu_t *menu, - int index); +void wuss_menu_tick_exclusive_live(wuss_menu_handle_t handle, + const wuss_menu_t *menu, + int index); /** - * Set or clear a single item's tick on a currently open menu level in place, - * leaving every other item's tick untouched. As wuss_menu_set_ticked, but - * for a menu that tracks more than one independent tick at the same level - * (e.g. a selection plus an unrelated toggle) where unticking every other - * row would clobber state the caller wanted to keep. + * Set every row's tick from \p ticked (row i ticked iff bit i is set) on a + * currently open menu level in place, then invalidates the changed rows. + * Live equivalent of wuss_menu_tick_set. A no-op if \p handle is + * stale/closed or \p menu is not an open level of its chain. \c menu->nitems + * must not exceed the bit width of unsigned int. * * \param[in] handle Chain handle from wuss_menu_open. * \param[in] menu The (sub)menu level to update; matched by pointer * against the description passed to wuss_menu_open or * reached via a wuss_menu_item_t.submenu. - * \param[in] index Row to update. A no-op if out of range. - * \param[in] ticked Non-zero to tick the row, zero to untick it. - */ -void wuss_menu_set_item_ticked(wuss_menu_handle_t handle, - const wuss_menu_t *menu, - int index, - int ticked); - -/** - * True when \p ev is a wuss_EVENT_MENU_SELECT whose pick keeps the chain - * open -- an ADJUST-button release (see wuss_menu_open). A SELECT-button - * pick has already closed and freed the chain by the time the event arrives, - * so a task that stored the wuss_menu_open handle must drop it in that case: - * - * \code if (!wuss_menu_should_keep_open(ev)) task->menu_handle = NULL; - * \endcode - * - * \param[in] ev The event passed to the task's handle callback. - * \return Non-zero if \p ev is a MENU_SELECT that leaves the chain open. + * \param[in] ticked Bit i ticks row i; 0 unticks every row. */ -int wuss_menu_should_keep_open(const wuss_event_t *ev); +void wuss_menu_tick_set_live(wuss_menu_handle_t handle, + const wuss_menu_t *menu, + unsigned int ticked); /** - * Set every row's tick from \p ticked (row i ticked iff ticked[i] - * is non-zero), then open \p menu -- the tick-sync-then-open a task - * otherwise hand-rolls before each wuss_menu_open. \p menu is mutated (its - * items' wuss_MENU_ITEM_TICKED bit only); every other flag on each item is - * left as the caller set it. + * Set or clear a single item's tick on a currently open menu level in place, + * leaving every other item's tick untouched. As + * wuss_menu_tick_exclusive_live, but for a menu that tracks more than one + * independent tick at the same level (e.g. a selection plus an unrelated + * toggle) where unticking every other row would clobber state the caller + * wanted to keep. * - * \param[in] task As wuss_menu_open. - * \param[in] menu As wuss_menu_open, but non-const: its ticks are set. - * \param[in] ticked Array of \c menu->nitems flags; NULL unticks every row. - * \param[in] at As wuss_menu_open. - * \param[out] out As wuss_menu_open. - * \return As wuss_menu_open. + * \param[in] handle Chain handle from wuss_menu_open. + * \param[in] menu The (sub)menu level to update; matched by pointer + * against the description passed to wuss_menu_open or + * reached via a wuss_menu_item_t.submenu. + * \param[in] index Row to update. A no-op if out of range. + * \param[in] ticked Non-zero to tick the row, zero to untick it. */ -result_t wuss_menu_open_ticked(wuss_task_t *task, - wuss_menu_t *menu, - const int *ticked, - point_t at, - wuss_menu_handle_t *out); +void wuss_menu_tick_item_live(wuss_menu_handle_t handle, + const wuss_menu_t *menu, + int index, + int ticked); /* ----------------------------------------------------------------------- */ diff --git a/include/wuss/task.h b/include/wuss/task.h index e3ea2af..c5ad153 100644 --- a/include/wuss/task.h +++ b/include/wuss/task.h @@ -85,7 +85,19 @@ typedef enum wuss_event_kind * stored the wuss_menu_open handle must drop it here: the chain is * already freed. Not fired for a SELECT pick (a wuss_EVENT_MENU_SELECT * with a chain-closing button) or for a wuss_menu_close the task made. */ - wuss_EVENT_MENU_CLOSED + wuss_EVENT_MENU_CLOSED, + /** The pointer has moved onto this window's on-screen footprint (content + * or furniture) from elsewhere. Exactly one is outstanding per window at + * a time, always balanced by a later wuss_EVENT_POINTER_EXIT. Carries no + * data. Fires only on the window-crossing edge: moving between content + * and furniture within the same window does not re-fire it. */ + wuss_EVENT_POINTER_ENTER, + /** The pointer has left this window's footprint -- onto another window, + * onto no window, or because the window is closing under it. Balances an + * earlier wuss_EVENT_POINTER_ENTER. Carries no data. A task must not + * assume the window is still usable if this arrives during teardown; it + * is safe to read but a wuss_EVENT_CLOSE may follow immediately. */ + wuss_EVENT_POINTER_EXIT } wuss_event_kind_t; @@ -96,7 +108,8 @@ wuss_event_kind_t; * * Members: wuss_EVENT_REDRAW, wuss_EVENT_MOUSE, wuss_EVENT_ICON, * wuss_EVENT_SCROLL, wuss_EVENT_OPEN, wuss_EVENT_PRE_SHOW, wuss_EVENT_SHOW, - * wuss_EVENT_PRE_CLOSE, wuss_EVENT_CLOSE. + * wuss_EVENT_PRE_CLOSE, wuss_EVENT_CLOSE, wuss_EVENT_POINTER_ENTER, + * wuss_EVENT_POINTER_EXIT. */ typedef wuss_event_kind_t wuss_window_event_kind_t; @@ -161,11 +174,14 @@ typedef struct wuss_event mouse; /** wuss_EVENT_ICON: delivered instead of wuss_EVENT_MOUSE while the - * pointer is inside a wuss_ICON_TYPE_BUTTON icon's bounding box. + * pointer is inside a wuss_ICON_TYPE_ACTION icon's bounding box. * Label, hidden and disabled icons never raise this; those clicks * fall through as wuss_EVENT_MOUSE. action is DOWN/UP/MOVE; button * is a set of wuss_button_t flags, so test it with '&' rather than - * comparing for equality. In the task view (window == NULL) this is + * comparing for equality. value is the icon's current value for a + * wuss_ICON_TYPE_SLIDER (updated before this event is delivered, so it + * always reflects the click/drag that raised it); meaningless for every + * other icon type. In the task view (window == NULL) this is * reserved for a future shared/dock element and is never currently * emitted. */ struct @@ -173,6 +189,7 @@ typedef struct wuss_event wuss_icon_t *icon; wuss_mouse_action_t action; wuss_button_t button; + int value; } icon; diff --git a/include/wuss/window.h b/include/wuss/window.h index a491483..d02a4f9 100644 --- a/include/wuss/window.h +++ b/include/wuss/window.h @@ -47,8 +47,8 @@ extern "C" * \param[in] title Titlebar label, or NULL for none. Copied in, truncated * if too long. Ignored if flags includes * wuss_WINDOW_NO_TITLEBAR. - * \param[in] flags Appearance flags, e.g. wuss_WINDOW_NO_TITLEBAR / - * wuss_WINDOW_NO_OUTLINE, OR'd together, or + * \param[in] flags Appearance flags, e.g. wuss_WINDOW_CLOSE / + * wuss_WINDOW_VSCROLL, OR'd together, or * wuss_WINDOW_DEFAULT for the default furniture. * \param[in] bg Content background, filled by Wuss before each redraw: * a flat colour or an 8x8 fill pattern (see diff --git a/include/wuss/wuss.h b/include/wuss/wuss.h index 836a0f8..4943e26 100644 --- a/include/wuss/wuss.h +++ b/include/wuss/wuss.h @@ -145,7 +145,10 @@ typedef unsigned char wuss_colour_t; * wuss_COLOUR_BUTTON_PRESSED is button.pressed, * wuss_COLOUR_ACCENT is accent.colour, * wuss_COLOUR_BACKDROP is backdrop.colour, - * wuss_COLOUR_WINDOW is body.window, wuss_COLOUR_MENU is body.menu. */ + * wuss_COLOUR_WINDOW is body.window, wuss_COLOUR_MENU is body.menu, + * wuss_COLOUR_SLIDER_TRACK is slider.track, + * wuss_COLOUR_SLIDER_VALUE is slider.value, + * wuss_COLOUR_SLIDER_SURROUND is slider.surround. */ #define wuss_COLOUR_TITLE_BG (wuss_COLOUR_SYMBOLIC + 16) #define wuss_COLOUR_TITLE_FG (wuss_COLOUR_SYMBOLIC + 17) #define wuss_COLOUR_BUTTON_HILIGHT (wuss_COLOUR_SYMBOLIC + 18) @@ -158,6 +161,9 @@ typedef unsigned char wuss_colour_t; #define wuss_COLOUR_BUTTON_PRESSED (wuss_COLOUR_SYMBOLIC + 25) #define wuss_COLOUR_BORDER_DIVIDER (wuss_COLOUR_SYMBOLIC + 26) #define wuss_COLOUR_BUTTON_BG (wuss_COLOUR_SYMBOLIC + 27) +#define wuss_COLOUR_SLIDER_TRACK (wuss_COLOUR_SYMBOLIC + 28) +#define wuss_COLOUR_SLIDER_VALUE (wuss_COLOUR_SYMBOLIC + 29) +#define wuss_COLOUR_SLIDER_SURROUND (wuss_COLOUR_SYMBOLIC + 30) /** Furniture chrome colours, one entry per class of furniture. Title is * the only two-tone class (fill + text); the rest are drawn as a single @@ -193,48 +199,54 @@ wuss_furniture_palette_t; * * \note When the library is built with the WUSS_FURNITURE CMake option off, * every window is chromeless regardless of these flags and the - * wuss_WINDOW_NO_* bits are ignored. + * furniture bits (wuss_WINDOW_NO_TITLEBAR and below) are ignored. */ typedef enum wuss_window_flags { - /** Default: every furniture region drawn. */ - wuss_WINDOW_DEFAULT = 0, - /** - * No titlebar; content fills the full visible area, and no drag handle - * exists. + * Titlebar suppressed; content fills the full visible area and + * wuss_WINDOW_CLOSE/BACK/TOGGLE_SIZE are ignored. Without this flag, a + * titlebar is present, content is inset below it, and it offers a drag + * handle. */ - wuss_WINDOW_NO_TITLEBAR = 1 << 0, + wuss_WINDOW_NO_TITLEBAR = 1 << 0, - /** No 1px border drawn around the visible area. */ - wuss_WINDOW_NO_OUTLINE = 1 << 1, + /** 1px border suppressed; without this flag it is drawn around the + * visible area. */ + wuss_WINDOW_NO_OUTLINE = 1 << 1, /** - * No close icon in the titlebar. Ignored if flags includes + * Close icon in the titlebar. Ignored if flags also includes * wuss_WINDOW_NO_TITLEBAR. */ - wuss_WINDOW_NO_CLOSE = 1 << 2, + wuss_WINDOW_CLOSE = 1 << 2, /** - * No send-to-back icon in the titlebar. Ignored if flags includes + * Send-to-back icon in the titlebar. Ignored if flags also includes * wuss_WINDOW_NO_TITLEBAR. */ - wuss_WINDOW_NO_BACK = 1 << 3, + wuss_WINDOW_BACK = 1 << 3, /** - * No toggle-size icon in the titlebar. Ignored if flags includes + * Toggle-size icon in the titlebar. Ignored if flags also includes * wuss_WINDOW_NO_TITLEBAR. */ - wuss_WINDOW_NO_TOGGLE_SIZE = 1 << 4, + wuss_WINDOW_TOGGLE_SIZE = 1 << 4, - /** No vertical scrollbar on the right edge. */ - wuss_WINDOW_NO_VSCROLL = 1 << 5, + /** Vertical scrollbar on the right edge. */ + wuss_WINDOW_VSCROLL = 1 << 5, - /** No horizontal scrollbar on the bottom edge. */ - wuss_WINDOW_NO_HSCROLL = 1 << 6, + /** Horizontal scrollbar on the bottom edge. */ + wuss_WINDOW_HSCROLL = 1 << 6, - /** No resize icon in the bottom-right corner. */ - wuss_WINDOW_NO_RESIZE = 1 << 7, + /** Resize icon in the bottom-right corner. */ + wuss_WINDOW_RESIZE = 1 << 7, + + /** Default: every furniture region drawn. */ + wuss_WINDOW_DEFAULT = wuss_WINDOW_CLOSE | wuss_WINDOW_BACK | + wuss_WINDOW_TOGGLE_SIZE | + wuss_WINDOW_VSCROLL | wuss_WINDOW_HSCROLL | + wuss_WINDOW_RESIZE, /** * A resize (drag or toggle-size) always fully redraws the window's content @@ -249,8 +261,8 @@ typedef enum wuss_window_flags * keeps its place in the z-order but is not drawn and not hit-tested, so * it neither occludes other windows nor catches the pointer. Its position * can still be changed with wuss_window_move while hidden, ready for when - * it is shown again. Unlike the wuss_WINDOW_NO_* bits this one is toggled - * at runtime, and it is honoured regardless of the WUSS_FURNITURE build + * it is shown again. Unlike the furniture bits this one is toggled at + * runtime, and it is honoured regardless of the WUSS_FURNITURE build * option. */ wuss_WINDOW_HIDDEN = 1 << 9, @@ -309,6 +321,10 @@ wuss_backdrop_t; #define wuss_BACKDROP_COLOUR(c) \ ((wuss_backdrop_t) { (c), screen_PATTERN_SOLID, wuss_NO_BACKGROUND }) +/** wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND): no fill at all, background + * painting left to the task. */ +#define wuss_NO_BACKDROP wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND) + /** A patterned wuss_backdrop_t: 8x8 pattern p tiled in colour c over * background colour b. */ #define wuss_BACKDROP_PATTERN(c, p, b) ((wuss_backdrop_t) { (c), (p), (b) }) @@ -353,7 +369,7 @@ typedef struct wuss_config /** * Work-area button colours, as indices into the system palette. bg is the - * button face, used when a wuss_ICON_TYPE_BUTTON spec passes + * button face, used when a wuss_ICON_TYPE_ACTION spec passes * wuss_NO_BACKGROUND rather than its own fill. fg is the button label. * pressed replaces bg on the face while the button is held down. bg and * pressed default to bevel.light and bevel.dark when config is NULL; fg @@ -371,7 +387,7 @@ typedef struct wuss_config button; /** - * Accent colour for a default action button -- a wuss_ICON_TYPE_BUTTON + * Accent colour for a default action button -- a wuss_ICON_TYPE_ACTION * created with wuss_ICON_FLAGS_DEFAULT, drawn to stand out from the * ordinary bevelled buttons around it (RISC OS's "default action button"): * its face fill at rest, and the moat of the wuss_ICON_BORDER_ACTION @@ -385,6 +401,25 @@ typedef struct wuss_config } accent; + /** + * wuss_ICON_TYPE_SLIDER colours, as indices into the system palette: + * surround is the icon's full bounding box, including the fixed 4px gap + * around the inner slider rect; track is the sunken groove's ground (the + * unfilled portion) within that inner rect; value is the fill marking the + * current value. surround and value default to button.bg and track to + * body.window when config is NULL. Read back through + * wuss_COLOUR_SLIDER_SURROUND / wuss_COLOUR_SLIDER_TRACK / + * wuss_COLOUR_SLIDER_VALUE. Ignored when WUSS_ICONS is off. + */ + struct + { + wuss_colour_t surround; /**< Bounding box and gap around the + * inner rect. */ + wuss_colour_t track; /**< Groove ground (unfilled portion). */ + wuss_colour_t value; /**< Fill marking the current value. */ + } + slider; + /** * Conventional body fills, as system-palette indices: window is the * work-area behind a task's content, menu the pop-up menu background. Read @@ -464,11 +499,16 @@ wuss_font_desc_t; * default palette. * \param[in] npalette Number of entries in palette. Ignored if palette is * NULL. - * \param[in] config Creation-time configuration, or NULL for defaults. - * \param[in] alloc Allocator hooks, copied in, or NULL for \ref - * wuss_alloc (plain stdlib). Must outlive nothing -- - * only the three function pointers are kept. - * \param[out] wuss Newly created window manager. + * \param[in] config Creation-time configuration, or NULL for defaults. + * \param[in] alloc Allocator hooks, copied in, or NULL for \ref + * wuss_alloc (plain stdlib). Must outlive nothing -- + * only the three function pointers are kept. + * \param[in] resources Borrowed root path a caller-defined convention (e.g. + * a demo app's task modules) can use to locate its own + * bundled files; wuss itself never reads it. Must + * outlive the wuss_t. NULL if not needed -- see + * wuss_get_resources. + * \param[out] wuss Newly created window manager. * \return \ref result_OK on success, \ref result_BAD_ARG if \p nfonts is * negative or exceeds \ref wuss_MAX_FONTS, \ref * result_WUSS_BAD_COLOUR if any of config's palette entries are out @@ -481,8 +521,20 @@ result_t wuss_create(screen_t *scr, int npalette, const wuss_config_t *config, const wuss_alloc_t *alloc, + const char *resources, wuss_t **wuss); +/** + * Fetch the resources root path passed to wuss_create (see its \p resources + * parameter), for a task to locate its own bundled files without every + * caller threading the path through its own create function. + * + * \param[in] wuss Window manager. + * \return The resources root, borrowed, valid until wuss_destroy; or NULL if + * wuss_create was given none. + */ +const char *wuss_get_resources(const wuss_t *wuss); + /** * Replace the system palette partway through a session. * @@ -590,6 +642,63 @@ wuss_font_class_t wuss_get_font_class_n(const wuss_t *wuss, int index); */ const char *wuss_get_font_name_n(const wuss_t *wuss, int index); +/** + * Measure a run of text in one of wuss's configured fonts (see + * wuss_get_font_n). Equivalent to bmfont_measure, kept alongside + * wuss_text_draw so tasks need not include framebuf/bmfont.h themselves for + * simple text. + * + * \param[in] wuss Window manager. + * \param[in] index Font slot, 0..\ref wuss_MAX_FONTS - 1. + * \param[in] text Text to measure. + * \param[in] len Number of characters to consider. + * \param[in] target_width Width to split within, or \ref + * bmfont_width_UNLIMITED for none. + * \param[out] split_point Character count that fits within \p + * target_width, or NULL if not wanted. + * \param[out] actual_width Width of the (possibly split) text, or NULL if + * not wanted. + * \return \ref result_OK on success, \ref result_WUSS_BAD_INDEX if \p index + * is out of range or its slot is empty. + */ +result_t wuss_text_measure(const wuss_t *wuss, + int index, + const char *text, + int len, + bmfont_width_t target_width, + int *split_point, + bmfont_width_t *actual_width); + +/** + * Draw a run of text in one of wuss's configured fonts (see + * wuss_get_font_n). Equivalent to bmfont_draw, kept alongside + * wuss_text_measure so tasks need not include framebuf/bmfont.h themselves + * for simple text. + * + * \param[in] wuss Window manager. + * \param[in] index Font slot, 0..\ref wuss_MAX_FONTS - 1. + * \param[in] scr Screen to draw to. + * \param[in] text Text to draw. + * \param[in] len Number of characters to consider. + * \param[in] fg Foreground (ink) colour. + * \param[in] bg Background colour, or \ref colour_NONE for + * transparent. + * \param[in] pos Baseline position to start drawing at. + * \param[out] end_pos Baseline position after the drawn text, or NULL if not + * wanted. + * \return \ref result_OK on success, \ref result_WUSS_BAD_INDEX if \p index + * is out of range or its slot is empty. + */ +result_t wuss_text_draw(const wuss_t *wuss, + int index, + screen_t *scr, + const char *text, + int len, + colour_t fg, + colour_t bg, + const point_t *pos, + point_t *end_pos); + /** * The last pointer position seen by wuss_mouse_click or wuss_mouse_move, * screen space. (0,0) until the first mouse event. Handy for opening a @@ -677,6 +786,31 @@ int wuss_get_dirty_count(const wuss_t *wuss); */ void wuss_get_dirty(const wuss_t *wuss, int index, box_t *out); +/** + * Fetch the bounding box of every screen-space region "touched" since the + * last wuss_clear_touched -- pixels a fast blit path (window move/resize, + * scroll) slid to a new position without a repaint, because the backing + * bitmap was already correct there. wuss_redraw_dirty never repaints these; + * they exist for a frontend that re-uploads the screen to a display surface + * (e.g. a GPU texture) piecemeal and needs to know such pixels moved even + * though wuss itself did no drawing there this frame. + * + * \param[in] wuss Window manager. + * \param[out] out Filled in with the union of every touched region. + * \return 1 if anything was touched (and \p out was filled in), 0 if nothing + * was. + */ +int wuss_get_touched_extent(const wuss_t *wuss, box_t *out); + +/** + * Clear the regions accumulated for wuss_get_touched_extent. A frontend + * calls this once it has folded the touched extent into whatever it presents + * this frame, so the next frame starts from empty. + * + * \param[in] wuss Window manager. + */ +void wuss_clear_touched(wuss_t *wuss); + /** * Deliver a mouse-down or mouse-up event (action must be wuss_MOUSE_DOWN or * wuss_MOUSE_UP). Hit-tests the topmost window at (x,y). On a down, a diff --git a/libraries/databases/filename-db/filename-db.c b/libraries/databases/filename-db/filename-db.c index 9ba41f7..e639c6f 100644 --- a/libraries/databases/filename-db/filename-db.c +++ b/libraries/databases/filename-db/filename-db.c @@ -106,7 +106,7 @@ static result_t unformat_key(const char *buf, void **key, void *opaque) { - result_t err; + result_t err; unsigned char hash[digestdb_DIGESTSZ]; int kindex; @@ -134,7 +134,7 @@ static result_t unformat_value(const char *buf, void **value, void *opaque) { - result_t err; + result_t err; filenamedb_t *db = opaque; atom_t vindex; @@ -164,7 +164,7 @@ static const pickle_unformat_methods_t unformat_methods = result_t filenamedb_open(const char *filename, filenamedb_t **pdb) { - result_t err; + result_t err; char *filenamecopy = NULL; atom_set_t *filenames = NULL; hash_t *hash = NULL; @@ -316,7 +316,7 @@ result_t filenamedb_add(filenamedb_t *db, const char *id, const char *filename) { - result_t err; + result_t err; int kindex; atom_t vindex; const unsigned char *key; diff --git a/libraries/databases/pickle/pickle.c b/libraries/databases/pickle/pickle.c index eadb172..0feb5f7 100644 --- a/libraries/databases/pickle/pickle.c +++ b/libraries/databases/pickle/pickle.c @@ -27,6 +27,7 @@ static result_t pickle__write_header(FILE *f, size_t commentslen) { static const char commentchar[] = "# "; + int rc; rc = fprintf(f, "%s%.*s\n%s\n", diff --git a/libraries/databases/pickle/unpickle.c b/libraries/databases/pickle/unpickle.c index a0c2be2..aa803b2 100644 --- a/libraries/databases/pickle/unpickle.c +++ b/libraries/databases/pickle/unpickle.c @@ -46,13 +46,13 @@ static result_t unpickle__parse_line(unpickle__state_t *state, { result_t err; - char *key; - char *keyend; - void *fmtkey; + char *key; + char *keyend; + void *fmtkey; - char *value; - char *valueend; - void *fmtvalue; + char *value; + char *valueend; + void *fmtvalue; key = buf; diff --git a/libraries/databases/tag-db/tag-db.c b/libraries/databases/tag-db/tag-db.c index 95865d1..af6b12d 100644 --- a/libraries/databases/tag-db/tag-db.c +++ b/libraries/databases/tag-db/tag-db.c @@ -210,7 +210,7 @@ static void destroy_hash_value(void *value) result_t tagdb_open(const char *filename, tagdb_t **pdb) { - result_t err; + result_t err; char *filenamecopy = NULL; atom_set_t *tags = NULL; hash_t *hash = NULL; @@ -890,9 +890,9 @@ static int getidbytags_cb(const void *key, const void *value, void *opaque) { result_t err; struct enumerate_state *state = opaque; - const bitvec_t *v; - bitvec_t *have; - int eq; + const bitvec_t *v; + bitvec_t *have; + int eq; /* work out where we are by counting callbacks (ugh) */ if (state->count++ < state->start) @@ -927,9 +927,9 @@ result_t tagdb_enumerate_ids_by_tags(tagdb_t *db, unsigned char *buf, size_t bufsz) { - result_t err; - bitvec_t *want = NULL; - int i; + result_t err; + bitvec_t *want = NULL; + int i; struct enumerate_state state; assert(db); diff --git a/libraries/datastruct/atom/new.c b/libraries/datastruct/atom/new.c index f8a3b23..d6cbc01 100644 --- a/libraries/datastruct/atom/new.c +++ b/libraries/datastruct/atom/new.c @@ -155,14 +155,14 @@ result_t atom_new(atom_set_t *s, size_t sizet_length, atom_t *patom) { - result_t err; - atom_t atom; - int length; - locpool_t *pend; - locpool_t *p; - loc_t *lend; - loc_t *l; - unsigned int i; + result_t err; + atom_t atom; + int length; + locpool_t *pend; + locpool_t *p; + loc_t *lend; + loc_t *l; + unsigned int i; assert(s); assert(block); diff --git a/libraries/datastruct/bitvec/ensure.c b/libraries/datastruct/bitvec/ensure.c index 02f74c0..249a194 100644 --- a/libraries/datastruct/bitvec/ensure.c +++ b/libraries/datastruct/bitvec/ensure.c @@ -17,8 +17,8 @@ result_t bitvec_ensure(bitvec_t *v, unsigned int need) { if (need > v->length) { - unsigned int length; - bitvec_T *vec; + unsigned int length; + bitvec_T *vec; length = need; diff --git a/libraries/datastruct/cache/cache.c b/libraries/datastruct/cache/cache.c index d911b8b..623fd7a 100644 --- a/libraries/datastruct/cache/cache.c +++ b/libraries/datastruct/cache/cache.c @@ -581,8 +581,8 @@ result_t cache_put(cache_t *c, { const size_t quantum = sizeof(free_t); - size_t rounded_length; - void *storeptr; + size_t rounded_length; + void *storeptr; assert(c != NULL); assert(data != NULL); diff --git a/libraries/datastruct/vector/set-width.c b/libraries/datastruct/vector/set-width.c index 26d4fd6..705038d 100644 --- a/libraries/datastruct/vector/set-width.c +++ b/libraries/datastruct/vector/set-width.c @@ -15,9 +15,9 @@ result_t vector_set_width(vector_t *v, size_t width) { - void *newbase; - size_t currsz; - size_t newsz; + void *newbase; + size_t currsz; + size_t newsz; if (width == 0) return result_BAD_ARG; diff --git a/libraries/framebuf/bitmap/bitmap.c b/libraries/framebuf/bitmap/bitmap.c index 04c07a3..25c5ac0 100644 --- a/libraries/framebuf/bitmap/bitmap.c +++ b/libraries/framebuf/bitmap/bitmap.c @@ -186,6 +186,114 @@ static result_t bmconv_p4_to_bgrx8888(const bitmap_t *src, bitmap_t **pdst) return rc; } +/* As bmconv_p4_to_bgrx8888 but for 8bpp paletted: one index per byte, so the + * inner loop is a plain byte read and table lookup. */ +static result_t bmconv_p8_to_bgrx8888(const bitmap_t *src, bitmap_t **pdst) +{ + result_t rc; + const pixelmap_t *pm; + const pixelfmt_bgrx8888_t *map; + bitmap_t *dst; + pixelfmt_bgrx8888_t *outpixels; + const unsigned char *inrow; + int x, y; + + assert(src); + assert(src->palette); + + pm = pixelmap_get(pixelfmt_p8, pixelfmt_bgrx8888, src->palette, 256); + if (pm == NULL) + return result_NOT_SUPPORTED; + map = (const pixelfmt_bgrx8888_t *) pm->entries; + + outpixels = malloc(src->size.w * sizeof(pixelfmt_bgrx8888_t) * src->size.h); + if (outpixels == NULL) + return result_OOM; + + dst = malloc(sizeof(*dst)); + if (dst == NULL) + { + free(outpixels); + return result_OOM; + } + + rc = bitmap_init(dst, + src->size, + pixelfmt_bgrx8888, + src->size.w * sizeof(pixelfmt_bgrx8888_t), + NULL, + outpixels); + if (rc) + return rc; + + inrow = src->base; + for (y = 0; y < src->size.h; y++) + { + for (x = 0; x < src->size.w; x++) + *outpixels++ = map[inrow[x]]; + inrow += src->rowbytes; + } + + *pdst = dst; + + return rc; +} + +/* As bmconv_p8_to_bgrx8888 but into rgbx8888 -- for a caller (e.g. + * screen_copy_bitmap) that assumes the R,G,B,A/X byte order documented on + * bitmap_load_png(), rather than the SDL-display byte order the *_to_bgrx8888 + * family targets. */ +static result_t bmconv_p8_to_rgbx8888(const bitmap_t *src, bitmap_t **pdst) +{ + result_t rc; + const pixelmap_t *pm; + const pixelfmt_rgbx8888_t *map; + bitmap_t *dst; + pixelfmt_rgbx8888_t *outpixels; + const unsigned char *inrow; + int x, y; + + assert(src); + assert(src->palette); + + pm = pixelmap_get(pixelfmt_p8, pixelfmt_rgbx8888, src->palette, 256); + if (pm == NULL) + return result_NOT_SUPPORTED; + map = (const pixelfmt_rgbx8888_t *) pm->entries; + + outpixels = malloc(src->size.w * sizeof(pixelfmt_rgbx8888_t) * src->size.h); + if (outpixels == NULL) + return result_OOM; + + dst = malloc(sizeof(*dst)); + if (dst == NULL) + { + free(outpixels); + return result_OOM; + } + + rc = bitmap_init(dst, + src->size, + pixelfmt_rgbx8888, + src->size.w * sizeof(pixelfmt_rgbx8888_t), + NULL, + outpixels); + if (rc) + return rc; + + inrow = src->base; + for (y = 0; y < src->size.h; y++) + { + for (x = 0; x < src->size.w; x++) + *outpixels++ = map[inrow[x]]; + inrow += src->rowbytes; + } + + *pdst = dst; + + return rc; +} + /* As bmconv_p4_to_bgrx8888 but for 1bpp paletted, packed MSB-first (bit 7 is * the leftmost pixel) to match the screen p1 plot helpers. */ static result_t bmconv_p1_to_bgrx8888(const bitmap_t *src, bitmap_t **pdst) @@ -336,6 +444,20 @@ result_t bitmap_convert(const bitmap_t *src, } break; + case pixelfmt_p8: + switch (newfmt) + { + case pixelfmt_bgrx8888: + return bmconv_p8_to_bgrx8888(src, dst); + + case pixelfmt_rgbx8888: + return bmconv_p8_to_rgbx8888(src, dst); + + default: + return result_NOT_SUPPORTED; + } + break; + default: return result_NOT_SUPPORTED; } diff --git a/libraries/framebuf/bitmap/load.c b/libraries/framebuf/bitmap/load.c index 9bc2230..28be7f0 100644 --- a/libraries/framebuf/bitmap/load.c +++ b/libraries/framebuf/bitmap/load.c @@ -34,6 +34,8 @@ result_t bitmap_load_png(bitmap_t *bm, const char *filename) png_bytep *row_pointers = NULL; pixelfmt_t bm_fmt; int bm_rowbytes; + colour_t plte[256]; + colour_t *bm_palette = NULL; png_uint_32 h; fp = fopen(filename, "rb"); @@ -77,8 +79,8 @@ result_t bitmap_load_png(bitmap_t *bm, const char *filename) NULL, NULL, NULL); /* paletted PNGs may be 1/2/4/8 bpp; png_set_packing() unpacks the - * sub-byte cases to one index per byte before the palette expansion - * below. every other colour type must already be 8-bit. */ + * sub-byte cases to one index per byte. every other colour type must + * already be 8-bit. */ if (pngbitdepth != 8 && pngcolourtype != PNG_COLOR_TYPE_PALETTE) { rc = result_INCOMPATIBLE; @@ -88,17 +90,41 @@ result_t bitmap_load_png(bitmap_t *bm, const char *filename) switch (pngcolourtype) { case PNG_COLOR_TYPE_PALETTE: - png_set_packing(png_ptr); - png_set_palette_to_rgb(png_ptr); - if (png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS)) + /* keep a native paletted PNG as pixelfmt_p8 (one index per byte), + * carrying its PLTE across as the bitmap's palette, rather than + * expanding to RGB */ { - png_set_tRNS_to_alpha(png_ptr); - bm_fmt = pixelfmt_rgba8888; - } - else - { - bm_fmt = pixelfmt_rgbx8888; - png_set_filler(png_ptr, 0xFF, PNG_FILLER_AFTER); + png_colorp png_palette; + int nplte; + png_bytep trns = NULL; + int ntrns = 0; + int i; + + png_set_packing(png_ptr); + + if (png_get_PLTE(png_ptr, info_ptr, &png_palette, &nplte) == 0) + { + rc = result_INCOMPATIBLE; + goto cleanup; + } + if (png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS)) + png_get_tRNS(png_ptr, info_ptr, &trns, &ntrns, NULL); + + for (i = 0; i < nplte; i++) + { + int a; + + a = (i < ntrns) ? trns[i] : 0xFF; + plte[i] = colour_rgba(png_palette[i].red, + png_palette[i].green, + png_palette[i].blue, + a); + } + for (; i < 256; i++) + plte[i] = colour_rgba(0, 0, 0, 0xFF); + + bm_fmt = pixelfmt_p8; + bm_palette = plte; } break; case PNG_COLOR_TYPE_RGB: @@ -128,15 +154,16 @@ result_t bitmap_load_png(bitmap_t *bm, const char *filename) png_read_image(png_ptr, row_pointers); - bitmap_init(bm, SIZE2D(pngwidth, pngheight), - bm_fmt, - bm_rowbytes, - NULL, /* no palette */ - pixels); + rc = bitmap_init(bm, SIZE2D(pngwidth, pngheight), + bm_fmt, + bm_rowbytes, + bm_palette, /* p8: PLTE copied in; NULL for deep formats */ + pixels); + if (rc != result_OK) + goto cleanup; pixels = NULL; /* belongs to bitmap now */ - rc = result_OK; goto cleanup; oom: diff --git a/libraries/framebuf/bitmap/save.c b/libraries/framebuf/bitmap/save.c index a8ce58a..c30590c 100755 --- a/libraries/framebuf/bitmap/save.c +++ b/libraries/framebuf/bitmap/save.c @@ -16,13 +16,17 @@ result_t bitmap_save_png(const bitmap_t *bm, const char *filename) result_t rc; volatile pixelfmt_t fmt; volatile size_t bytespp; - FILE *fp; - png_structp png_ptr = NULL; + volatile int paletted; + FILE *fp; + png_structp png_ptr = NULL; png_infop volatile info_ptr = NULL; png_bytep volatile outrow = NULL; + png_colorp volatile png_plte = NULL; pixelfmt_xxxa8888_t *inrow; // more like xxxx8888 int x,y; + paletted = 0; + switch (bm->format) { case pixelfmt_bgrx8888: @@ -33,6 +37,13 @@ result_t bitmap_save_png(const bitmap_t *bm, const char *filename) fmt = PNG_COLOR_TYPE_RGBA; bytespp = 4; break; + case pixelfmt_p8: + if (bm->palette == NULL) + return result_NOT_SUPPORTED; + fmt = PNG_COLOR_TYPE_PALETTE; + bytespp = 1; + paletted = 1; + break; default: return result_NOT_SUPPORTED; } @@ -74,6 +85,38 @@ result_t bitmap_save_png(const bitmap_t *bm, const char *filename) PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); + if (paletted) + { + png_byte trns[256]; + int have_trns; + int i; + + png_plte = png_malloc(png_ptr, 256 * sizeof(*png_plte)); + if (png_plte == NULL) + { + rc = result_OOM; + goto cleanup; + } + + have_trns = 0; + for (i = 0; i < 256; i++) + { + pixelfmt_rgba8888_t px = bm->palette[i].primary; + + png_plte[i].red = PIXELFMT_Rxxx8888(px); + png_plte[i].green = PIXELFMT_xGxx8888(px); + png_plte[i].blue = PIXELFMT_xxBx8888(px); + + trns[i] = PIXELFMT_xxxA8888(px); + if (trns[i] != 0xFF) + have_trns = 1; + } + + png_set_PLTE(png_ptr, info_ptr, png_plte, 256); + if (have_trns) + png_set_tRNS(png_ptr, info_ptr, trns, 256, NULL); + } + png_write_info(png_ptr, info_ptr); // consider: @@ -90,6 +133,21 @@ result_t bitmap_save_png(const bitmap_t *bm, const char *filename) inrow = bm->base; + if (paletted) + { + const unsigned char *srcrow = bm->base; + + for (y = 0; y < bm->size.h; y++) + { + png_write_row(png_ptr, srcrow); + srcrow += bm->rowbytes; + } + + png_write_end(png_ptr, NULL); + rc = result_OK; + goto cleanup; + } + for (y = 0; y < bm->size.h; y++) { png_bytep pout = &outrow[0]; @@ -131,6 +189,8 @@ result_t bitmap_save_png(const bitmap_t *bm, const char *filename) cleanup: fclose(fp); + if (png_plte != NULL) + png_free(png_ptr, png_plte); png_free_data(png_ptr, info_ptr, PNG_FREE_ALL, -1); png_destroy_write_struct(&png_ptr, NULL); free(outrow); diff --git a/libraries/framebuf/bitmap/test/rle-test.c b/libraries/framebuf/bitmap/test/rle-test.c index a70e95a..d0b3849 100644 --- a/libraries/framebuf/bitmap/test/rle-test.c +++ b/libraries/framebuf/bitmap/test/rle-test.c @@ -370,12 +370,123 @@ static int test_y8(void) /* ----------------------------------------------------------------------- */ +/* Synthesise a p8 bitmap with a known palette, save it as a palette-type PNG, + * reload it and check the format, palette (RGB + tRNS alpha) and pixel indices + * survive the round trip. Then bitmap_convert it to bgrx8888 and spot-check a + * pixel against its palette entry. */ +static int test_p8_png(void) +{ +#ifdef DPTLIB_IMAGES_READ_ONLY + return 1; /* ponytail: save disabled at build time, nothing to round trip */ +#else +#define P8W 24 +#define P8H 8 + bitmap_t bm; + bitmap_t rt; + bitmap_t *deep = NULL; + colour_t pal[256]; + colour_t rtpal[256]; + unsigned char pix[P8W * P8H]; + const char *fn; + result_t rc; + int i; + int ok = 1; + + fn = path_join_leafname("rle-test-p8", "png"); + + /* palette: greyscale ramp, plus a few non-opaque entries to exercise tRNS */ + for (i = 0; i < 256; i++) + pal[i] = colour_rgba((unsigned char) i, (unsigned char) i, + (unsigned char) i, 0xFF); + pal[0] = colour_rgba(0, 0, 0, 0x00); + pal[1] = colour_rgba(255, 0, 0, 0x80); + pal[200] = colour_rgba(10, 20, 30, 0xFF); + + for (i = 0; i < P8W * P8H; i++) + pix[i] = (unsigned char) ((i * 7 + i / P8W) & 0xFF); + + bitmap_init(&bm, SIZE2D(P8W, P8H), pixelfmt_p8, P8W, pal, pix); + + rc = bitmap_save_png(&bm, fn); + if (rc) { fprintf(stderr, "p8 png: save rc=&%x\n", rc); return 0; } + + rc = bitmap_load_png(&rt, fn); + remove(fn); + if (rc) { fprintf(stderr, "p8 png: load rc=&%x\n", rc); return 0; } + + if (rt.format != pixelfmt_p8) + { + fprintf(stderr, "p8 png: reloaded format &%x not p8\n", rt.format); + ok = 0; goto done; + } + if (rt.size.w != P8W || rt.size.h != P8H) + { + fprintf(stderr, "p8 png: reloaded size wrong\n"); + ok = 0; goto done; + } + if (rt.palette == NULL) + { + fprintf(stderr, "p8 png: reloaded bitmap has no palette\n"); + ok = 0; goto done; + } + + memcpy(rtpal, rt.palette, sizeof rtpal); + for (i = 0; i < 256; i++) + { + if (rtpal[i].primary != pal[i].primary) + { + fprintf(stderr, "p8 png: palette entry %d &%08x != &%08x\n", + i, rtpal[i].primary, pal[i].primary); + ok = 0; goto done; + } + } + + for (i = 0; i < P8W * P8H; i++) + { + if (((const unsigned char *) rt.base)[i] != pix[i]) + { + fprintf(stderr, "p8 png: pixel %d %u != %u\n", + i, ((const unsigned char *) rt.base)[i], pix[i]); + ok = 0; goto done; + } + } + + rc = bitmap_convert(&rt, pixelfmt_bgrx8888, &deep); + if (rc) { fprintf(stderr, "p8 png: convert rc=&%x\n", rc); ok = 0; goto done; } + + { + unsigned int idx = pix[0]; + pixelfmt_bgrx8888_t got = ((const pixelfmt_bgrx8888_t *) deep->base)[0]; + pixelfmt_rgba8888_t want = pal[idx].primary; + + if (PIXELFMT_xxRx8888(got) != PIXELFMT_Rxxx8888(want) + || PIXELFMT_xGxx8888(got) != PIXELFMT_xGxx8888(want) + || PIXELFMT_Bxxx8888(got) != PIXELFMT_xxBx8888(want)) + { + fprintf(stderr, "p8 png: converted pixel 0 &%08x != palette &%08x\n", + got, want); + ok = 0; + } + } + +done: + if (deep != NULL) { free(deep->base); free(deep); } + free(rt.base); + return ok; +#undef P8W +#undef P8H +#endif +} + +/* ----------------------------------------------------------------------- */ + result_t bitmap_rle_test(const char *resources) { int ok = 1; ok &= test_synthetic(); ok &= test_y8(); + ok &= test_p8_png(); ok &= test_blit(resources); return ok ? result_TEST_PASSED : result_TEST_FAILED; diff --git a/libraries/framebuf/bmfont/bmfont.c b/libraries/framebuf/bmfont/bmfont.c index aae597d..49d22ea 100644 --- a/libraries/framebuf/bmfont/bmfont.c +++ b/libraries/framebuf/bmfont/bmfont.c @@ -31,13 +31,18 @@ /* Input font PNGs must have 32 characters per row */ #define CHARS_PER_ROW (32) +/* Input advance widths are encoded 1px short of the true glyph advance; the + * missing pixel is letter spacing, added back in at use, not storage. */ +#define LETTER_SPACING (1) + /* -------------------------------------------------------------------------- */ /* Pixel values */ #define PIXEL_BG_IDX (0) /* background */ #define PIXEL_FG_IDX (1) /* font */ #define PIXEL_WIDTH_IDX (2) /* advance widths */ -#define PIXEL_GRID_IDX (3) /* grid (not presently interpreted) */ +#define PIXEL_GRID_IDX (3) /* grid: left sidebearing (cosmetic), baseline and + * cell-bottom rows (read back for ascent/descent) */ /* -------------------------------------------------------------------------- */ @@ -45,6 +50,8 @@ struct bmfont { png_uint_32 gridwidth, gridheight; /* pixels */ png_uint_32 charwidth, charheight; /* em size in pixels */ + int ascent; /* baseline offset from the top of a glyph cell */ + int descent; /* offset from the baseline to the cell bottom */ int totalchars; void *glyphs; @@ -60,13 +67,28 @@ struct bmfont /* -------------------------------------------------------------------------- */ -/** The advance width to use for glyph \p gid, honouring the monospace flag. */ -static bmfont_width_t bmfont_advance_for(const bmfont_t *bmfont, int gid) +/** The advance width to use for glyph \p gid, honouring the monospace flag + * and any caller-supplied extra letter/word spacing. Includes the + * LETTER_SPACING pixel not stored in the font. */ +static bmfont_width_t bmfont_advance_for(const bmfont_t *bmfont, + int gid, + int c, + const bmfont_spacing_t *spacing) { - if (bmfont->flags & bmfont_FLAG_MONOSPACE) - return bmfont->maxadw; + bmfont_width_t adw; + + adw = (bmfont->flags & bmfont_FLAG_MONOSPACE) ? bmfont->maxadw : + bmfont->adw[gid]; + adw += LETTER_SPACING; + + if (spacing) + { + adw += spacing->letter_spacing; + if (c == ' ') + adw += spacing->word_spacing; + } - return bmfont->adw[gid]; + return adw; } /* -------------------------------------------------------------------------- */ @@ -174,6 +196,67 @@ static int detect_gridheight(const unsigned char *pixels, return 0; } +/** True if row \p y of the cell is full-width PIXEL_GRID_IDX. */ +static int row_is_grid(const unsigned char *pixels, + size_t rowbytes, + png_uint_32 gridwidth, + png_uint_32 y) +{ + const unsigned char *row = pixels + rowbytes * y; + png_uint_32 x; + + for (x = 0; x < gridwidth; x++) + { + int px = (row[x >> 2] >> (6 - 2 * (x & 3))) & 3; + if (px != PIXEL_GRID_IDX) + return 0; + } + + return 1; +} + +/** + * Find the baseline and descent within a glyph cell. + * + * The space glyph (grid cell 0, gid 0) carries no ink, so its grid rows -- + * drawn by ttf2bmfont.py as full-width rows of PIXEL_GRID_IDX pixels across + * the cell, one at the baseline and one at the cell bottom -- are + * unambiguous. The first such row within the cell body is the baseline + * (*ascent is its offset from the top of the cell); the next one found at or + * below it is the cell bottom (*descent is its offset from the baseline). + * Returns 0 if no baseline row is found, e.g. a font predating this + * convention -- the caller falls back to treating the whole cell body as + * ascent, with no descender. + */ +static int detect_baseline_metrics(const unsigned char *pixels, + size_t rowbytes, + png_uint_32 gridwidth, + png_uint_32 gridheight, + int *ascent, + int *descent) +{ + png_uint_32 y; + int baseline_y = -1; + + for (y = 1; y < gridheight; y++) /* row 0 is the advance-width strip */ + { + if (!row_is_grid(pixels, rowbytes, gridwidth, y)) + continue; + + if (baseline_y < 0) + { + baseline_y = (int) y; + continue; + } + + *ascent = baseline_y - 1; /* -1: ascent excludes the strip row */ + *descent = (int) y - baseline_y; + return 1; + } + + return 0; +} + /** Verify the font format and build the advance width table. */ static result_t extract_advance_widths(bmfont_t *bmfont, void *voidpixels, @@ -558,11 +641,20 @@ result_t bmfont_create(const char *png, bmfont_t **pbmfont) bmfont->charheight = gridheight - 1; /* -1 for the advance width row */ bmfont->totalchars = CHARS_PER_ROW * pngheight / gridheight; bmfont->glyphrowbytes = (gridwidth + 7) / 8; /* stored glyph row: 1 or 2 */ + if (!detect_baseline_metrics(pixels, pngrowbytes, + (png_uint_32) gridwidth, + (png_uint_32) gridheight, + &bmfont->ascent, &bmfont->descent)) + { + bmfont->ascent = bmfont->charheight; /* no grid row: full height, no + * descender */ + bmfont->descent = 0; + } - logf_info("bmfont load png: charwidth=%d charheight=%d totalchars=%d " - "glyphrowbytes=%d", - bmfont->charwidth, bmfont->charheight, - bmfont->totalchars, bmfont->glyphrowbytes); + logf_info("bmfont load png: charwidth=%d charheight=%d ascent=%d " + "descent=%d totalchars=%d glyphrowbytes=%d", + bmfont->charwidth, bmfont->charheight, bmfont->ascent, + bmfont->descent, bmfont->totalchars, bmfont->glyphrowbytes); rc = extract_advance_widths(bmfont, pixels, pngwidth, pngheight, pngrowbytes); @@ -607,12 +699,20 @@ void bmfont_set_flags(bmfont_t *bmfont, bmfont_flags_t flags) bmfont->flags = flags; } -void bmfont_get_info(bmfont_t *bmfont, int *width, int *height) +void bmfont_get_info(bmfont_t *bmfont, + int *width, + int *height, + int *ascent, + int *descent) { if (width) - *width = bmfont->charwidth; + *width = bmfont->charwidth; if (height) - *height = bmfont->charheight; + *height = bmfont->charheight; + if (ascent) + *ascent = bmfont->ascent; + if (descent) + *descent = bmfont->descent; } int bmfont_get_count(bmfont_t *bmfont) @@ -620,15 +720,18 @@ int bmfont_get_count(bmfont_t *bmfont) return bmfont->totalchars; } -result_t bmfont_measure(bmfont_t *bmfont, - const char *text, - int textlen, - bmfont_width_t target_width, - int *split_point, - bmfont_width_t *actual_width) +result_t bmfont_measure(bmfont_t *bmfont, + const char *text, + int textlen, + const bmfont_spacing_t *spacing, + bmfont_width_t target_width, + int *split_point, + bmfont_width_t *actual_width) { bmfont_width_t current_width; int len; + int any_drawn; + int last_c; assert(bmfont); assert(text); @@ -637,6 +740,8 @@ result_t bmfont_measure(bmfont_t *bmfont, /* split_point, actual_width may be NULL */ current_width = 0; + any_drawn = 0; + last_c = 0; for (len = textlen; len; len--) { int c; @@ -648,19 +753,37 @@ result_t bmfont_measure(bmfont_t *bmfont, continue; gid = c - ' '; - advance = (gid < bmfont->totalchars) ? bmfont_advance_for(bmfont, gid) : 0; + advance = (gid < bmfont->totalchars) ? + bmfont_advance_for(bmfont, gid, c, spacing) : 0; next_width = current_width + advance; if (next_width > target_width) break; current_width = next_width; + any_drawn = 1; + last_c = c; } if (split_point) *split_point = textlen - len; if (actual_width) - *actual_width = current_width; + { + bmfont_width_t trailing_trim = 0; + + if (any_drawn) + { + trailing_trim = LETTER_SPACING; + if (spacing) + { + trailing_trim += spacing->letter_spacing; + if (last_c == ' ') + trailing_trim += spacing->word_spacing; + } + } + + *actual_width = current_width - trailing_trim; + } return result_OK; } @@ -1497,22 +1620,218 @@ static void bmfont_drawchar_any8888_2w_t(void *vscreen, } } -result_t bmfont_draw(bmfont_t *bmfont, - screen_t *scr, - const char *text, - int len, - colour_t fg, - colour_t bg, - const point_t *pos, - point_t *end_pos) +/* Draw a character, a maximum of one byte wide, to a p8 screen using an + * opaque background. */ +static void bmfont_drawchar_p8_1w_o(void *vscreen, + const void *vglyph, + int top_skip, + int right_skip, + int shift, + int rowbytes, + int charwidth, + int charheight, + pixelfmt_any_t fg, + pixelfmt_any_t bg) +{ + unsigned char *scr = vscreen; + const unsigned char *gly = vglyph; + int stride; + + NOT_USED(shift); + + gly += top_skip; + stride = rowbytes - charwidth; + + while (charheight--) + { + unsigned int row = *gly++; /* 1 byte wide font data */ + row >>= right_skip; /* compensate when right hand clipping */ + + switch (charwidth) /* jump table test */ + { + case 8: *scr++ = (row & (1u << 7)) ? fg : bg; /* fallthrough! */ + case 7: *scr++ = (row & (1u << 6)) ? fg : bg; + case 6: *scr++ = (row & (1u << 5)) ? fg : bg; + case 5: *scr++ = (row & (1u << 4)) ? fg : bg; + case 4: *scr++ = (row & (1u << 3)) ? fg : bg; + case 3: *scr++ = (row & (1u << 2)) ? fg : bg; + case 2: *scr++ = (row & (1u << 1)) ? fg : bg; + case 1: *scr++ = (row & (1u << 0)) ? fg : bg; + } + + scr += stride; + } +} + +/* Draw a character, a maximum of one byte wide, to a p8 screen using a + * transparent background. */ +static void bmfont_drawchar_p8_1w_t(void *vscreen, + const void *vglyph, + int top_skip, + int right_skip, + int shift, + int rowbytes, + int charwidth, + int charheight, + pixelfmt_any_t fg, + pixelfmt_any_t bg) +{ + unsigned char *scr = vscreen; + const unsigned char *gly = vglyph; + int stride; + + NOT_USED(shift); + NOT_USED(bg); + + gly += top_skip; + stride = rowbytes - charwidth; + + while (charheight--) + { + unsigned int row = *gly++; /* 1 byte wide font data */ + row >>= right_skip; /* compensate when right hand clipping */ + + switch (charwidth) /* jump table test */ + { + case 8: if (row & (1u << 7)) *scr = fg; scr++; /* fallthrough! */ + case 7: if (row & (1u << 6)) *scr = fg; scr++; + case 6: if (row & (1u << 5)) *scr = fg; scr++; + case 5: if (row & (1u << 4)) *scr = fg; scr++; + case 4: if (row & (1u << 3)) *scr = fg; scr++; + case 3: if (row & (1u << 2)) *scr = fg; scr++; + case 2: if (row & (1u << 1)) *scr = fg; scr++; + case 1: if (row & (1u << 0)) *scr = fg; scr++; + } + + scr += stride; + } +} + +/* Draw a character, a maximum of two bytes wide, to a p8 screen using an + * opaque background. */ +static void bmfont_drawchar_p8_2w_o(void *vscreen, + const void *vglyph, + int top_skip, + int right_skip, + int shift, + int rowbytes, + int charwidth, + int charheight, + pixelfmt_any_t fg, + pixelfmt_any_t bg) +{ + unsigned char *scr = vscreen; + const unsigned short *gly = vglyph; + int stride; + + NOT_USED(shift); + + gly += top_skip; + stride = rowbytes - charwidth; + + while (charheight--) + { + unsigned int row = *gly++; /* 2 bytes wide font data */ + row >>= right_skip; /* compensate when right hand clipping */ + + switch (charwidth) /* jump table test */ + { + case 16: *scr++ = (row & (1u << 15)) ? fg : bg; /* fallthrough! */ + case 15: *scr++ = (row & (1u << 14)) ? fg : bg; + case 14: *scr++ = (row & (1u << 13)) ? fg : bg; + case 13: *scr++ = (row & (1u << 12)) ? fg : bg; + case 12: *scr++ = (row & (1u << 11)) ? fg : bg; + case 11: *scr++ = (row & (1u << 10)) ? fg : bg; + case 10: *scr++ = (row & (1u << 9)) ? fg : bg; + case 9: *scr++ = (row & (1u << 8)) ? fg : bg; + case 8: *scr++ = (row & (1u << 7)) ? fg : bg; + case 7: *scr++ = (row & (1u << 6)) ? fg : bg; + case 6: *scr++ = (row & (1u << 5)) ? fg : bg; + case 5: *scr++ = (row & (1u << 4)) ? fg : bg; + case 4: *scr++ = (row & (1u << 3)) ? fg : bg; + case 3: *scr++ = (row & (1u << 2)) ? fg : bg; + case 2: *scr++ = (row & (1u << 1)) ? fg : bg; + case 1: *scr++ = (row & (1u << 0)) ? fg : bg; + } + + scr += stride; + } +} + +/* Draw a character, a maximum of two bytes wide, to a p8 screen using a + * transparent background. */ +static void bmfont_drawchar_p8_2w_t(void *vscreen, + const void *vglyph, + int top_skip, + int right_skip, + int shift, + int rowbytes, + int charwidth, + int charheight, + pixelfmt_any_t fg, + pixelfmt_any_t bg) +{ + unsigned char *scr = vscreen; + const unsigned short *gly = vglyph; + int stride; + + NOT_USED(shift); + NOT_USED(bg); + + gly += top_skip; + stride = rowbytes - charwidth; + + while (charheight--) + { + unsigned int row = *gly++; /* 2 bytes wide font data */ + row >>= right_skip; /* compensate when right hand clipping */ + + switch (charwidth) /* jump table test */ + { + case 16: if (row & (1u << 15)) *scr = fg; scr++; /* fallthrough! */ + case 15: if (row & (1u << 14)) *scr = fg; scr++; + case 14: if (row & (1u << 13)) *scr = fg; scr++; + case 13: if (row & (1u << 12)) *scr = fg; scr++; + case 12: if (row & (1u << 11)) *scr = fg; scr++; + case 11: if (row & (1u << 10)) *scr = fg; scr++; + case 10: if (row & (1u << 9)) *scr = fg; scr++; + case 9: if (row & (1u << 8)) *scr = fg; scr++; + case 8: if (row & (1u << 7)) *scr = fg; scr++; + case 7: if (row & (1u << 6)) *scr = fg; scr++; + case 6: if (row & (1u << 5)) *scr = fg; scr++; + case 5: if (row & (1u << 4)) *scr = fg; scr++; + case 4: if (row & (1u << 3)) *scr = fg; scr++; + case 3: if (row & (1u << 2)) *scr = fg; scr++; + case 2: if (row & (1u << 1)) *scr = fg; scr++; + case 1: if (row & (1u << 0)) *scr = fg; scr++; + } + + scr += stride; + } +} + +result_t bmfont_draw(bmfont_t *bmfont, + screen_t *scr, + const char *text, + int len, + colour_t fg, + colour_t bg, + const bmfont_spacing_t *spacing, + const point_t *pos, + point_t *end_pos) { bmfont_drawchar_t *drawfn; box_t scrclip; box_t drawbox; box_t drawclip; + point_t top; unsigned int bgalpha = colour_get_alpha(&bg); + /* pos is the baseline; glyph cells are drawn from their top, ascent above + * the baseline */ + top = POINT(pos->x, pos->y - bmfont->ascent); + switch (scr->format) { case pixelfmt_p1: @@ -1542,6 +1861,15 @@ result_t bmfont_draw(bmfont_t *bmfont, } break; + case pixelfmt_p8: + switch (bmfont->glyphrowbytes) + { + case 1: drawfn = (bgalpha < 255) ? bmfont_drawchar_p8_1w_t : bmfont_drawchar_p8_1w_o; break; + case 2: drawfn = (bgalpha < 255) ? bmfont_drawchar_p8_2w_t : bmfont_drawchar_p8_2w_o; break; + default: assert(0); return result_NOT_SUPPORTED; + } + break; + case pixelfmt_bgra8888: case pixelfmt_bgrx8888: switch (bmfont->glyphrowbytes) @@ -1559,10 +1887,17 @@ result_t bmfont_draw(bmfont_t *bmfont, if (screen_get_clip(scr, &scrclip)) return result_OK; /* invalid clipped screen */ - drawbox.x0 = pos->x; - drawbox.y0 = pos->y; - drawbox.x1 = pos->x + bmfont->charwidth * len; /* worst-case estimate */ - drawbox.y1 = pos->y + bmfont->charheight; + { + int extra_per_char; + + extra_per_char = spacing ? + MAX(spacing->letter_spacing + spacing->word_spacing, 0) : 0; + + drawbox.x0 = top.x; + drawbox.y0 = top.y; + drawbox.x1 = top.x + (bmfont->charwidth + extra_per_char) * len; /* worst-case estimate */ + drawbox.y1 = top.y + bmfont->charheight; + } if (!box_intersects(&scrclip, &drawbox)) return result_OK; /* not visible */ @@ -1575,8 +1910,8 @@ result_t bmfont_draw(bmfont_t *bmfont, int bottom_skip = drawclip.y1; /* ensure that any computed positions are inside the clip box */ - unsigned int clamped_pos_x = CLAMP(pos->x, scrclip.x0, scrclip.x1 - 1); - unsigned int clamped_pos_y = CLAMP(pos->y, scrclip.y0, scrclip.y1 - 1); + unsigned int clamped_pos_x = CLAMP(top.x, scrclip.x0, scrclip.x1 - 1); + unsigned int clamped_pos_y = CLAMP(top.y, scrclip.y0, scrclip.y1 - 1); int log2bpp = pixelfmt_log2bpp(scr->format); unsigned char *screen = (unsigned char *) scr->base + clamped_pos_y * scr->rowbytes + ((clamped_pos_x << log2bpp) >> 3); @@ -1592,8 +1927,6 @@ result_t bmfont_draw(bmfont_t *bmfont, pixelfmt_any_t nativebg = colour_to_pixel(scr->palette, scr->palette ? 1 << (1 << log2bpp) : 0, bg, scr->format); int remaining = scrclip.x1 - clamped_pos_x; /* in pixels */ - const int tracking = 0; /* note: +ve can break stuff! */ - int x = pos->x; /* box_intersects said something is visible, but a glyph can still be fully @@ -1624,7 +1957,7 @@ result_t bmfont_draw(bmfont_t *bmfont, if (gid >= bmfont->totalchars) continue; - advance = bmfont_advance_for(bmfont, gid) + tracking; + advance = bmfont_advance_for(bmfont, gid, c, spacing); x += advance; @@ -1635,44 +1968,89 @@ result_t bmfont_draw(bmfont_t *bmfont, continue; } - /* draw */ + /* draw: the glyph cell itself, then any leftover advance (letter + * spacing, or monospace padding) as a second, glyph-less segment of + * the same width the drawfn already knows how to paint as pure + * background (opaque) or leave untouched (transparent) -- a row of + * zero bits reads as "no ink" either way. */ { - glyph = (unsigned char *) bmfont->glyphs + gid * glyphbytes; + static const unsigned char zero_glyph[128] = { 0 }; - int clippedcharwidth = MIN(remaining, advance - left_skip); /* in pixels */ + int cellwidth; + int cellpad; + int clippedcellwidth; - if (clippedcharwidth <= 0) + glyph = (unsigned char *) bmfont->glyphs + gid * glyphbytes; + cellwidth = MIN(advance, charwidth); + cellpad = advance - cellwidth; + + clippedcellwidth = MIN(remaining, cellwidth - left_skip); /* in pixels */ + + if (clippedcellwidth > 0) + { + int right_skip = charwidth - left_skip - clippedcellwidth; + + drawfn(screen, glyph, + top_skip, right_skip, + shift, rowbytes, + clippedcellwidth, clippedcharheight, + nativefg, nativebg); + + if ((remaining -= clippedcellwidth) <= 0) + break; /* stop drawing */ + + { + int pixelsused_bits = clippedcellwidth << log2bpp; + int nbits = shift + pixelsused_bits; + screen = (unsigned char *) screen + (nbits >> 3); + shift = nbits & 7; + } + + left_skip = 0; + } + else { /* still entirely left-clipped: the visible clip is narrower than * this character's remaining left-clip amount. Skip it exactly * like the whole-character skip above, without touching * "remaining" or the screen pointer/shift, since nothing of it * was drawn. */ - left_skip -= advance; - continue; + left_skip -= cellwidth; } + if (cellpad > 0 && left_skip < cellpad) { - int right_skip = charwidth - left_skip - clippedcharwidth; + int clippedspacing = MIN(remaining, cellpad - left_skip); - drawfn(screen, glyph, - top_skip, right_skip, - shift, rowbytes, - clippedcharwidth, clippedcharheight, - nativefg, nativebg); - } - - if ((remaining -= clippedcharwidth) <= 0) - break; /* stop drawing */ + if (clippedspacing > 0) + { + assert((size_t) clippedcharheight * bmfont->glyphrowbytes <= + sizeof(zero_glyph)); + + drawfn(screen, zero_glyph, + top_skip, 0, + shift, rowbytes, + clippedspacing, clippedcharheight, + nativefg, nativebg); + + if ((remaining -= clippedspacing) <= 0) + break; /* stop drawing */ + + { + int pixelsused_bits = clippedspacing << log2bpp; + int nbits = shift + pixelsused_bits; + screen = (unsigned char *) screen + (nbits >> 3); + shift = nbits & 7; + } + } - /* advance the screen pointer */ - int pixelsused_bits = clippedcharwidth << log2bpp; - int nbits = shift + pixelsused_bits; - screen = (unsigned char *) screen + (nbits >> 3); - shift = nbits & 7; + left_skip = 0; + } + else if (cellpad > 0) + { + left_skip -= cellpad; + } } - - left_skip = 0; } if (end_pos) @@ -1693,7 +2071,7 @@ result_t bmfont_draw(bmfont_t *bmfont, if (gid >= bmfont->totalchars) continue; - advance = bmfont_advance_for(bmfont, gid) + tracking; + advance = bmfont_advance_for(bmfont, gid, c, spacing); x += advance; } @@ -1707,15 +2085,16 @@ result_t bmfont_draw(bmfont_t *bmfont, /* -------------------------------------------------------------------------- */ -result_t bmfont_draw_relief(bmfont_t *bmfont, - screen_t *scr, - const char *text, - int len, - colour_t fg, - colour_t shadow, - const point_t *pos, - const point_t *offset, - point_t *end_pos) +result_t bmfont_draw_relief(bmfont_t *bmfont, + screen_t *scr, + const char *text, + int len, + colour_t fg, + colour_t shadow, + const bmfont_spacing_t *spacing, + const point_t *pos, + const point_t *offset, + point_t *end_pos) { result_t rc; colour_t transparent; @@ -1724,12 +2103,13 @@ result_t bmfont_draw_relief(bmfont_t *bmfont, transparent = colour_rgba(0, 0, 0, 0); shadowpos = POINT(pos->x + offset->x, pos->y + offset->y); - rc = bmfont_draw(bmfont, scr, text, len, shadow, transparent, + rc = bmfont_draw(bmfont, scr, text, len, shadow, transparent, spacing, &shadowpos, NULL); if (rc) return rc; - return bmfont_draw(bmfont, scr, text, len, fg, transparent, pos, end_pos); + return bmfont_draw(bmfont, scr, text, len, fg, transparent, spacing, pos, + end_pos); } /* -------------------------------------------------------------------------- */ diff --git a/libraries/framebuf/bmfont/enumerate.c b/libraries/framebuf/bmfont/enumerate.c index 7fe9dfc..4343a71 100644 --- a/libraries/framebuf/bmfont/enumerate.c +++ b/libraries/framebuf/bmfont/enumerate.c @@ -2,16 +2,15 @@ #include #include -#include #include "base/result.h" #include "framebuf/bmfont.h" #include "io/dirscan.h" +#include "io/path.h" /* ----------------------------------------------------------------------- */ -#define BMFONT_EXT ".png" -#define BMFONT_EXT_LEN 4 +#define BMFONT_EXT ".png" typedef struct { @@ -24,22 +23,18 @@ bmfont_enumerate_ctx_t; static result_t bmfont_enumerate_entry(const char *leaf, void *opaque) { bmfont_enumerate_ctx_t *ctx; - size_t leaflen; - char name[256]; - char path[512]; + char name[256]; + char path[512]; - ctx = opaque; - leaflen = strlen(leaf); + ctx = opaque; - if (leaflen <= BMFONT_EXT_LEN || leaflen - BMFONT_EXT_LEN >= sizeof(name)) + if (!path_leaf_strip_ext(leaf, BMFONT_EXT, name, sizeof(name))) return result_OK; - if (strcmp(leaf + leaflen - BMFONT_EXT_LEN, BMFONT_EXT) != 0) - return result_OK; - - memcpy(name, leaf, leaflen - BMFONT_EXT_LEN); - name[leaflen - BMFONT_EXT_LEN] = '\0'; -#ifdef TARGET_RISCOS + /* Built locally rather than via path_join_filename: that returns a single + * static buffer, which one call per entry here would repeatedly clobber + * from under the caller's own "dir" pointer. */ +#ifdef __riscos snprintf(path, sizeof(path), "%s.%s", ctx->dir, leaf); #else snprintf(path, sizeof(path), "%s/%s", ctx->dir, leaf); diff --git a/libraries/framebuf/bmfont/test/bmfont-test.c b/libraries/framebuf/bmfont/test/bmfont-test.c index 11c7f25..f7ce7d5 100644 --- a/libraries/framebuf/bmfont/test/bmfont-test.c +++ b/libraries/framebuf/bmfont/test/bmfont-test.c @@ -187,14 +187,14 @@ bmtestline_t; static bmtestfont_t bmfonts[MAXFONTS] = { - { "Daydream", NULL }, - { "GliderRider", NULL }, + { "DPT-Daydream", NULL }, + { "ZX-GliderRider", NULL }, { "Tiny", NULL }, - { "Henry", NULL }, - { "CookeTall", NULL }, + { "DPT-Henry", NULL }, + { "DPT-CookeTall", NULL }, { "MS Sans Serif", NULL }, - { "Digits-Regular", NULL }, - { "Digits-Bold", NULL } + { "DPT-Digits-Regular", NULL }, + { "DPT-Digits-Bold", NULL } }; /* Fixture PNGs not in bmfonts[] above (which is the Latin-text set the @@ -295,12 +295,13 @@ static result_t bmfont_clipping_test(bmfontteststate_t *state) { for (transparent = 0; transparent < 2; transparent++) { - int fontwidth, fontheight; + int fontwidth, fontheight, fontascent; int i; bitmap_clear(&state->bm, state->palette[palette_PICO8_DARK_GREEN]); - bmfont_get_info(bmfonts[font].bmfont, &fontwidth, &fontheight); + bmfont_get_info(bmfonts[font].bmfont, &fontwidth, &fontheight, + &fontascent, NULL); for (i = 0; i < NELEMS(centres); i++) { @@ -312,6 +313,7 @@ static result_t bmfont_clipping_test(bmfontteststate_t *state) rc = bmfont_measure(bmfonts[font].bmfont, lorem_ipsum, nchars, + NULL, INT_MAX, NULL, &stringwidth); @@ -319,7 +321,7 @@ static result_t bmfont_clipping_test(bmfontteststate_t *state) return rc; pos.x = centres[i].x - stringwidth / 2; - pos.y = centres[i].y - fontheight / 2; + pos.y = centres[i].y - fontheight / 2 + fontascent; bg = transparent ? state->transparent : state->palette[palette_PICO8_GREEN]; @@ -333,6 +335,7 @@ static result_t bmfont_clipping_test(bmfontteststate_t *state) nchars, state->palette[palette_PICO8_WHITE], state->palette[palette_PICO8_BLACK], + NULL, &pos, &shadow_offset, NULL /*endpos*/); @@ -347,6 +350,7 @@ static result_t bmfont_clipping_test(bmfontteststate_t *state) nchars, state->palette[palette_PICO8_WHITE], bg, + NULL, &pos, NULL /*endpos*/); if (rc) @@ -377,7 +381,7 @@ static result_t bmfont_layout_test(bmfontteststate_t *state) for (transparent = 0; transparent < 2; transparent++) { bmfont_t *bmfont = bmfonts[font].bmfont; - int glyphwidth, glyphheight; + int glyphwidth, glyphheight, glyphascent; const char *string; size_t stringlen; point_t origin = {0,0}; @@ -385,7 +389,8 @@ static result_t bmfont_layout_test(bmfontteststate_t *state) bitmap_clear(&state->bm, state->palette[palette_PICO8_DARK_GREEN]); - bmfont_get_info(bmfont, &glyphwidth, &glyphheight); + bmfont_get_info(bmfont, &glyphwidth, &glyphheight, &glyphascent, NULL); + origin.y += glyphascent; for (;;) { @@ -404,6 +409,7 @@ static result_t bmfont_layout_test(bmfontteststate_t *state) (void) bmfont_measure(bmfont, string, (int) stringlen, + NULL, state->scr_width - origin.x, &absolute_break, /* if no break returns strlen */ &width); @@ -439,6 +445,7 @@ static result_t bmfont_layout_test(bmfontteststate_t *state) friendly_break, state->palette[palette_PICO8_WHITE], bg, + NULL, &origin, &endpos); if (rc) @@ -628,6 +635,7 @@ static result_t bmfont_interactive_test(bmfontteststate_t *state) message, (int) msglen, state->palette[palette_PICO8_BLACK], bg, + NULL, &origin, NULL /*endpos*/); @@ -639,6 +647,7 @@ static result_t bmfont_interactive_test(bmfontteststate_t *state) message, (int) msglen, fg, bg, + NULL, &origin, NULL /*endpos*/); @@ -652,6 +661,7 @@ static result_t bmfont_interactive_test(bmfontteststate_t *state) message, (int) msglen, fg, bg, + NULL, &origin, NULL /*endpos*/); @@ -916,7 +926,9 @@ result_t bmfont_test_one_format(const char *resources, bmfont_get_info(bmfonts[font].bmfont, &bmfonts[font].width, - &bmfonts[font].height); + &bmfonts[font].height, + NULL, + NULL); } /* ------------------------------------------------------------------------ */ @@ -966,11 +978,12 @@ static result_t bmfont_monospace_test(const char *resources) { static const char sample[] = "WiWiWi.1jm"; + result_t rc; const char *leafname; const char *filename; bmfont_t *bmfont = NULL; - result_t rc; bmfont_width_t onechar; + bmfont_width_t step; bmfont_width_t prev; int i; @@ -987,24 +1000,32 @@ static result_t bmfont_monospace_test(const char *resources) bmfont_set_flags(bmfont, bmfont_FLAG_MONOSPACE); onechar = 0; + step = 0; prev = 0; for (i = 1; i <= (int) strlen(sample); i++) { bmfont_width_t width = 0; + bmfont_width_t expected; - rc = bmfont_measure(bmfont, sample, i, INT_MAX, NULL, &width); + rc = bmfont_measure(bmfont, sample, i, NULL, INT_MAX, NULL, &width); if (rc) goto Failure; if (i == 1) + { onechar = width; + step = width + 1; /* per-glyph advance, including letter spacing + * trimmed from the measured string's end */ + } - if (width != onechar * i || width - prev != onechar) + expected = onechar + (i - 1) * step; + + if (width != expected || (i > 1 && width - prev != step)) { fprintf(stderr, "error: monospace width mismatch at len %d: got %d, " "expected %d\n", - i, width, onechar * i); + i, width, expected); goto Failure; } @@ -1015,6 +1036,154 @@ static result_t bmfont_monospace_test(const char *resources) return result_TEST_PASSED; +Failure: + bmfont_destroy(bmfont); + return result_TEST_FAILED; +} + +/* ----------------------------------------------------------------------- */ + +/* letter_spacing adds a fixed amount between every pair of glyphs (N-1 gaps + * for N glyphs); word_spacing adds on top of that for space glyphs only. + * Also exercises bmfont_draw with a non-NULL spacing -- this is the path + * whose worst-case clip-box estimate used to go stale when tracking was + * nonzero, so a mismatch here would show up as dropped or corrupted glyphs + * under ASan/UBSan rather than just a wrong measurement. */ +static result_t bmfont_spacing_test(const char *resources) +{ + static const char letters[] = "WiWiWiWiWi"; + static const char worded[] = "a a"; + + const bmfont_spacing_t letter_only = { 3, 0 }; + const bmfont_spacing_t word_only = { 0, 5 }; + + result_t rc; + const char *leafname; + const char *filename; + bmfont_t *bmfont = NULL; + bmfont_width_t plain_word_width; + bmfont_width_t spaced_word_width; + point_t pos; + point_t end_pos; + int i; + + leafname = path_join_leafname("MS Sans Serif", "png"); + filename = path_join_filename(resources, 3, "resources", "bmfonts", leafname); + + rc = bmfont_create(filename, &bmfont); + if (rc) + { + fprintf(stderr, "Error: Failed to load font %s\n", filename); + return result_TEST_FAILED; + } + + /* letter_spacing adds a fixed gap between glyphs, not after the last one + * (bmfont_measure trims the trailing gap, same as it already does for the + * font's own built-in letter spacing) -- so an N-character run has N-1 + * spaced gaps. "WiWiWiWiWi" mixes glyphs of different base advance widths, + * so the per-character step isn't itself constant in a proportional font + * (unlike bmfont_monospace_test) -- only the total added width is, so + * that's the invariant checked here. */ + for (i = 1; i <= (int) strlen(letters); i++) + { + bmfont_width_t unspaced = 0; + bmfont_width_t spaced = 0; + + rc = bmfont_measure(bmfont, letters, i, NULL, INT_MAX, NULL, &unspaced); + if (rc) + goto Failure; + rc = bmfont_measure(bmfont, letters, i, &letter_only, INT_MAX, NULL, + &spaced); + if (rc) + goto Failure; + + if (spaced - unspaced != letter_only.letter_spacing * (i - 1)) + { + fprintf(stderr, + "error: letter_spacing total mismatch at len %d: " + "spaced=%d unspaced=%d\n", + i, spaced, unspaced); + goto Failure; + } + } + + /* word_spacing: only the two space glyphs in "a a" should widen, by + * exactly word_spacing each -- the letters either side are untouched. */ + rc = bmfont_measure(bmfont, worded, (int) strlen(worded), NULL, INT_MAX, + NULL, &plain_word_width); + if (rc) + goto Failure; + rc = bmfont_measure(bmfont, worded, (int) strlen(worded), &word_only, + INT_MAX, NULL, &spaced_word_width); + if (rc) + goto Failure; + + if (spaced_word_width - plain_word_width != word_only.word_spacing) + { + fprintf(stderr, + "error: word_spacing mismatch: plain=%d spaced=%d " + "expected delta=%d\n", + plain_word_width, spaced_word_width, word_only.word_spacing); + goto Failure; + } + + /* bmfont_draw with spacing must not mis-clip: end_pos is the pen position + * (unlike bmfont_measure's ink width, it includes the trailing glyph's + * own advance -- the font's built-in LETTER_SPACING pixel, uninvolved + * here since word_only has zero letter_spacing and "a a" ends on 'a' not + * ' '), on a screen wide enough that clipping cannot silently truncate + * the string. */ + { + bitmap_t bm; + screen_t scr; + colour_t black = colour_rgb(0, 0, 0); + colour_t white = colour_rgb(0xFF, 0xFF, 0xFF); + bmfont_width_t expected_end_x; + int scr_width; + int rowbytes; + void *pixels; + + scr_width = spaced_word_width + 32; + rowbytes = (scr_width << pixelfmt_log2bpp(pixelfmt_bgrx8888)) / 8; + pixels = malloc(rowbytes * 32); + if (pixels == NULL) + { + rc = result_OOM; + goto Failure; + } + + bitmap_init(&bm, SIZE2D(scr_width, 32), pixelfmt_bgrx8888, rowbytes, + NULL, pixels); + bitmap_clear(&bm, black); + screen_for_bitmap(&scr, &bm); + + pos = POINT(4, 16); + rc = bmfont_draw(bmfont, &scr, worded, (int) strlen(worded), white, + black, &word_only, &pos, &end_pos); + free(pixels); + if (rc) + goto Failure; + + /* "a a" ends on 'a': measure trims one pixel of the font's built-in + * letter spacing (no custom letter_spacing or trailing word_spacing + * here -- see bmfont_monospace_test's "width + 1" for the same + * constant), so the untrimmed pen position is the measured width plus + * that one pixel back. */ + expected_end_x = pos.x + spaced_word_width + 1; + if (end_pos.x != expected_end_x || end_pos.y != pos.y) + { + fprintf(stderr, + "error: bmfont_draw end_pos mismatch with spacing: " + "got (%d,%d), expected (%d,%d)\n", + end_pos.x, end_pos.y, expected_end_x, pos.y); + goto Failure; + } + } + + bmfont_destroy(bmfont); + return result_TEST_PASSED; + + Failure: bmfont_destroy(bmfont); return result_TEST_FAILED; @@ -1049,6 +1218,10 @@ result_t bmfont_test(const char *resources) if (rc != result_TEST_PASSED) return rc; + rc = bmfont_spacing_test(resources); + if (rc != result_TEST_PASSED) + return rc; + for (i = 0; i < NELEMS(tab); i++) { rc = bmfont_test_one_format(resources, diff --git a/libraries/framebuf/pattern/pattern.c b/libraries/framebuf/pattern/pattern.c index 42634b9..6e3c5b2 100644 --- a/libraries/framebuf/pattern/pattern.c +++ b/libraries/framebuf/pattern/pattern.c @@ -137,3 +137,25 @@ pattern_t pattern_from_mask(const uint8_t mask[8], colour_t colour) return pat; } + +/* ----------------------------------------------------------------------- */ + +/* The recursively-built 8x8 ordered dither matrix, cell values 0..63. The + * BAYER* tiles above are this same matrix thresholded at each coverage + * level; here it is kept whole for per-pixel continuous dithering. */ +static const uint8_t bayer8x8[8][8] = +{ + { 0, 32, 8, 40, 2, 34, 10, 42 }, + { 48, 16, 56, 24, 50, 18, 58, 26 }, + { 12, 44, 4, 36, 14, 46, 6, 38 }, + { 60, 28, 52, 20, 62, 30, 54, 22 }, + { 3, 35, 11, 43, 1, 33, 9, 41 }, + { 51, 19, 59, 27, 49, 17, 57, 25 }, + { 15, 47, 7, 39, 13, 45, 5, 37 }, + { 63, 31, 55, 23, 61, 29, 53, 21 } +}; + +int pattern_bayer_threshold(int x, int y) +{ + return bayer8x8[y & 7][x & 7]; +} diff --git a/libraries/framebuf/pixelmap/get.c b/libraries/framebuf/pixelmap/get.c index 720a986..f04cf77 100644 --- a/libraries/framebuf/pixelmap/get.c +++ b/libraries/framebuf/pixelmap/get.c @@ -175,12 +175,12 @@ const pixelmap_t *pixelmap_get(pixelfmt_t srcfmt, int nentries) { static struct pixelmap__cacheent cache[PIXELMAP_CACHE_SIZE]; - static unsigned int clock; + static unsigned int clock; - unsigned int palhash; - int victim; - unsigned int oldest; - int i; + unsigned int palhash; + int victim; + unsigned int oldest; + int i; if (palette == NULL) return NULL; diff --git a/libraries/framebuf/pixelmap/impl.h b/libraries/framebuf/pixelmap/impl.h index 4ed94fb..9004876 100644 --- a/libraries/framebuf/pixelmap/impl.h +++ b/libraries/framebuf/pixelmap/impl.h @@ -11,12 +11,13 @@ /* ----------------------------------------------------------------------- */ /** - * Fill in the channel layout of `out` (rbits/gbits/bbits, rshift/gshift/bshift, - * destfmt, dest_log2bpp, nentries) for the given format pair, from the built-in - * policy table. + * Fill in the channel layout of `out` (rbits/gbits/bbits, + * rshift/gshift/bshift, destfmt, dest_log2bpp, nentries) for the given + * format pair, from the built-in policy table. * - * The source format must be a 32bpp `*8888` format; its channels are extracted - * with an 8-bit right shift (\ref pixelmap_t rshift/gshift/bshift). + * The source format must be a 32bpp `*8888` format; its channels are + * extracted with an 8-bit right shift (\ref pixelmap_t + * rshift/gshift/bshift). * * \param[in] srcfmt Source pixel format. * \param[in] destfmt Destination format. diff --git a/libraries/framebuf/screen/screen-copy-bitmap-rle.c b/libraries/framebuf/screen/screen-copy-bitmap-rle.c index 3f91735..41620e7 100644 --- a/libraries/framebuf/screen/screen-copy-bitmap-rle.c +++ b/libraries/framebuf/screen/screen-copy-bitmap-rle.c @@ -141,6 +141,95 @@ static result_t screen_copy_bitmap_rle_p4(screen_t *scr, return result_OK; } +/* As screen_copy_bitmap_rle_p4 but for an 8bpp paletted screen: the deep + * -> index table is a plain byte per entry and the screen store is a plain + * byte, so there is no sub-byte packing. */ +static result_t screen_copy_bitmap_rle_p8(screen_t *scr, + int x, + int y, + const bitmap_t *src, + const box_t *draw_box) +{ + pixelfmt_t base; + int log2bpp; + const uint8_t *blob; + const uint8_t *p; + const uint8_t *end; + uint8_t *dstbase; + pixelfmt_rgba8888_t *scratch; + const pixelmap_t *pm; + int firstrow; + int skip, plot; + int rows; + int i; + + base = pixelfmt_base(src->format); + log2bpp = pixelfmt_log2bpp(base); + + if (log2bpp != 5) + return result_NOT_SUPPORTED; /* v1: 32bpp source only */ + + blob = src->base; + p = blob + bitmap__RLE_HEADER_SIZE; + end = p + bitmap__rle_get32(blob + 4); + + firstrow = draw_box->y0 - y; + for (i = 0; i < firstrow; i++) + p = bitmap__rle_skip_row(p, end, log2bpp); + + skip = draw_box->x0 - x; + plot = draw_box->x1 - draw_box->x0; + rows = draw_box->y1 - draw_box->y0; + + pm = pixelmap_get(pixelfmt_rgba8888, scr->format, scr->palette, 256); + if (pm == NULL) + return result_NOT_SUPPORTED; + + scratch = malloc((size_t) plot * sizeof(*scratch)); + if (scratch == NULL) + return result_OOM; + + dstbase = scr->base; + for (i = 0; i < rows; i++) + { + uint8_t *rowp; + int xx; + + for (xx = 0; xx < plot; xx++) + scratch[xx] = 0; /* zero_skip below only fills covered skip runs */ + + p = bitmap__rle_decode_row(p, end, log2bpp, scratch, skip, plot, 1); + + rowp = dstbase + (size_t) (draw_box->y0 + i) * scr->rowbytes; + + for (xx = 0; xx < plot; xx++) + { + colour_t c; + unsigned int r, g, b, idx; + int dstx; + + c.primary = scratch[xx]; + if (colour_get_alpha(&c) == 0) + continue; + + dstx = draw_box->x0 + xx; + + r = (c.primary >> pm->rshift) & 0xFF; + g = (c.primary >> pm->gshift) & 0xFF; + b = (c.primary >> pm->bshift) & 0xFF; + idx = ((r >> (8 - pm->rbits)) << (pm->gbits + pm->bbits)) + | ((g >> (8 - pm->gbits)) << pm->bbits) + | ( b >> (8 - pm->bbits)); + + rowp[dstx] = pm->entries[idx]; + } + } + + free(scratch); + + return result_OK; +} + result_t screen_copy_bitmap_rle(screen_t *scr, int x, int y, @@ -165,6 +254,8 @@ result_t screen_copy_bitmap_rle(screen_t *scr, if (pixelfmt_log2bpp(scr->format) == 2) return screen_copy_bitmap_rle_p4(scr, x, y, src, draw_box); + if (pixelfmt_log2bpp(scr->format) == 3) + return screen_copy_bitmap_rle_p8(scr, x, y, src, draw_box); base = pixelfmt_base(src->format); diff --git a/libraries/framebuf/screen/screen-copy-rect.c b/libraries/framebuf/screen/screen-copy-rect.c index 36eed1a..11b0da1 100644 --- a/libraries/framebuf/screen/screen-copy-rect.c +++ b/libraries/framebuf/screen/screen-copy-rect.c @@ -4,25 +4,33 @@ #include "framebuf/screen.h" -/* 4bpp paletted screens pack two pixels per byte, so a moving window's - * source and destination nibbles don't line up on byte boundaries in - * general. Copied in place, pixel by pixel, masking each nibble out and - * rolling it into place: safe under self-overlap by walking rows in the - * direction "dy" dictates (as the byte path already does) and, since a - * nibble's own byte-mate is its immediate horizontal neighbour, walking - * columns within each row by the same rule applied to "dx" -- the standard - * two-axis blit-direction trick, so every pixel is read before anything - * that could overwrite it is written. */ -static result_t screen_copy_rect_p4(screen_t *scr, - const box_t *s, - const box_t *d, - int width, - int height, - int dx, - int dy) +/* Sub-byte paletted screens (1/2/4bpp) pack several pixels per byte, so a + * moving window's source and destination pixels don't line up on byte + * boundaries in general. Copied in place, pixel by pixel, masking each one + * out and rolling it into place: safe under self-overlap by walking rows in + * the direction "dy" dictates (as the byte path already does) and, since a + * pixel's byte-mates are its immediate horizontal neighbours, walking columns + * within each row by the same rule applied to "dx" -- the standard two-axis + * blit-direction trick, so every pixel is read before anything that could + * overwrite it is written. "bpp_bits" is 1, 2 or 4. + * + * The within-byte pixel order must match the rest of the screen code, which + * is not uniform: p1 and p2 are MSB-first (bit 7 / bits 7..6 are the leftmost + * pixel -- see screen_set_pixel_p1/p2 and screen_copy_bitmap_p1/p2) while p4 + * is LSB-first (see screen_set_pixel_p4). "msb_first" carries that. */ +static result_t screen_copy_rect_packed(screen_t *scr, + const box_t *s, + const box_t *d, + int width, + int height, + int dx, + int dy, + int bpp_bits) { unsigned char *base; int rowbytes; + int ppb, xshift, xmask, msb_first; + unsigned char pmask; int row_first, row_last, row_step; int col_first, col_last, col_step; int row, col; @@ -30,6 +38,12 @@ static result_t screen_copy_rect_p4(screen_t *scr, base = scr->base; rowbytes = scr->rowbytes; + ppb = 8 / bpp_bits; /* pixels per byte: 8, 4 or 2 */ + xshift = (bpp_bits == 1) ? 3 : (bpp_bits == 2) ? 2 : 1; + xmask = ppb - 1; + pmask = (unsigned char) ((1u << bpp_bits) - 1); + msb_first = (bpp_bits != 4); + if (dy > 0) { row_first = height - 1; row_last = -1; row_step = -1; } else { row_first = 0; row_last = height; row_step = 1; } @@ -42,20 +56,25 @@ static result_t screen_copy_rect_p4(screen_t *scr, { int sx, sy, dxp, dyp; const unsigned char *scrp_s; - unsigned char *scrp_d; - int shift_s, shift_d; - unsigned char nib; + unsigned char *scrp_d; + int shift_s, shift_d; + unsigned char pix; sx = s->x0 + col; sy = s->y0 + row; dxp = d->x0 + col; dyp = d->y0 + row; - scrp_s = base + (size_t) sy * rowbytes + (sx >> 1); - shift_s = (sx & 1) * 4; - nib = (unsigned char) ((*scrp_s >> shift_s) & 0xF); - - scrp_d = base + (size_t) dyp * rowbytes + (dxp >> 1); - shift_d = (dxp & 1) * 4; - *scrp_d = (unsigned char) ((*scrp_d & ~(0xF << shift_d)) | (nib << shift_d)); + scrp_s = base + (size_t) sy * rowbytes + (sx >> xshift); + shift_s = (sx & xmask) * bpp_bits; + if (msb_first) + shift_s = 8 - bpp_bits - shift_s; + pix = (unsigned char) ((*scrp_s >> shift_s) & pmask); + + scrp_d = base + (size_t) dyp * rowbytes + (dxp >> xshift); + shift_d = (dxp & xmask) * bpp_bits; + if (msb_first) + shift_d = 8 - bpp_bits - shift_d; + *scrp_d = (unsigned char) ((*scrp_d & ~(pmask << shift_d)) + | (pix << shift_d)); } } @@ -146,7 +165,12 @@ result_t screen_copy_rect(screen_t *scr, switch (pixelfmt_log2bpp(scr->format)) { - case 2: return screen_copy_rect_p4(scr, &s, &d_clipped, width, height, dx, dy); + case 0: return screen_copy_rect_packed(scr, &s, &d_clipped, + width, height, dx, dy, 1); + case 1: return screen_copy_rect_packed(scr, &s, &d_clipped, + width, height, dx, dy, 2); + case 2: return screen_copy_rect_packed(scr, &s, &d_clipped, + width, height, dx, dy, 4); case 3: bpp = 1; break; case 4: bpp = 2; break; diff --git a/libraries/framebuf/screen/screen-draw.c b/libraries/framebuf/screen/screen-draw.c index 877d010..8e885f8 100644 --- a/libraries/framebuf/screen/screen-draw.c +++ b/libraries/framebuf/screen/screen-draw.c @@ -9,6 +9,7 @@ #include "base/utils.h" #include "framebuf/colour.h" +#include "framebuf/pattern.h" #include "framebuf/pixelmap.h" #include "framebuf/span-registry.h" #include "geom/line.h" @@ -193,6 +194,23 @@ static void screen_blend_pixel_p4(screen_t *scr, *scrp = (unsigned char) ((*scrp & ~(0xF << shift)) | ((out & 0xF) << shift)); } +static void screen_blend_pixel_p8(screen_t *scr, + int x, + int y, + colour_t colour, + int alpha) +{ + unsigned char *scrp; + unsigned char idx, out; + + scrp = (unsigned char *) scr->base + y * scr->rowbytes + x; + idx = *scrp; + + scr->span->blendconst(&out, &idx, &colour, 1, alpha, scr->palette); + + *scrp = out; +} + static void screen_blend_pixel_32(screen_t *scr, int x, int y, @@ -229,6 +247,7 @@ static void screen_blend_pixel(screen_t *scr, case 0: screen_blend_pixel_p1(scr, x, y, colour, alpha); break; case 1: screen_blend_pixel_p2(scr, x, y, colour, alpha); break; case 2: screen_blend_pixel_p4(scr, x, y, colour, alpha); break; + case 3: screen_blend_pixel_p8(scr, x, y, colour, alpha); break; case 5: screen_blend_pixel_32(scr, x, y, colour, alpha); break; default: @@ -292,56 +311,181 @@ void screen_fill_square(screen_t *scr, /* ----------------------------------------------------------------------- */ +/* Per-blit ordered-dither bias table: one signed offset for each of the 64 + * cells of the 8x8 Bayer matrix, indexed by pattern_bayer_threshold(sx, sy). + * + * The mean gap between adjacent levels of a linear ramp of "nlevels" entries + * is 255 / (nlevels - 1); the Bayer cell (-32..31 about zero) nudges a channel + * value by up to half that gap either way, so a value sitting between two + * entries lands on one or the other in a fixed 8x8 pattern instead of always + * snapping to the nearer. Building the table hoists the one divide out of the + * inner loop -- the per-pixel cost drops to a lookup and an add. + * + * "nlevels" must be the per-channel quantisation level count (e.g. 1 << + * pm->rbits), not the palette's total entry count: pixelmap quantises R/G/B + * independently (4:4:4 for p1/p2/p4, 5:6:5 for p8) regardless of how many + * palette entries that yields, so a p8 screen has only 32-64 levels per + * channel despite 256 total entries. Keying the gap off the entry count + * instead makes the bias round to zero on p8 -- the dither vanishes. + * + * dither_bias_build fills "tab" and returns 1 when dithering is on and useful + * (>= 2 levels), 0 when the caller should skip the bias entirely. + * + * ponytail: this assumes a roughly even, roughly greyscale palette -- the one + * case (shallow paletted screen showing a gradient) the dithered blit is for. + * A wildly non-uniform palette dithers weakly, not wrongly; swap in a + * per-entry nearest-two search keyed on the real palette if that matters. */ +static int dither_bias_build(int tab[64], int nlevels, int dither) +{ + int gap; + int i; + + if (!dither || nlevels < 2) + return 0; + + gap = 255 / (nlevels - 1); + for (i = 0; i < 64; i++) + tab[i] = ((i - 32) * gap) / 64; + + return 1; +} + +/* Per-channel level count driving the dither bias for a deep->paletted + * pixelmap: the worst (fewest-level) of R/G/B, since one shared bias table + * nudges all three channels. */ +static int dither_levels_for(const pixelmap_t *pm) +{ + return 1 << MIN(pm->rbits, MIN(pm->gbits, pm->bbits)); +} + +/* Apply the pre-built bias for sprite-local pixel (sx, sy) to channel value + * "v" (0..255), result clamped to 0..255. Coordinates are relative to the + * source bitmap's top-left, not the screen, so the dither pattern stays fixed + * to the sprite as its window moves rather than crawling across it. */ +static unsigned int dither_channel(unsigned int v, + const int tab[64], + int sx, + int sy) +{ + int adj; + + adj = tab[pattern_bayer_threshold(sx, sy)]; + + return (unsigned int) CLAMP((int) v + adj, 0, 255); +} + +/* Read source pixel (srcx, srcy) -- relative to the source bitmap's own + * top-left, not the screen -- as an rgba8888 colour_t. "srcpm" is the + * paletted->rgba8888 pixelmap for src->format (from pixelmap_get), or NULL + * when src is already a deep rgba8888/bgra8888 bitmap; either way this is the + * one place that knows how to decode a packed p1/p2/p4/p8 index versus a + * plain 32bpp read, so every screen_copy_bitmap_p1/p2/p4/p8/32 helper below + * shares it instead of striding a 32bpp read through a narrower source or + * unpacking to a scratch buffer first. */ +static colour_t src_fetch_rgba(const bitmap_t *src, + const pixelmap_t *srcpm, + int srcx, + int srcy) +{ + colour_t c; + const unsigned char *row; + + row = (const unsigned char *) src->base + srcy * src->rowbytes; + + if (srcpm == NULL) + { + c.primary = ((const pixelfmt_rgba8888_t *) row)[srcx]; + } + else + { + int idx; + + switch (pixelfmt_log2bpp(src->format)) + { + case 0: idx = (row[srcx >> 3] >> (7 - (srcx & 7))) & 1; break; + case 1: idx = (row[srcx >> 2] >> (6 - ((srcx & 3) << 1))) & 3; break; + case 2: idx = (row[srcx >> 1] >> ((srcx & 1) * 4)) & 0xF; break; + default: idx = row[srcx]; break; + } + + c.primary = ((const pixelfmt_rgba8888_t *) srcpm->entries)[idx]; + } + + return c; +} + +/* pixelmap_get() for the paletted->rgba8888 direction used by + * src_fetch_rgba(), or NULL (no lookup needed) when src is already deep. */ +static const pixelmap_t *src_pixelmap_for(const bitmap_t *src) +{ + if (pixelfmt_log2bpp(src->format) == 5) + return NULL; + + return pixelmap_get(src->format, pixelfmt_rgba8888, src->palette, + pixelfmt_paletted_nentries(src->format)); +} + /* Blit "src" onto the paletted screen, its top-left at (x, y), clipped to * "draw_box". No linear channel bits to blend, so this does an alpha-tested * transfer (skip fully transparent, else nearest palette match) rather than - * true blending, matching screen_set_pixel's case 2. Returns - * result_NOT_SUPPORTED if there is no deep->paletted conversion table for the - * screen's format. */ + * true blending, matching screen_set_pixel's case 2. With "dither" set the + * source RGB is ordered-dithered per pixel before the nearest-match lookup, + * breaking up the banding a shallow palette otherwise shows on a gradient; the + * Bayer cell is keyed on the sprite-local pixel so the pattern travels with + * the sprite. Returns result_NOT_SUPPORTED if there is no deep->paletted (or + * paletted->deep, for a paletted source) conversion table. */ static result_t screen_copy_bitmap_p4(screen_t *scr, int x, int y, const bitmap_t *src, const box_t *draw_box, - int has_alpha) + int has_alpha, + int dither) { - const unsigned char *srcrow; - unsigned char *dstbase; - const pixelmap_t *pm; - int clipped_width, clipped_height; - int yy; + unsigned char *dstbase; + const pixelmap_t *pm; + const pixelmap_t *srcpm; + int bias[64]; + int do_dither; + int clipped_width, clipped_height; + int yy; clipped_width = draw_box->x1 - draw_box->x0; clipped_height = draw_box->y1 - draw_box->y0; - srcrow = (const unsigned char *) src->base + (draw_box->y0 - y) * src->rowbytes; dstbase = scr->base; - /* Source pixels here are RGBA8888 byte order (see screen_copy_bitmap). The - * cached RGB->index table turns the inner loop into a mask-and-lookup. */ + srcpm = src_pixelmap_for(src); + if (srcpm == NULL && pixelfmt_log2bpp(src->format) != 5) + return result_NOT_SUPPORTED; + + /* Cached RGB->index table turns the inner loop into a mask-and-lookup. */ pm = pixelmap_get(pixelfmt_rgba8888, scr->format, scr->palette, 16); if (pm == NULL) return result_NOT_SUPPORTED; + do_dither = dither_bias_build(bias, dither_levels_for(pm), dither); + for (yy = 0; yy < clipped_height; yy++) { - const pixelfmt_rgba8888_t *srcpx; - unsigned char *rowp; - int xx; + unsigned char *rowp; + int srcy; + int xx; - srcpx = (const pixelfmt_rgba8888_t *) srcrow + (draw_box->x0 - x); - rowp = dstbase + (draw_box->y0 + yy) * scr->rowbytes; + srcy = draw_box->y0 - y + yy; + rowp = dstbase + (draw_box->y0 + yy) * scr->rowbytes; for (xx = 0; xx < clipped_width; xx++) { colour_t c; unsigned int r, g, b, idx; - int dstx; + int dstx, srcx; unsigned char *scrp; int shift; pixelfmt_any_t pxl; - c.primary = srcpx[xx]; + srcx = draw_box->x0 + xx - x; + c = src_fetch_rgba(src, srcpm, srcx, srcy); if (has_alpha && colour_get_alpha(&c) == 0) continue; /* fully transparent: leave background alone */ @@ -352,6 +496,12 @@ static result_t screen_copy_bitmap_p4(screen_t *scr, r = (c.primary >> pm->rshift) & 0xFF; g = (c.primary >> pm->gshift) & 0xFF; b = (c.primary >> pm->bshift) & 0xFF; + if (do_dither) + { + r = dither_channel(r, bias, dstx - x, draw_box->y0 + yy - y); + g = dither_channel(g, bias, dstx - x, draw_box->y0 + yy - y); + b = dither_channel(b, bias, dstx - x, draw_box->y0 + yy - y); + } idx = ((r >> (8 - pm->rbits)) << (pm->gbits + pm->bbits)) | ((g >> (8 - pm->gbits)) << pm->bbits) | ( b >> (8 - pm->bbits)); @@ -359,8 +509,85 @@ static result_t screen_copy_bitmap_p4(screen_t *scr, *scrp = (unsigned char) ((*scrp & ~(0xF << shift)) | ((pxl & 0xF) << shift)); } + } + + return result_OK; +} + +/* As screen_copy_bitmap_p4 but for the 8bpp screen: alpha-tested transfer, + * optional ordered dither, nearest palette match. p8 is a plain byte per + * pixel, so both the deep->index table lookup and the screen store are + * straight byte accesses with no sub-byte packing. Returns + * result_NOT_SUPPORTED if there is no deep->paletted conversion table for + * the screen's format. */ +static result_t screen_copy_bitmap_p8(screen_t *scr, + int x, + int y, + const bitmap_t *src, + const box_t *draw_box, + int has_alpha, + int dither) +{ + unsigned char *dstbase; + const pixelmap_t *pm; + const pixelmap_t *srcpm; + int bias[64]; + int do_dither; + int clipped_width, clipped_height; + int yy; + + clipped_width = draw_box->x1 - draw_box->x0; + clipped_height = draw_box->y1 - draw_box->y0; + + dstbase = scr->base; + + srcpm = src_pixelmap_for(src); + if (srcpm == NULL && pixelfmt_log2bpp(src->format) != 5) + return result_NOT_SUPPORTED; + + pm = pixelmap_get(pixelfmt_rgba8888, scr->format, scr->palette, 256); + if (pm == NULL) + return result_NOT_SUPPORTED; + + do_dither = dither_bias_build(bias, dither_levels_for(pm), dither); + + for (yy = 0; yy < clipped_height; yy++) + { + unsigned char *rowp; + int srcy; + int xx; + + srcy = draw_box->y0 - y + yy; + rowp = dstbase + (draw_box->y0 + yy) * scr->rowbytes; + + for (xx = 0; xx < clipped_width; xx++) + { + colour_t c; + unsigned int r, g, b, idx; + int dstx, srcx; + + srcx = draw_box->x0 + xx - x; + c = src_fetch_rgba(src, srcpm, srcx, srcy); + if (has_alpha && colour_get_alpha(&c) == 0) + continue; /* fully transparent: leave background alone */ + + dstx = draw_box->x0 + xx; + + r = (c.primary >> pm->rshift) & 0xFF; + g = (c.primary >> pm->gshift) & 0xFF; + b = (c.primary >> pm->bshift) & 0xFF; + if (do_dither) + { + r = dither_channel(r, bias, dstx - x, draw_box->y0 + yy - y); + g = dither_channel(g, bias, dstx - x, draw_box->y0 + yy - y); + b = dither_channel(b, bias, dstx - x, draw_box->y0 + yy - y); + } + idx = ((r >> (8 - pm->rbits)) << (pm->gbits + pm->bbits)) + | ((g >> (8 - pm->gbits)) << pm->bbits) + | ( b >> (8 - pm->bbits)); - srcrow += src->rowbytes; + rowp[dstx] = pm->entries[idx]; + } } return result_OK; @@ -369,51 +596,61 @@ static result_t screen_copy_bitmap_p4(screen_t *scr, /* Blit "src" onto the 1bpp screen, its top-left at (x, y), clipped to * "draw_box". Like screen_copy_bitmap_p4 this is an alpha-tested transfer * (skip fully transparent, else nearest of the two palette entries) rather - * than a blend -- a 1bpp screen has no channel bits to blend. The cached - * RGB->index table turns the inner loop into a mask-and-lookup; bit 7 of a - * byte is the leftmost pixel. Returns result_NOT_SUPPORTED if there is no - * deep->paletted conversion table for the screen's format. */ + * than a blend -- a 1bpp screen has no channel bits to blend. "dither" + * ordered-dithers the source RGB per pixel first. The cached RGB->index table + * turns the inner loop into a mask-and-lookup; bit 7 of a byte is the + * leftmost pixel. Returns result_NOT_SUPPORTED if there is no deep->paletted + * conversion table for the screen's format. */ static result_t screen_copy_bitmap_p1(screen_t *scr, int x, int y, const bitmap_t *src, const box_t *draw_box, - int has_alpha) + int has_alpha, + int dither) { - const unsigned char *srcrow; - unsigned char *dstbase; - const pixelmap_t *pm; - int clipped_width, clipped_height; - int yy; + unsigned char *dstbase; + const pixelmap_t *pm; + const pixelmap_t *srcpm; + int bias[64]; + int do_dither; + int clipped_width, clipped_height; + int yy; clipped_width = draw_box->x1 - draw_box->x0; clipped_height = draw_box->y1 - draw_box->y0; - srcrow = (const unsigned char *) src->base + (draw_box->y0 - y) * src->rowbytes; dstbase = scr->base; + srcpm = src_pixelmap_for(src); + if (srcpm == NULL && pixelfmt_log2bpp(src->format) != 5) + return result_NOT_SUPPORTED; + pm = pixelmap_get(pixelfmt_rgba8888, scr->format, scr->palette, 2); if (pm == NULL) return result_NOT_SUPPORTED; + do_dither = dither_bias_build(bias, dither_levels_for(pm), dither); + for (yy = 0; yy < clipped_height; yy++) { - const pixelfmt_rgba8888_t *srcpx; - unsigned char *rowp; - int xx; + unsigned char *rowp; + int srcy; + int xx; - srcpx = (const pixelfmt_rgba8888_t *) srcrow + (draw_box->x0 - x); - rowp = dstbase + (draw_box->y0 + yy) * scr->rowbytes; + srcy = draw_box->y0 - y + yy; + rowp = dstbase + (draw_box->y0 + yy) * scr->rowbytes; for (xx = 0; xx < clipped_width; xx++) { colour_t c; unsigned int r, g, b, idx; - int dstx, shift; + int dstx, shift, srcx; unsigned char *scrp; pixelfmt_any_t pxl; - c.primary = srcpx[xx]; + srcx = draw_box->x0 + xx - x; + c = src_fetch_rgba(src, srcpm, srcx, srcy); if (has_alpha && colour_get_alpha(&c) == 0) continue; /* fully transparent: leave background alone */ @@ -424,6 +661,12 @@ static result_t screen_copy_bitmap_p1(screen_t *scr, r = (c.primary >> pm->rshift) & 0xFF; g = (c.primary >> pm->gshift) & 0xFF; b = (c.primary >> pm->bshift) & 0xFF; + if (do_dither) + { + r = dither_channel(r, bias, dstx - x, draw_box->y0 + yy - y); + g = dither_channel(g, bias, dstx - x, draw_box->y0 + yy - y); + b = dither_channel(b, bias, dstx - x, draw_box->y0 + yy - y); + } idx = ((r >> (8 - pm->rbits)) << (pm->gbits + pm->bbits)) | ((g >> (8 - pm->gbits)) << pm->bbits) | ( b >> (8 - pm->bbits)); @@ -431,8 +674,6 @@ static result_t screen_copy_bitmap_p1(screen_t *scr, *scrp = (unsigned char) ((*scrp & ~(1 << shift)) | ((pxl & 1) << shift)); } - - srcrow += src->rowbytes; } return result_OK; @@ -442,49 +683,59 @@ static result_t screen_copy_bitmap_p1(screen_t *scr, * "draw_box". Alpha-tested transfer to the nearest of the four palette * entries, exactly as screen_copy_bitmap_p1 but two bits per pixel: bits * 7..6 of a byte are the leftmost pixel and the cached RGB->index table is - * packed four entries to the byte. Returns result_NOT_SUPPORTED if there is - * no deep->paletted conversion table for the screen's format. */ + * packed four entries to the byte. "dither" ordered-dithers the source RGB + * per pixel first. Returns result_NOT_SUPPORTED if there is no deep->paletted + * conversion table for the screen's format. */ static result_t screen_copy_bitmap_p2(screen_t *scr, int x, int y, const bitmap_t *src, const box_t *draw_box, - int has_alpha) + int has_alpha, + int dither) { - const unsigned char *srcrow; - unsigned char *dstbase; - const pixelmap_t *pm; - int clipped_width, clipped_height; - int yy; + unsigned char *dstbase; + const pixelmap_t *pm; + const pixelmap_t *srcpm; + int bias[64]; + int do_dither; + int clipped_width, clipped_height; + int yy; clipped_width = draw_box->x1 - draw_box->x0; clipped_height = draw_box->y1 - draw_box->y0; - srcrow = (const unsigned char *) src->base + (draw_box->y0 - y) * src->rowbytes; dstbase = scr->base; + srcpm = src_pixelmap_for(src); + if (srcpm == NULL && pixelfmt_log2bpp(src->format) != 5) + return result_NOT_SUPPORTED; + pm = pixelmap_get(pixelfmt_rgba8888, scr->format, scr->palette, 4); if (pm == NULL) return result_NOT_SUPPORTED; + do_dither = dither_bias_build(bias, dither_levels_for(pm), dither); + for (yy = 0; yy < clipped_height; yy++) { - const pixelfmt_rgba8888_t *srcpx; - unsigned char *rowp; - int xx; + unsigned char *rowp; + int srcy; + int xx; - srcpx = (const pixelfmt_rgba8888_t *) srcrow + (draw_box->x0 - x); - rowp = dstbase + (draw_box->y0 + yy) * scr->rowbytes; + srcy = draw_box->y0 - y + yy; + rowp = dstbase + (draw_box->y0 + yy) * scr->rowbytes; for (xx = 0; xx < clipped_width; xx++) { colour_t c; unsigned int r, g, b, idx; - int dstx, shift; + int dstx, shift, srcx; unsigned char *scrp; pixelfmt_any_t pxl; - c.primary = srcpx[xx]; + srcx = draw_box->x0 + xx - x; + c = src_fetch_rgba(src, srcpm, srcx, srcy); if (has_alpha && colour_get_alpha(&c) == 0) continue; /* fully transparent: leave background alone */ @@ -495,6 +746,12 @@ static result_t screen_copy_bitmap_p2(screen_t *scr, r = (c.primary >> pm->rshift) & 0xFF; g = (c.primary >> pm->gshift) & 0xFF; b = (c.primary >> pm->bshift) & 0xFF; + if (do_dither) + { + r = dither_channel(r, bias, dstx - x, draw_box->y0 + yy - y); + g = dither_channel(g, bias, dstx - x, draw_box->y0 + yy - y); + b = dither_channel(b, bias, dstx - x, draw_box->y0 + yy - y); + } idx = ((r >> (8 - pm->rbits)) << (pm->gbits + pm->bbits)) | ((g >> (8 - pm->gbits)) << pm->bbits) | ( b >> (8 - pm->bbits)); @@ -502,45 +759,52 @@ static result_t screen_copy_bitmap_p2(screen_t *scr, *scrp = (unsigned char) ((*scrp & ~(3 << shift)) | ((pxl & 3) << shift)); } - - srcrow += src->rowbytes; } return result_OK; } /* Blit "src" onto the 32bpp screen, its top-left at (x, y), clipped to - * "draw_box", alpha-blending row spans through the span registry. */ -static void screen_copy_bitmap_32(screen_t *scr, - int x, - int y, - const bitmap_t *src, - const box_t *draw_box, - int has_alpha) + * "draw_box", alpha-blending row spans through the span registry. A paletted + * "src" is decoded through "srcpm" (see src_fetch_rgba) a pixel at a time + * into the same colbuf/alphabuf chunk a deep source fills, so the span blend + * call itself doesn't care which kind of source it was. Returns + * result_NOT_SUPPORTED if "src" is paletted and has no paletted->deep + * conversion table. */ +static result_t screen_copy_bitmap_32(screen_t *scr, + int x, + int y, + const bitmap_t *src, + const box_t *draw_box, + int has_alpha) { - pixelfmt_any32_t colbuf[BITMAP_BLIT_CHUNK]; - unsigned char alphabuf[BITMAP_BLIT_CHUNK]; - const unsigned char *srcrow; - pixelfmt_any32_t *dstrow; - int clipped_width, clipped_height; - int yy; + pixelfmt_any32_t colbuf[BITMAP_BLIT_CHUNK]; + unsigned char alphabuf[BITMAP_BLIT_CHUNK]; + const pixelmap_t *srcpm; + pixelfmt_any32_t *dstrow; + int clipped_width, clipped_height; + int yy; clipped_width = draw_box->x1 - draw_box->x0; clipped_height = draw_box->y1 - draw_box->y0; - srcrow = (const unsigned char *) src->base + (draw_box->y0 - y) * src->rowbytes; + srcpm = src_pixelmap_for(src); + if (srcpm == NULL && pixelfmt_log2bpp(src->format) != 5) + return result_NOT_SUPPORTED; + dstrow = scr->base; dstrow += draw_box->y0 * scr->rowbytes / (int) sizeof(*dstrow) + draw_box->x0; for (yy = 0; yy < clipped_height; yy++) { - const pixelfmt_rgba8888_t *srcpx; - pixelfmt_any32_t *dstpx; - int remaining; + pixelfmt_any32_t *dstpx; + int srcy; + int remaining, done; - srcpx = (const pixelfmt_rgba8888_t *) srcrow + (draw_box->x0 - x); + srcy = draw_box->y0 - y + yy; dstpx = dstrow; remaining = clipped_width; + done = 0; while (remaining > 0) { @@ -552,29 +816,42 @@ static void screen_copy_bitmap_32(screen_t *scr, { colour_t c; - c.primary = srcpx[i]; + c = src_fetch_rgba(src, srcpm, draw_box->x0 + done + i - x, srcy); colbuf[i] = colour_to_pixel(scr->palette, 0, c, scr->format); alphabuf[i] = has_alpha ? colour_get_alpha(&c) : PIXELFMT_OPAQUE; } scr->span->blendarray(dstpx, dstpx, colbuf, chunk, alphabuf); - srcpx += chunk; dstpx += chunk; + done += chunk; remaining -= chunk; } - srcrow += src->rowbytes; dstrow += scr->rowbytes / (int) sizeof(*dstrow); } + + return result_OK; } -result_t screen_copy_bitmap(screen_t *scr, int x, int y, const bitmap_t *src) +/* Shared body for screen_copy_bitmap and screen_copy_bitmap_dithered. With + * "dither" set the paletted-screen (p1/p2/p4/p8) paths ordered-dither the + * source RGB before the nearest-match lookup; the 32bpp path and the RLE path + * ignore it (a 32bpp screen has the channel depth not to band, and an RLE + * source is pre-quantised UI art). A paletted source (p1/p2/p4/p8, e.g. a PNG + * saved with a PLTE chunk) is decoded through its own palette a pixel at a + * time by src_fetch_rgba rather than unpacked to a scratch buffer up front. */ +static result_t screen_copy_bitmap_i(screen_t *scr, + int x, + int y, + const bitmap_t *src, + int dither) { - box_t clip_box; - box_t src_box; - box_t draw_box; - int has_alpha; + box_t clip_box; + box_t src_box; + box_t draw_box; + int has_alpha; + result_t rc; if (screen_get_clip(scr, &clip_box)) return result_OK; /* invalid clipped screen: nothing to draw */ @@ -590,23 +867,43 @@ result_t screen_copy_bitmap(screen_t *scr, int x, int y, const bitmap_t *src) 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. */ - has_alpha = (src->format == pixelfmt_rgba8888 || src->format == pixelfmt_bgra8888); + * (see bitmap_load_png()), the same layout colour_t::primary uses, so a + * deep source pixel is read directly into a colour_t with no conversion. A + * paletted source's tRNS-derived alpha survives into its palette's + * rgba8888 entries (see bitmap_load_png's plte[i] = colour_rgba(..., a)), + * so it always carries real per-pixel alpha and must be alpha-tested same + * as an rgba8888/bgra8888 source would be. */ + has_alpha = pixelfmt_log2bpp(src->format) != 5 || + pixelfmt_has_alpha(src->format); switch (pixelfmt_log2bpp(scr->format)) { - case 0: return screen_copy_bitmap_p1(scr, x, y, src, &draw_box, has_alpha); - case 1: return screen_copy_bitmap_p2(scr, x, y, src, &draw_box, has_alpha); - case 2: return screen_copy_bitmap_p4(scr, x, y, src, &draw_box, has_alpha); - case 5: screen_copy_bitmap_32(scr, x, y, src, &draw_box, has_alpha); break; + case 0: rc = screen_copy_bitmap_p1(scr, x, y, src, &draw_box, has_alpha, dither); break; + case 1: rc = screen_copy_bitmap_p2(scr, x, y, src, &draw_box, has_alpha, dither); break; + case 2: rc = screen_copy_bitmap_p4(scr, x, y, src, &draw_box, has_alpha, dither); break; + case 3: rc = screen_copy_bitmap_p8(scr, x, y, src, &draw_box, has_alpha, dither); break; + case 5: rc = screen_copy_bitmap_32(scr, x, y, src, &draw_box, has_alpha); break; default: assert(!"Unimplemented pixel format"); - return result_NOT_SUPPORTED; + rc = result_NOT_SUPPORTED; + break; } - return result_OK; + return rc; +} + +result_t screen_copy_bitmap(screen_t *scr, int x, int y, const bitmap_t *src) +{ + return screen_copy_bitmap_i(scr, x, y, src, 0); +} + +result_t screen_copy_bitmap_dithered(screen_t *scr, + int x, + int y, + const bitmap_t *src) +{ + return screen_copy_bitmap_i(scr, x, y, src, 1); } /* ----------------------------------------------------------------------- */ diff --git a/libraries/framebuf/screen/screen-fill-pattern.c b/libraries/framebuf/screen/screen-fill-pattern.c index 16a4e69..0cd1528 100644 --- a/libraries/framebuf/screen/screen-fill-pattern.c +++ b/libraries/framebuf/screen/screen-fill-pattern.c @@ -134,6 +134,36 @@ static void screen_fill_pattern_p4(screen_t *scr, } } +static void screen_fill_pattern_p8(screen_t *scr, + const pattern_t *pattern, + const box_t *draw_box, + int stencil, + pattern_runs_t runs) +{ + unsigned char *rowp; + int row, col, x, y; + + rowp = (unsigned char *) scr->base + draw_box->y0 * scr->rowbytes; + for (y = draw_box->y0; y < draw_box->y1; y++) + { + const pixelfmt_any_t *run; + uint8_t bits; + + row = (y - pattern->origin.y) & 7; + run = runs[row]; + bits = pattern->bits[row]; + col = 0; + for (x = draw_box->x0; x < draw_box->x1; x++) + { + if (!stencil || + (bits & (0x80u >> ((x - pattern->origin.x) & 7)))) + rowp[x] = (unsigned char) (run[col] & 0xFF); + col = (col + 1) & 7; + } + rowp += scr->rowbytes; + } +} + static void screen_fill_pattern_32(screen_t *scr, const pattern_t *pattern, const box_t *draw_box, @@ -240,6 +270,10 @@ void screen_fill_pattern(screen_t *scr, screen_fill_pattern_p4(scr, pattern, &draw_box, stencil, runs); break; + case 3: + screen_fill_pattern_p8(scr, pattern, &draw_box, stencil, runs); + break; + case 5: screen_fill_pattern_32(scr, pattern, &draw_box, stencil, fg_fmt, runs); break; diff --git a/libraries/framebuf/screen/test/screen-test.c b/libraries/framebuf/screen/test/screen-test.c index 8bacb42..c0afd94 100644 --- a/libraries/framebuf/screen/test/screen-test.c +++ b/libraries/framebuf/screen/test/screen-test.c @@ -958,6 +958,478 @@ static result_t test_copy_bitmap_p2(void) return result_TEST_PASSED; } +/* Blit an rgba8888 source onto an 8bpp paletted screen: each pixel quantises + * to the nearest palette entry and is stored as one plain byte. Also covers + * set_pixel / fill_hline / fill_pattern / blend_pixel on the p8 path. */ +static result_t test_copy_bitmap_p8(void) +{ +#define P8_ROWBYTES (WIDTH) + static unsigned char p8pixels[P8_ROWBYTES * HEIGHT]; + static pixelfmt_rgba8888_t srcbuf[16 * 4]; + + screen_t scr; + bitmap_t src; + colour_t pal[256]; + int x, y; + + /* a grey ramp: entry i is (i, i, i), so nearest-match is predictable */ + for (x = 0; x < 256; x++) + pal[x] = colour_rgb(x, x, x); + + /* 16x4 source: left half near-black (-> index ~16), right half near-white + * (-> index ~240) */ + for (y = 0; y < 4; y++) + for (x = 0; x < 16; x++) + srcbuf[y * 16 + x] = (x < 8 ? colour_rgb(0x10, 0x10, 0x10) + : colour_rgb(0xF0, 0xF0, 0xF0)).primary; + + bitmap_init(&src, SIZE2D(16, 4), pixelfmt_rgba8888, + 16 * (int) sizeof(srcbuf[0]), NULL, srcbuf); + + memset(p8pixels, 0, sizeof(p8pixels)); + screen_init(&scr, SIZE2D(WIDTH, HEIGHT), pixelfmt_p8, P8_ROWBYTES, pal, + p8pixels); + + if (screen_copy_bitmap(&scr, 0, 0, &src) != result_OK) + { + printf("screen: copy_bitmap to p8 screen failed\n"); + return result_TEST_FAILED; + } + + /* row 0: cols 0..7 quantise near 0x10, cols 8..15 near 0xF0. The pixelmap + * quantises via 5:6:5 before the nearest-ramp lookup, so allow a small + * slop rather than demanding the exact byte. */ + for (x = 0; x < 8; x++) + { + if (p8pixels[x] > 0x20) + { + printf("screen: p8 blit dark col x=%d too bright (0x%02X)\n", + x, p8pixels[x]); + return result_TEST_FAILED; + } + if (p8pixels[x + 8] < 0xE0) + { + printf("screen: p8 blit light col x=%d too dark (0x%02X)\n", + x + 8, p8pixels[x + 8]); + return result_TEST_FAILED; + } + } + + /* an untouched row past the source stays clear */ + for (x = 0; x < 16; x++) + if (p8pixels[4 * P8_ROWBYTES + x] != 0x00) + { + printf("screen: p8 blit spilled past the source\n"); + return result_TEST_FAILED; + } + + /* screen_set_pixel: entry 200 at x=5 lands as a plain byte */ + memset(p8pixels, 0, sizeof(p8pixels)); + screen_set_pixel(&scr, 5, 0, pal[200]); + if (p8pixels[5] != 200) + { + printf("screen: p8 set_pixel wrong (0x%02X)\n", p8pixels[5]); + return result_TEST_FAILED; + } + + /* screen_fill_hline: 6 pixels of entry 128 from x=2 on row 1 */ + memset(p8pixels, 0, sizeof(p8pixels)); + screen_fill_hline(&scr, 2, 1, 6, pal[128]); + for (x = 2; x < 8; x++) + if (p8pixels[P8_ROWBYTES + x] != 128) + { + printf("screen: p8 fill_hline wrong at x=%d (0x%02X)\n", + x, p8pixels[P8_ROWBYTES + x]); + return result_TEST_FAILED; + } + if (p8pixels[P8_ROWBYTES + 1] != 0 || p8pixels[P8_ROWBYTES + 8] != 0) + { + printf("screen: p8 fill_hline smeared past its run\n"); + return result_TEST_FAILED; + } + + /* screen_fill_pattern: solid all-bits pattern in fg = entry 77 */ + memset(p8pixels, 0, sizeof(p8pixels)); + { + pattern_t pat; + box_t b; + + memset(&pat, 0, sizeof(pat)); + memset(pat.bits, 0xFF, sizeof(pat.bits)); + pat.fg = pal[77]; + pat.bg = pal[0]; + b.x0 = 0; b.y0 = 2; b.x1 = 8; b.y1 = 3; + screen_fill_pattern(&scr, &b, &pat); + for (x = 0; x < 8; x++) + if (p8pixels[2 * P8_ROWBYTES + x] != 77) + { + printf("screen: p8 fill_pattern wrong at x=%d (0x%02X)\n", + x, p8pixels[2 * P8_ROWBYTES + x]); + return result_TEST_FAILED; + } + } + + /* screen_copy_rect on p8 (the whole-byte bytes path, bpp=1): lay down a + * distinct-per-column run, slide it right by 5, check it moved intact and + * did not smear past its destination */ + memset(p8pixels, 0, sizeof(p8pixels)); + for (x = 0; x < 8; x++) + screen_set_pixel(&scr, x, 0, pal[1 + x]); + { + box_t s = { 0, 0, 8, 1 }; + box_t got; + + if (screen_copy_rect(&scr, &s, POINT(5, 0), &got) != result_OK) + { + printf("screen: copy_rect declined a p8 screen\n"); + return result_TEST_FAILED; + } + for (x = 0; x < 8; x++) + if (p8pixels[x + 5] != 1 + x) + { + printf("screen: copy_rect p8 wrong pixel at x=%d (got %d want %d)\n", + x + 5, p8pixels[x + 5], 1 + x); + return result_TEST_FAILED; + } + if (p8pixels[13] != 0) + { + printf("screen: copy_rect p8 smeared past its destination\n"); + return result_TEST_FAILED; + } + } + + /* screen_blend_pixel via an anti-aliased diagonal: the p8 blend path must + * write *some* mid-ramp index between the black background and white line, + * not leave the pixel at 0 and not slam it to 255 */ + memset(p8pixels, 0, sizeof(p8pixels)); + screen_draw_line_wu_float(&scr, 0.0f, 0.0f, 8.0f, 3.0f, + colour_rgb(0xFF, 0xFF, 0xFF)); + { + int seen_partial = 0; + + for (y = 0; y < 4; y++) + for (x = 0; x < 9; x++) + { + unsigned char v = p8pixels[y * P8_ROWBYTES + x]; + if (v > 0 && v < 255) + seen_partial = 1; + } + if (!seen_partial) + { + printf("screen: p8 blend_pixel produced no partial-coverage pixel\n"); + return result_TEST_FAILED; + } + } + + return result_TEST_PASSED; +#undef P8_ROWBYTES +} + +/* screen_copy_bitmap with a *paletted* source (p4) onto a 32bpp screen: the + * unpack path in screen_copy_bitmap_i must decode the packed source indices + * through its own palette before the 32bpp blit runs, landing exact colours + * -- unlike the deep-source tests above there is no quantisation step to + * allow slop for. */ +static result_t test_copy_bitmap_paletted_source(void) +{ +#define P4_SRC_ROWBYTES (8 / 2) + static unsigned char p4srcbuf[P4_SRC_ROWBYTES * 2]; + static pixelfmt_any32_t scrbuf[WIDTH * HEIGHT]; + + screen_t scr; + bitmap_t src; + colour_t srcpal[16]; + colour_t got; + int x; + + /* row 0: indices 0..7 packed two to the byte -- even x in the low nibble, + * odd x in the high nibble (matches screen_set_pixel_p4's own bit order) */ + for (x = 0; x < 8; x++) + p4srcbuf[x >> 1] |= (unsigned char) (x << ((x & 1) ? 4 : 0)); + memset(p4srcbuf + P4_SRC_ROWBYTES, 0, P4_SRC_ROWBYTES); /* row 1: all index 0 */ + + for (x = 0; x < 16; x++) + srcpal[x] = colour_rgb((unsigned char) (x * 16), + (unsigned char) (x * 8), + (unsigned char) (255 - x * 16)); + + bitmap_init(&src, SIZE2D(8, 2), pixelfmt_p4, P4_SRC_ROWBYTES, srcpal, + p4srcbuf); + + memset(scrbuf, 0, sizeof(scrbuf)); + screen_init(&scr, SIZE2D(WIDTH, HEIGHT), pixelfmt_rgbx8888, + WIDTH * (int) sizeof(scrbuf[0]), NULL, scrbuf); + + if (screen_copy_bitmap(&scr, 2, 3, &src) != result_OK) + { + printf("screen: copy_bitmap of a p4 source onto a 32bpp screen failed\n"); + return result_TEST_FAILED; + } + + for (x = 0; x < 8; x++) + { + got.primary = scrbuf[3 * WIDTH + 2 + x]; + if (got.primary != srcpal[x].primary) + { + printf("screen: paletted-source blit wrong at x=%d " + "(got 0x%08X want 0x%08X)\n", + x, got.primary, srcpal[x].primary); + return result_TEST_FAILED; + } + } + + /* a pixel just outside the blitted box on every side stays clear, i.e. the + * unpack scratch buffer's extent matches the clipped draw box exactly */ + if (scrbuf[3 * WIDTH + 1] != 0 || scrbuf[3 * WIDTH + 10] != 0 || + scrbuf[2 * WIDTH + 2] != 0 || scrbuf[5 * WIDTH + 2] != 0) + { + printf("screen: paletted-source blit spilled past its box\n"); + return result_TEST_FAILED; + } + + /* a fully-transparent palette entry (as a PNG's tRNS chunk can produce) + * must alpha-test out, same as a transparent rgba8888/bgra8888 source + * pixel would: the unpack step must carry the tRNS-derived alpha through + * rather than baking every unpacked pixel opaque */ + { + pixelfmt_any32_t sentinel = 0xDEADBEEFu; + + srcpal[0] = colour_rgba(0xFF, 0x00, 0x00, 0x00); /* red, fully transparent */ + bitmap_set_palette(&src, srcpal); /* src.palette is its own copy */ + scrbuf[3 * WIDTH + 2] = sentinel; + + if (screen_copy_bitmap(&scr, 2, 3, &src) != result_OK) + { + printf("screen: copy_bitmap of a p4 source (2nd pass) failed\n"); + return result_TEST_FAILED; + } + + if (scrbuf[3 * WIDTH + 2] != sentinel) + { + printf("screen: paletted-source blit drew a fully-transparent index " + "(0x%08X, background was 0x%08X)\n", + scrbuf[3 * WIDTH + 2], sentinel); + return result_TEST_FAILED; + } + } + + return result_TEST_PASSED; +#undef P4_SRC_ROWBYTES +} + +/* Unpack the p2 pixel at column "px" of a row's byte buffer (bits 7..6 are + * column 0, MSB-first, four to the byte). */ +static int p2_pixel_at(const unsigned char *rowbuf, int px) +{ + return (rowbuf[px >> 2] >> (6 - ((px & 3) << 1))) & 3; +} + +/* screen_copy_bitmap_dithered on a p2 screen: a flat mid-grey source, which + * screen_copy_bitmap would quantise to one uniform index across the row, must + * come out as a stipple of at least two indices; the flat black and white + * ends must still be uniform (the Bayer nudge can't push them off their + * clamp). */ +static result_t test_copy_bitmap_dithered(void) +{ +#define PD_ROWBYTES (WIDTH / 4) + static unsigned char pdpixels[PD_ROWBYTES * HEIGHT]; + static pixelfmt_rgba8888_t srcbuf[16 * 8]; + + screen_t scr; + bitmap_t src; + colour_t pal[4]; + int first; + int varied; + int px; + int x, y; + + pal[0] = colour_rgb(0x00, 0x00, 0x00); + pal[1] = colour_rgb(0x55, 0x55, 0x55); + pal[2] = colour_rgb(0xAA, 0xAA, 0xAA); + pal[3] = colour_rgb(0xFF, 0xFF, 0xFF); + + /* 16x8 flat mid-grey (0x80): sits between pal[1] and pal[2]. */ + for (y = 0; y < 8; y++) + for (x = 0; x < 16; x++) + srcbuf[y * 16 + x] = colour_rgb(0x80, 0x80, 0x80).primary; + + bitmap_init(&src, SIZE2D(16, 8), pixelfmt_rgba8888, + 16 * (int) sizeof(srcbuf[0]), NULL, srcbuf); + + memset(pdpixels, 0, sizeof(pdpixels)); + screen_init(&scr, SIZE2D(WIDTH, HEIGHT), pixelfmt_p2, PD_ROWBYTES, pal, + pdpixels); + + if (screen_copy_bitmap_dithered(&scr, 0, 0, &src) != result_OK) + { + printf("screen: copy_bitmap_dithered to p2 screen failed\n"); + return result_TEST_FAILED; + } + + /* plain blit of the same source: every one of the first 16 pixels + * quantises to the same index. */ + { + unsigned char plain[PD_ROWBYTES * HEIGHT]; + screen_t pscr; + + memset(plain, 0, sizeof(plain)); + screen_init(&pscr, SIZE2D(WIDTH, HEIGHT), pixelfmt_p2, PD_ROWBYTES, pal, + plain); + screen_copy_bitmap(&pscr, 0, 0, &src); + for (px = 1; px < 16; px++) + if (p2_pixel_at(plain, px) != p2_pixel_at(plain, 0)) + { + printf("screen: plain p2 blit of a flat source was not uniform\n"); + return result_TEST_FAILED; + } + } + + /* dithered: at least one of the first 16 pixels differs from pixel 0 (the + * 8x8 Bayer cell varies across the row). */ + first = p2_pixel_at(pdpixels, 0); + varied = 0; + for (px = 1; px < 16; px++) + if (p2_pixel_at(pdpixels, px) != first) + varied = 1; + if (!varied) + { + printf("screen: dithered p2 blit of a flat mid-grey did not stipple\n"); + return result_TEST_FAILED; + } + + /* flat black stays index 0 across the row, flat white stays index 3. */ + for (y = 0; y < 8; y++) + for (x = 0; x < 16; x++) + srcbuf[y * 16 + x] = colour_rgb(0x00, 0x00, 0x00).primary; + memset(pdpixels, 0xAA, sizeof(pdpixels)); + screen_copy_bitmap_dithered(&scr, 0, 0, &src); + if (pdpixels[0] != 0x00 || pdpixels[1] != 0x00 || + pdpixels[2] != 0x00 || pdpixels[3] != 0x00) + { + printf("screen: dithered p2 blit pushed flat black off index 0\n"); + return result_TEST_FAILED; + } + + for (y = 0; y < 8; y++) + for (x = 0; x < 16; x++) + srcbuf[y * 16 + x] = colour_rgb(0xFF, 0xFF, 0xFF).primary; + memset(pdpixels, 0x00, sizeof(pdpixels)); + screen_copy_bitmap_dithered(&scr, 0, 0, &src); + if (pdpixels[0] != 0xFF || pdpixels[1] != 0xFF || + pdpixels[2] != 0xFF || pdpixels[3] != 0xFF) + { + printf("screen: dithered p2 blit pushed flat white off index 3\n"); + return result_TEST_FAILED; + } + + return result_TEST_PASSED; +} + +/* ----------------------------------------------------------------------- */ + +/* Read the "bpp_bits"-deep pixel at column "px" of a byte buffer using the + * house within-byte order: p1/p2 MSB-first (bit 7 leftmost), p4 LSB-first -- + * matching screen_set_pixel_p1/p2/p4. The source block is laid down with + * screen_set_pixel, so a wrong-endian copy shows up here as a shuffled row. */ +static int house_pixel_at(const unsigned char *buf, int px, int bpp_bits) +{ + int ppb = 8 / bpp_bits; + int mask = (1 << bpp_bits) - 1; + int shift = (px % ppb) * bpp_bits; + + if (bpp_bits != 4) + shift = 8 - bpp_bits - shift; /* MSB-first for p1/p2 */ + + return (buf[px / ppb] >> shift) & mask; +} + +/* screen_copy_rect on the sub-byte paletted formats (p1/p2/p4): lay down an + * 8x8 block with screen_set_pixel, copy it right by an odd (non-byte-aligned) + * offset, and check the pixels landed at the destination in the right order, + * did not smear past it, and left the row below the source clear. Regression: + * these formats used to fall through to result_NOT_SUPPORTED (full repaint on + * every low-bpp window move); the first packed implementation then copied + * p1/p2 with the wrong within-byte order and shuffled the row. */ +static result_t test_copy_rect_packed(void) +{ + static const struct { pixelfmt_t fmt; int bits; } cases[] = + { + { pixelfmt_p1, 1 }, + { pixelfmt_p2, 2 }, + { pixelfmt_p4, 4 }, + }; + + static unsigned char buf[WIDTH / 2 * HEIGHT]; /* widest sub-byte case */ + + colour_t pal[16]; + size_t ci; + int i; + + for (i = 0; i < 16; i++) + pal[i] = colour_rgb((unsigned char) (i * 17), + (unsigned char) (i * 17), + (unsigned char) (i * 17)); + + for (ci = 0; ci < NELEMS(cases); ci++) + { + screen_t scr; + box_t src; + box_t got; + int rowbytes, fill, x, y; + + rowbytes = WIDTH * cases[ci].bits / 8; + fill = (1 << cases[ci].bits) - 1; /* max index for this depth */ + + memset(buf, 0, sizeof(buf)); + screen_init(&scr, SIZE2D(WIDTH, HEIGHT), cases[ci].fmt, rowbytes, pal, buf); + + /* an 8-wide, 8-tall block at the top-left, laid down the house way. Each + * column carries a distinct non-zero index (1 + x % fill; adjacent + * columns -- including byte-mates -- differ) so a within-byte reordering + * can't hide behind a uniform fill. p1 has only index 1 available, so its + * shuffle is caught by the no-smear check below instead. */ + for (y = 0; y < 8; y++) + for (x = 0; x < 8; x++) + screen_set_pixel(&scr, x, y, pal[1 + x % fill]); + + src = (box_t) { 0, 0, 8, 8 }; + if (screen_copy_rect(&scr, &src, POINT(5, 0), &got) != result_OK) + { + printf("screen: copy_rect declined a p%d screen\n", cases[ci].bits); + return result_TEST_FAILED; + } + + /* moved: column c of the source now sits at c+5, same index */ + for (x = 0; x < 8; x++) + if (house_pixel_at(buf, x + 5, cases[ci].bits) != 1 + x % fill) + { + printf("screen: copy_rect p%d wrong pixel at x=%d " + "(got %d want %d)\n", cases[ci].bits, x + 5, + house_pixel_at(buf, x + 5, cases[ci].bits), 1 + x % fill); + return result_TEST_FAILED; + } + + /* read-before-write: column 13, just past the destination, is untouched */ + if (house_pixel_at(buf, 13, cases[ci].bits) != 0) + { + printf("screen: copy_rect p%d smeared past its destination\n", + cases[ci].bits); + return result_TEST_FAILED; + } + + /* row 8, the first row below the 8-tall source, stayed clear */ + for (x = 0; x < 16; x++) + if (house_pixel_at(buf + (size_t) 8 * rowbytes, x, cases[ci].bits) != 0) + { + printf("screen: copy_rect p%d touched a row below the source\n", + cases[ci].bits); + return result_TEST_FAILED; + } + } + + return result_TEST_PASSED; +} + /* ----------------------------------------------------------------------- */ result_t screen_test(const char *resources) @@ -977,7 +1449,11 @@ result_t screen_test(const char *resources) test_draw_circle, test_fill_circle, test_copy_bitmap_p1, - test_copy_bitmap_p2 + test_copy_bitmap_p2, + test_copy_bitmap_p8, + test_copy_bitmap_paletted_source, + test_copy_bitmap_dithered, + test_copy_rect_packed }; result_t rc; diff --git a/libraries/framebuf/span-registry/regdata.h b/libraries/framebuf/span-registry/regdata.h index 546e6ae..7d91d96 100644 --- a/libraries/framebuf/span-registry/regdata.h +++ b/libraries/framebuf/span-registry/regdata.h @@ -9,6 +9,7 @@ #include "framebuf/span-p1.h" #include "framebuf/span-p2.h" #include "framebuf/span-p4.h" +#include "framebuf/span-p8.h" #include "framebuf/span-rgbx8888.h" #include "framebuf/span-xbgr8888.h" @@ -20,6 +21,7 @@ static const span_t *spans[] = &span_p1, &span_p2, &span_p4, + &span_p8, &span_rgbx8888, &span_xbgr8888, }; diff --git a/libraries/framebuf/span/p1.c b/libraries/framebuf/span/p1.c index b1e32d5..9e529a9 100644 --- a/libraries/framebuf/span/p1.c +++ b/libraries/framebuf/span/p1.c @@ -36,11 +36,11 @@ static void span_p1_blendconst(void *vdst, while (length--) { - colour_t old_colour, new_colour, blended; + colour_t old_colour, new_colour, blended; pixelfmt_rgba8888_t oldpx, newpx; - int old_r, old_g, old_b; - int new_r, new_g, new_b; - int blend_r, blend_g, blend_b; + int old_r, old_g, old_b; + int new_r, new_g, new_b; + int blend_r, blend_g, blend_b; old_colour = palette[*psrc1 & 1]; oldpx = old_colour.primary; diff --git a/libraries/framebuf/span/p4.c b/libraries/framebuf/span/p4.c index e612bce..c0848b4 100644 --- a/libraries/framebuf/span/p4.c +++ b/libraries/framebuf/span/p4.c @@ -36,11 +36,11 @@ static void span_p4_blendconst(void *vdst, while (length--) { - colour_t old_colour, new_colour, blended; + colour_t old_colour, new_colour, blended; pixelfmt_rgba8888_t oldpx, newpx; - int old_r, old_g, old_b; - int new_r, new_g, new_b; - int blend_r, blend_g, blend_b; + int old_r, old_g, old_b; + int new_r, new_g, new_b; + int blend_r, blend_g, blend_b; old_colour = palette[*psrc1]; oldpx = old_colour.primary; diff --git a/libraries/framebuf/span/p8.c b/libraries/framebuf/span/p8.c new file mode 100644 index 0000000..ee14913 --- /dev/null +++ b/libraries/framebuf/span/p8.c @@ -0,0 +1,97 @@ +/* framebuf/span/p8.c -- P8 (8bpp paletted) format plot methods */ + +#include +#include + +#include "framebuf/colour.h" +#include "framebuf/pixelfmt.h" + +#include "framebuf/span.h" + +#include "framebuf/span-p8.h" + +#define SPAN_P8_NENTRIES 256 + +/* whole-byte format: one palette index per byte, so a run of pixels is a + * plain contiguous byte range and copy is a straight memcpy. */ +static void span_p8_copy(void *vdst, const void *vsrc, int length) +{ + memcpy(vdst, vsrc, (size_t) length); +} + +/* like span_p4_blendconst, but p8 is a plain byte store so there is no + * nibble packing: "src1" is one index per byte, "src2" an array of colour_t + * blended in full RGB precision before re-quantising to the nearest palette + * entry, "context" the palette. */ +static void span_p8_blendconst(void *vdst, + const void *vsrc1, + const void *vsrc2, + int length, + int alpha, + const void *context) +{ + unsigned char *pdst; + const unsigned char *psrc1; + const colour_t *psrc2; + const colour_t *palette; + + pdst = vdst; + psrc1 = vsrc1; + psrc2 = vsrc2; + palette = context; + + while (length--) + { + colour_t old_colour, new_colour, blended; + pixelfmt_rgba8888_t oldpx, newpx; + int old_r, old_g, old_b; + int new_r, new_g, new_b; + int blend_r, blend_g, blend_b; + + old_colour = palette[*psrc1]; + oldpx = old_colour.primary; + old_r = PIXELFMT_Rxxx8888(oldpx); + old_g = PIXELFMT_xGxx8888(oldpx); + old_b = PIXELFMT_xxBx8888(oldpx); + + new_colour = *psrc2; + newpx = new_colour.primary; + new_r = PIXELFMT_Rxxx8888(newpx); + new_g = PIXELFMT_xGxx8888(newpx); + new_b = PIXELFMT_xxBx8888(newpx); + + blend_r = (new_r * alpha + old_r * (255 - alpha)) / 255; + blend_g = (new_g * alpha + old_g * (255 - alpha)) / 255; + blend_b = (new_b * alpha + old_b * (255 - alpha)) / 255; + + blended = colour_rgb(blend_r, blend_g, blend_b); + *pdst = (unsigned char) colour_to_pixel(palette, SPAN_P8_NENTRIES, + blended, pixelfmt_p8); + + pdst++; + psrc1++; + psrc2++; + } +} + +/* "dst" is the row base, "first" the pixel index into it; whole-byte, so + * this is just a byte store. "pixel" is a palette index in its low byte. */ +static void span_p8_fill(void *vdst, + int first, + pixelfmt_any_t pixel, + int length) +{ + unsigned char *base; + + base = vdst; + memset(base + first, (unsigned char) (pixel & 0xFF), (size_t) length); +} + +const span_t span_p8 = +{ + pixelfmt_p8, + span_p8_copy, + span_p8_fill, + span_p8_blendconst, + NULL, /* blendarray: unneeded so far */ +}; diff --git a/libraries/geom/box/grow.c b/libraries/geom/box/grow.c index e07aee9..d86963f 100644 --- a/libraries/geom/box/grow.c +++ b/libraries/geom/box/grow.c @@ -17,3 +17,15 @@ void box_grow(box_t *box, int change) box->x1 += change; box->y1 += change; } + +box_t box_grown(const box_t *b, int change) +{ + box_t result; + + result.x0 = b->x0 - change; + result.y0 = b->y0 - change; + result.x1 = b->x1 + change; + result.y1 = b->y1 + change; + + return result; +} diff --git a/libraries/geom/packer/test/packer-test.c b/libraries/geom/packer/test/packer-test.c index ac0015b..fae6733 100644 --- a/libraries/geom/packer/test/packer-test.c +++ b/libraries/geom/packer/test/packer-test.c @@ -405,10 +405,10 @@ static int test3(void) { static const box_t pagedims = { 0, 0, 100, 100 }; + result_t err; packer_t *packer; const box_t *a, *b, *c; box_t freed; - result_t err; printf("test3: packer_release\n"); @@ -542,12 +542,12 @@ static int test4(void) static const int w[6] = { 40, 55, 40, 70, 40, 50 }; static const int h[6] = { 30, 40, 30, 35, 30, 45 }; + result_t err; packer_t *packer; const box_t *slot; box_t held[6]; box_t full; struct scan s; - result_t err; int cycle; int i; diff --git a/libraries/geom/stack/impl.h b/libraries/geom/stack/impl.h new file mode 100644 index 0000000..22a7f4f --- /dev/null +++ b/libraries/geom/stack/impl.h @@ -0,0 +1,18 @@ +/* geom/stack/impl.h -- box-stack layout solver */ + +#ifndef IMPL_H +#define IMPL_H + +#include "geom/stack.h" + +/** + * Affirms if `items[0..n)` form a valid parents-before-children forest + * rooted at index 0. + * + * \param[in] items Array of stack items. + * \param[in] n Number of items in the array. + * \return Non-zero if the tree is valid. + */ +int stack__valid_tree(const stack_item_t *items, int n); + +#endif /* IMPL_H */ diff --git a/libraries/geom/stack/measure-min.c b/libraries/geom/stack/measure-min.c new file mode 100644 index 0000000..27f6045 --- /dev/null +++ b/libraries/geom/stack/measure-min.c @@ -0,0 +1,90 @@ +/* geom/stack/measure-min.c -- minimum-size measurement for a box-stack tree */ + +#include "base/utils.h" +#include "geom/box.h" +#include "geom/size.h" + +#include "geom/stack.h" + +#include "impl.h" + +/* ----------------------------------------------------------------------- */ + +/* A leaf's own minimum extent needs no recursion; a container's is derived + * from its children, recursing depth-first so each child's minimum is known + * before its parent sums/maxes over it. */ +static void stack__measure(const stack_item_t *items, + int n, + int index, + size2d_t *mins) +{ + int horiz; + int main_min, cross_min; + int nchildren; + int i; + + horiz = (items[index].kind == stack_KIND_HBOX); + + main_min = 0; + cross_min = 0; + nchildren = 0; + + for (i = 0; i < n; i++) + { + int child_main, child_cross; + + if (items[i].parent != index) + continue; + + if (items[i].kind == stack_KIND_HBOX || items[i].kind == stack_KIND_VBOX) + { + stack__measure(items, n, i, mins); + child_main = horiz ? mins[i].w : mins[i].h; + child_cross = horiz ? mins[i].h : mins[i].w; + } + else if (items[i].kind == stack_KIND_SPACER) + { + child_main = 0; + child_cross = 0; + } + else /* stack_KIND_LEAF */ + { + child_main = items[i].axis_size ? items[i].axis_size : items[i].min; + child_cross = items[i].cross_size; + } + + main_min += child_main + (nchildren > 0 ? items[index].gap : 0); + cross_min = MAX(cross_min, child_cross); + nchildren++; + } + + main_min = MAX(main_min, items[index].axis_size ? items[index].axis_size : items[index].min); + + if (horiz) + { + mins[index].w = main_min + items[index].pad_l + items[index].pad_r; + mins[index].h = cross_min + items[index].pad_t + items[index].pad_b; + } + else + { + mins[index].w = cross_min + items[index].pad_l + items[index].pad_r; + mins[index].h = main_min + items[index].pad_t + items[index].pad_b; + } +} + +result_t stack_smallest(const stack_item_t *items, + int n, + size2d_t *out) +{ + size2d_t mins[STACK_MAX_ITEMS]; + + if (n > STACK_MAX_ITEMS) + return result_STACK_BAD_TREE; + if (!stack__valid_tree(items, n)) + return result_STACK_BAD_TREE; + + stack__measure(items, n, 0, mins); + *out = mins[0]; + + return result_OK; +} diff --git a/libraries/geom/stack/solve.c b/libraries/geom/stack/solve.c new file mode 100644 index 0000000..bd51b7d --- /dev/null +++ b/libraries/geom/stack/solve.c @@ -0,0 +1,244 @@ +/* geom/stack/solve.c -- box-stack layout solver */ + +#include + +#include "base/utils.h" +#include "geom/box.h" + +#include "geom/stack.h" + +#include "impl.h" + +/* ----------------------------------------------------------------------- */ + +/* A child's resolved main-axis extent, and the room left to grow it. */ +typedef struct stack__child +{ + int index; + int extent; + int clamped; /* non-zero once this child can no longer grow */ +} +stack__child_t; + +/* ----------------------------------------------------------------------- */ + +/* Distributes `avail` px of main-axis space across the children of + * `parent`, honouring `size`/`min`/`flex`/`max`, and returns the number of + * children found. `children[i].extent` holds each child's resolved + * main-axis extent on return. */ +static int stack__distribute(const stack_item_t *items, + int n, + int parent, + int avail, + stack__child_t *children) +{ + int nchildren; + int used; + int slack; + int i; + int round; + + nchildren = 0; + used = 0; + + for (i = 0; i < n; i++) + { + if (items[i].parent != parent) + continue; + + children[nchildren].index = i; + children[nchildren].extent = items[i].axis_size ? items[i].axis_size : items[i].min; + children[nchildren].clamped = (items[i].flex == 0); + + used += children[nchildren].extent; + nchildren++; + } + + slack = avail - used; + + /* Hand slack to flexible children in proportion to their weights, then + * the leftover px from integer division one per child, earliest first, + * so extents sum to exactly 'avail'. Clamping a child to its 'max' + * returns its excess to 'slack' for another such pass over the children + * still able to grow; bounded by 'nchildren' passes, as each pass + * clamps at least one more child or terminates. */ + for (round = 0; round < nchildren && slack > 0; round++) + { + int flexweight; + int given; + int c; + + flexweight = 0; + for (c = 0; c < nchildren; c++) + if (!children[c].clamped) + flexweight += items[children[c].index].flex; + + if (flexweight == 0) + break; + + given = 0; + for (c = 0; c < nchildren; c++) + { + int share; + + if (children[c].clamped) + continue; + + share = slack * items[children[c].index].flex / flexweight; + children[c].extent += share; + given += share; + } + + for (c = 0; c < nchildren && given < slack; c++) + { + if (children[c].clamped) + continue; + children[c].extent++; + given++; + } + + for (c = 0; c < nchildren; c++) + { + int item_max; + + if (children[c].clamped) + continue; + + item_max = items[children[c].index].max; + if (item_max > 0 && children[c].extent > item_max) + { + given -= children[c].extent - item_max; + children[c].extent = item_max; + children[c].clamped = 1; + } + } + + slack -= given; + } + + return nchildren; +} + +/* ----------------------------------------------------------------------- */ + +static void stack__place_container(const stack_item_t *items, + int n, + int index, + box_t *out) +{ + int horiz; + stack__child_t children[STACK_MAX_ITEMS]; /* upper bound: at most n-1 + * children, n capped by the + * caller (stack_solve) */ + int nchildren; + const box_t *box; + int inner_main0, inner_main1; + int cross0, cross1; + int avail; + int pos; + int c; + + horiz = (items[index].kind == stack_KIND_HBOX); + box = &out[index]; + + if (horiz) + { + inner_main0 = box->x0 + items[index].pad_l; + inner_main1 = box->x1 - items[index].pad_r; + cross0 = box->y0 + items[index].pad_t; + cross1 = box->y1 - items[index].pad_b; + } + else + { + inner_main0 = box->y0 + items[index].pad_t; + inner_main1 = box->y1 - items[index].pad_b; + cross0 = box->x0 + items[index].pad_l; + cross1 = box->x1 - items[index].pad_r; + } + + nchildren = 0; + for (c = 0; c < n; c++) + if (items[c].parent == index) + nchildren++; + + avail = inner_main1 - inner_main0 - items[index].gap * MAX(0, nchildren - 1); + + nchildren = stack__distribute(items, n, index, avail, children); + + pos = inner_main0; + + for (c = 0; c < nchildren; c++) + { + int ci; + box_t *cbox; + int cross_extent; + int cross_pos; + + ci = children[c].index; + cbox = &out[ci]; + + if (c > 0) + pos += items[index].gap; + + switch (items[ci].align) + { + case stack_ALIGN_FILL: + cross_extent = cross1 - cross0; + cross_pos = cross0; + break; + case stack_ALIGN_CENTRE: + cross_extent = items[ci].cross_size ? items[ci].cross_size : (cross1 - cross0); + cross_pos = cross0 + (cross1 - cross0 - cross_extent) / 2; + break; + case stack_ALIGN_END: + cross_extent = items[ci].cross_size ? items[ci].cross_size : (cross1 - cross0); + cross_pos = cross1 - cross_extent; + break; + case stack_ALIGN_START: + default: + cross_extent = items[ci].cross_size ? items[ci].cross_size : (cross1 - cross0); + cross_pos = cross0; + break; + } + + if (horiz) + { + cbox->x0 = pos; + cbox->x1 = pos + children[c].extent; + cbox->y0 = cross_pos; + cbox->y1 = cross_pos + cross_extent; + } + else + { + cbox->y0 = pos; + cbox->y1 = pos + children[c].extent; + cbox->x0 = cross_pos; + cbox->x1 = cross_pos + cross_extent; + } + + pos += children[c].extent; + + if (items[ci].kind == stack_KIND_HBOX || items[ci].kind == stack_KIND_VBOX) + stack__place_container(items, n, ci, out); + } +} + +/* ----------------------------------------------------------------------- */ + +result_t stack_solve(const stack_item_t *items, + int n, + const box_t *root, + box_t *out) +{ + if (n > STACK_MAX_ITEMS) + return result_STACK_BAD_TREE; + if (!stack__valid_tree(items, n)) + return result_STACK_BAD_TREE; + + out[0] = *root; + + if (items[0].kind == stack_KIND_HBOX || items[0].kind == stack_KIND_VBOX) + stack__place_container(items, n, 0, out); + + return result_OK; +} diff --git a/libraries/geom/stack/test/stack-test.c b/libraries/geom/stack/test/stack-test.c new file mode 100644 index 0000000..199aa74 --- /dev/null +++ b/libraries/geom/stack/test/stack-test.c @@ -0,0 +1,566 @@ +/* geom/stack/test/stack-test.c */ + +#include "base/utils.h" +#include "geom/box.h" + +#include "geom/stack.h" + +#include "test/all-tests.h" + +/* ----------------------------------------------------------------------- */ + +/* 0 34 67 100 + * +---------+-----+---------+ + * | A f=1 | B f1| C f=1 | + * +---------+-----+---------+ + * + * Three flex-1 children in a 100px box divide 34/33/33, abutting exactly. */ +static result_t test_flex_split(void) +{ + enum { ROOT, A, B, C, N }; + static const box_t root = { 0, 0, 100, 10 }; + static const stack_item_t items[N] = + { + [ROOT] = { .kind = stack_KIND_HBOX, .parent = -1 }, + [A] = { .kind = stack_KIND_LEAF, .parent = ROOT, .flex = 1 }, + [B] = { .kind = stack_KIND_LEAF, .parent = ROOT, .flex = 1 }, + [C] = { .kind = stack_KIND_LEAF, .parent = ROOT, .flex = 1 }, + }; + + result_t err; + box_t out[N]; + + err = stack_solve(items, N, &root, out); + if (err != result_OK) + return result_TEST_FAILED; + + if (out[A].x0 != 0 || out[A].x1 != 34) + return result_TEST_FAILED; + if (out[B].x0 != 34 || out[B].x1 != 67) + return result_TEST_FAILED; + if (out[C].x0 != 67 || out[C].x1 != 100) + return result_TEST_FAILED; + + return result_TEST_PASSED; +} + +/* 0 40 200 + * +----------+----------------------+ + * | LBL1(40) | FLD1 flex=1 | ROW1 (h=20) + * +----------+----------------------+ + * | LBL2(40) | FLD2 flex=1 | ROW2 (h=20) + * +----------+----------------------+ + * + * Two independent rows with label leaves fed the same fixed width line up + * their field boxes at a common x0. */ +static result_t test_aligned_labels(void) +{ + enum { ROOT, ROW1, LBL1, FLD1, ROW2, LBL2, FLD2, N }; + static const box_t root = { 0, 0, 200, 40 }; + static const stack_item_t items[N] = + { + [ROOT] = { .kind = stack_KIND_VBOX, .parent = -1 }, + + [ROW1] = { .kind = stack_KIND_HBOX, .parent = ROOT, .axis_size = 20 }, + [LBL1] = { .kind = stack_KIND_LEAF, .parent = ROW1, .axis_size = 40, .align = stack_ALIGN_END }, + [FLD1] = { .kind = stack_KIND_LEAF, .parent = ROW1, .flex = 1 }, + + [ROW2] = { .kind = stack_KIND_HBOX, .parent = ROOT, .axis_size = 20 }, + [LBL2] = { .kind = stack_KIND_LEAF, .parent = ROW2, .axis_size = 40, .align = stack_ALIGN_END }, + [FLD2] = { .kind = stack_KIND_LEAF, .parent = ROW2, .flex = 1 }, + }; + + result_t err; + box_t out[N]; + + err = stack_solve(items, N, &root, out); + if (err != result_OK) + return result_TEST_FAILED; + + if (out[FLD1].x0 != out[FLD2].x0) + return result_TEST_FAILED; + if (out[FLD1].x0 != 40) + return result_TEST_FAILED; + + return result_TEST_PASSED; +} + +/* 0,0 +--------+ 10,0 + * | A | flex=1 y: 0..50 + * +--------+ + * | B | flex=1 y: 50..100 + * +--------+ + * 0,100 10,100 + * + * A VBOX splits its children top to bottom, cross axis is x not y. */ +static result_t test_vbox_flex_split(void) +{ + enum { ROOT, A, B, N }; + static const box_t root = { 0, 0, 10, 100 }; + static const stack_item_t items[N] = + { + [ROOT] = { .kind = stack_KIND_VBOX, .parent = -1 }, + [A] = { .kind = stack_KIND_LEAF, .parent = ROOT, .flex = 1 }, + [B] = { .kind = stack_KIND_LEAF, .parent = ROOT, .flex = 1 }, + }; + + result_t err; + box_t out[N]; + + err = stack_solve(items, N, &root, out); + if (err != result_OK) + return result_TEST_FAILED; + + if (out[A].y0 != 0 || out[A].y1 != 50) + return result_TEST_FAILED; + if (out[B].y0 != 50 || out[B].y1 != 100) + return result_TEST_FAILED; + + return result_TEST_PASSED; +} + +/* 0 45 gap=10 55 100 + * +---------+ .... +---------+ + * | A flex=1| .... | B flex=1| + * +---------+ .... +---------+ + * + * A gap is inserted between adjacent children but not before the first or + * after the last. */ +static result_t test_gap(void) +{ + enum { ROOT, A, B, N }; + static const box_t root = { 0, 0, 100, 10 }; + static const stack_item_t items[N] = + { + [ROOT] = { .kind = stack_KIND_HBOX, .parent = -1, .gap = 10 }, + [A] = { .kind = stack_KIND_LEAF, .parent = ROOT, .flex = 1 }, + [B] = { .kind = stack_KIND_LEAF, .parent = ROOT, .flex = 1 }, + }; + + result_t err; + box_t out[N]; + + err = stack_solve(items, N, &root, out); + if (err != result_OK) + return result_TEST_FAILED; + + if (out[A].x0 != 0 || out[A].x1 != 45) + return result_TEST_FAILED; + if (out[B].x0 != 55 || out[B].x1 != 100) + return result_TEST_FAILED; + + return result_TEST_PASSED; +} + +/* 0,0 +------------------------+ 100,0 + * | pad_t=6 | + * | +----------------+ | + * |pl | A (fill) |pr | pad_l=5, pad_r=7 + * | +----------------+ | + * | pad_b=8 | + * +------------------------+ + * 0,100 100,100 + * + * Container padding insets the area available to its children on every + * edge. */ +static result_t test_padding(void) +{ + enum { ROOT, A, N }; + static const box_t root = { 0, 0, 100, 100 }; + static const stack_item_t items[N] = + { + [ROOT] = { .kind = stack_KIND_HBOX, .parent = -1, + .pad_l = 5, .pad_t = 6, .pad_r = 7, .pad_b = 8 }, + [A] = { .kind = stack_KIND_LEAF, .parent = ROOT, .flex = 1, + .align = stack_ALIGN_FILL }, + }; + + result_t err; + box_t out[N]; + + err = stack_solve(items, N, &root, out); + if (err != result_OK) + return result_TEST_FAILED; + + if (out[A].x0 != 5 || out[A].x1 != 93) + return result_TEST_FAILED; + if (out[A].y0 != 6 || out[A].y1 != 92) + return result_TEST_FAILED; + + return result_TEST_PASSED; +} + +/* 0 80 100 + * +-------------------+---------+ + * | A min=80 | B flex=1| + * +-------------------+---------+ + * + * A child's 'min' floors its extent below what an even flex split would + * give it; the remaining flex child absorbs the rest. */ +static result_t test_min_clamp(void) +{ + enum { ROOT, A, B, N }; + static const box_t root = { 0, 0, 100, 10 }; + static const stack_item_t items[N] = + { + [ROOT] = { .kind = stack_KIND_HBOX, .parent = -1 }, + [A] = { .kind = stack_KIND_LEAF, .parent = ROOT, .min = 80 }, + [B] = { .kind = stack_KIND_LEAF, .parent = ROOT, .flex = 1 }, + }; + + result_t err; + box_t out[N]; + + err = stack_solve(items, N, &root, out); + if (err != result_OK) + return result_TEST_FAILED; + + if (out[A].x1 - out[A].x0 != 80) + return result_TEST_FAILED; + if (out[B].x1 - out[B].x0 != 20) + return result_TEST_FAILED; + + return result_TEST_PASSED; +} + +/* 0 20 100 + * +--------+----------------------+ + * |A flex=1| B flex=1 (absorbs | + * |max=20 | the excess slack) | + * +--------+----------------------+ + * + * A child's 'max' ceilings the space handed to it by flex growth; the + * excess slack is returned to its sibling. */ +static result_t test_max_clamp(void) +{ + enum { ROOT, A, B, N }; + static const box_t root = { 0, 0, 100, 10 }; + static const stack_item_t items[N] = + { + [ROOT] = { .kind = stack_KIND_HBOX, .parent = -1 }, + [A] = { .kind = stack_KIND_LEAF, .parent = ROOT, .flex = 1, .max = 20 }, + [B] = { .kind = stack_KIND_LEAF, .parent = ROOT, .flex = 1 }, + }; + + result_t err; + box_t out[N]; + + err = stack_solve(items, N, &root, out); + if (err != result_OK) + return result_TEST_FAILED; + + if (out[A].x1 - out[A].x0 != 20) + return result_TEST_FAILED; + if (out[B].x1 - out[B].x0 != 80) + return result_TEST_FAILED; + + return result_TEST_PASSED; +} + +/* 0 10 90 100 + * +------+--------------------+------+ + * |A sz10| SPACER flex=1 |B sz10| + * +------+--------------------+------+ + * + * A SPACER consumes main-axis space like a leaf but is otherwise just + * another flexible child. */ +static result_t test_spacer(void) +{ + enum { ROOT, A, SPACER, B, N }; + static const box_t root = { 0, 0, 100, 10 }; + static const stack_item_t items[N] = + { + [ROOT] = { .kind = stack_KIND_HBOX, .parent = -1 }, + [A] = { .kind = stack_KIND_LEAF, .parent = ROOT, .axis_size = 10 }, + [SPACER] = { .kind = stack_KIND_SPACER, .parent = ROOT, .flex = 1 }, + [B] = { .kind = stack_KIND_LEAF, .parent = ROOT, .axis_size = 10 }, + }; + + result_t err; + box_t out[N]; + + err = stack_solve(items, N, &root, out); + if (err != result_OK) + return result_TEST_FAILED; + + if (out[A].x0 != 0 || out[A].x1 != 10) + return result_TEST_FAILED; + if (out[B].x0 != 90 || out[B].x1 != 100) + return result_TEST_FAILED; + + return result_TEST_PASSED; +} + +/* y=0 +----------------------+ + * | | + * | A | fills full 40px cross extent + * | | + * y=40 +----------------------+ + * + * ALIGN_FILL spans the full cross extent of the container. */ +static result_t test_align_fill(void) +{ + enum { ROOT, A, N }; + static const box_t root = { 0, 0, 100, 40 }; + static const stack_item_t items[N] = + { + [ROOT] = { .kind = stack_KIND_HBOX, .parent = -1 }, + [A] = { .kind = stack_KIND_LEAF, .parent = ROOT, .flex = 1, + .align = stack_ALIGN_FILL }, + }; + + result_t err; + box_t out[N]; + + err = stack_solve(items, N, &root, out); + if (err != result_OK) + return result_TEST_FAILED; + + if (out[A].y0 != 0 || out[A].y1 != 40) + return result_TEST_FAILED; + + return result_TEST_PASSED; +} + +/* y=0 +----------------------+ + * | | + * y=15 | +---A(10)---+ | + * | +-----------+ | + * y=25 | | + * y=40 +----------------------+ + * + * ALIGN_CENTRE centres a sized child within the container's cross + * extent. */ +static result_t test_align_centre(void) +{ + enum { ROOT, A, N }; + static const box_t root = { 0, 0, 100, 40 }; + static const stack_item_t items[N] = + { + [ROOT] = { .kind = stack_KIND_HBOX, .parent = -1 }, + [A] = { .kind = stack_KIND_LEAF, .parent = ROOT, .flex = 1, + .cross_size = 10, .align = stack_ALIGN_CENTRE }, + }; + + result_t err; + box_t out[N]; + + err = stack_solve(items, N, &root, out); + if (err != result_OK) + return result_TEST_FAILED; + + if (out[A].y0 != 15 || out[A].y1 != 25) + return result_TEST_FAILED; + + return result_TEST_PASSED; +} + +/* 0,0 100,0 + * +------------------------------+ + * | ROW (HBOX, fill) | + * | +------------+-------------+ | + * | | A flex=1 | B flex=1 | | + * | +------------+-------------+ | + * +------------------------------+ + * 0,50 100,50 + * + * An HBOX nested inside a VBOX is itself placed and then lays out its own + * children, recursing correctly through mixed axes. */ +static result_t test_nested_containers(void) +{ + enum { ROOT, ROW, A, B, N }; + static const box_t root = { 0, 0, 100, 50 }; + static const stack_item_t items[N] = + { + [ROOT] = { .kind = stack_KIND_VBOX, .parent = -1 }, + [ROW] = { .kind = stack_KIND_HBOX, .parent = ROOT, .flex = 1, + .align = stack_ALIGN_FILL }, + [A] = { .kind = stack_KIND_LEAF, .parent = ROW, .flex = 1 }, + [B] = { .kind = stack_KIND_LEAF, .parent = ROW, .flex = 1 }, + }; + + result_t err; + box_t out[N]; + + err = stack_solve(items, N, &root, out); + if (err != result_OK) + return result_TEST_FAILED; + + if (out[ROW].x0 != 0 || out[ROW].x1 != 100) + return result_TEST_FAILED; + if (out[A].x0 != 0 || out[A].x1 != 50) + return result_TEST_FAILED; + if (out[B].x0 != 50 || out[B].x1 != 100) + return result_TEST_FAILED; + + return result_TEST_PASSED; +} + +/* 1,2 +------+ 3,2 + * | ROOT | no children + * +------+ + * 1,4 3,4 + * + * A tree of just the root, with no children, solves trivially to the root + * box itself. */ +static result_t test_root_only(void) +{ + enum { ROOT, N }; + static const box_t root = { 1, 2, 3, 4 }; + static const stack_item_t items[N] = + { + [ROOT] = { .kind = stack_KIND_HBOX, .parent = -1 }, + }; + + result_t err; + box_t out[N]; + + err = stack_solve(items, N, &root, out); + if (err != result_OK) + return result_TEST_FAILED; + + if (out[ROOT].x0 != 1 || out[ROOT].y0 != 2 || + out[ROOT].x1 != 3 || out[ROOT].y1 != 4) + return result_TEST_FAILED; + + return result_TEST_PASSED; +} + +/* ROOT + * | + * A ---parent---> A (self-loop, invalid) + * + * A parent index pointing forward (or at itself) is rejected. */ +static result_t test_bad_tree(void) +{ + enum { ROOT, A, N }; + static const box_t root = { 0, 0, 10, 10 }; + static const stack_item_t items[N] = + { + [ROOT] = { .kind = stack_KIND_HBOX, .parent = -1 }, + [A] = { .kind = stack_KIND_LEAF, .parent = A }, /* self-referential */ + }; + + result_t err; + box_t out[N]; + + err = stack_solve(items, N, &root, out); + if (err != result_STACK_BAD_TREE) + return result_TEST_FAILED; + + return result_TEST_PASSED; +} + +/* ROOT (VBOX, pad 2 all round) + * | + * +-- ROW (HBOX, gap=3) + * +-- LBL min=20 + * +-- FLD flex=1, min=15 + * + * ROW's minimum main axis is 20+3+15=38; ROOT's minimum is that plus its own + * padding (2+2=4) on each axis -- width 42, height (ROW has no explicit + * axis_size/min, so 0) 4. Measuring, then solving into exactly that size, + * must leave every leaf at its minimum (no slack for FLD's flex to grow + * into). */ +static result_t test_measure_min(void) +{ + enum { ROOT, ROW, LBL, FLD, N }; + static const stack_item_t items[N] = + { + [ROOT] = { .kind = stack_KIND_VBOX, .parent = -1, + .pad_l = 2, .pad_t = 2, .pad_r = 2, .pad_b = 2 }, + [ROW] = { .kind = stack_KIND_HBOX, .parent = ROOT, .gap = 3 }, + [LBL] = { .kind = stack_KIND_LEAF, .parent = ROW, .min = 20 }, + [FLD] = { .kind = stack_KIND_LEAF, .parent = ROW, .flex = 1, .min = 15 }, + }; + + result_t err; + size2d_t sz; + box_t root, out[N]; + + err = stack_smallest(items, N, &sz); + if (err != result_OK) + return result_TEST_FAILED; + + if (sz.w != 42 || sz.h != 4) + return result_TEST_FAILED; + + root = (box_t) BOX_POS_SIZE(0, 0, sz.w, sz.h); + err = stack_solve(items, N, &root, out); + if (err != result_OK) + return result_TEST_FAILED; + + if (out[LBL].x1 - out[LBL].x0 != 20) + return result_TEST_FAILED; + if (out[FLD].x1 - out[FLD].x0 != 15) + return result_TEST_FAILED; + + return result_TEST_PASSED; +} + +/* ----------------------------------------------------------------------- */ + +result_t stack_test(const char *resources) +{ + result_t err; + + NOT_USED(resources); + + err = test_flex_split(); + if (err != result_TEST_PASSED) + return err; + + err = test_aligned_labels(); + if (err != result_TEST_PASSED) + return err; + + err = test_vbox_flex_split(); + if (err != result_TEST_PASSED) + return err; + + err = test_gap(); + if (err != result_TEST_PASSED) + return err; + + err = test_padding(); + if (err != result_TEST_PASSED) + return err; + + err = test_min_clamp(); + if (err != result_TEST_PASSED) + return err; + + err = test_max_clamp(); + if (err != result_TEST_PASSED) + return err; + + err = test_spacer(); + if (err != result_TEST_PASSED) + return err; + + err = test_align_fill(); + if (err != result_TEST_PASSED) + return err; + + err = test_align_centre(); + if (err != result_TEST_PASSED) + return err; + + err = test_nested_containers(); + if (err != result_TEST_PASSED) + return err; + + err = test_root_only(); + if (err != result_TEST_PASSED) + return err; + + err = test_bad_tree(); + if (err != result_TEST_PASSED) + return err; + + err = test_measure_min(); + if (err != result_TEST_PASSED) + return err; + + return result_TEST_PASSED; +} diff --git a/libraries/geom/stack/valid-tree.c b/libraries/geom/stack/valid-tree.c new file mode 100644 index 0000000..0c1de05 --- /dev/null +++ b/libraries/geom/stack/valid-tree.c @@ -0,0 +1,24 @@ +/* geom/stack/valid-tree.c -- validate a stack item array is a forest rooted at 0 */ + +#include "geom/stack.h" + +#include "impl.h" + +/* ----------------------------------------------------------------------- */ + +int stack__valid_tree(const stack_item_t *items, int n) +{ + int i; + + if (n <= 0) + return 0; + + if (items[0].parent != -1) + return 0; + + for (i = 1; i < n; i++) + if (items[i].parent < 0 || items[i].parent >= i) + return 0; + + return 1; +} diff --git a/libraries/io/dirscan/dirscan.c b/libraries/io/dirscan/dirscan.c index fd70def..c4fa085 100644 --- a/libraries/io/dirscan/dirscan.c +++ b/libraries/io/dirscan/dirscan.c @@ -111,8 +111,8 @@ result_t dirscan_walk(const char *dir, dirscan_fn *fn, void *opaque) result_t dirscan_walk(const char *dir, dirscan_fn *fn, void *opaque) { - result_t rc; - DIR *dp; + result_t rc; + DIR *dp; struct dirent *de; if (dir == NULL || fn == NULL) diff --git a/libraries/io/namelist/namelist.c b/libraries/io/namelist/namelist.c index 43d3964..84b9ca9 100644 --- a/libraries/io/namelist/namelist.c +++ b/libraries/io/namelist/namelist.c @@ -6,6 +6,7 @@ #include "base/result.h" #include "io/dirscan.h" +#include "io/path.h" #include "io/namelist.h" @@ -17,7 +18,6 @@ typedef struct namelist_ctx size_t stride; int cap; const char *ext; /* NULL or "" for "match everything" */ - size_t extlen; int count; } namelist_ctx_t; @@ -25,32 +25,29 @@ namelist_ctx_t; static result_t namelist_entry(const char *leaf, void *opaque) { namelist_ctx_t *ctx; - size_t leaflen; - size_t namelen; + char name[DPTLIB_MAXPATH]; - ctx = opaque; - leaflen = strlen(leaf); + ctx = opaque; if (ctx->count >= ctx->cap) return result_STOP_WALK; - if (ctx->extlen > 0) + if (ctx->ext != NULL && ctx->ext[0] != '\0') { - if (leaflen <= ctx->extlen || - strcmp(leaf + leaflen - ctx->extlen, ctx->ext) != 0) + if (!path_leaf_strip_ext(leaf, ctx->ext, name, sizeof(name))) return result_OK; - namelen = leaflen - ctx->extlen; } else { - namelen = leaflen; + if (strlen(leaf) + 1 > sizeof(name)) + return result_OK; + strcpy(name, leaf); } - if (namelen + 1 > ctx->stride) + if (strlen(name) + 1 > ctx->stride) return result_OK; /* would not fit its slot */ - memcpy(ctx->names + (size_t) ctx->count * ctx->stride, leaf, namelen); - ctx->names[(size_t) ctx->count * ctx->stride + namelen] = '\0'; + strcpy(ctx->names + (size_t) ctx->count * ctx->stride, name); ctx->count++; return result_OK; @@ -81,7 +78,6 @@ result_t namelist_scan(const char *dir, ctx.stride = stride; ctx.cap = cap; ctx.ext = ext; - ctx.extlen = (ext != NULL) ? strlen(ext) : 0; ctx.count = 0; rc = dirscan_walk(dir, namelist_entry, &ctx); diff --git a/libraries/io/path/path.c b/libraries/io/path/path.c index 42acdc1..d2b18e1 100644 --- a/libraries/io/path/path.c +++ b/libraries/io/path/path.c @@ -12,17 +12,23 @@ const char *path_join_leafname(const char *leaf, const char *ext) { static char buf[DPTLIB_MAXPATH]; - const char *fmt = + assert(leaf); + assert(ext); + #ifdef __riscos - "%s/%s"; + + /* No dotted extension in a RISC OS leafname -- file type is separate + * filesystem metadata, set with OS_File 18 (Set_Type), never part of the + * pathname string -- so there is nothing to append. */ + NOT_USED(ext); + + snprintf(buf, sizeof(buf), "%s", leaf); + #else - "%s.%s"; -#endif - assert(leaf); - assert(ext); + snprintf(buf, sizeof(buf), "%s.%s", leaf, ext); - snprintf(buf, sizeof(buf), fmt, leaf, ext); +#endif return buf; } @@ -37,9 +43,9 @@ const char *path_join_filename(const char *root, int nbranches, ...) #else "/"; #endif - va_list args; - size_t used; - int rc; + va_list args; + size_t used; + int rc; assert(root); assert(nbranches < 1000); @@ -60,3 +66,53 @@ const char *path_join_filename(const char *root, int nbranches, ...) return buf; } + +int path_leaf_strip_ext(const char *leaf, + const char *ext, + char *name, + size_t cap) +{ + assert(leaf); + assert(ext); + assert(name); + +#ifdef __riscos + + { + size_t leaflen; + + /* No extension in the string to match against -- filetype is separate + * metadata -- so every leaf matches, unchanged. */ + NOT_USED(ext); + + leaflen = strlen(leaf); + if (leaflen + 1 > cap) + return 0; + + memcpy(name, leaf, leaflen + 1); + + return 1; + } + +#else + + { + size_t leaflen; + size_t extlen; + + leaflen = strlen(leaf); + extlen = strlen(ext); + + if (leaflen <= extlen || leaflen - extlen >= cap) + return 0; + if (strcmp(leaf + leaflen - extlen, ext) != 0) + return 0; + + memcpy(name, leaf, leaflen - extlen); + name[leaflen - extlen] = '\0'; + + return 1; + } + +#endif +} diff --git a/libraries/text/bmtext/draw.c b/libraries/text/bmtext/draw.c index d64a8de..721fe7f 100644 --- a/libraries/text/bmtext/draw.c +++ b/libraries/text/bmtext/draw.c @@ -8,25 +8,28 @@ #include "text/bmtext.h" -void bmtext_draw(bmfont_t *font, - screen_t *scr, - const bmtext_line_t *lines, - int nlines, - colour_t fg, - colour_t bg, - int leading, - point_t origin) +void bmtext_draw(bmfont_t *font, + screen_t *scr, + const bmtext_line_t *lines, + int nlines, + colour_t fg, + colour_t bg, + int leading, + point_t origin, + const bmfont_spacing_t *spacing) { - int font_height; + int font_height, font_ascent; point_t pos; int i; - bmfont_get_info(font, NULL, &font_height); + bmfont_get_info(font, NULL, &font_height, &font_ascent, NULL); - pos = origin; + pos = origin; + pos.y += font_ascent; /* origin is top-left; bmfont_draw wants the baseline */ for (i = 0; i < nlines; i++) { - bmfont_draw(font, scr, lines[i].str, lines[i].len, fg, bg, &pos, NULL); + bmfont_draw(font, scr, lines[i].str, lines[i].len, fg, bg, spacing, &pos, + NULL); pos.y += font_height + leading; } } diff --git a/libraries/text/bmtext/layout.c b/libraries/text/bmtext/layout.c index b6f30b6..6f8a871 100644 --- a/libraries/text/bmtext/layout.c +++ b/libraries/text/bmtext/layout.c @@ -1,17 +1,19 @@ /* text/bmtext/layout.c -- break a string into pixel-fitted lines */ #include +#include #include "framebuf/bmfont.h" #include "text/bmtext.h" -int bmtext_layout(bmfont_t *font, - const char *string, - int stringlen, - int wrap_width, - bmtext_line_t *lines, - int max) +int bmtext_layout(bmfont_t *font, + const char *string, + int stringlen, + int wrap_width, + const bmfont_spacing_t *spacing, + bmtext_line_t *lines, + int max) { int nlines; @@ -23,7 +25,8 @@ int bmtext_layout(bmfont_t *font, bmfont_width_t width; int friendly_break; - bmfont_measure(font, string, stringlen, wrap_width, &absolute_break, &width); + bmfont_measure(font, string, stringlen, spacing, wrap_width, + &absolute_break, &width); friendly_break = absolute_break; if (absolute_break < stringlen) diff --git a/libraries/utils/pack/test/pack-test.c b/libraries/utils/pack/test/pack-test.c index 64d7e04..2838bd4 100644 --- a/libraries/utils/pack/test/pack-test.c +++ b/libraries/utils/pack/test/pack-test.c @@ -28,13 +28,13 @@ result_t pack_test(const char *resources) }; static const char fmt[] = "2c2s2i"; - const size_t sz = 14; - unsigned char buf[100]; - size_t n; - signed char c1, c2; - short s1, s2; - int l1, l2; + const size_t sz = 14; + unsigned char buf[100]; + size_t n; + signed char c1, c2; + short s1, s2; + int l1, l2; n = pack(buf, fmt, data[0], data[1], data[2], data[3], data[4], data[5]); if (n != sz) @@ -103,10 +103,10 @@ result_t pack_test(const char *resources) 0x00, 0xff }; static const char fmt[] = "*c*s*i"; - const size_t sz = 2 + 4 + 8; - unsigned char buf[100]; - size_t n; + const size_t sz = 2 + 4 + 8; + unsigned char buf[100]; + size_t n; n = pack(buf, fmt, 2, cdata, 2, sdata, 2, ldata); if (n != sz) diff --git a/libraries/wuss/TODO.txt b/libraries/wuss/TODO.txt index d39e91c..b0471e9 100644 --- a/libraries/wuss/TODO.txt +++ b/libraries/wuss/TODO.txt @@ -4,12 +4,22 @@ Ordered by dependency. Each layer builds on the ones above it. Items marked (blocks: X) are prerequisites for X. +Add a panic redraw indicator? + +Furniture clicks / scrolls still invalidate the entire furniture set - fix. + (window move fixed: layout cache is now translated in place, not rebuilt) + +Fast scrolling can invalidate areas and fail to copy the expected known good screen content. + -- Tried and tried to get Claude to fix but no good. + +Too much slider boilerplate -- need to make label+slider+value a standard component. +It also has the client do the rounding/snapping which isn't ideal. + + 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) @@ -19,15 +29,14 @@ LAYER 1 - CORE PLUMBING (no wuss deps; unblocks everything else) blocks: Draw bitmap through mask, Masked sprites/bitmaps, furniture tinting - UTF-8 everywhere blocks: text abstraction, window/menu templates -- Is having font handling in wuss-core a bad thing? Decide arch now. - blocks: Wuss text abstraction +- Icon type taxonomy +- Window layout / constraint system / stacking primitives? +- I still think there's over-draw when toggling window sizes. 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 @@ -36,15 +45,15 @@ LAYER 3 - SPRITES (needs masks) - Sprites (with names, with masks, with multiple sprites) - Masked sprites/bitmaps. Covered by RLE work? - Sprite scaling -- Draw bitmap through (bayer) mask - Import Kare style icons +- Sprites noticably brighter / colour matching needs work +- App sprites LAYER 4 - WINDOW FURNITURE & INTERACTION (needs bug layer, text layer) - Set wuss window config after the fact (goes with palette work) blocks: Furniture icons toggling state, min window size -- Proper furniture: close, minimise, maximise, scrolling (half done) - Minimum window size based on furniture requested (later: squash-able furniture) - Furniture icons toggling state when clicked @@ -52,8 +61,8 @@ LAYER 4 - WINDOW FURNITURE & INTERACTION (needs bug layer, text layer) - Snap to edges (outline-click bug fixed; unblocked) - Dragging against window edge shuffles window in opposite direction (outline-click bug fixed; unblocked) +- 2D Adjust window scroll drag - Input focus, text entry, furniture tinting -- Icon layout - Furniture colour selection needs improvements (e.g. losing menu readability when palette changed) @@ -70,13 +79,19 @@ LAYER 6 - MULTI-TASK (needs stable windows, templates) - Messaging between tasks blocks: Desktop cohort - Backdrop: register events, or a task that covers it like Pinboard -- Icon bar - a separate task? +- Icon bar - a separate task or module? - Test submenu dialogues -- Dialogue abstraction from app engine? +- Pull in dialogue abstraction from app engine? +- Dynamic submenus: a pre-open event (like RISC OS's MenuWarning) fired + before a submenu-style item opens, so a task can retitle/retarget a + shared submenu instance first. Currently only item->window leaves get + a pre-open hook (wuss_EVENT_PRE_SHOW); item->submenu leaves have none. + (see saturn.c's fg/bg colourmenu split, kept as two instances for this reason) LAYER 7 - APPS & POLISH (needs most of the above) +- Post-Its app with explanatory text for newbies - Have a Paint app as a target - Desktop cohort: Filer, Task Manager, Palette Util - Colour Picker component @@ -92,7 +107,6 @@ NO STRONG DEPS (do any time) - Optimise - Generalise APIs (make more Wimp-like) -- Make as much use of DPTLib functions as possible - Fix coordinates to be exactly like Wimp/RO? - Set overall UI scale (like EIG values) - Window stacks (a-la nested wimp) diff --git a/libraries/wuss/component/colourmenu.c b/libraries/wuss/component/colourmenu.c index faa7755..f90ef26 100644 --- a/libraries/wuss/component/colourmenu.c +++ b/libraries/wuss/component/colourmenu.c @@ -66,6 +66,11 @@ result_t wuss_colourmenu_create(wuss_colourmenu_t **out, a = &wuss->alloc; n = wuss->npalette; + if (n > wuss_COLOUR_SYMBOLIC) + n = wuss_COLOUR_SYMBOLIC; /* wuss_colour_t indices above this are the + * symbolic/chrome-role namespace, not real + * palette slots -- don't hand them out as + * swatches */ cm = a->malloc(sizeof(*cm)); if (cm == NULL) diff --git a/libraries/wuss/component/dialogue.c b/libraries/wuss/component/dialogue.c new file mode 100644 index 0000000..0aeb6cf --- /dev/null +++ b/libraries/wuss/component/dialogue.c @@ -0,0 +1,196 @@ +/* wuss/component/dialogue.c -- interactive dialogue base class */ + +#include +#include + +#ifdef FORTIFY +#include "fortify/fortify.h" +#endif + +#include "base/result.h" +#include "geom/box.h" + +#include "wuss/task.h" +#include "wuss/window.h" + +#include "wuss/component/dialogue.h" + +#include "../core/impl.h" + +/* ----------------------------------------------------------------------- */ + +/* The window is created hidden, without wuss_WINDOW_CLOSE, for the same + * reason as wuss/component/info.c: it may be borrowed as a + * wuss_menu_item_t::window, and the caller (not this component) decides when + * a Cancel/OK click dismisses it. + * + * owns_window is 0 for a dialogue built via wuss_dialogue_create_on_window + * (e.g. composed with a wuss_info_t, which owns and resizes the window + * itself): wuss_dialogue_destroy must then leave the window alone, for the + * other component's own destroy call to close. */ +struct wuss_dialogue +{ + wuss_alloc_t alloc; /* copied hooks; wuss_t itself not retained */ + wuss_window_t *window; + int owns_window; + wuss_dialogue_fillout_fn_t *fillout; + void *opaque; + wuss_dialogue_action_t actions[WUSS_DIALOGUE_MAX_ACTIONS]; + int nactions; +}; + +/* ----------------------------------------------------------------------- */ + +static wuss_dialogue_t *dialogue_alloc(wuss_alloc_t alloc, + wuss_dialogue_fillout_fn_t *fillout, + void *opaque) +{ + wuss_dialogue_t *dialogue; + + dialogue = alloc.malloc(sizeof(*dialogue)); + if (dialogue == NULL) + return NULL; + + dialogue->alloc = alloc; + dialogue->window = NULL; + dialogue->fillout = fillout; + dialogue->opaque = opaque; + dialogue->nactions = 0; + + return dialogue; +} + +result_t wuss_dialogue_create(wuss_dialogue_t **out, + wuss_task_t *task, + size2d_t size, + const char *title, + wuss_dialogue_fillout_fn_t *fillout, + void *opaque) +{ + result_t rc; + wuss_dialogue_t *dialogue; + box_t content; + + if (out == NULL || task == NULL) + return result_NULL_ARG; + + dialogue = dialogue_alloc(task->wuss->alloc, fillout, opaque); + if (dialogue == NULL) + return result_OOM; + dialogue->owns_window = 1; + + content = (box_t) BOX_POS_SIZE(0, 0, size.w, size.h); + rc = wuss_window_create(task, + &content, + title, + wuss_WINDOW_HIDDEN | wuss_WINDOW_NO_REDRAW, + wuss_BACKDROP_COLOUR(wuss_COLOUR_WINDOW), + size, + size, + &dialogue->window); + if (rc != result_OK) + { + dialogue->alloc.free(dialogue); + return rc; + } + + *out = dialogue; + return result_OK; +} + +result_t wuss_dialogue_create_on_window(wuss_dialogue_t **out, + wuss_t *wuss, + wuss_window_t *window, + wuss_dialogue_fillout_fn_t *fillout, + void *opaque) +{ + wuss_dialogue_t *dialogue; + + if (out == NULL || wuss == NULL || window == NULL) + return result_NULL_ARG; + + dialogue = dialogue_alloc(wuss->alloc, fillout, opaque); + if (dialogue == NULL) + return result_OOM; + dialogue->owns_window = 0; + dialogue->window = window; + + *out = dialogue; + return result_OK; +} + +void wuss_dialogue_destroy(wuss_dialogue_t *doomed) +{ + wuss_alloc_t alloc; + + if (doomed == NULL) + return; + + alloc = doomed->alloc; + if (doomed->owns_window) + wuss_window_close(doomed->window); /* frees the window and its icons */ + alloc.free(doomed); +} + +/* ----------------------------------------------------------------------- */ + +void wuss_dialogue_set_opaque(wuss_dialogue_t *dialogue, void *opaque) +{ + dialogue->opaque = opaque; +} + +result_t wuss_dialogue_set_actions(wuss_dialogue_t *dialogue, + const wuss_dialogue_action_t *actions, + int nactions) +{ + if (nactions < 0 || nactions > WUSS_DIALOGUE_MAX_ACTIONS) + return result_BAD_ARG; + + dialogue->nactions = nactions; + if (nactions > 0) + memcpy(dialogue->actions, actions, sizeof(*actions) * (size_t) nactions); + + return result_OK; +} + +int wuss_dialogue_handle_icon(wuss_dialogue_t *dialogue, + const wuss_event_t *event, + result_t *out_result) +{ + int i; + + if (dialogue == NULL || event == NULL) + return 0; + if (event->data.icon.action != wuss_MOUSE_UP) + return 0; + + for (i = 0; i < dialogue->nactions; i++) + { + if (dialogue->actions[i].icon == event->data.icon.icon) + { + *out_result = dialogue->actions[i].fn(dialogue->opaque, + event->data.icon.button); + return 1; + } + } + + return 0; +} + +result_t wuss_dialogue_handle_pre_show(wuss_dialogue_t *dialogue) +{ + if (dialogue->fillout == NULL) + return result_OK; + + return dialogue->fillout(dialogue->opaque); +} + +result_t wuss_dialogue_hide(wuss_dialogue_t *dialogue) +{ + return wuss_window_set_hidden(dialogue->window, 1); +} + +wuss_window_t *wuss_dialogue_window(const wuss_dialogue_t *dialogue) +{ + return dialogue ? dialogue->window : NULL; +} diff --git a/libraries/wuss/component/info.c b/libraries/wuss/component/info.c index 7bd7aee..7e0c9e6 100644 --- a/libraries/wuss/component/info.c +++ b/libraries/wuss/component/info.c @@ -36,16 +36,22 @@ #define INFO_FIELD_PAD 4 /* px each side of the text inside a value field */ #define INFO_MAX_ROWS 16 -/* The dialogue is one window on a task the caller passes in. It carries - * wuss_WINDOW_NO_CLOSE so a menu chain that borrows it as a +/* The dialogue is one window on a task the caller passes in. It omits + * wuss_WINDOW_CLOSE so a menu chain that borrows it as a * wuss_menu_item_t::window can't have it pulled from under it, and * wuss_info_destroy closes it explicitly. The task must therefore not be an * autoclose one -- its window list would never empty -- and must outlive the - * handle. */ + * handle. + * + * icons/nicons track the live label:value icons so wuss_info_set_rows can + * delete them before rebuilding at a new size -- wuss has no "clear all + * icons on this window" call. */ struct wuss_info { - wuss_alloc_t alloc; /* copied hooks; the wuss_t itself is not retained */ - wuss_window_t *window; /* owned; closed by wuss_info_destroy */ + wuss_alloc_t alloc; /* copied hooks; wuss_t itself not retained */ + wuss_window_t *window; /* owned; closed by wuss_info_destroy */ + wuss_icon_t *icons[INFO_MAX_ROWS * 2]; + int nicons; }; /* ----------------------------------------------------------------------- */ @@ -64,34 +70,24 @@ static int info_widen(bmfont_t *font, const char *s, int cur) return MAX(cur, (int) w); } -result_t wuss_info_create(wuss_info_t **out, - wuss_task_t *task, - const char *title, - const wuss_info_row_t *rows, - int nrows) +/* Computes the label:value grid's column widths, row pitch and overall size + * for \p rows against \p font (may be NULL). Shared by wuss_info_create (to + * size the window it creates) and wuss_info_set_rows (to resize the window + * it reuses). */ +static void info_measure(bmfont_t *font, + const wuss_info_row_t *rows, + int nrows, + int *out_labelw, + int *out_valuew, + int *out_rowh, + int *out_fieldh, + int *out_w, + int *out_h) { - result_t rc; - wuss_t *wuss; - bmfont_t *font; - wuss_info_t *info; - int fonth; - int rowh; - int fieldh; - int labelw; - int valuew; - int w; - int h; - box_t content; - wuss_icon_spec_t specs[INFO_MAX_ROWS * 2]; - int i; - - if (out == NULL || task == NULL || title == NULL || rows == NULL) - return result_NULL_ARG; - if (nrows < 1 || nrows > INFO_MAX_ROWS) - return result_BAD_ARG; - - wuss = task->wuss; - font = wuss_get_font(wuss); + int fonth; + int labelw, valuew; + int rowh, fieldh; + int i; /* Column widths: the widest label on the left, the widest value on the * right. A NULL font leaves both at 0; the icons still lay out, just with @@ -106,46 +102,37 @@ result_t wuss_info_create(wuss_info_t **out, fonth = 0; if (font != NULL) - bmfont_get_info(font, NULL, &fonth); + bmfont_get_info(font, NULL, &fonth, NULL, NULL); rowh = MAX(fonth + INFO_ROW_PAD, 12) + INFO_ROW_LEADING; fieldh = rowh - INFO_ROW_LEADING; /* rowh is the pitch; leave a gap */ labelw = MAX(labelw, 1); valuew = MAX(valuew, 1) + INFO_FIELD_PAD * 2; /* groove border + inset */ - w = INFO_MARGIN * 2 + labelw + INFO_GAP + valuew; + *out_labelw = labelw; + *out_valuew = valuew; + *out_rowh = rowh; + *out_fieldh = fieldh; + *out_w = INFO_MARGIN * 2 + labelw + INFO_GAP + valuew; /* rowh is the pitch (field + leading); the last row has no trailing leading */ - h = INFO_MARGIN * 2 + rowh * nrows - INFO_ROW_LEADING; - - info = wuss->alloc.malloc(sizeof(*info)); - if (info == NULL) - return result_OOM; - info->alloc = wuss->alloc; /* the copy outlives the wuss_t */ - info->window = NULL; + *out_h = INFO_MARGIN * 2 + rowh * nrows - INFO_ROW_LEADING; +} - content = (box_t) BOX_POS_SIZE(0, 0, w, h); - rc = wuss_window_create(task, - &content, - title, - wuss_WINDOW_NO_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; - } +/* Fills \p specs (>= nrows * 2 entries) with one right-justified label and + * one centred value per row, laid out per info_measure's labelw/valuew/rowh/ + * fieldh. Zeroed first so the icon-spec fields this component does not set + * (pattern, bitmap, group, swatch) are their safe defaults. */ +static void info_fill_specs(wuss_icon_spec_t *specs, + const wuss_info_row_t *rows, + int nrows, + int labelw, + int valuew, + int rowh, + int fieldh) +{ + int i; - /* One right-justified label and one centred value per row. Zeroed so the - * icon-spec fields this component does not set (pattern, bitmap, group, - * swatch) are their safe defaults. wuss_icon_create deep-copies each spec - * and its text, so this stack array can go out of scope after the call. */ - memset(specs, 0, sizeof(specs)); + memset(specs, 0, sizeof(*specs) * (size_t) nrows * 2); for (i = 0; i < nrows; i++) { wuss_icon_spec_t *label; @@ -169,22 +156,115 @@ result_t wuss_info_create(wuss_info_t **out, value->text = rows[i].value; value->fg = wuss_COLOUR_BLACK; value->bg = wuss_NO_BACKGROUND; - value->border = wuss_ICON_BORDER_GROOVE; /* RISC OS display field */ + value->u.label.border = wuss_ICON_BORDER_GROOVE; /* RISC OS display field */ value->flags = wuss_ICON_FLAGS_JUSTIFY_CENTRE; } +} + +result_t wuss_info_create(wuss_info_t **out, + wuss_task_t *task, + const char *title, + const wuss_info_row_t *rows, + int nrows) +{ + result_t rc; + wuss_t *wuss; + bmfont_t *font; + wuss_info_t *info; + int labelw, valuew; + int rowh, fieldh; + int w, h; + box_t content; + wuss_icon_spec_t specs[INFO_MAX_ROWS * 2]; + + if (out == NULL || task == NULL || title == NULL || rows == NULL) + return result_NULL_ARG; + if (nrows < 1 || nrows > INFO_MAX_ROWS) + return result_BAD_ARG; + + wuss = task->wuss; + font = wuss_get_font(wuss); + info_measure(font, rows, nrows, &labelw, &valuew, &rowh, &fieldh, &w, &h); + + info = wuss->alloc.malloc(sizeof(*info)); + if (info == NULL) + return result_OOM; + info->alloc = wuss->alloc; /* the copy outlives the wuss_t */ + info->window = NULL; + info->nicons = 0; + + content = (box_t) BOX_POS_SIZE(0, 0, w, h); + rc = wuss_window_create(task, + &content, + title, + wuss_WINDOW_NO_REDRAW | wuss_WINDOW_HIDDEN, + wuss_BACKDROP_COLOUR(wuss_COLOUR_WINDOW), + SIZE2D(w, h), + SIZE2D(w, h), + &info->window); + if (rc != result_OK) + { + info->alloc.free(info); + return rc; + } - rc = wuss_icon_create_array(info->window, specs, nrows * 2, NULL); + /* wuss_icon_create deep-copies each spec and its text, so this stack array + * can go out of scope once wuss_icon_create_array returns. */ + info_fill_specs(specs, rows, nrows, labelw, valuew, rowh, fieldh); + rc = wuss_icon_create_array(info->window, specs, nrows * 2, info->icons); if (rc != result_OK) { wuss_window_close(info->window); /* frees the window and any icons on it */ info->alloc.free(info); return rc; } + info->nicons = nrows * 2; *out = info; return result_OK; } +result_t wuss_info_set_rows(wuss_info_t *info, + const wuss_info_row_t *rows, + int nrows) +{ + result_t rc; + bmfont_t *font; + int labelw, valuew; + int rowh, fieldh; + int w, h; + wuss_icon_spec_t specs[INFO_MAX_ROWS * 2]; + wuss_icon_t *made[INFO_MAX_ROWS * 2]; + int i; + + if (info == NULL || rows == NULL) + return result_NULL_ARG; + if (nrows < 1 || nrows > INFO_MAX_ROWS) + return result_BAD_ARG; + + font = wuss_get_font(info->window->wuss); + info_measure(font, rows, nrows, &labelw, &valuew, &rowh, &fieldh, &w, &h); + + rc = wuss_window_resize(info->window, SIZE2D(w, h)); + if (rc != result_OK) + return rc; + rc = wuss_window_set_doc(info->window, SIZE2D(w, h)); + if (rc != result_OK) + return rc; + + info_fill_specs(specs, rows, nrows, labelw, valuew, rowh, fieldh); + rc = wuss_icon_create_array(info->window, specs, nrows * 2, made); + if (rc != result_OK) + return rc; + + for (i = 0; i < info->nicons; i++) + wuss_icon_delete(info->window, info->icons[i]); + memcpy(info->icons, made, sizeof(*made) * (size_t) nrows * 2); + info->nicons = nrows * 2; + + return result_OK; +} + void wuss_info_destroy(wuss_info_t *doomed) { wuss_alloc_t alloc; diff --git a/libraries/wuss/component/proginfo.c b/libraries/wuss/component/proginfo.c index 991865d..d16b476 100644 --- a/libraries/wuss/component/proginfo.c +++ b/libraries/wuss/component/proginfo.c @@ -1,6 +1,7 @@ -/* wuss/component/proginfo.c -- a "program information" standard dialogue */ +/* wuss/component/proginfo.c -- a shared "program information" dialogue */ #include +#include #ifdef FORTIFY #include "fortify/fortify.h" @@ -9,24 +10,38 @@ #include "base/result.h" #include "wuss/task.h" +#include "wuss/window.h" +#include "wuss/component/dialogue.h" #include "wuss/component/info.h" #include "wuss/component/proginfo.h" -/* ----------------------------------------------------------------------- */ +#include "../core/impl.h" -/* ponytail: this whole component is now the desc -> rows mapping below; - * wuss/component/info.c does the window, the measuring and the layout. */ +/* ----------------------------------------------------------------------- */ -result_t wuss_proginfo_create(wuss_proginfo_t **out, - wuss_task_t *task, - const wuss_proginfo_desc_t *desc) +/* Owns a wuss_info_t (window, layout) wrapped by a wuss_dialogue_t (fillout + * dispatch only -- wuss_dialogue_create_on_window, so it does not own or + * close info's window). pending is the desc last passed to + * wuss_proginfo_set_desc, applied to info by wuss_proginfo_handle_pre_show; + * it is only read back then, so no copy is needed. */ +struct wuss_proginfo { - wuss_info_row_t rows[4]; - int nrows; + wuss_alloc_t alloc; /* copied hooks; wuss_t itself not retained */ + wuss_info_t *info; + wuss_dialogue_t *dialogue; + wuss_proginfo_desc_t pending; +}; - if (out == NULL || task == NULL || desc == NULL || desc->name == NULL) - return result_NULL_ARG; +/* ----------------------------------------------------------------------- */ + +/* Fills rows (>= 4 entries) from desc's non-NULL fields, in Name, Purpose, + * Author, Version order. Shared by wuss_proginfo_create (initial layout) and + * proginfo_fillout (relayout on show). */ +static int proginfo_rows(const wuss_proginfo_desc_t *desc, + wuss_info_row_t *rows) +{ + int nrows; nrows = 0; if (desc->name != NULL) @@ -54,5 +69,89 @@ result_t wuss_proginfo_create(wuss_proginfo_t **out, nrows++; } - return wuss_info_create(out, task, "About this program", rows, nrows); + return nrows; +} + +static result_t proginfo_fillout(void *opaque) +{ + wuss_proginfo_t *pi; + wuss_info_row_t rows[4]; + int nrows; + + pi = opaque; + nrows = proginfo_rows(&pi->pending, rows); + + return wuss_info_set_rows(pi->info, rows, nrows); +} + +result_t wuss_proginfo_create(wuss_proginfo_t **out, + wuss_task_t *task, + const wuss_proginfo_desc_t *desc) +{ + result_t rc; + wuss_proginfo_t *pi; + wuss_info_row_t rows[4]; + int nrows; + + if (out == NULL || task == NULL || desc == NULL || desc->name == NULL) + return result_NULL_ARG; + + pi = task->wuss->alloc.malloc(sizeof(*pi)); + if (pi == NULL) + return result_OOM; + pi->alloc = task->wuss->alloc; + pi->pending = *desc; + + nrows = proginfo_rows(desc, rows); + rc = wuss_info_create(&pi->info, task, "About this program", rows, nrows); + if (rc != result_OK) + { + task->wuss->alloc.free(pi); + return rc; + } + + rc = wuss_dialogue_create_on_window(&pi->dialogue, task->wuss, + wuss_info_window(pi->info), + proginfo_fillout, pi); + if (rc != result_OK) + { + wuss_info_destroy(pi->info); + task->wuss->alloc.free(pi); + return rc; + } + + *out = pi; + return result_OK; +} + +void wuss_proginfo_destroy(wuss_proginfo_t *doomed) +{ + wuss_alloc_t alloc; + + if (doomed == NULL) + return; + + alloc = doomed->alloc; + wuss_dialogue_destroy(doomed->dialogue); + wuss_info_destroy(doomed->info); + alloc.free(doomed); +} + +void wuss_proginfo_set_desc(wuss_proginfo_t *pi, + const wuss_proginfo_desc_t *desc) +{ + if (pi == NULL || desc == NULL) + return; + + pi->pending = *desc; +} + +result_t wuss_proginfo_handle_pre_show(wuss_proginfo_t *pi) +{ + return wuss_dialogue_handle_pre_show(pi->dialogue); +} + +wuss_window_t *wuss_proginfo_window(const wuss_proginfo_t *pi) +{ + return pi ? wuss_dialogue_window(pi->dialogue) : NULL; } diff --git a/libraries/wuss/core/create.c b/libraries/wuss/core/create.c index b75fd87..7fd54a0 100644 --- a/libraries/wuss/core/create.c +++ b/libraries/wuss/core/create.c @@ -61,18 +61,18 @@ result_t wuss_create(screen_t *scr, int npalette, const wuss_config_t *config, const wuss_alloc_t *alloc, + const char *resources, wuss_t **wuss) { - bmfont_t *font; - wuss_alloc_t al; - wuss_t *w; + wuss_alloc_t al; + wuss_t *w; #ifdef WUSS_FURNITURE wuss_furniture_palette_t pal; - wuss_colour_t bg, fg; + wuss_colour_t bg, fg; #endif #if defined(WUSS_FURNITURE) || defined(WUSS_ICONS) - wuss_colour_t blight, bdark, bdivider; - wuss_colour_t btnbg, btnfg, btnpressed, accent; + wuss_colour_t blight, bdark, bdivider; + wuss_colour_t btnbg, btnfg, btnpressed, accent; #endif #ifdef WUSS_FURNITURE int font_height; @@ -84,8 +84,6 @@ result_t wuss_create(screen_t *scr, if (nfonts < 0 || nfonts > wuss_MAX_FONTS || (nfonts > 0 && fonts == NULL)) return result_BAD_ARG; - font = (nfonts > 0) ? fonts[0].font : NULL; - al = (alloc != NULL) ? *alloc : wuss_alloc; w = al.malloc(sizeof(*w)); @@ -93,6 +91,8 @@ result_t wuss_create(screen_t *scr, return result_OOM; w->alloc = al; + wuss__fontset_init(&w->fonts, fonts, nfonts); + if (palette == NULL) { palette = wuss__default_palette; @@ -229,23 +229,15 @@ result_t wuss_create(screen_t *scr, { w->titlebar_height = config->titlebar_height; } - else if (font != NULL) - { - /* titles draw in the bold weight when one was supplied; size the - * titlebar to whichever weight is taller */ - bmfont_get_info(font, NULL, &font_height); - if (nfonts > 1 && fonts[1].font != NULL) - { - int titleh; - - bmfont_get_info(fonts[1].font, NULL, &titleh); - font_height = MAX(font_height, titleh); - } - w->titlebar_height = font_height + 4; - } else { - w->titlebar_height = WUSS_DEFAULT_TITLEBAR_HEIGHT; + /* titles draw in the bold weight when one was supplied; size the + * titlebar to whichever weight is taller. Both slots empty -> fall back + * to the default. */ + font_height = MAX(wuss__fontset_height(&w->fonts, 0), + wuss__fontset_height(&w->fonts, 1)); + w->titlebar_height = (font_height > 0) ? font_height + 4 + : WUSS_DEFAULT_TITLEBAR_HEIGHT; } #else /* !WUSS_FURNITURE */ #ifdef WUSS_ICONS @@ -299,28 +291,49 @@ result_t wuss_create(screen_t *scr, #endif #endif /* WUSS_FURNITURE */ - /* Second pass: the chrome colours are stored and concrete now, so fill in - * the chrome-role symbolic slots (wuss_COLOUR_TITLE_BG etc.). */ - wuss__rebuild_palettecache(w); - - w->scr = scr; +#ifdef WUSS_ICONS { - int i; + wuss_colour_t track, value, surround; - for (i = 0; i < nfonts; i++) + if (config != NULL) { - w->fonts[i] = fonts[i].font; - w->font_classes[i] = fonts[i].font_class; - w->font_names[i] = fonts[i].name; + track = (config->slider.track == wuss_NO_BACKGROUND) + ? w->window_bg + : wuss__resolve_colour(w, config->slider.track); + value = (config->slider.value == wuss_NO_BACKGROUND) + ? w->button_bg + : wuss__resolve_colour(w, config->slider.value); + surround = (config->slider.surround == wuss_NO_BACKGROUND) + ? w->button_bg + : wuss__resolve_colour(w, config->slider.surround); } - for (; i < wuss_MAX_FONTS; i++) + else { - w->fonts[i] = NULL; - w->font_classes[i] = wuss_FONT_CLASS_NONE; - w->font_names[i] = NULL; + track = w->window_bg; + value = w->button_bg; + surround = w->button_bg; } - w->nfonts = nfonts; + + if (track >= w->npalette || value >= w->npalette || surround >= w->npalette) + { + wuss__free(w, w->palette); + wuss__free(w, w); + return result_WUSS_BAD_COLOUR; + } + + w->slider_track = track; + w->slider_value = value; + w->slider_surround = surround; } +#endif + + /* Second pass: the chrome colours are stored and concrete now, so fill in + * the chrome-role symbolic slots (wuss_COLOUR_TITLE_BG etc.). */ + wuss__rebuild_palettecache(w); + + w->scr = scr; + w->resources = resources; + w->pointer_window = NULL; #ifdef WUSS_FURNITURE w->furniture.dragging = NULL; w->furniture.drag.x = 0; @@ -329,7 +342,9 @@ result_t wuss_create(screen_t *scr, #endif #ifdef WUSS_ICONS w->pressed_icon = NULL; + w->pressed_window = NULL; w->hover_icon = NULL; + w->hover_window = NULL; w->icon.names = NULL; w->icon.atoms = NULL; w->icon.bitmaps = NULL; @@ -341,7 +356,8 @@ result_t wuss_create(screen_t *scr, w->menu_eat_up = 0; #endif - w->ndirty = 0; + w->ndirty = 0; + w->ntouched = 0; w->layout = NULL; w->cascade.x = 0; diff --git a/libraries/wuss/core/get-font.c b/libraries/wuss/core/get-font.c deleted file mode 100644 index 8e2f75a..0000000 --- a/libraries/wuss/core/get-font.c +++ /dev/null @@ -1,42 +0,0 @@ -/* wuss/core/get-font.c -- look up a registered font slot by index */ - -#include - -#include "impl.h" - -bmfont_t *wuss_get_font(const wuss_t *wuss) -{ - assert(wuss != NULL); - - return wuss->fonts[0]; -} - -bmfont_t *wuss_get_font_n(const wuss_t *wuss, int index) -{ - assert(wuss != NULL); - - if (index < 0 || index >= wuss_MAX_FONTS) - return NULL; - - return wuss->fonts[index]; -} - -wuss_font_class_t wuss_get_font_class_n(const wuss_t *wuss, int index) -{ - assert(wuss != NULL); - - if (index < 0 || index >= wuss_MAX_FONTS) - return wuss_FONT_CLASS_NONE; - - return wuss->font_classes[index]; -} - -const char *wuss_get_font_name_n(const wuss_t *wuss, int index) -{ - assert(wuss != NULL); - - if (index < 0 || index >= wuss_MAX_FONTS) - return NULL; - - return wuss->font_names[index]; -} diff --git a/libraries/wuss/core/get-resources.c b/libraries/wuss/core/get-resources.c new file mode 100644 index 0000000..3d91bc5 --- /dev/null +++ b/libraries/wuss/core/get-resources.c @@ -0,0 +1,12 @@ +/* wuss/get-resources.c -- wuss - minimal window manager */ + +#include + +#include "impl.h" + +const char *wuss_get_resources(const wuss_t *wuss) +{ + assert(wuss != NULL); + + return wuss->resources; +} diff --git a/libraries/wuss/core/impl.h b/libraries/wuss/core/impl.h index 30ebc4e..9dc3731 100644 --- a/libraries/wuss/core/impl.h +++ b/libraries/wuss/core/impl.h @@ -21,6 +21,8 @@ #include "wuss/window.h" #include "wuss/task.h" +#include "../font/font.h" + #ifdef WUSS_FURNITURE #include "../furniture.h" #endif @@ -49,6 +51,8 @@ #define WUSS_FRAME_CAPTION_INSET 8 /* x offset of a wuss_ICON_TYPE_FRAME caption from the frame's left edge */ #define WUSS_FRAME_CAPTION_PAD 2 /* gap left in the frame's top edge either side of the caption */ +#define WUSS_SLIDER_GAP 4 /* fixed inset on all four sides of a wuss_ICON_TYPE_SLIDER bbox, between the surround and the inner (clickable) rect */ + /* Font slot (see wuss_create's fonts[]) consulted for menu decoration glyphs: * the selection tick and the submenu arrow. When the slot is empty those are * drawn as vector strokes instead. A font in this slot is expected to carry a @@ -101,14 +105,11 @@ struct wuss_task struct wuss { screen_t *scr; - bmfont_t *fonts[wuss_MAX_FONTS]; /* slot 0 is the system - * font; unset slots NULL; not owned */ - wuss_font_class_t font_classes[wuss_MAX_FONTS]; /* parallel to - * fonts[]; NONE for an unset slot */ - const char *font_names[wuss_MAX_FONTS]; /* parallel to - * fonts[]; borrowed; NULL if unset - * or given no name */ - int nfonts; /* entries filled from wuss_create */ + const char *resources; /* borrowed root path from wuss_create, + * for wuss_get_resources; caller + * convention only, unused internally */ + struct wuss_fontset fonts; /* font slots from wuss_create; slot 0 + * is the system font. See font/font.h */ wuss_alloc_t alloc; /* malloc/realloc/free hooks, copied in * by wuss_create; used for every heap * block this wuss_t owns */ @@ -130,6 +131,11 @@ struct wuss wuss_colour_t button_fg; /* button label */ wuss_colour_t button_pressed; /* button face while held */ wuss_colour_t accent; /* default action button fill */ +#endif +#ifdef WUSS_ICONS + wuss_colour_t slider_track; /* slider groove ground */ + wuss_colour_t slider_value; /* slider fill */ + wuss_colour_t slider_surround; /* slider bbox and gap */ #endif wuss_colour_t window_bg; /* work-area body fill; wuss_COLOUR_WINDOW */ wuss_colour_t menu_bg; /* menu body fill; wuss_COLOUR_MENU */ @@ -148,6 +154,8 @@ struct wuss #endif box_t dirty[WUSS_MAX_DIRTY]; /* accumulated by wuss_invalidate; reset by a redraw */ int ndirty; + box_t touched[WUSS_MAX_DIRTY]; /* accumulated by wuss__touch; reset by wuss_clear_touched */ + int ntouched; packer_t *layout; /* owned; occupied screen area for * wuss_window_create_placed, lazily * created on first auto-placement */ @@ -155,15 +163,24 @@ struct wuss * layout packer has no room left */ point_t pointer; /* last pointer position, screen * space, from any mouse click/move */ + wuss_window_t *pointer_window; /* window whose on-screen + * footprint the pointer was last + * inside (content or furniture), + * NULL if none; drives + * wuss_EVENT_POINTER_ENTER/EXIT */ #ifdef WUSS_ICONS wuss_icon_t *pressed_icon; /* button icon held down, NULL when * idle; released on any MOUSE_UP * even if a new window now covers * its owner */ + wuss_window_t *pressed_window; /* the window pressed_icon is on; + * NULL iff pressed_icon is NULL */ wuss_icon_t *hover_icon; /* icon the pointer is currently * over, NULL when none; drives * hover-highlight repaint of * menu-entry icons */ + wuss_window_t *hover_window; /* the window hover_icon is on; + * NULL iff hover_icon is NULL */ /* Icon set loaded by wuss_icons_load. index i is the i-th ".png" the * directory scan yielded (order unspecified -- address by name). * names interns the leafnames-sans-".png"; atoms[i] is entry i's atom @@ -295,25 +312,6 @@ static inline void wuss__chrome_invalidate_layout(wuss_window_t *window) wuss_window_t *wuss__window_at(wuss_t *wuss, point_t p); -/* Centralised text rendering. Every wuss text draw goes through - * wuss__text_draw so an optical vertical bias (WUSS_TEXT_BASELINE_ADJUST, - * default 1px down) is applied uniformly; wuss__text_measure is a plain - * pass-through kept alongside for a single point of policy. */ -result_t wuss__text_measure(bmfont_t *font, - const char *text, - int len, - bmfont_width_t target_width, - int *split_point, - bmfont_width_t *actual_width); -result_t wuss__text_draw(bmfont_t *font, - screen_t *scr, - const char *text, - int len, - colour_t fg, - colour_t bg, - const point_t *pos, - point_t *end_pos); - /* Rebuild wuss->palettecache (white, black and the symbolic[] table) from * the current palette and the stored chrome colours. Call after the palette * or any chrome colour changes; the chrome fields must already be concrete @@ -440,6 +438,7 @@ void wuss__invalidate_minus(wuss_t *wuss, const box_t *whole, const box_t *keep); void wuss__invalidate_uncovered(wuss_window_t *window); +void wuss__touch(wuss_t *wuss, const box_t *box); /* Clip "box" (screen space) down to the parts not already covered by * windows above "window" in the z-order, writing the surviving pieces to @@ -456,6 +455,17 @@ int wuss__subtract_boxes(const box_t *whole, int ncuts, box_t *out); +/* Filter "clean" (nclean pieces already clipped clear of occluders) down to + * the parts not also covered by "stale" (nstale pending-dirty boxes not yet + * repainted), writing survivors back into "clean" and returning the new + * count. Each piece is subtracted independently, so the result can hold more + * than "nclean" entries; excess beyond WUSS_MAX_INVALIDATE_PIECES is + * dropped. */ +int wuss__filter_settled(box_t *clean, + int nclean, + const box_t *stale, + int nstale); + /* Given "n" single-rect blits, each moving "clean[i]" to "dest[i]", find an * order in which no blit's destination overwrites a still-unread source of a * later blit. Writes the piece indices to "order" (capacity @@ -531,6 +541,45 @@ static inline void wuss__notify_open(wuss_window_t *window) (void) wuss__deliver(window->task, window, &event); } +/* Move the "pointer is inside this window's footprint" tracker to "now" + * (which may be NULL). If it changed, deliver wuss_EVENT_POINTER_EXIT to the + * window it left and wuss_EVENT_POINTER_ENTER to the one it reached, in that + * order. Called from every pointer-position update. Fires only on the + * window-crossing edge; content<->furniture moves within one window keep the + * same tracker and emit nothing. */ +static inline void wuss__pointer_set_window(wuss_t *wuss, wuss_window_t *now) +{ + wuss_window_t *was; + wuss_event_t event; + + was = wuss->pointer_window; + if (now == was) + return; + + wuss->pointer_window = now; + + if (was != NULL) + { + event.kind = wuss_EVENT_POINTER_EXIT; + (void) wuss__deliver(was->task, was, &event); + } + if (now != NULL) + { + event.kind = wuss_EVENT_POINTER_ENTER; + (void) wuss__deliver(now->task, now, &event); + } +} + +/* Drop "window" from the pointer tracker without delivering EXIT -- for the + * window teardown path, where wuss_EVENT_CLOSE already tells the task and the + * window is about to be freed. No-op unless the pointer was inside it. */ +static inline void wuss__pointer_forget_window(wuss_t *wuss, + wuss_window_t *window) +{ + if (wuss->pointer_window == window) + wuss->pointer_window = NULL; +} + static inline int wuss__size_ok(int width, int height) { return width > 0 && height > 0; @@ -598,7 +647,7 @@ static inline int wuss__outline_px(const wuss_window_t *window) } /* ponytail: falls back to wuss's own titlebar height when the window has - * none, so NO_TITLEBAR windows that still opt into scrollbars/resize match + * none, so titleless windows that still opt into scrollbars/resize match * their titled siblings instead of a hardcoded size; the hardcoded default * is only a last-resort floor if even that isn't positive */ static inline int wuss__button_size_for(const wuss_t *wuss, @@ -629,12 +678,12 @@ static inline void wuss__furniture_carve_for(wuss_window_flags_t flags, int button_size, point_t *carve) { - carve->x = (flags & wuss_WINDOW_NO_VSCROLL) ? 0 : button_size; - carve->y = (flags & wuss_WINDOW_NO_HSCROLL) ? 0 : button_size; + carve->x = (flags & wuss_WINDOW_VSCROLL) ? button_size : 0; + carve->y = (flags & wuss_WINDOW_HSCROLL) ? button_size : 0; - if (!(flags & wuss_WINDOW_NO_RESIZE) && - (flags & wuss_WINDOW_NO_VSCROLL) && - (flags & wuss_WINDOW_NO_HSCROLL)) + if ((flags & wuss_WINDOW_RESIZE) && + !(flags & wuss_WINDOW_VSCROLL) && + !(flags & wuss_WINDOW_HSCROLL)) { carve->x = button_size; carve->y = button_size; @@ -748,4 +797,38 @@ static inline void wuss__max_content_anywhere_on_screen(const wuss_window_t *win max->h = MAX(max->h, WUSS_MIN_CONTENT); } +/* Nudge window->visible back onto the screen by the minimum needed to bring + * its top-left (titlebar/close-icon) edge into view: shift right/down if it + * is off the top or left, shift left/up if it hangs off the right or bottom, + * but never so far that the top-left goes off the opposite edge -- a window + * bigger than the screen keeps its top-left on and overhangs bottom-right. + * Only the position moves, not the size. Shared by wuss_window_create and the + * menu code's borrowed-window opener so no menu can place a window off-screen. + */ +static inline void wuss__nudge_visible_onscreen(wuss_window_t *window) +{ + int scr_width, scr_height, dx, dy; + + scr_width = window->wuss->scr->size.w; + scr_height = window->wuss->scr->size.h; + + dx = 0; + if (window->visible.x0 < 0) + dx = -window->visible.x0; + else if (window->visible.x1 > scr_width) + dx = scr_width - window->visible.x1; + if (window->visible.x0 + dx < 0) + dx = -window->visible.x0; + + dy = 0; + if (window->visible.y0 < 0) + dy = -window->visible.y0; + else if (window->visible.y1 > scr_height) + dy = scr_height - window->visible.y1; + if (window->visible.y0 + dy < 0) + dy = -window->visible.y0; + + box_translated(&window->visible, dx, dy, &window->visible); +} + #endif /* IMPL_H */ diff --git a/libraries/wuss/core/invalidate.c b/libraries/wuss/core/invalidate.c index d1a8535..c8f3f06 100644 --- a/libraries/wuss/core/invalidate.c +++ b/libraries/wuss/core/invalidate.c @@ -28,38 +28,40 @@ static int box_merge(box_t *a, const box_t *b) return 0; } -result_t wuss_invalidate(wuss_t *wuss, const box_t *box) +/* Shared by wuss_invalidate (wuss->dirty) and wuss__touch (wuss->touched): + * repeatedly fold any existing entry that "cur" now covers, or that shares a + * complete edge with it, into "cur" itself; growing "cur" can bring further + * entries into range, so this settles to a fixed point before "cur" is + * (re)inserted. Bounded by "max", so an O(n^2) settle is cheap. Returns 1 if + * "box" was already fully covered (nothing to do). */ +static int mark_region(box_t *arr, + int *pn, + int max, + const char *what, + const box_t *box) { box_t cur; int changed; - assert(wuss != NULL); - assert(box != NULL); - if (box_is_empty(box)) - return result_OK; + return 1; cur = *box; - /* Repeatedly fold any existing entry that "cur" now covers, or that - * shares a complete edge with it, into "cur" itself; growing "cur" can - * bring further entries into range, so this settles to a fixed point - * before "cur" is (re)inserted. Bounded by WUSS_MAX_DIRTY, so an O(n^2) - * settle is cheap. */ do { int i; changed = 0; - for (i = 0; i < wuss->ndirty; i++) + for (i = 0; i < *pn; i++) { - if (box_contains_box(&cur, &wuss->dirty[i])) - return result_OK; /* already covered */ + if (box_contains_box(&cur, &arr[i])) + return 1; /* already covered */ - if (box_contains_box(&wuss->dirty[i], &cur) || box_merge(&cur, &wuss->dirty[i])) + if (box_contains_box(&arr[i], &cur) || box_merge(&cur, &arr[i])) { - wuss->dirty[i] = wuss->dirty[--wuss->ndirty]; /* absorbed into cur */ + arr[i] = arr[--(*pn)]; /* absorbed into cur */ changed = 1; break; } @@ -67,23 +69,47 @@ result_t wuss_invalidate(wuss_t *wuss, const box_t *box) } while (changed); - if (wuss->ndirty < WUSS_MAX_DIRTY) + if (*pn < max) { - wuss->dirty[wuss->ndirty++] = cur; + arr[(*pn)++] = cur; } else { /* ponytail: array full, fold into the last entry rather than growing * storage; over-approximates that entry's area but stays correct */ - logf_info("wuss_invalidate: %d dirty regions, coalescing " + logf_info("wuss_invalidate: %d %s regions, coalescing " "(%d,%d)-(%d,%d) into the last entry", - WUSS_MAX_DIRTY, cur.x0, cur.y0, cur.x1, cur.y1); - box_union(&wuss->dirty[WUSS_MAX_DIRTY - 1], &cur, &wuss->dirty[WUSS_MAX_DIRTY - 1]); + max, what, cur.x0, cur.y0, cur.x1, cur.y1); + box_union(&arr[max - 1], &cur, &arr[max - 1]); } + return 0; +} + +result_t wuss_invalidate(wuss_t *wuss, const box_t *box) +{ + assert(wuss != NULL); + assert(box != NULL); + + mark_region(wuss->dirty, &wuss->ndirty, WUSS_MAX_DIRTY, "dirty", box); + return result_OK; } +/* Mark a screen-space region as having changed pixels without needing a + * repaint -- e.g. wuss__blit_pieces sliding a window's own content to a new + * position: the backing bitmap is already correct there, but a frontend that + * only re-uploads what wuss_get_touched_extent reports still needs to know + * the pixels moved. Never folded into wuss->dirty, so wuss_redraw_dirty + * never repaints it. */ +void wuss__touch(wuss_t *wuss, const box_t *box) +{ + assert(wuss != NULL); + assert(box != NULL); + + mark_region(wuss->touched, &wuss->ntouched, WUSS_MAX_DIRTY, "touched", box); +} + int wuss_get_dirty_count(const wuss_t *wuss) { assert(wuss != NULL); @@ -99,3 +125,30 @@ void wuss_get_dirty(const wuss_t *wuss, int index, box_t *out) *out = wuss->dirty[index]; } + +int wuss_get_touched_extent(const wuss_t *wuss, box_t *out) +{ + box_t u; + int i; + + assert(wuss != NULL); + assert(out != NULL); + + if (wuss->ntouched == 0) + return 0; + + u = wuss->touched[0]; + for (i = 1; i < wuss->ntouched; i++) + box_union(&u, &wuss->touched[i], &u); + + *out = u; + + return 1; +} + +void wuss_clear_touched(wuss_t *wuss) +{ + assert(wuss != NULL); + + wuss->ntouched = 0; +} diff --git a/libraries/wuss/core/mouse-click.c b/libraries/wuss/core/mouse-click.c index a02ebdb..e04dced 100644 --- a/libraries/wuss/core/mouse-click.c +++ b/libraries/wuss/core/mouse-click.c @@ -8,9 +8,9 @@ result_t wuss_mouse_click(wuss_t *wuss, wuss_mouse_action_t action, wuss_window_t **hit) { - wuss_window_t *win; - wuss_event_t event; - int x, y; + wuss_window_t *win; + wuss_event_t event; + int x, y; x = p.x; y = p.y; @@ -26,7 +26,7 @@ result_t wuss_mouse_click(wuss_t *wuss, if (action == wuss_MOUSE_UP && wuss->pressed_icon != NULL) { wuss_icon_t *pressed = wuss->pressed_icon; - wuss_window_t *presswin = pressed->window; + wuss_window_t *presswin = wuss->pressed_window; box_t content; point_t doc_point; int still_over; @@ -40,11 +40,12 @@ result_t wuss_mouse_click(wuss_t *wuss, if (!still_over) { - wuss->pressed_icon = NULL; + wuss->pressed_icon = NULL; + wuss->pressed_window = NULL; if (wuss__icon_pressed(pressed)) { wuss__icon_set_state(pressed, wuss_ICON_STATE_PRESSED, 0); - wuss__icon_invalidate(pressed); + wuss__icon_invalidate(presswin, pressed); } } } @@ -67,6 +68,10 @@ result_t wuss_mouse_click(wuss_t *wuss, if (hit != NULL) *hit = win; + /* A click can land the pointer on a window without a preceding move + * (window opened under it, or a synthetic tap); keep enter/exit balanced. */ + wuss__pointer_set_window(wuss, win); + #ifdef WUSS_MENUS /* The MOUSE_UP that follows the MENU press which opened the chain is spent: * without this it would land on the new menu's row 0 and pick it. */ @@ -191,6 +196,7 @@ result_t wuss_mouse_click(wuss_t *wuss, (button & (wuss_BUTTON_SELECT | wuss_BUTTON_ADJUST))) { point_t scroll; + int paged = 0; /* Only resize raises the window; dragging a scrollbar well must not * reorder the stack. */ @@ -199,16 +205,19 @@ result_t wuss_mouse_click(wuss_t *wuss, /* A click landing in the well itself (not on the sausage) pages the * content one visible extent towards the click, RISC OS style, keeping - * one step of overlap. The drag state is still armed below so a press - * that then moves onto the sausage keeps working. */ + * one step of overlap. ADJUST reverses that -- it pages away from the + * click instead, matching RISC OS's SELECT/ADJUST mirroring elsewhere + * (e.g. the arrow icons). A direct hit on the sausage itself pages + * nothing and falls through to arm the drag below. */ if (region == wuss_FURNITURE_VSCROLL_WELL || region == wuss_FURNITURE_HSCROLL_WELL) { box_t sausage; box_t content; - int page; + int sign, page; wuss__content_box(win, &content); + sign = (button & wuss_BUTTON_ADJUST) ? -1 : 1; if (region == wuss_FURNITURE_VSCROLL_WELL) { @@ -216,9 +225,15 @@ result_t wuss_mouse_click(wuss_t *wuss, page = (content.y1 - content.y0) - WUSS_SCROLL_STEP; page = MAX(page, 1); if (y < sausage.y0) - wuss__scroll_step(win, POINT(0, -page)); + { + wuss__scroll_step(win, POINT(0, -page * sign)); + paged = 1; + } else if (y > sausage.y1) - wuss__scroll_step(win, POINT(0, page)); + { + wuss__scroll_step(win, POINT(0, page * sign)); + paged = 1; + } } else { @@ -226,30 +241,43 @@ result_t wuss_mouse_click(wuss_t *wuss, page = (content.x1 - content.x0) - WUSS_SCROLL_STEP; page = MAX(page, 1); if (x < sausage.x0) - wuss__scroll_step(win, POINT(-page, 0)); + { + wuss__scroll_step(win, POINT(-page * sign, 0)); + paged = 1; + } else if (x > sausage.x1) - wuss__scroll_step(win, POINT(page, 0)); + { + wuss__scroll_step(win, POINT(page * sign, 0)); + paged = 1; + } } } - wuss_window_get_scroll(win, &scroll); - - wuss->furniture.dragging = win; - wuss->furniture.drag_kind = wuss__furniture_drag_kind(region); - wuss->furniture.drag.x = x; - wuss->furniture.drag.y = y; - wuss->furniture.drag_scroll_start = (region == wuss_FURNITURE_VSCROLL_WELL) ? scroll.y : scroll.x; - - /* Resize needs the pointer's offset from the content box's current - * bottom-right corner, so the point grabbed on the resize icon stays - * under the pointer as it moves, rather than that corner jumping to - * meet the pointer on the very first move. */ - if (region == wuss_FURNITURE_RESIZE) + /* A well click that paged must not also arm a sausage drag, or + * holding the button after the page would start scrolling live as + * though the pointer had grabbed the sausage itself. A direct hit on + * the sausage (paged == 0 for a well region) still arms normally. */ + if (!paged) { - box_t content; - wuss__content_box(win, &content); - wuss->furniture.drag_offset.x = x - content.x1; - wuss->furniture.drag_offset.y = y - content.y1; + wuss_window_get_scroll(win, &scroll); + + wuss->furniture.dragging = win; + wuss->furniture.drag_kind = wuss__furniture_drag_kind(region); + wuss->furniture.drag.x = x; + wuss->furniture.drag.y = y; + wuss->furniture.drag_scroll_start = (region == wuss_FURNITURE_VSCROLL_WELL) ? scroll.y : scroll.x; + + /* Resize needs the pointer's offset from the content box's current + * bottom-right corner, so the point grabbed on the resize icon stays + * under the pointer as it moves, rather than that corner jumping to + * meet the pointer on the very first move. */ + if (region == wuss_FURNITURE_RESIZE) + { + box_t content; + wuss__content_box(win, &content); + wuss->furniture.drag_offset.x = x - content.x1; + wuss->furniture.drag_offset.y = y - content.y1; + } } } return result_OK; @@ -259,8 +287,8 @@ result_t wuss_mouse_click(wuss_t *wuss, if (win->task->handle != NULL) { - box_t content; - point_t doc_point; + box_t content; + point_t doc_point; wuss__content_box(win, &content); doc_point.x = x - content.x0 + win->scroll.x; @@ -277,21 +305,31 @@ result_t wuss_mouse_click(wuss_t *wuss, (button & (wuss_BUTTON_SELECT | wuss_BUTTON_ADJUST))) { wuss__icon_set_state(icon, wuss_ICON_STATE_PRESSED, 1); - wuss->pressed_icon = icon; - wuss__icon_invalidate(icon); + wuss->pressed_icon = icon; + wuss->pressed_window = win; + wuss__icon_invalidate(win, icon); + + /* a slider jumps straight to the click point rather than waiting + * for a completed press/release, and keeps updating on MOVE while + * held (core/mouse-move.c) */ + if (icon->spec.type == wuss_ICON_TYPE_SLIDER) + wuss__icon_set_value(win, icon, + wuss__slider_value_for_point(win, icon, + POINT(x, y))); } else if (action == wuss_MOUSE_UP && wuss__icon_pressed(icon)) { wuss__icon_set_state(icon, wuss_ICON_STATE_PRESSED, 0); - wuss->pressed_icon = NULL; - wuss__icon_invalidate(icon); + wuss->pressed_icon = NULL; + wuss->pressed_window = NULL; + wuss__icon_invalidate(win, icon); /* a completed click latches radio/option state before the task is * told, so the wuss_EVENT_ICON handler sees the new value */ - if (icon->type == wuss_ICON_TYPE_OPTION) - wuss__icon_select(icon, !wuss__icon_selected(icon)); - else if (icon->type == wuss_ICON_TYPE_RADIO) - wuss__icon_select(icon, + if (icon->spec.type == wuss_ICON_TYPE_OPTION) + wuss__icon_select(win, icon, !wuss__icon_selected(icon)); + else if (icon->spec.type == wuss_ICON_TYPE_RADIO) + wuss__icon_select(win, icon, (button & wuss_BUTTON_ADJUST) ? !wuss__icon_selected(icon) : 1); } @@ -300,6 +338,7 @@ result_t wuss_mouse_click(wuss_t *wuss, event.data.icon.icon = icon; event.data.icon.action = action; event.data.icon.button = button; + event.data.icon.value = icon->value; return wuss__deliver(win->task, win, &event); } } diff --git a/libraries/wuss/core/mouse-move.c b/libraries/wuss/core/mouse-move.c index b0e5b24..70f7d68 100644 --- a/libraries/wuss/core/mouse-move.c +++ b/libraries/wuss/core/mouse-move.c @@ -52,10 +52,14 @@ result_t wuss_mouse_move(wuss_t *wuss, point_t p, wuss_window_t **hit) if (hit != NULL) *hit = win; + /* Whole-footprint enter/exit tracking, before any furniture/no-handler + * early return -- a furniture hover still counts as "inside the window". */ + wuss__pointer_set_window(wuss, win); + if (win == NULL) { #ifdef WUSS_ICONS - wuss__icon_set_hover(wuss, NULL); + wuss__icon_set_hover(wuss, NULL, NULL); #endif return result_OK; } @@ -64,7 +68,7 @@ result_t wuss_mouse_move(wuss_t *wuss, point_t p, wuss_window_t **hit) if (wuss->furniture_ops->hit_test(win, POINT(x, y)) != wuss_FURNITURE_CONTENT) { #ifdef WUSS_ICONS - wuss__icon_set_hover(wuss, NULL); + wuss__icon_set_hover(wuss, NULL, NULL); #endif return result_OK; } @@ -73,7 +77,7 @@ result_t wuss_mouse_move(wuss_t *wuss, point_t p, wuss_window_t **hit) if (win->task->handle == NULL) { #ifdef WUSS_ICONS - wuss__icon_set_hover(wuss, NULL); + wuss__icon_set_hover(wuss, NULL, NULL); #endif return result_OK; } @@ -87,6 +91,27 @@ result_t wuss_mouse_move(wuss_t *wuss, point_t p, wuss_window_t **hit) doc_point.x = x - content.x0 + win->scroll.x; doc_point.y = y - content.y0 + win->scroll.y; +#ifdef WUSS_ICONS + /* a slider drag keeps tracking the pointer even once it strays outside + * the icon's own bbox, matching a furniture sausage drag */ + if (wuss->pressed_icon != NULL && wuss->pressed_window == win && + wuss->pressed_icon->spec.type == wuss_ICON_TYPE_SLIDER) + { + wuss_icon_t *icon = wuss->pressed_icon; + + wuss__icon_set_value(win, icon, + wuss__slider_value_for_point(win, icon, + POINT(x, y))); + + event.kind = wuss_EVENT_ICON; + event.data.icon.icon = icon; + event.data.icon.action = wuss_MOUSE_MOVE; + event.data.icon.button = wuss_BUTTON_SELECT; + event.data.icon.value = icon->value; + return wuss__deliver(win->task, win, &event); + } +#endif + #ifdef WUSS_ICONS { wuss_icon_t *icon; @@ -94,7 +119,7 @@ result_t wuss_mouse_move(wuss_t *wuss, point_t p, wuss_window_t **hit) icon = wuss__icon_hit_test(win, doc_point); - wuss__icon_set_hover(wuss, icon); + wuss__icon_set_hover(wuss, win, icon); /* Clear the pressed state of any button the pointer has left. This does * not re-press a button on drag-back-in, and does not track which mouse @@ -108,17 +133,26 @@ result_t wuss_mouse_move(wuss_t *wuss, point_t p, wuss_window_t **hit) { wuss__icon_set_state(it, wuss_ICON_STATE_PRESSED, 0); if (wuss->pressed_icon == it) - wuss->pressed_icon = NULL; - wuss__icon_invalidate(it); + { + wuss->pressed_icon = NULL; + wuss->pressed_window = NULL; + } + wuss__icon_invalidate(win, it); } } - if (icon != NULL) + /* a slider only wants MOVE while its own drag is tracked above; without + * this it would also get one on every plain hover, with no button + * actually held. Other icon types (menu rows, buttons) rely on this + * hover MOVE to light up/open on mouse-over, so keep it for them. */ + if (icon != NULL && + (icon->spec.type != wuss_ICON_TYPE_SLIDER || icon == wuss->pressed_icon)) { event.kind = wuss_EVENT_ICON; event.data.icon.icon = icon; event.data.icon.action = wuss_MOUSE_MOVE; event.data.icon.button = wuss_BUTTON_SELECT; + event.data.icon.value = icon->value; return wuss__deliver(win->task, win, &event); } } diff --git a/libraries/wuss/core/rebuild-palettecache.c b/libraries/wuss/core/rebuild-palettecache.c index 7b904b4..3ddffb7 100644 --- a/libraries/wuss/core/rebuild-palettecache.c +++ b/libraries/wuss/core/rebuild-palettecache.c @@ -55,6 +55,11 @@ void wuss__rebuild_palettecache(wuss_t *wuss) cache[wuss_COLOUR_BUTTON_FG - wuss_COLOUR_SYMBOLIC] = wuss->button_fg; cache[wuss_COLOUR_BUTTON_PRESSED - wuss_COLOUR_SYMBOLIC] = wuss->button_pressed; cache[wuss_COLOUR_ACCENT - wuss_COLOUR_SYMBOLIC] = wuss->accent; +#endif +#ifdef WUSS_ICONS + cache[wuss_COLOUR_SLIDER_TRACK - wuss_COLOUR_SYMBOLIC] = wuss->slider_track; + cache[wuss_COLOUR_SLIDER_VALUE - wuss_COLOUR_SYMBOLIC] = wuss->slider_value; + cache[wuss_COLOUR_SLIDER_SURROUND - wuss_COLOUR_SYMBOLIC] = wuss->slider_surround; #endif if (wuss->backdrop.colour != wuss_NO_BACKGROUND) cache[wuss_COLOUR_BACKDROP - wuss_COLOUR_SYMBOLIC] = wuss->backdrop.colour; diff --git a/libraries/wuss/core/redraw.c b/libraries/wuss/core/redraw.c index d06481a..71dd784 100644 --- a/libraries/wuss/core/redraw.c +++ b/libraries/wuss/core/redraw.c @@ -112,7 +112,7 @@ static void redraw_window(wuss_t *wuss, /* draw in array order so later-created icons paint on top, matching * wuss__icon_hit_test's reverse scan */ for (k = 0; k < win->nicons; k++) - wuss__icon_draw(wuss, win->icons[k], &content, win->scroll); + wuss__icon_draw(wuss, win, win->icons[k], &content, win->scroll); } #endif } diff --git a/libraries/wuss/core/scroll-step.c b/libraries/wuss/core/scroll-step.c index 23b653b..146d338 100644 --- a/libraries/wuss/core/scroll-step.c +++ b/libraries/wuss/core/scroll-step.c @@ -27,9 +27,9 @@ void wuss__scroll_step(wuss_window_t *window, point_t delta) /* A window that declared an axis non-scrollable (e.g. a pop-up menu) has no * scrollbar to clamp against and its geometry assumes scroll == 0, so a wheel * turn over it must not move the content -- doing so corrupts the display. */ - if (window->flags & wuss_WINDOW_NO_HSCROLL) + if (!(window->flags & wuss_WINDOW_HSCROLL)) delta.x = 0; - if (window->flags & wuss_WINDOW_NO_VSCROLL) + if (!(window->flags & wuss_WINDOW_VSCROLL)) delta.y = 0; if (delta.x == 0 && delta.y == 0) return; diff --git a/libraries/wuss/core/scroll.c b/libraries/wuss/core/scroll.c index 45dff36..b4f3c11 100644 --- a/libraries/wuss/core/scroll.c +++ b/libraries/wuss/core/scroll.c @@ -18,7 +18,7 @@ static void wuss__scroll_rehover(wuss_t *wuss, doc_point.x = screen_point.x - content.x0 + win->scroll.x; doc_point.y = screen_point.y - content.y0 + win->scroll.y; - wuss__icon_set_hover(wuss, wuss__icon_hit_test(win, doc_point)); + wuss__icon_set_hover(wuss, win, wuss__icon_hit_test(win, doc_point)); #else (void) wuss; (void) win; diff --git a/libraries/wuss/core/set-backdrop.c b/libraries/wuss/core/set-backdrop.c index 48a9f14..c7fccdf 100644 --- a/libraries/wuss/core/set-backdrop.c +++ b/libraries/wuss/core/set-backdrop.c @@ -8,8 +8,8 @@ result_t wuss_set_backdrop(wuss_t *wuss, const wuss_backdrop_t *backdrop) { - result_t rc; - box_t screen; + result_t rc; + box_t screen; wuss_backdrop_t resolved; assert(wuss != NULL); diff --git a/libraries/wuss/core/task/deliver.c b/libraries/wuss/core/task/deliver.c index 02d8062..b610a1c 100644 --- a/libraries/wuss/core/task/deliver.c +++ b/libraries/wuss/core/task/deliver.c @@ -23,6 +23,8 @@ static int wuss__kind_ok_for(wuss_event_kind_t kind, int have_window) case wuss_EVENT_SHOW: case wuss_EVENT_PRE_CLOSE: case wuss_EVENT_CLOSE: + case wuss_EVENT_POINTER_ENTER: + case wuss_EVENT_POINTER_EXIT: return have_window; case wuss_EVENT_IDLE: diff --git a/libraries/wuss/core/text.c b/libraries/wuss/core/text.c deleted file mode 100644 index 6d04abe..0000000 --- a/libraries/wuss/core/text.c +++ /dev/null @@ -1,43 +0,0 @@ -/* 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/close.c b/libraries/wuss/core/window/close.c index 5563468..8e7361d 100644 --- a/libraries/wuss/core/window/close.c +++ b/libraries/wuss/core/window/close.c @@ -10,9 +10,9 @@ void wuss_window_close(wuss_window_t *doomed) { - wuss_task_t *task; - wuss_event_t event; - wuss_t *wuss; + wuss_task_t *task; + wuss_event_t event; + wuss_t *wuss; if (doomed == NULL) return; @@ -24,11 +24,20 @@ void wuss_window_close(wuss_window_t *doomed) wuss->furniture.dragging = NULL; #endif #ifdef WUSS_ICONS - if (wuss->pressed_icon != NULL && wuss->pressed_icon->window == doomed) - wuss->pressed_icon = NULL; - if (wuss->hover_icon != NULL && wuss->hover_icon->window == doomed) - wuss->hover_icon = NULL; + if (wuss->pressed_window == doomed) + { + wuss->pressed_icon = NULL; + wuss->pressed_window = NULL; + } + if (wuss->hover_window == doomed) + { + wuss->hover_icon = NULL; + wuss->hover_window = NULL; + } #endif + /* No wuss_EVENT_POINTER_EXIT: the task hears about this window going away + * through PRE_CLOSE/CLOSE, and the struct is freed below. */ + wuss__pointer_forget_window(wuss, doomed); wuss__release_packed(doomed); diff --git a/libraries/wuss/core/window/create-placed.c b/libraries/wuss/core/window/create-placed.c index 7c1b739..da918e9 100644 --- a/libraries/wuss/core/window/create-placed.c +++ b/libraries/wuss/core/window/create-placed.c @@ -83,14 +83,14 @@ result_t wuss_window_create_placed(wuss_task_t *task, size2d_t min_doc, wuss_window_t **window) { - result_t rc; - wuss_t *wuss; - int left, top, right, bottom; - int fw, fh; - box_t screen, content, consumed; - point_t origin; - const box_t *slot; - int tracked; + result_t rc; + wuss_t *wuss; + int left, top, right, bottom; + int fw, fh; + box_t screen, content, consumed; + point_t origin; + const box_t *slot; + int tracked; assert(task != NULL); assert(window != NULL); diff --git a/libraries/wuss/core/window/create.c b/libraries/wuss/core/window/create.c index 941807f..5b5d9b9 100644 --- a/libraries/wuss/core/window/create.c +++ b/libraries/wuss/core/window/create.c @@ -21,9 +21,8 @@ result_t wuss_window_create(wuss_task_t *task, { wuss_t *wuss; wuss_window_t *win; - int width, height, outline_px, titlebar_height; - int scr_width, scr_height, dx, dy; - point_t carve; + int width, height, outline_px, titlebar_height; + point_t carve; assert(task != NULL); assert(content != NULL); @@ -50,31 +49,13 @@ result_t wuss_window_create(wuss_task_t *task, win->visible.x1 = content->x1 + outline_px + carve.x; win->visible.y1 = content->y1 + outline_px + carve.y; + win->flags = flags; + /* nudge back on-screen so the titlebar/close icon stay reachable; a * window bigger than the screen keeps its top-left (titlebar) edge - * on-screen rather than being centred or left alone */ - scr_width = wuss->scr->size.w; - scr_height = wuss->scr->size.h; - - dx = 0; - if (win->visible.x0 < 0) - dx = -win->visible.x0; - else if (win->visible.x1 > scr_width) - dx = scr_width - win->visible.x1; - if (win->visible.x0 + dx < 0) - dx = -win->visible.x0; - - dy = 0; - if (win->visible.y0 < 0) - dy = -win->visible.y0; - else if (win->visible.y1 > scr_height) - dy = scr_height - win->visible.y1; - if (win->visible.y0 + dy < 0) - dy = -win->visible.y0; - - box_translated(&win->visible, dx, dy, &win->visible); - - win->flags = flags; + * on-screen rather than being centred or left alone. Needs win->flags + * set (the helper reads the carve off it). */ + wuss__nudge_visible_onscreen(win); /* clamp so the window can never be born larger than the screen; done * after the on-screen nudge above so it measures from the final diff --git a/libraries/wuss/core/window/invalidate.c b/libraries/wuss/core/window/invalidate.c index d6b7032..263505d 100644 --- a/libraries/wuss/core/window/invalidate.c +++ b/libraries/wuss/core/window/invalidate.c @@ -42,38 +42,41 @@ static void box_subtract_into(const box_t *piece, } } -/* Clip "box" (screen space) down to the parts not already covered by - * windows above "window" in the z-order, writing the surviving pieces to - * "out" (capacity WUSS_MAX_INVALIDATE_PIECES) and returning their count. */ -int wuss__clip_to_visible(wuss_window_t *window, - const box_t *box, - box_t *out) +/* Shared ping-pong carve loop: whittle "box" down by subtracting, in turn, + * each of "ncuts" cut boxes fetched one at a time via "get_cut" (opaque + * "ctx" threaded through), writing the surviving pieces to "out" (capacity + * WUSS_MAX_INVALIDATE_PIECES) and returning their count. A cut for which + * get_cut returns 0 is skipped (used to drop hidden occluders without the + * caller pre-filtering its list). */ +static int carve_by_cuts(const box_t *box, + int ncuts, + int (*get_cut)(void *ctx, int i, box_t *cut), + void *ctx, + box_t *out) { - box_t scratch[WUSS_MAX_INVALIDATE_PIECES]; - box_t *cur, *nxt, *tmp; - int ncur; - list_t *e; + box_t scratch[WUSS_MAX_INVALIDATE_PIECES]; + box_t *cur, *nxt, *tmp; + int ncur, i; cur = out; nxt = scratch; cur[0] = *box; ncur = 1; - for (e = window->wuss->z_order.next; e != &window->link; e = e->next) + for (i = 0; i < ncuts; i++) { - wuss_window_t *occluder; - int nnext, p; + box_t occluder; + int p, nnext; - occluder = wuss__window_from_link(e); - if (occluder->flags & wuss_WINDOW_HIDDEN) - continue; /* a hidden window occludes nothing */ - nnext = 0; + if (!get_cut(ctx, i, &occluder)) + continue; /* e.g. a hidden window occludes nothing */ + nnext = 0; for (p = 0; p < ncur; p++) { box_t cut; - if (box_intersection(&occluder->visible, &cur[p], &cut)) + if (box_intersection(&occluder, &cur[p], &cut)) { if (nnext < WUSS_MAX_INVALIDATE_PIECES) nxt[nnext++] = cur[p]; /* no overlap: piece survives untouched */ @@ -99,6 +102,63 @@ int wuss__clip_to_visible(wuss_window_t *window, return ncur; } +/* carve_by_cuts callback context/fetcher for wuss__clip_to_visible: walks + * the z-order list occluder-by-occluder, in the same order carve_by_cuts + * counts "i", skipping hidden windows. */ +typedef struct +{ + list_t *e; +} +zorder_ctx_t; + +static int zorder_get_cut(void *vctx, int i, box_t *cut) +{ + zorder_ctx_t *ctx; + wuss_window_t *occluder; + + (void) i; + + ctx = vctx; + occluder = wuss__window_from_link(ctx->e); + ctx->e = ctx->e->next; + + if (occluder->flags & wuss_WINDOW_HIDDEN) + return 0; /* a hidden window occludes nothing */ + + *cut = occluder->visible; + return 1; +} + +/* carve_by_cuts callback fetcher for wuss__subtract_boxes: plain indexed + * array lookup, every entry used. */ +static int array_get_cut(void *vctx, int i, box_t *cut) +{ + const box_t *cuts = vctx; + + *cut = cuts[i]; + return 1; +} + +/* Clip "box" (screen space) down to the parts not already covered by + * windows above "window" in the z-order, writing the surviving pieces to + * "out" (capacity WUSS_MAX_INVALIDATE_PIECES) and returning their count. */ +int wuss__clip_to_visible(wuss_window_t *window, + const box_t *box, + box_t *out) +{ + zorder_ctx_t ctx; + list_t *e; + int n; + + n = 0; + for (e = window->wuss->z_order.next; e != &window->link; e = e->next) + n++; + + ctx.e = window->wuss->z_order.next; + + return carve_by_cuts(box, n, zorder_get_cut, &ctx, out); +} + /* Subtract each of "cuts" (an array of "ncuts" boxes) from "whole", writing * the surviving pieces to "out" (capacity WUSS_MAX_INVALIDATE_PIECES) and * returning their count. */ @@ -107,49 +167,33 @@ int wuss__subtract_boxes(const box_t *whole, int ncuts, box_t *out) { - box_t scratch[WUSS_MAX_INVALIDATE_PIECES]; - box_t *cur, *nxt, *tmp; - int ncur, i; - - cur = out; - nxt = scratch; - cur[0] = *whole; - ncur = 1; - - for (i = 0; i < ncuts; i++) - { - int p, nnext; - - nnext = 0; + return carve_by_cuts(whole, ncuts, array_get_cut, (void *) cuts, out); +} - for (p = 0; p < ncur; p++) - { - box_t cut; +int wuss__filter_settled(box_t *clean, + int nclean, + const box_t *stale, + int nstale) +{ + box_t settled[WUSS_MAX_INVALIDATE_PIECES]; + int nsettled, c; - if (box_intersection(&cuts[i], &cur[p], &cut)) - { - if (nnext < WUSS_MAX_INVALIDATE_PIECES) - nxt[nnext++] = cur[p]; /* no overlap: piece survives untouched */ - } - else - { - box_subtract_into(&cur[p], &cut, nxt, &nnext); - } - } + if (nstale == 0) + return nclean; - ncur = nnext; - tmp = cur; - cur = nxt; - nxt = tmp; + nsettled = 0; + for (c = 0; c < nclean && nsettled < WUSS_MAX_INVALIDATE_PIECES; c++) + { + box_t piece[WUSS_MAX_INVALIDATE_PIECES]; + int npiece, s; - if (ncur == 0) - break; + npiece = wuss__subtract_boxes(&clean[c], stale, nstale, piece); + for (s = 0; s < npiece && nsettled < WUSS_MAX_INVALIDATE_PIECES; s++) + settled[nsettled++] = piece[s]; } - if (cur != out) - memcpy(out, cur, ncur * sizeof(*out)); - - return ncur; + memcpy(clean, settled, (size_t) nsettled * sizeof(*clean)); + return nsettled; } /* Sequential single-rect blits (each a self-consistent memmove) can still @@ -173,6 +217,15 @@ int wuss__order_pieces(const box_t *clean, int queue[WUSS_MAX_INVALIDATE_PIECES]; int i, j, head, tail, nout, u; + if (n <= 1) + { + /* a single piece (or none) can't clobber itself: skip the O(n^2) graph + * build, the common case on every plain move/resize/scroll blit */ + if (n == 1) + order[0] = 0; + return 1; + } + for (i = 0; i < n; i++) indeg[i] = 0; for (j = 0; j < n; j++) @@ -253,6 +306,16 @@ int wuss__blit_pieces(wuss_window_t *window, int nvis; box_translated(&src[i], dx, dy, &want); + + /* A shifted piece can slide past "clip" (set-scroll's content box) -- + * e.g. a fast scroll on a since-shrunk window -- and wuss__clip_to_visible + * below only clips against occluding windows, not against "clip". Left + * unclamped, the excess ends up in blit_dest and is later handed + * unclipped to wuss__invalidate_minus, marking screen outside the window + * dirty for the next redraw. */ + if (clip != NULL && box_intersection(clip, &want, &want)) + continue; + nvis = wuss__clip_to_visible(window, &want, vis); for (j = 0; j < nvis; j++) { @@ -297,6 +360,11 @@ int wuss__blit_pieces(wuss_window_t *window, * Safe raw -- blit_dest[idx] was already clipped clear of occluders. */ wuss__invalidate_minus(window->wuss, &blit_dest[idx], &got); + /* "got" itself needs no repaint -- screen_copy_rect already gave it + * correct pixels -- but a frontend that only re-uploads what + * wuss_get_touched_extent reports still needs to know they moved. */ + wuss__touch(window->wuss, &got); + if (*ncopied < WUSS_MAX_INVALIDATE_PIECES) copied[(*ncopied)++] = got; } @@ -368,23 +436,32 @@ void wuss__invalidate_minus(wuss_t *wuss, void wuss_window_invalidate(wuss_window_t *window, const box_t *local_box) { - box_t screen_box, content, whole; + box_t screen_box, content, whole, clipped; wuss__content_box(window, &content); if (local_box == NULL) { - whole.x0 = 0; - whole.y0 = 0; - whole.x1 = content.x1 - content.x0; - whole.y1 = content.y1 - content.y0; + /* doc-space box of the visible rect at the current scroll, so the + * "+ content.x0 - scroll" translation below lands back on the window's + * actual screen box instead of sliding it by scroll a second time. */ + whole.x0 = window->scroll.x; + whole.y0 = window->scroll.y; + whole.x1 = window->scroll.x + (content.x1 - content.x0); + whole.y1 = window->scroll.y + (content.y1 - content.y0); local_box = &whole; } box_translated(local_box, content.x0 - window->scroll.x, content.y0 - window->scroll.y, &screen_box); - wuss__invalidate_clipped(window, &screen_box); + /* clients pass arbitrary boxes (e.g. a ball's swept box near an edge); + * clamp to the content area so a client can never dirty the furniture + * around it. */ + if (box_intersection(&screen_box, &content, &clipped)) + return; /* no overlap with content at all */ + + wuss__invalidate_clipped(window, &clipped); } void wuss_window_invalidate_extent(wuss_window_t *window) diff --git a/libraries/wuss/core/window/move.c b/libraries/wuss/core/window/move.c index 6d1357d..0da028d 100644 --- a/libraries/wuss/core/window/move.c +++ b/libraries/wuss/core/window/move.c @@ -4,23 +4,17 @@ /* p is the window's content top-left; the furniture offset (outline plus * any titlebar) is constant for a given window, so the footprint just - * follows it */ + * follows it. The cached furniture layout is translated in place below + * rather than invalidated -- a pure move changes no piece's size, so there + * is nothing to rebuild. */ void wuss_window_move(wuss_window_t *window, point_t p) { - box_t clean[WUSS_MAX_INVALIDATE_PIECES]; - box_t full_dest[WUSS_MAX_INVALIDATE_PIECES]; - box_t copied[WUSS_MAX_INVALIDATE_PIECES]; - int width, height, outline_px, titlebar_height; - int dx, dy, nclean, ncopied, i; - box_t before, dirty; - - /* a manual move desyncs the window from its layout-packer slot; hand the - * slot back and stop tracking this window's position */ - wuss__release_packed(window); - - /* 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); + box_t clean[WUSS_MAX_INVALIDATE_PIECES]; + box_t full_dest[WUSS_MAX_INVALIDATE_PIECES]; + box_t copied[WUSS_MAX_INVALIDATE_PIECES]; + int width, height, outline_px, titlebar_height; + int dx, dy, nclean, ncopied, i; + box_t before, dirty; width = window->visible.x1 - window->visible.x0; height = window->visible.y1 - window->visible.y0; @@ -28,10 +22,29 @@ void wuss_window_move(wuss_window_t *window, point_t p) titlebar_height = wuss__titlebar_height(window); before = window->visible; + /* a drag delivers one call per pointer-move event, not one per actual + * change of position -- a window pinned against the screen edge, or a + * backend that reports redundant motion, can call this with the same + * target it's already at. width/height never change here, so an + * unchanged origin means an unchanged box; skip the packer release, + * layout translate and blit/invalidate machinery entirely. */ + if (p.x - outline_px == before.x0 && + p.y - outline_px - titlebar_height == before.y0) + return; + + /* a manual move desyncs the window from its layout-packer slot; hand the + * slot back and stop tracking this window's position */ + wuss__release_packed(window); + /* a hidden window has nothing on screen to slide and must paint nothing; * just translate its footprint so it is in place when shown again */ if (window->flags & wuss_WINDOW_HIDDEN) { + /* nothing is drawn from the cache while hidden, but the next unhide must + * not paint it back at the old position, so just drop it -- there's no + * screen-visible dirty region to queue either way */ + wuss__chrome_invalidate_layout(window); + window->visible.x0 = p.x - outline_px; window->visible.y0 = p.y - outline_px - titlebar_height; window->visible.x1 = window->visible.x0 + width; @@ -55,25 +68,8 @@ void wuss_window_move(wuss_window_t *window, point_t p) * would just paste it, untouched, onto the window's new position. Strip * every pending-dirty region out of "clean" first so only pixels already * settled on screen are treated as a valid blit source. */ - if (window->wuss->ndirty > 0) - { - box_t settled[WUSS_MAX_INVALIDATE_PIECES]; - int nsettled, c; - - nsettled = 0; - for (c = 0; c < nclean && nsettled < WUSS_MAX_INVALIDATE_PIECES; c++) - { - box_t piece[WUSS_MAX_INVALIDATE_PIECES]; - int npiece, s; - - npiece = wuss__subtract_boxes(&clean[c], window->wuss->dirty, - window->wuss->ndirty, piece); - for (s = 0; s < npiece && nsettled < WUSS_MAX_INVALIDATE_PIECES; s++) - settled[nsettled++] = piece[s]; - } - nclean = nsettled; - memcpy(clean, settled, (size_t) nclean * sizeof(*clean)); - } + nclean = wuss__filter_settled(clean, nclean, window->wuss->dirty, + window->wuss->ndirty); window->visible.x0 = p.x - outline_px; window->visible.y0 = p.y - outline_px - titlebar_height; @@ -85,6 +81,13 @@ void wuss_window_move(wuss_window_t *window, point_t p) dx = window->visible.x0 - before.x0; dy = window->visible.y0 - before.y0; + /* a pure translation leaves every cached rect correct relative to the + * window, just offset in screen space -- shift it instead of dropping the + * cache and rebuilding all pieces on the next paint of even a thin + * sliver */ + if (window->furniture_layout.valid) + wuss__furniture_layout_translate(window, dx, dy); + for (i = 0; i < nclean; i++) box_translated(&clean[i], dx, dy, &full_dest[i]); diff --git a/libraries/wuss/core/window/resize.c b/libraries/wuss/core/window/resize.c index c876422..db71ee6 100644 --- a/libraries/wuss/core/window/resize.c +++ b/libraries/wuss/core/window/resize.c @@ -34,17 +34,15 @@ static void invalidate_grown_or_shrunk(wuss_window_t *window, result_t wuss_window_resize(wuss_window_t *window, size2d_t size) { int outline_px, titlebar_height; - box_t before, before_content; - point_t carve; + box_t before; point_t old_scroll; + box_t before_content; + point_t carve; point_t clamped; if (!wuss__size_ok(size.w, size.h)) return result_WUSS_TOO_SMALL; - /* a manual resize desyncs the window from its layout-packer slot */ - wuss__release_packed(window); - /* The requested content size is honoured verbatim: a window may end up * overhanging the screen edge (same as one dragged there, or one whose * far corner is off-screen when toggle-size floors it at WUSS_MIN_CONTENT). @@ -57,6 +55,19 @@ result_t wuss_window_resize(wuss_window_t *window, size2d_t size) wuss__content_box(window, &before_content); wuss__furniture_carve_for(window->flags, wuss__button_size(window), &carve); + /* a resize drag delivers one call per pointer-move event, not one per + * actual change of size -- a window pinned against the screen edge or + * the minimum-size clamp can call this repeatedly at the size it's + * already at. The top-left corner never moves here, so an unchanged + * bottom-right corner means an unchanged box; skip the packer release + * and the invalidate/blit machinery entirely. */ + if (window->visible.x0 + size.w + 2 * outline_px + carve.x == before.x1 && + window->visible.y0 + size.h + titlebar_height + 2 * outline_px + carve.y == before.y1) + return result_OK; + + /* a manual resize desyncs the window from its layout-packer slot */ + wuss__release_packed(window); + window->visible.x1 = window->visible.x0 + size.w + 2 * outline_px + carve.x; window->visible.y1 = window->visible.y0 + size.h + titlebar_height + 2 * outline_px + carve.y; @@ -79,28 +90,62 @@ result_t wuss_window_resize(wuss_window_t *window, size2d_t size) if (clamped.x != window->scroll.x || clamped.y != window->scroll.y) { box_t content; - box_t src[WUSS_MAX_INVALIDATE_PIECES]; + box_t stale[WUSS_MAX_DIRTY]; + box_t clean[WUSS_MAX_INVALIDATE_PIECES]; box_t copied[WUSS_MAX_INVALIDATE_PIECES]; box_t dirty[WUSS_MAX_INVALIDATE_PIECES]; - int dx, dy, nsrc, ncopied, ndirty, i; + int dx, dy, nstale, nclean, nsrc, ncopied, ndirty, i, overflow; dx = clamped.x - old_scroll.x; dy = clamped.y - old_scroll.y; window->scroll = clamped; wuss__content_box(window, &content); - nsrc = wuss__clip_to_visible(window, &before_content, src); + /* A resize drag delivers several calls per frame, same as a fast wheel + * spin (see wuss_window_set_scroll): an earlier call this frame may have + * queued part of "before_content" in wuss->dirty[] without a redraw + * having painted it yet. Sliding that stale ground would smear garbage + * into the window instead of this window's own settled pixels. */ + nstale = 0; + for (i = 0; i < window->wuss->ndirty; i++) + if (box_intersects(&window->wuss->dirty[i], &before_content)) + stale[nstale++] = window->wuss->dirty[i]; + + /* Each subtract can split a piece into up to four bands, so this can + * overflow the piece budget on a badly fragmented window -- treat that + * as "no safe fast path" (see wuss_window_set_scroll) rather than + * silently dropping survivors: a dropped clean piece never gets + * invalidated either, since the invalidate below only covers "content + * minus copied", not "content minus every clean piece found" -- so a + * silently-dropped piece would leave genuinely stale pre-resize pixels + * on screen with nothing left to repaint them. wuss__filter_settled caps + * at WUSS_MAX_INVALIDATE_PIECES the same way, so treat a full result as + * a possible overflow too. */ + nclean = wuss__clip_to_visible(window, &before_content, clean); + nsrc = wuss__filter_settled(clean, nclean, stale, nstale); + overflow = (nsrc == WUSS_MAX_INVALIDATE_PIECES); + ncopied = 0; - window->wuss->scr->clip = content; - for (i = 0; i < nsrc; i++) + if (!overflow) { - box_t got; - - if (screen_copy_rect(window->wuss->scr, &src[i], - POINT(src[i].x0 - dx, src[i].y0 - dy), - &got) == result_OK && - ncopied < WUSS_MAX_INVALIDATE_PIECES) - copied[ncopied++] = got; + window->wuss->scr->clip = content; + for (i = 0; i < nsrc; i++) + { + box_t got; + + if (screen_copy_rect(window->wuss->scr, &clean[i], + POINT(clean[i].x0 - dx, clean[i].y0 - dy), + &got) == result_OK) + { + /* "got" is already correct on screen -- the blit reused it -- + * but a frontend re-uploading only wuss_get_touched_extent still + * needs to know it moved. */ + wuss__touch(window->wuss, &got); + + if (ncopied < WUSS_MAX_INVALIDATE_PIECES) + copied[ncopied++] = got; + } + } } if (ncopied > 0) @@ -115,13 +160,14 @@ result_t wuss_window_resize(wuss_window_t *window, size2d_t size) } else { - /* the scroll-reclamp blit copied nothing (paletted screen, or every - * piece off-screen): repaint the whole content box */ + /* the scroll-reclamp blit copied nothing (paletted screen, every + * piece off-screen, or the piece budget overflowed): repaint the + * whole content box */ logf_warning("wuss_window_resize: scroll-reclamp blit copied nothing, " - "repainting the whole content box (nsrc=%d)", nsrc); + "repainting the whole content box (nsrc=%d, overflow=%d)", + nsrc, overflow); wuss__invalidate_clipped(window, &content); } - wuss__chrome_repaint(window); } if (window->flags & wuss_WINDOW_NO_RESIZE_BLIT) diff --git a/libraries/wuss/core/window/set-hidden.c b/libraries/wuss/core/window/set-hidden.c index f1b9467..e78232f 100644 --- a/libraries/wuss/core/window/set-hidden.c +++ b/libraries/wuss/core/window/set-hidden.c @@ -31,11 +31,20 @@ result_t wuss_window_set_hidden(wuss_window_t *window, int hidden) } #endif #ifdef WUSS_ICONS - if (wuss->pressed_icon != NULL && wuss->pressed_icon->window == window) - wuss->pressed_icon = NULL; - if (wuss->hover_icon != NULL && wuss->hover_icon->window == window) - wuss->hover_icon = NULL; + if (wuss->pressed_window == window) + { + wuss->pressed_icon = NULL; + wuss->pressed_window = NULL; + } + if (wuss->hover_window == window) + { + wuss->hover_icon = NULL; + wuss->hover_window = NULL; + } #endif + /* A hidden window is off the mouse; drop it from enter/exit tracking. No + * EXIT event -- it stops receiving pointer events regardless. */ + wuss__pointer_forget_window(wuss, window); /* still on screen: repaint its footprint now, then mark it gone */ wuss_invalidate(wuss, &window->visible); diff --git a/libraries/wuss/font/font.h b/libraries/wuss/font/font.h new file mode 100644 index 0000000..8374ff5 --- /dev/null +++ b/libraries/wuss/font/font.h @@ -0,0 +1,61 @@ +/* wuss/font/font.h -- wuss font registry and centralised text rendering */ + +#ifndef WUSS_FONT_H +#define WUSS_FONT_H + +#include "geom/point.h" +#include "framebuf/bmfont.h" +#include "framebuf/screen.h" + +#include "base/result.h" + +#include "wuss/wuss.h" + +/* The set of fonts a wuss_t was created with: slot 0 is the system font used + * for window titlebars and offered to tasks via wuss_get_font; further slots + * hold optional bold/symbol faces. Populated once by wuss__fontset_init from + * wuss_create's argument and never changed after. Fonts are borrowed, not + * owned. */ +struct wuss_fontset +{ + /* unset slots NULL */ + bmfont_t *fonts[wuss_MAX_FONTS]; + /* parallel to fonts[]; wuss_FONT_CLASS_NONE for an unset slot */ + wuss_font_class_t font_classes[wuss_MAX_FONTS]; + /* parallel to fonts[]; borrowed; NULL if unset or given no name */ + const char *font_names[wuss_MAX_FONTS]; + /* slots filled from the wuss_create argument */ + int nfonts; +}; + +/* Copy the first "nfonts" descriptors into "set" and NULL the rest. "nfonts" + * has already been range-checked by the caller (0..wuss_MAX_FONTS). */ +void wuss__fontset_init(struct wuss_fontset *set, + const wuss_font_desc_t *fonts, + int nfonts); + +/* Pixel height of the font in "slot", or 0 if that slot is empty or out of + * range. Used by wuss_create to size the titlebar to the tallest of the + * regular and bold faces. */ +int wuss__fontset_height(const struct wuss_fontset *set, int slot); + +/* Centralised text rendering. wuss__text_draw is a plain pass-through to + * bmfont_draw (pos/end_pos are baseline positions, per bmfont_draw's + * contract), kept alongside wuss__text_measure for a single point of + * policy. */ +result_t wuss__text_measure(bmfont_t *font, + const char *text, + int len, + bmfont_width_t target_width, + int *split_point, + bmfont_width_t *actual_width); +result_t wuss__text_draw(bmfont_t *font, + screen_t *scr, + const char *text, + int len, + colour_t fg, + colour_t bg, + const point_t *pos, + point_t *end_pos); + +#endif /* WUSS_FONT_H */ diff --git a/libraries/wuss/font/registry.c b/libraries/wuss/font/registry.c new file mode 100644 index 0000000..9e65e2d --- /dev/null +++ b/libraries/wuss/font/registry.c @@ -0,0 +1,86 @@ +/* wuss/font/registry.c -- wuss font slot registry and lookups */ + +#include + +#include "framebuf/bmfont.h" + +#include "wuss/wuss.h" + +#include "../core/impl.h" +#include "font.h" + +void wuss__fontset_init(struct wuss_fontset *set, + const wuss_font_desc_t *fonts, + int nfonts) +{ + int i; + + assert(set != NULL); + assert(nfonts >= 0 && nfonts <= wuss_MAX_FONTS); + + for (i = 0; i < nfonts; i++) + { + set->fonts[i] = fonts[i].font; + set->font_classes[i] = fonts[i].font_class; + set->font_names[i] = fonts[i].name; + } + for (; i < wuss_MAX_FONTS; i++) + { + set->fonts[i] = NULL; + set->font_classes[i] = wuss_FONT_CLASS_NONE; + set->font_names[i] = NULL; + } + set->nfonts = nfonts; +} + +int wuss__fontset_height(const struct wuss_fontset *set, int slot) +{ + int height; + + assert(set != NULL); + + if (slot < 0 || slot >= wuss_MAX_FONTS || set->fonts[slot] == NULL) + return 0; + + bmfont_get_info(set->fonts[slot], NULL, &height, NULL, NULL); + return height; +} + +/* ----------------------------------------------------------------------- */ + +bmfont_t *wuss_get_font(const wuss_t *wuss) +{ + assert(wuss != NULL); + + return wuss->fonts.fonts[0]; +} + +bmfont_t *wuss_get_font_n(const wuss_t *wuss, int index) +{ + assert(wuss != NULL); + + if (index < 0 || index >= wuss_MAX_FONTS) + return NULL; + + return wuss->fonts.fonts[index]; +} + +wuss_font_class_t wuss_get_font_class_n(const wuss_t *wuss, int index) +{ + assert(wuss != NULL); + + if (index < 0 || index >= wuss_MAX_FONTS) + return wuss_FONT_CLASS_NONE; + + return wuss->fonts.font_classes[index]; +} + +const char *wuss_get_font_name_n(const wuss_t *wuss, int index) +{ + assert(wuss != NULL); + + if (index < 0 || index >= wuss_MAX_FONTS) + return NULL; + + return wuss->fonts.font_names[index]; +} diff --git a/libraries/wuss/font/text.c b/libraries/wuss/font/text.c new file mode 100644 index 0000000..47b0480 --- /dev/null +++ b/libraries/wuss/font/text.c @@ -0,0 +1,71 @@ +/* wuss/font/text.c -- wuss centralised bitmap-font text rendering */ + +#include "geom/point.h" +#include "framebuf/bmfont.h" +#include "framebuf/screen.h" + +#include "font.h" + +/* ----------------------------------------------------------------------- */ + +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, NULL, 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) +{ + return bmfont_draw(font, scr, text, len, fg, bg, NULL, pos, end_pos); +} + +/* ----------------------------------------------------------------------- */ + +result_t wuss_text_measure(const wuss_t *wuss, + int index, + const char *text, + int len, + bmfont_width_t target_width, + int *split_point, + bmfont_width_t *actual_width) +{ + bmfont_t *font; + + font = wuss_get_font_n(wuss, index); + if (font == NULL) + return result_WUSS_BAD_INDEX; + + return wuss__text_measure(font, text, len, target_width, split_point, + actual_width); +} + +result_t wuss_text_draw(const wuss_t *wuss, + int index, + screen_t *scr, + const char *text, + int len, + colour_t fg, + colour_t bg, + const point_t *pos, + point_t *end_pos) +{ + bmfont_t *font; + + font = wuss_get_font_n(wuss, index); + if (font == NULL) + return result_WUSS_BAD_INDEX; + + return wuss__text_draw(font, scr, text, len, fg, bg, pos, end_pos); +} diff --git a/libraries/wuss/furniture.h b/libraries/wuss/furniture.h index 052e4fe..d0d218c 100644 --- a/libraries/wuss/furniture.h +++ b/libraries/wuss/furniture.h @@ -63,6 +63,12 @@ wuss__furniture_layout_t; /* Populate window->furniture_layout from the current geometry and flags. */ void wuss__furniture_layout_build(wuss_window_t *window); +/* Shift an already-valid window->furniture_layout by (dx, dy) in place -- + * for a pure window move, where every cached rect stays correct relative to + * the window, just offset in screen space, so there is no need to drop the + * cache and rebuild all pieces from scratch. */ +void wuss__furniture_layout_translate(wuss_window_t *window, int dx, int dy); + /* Which region of a window's border (or its content) a point falls in. */ typedef enum wuss_furniture_region { diff --git a/libraries/wuss/furniture/close-box.c b/libraries/wuss/furniture/close-box.c index 3de8147..18d3c1e 100644 --- a/libraries/wuss/furniture/close-box.c +++ b/libraries/wuss/furniture/close-box.c @@ -13,7 +13,7 @@ void wuss__close_box(const wuss_window_t *window, box_t *out) size = wuss__button_size(window); out->x0 = titlebar.x0 + inset; - if (!(window->flags & wuss_WINDOW_NO_BACK)) + if (window->flags & wuss_WINDOW_BACK) out->x0 += size + inset; /* shift right, clear of the back icon */ out->y0 = titlebar.y0 + inset; out->x1 = out->x0 + size; @@ -30,7 +30,7 @@ void wuss__close_hit_box(const wuss_window_t *window, box_t *out) wuss__close_box(window, &drawn); - out->x0 = (window->flags & wuss_WINDOW_NO_BACK) ? window->visible.x0 : drawn.x0; + out->x0 = (window->flags & wuss_WINDOW_BACK) ? drawn.x0 : window->visible.x0; out->y0 = window->visible.y0; out->x1 = drawn.x1; out->y1 = drawn.y1; diff --git a/libraries/wuss/furniture/drag-resize.c b/libraries/wuss/furniture/drag-resize.c index 00acdc0..ee84ddd 100644 --- a/libraries/wuss/furniture/drag-resize.c +++ b/libraries/wuss/furniture/drag-resize.c @@ -7,11 +7,12 @@ void wuss__furniture_drag_resize(wuss_window_t *window, point_t p) { box_t content; - size2d_t min; + size2d_t min, max; int width, height; wuss__content_box(window, &content); wuss__min_content(window, &min); + wuss__max_content_anywhere_on_screen(window, &max); /* Subtract the offset recorded at drag start so the point originally * grabbed on the resize icon stays under the pointer, rather than the @@ -20,6 +21,8 @@ void wuss__furniture_drag_resize(wuss_window_t *window, point_t p) height = p.y - window->wuss->furniture.drag_offset.y - content.y0; width = CLAMP(width, min.w, MAX(window->doc.w, min.w)); height = CLAMP(height, min.h, MAX(window->doc.h, min.h)); + width = MIN(width, max.w); + height = MIN(height, max.h); wuss_window_resize(window, SIZE2D(width, height)); } diff --git a/libraries/wuss/furniture/draw.c b/libraries/wuss/furniture/draw.c index a9b9405..3a6a84c 100644 --- a/libraries/wuss/furniture/draw.c +++ b/libraries/wuss/furniture/draw.c @@ -67,7 +67,7 @@ static void draw_title(wuss_t *wuss, { bmfont_t *titlefont; point_t pos; - int text_x0, text_x1, titlelen, split_point; + int text_x0, text_x1, titlelen, split_point, ascent; bmfont_width_t width; box_t text_box, text_clip; @@ -76,21 +76,21 @@ static void draw_title(wuss_t *wuss, /* window titles are drawn in the bold weight (font slot 1) when one was * supplied, falling back to the system font */ - titlefont = (wuss->nfonts > 1 && wuss->fonts[1] != NULL) - ? wuss->fonts[1] - : wuss->fonts[0]; + titlefont = (wuss->fonts.nfonts > 1 && wuss->fonts.fonts[1] != NULL) + ? wuss->fonts.fonts[1] + : wuss->fonts.fonts[0]; if (titlefont == NULL) return; text_x0 = titlebar->x0 + 2; - if (!(window->flags & wuss_WINDOW_NO_CLOSE)) + if (window->flags & wuss_WINDOW_CLOSE) { box_t close; wuss__close_box(window, &close); text_x0 = close.x1 + 2; } - else if (!(window->flags & wuss_WINDOW_NO_BACK)) + else if (window->flags & wuss_WINDOW_BACK) { box_t back; @@ -99,7 +99,7 @@ static void draw_title(wuss_t *wuss, } text_x1 = titlebar->x1 - 2; - if (!(window->flags & wuss_WINDOW_NO_TOGGLE_SIZE)) + if (window->flags & wuss_WINDOW_TOGGLE_SIZE) { box_t toggle; @@ -121,8 +121,10 @@ static void draw_title(wuss_t *wuss, titlelen = (int) strlen(window->title); wuss__text_measure(titlefont, window->title, titlelen, text_x1 - text_x0, &split_point, &width); + bmfont_get_info(titlefont, NULL, NULL, &ascent, NULL); + pos.x = (split_point < titlelen) ? text_x0 : text_x0 + MAX(0, ((text_x1 - text_x0) - width) / 2); - pos.y = titlebar->y0 + 2; + pos.y = titlebar->y0 + 2 + ascent; wuss->scr->clip = text_clip; wuss__text_draw(titlefont, wuss->scr, window->title, titlelen, wuss->palette[wuss->furniture_colours.title.fg], @@ -156,14 +158,14 @@ void wuss__furniture_draw(wuss_t *wuss, /* the two scrollbar sausages: geometry depends on window->scroll, so they * are computed live rather than cached */ - if (!(window->flags & wuss_WINDOW_NO_VSCROLL)) + if (window->flags & wuss_WINDOW_VSCROLL) { wuss__vscroll_sausage_box(window, &sausage); fill_furniture_rect(wuss, &sausage, full, wuss->palette[wuss->furniture_colours.scroll.sausages]); } - if (!(window->flags & wuss_WINDOW_NO_HSCROLL)) + if (window->flags & wuss_WINDOW_HSCROLL) { wuss__hscroll_sausage_box(window, &sausage); fill_furniture_rect(wuss, &sausage, full, diff --git a/libraries/wuss/furniture/hit-test.c b/libraries/wuss/furniture/hit-test.c index 04bc36c..ee2cc06 100644 --- a/libraries/wuss/furniture/hit-test.c +++ b/libraries/wuss/furniture/hit-test.c @@ -18,9 +18,9 @@ static wuss_furniture_region_t nearest_edge_region(const wuss_window_t *window, wuss__content_box(window, &content); - has_v = !(window->flags & wuss_WINDOW_NO_VSCROLL); - has_h = !(window->flags & wuss_WINDOW_NO_HSCROLL); - has_title = !(window->flags & wuss_WINDOW_NO_TITLEBAR); + has_v = (window->flags & wuss_WINDOW_VSCROLL) != 0; + has_h = (window->flags & wuss_WINDOW_HSCROLL) != 0; + has_title = (window->flags & wuss_WINDOW_NO_TITLEBAR) == 0; /* bottom edge: the hscroll strip runs its full width */ if (p.y >= content.y1) @@ -61,21 +61,21 @@ wuss_furniture_region_t wuss__furniture_hit_test(const wuss_window_t *window, if (!(window->flags & wuss_WINDOW_NO_TITLEBAR)) { - if (!(window->flags & wuss_WINDOW_NO_BACK)) + if (window->flags & wuss_WINDOW_BACK) { wuss__back_hit_box(window, &box); if (box_contains_point(&box, p.x, p.y)) return wuss_FURNITURE_BACK; } - if (!(window->flags & wuss_WINDOW_NO_CLOSE)) + if (window->flags & wuss_WINDOW_CLOSE) { wuss__close_hit_box(window, &box); if (box_contains_point(&box, p.x, p.y)) return wuss_FURNITURE_CLOSE; } - if (!(window->flags & wuss_WINDOW_NO_TOGGLE_SIZE)) + if (window->flags & wuss_WINDOW_TOGGLE_SIZE) { wuss__toggle_hit_box(window, &box); if (box_contains_point(&box, p.x, p.y)) @@ -87,14 +87,14 @@ wuss_furniture_region_t wuss__furniture_hit_test(const wuss_window_t *window, return wuss_FURNITURE_TITLE; } - if (!(window->flags & wuss_WINDOW_NO_RESIZE)) + if (window->flags & wuss_WINDOW_RESIZE) { wuss__resize_hit_box(window, &box); if (box_contains_point(&box, p.x, p.y)) return wuss_FURNITURE_RESIZE; } - if (!(window->flags & wuss_WINDOW_NO_VSCROLL)) + if (window->flags & wuss_WINDOW_VSCROLL) { wuss__vscroll_up_hit_box(window, &box); if (box_contains_point(&box, p.x, p.y)) @@ -109,7 +109,7 @@ wuss_furniture_region_t wuss__furniture_hit_test(const wuss_window_t *window, return wuss_FURNITURE_VSCROLL_WELL; } - if (!(window->flags & wuss_WINDOW_NO_HSCROLL)) + if (window->flags & wuss_WINDOW_HSCROLL) { wuss__hscroll_left_hit_box(window, &box); if (box_contains_point(&box, p.x, p.y)) diff --git a/libraries/wuss/furniture/layout.c b/libraries/wuss/furniture/layout.c index 9888b82..ed9b81f 100644 --- a/libraries/wuss/furniture/layout.c +++ b/libraries/wuss/furniture/layout.c @@ -69,7 +69,7 @@ void wuss__furniture_layout_build(wuss_window_t *window) push_piece(layout, &rule, wuss__FURNITURE_PAINT_OUTLINE); } - if (!(window->flags & wuss_WINDOW_NO_CLOSE)) + if (window->flags & wuss_WINDOW_CLOSE) { box_t close; @@ -77,7 +77,7 @@ void wuss__furniture_layout_build(wuss_window_t *window) push_piece(layout, &close, wuss__FURNITURE_PAINT_CLOSE); } - if (!(window->flags & wuss_WINDOW_NO_BACK)) + if (window->flags & wuss_WINDOW_BACK) { box_t back; @@ -85,7 +85,7 @@ void wuss__furniture_layout_build(wuss_window_t *window) push_piece(layout, &back, wuss__FURNITURE_PAINT_BACK); } - if (!(window->flags & wuss_WINDOW_NO_TOGGLE_SIZE)) + if (window->flags & wuss_WINDOW_TOGGLE_SIZE) { box_t toggle; @@ -98,9 +98,9 @@ void wuss__furniture_layout_build(wuss_window_t *window) * Both scrollbars off but a resize icon on: the content box is still * carved back on its right and bottom and no scroll strip paints that * margin, so furniture fills it (see the note in the old draw.c). */ - if ((window->flags & wuss_WINDOW_NO_VSCROLL) && - (window->flags & wuss_WINDOW_NO_HSCROLL) && - !(window->flags & wuss_WINDOW_NO_RESIZE)) + if (!(window->flags & wuss_WINDOW_VSCROLL) && + !(window->flags & wuss_WINDOW_HSCROLL) && + (window->flags & wuss_WINDOW_RESIZE)) { box_t band; @@ -125,15 +125,15 @@ void wuss__furniture_layout_build(wuss_window_t *window) } /* resize icon + its dividing seams ---------------------------------- */ - if (!(window->flags & wuss_WINDOW_NO_RESIZE)) + if (window->flags & wuss_WINDOW_RESIZE) { box_t resize, rule; int top_seam, left_seam; wuss__resize_box(window, &resize); - top_seam = (window->flags & wuss_WINDOW_NO_VSCROLL) ? 0 : WUSS_DIVIDER_PX; - left_seam = (window->flags & wuss_WINDOW_NO_HSCROLL) ? 0 : WUSS_DIVIDER_PX; + top_seam = (window->flags & wuss_WINDOW_VSCROLL) ? WUSS_DIVIDER_PX : 0; + left_seam = (window->flags & wuss_WINDOW_HSCROLL) ? WUSS_DIVIDER_PX : 0; push_piece(layout, &resize, wuss__FURNITURE_PAINT_RESIZE); @@ -157,7 +157,7 @@ void wuss__furniture_layout_build(wuss_window_t *window) } /* vertical scrollbar: arrows + well (sausage is drawn live) --------- */ - if (!(window->flags & wuss_WINDOW_NO_VSCROLL)) + if (window->flags & wuss_WINDOW_VSCROLL) { box_t up, down, well; @@ -172,7 +172,7 @@ void wuss__furniture_layout_build(wuss_window_t *window) } /* horizontal scrollbar: arrows + well ----------------------------- */ - if (!(window->flags & wuss_WINDOW_NO_HSCROLL)) + if (window->flags & wuss_WINDOW_HSCROLL) { box_t left, right, well; @@ -237,3 +237,17 @@ void wuss__furniture_layout_build(wuss_window_t *window) layout->valid = 1; } + +void wuss__furniture_layout_translate(wuss_window_t *window, int dx, int dy) +{ + wuss__furniture_layout_t *layout; + int i; + + layout = &window->furniture_layout; + + for (i = 0; i < layout->npieces; i++) + box_translated(&layout->pieces[i].rect, dx, dy, &layout->pieces[i].rect); + + if (layout->has_titlebar) + box_translated(&layout->titlebar, dx, dy, &layout->titlebar); +} diff --git a/libraries/wuss/furniture/scroll-box.c b/libraries/wuss/furniture/scroll-box.c index c56d69d..9bbde46 100644 --- a/libraries/wuss/furniture/scroll-box.c +++ b/libraries/wuss/furniture/scroll-box.c @@ -28,8 +28,8 @@ static void scroll_strip(const wuss_window_t *window, outline_px = wuss__outline_px(window); size = wuss__button_size(window); - has_resize = !(window->flags & wuss_WINDOW_NO_RESIZE); - has_vscroll = !(window->flags & wuss_WINDOW_NO_VSCROLL); + has_resize = (window->flags & wuss_WINDOW_RESIZE) != 0; + has_vscroll = (window->flags & wuss_WINDOW_VSCROLL) != 0; if (horizontal) { @@ -74,8 +74,8 @@ static void scroll_strip_hit(const wuss_window_t *window, outline_px = wuss__outline_px(window); size = wuss__button_size(window); - has_resize = !(window->flags & wuss_WINDOW_NO_RESIZE); - has_vscroll = !(window->flags & wuss_WINDOW_NO_VSCROLL); + has_resize = (window->flags & wuss_WINDOW_RESIZE) != 0; + has_vscroll = (window->flags & wuss_WINDOW_VSCROLL) != 0; wuss__furniture_carve_for(window->flags, size, &carve); if (horizontal) @@ -193,7 +193,7 @@ static void scroll_sausage(const wuss_window_t *window, doc_size = MAX(doc_size, content_size); sausage_px = (doc_size > 0) ? track_px * content_size / doc_size : track_px; - sausage_px = CLAMP(sausage_px, WUSS_MIN_SAUSAGE, track_px); + sausage_px = CLAMP(sausage_px, WUSS_MIN_SAUSAGE, MAX(WUSS_MIN_SAUSAGE, track_px)); scroll = horizontal ? window->scroll.x : window->scroll.y; diff --git a/libraries/wuss/furniture/toggle-action.c b/libraries/wuss/furniture/toggle-action.c index 18c0246..e2c6901 100644 --- a/libraries/wuss/furniture/toggle-action.c +++ b/libraries/wuss/furniture/toggle-action.c @@ -127,7 +127,13 @@ void wuss__furniture_toggle_size(wuss_window_t *window) if (screen_copy_rect(window->wuss->scr, &blit_src, POINT(blit_dst.x0, blit_dst.y0), &copied) == result_OK) + { + /* "copied" is already correct on screen -- the blit reused it -- + * but a frontend re-uploading only wuss_get_touched_extent still + * needs to know it moved. */ + wuss__touch(window->wuss, &copied); blitted = 1; + } } } diff --git a/libraries/wuss/helpers/create-from-desc.c b/libraries/wuss/helpers/create-from-desc.c index baf3dc3..49b1d4c 100644 --- a/libraries/wuss/helpers/create-from-desc.c +++ b/libraries/wuss/helpers/create-from-desc.c @@ -43,10 +43,16 @@ typedef struct } Parser; -static void getopt_(Parser *parser) +/* '!' and '~' no longer carry a fixed truth -- each pulls the next int + * vararg (must be 0 or 1) at the point it is scanned, so a task's live state + * drives the flag directly instead of ticking/shading everything then + * mutating the string or item array afterwards. Returns 0 on success, -1 if + * a pulled vararg was not 0/1. */ +static int getopt_(Parser *parser, va_list *ap) { const char *p; int c; + int v; parser->flags = 0; p = parser->p; @@ -54,10 +60,25 @@ static void getopt_(Parser *parser) for (;;) { c = *p; - if (c == '!') parser->flags |= Tick; - else if (c == '>') parser->flags |= SubMenu; - else if (c == '~') parser->flags |= Shade; - else { parser->p = p; return; } + switch (c) + { + case '!': + v = va_arg(*ap, int); + if (v != 0 && v != 1) + return -1; + if (v) + parser->flags |= Tick; + break; + case '>': parser->flags |= SubMenu; break; + case '~': + v = va_arg(*ap, int); + if (v != 0 && v != 1) + return -1; + if (v) + parser->flags |= Shade; + break; + default: parser->p = p; return 0; + } p++; } } @@ -67,11 +88,12 @@ static int isdelim(int c) return c == ',' || c == '|' || c == '{' || c == '}' || c == '\0'; } -static void getname(Parser *parser) +static int getname(Parser *parser, va_list *ap) { const char *p; - getopt_(parser); + if (getopt_(parser, ap)) + return -1; p = parser->p; parser->start = p; @@ -85,16 +107,18 @@ static void getname(Parser *parser) p--; parser->end = p; + return 0; } -static Token getnext(Parser *parser) +static Token getnext(Parser *parser, va_list *ap) { while (isspace((unsigned char) *parser->p)) parser->p++; if (!isdelim(*parser->p)) { - getname(parser); + if (getname(parser, ap)) + return Error; if (parser->start == parser->end) return Error; return Name; @@ -124,7 +148,8 @@ void wuss_menu_destroy(wuss_menu_t *menu) for (i = 0; i < menu->nitems; i++) { free((void *) menu->items[i].text); /* discard const */ - wuss_menu_destroy((wuss_menu_t *) menu->items[i].submenu); + if (!(menu->items[i].flags & wuss_MENU_ITEM_BORROWED_SUBMENU)) + wuss_menu_destroy((wuss_menu_t *) menu->items[i].submenu); } free((void *) menu->title); /* discard const */ free((void *) menu->items); /* discard const */ @@ -339,7 +364,7 @@ result_t wuss_menu_create_from_desc(wuss_menu_t **out, const char *desc, ...) { Token tok; - tok = getnext(&parser); + tok = getnext(&parser, &ap); if (tok == End) break; diff --git a/libraries/wuss/helpers/icon-spec.c b/libraries/wuss/helpers/icon-spec.c new file mode 100644 index 0000000..9c60614 --- /dev/null +++ b/libraries/wuss/helpers/icon-spec.c @@ -0,0 +1,166 @@ +/* wuss/helpers/icon-spec.c -- fill helpers for common wuss_icon_spec_t shapes */ + +#include +#include + +#ifdef FORTIFY +#include "fortify/fortify.h" +#endif + +#include "base/utils.h" +#include "wuss/icon-spec.h" + +#ifdef WUSS_ICONS + +/* value label buffer: ample for any int plus a short unit suffix in fmt */ +#define SLIDER_ROW_BUF 32 + +/* nearest multiple of step above min, clamped to [min,max] -- min may be + * greater than max for a reversed (fill-backwards) slider, so clamp against + * the ordered pair rather than assuming min <= max */ +static int wuss__slider_row_snap(int v, int min, int max, int step) +{ + int lo, hi; + + v = ((v - min + step / 2) / step) * step + min; + + lo = MIN(min, max); + hi = MAX(min, max); + + return CLAMP(v, lo, hi); +} + +/* ----------------------------------------------------------------------- */ + +void wuss_icon_spec_label(wuss_icon_spec_t *spec, + box_t bbox, + const char *text, + wuss_colour_t fg, + int justify_right) +{ + memset(spec, 0, sizeof(*spec)); + spec->bbox = bbox; + spec->type = wuss_ICON_TYPE_LABEL; + spec->text = text; + spec->fg = fg; + spec->bg = wuss_NO_BACKGROUND; + spec->flags = justify_right ? wuss_ICON_FLAGS_JUSTIFY_RIGHT + : wuss_ICON_FLAGS_NONE; +} + +void wuss_icon_spec_slider(wuss_icon_spec_t *spec, + box_t bbox, + wuss_colour_t fg, + wuss_slider_orientation_t orientation, + int min, + int max, + int default_value) +{ + memset(spec, 0, sizeof(*spec)); + spec->bbox = bbox; + spec->type = wuss_ICON_TYPE_SLIDER; + spec->fg = fg; + spec->bg = wuss_NO_BACKGROUND; + spec->u.slider.orientation = orientation; + spec->u.slider.min = min; + spec->u.slider.max = max; + spec->u.slider.default_value = default_value; +} + +void wuss_icon_spec_action(wuss_icon_spec_t *spec, + box_t bbox, + const char *text, + wuss_colour_t fg, + wuss_colour_t bg, + int is_default) +{ + memset(spec, 0, sizeof(*spec)); + spec->bbox = bbox; + spec->type = wuss_ICON_TYPE_ACTION; + spec->text = text; + spec->fg = fg; + spec->bg = bg; + spec->flags = is_default ? wuss_ICON_FLAGS_DEFAULT : wuss_ICON_FLAGS_NONE; +} + +void wuss_icon_spec_slider_row(wuss_icon_spec_t *slider_spec, + wuss_icon_spec_t *value_spec, + box_t slider_bbox, + box_t value_bbox, + wuss_colour_t fg, + wuss_slider_orientation_t orientation, + int min, + int max, + int default_value, + const char *fmt, + int step) +{ + char buf[SLIDER_ROW_BUF]; + + if (step != 0) + default_value = wuss__slider_row_snap(default_value, min, max, step); + wuss_icon_spec_slider(slider_spec, slider_bbox, fg, orientation, min, max, + default_value); + snprintf(buf, sizeof(buf), fmt ? fmt : "%d", default_value); + wuss_icon_spec_label(value_spec, value_bbox, buf, fg, 0); +} + +void wuss_slider_row_bind(wuss_slider_row_t *row, + wuss_icon_t *slider, + wuss_icon_t *value, + const char *fmt, + int min, + int max, + int step) +{ + row->slider = slider; + row->value = value; + row->fmt = fmt; + row->min = min; + row->max = max; + row->step = step; +} + +result_t wuss_slider_row_set(wuss_window_t *window, + const wuss_slider_row_t *row, + int value) +{ + char buf[SLIDER_ROW_BUF]; + + if (row->step != 0) + value = wuss__slider_row_snap(value, row->min, row->max, row->step); + wuss_icon_set_value(window, row->slider, value); + snprintf(buf, sizeof(buf), row->fmt ? row->fmt : "%d", + wuss_icon_get_value(row->slider)); + return wuss_icon_set_text(window, row->value, buf); +} + +int wuss_slider_row_event(wuss_window_t *window, + const wuss_slider_row_t *row, + const wuss_event_t *event, + int *value) +{ + char buf[SLIDER_ROW_BUF]; + int v; + + if (event->kind != wuss_EVENT_ICON || event->data.icon.icon != row->slider) + return 0; + if (event->data.icon.action != wuss_MOUSE_DOWN && + event->data.icon.action != wuss_MOUSE_MOVE) + return 0; + + v = event->data.icon.value; + if (row->step != 0) + { + v = wuss__slider_row_snap(v, row->min, row->max, row->step); + wuss_icon_set_value(window, row->slider, v); /* visibly jump to it */ + } + snprintf(buf, sizeof(buf), row->fmt ? row->fmt : "%d", v); + wuss_icon_set_text(window, row->value, buf); + + if (value != NULL) + *value = v; + return 1; +} + +#endif /* WUSS_ICONS */ diff --git a/libraries/wuss/icon.h b/libraries/wuss/icon.h index b553eb3..7891f1c 100644 --- a/libraries/wuss/icon.h +++ b/libraries/wuss/icon.h @@ -24,21 +24,18 @@ typedef enum wuss_icon_state } wuss_icon_state_t; +/* A live icon is its creation spec plus transient runtime state. + * wuss__icon_from_spec validates a wuss_icon_spec_t and stores it in "spec" + * with fg/bg/swatch resolved to concrete palette indices and, for a BITMAP, + * u.bitmap.image resolved from u.bitmap.set. spec.text is owned (strdup'd by + * wuss_icon_create, aliased by wuss_icon_plot) -- never NULL, "" instead. */ struct wuss_icon { - wuss_window_t *window; /* owner; back-pointer for invalidate/get_window */ - box_t bbox; /* virtual document space */ - wuss_icon_type_t type; - char *text; /* owned; never NULL ("" instead) */ - wuss_colour_t fg, bg; - screen_pattern_t pattern; /* wuss_ICON_TYPE_PATTERN tile; 0 otherwise */ - const bitmap_t *bitmap; /* wuss_ICON_TYPE_BITMAP image; borrowed, or NULL */ - int group; /* radio: exclusive-selection group; 0 = none */ - wuss_colour_t swatch; /* menu entry + FLAGS_SWATCH: left-gutter chip - * colour; wuss_NO_BACKGROUND otherwise */ - wuss_icon_border_t border; /* label: inside-bbox border; NONE otherwise */ - wuss_icon_flags_t flags; + wuss_icon_spec_t spec; /* bbox in virtual document space; text owned */ wuss_icon_state_t state; + int value; /* wuss_ICON_TYPE_SLIDER: current value, in + * [spec.u.slider.min,max]; spec.u.slider.default_value + * is creation input only, never updated */ }; static inline int wuss__icon_pressed(const wuss_icon_t *icon) @@ -67,14 +64,29 @@ static inline void wuss__icon_set_state(wuss_icon_t *icon, } /* Set icon->selected, invalidating it. For a radio with a non-zero group, - * selecting it also clears every other selected radio on the same window with - * that group. Ignored for types with no latched state. */ -void wuss__icon_select(wuss_icon_t *icon, int selected); - -/* Make "icon" (may be NULL) the hovered icon: clears the hovered flag on the - * previous wuss->hover_icon and sets it on the new one, invalidating whichever - * of the two changed. A no-op if nothing changed. */ -void wuss__icon_set_hover(wuss_t *wuss, wuss_icon_t *icon); + * selecting it also clears every other selected radio on "window" with that + * group. Ignored for types with no latched state. "icon" must be an icon of + * "window". */ +void wuss__icon_select(wuss_window_t *window, + wuss_icon_t *icon, + int selected); + +/* Set icon->value, clamped to spec.u.slider.min/max, invalidating it if the + * clamped value changed. Ignored for types with no value. "icon" must be an + * icon of "window". Shared by wuss_icon_set_value and the slider drag path + * (core/mouse-click.c, core/mouse-move.c), which additionally raise + * wuss_EVENT_ICON with the new value -- this helper never does. */ +void wuss__icon_set_value(wuss_window_t *window, + wuss_icon_t *icon, + int value); + +/* Make "icon" (an icon of "window", or NULL) the hovered icon: clears the + * hovered flag on the previous wuss->hover_icon and sets it on the new one, + * invalidating whichever of the two changed. "window" is ignored when "icon" + * is NULL. A no-op if nothing changed. */ +void wuss__icon_set_hover(wuss_t *wuss, + wuss_window_t *window, + wuss_icon_t *icon); /* Map an icon bbox (virtual document space) into screen space: * screen = content.x0 - scroll.x + bbox. wuss__icon_draw paints through this @@ -84,14 +96,59 @@ void wuss__icon_box_to_screen(const box_t *content, const box_t *bbox, box_t *out); -/* Invalidate exactly this icon's bbox, via wuss_window_invalidate, so a - * set_text / pressed-state / hide change repaints just the icon. */ -void wuss__icon_invalidate(const wuss_icon_t *icon); - -/* Validate a spec against the palette and fill "out" with a detached icon (no - * window, no owned text -- out->text is aliased to spec->text or ""). Shared - * by wuss_icon_create and wuss_icon_plot. Returns result_WUSS_BAD_ICON / - * result_WUSS_BAD_COLOUR as wuss_icon_create documents, else result_OK. */ +/* wuss_ICON_TYPE_SLIDER geometry and value/pixel conversion, shared by + * wuss__icon_draw and the click/drag path (core/mouse-click.c, + * core/mouse-move.c) so hit-testing cannot drift from what is drawn. + * "screen_box" is the icon's screen-space box, as wuss__icon_box_to_screen + * gives it. */ + +/* The inner rect: "screen_box" (the icon's full bbox) inset by + * WUSS_SLIDER_GAP on all four sides. This is both the groove drawn inside it + * and the only area wuss__icon_hit_test accepts clicks/drags on; the gap and + * the rest of "screen_box" are painted with slider.surround and never + * interactive. */ +void wuss__slider_groove_box(const box_t *screen_box, box_t *out); + +/* Convert "value" (clamped to [lo,hi], lo <= hi) to a fill length in pixels + * along "groove"'s long axis, 0 at lo and the full long-axis extent at hi. + * For wuss_SLIDER_HORIZONTAL pixel 0 is the groove's left (x0); for VERTICAL + * it is the groove's bottom (y1), so value grows upward on screen even + * though screen y grows downward. */ +int wuss__slider_value_to_px(const box_t *groove, + wuss_slider_orientation_t orientation, + int value, + int lo, + int hi); + +/* Convert a screen-space point's long-axis coordinate back to a value in + * [lo,hi] (lo <= hi), inverting wuss__slider_value_to_px (same pixel-0 end + * per orientation). */ +int wuss__slider_px_to_value(const box_t *groove, + wuss_slider_orientation_t orientation, + point_t screen_point, + int lo, + int hi); + +/* "icon" (a slider icon of "window") maps a raw screen-space point (as passed + * to wuss_mouse_click/wuss_mouse_move) to the value its groove would read at + * that point, honouring a min > max reversed fill. Shared by the click-jump + * and drag-continuation paths (core/mouse-click.c, core/mouse-move.c). */ +int wuss__slider_value_for_point(wuss_window_t *window, + const wuss_icon_t *icon, + point_t screen_point); + +/* Invalidate exactly this icon's bbox on "window", via wuss_window_invalidate, + * so a set_text / pressed-state / hide change repaints just the icon. "icon" + * must be an icon of "window". */ +void wuss__icon_invalidate(wuss_window_t *window, const wuss_icon_t *icon); + +/* Validate a spec against the palette and fill "out" with a detached icon: + * "out->spec" is a copy of "spec" with fg/bg/swatch resolved to palette + * indices and, for a BITMAP, u.bitmap.image resolved from u.bitmap.set. + * out->spec.text is not owned -- it aliases spec->text, or "" when that is + * NULL. Shared by wuss_icon_create and wuss_icon_plot. Returns + * result_WUSS_BAD_ICON / result_WUSS_BAD_COLOUR as wuss_icon_create + * documents, else result_OK. */ result_t wuss__icon_from_spec(const wuss_t *wuss, const wuss_icon_spec_t *spec, wuss_icon_t *out); @@ -100,10 +157,11 @@ result_t wuss__icon_from_spec(const wuss_t *wuss, * the surviving content piece and the background already filled. "content" is * the window's full (unclipped) content box, screen space; "scroll" is * window->scroll. */ -void wuss__icon_draw(wuss_t *wuss, - const wuss_icon_t *icon, - const box_t *content, - point_t scroll); +void wuss__icon_draw(wuss_t *wuss, + const wuss_window_t *window, + const wuss_icon_t *icon, + const box_t *content, + point_t scroll); /* Hit-test every visible, enabled button icon of "window" against a point given * in virtual document space. Returns the topmost (last-created wins) match, or diff --git a/libraries/wuss/icon/create-array.c b/libraries/wuss/icon/create-array.c index 879b410..4fcd343 100644 --- a/libraries/wuss/icon/create-array.c +++ b/libraries/wuss/icon/create-array.c @@ -31,7 +31,7 @@ result_t wuss_icon_create_array(wuss_window_t *window, /* all-or-nothing: unwind the icons this call already created. They are * the last (i) entries on the window's icon list, newest last. */ for (j = 0; j < i; j++) - wuss_icon_delete(window->icons[window->nicons - 1]); + wuss_icon_delete(window, window->icons[window->nicons - 1]); return rc; } diff --git a/libraries/wuss/icon/create.c b/libraries/wuss/icon/create.c index be9ef71..4b281d8 100644 --- a/libraries/wuss/icon/create.c +++ b/libraries/wuss/icon/create.c @@ -1,8 +1,6 @@ /* wuss/icon/create.c -- create a work-area icon */ #include -#include -#include #ifdef FORTIFY #include "fortify/fortify.h" @@ -14,14 +12,10 @@ result_t wuss_icon_create(wuss_window_t *window, const wuss_icon_spec_t *spec, wuss_icon_t **icon) { - result_t rc; - wuss_t *w; - wuss_icon_t *it; - wuss_icon_t **grown; - wuss_icon_t scratch; - const char *src; - size_t len; - int newcap; + result_t rc; + wuss_t *w; + wuss_icon_t *it; + wuss_icon_t scratch; assert(window != NULL); assert(spec != NULL); @@ -32,36 +26,28 @@ result_t wuss_icon_create(wuss_window_t *window, if (rc != result_OK) return rc; - if (window->nicons == window->cap_icons) - { - newcap = (window->cap_icons == 0) ? 4 : window->cap_icons * 2; - grown = wuss__realloc(w, window->icons, newcap * sizeof(*window->icons)); - if (grown == NULL) - return result_OOM; - window->icons = grown; - window->cap_icons = newcap; - } + if (wuss__array_grow(&w->alloc, (void **) &window->icons, + sizeof(*window->icons), window->nicons, + &window->cap_icons, 1, 4) != 0) + return result_OOM; it = wuss__malloc(w, sizeof(*it)); if (it == NULL) return result_OOM; - *it = scratch; /* scratch.text aliases spec->text; replaced with an owned copy below */ - it->window = window; + *it = scratch; /* scratch.spec.text aliases spec->text; replaced with an owned copy below */ - src = (spec->text != NULL) ? spec->text : ""; - len = strlen(src); - it->text = wuss__malloc(w, len + 1); - if (it->text == NULL) + it->spec.text = wuss__alloc_strdup(&w->alloc, + spec->text != NULL ? spec->text : ""); + if (it->spec.text == NULL) { wuss__free(w, it); return result_OOM; } - memcpy(it->text, src, len + 1); window->icons[window->nicons++] = it; - wuss__icon_invalidate(it); + wuss__icon_invalidate(window, it); if (icon != NULL) *icon = it; diff --git a/libraries/wuss/icon/delete.c b/libraries/wuss/icon/delete.c index cc3f1be..116c774 100644 --- a/libraries/wuss/icon/delete.c +++ b/libraries/wuss/icon/delete.c @@ -8,22 +8,25 @@ #include "../core/impl.h" -void wuss_icon_delete(wuss_icon_t *icon) +void wuss_icon_delete(wuss_window_t *window, wuss_icon_t *icon) { - wuss_window_t *window; - int i; + int i; if (icon == NULL) return; - window = icon->window; - if (window->wuss->pressed_icon == icon) - window->wuss->pressed_icon = NULL; + { + window->wuss->pressed_icon = NULL; + window->wuss->pressed_window = NULL; + } if (window->wuss->hover_icon == icon) - window->wuss->hover_icon = NULL; + { + window->wuss->hover_icon = NULL; + window->wuss->hover_window = NULL; + } - wuss__icon_invalidate(icon); + wuss__icon_invalidate(window, icon); for (i = 0; i < window->nicons; i++) { @@ -34,6 +37,6 @@ void wuss_icon_delete(wuss_icon_t *icon) } } - wuss__free(window->wuss, icon->text); + wuss__free(window->wuss, (char *) icon->spec.text); wuss__free(window->wuss, icon); } diff --git a/libraries/wuss/icon/draw.c b/libraries/wuss/icon/draw.c index 954e77a..7872a8d 100644 --- a/libraries/wuss/icon/draw.c +++ b/libraries/wuss/icon/draw.c @@ -17,21 +17,32 @@ /* Shared per-draw state, resolved once by wuss__icon_draw and handed to each * type's draw helper. "b" is the icon's screen box; "fg" is its resolved * foreground; "font" is the icon's selected weight (falling back to slot 0); - * "font_height" is 0 and "have_font" is 0 when there is no font or no text to - * draw. */ + * "font_height", "font_ascent" and "have_font" are 0 when there is no font or + * no text to draw. Draw helpers vertically centre text via + * icon_text_baseline_y(). */ typedef struct icon_draw_ctx { - wuss_t *wuss; - screen_t *scr; - const wuss_icon_t *icon; - bmfont_t *font; - box_t b; - colour_t fg; - int font_height; - int have_font; + wuss_t *wuss; + const wuss_window_t *window; + screen_t *scr; + const wuss_icon_t *icon; + bmfont_t *font; + box_t b; + colour_t fg; + int font_height; + int font_ascent; + int have_font; } icon_draw_ctx_t; +/* Baseline y for text centred by ascender alone (not the descender too, so a + * run of ascender-only text -- e.g. all caps and digits -- sits visually + * centred rather than pulled high by unused descender space) in box "b". */ +static int icon_text_baseline_y(const icon_draw_ctx_t *c, const box_t *b) +{ + return b->y0 + (b->y1 - b->y0 - c->font_ascent) / 2 + c->font_ascent; +} + /* ----------------------------------------------------------------------- */ static void icon_fill_bevel(screen_t *scr, @@ -40,8 +51,7 @@ static void icon_fill_bevel(screen_t *scr, colour_t light, colour_t dark) { - screen_fill_rect(scr, b->x0, b->y0, - SIZE2D(b->x1 - b->x0, b->y1 - b->y0), fill); + screen_fill_rect(scr, b->x0, b->y0, box_size(b), fill); screen_draw_bevel_edge(scr, b, light, dark); } @@ -62,12 +72,10 @@ static void icon_draw_action_border(screen_t *scr, ring = *b; screen_draw_bevel_edge(scr, &ring, dark, light); - ring = (box_t) BOX_POS_SIZE(b->x0 + 2, b->y0 + 2, - b->x1 - b->x0 - 4, b->y1 - b->y0 - 4); + ring = box_grown(b, -2); screen_draw_bevel_edge(scr, &ring, accent, accent); - ring = (box_t) BOX_POS_SIZE(b->x0 + 4, b->y0 + 4, - b->x1 - b->x0 - 8, b->y1 - b->y0 - 8); + ring = box_grown(b, -4); screen_draw_bevel_edge(scr, &ring, pressed ? dark : light, pressed ? light : dark); } @@ -86,8 +94,7 @@ static void icon_draw_divider_border(screen_t *scr, ring = *b; screen_draw_bevel_edge(scr, &ring, divider, light); - ring = (box_t) BOX_POS_SIZE(b->x0 + 2, b->y0 + 2, - b->x1 - b->x0 - 4, b->y1 - b->y0 - 4); + ring = box_grown(b, -2); screen_draw_bevel_edge(scr, &ring, light, divider); } @@ -98,15 +105,15 @@ static void icon_draw_divider_border(screen_t *scr, static colour_t icon_blend_ground(const icon_draw_ctx_t *c, colour_t fallback) { - const wuss_icon_t *icon = c->icon; + const wuss_icon_spec_t *icon = &c->icon->spec; if (icon->bg != wuss_NO_BACKGROUND) return c->wuss->palette[icon->bg]; - if (icon->window->bg.colour != wuss_NO_BACKGROUND) - return (icon->window->bg.pattern != screen_PATTERN_SOLID) - ? c->wuss->palette[icon->window->bg.pattern_bg] - : c->wuss->palette[icon->window->bg.colour]; + if (c->window->bg.colour != wuss_NO_BACKGROUND) + return (c->window->bg.pattern != screen_PATTERN_SOLID) + ? c->wuss->palette[c->window->bg.pattern_bg] + : c->wuss->palette[c->window->bg.colour]; return fallback; } @@ -124,9 +131,9 @@ static int wuss__draw_symbol_glyph(const wuss_t *wuss, { bmfont_t *font; point_t pos; - int font_width, font_height; + int font_width, font_height, font_ascent; - font = wuss->fonts[WUSS_SYMBOL_FONT]; + font = wuss->fonts.fonts[WUSS_SYMBOL_FONT]; if (font == NULL) return 0; @@ -134,10 +141,10 @@ static int wuss__draw_symbol_glyph(const wuss_t *wuss, (unsigned char) glyph - ' ' >= bmfont_get_count(font)) return 0; - bmfont_get_info(font, &font_width, &font_height); + bmfont_get_info(font, &font_width, &font_height, &font_ascent, NULL); pos.x = centre.x - font_width / 2; - pos.y = centre.y - font_height / 2; - bmfont_draw(font, scr, &glyph, 1, ink, ground, &pos, NULL); + pos.y = centre.y - font_height / 2 + font_ascent; + bmfont_draw(font, scr, &glyph, 1, ink, ground, NULL, &pos, NULL); return 1; } @@ -147,8 +154,8 @@ static void wuss__icon_draw_pattern(const icon_draw_ctx_t *c, const box_t *content, point_t scroll) { - const wuss_icon_t *icon = c->icon; - colour_t pat_fg; + const wuss_icon_spec_t *icon = &c->icon->spec; + colour_t pat_fg; /* disabled: fold the pattern into its own ground so it reads as greyed, * mirroring the button's fg-swap */ @@ -159,7 +166,7 @@ static void wuss__icon_draw_pattern(const icon_draw_ctx_t *c, { pattern_t pat; - pat = pattern_from_preset(icon->pattern, + pat = pattern_from_preset(icon->u.pattern.tile, pat_fg, c->wuss->palette[icon->bg]); pat.origin = POINT(content->x0 - scroll.x, content->y0 - scroll.y); screen_fill_pattern(c->scr, &c->b, &pat); @@ -170,24 +177,23 @@ static void wuss__icon_draw_pattern(const icon_draw_ctx_t *c, static void wuss__icon_draw_label(const icon_draw_ctx_t *c) { - const wuss_icon_t *icon = c->icon; - const box_t *b = &c->b; - colour_t bg; - point_t pos; - bmfont_width_t width; + const wuss_icon_spec_t *icon = &c->icon->spec; + const box_t *b = &c->b; + colour_t bg; + point_t pos; + bmfont_width_t width; if (icon->bg != wuss_NO_BACKGROUND) { bg = c->wuss->palette[icon->bg]; - screen_fill_rect(c->scr, b->x0, b->y0, - SIZE2D(b->x1 - b->x0, b->y1 - b->y0), bg); + screen_fill_rect(c->scr, b->x0, b->y0, box_size(b), bg); } else { bg = icon_blend_ground(c, c->fg); } - if (icon->border != wuss_ICON_BORDER_NONE) + if (icon->u.label.border != wuss_ICON_BORDER_NONE) { colour_t light, dark, accent, divider; @@ -196,11 +202,11 @@ static void wuss__icon_draw_label(const icon_draw_ctx_t *c) accent = c->wuss->palette[c->wuss->accent]; /* the action-button fill */ divider = c->wuss->palette[c->wuss->bevel_divider]; - if (icon->border == wuss_ICON_BORDER_ACTION) + if (icon->u.label.border == wuss_ICON_BORDER_ACTION) { icon_draw_action_border(c->scr, b, light, dark, accent, 0); } - else if (icon->border == wuss_ICON_BORDER_DIVIDER) + else if (icon->u.label.border == wuss_ICON_BORDER_DIVIDER) { icon_draw_divider_border(c->scr, b, light, divider); } @@ -210,7 +216,7 @@ static void wuss__icon_draw_label(const icon_draw_ctx_t *c) * bevel ring. */ box_t ring = *b; - if (icon->border == wuss_ICON_BORDER_RIDGE) + if (icon->u.label.border == wuss_ICON_BORDER_RIDGE) screen_draw_bevel_edge(c->scr, &ring, light, dark); else screen_draw_bevel_edge(c->scr, &ring, dark, light); @@ -232,7 +238,7 @@ static void wuss__icon_draw_label(const icon_draw_ctx_t *c) else pos.x = b->x0 + 1; - pos.y = b->y0 + (b->y1 - b->y0 - c->font_height) / 2; + pos.y = icon_text_baseline_y(c, b); wuss__text_draw(c->font, c->scr, icon->text, (int) strlen(icon->text), c->fg, bg, &pos, NULL); @@ -242,10 +248,10 @@ static void wuss__icon_draw_label(const icon_draw_ctx_t *c) static void wuss__icon_draw_frame(const icon_draw_ctx_t *c) { - const wuss_icon_t *icon = c->icon; - const box_t *b = &c->b; - colour_t bg, light, divider; - int cap_w, cap_x, gap_x0, gap_x1; + const wuss_icon_spec_t *icon = &c->icon->spec; + const box_t *b = &c->b; + colour_t bg, light, divider; + int cap_w, cap_x, gap_x0, gap_x1; bg = icon_blend_ground(c, c->fg); light = c->wuss->palette[c->wuss->bevel_light]; @@ -283,7 +289,7 @@ static void wuss__icon_draw_frame(const icon_draw_ctx_t *c) point_t pos; pos.x = cap_x; - pos.y = b->y0; + pos.y = b->y0 + c->font_ascent; wuss__text_draw(c->font, c->scr, icon->text, (int) strlen(icon->text), c->fg, bg, &pos, NULL); } @@ -293,12 +299,12 @@ static void wuss__icon_draw_frame(const icon_draw_ctx_t *c) static void wuss__icon_draw_button(const icon_draw_ctx_t *c) { - const wuss_icon_t *icon = c->icon; - const box_t *b = &c->b; - colour_t light, dark, base, label; - int pressed, is_default; + const wuss_icon_spec_t *icon = &c->icon->spec; + const box_t *b = &c->b; + colour_t light, dark, base, label; + int pressed, is_default; - pressed = wuss__icon_pressed(icon); + pressed = wuss__icon_pressed(c->icon); is_default = (icon->flags & wuss_ICON_FLAGS_DEFAULT) != 0; /* a plain button whose spec left bg as wuss_NO_BACKGROUND takes the config @@ -325,8 +331,7 @@ static void wuss__icon_draw_button(const icon_draw_ctx_t *c) * "action" surround as wuss_ICON_BORDER_ACTION, distinct from the plain * bevelled buttons around it. */ - screen_fill_rect(c->scr, b->x0, b->y0, - SIZE2D(b->x1 - b->x0, b->y1 - b->y0), base); + screen_fill_rect(c->scr, b->x0, b->y0, box_size(b), base); icon_draw_action_border(c->scr, b, light, dark, c->wuss->palette[c->wuss->accent], pressed); } @@ -348,7 +353,7 @@ static void wuss__icon_draw_button(const icon_draw_ctx_t *c) interior_w, &split_point, &width); pos.x = b->x0 + ((b->x1 - b->x0) - width) / 2; - pos.y = b->y0 + (b->y1 - b->y0 - c->font_height) / 2; + pos.y = icon_text_baseline_y(c, b); if (pressed) { pos.x += 1; @@ -362,62 +367,52 @@ static void wuss__icon_draw_button(const icon_draw_ctx_t *c) /* ----------------------------------------------------------------------- */ -/* Blit the icon-set bitmap for a radio/option's current state, centred in the - * glyph square "g", clipped to it. Names are radon/radoff for RADIO and - * opton/optoff for OPTION. Returns 1 when a bitmap was drawn, 0 when the set is - * absent or lacks that entry -- the caller then draws the vector glyph. */ -static int wuss__icon_blit_radio_option(const icon_draw_ctx_t *c, - const box_t *g) +/* The icon-set bitmap for a radio/option's current state, or NULL when the set + * is absent or lacks that entry. Names are radon/radoff for RADIO and + * opton/optoff for OPTION. */ +static const bitmap_t *wuss__icon_radio_option_bitmap(const icon_draw_ctx_t *c) { - const char *name; - const bitmap_t *bm; - screen_t clipped; - int idx, bx, by; + const char *name; + int idx; - if (c->icon->type == wuss_ICON_TYPE_RADIO) + if (c->icon->spec.type == wuss_ICON_TYPE_RADIO) name = wuss__icon_selected(c->icon) ? "radon" : "radoff"; else name = wuss__icon_selected(c->icon) ? "opton" : "optoff"; idx = wuss_icons_lookup(c->wuss, name); if (idx < 0) - return 0; - - bm = wuss_icons_bitmap(c->wuss, idx); - if (bm == NULL) - return 0; - - bx = g->x0 + ((g->x1 - g->x0) - bm->size.w) / 2; - by = g->y0 + ((g->y1 - g->y0) - bm->size.h) / 2; + return NULL; - clipped = *c->scr; - if (box_intersection(&c->scr->clip, g, &clipped.clip)) - return 1; /* fully clipped away, but still "handled" -- no vector fallback */ - - screen_copy_bitmap(&clipped, bx, by, bm); - return 1; + return wuss_icons_bitmap(c->wuss, idx); } -/* wuss_ICON_TYPE_RADIO and wuss_ICON_TYPE_OPTION: a font-height square glyph at - * the left, vertically centred, with the label to its right. If the icon set - * carries radon/radoff (RADIO) or opton/optoff (OPTION) those bitmaps are - * blitted for the state; otherwise RADIO draws a square ring with a solid - * centre when selected and OPTION draws a box with a tick when selected. */ +/* wuss_ICON_TYPE_RADIO and wuss_ICON_TYPE_OPTION: a glyph at the left, + * vertically centred, with the label to its right. If the icon set carries + * radon/radoff (RADIO) or opton/optoff (OPTION) those bitmaps are blitted for + * the state, and the glyph box and label offset follow the bitmap's own size; + * otherwise a font-height square is used -- RADIO draws a square ring with a + * solid centre when selected and OPTION draws a box with a tick when + * selected. */ static void wuss__icon_draw_radio_option(const icon_draw_ctx_t *c) { - const wuss_icon_t *icon = c->icon; - const box_t *b = &c->b; - colour_t glyph, bg; - box_t g; - int gsz, gy, tx; + const wuss_icon_spec_t *icon = &c->icon->spec; + const box_t *b = &c->b; + const bitmap_t *bm; + colour_t glyph, bg; + box_t g; + int gw, gh, gy, tx; - gsz = CLAMP(c->font_height, 8, b->y1 - b->y0); - gy = b->y0 + (b->y1 - b->y0 - gsz) / 2; + bm = wuss__icon_radio_option_bitmap(c); + + gw = bm ? bm->size.w : CLAMP(c->font_height, 8, b->x1 - b->x0); + gh = bm ? bm->size.h : CLAMP(c->font_height, 8, b->y1 - b->y0); + gy = b->y0 + (b->y1 - b->y0 - gh) / 2; g.x0 = b->x0; g.y0 = gy; - g.x1 = b->x0 + gsz; - g.y1 = gy + gsz; + g.x1 = b->x0 + gw; + g.y1 = gy + gh; glyph = (icon->flags & wuss_ICON_FLAGS_DISABLED) ? c->wuss->palette[c->wuss->bevel_dark] @@ -426,12 +421,17 @@ static void wuss__icon_draw_radio_option(const icon_draw_ctx_t *c) bg = icon_blend_ground(c, glyph); if (icon->bg != wuss_NO_BACKGROUND) - screen_fill_rect(c->scr, b->x0, b->y0, - SIZE2D(b->x1 - b->x0, b->y1 - b->y0), bg); + screen_fill_rect(c->scr, b->x0, b->y0, box_size(b), bg); + else + /* no explicit bg to repaint the whole box, but the state we are about to + * draw (a smaller radon/radoff swap, or a cleared tick/centre) can leave + * fewer pixels than last time -- clear just the glyph box so no stale + * glyph shows through. The label to its right never changes on select. */ + screen_fill_rect(c->scr, g.x0, g.y0, box_size(&g), bg); - if (wuss__icon_blit_radio_option(c, &g)) + if (bm != NULL) { - /* bitmap drawn -- fall through to the label */ + screen_copy_bitmap(c->scr, g.x0, g.y0, bm); } else if (icon->type == wuss_ICON_TYPE_RADIO) { @@ -440,20 +440,19 @@ static void wuss__icon_draw_radio_option(const icon_draw_ctx_t *c) screen_draw_line(c->scr, g.x0 + 2, g.y1 - 1, g.x1 - 3, g.y1 - 1, glyph); screen_draw_line(c->scr, g.x0, g.y0 + 2, g.x0, g.y1 - 3, glyph); screen_draw_line(c->scr, g.x1 - 1, g.y0 + 2, g.x1 - 1, g.y1 - 3, glyph); - if (wuss__icon_selected(icon)) + if (wuss__icon_selected(c->icon)) screen_fill_rect(c->scr, g.x0 + 3, g.y0 + 3, - SIZE2D(gsz - 6, gsz - 6), glyph); + SIZE2D(gw - 6, gh - 6), glyph); } else { /* a box; a tick (two strokes) when selected */ - screen_draw_rect(c->scr, g.x0, g.y0, - SIZE2D(g.x1 - g.x0, g.y1 - g.y0), glyph); - if (wuss__icon_selected(icon)) + screen_draw_rect(c->scr, g.x0, g.y0, box_size(&g), glyph); + if (wuss__icon_selected(c->icon)) { - screen_draw_line(c->scr, g.x0 + 2, g.y0 + gsz / 2, - g.x0 + gsz / 2 - 1, g.y1 - 3, glyph); - screen_draw_line(c->scr, g.x0 + gsz / 2 - 1, g.y1 - 3, + screen_draw_line(c->scr, g.x0 + 2, g.y0 + gh / 2, + g.x0 + gw / 2 - 1, g.y1 - 3, glyph); + screen_draw_line(c->scr, g.x0 + gw / 2 - 1, g.y1 - 3, g.x1 - 3, g.y0 + 2, glyph); } } @@ -472,7 +471,7 @@ static void wuss__icon_draw_radio_option(const icon_draw_ctx_t *c) NOT_USED(width); pos.x = tx; - pos.y = b->y0 + (b->y1 - b->y0 - c->font_height) / 2; + pos.y = icon_text_baseline_y(c, b); wuss__text_draw(c->font, c->scr, icon->text, (int) strlen(icon->text), glyph, bg, &pos, NULL); } @@ -485,7 +484,7 @@ static void wuss__icon_draw_bitmap(const icon_draw_ctx_t *c) const box_t *b = &c->b; screen_t clipped; - if (c->icon->bitmap == NULL) + if (c->icon->spec.u.bitmap.image == NULL) return; /* screen_copy_bitmap clips to scr->clip and does not scale, so narrow the @@ -495,20 +494,20 @@ static void wuss__icon_draw_bitmap(const icon_draw_ctx_t *c) if (box_intersection(&c->scr->clip, b, &clipped.clip)) return; - screen_copy_bitmap(&clipped, b->x0, b->y0, c->icon->bitmap); + screen_copy_bitmap(&clipped, b->x0, b->y0, c->icon->spec.u.bitmap.image); } /* ----------------------------------------------------------------------- */ static void wuss__icon_draw_menu_entry(const icon_draw_ctx_t *c) { - const wuss_icon_t *icon = c->icon; - const box_t *b = &c->b; - colour_t ink, ground, text_ink, text_ground, tmp; - int disabled, highlit, pad, text_x0, text_x1; + const wuss_icon_spec_t *icon = &c->icon->spec; + const box_t *b = &c->b; + colour_t ink, ground, text_ink, text_ground, tmp; + int disabled, highlit, pad, text_x0, text_x1; disabled = (icon->flags & wuss_ICON_FLAGS_DISABLED) != 0; - highlit = wuss__icon_hovered(icon) && !disabled; + highlit = wuss__icon_hovered(c->icon) && !disabled; pad = 4; /* resolve the row's own ink over its own/inherited ground */ @@ -535,12 +534,11 @@ static void wuss__icon_draw_menu_entry(const icon_draw_ctx_t *c) screen_fill_rect(c->scr, text_x0, b->y0, SIZE2D(text_x1 - text_x0, b->y1 - b->y0), text_ground); else if (icon->bg != wuss_NO_BACKGROUND) - screen_fill_rect(c->scr, b->x0, b->y0, - SIZE2D(b->x1 - b->x0, b->y1 - b->y0), ground); + screen_fill_rect(c->scr, b->x0, b->y0, box_size(b), ground); /* left-edge colour chip (wins over the tick) or tick when selected */ if ((icon->flags & wuss_ICON_FLAGS_SWATCH) && - icon->swatch != wuss_NO_BACKGROUND) + icon->u.menu_entry.swatch != wuss_NO_BACKGROUND) { int cx, cy, h; @@ -548,10 +546,10 @@ static void wuss__icon_draw_menu_entry(const icon_draw_ctx_t *c) cx = b->x0 + pad; cy = b->y0 + (b->y1 - b->y0 - h) / 2; screen_fill_rect(c->scr, cx, cy, SIZE2D(h, h), - c->wuss->palette[icon->swatch]); + c->wuss->palette[icon->u.menu_entry.swatch]); screen_draw_rect(c->scr, cx, cy, SIZE2D(h, h), ink); /* 1px border */ } - else if (wuss__icon_selected(icon)) + else if (wuss__icon_selected(c->icon)) { int cx, cy, h; point_t centre; @@ -602,7 +600,7 @@ static void wuss__icon_draw_menu_entry(const icon_draw_ctx_t *c) wuss__text_measure(c->font, " ", 1, INT_MAX, NULL, &space_w); pos.x = text_x0 + (int) space_w; - pos.y = b->y0 + (b->y1 - b->y0 - c->font_height) / 2; + pos.y = icon_text_baseline_y(c, b); wuss__text_draw(c->font, c->scr, icon->text, (int) strlen(icon->text), text_ink, text_ground, &pos, NULL); } @@ -625,40 +623,95 @@ static void wuss__icon_draw_rule(const icon_draw_ctx_t *c) /* ----------------------------------------------------------------------- */ -void wuss__icon_draw(wuss_t *wuss, - const wuss_icon_t *icon, - const box_t *content, - point_t scroll) +static void wuss__icon_draw_slider(const icon_draw_ctx_t *c) +{ + const wuss_icon_spec_t *icon = &c->icon->spec; + colour_t surround, track, value, light, dark; + box_t surround_box, groove, fill; + int lo, hi, px; + + surround = c->wuss->palette[c->wuss->slider_surround]; + track = c->wuss->palette[c->wuss->slider_track]; + value = c->wuss->palette[c->wuss->slider_value]; + light = c->wuss->palette[c->wuss->bevel_light]; + dark = c->wuss->palette[c->wuss->bevel_dark]; + + /* draw the surround */ + + surround_box = box_grown(&c->b, -2); /* inset by bevel width to avoid overdraw */ + screen_fill_rect(c->scr, surround_box.x0, surround_box.y0, + box_size(&surround_box), surround); + + /* draw the bevel */ + + screen_draw_bevel_edge(c->scr, &c->b, dark, light); + + wuss__slider_groove_box(&surround_box, &groove); + + /* draw the track */ + + lo = MIN(icon->u.slider.min, icon->u.slider.max); + hi = MAX(icon->u.slider.min, icon->u.slider.max); + px = wuss__slider_value_to_px(&groove, icon->u.slider.orientation, + c->icon->value, lo, hi); + if (icon->u.slider.min > icon->u.slider.max) + px = (icon->u.slider.orientation == wuss_SLIDER_HORIZONTAL) + ? (groove.x1 - groove.x0) - px + : (groove.y1 - groove.y0) - px; + + fill = groove; + if (icon->u.slider.orientation == wuss_SLIDER_HORIZONTAL) + fill.x1 = fill.x0 + px; + else + fill.y0 = fill.y1 - px; + + screen_fill_rect(c->scr, groove.x0, groove.y0, box_size(&groove), track); + if (px > 0) + screen_fill_rect(c->scr, fill.x0, fill.y0, box_size(&fill), value); +} + +/* ----------------------------------------------------------------------- */ + +void wuss__icon_draw(wuss_t *wuss, + const wuss_window_t *window, + const wuss_icon_t *icon, + const box_t *content, + point_t scroll) { - icon_draw_ctx_t c; - int fontidx; + const wuss_icon_spec_t *spec = &icon->spec; + icon_draw_ctx_t c; + int fontidx; - if (icon->flags & wuss_ICON_FLAGS_HIDDEN) + if (spec->flags & wuss_ICON_FLAGS_HIDDEN) return; - wuss__icon_box_to_screen(content, scroll, &icon->bbox, &c.b); + wuss__icon_box_to_screen(content, scroll, &spec->bbox, &c.b); if (box_is_empty(&c.b)) return; - c.wuss = wuss; - c.scr = wuss->scr; - c.icon = icon; - c.fg = wuss->palette[icon->fg]; + c.wuss = wuss; + c.window = window; + c.scr = wuss->scr; + c.icon = icon; + c.fg = wuss->palette[spec->fg]; /* pick the icon's requested weight; fall back to the system font */ - fontidx = wuss_ICON_FONT_OF(icon->flags); - c.font = wuss->fonts[fontidx]; + fontidx = wuss_ICON_FONT_OF(spec->flags); + c.font = wuss->fonts.fonts[fontidx]; if (c.font == NULL) - c.font = wuss->fonts[0]; + c.font = wuss->fonts.fonts[0]; - c.have_font = (c.font != NULL && icon->text[0] != '\0'); + c.have_font = (c.font != NULL && spec->text[0] != '\0'); if (c.have_font) - bmfont_get_info(c.font, NULL, &c.font_height); + bmfont_get_info(c.font, NULL, &c.font_height, &c.font_ascent, NULL); else + { c.font_height = 0; + c.font_ascent = 0; + } - switch (icon->type) + switch (spec->type) { case wuss_ICON_TYPE_PATTERN: wuss__icon_draw_pattern(&c, content, scroll); @@ -672,7 +725,7 @@ void wuss__icon_draw(wuss_t *wuss, wuss__icon_draw_frame(&c); break; - case wuss_ICON_TYPE_BUTTON: + case wuss_ICON_TYPE_ACTION: wuss__icon_draw_button(&c); break; @@ -692,5 +745,18 @@ void wuss__icon_draw(wuss_t *wuss, case wuss_ICON_TYPE_RULE: wuss__icon_draw_rule(&c); break; + + case wuss_ICON_TYPE_SLIDER: + wuss__icon_draw_slider(&c); + break; + + /* reserved types with no renderer yet: fall back to a plain label */ + case wuss_ICON_TYPE_DISPLAY: + case wuss_ICON_TYPE_WRITABLE: + case wuss_ICON_TYPE_NUMBER: + case wuss_ICON_TYPE_STRING_SET: + case wuss_ICON_TYPE_DRAGGABLE: + wuss__icon_draw_label(&c); + break; } } diff --git a/libraries/wuss/icon/free.c b/libraries/wuss/icon/free.c index cb4c5e0..b2ce4fc 100644 --- a/libraries/wuss/icon/free.c +++ b/libraries/wuss/icon/free.c @@ -17,7 +17,7 @@ void wuss__icons_free(wuss_window_t *window) for (i = 0; i < window->nicons; i++) { - wuss__free(w, window->icons[i]->text); + wuss__free(w, (char *) window->icons[i]->spec.text); wuss__free(w, window->icons[i]); } diff --git a/libraries/wuss/icon/from-spec.c b/libraries/wuss/icon/from-spec.c index e1e0852..5cd42fb 100644 --- a/libraries/wuss/icon/from-spec.c +++ b/libraries/wuss/icon/from-spec.c @@ -25,13 +25,13 @@ result_t wuss__icon_from_spec(const wuss_t *w, has_swatch = (spec->type == wuss_ICON_TYPE_MENU_ENTRY) && (spec->flags & wuss_ICON_FLAGS_SWATCH); - swatch = has_swatch ? wuss__resolve_colour(w, spec->swatch) + swatch = has_swatch ? wuss__resolve_colour(w, spec->u.menu_entry.swatch) : wuss_NO_BACKGROUND; switch (spec->type) { case wuss_ICON_TYPE_LABEL: - case wuss_ICON_TYPE_BUTTON: + case wuss_ICON_TYPE_ACTION: case wuss_ICON_TYPE_PATTERN: case wuss_ICON_TYPE_FRAME: case wuss_ICON_TYPE_RADIO: @@ -39,24 +39,36 @@ result_t wuss__icon_from_spec(const wuss_t *w, case wuss_ICON_TYPE_BITMAP: case wuss_ICON_TYPE_MENU_ENTRY: case wuss_ICON_TYPE_RULE: + case wuss_ICON_TYPE_DISPLAY: + case wuss_ICON_TYPE_WRITABLE: + case wuss_ICON_TYPE_NUMBER: + case wuss_ICON_TYPE_STRING_SET: + case wuss_ICON_TYPE_DRAGGABLE: + break; + + case wuss_ICON_TYPE_SLIDER: + if (spec->u.slider.orientation != wuss_SLIDER_HORIZONTAL && + spec->u.slider.orientation != wuss_SLIDER_VERTICAL) + return result_WUSS_BAD_ICON; break; default: return result_WUSS_BAD_ICON; } - /* a BUTTON may leave bg unset -- it then draws on the config button face + /* an ACTION may leave bg unset -- it then draws on the config button face * (wuss->button_bg); a PATTERN needs a concrete clear-bit colour */ if (spec->type == wuss_ICON_TYPE_PATTERN && bg == wuss_NO_BACKGROUND) return result_WUSS_BAD_ICON; - /* A BITMAP icon draws spec->bitmap, or -- when that is NULL -- the loaded - * icon-set entry spec->icon_set names (wuss_ICON_SET encodes the 0-based - * index +1, so 0 means "no entry"). */ - bitmap = spec->bitmap; - if (spec->type == wuss_ICON_TYPE_BITMAP && bitmap == NULL && spec->icon_set > 0) + /* A BITMAP icon draws spec->u.bitmap.image, or -- when that is NULL -- the + * loaded icon-set entry spec->u.bitmap.set names (wuss_ICON_SET encodes the + * 0-based index +1, so 0 means "no entry"). */ + bitmap = spec->u.bitmap.image; + if (spec->type == wuss_ICON_TYPE_BITMAP && bitmap == NULL && + spec->u.bitmap.set > 0) { - bitmap = wuss_icons_bitmap(w, spec->icon_set - 1); + bitmap = wuss_icons_bitmap(w, spec->u.bitmap.set - 1); if (bitmap == NULL) return result_WUSS_BAD_INDEX; } @@ -65,11 +77,11 @@ result_t wuss__icon_from_spec(const wuss_t *w, return result_WUSS_BAD_ICON; if (spec->type == wuss_ICON_TYPE_PATTERN && - spec->pattern >= screen_PATTERN__LIMIT) + spec->u.pattern.tile >= screen_PATTERN__LIMIT) return result_WUSS_BAD_ICON; if (spec->type == wuss_ICON_TYPE_LABEL && - spec->border > wuss_ICON_BORDER_DIVIDER) + spec->u.label.border > wuss_ICON_BORDER_DIVIDER) return result_WUSS_BAD_ICON; if (fg >= w->npalette) @@ -81,21 +93,78 @@ result_t wuss__icon_from_spec(const wuss_t *w, if (has_swatch && swatch >= w->npalette) return result_WUSS_BAD_COLOUR; - out->window = NULL; - out->bbox = spec->bbox; - out->type = spec->type; - out->text = (char *) ((spec->text != NULL) ? spec->text : ""); - out->fg = fg; - out->bg = bg; - out->pattern = (spec->type == wuss_ICON_TYPE_PATTERN) ? spec->pattern - : screen_PATTERN_SOLID; - out->bitmap = (spec->type == wuss_ICON_TYPE_BITMAP) ? bitmap : NULL; - out->group = (spec->type == wuss_ICON_TYPE_RADIO) ? spec->group : 0; - out->swatch = swatch; - out->border = (spec->type == wuss_ICON_TYPE_LABEL) ? spec->border - : wuss_ICON_BORDER_NONE; - out->flags = spec->flags; - out->state = wuss_ICON_STATE_NONE; + out->spec = *spec; + out->spec.text = (spec->text != NULL) ? spec->text : ""; + out->spec.fg = fg; + out->spec.bg = bg; + + /* A RADIO/OPTION glyph is drawn from the icon-set bitmap (radon/radoff, + * opton/optoff) when the set carries one, vertically centred in the bbox + * and offset from the bbox top-left. If a state bitmap is larger than the + * caller's bbox the glyph overhangs it, and a select-time invalidate -- + * which only covers the bbox -- then leaves the overhanging edges stale. + * Grow the bbox to the largest of the two state bitmaps so the drawn glyph + * always sits inside the box that hit-test, layout and invalidate use. */ + if (spec->type == wuss_ICON_TYPE_RADIO || spec->type == wuss_ICON_TYPE_OPTION) + { + const char *names[2]; + const bitmap_t *state_bm; + int idx; + int gw, gh; + int i; + + names[0] = (spec->type == wuss_ICON_TYPE_RADIO) ? "radon" : "opton"; + names[1] = (spec->type == wuss_ICON_TYPE_RADIO) ? "radoff" : "optoff"; + + gw = 0; + gh = 0; + for (i = 0; i < 2; i++) + { + idx = wuss_icons_lookup(w, names[i]); + if (idx < 0) + continue; + state_bm = wuss_icons_bitmap(w, idx); + if (state_bm == NULL) + continue; + gw = MAX(gw, state_bm->size.w); + gh = MAX(gh, state_bm->size.h); + } + + if (gw > 0 && out->spec.bbox.x1 - out->spec.bbox.x0 < gw) + out->spec.bbox.x1 = out->spec.bbox.x0 + gw; + if (gh > 0 && out->spec.bbox.y1 - out->spec.bbox.y0 < gh) + out->spec.bbox.y1 = out->spec.bbox.y0 + gh; + } + + /* out->spec.u is already the caller's copy; overwrite only the arms whose + * value we resolved (bitmap image from the icon set, swatch to a palette + * index) */ + switch (spec->type) + { + case wuss_ICON_TYPE_BITMAP: + out->spec.u.bitmap.image = bitmap; + break; + case wuss_ICON_TYPE_MENU_ENTRY: + out->spec.u.menu_entry.swatch = swatch; + break; + default: + break; + } + + out->state = wuss_ICON_STATE_NONE; + + if (spec->type == wuss_ICON_TYPE_SLIDER) + { + int lo, hi; + + lo = MIN(spec->u.slider.min, spec->u.slider.max); + hi = MAX(spec->u.slider.min, spec->u.slider.max); + out->value = CLAMP(spec->u.slider.default_value, lo, hi); + } + else + { + out->value = 0; + } return result_OK; } diff --git a/libraries/wuss/icon/get-bbox.c b/libraries/wuss/icon/get-bbox.c index 99b42c1..bdc867d 100644 --- a/libraries/wuss/icon/get-bbox.c +++ b/libraries/wuss/icon/get-bbox.c @@ -4,5 +4,5 @@ void wuss_icon_get_bbox(const wuss_icon_t *icon, box_t *bbox) { - *bbox = icon->bbox; + *bbox = icon->spec.bbox; } diff --git a/libraries/wuss/icon/get-text.c b/libraries/wuss/icon/get-text.c index b3f8de3..3cf0151 100644 --- a/libraries/wuss/icon/get-text.c +++ b/libraries/wuss/icon/get-text.c @@ -4,5 +4,5 @@ const char *wuss_icon_get_text(const wuss_icon_t *icon) { - return icon->text; + return icon->spec.text; } diff --git a/libraries/wuss/icon/get-type.c b/libraries/wuss/icon/get-type.c index f988044..ef5271d 100644 --- a/libraries/wuss/icon/get-type.c +++ b/libraries/wuss/icon/get-type.c @@ -4,5 +4,5 @@ wuss_icon_type_t wuss_icon_get_type(const wuss_icon_t *icon) { - return icon->type; + return icon->spec.type; } diff --git a/libraries/wuss/icon/get-value.c b/libraries/wuss/icon/get-value.c new file mode 100644 index 0000000..0ed9b56 --- /dev/null +++ b/libraries/wuss/icon/get-value.c @@ -0,0 +1,12 @@ +/* wuss/icon/get-value.c -- query a slider icon's current value */ + +#include + +#include "../core/impl.h" + +int wuss_icon_get_value(const wuss_icon_t *icon) +{ + assert(icon != NULL); + + return (icon->spec.type == wuss_ICON_TYPE_SLIDER) ? icon->value : 0; +} diff --git a/libraries/wuss/icon/get-window.c b/libraries/wuss/icon/get-window.c deleted file mode 100644 index a5e41e6..0000000 --- a/libraries/wuss/icon/get-window.c +++ /dev/null @@ -1,8 +0,0 @@ -/* wuss/icon/get-window.c -- read a work-area icon's owning window */ - -#include "../core/impl.h" - -wuss_window_t *wuss_icon_get_window(const wuss_icon_t *icon) -{ - return icon->window; -} diff --git a/libraries/wuss/icon/hit-test.c b/libraries/wuss/icon/hit-test.c index d734b9c..0ee41fc 100644 --- a/libraries/wuss/icon/hit-test.c +++ b/libraries/wuss/icon/hit-test.c @@ -7,6 +7,7 @@ wuss_icon_t *wuss__icon_hit_test(wuss_window_t *window, point_t doc_point) { wuss_icon_t *it; + box_t box; int i; /* last created wins, matching the draw order in redraw_window */ @@ -16,23 +17,38 @@ wuss_icon_t *wuss__icon_hit_test(wuss_window_t *window, point_t doc_point) /* a bitmap icon is interactive only when it asks to be; RULE and the * static types are always inert; the other types always click */ - if (it->type == wuss_ICON_TYPE_BITMAP) + if (it->spec.type == wuss_ICON_TYPE_BITMAP) { - if (!(it->flags & wuss_ICON_FLAGS_INTERACTIVE)) + if (!(it->spec.flags & wuss_ICON_FLAGS_INTERACTIVE)) continue; } - else if (it->type != wuss_ICON_TYPE_BUTTON && - it->type != wuss_ICON_TYPE_RADIO && - it->type != wuss_ICON_TYPE_OPTION && - it->type != wuss_ICON_TYPE_MENU_ENTRY) + else if (it->spec.type != wuss_ICON_TYPE_ACTION && + it->spec.type != wuss_ICON_TYPE_RADIO && + it->spec.type != wuss_ICON_TYPE_OPTION && + it->spec.type != wuss_ICON_TYPE_MENU_ENTRY && + it->spec.type != wuss_ICON_TYPE_SLIDER) { continue; } - if (it->flags & (wuss_ICON_FLAGS_HIDDEN | wuss_ICON_FLAGS_DISABLED)) + if (it->spec.flags & (wuss_ICON_FLAGS_HIDDEN | wuss_ICON_FLAGS_DISABLED)) continue; - if (box_contains_point(&it->bbox, doc_point.x, doc_point.y)) + /* only the inner rect (bbox inset by the gap) reacts to clicks; the + * surround and the gap around it are decorative only */ + if (it->spec.type == wuss_ICON_TYPE_SLIDER) + { + box.x0 = it->spec.bbox.x0 + WUSS_SLIDER_GAP; + box.y0 = it->spec.bbox.y0 + WUSS_SLIDER_GAP; + box.x1 = it->spec.bbox.x1 - WUSS_SLIDER_GAP; + box.y1 = it->spec.bbox.y1 - WUSS_SLIDER_GAP; + } + else + { + box = it->spec.bbox; + } + + if (box_contains_point(&box, doc_point.x, doc_point.y)) return it; } diff --git a/libraries/wuss/icon/invalidate.c b/libraries/wuss/icon/invalidate.c index 156c574..12d2a7a 100644 --- a/libraries/wuss/icon/invalidate.c +++ b/libraries/wuss/icon/invalidate.c @@ -2,9 +2,9 @@ #include "../core/impl.h" -void wuss__icon_invalidate(const wuss_icon_t *icon) +void wuss__icon_invalidate(wuss_window_t *window, const wuss_icon_t *icon) { /* the icon's bbox is already in the window-local (pre-scroll) coordinates * wuss_window_invalidate expects */ - wuss_window_invalidate(icon->window, &icon->bbox); + wuss_window_invalidate(window, &icon->spec.bbox); } diff --git a/libraries/wuss/icon/plot.c b/libraries/wuss/icon/plot.c index caa1c4b..5575db6 100644 --- a/libraries/wuss/icon/plot.c +++ b/libraries/wuss/icon/plot.c @@ -24,9 +24,7 @@ result_t wuss_icon_plot(wuss_window_t *window, if (rc != result_OK) return rc; - scratch.window = window; - - wuss__icon_draw(window->wuss, &scratch, content, scroll); + wuss__icon_draw(window->wuss, window, &scratch, content, scroll); return result_OK; } diff --git a/libraries/wuss/icon/registry.c b/libraries/wuss/icon/registry.c index 341d9f8..10d9fed 100644 --- a/libraries/wuss/icon/registry.c +++ b/libraries/wuss/icon/registry.c @@ -39,23 +39,6 @@ typedef struct icons_load } icons_load_t; -/* True when "leaf" ends in ".png" (case-sensitive, matching the fixtures). - * Writes the leaf sans that suffix into "name" (capacity "cap"). */ -static int png_leaf(const char *leaf, char *name, size_t cap) -{ - size_t len; - - len = strlen(leaf); - if (len < 4 || len - 4 >= cap) - return 0; - if (strcmp(leaf + len - 4, ".png") != 0) - return 0; - - memcpy(name, leaf, len - 4); - name[len - 4] = '\0'; - return 1; -} - /* The RLE blit (screen_copy_bitmap_rle) decodes straight into the screen's * byte layout -- no per-pixel format convert -- so a compressed icon must * already be in screen order. bitmap_load_png yields rgb(x|a)8888; the screen @@ -116,7 +99,7 @@ static result_t icons_load_entry(const char *leaf, void *opaque) atom_t atom; int oldcap; - if (!png_leaf(leaf, name, sizeof(name))) + if (!path_leaf_strip_ext(leaf, ".png", name, sizeof(name))) return result_OK; /* not a ".png", or name too long -- skip */ path = path_join_filename(st->dir, 1, leaf); diff --git a/libraries/wuss/icon/set-hidden.c b/libraries/wuss/icon/set-hidden.c index 996c2fd..52bbf9a 100644 --- a/libraries/wuss/icon/set-hidden.c +++ b/libraries/wuss/icon/set-hidden.c @@ -2,12 +2,14 @@ #include "../core/impl.h" -void wuss_icon_set_hidden(wuss_icon_t *icon, int hidden) +void wuss_icon_set_hidden(wuss_window_t *window, + wuss_icon_t *icon, + int hidden) { if (hidden) - icon->flags |= wuss_ICON_FLAGS_HIDDEN; + icon->spec.flags |= wuss_ICON_FLAGS_HIDDEN; else - icon->flags &= (wuss_icon_flags_t) ~wuss_ICON_FLAGS_HIDDEN; + icon->spec.flags &= (wuss_icon_flags_t) ~wuss_ICON_FLAGS_HIDDEN; - wuss__icon_invalidate(icon); + wuss__icon_invalidate(window, icon); } diff --git a/libraries/wuss/icon/set-hover.c b/libraries/wuss/icon/set-hover.c index 090c91d..61cab05 100644 --- a/libraries/wuss/icon/set-hover.c +++ b/libraries/wuss/icon/set-hover.c @@ -9,14 +9,18 @@ * so redrawing them on pointer enter/leave is wasted work. */ static int wuss__icon_hover_visible(const wuss_icon_t *icon) { - return icon->type == wuss_ICON_TYPE_MENU_ENTRY; + return icon->spec.type == wuss_ICON_TYPE_MENU_ENTRY; } -void wuss__icon_set_hover(wuss_t *wuss, wuss_icon_t *icon) +void wuss__icon_set_hover(wuss_t *wuss, + wuss_window_t *window, + wuss_icon_t *icon) { - wuss_icon_t *prev; + wuss_icon_t *prev; + wuss_window_t *prevwin; - prev = wuss->hover_icon; + prev = wuss->hover_icon; + prevwin = wuss->hover_window; if (prev == icon) return; @@ -31,15 +35,16 @@ void wuss__icon_set_hover(wuss_t *wuss, wuss_icon_t *icon) { wuss__icon_set_state(prev, wuss_ICON_STATE_HOVERED, 0); if (wuss__icon_hover_visible(prev)) - wuss__icon_invalidate(prev); + wuss__icon_invalidate(prevwin, prev); } - wuss->hover_icon = icon; + wuss->hover_icon = icon; + wuss->hover_window = (icon != NULL) ? window : NULL; if (icon != NULL) { wuss__icon_set_state(icon, wuss_ICON_STATE_HOVERED, 1); if (wuss__icon_hover_visible(icon)) - wuss__icon_invalidate(icon); + wuss__icon_invalidate(window, icon); } } diff --git a/libraries/wuss/icon/set-selected.c b/libraries/wuss/icon/set-selected.c index 712aee5..58640f6 100644 --- a/libraries/wuss/icon/set-selected.c +++ b/libraries/wuss/icon/set-selected.c @@ -4,38 +4,40 @@ #include "../core/impl.h" -void wuss__icon_select(wuss_icon_t *icon, int selected) +void wuss__icon_select(wuss_window_t *window, + wuss_icon_t *icon, + int selected) { - wuss_window_t *window; - wuss_icon_t *other; - int i; + wuss_icon_t *other; + int i; - assert(icon != NULL); + assert(window != NULL); + assert(icon != NULL); - if (icon->type != wuss_ICON_TYPE_RADIO && - icon->type != wuss_ICON_TYPE_OPTION && - icon->type != wuss_ICON_TYPE_MENU_ENTRY) + if (icon->spec.type != wuss_ICON_TYPE_RADIO && + icon->spec.type != wuss_ICON_TYPE_OPTION && + icon->spec.type != wuss_ICON_TYPE_MENU_ENTRY) return; selected = selected ? 1 : 0; /* selecting a grouped radio clears its siblings first */ if (selected && - icon->type == wuss_ICON_TYPE_RADIO && - icon->group != 0) + icon->spec.type == wuss_ICON_TYPE_RADIO && + icon->spec.u.radio.group != 0) { - window = icon->window; for (i = 0; i < window->nicons; i++) { other = window->icons[i]; if (other == icon) continue; - if (other->type != wuss_ICON_TYPE_RADIO || other->group != icon->group) + if (other->spec.type != wuss_ICON_TYPE_RADIO || + other->spec.u.radio.group != icon->spec.u.radio.group) continue; if (!wuss__icon_selected(other)) continue; wuss__icon_set_state(other, wuss_ICON_STATE_SELECTED, 0); - wuss__icon_invalidate(other); + wuss__icon_invalidate(window, other); } } @@ -43,10 +45,12 @@ void wuss__icon_select(wuss_icon_t *icon, int selected) return; wuss__icon_set_state(icon, wuss_ICON_STATE_SELECTED, selected); - wuss__icon_invalidate(icon); + wuss__icon_invalidate(window, icon); } -void wuss_icon_set_selected(wuss_icon_t *icon, int selected) +void wuss_icon_set_selected(wuss_window_t *window, + wuss_icon_t *icon, + int selected) { - wuss__icon_select(icon, selected); + wuss__icon_select(window, icon, selected); } diff --git a/libraries/wuss/icon/set-text.c b/libraries/wuss/icon/set-text.c index 35c8fcc..75c2c12 100644 --- a/libraries/wuss/icon/set-text.c +++ b/libraries/wuss/icon/set-text.c @@ -1,34 +1,27 @@ /* wuss/icon/set-text.c -- replace a work-area icon's label */ -#include -#include - #ifdef FORTIFY #include "fortify/fortify.h" #endif #include "../core/impl.h" -result_t wuss_icon_set_text(wuss_icon_t *icon, const char *text) +result_t wuss_icon_set_text(wuss_window_t *window, + wuss_icon_t *icon, + const char *text) { - wuss_t *w; - const char *src; - char *dup; - size_t len; - - w = icon->window->wuss; - src = (text != NULL) ? text : ""; - len = strlen(src); + wuss_t *w; + char *dup; - dup = wuss__malloc(w, len + 1); + w = window->wuss; + dup = wuss__alloc_strdup(&w->alloc, text != NULL ? text : ""); if (dup == NULL) return result_OOM; - memcpy(dup, src, len + 1); - wuss__free(w, icon->text); - icon->text = dup; + wuss__free(w, (char *) icon->spec.text); + icon->spec.text = dup; - wuss__icon_invalidate(icon); + wuss__icon_invalidate(window, icon); return result_OK; } diff --git a/libraries/wuss/icon/set-value.c b/libraries/wuss/icon/set-value.c new file mode 100644 index 0000000..9780852 --- /dev/null +++ b/libraries/wuss/icon/set-value.c @@ -0,0 +1,35 @@ +/* wuss/icon/set-value.c -- set a slider icon's current value */ + +#include + +#include "../core/impl.h" + +void wuss__icon_set_value(wuss_window_t *window, + wuss_icon_t *icon, + int value) +{ + int lo, hi, clamped; + + assert(window != NULL); + assert(icon != NULL); + + if (icon->spec.type != wuss_ICON_TYPE_SLIDER) + return; + + lo = MIN(icon->spec.u.slider.min, icon->spec.u.slider.max); + hi = MAX(icon->spec.u.slider.min, icon->spec.u.slider.max); + clamped = CLAMP(value, lo, hi); + + if (clamped == icon->value) + return; + + icon->value = clamped; + wuss__icon_invalidate(window, icon); +} + +void wuss_icon_set_value(wuss_window_t *window, + wuss_icon_t *icon, + int value) +{ + wuss__icon_set_value(window, icon, value); +} diff --git a/libraries/wuss/icon/slider-box.c b/libraries/wuss/icon/slider-box.c new file mode 100644 index 0000000..5dd0029 --- /dev/null +++ b/libraries/wuss/icon/slider-box.c @@ -0,0 +1,88 @@ +/* wuss/icon/slider-box.c -- slider groove geometry and value/pixel mapping */ + +#include "geom/box.h" +#include "geom/point.h" + +#include "../core/impl.h" + +void wuss__slider_groove_box(const box_t *screen_box, box_t *out) +{ + out->x0 = screen_box->x0 + WUSS_SLIDER_GAP; + out->y0 = screen_box->y0 + WUSS_SLIDER_GAP; + out->x1 = screen_box->x1 - WUSS_SLIDER_GAP; + out->y1 = screen_box->y1 - WUSS_SLIDER_GAP; +} + +int wuss__slider_value_to_px(const box_t *groove, + wuss_slider_orientation_t orientation, + int value, + int lo, + int hi) +{ + int span, extent; + + value = CLAMP(value, lo, hi); + span = hi - lo; + extent = (orientation == wuss_SLIDER_HORIZONTAL) + ? groove->x1 - groove->x0 + : groove->y1 - groove->y0; + + if (span == 0) + return 0; + + return (int) ((long) (value - lo) * extent / span); +} + +int wuss__slider_px_to_value(const box_t *groove, + wuss_slider_orientation_t orientation, + point_t screen_point, + int lo, + int hi) +{ + int px, extent, span, value; + + extent = (orientation == wuss_SLIDER_HORIZONTAL) + ? groove->x1 - groove->x0 + : groove->y1 - groove->y0; + /* screen y grows downward but value grows upward for a vertical slider, so + * pixel 0 is the groove's bottom (y1), mirroring HORIZONTAL's pixel 0 at + * its left (x0) */ + px = (orientation == wuss_SLIDER_HORIZONTAL) + ? screen_point.x - groove->x0 + : groove->y1 - screen_point.y; + span = hi - lo; + + if (extent <= 0) + return lo; + + px = CLAMP(px, 0, extent); + value = lo + (int) (((long) px * span + extent / 2) / extent); + + return CLAMP(value, lo, hi); +} + +int wuss__slider_value_for_point(wuss_window_t *window, + const wuss_icon_t *icon, + point_t screen_point) +{ + box_t content, screen_box, groove; + int lo, hi, value; + + wuss__content_box(window, &content); + wuss__icon_box_to_screen(&content, window->scroll, &icon->spec.bbox, + &screen_box); + wuss__slider_groove_box(&screen_box, &groove); + + lo = MIN(icon->spec.u.slider.min, icon->spec.u.slider.max); + hi = MAX(icon->spec.u.slider.min, icon->spec.u.slider.max); + value = wuss__slider_px_to_value(&groove, icon->spec.u.slider.orientation, + screen_point, lo, hi); + + /* min > max runs the fill backwards: the value nominally at the groove's + * start (min) then sits at its far/high pixel end, so mirror the reading + * around the [lo,hi] midpoint */ + if (icon->spec.u.slider.min > icon->spec.u.slider.max) + value = lo + hi - value; + + return value; +} diff --git a/libraries/wuss/menu/menu.c b/libraries/wuss/menu/menu.c index 6124b6c..0a1398f 100644 --- a/libraries/wuss/menu/menu.c +++ b/libraries/wuss/menu/menu.c @@ -215,6 +215,10 @@ static result_t wuss__menu_open_window(struct wuss__menu *self, int index) at = wuss__submenu_anchor(self, self->icons[index]); wuss_window_move(win, at); + /* the anchor sits off the parent's right edge; if the parent menu is near + * the screen edge that puts the window (partly) off-screen. wuss_window_move + * does not clamp -- drags rely on it not clamping -- so pull it back here. */ + wuss__nudge_visible_onscreen(win); if (wuss_window_set_hidden(win, 0) != result_OK) { wuss_window_move(win, prev); /* vetoed: undo the anchor move */ @@ -227,6 +231,28 @@ static result_t wuss__menu_open_window(struct wuss__menu *self, int index) return result_OK; } +/* True if the wuss pointer sits over `window`'s chrome above its content area + * -- the titlebar strip -- and `window` is the frontmost window there. Core + * routes furniture hovers straight to the window manager and delivers no event + * to the menu delegate (see wuss_mouse_move), so a titlebar hover on a parent + * level cannot be caught in the ICON handler; the IDLE tick polls this instead + * to close a submenu the pointer has slid up onto the parent's titlebar to + * reach (e.g. to drag the parent). The frontmost check stops a child menu + * dragged back over its parent's titlebar -- the child window is then on top + * at that point -- from being read as a parent-titlebar hover and closed. */ +static int wuss__pointer_over_titlebar(const wuss_window_t *window) +{ + point_t p; + box_t content; + + p = wuss_get_pointer(window->wuss); + wuss__content_box(window, &content); + + return box_contains_point(&window->visible, p.x, p.y) + && p.y < content.y0 + && wuss__window_at(window->wuss, p) == window; +} + /* ----------------------------------------------------------------------- */ /* The menu window's task delegate. Shared across every borderless menu @@ -243,18 +269,43 @@ static result_t wuss__menu_handle(wuss_window_t *window, int i; /* IDLE arrives with window == NULL; task_data is the wuss_t (see - * wuss__menu_task). Drive any running SELECT-pick flash off it. */ + * wuss__menu_task). Drive any running SELECT-pick flash off it, and close a + * submenu the pointer has slid up onto its parent's titlebar. */ if (event->kind == wuss_EVENT_IDLE) { struct wuss__menu *node; wuss = task_data; + for (node = wuss->menu_chain; node != NULL; node = node->child) if (!node->borrowed && node->flash.frames > 0) { wuss__menu_flash_step(node); break; /* node may be freed; the chain is gone if the flash ended */ } + + for (node = wuss->menu_chain; node != NULL; node = node->child) + if (node->child != NULL + && node->flash.frames == 0 + && wuss__pointer_over_titlebar(node->window)) + { + wuss_icon_t *was_parent; + + was_parent = (node->open_index >= 0) ? node->icons[node->open_index] + : NULL; + + wuss__menu_close_from(node->child); + node->child = NULL; + node->open_index = -1; + + if (was_parent != NULL) + { + wuss__icon_set_state(was_parent, wuss_ICON_STATE_HOVERED, 0); + wuss__icon_invalidate(node->window, was_parent); + } + break; /* closing the child re-shaped the chain past this node */ + } + return result_OK; } @@ -281,16 +332,17 @@ static result_t wuss__menu_handle(wuss_window_t *window, item = &self->menu->items[index]; - if (item->flags & wuss_MENU_ITEM_DISABLED) - return result_OK; - if (event->data.icon.action == wuss_MOUSE_MOVE) { point_t at; int has_child_row; int on_arrow; - has_child_row = (item->submenu != NULL || item->window != NULL); + /* A disabled row still has to run the close-on-move-away logic below -- + * hovering off an open submenu onto a disabled sibling must collapse it + * -- but can never itself open a submenu. */ + has_child_row = !(item->flags & wuss_MENU_ITEM_DISABLED) + && (item->submenu != NULL || item->window != NULL); on_arrow = has_child_row && wuss__pointer_over_row_arrow(self->window, icon); @@ -314,7 +366,7 @@ static result_t wuss__menu_handle(wuss_window_t *window, if (was_parent != NULL && was_parent != icon) { wuss__icon_set_state(was_parent, wuss_ICON_STATE_HOVERED, 0); - wuss__icon_invalidate(was_parent); + wuss__icon_invalidate(self->window, was_parent); } } @@ -348,6 +400,9 @@ static result_t wuss__menu_handle(wuss_window_t *window, wuss_task_t *owner; const wuss_menu_t *menu; + if (item->flags & wuss_MENU_ITEM_DISABLED) + return result_OK; + button = event->data.icon.button; if (item->submenu != NULL || item->window != NULL) @@ -393,7 +448,7 @@ static result_t wuss__menu_handle(wuss_window_t *window, /* first blink now: the row is already highlit under the pointer, so * drop the highlight this frame for an immediate visible change */ wuss__icon_set_state(row, wuss_ICON_STATE_HOVERED, 0); - wuss__icon_invalidate(row); + wuss__icon_invalidate(self->window, row); } return result_OK; } @@ -462,7 +517,7 @@ static void wuss__menu_flash_finish(struct wuss__menu *self) on = keep_open && wuss__pointer_over_icon(self->window, row); wuss__icon_set_state(row, wuss_ICON_STATE_HOVERED, on); - wuss__icon_invalidate(row); + wuss__icon_invalidate(self->window, row); if (!keep_open) { @@ -508,7 +563,7 @@ static void wuss__menu_flash_step(struct wuss__menu *self) int on = ((self->flash.frames / WUSS_MENU_FLASH_PERIOD) & 1) == 0; wuss__icon_set_state(row, wuss_ICON_STATE_HOVERED, on); - wuss__icon_invalidate(row); + wuss__icon_invalidate(self->window, row); } } @@ -550,7 +605,7 @@ static result_t wuss__menu_spawn(wuss_t *wuss, assert(wuss != NULL); assert(menu != NULL); - assert(wuss->fonts[0] != NULL); + assert(wuss->fonts.fonts[0] != NULL); if (menu->nitems <= 0) return result_WUSS_BAD_ICON; @@ -559,15 +614,12 @@ static result_t wuss__menu_spawn(wuss_t *wuss, if (menu_task == NULL) return result_OOM; - menu_flags = wuss_WINDOW_NO_CLOSE | wuss_WINDOW_NO_BACK - | wuss_WINDOW_NO_TOGGLE_SIZE - | wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL - | wuss_WINDOW_NO_RESIZE; + menu_flags = 0; outline_px = wuss__outline_px_for(menu_flags); titlebar_height = wuss__titlebar_height_for(wuss, menu_flags); - bmfont_get_info(wuss->fonts[0], NULL, &fh); + bmfont_get_info(wuss->fonts.fonts[0], NULL, &fh, NULL, NULL); pitch = fh + 2 * WUSS_MENU_ROW_PAD; sep_h = 2 * WUSS_MENU_ROW_PAD; @@ -589,7 +641,7 @@ static result_t wuss__menu_spawn(wuss_t *wuss, len = (int) strlen(text); if (len == 0) continue; /* empty label (bare rule row); nothing to measure */ - if (wuss__text_measure(wuss->fonts[0], text, len, + if (wuss__text_measure(wuss->fonts.fonts[0], text, len, INT_MAX, &split, &w) == result_OK && (int) w > widest) widest = (int) w; } @@ -600,7 +652,7 @@ static result_t wuss__menu_spawn(wuss_t *wuss, { bmfont_width_t space_w = 0; - wuss__text_measure(wuss->fonts[0], " ", 1, INT_MAX, NULL, &space_w); + wuss__text_measure(wuss->fonts.fonts[0], " ", 1, INT_MAX, NULL, &space_w); widest += 2 * (int) space_w; } @@ -613,12 +665,13 @@ static result_t wuss__menu_spawn(wuss_t *wuss, if (menu->title != NULL && menu->title[0] != '\0') { bmfont_t *titlefont; - int titlelen; - int split; - bmfont_width_t title_w; + int titlelen; + int split; + bmfont_width_t title_w; - titlefont = (wuss->nfonts > 1 && wuss->fonts[1] != NULL) ? wuss->fonts[1] - : wuss->fonts[0]; + titlefont = (wuss->fonts.nfonts > 1 && wuss->fonts.fonts[1] != NULL) + ? wuss->fonts.fonts[1] + : wuss->fonts.fonts[0]; titlelen = (int) strlen(menu->title); if (wuss__text_measure(titlefont, menu->title, titlelen, INT_MAX, &split, &title_w) == result_OK && @@ -638,7 +691,7 @@ static result_t wuss__menu_spawn(wuss_t *wuss, if (doc_h > max_h) { height = max_h; - menu_flags &= (wuss_window_flags_t) ~wuss_WINDOW_NO_VSCROLL; + menu_flags |= wuss_WINDOW_VSCROLL; } else { @@ -715,7 +768,7 @@ static result_t wuss__menu_spawn(wuss_t *wuss, specs[s].text = ""; specs[s].fg = wuss_COLOUR_BLACK; specs[s].bg = wuss_NO_BACKGROUND; - specs[s].swatch = wuss_NO_BACKGROUND; + specs[s].u.menu_entry.swatch = wuss_NO_BACKGROUND; specs[s].flags = wuss_ICON_FLAGS_NONE; s++; y += sep_h; @@ -736,8 +789,8 @@ static result_t wuss__menu_spawn(wuss_t *wuss, specs[s].text = item->text ? item->text : ""; specs[s].fg = wuss_COLOUR_BLACK; specs[s].bg = wuss_NO_BACKGROUND; - specs[s].swatch = (item->flags & wuss_MENU_ITEM_SWATCH) ? item->swatch - : wuss_NO_BACKGROUND; + specs[s].u.menu_entry.swatch = + (item->flags & wuss_MENU_ITEM_SWATCH) ? item->swatch : wuss_NO_BACKGROUND; specs[s].flags = flags; s++; @@ -795,7 +848,7 @@ static result_t wuss__menu_spawn(wuss_t *wuss, s++; if (menu->items[i].flags & wuss_MENU_ITEM_TICKED) - wuss_icon_set_selected(node->icons[i], 1); + wuss_icon_set_selected(node->window, node->icons[i], 1); } wuss__free(wuss, made); @@ -855,23 +908,11 @@ int wuss_menu_should_keep_open(const wuss_event_t *ev) result_t wuss_menu_open_ticked(wuss_task_t *task, wuss_menu_t *menu, - const int *ticked, + unsigned int ticked, point_t at, wuss_menu_handle_t *out) { - wuss_menu_item_t *item; - int i; - - assert(menu != NULL); - - for (i = 0; i < menu->nitems; i++) - { - item = (wuss_menu_item_t *) &menu->items[i]; - if (ticked != NULL && ticked[i]) - item->flags |= wuss_MENU_ITEM_TICKED; - else - item->flags &= ~(wuss_menu_item_flags_t) wuss_MENU_ITEM_TICKED; - } + wuss_menu_tick_set(menu, ticked); return wuss_menu_open(task, menu, at, out); } @@ -909,8 +950,8 @@ int wuss_menu_is_open(wuss_menu_handle_t handle) } /* Find the open chain level showing `menu`, or NULL if `handle` is stale or - * `menu` is not a level of its chain. Shared by wuss_menu_set_ticked and - * wuss_menu_set_item_ticked. */ + * `menu` is not a level of its chain. Shared by wuss_menu_tick_exclusive_live + * and wuss_menu_tick_item_live. */ static struct wuss__menu *wuss__menu_open_level(wuss_menu_handle_t handle, const wuss_menu_t *menu) { @@ -934,9 +975,64 @@ static struct wuss__menu *wuss__menu_open_level(wuss_menu_handle_t handle, return NULL; /* menu is not a level of this chain */ } -void wuss_menu_set_ticked(wuss_menu_handle_t handle, - const wuss_menu_t *menu, - int index) +/* ----------------------------------------------------------------------- */ + +void wuss_menu_tick_set(wuss_menu_t *menu, unsigned int ticked) +{ + wuss_menu_item_t *item; + int i; + + assert(menu != NULL); + assert(menu->nitems <= (int) (sizeof(ticked) * CHAR_BIT)); + + for (i = 0; i < menu->nitems; i++) + { + item = (wuss_menu_item_t *) &menu->items[i]; + if (ticked & (1u << i)) + item->flags |= wuss_MENU_ITEM_TICKED; + else + item->flags &= ~(wuss_menu_item_flags_t) wuss_MENU_ITEM_TICKED; + } +} + +void wuss_menu_tick_exclusive(wuss_menu_t *menu, int index) +{ + wuss_menu_item_t *item; + int i; + + assert(menu != NULL); + + for (i = 0; i < menu->nitems; i++) + { + item = (wuss_menu_item_t *) &menu->items[i]; + if (i == index) + item->flags |= wuss_MENU_ITEM_TICKED; + else + item->flags &= ~(wuss_menu_item_flags_t) wuss_MENU_ITEM_TICKED; + } +} + +void wuss_menu_tick_item(wuss_menu_t *menu, int index, int ticked) +{ + wuss_menu_item_t *item; + + assert(menu != NULL); + + if (index < 0 || index >= menu->nitems) + return; + + item = (wuss_menu_item_t *) &menu->items[index]; + if (ticked) + item->flags |= wuss_MENU_ITEM_TICKED; + else + item->flags &= ~(wuss_menu_item_flags_t) wuss_MENU_ITEM_TICKED; +} + +/* ----------------------------------------------------------------------- */ + +void wuss_menu_tick_exclusive_live(wuss_menu_handle_t handle, + const wuss_menu_t *menu, + int index) { struct wuss__menu *node; int i; @@ -946,13 +1042,31 @@ void wuss_menu_set_ticked(wuss_menu_handle_t handle, return; for (i = 0; i < menu->nitems; i++) - wuss_icon_set_selected(node->icons[i], i == index); + wuss_icon_set_selected(node->window, node->icons[i], i == index); +} + +void wuss_menu_tick_set_live(wuss_menu_handle_t handle, + const wuss_menu_t *menu, + unsigned int ticked) +{ + struct wuss__menu *node; + int i; + + assert(menu->nitems <= (int) (sizeof(ticked) * CHAR_BIT)); + + node = wuss__menu_open_level(handle, menu); + if (node == NULL) + return; + + for (i = 0; i < menu->nitems; i++) + wuss_icon_set_selected(node->window, node->icons[i], + (ticked & (1u << i)) != 0); } -void wuss_menu_set_item_ticked(wuss_menu_handle_t handle, - const wuss_menu_t *menu, - int index, - int ticked) +void wuss_menu_tick_item_live(wuss_menu_handle_t handle, + const wuss_menu_t *menu, + int index, + int ticked) { struct wuss__menu *node; @@ -963,7 +1077,7 @@ void wuss_menu_set_item_ticked(wuss_menu_handle_t handle, if (index < 0 || index >= menu->nitems) return; - wuss_icon_set_selected(node->icons[index], ticked); + wuss_icon_set_selected(node->window, node->icons[index], ticked); } /* ----------------------------------------------------------------------- */ diff --git a/libraries/wuss/test/tasks/ball.c b/libraries/wuss/test/tasks/ball.c index 353d60e..02210b8 100644 --- a/libraries/wuss/test/tasks/ball.c +++ b/libraries/wuss/test/tasks/ball.c @@ -51,12 +51,17 @@ static box_t ball_local_box(int vx0, int vy0, int vx1, int vy1, int radius) return local; } -result_t ball_create(wuss_t *wuss, ball_task_t *task) +result_t ball_create(wuss_t *wuss, ball_task_t **out) { result_t rc; + ball_task_t *task; wuss_task_t *delegate; wuss_task_desc_t delegate_desc; + task = calloc(1, sizeof(*task)); + if (task == NULL) + return result_OOM; + task->bg = colour_rgb(0xFF, 0x00, 0x00); task->nballs = 1; @@ -74,7 +79,7 @@ result_t ball_create(wuss_t *wuss, ball_task_t *task) rc = wuss_task_create(wuss, &delegate_desc, &delegate); if (rc != result_OK) { - free(task); /* nothing registered yet; the spawner will not free it */ + free(task); /* nothing registered yet; nobody else owns it */ return rc; } wuss_task_set_autoclose(delegate, 1); @@ -83,14 +88,25 @@ result_t ball_create(wuss_t *wuss, ball_task_t *task) SIZE2D(200, 160), "Bouncing Ball", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(200, 160), SIZE2D(0, 0), &task->window); if (rc != result_OK) + { wuss_task_destroy(delegate); /* unregister; its QUIT frees the task block */ + return rc; + } + + if (out) + *out = task; - return rc; + return result_OK; +} + +void ball_destroy(ball_task_t *task) +{ + free(task); } static result_t ball_redraw(const wuss_event_t *event, void *task_data) @@ -208,10 +224,10 @@ static result_t ball_idle(void *task_data) b->x += b->dx; b->y += b->dy; - if (b->x - b->radius < scroll.x) { b->x = scroll.x + b->radius; b->dx = -b->dx; } - else if (b->x + b->radius > scroll.x + width) { b->x = scroll.x + width - b->radius; b->dx = -b->dx; } - if (b->y - b->radius < scroll.y) { b->y = scroll.y + b->radius; b->dy = -b->dy; } - else if (b->y + b->radius > scroll.y + height){ b->y = scroll.y + height - b->radius; b->dy = -b->dy; } + if (b->x - b->radius < scroll.x) { b->x = scroll.x + b->radius; b->dx = -b->dx; } + else if (b->x + b->radius >= scroll.x + width) { b->x = scroll.x + width - 1 - b->radius; b->dx = -b->dx; } + if (b->y - b->radius < scroll.y) { b->y = scroll.y + b->radius; b->dy = -b->dy; } + else if (b->y + b->radius >= scroll.y + height){ b->y = scroll.y + height - 1 - b->radius; b->dy = -b->dy; } local = ball_local_box(MIN(old_x, b->x), MIN(old_y, b->y), MAX(old_x, b->x), MAX(old_y, b->y), @@ -244,7 +260,7 @@ result_t ball_handle(wuss_window_t *window, return ball_idle(task_data); case wuss_EVENT_QUIT: - free(bc); /* task_data was calloc'd per instance by the spawner */ + ball_destroy(bc); return result_OK; default: diff --git a/libraries/wuss/test/tasks/ball.h b/libraries/wuss/test/tasks/ball.h index a1b8d9f..1171c57 100644 --- a/libraries/wuss/test/tasks/ball.h +++ b/libraries/wuss/test/tasks/ball.h @@ -35,9 +35,14 @@ ball_task_t; wuss_window_fn_t ball_handle; -/* create the bouncing-ball window against the given wuss instance; "task" is a - * per-instance block owned by the window and freed when it closes */ -result_t ball_create(wuss_t *wuss, ball_task_t *task); +/* create the bouncing-ball window against the given wuss instance; the task + * block is allocated here, owned by the window, and freed when it closes. + * if out is non-NULL, the task block is also returned through it */ +result_t ball_create(wuss_t *wuss, ball_task_t **out); + +/* free a task block allocated by ball_create; normally called by the + * window's wuss_EVENT_QUIT handler, not by callers directly */ +void ball_destroy(ball_task_t *task); #endif /* WUSS_APP */ diff --git a/libraries/wuss/test/tasks/blank.c b/libraries/wuss/test/tasks/blank.c index f1ef9b6..3d36e7b 100644 --- a/libraries/wuss/test/tasks/blank.c +++ b/libraries/wuss/test/tasks/blank.c @@ -17,12 +17,17 @@ #define BLANK_CYCLE_FRAMES 30 /* colour advances every half-second at 60fps */ -result_t blank_create(wuss_t *wuss, blank_task_t *task) +result_t blank_create(wuss_t *wuss, blank_task_t **out) { result_t rc; + blank_task_t *task; wuss_task_t *delegate; wuss_task_desc_t delegate_desc; + task = calloc(1, sizeof(*task)); + if (task == NULL) + return result_OOM; + task->npalette = 16; // TODO: Read max palette index from wuss task->index = 0; task->frame_count = 0; @@ -42,15 +47,27 @@ result_t blank_create(wuss_t *wuss, blank_task_t *task) rc = wuss_window_create_placed(delegate, SIZE2D(200, 160), NULL, - wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE, + wuss_WINDOW_VSCROLL | wuss_WINDOW_HSCROLL | + wuss_WINDOW_RESIZE, wuss_BACKDROP_COLOUR(task->index), SIZE2D(200, 160), SIZE2D(0, 0), &task->window); if (rc != result_OK) + { wuss_task_destroy(delegate); /* unregister; its QUIT frees the task block */ + return rc; + } - return rc; + if (out) + *out = task; + + return result_OK; +} + +void blank_destroy(blank_task_t *task) +{ + free(task); } static result_t blank_idle(void *task_data) @@ -80,7 +97,7 @@ result_t blank_handle(wuss_window_t *window, { if (event->kind == wuss_EVENT_QUIT) { - free(task_data); /* calloc'd per instance by the spawner */ + blank_destroy(task_data); return result_OK; } diff --git a/libraries/wuss/test/tasks/blank.h b/libraries/wuss/test/tasks/blank.h index 34ea545..fb8c53d 100644 --- a/libraries/wuss/test/tasks/blank.h +++ b/libraries/wuss/test/tasks/blank.h @@ -23,9 +23,13 @@ blank_task_t; wuss_window_fn_t blank_handle; -/* create the colour-cycling blank window against the given wuss instance */ -result_t blank_create(wuss_t *wuss, blank_task_t *task); +/* create the colour-cycling blank window against the given wuss instance; + * if out is non-NULL, the task block is also returned through it */ +result_t blank_create(wuss_t *wuss, blank_task_t **out); +/* free a task block allocated by blank_create; normally called by the + * window's wuss_EVENT_QUIT handler, not by callers directly */ +void blank_destroy(blank_task_t *task); #endif /* WUSS_APP */ diff --git a/libraries/wuss/test/tasks/chars.c b/libraries/wuss/test/tasks/chars.c index 4156fcd..1909026 100644 --- a/libraries/wuss/test/tasks/chars.c +++ b/libraries/wuss/test/tasks/chars.c @@ -17,6 +17,7 @@ #include "geom/box.h" #include "geom/point.h" #include "io/path.h" +#include "wuss/icon.h" #include "wuss/menu.h" #include "chars.h" @@ -25,24 +26,43 @@ #define CHARS_ROWS 16 #define CHARS_PAD 1 -/* keep the resources root the task was created with; the picker loads a font - * on demand by leafname and chars_create does not stash it elsewhere. - * ponytail: one demo, one instance at a time -- a file-scope copy is fine. */ -static char chars_resources[256]; - /* ----------------------------------------------------------------------- */ -/* content size for the grid at the given font's cell metrics. Each cell +/* the rows of the CHARS_COLS x CHARS_ROWS grid that hold at least one glyph + * of font: [*first_row, *first_row + *nrows). first_row * CHARS_COLS is the + * byte value of the top-left cell of the first such row. */ +static void chars_row_range(bmfont_t *font, int *first_row, int *nrows) +{ + int first, count, last_row; + + first = ' '; /* bmfont glyphs are laid out contiguously starting here */ + count = bmfont_get_count(font); + /* bmfont indexes its glyph table off a plain char, so a byte value above + * CHAR_MAX would index negatively on a signed-char platform. Never draw + * one, however many glyphs the font claims. */ + if (first + count > CHAR_MAX + 1) + count = CHAR_MAX + 1 - first; + + *first_row = first / CHARS_COLS; + last_row = (first + count - 1) / CHARS_COLS; + *nrows = last_row - *first_row + 1; +} + +/* content size for the grid at the given font's cell metrics, less any + * contiguous leading/trailing rows the font has no glyphs in. Each cell * stacks the index (drawn in the wuss system font) above the glyph. */ static size2d_t chars_window_size(chars_task_t *task, bmfont_t *font) { int fw, fh, ifw, ifh, cell_w, cell_h; + int first_row, nrows; - bmfont_get_info(font, &fw, &fh); - bmfont_get_info(wuss_get_font(task->wuss), &ifw, &ifh); + bmfont_get_info(font, &fw, &fh, NULL, NULL); + bmfont_get_info(wuss_get_font(task->wuss), &ifw, &ifh, NULL, NULL); cell_w = MAX(fw, ifw * 3) + CHARS_PAD * 2; cell_h = ifh + fh + CHARS_PAD * 3; - return SIZE2D(cell_w * CHARS_COLS, cell_h * CHARS_ROWS); + chars_row_range(font, &first_row, &nrows); + return SIZE2D(cell_w * CHARS_COLS + wuss_STD_INSET * 2, + cell_h * nrows + wuss_STD_INSET * 2); } /* load fonts[idx] if not already in hand; returns it or NULL on failure. @@ -52,6 +72,7 @@ static bmfont_t *chars_load_font(chars_task_t *task, const char *name) { result_t rc; + const char *resources; const char *leaf; const char *filename; bmfont_t *font; @@ -59,8 +80,9 @@ static bmfont_t *chars_load_font(chars_task_t *task, if (task->fonts[idx] != NULL) return task->fonts[idx]; - leaf = path_join_leafname(name, "png"); - filename = path_join_filename(chars_resources, 3, "resources", "bmfonts", + resources = wuss_get_resources(task->wuss); + leaf = path_join_leafname(name, "png"); + filename = path_join_filename(resources, 3, "resources", "bmfonts", leaf); rc = bmfont_create(filename, &font); @@ -86,7 +108,17 @@ static result_t chars_set_font(chars_task_t *task, int idx, const char *name) task->font = font; task->current = idx; - wuss_window_resize(task->window, chars_window_size(task, font)); + /* the grid's cell metrics scale with the font, so both the window and its + * scrollable extent have to follow the new size -- resize alone would leave + * the doc (and so the scroll range) sized to the font the window was + * created with, clipping the far cells of a larger font unreachably */ + { + size2d_t grid; + + grid = chars_window_size(task, font); + wuss_window_resize(task->window, grid); + wuss_window_set_doc(task->window, grid); + } wuss_window_invalidate_visible(task->window); return result_OK; } @@ -103,26 +135,27 @@ static result_t chars_open_menu(chars_task_t *task) /* ----------------------------------------------------------------------- */ -result_t chars_create(wuss_t *wuss, - const char *resources, - chars_task_t *task) +result_t chars_create(wuss_t *wuss, chars_task_t **out) { result_t rc; + chars_task_t *task; wuss_task_t *delegate; wuss_task_desc_t delegate_desc; bmfont_t *font; + const char *resources; const char *bmfonts_dir; const wuss_menu_t *menu; + size2d_t grid; font = wuss_get_font(wuss); if (font == NULL) - { - task->window = NULL; - return result_OK; - } + return result_OK; /* no window opened; nothing to free */ + + task = calloc(1, sizeof(*task)); + if (task == NULL) + return result_OOM; - strncpy(chars_resources, resources, sizeof(chars_resources) - 1); - chars_resources[sizeof(chars_resources) - 1] = '\0'; + resources = wuss_get_resources(wuss); task->wuss = wuss; task->font = font; @@ -134,7 +167,7 @@ result_t chars_create(wuss_t *wuss, /* the picker: every ".png" font under resources/bmfonts, sorted, less any * SYSTEM-class font (e.g. the one wuss draws menu ticks/arrows from) */ - bmfonts_dir = path_join_filename(chars_resources, 2, "resources", "bmfonts"); + bmfonts_dir = path_join_filename(resources, 2, "resources", "bmfonts"); rc = wuss_fontmenu_create(&task->fontmenu, bmfonts_dir, "Font", wuss, NULL); if (rc != result_OK) { @@ -168,18 +201,37 @@ result_t chars_create(wuss_t *wuss, wuss_task_set_autoclose(delegate, 1); task->delegate = delegate; + grid = chars_window_size(task, font); rc = wuss_window_create_placed(delegate, - chars_window_size(task, font), + grid, "Chars", - wuss_WINDOW_NO_HSCROLL, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), - chars_window_size(task, font), + wuss_WINDOW_DEFAULT & ~wuss_WINDOW_HSCROLL, + wuss_NO_BACKDROP, + grid, SIZE2D(64, 64), &task->window); if (rc != result_OK) + { wuss_task_destroy(delegate); /* unregister; its QUIT frees the task block */ + return rc; + } + + if (out) + *out = task; - return rc; + return result_OK; +} + +void chars_destroy(chars_task_t *task) +{ + int i; + + for (i = 0; i < task->nfonts; i++) + if (task->fonts[i] != NULL) + bmfont_destroy(task->fonts[i]); + free(task->fonts); + wuss_fontmenu_destroy(task->fontmenu); + free(task); } static result_t chars_redraw(const wuss_event_t *event, void *task_data) @@ -188,9 +240,11 @@ static result_t chars_redraw(const wuss_event_t *event, void *task_data) screen_t *scr; bmfont_t *sysfont; const box_t *bounds; - int font_width, font_height, sysfont_width, sysfont_height; + int font_width, font_height, font_ascent; + int sysfont_width, sysfont_height, sysfont_ascent; int cell_w, cell_h; int first, count; + int first_row, nrows; int i, sx, sy; cc = task_data; @@ -202,8 +256,9 @@ static result_t chars_redraw(const wuss_event_t *event, void *task_data) sysfont = wuss_get_font(cc->wuss); - bmfont_get_info(cc->font, &font_width, &font_height); - bmfont_get_info(sysfont, &sysfont_width, &sysfont_height); + bmfont_get_info(cc->font, &font_width, &font_height, &font_ascent, NULL); + bmfont_get_info(sysfont, &sysfont_width, &sysfont_height, &sysfont_ascent, + NULL); cell_w = MAX(font_width, sysfont_width * 3) + CHARS_PAD * 2; cell_h = sysfont_height + font_height + CHARS_PAD * 3; @@ -214,36 +269,60 @@ static result_t chars_redraw(const wuss_event_t *event, void *task_data) * one, however many glyphs the font claims. */ if (first + count > CHAR_MAX + 1) count = CHAR_MAX + 1 - first; + chars_row_range(cc->font, &first_row, &nrows); - for (i = 0; i < CHARS_COLS * CHARS_ROWS; i++) + /* the wuss_STD_INSET margin around the grid falls outside every cell's own + * fill below, so paint the whole dirty rect first -- covers that margin + * and any dirty strip past the last row/column of cells too. */ + screen_fill_rect(scr, bounds->x0, bounds->y0, + SIZE2D(bounds->x1 - bounds->x0, + bounds->y1 - bounds->y0), cc->bg); + + for (i = 0; i < CHARS_COLS * nrows; i++) { - int col, row, x, y; + int col, row, x, y, byte; char ch; char label[4]; point_t pos; - col = i % CHARS_COLS; - row = i / CHARS_COLS; - x = bounds->x0 - sx + col * cell_w; - y = bounds->y0 - sy + row * cell_h; + col = i % CHARS_COLS; + row = i / CHARS_COLS; + byte = (first_row + row) * CHARS_COLS + col; + x = bounds->x0 - sx + wuss_STD_INSET + col * cell_w; + y = bounds->y0 - sy + wuss_STD_INSET + row * cell_h; screen_fill_rect(scr, x, y, SIZE2D(cell_w, cell_h), cc->bg); screen_draw_line(scr, x, y, x + cell_w - 1, y, cc->mg); screen_draw_line(scr, x, y, x, y + cell_h - 1, cc->mg); - snprintf(label, 4, "%d", i); + snprintf(label, 4, "%d", byte); pos.x = x + CHARS_PAD; - pos.y = y + CHARS_PAD; - bmfont_draw(sysfont, scr, label, (int) strlen(label), cc->mg, cc->bg, - &pos, NULL); + pos.y = y + CHARS_PAD + sysfont_ascent; + wuss_text_draw(cc->wuss, 0, scr, label, (int) strlen(label), cc->mg, + cc->bg, &pos, NULL); - if (i < first || i >= first + count) + if (byte < first || byte >= first + count) continue; /* no glyph for this byte value: leave the cell blank */ - ch = (char) i; + ch = (char) byte; pos.x = x + CHARS_PAD; - pos.y = y + CHARS_PAD * 2 + sysfont_height; - bmfont_draw(cc->font, scr, &ch, 1, cc->fg, cc->bg, &pos, NULL); + pos.y = y + CHARS_PAD * 2 + sysfont_height + font_ascent; + screen_draw_dashed_line(scr, x + CHARS_PAD, pos.y, + x + cell_w - 1 - CHARS_PAD, pos.y, 1, 1, cc->mg); + bmfont_draw(cc->font, scr, &ch, 1, cc->fg, cc->bg, NULL, &pos, NULL); + + /* advance width: a blue rule under the glyph spanning pos.x..pos.x+advance, + * a ruler for how far this glyph pushes the pen */ + { + bmfont_width_t advance; + colour_t blue; + int adv_y; + + bmfont_measure(cc->font, &ch, 1, NULL, INT_MAX, NULL, &advance); + blue = colour_rgb(0x66, 0x66, 0xFF); + adv_y = y + cell_h - 1 - CHARS_PAD; + screen_draw_line(scr, pos.x, adv_y, pos.x + advance - 1, adv_y, blue); + } } return result_OK; @@ -260,16 +339,7 @@ result_t chars_handle(wuss_window_t *window, switch (event->kind) { case wuss_EVENT_QUIT: - { - int i; - - for (i = 0; i < cc->nfonts; i++) - if (cc->fonts[i] != NULL) - bmfont_destroy(cc->fonts[i]); - free(cc->fonts); - wuss_fontmenu_destroy(cc->fontmenu); - free(cc); /* calloc'd per instance by the spawner */ - } + chars_destroy(cc); return result_OK; case wuss_EVENT_MOUSE: @@ -293,8 +363,9 @@ result_t chars_handle(wuss_window_t *window, /* ADJUST keeps the chain open without rebuilding it, so the * fresh-open tick set in chars_open_menu is now stale on screen; * retick the still-open chain in place to match cc->current */ - wuss_menu_set_ticked(cc->menu_handle, - wuss_fontmenu_menu(cc->fontmenu), cc->current); + wuss_menu_tick_exclusive_live(cc->menu_handle, + wuss_fontmenu_menu(cc->fontmenu), + cc->current); else /* SELECT has already closed and freed the chain by the time this * event arrives; the handle is stale, don't touch it */ diff --git a/libraries/wuss/test/tasks/chars.h b/libraries/wuss/test/tasks/chars.h index 9fa940f..79e9231 100644 --- a/libraries/wuss/test/tasks/chars.h +++ b/libraries/wuss/test/tasks/chars.h @@ -36,11 +36,16 @@ chars_task_t; wuss_window_fn_t chars_handle; /* create the glyph-grid window against the given wuss instance; does - * nothing and returns result_OK if wuss has no system font. resources is the - * root the font picker loads bmfonts from (resources/bmfonts/.png). */ -result_t chars_create(wuss_t *wuss, - const char *resources, - chars_task_t *task); + * nothing and returns result_OK if wuss has no system font. the font picker + * loads bmfonts from wuss_get_resources(wuss)/resources/bmfonts/.png. + * if out is non-NULL, the task block is also returned through it -- but only + * on the path where one is actually allocated; *out is left untouched on the + * font-less early return, same as on any other non-result_OK path */ +result_t chars_create(wuss_t *wuss, chars_task_t **out); + +/* free a task block allocated by chars_create; normally called by the + * window's wuss_EVENT_QUIT handler, not by callers directly */ +void chars_destroy(chars_task_t *task); #endif /* WUSS_APP */ diff --git a/libraries/wuss/test/tasks/checker.c b/libraries/wuss/test/tasks/checker.c index a9c55eb..d9c7e5a 100644 --- a/libraries/wuss/test/tasks/checker.c +++ b/libraries/wuss/test/tasks/checker.c @@ -18,12 +18,17 @@ #define CHECKER_BAND_MIN 1 #define CHECKER_BAND_MAX 32 -result_t checker_create(wuss_t*wuss, checker_task_t *task) +result_t checker_create(wuss_t *wuss, checker_task_t **out) { result_t rc; + checker_task_t *task; wuss_task_t *delegate; wuss_task_desc_t delegate_desc; + task = calloc(1, sizeof(*task)); + if (task == NULL) + return result_OOM; + task->black = colour_rgb(0x00, 0x00, 0x00); task->white = colour_rgb(0xFF, 0xFF, 0xFF); task->pattern = checker_PATTERN_CHECKERBOARD; @@ -46,7 +51,7 @@ result_t checker_create(wuss_t*wuss, checker_task_t *task) SIZE2D(160, 160), "Checker 1", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(160, 160), SIZE2D(0, 0), &task->window); @@ -60,7 +65,7 @@ result_t checker_create(wuss_t*wuss, checker_task_t *task) SIZE2D(160, 160), "Checker 2", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(160, 160), SIZE2D(0, 0), &task->window2); @@ -74,9 +79,17 @@ result_t checker_create(wuss_t*wuss, checker_task_t *task) * wuss_EVENT_QUIT frees task_data */ wuss_task_set_autoclose(delegate, 1); + if (out) + *out = task; + return result_OK; } +void checker_destroy(checker_task_t *task) +{ + free(task); +} + static result_t checker_redraw(wuss_window_t *window, const wuss_event_t *event, void *task_data) @@ -185,7 +198,7 @@ result_t checker_handle(wuss_window_t *window, case wuss_EVENT_QUIT: /* one calloc'd block backs both windows; the task autocloses once the * second window goes, so free it here */ - free(cc); + checker_destroy(cc); return result_OK; default: diff --git a/libraries/wuss/test/tasks/checker.h b/libraries/wuss/test/tasks/checker.h index 63f7d4b..da576f0 100644 --- a/libraries/wuss/test/tasks/checker.h +++ b/libraries/wuss/test/tasks/checker.h @@ -32,8 +32,13 @@ checker_task_t; wuss_window_fn_t checker_handle; -/* create the two checkerboard windows against the given wuss instance */ -result_t checker_create(wuss_t *wuss, checker_task_t *task); +/* create the two checkerboard windows against the given wuss instance; + * if out is non-NULL, the task block is also returned through it */ +result_t checker_create(wuss_t *wuss, checker_task_t **out); + +/* free a task block allocated by checker_create; normally called by the + * window's wuss_EVENT_QUIT handler, not by callers directly */ +void checker_destroy(checker_task_t *task); #endif /* WUSS_APP */ diff --git a/libraries/wuss/test/tasks/clock.c b/libraries/wuss/test/tasks/clock.c index 5c2e99c..bfb50be 100644 --- a/libraries/wuss/test/tasks/clock.c +++ b/libraries/wuss/test/tasks/clock.c @@ -75,15 +75,16 @@ static void clock_draw_centred(bmfont_t *font, { bmfont_width_t width; point_t pos; - int len, fh; + int len, fh, ascent; len = (int) strlen(text); - bmfont_measure(font, text, len, INT_MAX, NULL, &width); - bmfont_get_info(font, NULL, &fh); + bmfont_measure(font, text, len, NULL, INT_MAX, NULL, &width); + bmfont_get_info(font, NULL, &fh, &ascent, NULL); pos.x = (int) (x - width / 2.0); - pos.y = (int) (y - fh / 2.0); - bmfont_draw(font, scr, text, len, fg, colour_rgba(0, 0, 0, 0), &pos, NULL); + pos.y = (int) (y - fh / 2.0) + ascent; + bmfont_draw(font, scr, text, len, fg, colour_rgba(0, 0, 0, 0), NULL, &pos, + NULL); } static result_t clock_create_window(wuss_t *wuss, @@ -94,19 +95,24 @@ static result_t clock_create_window(wuss_t *wuss, SIZE2D(160, 160), "Clock", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(160, 160), SIZE2D(0, 0), &task->window); } -result_t clock_create(wuss_t *wuss, bmfont_t *font, clock_task_t *task) +result_t clock_create(wuss_t *wuss, clock_task_t **out) { result_t rc; + clock_task_t *task; wuss_task_t *delegate; wuss_task_desc_t delegate_desc; - task->font = font; + task = calloc(1, sizeof(*task)); + if (task == NULL) + return result_OOM; + + task->font = wuss_get_font_n(wuss, 0); task->bg = colour_rgb(0x1D, 0x2B, 0x53); task->bezel = colour_rgb(0xFF, 0xF1, 0xE8); task->hand = colour_rgb(0xFF, 0xF1, 0xE8); @@ -127,9 +133,20 @@ result_t clock_create(wuss_t *wuss, bmfont_t *font, clock_task_t *task) rc = clock_create_window(wuss, task, delegate); if (rc != result_OK) + { wuss_task_destroy(delegate); /* unregister; its QUIT frees the task block */ + return rc; + } + + if (out) + *out = task; - return rc; + return result_OK; +} + +void clock_destroy(clock_task_t *task) +{ + free(task); } static result_t clock_redraw(const wuss_event_t *event, void *task_data) @@ -139,9 +156,9 @@ static result_t clock_redraw(const wuss_event_t *event, void *task_data) const box_t *content, *bounds; time_t now; struct tm *lt; - double cx, cy, r; - double hour_angle, minute_angle, second_angle; - int i, sx, sy; + double cx, cy, r; + double hour_angle, minute_angle, second_angle; + int i, sx, sy; cc = task_data; @@ -245,7 +262,7 @@ result_t clock_handle(wuss_window_t *window, return result_OK; case wuss_EVENT_QUIT: - free(cc); /* task_data was calloc'd per instance by the spawner */ + clock_destroy(cc); return result_OK; default: diff --git a/libraries/wuss/test/tasks/clock.h b/libraries/wuss/test/tasks/clock.h index 8fa7241..1e8ae8e 100644 --- a/libraries/wuss/test/tasks/clock.h +++ b/libraries/wuss/test/tasks/clock.h @@ -26,8 +26,13 @@ clock_task_t; wuss_window_fn_t clock_handle; /* create the clock window against the given wuss instance, lettering the - * face with the given (caller-owned) font */ -result_t clock_create(wuss_t *wuss, bmfont_t *font, clock_task_t *task); + * face with wuss's own regular font (wuss_get_font_n(wuss, 0)). if out is + * non-NULL, the task block is also returned through it */ +result_t clock_create(wuss_t *wuss, clock_task_t **out); + +/* free a task block allocated by clock_create; normally called by the + * window's wuss_EVENT_QUIT handler, not by callers directly */ +void clock_destroy(clock_task_t *task); #endif /* WUSS_APP */ diff --git a/libraries/wuss/test/tasks/curve.c b/libraries/wuss/test/tasks/curve.c index 9e39148..f00c703 100644 --- a/libraries/wuss/test/tasks/curve.c +++ b/libraries/wuss/test/tasks/curve.c @@ -79,12 +79,17 @@ static int curve_convex_hull(const point_t *src, int n, point_t *hull) return k; /* hull[0] == hull[k - 1], a closed loop */ } -result_t curve_create(wuss_t *wuss, curve_task_t *task) +result_t curve_create(wuss_t *wuss, curve_task_t **out) { result_t rc; + curve_task_t *task; wuss_task_t *delegate; wuss_task_desc_t delegate_desc; + task = calloc(1, sizeof(*task)); + if (task == NULL) + return result_OOM; + task->bg = colour_rgb(0xFF, 0xFF, 0xFF); task->line = colour_rgb(0x00, 0x00, 0x00); task->blob = colour_rgb(0xFF, 0x00, 0x00); @@ -116,14 +121,25 @@ result_t curve_create(wuss_t *wuss, curve_task_t *task) SIZE2D(220, 160), "Curve", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(220, 160), SIZE2D(0, 0), &task->window); if (rc != result_OK) + { wuss_task_destroy(delegate); /* unregister; its QUIT frees the task block */ + return rc; + } + + if (out) + *out = task; + + return result_OK; +} - return rc; +void curve_destroy(curve_task_t *task) +{ + free(task); } static int blob_hit(const point_t *p, int x, int y) @@ -239,17 +255,24 @@ static result_t curve_redraw(const wuss_event_t *event, curve_task_t *task) blob_colour(task, i)); } - /* curve-type label, pinned to the content area's top-left corner (bounds, - * not the per-redraw dirty piece "content", and not the scrolled document) - * so it stays put and readable on a partial redraw */ + /* curve-type label, anchored at document (2, 2) so it scrolls with the + * content: screen pos = content top-left - scroll + doc offset. Uses bounds, + * not the per-redraw dirty piece "content", so it is drawn whole on a + * partial redraw */ { bmfont_t *font = wuss_get_font(task->wuss); const char *name = curve_kind_name(task->npoints); - point_t pos = POINT(bounds->x0 + 2, bounds->y0 + 2); if (font != NULL) - bmfont_draw(font, scr, name, (int) strlen(name), - task->line, task->bg, &pos, NULL); + { + int ascent; + point_t pos; + + bmfont_get_info(font, NULL, NULL, &ascent, NULL); + pos = POINT(bounds->x0 - sx + 2, bounds->y0 - sy + 2 + ascent); + wuss_text_draw(task->wuss, 0, scr, name, (int) strlen(name), + task->line, task->bg, &pos, NULL); + } } return result_OK; @@ -341,7 +364,7 @@ result_t curve_handle(wuss_window_t *window, return curve_scroll(task, event->data.scroll.delta, window); case wuss_EVENT_QUIT: - free(task); /* task_data was calloc'd per instance by the spawner */ + curve_destroy(task); return result_OK; default: diff --git a/libraries/wuss/test/tasks/curve.h b/libraries/wuss/test/tasks/curve.h index 8c32415..fb729f0 100644 --- a/libraries/wuss/test/tasks/curve.h +++ b/libraries/wuss/test/tasks/curve.h @@ -31,8 +31,13 @@ curve_task_t; wuss_window_fn_t curve_handle; -/* create the curve window against the given wuss instance */ -result_t curve_create(wuss_t *wuss, curve_task_t *task); +/* create the curve window against the given wuss instance; if out is + * non-NULL, the task block is also returned through it */ +result_t curve_create(wuss_t *wuss, curve_task_t **out); + +/* free a task block allocated by curve_create; normally called by the + * window's wuss_EVENT_QUIT handler, not by callers directly */ +void curve_destroy(curve_task_t *task); #endif /* WUSS_APP */ diff --git a/libraries/wuss/test/tasks/gradient.c b/libraries/wuss/test/tasks/gradient.c index 1a21c87..b370e3e 100644 --- a/libraries/wuss/test/tasks/gradient.c +++ b/libraries/wuss/test/tasks/gradient.c @@ -78,12 +78,17 @@ static int dither(int index, int v, int x, int y) return CLAMP(v + (m * 16 / (n - 1)) - 8, 0, 255); } -result_t gradient_create(wuss_t *wuss, gradient_task_t *task) +result_t gradient_create(wuss_t *wuss, gradient_task_t **out) { result_t rc; + gradient_task_t *task; wuss_task_t *delegate; wuss_task_desc_t delegate_desc; + task = calloc(1, sizeof(*task)); + if (task == NULL) + return result_OOM; + task->wuss = wuss; task->dither_index = 1; /* 4x4, matching the original */ @@ -103,14 +108,25 @@ result_t gradient_create(wuss_t *wuss, gradient_task_t *task) SIZE2D(GRADIENT_OPEN_WIDTH, GRADIENT_OPEN_HEIGHT), "Gradient", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(GRADIENT_DOC_WIDTH, GRADIENT_DOC_HEIGHT), SIZE2D(0, 0), &task->window); if (rc != result_OK) + { wuss_task_destroy(delegate); /* unregister; its QUIT frees the task block */ + return rc; + } + + if (out) + *out = task; + + return result_OK; +} - return rc; +void gradient_destroy(gradient_task_t *task) +{ + free(task); } static result_t gradient_redraw(const wuss_event_t *event, void *task_data) @@ -143,21 +159,28 @@ static result_t gradient_redraw(const wuss_event_t *event, void *task_data) } } - /* matrix-size label, pinned to the content area's top-left corner (bounds, - * not the per-redraw dirty piece, and not the scrolled document) so it - * stays put on a partial redraw */ + /* matrix-size label, anchored at document (2, 2) so it scrolls with the + * content: screen pos = content top-left - scroll + doc offset. Uses bounds, + * not the per-redraw dirty piece, so it is drawn whole on a partial redraw */ { bmfont_t *font = wuss_get_font(gc->wuss); - int dim = gradient_dithers[di].dim; - char label[8]; - point_t pos = POINT(bounds->x0 + 2, bounds->y0 + 2); + int dim = gradient_dithers[di].dim; + char label[8]; colour_t ink = colour_rgb(0xFF, 0xFF, 0xFF); colour_t bg = colour_rgba(0, 0, 0, 0); /* transparent */ sprintf(label, "%dx%d", dim, dim); if (font != NULL) - bmfont_draw(font, scr, label, (int) strlen(label), ink, bg, &pos, NULL); + { + int ascent; + point_t pos; + + bmfont_get_info(font, NULL, NULL, &ascent, NULL); + pos = POINT(bounds->x0 - sx + 2, bounds->y0 - sy + 2 + ascent); + wuss_text_draw(gc->wuss, 0, scr, label, (int) strlen(label), ink, bg, + &pos, NULL); + } } return result_OK; @@ -208,7 +231,7 @@ result_t gradient_handle(wuss_window_t *window, return gradient_mouse(event, task_data); case wuss_EVENT_QUIT: - free(gc); /* task_data was calloc'd per instance by the spawner */ + gradient_destroy(gc); return result_OK; default: diff --git a/libraries/wuss/test/tasks/gradient.h b/libraries/wuss/test/tasks/gradient.h index e6ce817..09d8a00 100644 --- a/libraries/wuss/test/tasks/gradient.h +++ b/libraries/wuss/test/tasks/gradient.h @@ -21,9 +21,13 @@ gradient_task_t; wuss_window_fn_t gradient_handle; -/* create the gradient window against the given wuss instance */ -result_t gradient_create(wuss_t *wuss, gradient_task_t *task); +/* create the gradient window against the given wuss instance; if out is + * non-NULL, the task block is also returned through it */ +result_t gradient_create(wuss_t *wuss, gradient_task_t **out); +/* free a task block allocated by gradient_create; normally called by the + * window's wuss_EVENT_QUIT handler, not by callers directly */ +void gradient_destroy(gradient_task_t *task); #endif /* WUSS_APP */ diff --git a/libraries/wuss/test/tasks/greeble.c b/libraries/wuss/test/tasks/greeble.c index c2f9755..0d39cae 100644 --- a/libraries/wuss/test/tasks/greeble.c +++ b/libraries/wuss/test/tasks/greeble.c @@ -217,10 +217,10 @@ static void greeble_stamp(screen_t *scr, static result_t greeble_redraw(const wuss_event_t *event, greeble_task_t *task) { - screen_t *scr; - const box_t *content, *bounds; - colour_t palette[GREEBLE_NPALETTE][4]; - int p, r, c, sx, sy, ox, oy; + screen_t *scr; + const box_t *content, *bounds; + colour_t palette[GREEBLE_NPALETTE][4]; + int p, r, c, sx, sy, ox, oy; scr = event->data.redraw.scr; content = event->data.redraw.content; @@ -320,9 +320,9 @@ static result_t greeble_menu_select(greeble_task_t *task, rc = greeble_toggle_randpal(task, task->window); if (wuss_menu_should_keep_open(event)) - wuss_menu_set_item_ticked(task->menu_handle, &g_greeble_menu, - GREEBLE_MENU_RANDPAL, - task->random_prefab_palettes); + wuss_menu_tick_item_live(task->menu_handle, &g_greeble_menu, + GREEBLE_MENU_RANDPAL, + task->random_prefab_palettes); else task->menu_handle = NULL; @@ -349,9 +349,9 @@ result_t greeble_handle(wuss_window_t *window, { /* the menu struct is shared by every greeble window; sync its sole * tick to this window's state before it opens */ - int ticks[1]; + unsigned int ticks; - ticks[GREEBLE_MENU_RANDPAL] = task->random_prefab_palettes; + ticks = task->random_prefab_palettes ? 1u << GREEBLE_MENU_RANDPAL : 0; return wuss_menu_open_ticked(task->delegate, &g_greeble_menu, ticks, wuss_get_pointer(task->wuss), &task->menu_handle); @@ -370,7 +370,7 @@ result_t greeble_handle(wuss_window_t *window, return result_OK; case wuss_EVENT_QUIT: - free(task); /* task_data was calloc'd per instance by the spawner */ + greeble_destroy(task); return result_OK; default: @@ -378,14 +378,19 @@ result_t greeble_handle(wuss_window_t *window, } } -result_t greeble_create(wuss_t *wuss, greeble_task_t *task) +result_t greeble_create(wuss_t *wuss, greeble_task_t **out) { result_t rc; + greeble_task_t *task; wuss_task_t *delegate; wuss_task_desc_t delegate_desc; size2d_t grid_px; box_t content; + task = calloc(1, sizeof(*task)); + if (task == NULL) + return result_OOM; + /* the full generator grid in pixels: content sized so cols/rows land * exactly on the GREEBLE_MAX_* caps in 8-pixel tiles */ grid_px = SIZE2D(GREEBLE_MAX_COLS * GREEBLE_TILE_PX, @@ -414,7 +419,7 @@ result_t greeble_create(wuss_t *wuss, greeble_task_t *task) grid_px, "Greeble", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, grid_px, SIZE2D(0, 0), &task->window); @@ -427,7 +432,15 @@ result_t greeble_create(wuss_t *wuss, greeble_task_t *task) wuss_window_get_content_bounds(task->window, &content); greeble_relayout(task, &content); + if (out) + *out = task; + return result_OK; } +void greeble_destroy(greeble_task_t *task) +{ + free(task); +} + #endif /* WUSS_APP */ diff --git a/libraries/wuss/test/tasks/greeble.h b/libraries/wuss/test/tasks/greeble.h index e44a0bd..9c84325 100644 --- a/libraries/wuss/test/tasks/greeble.h +++ b/libraries/wuss/test/tasks/greeble.h @@ -47,8 +47,13 @@ greeble_task_t; wuss_window_fn_t greeble_handle; -/* create the greebling window against the given wuss instance */ -result_t greeble_create(wuss_t *wuss, greeble_task_t *task); +/* create the greebling window against the given wuss instance; if out is + * non-NULL, the task block is also returned through it */ +result_t greeble_create(wuss_t *wuss, greeble_task_t **out); + +/* free a task block allocated by greeble_create; normally called by the + * window's wuss_EVENT_QUIT handler, not by callers directly */ +void greeble_destroy(greeble_task_t *task); #endif /* WUSS_APP */ diff --git a/libraries/wuss/test/tasks/icons.c b/libraries/wuss/test/tasks/icons.c index 44cb800..c3b5034 100644 --- a/libraries/wuss/test/tasks/icons.c +++ b/libraries/wuss/test/tasks/icons.c @@ -23,7 +23,7 @@ #include "icons.h" #define ICONS_DOC_W 260 -#define ICONS_DOC_H 1160 /* taller than the window, so scrolling is exercised */ +#define ICONS_DOC_H 1310 /* taller than the window, so scrolling is exercised */ #define ICONS_MARGIN 28 /* left edge of everything except frame captions */ #define ICONS_ROW 20 /* vertical pitch between stacked simple icons */ @@ -40,10 +40,11 @@ enum ICONS_N_ICONSET = 5, /* frame + opton + optoff + radon + radoff from the loaded set */ ICONS_N_PATTERN = 2, /* frame + one PATTERN swatch */ ICONS_N_BORDERS = 6, /* frame + GROOVE + RIDGE + ACTION + DIVIDER labels */ + ICONS_N_SLIDERS = 4, /* frame + horizontal + vertical slider + state label */ ICONS_N_MENU = 8, /* frame + plain, ticked, swatch, submenu, disabled, rule, separator entry */ ICONS_NSPECS = ICONS_N_INTRO + ICONS_N_BUTTONS + ICONS_N_RADIOS + ICONS_N_BITMAPS + ICONS_N_ICONSET + ICONS_N_PATTERN + - ICONS_N_BORDERS + ICONS_N_MENU + ICONS_N_BORDERS + ICONS_N_SLIDERS + ICONS_N_MENU }; /* Running state threaded through the icons_add_* helpers: where to write the @@ -84,7 +85,7 @@ static void icons_add_intro(icons_layout_t *lay, int *button, int *counter) /* 4px larger on every side than a plain button to seat the DEFAULT icon's * 6px action surround */ s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN - 4, lay->y - 4, 88, 30); - s->type = wuss_ICON_TYPE_BUTTON; + s->type = wuss_ICON_TYPE_ACTION; s->text = "Press me"; s->fg = lay->black; s->bg = lay->window; @@ -103,7 +104,7 @@ static void icons_add_intro(icons_layout_t *lay, int *button, int *counter) s = &lay->specs[lay->n]; s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN, 980, 90, 52); - s->type = wuss_ICON_TYPE_BUTTON; + s->type = wuss_ICON_TYPE_ACTION; s->text = "Scrolled"; s->fg = lay->black; s->bg = lay->window; @@ -142,7 +143,7 @@ static void icons_add_buttons(icons_layout_t *lay) s = &lay->specs[lay->n]; s->fg = lay->black; s->bg = lay->window; - s->type = wuss_ICON_TYPE_BUTTON; + s->type = wuss_ICON_TYPE_ACTION; if (col == 0) { @@ -212,7 +213,7 @@ static void icons_add_radios(icons_layout_t *lay, int *opt, int *state) s->text = (r == 0) ? "Red" : (r == 1) ? "Green" : "Blue"; s->fg = lay->black; s->bg = wuss_NO_BACKGROUND; - s->group = 1; + s->u.radio.group = 1; lay->n++; } @@ -265,14 +266,14 @@ static void icons_add_bitmaps(icons_layout_t *lay, s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN + 10, top + 20, sprite->size.w, sprite->size.h); s->type = wuss_ICON_TYPE_BITMAP; - s->bitmap = sprite; + s->u.bitmap.image = sprite; lay->n++; s = &lay->specs[lay->n]; s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN + 20 + sprite->size.w, top + 20, sprite->size.w, sprite->size.h); s->type = wuss_ICON_TYPE_BITMAP; - s->bitmap = sprite; + s->u.bitmap.image = sprite; s->flags = wuss_ICON_FLAGS_INTERACTIVE; *hotspot = lay->n; lay->n++; @@ -287,10 +288,11 @@ static void icons_add_bitmaps(icons_layout_t *lay, static void icons_add_iconset(icons_layout_t *lay, const wuss_t *wuss) { static const char *const names[4] = { "opton", "optoff", "radon", "radoff" }; - wuss_icon_spec_t *s; - int top; - int idx[4]; - int i; + + wuss_icon_spec_t *s; + int top; + int idx[4]; + int i; for (i = 0; i < 4; i++) { @@ -321,7 +323,7 @@ static void icons_add_iconset(icons_layout_t *lay, const wuss_t *wuss) s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN + 10 + i * (w + 6), top + 20, w, h); s->type = wuss_ICON_TYPE_BITMAP; - s->icon_set = wuss_ICON_SET(idx[i]); + s->u.bitmap.set = wuss_ICON_SET(idx[i]); lay->n++; } @@ -348,7 +350,7 @@ static void icons_add_pattern(icons_layout_t *lay) s->type = wuss_ICON_TYPE_PATTERN; s->fg = lay->black; s->bg = lay->window; - s->pattern = screen_PATTERN_DIAGONAL; + s->u.pattern.tile = screen_PATTERN_DIAGONAL; lay->n++; lay->y = top + 76; @@ -379,7 +381,7 @@ static void icons_add_borders(icons_layout_t *lay) s->text = "Groove"; s->fg = lay->black; s->bg = lay->window; - s->border = wuss_ICON_BORDER_GROOVE; + s->u.label.border = wuss_ICON_BORDER_GROOVE; s->flags = wuss_ICON_FLAGS_JUSTIFY_CENTRE; lay->n++; @@ -389,7 +391,7 @@ static void icons_add_borders(icons_layout_t *lay) s->text = "Ridge"; s->fg = lay->black; s->bg = lay->window; - s->border = wuss_ICON_BORDER_RIDGE; + s->u.label.border = wuss_ICON_BORDER_RIDGE; s->flags = wuss_ICON_FLAGS_JUSTIFY_CENTRE; lay->n++; @@ -399,7 +401,7 @@ static void icons_add_borders(icons_layout_t *lay) s->text = "Action"; s->fg = lay->black; s->bg = lay->window; - s->border = wuss_ICON_BORDER_ACTION; + s->u.label.border = wuss_ICON_BORDER_ACTION; s->flags = wuss_ICON_FLAGS_JUSTIFY_CENTRE; lay->n++; @@ -409,13 +411,70 @@ static void icons_add_borders(icons_layout_t *lay) s->text = "Divider"; s->fg = lay->black; s->bg = lay->window; - s->border = wuss_ICON_BORDER_DIVIDER; + s->u.label.border = wuss_ICON_BORDER_DIVIDER; s->flags = wuss_ICON_FLAGS_JUSTIFY_CENTRE; lay->n++; lay->y = top + 166; } +/* A grouping frame captioned "Sliders", holding a horizontal slider, a + * vertical slider beside it and a label echoing whichever last moved. + * Returns the horizontal/vertical slider and echo-label indices via + * horiz/vert/state. */ +static void icons_add_sliders(icons_layout_t *lay, + int *horiz, + int *vert, + int *state) +{ + wuss_icon_spec_t *s; + int top; + + top = lay->y; + s = &lay->specs[lay->n]; + s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN, top, 200, 130); + s->type = wuss_ICON_TYPE_FRAME; + s->text = "Sliders"; + s->fg = lay->black; + s->bg = wuss_NO_BACKGROUND; + lay->n++; + + *horiz = lay->n; + s = &lay->specs[lay->n]; + s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN + 10, top + 20, 160, 20); + s->type = wuss_ICON_TYPE_SLIDER; + s->fg = lay->black; + s->bg = wuss_NO_BACKGROUND; + s->u.slider.orientation = wuss_SLIDER_HORIZONTAL; + s->u.slider.min = 0; + s->u.slider.max = 100; + s->u.slider.default_value = 25; + lay->n++; + + *vert = lay->n; + s = &lay->specs[lay->n]; + s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN + 160, top + 50, 20, 70); + s->type = wuss_ICON_TYPE_SLIDER; + s->fg = lay->black; + s->bg = wuss_NO_BACKGROUND; + s->u.slider.orientation = wuss_SLIDER_VERTICAL; + s->u.slider.min = 0; + s->u.slider.max = 10; + s->u.slider.default_value = 10; + lay->n++; + + *state = lay->n; + s = &lay->specs[lay->n]; + s->bbox = (box_t) BOX_POS_SIZE(ICONS_MARGIN + 10, top + 50, 140, 14); + s->type = wuss_ICON_TYPE_LABEL; + s->text = "horizontal: 25"; + s->fg = lay->black; + s->bg = wuss_NO_BACKGROUND; + lay->n++; + + lay->y = top + 146; +} + /* A grouping frame captioned "Menu", holding a menu-entry strip: plain, * ticked, a swatch entry, a submenu entry, a disabled entry, then a dashed * rule and a SEPARATOR-flagged entry below it. Hover the pointer over any live @@ -460,7 +519,7 @@ static void icons_add_menu(icons_layout_t *lay, int *ticked) s->text = "Layer colour"; s->fg = lay->black; s->bg = wuss_NO_BACKGROUND; - s->swatch = lay->red; + s->u.menu_entry.swatch = lay->red; s->flags = wuss_ICON_FLAGS_SWATCH; lay->n++; @@ -503,21 +562,25 @@ static void icons_add_menu(icons_layout_t *lay, int *ticked) /* ----------------------------------------------------------------------- */ -result_t icons_create(wuss_t *wuss, - bmfont_t *font, - const char *resources, - icons_task_t *task) +result_t icons_create(wuss_t *wuss, icons_task_t **out) { wuss_task_t *delegate; wuss_task_desc_t delegate_desc; wuss_icon_spec_t specs[ICONS_NSPECS]; wuss_icon_t *made[ICONS_NSPECS]; icons_layout_t lay; + icons_task_t *task; + const char *resources; const char *sprite_path; int i_button, i_counter, i_opt, i_state, i_hotspot, i_ticked; + int i_shoriz, i_svert, i_sstate; result_t rc; - task->font = font; + task = calloc(1, sizeof(*task)); + if (task == NULL) + return result_OOM; + + task->font = wuss_get_font_n(wuss, 0); task->label = colour_rgb(0x00, 0x00, 0x00); /* only the ruler-text glyph blend; approximates the wuss_COLOUR_WINDOW / * wuss_COLOUR_BACKDROP crosshatch the rulers sit on -- no public call @@ -529,9 +592,13 @@ result_t icons_create(wuss_t *wuss, task->count = 0; task->opt = NULL; task->state = NULL; - task->has_sprite = 0; - task->hotspot = NULL; + task->has_sprite = 0; + task->hotspot = NULL; + task->slider_horiz = NULL; + task->slider_vert = NULL; + task->slider_state = NULL; + resources = wuss_get_resources(wuss); sprite_path = path_join_filename(resources, 3, "resources", "wuss", path_join_leafname("ninepatch", "png")); if (bitmap_load_png(&task->sprite, sprite_path) == result_OK) @@ -557,7 +624,7 @@ result_t icons_create(wuss_t *wuss, { if (task->has_sprite) free(task->sprite.base); - free(task); /* nothing registered yet; the spawner will not free it */ + free(task); /* nothing registered yet; nobody else owns it */ return rc; } @@ -600,6 +667,7 @@ result_t icons_create(wuss_t *wuss, icons_add_iconset(&lay, wuss); icons_add_pattern(&lay); icons_add_borders(&lay); + icons_add_sliders(&lay, &i_shoriz, &i_svert, &i_sstate); icons_add_menu(&lay, &i_ticked); rc = wuss_icon_create_array(task->window, specs, lay.n, made); @@ -612,12 +680,18 @@ result_t icons_create(wuss_t *wuss, task->state = made[i_state]; if (i_hotspot >= 0) task->hotspot = made[i_hotspot]; - wuss_icon_set_selected(made[i_ticked], 1); /* "Show grid" starts ticked */ + task->slider_horiz = made[i_shoriz]; + task->slider_vert = made[i_svert]; + task->slider_state = made[i_sstate]; + wuss_icon_set_selected(task->window, made[i_ticked], 1); /* "Show grid" starts ticked */ /* fully built: from here a last-window close reaps the task and its * wuss_EVENT_QUIT frees task_data */ wuss_task_set_autoclose(delegate, 1); + if (out) + *out = task; + return result_OK; failure: @@ -625,6 +699,13 @@ result_t icons_create(wuss_t *wuss, return rc; } +void icons_destroy(icons_task_t *task) +{ + if (task->has_sprite) + free(task->sprite.base); + free(task); +} + #define ICONS_GRID 16 /* document-space pitch of the backdrop grid */ #define ICONS_AXIS_LABEL 64 /* label every Nth grid line along each axis */ @@ -657,6 +738,7 @@ static result_t icons_redraw(const wuss_event_t *event, void *task_data) int doc; int x; int y; + int ascent; tcx = task_data; @@ -668,6 +750,8 @@ static result_t icons_redraw(const wuss_event_t *event, void *task_data) ox = bounds->x0 - scroll.x; oy = bounds->y0 - scroll.y; + bmfont_get_info(tcx->font, NULL, NULL, &ascent, NULL); + /* Everything this task paints is anchored to the document, not the window, * so it scrolls rigidly with the content -- which is what Wuss's scroll * blit assumes. Nothing here is pinned to a window edge. Every draw is @@ -688,9 +772,9 @@ static result_t icons_redraw(const wuss_event_t *event, void *task_data) continue; snprintf(buf, sizeof(buf), "%d", doc); - pos = POINT(x + 2, oy + 2); + pos = POINT(x + 2, oy + 2 + ascent); bmfont_draw(tcx->font, scr, buf, (int) strlen(buf), - tcx->label, tcx->paper, &pos, NULL); + tcx->label, tcx->paper, NULL, &pos, NULL); } /* y-axis ruler: document y printed just right of the x=0 line, at each @@ -704,9 +788,9 @@ static result_t icons_redraw(const wuss_event_t *event, void *task_data) continue; snprintf(buf, sizeof(buf), "%d", doc); - pos = POINT(ox + 2, y + 2); + pos = POINT(ox + 2, y + 2 + ascent); bmfont_draw(tcx->font, scr, buf, (int) strlen(buf), - tcx->label, tcx->paper, &pos, NULL); + tcx->label, tcx->paper, NULL, &pos, NULL); } return result_OK; @@ -721,6 +805,15 @@ static result_t icons_icon(const wuss_event_t *event, void *task_data) tcx = task_data; icon = event->data.icon.icon; + if (icon == tcx->slider_horiz || icon == tcx->slider_vert) + { + const char *name; + + name = (icon == tcx->slider_horiz) ? "horizontal" : "vertical"; + snprintf(buf, sizeof(buf), "%s: %d", name, event->data.icon.value); + return wuss_icon_set_text(tcx->window, tcx->slider_state, buf); + } + /* a radio/option latches on MOUSE_UP -- report the state then */ if (event->data.icon.action == wuss_MOUSE_UP && (wuss_icon_get_type(icon) == wuss_ICON_TYPE_RADIO || @@ -730,7 +823,7 @@ static result_t icons_icon(const wuss_event_t *event, void *task_data) wuss_icon_get_text(icon), wuss_icon_get_selected(icon) ? "on" : "off", (icon == tcx->opt) ? "" : " (radio)"); - return wuss_icon_set_text(tcx->state, buf); + return wuss_icon_set_text(tcx->window, tcx->state, buf); } if (event->data.icon.action != wuss_MOUSE_DOWN) @@ -741,7 +834,7 @@ static result_t icons_icon(const wuss_event_t *event, void *task_data) tcx->count++; snprintf(buf, sizeof(buf), "%d", tcx->count); - return wuss_icon_set_text(tcx->counter, buf); + return wuss_icon_set_text(tcx->window, tcx->counter, buf); } result_t icons_handle(wuss_window_t *window, @@ -761,9 +854,7 @@ result_t icons_handle(wuss_window_t *window, return icons_icon(event, task_data); case wuss_EVENT_QUIT: - if (tcx->has_sprite) - free(tcx->sprite.base); - free(tcx); /* calloc'd per instance by the spawner */ + icons_destroy(tcx); return result_OK; default: diff --git a/libraries/wuss/test/tasks/icons.h b/libraries/wuss/test/tasks/icons.h index 775a2d2..c586ef3 100644 --- a/libraries/wuss/test/tasks/icons.h +++ b/libraries/wuss/test/tasks/icons.h @@ -12,9 +12,9 @@ #include "wuss/window.h" /* demonstrates wuss-managed work-area icons: labelled groups covering every - * icon type (buttons, radios/options, bitmaps, a pattern swatch, a menu-entry - * strip) plus a button placed far down the document to show icons scroll with - * the content and stay clickable */ + * icon type (buttons, radios/options, bitmaps, a pattern swatch, sliders, a + * menu-entry strip) plus a button placed far down the document to show icons + * scroll with the content and stay clickable */ typedef struct icons_task { wuss_window_t *window; @@ -28,17 +28,25 @@ typedef struct icons_task wuss_icon_t *state; /* label echoing radio/option selection */ bitmap_t sprite; /* borrowed by the two BITMAP icons; freed on close */ int has_sprite; - wuss_icon_t *hotspot; /* the interactive BITMAP icon */ + wuss_icon_t *hotspot; /* the interactive BITMAP icon */ + wuss_icon_t *slider_horiz; /* horizontal slider */ + wuss_icon_t *slider_vert; /* vertical slider */ + wuss_icon_t *slider_state; /* label echoing whichever slider last moved */ } icons_task_t; wuss_window_fn_t icons_handle; -/* create the icons window against the given wuss instance */ -result_t icons_create(wuss_t *wuss, - bmfont_t *font, - const char *resources, - icons_task_t *task); +/* create the icons window against the given wuss instance, lettering the + * axis rulers with wuss's own regular font (wuss_get_font_n(wuss, 0)); the + * sprite and icon set are loaded from + * wuss_get_resources(wuss)/resources/wuss/. if out is non-NULL, the task + * block is also returned through it */ +result_t icons_create(wuss_t *wuss, icons_task_t **out); + +/* free a task block allocated by icons_create; normally called by the + * window's wuss_EVENT_QUIT handler, not by callers directly */ +void icons_destroy(icons_task_t *task); #endif /* WUSS_APP */ diff --git a/libraries/wuss/test/tasks/image.c b/libraries/wuss/test/tasks/image.c index f4f56c3..c04da8b 100644 --- a/libraries/wuss/test/tasks/image.c +++ b/libraries/wuss/test/tasks/image.c @@ -21,54 +21,102 @@ #include "image.h" -#define NINEPATCHSZ 9 +#define NINEPATCHSZ 9 #define IMAGE_BORDERSZ 8 /* solid inset band drawn inside the ninepatch */ #define IMAGE_MARGINSZ (NINEPATCHSZ + IMAGE_BORDERSZ) -#define IMAGE_EXT ".png" +#define IMAGE_EXT ".png" + +/* screen_copy_bitmap and screen_copy_ninepatch only understand a deep 32bpp + * source in R,G,B,A/X byte order (see bitmap_load_png()); bitmap_load_png + * keeps a palette-type PNG as pixelfmt_p8, so convert one back to rgbx8888 + * here rather than teach every blitter a paletted source. rgbx8888, not + * bgrx8888: the latter is BGR (SDL display byte order) and would swap red + * and blue once the blitters re-read it as rgba8888. */ +static result_t load_png_deep(bitmap_t *bm, const char *filename) +{ + result_t rc; + bitmap_t *deep; + + rc = bitmap_load_png(bm, filename); + if (rc != result_OK) + return rc; + + if (bm->format != pixelfmt_p8) + return result_OK; + + rc = bitmap_convert(bm, pixelfmt_rgbx8888, &deep); + if (rc != result_OK) + { + free(bm->base); + return rc; + } + + free(bm->base); + free(bm->palette); + *bm = *deep; + free(deep); -result_t image_create(wuss_t *wuss, - const char *resources, - const char *path, - const char *background_path, - image_task_t *task) + return result_OK; +} + +result_t image_create(wuss_t *wuss, image_task_t **out) { result_t rc; + image_task_t *task; wuss_task_t *delegate; wuss_task_desc_t delegate_desc; + const char *resources; const char *images_dir; + const char *path; + const char *background_path; size2d_t sz; + task = calloc(1, sizeof(*task)); + if (task == NULL) + return result_OOM; + task->wuss = wuss; task->delegate = NULL; - task->resources = resources; task->index = 0; task->nnames = 0; task->menu = NULL; task->proginfo = NULL; + task->colourmenu = NULL; task->menu_handle = NULL; + task->dithering = 1; + resources = wuss_get_resources(wuss); images_dir = path_join_filename(resources, 2, "resources", "images"); rc = namelist_scan(images_dir, IMAGE_EXT, task->names[0], sizeof(task->names[0]), IMAGE_MAX_NAMES, 0 /* unsorted */, &task->nnames); if (rc != result_OK) { - free(task); /* nothing registered yet; the spawner will not free it */ + free(task); /* nothing registered yet; nobody else owns it */ return rc; } + if (task->nnames == 0) + { + free(task); /* nothing registered yet; nobody else owns it */ + return result_BAD_ARG; /* no PNGs found under resources/images */ + } - rc = bitmap_load_png(&task->bitmap, path); + path = path_join_filename(resources, 3, "resources", "images", + path_join_leafname(task->names[0], "png")); + rc = load_png_deep(&task->bitmap, path); if (rc != result_OK) { - free(task); /* nothing registered yet; the spawner will not free it */ + free(task); /* nothing registered yet; nobody else owns it */ return rc; } - rc = bitmap_load_png(&task->ninepatch, background_path); + background_path = path_join_filename(resources, 2, "resources", "wuss", + path_join_leafname("ninepatch", "png")); + rc = load_png_deep(&task->ninepatch, background_path); if (rc != result_OK) { free(task->bitmap.base); - free(task); /* nothing registered yet; the spawner will not free it */ + free(task); /* nothing registered yet; nobody else owns it */ return rc; } @@ -81,7 +129,7 @@ result_t image_create(wuss_t *wuss, { free(task->bitmap.base); free(task->ninepatch.base); - free(task); /* nothing registered yet; the spawner will not free it */ + free(task); /* nothing registered yet; nobody else owns it */ return rc; } task->delegate = delegate; @@ -97,7 +145,7 @@ result_t image_create(wuss_t *wuss, SIZE2D(sz.w, sz.h), "Image", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(palette_PICO8_PINK), + wuss_NO_BACKDROP, sz, SIZE2D(32, 32), &task->window); @@ -114,7 +162,7 @@ result_t image_create(wuss_t *wuss, static const wuss_proginfo_desc_t desc = { "Image", - "Cycle the PNGs under resources/images", + "View the PNGs under resources/images", "(c) DPTLib contributors", "1.0 (" __DATE__ ")" }; @@ -123,18 +171,43 @@ result_t image_create(wuss_t *wuss, task->proginfo = NULL; } + /* The "Background" menu row's submenu. Hung off the descriptor menu as a + * wuss_menu_item_t.submenu in image_open_menu. A create failure is + * non-fatal -- the task just runs without a Background submenu. */ + if (wuss_colourmenu_create(&task->colourmenu, wuss, "Background") != result_OK) + task->colourmenu = NULL; + + if (out) + *out = task; + return result_OK; } +void image_destroy(image_task_t *task) +{ + /* close any open chain first: it may hold the proginfo window as a + * borrowed wuss_menu_item_t.window, and destroying that below would leave + * the chain pointing at freed memory */ + wuss_menu_close(task->menu_handle); + wuss_menu_destroy(task->menu); + wuss_proginfo_destroy(task->proginfo); /* closes its dialogue window */ + wuss_colourmenu_destroy(task->colourmenu); + free(task->bitmap.base); + free(task->ninepatch.base); + free(task); /* task_data was calloc'd per instance by the spawner */ +} + static result_t image_redraw(const wuss_event_t *event, void *task_data) { - image_task_t *ic; - screen_t *scr; - const box_t *bounds; - int sx, sy; - int bx, by; - box_t behind; - box_t band[4]; + image_task_t *ic; + screen_t *scr; + const box_t *bounds; + int sx, sy; + int bx, by; + int bx0, bx1, by1; + box_t behind; + box_t band[4]; + const colour_t *palette; ic = task_data; @@ -144,33 +217,28 @@ static result_t image_redraw(const wuss_event_t *event, void *task_data) sy = event->data.redraw.scroll.y; bx = bounds->x0 - sx + IMAGE_MARGINSZ; by = bounds->y0 - sy + IMAGE_MARGINSZ; + bx0 = bx - IMAGE_BORDERSZ; + bx1 = bx + ic->bitmap.size.w + IMAGE_BORDERSZ; + by1 = by + ic->bitmap.size.h; behind.x0 = bx - IMAGE_MARGINSZ; behind.y0 = by - IMAGE_MARGINSZ; behind.x1 = behind.x0 + ic->bitmap.size.w + IMAGE_MARGINSZ * 2; behind.y1 = behind.y0 + ic->bitmap.size.h + IMAGE_MARGINSZ * 2; - screen_copy_ninepatch(scr, &behind, &ic->ninepatch, 0); + screen_copy_ninepatch(scr, &behind, &ic->ninepatch, screen_NINEPATCH_NO_CENTRE); /* solid 8px band between the ninepatch frame and the image */ - band[0].x0 = bx - IMAGE_BORDERSZ; - band[0].y0 = by - IMAGE_BORDERSZ; - band[0].x1 = bx + ic->bitmap.size.w + IMAGE_BORDERSZ; - band[0].y1 = by; /* top */ - band[1].x0 = bx - IMAGE_BORDERSZ; - band[1].y0 = by + ic->bitmap.size.h; - band[1].x1 = bx + ic->bitmap.size.w + IMAGE_BORDERSZ; - band[1].y1 = by + ic->bitmap.size.h + IMAGE_BORDERSZ; /* bottom */ - band[2].x0 = bx - IMAGE_BORDERSZ; - band[2].y0 = by; - band[2].x1 = bx; - band[2].y1 = by + ic->bitmap.size.h; /* left */ - band[3].x0 = bx + ic->bitmap.size.w; - band[3].y0 = by; - band[3].x1 = bx + ic->bitmap.size.w + IMAGE_BORDERSZ; - band[3].y1 = by + ic->bitmap.size.h; /* right */ - screen_fill_rects(scr, band, 4, colour_rgb(0xFF, 0x77, 0xA8)); /* PICO-8 pink */ - - screen_copy_bitmap(scr, bx, by, &ic->bitmap); + band[0] = (box_t) { bx0, by - IMAGE_BORDERSZ, bx1, by }; /* top */ + band[1] = (box_t) { bx0, by1, bx1, by1 + IMAGE_BORDERSZ }; /* bottom */ + band[2] = (box_t) { bx0, by, bx, by1 }; /* left */ + band[3] = (box_t) { bx + ic->bitmap.size.w, by, bx1, by1 }; /* right */ + palette = wuss_get_palette(ic->wuss, NULL); + screen_fill_rects(scr, band, 4, palette[ic->background]); + + if (ic->dithering) + screen_copy_bitmap_dithered(scr, bx, by, &ic->bitmap); + else + screen_copy_bitmap(scr, bx, by, &ic->bitmap); return result_OK; } @@ -180,6 +248,7 @@ static result_t image_click(wuss_window_t *window, int step) { result_t rc; + const char *resources; const char *leafname; const char *filename; char buf[DPTLIB_MAXPATH]; @@ -191,12 +260,13 @@ static result_t image_click(wuss_window_t *window, ic->index = (ic->index + step + ic->nnames) % ic->nnames; - leafname = path_join_leafname(ic->names[ic->index], "png"); - filename = path_join_filename(ic->resources, 3, "resources", "images", - leafname); + resources = wuss_get_resources(ic->wuss); + leafname = path_join_leafname(ic->names[ic->index], "png"); + filename = path_join_filename(resources, 3, "resources", "images", + leafname); strcpy(buf, filename); - rc = bitmap_load_png(&next, buf); + rc = load_png_deep(&next, buf); if (rc != result_OK) { logf_warning("image: skipping \"%s\" (rc=0x%X)", buf, rc); @@ -237,9 +307,13 @@ static result_t image_open_menu(image_task_t *ic) wuss_menu_t *m; int i; + /* '!Dithering' pulls ic->dithering directly, so the row's tick already + * matches live state -- no separate wuss_menu_open_ticked pass needed. + * '!Wireframe' has no backing field; it is a demo row always ticked. */ rc = wuss_menu_create_from_desc(&m, - "Image, Info, New..., Open, !Show grid, !Wireframe, >Export, |Quit", - &image_menu_export); + "Image, Info, New..., Open, !Dithering, !Wireframe, >Export, " + "Background, |Quit", + ic->dithering, 1, &image_menu_export); if (rc != result_OK) return rc; @@ -256,11 +330,29 @@ static result_t image_open_menu(image_task_t *ic) break; } + /* Same trick for "Background": point it at the colourmenu's own menu tree + * as a submenu, so it gets the usual arrow-and-hover behaviour. Marked + * BORROWED_SUBMENU so wuss_menu_destroy leaves it alone -- it is owned by + * ic->colourmenu, built once at task creation and reused on every open, + * not by this per-open tree. */ + if (ic->colourmenu != NULL) + for (i = 0; i < m->nitems; i++) + if (m->items[i].text != NULL && strcmp(m->items[i].text, "Background") == 0) + { + wuss_menu_item_t *it = (wuss_menu_item_t *) &m->items[i]; + + it->submenu = wuss_colourmenu_menu(ic->colourmenu); + it->flags |= wuss_MENU_ITEM_BORROWED_SUBMENU; + break; + } + wuss_menu_destroy(ic->menu); ic->menu = m; - return wuss_menu_open(ic->delegate, ic->menu, wuss_get_pointer(ic->wuss), - &ic->menu_handle); + return wuss_menu_open(ic->delegate, + ic->menu, + wuss_get_pointer(ic->wuss), + &ic->menu_handle); } result_t image_handle(wuss_window_t *window, @@ -292,28 +384,46 @@ result_t image_handle(wuss_window_t *window, return result_OK; case wuss_EVENT_MENU_SELECT: + { + wuss_colour_t picked; + int mine; + + picked = wuss_colourmenu_selected(ic->colourmenu, event, &mine); + if (mine) + { + ic->background = picked; + wuss_window_invalidate_visible(ic->window); + if (!wuss_menu_should_keep_open(event)) + ic->menu_handle = NULL; /* SELECT pick already freed the chain */ + return result_OK; + } + } + menu = event->data.menu_select.menu; index = event->data.menu_select.index; printf("image menu: picked \"%s\"\n", menu->items[index].text ? menu->items[index].text : "(sep)"); + if (index == 3) { + ic->dithering = !ic->dithering; + wuss_window_invalidate_visible(ic->window); + } if (!wuss_menu_should_keep_open(event)) ic->menu_handle = NULL; /* SELECT pick already freed the chain */ + else if (index == 3) + wuss_menu_tick_item_live(ic->menu_handle, ic->menu, 3, ic->dithering); return result_OK; case wuss_EVENT_MENU_CLOSED: ic->menu_handle = NULL; /* wuss closed the chain under us */ return result_OK; + case wuss_EVENT_PRE_SHOW: + if (window == wuss_proginfo_window(ic->proginfo)) + return wuss_proginfo_handle_pre_show(ic->proginfo); + return result_OK; + case wuss_EVENT_QUIT: - /* close any open chain first: it may hold the proginfo window as a - * borrowed wuss_menu_item_t.window, and destroying that below would leave - * 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 */ + image_destroy(ic); return result_OK; default: diff --git a/libraries/wuss/test/tasks/image.h b/libraries/wuss/test/tasks/image.h index 6c4f3ab..405058f 100644 --- a/libraries/wuss/test/tasks/image.h +++ b/libraries/wuss/test/tasks/image.h @@ -6,6 +6,7 @@ #ifdef WUSS_APP #include "framebuf/bitmap.h" +#include "wuss/component/colourmenu.h" #include "wuss/component/proginfo.h" #include "wuss/menu.h" #include "wuss/window.h" @@ -23,7 +24,6 @@ typedef struct image_task wuss_task_t *delegate; /* the wuss task backing this window */ bitmap_t bitmap; /* owned: base freed by the caller when done */ bitmap_t ninepatch; /* owned: 9-patch tiled behind the main image */ - const char *resources; /* root passed to path_join_filename on click */ int index; /* current position in names[] */ char names[IMAGE_MAX_NAMES][IMAGE_MAX_NAME_LEN]; /* owned: * leafnames (extension stripped) found under @@ -35,22 +35,29 @@ typedef struct image_task wuss_proginfo_t *proginfo; /* owned: the "Info" row's standard dialogue, * hung off the menu as a wuss_menu_item_t.window; * freed at QUIT */ + wuss_colourmenu_t *colourmenu; /* owned: the "Background" row's submenu, + * hung off the menu as a wuss_menu_item_t.submenu; + * freed at QUIT */ wuss_menu_handle_t menu_handle; /* the open chain, if any -- closed before the * proginfo it borrows is destroyed at QUIT */ + int dithering; /* enable Bayer ordered dithering */ + wuss_colour_t background; } image_task_t; wuss_window_fn_t image_handle; -/* load the PNG at path (and the 9-patch PNG at background_path, drawn tiled - * behind it) and create its window against the given wuss instance. - * resources is the root resource directory, kept so a click can cycle - * through resources/images (PNG files) by reloading a different leafname. */ -result_t image_create(wuss_t *wuss, - const char *resources, - const char *path, - const char *background_path, - image_task_t *task); +/* scan wuss_get_resources(wuss)/resources/images for PNGs, load the first + * one found (and the 9-patch PNG at + * wuss_get_resources(wuss)/resources/wuss/ninepatch.png, drawn tiled behind + * it), and create its window against the given wuss instance. a click cycles + * to a different leafname from the scan. if out is non-NULL, the task block + * is also returned through it */ +result_t image_create(wuss_t *wuss, image_task_t **out); + +/* free a task block allocated by image_create; normally called by the + * window's wuss_EVENT_QUIT handler, not by callers directly */ +void image_destroy(image_task_t *task); #endif /* WUSS_APP */ diff --git a/libraries/wuss/test/tasks/lissajous.c b/libraries/wuss/test/tasks/lissajous.c index 69a6c04..9b44045 100644 --- a/libraries/wuss/test/tasks/lissajous.c +++ b/libraries/wuss/test/tasks/lissajous.c @@ -21,11 +21,16 @@ static const int lissajous_freqs[][2] = { 3, 2 }, { 5, 4 }, { 3, 4 }, { 5, 6 }, { 1, 2 }, { 7, 4 } }; -result_t lissajous_create(wuss_t *wuss, lissajous_task_t *task) +result_t lissajous_create(wuss_t *wuss, lissajous_task_t **out) { - result_t rc; - wuss_task_t *delegate; - wuss_task_desc_t delegate_desc; + result_t rc; + lissajous_task_t *task; + wuss_task_t *delegate; + wuss_task_desc_t delegate_desc; + + task = calloc(1, sizeof(*task)); + if (task == NULL) + return result_OOM; task->bg = colour_rgb(0x00, 0x00, 0x00); task->fg = colour_rgb(0x00, 0xFF, 0x00); @@ -51,14 +56,25 @@ result_t lissajous_create(wuss_t *wuss, lissajous_task_t *task) SIZE2D(220, 220), "Lissajous", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(220, 220), SIZE2D(0, 0), &task->window); if (rc != result_OK) + { wuss_task_destroy(delegate); /* unregister; its QUIT frees the task block */ + return rc; + } + + if (out) + *out = task; - return rc; + return result_OK; +} + +void lissajous_destroy(lissajous_task_t *task) +{ + free(task); } static result_t lissajous_redraw(const wuss_event_t *event, void *task_data) @@ -169,7 +185,7 @@ result_t lissajous_handle(wuss_window_t *window, return lissajous_idle(task_data); case wuss_EVENT_QUIT: - free(lc); /* task_data was calloc'd per instance by the spawner */ + lissajous_destroy(lc); return result_OK; default: diff --git a/libraries/wuss/test/tasks/lissajous.h b/libraries/wuss/test/tasks/lissajous.h index 2fd194e..20a5cc7 100644 --- a/libraries/wuss/test/tasks/lissajous.h +++ b/libraries/wuss/test/tasks/lissajous.h @@ -27,8 +27,13 @@ lissajous_task_t; wuss_window_fn_t lissajous_handle; /* create the Lissajous window against the given wuss instance; "task" is a - * per-instance block owned by the window and freed when it closes */ -result_t lissajous_create(wuss_t *wuss, lissajous_task_t *task); + * per-instance block owned by the window and freed when it closes. if out is + * non-NULL, the task block is also returned through it */ +result_t lissajous_create(wuss_t *wuss, lissajous_task_t **out); + +/* free a task block allocated by lissajous_create; normally called by the + * window's wuss_EVENT_QUIT handler, not by callers directly */ +void lissajous_destroy(lissajous_task_t *task); #endif /* WUSS_APP */ diff --git a/libraries/wuss/test/tasks/minesweeper.c b/libraries/wuss/test/tasks/minesweeper.c index ceeeea2..3432976 100644 --- a/libraries/wuss/test/tasks/minesweeper.c +++ b/libraries/wuss/test/tasks/minesweeper.c @@ -23,21 +23,65 @@ #define MS_BORDER MINESWEEPER_CELL /* one grid unit all round */ #define MS_HUD_H MINESWEEPER_CELL /* extra strip above the border for the * mine counter and timer */ -#define MS_GRID_W (MINESWEEPER_COLS * MINESWEEPER_CELL) -#define MS_GRID_H (MINESWEEPER_ROWS * MINESWEEPER_CELL) -#define MS_WIDTH (MS_GRID_W + 2 * MS_BORDER) -#define MS_HEIGHT (MS_GRID_H + 2 * MS_BORDER + MS_HUD_H) + +/* board geometry depends on the current grid size, so these are functions + * rather than macros; MS_GRID_W/H, MS_WIDTH/HEIGHT below are their obvious + * shorthand */ +#define MS_GRID_W(ms) ((ms)->cols * MINESWEEPER_CELL) +#define MS_GRID_H(ms) ((ms)->rows * MINESWEEPER_CELL) +#define MS_WIDTH(ms) (MS_GRID_W(ms) + 2 * MS_BORDER) +#define MS_HEIGHT(ms) (MS_GRID_H(ms) + 2 * MS_BORDER + MS_HUD_H) /* the timer's 3-digit field, window-local content coords: right-aligned in * the HUD strip, mirroring minesweeper_draw_hud's placement */ -#define MS_TIMER_X0 (MS_WIDTH - MS_BORDER - MINESWEEPER_CELL * 2) -#define MS_TIMER_BOX \ - ((box_t) { MS_TIMER_X0, 0, MS_WIDTH - MS_BORDER, MS_HUD_H }) +#define MS_TIMER_X0(ms) (MS_WIDTH(ms) - MS_BORDER - MINESWEEPER_CELL * 2) +#define MS_TIMER_BOX(ms) \ + ((box_t) { MS_TIMER_X0(ms), 0, MS_WIDTH(ms) - MS_BORDER, MS_HUD_H }) + +/* the mines-remaining field, window-local content coords: left-aligned in + * the HUD strip, mirroring minesweeper_draw_hud's placement */ +#define MS_MINES_BOX(ms) \ + ((box_t) { MS_BORDER, 0, MS_BORDER + MINESWEEPER_CELL * 2, MS_HUD_H }) + +/* window-local pixel box of the cell range [r0,r1) x [c0,c1); mirrors + * minesweeper_draw_cell's own x,y placement */ +#define MS_CELLS_BOX(r0, c0, r1, c1) \ + ((box_t) { MS_BORDER + (c0) * MINESWEEPER_CELL, \ + MS_HUD_H + MS_BORDER + (r0) * MINESWEEPER_CELL, \ + MS_BORDER + (c1) * MINESWEEPER_CELL, \ + MS_HUD_H + MS_BORDER + (r1) * MINESWEEPER_CELL }) + +/* rows, cols, mines offered by the "Grid Size" submenu, indexed by + * minesweeper_size_t; mine counts follow the classic ~14% density */ +static const struct { int rows, cols, mines; } +g_minesweeper_sizes[minesweeper_NSIZES] = +{ + { 24, 24, 99 }, + { 12, 24, 50 }, + { 16, 16, 40 }, + { 12, 16, 30 }, + { 12, 12, 20 } +}; + +static wuss_menu_item_t g_minesweeper_size_items[] = +{ + { "24x24", wuss_MENU_ITEM_NONE, NULL, NULL }, + { "24x12", wuss_MENU_ITEM_NONE, NULL, NULL }, + { "16x16", wuss_MENU_ITEM_NONE, NULL, NULL }, + { "16x12", wuss_MENU_ITEM_NONE, NULL, NULL }, + { "12x12", wuss_MENU_ITEM_NONE, NULL, NULL } +}; + +static const wuss_menu_t g_minesweeper_size_menu = +{ + "Grid Size", g_minesweeper_size_items, NELEMS(g_minesweeper_size_items) +}; -/* MENU click over the board pops this single-item menu */ +/* MENU click over the board pops this menu */ static const wuss_menu_item_t g_minesweeper_menu_items[] = { - { "New Game", wuss_MENU_ITEM_NONE, NULL, NULL } + { "New Game", wuss_MENU_ITEM_NONE, NULL, NULL }, + { "Grid Size", wuss_MENU_ITEM_NONE, &g_minesweeper_size_menu, NULL } }; static const wuss_menu_t g_minesweeper_menu = @@ -64,9 +108,9 @@ static colour_t minesweeper_number_colour(int n) /* ----------------------------------------------------------------------- */ -static bool minesweeper_in_bounds(int r, int c) +static bool minesweeper_in_bounds(minesweeper_task_t *ms, int r, int c) { - return r >= 0 && r < MINESWEEPER_ROWS && c >= 0 && c < MINESWEEPER_COLS; + return r >= 0 && r < ms->rows && c >= 0 && c < ms->cols; } static int minesweeper_count_neighbours(minesweeper_task_t *ms, int r, int c) @@ -77,7 +121,8 @@ static int minesweeper_count_neighbours(minesweeper_task_t *ms, int r, int c) for (dr = -1; dr <= 1; dr++) for (dc = -1; dc <= 1; dc++) if ((dr || dc) && - minesweeper_in_bounds(r + dr, c + dc) && ms->mine[r + dr][c + dc]) + minesweeper_in_bounds(ms, r + dr, c + dc) && + ms->mine[r + dr][c + dc]) n++; return n; @@ -92,10 +137,10 @@ static void minesweeper_place_mines(minesweeper_task_t *ms, int placed, r, c; placed = 0; - while (placed < MINESWEEPER_MINES) + while (placed < ms->mines) { - r = rand() % MINESWEEPER_ROWS; - c = rand() % MINESWEEPER_COLS; + r = rand() % ms->rows; + c = rand() % ms->cols; if (ms->mine[r][c] || (abs(r - safe_r) <= 1 && abs(c - safe_c) <= 1)) continue; @@ -106,13 +151,26 @@ static void minesweeper_place_mines(minesweeper_task_t *ms, ms->placed = true; } +/* inclusive-exclusive board-cell range [r0,r1) x [c0,c1) touched by a flood + * reveal, accumulated by minesweeper_reveal so the caller can invalidate + * just that area instead of the whole board */ +typedef struct { int r0, c0, r1, c1; } minesweeper_cellbox_t; + /* reveals (r,c) and, if it has no adjacent mines, floods outward to its * neighbours; recursion depth is bounded by the (small, fixed) board size */ -static void minesweeper_reveal(minesweeper_task_t *ms, int r, int c) +static void minesweeper_reveal(minesweeper_task_t *ms, + int r, + int c, + minesweeper_cellbox_t *touched) { int dr, dc; - if (!minesweeper_in_bounds(r, c) || ms->state[r][c] != minesweeper_HIDDEN) + if (r < touched->r0) touched->r0 = r; + if (c < touched->c0) touched->c0 = c; + if (r + 1 > touched->r1) touched->r1 = r + 1; + if (c + 1 > touched->c1) touched->c1 = c + 1; + + if (!minesweeper_in_bounds(ms, r, c) || ms->state[r][c] != minesweeper_HIDDEN) return; ms->state[r][c] = minesweeper_REVEALED; @@ -127,7 +185,7 @@ static void minesweeper_reveal(minesweeper_task_t *ms, int r, int c) for (dr = -1; dr <= 1; dr++) for (dc = -1; dc <= 1; dc++) if (dr || dc) - minesweeper_reveal(ms, r + dr, c + dc); + minesweeper_reveal(ms, r + dr, c + dc, touched); } /* on death, reveal every mine so the player sees where they all were */ @@ -135,8 +193,8 @@ static void minesweeper_reveal_all_mines(minesweeper_task_t *ms) { int r, c; - for (r = 0; r < MINESWEEPER_ROWS; r++) - for (c = 0; c < MINESWEEPER_COLS; c++) + for (r = 0; r < ms->rows; r++) + for (c = 0; c < ms->cols; c++) if (ms->mine[r][c]) ms->state[r][c] = minesweeper_REVEALED; } @@ -145,8 +203,8 @@ static bool minesweeper_check_won(minesweeper_task_t *ms) { int r, c; - for (r = 0; r < MINESWEEPER_ROWS; r++) - for (c = 0; c < MINESWEEPER_COLS; c++) + for (r = 0; r < ms->rows; r++) + for (c = 0; c < ms->cols; c++) if (!ms->mine[r][c] && ms->state[r][c] != minesweeper_REVEALED) return false; @@ -164,6 +222,19 @@ static void minesweeper_reset(minesweeper_task_t *ms) memset(ms->state, 0, sizeof(ms->state)); /* minesweeper_HIDDEN == 0 */ } +/* applies a grid size (rows/cols/mines) and clears the board; does not touch + * the window, so it is also used at creation before task->window exists -- + * the wuss_EVENT_MENU_SELECT handler resizes the window itself afterwards */ +static void minesweeper_set_size(minesweeper_task_t *ms, + minesweeper_size_t size) +{ + ms->size = size; + ms->rows = g_minesweeper_sizes[size].rows; + ms->cols = g_minesweeper_sizes[size].cols; + ms->mines = g_minesweeper_sizes[size].mines; + minesweeper_reset(ms); +} + /* seconds since the first reveal, frozen once the game has ended */ static void minesweeper_tick_clock(minesweeper_task_t *ms) { @@ -176,17 +247,20 @@ static void minesweeper_tick_clock(minesweeper_task_t *ms) /* ----------------------------------------------------------------------- */ -result_t minesweeper_create(wuss_t *wuss, - bmfont_t *font, - minesweeper_task_t *task) +result_t minesweeper_create(wuss_t *wuss, minesweeper_task_t **out) { - result_t rc; - wuss_task_t *delegate; - wuss_task_desc_t delegate_desc; + result_t rc; + minesweeper_task_t *task; + wuss_task_t *delegate; + wuss_task_desc_t delegate_desc; + + task = calloc(1, sizeof(*task)); + if (task == NULL) + return result_OOM; task->wuss = wuss; - task->font = font; - minesweeper_reset(task); + task->font = wuss_get_font_n(wuss, 1); + minesweeper_set_size(task, minesweeper_SIZE_12X12); delegate_desc.handle = minesweeper_handle; delegate_desc.task_data = task; @@ -194,24 +268,35 @@ result_t minesweeper_create(wuss_t *wuss, rc = wuss_task_create(wuss, &delegate_desc, &delegate); if (rc != result_OK) { - free(task); /* nothing registered yet; the spawner will not free it */ + free(task); /* nothing registered yet; nobody else owns it */ return rc; } task->task = delegate; wuss_task_set_autoclose(delegate, 1); rc = wuss_window_create_placed(delegate, - SIZE2D(MS_WIDTH, MS_HEIGHT), + SIZE2D(MS_WIDTH(task), MS_HEIGHT(task)), "Minesweeper", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), - SIZE2D(MS_WIDTH, MS_HEIGHT), + wuss_NO_BACKDROP, + SIZE2D(MS_WIDTH(task), MS_HEIGHT(task)), SIZE2D(0, 0), &task->window); if (rc != result_OK) + { wuss_task_destroy(delegate); /* unregister; its QUIT frees the task block */ + return rc; + } + + if (out) + *out = task; - return rc; + return result_OK; +} + +void minesweeper_destroy(minesweeper_task_t *task) +{ + free(task); } static void minesweeper_draw_cell(minesweeper_task_t *ms, @@ -260,16 +345,16 @@ static void minesweeper_draw_cell(minesweeper_task_t *ms, char buf[2]; point_t pos; bmfont_width_t width; - int fh; + int fh, ascent; buf[0] = (char) ('0' + n); buf[1] = '\0'; - bmfont_measure(ms->font, buf, 1, INT_MAX, NULL, &width); - bmfont_get_info(ms->font, NULL, &fh); + bmfont_measure(ms->font, buf, 1, NULL, INT_MAX, NULL, &width); + bmfont_get_info(ms->font, NULL, &fh, &ascent, NULL); pos.x = x + (MINESWEEPER_CELL - width) / 2; - pos.y = y + (MINESWEEPER_CELL - fh) / 2; + pos.y = y + (MINESWEEPER_CELL - fh) / 2 + ascent; bmfont_draw(ms->font, scr, buf, 1, minesweeper_number_colour(n), - colour_rgba(0, 0, 0, 0), &pos, NULL); + colour_rgba(0, 0, 0, 0), NULL, &pos, NULL); } } @@ -279,23 +364,23 @@ static void minesweeper_draw_hud(minesweeper_task_t *ms, screen_t *scr, const box_t *bounds) { - char buf[8]; - point_t pos; + char buf[8]; + point_t pos; colour_t fg, bg; - int fh; + int fh, ascent; fg = colour_rgb(0xFF, 0x00, 0x00); bg = colour_rgb(0x00, 0x00, 0x00); - bmfont_get_info(ms->font, NULL, &fh); + bmfont_get_info(ms->font, NULL, &fh, &ascent, NULL); - sprintf(buf, "%03d", MINESWEEPER_MINES - ms->flags); + sprintf(buf, "%03d", ms->mines - ms->flags); pos.x = bounds->x0 + MS_BORDER; - pos.y = bounds->y0 + (MS_HUD_H - fh) / 2; - bmfont_draw(ms->font, scr, buf, 3, fg, bg, &pos, NULL); + pos.y = bounds->y0 + (MS_HUD_H - fh) / 2 + ascent; + bmfont_draw(ms->font, scr, buf, 3, fg, bg, NULL, &pos, NULL); sprintf(buf, "%03d", ms->elapsed); - pos.x = bounds->x0 + MS_WIDTH - MS_BORDER - MINESWEEPER_CELL * 2; - bmfont_draw(ms->font, scr, buf, 3, fg, bg, &pos, NULL); + pos.x = bounds->x0 + MS_WIDTH(ms) - MS_BORDER - MINESWEEPER_CELL * 2; + bmfont_draw(ms->font, scr, buf, 3, fg, bg, NULL, &pos, NULL); } /* draws a translucent-looking banner strip across the middle of the board; @@ -309,63 +394,77 @@ static void minesweeper_draw_banner(minesweeper_task_t *ms, { bmfont_width_t width; point_t pos; - int len, fh, strip_y; + int len, fh, ascent, strip_y; len = (int) strlen(text); - bmfont_measure(ms->font, text, len, INT_MAX, NULL, &width); - bmfont_get_info(ms->font, NULL, &fh); + bmfont_measure(ms->font, text, len, NULL, INT_MAX, NULL, &width); + bmfont_get_info(ms->font, NULL, &fh, &ascent, NULL); - strip_y = bounds->y0 + MS_HUD_H + (MS_GRID_H + 2 * MS_BORDER - fh) / 2 - 2; - screen_fill_rect(scr, bounds->x0, strip_y, SIZE2D(MS_WIDTH, fh + 4), bg); + strip_y = bounds->y0 + MS_HUD_H + (MS_GRID_H(ms) + 2 * MS_BORDER - fh) / 2 - 2; + screen_fill_rect(scr, bounds->x0, strip_y, SIZE2D(MS_WIDTH(ms), fh + 4), bg); - pos.x = bounds->x0 + (MS_WIDTH - width) / 2; - pos.y = strip_y + 2; - bmfont_draw(ms->font, scr, text, len, fg, colour_rgba(0, 0, 0, 0), &pos, - NULL); + pos.x = bounds->x0 + (MS_WIDTH(ms) - width) / 2; + pos.y = strip_y + 2 + ascent; + bmfont_draw(ms->font, scr, text, len, fg, colour_rgba(0, 0, 0, 0), NULL, + &pos, NULL); } static result_t minesweeper_redraw(const wuss_event_t *event, void *task_data) { minesweeper_task_t *ms; - screen_t *scr; - const box_t *bounds; - const box_t *clip; - int board_y0; - bool board_dirty; - int r, c; + screen_t *scr; + const box_t *bounds; + const box_t *clip; + int board_y0; + int r0, c0, r1, c1; + int r, c; ms = task_data; scr = event->data.redraw.scr; bounds = event->data.redraw.bounds; clip = event->data.redraw.content; - /* the HUD strip is cheap and always redrawn; the frame, cells and banner - * only when the dirty region reaches below the strip (an idle timer tick - * invalidates just the timer field, so it skips all of that) */ - board_y0 = bounds->y0 + MS_HUD_H; - board_dirty = clip->y1 > board_y0; + /* the HUD strip and the board below are repainted independently, each only + * when the dirty region actually reaches it -- a flag toggle invalidates + * one cell plus the mine counter, a timer tick just the timer field, so + * either half alone is the common case and this keeps both cheap */ + board_y0 = bounds->y0 + MS_HUD_H; minesweeper_tick_clock(ms); - /* the window owns every pixel (wuss_NO_BACKGROUND), so the HUD strip's own - * backdrop is painted here before the counters go on top */ - screen_fill_rect(scr, bounds->x0, bounds->y0, SIZE2D(MS_WIDTH, MS_HUD_H), - colour_rgb(0x80, 0x80, 0x80)); - minesweeper_draw_hud(ms, scr, bounds); + if (clip->y0 < board_y0) + { + /* the window owns every pixel (wuss_NO_BACKGROUND), so the HUD strip's + * own backdrop is painted here before the counters go on top */ + screen_fill_rect(scr, bounds->x0, bounds->y0, + SIZE2D(MS_WIDTH(ms), MS_HUD_H), + colour_rgb(0x80, 0x80, 0x80)); + minesweeper_draw_hud(ms, scr, bounds); + } - if (!board_dirty) + if (clip->y1 <= board_y0) return result_OK; - /* one-cell frame right round the grid */ + /* one-cell frame right round the grid; drawn under the cells, so only the + * margin strip actually needs filling -- but that strip is thin and + * irregular to clip precisely, and it changes only on a resize/new game, + * which already invalidates the whole window, so no clipping is worth + * doing here */ screen_fill_rect(scr, bounds->x0, board_y0, - SIZE2D(MS_WIDTH, MS_HEIGHT - MS_HUD_H), + SIZE2D(MS_WIDTH(ms), MS_HEIGHT(ms) - MS_HUD_H), colour_rgb(0x80, 0x80, 0x80)); - /* board is small and fixed, so just repaint every cell rather than working - * out which ones overlap event->data.redraw.content */ - for (r = 0; r < MINESWEEPER_ROWS; r++) - for (c = 0; c < MINESWEEPER_COLS; c++) + /* only the cells overlapping the dirty rect need redrawing */ + r0 = MAX(0, (clip->y0 - board_y0 - MS_BORDER) / MINESWEEPER_CELL); + c0 = MAX(0, (clip->x0 - bounds->x0 - MS_BORDER) / MINESWEEPER_CELL); + r1 = MIN(ms->rows, (clip->y1 - board_y0 - MS_BORDER + MINESWEEPER_CELL - 1) / + MINESWEEPER_CELL); + c1 = MIN(ms->cols, (clip->x1 - bounds->x0 - MS_BORDER + MINESWEEPER_CELL - 1) / + MINESWEEPER_CELL); + + for (r = r0; r < r1; r++) + for (c = c0; c < c1; c++) minesweeper_draw_cell(ms, scr, r, c, bounds->x0 + MS_BORDER, bounds->y0 + MS_HUD_H + MS_BORDER); @@ -388,8 +487,12 @@ static result_t minesweeper_mouse(minesweeper_task_t *ms, int r, c; if (button & wuss_BUTTON_MENU) + { + wuss_menu_tick_exclusive((wuss_menu_t *) &g_minesweeper_size_menu, + ms->size); return wuss_menu_open(ms->task, &g_minesweeper_menu, wuss_get_pointer(ms->wuss), NULL); + } if (ms->dead || ms->won) { @@ -405,11 +508,14 @@ static result_t minesweeper_mouse(minesweeper_task_t *ms, c = (point.x - MS_BORDER) / MINESWEEPER_CELL; r = (point.y - MS_HUD_H - MS_BORDER) / MINESWEEPER_CELL; if (point.x < MS_BORDER || point.y < MS_HUD_H + MS_BORDER || - !minesweeper_in_bounds(r, c)) + !minesweeper_in_bounds(ms, r, c)) return result_OK; if (button & wuss_BUTTON_SELECT) { + minesweeper_cellbox_t touched; + box_t local; + if (ms->state[r][c] == minesweeper_FLAGGED) return result_OK; if (!ms->placed) @@ -417,15 +523,33 @@ static result_t minesweeper_mouse(minesweeper_task_t *ms, minesweeper_place_mines(ms, r, c); ms->start_time = time(NULL); } - minesweeper_reveal(ms, r, c); + + touched.r0 = touched.r1 = r; + touched.c0 = touched.c1 = c; + minesweeper_reveal(ms, r, c, &touched); + if (ms->dead) + { minesweeper_reveal_all_mines(ms); + wuss_window_invalidate_visible(ms->window); /* every mine, plus banner */ + } else + { ms->won = minesweeper_check_won(ms); + if (ms->won) + wuss_window_invalidate_visible(ms->window); /* banner covers the lot */ + else + { + local = MS_CELLS_BOX(touched.r0, touched.c0, touched.r1, touched.c1); + wuss_window_invalidate(ms->window, &local); + } + } minesweeper_tick_clock(ms); } else if (button & wuss_BUTTON_ADJUST) { + box_t local = MS_CELLS_BOX(r, c, r + 1, c + 1); + if (ms->state[r][c] == minesweeper_HIDDEN) { ms->state[r][c] = minesweeper_FLAGGED; @@ -436,14 +560,24 @@ static result_t minesweeper_mouse(minesweeper_task_t *ms, ms->state[r][c] = minesweeper_HIDDEN; ms->flags--; } + else + { + return result_OK; + } + + /* the flag count in the HUD changed too */ + { + box_t mines_box = MS_MINES_BOX(ms); + + wuss_window_invalidate(ms->window, &mines_box); + } + wuss_window_invalidate(ms->window, &local); } else { return result_OK; } - wuss_window_invalidate_visible(ms->window); - return result_OK; } @@ -477,7 +611,7 @@ result_t minesweeper_handle(wuss_window_t *window, minesweeper_tick_clock(ms); if (ms->elapsed != was) { - box_t timer = MS_TIMER_BOX; + box_t timer = MS_TIMER_BOX(ms); wuss_window_invalidate(ms->window, &timer); } @@ -485,13 +619,23 @@ result_t minesweeper_handle(wuss_window_t *window, return result_OK; case wuss_EVENT_MENU_SELECT: - /* only item is "New Game" */ - minesweeper_reset(ms); + if (event->data.menu_select.menu == &g_minesweeper_size_menu) + minesweeper_set_size(ms, (minesweeper_size_t) + event->data.menu_select.index); + else /* top-level menu; only other item is "New Game" */ + minesweeper_reset(ms); + + { + size2d_t sz = SIZE2D(MS_WIDTH(ms), MS_HEIGHT(ms)); + + wuss_window_resize(ms->window, sz); + wuss_window_set_doc(ms->window, sz); + } wuss_window_invalidate_visible(ms->window); return result_OK; case wuss_EVENT_QUIT: - free(ms); /* task_data was calloc'd per instance by the spawner */ + minesweeper_destroy(ms); return result_OK; default: diff --git a/libraries/wuss/test/tasks/minesweeper.h b/libraries/wuss/test/tasks/minesweeper.h index 537fff5..d686f0a 100644 --- a/libraries/wuss/test/tasks/minesweeper.h +++ b/libraries/wuss/test/tasks/minesweeper.h @@ -13,11 +13,24 @@ #include "wuss/task.h" #include "wuss/window.h" -#define MINESWEEPER_COLS 12 -#define MINESWEEPER_ROWS 12 -#define MINESWEEPER_MINES 20 #define MINESWEEPER_CELL 16 /* pixels per cell */ +/* board sizes offered on the "Grid Size" menu; arrays are sized for the + * largest so a resize just changes ms->rows/cols/mines and re-clears */ +#define MINESWEEPER_MAX_ROWS 24 +#define MINESWEEPER_MAX_COLS 24 + +typedef enum minesweeper_size +{ + minesweeper_SIZE_24X24, + minesweeper_SIZE_24X12, + minesweeper_SIZE_16X16, + minesweeper_SIZE_16X12, + minesweeper_SIZE_12X12, + minesweeper_NSIZES +} +minesweeper_size_t; + typedef enum minesweeper_cell_state { minesweeper_HIDDEN, @@ -28,16 +41,18 @@ minesweeper_cell_state_t; /* classic minesweeper: Select reveals a cell (flood-filling neighbouring * zeros), Adjust toggles a flag. Mines are placed on the first reveal so the - * opening click is never a mine. A MENU-button click pops a "New Game" menu - * that resets the board. */ + * opening click is never a mine. A MENU-button click pops a menu with "New + * Game" and a "Grid Size" submenu that resets the board at a new size. */ typedef struct minesweeper_task { wuss_t *wuss; wuss_window_t *window; wuss_task_t *task; /* delegate; opens the New Game menu */ bmfont_t *font; /* borrowed; draws the neighbour counts */ - bool mine[MINESWEEPER_ROWS][MINESWEEPER_COLS]; - minesweeper_cell_state_t state[MINESWEEPER_ROWS][MINESWEEPER_COLS]; + minesweeper_size_t size; + int rows, cols, mines; + bool mine[MINESWEEPER_MAX_ROWS][MINESWEEPER_MAX_COLS]; + minesweeper_cell_state_t state[MINESWEEPER_MAX_ROWS][MINESWEEPER_MAX_COLS]; bool placed; /* mines placed yet? */ bool dead; /* a mine was revealed */ bool won; @@ -50,10 +65,13 @@ minesweeper_task_t; wuss_window_fn_t minesweeper_handle; /* create the minesweeper window against the given wuss instance, lettering - * neighbour counts with the given (caller-owned) font */ -result_t minesweeper_create(wuss_t *wuss, - bmfont_t *font, - minesweeper_task_t *task); + * neighbour counts with wuss's own bold font (wuss_get_font_n(wuss, 1)). + * if out is non-NULL, the task block is also returned through it */ +result_t minesweeper_create(wuss_t *wuss, minesweeper_task_t **out); + +/* free a task block allocated by minesweeper_create; normally called by the + * window's wuss_EVENT_QUIT handler, not by callers directly */ +void minesweeper_destroy(minesweeper_task_t *task); #endif /* WUSS_APP */ diff --git a/libraries/wuss/test/tasks/palette.c b/libraries/wuss/test/tasks/palette.c index 105dc64..5dfaa5c 100644 --- a/libraries/wuss/test/tasks/palette.c +++ b/libraries/wuss/test/tasks/palette.c @@ -11,7 +11,9 @@ #endif #include "base/utils.h" +#include "framebuf/bmfont.h" #include "framebuf/palettes.h" +#include "framebuf/pixelfmt.h" #include "geom/box.h" #include "io/namelist.h" #include "io/path.h" @@ -71,36 +73,50 @@ result_t palette_load_hex(const char *resources, /* ----------------------------------------------------------------------- */ -result_t palette_create(wuss_t *wuss, - const char *resources, - const char *startup_name, - palette_task_t *task) +result_t palette_create(wuss_t *wuss, palette_task_t **out) { - result_t rc; + result_t rc; + palette_task_t *task; wuss_task_desc_t delegate_desc; - const char *dir; - char dirbuf[DPTLIB_MAXPATH]; - int i; + const char *resources; + const char *dir; + char dirbuf[DPTLIB_MAXPATH]; + const colour_t *current; + int ncurrent; + int i; + + task = calloc(1, sizeof(*task)); + if (task == NULL) + return result_OOM; task->wuss = wuss; - task->resources = resources; task->invert = false; task->nnames = 0; task->selected = 0; - dir = path_join_filename(resources, 2, "resources", "palettes"); + resources = wuss_get_resources(wuss); + dir = path_join_filename(resources, 2, "resources", "palettes"); strcpy(dirbuf, dir); /* path_join_filename's buffer is reused by the scan */ namelist_scan(dirbuf, PALETTE_HEX_EXT, task->names[0], sizeof(task->names[0]), PALETTE_MAX_FILES, 1 /* sorted */, &task->nnames); - if (startup_name != NULL) + /* tick whichever *.hex file matches wuss's current system palette, so the + * picker starts in sync with what wuss_create actually loaded, without the + * caller having to tell us its leafname separately */ + current = wuss_get_palette(wuss, &ncurrent); + if (ncurrent == PALETTE_NCOLOURS) for (i = 0; i < task->nnames; i++) - if (strcmp(task->names[i], startup_name) == 0) + { + colour_t candidate[PALETTE_NCOLOURS]; + + if (palette_load_hex(resources, task->names[i], candidate) == result_OK && + memcmp(candidate, current, sizeof(candidate)) == 0) { task->selected = i; break; } + } /* built once; ticks are refreshed from task->selected/invert on each open */ for (i = 0; i < task->nnames; i++) @@ -127,32 +143,92 @@ result_t palette_create(wuss_t *wuss, free(task); /* nothing registered yet; the spawner will not free it */ return rc; } - wuss_task_set_autoclose(task->delegate, 1); rc = wuss_window_create_placed(task->delegate, SIZE2D(100, 100), "Palette", - wuss_WINDOW_NO_RESIZE_BLIT, /* swatch grid is laid out across the whole window, so a resize must redraw all of it, not just the newly (un)covered edge */ + wuss_WINDOW_DEFAULT | wuss_WINDOW_NO_RESIZE_BLIT, /* swatch grid is laid out across the whole window, so a resize must redraw all of it, not just the newly (un)covered edge */ wuss_BACKDROP_COLOUR(palette_PICO8_BLACK), SIZE2D(100, 100), SIZE2D(0, 0), &task->window); if (rc != result_OK) + { wuss_task_destroy(task->delegate); /* unregister; its QUIT frees the task block */ + return rc; + } + + rc = wuss_window_create_placed(task->delegate, + SIZE2D(100, 100), + "Screen", + wuss_WINDOW_DEFAULT | wuss_WINDOW_NO_RESIZE_BLIT, + wuss_BACKDROP_COLOUR(palette_PICO8_BLACK), + SIZE2D(100, 100), + SIZE2D(0, 0), + &task->window2); + if (rc != result_OK) + { + wuss_task_destroy(task->delegate); /* closes "Palette", QUIT frees the block */ + return rc; + } + + /* both windows up: from here, closing the last one reaps the task and its + * wuss_EVENT_QUIT frees task_data */ + wuss_task_set_autoclose(task->delegate, 1); + + if (out) + *out = task; + + return result_OK; +} + +void palette_destroy(palette_task_t *task) +{ + free(task); +} + +/* shared by both windows: paint a roughly-square grid of npalette swatches + * across bounds */ +static void palette_draw_grid(screen_t *scr, + const box_t *bounds, + int sx, + int sy, + const colour_t *palette, + int npalette) +{ + int cols, rows; + int cell_w, cell_h; + int i; + + cols = 1; + while (cols * cols < npalette) + cols++; + rows = (npalette + cols - 1) / cols; + + cell_w = (bounds->x1 - bounds->x0) / cols; + cell_h = (bounds->y1 - bounds->y0) / rows; + + for (i = 0; i < npalette; i++) + { + int col, row, x, y; + + col = i % cols; + row = i / cols; + x = bounds->x0 - sx + col * cell_w; + y = bounds->y0 - sy + row * cell_h; - return rc; + screen_fill_rect(scr, x, y, SIZE2D(cell_w, cell_h), palette[i]); + } } static result_t palette_redraw(const wuss_event_t *event, void *task_data) { palette_task_t *pc; const colour_t *palette; - int npalette; + int npalette; screen_t *scr; const box_t *bounds; - int cols, rows; - int cell_w, cell_h; - int i, sx, sy; + int sx, sy; pc = task_data; palette = wuss_get_palette(pc->wuss, &npalette); @@ -165,26 +241,54 @@ static result_t palette_redraw(const wuss_event_t *event, void *task_data) sx = event->data.redraw.scroll.x; sy = event->data.redraw.scroll.y; - cols = 1; - while (cols * cols < npalette) - cols++; - rows = (npalette + cols - 1) / cols; + palette_draw_grid(scr, bounds, sx, sy, palette, npalette); - cell_w = (bounds->x1 - bounds->x0) / cols; - cell_h = (bounds->y1 - bounds->y0) / rows; + return result_OK; +} - for (i = 0; i < npalette; i++) +/* "Screen" window: the physical screen bitmap's own palette, whatever size + * its pixel format needs (2/4/16/256 for 1/2/4/8bpp); 32bpp has none, so + * just label it */ +static result_t palette_redraw_screen(palette_task_t *pc, + const wuss_event_t *event) +{ + screen_t *scr; + const box_t *bounds; + int sx, sy, npalette; + + scr = event->data.redraw.scr; + bounds = event->data.redraw.bounds; + sx = event->data.redraw.scroll.x; + sy = event->data.redraw.scroll.y; + + if (scr->palette == NULL) { - int col, row, x, y; + static const char label[] = "32bpp (none)"; - col = i % cols; - row = i / cols; - x = bounds->x0 - sx + col * cell_w; - y = bounds->y0 - sy + row * cell_h; + bmfont_t *font = wuss_get_font(pc->wuss); + colour_t ink = colour_rgb(0xFF, 0xFF, 0xFF); + colour_t bg = colour_rgba(0, 0, 0, 0); /* transparent */ - screen_fill_rect(scr, x, y, SIZE2D(cell_w, cell_h), palette[i]); + screen_fill_rect(scr, bounds->x0 - sx, bounds->y0 - sy, + SIZE2D(bounds->x1 - bounds->x0, bounds->y1 - bounds->y0), + colour_rgb(0x00, 0x00, 0x00)); + if (font != NULL) + { + int ascent; + point_t pos; + + bmfont_get_info(font, NULL, NULL, &ascent, NULL); + pos = POINT(bounds->x0 - sx + 2, bounds->y0 - sy + 2 + ascent); + wuss_text_draw(pc->wuss, 0, scr, label, (int) strlen(label), ink, bg, + &pos, NULL); + } + return result_OK; } + npalette = 1 << (1 << pixelfmt_log2bpp(scr->format)); + + palette_draw_grid(scr, bounds, sx, sy, scr->palette, npalette); + return result_OK; } @@ -195,12 +299,11 @@ static result_t palette_redraw(const wuss_event_t *event, void *task_data) * touches the TICKED bit. */ static result_t palette_menu_open(palette_task_t *pc) { - int ticks[PALETTE_MAX_FILES + 1]; - int i; + unsigned int ticks; - for (i = 0; i < pc->nnames; i++) - ticks[i] = (i == pc->selected); - ticks[PALETTE_MENU_INVERT_INDEX(pc)] = pc->invert; + ticks = 1u << pc->selected; + if (pc->invert) + ticks |= 1u << PALETTE_MENU_INVERT_INDEX(pc); return wuss_menu_open_ticked(pc->delegate, &pc->menu, ticks, wuss_get_pointer(pc->wuss), &pc->menu_handle); @@ -224,7 +327,7 @@ static result_t palette_click(palette_task_t *pc, const wuss_event_t *event) * resulting wuss_EVENT_PALETTE and reads the new array back with * wuss_get_palette. A load failure is silently ignored: the picker just * stays on the previous selection. Ticks are also updated in place via - * wuss_menu_set_item_ticked, so an ADJUST pick (which keeps the chain open) + * wuss_menu_tick_item_live, so an ADJUST pick (which keeps the chain open) * shows the new tick without the menu being rebuilt or moved. */ static result_t palette_menu_select(palette_task_t *pc, const wuss_event_t *event) @@ -244,7 +347,7 @@ static result_t palette_menu_select(palette_task_t *pc, { pc->invert = !pc->invert; if (event->data.menu_select.button & wuss_BUTTON_ADJUST) - wuss_menu_set_item_ticked(pc->menu_handle, &pc->menu, index, pc->invert); + wuss_menu_tick_item_live(pc->menu_handle, &pc->menu, index, pc->invert); } else if (index >= 0 && index < pc->nnames) { @@ -253,10 +356,10 @@ static result_t palette_menu_select(palette_task_t *pc, pc->invert = false; if (event->data.menu_select.button & wuss_BUTTON_ADJUST) { - wuss_menu_set_item_ticked(pc->menu_handle, &pc->menu, old, 0); - wuss_menu_set_item_ticked(pc->menu_handle, &pc->menu, index, 1); - wuss_menu_set_item_ticked(pc->menu_handle, &pc->menu, - PALETTE_MENU_INVERT_INDEX(pc), 0); + wuss_menu_tick_item_live(pc->menu_handle, &pc->menu, old, 0); + wuss_menu_tick_item_live(pc->menu_handle, &pc->menu, index, 1); + wuss_menu_tick_item_live(pc->menu_handle, &pc->menu, + PALETTE_MENU_INVERT_INDEX(pc), 0); } } else @@ -270,7 +373,8 @@ static result_t palette_menu_select(palette_task_t *pc, if (pc->nnames == 0) return result_OK; - rc = palette_load_hex(pc->resources, pc->names[pc->selected], loaded); + rc = palette_load_hex(wuss_get_resources(pc->wuss), pc->names[pc->selected], + loaded); if (rc != result_OK) return result_OK; @@ -289,13 +393,13 @@ result_t palette_handle(wuss_window_t *window, { palette_task_t *pc; - NOT_USED(window); - pc = task_data; switch (event->kind) { case wuss_EVENT_REDRAW: + if (window == pc->window2) + return palette_redraw_screen(pc, event); return palette_redraw(event, task_data); case wuss_EVENT_MOUSE: @@ -308,8 +412,17 @@ result_t palette_handle(wuss_window_t *window, pc->menu_handle = NULL; /* wuss closed the chain under us */ return result_OK; + case wuss_EVENT_CLOSE: + if (window == pc->window2) + pc->window2 = NULL; + else + pc->window = NULL; + return result_OK; + case wuss_EVENT_QUIT: - free(task_data); /* calloc'd per instance by the spawner */ + /* one calloc'd block backs both windows; the task autocloses once the + * second one goes, so free it here */ + palette_destroy(pc); return result_OK; default: diff --git a/libraries/wuss/test/tasks/palette.h b/libraries/wuss/test/tasks/palette.h index c699ead..1d966c9 100644 --- a/libraries/wuss/test/tasks/palette.h +++ b/libraries/wuss/test/tasks/palette.h @@ -23,13 +23,21 @@ * an Invert toggle. A pick installs the loaded/inverted array via * wuss_set_palette; there is no callback here, since every other interested * party (framebuffer bitmap, physical palette) reacts to the resulting - * wuss_EVENT_PALETTE and reads the array back with wuss_get_palette. */ + * wuss_EVENT_PALETTE and reads the array back with wuss_get_palette. + * + * A second window ("Screen") draws the same kind of grid but for the + * physical screen bitmap's own palette (event->data.redraw.scr->palette), + * which is whatever size the screen's pixel format needs (2/4/16/256 + * entries for 1/2/4/8bpp) rather than the fixed 16-entry system palette + * above; a NULL screen palette (32bpp, no palette) just shows a label. */ typedef struct palette_task { wuss_t *wuss; wuss_task_t *delegate; wuss_window_t *window; - const char *resources; + wuss_window_t *window2; /* "Screen" window: the physical screen's + * own palette (1/2/4/8bpp) or a "none" + * label (32bpp has no palette) */ bool invert; /* *.hex files found under resources/palettes at create time, leafname @@ -44,7 +52,7 @@ typedef struct palette_task * copy built fresh on each open would not do */ wuss_menu_item_t menu_items[PALETTE_MAX_FILES + 1]; wuss_menu_t menu; - wuss_menu_handle_t menu_handle; /* for wuss_menu_set_item_ticked on an + wuss_menu_handle_t menu_handle; /* for wuss_menu_tick_item_live on an * ADJUST pick, which keeps the chain * open */ } @@ -61,17 +69,15 @@ result_t palette_load_hex(const char *resources, colour_t *out); /* create the palette-swatch-grid window against the given wuss instance. - * `resources` is the resources root (as passed to path_join_filename, i.e. - * "resources/palettes" holds the *.hex files); it is scanned once here to - * build the picker menu. The swatch grid always draws wuss's current system - * palette (wuss_get_palette), so `startup_name` -- its *.hex leafname, no - * extension -- is used only to tick the matching row in the picker menu; NULL - * ticks none. */ -result_t palette_create(wuss_t *wuss, - const char *resources, - const char *startup_name, - palette_task_t *task); + * wuss_get_resources(wuss)/resources/palettes holds the *.hex files; it is + * scanned once here to build the picker menu, ticking whichever file's + * content matches wuss's current system palette (wuss_get_palette), if any. + * if out is non-NULL, the task block is also returned through it. */ +result_t palette_create(wuss_t *wuss, palette_task_t **out); +/* free a task block allocated by palette_create; normally called by the + * window's wuss_EVENT_QUIT handler, not by callers directly */ +void palette_destroy(palette_task_t *task); #endif /* WUSS_APP */ diff --git a/libraries/wuss/test/tasks/porter-duff.c b/libraries/wuss/test/tasks/porter-duff.c index 24fadba..eef3712 100644 --- a/libraries/wuss/test/tasks/porter-duff.c +++ b/libraries/wuss/test/tasks/porter-duff.c @@ -157,17 +157,21 @@ static result_t load_demo_png(bitmap_t *bm, /* ----------------------------------------------------------------------- */ -result_t porter_duff_create(wuss_t *wuss, - const colour_t *palette, - bmfont_t *font, - const char *resources, - porter_duff_task_t *task) +result_t porter_duff_create(wuss_t *wuss, porter_duff_task_t **out) { - result_t rc; - wuss_task_t *delegate; - wuss_task_desc_t delegate_desc; + result_t rc; + porter_duff_task_t *task; + wuss_task_t *delegate; + wuss_task_desc_t delegate_desc; + const char *resources; + const colour_t *palette; + + task = calloc(1, sizeof(*task)); + if (task == NULL) + return result_OOM; - task->font = font; + palette = wuss_get_palette(wuss, NULL); + task->font = wuss_get_font_n(wuss, 0); task->rule = composite_RULE_CLEAR; task->frame = 0; task->frames_per_rule = PD_FRAMES_DEFAULT; @@ -176,6 +180,8 @@ result_t porter_duff_create(wuss_t *wuss, task->fg = palette[palette_PICO8_WHITE]; task->bg = palette[palette_PICO8_BLACK]; + resources = wuss_get_resources(wuss); + rc = load_demo_png(&task->a, resources, "A"); if (rc != result_OK) return rc; @@ -198,14 +204,14 @@ result_t porter_duff_create(wuss_t *wuss, delegate_desc.name = "porter-duff"; rc = wuss_task_create(wuss, &delegate_desc, &delegate); if (rc != result_OK) - goto free_dst; /* nothing registered yet; the spawner will not free task */ + goto free_dst; /* nothing registered yet; nobody else owns task */ wuss_task_set_autoclose(delegate, 1); rc = wuss_window_create_placed(delegate, SIZE2D(PD_SIZE, PD_SIZE + PD_LABEL_HEIGHT), "Porter-Duff", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(PD_SIZE, PD_SIZE + PD_LABEL_HEIGHT), SIZE2D(0, 0), &task->window); @@ -215,6 +221,9 @@ result_t porter_duff_create(wuss_t *wuss, return rc; } + if (out) + *out = task; + return result_OK; free_dst: @@ -225,11 +234,20 @@ result_t porter_duff_create(wuss_t *wuss, free(task->b.base); free_a: free(task->a.base); - free(task); /* no task was registered on any goto here; spawner won't free */ + free(task); /* no task was registered on any goto here; nobody else owns it */ return rc; } +void porter_duff_destroy(porter_duff_task_t *task) +{ + free(task->dst.base); + free(task->src.base); + free(task->b.base); + free(task->a.base); + free(task); +} + /* ----------------------------------------------------------------------- */ /* Triangle ramp: 0 at the start of the rule's turn, 255 at its midpoint, back @@ -275,15 +293,17 @@ static void porter_duff_ramp_src(porter_duff_task_t *pd, int ramp) static void porter_duff_draw_checkerboard(const porter_duff_task_t *pd, screen_t *scr, const box_t *content, - const box_t *bounds) + const box_t *bounds, + int sx, + int sy) { int x, y, lx, ly, band; for (y = content->y0; y < content->y1; y++) for (x = content->x0; x < content->x1; x++) { - lx = x - bounds->x0; - ly = y - bounds->y0; + lx = x - bounds->x0 + sx; + ly = y - bounds->y0 + sy; band = lx / PD_CHECKER_BAND + ly / PD_CHECKER_BAND; screen_set_pixel(scr, x, y, (band & 1) ? pd->dark : pd->light); @@ -299,14 +319,17 @@ static result_t porter_duff_redraw(const wuss_event_t *event, const box_t *content, *bounds; const char *name; point_t pos; + int sx, sy; pd = task_data; scr = event->data.redraw.scr; content = event->data.redraw.content; bounds = event->data.redraw.bounds; + sx = event->data.redraw.scroll.x; + sy = event->data.redraw.scroll.y; - porter_duff_draw_checkerboard(pd, scr, content, bounds); + porter_duff_draw_checkerboard(pd, scr, content, bounds, sx, sy); /* ponytail: the whole 256x256 pane is recomposited on every redraw -- two * full-image memcpys plus two full-image passes. Fine for one window in a @@ -322,14 +345,19 @@ static result_t porter_duff_redraw(const wuss_event_t *event, if (rc != result_OK) return rc; - screen_copy_bitmap(scr, bounds->x0, bounds->y0, &pd->dst); + screen_copy_bitmap(scr, bounds->x0 - sx, bounds->y0 - sy, &pd->dst); - name = rule_names[pd->rule]; - pos.x = bounds->x0 + 2; - pos.y = bounds->y0 + PD_SIZE + 2; + { + int ascent; + + bmfont_get_info(pd->font, NULL, NULL, &ascent, NULL); + name = rule_names[pd->rule]; + pos.x = bounds->x0 - sx + 2; + pos.y = bounds->y0 - sy + PD_SIZE + 2 + ascent; + } return bmfont_draw(pd->font, scr, name, (int) strlen(name), - pd->fg, pd->bg, &pos, NULL); + pd->fg, pd->bg, NULL, &pos, NULL); } static result_t porter_duff_idle(void *task_data) @@ -408,11 +436,7 @@ result_t porter_duff_handle(wuss_window_t *window, return porter_duff_idle(task_data); case wuss_EVENT_QUIT: - free(pd->dst.base); - free(pd->src.base); - free(pd->b.base); - free(pd->a.base); - free(pd); /* calloc'd per instance by the spawner */ + porter_duff_destroy(pd); return result_OK; default: diff --git a/libraries/wuss/test/tasks/porter-duff.h b/libraries/wuss/test/tasks/porter-duff.h index c617aa2..5e15dde 100644 --- a/libraries/wuss/test/tasks/porter-duff.h +++ b/libraries/wuss/test/tasks/porter-duff.h @@ -35,13 +35,16 @@ porter_duff_task_t; wuss_window_fn_t porter_duff_handle; /* load the two demo images and create the window against the given wuss - * instance; resources is the DPTLib repo root, for locating the bundled PNGs */ -result_t porter_duff_create(wuss_t *wuss, - const colour_t *palette, - bmfont_t *font, - const char *resources, - porter_duff_task_t *task); - + * instance, colouring the checkerboard/label from wuss's own palette + * (wuss_get_palette) and lettering the rule name with wuss's own regular + * font (wuss_get_font_n(wuss, 0)); the images are loaded from + * wuss_get_resources(wuss) (the DPTLib repo root); if out is non-NULL, the + * task block is also returned through it */ +result_t porter_duff_create(wuss_t *wuss, porter_duff_task_t **out); + +/* free a task block allocated by porter_duff_create; normally called by the + * window's wuss_EVENT_QUIT handler, not by callers directly */ +void porter_duff_destroy(porter_duff_task_t *task); #endif /* WUSS_APP */ diff --git a/libraries/wuss/test/tasks/saturn.c b/libraries/wuss/test/tasks/saturn.c index c28c4ea..4630840 100644 --- a/libraries/wuss/test/tasks/saturn.c +++ b/libraries/wuss/test/tasks/saturn.c @@ -3,7 +3,10 @@ #ifdef WUSS_APP #include +#include +#include #include +#include #ifdef FORTIFY #include "fortify/fortify.h" @@ -12,7 +15,12 @@ #include "base/utils.h" #include "framebuf/palettes.h" #include "geom/box.h" +#include "geom/size.h" +#include "geom/stack.h" #include "utils/rng.h" +#include "wuss/component/dialogue.h" +#include "wuss/component/proginfo.h" +#include "wuss/menu.h" #include "saturn.h" @@ -28,28 +36,26 @@ * bottom-up and wuss's run top-down, so that same expression - kept verbatim * here, only without the *4 - lands the sketch the same way up as it drew on * a BBC/RISC OS screen. The shear in loop 2 is asymmetric in y, so this is - * the term that actually decides which way the ring's shadow band tilts. + * the term that actually decides which way the ring's shadow tilts. * - * loop 1: X%,Y% in -127..127; keep if (X*X+Y*Y)/256 > 17 -> the ring + * loop 1: X%,Y% in -127..127; keep if (X*X+Y*Y)/256 > 17 -> the stars * loop 2: sheared X% = R6+R5/4, Y% = R6; keep if 32<=E<80 and - * (R5<0 or (X*X+Y*Y)/256 > 16) -> ring shadow band + * (R5<0 or (X*X+Y*Y)/256 > 16) -> the ring * loop 3: R1,R2 random; keep if R1*R1+R2*R2 < 16384; * x = sqrt(16384-P)/2 -> planet body */ -#define SATURN_SIZE 256 /* window is SATURN_SIZE x SATURN_SIZE */ +/* the sample space matches task->config.size (square), scaling the original + * -128..127 signed-byte range up or down with it; SATURN_SIZE_DEFAULT is + * only the value saturn_create falls back to when the caller passes NULL + * (see SATURN_CONFIG_DEFAULT in saturn.h). */ +#define SATURN_SIZE_DEFAULT 256 -/* The original works in a -128..127 sample space (BBC BASIC signed byte). */ -#define SATURN_HALF 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 */ +/* The original works in a -128..127 sample space (BBC BASIC signed byte), + * here scaled to whatever size is live; saturn_redraw derives these per-call + * as half/range/flip/energy_shift from task->config.size. */ -#define SATURN_RING_ITERS 477 /* loop 1: the ring */ -#define SATURN_BAND_ITERS 1280 /* loop 2: ring shadow band */ -#define SATURN_BODY_ITERS 1280 /* loop 3: planet body */ - -#define SATURN_BODY_R2 16384 /* planet body: keep if r1*r1+r2*r2 < this */ +#define SATURN_BODY_R2(half) ((half) * (half)) /* planet body: keep if r1*r1+r2*r2 < this */ /* BBC BASIC RND(n>0) returns an integer 1..n. utils/rng's LCG stands in for * it (its high bits, which is where an LCG's usable randomness sits) so a @@ -66,18 +72,129 @@ static int saturn_rnd(int n) return (int) ((rng_lcg32(&saturn_rnd_state) >> 16) % (uint32_t) n) + 1; } -/* one signed sample in the original -128..127 space */ -#define SATURN_SAMPLE() (saturn_rnd(SATURN_RANGE) - SATURN_HALF) +/* one signed sample in the original -128..127 space, scaled to half/range */ +#define SATURN_SAMPLE(half, range) (saturn_rnd(range) - (half)) + +/* MENU click over the content pops this. "Colours" leads to a submenu with + * one row per task->fg/task->bg, each of which pops a wuss_colourmenu (see + * saturn_create -- the two leaf items' submenu pointers are patched in there, + * once the colourmenus exist). "Configuration" and "Info" hover-open + * task->size_dialogue / task->proginfo's window, both borrowed windows built + * once in saturn_create and patched into g_saturn_menu_items[...].window + * there, the same way. */ +enum { SATURN_MENU_INFO = 0, SATURN_MENU_COLOURS, SATURN_MENU_SIZE }; +enum { SATURN_COLOURS_MENU_FOREGROUND = 0, SATURN_COLOURS_MENU_BACKGROUND }; + +/* the size dialogue's icons, in creation order (see saturn_conf_dialogue_create): + * one label/slider/value triple per SATURN_SIZEDLG_ROW_*, then the buttons */ +enum { + SATURN_SIZE_ICON_LABEL1 = 0, + SATURN_SIZE_ICON_SLIDER1, + SATURN_SIZE_ICON_VALUE1, + + SATURN_SIZE_ICON_LABEL2, + SATURN_SIZE_ICON_SLIDER2, + SATURN_SIZE_ICON_VALUE2, + + SATURN_SIZE_ICON_LABEL3, + SATURN_SIZE_ICON_SLIDER3, + SATURN_SIZE_ICON_VALUE3, + + SATURN_SIZE_ICON_LABEL4, + SATURN_SIZE_ICON_SLIDER4, + SATURN_SIZE_ICON_VALUE4, + + SATURN_SIZE_ICON_DEFAULT, + SATURN_SIZE_ICON_CANCEL, + SATURN_SIZE_ICON_APPLY, + + SATURN_SIZE_NICONS +}; + +/* per-row static description, indexed by SATURN_SIZEDLG_ROW_*: label text, + * slider [min,max] and the byte offset of the saturn_config_t field the row + * reads/writes (via *(int *) ((char *) &task->config + offset)), shared by + * saturn_conf_dialogue_create (to fill specs) and + * saturn_sizedlg_pre_show/saturn_sizedlg_apply (to sync task->config) */ +typedef struct saturn_sizedlg_rowdesc +{ + const char *label; + int min, max, step; + size_t config_offset; +} +saturn_sizedlg_rowdesc_t; + +static const saturn_sizedlg_rowdesc_t g_saturn_sizedlg_rows[SATURN_SIZEDLG_NROWS] = +{ + [SATURN_SIZEDLG_ROW_SIZE] = { "Size", SATURN_SIZE_MIN, SATURN_SIZE_MAX, SATURN_SIZE_STEP, offsetof(saturn_config_t, size) }, + [SATURN_SIZEDLG_ROW_STARS] = { "Stars", 1, 9999, 0, offsetof(saturn_config_t, stars_iters) }, + [SATURN_SIZEDLG_ROW_RING] = { "Ring", 1, 9999, 0, offsetof(saturn_config_t, ring_iters) }, + [SATURN_SIZEDLG_ROW_BODY] = { "Body", 1, 9999, 0, offsetof(saturn_config_t, body_iters) }, +}; + +/* task->config field a row reads/writes, per g_saturn_sizedlg_rows */ +static int *saturn_sizedlg_field(saturn_task_t *task, int row) +{ + return (int *) ((char *) &task->config + g_saturn_sizedlg_rows[row].config_offset); +} + +static wuss_menu_item_t g_saturn_colours_items[] = +{ + { "Foreground", wuss_MENU_ITEM_BORROWED_SUBMENU, NULL, NULL, 0 }, + { "Background", wuss_MENU_ITEM_BORROWED_SUBMENU, NULL, NULL, 0 } +}; -result_t saturn_create(wuss_t *wuss, saturn_task_t *task) +static wuss_menu_t g_saturn_colours_menu = { + "Colours", g_saturn_colours_items, NELEMS(g_saturn_colours_items) +}; + +static wuss_menu_item_t g_saturn_menu_items[] = +{ + { "Info", wuss_MENU_ITEM_BORROWED_SUBMENU, NULL, NULL, 0 }, + { "Colours", wuss_MENU_ITEM_NONE, &g_saturn_colours_menu, NULL, 0 }, + { "Configuration", wuss_MENU_ITEM_BORROWED_SUBMENU, NULL, NULL, 0 } +}; + +static wuss_menu_t g_saturn_menu = +{ + "Saturn", g_saturn_menu_items, NELEMS(g_saturn_menu_items) +}; + +static result_t saturn_conf_dialogue_create(saturn_task_t *task); +static result_t saturn_conf_fillout(void *opaque); +static result_t saturn_conf_cancel(void *opaque, wuss_button_t button); +static result_t saturn_conf_apply_action(void *opaque, wuss_button_t button); +static result_t saturn_conf_default_action(void *opaque, + wuss_button_t button); + +result_t saturn_create(wuss_t *wuss, saturn_task_t **out) +{ + static const saturn_config_t default_config = SATURN_CONFIG_DEFAULT; + result_t rc; + saturn_task_t *task; wuss_task_t *delegate; wuss_task_desc_t delegate_desc; - task->bg = colour_rgb(0x00, 0x00, 0x00); - task->fg = colour_rgb(0xFF, 0xFF, 0xFF); - task->seed = 1; + task = calloc(1, sizeof(*task)); + if (task == NULL) + return result_OOM; + + task->wuss = wuss; + task->bg = colour_rgb(0x00, 0x00, 0x00); + task->fg = colour_rgb(0xFF, 0xFF, 0xFF); + task->seed = 1; + task->config = default_config; + task->fg_colourmenu = NULL; + task->bg_colourmenu = NULL; + task->menu_handle = NULL; + task->conf.dialogue = NULL; + memset(task->conf.rows, 0, sizeof(task->conf.rows)); + task->conf.deflt = NULL; + task->conf.cancel = NULL; + task->conf.apply = NULL; + task->proginfo = NULL; /* saturn_redraw paints its own background */ delegate_desc.handle = saturn_handle; @@ -86,36 +203,111 @@ result_t saturn_create(wuss_t *wuss, saturn_task_t *task) rc = wuss_task_create(wuss, &delegate_desc, &delegate); if (rc != result_OK) { - free(task); /* nothing registered yet; the spawner will not free it */ + free(task); /* nothing registered yet; nobody else owns it */ return rc; } + task->delegate = delegate; /* the task the menu opens against */ wuss_task_set_autoclose(delegate, 1); + /* one instance per target, not shared: a submenu leaf (wuss_MENU_ITEM_BORROWED_SUBMENU) + * opens on hover with no pre-open hook to retitle/retarget a shared + * colourmenu first -- only item->window leaves get that (wuss_EVENT_PRE_SHOW), + * and colourmenu owns its own wuss_menu_t, not a window. */ + rc = wuss_colourmenu_create(&task->fg_colourmenu, wuss, "Foreground"); + if (rc != result_OK) + goto fail_delegate; + + rc = wuss_colourmenu_create(&task->bg_colourmenu, wuss, "Background"); + if (rc != result_OK) + goto fail_fg_colourmenu; + + g_saturn_colours_items[SATURN_COLOURS_MENU_FOREGROUND].submenu = + wuss_colourmenu_menu(task->fg_colourmenu); + g_saturn_colours_items[SATURN_COLOURS_MENU_BACKGROUND].submenu = + wuss_colourmenu_menu(task->bg_colourmenu); + rc = wuss_window_create_placed(delegate, - SIZE2D(SATURN_SIZE, SATURN_SIZE), + SIZE2D(task->config.size, task->config.size), "Saturn", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), - SIZE2D(SATURN_SIZE, SATURN_SIZE), + wuss_NO_BACKDROP, + SIZE2D(task->config.size, task->config.size), SIZE2D(0, 0), &task->window); if (rc != result_OK) - wuss_task_destroy(delegate); /* unregister; its QUIT frees the task block */ + goto fail_bg_colourmenu; + + rc = saturn_conf_dialogue_create(task); + if (rc != result_OK) + goto fail_bg_colourmenu; /* wuss_task_destroy closes task->window too */ + + g_saturn_menu_items[SATURN_MENU_SIZE].window = + wuss_dialogue_window(task->conf.dialogue); + + /* The "Info" menu row's standard dialogue. A create failure is non-fatal + * -- the task just runs without an Info dialogue (see image.c). */ + { + static const wuss_proginfo_desc_t desc = + { + "Saturn", + "Elite loading-screen planet, recreated", + "(c) DPTLib contributors", + "1.0 (" __DATE__ ")" + }; + + if (wuss_proginfo_create(&task->proginfo, delegate, &desc) != result_OK) + task->proginfo = NULL; + } + g_saturn_menu_items[SATURN_MENU_INFO].window = + wuss_proginfo_window(task->proginfo); + if (out) + *out = task; + + return result_OK; + +fail_bg_colourmenu: + wuss_colourmenu_destroy(task->bg_colourmenu); +fail_fg_colourmenu: + wuss_colourmenu_destroy(task->fg_colourmenu); +fail_delegate: + wuss_task_destroy(delegate); /* unregisters; its QUIT frees the task block */ return rc; } +void saturn_destroy(saturn_task_t *task) +{ + /* task->conf.dialogue's window is borrowed into g_saturn_menu_items as a + * submenu leaf; if the chain is still open at QUIT (wuss_destroy sweeps + * tasks before closing any leftover chain -- see its comment) that leaf's + * node->window would dangle once wuss_dialogue_destroy below frees it. + * Close our own chain first, same as wuss_destroy expects every task to + * do for whatever it still holds. */ + if (task->menu_handle != NULL) + { + wuss_menu_close(task->menu_handle); + task->menu_handle = NULL; + } + + wuss_colourmenu_destroy(task->fg_colourmenu); + wuss_colourmenu_destroy(task->bg_colourmenu); + wuss_dialogue_destroy(task->conf.dialogue); + wuss_proginfo_destroy(task->proginfo); + free(task); /* task_data was calloc'd per instance by the spawner */ +} + /* plot one point in window content space, clipped to the window. x,y are * already in the window's top-down pixel space (callers apply the RISC OS * (255 - ...) flip). */ static void saturn_plot(screen_t *scr, int ox, int oy, + int size, int x, int y, colour_t c) { - if (x < 0 || x >= SATURN_SIZE || y < 0 || y >= SATURN_SIZE) + if (x < 0 || x >= size || y < 0 || y >= size) return; screen_set_pixel(scr, ox + x, oy + y, c); @@ -125,6 +317,8 @@ static result_t saturn_redraw(const wuss_event_t *event, saturn_task_t *task) { screen_t *scr; const box_t *content, *bounds; + int size, half, range, flip, energy_shift; + int stars_p, ring_p, e_lo, e_hi; int ox, oy; int i; int x, y, p; @@ -135,75 +329,89 @@ static result_t saturn_redraw(const wuss_event_t *event, saturn_task_t *task) screen_fill_rect(scr, content->x0, content->y0, box_size(content), task->bg); - /* plot in doc space (saturn_plot clips to 0..SATURN_SIZE); origin carries - * the scroll so a scrolled/shrunk window shows the right slice */ + /* sample space scales with the window: -size/2..size/2-1, matching the + * original's -128..127 at task->config.size == SATURN_SIZE_DEFAULT */ + size = task->config.size; + half = size / 2; + range = size - 1; + flip = size - 1; + energy_shift = size; + + /* p's range grows linearly with size (p_max = size/2), so the original's + * fixed energy-gate constants (17, 16, 32, 80 at size==256) must scale + * with size too, or the exclusion disc shrinks relative to half as size + * grows and the stars/ring creep into the planet body. */ + stars_p = 17 * size / SATURN_SIZE_DEFAULT; + ring_p = 16 * size / SATURN_SIZE_DEFAULT; + e_lo = 32 * size / SATURN_SIZE_DEFAULT; + e_hi = 80 * size / SATURN_SIZE_DEFAULT; + + /* plot in doc space (saturn_plot clips to 0..size); origin carries the + * scroll so a scrolled/shrunk window shows the right slice */ ox = bounds->x0 - event->data.redraw.scroll.x; 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++) + /* loop 1 - the stars: keep points outside the inner disc */ + for (i = 0; i <= task->config.stars_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); + x = SATURN_SAMPLE(half, range); + y = SATURN_SAMPLE(half, range); + p = (x * x + y * y) / energy_shift; + if (p > stars_p) + saturn_plot(scr, ox, oy, size, x + half, flip - (y + half), task->fg); } - /* loop 2 - ring shadow band: sheared sample with a banded energy gate */ - for (i = 0; i <= SATURN_BAND_ITERS; i++) + /* loop 2 - the ring: sheared sample with a banded energy gate */ + for (i = 0; i <= task->config.ring_iters; i++) { int r5, r6, r7, e; - r5 = SATURN_SAMPLE(); - r6 = SATURN_SAMPLE(); + r5 = SATURN_SAMPLE(half, range); + r6 = SATURN_SAMPLE(half, range); r7 = r5 / 4; x = r6 + r7; y = r6; - p = (x * x + y * y) / SATURN_ENERGY_SHIFT; - e = ((r6 + r7) * (r6 + r7) + r5 * r5 + r6 * r6) / SATURN_ENERGY_SHIFT; - if (e >= 32 && e < 80 && (r5 < 0 || p > 16)) - saturn_plot(scr, ox, oy, x + SATURN_HALF, y + SATURN_HALF, task->fg); + p = (x * x + y * y) / energy_shift; + e = ((r6 + r7) * (r6 + r7) + r5 * r5 + r6 * r6) / energy_shift; + if (e >= e_lo && e < e_hi && (r5 < 0 || p > ring_p)) + saturn_plot(scr, ox, oy, size, x + half, y + half, task->fg); } /* loop 3 - planet body: filled half-disc offset right */ - for (i = 0; i <= SATURN_BODY_ITERS; i++) + for (i = 0; i <= task->config.body_iters; i++) { - int r1, r2; + int r1, r2, body_r2; - r1 = SATURN_SAMPLE(); - r2 = SATURN_SAMPLE(); - p = r1 * r1 + r2 * r2; - if (p < SATURN_BODY_R2) + r1 = SATURN_SAMPLE(half, range); + r2 = SATURN_SAMPLE(half, range); + p = r1 * r1 + r2 * r2; + body_r2 = SATURN_BODY_R2(half); + if (p < body_r2) { - x = (int) (sqrt((double) (SATURN_BODY_R2 - p)) / 2.0) + SATURN_HALF; - y = SATURN_FLIP - (r2 / 2 + SATURN_HALF); - saturn_plot(scr, ox, oy, x, y, task->fg); + x = (int) (sqrt((double) (body_r2 - p)) / 2.0) + half; + y = flip - (r2 / 2 + half); + saturn_plot(scr, ox, oy, size, x, y, task->fg); } } 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) + if (action != wuss_MOUSE_DOWN || window != task->window) return result_OK; + if (button & wuss_BUTTON_MENU) + return wuss_menu_open(task->delegate, &g_saturn_menu, + wuss_get_pointer(task->wuss), + &task->menu_handle); + if (button & wuss_BUTTON_SELECT) { task->seed += 0x9E3779B9UL; /* fresh sketch */ @@ -213,6 +421,361 @@ static result_t saturn_mouse(saturn_task_t *task, return result_OK; } +/* stack items for the size dialogue's layout: a VBOX of label / slider / + * value-echo / button-row, with fixed-size spacers standing in for the + * (non-uniform) gaps between them. BTN_ROW is an HBOX with Cancel and + * Apply side by side, gapped and top-aligned (Apply is taller than + * Cancel, both start flush with the row's top edge). */ +enum +{ + ST_ROOT, + + ST_ROW1, + ST_LABL1, + ST_SLDR1, + ST_VAL1, + + ST_ROW2, + ST_LABL2, + ST_SLDR2, + ST_VAL2, + + ST_ROW3, + ST_LABL3, + ST_SLDR3, + ST_VAL3, + + ST_ROW4, + ST_LABL4, + ST_SLDR4, + ST_VAL4, + + ST_BTNS, + ST_DFLT, + ST_CNCL, + ST_APLY, + + SIZE_STACK__LIMIT +}; + +/* Leaf main-axis sizes, named so saturn_conf_dialogue_create's hand-computed + * minimum window size can share them with the table below instead of + * repeating the numbers as bare literals. */ +#define ST_LABEL_W (5*6) /* enough for "Iters" */ +#define ST_LABEL2_W (4*6) /* enough for "9999" */ +#define ST_SLIDER_MIN_W (64) +#define ST_ACTION_WIDTH(W) ((W)*6+2*4) +#define ST_DEFAULT_WIDTH(W) ((W)*6+2*6) +#define ST_DEFAULT_W ST_ACTION_WIDTH(9) +#define ST_CANCEL_W ST_ACTION_WIDTH(9) +#define ST_APPLY_W ST_DEFAULT_WIDTH(9) + +static const stack_item_t g_saturn_conf_stack[SIZE_STACK__LIMIT] = +{ + [ST_ROOT] = STACK_VBOX_EX(-1, 0, wuss_STD_GAP, wuss_STD_INSET, wuss_STD_INSET, wuss_STD_INSET, wuss_STD_INSET), + + [ST_ROW1] = STACK_HBOX(ST_ROOT, wuss_STD_SLIDER_HEIGHT, wuss_STD_GAP, stack_ALIGN_START), + [ST_LABL1] = STACK_LEAF(ST_ROW1, ST_LABEL_W, 16, stack_ALIGN_CENTRE), + [ST_SLDR1] = STACK_LEAF_EX(ST_ROW1, 0, wuss_STD_SLIDER_HEIGHT, stack_ALIGN_CENTRE, 1, ST_SLIDER_MIN_W, 0), + [ST_VAL1] = STACK_LEAF(ST_ROW1, ST_LABEL2_W, 16, stack_ALIGN_CENTRE), + + [ST_ROW2] = STACK_HBOX(ST_ROOT, wuss_STD_SLIDER_HEIGHT, wuss_STD_GAP, stack_ALIGN_START), + [ST_LABL2] = STACK_LEAF(ST_ROW2, ST_LABEL_W, 16, stack_ALIGN_CENTRE), + [ST_SLDR2] = STACK_LEAF_EX(ST_ROW2, 0, wuss_STD_SLIDER_HEIGHT, stack_ALIGN_CENTRE, 1, ST_SLIDER_MIN_W, 0), + [ST_VAL2] = STACK_LEAF(ST_ROW2, ST_LABEL2_W, 16, stack_ALIGN_CENTRE), + + [ST_ROW3] = STACK_HBOX(ST_ROOT, wuss_STD_SLIDER_HEIGHT, wuss_STD_GAP, stack_ALIGN_START), + [ST_LABL3] = STACK_LEAF(ST_ROW3, ST_LABEL_W, 16, stack_ALIGN_CENTRE), + [ST_SLDR3] = STACK_LEAF_EX(ST_ROW3, 0, wuss_STD_SLIDER_HEIGHT, stack_ALIGN_CENTRE, 1, ST_SLIDER_MIN_W, 0), + [ST_VAL3] = STACK_LEAF(ST_ROW3, ST_LABEL2_W, 16, stack_ALIGN_CENTRE), + + [ST_ROW4] = STACK_HBOX(ST_ROOT, wuss_STD_SLIDER_HEIGHT, wuss_STD_GAP, stack_ALIGN_START), + [ST_LABL4] = STACK_LEAF(ST_ROW4, ST_LABEL_W, 16, stack_ALIGN_CENTRE), + [ST_SLDR4] = STACK_LEAF_EX(ST_ROW4, 0, wuss_STD_SLIDER_HEIGHT, stack_ALIGN_CENTRE, 1, ST_SLIDER_MIN_W, 0), + [ST_VAL4] = STACK_LEAF(ST_ROW4, ST_LABEL2_W, 16, stack_ALIGN_CENTRE), + + [ST_BTNS] = STACK_HBOX(ST_ROOT, wuss_STD_PRIMARY_BUTTON_HEIGHT, wuss_STD_GAP, stack_ALIGN_END), + [ST_DFLT] = STACK_LEAF(ST_BTNS, ST_DEFAULT_W, wuss_STD_SECONDARY_BUTTON_HEIGHT, stack_ALIGN_CENTRE), + [ST_CNCL] = STACK_LEAF(ST_BTNS, ST_CANCEL_W, wuss_STD_SECONDARY_BUTTON_HEIGHT, stack_ALIGN_CENTRE), + [ST_APLY] = STACK_LEAF(ST_BTNS, ST_APPLY_W, wuss_STD_PRIMARY_BUTTON_HEIGHT, stack_ALIGN_CENTRE), +}; + +/* Build the size dialogue once: a label, a slider snapped to + * SATURN_SIZE_STEP, a label echoing the slider's current value, and + * Cancel/Apply buttons, positioned by stack_solve. Created hidden -- wuss + * shows and hides it itself, as a menu leaf's borrowed window (see + * g_saturn_menu_items[SATURN_MENU_SIZE] and wuss_menu_item_t::window), so + * it must outlive the open menu chain and is never closed here, only + * hidden. */ +static result_t saturn_conf_dialogue_create(saturn_task_t *task) +{ + static const int label_box[SATURN_SIZEDLG_NROWS] = { ST_LABL1, ST_LABL2, ST_LABL3, ST_LABL4 }; + static const int slider_box[SATURN_SIZEDLG_NROWS] = { ST_SLDR1, ST_SLDR2, ST_SLDR3, ST_SLDR4 }; + static const int value_box[SATURN_SIZEDLG_NROWS] = { ST_VAL1, ST_VAL2, ST_VAL3, ST_VAL4 }; + static const int label_icon[SATURN_SIZEDLG_NROWS] = { SATURN_SIZE_ICON_LABEL1, SATURN_SIZE_ICON_LABEL2, SATURN_SIZE_ICON_LABEL3, SATURN_SIZE_ICON_LABEL4 }; + static const int slider_icon[SATURN_SIZEDLG_NROWS] = { SATURN_SIZE_ICON_SLIDER1, SATURN_SIZE_ICON_SLIDER2, SATURN_SIZE_ICON_SLIDER3, SATURN_SIZE_ICON_SLIDER4 }; + static const int value_icon[SATURN_SIZEDLG_NROWS] = { SATURN_SIZE_ICON_VALUE1, SATURN_SIZE_ICON_VALUE2, SATURN_SIZE_ICON_VALUE3, SATURN_SIZE_ICON_VALUE4 }; + + result_t rc; + wuss_icon_spec_t specs[SATURN_SIZE_NICONS]; + wuss_icon_t *made[SATURN_SIZE_NICONS]; + box_t boxes[SIZE_STACK__LIMIT]; + box_t root; + int value, row; + size2d_t sz; + + /* smallest window the layout can be solved into without any flex item + * (the sliders) growing past its minimum */ + rc = stack_smallest(g_saturn_conf_stack, NELEMS(g_saturn_conf_stack), &sz); + if (rc != result_OK) + return rc; + + rc = wuss_dialogue_create(&task->conf.dialogue, task->delegate, sz, + "Configuration", saturn_conf_fillout, task); + if (rc != result_OK) + return rc; + + root = (box_t) BOX_POS_SIZE(0, 0, sz.w, sz.h); + rc = stack_solve(g_saturn_conf_stack, NELEMS(g_saturn_conf_stack), &root, boxes); + if (rc != result_OK) + goto exit; + + for (row = 0; row < SATURN_SIZEDLG_NROWS; row++) + { + const saturn_sizedlg_rowdesc_t *desc = &g_saturn_sizedlg_rows[row]; + + value = *saturn_sizedlg_field(task, row); + + wuss_icon_spec_label(&specs[label_icon[row]], boxes[label_box[row]], + desc->label, wuss_COLOUR_BLACK, 1); + wuss_icon_spec_slider_row(&specs[slider_icon[row]], &specs[value_icon[row]], + boxes[slider_box[row]], boxes[value_box[row]], + wuss_COLOUR_BLACK, wuss_SLIDER_HORIZONTAL, + desc->min, desc->max, value, NULL, desc->step); + } + + wuss_icon_spec_action(&specs[SATURN_SIZE_ICON_DEFAULT], boxes[ST_DFLT], "Default", wuss_COLOUR_BLACK, wuss_COLOUR_WINDOW, 0); + wuss_icon_spec_action(&specs[SATURN_SIZE_ICON_CANCEL], boxes[ST_CNCL], "Cancel", wuss_COLOUR_BLACK, wuss_COLOUR_WINDOW, 0); + wuss_icon_spec_action(&specs[SATURN_SIZE_ICON_APPLY], boxes[ST_APLY], "Apply", wuss_COLOUR_BLACK, wuss_COLOUR_WINDOW, 1); + + rc = wuss_icon_create_array(wuss_dialogue_window(task->conf.dialogue), + specs, SATURN_SIZE_NICONS, made); + if (rc != result_OK) + goto exit; + + for (row = 0; row < SATURN_SIZEDLG_NROWS; row++) + wuss_slider_row_bind(&task->conf.rows[row], made[slider_icon[row]], + made[value_icon[row]], NULL, + g_saturn_sizedlg_rows[row].min, + g_saturn_sizedlg_rows[row].max, + g_saturn_sizedlg_rows[row].step); + task->conf.deflt = made[SATURN_SIZE_ICON_DEFAULT]; + task->conf.cancel = made[SATURN_SIZE_ICON_CANCEL]; + task->conf.apply = made[SATURN_SIZE_ICON_APPLY]; + + { + wuss_dialogue_action_t actions[3]; + + actions[0].icon = task->conf.deflt; + actions[0].fn = saturn_conf_default_action; + actions[1].icon = task->conf.cancel; + actions[1].fn = saturn_conf_cancel; + actions[2].icon = task->conf.apply; + actions[2].fn = saturn_conf_apply_action; + + rc = wuss_dialogue_set_actions(task->conf.dialogue, actions, + NELEMS(actions)); + if (rc != result_OK) + goto exit; + } + + return result_OK; + + +exit: + wuss_dialogue_destroy(task->conf.dialogue); /* not yet a menu leaf: safe to close */ + task->conf.dialogue = NULL; + return rc; +} + +/* Dialogue fillout callback: resync the slider and its echo label to + * task->config.size, in case Apply (or a config passed to saturn_create) + * changed it since the dialogue was last shown. Called by + * wuss_dialogue_handle_pre_show on every reveal, and directly by + * saturn_conf_cancel to reset the dialogue on an Adjust-Cancel click. */ +static result_t saturn_conf_fillout(void *opaque) +{ + result_t rc; + saturn_task_t *task; + int row, value; + + task = opaque; + + rc = result_OK; + for (row = 0; row < SATURN_SIZEDLG_NROWS; row++) + { + value = *saturn_sizedlg_field(task, row); + rc = wuss_slider_row_set(wuss_dialogue_window(task->conf.dialogue), + &task->conf.rows[row], value); + } + + return rc; +} + +/* Applies the slider's current value to task->window (resize + doc extent), + * shared by a Select and an Adjust click on Apply. */ +static result_t saturn_conf_apply(saturn_task_t *task) +{ + result_t rc; + int row, size_value; + + for (row = 0; row < SATURN_SIZEDLG_NROWS; row++) + *saturn_sizedlg_field(task, row) = + wuss_icon_get_value(task->conf.rows[row].slider); + + size_value = task->config.size; + rc = wuss_window_resize(task->window, SIZE2D(size_value, size_value)); + if (rc != result_OK) + return rc; + return wuss_window_set_doc(task->window, SIZE2D(size_value, size_value)); +} + +/* Dialogue action callback for Cancel, split by button per the RISC OS + * "Adjust doesn't dismiss" convention: Select dismisses the menu chain; + * Adjust resets the dialogue to task->config.size instead. Dismissing goes + * through wuss_menu_close rather than touching the (borrowed, reused) window + * directly -- that is what hides it, same as a click outside the chain + * would. */ +static result_t saturn_conf_cancel(void *opaque, wuss_button_t button) +{ + saturn_task_t *task; + + task = opaque; + + if (button & wuss_BUTTON_SELECT) + { + wuss_menu_close(task->menu_handle); + task->menu_handle = NULL; + return result_OK; + } + if (button & wuss_BUTTON_ADJUST) + return saturn_conf_fillout(task); + return result_OK; +} + +/* Dialogue action callback for Apply: Select applies and dismisses; Adjust + * applies but leaves the dialogue open. */ +static result_t saturn_conf_apply_action(void *opaque, wuss_button_t button) +{ + saturn_task_t *task; + + task = opaque; + + if (button & wuss_BUTTON_SELECT) + { + wuss_menu_close(task->menu_handle); + task->menu_handle = NULL; + return saturn_conf_apply(task); + } + if (button & wuss_BUTTON_ADJUST) + return saturn_conf_apply(task); + return result_OK; +} + +/* Dialogue action callback for Default: resets task->config to + * SATURN_CONFIG_DEFAULT, refills the sliders/echo labels to match, and + * applies it (window resize + doc extent) -- Select then dismisses the + * menu chain same as Apply's Select; Adjust leaves the dialogue open, + * showing the reset values. */ +static result_t saturn_conf_default_action(void *opaque, + wuss_button_t button) +{ + static const saturn_config_t default_config = SATURN_CONFIG_DEFAULT; + + result_t rc; + saturn_task_t *task; + + task = opaque; + + task->config = default_config; + rc = saturn_conf_fillout(task); + if (rc != result_OK) + return rc; + + if (button & wuss_BUTTON_SELECT) + { + wuss_menu_close(task->menu_handle); + task->menu_handle = NULL; + return saturn_conf_apply(task); + } + if (button & wuss_BUTTON_ADJUST) + return saturn_conf_apply(task); + return result_OK; +} + +/* wuss_EVENT_ICON on the size dialogue: slider drag updates the echo label + * (snapped to SATURN_SIZE_STEP) live on DOWN/MOVE, handled here directly; + * a Cancel/Apply click (UP only -- a press that drags off the button before + * release is not taken as a click) is dispatched through the dialogue's + * action table. */ +static result_t saturn_conf_dialogue_icon(saturn_task_t *task, + const wuss_event_t *event) +{ + result_t rc; + int row; + + for (row = 0; row < SATURN_SIZEDLG_NROWS; row++) + if (wuss_slider_row_event(wuss_dialogue_window(task->conf.dialogue), + &task->conf.rows[row], event, NULL)) + return result_OK; + + if (wuss_dialogue_handle_icon(task->conf.dialogue, event, &rc)) + return rc; + + return result_OK; +} + +/* Applies event to *colour if it's a pick from colourmenu, returns whether + * it was. Shared by saturn_menu_select's fg/bg attempts below. */ +static int saturn_menu_select_apply(const wuss_colourmenu_t *colourmenu, + colour_t *colour, + const colour_t *palette, + int npalette, + const wuss_event_t *event) +{ + wuss_colour_t picked; + int mine; + + picked = wuss_colourmenu_selected(colourmenu, event, &mine); + if (!mine) + return 0; + + if (picked < npalette) + *colour = palette[picked]; + return 1; +} + +/* A pick from either colour submenu, resolved against whichever colourmenu + * it came from. "Configuration" is a wuss_menu_item_t::window leaf, not a leaf pick, + * so it never reaches here -- see saturn_conf_dialogue_icon and + * saturn_sizedlg_pre_show. */ +static result_t saturn_menu_select(saturn_task_t *task, + const wuss_event_t *event) +{ + const colour_t *palette; + int npalette; + + palette = wuss_get_palette(task->wuss, &npalette); + + if (saturn_menu_select_apply(task->fg_colourmenu, &task->fg, palette, + npalette, event) || + saturn_menu_select_apply(task->bg_colourmenu, &task->bg, palette, + npalette, event)) + wuss_window_invalidate_visible(task->window); + + return result_OK; +} + result_t saturn_handle(wuss_window_t *window, const wuss_event_t *event, void *task_data) @@ -230,11 +793,27 @@ result_t saturn_handle(wuss_window_t *window, return saturn_mouse(task, event->data.mouse.action, event->data.mouse.button, window); - case wuss_EVENT_IDLE: - return saturn_idle(task); + case wuss_EVENT_ICON: + if (window == wuss_dialogue_window(task->conf.dialogue)) + return saturn_conf_dialogue_icon(task, event); + return result_OK; + + case wuss_EVENT_PRE_SHOW: + if (window == wuss_dialogue_window(task->conf.dialogue)) + return wuss_dialogue_handle_pre_show(task->conf.dialogue); + if (window == wuss_proginfo_window(task->proginfo)) + return wuss_proginfo_handle_pre_show(task->proginfo); + return result_OK; + + case wuss_EVENT_MENU_SELECT: + return saturn_menu_select(task, event); + + case wuss_EVENT_MENU_CLOSED: + task->menu_handle = NULL; /* wuss closed the chain under us */ + return result_OK; case wuss_EVENT_QUIT: - free(task); /* task_data was calloc'd per instance by the spawner */ + saturn_destroy(task); return result_OK; default: diff --git a/libraries/wuss/test/tasks/saturn.h b/libraries/wuss/test/tasks/saturn.h index d9a6630..000234b 100644 --- a/libraries/wuss/test/tasks/saturn.h +++ b/libraries/wuss/test/tasks/saturn.h @@ -5,7 +5,16 @@ #ifdef WUSS_APP +/* ponytail: no #ifdef WUSS_COMPONENTS guard -- it is PUBLIC on DPTLib and ON + * by default, so the wuss app always has the colourmenu component. */ #include "framebuf/colour.h" +#include "wuss/component/colourmenu.h" +#include "wuss/component/dialogue.h" +#include "wuss/component/proginfo.h" +#include "wuss/icon-spec.h" +#include "wuss/icon.h" +#include "wuss/menu.h" +#include "wuss/task.h" #include "wuss/window.h" /* window task: recreates the ringed planet from the loading screen of @@ -15,19 +24,74 @@ * planet body. Select re-seeds the RNG for a fresh sketch; the idle * handler re-seeds every null event so it churns. The plot is * deterministic in the seed. */ +/* iteration counts for the three rejection-sampling loops, plus the window's + * size, in document pixels (the window is always square); saturn_create + * fills in SATURN_CONFIG_DEFAULT values when the caller passes NULL */ +typedef struct saturn_config +{ + int stars_iters; /* loop 1: the ring */ + int ring_iters; /* loop 2: ring shadow band */ + int body_iters; /* loop 3: planet body */ + int size; /* window size (both axes) */ +} +saturn_config_t; + +#define SATURN_CONFIG_DEFAULT { 477, 1280, 1280, 256 } + +/* the "Size..." dialogue's slider: [min,max] and the step it snaps to */ +#define SATURN_SIZE_MIN 128 +#define SATURN_SIZE_MAX 512 +#define SATURN_SIZE_STEP 64 + +/* the size dialogue's slider rows, in creation/layout order */ +enum +{ + SATURN_SIZEDLG_ROW_SIZE = 0, + SATURN_SIZEDLG_ROW_STARS, + SATURN_SIZEDLG_ROW_RING, + SATURN_SIZEDLG_ROW_BODY, + + SATURN_SIZEDLG_NROWS +}; + +/* the size dialogue: built once, hidden; a menu leaf (see + * g_saturn_menu_items[SATURN_MENU_SIZE].window) */ +typedef struct saturn_conf +{ + wuss_dialogue_t *dialogue; + wuss_slider_row_t rows[SATURN_SIZEDLG_NROWS]; /* size, stars, ring, body */ + wuss_icon_t *deflt; + wuss_icon_t *cancel; + wuss_icon_t *apply; +} +saturn_conf_t; + typedef struct saturn_task { - wuss_window_t *window; - colour_t bg, fg; - unsigned long seed; /* RNG state; a Select click bumps it */ + wuss_t *wuss; /* for wuss_get_pointer/wuss_get_palette */ + wuss_window_t *window; + wuss_task_t *delegate; /* the task that owns the menu */ + colour_t bg, fg; + unsigned long seed; /* RNG state; a Select click bumps it */ + saturn_config_t config; + wuss_colourmenu_t *fg_colourmenu, *bg_colourmenu; + wuss_menu_handle_t menu_handle; /* live only between open and a SELECT pick */ + saturn_conf_t conf; + wuss_proginfo_t *proginfo; } 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); +/* create the planet window against the given wuss instance, always starting + * at SATURN_CONFIG_DEFAULT; the task block is allocated here, owned by the + * window and freed when it closes. + * if out is non-NULL, the task block is also returned through it */ +result_t saturn_create(wuss_t *wuss, saturn_task_t **out); + +/* free a task block allocated by saturn_create; normally called by the + * window's wuss_EVENT_QUIT handler, not by callers directly */ +void saturn_destroy(saturn_task_t *task); #endif /* WUSS_APP */ diff --git a/libraries/wuss/test/tasks/sofa.c b/libraries/wuss/test/tasks/sofa.c index fce7065..6e1dc62 100644 --- a/libraries/wuss/test/tasks/sofa.c +++ b/libraries/wuss/test/tasks/sofa.c @@ -354,12 +354,17 @@ static void draw_vertex_dots(screen_t *scr, colour); } -result_t sofa_create(wuss_t*wuss, sofa_task_t*task) +result_t sofa_create(wuss_t *wuss, sofa_task_t **out) { result_t rc; + sofa_task_t *task; wuss_task_t *delegate; wuss_task_desc_t delegate_desc; + task = calloc(1, sizeof(*task)); + if (task == NULL) + return result_OOM; + task->bg = colour_rgb(0x7E, 0x25, 0x53); task->line = colour_rgb(0xFF, 0xA3, 0x00); task->dot = colour_rgb(0xFF, 0xFF, 0xFF); @@ -385,14 +390,25 @@ result_t sofa_create(wuss_t*wuss, sofa_task_t*task) SIZE2D(180, 160), "Sofa", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(180, 160), SIZE2D(0, 0), &task->window); if (rc != result_OK) + { wuss_task_destroy(delegate); /* unregister; its QUIT frees the task block */ + return rc; + } + + if (out) + *out = task; - return rc; + return result_OK; +} + +void sofa_destroy(sofa_task_t *task) +{ + free(task); } static result_t sofa_redraw(const wuss_event_t *event, void *task_data) @@ -487,7 +503,7 @@ static result_t sofa_redraw(const wuss_event_t *event, void *task_data) { const wireframe_t *wf; fix8_point_t screen[NELEMS(dodeca_vertices)]; /* largest solid */ - int i; + int i; wf = &polyhedra[sc->shape - sofa_SHAPE_TETRAHEDRON]; @@ -597,7 +613,7 @@ result_t sofa_handle(wuss_window_t *window, return sofa_idle(task_data); case wuss_EVENT_QUIT: - free(sc); /* task_data was calloc'd per instance by the spawner */ + sofa_destroy(sc); return result_OK; default: diff --git a/libraries/wuss/test/tasks/sofa.h b/libraries/wuss/test/tasks/sofa.h index 732d5dc..d34c46a 100644 --- a/libraries/wuss/test/tasks/sofa.h +++ b/libraries/wuss/test/tasks/sofa.h @@ -42,8 +42,13 @@ sofa_task_t; wuss_window_fn_t sofa_handle; -/* create the sofa window against the given wuss instance */ -result_t sofa_create(wuss_t*wuss, sofa_task_t*task); +/* create the sofa window against the given wuss instance; if out is + * non-NULL, the task block is also returned through it */ +result_t sofa_create(wuss_t *wuss, sofa_task_t **out); + +/* free a task block allocated by sofa_create; normally called by the + * window's wuss_EVENT_QUIT handler, not by callers directly */ +void sofa_destroy(sofa_task_t *task); #endif /* WUSS_APP */ diff --git a/libraries/wuss/test/tasks/swatches.c b/libraries/wuss/test/tasks/swatches.c index deb23d9..7cba50b 100644 --- a/libraries/wuss/test/tasks/swatches.c +++ b/libraries/wuss/test/tasks/swatches.c @@ -59,7 +59,7 @@ static result_t swatches_redraw(swatches_task_t *task, pat * SWATCHES_CELL, SWATCHES_CELL, SWATCHES_CELL); spec.fg = (wuss_colour_t) col; - spec.pattern = (screen_pattern_t) pat; + spec.u.pattern.tile = (screen_pattern_t) pat; rc = wuss_icon_plot(task->window, &spec, bounds, scroll); if (rc != result_OK) @@ -70,12 +70,17 @@ static result_t swatches_redraw(swatches_task_t *task, return result_OK; } -result_t swatches_create(wuss_t *wuss, swatches_task_t *task) +result_t swatches_create(wuss_t *wuss, swatches_task_t **out) { result_t rc; + swatches_task_t *task; wuss_task_t *delegate; wuss_task_desc_t delegate_desc; + task = calloc(1, sizeof(*task)); + if (task == NULL) + return result_OOM; + task->wuss = wuss; task->window = NULL; task->task = NULL; @@ -103,7 +108,7 @@ result_t swatches_create(wuss_t *wuss, swatches_task_t *task) rc = wuss_window_create_placed(delegate, SIZE2D(SWATCHES_DOC_W, 140), "Swatches", - wuss_WINDOW_NO_RESIZE_BLIT, /* grid spans the whole window; a resize redraws all of it */ + wuss_WINDOW_DEFAULT | wuss_WINDOW_NO_RESIZE_BLIT, /* grid spans the whole window; a resize redraws all of it */ wuss_BACKDROP_COLOUR(wuss_COLOUR_WINDOW), SIZE2D(SWATCHES_DOC_W, SWATCHES_DOC_H), SIZE2D(0, 0), @@ -119,9 +124,18 @@ result_t swatches_create(wuss_t *wuss, swatches_task_t *task) * wuss_EVENT_QUIT frees task_data */ wuss_task_set_autoclose(delegate, 1); + if (out) + *out = task; + return result_OK; } +void swatches_destroy(swatches_task_t *task) +{ + wuss_colourmenu_destroy(task->colourmenu); + free(task); +} + /* A SELECT click on a cell makes that cell -- its fill pattern, its palette * colour as the ink, the current paper -- the desktop backdrop. A MENU click * pops the colour menu; its pick becomes the paper. The grid has no retained @@ -199,8 +213,7 @@ result_t swatches_handle(wuss_window_t *window, return swatches_menu_select(task, event); case wuss_EVENT_QUIT: - wuss_colourmenu_destroy(task->colourmenu); - free(task_data); /* calloc'd per instance by the spawner */ + swatches_destroy(task); return result_OK; default: diff --git a/libraries/wuss/test/tasks/swatches.h b/libraries/wuss/test/tasks/swatches.h index 71a6877..4866125 100644 --- a/libraries/wuss/test/tasks/swatches.h +++ b/libraries/wuss/test/tasks/swatches.h @@ -30,8 +30,13 @@ swatches_task_t; wuss_window_fn_t swatches_handle; -/* create the swatches window against the given wuss instance */ -result_t swatches_create(wuss_t *wuss, swatches_task_t *task); +/* create the swatches window against the given wuss instance; if out is + * non-NULL, the task block is also returned through it */ +result_t swatches_create(wuss_t *wuss, swatches_task_t **out); + +/* free a task block allocated by swatches_create; normally called by the + * window's wuss_EVENT_QUIT handler, not by callers directly */ +void swatches_destroy(swatches_task_t *task); #endif /* WUSS_APP */ diff --git a/libraries/wuss/test/tasks/text.c b/libraries/wuss/test/tasks/text.c index f2c70d9..14391b8 100644 --- a/libraries/wuss/test/tasks/text.c +++ b/libraries/wuss/test/tasks/text.c @@ -38,30 +38,24 @@ text_sample_t; static const text_sample_t text_samples[] = { + { "Lorem Ipsum", + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed eros lacus, imperdiet eget finibus ac, tempus vel risus. Donec scelerisque, elit quis pretium imperdiet, orci magna varius tellus, sit amet sodales ante orci nec nibh. Pellentesque placerat eu diam vitae pharetra. Nunc aliquet ante mi, vulputate commodo dui placerat eu. Morbi velit ex, scelerisque vel mi elementum, tristique viverra enim. Integer a interdum eros, id fringilla nunc. Nunc non felis nisi. Aliquam nec ullamcorper tellus. Maecenas sed aliquam diam. Duis pretium aliquet metus. Suspendisse rhoncus turpis vel dui euismod fringilla. Vivamus efficitur leo vel metus condimentum, tempor bibendum augue vehicula. Nunc eleifend sagittis tortor eget pretium. Fusce interdum tortor eget sapien blandit consectetur. Aliquam vestibulum euismod eros a luctus. Etiam nec nisl et diam imperdiet lobortis. Sed in eros sed tellus commodo bibendum. Sed ipsum velit, sodales a pulvinar non, pharetra ut est. Curabitur eu odio id magna posuere eleifend non id erat. Pellentesque commodo blandit mauris, ac consequat nisi dapibus eget. Mauris sollicitudin molestie urna, sit amet ornare turpis tincidunt bibendum. Vivamus interdum bibendum luctus. Suspendisse in arcu velit. Aenean eget bibendum dolor. Quisque tristique porta purus ornare tincidunt. Etiam hendrerit nunc tellus, et tempus ligula laoreet eu. Quisque pellentesque malesuada tempor. Mauris eu lectus ut neque fringilla hendrerit. Sed scelerisque laoreet felis a eleifend. Fusce sit amet mauris tellus. Sed orci ipsum, consectetur vitae blandit ut, egestas vehicula dolor. Integer ullamcorper, metus a vulputate mattis, elit orci accumsan ligula, eget elementum nibh tortor sit amet mauris. In aliquet nibh at scelerisque suscipit. Sed elit purus, sagittis eu accumsan ultricies, lobortis a odio. Duis libero sem, tempus in fringilla nec, bibendum eu metus. Nulla eget justo metus. In luctus ante massa, pellentesque commodo lorem pretium sed. Cras ultricies est lacus, ut dictum lorem gravida sed. Ut augue mauris, dignissim a pulvinar eget, pharetra ac turpis. Sed ultricies nulla mauris, id dictum ex scelerisque sit amet. Morbi et placerat enim. Phasellus arcu nisl, tempor vitae lacinia et, finibus quis justo. Fusce ipsum mi, porttitor nec faucibus at, fermentum ut massa. Cras faucibus molestie mauris. Sed et metus eget lectus luctus cursus a sit amet eros. Sed enim ligula, gravida eget magna eu, suscipit fermentum lorem. Maecenas vestibulum mollis lacus nec accumsan. Nullam molestie justo eu turpis facilisis tempus quis quis velit. Phasellus gravida mollis condimentum. Nam fringilla mollis dolor, quis posuere quam iaculis ac. Suspendisse ac maximus mi. Pellentesque aliquam ante ante, sed facilisis sapien dictum ac. Nullam pulvinar ante vitae dictum rhoncus. Praesent in pretium justo. Quisque pellentesque at sapien at pulvinar. Aenean a lorem at sapien molestie ullamcorper in dignissim ante. Nunc sagittis mi at dolor accumsan laoreet. Ut id congue elit, ut semper metus. Praesent tellus orci, feugiat suscipit diam sit amet, malesuada efficitur metus. Phasellus condimentum justo ipsum, et lobortis mi ultrices a. Interdum et malesuada fames ac ante ipsum primis in faucibus. Nunc vestibulum volutpat laoreet. Vestibulum convallis lectus at accumsan imperdiet. Aliquam suscipit, justo condimentum sodales iaculis, tellus enim fermentum lacus, quis volutpat sapien tortor quis enim. Vestibulum vehicula turpis eu lorem gravida, nec volutpat massa dapibus. Mauris egestas accumsan mattis. Nullam ex risus, imperdiet ut vestibulum a, malesuada at odio. Praesent congue, nulla a eleifend dignissim, ligula arcu tincidunt tortor, vel vestibulum quam ipsum vitae metus. Nullam lacinia interdum enim id bibendum. Praesent quis elit id turpis cursus auctor. Etiam turpis massa, finibus sed odio quis, dapibus tristique magna. Morbi quis commodo tortor. Pellentesque hendrerit non libero non pretium. Aenean nunc est, aliquet eget tincidunt id, auctor ultrices orci velit." }, { "Quick Brown Fox", "The quick brown fox jumps over the lazy dog." }, { "Pangram (Cwm Fjord)", "Cwm fjord bank glyphs vext quiz." }, { "Pangram (Waltz)", - "Waltz, bad nymph, for quick jigs vex." }, - { "Lorem Ipsum", - "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod " - "tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim " - "veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea " - "commodo consequat. Duis aute irure dolor in reprehenderit in voluptate " - "velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint " - "occaecat cupidatat non proident, sunt in culpa qui officia deserunt " - "mollit anim id est laborum." } + "Waltz, bad nymph, for quick jigs vex." } }; -#define TEXT_DEFAULT_SAMPLE (NELEMS(text_samples) - 1) /* "Lorem Ipsum" */ +#define TEXT_DEFAULT_SAMPLE (0) /* "Lorem Ipsum" */ static const wuss_menu_item_t text_sample_items[] = { + { "Lorem Ipsum", wuss_MENU_ITEM_NONE, NULL }, { "Quick Brown Fox", wuss_MENU_ITEM_NONE, NULL }, { "Pangram (Cwm Fjord)", wuss_MENU_ITEM_NONE, NULL }, - { "Pangram (Waltz)", wuss_MENU_ITEM_NONE, NULL }, - { "Lorem Ipsum", wuss_MENU_ITEM_NONE, NULL } + { "Pangram (Waltz)", wuss_MENU_ITEM_NONE, NULL } }; static const wuss_menu_t text_sample_menu = @@ -69,6 +63,41 @@ static const wuss_menu_t text_sample_menu = "Sample", text_sample_items, NELEMS(text_sample_items) }; +/* one entry per row of the "Spacing" submenu */ +typedef struct text_spacing_preset +{ + const char *name; + int letter_spacing; + int word_spacing; +} +text_spacing_preset_t; + +static const text_spacing_preset_t text_spacing_presets[] = +{ + { "Normal", 0, 0 }, + { "Letter", 1, 0 }, + { "Word", 0, 1 }, + { "Letter+Word", 1, 1 } +}; + +#define TEXT_DEFAULT_SPACING 0 /* "Normal" */ + +static wuss_menu_item_t text_spacing_items[] = +{ + { "Normal", wuss_MENU_ITEM_TICKED, NULL }, + { "Letter", wuss_MENU_ITEM_NONE, NULL }, + { "Word", wuss_MENU_ITEM_NONE, NULL }, + { "Letter+Word", wuss_MENU_ITEM_NONE, NULL } +}; + +static wuss_menu_t text_spacing_menu = +{ + "Spacing", text_spacing_items, NELEMS(text_spacing_items) +}; + +/* index into task->top_items[] of the tickable "No Background" leaf */ +#define TEXT_MENU_NO_BACKGROUND 5 + /* ----------------------------------------------------------------------- */ /* load fonts[idx] if not already in hand; returns it or NULL on failure. @@ -105,7 +134,7 @@ static result_t text_set_font(text_task_t *task, int idx, const char *name) if (idx < 0 || idx >= task->nfonts || idx == task->current) return result_OK; - font = text_load_font(task, task->resources, idx, name); + font = text_load_font(task, wuss_get_resources(task->wuss), idx, name); if (font == NULL) return result_OK; /* leave the current font in place */ @@ -129,42 +158,99 @@ static result_t text_set_sample(text_task_t *task, int idx) return result_OK; } +/* toggle whether the paragraph is drawn with its picked background colour or + * none, so glyphs blend straight onto whatever is already behind the + * window's content */ +static result_t text_toggle_bg(text_task_t *task) +{ + wuss_menu_item_t *item; + + task->bg_transparent = !task->bg_transparent; + + item = &task->top_items[TEXT_MENU_NO_BACKGROUND]; + if (task->bg_transparent) + item->flags |= wuss_MENU_ITEM_TICKED; + else + item->flags &= ~(wuss_menu_item_flags_t) wuss_MENU_ITEM_TICKED; + + wuss_window_invalidate_visible(task->window); + return result_OK; +} + +/* switch the paragraph's foreground to system palette index "picked" */ +static result_t text_set_fg(text_task_t *task, wuss_colour_t picked) +{ + task->fg_index = picked; + wuss_window_invalidate_visible(task->window); + return result_OK; +} + +/* switch the paragraph's background to system palette index "picked" */ +static result_t text_set_bg(text_task_t *task, wuss_colour_t picked) +{ + task->bg_index = picked; + wuss_window_invalidate_visible(task->window); + return result_OK; +} + +/* switch letter/word spacing to text_spacing_presets[idx] */ +static result_t text_set_spacing(text_task_t *task, int idx) +{ + if (idx < 0 || idx >= NELEMS(text_spacing_presets) || idx == task->spacing_idx) + return result_OK; + + task->spacing_idx = idx; + task->spacing.letter_spacing = text_spacing_presets[idx].letter_spacing; + task->spacing.word_spacing = text_spacing_presets[idx].word_spacing; + + wuss_menu_tick_exclusive(&text_spacing_menu, idx); + wuss_menu_tick_exclusive_live(task->menu_handle, &text_spacing_menu, idx); + wuss_window_invalidate_visible(task->window); + return result_OK; +} + static result_t text_open_menu(text_task_t *task) { return wuss_menu_open(task->delegate, &task->top_menu, - wuss_get_pointer(task->wuss), NULL); + wuss_get_pointer(task->wuss), &task->menu_handle); } /* ----------------------------------------------------------------------- */ -result_t text_create(wuss_t *wuss, - const char *resources, - text_task_t *task) +result_t text_create(wuss_t *wuss, text_task_t **out) { result_t rc; + text_task_t *task; wuss_task_t *delegate; wuss_task_desc_t delegate_desc; + const char *resources; const char *bmfonts_dir; const wuss_menu_t *menu; size2d_t sz; + task = calloc(1, sizeof(*task)); + if (task == NULL) + return result_OOM; + task->wuss = wuss; task->font = wuss_get_font(wuss); task->current = -1; /* the wuss system font is none of the picker's */ task->sample = TEXT_DEFAULT_SAMPLE; task->text = text_samples[TEXT_DEFAULT_SAMPLE].text; - task->bg = colour_rgb(0xFF, 0xFF, 0xFF); - task->fg = colour_rgb(0x00, 0x00, 0x00); + task->spacing_idx = TEXT_DEFAULT_SPACING; + task->spacing.letter_spacing = text_spacing_presets[TEXT_DEFAULT_SPACING].letter_spacing; + task->spacing.word_spacing = text_spacing_presets[TEXT_DEFAULT_SPACING].word_spacing; + task->fg_index = 0; /* wuss__default_palette: 0 is black */ + task->bg_index = 7; /* wuss__default_palette: 7 is white */ task->frame_count = 0; task->resizing = true; - strncpy(task->resources, resources, sizeof(task->resources) - 1); - task->resources[sizeof(task->resources) - 1] = '\0'; + resources = wuss_get_resources(wuss); /* the picker: every ".png" font under resources/bmfonts, sorted, less any * SYSTEM-class font (e.g. the one wuss draws menu ticks/arrows from) */ - bmfonts_dir = path_join_filename(task->resources, 2, "resources", "bmfonts"); + bmfonts_dir = path_join_filename(resources, 2, "resources", "bmfonts"); rc = wuss_fontmenu_create(&task->fontmenu, bmfonts_dir, "Font", wuss, NULL); if (rc != result_OK) { @@ -182,9 +268,29 @@ result_t text_create(wuss_t *wuss, return result_OOM; } + rc = wuss_colourmenu_create(&task->fgmenu, wuss, "Foreground"); + if (rc != result_OK) + { + wuss_fontmenu_destroy(task->fontmenu); + free(task->fonts); + free(task); + return rc; + } + + rc = wuss_colourmenu_create(&task->bgmenu, wuss, "Background"); + if (rc != result_OK) + { + wuss_colourmenu_destroy(task->fgmenu); + wuss_fontmenu_destroy(task->fontmenu); + free(task->fonts); + free(task); + return rc; + } + /* top-level menu: "Font" borrows the fontmenu's own live wuss_menu_t (so * ticks and wuss_fontmenu_selected keep working), "Sample" is the static - * text_sample_menu declared above */ + * text_sample_menu declared above, "Foreground"/"Background" each borrow + * their colourmenu's live wuss_menu_t the same way */ task->top_items[0].text = "Font"; task->top_items[0].flags = wuss_MENU_ITEM_NONE; task->top_items[0].submenu = menu; @@ -193,6 +299,22 @@ result_t text_create(wuss_t *wuss, task->top_items[1].flags = wuss_MENU_ITEM_NONE; task->top_items[1].submenu = &text_sample_menu; task->top_items[1].window = NULL; + task->top_items[2].text = "Spacing"; + task->top_items[2].flags = wuss_MENU_ITEM_NONE; + task->top_items[2].submenu = &text_spacing_menu; + task->top_items[2].window = NULL; + task->top_items[3].text = "Foreground"; + task->top_items[3].flags = wuss_MENU_ITEM_NONE; + task->top_items[3].submenu = wuss_colourmenu_menu(task->fgmenu); + task->top_items[3].window = NULL; + task->top_items[4].text = "Background"; + task->top_items[4].flags = wuss_MENU_ITEM_NONE; + task->top_items[4].submenu = wuss_colourmenu_menu(task->bgmenu); + task->top_items[4].window = NULL; + task->top_items[5].text = "No Background"; + task->top_items[5].flags = wuss_MENU_ITEM_NONE; + task->top_items[5].submenu = NULL; + task->top_items[5].window = NULL; task->top_menu.title = "Text"; task->top_menu.items = task->top_items; @@ -212,22 +334,42 @@ result_t text_create(wuss_t *wuss, wuss_task_set_autoclose(delegate, 1); task->delegate = delegate; - sz = SIZE2D(220, 180); + sz = SIZE2D(220, 220); task->base_width = sz.w; task->base_height = sz.h; rc = wuss_window_create_placed(delegate, sz, "Sample Text", - wuss_WINDOW_NO_RESIZE_BLIT, /* paragraph reflows across the whole window, so a resize must redraw all of it, not just the newly (un)covered edge */ + wuss_WINDOW_DEFAULT | wuss_WINDOW_NO_RESIZE_BLIT, /* paragraph reflows across the whole window, so a resize must redraw all of it, not just the newly (un)covered edge */ wuss_BACKDROP_COLOUR(wuss_COLOUR_WINDOW), - sz, + SIZE2D(sz.w, 220 * 4), SIZE2D(0, 0), &task->window); if (rc != result_OK) + { wuss_task_destroy(delegate); /* unregister; its QUIT frees the task block */ + return rc; + } - return rc; + if (out) + *out = task; + + return result_OK; +} + +void text_destroy(text_task_t *task) +{ + int i; + + for (i = 0; i < task->nfonts; i++) + if (task->fonts[i] != NULL) + bmfont_destroy(task->fonts[i]); + free(task->fonts); + wuss_fontmenu_destroy(task->fontmenu); + wuss_colourmenu_destroy(task->fgmenu); + wuss_colourmenu_destroy(task->bgmenu); + free(task); } #define INSET 4 @@ -236,13 +378,15 @@ result_t text_create(wuss_t *wuss, static result_t text_redraw(const wuss_event_t *event, void *task_data) { - text_task_t *tcx; - screen_t *scr; - const box_t *bounds; - int sx, sy; - bmtext_line_t lines[MAX_LINES]; - int nlines; - point_t origin; + text_task_t *tcx; + screen_t *scr; + const box_t *bounds; + int sx, sy; + const colour_t *palette; + colour_t fg, bg; + bmtext_line_t lines[MAX_LINES]; + int nlines; + point_t origin; tcx = task_data; @@ -251,17 +395,23 @@ static result_t text_redraw(const wuss_event_t *event, void *task_data) sx = event->data.redraw.scroll.x; sy = event->data.redraw.scroll.y; + palette = wuss_get_palette(tcx->wuss, NULL); + fg = palette[tcx->fg_index]; + bg = tcx->bg_transparent ? colour_rgba(0, 0, 0, 0) : palette[tcx->bg_index]; + nlines = bmtext_layout(tcx->font, tcx->text, (int) strlen(tcx->text), (bounds->x1 - INSET) - (bounds->x0 + INSET), + &tcx->spacing, lines, MAX_LINES); origin.x = bounds->x0 - sx + INSET; origin.y = bounds->y0 - sy + INSET; - bmtext_draw(tcx->font, scr, lines, nlines, tcx->fg, tcx->bg, LEADING, origin); + bmtext_draw(tcx->font, scr, lines, nlines, fg, bg, LEADING, origin, + &tcx->spacing); return result_OK; } @@ -331,27 +481,31 @@ result_t text_handle(wuss_window_t *window, case wuss_EVENT_MENU_SELECT: { - const char *name; + const char *name; + wuss_colour_t picked; + int mine; name = wuss_fontmenu_selected(tcx->fontmenu, event); if (name != NULL) return text_set_font(tcx, event->data.menu_select.index, name); if (event->data.menu_select.menu == &text_sample_menu) return text_set_sample(tcx, event->data.menu_select.index); + if (event->data.menu_select.menu == &text_spacing_menu) + return text_set_spacing(tcx, event->data.menu_select.index); + picked = wuss_colourmenu_selected(tcx->fgmenu, event, &mine); + if (mine) + return text_set_fg(tcx, picked); + picked = wuss_colourmenu_selected(tcx->bgmenu, event, &mine); + if (mine) + return text_set_bg(tcx, picked); + if (event->data.menu_select.menu == &tcx->top_menu && + event->data.menu_select.index == TEXT_MENU_NO_BACKGROUND) + return text_toggle_bg(tcx); } return result_OK; case wuss_EVENT_QUIT: - { - int i; - - for (i = 0; i < tcx->nfonts; i++) - if (tcx->fonts[i] != NULL) - bmfont_destroy(tcx->fonts[i]); - free(tcx->fonts); - wuss_fontmenu_destroy(tcx->fontmenu); - free(tcx); /* task_data was calloc'd per instance by the spawner */ - } + text_destroy(tcx); return result_OK; case wuss_EVENT_IDLE: diff --git a/libraries/wuss/test/tasks/text.h b/libraries/wuss/test/tasks/text.h index 5c0da5c..dccec33 100644 --- a/libraries/wuss/test/tasks/text.h +++ b/libraries/wuss/test/tasks/text.h @@ -9,6 +9,7 @@ #include "framebuf/bmfont.h" #include "framebuf/colour.h" +#include "wuss/component/colourmenu.h" #include "wuss/component/fontmenu.h" #include "wuss/menu.h" #include "wuss/task.h" @@ -16,45 +17,75 @@ /* window B's task: flows a chosen sample string over its wuss-filled * background, one line per bmfont_draw call. A MENU click on the window opens - * a top-level menu with two submenus: "Font" -- a wuss_fontmenu over - * resources/bmfonts, swapping the paragraph font in place -- and "Sample", - * which swaps the shown string (a choice of pangrams and a lorem ipsum - * paragraph). */ + * a top-level menu with three submenus -- "Font" (a wuss_fontmenu over + * resources/bmfonts, swapping the paragraph font in place), "Sample" (swaps + * the shown string: a choice of pangrams and a lorem ipsum paragraph) and + * "Spacing" (swaps the letter/word spacing), "Foreground" and "Background" + * (each a wuss_colourmenu over the system palette) -- plus a tickable "No + * Background" leaf that unsets the paragraph's background colour so glyphs + * blend straight onto whatever is already behind the window's content. */ typedef struct text_task { - wuss_window_t *window; - wuss_task_t *delegate; /* the wuss task backing this window */ - wuss_t *wuss; /* for wuss_get_pointer when opening the menu */ - wuss_fontmenu_t *fontmenu; /* the font picker; owns the menu and names */ - bmfont_t *font; /* currently shown; fonts[current] or sysfont */ - bmfont_t **fonts; /* one slot per menu item, lazily loaded */ - int nfonts; /* length of fonts[]; == menu item count */ - int current; /* index into fonts[], or -1 for sysfont */ - wuss_menu_item_t top_items[2]; /* "Font" and "Sample", built once the - * fontmenu exists so items[0].submenu can - * borrow its live wuss_menu_t */ - wuss_menu_t top_menu; /* root menu passed to wuss_menu_open */ - int sample; /* index into text_samples[] currently shown */ - const char *text; /* text_samples[sample].text; what text_redraw - * lays out and draws */ - colour_t bg, fg; - char resources[256]; /* root the picker loads bmfonts from */ - int base_width; /* content width when the window was made */ - int base_height; /* content height when the window was made */ - int frame_count; - bool resizing; /* toggled by a content click; text_step only - * resizes the window while this is true */ + wuss_window_t *window; + wuss_task_t *delegate; /* the wuss task backing this window */ + wuss_t *wuss; /* for wuss_get_pointer when opening the + * menu */ + wuss_fontmenu_t *fontmenu; /* the font picker; owns the menu and + * names */ + bmfont_t *font; /* currently shown; fonts[current] or + * sysfont */ + bmfont_t **fonts; /* one slot per menu item, lazily loaded */ + int nfonts; /* length of fonts[]; == menu item count */ + int current; /* index into fonts[], or -1 for sysfont */ + wuss_colourmenu_t *fgmenu; /* the "Foreground" picker; owns its menu */ + wuss_colourmenu_t *bgmenu; /* the "Background" picker; owns its menu */ + wuss_menu_item_t top_items[6]; /* "Font", "Sample", "Spacing", + * "Foreground", "Background" and "No + * Background", built once the fontmenu and + * colourmenus exist so items[0/3/4].submenu + * can borrow their live wuss_menu_t */ + wuss_menu_t top_menu; /* root menu passed to wuss_menu_open */ + wuss_menu_handle_t menu_handle; /* chain handle from the last + * wuss_menu_open, for the _live tick calls + * when an ADJUST pick keeps the chain + * open; NULL if closed */ + int sample; /* index into text_samples[] currently + * shown */ + const char *text; /* text_samples[sample].text; what + * text_redraw lays out and draws */ + int spacing_idx; /* index into text_spacing_presets[] + * currently applied */ + bmfont_spacing_t spacing; /* text_spacing_presets[spacing_idx], + * passed to bmtext_layout/bmtext_draw */ + wuss_colour_t fg_index; /* system palette index picked from + * "Foreground" */ + wuss_colour_t bg_index; /* system palette index picked from + * "Background" */ + bool bg_transparent; /* "No Background" tick; bg drawn + * transparent while set, overriding + * bg_index */ + int base_width; /* content width when the window was + * made */ + int base_height; /* content height when the window was + * made */ + int frame_count; + bool resizing; /* toggled by a content click; text_step + * only resizes the window while this is + * true */ } text_task_t; wuss_window_fn_t text_handle; -/* create the sample-text window against the given wuss instance. - * resources is the root the font picker loads bmfonts from - * (resources/bmfonts/.png). */ -result_t text_create(wuss_t *wuss, - const char *resources, - text_task_t *task); +/* create the sample-text window against the given wuss instance; the font + * picker loads bmfonts from wuss_get_resources(wuss)/resources/bmfonts/ + * .png. if out is non-NULL, the task block is also returned through + * it */ +result_t text_create(wuss_t *wuss, text_task_t **out); + +/* free a task block allocated by text_create; normally called by the + * window's wuss_EVENT_QUIT handler, not by callers directly */ +void text_destroy(text_task_t *task); #endif /* WUSS_APP */ diff --git a/libraries/wuss/test/wuss-test.c b/libraries/wuss/test/wuss-test.c index 4667779..0369b30 100644 --- a/libraries/wuss/test/wuss-test.c +++ b/libraries/wuss/test/wuss-test.c @@ -38,7 +38,7 @@ #if defined(WUSS_FURNITURE) && defined(WUSS_ICONS) #include "../core/impl.h" #endif -#if defined(WUSS_MENUS) && defined(WUSS_ICONS) +#if defined(WUSS_ICONS) #include "../icon.h" #endif @@ -231,6 +231,33 @@ static result_t paint_handle(wuss_window_t *window, return result_OK; } +/* A redraw handler that ignores "clip" and always floods its whole content + * box with the colour_t task_data points to, relying entirely on wuss-core's + * own clipping (scr->clip during the redraw dispatch) to keep the fill + * inside the window -- matching a task such as saturn.c that redraws its + * full fixed sample space every call rather than restricting itself to the + * dirty piece it was handed. */ +static result_t flood_full_bounds_handle(wuss_window_t *window, + const wuss_event_t *event, + void *task_data) +{ + const box_t *bounds; + const colour_t *colour; + + NOT_USED(window); + + if (event->kind != wuss_EVENT_REDRAW) + return result_OK; + + bounds = event->data.redraw.bounds; + colour = task_data; + screen_fill_rect(event->data.redraw.scr, bounds->x0, bounds->y0, + SIZE2D(bounds->x1 - bounds->x0, bounds->y1 - bounds->y0), + *colour); + + return result_OK; +} + /* ----------------------------------------------------------------------- */ /* Most tests declare their test_task_t on the block stack. A registered @@ -396,8 +423,8 @@ static int dirty_union_area(wuss_t *wuss, const box_t *bounds) { static unsigned char covered[512 * 512]; - box_t region; - int w, h, i, x, y, area; + box_t region; + int w, h, i, x, y, area; w = bounds->x1 - bounds->x0; h = bounds->y1 - bounds->y0; @@ -529,7 +556,7 @@ result_t wuss_test(const char *resources) bad_config.button.fg = 0; bad_config.button.pressed = 0; bad_config.accent.colour = 0; - rc = wuss_create(&scr, NULL, 0, NULL, 0, &bad_config, NULL, &bad_wuss); + rc = wuss_create(&scr, NULL, 0, NULL, 0, &bad_config, NULL, NULL, &bad_wuss); if (rc != result_WUSS_BAD_COLOUR) goto Failure; @@ -538,7 +565,7 @@ result_t wuss_test(const char *resources) { wuss_t *custom_wuss; - rc = wuss_create(&scr, NULL, 0, custom_palette, 2, NULL, NULL, &custom_wuss); + rc = wuss_create(&scr, NULL, 0, custom_palette, 2, NULL, NULL, NULL, &custom_wuss); if (rc != result_OK) goto Failure; wuss_destroy(custom_wuss); @@ -546,7 +573,7 @@ result_t wuss_test(const char *resources) printf("test: wuss_create with default palette\n"); - rc = wuss_create(&scr, NULL, 0, NULL, 0, NULL, NULL, &wuss); + rc = wuss_create(&scr, NULL, 0, NULL, 0, NULL, NULL, NULL, &wuss); if (rc != result_OK) goto Failure; @@ -561,11 +588,12 @@ result_t wuss_test(const char *resources) { 0xFF0000FF }, { 0xFF00FF00 }, { 0xFFFF0000 }, { 0xFFFFFFFF }, { 0xFF000000 } }; - wuss_config_t symcfg; - wuss_t *symw; - wuss_window_t *symwin; - wuss_task_t *symdel; - box_t symbox; + + wuss_config_t symcfg; + wuss_t *symw; + wuss_window_t *symwin; + wuss_task_t *symdel; + box_t symbox; memset(&symcfg, 0, sizeof(symcfg)); symcfg.furniture.title.bg = wuss_COLOUR_BLUE; /* -> index 2 */ @@ -574,7 +602,7 @@ result_t wuss_test(const char *resources) symcfg.body.window = wuss_COLOUR_RED; /* -> index 0 */ symcfg.body.menu = wuss_COLOUR_WHITE; /* -> index 3 */ - rc = wuss_create(&scr, NULL, 0, sympal, 5, &symcfg, NULL, &symw); + rc = wuss_create(&scr, NULL, 0, sympal, 5, &symcfg, NULL, NULL, &symw); if (rc != result_OK) goto Failure; @@ -627,7 +655,7 @@ result_t wuss_test(const char *resources) &box_a, "toosmall", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, box_size(&box_a), SIZE2D(0, 0), &win_a); @@ -647,10 +675,10 @@ result_t wuss_test(const char *resources) rc = wuss_window_create(delegate_a, &box_a, "A", - wuss_WINDOW_NO_BACK | wuss_WINDOW_NO_TOGGLE_SIZE | - wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | - wuss_WINDOW_NO_RESIZE, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_WINDOW_DEFAULT & ~wuss_WINDOW_BACK & + ~wuss_WINDOW_TOGGLE_SIZE & ~wuss_WINDOW_VSCROLL & + ~wuss_WINDOW_HSCROLL & ~wuss_WINDOW_RESIZE, + wuss_NO_BACKDROP, box_size(&box_a), SIZE2D(0, 0), &win_a); @@ -668,10 +696,10 @@ result_t wuss_test(const char *resources) rc = wuss_window_create(delegate_b, &box_b, "B", - wuss_WINDOW_NO_BACK | wuss_WINDOW_NO_TOGGLE_SIZE | - wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | - wuss_WINDOW_NO_RESIZE, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_WINDOW_DEFAULT & ~wuss_WINDOW_BACK & + ~wuss_WINDOW_TOGGLE_SIZE & ~wuss_WINDOW_VSCROLL & + ~wuss_WINDOW_HSCROLL & ~wuss_WINDOW_RESIZE, + wuss_NO_BACKDROP, box_size(&box_b), SIZE2D(0, 0), &win_b); @@ -989,9 +1017,10 @@ result_t wuss_test(const char *resources) * visible top-left (10-1 outline) back to (0,0), then the clamp caps * the content at 200 - 2*1 - 20(titlebar) tall, 200 - 2*1 wide. */ box_big.x0 = 10; box_big.y0 = 10; box_big.x1 = 400; box_big.y1 = 400; - rc = wuss_window_create(mk_task(wuss, NULL, NULL), &box_big, "BIG", wuss_WINDOW_NO_VSCROLL | - wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + rc = wuss_window_create(mk_task(wuss, NULL, NULL), &box_big, "BIG", + wuss_WINDOW_DEFAULT & ~wuss_WINDOW_VSCROLL & + ~wuss_WINDOW_HSCROLL & ~wuss_WINDOW_RESIZE, + wuss_NO_BACKDROP, SIZE2D(400, 400), SIZE2D(0, 0), &win_big); if (rc != result_OK) goto Failure; @@ -1014,10 +1043,9 @@ result_t wuss_test(const char *resources) rc = wuss_window_create(delegate_d, &box_d, "ignored", - wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | - wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | - wuss_WINDOW_NO_RESIZE, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + /* fully chromeless: no titlebar, outline, scrollbars or resize */ + wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE, + wuss_NO_BACKDROP, box_size(&box_d), SIZE2D(0, 0), &win_d); @@ -1046,10 +1074,10 @@ result_t wuss_test(const char *resources) printf("test: content click on a title-less window does not change z-order\n"); { - static test_task_t tc_e, tc_f; - wuss_task_t *delegate_e, *delegate_f; - box_t box_e, box_f; - wuss_window_t *win_e, *win_f; + static test_task_t tc_e, tc_f; + wuss_task_t *delegate_e, *delegate_f; + box_t box_e, box_f; + wuss_window_t *win_e, *win_f; tc_e.redraw_count = 0; tc_e.mouse_count = 0; @@ -1061,10 +1089,8 @@ result_t wuss_test(const char *resources) rc = wuss_window_create(delegate_e, &box_e, NULL, - wuss_WINDOW_NO_TITLEBAR | - wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | - wuss_WINDOW_NO_RESIZE, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_WINDOW_NO_TITLEBAR, + wuss_NO_BACKDROP, box_size(&box_e), SIZE2D(0, 0), &win_e); @@ -1081,10 +1107,8 @@ result_t wuss_test(const char *resources) rc = wuss_window_create(delegate_f, &box_f, NULL, - wuss_WINDOW_NO_TITLEBAR | - wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | - wuss_WINDOW_NO_RESIZE, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_WINDOW_NO_TITLEBAR, + wuss_NO_BACKDROP, box_size(&box_f), SIZE2D(0, 0), &win_f); @@ -1132,11 +1156,11 @@ result_t wuss_test(const char *resources) printf("test: moving/resizing a window entirely behind an occluder has no visible effect\n"); { - static test_task_t tc_h, tc_g; - wuss_task_t *delegate_h, *delegate_g; - box_t box_h, box_g; - wuss_window_t *win_h, *win_g; - int before_h, before_g; + static test_task_t tc_h, tc_g; + wuss_task_t *delegate_h, *delegate_g; + box_t box_h, box_g; + wuss_window_t *win_h, *win_g; + int before_h, before_g; tc_h.redraw_count = 0; tc_h.mouse_count = 0; @@ -1148,10 +1172,9 @@ result_t wuss_test(const char *resources) rc = wuss_window_create(delegate_h, &box_h, NULL, - wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | - wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | - wuss_WINDOW_NO_RESIZE, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + /* fully chromeless: no titlebar, outline, scrollbars or resize */ + wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE, + wuss_NO_BACKDROP, box_size(&box_h), SIZE2D(0, 0), &win_h); @@ -1168,10 +1191,9 @@ result_t wuss_test(const char *resources) rc = wuss_window_create(delegate_g, &box_g, NULL, - wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | - wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | - wuss_WINDOW_NO_RESIZE, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + /* fully chromeless: no titlebar, outline, scrollbars or resize */ + wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE, + wuss_NO_BACKDROP, box_size(&box_g), SIZE2D(0, 0), &win_g); @@ -1214,10 +1236,10 @@ result_t wuss_test(const char *resources) printf("test: bring-to-front only invalidates the newly-uncovered part\n"); { - static test_task_t tc_i, tc_j; - wuss_task_t *delegate_i, *delegate_j; - box_t box_i, box_j, dirty; - wuss_window_t *win_i, *win_j; + static test_task_t tc_i, tc_j; + wuss_task_t *delegate_i, *delegate_j; + box_t box_i, box_j, dirty; + wuss_window_t *win_i, *win_j; tc_i.redraw_count = 0; tc_i.mouse_count = 0; @@ -1229,10 +1251,9 @@ result_t wuss_test(const char *resources) rc = wuss_window_create(delegate_i, &box_i, NULL, - wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | - wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | - wuss_WINDOW_NO_RESIZE, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + /* fully chromeless: no titlebar, outline, scrollbars or resize */ + wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE, + wuss_NO_BACKDROP, box_size(&box_i), SIZE2D(0, 0), &win_i); @@ -1249,10 +1270,9 @@ result_t wuss_test(const char *resources) rc = wuss_window_create(delegate_j, &box_j, NULL, - wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | - wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | - wuss_WINDOW_NO_RESIZE, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + /* fully chromeless: no titlebar, outline, scrollbars or resize */ + wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE, + wuss_NO_BACKDROP, box_size(&box_j), SIZE2D(0, 0), &win_j); @@ -1283,11 +1303,11 @@ result_t wuss_test(const char *resources) printf("test: dragging off-screen and back on repaints the reappearing edge\n"); { - static test_task_t tc_m; - wuss_task_t *delegate_m; - box_t box_m; - wuss_window_t *win_m; - int before_m; + static test_task_t tc_m; + wuss_task_t *delegate_m; + box_t box_m; + wuss_window_t *win_m; + int before_m; tc_m.redraw_count = 0; tc_m.mouse_count = 0; @@ -1299,10 +1319,9 @@ result_t wuss_test(const char *resources) rc = wuss_window_create(delegate_m, &box_m, NULL, - wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | - wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | - wuss_WINDOW_NO_RESIZE, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + /* fully chromeless: no titlebar, outline, scrollbars or resize */ + wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE, + wuss_NO_BACKDROP, box_size(&box_m), SIZE2D(0, 0), &win_m); @@ -1338,10 +1357,10 @@ result_t wuss_test(const char *resources) printf("test: Adjust-click on a window's back icon brings it to front\n"); { - static test_task_t tc_g, tc_h; - wuss_task_t *delegate_g, *delegate_h; - box_t box_g, box_h; - wuss_window_t *win_g, *win_h; + static test_task_t tc_g, tc_h; + wuss_task_t *delegate_g, *delegate_h; + box_t box_g, box_h; + wuss_window_t *win_g, *win_h; tc_h.redraw_count = 0; tc_h.mouse_count = 0; @@ -1354,7 +1373,7 @@ result_t wuss_test(const char *resources) &box_h, "H", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, box_size(&box_h), SIZE2D(0, 0), &win_h); @@ -1371,8 +1390,9 @@ result_t wuss_test(const char *resources) rc = wuss_window_create(delegate_g, &box_g, "G", - wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_WINDOW_DEFAULT & ~wuss_WINDOW_VSCROLL & + ~wuss_WINDOW_HSCROLL, + wuss_NO_BACKDROP, box_size(&box_g), SIZE2D(0, 0), &win_g); @@ -1478,10 +1498,10 @@ result_t wuss_test(const char *resources) printf("test: drag-resize stops at min_doc, not just WUSS_MIN_CONTENT\n"); { - static test_task_t tc_m; - wuss_task_t *delegate_m; - box_t box_m, content, visible; - wuss_window_t *win_m; + static test_task_t tc_m; + wuss_task_t *delegate_m; + box_t box_m, content, visible; + wuss_window_t *win_m; tc_m.redraw_count = 0; tc_m.mouse_count = 0; @@ -1491,7 +1511,7 @@ result_t wuss_test(const char *resources) box_m.x0 = 10; box_m.y0 = 10; box_m.x1 = 210; box_m.y1 = 210; /* 200x200 content, floored at 80x60 */ rc = wuss_window_create(delegate_m, &box_m, "M", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(200, 200), SIZE2D(80, 60), &win_m); if (rc != result_OK) @@ -1521,17 +1541,111 @@ result_t wuss_test(const char *resources) wuss_window_close(win_m); } + printf("test: drag-resize never grows a window's total size past the screen's, on either axis\n"); + + { + static test_task_t tc_s; + wuss_task_t *delegate_s; + box_t box_s, content, visible; + wuss_window_t *win_s; + + tc_s.redraw_count = 0; + tc_s.mouse_count = 0; + delegate_s = mk_task(wuss, test_handle, &tc_s); + if (delegate_s == NULL) goto Failure; + + box_s.x0 = 10; box_s.y0 = 10; + box_s.x1 = 30; box_s.y1 = 30; /* 20x20 content; doc is 1000x1000, far bigger than the 200x200 screen */ + rc = wuss_window_create(delegate_s, &box_s, "S", wuss_WINDOW_DEFAULT, + wuss_NO_BACKDROP, + SIZE2D(1000, 1000), SIZE2D(0, 0), &win_s); + if (rc != result_OK) + goto Failure; + + wuss_window_get_visible_bounds(win_s, &visible); + + rc = wuss_mouse_click(wuss, POINT(visible.x1 - 3, visible.y1 - 3), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* S's resize icon */ + if (rc != result_OK) + goto Failure; + if (hit != win_s) + goto Failure; + + rc = wuss_mouse_move(wuss, POINT(1000, 1000), &hit); /* drag far past the 200x200 screen */ + if (rc != result_OK) + goto Failure; + + rc = wuss_mouse_click(wuss, POINT(1000, 1000), wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); + if (rc != result_OK) + goto Failure; + + wuss_window_get_visible_bounds(win_s, &visible); + if (visible.x1 - visible.x0 > 200 || visible.y1 - visible.y0 > 200) + goto Failure; /* total footprint capped at the screen size, wherever the window sits */ + + wuss_window_close(win_s); + } + + { + /* Same cap, but for a window moved partway off the top-left of the + * screen: the mouse pointer itself is clamped to the screen (see + * wuss_mouse_move), so a positive content.x0 can never see a resize + * drag reach far enough to overhang -- the gap only shows up once + * content.x0 is negative, letting the drag's on-screen pointer range + * translate into a width bigger than the screen itself. */ + static test_task_t tc_e; + wuss_task_t *delegate_e; + box_t box_e, visible; + wuss_window_t *win_e; + + tc_e.redraw_count = 0; + tc_e.mouse_count = 0; + delegate_e = mk_task(wuss, test_handle, &tc_e); + if (delegate_e == NULL) goto Failure; + + box_e.x0 = 10; box_e.y0 = 10; + box_e.x1 = 30; box_e.y1 = 30; /* 20x20 content; doc is 1000x1000, far bigger than the 200x200 screen */ + rc = wuss_window_create(delegate_e, &box_e, "E", wuss_WINDOW_DEFAULT, + wuss_NO_BACKDROP, + SIZE2D(1000, 1000), SIZE2D(0, 0), &win_e); + if (rc != result_OK) + goto Failure; + + wuss_window_move(win_e, POINT(-100, -100)); /* content top-left off-screen, top-left corner */ + + wuss_window_get_visible_bounds(win_e, &visible); + + rc = wuss_mouse_click(wuss, POINT(visible.x1 - 3, visible.y1 - 3), wuss_BUTTON_SELECT, wuss_MOUSE_DOWN, &hit); /* E's resize icon */ + if (rc != result_OK) + goto Failure; + if (hit != win_e) + goto Failure; + + rc = wuss_mouse_move(wuss, POINT(1000, 1000), &hit); /* drag to the far corner; the pointer itself clamps to (199,199) */ + if (rc != result_OK) + goto Failure; + + rc = wuss_mouse_click(wuss, POINT(1000, 1000), wuss_BUTTON_SELECT, wuss_MOUSE_UP, &hit); + if (rc != result_OK) + goto Failure; + + wuss_window_get_visible_bounds(win_e, &visible); + if (visible.x1 - visible.x0 > 200 || visible.y1 - visible.y0 > 200) + goto Failure; /* total footprint still capped at the screen size, even with a negative x0/y0 */ + + wuss_window_close(win_e); + } + printf("test: toggle-size blits rather than redrawing the whole window\n"); { - static test_task_t tc_t; - wuss_task_t *delegate_t; - box_t box_t_win, before, after, titlebar, toggle; - wuss_window_t *win_t; - int outline_px, titlebar_height, inset, icon; - int i, dirty_area, full_area, cx, cy, old_icon_x, old_icon_y, found; - int interior_x, interior_y, interior_dirty; - int old_vscroll_x, old_vscroll_y, old_vscroll_found; + static test_task_t tc_t; + wuss_task_t *delegate_t; + box_t box_t_win, before, after, titlebar, toggle; + wuss_window_t *win_t; + int outline_px, titlebar_height, inset, icon; + int i, dirty_area, full_area, cx, cy, old_icon_x, old_icon_y, found; + int interior_x, interior_y, interior_dirty; + int old_vscroll_x, old_vscroll_y, old_vscroll_found; tc_t.redraw_count = 0; tc_t.mouse_count = 0; @@ -1541,7 +1655,7 @@ result_t wuss_test(const char *resources) box_t_win.x0 = 10; box_t_win.y0 = 10; box_t_win.x1 = 50; box_t_win.y1 = 50; /* 40x40 content, room to grow to a 150x150 doc without the toggled box needing to be repositioned off (10,10) -- this test is about the in-place grow blit */ rc = wuss_window_create(delegate_t, &box_t_win, "T", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(150, 150), SIZE2D(0, 0), &win_t); if (rc != result_OK) goto Failure; @@ -1693,12 +1807,12 @@ result_t wuss_test(const char *resources) printf("test: toggle-size that forces a scroll re-clamp invalidates the content it's about to redraw at the new offset, not just the blit's edge sliver\n"); { - static test_task_t tc_r; - wuss_task_t *delegate_r; - box_t box_r, before, titlebar, toggle; - wuss_window_t *win_r; - int outline_px, titlebar_height, inset, icon; - int i, interior_x, interior_y, interior_dirty, cx, cy; + static test_task_t tc_r; + wuss_task_t *delegate_r; + box_t box_r, before, titlebar, toggle; + wuss_window_t *win_r; + int outline_px, titlebar_height, inset, icon; + int i, interior_x, interior_y, interior_dirty, cx, cy; tc_r.redraw_count = 0; tc_r.mouse_count = 0; @@ -1708,7 +1822,7 @@ result_t wuss_test(const char *resources) box_r.x0 = 10; box_r.y0 = 10; box_r.x1 = 50; box_r.y1 = 50; /* 40x40 content; doc bigger than that, so it starts scrollable */ rc = wuss_window_create(delegate_r, &box_r, "R", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(70, 70), SIZE2D(0, 0), &win_r); if (rc != result_OK) goto Failure; @@ -1796,14 +1910,14 @@ result_t wuss_test(const char *resources) * grow forces a scroll re-clamp, the overlap of the old and new content * boxes is genuine on-screen content that just needs sliding by the * scroll delta -- only the newly-exposed strip needs a real repaint. */ - static test_task_t tc_d; - wuss_task_t *delegate_d; - box_t box_d, content_before; - wuss_window_t *win_d; - point_t scroll_d; - int i; - int top_x, top_y, mid_x, mid_y, bottom_x, bottom_y; - int top_dirty, mid_dirty, bottom_dirty; + static test_task_t tc_d; + wuss_task_t *delegate_d; + box_t box_d, content_before; + wuss_window_t *win_d; + point_t scroll_d; + int i; + int top_x, top_y, mid_x, mid_y, bottom_x, bottom_y; + int top_dirty, mid_dirty, bottom_dirty; tc_d.redraw_count = 0; tc_d.mouse_count = 0; @@ -1813,7 +1927,7 @@ result_t wuss_test(const char *resources) box_d.x0 = 10; box_d.y0 = 10; box_d.x1 = 90; box_d.y1 = 90; /* 80x80 content; doc taller, so it starts scrollable */ rc = wuss_window_create(delegate_d, &box_d, "D", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(80, 140), SIZE2D(0, 0), &win_d); if (rc != result_OK) goto Failure; @@ -1899,14 +2013,14 @@ result_t wuss_test(const char *resources) * the occluder's own correct pixels, so it must stay clean -- dirtying * it would redraw the occluding window for nothing. Only the un-occluded * newly-revealed top strip needs a repaint. */ - static test_task_t tc_d, tc_o; - wuss_task_t *delegate_d, *delegate_o; - box_t box_d, box_o, content_before; - wuss_window_t *win_d, *win_o; - point_t scroll_d; - int i, split_x; - int occluded_x, occluded_y, kept_x, kept_y, strip_x, strip_y; - int occluded_dirty, kept_dirty, strip_dirty; + static test_task_t tc_d, tc_o; + wuss_task_t *delegate_d, *delegate_o; + box_t box_d, box_o, content_before; + wuss_window_t *win_d, *win_o; + point_t scroll_d; + int i, split_x; + int occluded_x, occluded_y, kept_x, kept_y, strip_x, strip_y; + int occluded_dirty, kept_dirty, strip_dirty; tc_d.redraw_count = 0; tc_d.mouse_count = 0; tc_o.redraw_count = 0; tc_o.mouse_count = 0; @@ -1918,7 +2032,7 @@ result_t wuss_test(const char *resources) box_d.x0 = 10; box_d.y0 = 10; box_d.x1 = 110; box_d.y1 = 90; /* 100x80 content; doc taller, so scrollable */ rc = wuss_window_create(delegate_d, &box_d, "D", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(100, 140), SIZE2D(0, 0), &win_d); if (rc != result_OK) goto Failure; @@ -1930,7 +2044,7 @@ result_t wuss_test(const char *resources) box_o.x0 = split_x; box_o.y0 = content_before.y0 - 5; box_o.x1 = content_before.x1 + 40; box_o.y1 = content_before.y1 + 40; rc = wuss_window_create(delegate_o, &box_o, "O", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(box_o.x1 - box_o.x0, box_o.y1 - box_o.y0), SIZE2D(0, 0), &win_o); if (rc != result_OK) @@ -2012,13 +2126,13 @@ result_t wuss_test(const char *resources) * the occluder's own correct pixels, so it must stay clean -- dirtying * it would redraw the occluding window. Only the un-occluded * newly-exposed edge strip needs a repaint. */ - static test_task_t tc_s, tc_o; - wuss_task_t *delegate_s, *delegate_o; - box_t box_s, box_o, content; - wuss_window_t *win_s, *win_o; - int i, split_x; - int occluded_x, occluded_y, kept_x, kept_y, strip_x, strip_y; - int occluded_dirty, kept_dirty, strip_dirty; + static test_task_t tc_s, tc_o; + wuss_task_t *delegate_s, *delegate_o; + box_t box_s, box_o, content; + wuss_window_t *win_s, *win_o; + int i, split_x; + int occluded_x, occluded_y, kept_x, kept_y, strip_x, strip_y; + int occluded_dirty, kept_dirty, strip_dirty; tc_s.redraw_count = 0; tc_s.mouse_count = 0; tc_o.redraw_count = 0; tc_o.mouse_count = 0; @@ -2030,7 +2144,7 @@ result_t wuss_test(const char *resources) box_s.x0 = 10; box_s.y0 = 10; box_s.x1 = 110; box_s.y1 = 90; /* 100x80 content; doc taller, so scrollable */ rc = wuss_window_create(delegate_s, &box_s, "S", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(100, 200), SIZE2D(0, 0), &win_s); if (rc != result_OK) goto Failure; @@ -2041,7 +2155,7 @@ result_t wuss_test(const char *resources) box_o.x0 = split_x; box_o.y0 = content.y0 - 5; box_o.x1 = content.x1 + 40; box_o.y1 = content.y1 + 40; rc = wuss_window_create(delegate_o, &box_o, "O", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(box_o.x1 - box_o.x0, box_o.y1 - box_o.y0), SIZE2D(0, 0), &win_o); if (rc != result_OK) @@ -2114,13 +2228,13 @@ result_t wuss_test(const char *resources) * With the fix each destination is clipped against the occluders and only * the un-occluded sub-pieces are blitted, leaving O's own pixels intact. * Read the framebuffer behind O to check. */ - colour_t cm; - uint32_t fb_m, fb_o; - static test_task_t tc_m, tc_o; - wuss_task_t *delegate_m, *delegate_o; - box_t box_m, box_o, content; - wuss_window_t *win_m, *win_o; - int occ_x, occ_y, mid_x, mid_y, delta; + colour_t cm; + uint32_t fb_m, fb_o; + static test_task_t tc_m, tc_o; + wuss_task_t *delegate_m, *delegate_o; + box_t box_m, box_o, content; + wuss_window_t *win_m, *win_o; + int occ_x, occ_y, mid_x, mid_y, delta; cm = colour_rgb(0x11, 0x22, 0x33); tc_m.redraw_count = 0; tc_m.mouse_count = 0; @@ -2133,7 +2247,7 @@ result_t wuss_test(const char *resources) box_m.x0 = 10; box_m.y0 = 10; box_m.x1 = 110; box_m.y1 = 110; /* 100x100 content; doc taller, so scrollable */ rc = wuss_window_create(delegate_m, &box_m, "M", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(100, 300), SIZE2D(0, 0), &win_m); if (rc != result_OK) goto Failure; @@ -2148,7 +2262,7 @@ result_t wuss_test(const char *resources) box_o.x0 = content.x0 - 5; box_o.y0 = content.y0 + 40; box_o.x1 = content.x1 + 5; box_o.y1 = content.y0 + 70; rc = wuss_window_create(delegate_o, &box_o, "O", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(box_o.x1 - box_o.x0, box_o.y1 - box_o.y0), SIZE2D(0, 0), &win_o); if (rc != result_OK) @@ -2196,12 +2310,12 @@ result_t wuss_test(const char *resources) * bottom edge end up double-shifted. The blit must be ordered (or fall * back). paint_handle paints M as one-pixel rows whose blue channel * encodes document Y, so a mis-shifted row is detectable exactly. */ - colour_t cm; - static test_task_t tc_m, tc_o; - wuss_task_t *delegate_m, *delegate_o; - box_t box_m, box_o, content, ovis; - wuss_window_t *win_m, *win_o; - int gx, gy, delta, bad; + colour_t cm; + static test_task_t tc_m, tc_o; + wuss_task_t *delegate_m, *delegate_o; + box_t box_m, box_o, content, ovis; + wuss_window_t *win_m, *win_o; + int gx, gy, delta, bad; cm = colour_rgb(0x11, 0x22, 0x33); tc_m.redraw_count = 0; tc_m.mouse_count = 0; @@ -2214,7 +2328,7 @@ result_t wuss_test(const char *resources) box_m.x0 = 10; box_m.y0 = 10; box_m.x1 = 110; box_m.y1 = 110; /* 100x100 content; doc taller, scrollable */ rc = wuss_window_create(delegate_m, &box_m, "M", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(100, 300), SIZE2D(0, 0), &win_m); if (rc != result_OK) goto Failure; @@ -2225,7 +2339,7 @@ result_t wuss_test(const char *resources) box_o.x0 = content.x0 + 25; box_o.y0 = content.y0 + 25; box_o.x1 = content.x0 + 75; box_o.y1 = content.y0 + 65; rc = wuss_window_create(delegate_o, &box_o, "O", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(box_o.x1 - box_o.x0, box_o.y1 - box_o.y0), SIZE2D(0, 0), &win_o); if (rc != result_OK) @@ -2268,15 +2382,571 @@ result_t wuss_test(const char *resources) wuss_window_close(win_m); } + printf("test: scrolling a window shrunk below its document size never marks a neighbour's screen area dirty\n"); + + { + /* Regression: a window resized down below its document extent (so it + * becomes scrollable), then scrolled by wheel/scrollbar, must not touch + * a neighbour window's own screen area. flood_full_bounds_handle ignores + * "clip" entirely, mirroring a real task (e.g. saturn.c) that redraws + * its full sample space every call rather than honouring clip -- the + * wuss-core clip machinery, not the task, must be what keeps such a + * task's pixels inside its own window. */ + colour_t s_colour, n_colour; + static test_task_t tc_s, tc_n; + wuss_task_t *delegate_s, *delegate_n; + box_t box_s, box_n, content_n; + wuss_window_t *win_s, *win_n; + int nx, ny, bad; + + s_colour = colour_rgb(0xcc, 0xdd, 0xee); + n_colour = colour_rgb(0x40, 0x60, 0x80); + tc_s.redraw_count = 0; tc_s.mouse_count = 0; + tc_n.redraw_count = 0; tc_n.mouse_count = 0; + delegate_s = mk_task(wuss, flood_full_bounds_handle, &s_colour); + if (delegate_s == NULL) goto Failure; + delegate_n = mk_task(wuss, flood_full_bounds_handle, &n_colour); + if (delegate_n == NULL) goto Failure; + + /* S starts full-sized (matches its document, so no scrollbars yet), then + * gets shrunk -- exercising wuss_window_resize's scroll-reclamp path, + * distinct from creating it small to begin with. */ + box_s.x0 = 10; box_s.y0 = 10; + box_s.x1 = 90; box_s.y1 = 90; + rc = wuss_window_create(delegate_s, &box_s, "S", wuss_WINDOW_DEFAULT, + wuss_NO_BACKDROP, + SIZE2D(80, 80), SIZE2D(0, 0), &win_s); + if (rc != result_OK) + goto Failure; + + /* N sits immediately to the right of S's outer box, close enough that a + * blit destination sliding even a few pixels past S's own bounds lands + * on N. */ + box_n.x0 = box_s.x1 + 2; box_n.y0 = 10; + box_n.x1 = box_n.x0 + 40; box_n.y1 = 90; + rc = wuss_window_create(delegate_n, &box_n, "N", wuss_WINDOW_DEFAULT, + wuss_NO_BACKDROP, + SIZE2D(box_n.x1 - box_n.x0, box_n.y1 - box_n.y0), + SIZE2D(0, 0), &win_n); + if (rc != result_OK) + goto Failure; + + rc = wuss_redraw_dirty(wuss); /* flush both creates */ + if (rc != result_OK) + goto Failure; + + /* shrink S well below its 80x80 document -- this window is now + * scrollable */ + rc = wuss_window_resize(win_s, SIZE2D(30, 30)); + if (rc != result_OK) + goto Failure; + rc = wuss_redraw_dirty(wuss); + if (rc != result_OK) + goto Failure; + + /* scroll it, as a wheel/scrollbar-drag would */ + wuss_window_set_scroll(win_s, POINT(20, 20)); + rc = wuss_redraw_dirty(wuss); + if (rc != result_OK) + goto Failure; + + /* every pixel of N's own content (not its titlebar/outline chrome) must + * still show exactly N's own flood colour */ + wuss_window_get_content_bounds(win_n, &content_n); + bad = 0; + for (ny = content_n.y0; ny < content_n.y1 && !bad; ny++) + for (nx = content_n.x0; nx < content_n.x1; nx++) + { + uint32_t px; + + px = ((const uint32_t *) pixels)[ny * 200 + nx]; + if ((px & 0xffffff) != 0x406080) + { + bad = 1; + break; + } + } + if (bad) + goto Failure; /* S's scroll blit painted over N's screen area */ + + wuss_window_close(win_n); + wuss_window_close(win_s); + } + + printf("test: shrinking a window repaints its content right up to the new content box's own edge\n"); + + { + /* Regression: dragging a resize handle to make a window smaller must + * leave no stale sliver between the client's repainted content and the + * furniture (scrollbar/outline) now sitting at the new, smaller edge. */ + static test_task_t tc_s; + wuss_task_t *delegate_s; + box_t box_s, content_s; + wuss_window_t *win_s; + colour_t s_colour; + int nx, ny, bad; + + s_colour = colour_rgb(0xcc, 0xdd, 0xee); + tc_s.redraw_count = 0; tc_s.mouse_count = 0; + delegate_s = mk_task(wuss, flood_full_bounds_handle, &s_colour); + if (delegate_s == NULL) goto Failure; + + box_s.x0 = 10; box_s.y0 = 10; + box_s.x1 = 90; box_s.y1 = 90; + rc = wuss_window_create(delegate_s, &box_s, "S", wuss_WINDOW_DEFAULT, + wuss_NO_BACKDROP, + SIZE2D(80, 80), SIZE2D(200, 200), &win_s); + if (rc != result_OK) + goto Failure; + rc = wuss_redraw_dirty(wuss); /* flush the create, paint S's initial content */ + if (rc != result_OK) + goto Failure; + + /* scroll before shrinking -- the shrink must not leave a stale sliver + * at the new content edge regardless of scroll offset */ + wuss_window_set_scroll(win_s, POINT(20, 20)); + rc = wuss_redraw_dirty(wuss); + if (rc != result_OK) + goto Failure; + + /* an interactive drag delivers many small pointer-move events, each + * calling wuss_window_resize, before the display ever gets to redraw -- + * shrink one pixel at a time with no flush between them, matching that; + * throw in an overshoot-and-correct wobble too, as a real drag does */ + { + int step, w; + + for (step = 0, w = 80; w > 30; w--, step++) + { + rc = wuss_window_resize(win_s, SIZE2D(w, w)); + if (rc != result_OK) + goto Failure; + } + rc = wuss_window_resize(win_s, SIZE2D(35, 35)); /* overshoot back out */ + if (rc != result_OK) + goto Failure; + rc = wuss_window_resize(win_s, SIZE2D(30, 30)); /* settle */ + if (rc != result_OK) + goto Failure; + } + rc = wuss_redraw_dirty(wuss); + if (rc != result_OK) + goto Failure; + + /* every pixel of S's own (new, smaller) content box must show S's + * flood colour right up to content_s.x1-1 / content_s.y1-1 -- no gap + * between the repainted content and the furniture now abutting it */ + wuss_window_get_content_bounds(win_s, &content_s); + bad = 0; + for (ny = content_s.y0; ny < content_s.y1 && !bad; ny++) + for (nx = content_s.x0; nx < content_s.x1; nx++) + { + uint32_t px; + + px = ((const uint32_t *) pixels)[ny * 200 + nx]; + if ((px & 0xffffff) != 0xccddee) + { + bad = 1; + break; + } + } + if (bad) + goto Failure; /* stale sliver left along the new content box's edge */ + + wuss_window_close(win_s); + } + + printf("test: a fast resize-grow past the scroll clamp never blits a not-yet-painted sliver\n"); + + { + /* Regression: wuss_window_resize's scroll-reclamp blit (window scrolled + * to the document's bottom/right, then grown so the bigger viewport + * needs less scroll and the clamp pulls the offset back) reads its blit + * source straight from wuss__clip_to_visible over the *old* content box, + * unlike wuss_window_move/wuss_window_set_scroll which both also strip + * anything still sitting in wuss->dirty[] (queued by an earlier call this + * same frame, not yet painted). An interactive drag delivers several + * resize calls before a redraw ever runs: one grow queues its + * newly-exposed sliver dirty without painting it; that sliver sits + * inside the *next* call's "old content box", so the next call's + * scroll-reclamp blit slides it as if it were this window's own settled + * rendering -- smearing a stale/undrawn sliver that no later invalidate + * ever repaints. paint_handle's per-row blue-encoded doc_y makes a + * wrongly-slid row detectable exactly. */ + static test_task_t tc_s; + wuss_task_t *delegate_s; + box_t box_s, content_s; + wuss_window_t *win_s; + colour_t s_colour; + point_t scroll; + int sy, sx, bad; + + s_colour = colour_rgb(0x11, 0x22, 0x33); + tc_s.redraw_count = 0; tc_s.mouse_count = 0; + delegate_s = mk_task(wuss, paint_handle, &s_colour); + if (delegate_s == NULL) goto Failure; + + box_s.x0 = 10; box_s.y0 = 10; + box_s.x1 = 40; box_s.y1 = 40; /* 30x30 content; a doc far bigger than the + * screen so growing 1px at a time pulls the + * scroll clamp back by exactly 1px per + * call, keeping it non-zero (and so + * re-triggering the reclamp path) across + * the whole grow range below */ + rc = wuss_window_create(delegate_s, &box_s, "S", wuss_WINDOW_DEFAULT, + wuss_NO_BACKDROP, + SIZE2D(1000, 1000), SIZE2D(30, 30), &win_s); + if (rc != result_OK) + goto Failure; + rc = wuss_redraw_dirty(wuss); /* flush the create, paint S's initial content */ + if (rc != result_OK) + goto Failure; + + /* scroll to the document's bottom-right: no further offset possible at + * this size */ + wuss_window_set_scroll(win_s, POINT(970, 970)); + rc = wuss_redraw_dirty(wuss); + if (rc != result_OK) + goto Failure; + + /* grow two calls at a time with no flush between them, as an interactive + * drag delivers several pointer-move events per rendered frame -- each + * bigger size pulls the scroll clamp back by 1px, so every single call + * re-enters the reclamp path, and the second call of each pair reads its + * blit source over ground the first call of the pair only just + * invalidated (its own grown sliver) but never painted. Flushing after + * every pair (rather than only once at the end) matches a real redraw + * loop -- it also stops one pair's leftover full-footprint dirty rect + * from coalescing over a later pair's corruption and masking it. */ + { + int w; + + for (w = 30; w <= 90; w += 2) + { + rc = wuss_window_resize(win_s, SIZE2D(w, w)); + if (rc != result_OK) + goto Failure; + rc = wuss_window_resize(win_s, SIZE2D(w + 1, w + 1)); + if (rc != result_OK) + goto Failure; + + rc = wuss_redraw_dirty(wuss); + if (rc != result_OK) + goto Failure; + + /* every row of S's current content box must show exactly the + * document Y that its current scroll offset puts there -- a + * stale-sliver blit leaves some row(s) showing a stale doc_y (or + * whatever else was sitting in the framebuffer) instead */ + wuss_window_get_content_bounds(win_s, &content_s); + wuss_window_get_scroll(win_s, &scroll); + bad = 0; + for (sy = content_s.y0; sy < content_s.y1 && !bad; sy++) + { + uint32_t px, blue, want; + + want = (uint32_t) ((sy - content_s.y0 + scroll.y) & 0xff); + for (sx = content_s.x0; sx < content_s.x1; sx++) + { + px = ((const uint32_t *) pixels)[sy * 200 + sx]; + blue = px & 0xff; + if (blue != want) + { + bad = 1; + break; + } + } + } + if (bad) + goto Failure; /* a not-yet-painted sliver got blitted as if settled */ + } + } + + wuss_window_close(win_s); + } + + printf("test: a fast resize drag that jumps size and reverses direction never blits a not-yet-painted sliver\n"); + + { + /* Same hazard as the paired-grow test above, but matching a real + * interactive drag more closely: the pointer can jump the size by a + * large delta in one event (not just 1px), and can reverse from + * growing to shrinking and back within the same unflushed batch. A + * shrink never re-enters the reclamp path itself (it only ever raises + * the clamp ceiling), but it still queues its own shrunk-away sliver + * dirty -- and the very next call, if it grows back past the clamp, + * must not treat that still-unpainted sliver as settled ground either. */ + static test_task_t tc_e; + wuss_task_t *delegate_e; + box_t box_e, content_e; + wuss_window_t *win_e; + colour_t e_colour; + point_t scroll; + int sy, sx, bad, step; + int sizes[] = { 30, 90, 35, 120, 31, 90, 30, 60, 110, 30 }; + + e_colour = colour_rgb(0x11, 0x22, 0x33); + tc_e.redraw_count = 0; tc_e.mouse_count = 0; + delegate_e = mk_task(wuss, paint_handle, &e_colour); + if (delegate_e == NULL) goto Failure; + + box_e.x0 = 10; box_e.y0 = 10; + box_e.x1 = 40; box_e.y1 = 40; + rc = wuss_window_create(delegate_e, &box_e, "E", wuss_WINDOW_DEFAULT, + wuss_NO_BACKDROP, + SIZE2D(1000, 1000), SIZE2D(30, 30), &win_e); + if (rc != result_OK) + goto Failure; + rc = wuss_redraw_dirty(wuss); + if (rc != result_OK) + goto Failure; + + wuss_window_set_scroll(win_e, POINT(970, 970)); + rc = wuss_redraw_dirty(wuss); + if (rc != result_OK) + goto Failure; + + for (step = 0; step < 9; step += 2) + { + rc = wuss_window_resize(win_e, SIZE2D(sizes[step], sizes[step])); + if (rc != result_OK) + goto Failure; + rc = wuss_window_resize(win_e, SIZE2D(sizes[step + 1], sizes[step + 1])); + if (rc != result_OK) + goto Failure; + + rc = wuss_redraw_dirty(wuss); + if (rc != result_OK) + goto Failure; + + wuss_window_get_content_bounds(win_e, &content_e); + wuss_window_get_scroll(win_e, &scroll); + content_e.x1 = MIN(content_e.x1, 200); + content_e.y1 = MIN(content_e.y1, 200); + bad = 0; + for (sy = content_e.y0; sy < content_e.y1 && !bad; sy++) + { + uint32_t px, blue, want; + + want = (uint32_t) ((sy - content_e.y0 + scroll.y) & 0xff); + for (sx = content_e.x0; sx < content_e.x1; sx++) + { + px = ((const uint32_t *) pixels)[sy * 200 + sx]; + blue = px & 0xff; + if (blue != want) + { + bad = 1; + break; + } + } + } + if (bad) + goto Failure; /* a not-yet-painted sliver got blitted as if settled */ + } + + wuss_window_close(win_e); + } + + printf("test: a long unflushed batch of resize calls (matching a high-poll-rate mouse) never blits a not-yet-painted sliver\n"); + + { + /* The tests above batch only 1-2 resize calls per redraw; a real SDL + * drag drains its whole event queue before ever calling + * wuss_redraw_dirty (see apps/wuss/main.c's wuss_frame), so a fast + * high-poll-rate mouse can deliver dozens of resize calls in one + * batch. Stress that scale directly, using a cheap deterministic + * pseudo-random walk rather than a fixed table, so the sequence covers + * far more shapes (including piece-budget overflow) than a short + * hand-picked list ever would. */ + static test_task_t tc_f, tc_occ; + wuss_task_t *delegate_f, *delegate_occ; + box_t box_f, box_occ, content_f; + wuss_window_t *win_f, *win_occ; + colour_t f_colour; + point_t scroll; + unsigned int seed; + int sy, sx, bad, batch, step, sizew, sizeh; + + f_colour = colour_rgb(0x44, 0x55, 0x66); + tc_f.redraw_count = 0; tc_f.mouse_count = 0; + delegate_f = mk_task(wuss, paint_handle, &f_colour); + if (delegate_f == NULL) goto Failure; + + box_f.x0 = 10; box_f.y0 = 10; + box_f.x1 = 40; box_f.y1 = 40; + rc = wuss_window_create(delegate_f, &box_f, "F", wuss_WINDOW_DEFAULT, + wuss_NO_BACKDROP, + SIZE2D(1000, 1000), SIZE2D(30, 30), &win_f); + if (rc != result_OK) + goto Failure; + + /* An occluder created after (so it lands on top of) win_f, sat over its + * corner: real desktops are rarely a single window, and + * wuss__clip_to_visible's occlusion-carving path (untouched by every + * test above) can only interact with the reclamp blit's stale-region + * exclusion when there is something to occlude. New windows land at the + * z_order head (topmost) -- see wuss_window_create. A NULL task_data + * gives paint_handle's flat-fill branch, so the occluded area's colour + * can be told apart from win_f's doc-row pattern by eye if this ever + * fails; the pixel check below instead uses win_occ->visible (its real, + * furniture-adjusted footprint) rather than the requested box, since + * wuss__nudge_visible_onscreen/carve can move or grow it. */ + tc_occ.redraw_count = 0; tc_occ.mouse_count = 0; + delegate_occ = mk_task(wuss, paint_handle, NULL); + if (delegate_occ == NULL) goto Failure; + + box_occ.x0 = 25; box_occ.y0 = 25; + box_occ.x1 = 60; box_occ.y1 = 60; + rc = wuss_window_create(delegate_occ, &box_occ, "OCC", wuss_WINDOW_DEFAULT, + wuss_NO_BACKDROP, + SIZE2D(20, 20), SIZE2D(20, 20), &win_occ); + if (rc != result_OK) + goto Failure; + + rc = wuss_redraw_dirty(wuss); + if (rc != result_OK) + goto Failure; + + wuss_window_set_scroll(win_f, POINT(970, 970)); + rc = wuss_redraw_dirty(wuss); + if (rc != result_OK) + goto Failure; + + seed = 12345u; + sizew = 30; + sizeh = 30; + for (batch = 0; batch < 20; batch++) + { + for (step = 0; step < 25; step++) + { + seed = seed * 1103515245u + 12345u; + sizew += (int) (seed >> 24) % 21 - 10; /* +/-10 px per call */ + seed = seed * 1103515245u + 12345u; + sizeh += (int) (seed >> 24) % 21 - 10; + sizew = CLAMP(sizew, 30, 150); + sizeh = CLAMP(sizeh, 30, 150); + + rc = wuss_window_resize(win_f, SIZE2D(sizew, sizeh)); + if (rc != result_OK) + goto Failure; + } + + rc = wuss_redraw_dirty(wuss); + if (rc != result_OK) + goto Failure; + + wuss_window_get_content_bounds(win_f, &content_f); + wuss_window_get_scroll(win_f, &scroll); + content_f.x1 = MIN(content_f.x1, 200); + content_f.y1 = MIN(content_f.y1, 200); + bad = 0; + for (sy = content_f.y0; sy < content_f.y1 && !bad; sy++) + { + uint32_t px, blue, want; + + want = (uint32_t) ((sy - content_f.y0 + scroll.y) & 0xff); + for (sx = content_f.x0; sx < content_f.x1; sx++) + { + /* skip ground legitimately covered by the occluder's real, + * furniture-adjusted footprint -- it shows win_occ's own colour + * there, not win_f's doc-row pattern */ + if (sx >= win_occ->visible.x0 && sx < win_occ->visible.x1 && + sy >= win_occ->visible.y0 && sy < win_occ->visible.y1) + continue; + + px = ((const uint32_t *) pixels)[sy * 200 + sx]; + blue = px & 0xff; + if (blue != want) + { + bad = 1; + break; + } + } + } + if (bad) + goto Failure; /* a not-yet-painted sliver got blitted as if settled */ + } + + wuss_window_close(win_occ); + wuss_window_close(win_f); + } + + printf("test: wuss_window_invalidate_visible on a shrunk, scrolled window only dirties its own visible box\n"); + + { + /* Regression: wuss_window_invalidate(window, NULL) built its "whole + * visible rect" box already in screen space (offset by content.x0/y0), + * then translated it a second time by "content.x0 - scroll", so a + * scrolled window's dirty rect ended up shifted by -scroll off to one + * side of the window rather than sitting over it -- e.g. saturn.c's + * per-idle-tick wuss_window_invalidate_visible(), once its window was + * shrunk below its document and then scrolled, queued a dirty rect + * outside the window's own screen area (fill_backdrop_excluding_content + * then repaints that stray area as bare desktop every frame). */ + static test_task_t tc_s; + wuss_task_t *delegate_s; + box_t box_s, dirty; + wuss_window_t *win_s; + colour_t s_colour; + + s_colour = colour_rgb(0xcc, 0xdd, 0xee); + tc_s.redraw_count = 0; tc_s.mouse_count = 0; + delegate_s = mk_task(wuss, flood_full_bounds_handle, &s_colour); + if (delegate_s == NULL) goto Failure; + + box_s.x0 = 60; box_s.y0 = 60; + box_s.x1 = 140; box_s.y1 = 140; + rc = wuss_window_create(delegate_s, &box_s, "S", wuss_WINDOW_DEFAULT, + wuss_NO_BACKDROP, + SIZE2D(80, 80), SIZE2D(0, 0), &win_s); + if (rc != result_OK) + goto Failure; + rc = wuss_redraw_dirty(wuss); /* flush the create */ + if (rc != result_OK) + goto Failure; + + rc = wuss_window_resize(win_s, SIZE2D(30, 30)); + if (rc != result_OK) + goto Failure; + rc = wuss_redraw_dirty(wuss); + if (rc != result_OK) + goto Failure; + + wuss_window_set_scroll(win_s, POINT(20, 20)); + rc = wuss_redraw_dirty(wuss); + if (rc != result_OK) + goto Failure; + + /* mirrors saturn_idle: invalidate the whole visible rect with no + * explicit local_box, exactly what a task's idle handler calls every + * frame -- inspect the queued dirty rect directly rather than via + * pixels, since every window (including S itself) re-clips to its own + * content box on redraw regardless of what the dirty list says, so a + * stray dirty rect only ever shows up as a wasted backdrop repaint, not + * as a colour mismatch. */ + wuss_window_invalidate_visible(win_s); + if (wuss_get_dirty_count(wuss) != 1) + goto Failure; + wuss_get_dirty(wuss, 0, &dirty); + if (!box_contains_box(&dirty, &win_s->visible)) + goto Failure; /* dirty rect strayed outside S's own visible box */ + + rc = wuss_redraw_dirty(wuss); + if (rc != result_OK) + goto Failure; + + wuss_window_close(win_s); + } + printf("test: wuss_WINDOW_NO_RESIZE_BLIT redraws the whole window instead of blitting\n"); { - static test_task_t tc_nb; - wuss_task_t *delegate_nb; - box_t box_nb, before, titlebar, toggle; - wuss_window_t *win_nb; - int outline_px, titlebar_height, inset, icon; - int i, cx, cy, interior_x, interior_y, interior_dirty; + static test_task_t tc_nb; + wuss_task_t *delegate_nb; + box_t box_nb, before, titlebar, toggle; + wuss_window_t *win_nb; + int outline_px, titlebar_height, inset, icon; + int i, cx, cy, interior_x, interior_y, interior_dirty; tc_nb.redraw_count = 0; tc_nb.mouse_count = 0; @@ -2285,8 +2955,9 @@ result_t wuss_test(const char *resources) box_nb.x0 = 10; box_nb.y0 = 10; box_nb.x1 = 50; box_nb.y1 = 50; /* 40x40 content, room to grow to a 200x200 doc */ - rc = wuss_window_create(delegate_nb, &box_nb, "NB", wuss_WINDOW_NO_RESIZE_BLIT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + rc = wuss_window_create(delegate_nb, &box_nb, "NB", + wuss_WINDOW_DEFAULT | wuss_WINDOW_NO_RESIZE_BLIT, + wuss_NO_BACKDROP, SIZE2D(200, 200), SIZE2D(0, 0), &win_nb); if (rc != result_OK) goto Failure; @@ -2350,12 +3021,12 @@ result_t wuss_test(const char *resources) printf("test: toggle-size maximize accounts for scrollbar furniture, not just outline/titlebar\n"); { - static test_task_t tc_u; - wuss_task_t *delegate_u; - box_t box_u, ub, titlebar, toggle; - wuss_window_t *win_u; - int outline_px, titlebar_height, inset, icon; - int cx, cy; + static test_task_t tc_u; + wuss_task_t *delegate_u; + box_t box_u, ub, titlebar, toggle; + wuss_window_t *win_u; + int outline_px, titlebar_height, inset, icon; + int cx, cy; tc_u.redraw_count = 0; tc_u.mouse_count = 0; @@ -2365,7 +3036,7 @@ result_t wuss_test(const char *resources) box_u.x0 = 80; box_u.y0 = 80; box_u.x1 = 120; box_u.y1 = 120; /* 40x40 content */ rc = wuss_window_create(mk_task(wuss, NULL, NULL), &box_u, "U", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(70, 70), SIZE2D(0, 0), &win_u); /* doc size well within the 200x200 screen: growth is doc-limited, not screen-limited */ if (rc != result_OK) goto Failure; @@ -2448,12 +3119,12 @@ result_t wuss_test(const char *resources) printf("test: toggle-size maximize stays a valid box for a window dragged off the right/bottom edge\n"); { - static test_task_t tc_v; - wuss_task_t *delegate_v; - box_t box_v, vb, titlebar, toggle; - wuss_window_t *win_v; - int outline_px, titlebar_height, inset, icon; - int cx, cy; + static test_task_t tc_v; + wuss_task_t *delegate_v; + box_t box_v, vb, titlebar, toggle; + wuss_window_t *win_v; + int outline_px, titlebar_height, inset, icon; + int cx, cy; memset(&tc_v, 0, sizeof(tc_v)); tc_v.veto_pre_close = 1; /* this test pokes furniture with a window in a @@ -2475,7 +3146,7 @@ result_t wuss_test(const char *resources) * Doc big enough that maximize is * screen-limited, not doc-limited. */ rc = wuss_window_create(delegate_v, &box_v, "V", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(200, 200), SIZE2D(0, 0), &win_v); if (rc != result_OK) goto Failure; @@ -2590,7 +3261,7 @@ result_t wuss_test(const char *resources) * doc big enough that maximize is * screen-limited so the window must move */ rc = wuss_window_create(delegate_p, &box_p, "P", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(400, 400), SIZE2D(0, 0), &win_p); if (rc != result_OK) goto Failure; @@ -2727,7 +3398,7 @@ result_t wuss_test(const char *resources) box_z.x0 = 20; box_z.y0 = 20; box_z.x1 = 60; box_z.y1 = 60; /* 40x40 content */ rc = wuss_window_create(delegate_z, &box_z, "Z", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, axis == 0 ? SIZE2D(0, 0) : SIZE2D(60, 0), SIZE2D(0, 0), &win_z); if (rc != result_OK) @@ -2804,11 +3475,11 @@ result_t wuss_test(const char *resources) printf("test: dragging a back-most window with nothing above it still blits\n"); { - static test_task_t tc_k, tc_l; - wuss_task_t *delegate_k, *delegate_l; - box_t box_k, box_l; - wuss_window_t *win_k, *win_l; - int before_k, before_l; + static test_task_t tc_k, tc_l; + wuss_task_t *delegate_k, *delegate_l; + box_t box_k, box_l; + wuss_window_t *win_k, *win_l; + int before_k, before_l; tc_k.redraw_count = 0; tc_k.mouse_count = 0; @@ -2820,10 +3491,10 @@ result_t wuss_test(const char *resources) rc = wuss_window_create(delegate_k, &box_k, "K", - wuss_WINDOW_NO_BACK | wuss_WINDOW_NO_TOGGLE_SIZE | - wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | - wuss_WINDOW_NO_RESIZE, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_WINDOW_DEFAULT & ~wuss_WINDOW_BACK & + ~wuss_WINDOW_TOGGLE_SIZE & ~wuss_WINDOW_VSCROLL & + ~wuss_WINDOW_HSCROLL & ~wuss_WINDOW_RESIZE, + wuss_NO_BACKDROP, box_size(&box_k), SIZE2D(0, 0), &win_k); @@ -2840,10 +3511,10 @@ result_t wuss_test(const char *resources) rc = wuss_window_create(delegate_l, &box_l, "L", - wuss_WINDOW_NO_BACK | wuss_WINDOW_NO_TOGGLE_SIZE | - wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | - wuss_WINDOW_NO_RESIZE, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_WINDOW_DEFAULT & ~wuss_WINDOW_BACK & + ~wuss_WINDOW_TOGGLE_SIZE & ~wuss_WINDOW_VSCROLL & + ~wuss_WINDOW_HSCROLL & ~wuss_WINDOW_RESIZE, + wuss_NO_BACKDROP, box_size(&box_l), SIZE2D(0, 0), &win_l); @@ -2891,11 +3562,11 @@ result_t wuss_test(const char *resources) printf("test: resizing a window only invalidates the grown/shrunk sliver\n"); { - static test_task_t tc_m2; - wuss_task_t *delegate_m2; - box_t box_m2, before2, after2, region; - wuss_window_t *win_m2; - int i, dirty_area, full_area, interior_x, interior_y, interior_dirty; + static test_task_t tc_m2; + wuss_task_t *delegate_m2; + box_t box_m2, before2, after2, region; + wuss_window_t *win_m2; + int i, dirty_area, full_area, interior_x, interior_y, interior_dirty; tc_m2.redraw_count = 0; tc_m2.mouse_count = 0; @@ -2907,9 +3578,10 @@ result_t wuss_test(const char *resources) rc = wuss_window_create(delegate_m2, &box_m2, "M2", - wuss_WINDOW_NO_BACK | wuss_WINDOW_NO_TOGGLE_SIZE | - wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_WINDOW_DEFAULT & ~wuss_WINDOW_BACK & + ~wuss_WINDOW_TOGGLE_SIZE & ~wuss_WINDOW_VSCROLL & + ~wuss_WINDOW_HSCROLL, + wuss_NO_BACKDROP, box_size(&box_m2), SIZE2D(0, 0), &win_m2); @@ -2985,11 +3657,11 @@ result_t wuss_test(const char *resources) printf("test: resizing a wuss_WINDOW_NO_RESIZE_BLIT window redraws it fully\n"); { - static test_task_t tc_nb2; - wuss_task_t *delegate_nb2; - box_t box_nb2, before3, after3, region; - wuss_window_t *win_nb2; - int i, dirty_area, full_area; + static test_task_t tc_nb2; + wuss_task_t *delegate_nb2; + box_t box_nb2, before3, after3, region; + wuss_window_t *win_nb2; + int i, dirty_area, full_area; tc_nb2.redraw_count = 0; tc_nb2.mouse_count = 0; @@ -2998,8 +3670,9 @@ result_t wuss_test(const char *resources) box_nb2.x0 = 0; box_nb2.y0 = 0; box_nb2.x1 = 40; box_nb2.y1 = 40; - rc = wuss_window_create(delegate_nb2, &box_nb2, "NB2", wuss_WINDOW_NO_RESIZE_BLIT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + rc = wuss_window_create(delegate_nb2, &box_nb2, "NB2", + wuss_WINDOW_DEFAULT | wuss_WINDOW_NO_RESIZE_BLIT, + wuss_NO_BACKDROP, box_size(&box_nb2), SIZE2D(0, 0), &win_nb2); @@ -3034,11 +3707,11 @@ result_t wuss_test(const char *resources) "furniture layout so the chrome redraws at the new width\n"); { - static test_task_t tc_fl; - wuss_task_t *delegate_fl; - box_t box_fl, after_fl; - wuss_window_t *win_fl; - int i, old_titlebar_x1, found; + static test_task_t tc_fl; + wuss_task_t *delegate_fl; + box_t box_fl, after_fl; + wuss_window_t *win_fl; + int i, old_titlebar_x1, found; tc_fl.redraw_count = 0; tc_fl.mouse_count = 0; @@ -3047,8 +3720,9 @@ result_t wuss_test(const char *resources) box_fl.x0 = 0; box_fl.y0 = 0; box_fl.x1 = 60; box_fl.y1 = 60; - rc = wuss_window_create(delegate_fl, &box_fl, "FL", wuss_WINDOW_NO_RESIZE_BLIT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + rc = wuss_window_create(delegate_fl, &box_fl, "FL", + wuss_WINDOW_DEFAULT | wuss_WINDOW_NO_RESIZE_BLIT, + wuss_NO_BACKDROP, box_size(&box_fl), SIZE2D(0, 0), &win_fl); @@ -3096,11 +3770,11 @@ result_t wuss_test(const char *resources) printf("test: dragging a clear window onto an occluder leaves the occluder untouched\n"); { - static test_task_t tc_n, tc_o; - wuss_task_t *delegate_n, *delegate_o; - box_t box_n, box_o, visible_o, exposed, occluded, region; - wuss_window_t *win_n, *win_o; - int i, exposed_dirty, occluded_dirty; + static test_task_t tc_n, tc_o; + wuss_task_t *delegate_n, *delegate_o; + box_t box_n, box_o, visible_o, exposed, occluded, region; + wuss_window_t *win_n, *win_o; + int i, exposed_dirty, occluded_dirty; tc_n.redraw_count = 0; tc_n.mouse_count = 0; @@ -3110,10 +3784,10 @@ result_t wuss_test(const char *resources) box_n.x0 = 0; box_n.y0 = 140; /* clear of any occluder to start */ box_n.x1 = 60; box_n.y1 = 170; rc = wuss_window_create(delegate_n, &box_n, "N", - wuss_WINDOW_NO_BACK | wuss_WINDOW_NO_TOGGLE_SIZE | - wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | - wuss_WINDOW_NO_RESIZE, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_WINDOW_DEFAULT & ~wuss_WINDOW_BACK & + ~wuss_WINDOW_TOGGLE_SIZE & ~wuss_WINDOW_VSCROLL & + ~wuss_WINDOW_HSCROLL & ~wuss_WINDOW_RESIZE, + wuss_NO_BACKDROP, box_size(&box_n), SIZE2D(0, 0), &win_n); @@ -3128,10 +3802,10 @@ result_t wuss_test(const char *resources) box_o.x0 = 90; box_o.y0 = 140; /* N will be dragged partly on top of O */ box_o.x1 = 130; box_o.y1 = 180; rc = wuss_window_create(delegate_o, &box_o, "O", - wuss_WINDOW_NO_BACK | wuss_WINDOW_NO_TOGGLE_SIZE | - wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | - wuss_WINDOW_NO_RESIZE, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_WINDOW_DEFAULT & ~wuss_WINDOW_BACK & + ~wuss_WINDOW_TOGGLE_SIZE & ~wuss_WINDOW_VSCROLL & + ~wuss_WINDOW_HSCROLL & ~wuss_WINDOW_RESIZE, + wuss_NO_BACKDROP, box_size(&box_o), SIZE2D(0, 0), &win_o); @@ -3197,12 +3871,12 @@ result_t wuss_test(const char *resources) printf("test: moving a partly-occluded window blits its clean part and only repaints the occluded part\n"); { - static test_task_t tc_a, tc_b; - wuss_task_t *delegate_a, *delegate_b; - box_t box_a, box_b, visible_a, visible_b_before; - box_t clean_new, hidden_new, region; - wuss_window_t *win_a, *win_b; - int i, dx, clean_dirty, hidden_dirty; + static test_task_t tc_a, tc_b; + wuss_task_t *delegate_a, *delegate_b; + box_t box_a, box_b, visible_a, visible_b_before; + box_t clean_new, hidden_new, region; + wuss_window_t *win_a, *win_b; + int i, dx, clean_dirty, hidden_dirty; tc_b.redraw_count = 0; tc_b.mouse_count = 0; @@ -3212,10 +3886,9 @@ result_t wuss_test(const char *resources) box_b.x0 = 20; box_b.y0 = 10; /* left half will sit under A */ box_b.x1 = 80; box_b.y1 = 50; rc = wuss_window_create(delegate_b, &box_b, "B", - wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | - wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | - wuss_WINDOW_NO_RESIZE, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + /* fully chromeless: no titlebar, outline, scrollbars or resize */ + wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE, + wuss_NO_BACKDROP, box_size(&box_b), SIZE2D(0, 0), &win_b); @@ -3230,10 +3903,9 @@ result_t wuss_test(const char *resources) box_a.x0 = 0; box_a.y0 = 0; /* created after B, so A is topmost */ box_a.x1 = 40; box_a.y1 = 100; rc = wuss_window_create(delegate_a, &box_a, "A", - wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | - wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | - wuss_WINDOW_NO_RESIZE, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + /* fully chromeless: no titlebar, outline, scrollbars or resize */ + wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE, + wuss_NO_BACKDROP, box_size(&box_a), SIZE2D(0, 0), &win_a); @@ -3298,12 +3970,12 @@ result_t wuss_test(const char *resources) printf("test: moving a window whose occluded piece was never blitted doesn't redraw the occluder\n"); { - static test_task_t tc_a, tc_b; - wuss_task_t *delegate_a, *delegate_b; - box_t box_a, box_b, visible_a, visible_b_before; - box_t region; - wuss_window_t *win_a, *win_b; - int i, occluder_dirty; + static test_task_t tc_a, tc_b; + wuss_task_t *delegate_a, *delegate_b; + box_t box_a, box_b, visible_a, visible_b_before; + box_t region; + wuss_window_t *win_a, *win_b; + int i, occluder_dirty; tc_b.redraw_count = 0; tc_b.mouse_count = 0; @@ -3313,10 +3985,9 @@ result_t wuss_test(const char *resources) box_b.x0 = 0; box_b.y0 = 0; /* right part sits under A throughout */ box_b.x1 = 60; box_b.y1 = 40; rc = wuss_window_create(delegate_b, &box_b, "B", - wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | - wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | - wuss_WINDOW_NO_RESIZE, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + /* fully chromeless: no titlebar, outline, scrollbars or resize */ + wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE, + wuss_NO_BACKDROP, box_size(&box_b), SIZE2D(0, 0), &win_b); @@ -3331,10 +4002,9 @@ result_t wuss_test(const char *resources) box_a.x0 = 40; box_a.y0 = 0; /* created after B, so A is topmost */ box_a.x1 = 100; box_a.y1 = 40; rc = wuss_window_create(delegate_a, &box_a, "A", - wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | - wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | - wuss_WINDOW_NO_RESIZE, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + /* fully chromeless: no titlebar, outline, scrollbars or resize */ + wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE, + wuss_NO_BACKDROP, box_size(&box_a), SIZE2D(0, 0), &win_a); @@ -3376,12 +4046,12 @@ result_t wuss_test(const char *resources) printf("test: moving a window split by a mid-band occluder past the gap between bands blits both bands in a safe order\n"); { - static test_task_t tc_a, tc_b; - wuss_task_t *delegate_a, *delegate_b; - box_t box_a, box_b, visible_b_before; - box_t occluded_overlap, hidden_new, region; - wuss_window_t *win_a, *win_b; - int i, occluded_dirty, hidden_dirty; + static test_task_t tc_a, tc_b; + wuss_task_t *delegate_a, *delegate_b; + box_t box_a, box_b, visible_b_before; + box_t occluded_overlap, hidden_new, region; + wuss_window_t *win_a, *win_b; + int i, occluded_dirty, hidden_dirty; tc_b.redraw_count = 0; tc_b.mouse_count = 0; @@ -3391,10 +4061,9 @@ result_t wuss_test(const char *resources) box_b.x0 = 0; box_b.y0 = 0; /* middle band sits under A */ box_b.x1 = 60; box_b.y1 = 60; rc = wuss_window_create(delegate_b, &box_b, "B", - wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | - wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | - wuss_WINDOW_NO_RESIZE, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + /* fully chromeless: no titlebar, outline, scrollbars or resize */ + wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE, + wuss_NO_BACKDROP, box_size(&box_b), SIZE2D(0, 0), &win_b); @@ -3409,10 +4078,9 @@ result_t wuss_test(const char *resources) box_a.x0 = 0; box_a.y0 = 20; /* created after B, so A is topmost */ box_a.x1 = 60; box_a.y1 = 40; rc = wuss_window_create(delegate_a, &box_a, "A", - wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | - wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | - wuss_WINDOW_NO_RESIZE, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + /* fully chromeless: no titlebar, outline, scrollbars or resize */ + wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE, + wuss_NO_BACKDROP, box_size(&box_a), SIZE2D(0, 0), &win_a); @@ -3488,11 +4156,11 @@ result_t wuss_test(const char *resources) printf("test: dragging a window deeper under a corner occluder blits both L-shaped pieces in a safe order\n"); { - static test_task_t tc_a, tc_b; - wuss_task_t *delegate_a, *delegate_b; - box_t box_a, box_b, visible_b_before, visible_a, region; - wuss_window_t *win_a, *win_b; - int i; + static test_task_t tc_a, tc_b; + wuss_task_t *delegate_a, *delegate_b; + box_t box_a, box_b, visible_b_before, visible_a, region; + wuss_window_t *win_a, *win_b; + int i; tc_b.redraw_count = 0; tc_b.mouse_count = 0; @@ -3502,10 +4170,9 @@ result_t wuss_test(const char *resources) box_b.x0 = 80; box_b.y0 = 80; /* corner already under A */ box_b.x1 = 140; box_b.y1 = 140; rc = wuss_window_create(delegate_b, &box_b, "B", - wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | - wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | - wuss_WINDOW_NO_RESIZE, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + /* fully chromeless: no titlebar, outline, scrollbars or resize */ + wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE, + wuss_NO_BACKDROP, box_size(&box_b), SIZE2D(0, 0), &win_b); @@ -3520,10 +4187,9 @@ result_t wuss_test(const char *resources) box_a.x0 = 0; box_a.y0 = 0; /* created after B, so A is topmost */ box_a.x1 = 100; box_a.y1 = 100; rc = wuss_window_create(delegate_a, &box_a, "A", - wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | - wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | - wuss_WINDOW_NO_RESIZE, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + /* fully chromeless: no titlebar, outline, scrollbars or resize */ + wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE, + wuss_NO_BACKDROP, box_size(&box_a), SIZE2D(0, 0), &win_a); @@ -3554,8 +4220,8 @@ result_t wuss_test(const char *resources) /* The blit must have actually happened, not fallen back: B's own * footprint (outside A) must not be dirtied wholesale. */ { - box_t visible_b_after, whole_footprint, dirty_area_box; - int dirty_area, footprint_area; + box_t visible_b_after, whole_footprint, dirty_area_box; + int dirty_area, footprint_area; wuss_window_get_visible_bounds(win_b, &visible_b_after); box_union(&visible_b_before, &visible_b_after, &whole_footprint); @@ -3596,10 +4262,10 @@ result_t wuss_test(const char *resources) rc = wuss_window_create(delegate_c, &box_c, "C", - wuss_WINDOW_NO_BACK | wuss_WINDOW_NO_TOGGLE_SIZE | - wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL | - wuss_WINDOW_NO_RESIZE, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_WINDOW_DEFAULT & ~wuss_WINDOW_BACK & + ~wuss_WINDOW_TOGGLE_SIZE & ~wuss_WINDOW_VSCROLL & + ~wuss_WINDOW_HSCROLL & ~wuss_WINDOW_RESIZE, + wuss_NO_BACKDROP, box_size(&box_c), SIZE2D(0, 0), &win_c); @@ -3621,11 +4287,11 @@ result_t wuss_test(const char *resources) printf("test: mouse and scroll events arrive in virtual content space, with the scroll offset applied exactly once\n"); { - static test_task_t tc_s = { 0 }; - wuss_task_t *delegate_s; - box_t box_s, content_s; - wuss_window_t *win_s; - point_t scroll; + static test_task_t tc_s = { 0 }; + wuss_task_t *delegate_s; + box_t box_s, content_s; + wuss_window_t *win_s; + point_t scroll; delegate_s = mk_task(wuss, test_handle, &tc_s); if (delegate_s == NULL) goto Failure; @@ -3633,7 +4299,7 @@ result_t wuss_test(const char *resources) box_s.x0 = 10; box_s.y0 = 10; box_s.x1 = 60; box_s.y1 = 60; /* 50x50 content onto a 200x200 doc: room to scroll */ rc = wuss_window_create(delegate_s, &box_s, "S", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(200, 200), SIZE2D(0, 0), &win_s); if (rc != result_OK) goto Failure; @@ -3691,10 +4357,10 @@ result_t wuss_test(const char *resources) * content from them -- is added outside the requested content box, never * carved out of it, so what the caller asks for is what it gets, both at * creation and after a resize. */ - static test_task_t tc_r; - wuss_task_t *delegate_r; - box_t box_r, content_r; - wuss_window_t *win_r; + static test_task_t tc_r; + wuss_task_t *delegate_r; + box_t box_r, content_r; + wuss_window_t *win_r; memset(&tc_r, 0, sizeof(tc_r)); delegate_r = mk_task(wuss, test_handle, &tc_r); @@ -3708,7 +4374,7 @@ result_t wuss_test(const char *resources) &box_r, "rules", wuss_WINDOW_DEFAULT, /* all furniture present */ - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(400, 400), SIZE2D(0, 0), &win_r); @@ -3736,11 +4402,11 @@ result_t wuss_test(const char *resources) { /* Windows created without a position are packed towards the top-left and * must not overlap; closing one frees its slot for the next create. */ - static test_task_t tc_p[4]; - wuss_task_t *delegate_p; - wuss_window_t *win_p[4]; - box_t vis[4], probe; - int k, m; + static test_task_t tc_p[4]; + wuss_task_t *delegate_p; + wuss_window_t *win_p[4]; + box_t vis[4], probe; + int k, m; memset(tc_p, 0, sizeof(tc_p)); delegate_p = mk_task(wuss, test_handle, &tc_p[0]); @@ -3752,7 +4418,7 @@ result_t wuss_test(const char *resources) SIZE2D(40, 30), "P", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(40, 30), SIZE2D(0, 0), &win_p[k]); @@ -3775,7 +4441,7 @@ result_t wuss_test(const char *resources) SIZE2D(40, 30), "P", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(40, 30), SIZE2D(0, 0), &win_p[1]); @@ -3803,8 +4469,8 @@ result_t wuss_test(const char *resources) static const colour_t too_long[3] = { { 0xFF000000 }, { 0xFF808080 }, { 0xFFFFFFFF } }; - static test_task_t tc_pa, tc_pb; - wuss_task_t *delegate_pa, *delegate_pb; + static test_task_t tc_pa, tc_pb; + wuss_task_t *delegate_pa, *delegate_pb; /* every earlier block-local test_task_t is out of scope now; drop the * tasks that still point at it so the broadcast counts are exactly the @@ -3845,10 +4511,10 @@ result_t wuss_test(const char *resources) printf("test: wuss_window_set_hidden fires PRE_SHOW; a veto keeps the window hidden\n"); { - static test_task_t tc_ps; - wuss_task_t *delegate_ps; - box_t box_ps; - wuss_window_t *win_ps; + static test_task_t tc_ps; + wuss_task_t *delegate_ps; + box_t box_ps; + wuss_window_t *win_ps; memset(&tc_ps, 0, sizeof(tc_ps)); delegate_ps = mk_task(wuss, test_handle, &tc_ps); @@ -3858,7 +4524,7 @@ result_t wuss_test(const char *resources) box_ps.x0 = 0; box_ps.y0 = 0; box_ps.x1 = 60; box_ps.y1 = 60; rc = wuss_window_create(delegate_ps, &box_ps, "PS", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, box_size(&box_ps), SIZE2D(0, 0), &win_ps); if (rc != result_OK) goto Failure; @@ -3910,10 +4576,10 @@ result_t wuss_test(const char *resources) printf("test: wuss_task_destroy sends one wuss_EVENT_QUIT and closes the task's windows\n"); { - static test_task_t tc_q; - wuss_task_t *delegate_q; - box_t box_q; - wuss_window_t *win_q1, *win_q2; + static test_task_t tc_q; + wuss_task_t *delegate_q; + box_t box_q; + wuss_window_t *win_q1, *win_q2; memset(&tc_q, 0, sizeof(tc_q)); delegate_q = mk_task(wuss, test_handle, &tc_q); @@ -3922,12 +4588,12 @@ result_t wuss_test(const char *resources) box_q.x0 = 0; box_q.y0 = 0; box_q.x1 = 40; box_q.y1 = 40; rc = wuss_window_create(delegate_q, &box_q, "Q1", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, box_size(&box_q), SIZE2D(0, 0), &win_q1); if (rc != result_OK) goto Failure; rc = wuss_window_create(delegate_q, &box_q, "Q2", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, box_size(&box_q), SIZE2D(0, 0), &win_q2); if (rc != result_OK) goto Failure; @@ -3945,9 +4611,9 @@ result_t wuss_test(const char *resources) printf("test: an autoclose task self-destructs when its last window closes and stops receiving broadcasts\n"); { static test_task_t tc_ac; - wuss_task_t *delegate_ac; - box_t box_ac; - wuss_window_t *win_ac1, *win_ac2; + wuss_task_t *delegate_ac; + box_t box_ac; + wuss_window_t *win_ac1, *win_ac2; memset(&tc_ac, 0, sizeof(tc_ac)); delegate_ac = mk_task(wuss, test_handle, &tc_ac); @@ -3957,12 +4623,12 @@ result_t wuss_test(const char *resources) box_ac.x0 = 0; box_ac.y0 = 0; box_ac.x1 = 40; box_ac.y1 = 40; rc = wuss_window_create(delegate_ac, &box_ac, "AC1", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, box_size(&box_ac), SIZE2D(0, 0), &win_ac1); if (rc != result_OK) goto Failure; rc = wuss_window_create(delegate_ac, &box_ac, "AC2", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, box_size(&box_ac), SIZE2D(0, 0), &win_ac2); if (rc != result_OK) goto Failure; @@ -3995,9 +4661,9 @@ result_t wuss_test(const char *resources) printf("test: a task reaping itself from inside wuss_idle does not derail the task walk\n"); { static test_task_t tc_w1, tc_w2, tc_w3; - wuss_task_t *delegate_w1, *delegate_w2, *delegate_w3; - box_t box_w; - wuss_window_t *win_w1, *win_w3; + wuss_task_t *delegate_w1, *delegate_w2, *delegate_w3; + box_t box_w; + wuss_window_t *win_w1, *win_w3; memset(&tc_w1, 0, sizeof(tc_w1)); memset(&tc_w2, 0, sizeof(tc_w2)); @@ -4013,7 +4679,7 @@ result_t wuss_test(const char *resources) if (delegate_w1 == NULL) goto Failure; wuss_task_set_autoclose(delegate_w1, 1); rc = wuss_window_create(delegate_w1, &box_w, "W1", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, box_size(&box_w), SIZE2D(0, 0), &win_w1); if (rc != result_OK) goto Failure; @@ -4022,7 +4688,7 @@ result_t wuss_test(const char *resources) if (delegate_w2 == NULL) goto Failure; wuss_task_set_autoclose(delegate_w2, 1); rc = wuss_window_create(delegate_w2, &box_w, "W2", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, box_size(&box_w), SIZE2D(0, 0), &g_close_on_idle_win); if (rc != result_OK) @@ -4032,7 +4698,7 @@ result_t wuss_test(const char *resources) if (delegate_w3 == NULL) goto Failure; wuss_task_set_autoclose(delegate_w3, 1); rc = wuss_window_create(delegate_w3, &box_w, "W3", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, box_size(&box_w), SIZE2D(0, 0), &win_w3); if (rc != result_OK) goto Failure; @@ -4071,16 +4737,19 @@ result_t wuss_test(const char *resources) { "Help", borrowed_items, NELEMS(borrowed_items) }; - wuss_menu_t *m; + + wuss_menu_t *m; const wuss_menu_t *sub; /* "Root" is the root caption (first token), then items App, * File{ %s title discarded, Grid(!), Export(~), |Quit }, Help(> borrowed), - * with "File" substituted for both %s. '|Quit' sets DASHED on Quit itself, - * so the submenu holds 3 rows. */ + * with "File" substituted for both %s. '!'/'~' now pull an int vararg + * (nonzero applies the flag) rather than always applying, so Grid's tick + * and Export's shade are passed explicitly. '|Quit' sets DASHED on Quit + * itself, so the submenu holds 3 rows. */ rc = wuss_menu_create_from_desc(&m, "Root, App, %s { %s, !Grid, ~Export, |Quit }, >Help", - "File", "File", &borrowed); + "File", "File", 1, 1, &borrowed); if (rc != result_OK) goto Failure; @@ -4110,11 +4779,28 @@ result_t wuss_test(const char *resources) wuss_menu_destroy(m); + /* same desc, both bools false: '!'/'~' apply nothing */ + rc = wuss_menu_create_from_desc(&m, + "Root, App, %s { %s, !Grid, ~Export, |Quit }, >Help", + "File", "File", 0, 0, &borrowed); + if (rc != result_OK) + goto Failure; + sub = m->items[1].submenu; + if (sub == NULL) goto MenuFail; + if (sub->items[0].flags & wuss_MENU_ITEM_TICKED) goto MenuFail; + if (sub->items[1].flags & wuss_MENU_ITEM_DISABLED) goto MenuFail; + wuss_menu_destroy(m); + /* malformed: unbalanced brace */ rc = wuss_menu_create_from_desc(&m, "T, A { B, C"); if (rc != result_BAD_ARG) goto MenuFail; + /* malformed: '!' vararg not 0/1 */ + rc = wuss_menu_create_from_desc(&m, "T, !Grid", 2); + if (rc != result_BAD_ARG) + goto MenuFail; + rc = result_OK; goto MenuOK; @@ -4179,10 +4865,10 @@ MenuOK: ; /* a SYSTEM-class wuss font is dropped from the menu; NONE-class and * unnamed slots are not */ { - const char *sysfontfile; - bmfont_t *sysfont; - wuss_font_desc_t sysdescs[2]; - wuss_t *syswuss; + const char *sysfontfile; + bmfont_t *sysfont; + wuss_font_desc_t sysdescs[2]; + wuss_t *syswuss; sysfontfile = path_join_filename(resources, 3, "resources", "bmfonts", path_join_leafname("Symbols", "png")); @@ -4196,7 +4882,7 @@ MenuOK: ; sysdescs[1].font_class = wuss_FONT_CLASS_NONE; sysdescs[1].name = "Tiny"; /* NONE class: not skipped though named */ - rc = wuss_create(&scr, sysdescs, 2, NULL, 0, NULL, NULL, &syswuss); + rc = wuss_create(&scr, sysdescs, 2, NULL, 0, NULL, NULL, NULL, &syswuss); if (rc != result_OK) { bmfont_destroy(sysfont); @@ -4318,15 +5004,15 @@ ColourMenuOK: ; "Pick", flash_items, NELEMS(flash_items) }; - const char *fontfile; - bmfont_t *font = NULL; - wuss_font_desc_t fdesc; - screen_t fscr; - bitmap_t fbm; - void *fpixels; - wuss_t *fwuss; - test_task_t ftc; - wuss_task_t *fowner; + const char *fontfile; + bmfont_t *font = NULL; + wuss_font_desc_t fdesc; + screen_t fscr; + bitmap_t fbm; + void *fpixels; + wuss_t *fwuss; + test_task_t ftc; + wuss_task_t *fowner; struct wuss__menu *chain; int i; @@ -4351,7 +5037,7 @@ ColourMenuOK: ; fdesc.font = font; fdesc.font_class = wuss_FONT_CLASS_NONE; fdesc.name = NULL; - rc = wuss_create(&fscr, &fdesc, 1, NULL, 0, NULL, NULL, &fwuss); + rc = wuss_create(&fscr, &fdesc, 1, NULL, 0, NULL, NULL, NULL, &fwuss); if (rc != result_OK) goto FlashFailFree; memset(&ftc, 0, sizeof(ftc)); @@ -4451,15 +5137,15 @@ ColourMenuOK: ; "Root", sm_items, NELEMS(sm_items) }; - const char *fontfile; - bmfont_t *font = NULL; - wuss_font_desc_t fdesc; - screen_t sscr; - bitmap_t sbm; - void *spixels; - wuss_t *swuss; - test_task_t stc; - wuss_task_t *sowner; + const char *fontfile; + bmfont_t *font = NULL; + wuss_font_desc_t fdesc; + screen_t sscr; + bitmap_t sbm; + void *spixels; + wuss_t *swuss; + test_task_t stc; + wuss_task_t *sowner; struct wuss__menu *root; struct wuss__menu *sub; @@ -4482,7 +5168,7 @@ ColourMenuOK: ; fdesc.font = font; fdesc.font_class = wuss_FONT_CLASS_NONE; fdesc.name = NULL; - rc = wuss_create(&sscr, &fdesc, 1, NULL, 0, NULL, NULL, &swuss); + rc = wuss_create(&sscr, &fdesc, 1, NULL, 0, NULL, NULL, NULL, &swuss); if (rc != result_OK) goto SubHiFailFree; memset(&stc, 0, sizeof(stc)); @@ -4572,17 +5258,17 @@ ColourMenuOK: ; static const wuss_menu_t menu_a = { "A", menu_a_items, NELEMS(menu_a_items) }; static const wuss_menu_t menu_b = { "B", menu_b_items, NELEMS(menu_b_items) }; - const char *fontfile; - bmfont_t *font = NULL; - wuss_font_desc_t fdesc; - screen_t mscr; - bitmap_t mbm; - void *mpixels; - wuss_t *mwuss; - menu_task_t mta, mtb; - wuss_task_t *task_a, *task_b; - wuss_window_t *wa, *wb; - box_t ba, bb; + const char *fontfile; + bmfont_t *font = NULL; + wuss_font_desc_t fdesc; + screen_t mscr; + bitmap_t mbm; + void *mpixels; + wuss_t *mwuss; + menu_task_t mta, mtb; + wuss_task_t *task_a, *task_b; + wuss_window_t *wa, *wb; + box_t ba, bb; fontfile = path_join_filename(resources, 3, "resources", "bmfonts", path_join_leafname("Tiny", "png")); @@ -4603,7 +5289,7 @@ ColourMenuOK: ; fdesc.font = font; fdesc.font_class = wuss_FONT_CLASS_NONE; fdesc.name = NULL; - rc = wuss_create(&mscr, &fdesc, 1, NULL, 0, NULL, NULL, &mwuss); + rc = wuss_create(&mscr, &fdesc, 1, NULL, 0, NULL, NULL, NULL, &mwuss); if (rc != result_OK) goto MoveFailFree; memset(&mta, 0, sizeof(mta)); @@ -4624,20 +5310,16 @@ ColourMenuOK: ; bb.x0 = 130; bb.y0 = 130; bb.x1 = 190; bb.y1 = 190; rc = wuss_window_create(task_a, &ba, "A", - wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | - wuss_WINDOW_NO_CLOSE | wuss_WINDOW_NO_BACK | - wuss_WINDOW_NO_TOGGLE_SIZE | wuss_WINDOW_NO_VSCROLL | - wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + /* fully chromeless: no titlebar, outline, scrollbars or resize */ + wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE, + wuss_NO_BACKDROP, box_size(&ba), SIZE2D(0, 0), &wa); if (rc != result_OK) goto MoveDestroy; rc = wuss_window_create(task_b, &bb, "B", - 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), + /* fully chromeless: no titlebar, outline, scrollbars or resize */ + wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE, + wuss_NO_BACKDROP, box_size(&bb), SIZE2D(0, 0), &wb); if (rc != result_OK) goto MoveDestroy; @@ -4745,7 +5427,7 @@ ColourMenuOK: ; fdesc.font = font; fdesc.font_class = wuss_FONT_CLASS_NONE; fdesc.name = NULL; - rc = wuss_create(&qscr, &fdesc, 1, NULL, 0, NULL, NULL, &qwuss); + rc = wuss_create(&qscr, &fdesc, 1, NULL, 0, NULL, NULL, NULL, &qwuss); if (rc != result_OK) goto QuitFreeOnly; memset(&qmt, 0, sizeof(qmt)); @@ -4762,11 +5444,9 @@ ColourMenuOK: ; bq.x0 = 6; bq.y0 = 6; bq.x1 = 60; bq.y1 = 60; rc = wuss_window_create(task_q, &bq, "Q", - wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | - wuss_WINDOW_NO_CLOSE | wuss_WINDOW_NO_BACK | - wuss_WINDOW_NO_TOGGLE_SIZE | wuss_WINDOW_NO_VSCROLL | - wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + /* fully chromeless: no titlebar, outline, scrollbars or resize */ + wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE, + wuss_NO_BACKDROP, box_size(&bq), SIZE2D(0, 0), &wq); if (rc != result_OK) goto QuitDestroy; NOT_USED(wq); @@ -4799,11 +5479,9 @@ ColourMenuOK: ; if (task_q == NULL) { rc = result_OOM; goto QuitDestroy; } qmt.self = task_q; rc = wuss_window_create(task_q, &bq, "Q2", - wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | - wuss_WINDOW_NO_CLOSE | wuss_WINDOW_NO_BACK | - wuss_WINDOW_NO_TOGGLE_SIZE | wuss_WINDOW_NO_VSCROLL | - wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + /* fully chromeless: no titlebar, outline, scrollbars or resize */ + wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE, + wuss_NO_BACKDROP, box_size(&bq), SIZE2D(0, 0), &wq); if (rc != result_OK) goto QuitDestroy; wuss_mouse_click(qwuss, POINT(20, 20), wuss_BUTTON_MENU, @@ -4895,7 +5573,7 @@ ColourMenuOK: ; box_hs.x0 = 5; box_hs.y0 = 5; box_hs.x1 = 125; box_hs.y1 = 85; rc = wuss_window_create(delegate_hs, &box_hs, "HS", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(400, 400), SIZE2D(0, 0), &win_hs); if (rc != result_OK) goto Failure; @@ -4980,21 +5658,130 @@ ColourMenuOK: ; wuss_window_close(win_hs); } + printf("test: wuss_window_invalidate clamps a client box to the content " + "area, never dirtying the furniture around it\n"); + + { + static test_task_t tc_iv; + wuss_task_t *delegate_iv; + wuss_window_t *win_iv; + box_t box_iv, content, local, dirty; + int i, w, h; + + tc_iv.redraw_count = 0; + tc_iv.mouse_count = 0; + delegate_iv = mk_task(wuss, test_handle, &tc_iv); + if (delegate_iv == NULL) goto Failure; + + box_iv.x0 = 5; box_iv.y0 = 5; + box_iv.x1 = 125; box_iv.y1 = 85; + rc = wuss_window_create(delegate_iv, &box_iv, "IV", wuss_WINDOW_DEFAULT, + wuss_NO_BACKDROP, + SIZE2D(400, 400), SIZE2D(0, 0), &win_iv); + if (rc != result_OK) + goto Failure; + + wuss_window_get_content_bounds(win_iv, &content); + + rc = wuss_redraw_dirty(wuss); /* flush the create's own dirty region first */ + if (rc != result_OK) + goto Failure; + + /* a box like a ball's swept range at the bottom-right corner, straddling + * both the content and the furniture (titlebar/outline/scrollbars) + * around it -- content-local, so translate content's screen origin + * back off before building it. */ + w = content.x1 - content.x0; + h = content.y1 - content.y0; + local.x0 = w - 10; + local.y0 = h - 10; + local.x1 = w + 10; /* runs past content.x1 into the furniture */ + local.y1 = h + 10; /* runs past content.y1 into the furniture */ + wuss_window_invalidate(win_iv, &local); + + if (wuss_get_dirty_count(wuss) != 1) + goto Failure; + wuss_get_dirty(wuss, 0, &dirty); + if (dirty.x0 < content.x0 || dirty.y0 < content.y0 || + dirty.x1 > content.x1 || dirty.y1 > content.y1) + goto Failure; /* must be clamped to content, not spill into furniture */ + + rc = wuss_redraw_dirty(wuss); + if (rc != result_OK) + goto Failure; + + /* a box entirely outside the content (pure furniture, e.g. a client bug + * that never clamped at all) must not dirty anything. */ + before_a = tc_iv.redraw_count; + local.x0 = -50; local.y0 = -50; + local.x1 = -10; local.y1 = -10; + wuss_window_invalidate(win_iv, &local); + if (wuss_get_dirty_count(wuss) != 0) + goto Failure; + rc = wuss_redraw_dirty(wuss); + if (rc != result_OK) + goto Failure; + if (tc_iv.redraw_count != before_a) + goto Failure; + + /* sweep every furniture-adjacent edge: a content-local box straddling + * that edge must clamp to it, never crossing into visible's outline. */ + for (i = 0; i < 4; i++) + { + switch (i) + { + case 0: /* left edge */ + local.x0 = -10; local.y0 = 0; + local.x1 = 10; local.y1 = 10; + break; + case 1: /* top edge */ + local.x0 = 0; local.y0 = -10; + local.x1 = 10; local.y1 = 10; + break; + case 2: /* right edge */ + local.x0 = w - 10; local.y0 = 0; + local.x1 = w + 10; local.y1 = 10; + break; + default: /* bottom edge */ + local.x0 = 0; local.y0 = h - 10; + local.x1 = 10; local.y1 = h + 10; + break; + } + + wuss_window_invalidate(win_iv, &local); + if (wuss_get_dirty_count(wuss) > 0) + { + wuss_get_dirty(wuss, 0, &dirty); + if (dirty.x0 < content.x0 || dirty.y0 < content.y0 || + dirty.x1 > content.x1 || dirty.y1 > content.y1) + goto Failure; + } + rc = wuss_redraw_dirty(wuss); + if (rc != result_OK) + goto Failure; + } + + wuss_window_close(win_iv); + } + printf("test: furniture hit tiling holds for every furniture-flag combo\n"); { static const wuss_window_flags_t combos[] = { - wuss_WINDOW_NO_OUTLINE, - wuss_WINDOW_NO_RESIZE, - wuss_WINDOW_NO_VSCROLL | wuss_WINDOW_NO_HSCROLL, - wuss_WINDOW_NO_VSCROLL, /* one scrollbar + resize: carve is 0 on the */ - wuss_WINDOW_NO_HSCROLL, /* stripless axis, so the icon's near edge is */ - wuss_WINDOW_NO_BACK, /* the only thing sizing its hit box */ - wuss_WINDOW_NO_TOGGLE_SIZE, - wuss_WINDOW_NO_CLOSE, - wuss_WINDOW_NO_TITLEBAR + wuss_WINDOW_DEFAULT | wuss_WINDOW_NO_OUTLINE, + wuss_WINDOW_DEFAULT & ~wuss_WINDOW_RESIZE, + wuss_WINDOW_DEFAULT & ~wuss_WINDOW_VSCROLL & ~wuss_WINDOW_HSCROLL, + /* one scrollbar + resize: carve is 0 on the stripless axis, so the + * icon's near edge is the only thing sizing its hit box */ + wuss_WINDOW_DEFAULT & ~wuss_WINDOW_VSCROLL, + wuss_WINDOW_DEFAULT & ~wuss_WINDOW_HSCROLL, + wuss_WINDOW_DEFAULT & ~wuss_WINDOW_BACK, + wuss_WINDOW_DEFAULT & ~wuss_WINDOW_TOGGLE_SIZE, + wuss_WINDOW_DEFAULT & ~wuss_WINDOW_CLOSE, + wuss_WINDOW_DEFAULT | wuss_WINDOW_NO_TITLEBAR }; + static test_task_t tc_cb; wuss_task_t *delegate_cb; box_t box_cb, visible; @@ -5013,12 +5800,12 @@ ColourMenuOK: ; box_cb.x0 = 5; box_cb.y0 = 5; box_cb.x1 = 125; box_cb.y1 = 85; rc = wuss_window_create(delegate_cb, &box_cb, "CB", combos[i], - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(400, 400), SIZE2D(0, 0), &win_cb); if (rc != result_OK) goto Failure; - /* NO_TITLEBAR still keeps a 1px top frame with nothing behind it: a + /* No titlebar still keeps a 1px top frame with nothing behind it: a * click there is the documented CONTENT exception. */ leak = (combos[i] & wuss_WINDOW_NO_TITLEBAR) && !(combos[i] & wuss_WINDOW_NO_OUTLINE); @@ -5033,7 +5820,7 @@ ColourMenuOK: ; /* Where the window keeps its resize icon, the icon's own drawn centre * must hit RESIZE. The sweep alone misses a collapsed hit box: those * pixels just fall to a neighbouring region, still leak-free. */ - if (!(combos[i] & wuss_WINDOW_NO_RESIZE)) + if (combos[i] & wuss_WINDOW_RESIZE) { box_t resize; @@ -5048,7 +5835,7 @@ ColourMenuOK: ; } } - if ((combos[i] & wuss_WINDOW_NO_BACK) && + if (!(combos[i] & wuss_WINDOW_BACK) && wuss__furniture_hit_test(win_cb, POINT(box_cb.x0, box_cb.y0)) != wuss_FURNITURE_CLOSE) { wuss_window_get_visible_bounds(win_cb, &visible); @@ -5077,7 +5864,7 @@ ColourMenuOK: ; box_dv.x0 = 5; box_dv.y0 = 5; box_dv.x1 = 125; box_dv.y1 = 85; rc = wuss_window_create(delegate_dv, &box_dv, "DV", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(400, 400), SIZE2D(0, 0), &win_dv); if (rc != result_OK) goto Failure; @@ -5104,6 +5891,154 @@ ColourMenuOK: ; wuss_window_close(win_dv); } + printf("test: wuss__slider_value_to_px / wuss__slider_px_to_value convert " + "and round-trip\n"); + + { + box_t groove; + point_t pt; + + groove.x0 = 0; groove.y0 = 0; + groove.x1 = 100; groove.y1 = 20; + + /* endpoints and midpoint of a [0,100] range over a 100px groove: exact, + * no rounding needed */ + if (wuss__slider_value_to_px(&groove, wuss_SLIDER_HORIZONTAL, + 0, 0, 100) != 0) + goto Failure; + if (wuss__slider_value_to_px(&groove, wuss_SLIDER_HORIZONTAL, + 100, 0, 100) != 100) + goto Failure; + if (wuss__slider_value_to_px(&groove, wuss_SLIDER_HORIZONTAL, + 50, 0, 100) != 50) + goto Failure; + + /* a degenerate [lo,hi] span always reads back as pixel 0 */ + if (wuss__slider_value_to_px(&groove, wuss_SLIDER_HORIZONTAL, + 7, 7, 7) != 0) + goto Failure; + + /* px_to_value rounds to the nearest value rather than truncating: a 10px + * groove over [0,3] places value 1 at 3.33px, so pixel 3 (0.9 of the way + * to value 1) must round up to 1, not truncate down to 0 */ + groove.x0 = 0; groove.y0 = 0; + groove.x1 = 10; groove.y1 = 20; + + pt.x = 3; pt.y = 10; + if (wuss__slider_px_to_value(&groove, wuss_SLIDER_HORIZONTAL, + pt, 0, 3) != 1) + goto Failure; + + /* pixel 1 (0.3 of the way to value 1) is closer to 0, and must round + * down rather than up */ + pt.x = 1; pt.y = 10; + if (wuss__slider_px_to_value(&groove, wuss_SLIDER_HORIZONTAL, + pt, 0, 3) != 0) + goto Failure; + + /* an exact half-way pixel rounds up to the higher value: a 4px groove + * over [0,1] puts the 0/1 boundary at pixel 2 */ + groove.x0 = 0; groove.y0 = 0; + groove.x1 = 4; groove.y1 = 20; + + pt.x = 2; pt.y = 10; + if (wuss__slider_px_to_value(&groove, wuss_SLIDER_HORIZONTAL, + pt, 0, 1) != 1) + goto Failure; + + /* a point left of the groove clamps to lo, one past its right edge + * clamps to hi */ + pt.x = -50; pt.y = 10; + if (wuss__slider_px_to_value(&groove, wuss_SLIDER_HORIZONTAL, + pt, 0, 1) != 0) + goto Failure; + pt.x = 50; pt.y = 10; + if (wuss__slider_px_to_value(&groove, wuss_SLIDER_HORIZONTAL, + pt, 0, 1) != 1) + goto Failure; + + /* VERTICAL: pixel 0 is the groove's bottom (y1), value grows upward */ + groove.x0 = 0; groove.y0 = 0; + groove.x1 = 20; groove.y1 = 10; + + pt.x = 10; pt.y = groove.y1; + if (wuss__slider_px_to_value(&groove, wuss_SLIDER_VERTICAL, + pt, 0, 100) != 0) + goto Failure; + pt.x = 10; pt.y = groove.y0; + if (wuss__slider_px_to_value(&groove, wuss_SLIDER_VERTICAL, + pt, 0, 100) != 100) + goto Failure; + } + + printf("test: wuss__slider_value_for_point maps a screen click through a " + "real slider icon, honouring the groove gap and a reversed " + "min > max\n"); + + { + static test_task_t tc_sl; + wuss_task_t *delegate_sl; + wuss_window_t *win_sl; + wuss_icon_t *icon_sl; + wuss_icon_spec_t spec_sl; + box_t box_sl, content_sl, screen_box_sl, groove_sl; + point_t scroll_sl, pt_sl; + + delegate_sl = mk_task(wuss, test_handle, &tc_sl); + if (delegate_sl == NULL) goto Failure; + + box_sl.x0 = 5; box_sl.y0 = 5; box_sl.x1 = 125; box_sl.y1 = 105; + rc = wuss_window_create(delegate_sl, &box_sl, "SL", wuss_WINDOW_DEFAULT, + wuss_NO_BACKDROP, + SIZE2D(400, 400), SIZE2D(0, 0), &win_sl); + if (rc != result_OK) + goto Failure; + + memset(&spec_sl, 0, sizeof(spec_sl)); + spec_sl.bbox = (box_t) BOX_POS_SIZE(0, 0, 108, 20); + spec_sl.type = wuss_ICON_TYPE_SLIDER; + spec_sl.u.slider.orientation = wuss_SLIDER_HORIZONTAL; + spec_sl.u.slider.min = 0; + spec_sl.u.slider.max = 100; + spec_sl.u.slider.default_value = 0; + rc = wuss_icon_create(win_sl, &spec_sl, &icon_sl); + if (rc != result_OK) + goto Failure; + + wuss_window_get_content_bounds(win_sl, &content_sl); + wuss_window_get_scroll(win_sl, &scroll_sl); + wuss__icon_box_to_screen(&content_sl, scroll_sl, &spec_sl.bbox, + &screen_box_sl); + wuss__slider_groove_box(&screen_box_sl, &groove_sl); + + /* a click at the groove's on-screen left/right edges reads back as the + * slider's min/max, confirming wuss__slider_value_for_point applies the + * WUSS_SLIDER_GAP inset rather than reading off the full icon bbox */ + pt_sl.x = groove_sl.x0; pt_sl.y = (groove_sl.y0 + groove_sl.y1) / 2; + if (wuss__slider_value_for_point(win_sl, icon_sl, pt_sl) != 0) + goto Failure; + + pt_sl.x = groove_sl.x1; pt_sl.y = (groove_sl.y0 + groove_sl.y1) / 2; + if (wuss__slider_value_for_point(win_sl, icon_sl, pt_sl) != 100) + goto Failure; + + /* a reversed min > max mirrors the reading: the groove's left pixel + * (nominally "pixel 0") now yields the high end of [lo,hi] */ + wuss_icon_set_value(win_sl, icon_sl, 100); + icon_sl->spec.u.slider.min = 100; + icon_sl->spec.u.slider.max = 0; + + pt_sl.x = groove_sl.x0; pt_sl.y = (groove_sl.y0 + groove_sl.y1) / 2; + if (wuss__slider_value_for_point(win_sl, icon_sl, pt_sl) != 100) + goto Failure; + + pt_sl.x = groove_sl.x1; pt_sl.y = (groove_sl.y0 + groove_sl.y1) / 2; + if (wuss__slider_value_for_point(win_sl, icon_sl, pt_sl) != 0) + goto Failure; + + wuss_window_close(win_sl); + } + printf("test: wuss_icons_load scans resources/wuss/icons and compresses\n"); { @@ -5153,7 +6088,7 @@ ColourMenuOK: ; box_ic.x0 = 5; box_ic.y0 = 5; box_ic.x1 = 125; box_ic.y1 = 105; rc = wuss_window_create(delegate_ic, &box_ic, "IC", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(400, 400), SIZE2D(0, 0), &win_ic); if (rc != result_OK) goto Failure; @@ -5161,13 +6096,13 @@ ColourMenuOK: ; memset(&spec, 0, sizeof(spec)); spec.bbox = (box_t) BOX_POS_SIZE(0, 0, 16, 16); spec.type = wuss_ICON_TYPE_BITMAP; - spec.icon_set = wuss_ICON_SET(opton); + spec.u.bitmap.set = wuss_ICON_SET(opton); rc = wuss_icon_create(win_ic, &spec, &icon); if (rc != result_OK) goto Failure; /* a bogus index is rejected */ - spec.icon_set = wuss_ICON_SET(99); + spec.u.bitmap.set = wuss_ICON_SET(99); if (wuss_icon_create(win_ic, &spec, &icon) != result_WUSS_BAD_INDEX) goto Failure; rc = result_OK; @@ -5220,10 +6155,8 @@ result_t wuss_test(const char *resources) /* Force a chromeless window even in a build that still has furniture, so the * assertions below (content box == visible box) hold in every config. */ - const wuss_window_flags_t chromeless = - wuss_WINDOW_NO_TITLEBAR | wuss_WINDOW_NO_OUTLINE | wuss_WINDOW_NO_CLOSE | - wuss_WINDOW_NO_BACK | wuss_WINDOW_NO_TOGGLE_SIZE | wuss_WINDOW_NO_VSCROLL | - wuss_WINDOW_NO_HSCROLL | wuss_WINDOW_NO_RESIZE; + const wuss_window_flags_t chromeless = wuss_WINDOW_NO_TITLEBAR | + wuss_WINDOW_NO_OUTLINE; NOT_USED(resources); @@ -5241,7 +6174,7 @@ result_t wuss_test(const char *resources) printf("test: wuss_create (core)\n"); - rc = wuss_create(&scr, NULL, 0, NULL, 0, NULL, NULL, &wuss); + rc = wuss_create(&scr, NULL, 0, NULL, 0, NULL, NULL, NULL, &wuss); if (rc != result_OK) goto Failure; @@ -5249,7 +6182,7 @@ result_t wuss_test(const char *resources) box_a.x0 = 0; box_a.y0 = 0; box_a.x1 = 100; box_a.y1 = 0; rc = wuss_window_create(mk_task(wuss, NULL, NULL), &box_a, "toosmall", wuss_WINDOW_DEFAULT, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, box_size(&box_a), SIZE2D(0, 0), &win_a); if (rc != result_WUSS_TOO_SMALL) @@ -5266,7 +6199,7 @@ result_t wuss_test(const char *resources) box_a.x0 = 0; box_a.y0 = 0; box_a.x1 = 100; box_a.y1 = 100; rc = wuss_window_create(delegate_a, &box_a, "A", chromeless, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(400, 400), SIZE2D(0, 0), &win_a); if (rc != result_OK) @@ -5274,7 +6207,7 @@ result_t wuss_test(const char *resources) box_b.x0 = 50; box_b.y0 = 50; box_b.x1 = 150; box_b.y1 = 150; rc = wuss_window_create(delegate_b, &box_b, "B", chromeless, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(400, 400), SIZE2D(0, 0), &win_b); if (rc != result_OK) @@ -5330,7 +6263,7 @@ result_t wuss_test(const char *resources) * so the clamp caps content at the full 200x200 screen */ box_big.x0 = 10; box_big.y0 = 10; box_big.x1 = 400; box_big.y1 = 400; rc = wuss_window_create(mk_task(wuss, NULL, NULL), &box_big, "BIG", chromeless, - wuss_BACKDROP_COLOUR(wuss_NO_BACKGROUND), + wuss_NO_BACKDROP, SIZE2D(400, 400), SIZE2D(0, 0), &win_big); if (rc != result_OK) diff --git a/resources/bmfonts/04b_03.png b/resources/bmfonts/04b_03.png index 491fd8a..f2d1cb8 100644 Binary files a/resources/bmfonts/04b_03.png and b/resources/bmfonts/04b_03.png differ diff --git a/resources/bmfonts/04b_25.png b/resources/bmfonts/04b_25.png index d808fa2..9f0bf48 100644 Binary files a/resources/bmfonts/04b_25.png and b/resources/bmfonts/04b_25.png differ diff --git a/resources/bmfonts/CookeTall.png b/resources/bmfonts/CookeTall.png deleted file mode 100644 index 3f1c189..0000000 Binary files a/resources/bmfonts/CookeTall.png and /dev/null differ diff --git a/resources/bmfonts/DPT-CookeTall.png b/resources/bmfonts/DPT-CookeTall.png new file mode 100644 index 0000000..0d11acc Binary files /dev/null and b/resources/bmfonts/DPT-CookeTall.png differ diff --git a/resources/bmfonts/DPT-Daydream.png b/resources/bmfonts/DPT-Daydream.png new file mode 100644 index 0000000..f7db508 Binary files /dev/null and b/resources/bmfonts/DPT-Daydream.png differ diff --git a/resources/bmfonts/DPT-Digits-Bold.png b/resources/bmfonts/DPT-Digits-Bold.png new file mode 100644 index 0000000..f7d2eb3 Binary files /dev/null and b/resources/bmfonts/DPT-Digits-Bold.png differ diff --git a/resources/bmfonts/DPT-Digits-Regular.png b/resources/bmfonts/DPT-Digits-Regular.png new file mode 100644 index 0000000..5113389 Binary files /dev/null and b/resources/bmfonts/DPT-Digits-Regular.png differ diff --git a/resources/bmfonts/DPT-Henry.png b/resources/bmfonts/DPT-Henry.png new file mode 100644 index 0000000..36aad27 Binary files /dev/null and b/resources/bmfonts/DPT-Henry.png differ diff --git a/resources/bmfonts/Daydream.png b/resources/bmfonts/Daydream.png deleted file mode 100644 index 317ef09..0000000 Binary files a/resources/bmfonts/Daydream.png and /dev/null differ diff --git a/resources/bmfonts/Digits-Bold.png b/resources/bmfonts/Digits-Bold.png deleted file mode 100644 index a884102..0000000 Binary files a/resources/bmfonts/Digits-Bold.png and /dev/null differ diff --git a/resources/bmfonts/Digits-Regular.png b/resources/bmfonts/Digits-Regular.png deleted file mode 100755 index de4cf73..0000000 Binary files a/resources/bmfonts/Digits-Regular.png and /dev/null differ diff --git a/resources/bmfonts/Henry.png b/resources/bmfonts/Henry.png deleted file mode 100644 index 21fbf83..0000000 Binary files a/resources/bmfonts/Henry.png and /dev/null differ diff --git a/resources/bmfonts/MS Sans Serif.png b/resources/bmfonts/MS Sans Serif.png index 969ba1e..c852988 100644 Binary files a/resources/bmfonts/MS Sans Serif.png and b/resources/bmfonts/MS Sans Serif.png differ diff --git a/resources/bmfonts/Nokia.png b/resources/bmfonts/Nokia.png index 90bdfff..9648265 100644 Binary files a/resources/bmfonts/Nokia.png and b/resources/bmfonts/Nokia.png differ diff --git a/resources/bmfonts/Symbols.png b/resources/bmfonts/Symbols.png index f4c8a63..d0e2734 100644 Binary files a/resources/bmfonts/Symbols.png and b/resources/bmfonts/Symbols.png differ diff --git a/resources/bmfonts/Tiny.png b/resources/bmfonts/Tiny.png index dc14058..af0833b 100644 Binary files a/resources/bmfonts/Tiny.png and b/resources/bmfonts/Tiny.png differ diff --git a/resources/bmfonts/GliderRider.png b/resources/bmfonts/ZX-GliderRider.png similarity index 100% rename from resources/bmfonts/GliderRider.png rename to resources/bmfonts/ZX-GliderRider.png diff --git a/resources/wuss/ninepatch.png b/resources/wuss/ninepatch.png index 304e2af..e1afde0 100644 Binary files a/resources/wuss/ninepatch.png and b/resources/wuss/ninepatch.png differ diff --git a/tools/fmt/realign_decls.py b/tools/fmt/realign_decls.py new file mode 100644 index 0000000..01e2903 --- /dev/null +++ b/tools/fmt/realign_decls.py @@ -0,0 +1,219 @@ +#!/usr/bin/env python3 +"""realign_decls.py -- tighten C declaration-block alignment + +For each contiguous block of simple variable declarations at the top of a +function scope, re-pads the column between the type and the name/'*' so it +sits exactly one space past the block's own longest type token, rather than +whatever wider column happened to be in use before. + +Only touches declaration lines (`type name[, name2][ = init];`, optionally +`static`/`const` qualified, optionally array-subscripted, optionally with a +leading `*` on the name), and only inside executable-code braces -- never +inside a struct/enum/union body. Leaves function signatures, macros, +comments and all other code untouched. + +Usage: realign_decls.py FILE [FILE ...] +Edits files in place. +""" +import re +import sys + +DECL_RE = re.compile( + r'^(?P[ \t]*)' + r'(?P(?:static\s+|const\s+)*)' + r'(?P[A-Za-z_][A-Za-z0-9_]*(?:\s+(?:const|unsigned|signed|long|short|int|char)\b)*)' + r'(?P\s+)' + r'(?P\*+\s*)?' + r'(?P[A-Za-z_][A-Za-z0-9_]*)' + r'(?P(?:\s*\[[^\]]*\])*\s*(?:,\s*\*{0,3}\s*[A-Za-z_][A-Za-z0-9_]*(?:\s*\[[^\]]*\])*)*\s*(?:=[^;]*)?;.*)$' +) + +KEYWORDS = { + 'return', 'if', 'else', 'for', 'while', 'do', 'switch', 'break', + 'continue', 'goto', 'sizeof', 'typedef', 'struct', 'enum', 'union', + 'default', 'case', +} + +# a line that (on its own, or Allman-style with '{' on the next line) +# opens a struct/enum/union body -- its members must never be treated as a +# realignable declaration block +STRUCT_KW_RE = re.compile(r'\b(struct|enum|union)\b[^;{]*$') +STRUCT_OPEN_SAMELINE_RE = re.compile(r'\b(struct|enum|union)\b[^;{]*\{\s*$') +BARE_BRACE_RE = re.compile(r'^\s*\{\s*$') + +# lines that never end a declaration block and never count as a decl: +# blank lines and comment-only lines. They're transparent -- a block can +# run across them without breaking. +TRANSPARENT_RE = re.compile(r'^\s*(//.*|/\*.*\*/\s*)?$') + + +def parse_decl(line): + m = DECL_RE.match(line) + if not m: + return None + type_tok = (m.group('qual') + m.group('type')).strip() + if type_tok.split()[-1] in KEYWORDS: + return None + tail = m.group('name') + m.group('tail') + body = tail.split(';')[0] + has_call = '(' in body + return { + 'indent': m.group('indent'), + 'type': type_tok, + 'stars': (m.group('stars') or ''), + 'rest': tail, + 'editable': not has_call, # counts toward width either way + 'has_init': '=' in body, + } + + +def struct_body_lines(lines): + """Return the set of line indices that lie inside a struct/enum/union + body -- opened either by '... struct X {' on one line, or Allman-style + with the keyword line followed by a bare '{' -- so declaration scanning + can skip them.""" + in_struct = set() + stack = [] # stack of bool: True if this brace level is a struct body + pending_struct = False # previous line was a bare struct/enum/union kw + for i, line in enumerate(lines): + is_struct_open = bool(STRUCT_OPEN_SAMELINE_RE.search(line)) + if not is_struct_open and pending_struct and BARE_BRACE_RE.match(line): + is_struct_open = True + pending_struct = bool(STRUCT_KW_RE.search(line.strip())) and \ + not line.strip().endswith(';') and '{' not in line + for ch in line: + if ch == '{': + stack.append(is_struct_open) + is_struct_open = False + elif ch == '}': + if stack: + stack.pop() + if stack and stack[-1]: + in_struct.add(i) + return in_struct + + +def find_blocks(lines, skip): + """Yield (start, end, entries) for consecutive declaration lines sharing + the same indent, tolerating blank/comment-only lines in between (they + don't break the run, but aren't decls either), skipping any line index + in `skip`. entries[k] is None for a transparent (blank/comment) line.""" + blocks = [] + i = 0 + n = len(lines) + while i < n: + if i in skip: + i += 1 + continue + parsed = parse_decl(lines[i]) + if parsed is None: + i += 1 + continue + indent = parsed['indent'] + start = i + j = i + entries = [] + last_real = parsed + pending_transparent = False + while j < n and j not in skip: + line = lines[j] + p = parse_decl(line) + if p is not None and p['indent'] == indent: + if pending_transparent and p['has_init'] != last_real['has_init']: + # a blank/comment line separating an initialised decl + # from an uninitialised one (or vice versa) marks a + # deliberate group break (see + # feedback_var_declaration_order) -- stop here, don't + # bridge the two groups into one aligned block. + break + entries.append(p) + last_real = p + pending_transparent = False + j += 1 + continue + if p is None and TRANSPARENT_RE.match(line): + entries.append(None) + pending_transparent = True + j += 1 + continue + break + # trim trailing transparent lines -- they belong to whatever follows, + # not to this block + while entries and entries[-1] is None: + entries.pop() + j -= 1 + blocks.append((start, j, entries)) + i = j + return blocks + + +def reformat_block(lines, start, end, entries): + # the name (or, for a pointer, its '*') must start in the same column + # for every editable line in the block; a pointer's '*' occupies a + # column of its own, so it counts toward each line's "effective" + # type-side width. Lines with a call in the initializer, and blank/ + # comment lines, count toward the width but are never rewritten. + def effective_len(d): + return len(d['type']) + len(d['stars'].rstrip()) + + real = [d for d in entries if d is not None] + name_col = max(effective_len(d) for d in real) + 1 + for k, idx in enumerate(range(start, end)): + d = entries[k] + if d is None or not d['editable']: + continue + type_tok = d['type'] + stars = d['stars'].rstrip() + rest = d['rest'] + pad = name_col - effective_len(d) + new_line = '{}{}{}{}{}'.format( + d['indent'], type_tok, ' ' * pad, stars, rest) + lines[idx] = new_line + + +def reformat_text(text): + """Reformat declaration blocks in `text`. Returns (new_text, changed).""" + had_trailing_newline = text.endswith('\n') + lines = text.split('\n') + if had_trailing_newline: + lines = lines[:-1] + + skip = struct_body_lines(lines) + blocks = find_blocks(lines, skip) + changed = False + for start, end, entries in blocks: + real = [d for d in entries if d is not None] + if len(real) < 2: + continue + before = lines[start:end] + reformat_block(lines, start, end, entries) + if lines[start:end] != before: + changed = True + + out = '\n'.join(lines) + if had_trailing_newline: + out += '\n' + return out, changed + + +def process_file(path): + with open(path, 'r') as f: + text = f.read() + out, changed = reformat_text(text) + if changed: + with open(path, 'w') as f: + f.write(out) + return changed + + +def main(argv): + any_changed = False + for path in argv[1:]: + if process_file(path): + print('reformatted: {}'.format(path)) + any_changed = True + return 0 + + +if __name__ == '__main__': + sys.exit(main(sys.argv)) diff --git a/tools/fmt/test_realign_decls.py b/tools/fmt/test_realign_decls.py new file mode 100644 index 0000000..56c6847 --- /dev/null +++ b/tools/fmt/test_realign_decls.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +"""assert-based self-check for realign_decls.py""" +from realign_decls import reformat_text + + +def one(src): + out, changed = reformat_text(src) + return out if changed else None + + +def test_pointer_star_column(): + src = ( + " int ci;\n" + " box_t *cbox;\n" + " int cross_extent;\n" + ) + assert one(src) == ( + " int ci;\n" + " box_t *cbox;\n" + " int cross_extent;\n" + ) + + +def test_already_aligned_untouched(): + src = ( + " int ci;\n" + " box_t *cbox;\n" + ) + assert one(src) is None + + +def test_struct_body_untouched(): + src = ( + "typedef struct foo\n" + "{\n" + " int a;\n" + " box_t *b;\n" + "}\n" + "foo_t;\n" + ) + assert one(src) is None + + +def test_array_subscript(): + src = ( + " stack__child_t children[n];\n" + " int nchildren;\n" + ) + assert one(src) == ( + " stack__child_t children[n];\n" + " int nchildren;\n" + ) + + +def test_blank_and_comment_bridge_same_group(): + # both lines uninitialised -> the blank/comment gap is transparent + src = ( + " int left_skip;\n" + "\n" + " /* note */\n" + " unsigned int clamped_pos_x;\n" + ) + assert one(src) == ( + " int left_skip;\n" + "\n" + " /* note */\n" + " unsigned int clamped_pos_x;\n" + ) + + +def test_call_initializer_counts_width_but_not_rewritten(): + src = ( + " int log2bpp;\n" + " pixelfmt_any_t nativefg = colour_to_pixel(scr->palette, fg);\n" + ) + out = one(src) + assert out == ( + " int log2bpp;\n" + " pixelfmt_any_t nativefg = colour_to_pixel(scr->palette, fg);\n" + ) + + +def test_init_group_break_across_blank_line(): + # 'quantum' is initialised; the following group is not -- the blank + # line between them must NOT bridge the two into one aligned block + src = ( + " const size_t quantum = sizeof(free_t);\n" + "\n" + " int nentries;\n" + " cache_t *c;\n" + ) + assert one(src) == ( + " const size_t quantum = sizeof(free_t);\n" + "\n" + " int nentries;\n" + " cache_t *c;\n" + ) + + +def test_single_decl_untouched(): + assert one(" int only_one;\n") is None + + +def test_idempotent(): + src = ( + " int ci;\n" + " box_t *cbox;\n" + ) + out1 = one(src) + out2 = one(out1) + assert out2 is None + + +if __name__ == '__main__': + import sys + + mod = sys.modules[__name__] + for name in dir(mod): + if name.startswith('test_'): + getattr(mod, name)() + print('all tests passed') diff --git a/tools/test_wrap_doxygen.py b/tools/fmt/test_wrap_doxygen.py similarity index 100% rename from tools/test_wrap_doxygen.py rename to tools/fmt/test_wrap_doxygen.py diff --git a/tools/test_wrap_protos.py b/tools/fmt/test_wrap_protos.py similarity index 100% rename from tools/test_wrap_protos.py rename to tools/fmt/test_wrap_protos.py diff --git a/tools/wrap_doxygen.py b/tools/fmt/wrap_doxygen.py similarity index 100% rename from tools/wrap_doxygen.py rename to tools/fmt/wrap_doxygen.py diff --git a/tools/wrap_protos.py b/tools/fmt/wrap_protos.py similarity index 100% rename from tools/wrap_protos.py rename to tools/fmt/wrap_protos.py diff --git a/tools/ttf2bmfont.py b/tools/ttf2bmfont.py index e9e7bb0..0a9e4b9 100755 --- a/tools/ttf2bmfont.py +++ b/tools/ttf2bmfont.py @@ -6,7 +6,8 @@ * 4-entry palette, order is the contract: 0 = background, 1 = glyph ink, 2 = advance-width marker, 3 = grid - (index 3 is loaded but never interpreted -- it is a visual aid only). + (the left-sidebearing line index 3 draws is a visual aid only, but its + baseline and cell-bottom rows are load-bearing -- see below). * exactly 32 glyph cells per row, packed edge to edge; cellW = pngW / 32, and cellW must be <= 16. * the top row of every cell is the "advance strip": it holds `advance` @@ -15,8 +16,14 @@ * the remaining cellH-1 rows are the glyph bitmap; index-1 pixels are ink. * cell 0 is U+0020 (space); codepoints run contiguously upward. There is no character map and no stored first-codepoint. - * there is no baseline in the format; the grey grid lines this script draws - (left sidebearing, baseline, cell bottom) are purely cosmetic. + * the loader derives the font's ascent and descent from cell 0 (space, + which carries no ink to interfere): the first two full-width rows of + index-3 pixels within the cell body are the baseline and the cell + bottom -- ascent is the baseline's offset from the cell top, descent is + the cell bottom's offset from the baseline. This script draws those rows + at `ascent` and `char_h - 1` (see draw_grid below). --no-grid omits them + along with the other grid lines, and the loader then falls back to no + descender space for that font. Requires freetype-py ( pip install freetype-py ). PNG is written by hand so Pillow is not needed. @@ -113,7 +120,7 @@ def measure_cell(glyphs, pad, advance_source): def _ink_advance(g): if not g["bits"]: return 0 - return g["left"] + g["width"] + 1 + return g["left"] + g["width"] def advance_for(g, cell_w, advance_source): @@ -250,7 +257,7 @@ def main(argv): p.add_argument("--pad", type=int, default=1, help="extra pixels added to the glyph bbox for grid lines") p.add_argument("--advance-source", choices=("ttf", "ink"), default="ttf", - help="'ttf' uses the font's advance, 'ink' uses ink width+1") + help="'ttf' uses the font's advance, 'ink' uses exact ink width") p.add_argument("--no-grid", action="store_true", help="omit the grey guide lines") p.add_argument("--first", type=lambda s: int(s, 0), default=0x20)