From 16a9609b669fd1f1fe7e121d05d4c7419acb62e4 Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Tue, 20 Oct 2026 06:51:44 +0800 Subject: [PATCH 01/13] Fix image loaders and decode GIF frames on demand On macOS PNG colors came out wrong because an Apple-only pass swizzled pixels twice; libpng now writes native ARGB, and one row-size check replaces depth checks that read the IHDR from before the transforms, so 16-bit and sub-byte PNGs load. JPEG keeps its pixmap valid across longjmp() and fixes its A8 path. TinyVG checks color indices, allocation sizes and varuint widths, which crafted files overran. GIF frames decode into the pixmap the loader returns instead of one pixmap per frame, cutting nyancat.gif from about 880 KB to 88 KB, with real transparency and browser rules for disposal, loop counts and short delays. Interlaced frames two to four rows tall, clear codes at a code-size bump and frames past the canvas no longer corrupt the picture. An animation damages its pixmap when it draws a frame. --- apps/animation.c | 15 +- include/twin.h | 4 +- src/animation.c | 50 +-- src/image-gif.c | 784 +++++++++++++++++++-------------------------- src/image-jpeg.c | 18 +- src/image-png.c | 118 +++---- src/image-tvg.c | 153 +++------ src/pixmap.c | 53 +-- src/twin_private.h | 43 ++- tests/test-core.c | 224 ++++++++++++- 10 files changed, 713 insertions(+), 749 deletions(-) diff --git a/apps/animation.c b/apps/animation.c index 2d3ff463..b8df5a54 100644 --- a/apps/animation.c +++ b/apps/animation.c @@ -25,12 +25,10 @@ static void _apps_animation_paint(twin_custom_widget_t *custom) { apps_animation_data_t *anim = (apps_animation_data_t *) twin_custom_widget_data(custom); - twin_pixmap_t *current_frame = NULL; - - /* Paint the current frame; only the timer advances it, so repainting - * (exposure, a restore after eviction) does not skip frames */ - twin_animation_t *a = twin_pixmap_animation(anim->pix); - current_frame = a ? twin_animation_get_current_frame(a) : anim->pix; + /* Paint the current frame: an animation redraws its pixmap in place, + * and only the timer advances it, so repainting (exposure, a restore + * after eviction) does not skip frames */ + twin_pixmap_t *current_frame = anim->pix; /* A policy may give the window more room than the image (a phone's * work area, the whole screen without a window manager): center it on @@ -47,7 +45,8 @@ static void _apps_animation_paint(twin_custom_widget_t *custom) .source_kind = TWIN_PIXMAP, .u.pixmap = current_frame, }; - twin_composite(dst, x, y, &srcop, 0, 0, NULL, 0, 0, TWIN_SOURCE, fw, fh); + /* Over the background: a GIF's transparent pixels show it */ + twin_composite(dst, x, y, &srcop, 0, 0, NULL, 0, 0, TWIN_OVER, fw, fh); } static twin_time_t _apps_animation_timeout(twin_time_t now, void *closure) @@ -71,7 +70,7 @@ static twin_dispatch_result_t _apps_animation_dispatch( _apps_animation_paint(custom); break; case TwinEventDestroy: { - /* The pixmap owns the animation, so this frees every frame too */ + /* The pixmap owns the animation, so this frees the decoder too */ apps_animation_data_t *anim = (apps_animation_data_t *) twin_custom_widget_data(custom); twin_clear_timeout(anim->timeout); diff --git a/include/twin.h b/include/twin.h index c88feeca..ff23badb 100644 --- a/include/twin.h +++ b/include/twin.h @@ -553,8 +553,8 @@ twin_pixmap_t *twin_pixmap_from_file(const char *path); /* * Frame-based animations, as loaded from an animated GIF. The animation - * belongs to its pixmap (pixmap->animation): twin_pixmap_destroy() frees it - * along with every frame. + * belongs to its pixmap (pixmap->animation) and redraws that pixmap in place: + * the current frame is the pixmap itself. twin_pixmap_destroy() frees it. */ /* Get the number of milliseconds the current frame should be displayed. */ diff --git a/src/animation.c b/src/animation.c index 1fe39958..4701cb01 100644 --- a/src/animation.c +++ b/src/animation.c @@ -9,59 +9,27 @@ twin_time_t twin_animation_get_current_delay(const twin_animation_t *anim) { - if (!anim) - return 0; - return anim->iter->current_delay; + return anim ? anim->delay : 0; } twin_pixmap_t *twin_animation_get_current_frame(const twin_animation_t *anim) { - if (!anim) - return NULL; - return anim->iter->current_frame; -} - -static void _twin_animation_iter_advance(twin_animation_iter_t *iter) -{ - twin_animation_t *anim = iter->anim; - iter->current_index++; - if (iter->current_index >= anim->n_frames) - iter->current_index = anim->loop ? 0 : anim->n_frames - 1; - iter->current_frame = anim->frames[iter->current_index]; - iter->current_delay = anim->frame_delays[iter->current_index]; + return anim ? anim->pixmap : NULL; } void twin_animation_advance_frame(twin_animation_t *anim) { if (!anim) return; - _twin_animation_iter_advance(anim->iter); + /* The frame is drawn into the pixmap in place: show it where shown, + * unless the animation has played out and drew nothing */ + if (anim->advance(anim)) + twin_pixmap_damage(anim->pixmap, 0, 0, anim->pixmap->width, + anim->pixmap->height); } void _twin_animation_destroy(twin_animation_t *anim) { - if (!anim) - return; - - twin_free(anim->iter); - for (twin_count_t i = 0; i < anim->n_frames; i++) - twin_pixmap_destroy(anim->frames[i]); - twin_free(anim->frames); - twin_free(anim->frame_delays); - twin_free(anim); -} - -twin_animation_iter_t *_twin_animation_iter_init(twin_animation_t *anim) -{ - if (!anim || !anim->n_frames) - return NULL; - twin_animation_iter_t *iter = twin_malloc(sizeof(twin_animation_iter_t)); - if (!iter) - return NULL; - iter->current_index = 0; - iter->current_frame = anim->frames[0]; - iter->current_delay = anim->frame_delays[0]; - anim->iter = iter; - iter->anim = anim; - return iter; + if (anim) + anim->close(anim); } diff --git a/src/image-gif.c b/src/image-gif.c index e8b1e7c7..5d0b03c2 100644 --- a/src/image-gif.c +++ b/src/image-gif.c @@ -6,169 +6,99 @@ * This file integrates the GIF decoder originally developed by Marcel Rodrigues * as part of the gifdec project. You can find the original gifdec project at: * https://github.com/lecram/gifdec + * + * Frames decode on demand, straight into the pixmap the loader returns: an + * animation costs one ARGB32 canvas and an open file, however many frames it + * has, instead of a pixmap per frame. */ -#include #include +#include #include -#include #include "twin.h" #include "twin_private.h" typedef struct { - int size; uint8_t colors[0x100 * 3]; } gif_palette_t; +/* Graphic Control Extension: applies to the next image only */ typedef struct { uint16_t delay; uint8_t tindex; uint8_t disposal; - int input; - int transparency; + bool transparency; } gif_gce_t; -typedef struct _twin_gif { - int fd; - off_t anim_start; - twin_coord_t width, height; - twin_coord_t depth; - twin_count_t loop_count; - gif_gce_t gce; - gif_palette_t *palette; - gif_palette_t lct, gct; - uint16_t fx, fy, fw, fh; - uint8_t bgindex; - uint8_t *canvas, *frame; -} twin_gif_t; - -#define MIN(A, B) ((A) < (B) ? (A) : (B)) -#define MAX(A, B) ((A) > (B) ? (A) : (B)) - typedef struct { uint16_t length; uint16_t prefix; uint8_t suffix; } entry_t; +/* LZW codes are at most 12 bits: the table never outgrows 0x1000 entries, + * so it is allocated whole, once per decoder */ typedef struct { - int bulk; int n_entries; - entry_t *entries; + entry_t entries[0x1000]; } table_t; +typedef struct _twin_gif { + twin_animation_t anim; /* first: the pixmap's animation handle */ + FILE *f; + long anim_start; + int repeats; /* plays left after this one; -1: forever */ + bool counted; /* the NETSCAPE count was read: a rewind re-reads it */ + bool done; /* played out: the last frame stays, nothing more to read */ + gif_gce_t gce; + gif_palette_t *palette; + gif_palette_t lct, gct; + uint16_t fx, fy, fw, fh; /* frame rectangle, clipped to the canvas */ + uint16_t dw, dh; /* frame size as the image data lays it out */ + unsigned frames; /* frames drawn since the start or a rewind */ + table_t *table; + twin_argb32_t *saved; /* pixels under a restore-to-previous frame */ + size_t saved_size; /* ...and how many fit there */ +} twin_gif_t; + +/* LZW input: the current data sub-block and the bit position in it */ +typedef struct { + uint8_t buf[0xff]; + uint8_t len, pos; + uint8_t shift, byte; +} lzw_in_t; + +/* Stdio buffer for headers and extensions; image data is read a whole + * sub-block at a time */ +#define GIF_IO_BUFFER 512 + +#define MIN(A, B) ((A) < (B) ? (A) : (B)) + /* Read exactly @n bytes. A truncated file reads as zeros past its end, * which every caller treats as a terminator (a zero block size, a zero * separator), so a short file ends decoding instead of feeding it * uninitialized bytes or spinning on a size that never reaches zero. */ -static bool gif_read(int fd, void *buf, size_t n) +static bool gif_read(twin_gif_t *gif, void *buf, size_t n) { - uint8_t *p = buf; - while (n) { - ssize_t got = read(fd, p, n); - if (got <= 0) { - memset(p, 0, n); - return false; - } - p += got; - n -= (size_t) got; - } - return true; + size_t got = fread(buf, 1, n, gif->f); + if (got == n) + return true; + memset((uint8_t *) buf + got, 0, n - got); + return false; } -static uint16_t read_num(int fd) +static void gif_skip(twin_gif_t *gif, long n) { - uint8_t bytes[2]; - - gif_read(fd, bytes, 2); - return bytes[0] + (((uint16_t) bytes[1]) << 8); + fseek(gif->f, n, SEEK_CUR); } -static twin_gif_t *gif_open(const char *fname) +static uint16_t read_num(twin_gif_t *gif) { - uint8_t sigver[3]; - uint16_t width, height, depth; - uint8_t fdsz, bgidx, aspect; - int i; - uint8_t *bgcolor; - int gct_sz; - twin_gif_t *gif; - - int fd = open(fname, O_RDONLY); - if (fd == -1) - return NULL; -#ifdef _WIN32 - setmode(fd, O_BINARY); -#endif - /* Header */ - gif_read(fd, sigver, 3); - if (memcmp(sigver, "GIF", 3) != 0) { - log_error("Invalid signature"); - goto fail; - } - /* Version */ - gif_read(fd, sigver, 3); - if (memcmp(sigver, "89a", 3) != 0) { - log_error("Invalid version"); - goto fail; - } - /* Width x Height */ - width = read_num(fd); - height = read_num(fd); - /* Pixmaps index rows and columns with twin_coord_t */ - if (!width || !height || width > INT16_MAX || height > INT16_MAX) { - log_error("Unsupported size %ux%u", width, height); - goto fail; - } - /* FDSZ */ - gif_read(fd, &fdsz, 1); - /* Presence of GCT */ - if (!(fdsz & 0x80)) { - log_error("No global color table"); - goto fail; - } - /* Color Space's Depth */ - depth = ((fdsz >> 4) & 7) + 1; - /* Ignore Sort Flag. */ - /* GCT Size */ - gct_sz = 1 << ((fdsz & 0x07) + 1); - /* Background Color Index */ - gif_read(fd, &bgidx, 1); - /* Aspect Ratio */ - gif_read(fd, &aspect, 1); - /* Create twin_gif_t Structure. */ - gif = twin_calloc(1, sizeof(*gif)); - if (!gif) - goto fail; - gif->fd = fd; - gif->width = width; - gif->height = height; - gif->depth = depth; - /* Read GCT */ - gif->gct.size = gct_sz; - gif_read(fd, gif->gct.colors, 3 * gif->gct.size); - gif->palette = &gif->gct; - gif->bgindex = bgidx; - gif->frame = twin_calloc(4, (size_t) width * height); - if (!gif->frame) { - twin_free(gif); - goto fail; - } - gif->canvas = &gif->frame[width * height]; - if (gif->bgindex) - memset(gif->frame, gif->bgindex, gif->width * gif->height); - bgcolor = &gif->palette->colors[gif->bgindex * 3]; - if (bgcolor[0] || bgcolor[1] || bgcolor[2]) - for (i = 0; i < gif->width * gif->height; i++) - memcpy(&gif->canvas[i * 3], bgcolor, 3); - gif->anim_start = lseek(fd, 0, SEEK_CUR); - goto ok; -fail: - close(fd); - return NULL; -ok: - return gif; + uint8_t bytes[2]; + + gif_read(gif, bytes, 2); + return bytes[0] + (((uint16_t) bytes[1]) << 8); } static void discard_sub_blocks(twin_gif_t *gif) @@ -176,104 +106,86 @@ static void discard_sub_blocks(twin_gif_t *gif) uint8_t size; do { - gif_read(gif->fd, &size, 1); - lseek(gif->fd, size, SEEK_CUR); + gif_read(gif, &size, 1); + gif_skip(gif, size); } while (size); } -static table_t *table_new(int key_size) +/* A palette of 2 << (@flags & 7) colors, as image and screen descriptors + * size them */ +static void read_palette(twin_gif_t *gif, gif_palette_t *pal, uint8_t flags) { - int init_bulk = MAX(1 << (key_size + 1), 0x100); - table_t *table = twin_malloc(sizeof(*table) + sizeof(entry_t) * init_bulk); - if (table) { - table->bulk = init_bulk; - table->n_entries = (1 << key_size) + 2; - table->entries = (entry_t *) &table[1]; - for (int key = 0; key < (1 << key_size); key++) { - /* clang-format off */ - table->entries[key] = (entry_t) {1, 0xFFF, key}; - /* clang-format on */ - } - } - return table; + gif_read(gif, pal->colors, 3 * (2 << (flags & 0x07))); } -/* Add table entry. Return value: - * 0 on success - * +1 if key size must be incremented after this addition - * -1 if could not realloc table - */ -static int entry_add(table_t **table_p, - uint16_t length, - uint16_t prefix, - uint8_t suffix) +static void table_reset(table_t *table, int key_size) { - table_t *table = *table_p; - if (table->n_entries == table->bulk) { - table->bulk *= 2; - table = - twin_realloc(table, sizeof(*table) + sizeof(entry_t) * table->bulk); - if (!table) - return -1; - table->entries = (entry_t *) &table[1]; - *table_p = table; + table->n_entries = (1 << key_size) + 2; + for (int key = 0; key < (1 << key_size); key++) { + /* clang-format off */ + table->entries[key] = (entry_t) {1, 0xFFF, key}; + /* clang-format on */ } +} + +/* Add a table entry; the caller stops adding once the table is full. + * True if the key size must grow after this addition. */ +static bool entry_add(table_t *table, + uint16_t length, + uint16_t prefix, + uint8_t suffix) +{ /* clang-format off */ table->entries[table->n_entries] = (entry_t) {length, prefix, suffix}; /* clang-format on */ table->n_entries++; - if ((table->n_entries & (table->n_entries - 1)) == 0) - return 1; - return 0; + return (table->n_entries & (table->n_entries - 1)) == 0; } -static uint16_t get_key(twin_gif_t *gif, - int key_size, - uint8_t *sub_len, - uint8_t *shift, - uint8_t *byte) +/* Next @key_size-bit code, or 0x1000 at the data's terminating block */ +static uint16_t get_key(twin_gif_t *gif, lzw_in_t *in, int key_size) { - int bits_read; - int rpad; + uint16_t key = 0; int frag_size; - uint16_t key; - key = 0; - for (bits_read = 0; bits_read < key_size; bits_read += frag_size) { - rpad = (*shift + bits_read) % 8; + for (int bits_read = 0; bits_read < key_size; bits_read += frag_size) { + int rpad = (in->shift + bits_read) % 8; if (rpad == 0) { - /* Update byte. */ - if (*sub_len == 0) { - gif_read(gif->fd, sub_len, 1); /* Must be nonzero! */ - if (*sub_len == 0) + /* Next byte, from the next sub-block once this one is used */ + if (in->pos == in->len) { + gif_read(gif, &in->len, 1); + if (!in->len) return 0x1000; + gif_read(gif, in->buf, in->len); + in->pos = 0; } - gif_read(gif->fd, byte, 1); - (*sub_len)--; + in->byte = in->buf[in->pos++]; } frag_size = MIN(key_size - bits_read, 8 - rpad); - key |= ((uint16_t) ((*byte) >> rpad)) << bits_read; + key |= ((uint16_t) (in->byte >> rpad)) << bits_read; } /* Clear extra bits to the left. */ key &= (1 << key_size) - 1; - *shift = (*shift + key_size) % 8; + in->shift = (in->shift + key_size) % 8; return key; } -/* Compute output index of y-th input line, in frame of height h. */ +/* Compute output index of y-th input line, in frame of height h. The + * passes hold rows 0, 4, 2 and 1 modulo 8, 8, 4 and 2: count them with + * ceiling division, which stays exact for frames shorter than a pass. */ static int interlaced_line_index(int h, int y) { int p; /* number of lines in current pass */ - p = (h - 1) / 8 + 1; + p = (h + 7) / 8; if (y < p) /* pass 1 */ return y * 8; y -= p; - p = (h - 5) / 8 + 1; + p = (h + 3) / 8; if (y < p) /* pass 2 */ return y * 8 + 4; y -= p; - p = (h - 3) / 4 + 1; + p = (h + 1) / 4; if (y < p) /* pass 3 */ return y * 4 + 2; y -= p; @@ -281,175 +193,176 @@ static int interlaced_line_index(int h, int y) return y * 2 + 1; } -/* Decompress image pixels. - * Return 0 on success or -1 on out-of-memory (w.r.t. LZW code table_t). - */ -static int read_image_data(twin_gif_t *gif, int interlace) +/* Canvas row @y of the frame rectangle */ +static twin_argb32_t *frame_row(twin_gif_t *gif, int y) +{ + return twin_pixmap_pointer(gif->anim.pixmap, gif->fx, gif->fy + y).argb32; +} + +/* Canvas row for the frame's @y-th row of image data, or NULL for a row + * clipped off the canvas. Rows are laid out by the unclipped size. */ +static twin_argb32_t *data_row(twin_gif_t *gif, int y, bool interlace) +{ + int row = interlace ? interlaced_line_index(gif->dh, y) : y; + return row < gif->fh ? frame_row(gif, row) : NULL; +} + +/* Decompress image pixels into the canvas, leaving transparent ones as + * they were. Return 0 on success or -1 on a bad code size. */ +static int read_image_data(twin_gif_t *gif, bool interlace) { - uint8_t sub_len, shift, byte; int init_key_size, key_size; bool is_table_full = false; - int frm_off, frm_size, str_len = 0, i, p, x, y; + uint32_t frm_off, frm_size; + int str_len = 0, i; uint16_t key, clear, stop; - int ret; - table_t *table; + bool grow = false; + table_t *table = gif->table; entry_t entry = {0}; - off_t start, end; + twin_argb32_t lut[0x100]; + lzw_in_t in = {.len = 0}; + uint8_t byte; - gif_read(gif->fd, &byte, 1); + gif_read(gif, &byte, 1); key_size = (int) byte; if (key_size < 2 || key_size > 8) return -1; - start = lseek(gif->fd, 0, SEEK_CUR); - discard_sub_blocks(gif); - end = lseek(gif->fd, 0, SEEK_CUR); - lseek(gif->fd, start, SEEK_SET); + for (i = 0; i < 0x100; i++) { + const uint8_t *c = &gif->palette->colors[i * 3]; + lut[i] = + 0xFF000000U | (uint32_t) c[0] << 16 | (uint32_t) c[1] << 8 | c[2]; + } + int tindex = gif->gce.transparency ? gif->gce.tindex : -1; + clear = 1 << key_size; stop = clear + 1; - table = table_new(key_size); - if (!table) - return -1; + table_reset(table, key_size); key_size++; init_key_size = key_size; - sub_len = shift = 0; - key = get_key(gif, key_size, &sub_len, &shift, &byte); /* clear code */ + key = get_key(gif, &in, key_size); /* clear code */ frm_off = 0; - ret = 0; - frm_size = gif->fw * gif->fh; + frm_size = (uint32_t) gif->dw * gif->dh; while (frm_off < frm_size) { if (key == clear) { + /* A clear right at a code-size bump cancels it: encoders that + * clear early to keep codes short rely on this */ key_size = init_key_size; table->n_entries = (1 << (key_size - 1)) + 2; is_table_full = false; + grow = false; } else if (!is_table_full) { - ret = entry_add(&table, str_len + 1, key, entry.suffix); - if (ret == -1) { - twin_free(table); - return -1; - } + grow = entry_add(table, str_len + 1, key, entry.suffix); if (table->n_entries == 0x1000) { - ret = 0; + grow = false; is_table_full = true; } } - key = get_key(gif, key_size, &sub_len, &shift, &byte); + key = get_key(gif, &in, key_size); if (key == clear) continue; if (key == stop || key == 0x1000) break; if (key >= table->n_entries) break; - if (ret == 1) + if (grow) key_size++; entry = table->entries[key]; str_len = entry.length; /* A string running past the frame ends a malformed stream */ - if (str_len > frm_size - frm_off) + if ((uint32_t) str_len > frm_size - frm_off) break; + /* The string is written last pixel first: start at its end and step + * back one pixel per prefix, a row up at the left edge */ + uint32_t p = frm_off + str_len - 1; + int x = p % gif->dw, y = p / gif->dw; + twin_argb32_t *row = data_row(gif, y, interlace); for (i = 0; i < str_len; i++) { - p = frm_off + entry.length - 1; - x = p % gif->fw; - y = p / gif->fw; - if (interlace) - y = interlaced_line_index((int) gif->fh, y); - gif->frame[(gif->fy + y) * gif->width + gif->fx + x] = entry.suffix; + if (i && --x < 0) { + x = gif->dw - 1; + row = data_row(gif, --y, interlace); + } + if (row && x < gif->fw && entry.suffix != tindex) + row[x] = lut[entry.suffix]; if (entry.prefix == 0xFFF || entry.prefix >= table->n_entries) break; - else - entry = table->entries[entry.prefix]; + entry = table->entries[entry.prefix]; } frm_off += str_len; if (key < table->n_entries - 1 && !is_table_full) table->entries[table->n_entries - 1].suffix = entry.suffix; } - twin_free(table); - if (key == stop) - gif_read(gif->fd, &sub_len, 1); /* Must be zero! */ - lseek(gif->fd, end, SEEK_SET); + /* Skip the sub-blocks an early stop left; 0x1000 already read the + * terminator */ + if (key != 0x1000) + discard_sub_blocks(gif); return 0; } /* Read image. - * Return 0 on success or -1 on out-of-memory (w.r.t. LZW code table_t). + * Return 0 on success or -1 on a bad frame or out-of-memory. */ static int read_image(twin_gif_t *gif) { uint8_t fisrz; - int interlace; - - /* Image Descriptor. */ - gif->fx = read_num(gif->fd); - gif->fy = read_num(gif->fd); - - if (gif->fx >= gif->width || gif->fy >= gif->height) - return -1; - - gif->fw = read_num(gif->fd); - gif->fh = read_num(gif->fd); - - gif->fw = MIN(gif->fw, gif->width - gif->fx); - gif->fh = MIN(gif->fh, gif->height - gif->fy); - gif_read(gif->fd, &fisrz, 1); - interlace = fisrz & 0x40; + /* Image Descriptor. The rectangle is clipped to the canvas, to nothing + * for a frame placed off it: such a frame is still decoded, so the + * stream stays in step, but draws and disposes of no pixel. */ + uint16_t fx = read_num(gif), fy = read_num(gif); + uint16_t fw = read_num(gif), fh = read_num(gif); + const twin_pixmap_t *pix = gif->anim.pixmap; + gif->fx = fx; + gif->fy = fy; + gif->fw = fx < pix->width ? MIN(fw, pix->width - fx) : 0; + gif->fh = fy < pix->height ? MIN(fh, pix->height - fy) : 0; + /* Clipped in either direction, nothing is left: never form row + * pointers past the canvas for a zero-width frame */ + if (!gif->fw || !gif->fh) + gif->fw = gif->fh = 0; + gif->dw = fw; + gif->dh = fh; + + gif_read(gif, &fisrz, 1); /* Ignore Sort Flag. */ - /* Local Color table_t? */ - if (fisrz & 0x80) { - /* Read LCT */ - gif->lct.size = 1 << ((fisrz & 0x07) + 1); - gif_read(gif->fd, gif->lct.colors, 3 * gif->lct.size); + gif->palette = &gif->gct; + if (fisrz & 0x80) { /* Local color table */ + read_palette(gif, &gif->lct, fisrz); gif->palette = &gif->lct; - } else - gif->palette = &gif->gct; - /* Image Data. */ - return read_image_data(gif, interlace); -} - -static void render_frame_rect(twin_gif_t *gif, uint8_t *buffer) -{ - int i, j, k; - uint8_t index, *color; - i = gif->fy * gif->width + gif->fx; - for (j = 0; j < gif->fh; j++) { - for (k = 0; k < gif->fw; k++) { - index = gif->frame[(gif->fy + j) * gif->width + gif->fx + k]; - color = &gif->palette->colors[index * 3]; - if (!gif->gce.transparency || index != gif->gce.tindex) - memcpy(&buffer[(i + k) * 3], color, 3); - } - i += gif->width; } -} -static void dispose(twin_gif_t *gif) -{ - int i, j, k; - uint8_t *bgcolor; - switch (gif->gce.disposal) { - case 2: /* Restore to background color. */ - bgcolor = &gif->palette->colors[gif->bgindex * 3]; - i = gif->fy * gif->width + gif->fx; - for (j = 0; j < gif->fh; j++) { - for (k = 0; k < gif->fw; k++) - memcpy(&gif->canvas[(i + k) * 3], bgcolor, 3); - i += gif->width; + /* Keep what this frame covers for its restore-to-previous disposal */ + if (gif->gce.disposal == 3) { + size_t n = (size_t) gif->fw * gif->fh; + if (n > gif->saved_size) { + void *saved = twin_realloc(gif->saved, n * sizeof(twin_argb32_t)); + if (!saved) + return -1; + gif->saved = saved; + gif->saved_size = n; } - break; - case 3: /* Restore to previous, i.e., don't update canvas.*/ - break; - default: - /* Add frame non-transparent pixels to canvas. */ - render_frame_rect(gif, gif->canvas); + for (int y = 0; y < gif->fh; y++) + memcpy(&gif->saved[y * gif->fw], frame_row(gif, y), + gif->fw * sizeof(twin_argb32_t)); } + /* Image Data. */ + return read_image_data(gif, fisrz & 0x40); } -static void read_plain_text_ext(twin_gif_t *gif) +/* Undo the previous frame as its disposal method asks, before the next one + * is drawn. Restore to background clears to transparent, as browsers do: + * the background color is a hint from before alpha compositing. */ +static void dispose(twin_gif_t *gif, uint8_t disposal) { - /* Discard plain text metadata. */ - lseek(gif->fd, 13, SEEK_CUR); - /* Discard plain text sub-blocks. */ - discard_sub_blocks(gif); + size_t n = gif->fw * sizeof(twin_argb32_t); + + if (disposal == 2) { /* Restore to background */ + for (int y = 0; y < gif->fh; y++) + memset(frame_row(gif, y), 0, n); + } else if (disposal == 3 && gif->saved) { /* Restore to previous */ + for (int y = 0; y < gif->fh; y++) + memcpy(frame_row(gif, y), &gif->saved[y * gif->fw], n); + } } static void read_graphic_control_ext(twin_gif_t *gif) @@ -457,21 +370,14 @@ static void read_graphic_control_ext(twin_gif_t *gif) uint8_t rdit; /* Discard block size (always 0x04). */ - lseek(gif->fd, 1, SEEK_CUR); - gif_read(gif->fd, &rdit, 1); - gif->gce.disposal = (rdit >> 2) & 3; - gif->gce.input = rdit & 2; + gif_skip(gif, 1); + gif_read(gif, &rdit, 1); + gif->gce.disposal = (rdit >> 2) & 7; gif->gce.transparency = rdit & 1; - gif->gce.delay = read_num(gif->fd); - gif_read(gif->fd, &gif->gce.tindex, 1); + gif->gce.delay = read_num(gif); + gif_read(gif, &gif->gce.tindex, 1); /* Skip block terminator. */ - lseek(gif->fd, 1, SEEK_CUR); -} - -static void read_comment_ext(twin_gif_t *gif) -{ - /* Discard comment sub-blocks. */ - discard_sub_blocks(gif); + gif_skip(gif, 1); } static void read_application_ext(twin_gif_t *gif) @@ -480,17 +386,21 @@ static void read_application_ext(twin_gif_t *gif) char app_auth_code[3]; /* Discard block size (always 0x0B). */ - lseek(gif->fd, 1, SEEK_CUR); + gif_skip(gif, 1); /* Application Identifier. */ - gif_read(gif->fd, app_id, 8); + gif_read(gif, app_id, 8); /* Application Authentication Code. */ - gif_read(gif->fd, app_auth_code, 3); + gif_read(gif, app_auth_code, 3); if (!strncmp(app_id, "NETSCAPE", sizeof(app_id))) { /* Discard block size (0x03) and constant byte (0x01). */ - lseek(gif->fd, 2, SEEK_CUR); - gif->loop_count = read_num(gif->fd); + gif_skip(gif, 2); + /* Repeats after the first play, as browsers count; 0 is forever */ + uint16_t count = read_num(gif); + if (!gif->counted) + gif->repeats = count ? count : -1; + gif->counted = true; /* Skip block terminator. */ - lseek(gif->fd, 1, SEEK_CUR); + gif_skip(gif, 1); } else { discard_sub_blocks(gif); } @@ -500,33 +410,38 @@ static void read_ext(twin_gif_t *gif) { uint8_t label; - if (!gif_read(gif->fd, &label, 1)) + if (!gif_read(gif, &label, 1)) return; switch (label) { - case 0x01: - read_plain_text_ext(gif); + case 0x01: /* Plain text: skip its metadata, then the text */ + gif_skip(gif, 13); + discard_sub_blocks(gif); break; case 0xF9: read_graphic_control_ext(gif); break; - case 0xFE: - read_comment_ext(gif); + case 0xFE: /* Comment */ + discard_sub_blocks(gif); break; case 0xFF: read_application_ext(gif); break; default: log_error("Unknown extension: %02X", label); + discard_sub_blocks(gif); } } -/* Return 1 if got a frame; 0 if got GIF trailer; -1 if error. */ +/* Draw the next frame over the canvas. + * Return 1 if got a frame; 0 if got GIF trailer; -1 if error. Only a frame + * disposes of the previous one, so the last frame stays on the canvas. */ static int gif_get_frame(twin_gif_t *gif) { - char sep; + uint8_t disposal = gif->gce.disposal; + uint8_t sep; - dispose(gif); - if (!gif_read(gif->fd, &sep, 1)) + gif->gce = (gif_gce_t) {0}; + if (!gif_read(gif, &sep, 1)) return -1; /* truncated: no trailer */ while (sep != ',') { if (sep == ';') @@ -534,172 +449,131 @@ static int gif_get_frame(twin_gif_t *gif) if (sep != '!') return -1; read_ext(gif); - if (!gif_read(gif->fd, &sep, 1)) + if (!gif_read(gif, &sep, 1)) return -1; } - if (read_image(gif) == -1) + dispose(gif, disposal); + if (read_image(gif) == -1) { + /* Nothing drawn, so nothing to dispose of: the rectangle and the + * saved pixels may still be an earlier frame's */ + gif->gce.disposal = 0; return -1; + } + /* GIF delays are 1/100 s; browsers play shorter ones at 100 ms, which + * also keeps a delay of 0 from decoding frames in a tight loop */ + gif->anim.delay = gif->gce.delay < 2 ? 100 : gif->gce.delay * 10; + gif->frames++; return 1; } -static void gif_render_frame(twin_gif_t *gif, uint8_t *buffer) -{ - memcpy(buffer, gif->canvas, (size_t) gif->width * gif->height * 3); - render_frame_rect(gif, buffer); -} - -static int gif_is_bgcolor(const twin_gif_t *gif, const uint8_t *color) +static void gif_rewind(twin_gif_t *gif) { - return !memcmp(&gif->palette->colors[gif->bgindex * 3], color, 3); + fseek(gif->f, gif->anim_start, SEEK_SET); + gif->gce = (gif_gce_t) {0}; + gif->frames = 0; + twin_pixmap_t *pix = gif->anim.pixmap; + memset(pix->p.v, 0, (size_t) pix->stride * pix->height); } -static void gif_rewind(twin_gif_t *gif) +/* True when the canvas changed */ +static bool gif_advance(twin_animation_t *anim) { - lseek(gif->fd, gif->anim_start, SEEK_SET); + twin_gif_t *gif = (twin_gif_t *) anim; + if (gif->done) + return false; + int got = gif_get_frame(gif); + if (got > 0) + return true; + /* Played out, or a single frame (a still already on the canvas): stop + * reading past the end on every tick */ + if (!gif->repeats || (!got && gif->frames == 1)) { + gif->done = true; + return false; + } + if (gif->repeats > 0) + gif->repeats--; + /* Past the last frame, or a file that went bad since: start over once. + * A failure here holds a blank canvas until the next tick tries again. */ + gif_rewind(gif); + gif_get_frame(gif); + return true; /* rewound: cleared, at least */ } -static void gif_close(twin_gif_t *gif) +static void gif_close(twin_animation_t *anim) { - close(gif->fd); - twin_free(gif->frame); + twin_gif_t *gif = (twin_gif_t *) anim; + fclose(gif->f); + twin_free(gif->table); + twin_free(gif->saved); twin_free(gif); } -static twin_animation_t *_twin_animation_from_gif_file(const char *path) +/* A pixmap holding the first frame, with the animation that redraws it */ +static twin_pixmap_t *gif_open(const char *fname) { - twin_animation_t *anim = twin_malloc(sizeof(twin_animation_t)); - if (!anim) - return NULL; - - twin_gif_t *gif = gif_open(path); - if (!gif) { - twin_free(anim); - return NULL; - } + uint8_t sigver[6]; + uint8_t fdsz; - anim->n_frames = 0; - anim->frames = NULL; - anim->frame_delays = NULL; - anim->loop = gif->loop_count == 0; - anim->width = gif->width; - anim->height = gif->height; - - /* Count the frames; a file truncated or corrupt after some frames - * keeps those, one with none is rejected */ - int frame_count = 0; - while (gif_get_frame(gif) > 0) - frame_count++; - if (!frame_count) { - twin_free(anim); - gif_close(gif); - return NULL; - } - - anim->n_frames = frame_count; - anim->frames = twin_malloc(sizeof(twin_pixmap_t *) * frame_count); - if (!anim->frames) { - twin_free(anim); - gif_close(gif); + twin_gif_t *gif = twin_calloc(1, sizeof(*gif)); + if (!gif) return NULL; - } - anim->frame_delays = twin_malloc(sizeof(twin_time_t) * frame_count); - if (!anim->frame_delays) { - twin_free(anim->frames); - twin_free(anim); - gif_close(gif); + gif->f = fopen(fname, "rb"); + if (!gif->f) { + log_error("Failed to open %s", fname); + twin_free(gif); return NULL; } + setvbuf(gif->f, NULL, _IOFBF, GIF_IO_BUFFER); - gif_rewind(gif); - uint8_t *color, *frame; - frame = twin_malloc((size_t) gif->width * gif->height * 3); - if (!frame) { - twin_free(anim->frame_delays); - twin_free(anim->frames); - twin_free(anim); - gif_close(gif); - return NULL; + /* Header and version */ + gif_read(gif, sigver, 6); + if (memcmp(sigver, "GIF89a", 6) && memcmp(sigver, "GIF87a", 6)) { + log_error("Invalid signature"); + goto fail; } - for (twin_count_t i = 0; i < frame_count; i++) { - anim->frames[i] = - twin_pixmap_create(TWIN_ARGB32, gif->width, gif->height); - if (!anim->frames[i]) { - /* Free previously allocated pixmaps */ - for (twin_count_t j = 0; j < i; j++) - twin_pixmap_destroy(anim->frames[j]); - twin_free(frame); - twin_free(anim->frame_delays); - twin_free(anim->frames); - twin_free(anim); - gif_close(gif); - return NULL; - } - anim->frames[i]->format = TWIN_ARGB32; - - gif_render_frame(gif, frame); - color = frame; - twin_pointer_t p = twin_pixmap_pointer(anim->frames[i], 0, 0); - twin_coord_t row = 0, col = 0; - for (int j = 0; j < gif->width * gif->height; j++) { - uint8_t r = color[0], g = color[1], b = color[2]; - if (!gif_is_bgcolor(gif, color)) - *(p.argb32++) = 0xFF000000U | (r << 16) | (g << 8) | b; - /* Construct background */ - else if (((row >> 3) + (col >> 3)) & 1) - *(p.argb32++) = 0xFFAFAFAFU; - else - *(p.argb32++) = 0xFF7F7F7FU; - col++; - if (col == gif->width) { - row++; - col = 0; - } - /* next palette */ - color += 3; - } - /* GIF delay in units of 1/100 second */ - anim->frame_delays[i] = gif->gce.delay * 10; - gif_get_frame(gif); + /* Width x Height: the canvas, unless a pixmap cannot be that size */ + uint16_t width = read_num(gif), height = read_num(gif); + gif->anim.pixmap = _twin_pixmap_create_sized(TWIN_ARGB32, width, height); + if (!gif->anim.pixmap) { + log_error("Unsupported size %ux%u", width, height); + goto fail; } - anim->iter = _twin_animation_iter_init(anim); - if (!anim->iter) { - /* Free all allocated pixmaps */ - for (twin_count_t i = 0; i < frame_count; i++) - twin_pixmap_destroy(anim->frames[i]); - twin_free(frame); - twin_free(anim->frame_delays); - twin_free(anim->frames); - twin_free(anim); - gif_close(gif); - return NULL; + /* FDSZ */ + gif_read(gif, &fdsz, 1); + /* Presence of GCT */ + if (!(fdsz & 0x80)) { + log_error("No global color table"); + goto fail; } - twin_free(frame); - gif_close(gif); - return anim; + /* Background color index and aspect ratio: unused, transparency + * stands in for the background */ + gif_skip(gif, 2); + read_palette(gif, &gif->gct, fdsz); + gif->repeats = -1; /* forever, until a NETSCAPE block gives a count */ + gif->anim_start = ftell(gif->f); + + gif->table = twin_malloc(sizeof(table_t)); + if (!gif->table) + goto fail; + gif->anim.advance = gif_advance; + gif->anim.close = gif_close; + if (gif_get_frame(gif) <= 0) + goto fail; + gif->anim.pixmap->animation = &gif->anim; + return gif->anim.pixmap; + +fail: + /* The canvas does not own the decoder yet: free them apart */ + if (gif->anim.pixmap) + twin_pixmap_destroy(gif->anim.pixmap); + gif_close(&gif->anim); + return NULL; } twin_pixmap_t *_twin_gif_to_pixmap(const char *filepath, twin_format_t fmt) { - twin_pixmap_t *pix = NULL; - /* Current implementation only produces TWIN_ARGB32 */ if (fmt != TWIN_ARGB32) return NULL; - FILE *infile = fopen(filepath, "rb"); - if (!infile) { - log_error("Failed to open %s", filepath); - return NULL; - } - twin_animation_t *gif = _twin_animation_from_gif_file(filepath); - if (gif) { - /* The pixmap owns the animation from here on */ - pix = twin_pixmap_create(fmt, gif->width, gif->height); - if (pix) - pix->animation = gif; - else - _twin_animation_destroy(gif); - } - fclose(infile); - - return pix; + return gif_open(filepath); } diff --git a/src/image-jpeg.c b/src/image-jpeg.c index 16a33738..5cba46fb 100644 --- a/src/image-jpeg.c +++ b/src/image-jpeg.c @@ -43,7 +43,8 @@ static void twin_jpeg_error_exit(j_common_ptr cinfo) twin_pixmap_t *_twin_jpeg_to_pixmap(const char *filepath, twin_format_t fmt) { - twin_pixmap_t *pix = NULL; + /* Assigned after setjmp(): volatile so the error path sees it */ + twin_pixmap_t *volatile pix = NULL; /* Current implementation only produces TWIN_ARGB32 and TWIN_A8 */ if (fmt != TWIN_ARGB32 && fmt != TWIN_A8) @@ -75,23 +76,20 @@ twin_pixmap_t *_twin_jpeg_to_pixmap(const char *filepath, twin_format_t fmt) (void) jpeg_read_header(&cinfo, true); /* Configure */ - twin_coord_t width = cinfo.image_width, height = cinfo.image_height; if (fmt == TWIN_ARGB32) cinfo.out_color_space = JCS_RGB; else cinfo.out_color_space = JCS_GRAYSCALE; /* Allocate pixmap */ - pix = twin_pixmap_create(fmt, width, height); + pix = _twin_pixmap_create_sized(fmt, cinfo.image_width, cinfo.image_height); if (!pix) longjmp(jerr.jbuf, 1); /* Start decompressing */ (void) jpeg_start_decompress(&cinfo); - if ((fmt == TWIN_A8 && cinfo.output_components != 1) || - (fmt == TWIN_ARGB32 && - (cinfo.output_components != 3 && cinfo.output_components != 4))) + if (cinfo.output_components != (fmt == TWIN_A8 ? 1 : 3)) longjmp(jerr.jbuf, 1); int rowstride = cinfo.output_width * cinfo.output_components; @@ -103,11 +101,11 @@ twin_pixmap_t *_twin_jpeg_to_pixmap(const char *filepath, twin_format_t fmt) while (cinfo.output_scanline < cinfo.output_height) { twin_pointer_t p = twin_pixmap_pointer(pix, 0, cinfo.output_scanline); (void) jpeg_read_scanlines(&cinfo, rowbuf, 1); - if (fmt == TWIN_A8 || cinfo.output_components == 4) - memcpy(p.a8, rowbuf, rowstride); + JSAMPLE *s = rowbuf[0]; + if (fmt == TWIN_A8) + memcpy(p.a8, s, rowstride); else { - JSAMPLE *s = *rowbuf; - for (int i = 0; i < width; i++) { + for (JDIMENSION i = 0; i < cinfo.output_width; i++) { uint32_t r = *(s++); uint32_t g = *(s++); uint32_t b = *(s++); diff --git a/src/image-png.c b/src/image-png.c index b81c04f5..91512e1b 100644 --- a/src/image-png.c +++ b/src/image-png.c @@ -11,43 +11,20 @@ #include "twin_private.h" -/* libpng hands out straight alpha; twin composites premultiplied ARGB. */ -static twin_argb32_t _twin_apply_alpha(twin_argb32_t v) -{ - uint16_t t1, t2, t3; -#if defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ - twin_a8_t alpha = twin_get_8(v, 0); -#else - twin_a8_t alpha = twin_get_8(v, 24); -#endif - - /* clear RGB data if alpha is zero */ - if (!alpha) - return 0; - -#if defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ - return (uint32_t) alpha << 24 | - twin_int_mult(twin_get_8(v, 24), alpha, t1) << 16 | - twin_int_mult(twin_get_8(v, 16), alpha, t2) << 8 | - twin_int_mult(twin_get_8(v, 8), alpha, t3) << 0; -#else - return (uint32_t) alpha << 24 | - twin_int_mult(twin_get_8(v, 0), alpha, t1) << 16 | - twin_int_mult(twin_get_8(v, 8), alpha, t2) << 8 | - twin_int_mult(twin_get_8(v, 16), alpha, t3) << 0; -#endif -} - +/* libpng hands out straight alpha; twin composites premultiplied ARGB. + * Pixels arrive in native ARGB order, so only the channels are scaled. */ static void twin_premultiply_alpha(twin_pixmap_t *px) { - if (px->format != TWIN_ARGB32) - return; - for (twin_coord_t y = 0; y < px->height; y++) { - twin_pointer_t p = {.b = px->p.b + y * px->stride}; - - for (twin_coord_t x = 0; x < px->width; x++) - p.argb32[x] = _twin_apply_alpha(p.argb32[x]); + twin_argb32_t *p = twin_pixmap_pointer(px, 0, y).argb32; + + for (twin_coord_t x = 0; x < px->width; x++) { + twin_argb32_t v = p[x]; + twin_a8_t alpha = v >> 24; + uint16_t t1, t2, t3; + p[x] = (uint32_t) alpha << 24 | twin_in(v, 16, alpha, t1) | + twin_in(v, 8, alpha, t2) | twin_in(v, 0, alpha, t3); + } } } @@ -63,25 +40,6 @@ static void _twin_png_read(png_structp png, png_bytep data, png_size_t size) png_error(png, "end of file !\n"); } -/* FIXME: Utilize pixman or similar accelerated routine to convert */ -#if defined(__APPLE__) -static void _convertBGRtoARGB(uint8_t *data, int width, int height) -{ - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - int index = (y * width + x) * 4; - uint8_t b = data[index]; - uint8_t g = data[index + 1]; - uint8_t r = data[index + 2]; - data[index] = 255; /* Alpha */ - data[index + 1] = r; - data[index + 2] = g; - data[index + 3] = b; - } - } -} -#endif - twin_pixmap_t *_twin_png_to_pixmap(const char *filepath, twin_format_t fmt) { uint8_t signature[8]; @@ -90,7 +48,7 @@ twin_pixmap_t *_twin_png_to_pixmap(const char *filepath, twin_format_t fmt) /* Assigned after setjmp(): volatile so the error path sees them */ twin_pixmap_t *volatile pix = NULL; png_bytep *volatile rowp = NULL; - int depth, ctype, interlace; + int depth, ctype; int fd = open(filepath, O_RDONLY); if (fd < 0) @@ -124,48 +82,51 @@ twin_pixmap_t *_twin_png_to_pixmap(const char *filepath, twin_format_t fmt) png_read_info(png, info); png_uint_32 width, height; - png_get_IHDR(png, info, &width, &height, &depth, &ctype, &interlace, NULL, - NULL); - if (!width || !height || width > INT16_MAX || height > INT16_MAX) - goto bail_free; + png_get_IHDR(png, info, &width, &height, &depth, &ctype, NULL, NULL, NULL); if (depth == 16) png_set_strip_16(png); - if (ctype == PNG_COLOR_TYPE_PALETTE) - png_set_palette_to_rgb(png); if (ctype == PNG_COLOR_TYPE_GRAY && depth < 8) png_set_expand_gray_1_2_4_to_8(png); - if (png_get_valid(png, info, PNG_INFO_tRNS)) - png_set_tRNS_to_alpha(png); - - png_get_IHDR(png, info, &width, &height, &depth, &ctype, &interlace, NULL, - NULL); switch (fmt) { case TWIN_A8: - if (ctype != PNG_COLOR_TYPE_GRAY || depth != 8) + if (ctype != PNG_COLOR_TYPE_GRAY) goto bail_free; break; case TWIN_RGB16: /* unsupported for now */ goto bail_free; case TWIN_ARGB32: - png_set_filler(png, 0xff, PNG_FILLER_AFTER); + if (ctype == PNG_COLOR_TYPE_PALETTE) + png_set_palette_to_rgb(png); + if (png_get_valid(png, info, PNG_INFO_tRNS)) + png_set_tRNS_to_alpha(png); if (ctype == PNG_COLOR_TYPE_GRAY || ctype == PNG_COLOR_TYPE_GRAY_ALPHA) png_set_gray_to_rgb(png); - - png_get_IHDR(png, info, &width, &height, &depth, &ctype, &interlace, - NULL, NULL); - - if (depth != 8) - goto bail_free; + /* Native twin_argb32_t: bytes B, G, R, A on little endian, + * A, R, G, B on big endian */ +#if defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ + png_set_swap_alpha(png); + png_set_filler(png, 0xff, PNG_FILLER_BEFORE); +#else + png_set_bgr(png); + png_set_filler(png, 0xff, PNG_FILLER_AFTER); +#endif break; } + png_set_interlace_handling(png); + + /* IHDR keeps the file's layout until the transforms are applied; check + * what libpng will actually write against the pixmap's row size */ + png_read_update_info(png, info); + if (png_get_rowbytes(png, info) != width * twin_bytes_per_pixel(fmt)) + goto bail_free; rowp = twin_malloc((size_t) height * sizeof(png_bytep)); if (!rowp) goto bail_free; - pix = twin_pixmap_create(fmt, (twin_coord_t) width, (twin_coord_t) height); + pix = _twin_pixmap_create_sized(fmt, width, height); if (!pix) goto bail_free; /* libpng writes width * twin_bytes_per_pixel(fmt) bytes per row; rows @@ -177,13 +138,10 @@ twin_pixmap_t *_twin_png_to_pixmap(const char *filepath, twin_format_t fmt) png_read_end(png, NULL); - if (fmt == TWIN_ARGB32) { - /* Convert from BGR to ARGB if necessary */ -#if defined(__APPLE__) - _convertBGRtoARGB(pix->p.b, width, height); -#endif + /* Opaque images (filler, not alpha) are final as read */ + if (fmt == TWIN_ARGB32 && + (png_get_color_type(png, info) & PNG_COLOR_MASK_ALPHA)) twin_premultiply_alpha(pix); - } goto bail_free; bail_free: diff --git a/src/image-tvg.c b/src/image-tvg.c index 8363b796..010fd001 100644 --- a/src/image-tvg.c +++ b/src/image-tvg.c @@ -22,32 +22,6 @@ (((uint32_t) (a) << 24) | ((uint32_t) (r) << 16) | ((uint32_t) (g) << 8) | \ (b)) #define MIN(A, B) ((A) < (B) ? (A) : (B)) -#define ALIGN_UP(sz, alignment) \ - (((alignment) & ((alignment) - 1)) == 0 \ - ? (((sz) + (alignment) - 1) & ~((alignment) - 1)) \ - : ((((sz) + (alignment) - 1) / (alignment)) * (alignment))) - -static bool tvg_pixmap_alloc_size(twin_format_t fmt, - uint32_t width, - uint32_t height, - size_t *size_out) -{ - if (width == 0 || height == 0) - return false; - - size_t bpp = (size_t) twin_bytes_per_pixel(fmt); - if (bpp == 0 || (size_t) width > SIZE_MAX / bpp) - return false; - size_t stride = bpp * (size_t) width; - if (stride > SIZE_MAX - 3) - return false; - stride = ALIGN_UP(stride, 4); - if ((size_t) height > (SIZE_MAX - sizeof(twin_pixmap_t)) / stride) - return false; - - *size_out = sizeof(twin_pixmap_t) + stride * (size_t) height; - return true; -} enum { /* end of document This command determines the end of file. */ @@ -432,11 +406,16 @@ static tvg_result_t tvg_read_varuint(tvg_context_t *ctx, uint32_t *out_value) size_t read = 0; while (true) { READ_VALUE(read, &byte, 1); + /* The fifth group holds bits 28 to 31 only */ + if (count == 4 && (byte & 0x70)) + return TVG_E_INVALID_FORMAT; const uint32_t val = ((uint32_t) (byte & 0x7F)) << (7 * count); result |= val; if ((byte & 0x80) == 0) break; - ++count; + /* 32 bits take at most 5 groups of 7 */ + if (++count > 4) + return TVG_E_INVALID_FORMAT; } *out_value = result; return TVG_SUCCESS; @@ -494,10 +473,10 @@ static tvg_result_t tvg_parse_header(tvg_context_t *ctx, int dim_only) uint32_t color_count; res = tvg_read_varuint(ctx, &color_count); __return_val_if_fail(res, "Failed to read varuint"); - if (color_count == 0) { + if (!color_count) { return TVG_E_INVALID_FORMAT; } - ctx->colors = twin_malloc(color_count * sizeof(twin_argb32_t)); + ctx->colors = twin_calloc(color_count, sizeof(twin_argb32_t)); if (!ctx->colors) { return TVG_E_OUT_OF_MEMORY; } @@ -513,6 +492,18 @@ static tvg_result_t tvg_parse_header(tvg_context_t *ctx, int dim_only) return TVG_SUCCESS; } +/* A palette index, checked against the color table it indexes */ +static tvg_result_t tvg_read_color_index(tvg_context_t *ctx, uint32_t *out) +{ + tvg_result_t res = tvg_read_varuint(ctx, out); + __return_val_if_fail(res, "Failed to read varuint"); + if (*out >= ctx->colors_size) { + log_error("Invalid color index"); + return TVG_E_INVALID_FORMAT; + } + return TVG_SUCCESS; +} + static tvg_result_t tvg_parse_gradient(tvg_context_t *ctx, tvg_gradient_t *out_gradient) { @@ -524,19 +515,11 @@ static tvg_result_t tvg_parse_gradient(tvg_context_t *ctx, res = tvg_read_point(ctx, &pt); __return_val_if_fail(res, "Failed to read point"); out_gradient->point1 = pt; - res = tvg_read_varuint(ctx, &u32); - __return_val_if_fail(res, "Failed to read varuint"); + res = tvg_read_color_index(ctx, &u32); + __return_val_if_fail(res, "Failed to read color index"); out_gradient->color0 = u32; - if (u32 > ctx->colors_size) { - log_error("Invalid color index"); - return TVG_E_INVALID_FORMAT; - } - res = tvg_read_varuint(ctx, &u32); - __return_val_if_fail(res, "Failed to read varuint"); - if (u32 > ctx->colors_size) { - log_error("Invalid color index"); - return TVG_E_INVALID_FORMAT; - } + res = tvg_read_color_index(ctx, &u32); + __return_val_if_fail(res, "Failed to read color index"); out_gradient->color1 = u32; return TVG_SUCCESS; } @@ -551,8 +534,8 @@ static tvg_result_t tvg_parse_style(tvg_context_t *ctx, out_style->kind = kind; switch (kind) { case TVG_STYLE_FLAT: - res = tvg_read_varuint(ctx, &flat); - __return_val_if_fail(res, "Failed to read varuint"); + res = tvg_read_color_index(ctx, &flat); + __return_val_if_fail(res, "Failed to read color index"); out_style->flat = flat; break; case TVG_STYLE_LINEAR: @@ -842,12 +825,11 @@ static tvg_result_t tvg_parse_line_fill_rectangles( return TVG_SUCCESS; } -static tvg_result_t tvg_parse_fill_paths(tvg_context_t *ctx, - size_t size, - const tvg_style_t *style) +/* Read @size path lengths, then that many paths into ctx->path */ +static tvg_result_t tvg_parse_paths(tvg_context_t *ctx, size_t size) { tvg_result_t res = TVG_SUCCESS; - uint32_t *sizes = twin_malloc(size * sizeof(uint32_t)); + uint32_t *sizes = twin_calloc(size, sizeof(uint32_t)); if (!sizes) { return TVG_E_OUT_OF_MEMORY; } @@ -856,48 +838,39 @@ static tvg_result_t tvg_parse_fill_paths(tvg_context_t *ctx, ++sizes[i]; __goto_if_fail(res, error, "Failed to read varuint"); } - twin_path_t *path = ctx->path; - /* parse path */ for (size_t i = 0; i < size; ++i) { res = tvg_parse_path(ctx, sizes[i]); __goto_if_fail(res, error, "Failed to parse path"); } - _fill_path_with_style(ctx, style); - twin_path_empty(path); error: twin_free(sizes); return res; } +static tvg_result_t tvg_parse_fill_paths(tvg_context_t *ctx, + size_t size, + const tvg_style_t *style) +{ + tvg_result_t res = tvg_parse_paths(ctx, size); + __return_val_if_fail(res, "Failed to parse paths"); + _fill_path_with_style(ctx, style); + twin_path_empty(ctx->path); + return TVG_SUCCESS; +} + static tvg_result_t tvg_parse_line_paths(tvg_context_t *ctx, size_t size, const tvg_style_t *line_style, twin_fixed_t line_width) { - tvg_result_t res = TVG_SUCCESS; - uint32_t *sizes = twin_malloc(size * sizeof(uint32_t)); - if (!sizes) { - return TVG_E_OUT_OF_MEMORY; - } - twin_path_t *path = ctx->path; - for (size_t i = 0; i < size; ++i) { - res = tvg_read_varuint(ctx, &sizes[i]); - ++sizes[i]; - __goto_if_fail(res, error, "Failed to read varuint"); - } - /* parse path */ - for (size_t i = 0; i < size; ++i) { - res = tvg_parse_path(ctx, sizes[i]); - __goto_if_fail(res, error, "Failed to parse path"); - } + tvg_result_t res = tvg_parse_paths(ctx, size); + __return_val_if_fail(res, "Failed to parse paths"); if (line_width == 0) { line_width = TVG_HAIRLINE; } _stroke_path_with_style(ctx, line_style, line_width); - twin_path_empty(path); -error: - twin_free(sizes); - return res; + twin_path_empty(ctx->path); + return TVG_SUCCESS; } static tvg_result_t tvg_parse_line_fill_paths(tvg_context_t *ctx, @@ -906,32 +879,15 @@ static tvg_result_t tvg_parse_line_fill_paths(tvg_context_t *ctx, const tvg_style_t *line_style, twin_fixed_t line_width) { - tvg_result_t res = TVG_SUCCESS; - uint32_t *sizes = twin_malloc(size * sizeof(uint32_t)); - if (!sizes) { - return TVG_E_OUT_OF_MEMORY; - } - for (size_t i = 0; i < size; ++i) { - res = tvg_read_varuint(ctx, &sizes[i]); - ++sizes[i]; - __goto_if_fail(res, error, "Failed to read varuint"); - } - twin_path_t *path = ctx->path; - - /* parse path */ - for (size_t i = 0; i < size; ++i) { - res = tvg_parse_path(ctx, sizes[i]); - __goto_if_fail(res, error, "Failed to parse path"); - } + tvg_result_t res = tvg_parse_paths(ctx, size); + __return_val_if_fail(res, "Failed to parse paths"); if (line_width == 0) { line_width = TVG_THIN_LINE; } _fill_path_with_style(ctx, fill_style); _stroke_path_with_style(ctx, line_style, line_width); - twin_path_empty(path); -error: - twin_free(sizes); - return res; + twin_path_empty(ctx->path); + return TVG_SUCCESS; } static tvg_result_t tvg_parse_fill_polygon(tvg_context_t *ctx, @@ -1137,14 +1093,12 @@ static tvg_result_t tvg_document_dimensions(tvg_input_func_t in, uint32_t *out_height) { /* initialize the context */ - tvg_context_t ctx; + tvg_context_t ctx = {0}; if (!in) { return TVG_E_INVALID_ARG; } ctx.in = in; ctx.in_state = in_state; - ctx.colors = NULL; - ctx.colors_size = 0; /* parse the header, early outing before the color table */ tvg_result_t res = tvg_parse_header(&ctx, 1); __return_val_if_fail(res, "Failed to parse header"); @@ -1160,15 +1114,13 @@ static tvg_result_t tvg_render_document(tvg_input_func_t in, twin_fixed_t height_scale) { /* initialize the context */ - tvg_context_t ctx; + tvg_context_t ctx = {0}; /* the error path frees what was set up */ if (!in) { return TVG_E_INVALID_ARG; } ctx.in = in; ctx.in_state = in_state; ctx.pixmap = pix; - ctx.colors = NULL; - ctx.colors_size = 0; /* parse the header */ tvg_result_t res = tvg_parse_header(&ctx, 0); __goto_if_fail(res, error, "Failed to parse header"); @@ -1207,7 +1159,7 @@ static bool tvg_fit_budget(twin_format_t fmt, size_t budget) { size_t size; - if (tvg_pixmap_alloc_size(fmt, *w, *h, &size) && size <= budget) + if (_twin_pixmap_alloc_size(fmt, *w, *h, &size) && size <= budget) return true; uint32_t lo = 1, hi = *h, best_w = 0, best_h = 0; while (lo <= hi) { @@ -1215,7 +1167,8 @@ static bool tvg_fit_budget(twin_format_t fmt, uint32_t mid_w = (uint32_t) (((uint64_t) *w * mid_h) / *h); if (mid_w < 1) mid_w = 1; - if (tvg_pixmap_alloc_size(fmt, mid_w, mid_h, &size) && size <= budget) { + if (_twin_pixmap_alloc_size(fmt, mid_w, mid_h, &size) && + size <= budget) { best_w = mid_w; best_h = mid_h; lo = mid_h + 1; diff --git a/src/pixmap.c b/src/pixmap.c index f359d5ae..28828bc0 100644 --- a/src/pixmap.c +++ b/src/pixmap.c @@ -15,28 +15,21 @@ ? (((sz) + (alignment) - 1) & ~((alignment) - 1)) \ : ((((sz) + (alignment) - 1) / (alignment)) * (alignment))) -static bool twin_pixmap_alloc_size(twin_format_t format, - twin_coord_t width, - twin_coord_t height, - size_t *size_out) -{ - if (width <= 0 || height <= 0) - return false; - - size_t bpp = (size_t) twin_bytes_per_pixel(format); - if (bpp == 0 || (size_t) width > SIZE_MAX / bpp) - return false; - size_t stride = bpp * (size_t) width; - if (stride > SIZE_MAX - 3) +/* Bytes for a pixmap of @width x @height: false if it is empty, or a side + * or the stride overflows twin_coord_t. Sides are uint32_t so callers can + * pass sizes before narrowing them; a negative twin_coord_t converts past + * INT16_MAX. With every term that small, the size_t sums cannot wrap. */ +bool _twin_pixmap_alloc_size(twin_format_t format, + uint32_t width, + uint32_t height, + size_t *size_out) +{ + if (!width || !height || width > INT16_MAX || height > INT16_MAX) return false; - stride = ALIGN_UP(stride, 4); - /* Stride must fit in twin_coord_t (int16_t) for the pixmap struct. */ + size_t stride = ALIGN_UP((size_t) twin_bytes_per_pixel(format) * width, 4); if (stride > INT16_MAX) return false; - if ((size_t) height > (SIZE_MAX - sizeof(twin_pixmap_t)) / stride) - return false; - - *size_out = sizeof(twin_pixmap_t) + stride * (size_t) height; + *size_out = sizeof(twin_pixmap_t) + stride * height; return true; } @@ -63,25 +56,33 @@ void _twin_pixmap_init(twin_pixmap_t *pixmap, pixmap->p.v = pixels; } -twin_pixmap_t *twin_pixmap_create(twin_format_t format, - twin_coord_t width, - twin_coord_t height) +twin_pixmap_t *_twin_pixmap_create_sized(twin_format_t format, + uint32_t width, + uint32_t height) { size_t size; - if (!twin_pixmap_alloc_size(format, width, height, &size)) + if (!_twin_pixmap_alloc_size(format, width, height, &size)) return NULL; - twin_coord_t stride = (twin_coord_t) ALIGN_UP( - (size_t) twin_bytes_per_pixel(format) * width, 4); + twin_coord_t stride = + (twin_coord_t) ((size - sizeof(twin_pixmap_t)) / height); twin_pixmap_t *pixmap = twin_malloc(size); if (!pixmap) return NULL; - _twin_pixmap_init(pixmap, format, width, height, stride, pixmap + 1); + _twin_pixmap_init(pixmap, format, (twin_coord_t) width, + (twin_coord_t) height, stride, pixmap + 1); memset(pixmap->p.v, 0, size - sizeof(twin_pixmap_t)); return pixmap; } +twin_pixmap_t *twin_pixmap_create(twin_format_t format, + twin_coord_t width, + twin_coord_t height) +{ + return _twin_pixmap_create_sized(format, width, height); +} + twin_pixmap_t *_twin_pixmap_create_stub(twin_format_t format, twin_coord_t width, twin_coord_t height) diff --git a/src/twin_private.h b/src/twin_private.h index f6f3cec3..83e7d367 100644 --- a/src/twin_private.h +++ b/src/twin_private.h @@ -60,31 +60,17 @@ typedef union _twin_pointer { } twin_pointer_t; /** - * Animation iterator for frame-by-frame playback + * Animation that redraws its pixmap in place * - * Iterator state for stepping through animation frames. - * Maintains current position and timing information. - */ -typedef struct _twin_animation_iter { - twin_animation_t *anim; /**< Animation being iterated */ - twin_count_t current_index; /**< Current frame index */ - twin_pixmap_t *current_frame; /**< Current frame pixmap */ - twin_time_t current_delay; /**< Current frame delay time */ -} twin_animation_iter_t; - -/** - * Animation structure for frame-based animations - * - * Contains all data needed for frame-based animations including - * timing, frame data, and playback state. + * The decoder embeds this as its first member: @advance draws the next frame + * into @pixmap and sets @delay, @close frees the decoder (not the pixmap, + * which owns the animation). */ struct _twin_animation { - twin_pixmap_t **frames; /**< Array of frame pixmaps */ - twin_count_t n_frames; /**< Number of frames */ - twin_time_t *frame_delays; /**< Per-frame timing in milliseconds */ - bool loop; /**< Loop animation flag */ - twin_animation_iter_t *iter; /**< Playback iterator */ - twin_coord_t width, height; /**< Animation dimensions in pixels */ + twin_pixmap_t *pixmap; /**< Pixmap showing the current frame */ + twin_time_t delay; /**< Current frame's display time in ms */ + bool (*advance)(twin_animation_t *anim); /* false: nothing changed */ + void (*close)(twin_animation_t *anim); }; /** @@ -1320,6 +1306,18 @@ void _twin_pixmap_unlink(twin_pixmap_t *pixmap); /* Drop the screen's focus and pointer references to @pixmap */ void _twin_screen_forget(twin_screen_t *screen, const twin_pixmap_t *pixmap); +/* Bytes a @width x @height pixmap of @format needs; false if it cannot exist */ +bool _twin_pixmap_alloc_size(twin_format_t format, + uint32_t width, + uint32_t height, + size_t *size_out); + +/* twin_pixmap_create() for sizes read from a file: anything that does not + * fit a pixmap, negative or past INT16_MAX, is refused, never narrowed */ +twin_pixmap_t *_twin_pixmap_create_sized(twin_format_t format, + uint32_t width, + uint32_t height); + /* Pixmap header with no pixel storage; every drawing operation clips out */ twin_pixmap_t *_twin_pixmap_create_stub(twin_format_t format, twin_coord_t width, @@ -1544,7 +1542,6 @@ static inline int twin_ucs_char_in_page(uint32_t ucs4) /* Animations belong to their pixmap: twin_pixmap_destroy() frees them */ void _twin_animation_destroy(twin_animation_t *anim); -twin_animation_iter_t *_twin_animation_iter_init(twin_animation_t *anim); /* Title-bar icons of the desktop policy (built with CONFIG_WM_DESKTOP) */ typedef enum _twin_icon { diff --git a/tests/test-core.c b/tests/test-core.c index 5b615330..80a3e4e1 100644 --- a/tests/test-core.c +++ b/tests/test-core.c @@ -29,6 +29,7 @@ static twin_pixmap_t *solid(twin_format_t fmt, return p; } + static void test_damage_merge(void) { th_t *t = th_create(W, H); @@ -1138,6 +1139,10 @@ static void test_accessors(void) "screen size"); CHECK(twin_screen_stats(t->screen) == &t->screen->stats, "screen stats"); + CHECK(!twin_pixmap_create(TWIN_ARGB32, 1, -1) && + !twin_pixmap_create(TWIN_ARGB32, -1, 1), + "negative pixmap sizes rejected"); + twin_pixmap_t *p = twin_pixmap_create(TWIN_RGB16, 7, 5); twin_coord_t stride = 0; CHECK(twin_pixmap_width(p) == 7 && twin_pixmap_height(p) == 5 && @@ -1333,7 +1338,8 @@ static void test_box_background(void) } #endif -#if defined(CONFIG_LOADER_GIF) || defined(CONFIG_LOADER_TVG) +#if defined(CONFIG_LOADER_GIF) || defined(CONFIG_LOADER_TVG) || \ + defined(CONFIG_LOADER_PNG) /* Write @len bytes to a temporary file named in @path ("...XXXXXX"); a * fixture that cannot be written fails the run rather than passing as a * loader's rejection */ @@ -1382,13 +1388,49 @@ static void test_tvg_hairline(void) inked); twin_pixmap_destroy(p); } + +/* A palette index past the color table is rejected, not read out of + * bounds; so is one equal to the table size */ +static void test_tvg_color_index(void) +{ + uint8_t doc[] = { + 0x72, 0x56, 1, 0x44, 2, 2, 1, 0, 0, 0, 255, /* as above */ + 0x04, 0, 1, 0, /* color 1 of 1 */ + 2, 16, 30, 16, 0x00, + }; + char path[] = "/tmp/mado-tvg-XXXXXX"; + if (!write_fixture(path, doc, sizeof(doc))) + return; + twin_pixmap_t *p = twin_tvg_to_pixmap(path, 20, 20); + unlink(path); + CHECK(!p, "TVG color index past the table rejected"); + if (p) + twin_pixmap_destroy(p); + + /* A varuint's fifth byte carries bits 28 to 31 only: a color count of + * 1 + 2^32 must not alias to one color */ + uint8_t wide[] = { + 0x72, 0x56, 1, 0x44, 2, 2, /* header, 2x2 */ + 0x81, 0x80, 0x80, 0x80, 0x10, /* 1 + 2^32 colors */ + 0, 0, 0, 255, 0x00, /* one color, end */ + }; + char wide_path[] = "/tmp/mado-tvg-XXXXXX"; + if (!write_fixture(wide_path, wide, sizeof(wide))) + return; + p = twin_tvg_to_pixmap(wide_path, 20, 20); + unlink(wide_path); + CHECK(!p, "TVG varuint past 32 bits rejected"); + if (p) + twin_pixmap_destroy(p); +} + #endif -#if defined(CONFIG_LOADER_GIF) -/* Load @len bytes as a file; NULL is the only acceptable answer here */ +#if defined(CONFIG_LOADER_GIF) || defined(CONFIG_LOADER_PNG) +/* Load @len bytes as a file */ static twin_pixmap_t *load_bytes(const uint8_t *bytes, size_t len) { - char path[] = "/tmp/mado-gif-XXXXXX"; + char path[] = "/tmp/mado-img-XXXXXX"; if (!write_fixture(path, bytes, len)) return NULL; twin_pixmap_t *p = twin_pixmap_from_file(path); @@ -1396,6 +1438,175 @@ static twin_pixmap_t *load_bytes(const uint8_t *bytes, size_t len) return p; } +static twin_argb32_t pixel_at(twin_pixmap_t *p, int x, int y) +{ + return *twin_pixmap_pointer(p, x, y).argb32; +} +#endif + +#if defined(CONFIG_LOADER_PNG) +/* 16-bit RGB decodes (it was rejected before the transforms were checked), + * in ARGB order on every host */ +static void test_png_16bit(void) +{ + /* 2x1, 16 bits per channel: (253, 161, 6) and (200, 200, 200) */ + const uint8_t png[] = { + 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, + 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, + 0x00, 0x01, 0x10, 0x02, 0x00, 0x00, 0x00, 0x2b, 0xd0, 0x34, 0x9e, + 0x00, 0x00, 0x00, 0x12, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9c, 0x63, + 0xf8, 0xfb, 0x77, 0xe1, 0x42, 0x36, 0xb6, 0x13, 0x60, 0x00, 0x00, + 0x33, 0x7d, 0x07, 0xf9, 0x12, 0xbf, 0xb4, 0x11, 0x00, 0x00, 0x00, + 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82, + }; + twin_pixmap_t *p = load_bytes(png, sizeof(png)); + CHECK(p != NULL, "16-bit PNG loads"); + if (!p) + return; + CHECK_EQ(pixel_at(p, 0, 0), 0xfffda106, "16-bit PNG pixel 0"); + CHECK_EQ(pixel_at(p, 1, 0), 0xffc8c8c8, "16-bit PNG pixel 1"); + twin_pixmap_destroy(p); +} +#endif + +#if defined(CONFIG_LOADER_GIF) +/* Frames draw into the pixmap itself: transparent pixels stay clear, a + * two-row interlaced frame lands on its own rows, and looping restarts + * from a clear canvas */ +static void test_gif_frames(void) +{ + /* 1x2, palette black/white, every frame transparent on index 0 */ + const uint8_t gif[] = { + 'G', 'I', 'F', '8', '9', 'a', 1, 0, 2, 0, + 0x80, 0, 0, /* screen */ + 0, 0, 0, 0xff, 0xff, 0xff, /* GCT */ + '!', 0xf9, 4, 0x01, 1, 0, 0, 0, /* GCE */ + ',', 0, 0, 0, 0, 1, 0, 2, 0, 0x40, /* interlaced 1x2 */ + 2, 2, 0x44, 0x0a, 0, /* rows: 0, 1 */ + '!', 0xf9, 4, 0x01, 1, 0, 0, 0, /* GCE */ + ',', 0, 0, 0, 0, 1, 0, 2, 0, 0, /* 1x2 */ + 2, 2, 0x4c, 0x0a, 0, /* rows: 1, 1 */ + ';', + }; + twin_pixmap_t *p = load_bytes(gif, sizeof(gif)); + CHECK(p != NULL, "animated GIF loads"); + if (!p) + return; + twin_animation_t *a = twin_pixmap_animation(p); + CHECK(a && twin_animation_get_current_frame(a) == p, + "the pixmap shows the current frame"); + CHECK_EQ(pixel_at(p, 0, 0), 0, "transparent index left clear"); + CHECK_EQ(pixel_at(p, 0, 1), 0xffffffff, "interlaced row 1 in place"); + CHECK_EQ(twin_animation_get_current_delay(a), 100, + "1/100 s plays at 100 ms, as browsers do"); + twin_animation_advance_frame(a); + CHECK_EQ(pixel_at(p, 0, 0), 0xffffffff, "second frame drawn over"); + twin_animation_advance_frame(a); + CHECK_EQ(pixel_at(p, 0, 0), 0, "loop restarts from a clear canvas"); + twin_pixmap_destroy(p); + + /* A restore-to-previous first frame with a bad code size: rejected, + * and the saved canvas it allocated is freed with the decoder */ + uint8_t bad[sizeof(gif)]; + memcpy(bad, gif, sizeof(gif)); + bad[22] = 0x0c; /* GCE: disposal 3 */ + bad[37] = 0; /* LZW minimum code size 0 */ + size_t before = th_heap_raw(); + p = load_bytes(bad, sizeof(bad)); + CHECK(!p, "undecodable restore-to-previous frame rejected"); + CHECK_EQ(th_heap_raw(), before, "failed GIF open frees its decoder"); + if (p) + twin_pixmap_destroy(p); + + /* NETSCAPE count 1: one repeat after the first play, as browsers + * count, then the last frame stays */ + uint8_t twice[sizeof(gif) + 19]; + memcpy(twice, gif, 19); /* screen and global color table */ + memcpy(twice + 19, + (const uint8_t[]) {'!', 0xff, 11, 'N', 'E', 'T', 'S', 'C', 'A', 'P', + 'E', '2', '.', '0', 3, 1, 1, 0, 0}, + 19); + memcpy(twice + 38, gif + 19, sizeof(gif) - 19); + p = load_bytes(twice, sizeof(twice)); + CHECK(p != NULL, "counted GIF loads"); + if (p) { + a = twin_pixmap_animation(p); + twin_argb32_t seen[6]; + for (int i = 0; i < 6; i++) { + seen[i] = pixel_at(p, 0, 0); + twin_animation_advance_frame(a); + } + CHECK(!seen[0] && seen[1] && !seen[2] && seen[3] && seen[4] && seen[5], + "count 1 plays twice, then holds the last frame"); + + /* Played out, it draws nothing more, so a shown canvas is not + * repainted on every tick */ + th_t *t = th_create(16, 16); + twin_pixmap_show(p, t->screen, NULL); + th_frame(t); + twin_animation_advance_frame(a); + CHECK(!twin_screen_damaged(t->screen), + "a finished animation damages nothing"); + twin_pixmap_destroy(p); + th_destroy(t); + } + + /* 8x1, four colors, an encoder that clears every two codes, right at + * the code-size bump: the clear cancels the bump */ + const uint8_t early[] = { + 0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x08, 0x00, 0x01, 0x00, 0x81, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, + 0x00, 0x00, 0xff, 0x2c, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x01, + 0x00, 0x00, 0x02, 0x05, 0x44, 0xa8, 0x71, 0x62, 0x50, 0x00, 0x3b, + }; + p = load_bytes(early, sizeof(early)); + CHECK(p != NULL, "early-clear GIF loads"); + if (p) { + static const twin_argb32_t row[] = { + 0xff000000, 0xffffffff, 0xffff0000, 0xff0000ff, + 0xff0000ff, 0xffff0000, 0xffffffff, 0xff000000, + }; + bool same = true; + for (int x = 0; x < 8; x++) + same &= pixel_at(p, x, 0) == row[x]; + CHECK(same, "codes after an early clear keep their size"); + twin_pixmap_destroy(p); + } + + /* Play once; frame 2 asks for disposal 2 but sits at x=100, off the + * canvas: it draws nothing, disposes of nothing (its rectangle once + * made the next frame write outside the canvas), and its data is + * skipped so frame 3 still decodes */ + /* clang-format off */ + const uint8_t once[] = { + 'G', 'I', 'F', '8', '9', 'a', 1, 0, 2, 0, 0x80, 0, 0, /* screen */ + 0, 0, 0, 0xff, 0xff, 0xff, /* GCT */ + '!', 0xff, 11, 'N', 'E', 'T', 'S', 'C', 'A', 'P', 'E', '2', '.', '0', + 3, 1, 1, 0, 0, /* loop count 1 */ + '!', 0xf9, 4, 0x01, 1, 0, 0, 0, /* GCE */ + ',', 0, 0, 0, 0, 1, 0, 2, 0, 0x40, /* frame 1 */ + 2, 2, 0x44, 0x0a, 0, + '!', 0xf9, 4, 0x08, 1, 0, 0, 0, /* GCE: disposal 2 */ + ',', 100, 0, 0, 0, 1, 0, 2, 0, 0, /* x=100: off canvas */ + 2, 2, 0x4c, 0x0a, 0, + ',', 0, 0, 0, 0, 1, 0, 2, 0, 0, /* frame 3 */ + 2, 2, 0x4c, 0x0a, 0, + ';', + }; + /* clang-format on */ + p = load_bytes(once, sizeof(once)); + CHECK(p != NULL, "play-once GIF loads"); + if (!p) + return; + a = twin_pixmap_animation(p); + twin_animation_advance_frame(a); /* the off-canvas frame */ + CHECK(pixel_at(p, 0, 0) == 0 && pixel_at(p, 0, 1) == 0xffffffff, + "off-canvas frame leaves the canvas alone"); + twin_animation_advance_frame(a); + CHECK_EQ(pixel_at(p, 0, 0), 0xffffffff, "frame after it drawn"); + twin_pixmap_destroy(p); +} + /* Malformed GIFs fail to load instead of hanging or writing out of * bounds: a frame that cannot be decoded and no trailer, a zero size, and * a size that does not fit a pixmap */ @@ -1465,9 +1676,14 @@ int main(void) #endif #if defined(CONFIG_LOADER_GIF) test_gif_malformed(); + test_gif_frames(); #endif #if defined(CONFIG_LOADER_TVG) test_tvg_hairline(); + test_tvg_color_index(); +#endif +#if defined(CONFIG_LOADER_PNG) + test_png_16bit(); #endif test_leaks(); twin_cache_flush(); From 06030753f173a0a0db9838d1463289b473f3655c Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Tue, 20 Oct 2026 06:51:44 +0800 Subject: [PATCH 02/13] Shape window shadows and mask them by alpha A shadow was a blurred copy of the pixmap's bounding box, so the empty corner beside a title tab got one and see-through windows showed their own. Up to two rectangles now cast it: a window's client while it is opaque, and the desktop title tab. The casting pixmap's alpha masks the shadow, so it shows only outside the window. The desktop lights windows from above, inactive ones keep a lighter shadow, and the geometry options depend on the desktop policy. Scan-out stays procedural; per shadow pixel the old geometry costs as before. The animation demo becomes see-through. --- apps/animation.c | 11 +- configs/Kconfig | 33 ++- src/pixmap.c | 36 +++- src/screen.c | 464 ++++++++++++++++++++++++++++++++---------- src/twin_private.h | 22 +- src/window.c | 4 + src/wm-desktop.c | 51 ++++- tests/golden/core.txt | 2 +- tests/harness.c | 4 +- tests/test-core.c | 59 ++++++ tests/test-wm.c | 9 +- 11 files changed, 549 insertions(+), 146 deletions(-) diff --git a/apps/animation.c b/apps/animation.c index b8df5a54..f1610668 100644 --- a/apps/animation.c +++ b/apps/animation.c @@ -31,8 +31,7 @@ static void _apps_animation_paint(twin_custom_widget_t *custom) twin_pixmap_t *current_frame = anim->pix; /* A policy may give the window more room than the image (a phone's - * work area, the whole screen without a window manager): center it on - * the widget's black background */ + * work area, the whole screen without a window manager): center it */ twin_pixmap_t *dst = _apps_animation_pixmap(custom); twin_coord_t w = twin_custom_widget_width(custom); twin_coord_t h = twin_custom_widget_height(custom); @@ -45,7 +44,8 @@ static void _apps_animation_paint(twin_custom_widget_t *custom) .source_kind = TWIN_PIXMAP, .u.pixmap = current_frame, }; - /* Over the background: a GIF's transparent pixels show it */ + /* Over the cleared widget: a GIF's transparent pixels stay clear, and + * whatever is behind the window shows through them */ twin_composite(dst, x, y, &srcop, 0, 0, NULL, 0, 0, TWIN_OVER, fw, fh); } @@ -89,7 +89,7 @@ static twin_custom_widget_t *_apps_animation_init(twin_box_t *parent, twin_pixmap_t *pix) { twin_custom_widget_t *custom = twin_custom_widget_create( - parent, 0xff000000, 0, 0, _apps_animation_dispatch, + parent, 0x00000000, 0, 0, _apps_animation_dispatch, sizeof(apps_animation_data_t)); if (!custom) return NULL; @@ -134,6 +134,9 @@ bool apps_animation_start(twin_screen_t *screen, twin_pixmap_destroy(pix); return false; } + /* See-through: a clear box makes the window translucent, so the GIF's + * transparency reaches the screen instead of a solid fill */ + twin_widget_set(twin_box_widget(twin_toplevel_box(toplevel)), 0x00000000); /* The widget owns the image from here on */ if (!apps_animation_create(twin_toplevel_box(toplevel), pix)) { twin_toplevel_destroy(toplevel); diff --git a/configs/Kconfig b/configs/Kconfig index 02880f84..62ecbc86 100644 --- a/configs/Kconfig +++ b/configs/Kconfig @@ -352,33 +352,32 @@ config DROP_SHADOW config HORIZONTAL_OFFSET int "Horizontal offset" - default 6 - range 1 10 - depends on DROP_SHADOW + default 0 + range 0 10 + depends on DROP_SHADOW && WM_DESKTOP help - Horizontal shadow offset in pixels. - Default matches a CSS-style soft shadow (≈6 px offset). - Larger values push shadow further right. + Horizontal shadow offset of the active window, in pixels. Modern + desktops light windows from above, so the default is 0: the shadow + shows evenly left and right. Inactive windows use half. config VERTICAL_OFFSET int "Vertical offset" - default 6 - range 1 10 - depends on DROP_SHADOW + default 4 + range 0 10 + depends on DROP_SHADOW && WM_DESKTOP help - Vertical shadow offset in pixels. - Default matches a CSS-style soft shadow (≈6 px offset). - Larger values push shadow further down. + Vertical shadow offset of the active window, in pixels. A small + downward offset under a wide blur gives a halo all around that is + heavier below the window. Inactive windows use half. config SHADOW_BLUR int "Shadow blur radius" - default 12 + default 14 range 1 15 - depends on DROP_SHADOW + depends on DROP_SHADOW && WM_DESKTOP help - Width in pixels of the shadow's soft edge. - Default 12 approximates common CSS box-shadow softness. - Larger values create softer, more diffuse shadows. The shadow + Width in pixels of the active window's shadow edge; inactive windows + use half, so the active one appears to float higher. The shadow is computed while scanning out, so the radius does not change its cost per pixel. diff --git a/src/pixmap.c b/src/pixmap.c index 28828bc0..fc81e71e 100644 --- a/src/pixmap.c +++ b/src/pixmap.c @@ -50,6 +50,10 @@ void _twin_pixmap_init(twin_pixmap_t *pixmap, pixmap->clip.bottom = height; pixmap->scan_height = height; pixmap->opacity = 0xff; +#if defined(CONFIG_DROP_SHADOW) + /* The whole pixmap casts its shadow until told otherwise */ + pixmap->shadow.cast[0] = (twin_rect_t) {0, width, 0, height}; +#endif /* RGB16 has no alpha channel, so every pixel is opaque. */ if (format == TWIN_RGB16) pixmap->opaque_rect = (twin_rect_t) {0, width, 0, height}; @@ -258,6 +262,18 @@ void twin_pixmap_set_opacity(twin_pixmap_t *pixmap, twin_a8_t opacity) _twin_pixmap_damage_extents(pixmap); } +#if defined(CONFIG_DROP_SHADOW) +/* Change @pixmap's shadow to @s, damaging where it was and where it goes */ +static void _shadow_assign(twin_pixmap_t *pixmap, const twin_shadow_t *s) +{ + if (!memcmp(s, &pixmap->shadow, sizeof(*s))) + return; + _twin_pixmap_damage_extents(pixmap); + pixmap->shadow = *s; + _twin_pixmap_damage_extents(pixmap); +} +#endif + void twin_pixmap_set_shadow(twin_pixmap_t *pixmap, twin_coord_t dx, twin_coord_t dy, @@ -268,18 +284,28 @@ void twin_pixmap_set_shadow(twin_pixmap_t *pixmap, /* Built without shadow support */ (void) pixmap, (void) dx, (void) dy, (void) blur, (void) alpha; #else - twin_shadow_t s = {0}; + twin_shadow_t s = pixmap->shadow; + s.dx = s.dy = s.blur = s.alpha = 0; if (blur > 0 && alpha) { s.dx = (int8_t) (dx > 127 ? 127 : dx < -127 ? -127 : dx); s.dy = (int8_t) (dy > 127 ? 127 : dy < -127 ? -127 : dy); s.blur = (uint8_t) (blur > 63 ? 63 : blur); s.alpha = alpha; } - if (!memcmp(&s, &pixmap->shadow, sizeof(s))) + _shadow_assign(pixmap, &s); +#endif +} + +void twin_pixmap_set_shadow_cast(twin_pixmap_t *pixmap, int k, twin_rect_t r) +{ +#if !defined(CONFIG_DROP_SHADOW) + (void) pixmap, (void) k, (void) r; +#else + if (k < 0 || k >= TWIN_SHADOW_CASTS) return; - _twin_pixmap_damage_extents(pixmap); - pixmap->shadow = s; - _twin_pixmap_damage_extents(pixmap); + twin_shadow_t s = pixmap->shadow; + s.cast[k] = r; + _shadow_assign(pixmap, &s); #endif } diff --git a/src/screen.c b/src/screen.c index f6477c07..dee0b737 100644 --- a/src/screen.c +++ b/src/screen.c @@ -26,8 +26,10 @@ * * - Shadows are synthesized during scan-out from a separable smoothstep * profile, as a pseudo-layer directly under the casting pixmap. They - * need no pixel storage and no blur pass, and the opaque pixmap above - * culls the shadow's interior automatically. + * need no pixel storage and no blur pass. A window manager may give + * the shape casting it (a window's client and title tab, not the empty + * corner beside the tab), and the pixmap's own alpha masks it, so it + * only shows outside the window. */ #include @@ -285,28 +287,38 @@ static int _span_set_subtract(const twin_span_set_t *set, #if defined(CONFIG_DROP_SHADOW) /* * Shadow profile: smoothstep ramp across 2 * blur + 1 pixels, in 1/256 - * units. One entry is cached; radii rarely change between rows. + * units. The active and inactive windows' radii interleave on the same + * rows, and rebuilding the ramp whenever the radius changed cost more than + * the shadow pixels themselves, so two are kept. */ -static uint16_t shadow_ramp[2 * TWIN_SHADOW_MAX_BLUR + 1]; -/* Widest shadow piece whose alphas are kept between rows */ +#define SHADOW_RAMPS 2 +static struct { + int blur; /* 0: unused, as a shadow's blur is at least 1 */ + uint16_t v[2 * TWIN_SHADOW_MAX_BLUR + 1]; +} shadow_ramps[SHADOW_RAMPS]; +static int shadow_ramp_next; +/* Widest shadow piece whose alphas are kept between rows, and how many */ #define SHADOW_ROW_CACHE 64 -static int shadow_ramp_blur = -1; +#define SHADOW_ROWS 8 static const uint16_t *_shadow_ramp(int blur) { - if (blur != shadow_ramp_blur) { - int n = 2 * blur; - for (int i = 0; i <= n; i++) { - /* t in 16.16, smoothstep 3t^2 - 2t^3 */ - int64_t t = twin_fixed_div(i, n ? n : 1); - int64_t t2 = (t * t) >> 16; - int64_t t3 = (t2 * t) >> 16; - int64_t v = 3 * t2 - 2 * t3; - shadow_ramp[i] = (uint16_t) ((v * 256 + 0x8000) >> 16); - } - shadow_ramp_blur = blur; + for (int k = 0; k < SHADOW_RAMPS; k++) + if (shadow_ramps[k].blur == blur) + return shadow_ramps[k].v; + int k = shadow_ramp_next; + shadow_ramp_next = (k + 1) % SHADOW_RAMPS; + int n = 2 * blur; + for (int i = 0; i <= n; i++) { + /* t in 16.16, smoothstep 3t^2 - 2t^3 */ + int64_t t = twin_fixed_div(i, n); + int64_t t2 = (t * t) >> 16; + int64_t t3 = (t2 * t) >> 16; + int64_t v = 3 * t2 - 2 * t3; + shadow_ramps[k].v[i] = (uint16_t) ((v * 256 + 0x8000) >> 16); } - return shadow_ramp; + shadow_ramps[k].blur = blur; + return shadow_ramps[k].v; } /* Coverage of a blurred box edge pair at position @v, 0..256 */ @@ -325,15 +337,46 @@ static inline int _shadow_cover(const uint16_t *ramp, return c < 0 ? 0 : c; } -static inline twin_rect_t _shadow_rect(const twin_pixmap_t *p) +/* Where _shadow_cover is exactly 256 across @s, and where it can be + * nonzero at all */ +static inline bool _shadow_flat(const twin_rect_t *s, int blur, twin_coord_t x) { - twin_rect_t r = { - .left = p->x + p->shadow.dx, - .right = p->x + p->width + p->shadow.dx, - .top = p->y + p->shadow.dy, - .bottom = p->y + p->scan_height + p->shadow.dy, - }; - return r; + return x >= s->left + blur + 1 && x < s->right - blur - 1; +} + +static inline bool _shadow_reach(const twin_rect_t *s, int blur, twin_coord_t x) +{ + return x >= s->left - blur && x < s->right + blur; +} + +static inline bool _rect_empty(const twin_rect_t *r) +{ + return r->left >= r->right || r->top >= r->bottom; +} + +/* The rectangles casting @p's shadow, on screen and offset, clipped to the + * pixmap's shown rows (an iconified window's title alone). Returns how + * many. */ +static int _shadow_rects(const twin_pixmap_t *p, + twin_rect_t out[TWIN_SHADOW_CASTS]) +{ + int n = 0; + for (int k = 0; k < TWIN_SHADOW_CASTS; k++) { + twin_rect_t r = p->shadow.cast[k]; + r.left = max(r.left, (twin_coord_t) 0); + r.right = min(r.right, p->width); + r.top = max(r.top, (twin_coord_t) 0); + r.bottom = min(r.bottom, p->scan_height); + if (_rect_empty(&r)) + continue; + out[n++] = (twin_rect_t) { + .left = p->x + r.left + p->shadow.dx, + .right = p->x + r.right + p->shadow.dx, + .top = p->y + r.top + p->shadow.dy, + .bottom = p->y + r.bottom + p->shadow.dy, + }; + } + return n; } #endif /* CONFIG_DROP_SHADOW */ @@ -344,12 +387,15 @@ twin_rect_t _twin_pixmap_extents(const twin_pixmap_t *p) #if defined(CONFIG_DROP_SHADOW) if (p->shadow.blur) { int b = min((int) p->shadow.blur, TWIN_SHADOW_MAX_BLUR); - twin_rect_t s = _shadow_rect(p); - s.left -= b; - s.right += b; - s.top -= b; - s.bottom += b; - r = _rect_union(&r, &s); + twin_rect_t s[TWIN_SHADOW_CASTS]; + int n = _shadow_rects(p, s); + for (int k = 0; k < n; k++) { + s[k].left -= b; + s[k].right += b; + s[k].top -= b; + s[k].bottom += b; + r = _rect_union(&r, &s[k]); + } } #endif return r; @@ -367,85 +413,284 @@ static inline twin_argb32_t _shadow_over(twin_argb32_t v, uint32_t a) return (rb | ag << 8) + (a << 24); } -static void _shadow_span(twin_argb32_t *span, - twin_coord_t span_left, - const twin_pixmap_t *p, - twin_coord_t y, - twin_coord_t l, - twin_coord_t r) +/* What a row of @p's shadow needs, worked out once for all its pieces */ +typedef struct { + const twin_pixmap_t *p; + const uint16_t *ramp; + int blur, m; + twin_rect_t s[TWIN_SHADOW_CASTS]; /* the rectangles reaching the row */ + uint32_t row_alpha[TWIN_SHADOW_CASTS]; /* peak alpha times coverage */ +} shadow_row_t; + +/* False when no rectangle's shadow reaches row @y */ +static bool _shadow_row(shadow_row_t *sr, + const twin_pixmap_t *p, + twin_coord_t y) { - int blur = min((int) p->shadow.blur, TWIN_SHADOW_MAX_BLUR); - const uint16_t *ramp = _shadow_ramp(blur); - twin_rect_t s = _shadow_rect(p); - int fy = _shadow_cover(ramp, blur, y, s.top, s.bottom); - if (!fy) - return; - uint32_t row_alpha = (uint32_t) p->shadow.alpha * (uint32_t) fy; - twin_argb32_t *d = span + (l - span_left); + twin_rect_t c[TWIN_SHADOW_CASTS]; + int n = _shadow_rects(p, c); + sr->p = p; + sr->blur = min((int) p->shadow.blur, TWIN_SHADOW_MAX_BLUR); + sr->ramp = _shadow_ramp(sr->blur); + sr->m = 0; + for (int k = 0; k < n; k++) { + int fy = _shadow_cover(sr->ramp, sr->blur, y, c[k].top, c[k].bottom); + if (fy) { + sr->s[sr->m] = c[k]; + sr->row_alpha[sr->m++] = (uint32_t) p->shadow.alpha * (uint32_t) fy; + } + } + /* A rectangle whose columns lie within the other's, no stronger on this + * row, never wins: blurring a narrower span never covers more. That + * leaves one rectangle, and its fast paths, on all but the title rows. */ + if (sr->m == 2) { + for (int k = 0; k < 2; k++) { + const twin_rect_t *a = &sr->s[k], *b = &sr->s[1 - k]; + if (a->left >= b->left && a->right <= b->right && + sr->row_alpha[k] <= sr->row_alpha[1 - k]) { + sr->s[0] = *b; + sr->row_alpha[0] = sr->row_alpha[1 - k]; + sr->m = 1; + break; + } + } + } + return sr->m > 0; +} - /* A narrow piece (the fringe beside a sliding window) recurs with the - * same geometry on every row whose vertical coverage saturates: keep - * its alphas and skip the ramp lookups on those rows */ - static struct { +/* Shadow alpha at column @x: the largest of the rectangles' coverage */ +static inline uint32_t _shadow_alpha(const shadow_row_t *sr, twin_coord_t x) +{ + uint32_t a = 0; + for (int k = 0; k < sr->m; k++) { + const twin_rect_t *s = &sr->s[k]; + int fx = _shadow_flat(s, sr->blur, x) + ? 256 + : _shadow_cover(sr->ramp, sr->blur, x, s->left, s->right); + a = max(a, (sr->row_alpha[k] * (uint32_t) fx) >> 16); + } + return a; +} + +/* A row of the casting pixmap, whose alpha masks its own shadow */ +typedef struct { + const uint8_t *row; + twin_format_t format; + twin_coord_t x; /* the pixmap's screen column */ + twin_a8_t opacity; +} shadow_mask_t; + +/* The pixmap's alpha at screen column @x, faded by its opacity */ +static inline uint32_t _mask_alpha(const shadow_mask_t *mask, twin_coord_t x) +{ + int i = x - mask->x; + uint32_t wa = mask->format == TWIN_ARGB32 + ? ((const twin_argb32_t *) mask->row)[i] >> 24 + : mask->format == TWIN_A8 ? mask->row[i] + : 0xff; /* RGB16 */ + if (mask->opacity != 0xff) { + uint16_t t; + wa = twin_int_mult(wa, mask->opacity, t); + } + return wa; +} + +/* + * Shadow over [@l, @r) of a row, @d at column @l, masked by the + * casting window's row unless @mask is NULL. A rectangle's coverage + * changes only at four columns (see _shadow_flat and _shadow_reach), so the + * span splits there into segments where every rectangle is either on its + * plateau or out of reach: the alpha is constant across them, and a pixel + * costs a mask test and a blend. Only ramp segments look it up. + */ +static void _shadow_blend(const shadow_row_t *sr, + twin_argb32_t *d, + twin_coord_t l, + twin_coord_t r, + const shadow_mask_t *mask) +{ + int blur = sr->blur; + twin_coord_t cut[4 * TWIN_SHADOW_CASTS + 1]; + int nc = 0; + for (int k = 0; k < sr->m; k++) { + const twin_rect_t *s = &sr->s[k]; + twin_coord_t edge[4] = { + s->left - blur, + s->left + blur + 1, + s->right - blur - 1, + s->right + blur, + }; + for (int e = 0; e < 4; e++) { + if (edge[e] <= l || edge[e] >= r) + continue; + int i = nc++; + for (; i > 0 && cut[i - 1] > edge[e]; i--) + cut[i] = cut[i - 1]; + cut[i] = edge[e]; + } + } + cut[nc++] = r; + + for (int c = 0, x0 = l; c < nc; x0 = cut[c++]) { + twin_coord_t x1 = cut[c]; + /* The segment's alpha, if no rectangle ramps across it */ + bool flat = true; + uint32_t a = 0; + for (int k = 0; k < sr->m && flat; k++) { + if (_shadow_flat(&sr->s[k], blur, x0)) + a = max(a, sr->row_alpha[k] >> 8); + else if (_shadow_reach(&sr->s[k], blur, x0)) + flat = false; + } + if (flat && !a) + continue; + /* The plain plateau, most of a shadow, gets a loop of its own */ + if (flat && !mask) { + for (twin_coord_t x = x0; x < x1; x++) + d[x - l] = _shadow_over(d[x - l], a); + continue; + } + for (twin_coord_t x = x0; x < x1; x++) { + uint32_t wa = mask ? _mask_alpha(mask, x) : 0; + /* Opaque window pixels, most of a title bar, hide it */ + if (wa == 0xff) + continue; + uint32_t v = flat ? a : _shadow_alpha(sr, x); + uint16_t t; + if (wa) + v = twin_int_mult(v, 255 - wa, t); + if (v) + d[x - l] = _shadow_over(d[x - l], v); + } + } +} + +/* A lone rectangle's shadow over [@l, @r), @d at column @l. A narrow piece (a + * window's left or right fringe) recurs with the same geometry on every row + * whose vertical coverage saturates: its alphas are kept, which skips the + * ramp lookups on those rows. Every shadowed window has two such pieces + * per row, so several are kept. */ +static void _shadow_run(const shadow_row_t *sr, + twin_argb32_t *d, + twin_coord_t l, + twin_coord_t r) +{ + if (r - l > SHADOW_ROW_CACHE) { + _shadow_blend(sr, d, l, r, NULL); + return; + } + const twin_rect_t *s = &sr->s[0]; + /* Packed without padding, so a whole key compares with memcmp; an + * unused slot (zero alpha) never matches */ + typedef struct { uint32_t row_alpha; - int blur; + int32_t blur; twin_coord_t sl, sr, l, r; + } shadow_key_t; + static struct { + shadow_key_t key; uint8_t a[SHADOW_ROW_CACHE]; - } row; - if (r - l <= SHADOW_ROW_CACHE) { - if (row.r <= row.l || row.row_alpha != row_alpha || row.blur != blur || - row.sl != s.left || row.sr != s.right || row.l != l || row.r != r) { - for (twin_coord_t x = l; x < r; x++) { - int fx = _shadow_cover(ramp, blur, x, s.left, s.right); - row.a[x - l] = (uint8_t) ((row_alpha * (uint32_t) fx) >> 16); - } - row.row_alpha = row_alpha; - row.blur = blur; - row.sl = s.left; - row.sr = s.right; - row.l = l; - row.r = r; + } rows[SHADOW_ROWS]; + static int next_row; + shadow_key_t key = {sr->row_alpha[0], sr->blur, s->left, s->right, l, r}; + int k = 0; + while (k < SHADOW_ROWS && memcmp(&rows[k].key, &key, sizeof(key))) + k++; + if (k == SHADOW_ROWS) { + k = next_row; + next_row = (k + 1) % SHADOW_ROWS; + for (twin_coord_t x = l; x < r; x++) { + int fx = _shadow_cover(sr->ramp, sr->blur, x, s->left, s->right); + rows[k].a[x - l] = + (uint8_t) ((sr->row_alpha[0] * (uint32_t) fx) >> 16); } - for (twin_coord_t i = 0; i < r - l; i++) - if (row.a[i]) - d[i] = _shadow_over(d[i], row.a[i]); - return; + rows[k].key = key; } + const uint8_t *a = rows[k].a; + for (twin_coord_t i = 0; i < r - l; i++) + if (a[i]) + d[i] = _shadow_over(d[i], a[i]); +} - /* Between the two edge ramps both saturate and the horizontal coverage - * is exactly 256 (see _shadow_cover), so the alpha is constant there */ - twin_coord_t flat_l = s.left + blur + 1, flat_r = s.right - blur - 1; - uint32_t flat_a = (row_alpha * 256) >> 16; - - for (twin_coord_t x = l; x < r;) { - if (x >= flat_l && x < flat_r) { - twin_coord_t end = min(r, flat_r); - if (flat_a) { - for (; x < end; x++, d++) - *d = _shadow_over(*d, flat_a); - } else { - d += end - x; - x = end; - } +/* + * One piece [@l, @r) of a row set up by _shadow_row. Within @p's own + * columns its alpha masks the shadow, as a modern compositor clips a shadow + * to outside the window: nothing shows through an opaque pixel, the + * translucent title shows the desktop, not its own shadow, and a rounded + * corner gets shadow right up to its curve. The opaque part is culled + * before this runs, so the mask costs a lookup only on pixels where the + * window is not opaque. + */ +static void _shadow_piece(const shadow_row_t *sr, + twin_argb32_t *span, + twin_coord_t span_left, + twin_coord_t y, + twin_coord_t l, + twin_coord_t r) +{ + const twin_pixmap_t *p = sr->p; + + /* The columns @p covers on this row, masked; none off its rows */ + twin_coord_t ml = r, mr = r; + if (y >= p->y && y < p->y + p->scan_height) { + twin_coord_t right = p->x + p->width; + ml = l > p->x ? l : p->x; + if (ml > r) + ml = r; + mr = r < right ? r : right; + if (mr < ml) + mr = ml; + } + + /* Unmasked: a lone rectangle keeps its row cache */ + for (int side = 0; side < 2; side++) { + twin_coord_t pl = side ? mr : l, pr = side ? r : ml; + if (pl >= pr) continue; - } - int fx = _shadow_cover(ramp, blur, x, s.left, s.right); - uint32_t a = (row_alpha * (uint32_t) fx) >> 16; - if (a) - *d = _shadow_over(*d, a); - x++; - d++; + if (sr->m == 1) + _shadow_run(sr, span + (pl - span_left), pl, pr); + else + _shadow_blend(sr, span + (pl - span_left), pl, pr, NULL); } + + /* Masked by @p's own alpha, faded by its opacity. An unfaded RGB16 + * pixmap is opaque throughout, and an evicted stub has no pixels. */ + if (ml >= mr || _twin_pixmap_is_stub(p) || + (p->format == TWIN_RGB16 && p->opacity == 0xff)) + return; + shadow_mask_t mask = { + .row = p->p.b + (size_t) (y - p->y) * p->stride, + .format = p->format, + .x = p->x, + .opacity = p->opacity, + }; + _shadow_blend(sr, span + (ml - span_left), ml, mr, &mask); +} + +/* @p's shadow on row @y over its @count visible pieces */ +static void _shadow_span(twin_argb32_t *span, + twin_coord_t span_left, + const twin_pixmap_t *p, + twin_coord_t y, + const twin_span_t *pieces, + int count) +{ + shadow_row_t sr; + if (!_shadow_row(&sr, p, y)) + return; + for (int k = 0; k < count; k++) + _shadow_piece(&sr, span, span_left, y, pieces[k].l, pieces[k].r); } #else static inline void _shadow_span(twin_argb32_t *span, twin_coord_t span_left, const twin_pixmap_t *p, twin_coord_t y, - twin_coord_t l, - twin_coord_t r) + const twin_span_t *pieces, + int count) { - (void) span, (void) span_left, (void) p, (void) y, (void) l, (void) r; + (void) span, (void) span_left, (void) p, (void) y, (void) pieces, + (void) count; } #endif @@ -607,6 +852,14 @@ static void _composite_row(twin_screen_t *screen, } else { if (!_twin_pixmap_shadowed(p)) break; +#if defined(CONFIG_DROP_SHADOW) + /* The casters lie within the pixmap's shown rows: skip + * rows the blurred shadow cannot reach before building + * its extents */ + int b = p->shadow.blur, dy = p->shadow.dy; + if (y < p->y + dy - b || y >= p->y + p->scan_height + dy + b) + break; +#endif twin_rect_t e = _twin_pixmap_extents(p); if (y < e.top || y >= e.bottom) break; @@ -664,9 +917,9 @@ static void _composite_row(twin_screen_t *screen, continue; if (_twin_pixmap_shadowed(p)) { twin_rect_t e = _twin_pixmap_extents(p); + twin_span_t piece = {max(e.left, left), min(e.right, right)}; if (y >= e.top && y < e.bottom) - _shadow_span(span, left, p, y, max(e.left, left), - min(e.right, right)); + _shadow_span(span, left, p, y, &piece, 1); } if (y < p->y || y >= p->y + p->scan_height) continue; @@ -729,13 +982,16 @@ static void _composite_row(twin_screen_t *screen, for (int i = n_layers - 1; i >= 0; i--) { twin_row_layer_t *ly = &layers[i]; twin_coord_t visible = 0; + for (int k = 0; k < ly->count; k++) + visible += pieces[ly->first + k].r - pieces[ly->first + k].l; + if (ly->shadow) { + _shadow_span(span, left, ly->p, y, &pieces[ly->first], ly->count); + screen->stats.shadow_pixels += visible; + continue; + } for (int k = 0; k < ly->count; k++) { twin_span_t *pc = &pieces[ly->first + k]; - visible += pc->r - pc->l; - if (ly->shadow) { - _shadow_span(span, left, ly->p, y, pc->l, pc->r); - screen->stats.shadow_pixels += pc->r - pc->l; - } else if (onto & ((uint64_t) 1 << (ly->first + k))) { + if (onto & ((uint64_t) 1 << (ly->first + k))) { twin_pixmap_t *p = ly->p; twin_pointer_t src = twin_pixmap_pointer(p, pc->l - p->x, y - p->y); @@ -752,10 +1008,8 @@ static void _composite_row(twin_screen_t *screen, _composite_layer(span, left, ly, y, pc->l, pc->r); } } - if (!ly->shadow) { - screen->stats.layer_pixels += visible; - screen->stats.occluded_pixels += (ly->r - ly->l) - visible; - } + screen->stats.layer_pixels += visible; + screen->stats.occluded_pixels += (ly->r - ly->l) - visible; } } diff --git a/src/twin_private.h b/src/twin_private.h index 83e7d367..18e84801 100644 --- a/src/twin_private.h +++ b/src/twin_private.h @@ -73,18 +73,26 @@ struct _twin_animation { void (*close)(twin_animation_t *anim); }; +/* Rectangles that can cast one pixmap's shadow */ +#define TWIN_SHADOW_CASTS 2 + /** * Procedural drop shadow attached to a pixmap * * The compositor synthesizes the shadow while scanning out: each edge fades * along a smoothstep ramp 2 * blur + 1 pixels wide, a close match to a - * blurred rectangle. No pixels are stored: a shadow costs four bytes per - * pixmap and no blur pass. + * Gaussian-blurred rectangle. No pixels are stored and there is no blur + * pass. Rectangles cast it: the whole pixmap by default; for a window, its + * client while solid (kept by the window) and its title tab (set by the + * window manager). The pixmap's alpha masks it, so it only shows outside. + * It costs 20 bytes per pixmap: 4 for the shadow, 8 for each rectangle. */ typedef struct _twin_shadow { - int8_t dx, dy; /**< Offset of the shadow rectangle */ + int8_t dx, dy; /**< Offset of the shadow */ uint8_t blur; /**< Blur radius in pixels (0 disables the shadow) */ twin_a8_t alpha; /**< Peak shadow opacity */ + twin_rect_t cast[TWIN_SHADOW_CASTS]; /**< Pixmap coordinates; an empty + rectangle casts nothing */ } twin_shadow_t; /* The pixmap casts a shadow (only with CONFIG_DROP_SHADOW) */ @@ -1973,6 +1981,14 @@ void twin_pixmap_set_shadow(twin_pixmap_t *pixmap, twin_coord_t blur, twin_a8_t alpha); +/** + * Set one rectangle casting a pixmap's shadow + * @pixmap : Pixmap casting the shadow + * @k : Which one, below TWIN_SHADOW_CASTS; 0 starts as the whole pixmap + * @r : Pixmap coordinates; empty casts nothing + */ +void twin_pixmap_set_shadow_cast(twin_pixmap_t *pixmap, int k, twin_rect_t r); + void twin_pixmap_enable_update(twin_pixmap_t *pixmap); void twin_pixmap_disable_update(twin_pixmap_t *pixmap); diff --git a/src/window.c b/src/window.c index 6079ce1f..1d847c2c 100644 --- a/src/window.c +++ b/src/window.c @@ -81,6 +81,10 @@ static void _twin_window_update_opaque(twin_window_t *window) pixmap->opaque_rect = window->client; else pixmap->opaque_rect = (twin_rect_t) {0, 0, 0, 0}; + /* A solid client casts the shadow; a see-through one casts none and, + * masked by its alpha, shows what is behind instead */ + twin_pixmap_set_shadow_cast( + pixmap, 0, window->opaque ? window->client : (twin_rect_t) {0}); } /* Clip to @r (pixmap coordinates) and put the origin at the client corner */ diff --git a/src/wm-desktop.c b/src/wm-desktop.c index 347f1d49..77d6494e 100644 --- a/src/wm-desktop.c +++ b/src/wm-desktop.c @@ -32,7 +32,8 @@ #define TWIN_TITLE_BW ((TWIN_TITLE_HEIGHT + 11) / 12) #if defined(CONFIG_DROP_SHADOW) -#define SHADOW_ALPHA 0x80 +#define SHADOW_ALPHA 0x80 /* the active window */ +#define SHADOW_ALPHA_INACTIVE 0x48 /* the rest, closer to the desktop */ #endif #define DOUBLE_CLICK_MS 300 @@ -150,14 +151,49 @@ static void _draw_icon(twin_pixmap_t *pixmap, twin_icon_draw(pixmap, icon, m); } +#if defined(CONFIG_DROP_SHADOW) +/* Lit from above, as modern desktops are: the shadow falls mostly downward + * but shows all around, and the active window floats higher (a wider, + * darker shadow) than the rest instead of being the only one with any */ +static void _set_shadow(twin_window_t *window) +{ + int k = window->active ? 1 : 2; /* inactive: half the depth */ + twin_pixmap_set_shadow( + window->pixmap, CONFIG_HORIZONTAL_OFFSET / k, + CONFIG_VERTICAL_OFFSET / k, (CONFIG_SHADOW_BLUR + k - 1) / k, + window->active ? SHADOW_ALPHA : SHADOW_ALPHA_INACTIVE); +} + +/* The frame casts the shadow, not its bounding box: besides the client + * (the window's own concern), the title tab, inset from its rounded + * corners so they get a rounded shadow */ +static void _set_shadow_tab(twin_window_t *window, const twin_title_t *t) +{ + twin_coord_t inset = twin_fixed_to_int(t->t_arc_1); + twin_pixmap_set_shadow_cast( + window->pixmap, 1, + (twin_rect_t) {inset, + twin_fixed_to_int(twin_fixed_ceil(t->c_right)) - inset, + inset, window->client.top}); +} +#endif + void _twin_wm_decorate(twin_window_t *window) { twin_pixmap_t *pixmap = window->pixmap; twin_title_t t; - if (window->style != TwinWindowApplication) + if (window->style != TwinWindowApplication) { +#if defined(CONFIG_DROP_SHADOW) + /* No title tab: drop one an earlier style left casting */ + twin_pixmap_set_shadow_cast(pixmap, 1, (twin_rect_t) {0}); +#endif return; + } _title_metrics(window, &t); +#if defined(CONFIG_DROP_SHADOW) + _set_shadow_tab(window, &t); +#endif twin_pixmap_reset_clip(pixmap); pixmap->origin_x = pixmap->origin_y = 0; @@ -218,12 +254,7 @@ static void _set_active(twin_window_t *window, bool active) return; window->active = active; #if defined(CONFIG_DROP_SHADOW) - if (active) - twin_pixmap_set_shadow(window->pixmap, CONFIG_HORIZONTAL_OFFSET, - CONFIG_VERTICAL_OFFSET, CONFIG_SHADOW_BLUR, - SHADOW_ALPHA); - else - twin_pixmap_set_shadow(window->pixmap, 0, 0, 0, 0); + _set_shadow(window); #endif if (window->style == TwinWindowApplication) { window->frame_dirty = true; @@ -314,6 +345,10 @@ void _twin_wm_map(twin_window_t *window) if (!window->wm_placed) { _place_new(window); _twin_window_relayout(window); /* no-op unless it moved */ +#if defined(CONFIG_DROP_SHADOW) + if (window->style != TwinWindowPlain) + _set_shadow(window); /* inactive until activated, but floating */ +#endif } _twin_window_restore(window); _twin_window_raise(window); diff --git a/tests/golden/core.txt b/tests/golden/core.txt index b1180021..7c120a89 100644 --- a/tests/golden/core.txt +++ b/tests/golden/core.txt @@ -1,4 +1,4 @@ # Golden frame checksums for the core suite. # Regenerate with: make check-update -# config 6969dfe0 +# config b0fec876 scene 373a3045 diff --git a/tests/harness.c b/tests/harness.c index f27581a5..d4ead50d 100644 --- a/tests/harness.c +++ b/tests/harness.c @@ -355,9 +355,11 @@ static uint32_t config_fingerprint(void) #else 0, #endif -#if defined(CONFIG_DROP_SHADOW) +#if defined(CONFIG_SHADOW_BLUR) /* the desktop's shadow geometry */ CONFIG_HORIZONTAL_OFFSET, CONFIG_VERTICAL_OFFSET, CONFIG_SHADOW_BLUR, +#elif defined(CONFIG_DROP_SHADOW) /* fixed geometry, as mobile slides */ + -1, -1, -1, #else 0, 0, 0, #endif diff --git a/tests/test-core.c b/tests/test-core.c index 80a3e4e1..b3c2934e 100644 --- a/tests/test-core.c +++ b/tests/test-core.c @@ -29,6 +29,64 @@ static twin_pixmap_t *solid(twin_format_t fmt, return p; } +#if defined(CONFIG_DROP_SHADOW) +/* The shadow shows only outside its window: the shape picks what casts it, + * the window's alpha masks it (opacity included, RGB16 too), and narrow + * damage leaves pieces wholly beside the window that must stay in bounds */ +static void test_shadow_mask(void) +{ + th_t *t = th_create(W, H); + t->screen->background_color = 0xffffffff; + twin_screen_damage(t->screen, 0, 0, W, H); + + /* Left half solid, right half clear; only the solid half casts */ + twin_pixmap_t *glass = solid(TWIN_ARGB32, 40, 40, 0x00000000); + twin_fill(glass, 0xff000080, TWIN_SOURCE, 0, 0, 20, 40); + twin_pixmap_move(glass, 40, 40); + twin_pixmap_set_shadow(glass, 0, 4, 6, 0xc0); + twin_pixmap_set_shadow_cast(glass, 0, (twin_rect_t) {0, 20, 0, 40}); + twin_pixmap_show(glass, t->screen, NULL); + th_frame(t); + CHECK_EQ(th_pixel(t, 50, 60), 0xff000080, "no shadow over the window"); + CHECK((th_pixel(t, 62, 60) & 0xff) < 0xff, + "shadow shows through the clear half beside the caster"); + CHECK_EQ(th_pixel(t, 75, 60), 0xffffffff, + "clear half past the blur: no shadow from the bounding box"); + + /* Faded RGB16 is not opaque: part of its own shadow shows through */ + twin_pixmap_t *rgb = solid(TWIN_RGB16, 30, 30, 0xff808080); + twin_pixmap_move(rgb, 120, 40); + twin_pixmap_set_opacity(rgb, 0x80); + twin_pixmap_show(rgb, t->screen, NULL); + th_frame(t); + twin_argb32_t plain = th_pixel(t, 135, 55); + twin_pixmap_set_shadow(rgb, 0, 0, 6, 0xc0); + th_frame(t); + CHECK((th_pixel(t, 135, 55) & 0xff) < (plain & 0xff), + "half-faded RGB16 lets its shadow through: %08x vs %08x", + th_pixel(t, 135, 55), plain); + + /* An A8 pixmap masks by its bytes */ + twin_pixmap_t *a8 = solid(TWIN_A8, 8, 8, 0x80000000); + twin_pixmap_move(a8, 60, 110); + twin_pixmap_set_shadow(a8, 2, 2, 4, 0xc0); + twin_pixmap_show(a8, t->screen, NULL); + th_frame(t); + CHECK_EQ(th_damage_check(t), 0, "A8 shadow consistent"); + + /* Narrow strips: pieces left or right of a pixmap, never past them */ + for (twin_coord_t x = 0; x < W; x += 3) { + twin_screen_damage(t->screen, x, 0, x + 2, H); + th_frame(t); + } + CHECK_EQ(th_damage_check(t), 0, "strip damage matches a full repaint"); + + twin_pixmap_destroy(a8); + twin_pixmap_destroy(rgb); + twin_pixmap_destroy(glass); + th_destroy(t); +} +#endif static void test_damage_merge(void) { @@ -1650,6 +1708,7 @@ int main(void) test_row_fallback(); #if defined(CONFIG_DROP_SHADOW) test_shadow(); + test_shadow_mask(); #endif test_rgb16_and_opacity(); test_stub(); diff --git a/tests/test-wm.c b/tests/test-wm.c index cd2a1ccf..cc4fe3e2 100644 --- a/tests/test-wm.c +++ b/tests/test-wm.c @@ -151,8 +151,13 @@ static void test_desktop(void) CHECK(wa->pixmap->height == 100 + wa->client.top, "title bar added"); CHECK(wb->active && !wa->active, "last shown window is active"); #if defined(CONFIG_DROP_SHADOW) - CHECK(wb->pixmap->shadow.blur && !wa->pixmap->shadow.blur, - "only the active window casts a shadow"); + CHECK(wa->pixmap->shadow.blur && + wb->pixmap->shadow.blur >= wa->pixmap->shadow.blur && + wb->pixmap->shadow.alpha > wa->pixmap->shadow.alpha, + "the active window floats higher: a wider, darker shadow"); + CHECK(wb->pixmap->shadow.cast[0].top == wb->client.top && + wb->pixmap->shadow.cast[1].bottom == wb->client.top, + "the client and the title tab cast it, not the bounding box"); #endif CONSISTENT(t, "desktop: initial"); From 92ca2951b156ca00d4006f4fcbe08af1e96e7a7f Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Tue, 20 Oct 2026 06:51:44 +0800 Subject: [PATCH 03/13] Fill TinyVG paths by the even-odd rule comic.tvg drew wedges of black, and tiger.tvg and flowchart.tvg stray discs, because their nested outlines cut holes by the even-odd rule while twin always filled nonzero. A path now carries a fill rule, set with twin_path_set_fill_rule() as part of its saved state, and the TinyVG loader selects even-odd. Strokes and every other path keep the nonzero default. --- include/twin.h | 22 +++++++++++++++++----- src/image-tvg.c | 4 ++++ src/path.c | 6 ++++++ src/poly.c | 9 ++++++--- tests/test-core.c | 38 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 71 insertions(+), 8 deletions(-) diff --git a/include/twin.h b/include/twin.h index ff23badb..067241c3 100644 --- a/include/twin.h +++ b/include/twin.h @@ -243,6 +243,14 @@ typedef enum _twin_cap { TwinCapProjecting /**< Square line endings extending beyond path */, } twin_cap_t; +/** Which points a filled path covers */ +typedef enum _twin_fill_rule { + /** Where the outline winds around a nonzero number of times (default) */ + TwinFillNonZero, + /** Where a ray crosses the outline an odd number of times */ + TwinFillEvenOdd, +} twin_fill_rule_t; + /** * Graphics state for drawing operations * @@ -250,11 +258,12 @@ typedef enum _twin_cap { * saved and restored during drawing operations. */ typedef struct _twin_state { - twin_matrix_t matrix; /**< Current transformation */ - twin_font_t *font; /**< Font (NULL selects the default) */ - twin_fixed_t font_size; /**< Font size */ - twin_style_t font_style; /**< Font style */ - twin_cap_t cap_style; /**< Line cap style */ + twin_matrix_t matrix; /**< Current transformation */ + twin_font_t *font; /**< Font (NULL selects the default) */ + twin_fixed_t font_size; /**< Font size */ + twin_style_t font_style; /**< Font style */ + twin_cap_t cap_style; /**< Line cap style */ + twin_fill_rule_t fill_rule; /**< Fill rule (strokes always fill nonzero) */ } twin_state_t; /** @@ -680,6 +689,9 @@ void twin_path_set_font_style(twin_path_t *path, twin_style_t font_style); void twin_path_set_cap_style(twin_path_t *path, twin_cap_t cap_style); +/** Fill rule for painting @path; strokes are always filled nonzero */ +void twin_path_set_fill_rule(twin_path_t *path, twin_fill_rule_t rule); + twin_state_t twin_path_save(twin_path_t *path); void twin_path_restore(twin_path_t *path, twin_state_t *state); diff --git a/src/image-tvg.c b/src/image-tvg.c index 010fd001..83a43df7 100644 --- a/src/image-tvg.c +++ b/src/image-tvg.c @@ -1130,6 +1130,10 @@ static tvg_result_t tvg_render_document(tvg_input_func_t in, goto error; } twin_path_scale(ctx.path, width_scale, height_scale); + /* TinyVG fills by the even-odd rule: drawings converted from SVG, such + * as comic.tvg and tiger.tvg, cut their holes with nested outlines + * wound the same way, which nonzero would fill in */ + twin_path_set_fill_rule(ctx.path, TwinFillEvenOdd); res = tvg_parse_commands(&ctx); __goto_if_fail(res, error, "Failed to parse commands"); error: diff --git a/src/path.c b/src/path.c index 78a69134..98e139c9 100644 --- a/src/path.c +++ b/src/path.c @@ -476,6 +476,11 @@ void twin_path_set_cap_style(twin_path_t *path, twin_cap_t cap_style) path->state.cap_style = cap_style; } +void twin_path_set_fill_rule(twin_path_t *path, twin_fill_rule_t rule) +{ + path->state.fill_rule = rule; +} + void twin_path_empty(twin_path_t *path) { path->npoints = 0; @@ -490,6 +495,7 @@ static void _twin_path_reset_state(twin_path_t *path) path->state.font_size = TWIN_FIXED_ONE * 15; path->state.font_style = TwinStyleRoman; path->state.cap_style = TwinCapRound; + path->state.fill_rule = TwinFillNonZero; } void twin_path_set_font(twin_path_t *path, twin_font_t *font) diff --git a/src/poly.c b/src/poly.c index 6b9e15f0..dea4f2d2 100644 --- a/src/poly.c +++ b/src/poly.c @@ -323,6 +323,7 @@ static void _twin_edge_fill(twin_pixmap_t *pixmap, twin_edge_t **order, twin_edge_t **aet, int nedges, + bool even_odd, _band_state_t *band) { twin_sfixed_t x0 = 0; @@ -349,13 +350,14 @@ static void _twin_edge_fill(twin_pixmap_t *pixmap, by = y - twin_int_to_sfixed(band->top); } - /* walk this y value marking coverage */ + /* walk this y value marking coverage: inside while the winding + * number is nonzero, or for even-odd while its parity is odd */ int w = 0; for (int i = 0; i < na; i++) { twin_edge_t *a = aet[i]; if (w == 0) x0 = a->x; - w += a->winding; + w = even_odd ? w ^ 1 : w + a->winding; if (w == 0) _span_fill(pixmap, by, x0, a->x); } @@ -439,7 +441,8 @@ static void _twin_fill(twin_pixmap_t *pixmap, twin_edge_t **order = (twin_edge_t **) ((uint8_t *) edges + ptr_off); for (int i = 0; i < nedges; i++) order[i] = &edges[i]; - _twin_edge_fill(pixmap, order, order + nalloc, nedges, band); + _twin_edge_fill(pixmap, order, order + nalloc, nedges, + path->state.fill_rule == TwinFillEvenOdd, band); if (!from_scratch) twin_free(edges); } diff --git a/tests/test-core.c b/tests/test-core.c index b3c2934e..8fca1450 100644 --- a/tests/test-core.c +++ b/tests/test-core.c @@ -1191,6 +1191,16 @@ static void on_draw(twin_window_t *window) static void test_accessors(void) { + /* The fill rule is drawing state: restore brings back the saved one */ + twin_path_t *fp = twin_path_create(); + twin_path_set_fill_rule(fp, TwinFillEvenOdd); + twin_state_t saved = twin_path_save(fp); + twin_path_set_fill_rule(fp, TwinFillNonZero); + twin_path_restore(fp, &saved); + CHECK(twin_path_save(fp).fill_rule == TwinFillEvenOdd, + "restoring a path's state restores its fill rule"); + twin_path_destroy(fp); + th_t *t = th_create(W, H); CHECK( twin_screen_width(t->screen) == W && twin_screen_height(t->screen) == H, @@ -1482,6 +1492,33 @@ static void test_tvg_color_index(void) twin_pixmap_destroy(p); } +/* TinyVG fills by the even-odd rule: an inner square wound like the outer + * one cuts a hole */ +static void test_tvg_even_odd(void) +{ + /* clang-format off */ + const uint8_t doc[] = { + 0x72, 0x56, 1, 0x44, 2, 2, 1, 0, 0, 0, 255, /* 2x2, one color */ + 0x03, 1, 0, 3, 3, /* fill path: two segments, color 0, 4 cmds each */ + 2, 2, 0, 30, 2, 0, 30, 30, 0, 2, 30, 6, /* outer, closed */ + 10, 10, 0, 22, 10, 0, 22, 22, 0, 10, 22, 6, /* inner, same way */ + 0x00, + }; + /* clang-format on */ + char path[] = "/tmp/mado-tvg-XXXXXX"; + if (!write_fixture(path, doc, sizeof(doc))) + return; + twin_pixmap_t *p = twin_tvg_to_pixmap(path, 32, 32); + unlink(path); + CHECK(p != NULL, "nested TVG squares load"); + if (!p) + return; + CHECK((*twin_pixmap_pointer(p, 5, 16).argb32 >> 24) == 0xff, + "the ring between the squares is filled"); + CHECK_EQ(*twin_pixmap_pointer(p, 16, 16).argb32, 0, + "the inner square is a hole"); + twin_pixmap_destroy(p); +} #endif #if defined(CONFIG_LOADER_GIF) || defined(CONFIG_LOADER_PNG) @@ -1740,6 +1777,7 @@ int main(void) #if defined(CONFIG_LOADER_TVG) test_tvg_hairline(); test_tvg_color_index(); + test_tvg_even_odd(); #endif #if defined(CONFIG_LOADER_PNG) test_png_16bit(); From 6490e39b7dd40c0842fe83372a77c8762dadc01e Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Tue, 20 Oct 2026 06:51:44 +0800 Subject: [PATCH 04/13] Move window management to the top-level menu The window manager choice decides which other options apply, yet it sat in a submenu under Features. Only the menu order changes: defconfig and every CI configuration generate the same config.h. --- configs/Kconfig | 178 ++++++++++++++++++++++++------------------------ 1 file changed, 89 insertions(+), 89 deletions(-) diff --git a/configs/Kconfig b/configs/Kconfig index 62ecbc86..20dc33fc 100644 --- a/configs/Kconfig +++ b/configs/Kconfig @@ -156,6 +156,95 @@ config RENDERER_PIXMAN endchoice +menu "Window Management" + +choice + prompt "Window manager" + default WM_DESKTOP + help + Select the window manager built into the library. Every screen + runs it for its whole life: there is no switching at run time. + +config WM_DESKTOP + bool "Desktop (classic)" + help + Classic desktop: overlapping windows with title bars, click-to-raise, drag-to-move, + double-click maximize, edge snapping, corner resizing, + iconify/restore, close, and a drop shadow under the active window. + + Costs ~8 KB .text on RV32IM (title bars add src/icon.c). + +config WM_MOBILE + bool "Mobile (embedded)" + help + Embedded, phone-style shell: application windows stack over the work area + between optional system bars (status bar, navigation bar), only the + top application is visible, and push/pop/dismiss transitions plus + an edge-swipe back gesture animate retained pixels without + repainting applications. Escape also goes back. + +config WM_NONE + bool "None (minimal)" + help + Minimal, no window manager: no decorations, every application window covers + the screen, and the keyboard goes to the newest or the last pressed + window. The demo launcher starts only the first demo. + +endchoice + +config WM_MOBILE_EVICT + bool "Release pixels of covered applications" + default y + depends on WM_MOBILE + help + Free the pixmap of every application hidden under the top one and + repaint it from application state when it is revealed again. + Keeps memory at one work-area surface regardless of stack depth. + +config DROP_SHADOW + bool "Window drop shadows" + default y if !WM_NONE + help + Cast a soft shadow under the active desktop window and under the + sliding window of mobile transitions. The compositor synthesizes + it while scanning out, so it needs no extra pixmap memory and no + blur pass, but every shadowed span costs blending on scan-out; + small targets can turn it off and keep plain slides. Without it the + compositor carries no shadow code at all. + +config HORIZONTAL_OFFSET + int "Horizontal offset" + default 0 + range 0 10 + depends on DROP_SHADOW && WM_DESKTOP + help + Horizontal shadow offset of the active window, in pixels. Modern + desktops light windows from above, so the default is 0: the shadow + shows evenly left and right. Inactive windows use half. + +config VERTICAL_OFFSET + int "Vertical offset" + default 4 + range 0 10 + depends on DROP_SHADOW && WM_DESKTOP + help + Vertical shadow offset of the active window, in pixels. A small + downward offset under a wide blur gives a halo all around that is + heavier below the window. Inactive windows use half. + +config SHADOW_BLUR + int "Shadow blur radius" + default 14 + range 1 15 + depends on DROP_SHADOW && WM_DESKTOP + help + Width in pixels of the active window's shadow edge; inactive windows + use half, so the active one appears to float higher. The shadow + is computed while scanning out, so the radius does not change + its cost per pixel. + +endmenu + menu "Features" config LOGGING @@ -294,95 +383,6 @@ config CURSOR Enable custom cursor rendering and manipulation. Allows application control over cursor appearance. -menu "Window Management" - -choice - prompt "Window manager" - default WM_DESKTOP - help - Select the window manager built into the library. Every screen - runs it for its whole life: there is no switching at run time. - -config WM_DESKTOP - bool "Desktop (classic)" - help - Classic desktop: overlapping windows with title bars, click-to-raise, drag-to-move, - double-click maximize, edge snapping, corner resizing, - iconify/restore, close, and a drop shadow under the active window. - - Costs ~8 KB .text on RV32IM (title bars add src/icon.c). - -config WM_MOBILE - bool "Mobile (embedded)" - help - Embedded, phone-style shell: application windows stack over the work area - between optional system bars (status bar, navigation bar), only the - top application is visible, and push/pop/dismiss transitions plus - an edge-swipe back gesture animate retained pixels without - repainting applications. Escape also goes back. - -config WM_NONE - bool "None (minimal)" - help - Minimal, no window manager: no decorations, every application window covers - the screen, and the keyboard goes to the newest or the last pressed - window. The demo launcher starts only the first demo. - -endchoice - -config WM_MOBILE_EVICT - bool "Release pixels of covered applications" - default y - depends on WM_MOBILE - help - Free the pixmap of every application hidden under the top one and - repaint it from application state when it is revealed again. - Keeps memory at one work-area surface regardless of stack depth. - -config DROP_SHADOW - bool "Window drop shadows" - default y if !WM_NONE - help - Cast a soft shadow under the active desktop window and under the - sliding window of mobile transitions. The compositor synthesizes - it while scanning out, so it needs no extra pixmap memory and no - blur pass, but every shadowed span costs blending on scan-out; - small targets can turn it off and keep plain slides. Without it the - compositor carries no shadow code at all. - -config HORIZONTAL_OFFSET - int "Horizontal offset" - default 0 - range 0 10 - depends on DROP_SHADOW && WM_DESKTOP - help - Horizontal shadow offset of the active window, in pixels. Modern - desktops light windows from above, so the default is 0: the shadow - shows evenly left and right. Inactive windows use half. - -config VERTICAL_OFFSET - int "Vertical offset" - default 4 - range 0 10 - depends on DROP_SHADOW && WM_DESKTOP - help - Vertical shadow offset of the active window, in pixels. A small - downward offset under a wide blur gives a halo all around that is - heavier below the window. Inactive windows use half. - -config SHADOW_BLUR - int "Shadow blur radius" - default 14 - range 1 15 - depends on DROP_SHADOW && WM_DESKTOP - help - Width in pixels of the active window's shadow edge; inactive windows - use half, so the active one appears to float higher. The shadow - is computed while scanning out, so the radius does not change - its cost per pixel. - -endmenu - menu "Rendering" config SCRATCH_SIZE From d0bbdd8815a338c4841e831a521374a9a2f6b73a Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Tue, 20 Oct 2026 06:51:44 +0800 Subject: [PATCH 05/13] Fit the Vectors image to its window The demo rendered every drawing at 400x400 and drew it at the corner, so under the mobile policy, which gives the window the 240x258 work area, only a corner of each picture showed; its widget also claimed three quarters of the screen height, which stretched the button and broke the layout on tall desktops. The image now takes the room the button leaves, renders to fit it when that size changes, and is centered; the button keeps its label's height. twin_widget_set_stretch() lets a widget keep its preferred size in a box, and twin_tvg_to_pixmap() returns a pixmap of the drawing's aspect within the size asked, so no margin is stored beside it. --- apps/image.c | 66 +++++++++++++++++++++++++++------------------- include/twin.h | 21 ++++++++++++--- src/box.c | 12 +++++++-- src/image-tvg.c | 11 ++++++-- src/twin_private.h | 5 ++-- src/widget.c | 11 ++++++++ tests/test-core.c | 51 +++++++++++++++++++++++++++++++++++ 7 files changed, 141 insertions(+), 36 deletions(-) diff --git a/apps/image.c b/apps/image.c index 4693535c..d9c6a432 100644 --- a/apps/image.c +++ b/apps/image.c @@ -16,16 +16,14 @@ static inline twin_pixmap_t *_apps_image_pixmap(twin_custom_widget_t *image) } #define D(x) twin_double_to_fixed(x) +/* The desktop window; a mobile policy gives it the whole work area */ #define APP_WIDTH 400 #define APP_HEIGHT 400 -static twin_pixmap_t *load_tvg(const char *path) -{ - return twin_tvg_to_pixmap(path, APP_WIDTH, APP_HEIGHT); -} - typedef struct { twin_pixmap_t *current_pix; /* only the visible image is kept */ + twin_coord_t fit_w, fit_h; /* the widget size it was rendered for */ + bool tried; /* rendered, or failed, at that size */ int image_idx; } apps_image_data_t; @@ -48,16 +46,38 @@ static void _apps_image_paint(twin_custom_widget_t *custom) { apps_image_data_t *img = (apps_image_data_t *) twin_custom_widget_data(custom); + twin_coord_t w = twin_custom_widget_width(custom); + twin_coord_t h = twin_custom_widget_height(custom); + if (w <= 0 || h <= 0) + return; + + /* Render the image to fit the widget, again whenever its size changes + * (a phone's work area, a resized window) */ + if (img->fit_w != w || img->fit_h != h) { + if (img->current_pix) + twin_pixmap_destroy(img->current_pix); + img->current_pix = NULL; + img->tried = false; + } + /* A file that fails to render is not tried again on every repaint */ + if (!img->tried) { + img->current_pix = twin_tvg_to_pixmap(tvg_files[img->image_idx], w, h); + img->fit_w = w; + img->fit_h = h; + img->tried = true; + } if (!img->current_pix) return; + + /* It keeps the drawing's aspect: center it in the widget */ + twin_coord_t pw = twin_pixmap_width(img->current_pix); + twin_coord_t ph = twin_pixmap_height(img->current_pix); twin_operand_t srcop = { .source_kind = TWIN_PIXMAP, .u.pixmap = img->current_pix, }; - - twin_composite(_apps_image_pixmap(custom), 0, 0, &srcop, 0, 0, NULL, 0, 0, - TWIN_OVER, twin_pixmap_width(img->current_pix), - twin_pixmap_height(img->current_pix)); + twin_composite(_apps_image_pixmap(custom), (w - pw) / 2, (h - ph) / 2, + &srcop, 0, 0, NULL, 0, 0, TWIN_OVER, pw, ph); } static twin_dispatch_result_t _apps_image_dispatch(twin_custom_widget_t *custom, @@ -97,31 +117,24 @@ static twin_dispatch_result_t _apps_image_button_clicked(twin_widget_t *widget, const int n = sizeof(tvg_files) / sizeof(tvg_files[0]); img->image_idx = img->image_idx == n - 1 ? 0 : img->image_idx + 1; - /* Evict the old pixmap before loading the new one */ + /* Free the old image; the repaint renders the new one */ if (img->current_pix) { twin_pixmap_destroy(img->current_pix); img->current_pix = NULL; } - img->current_pix = load_tvg(tvg_files[img->image_idx]); - if (!img->current_pix) - return TwinDispatchContinue; + img->tried = false; twin_custom_widget_queue_paint(custom); return TwinDispatchDone; } -static twin_custom_widget_t *_apps_image_init(twin_box_t *parent, - twin_coord_t height) +/* The image takes all the room the button leaves, whatever the window */ +static twin_custom_widget_t *_apps_image_init(twin_box_t *parent) { twin_custom_widget_t *custom = twin_custom_widget_create( - parent, 0, 0, height, _apps_image_dispatch, sizeof(apps_image_data_t)); + parent, 0, 0, 0, _apps_image_dispatch, sizeof(apps_image_data_t)); if (!custom) return NULL; - apps_image_data_t *img = - (apps_image_data_t *) twin_custom_widget_data(custom); - img->image_idx = 0; - img->current_pix = load_tvg(tvg_files[0]); - twin_button_t *button = twin_button_create(parent, "Next Image", 0xFF482722, D(10), TwinStyleBold | TwinStyleOblique); @@ -131,17 +144,17 @@ static twin_custom_widget_t *_apps_image_init(twin_box_t *parent, twin_widget_set(bw, 0xFFFEE4CE); twin_widget_set_callback(bw, _apps_image_button_clicked, custom); twin_widget_set_shape(bw, TwinShapeRectangle); + twin_widget_set_stretch(bw, 1, 0); /* as tall as its label, no taller */ return custom; } -static twin_custom_widget_t *apps_image_create(twin_box_t *parent, - twin_coord_t height) +static twin_custom_widget_t *apps_image_create(twin_box_t *parent) { - return _apps_image_init(parent, height); + return _apps_image_init(parent); } -/* Fixed size: @w and @h are ignored */ +/* APP_WIDTH x APP_HEIGHT on a desktop: @w and @h are ignored */ bool apps_image_start(twin_screen_t *screen, const char *name, int x, @@ -154,8 +167,7 @@ bool apps_image_start(twin_screen_t *screen, twin_toplevel_create(screen, x, y, APP_WIDTH, APP_HEIGHT, name); if (!toplevel) return false; - if (!apps_image_create(twin_toplevel_box(toplevel), - twin_screen_height(screen) * 3 / 4)) { + if (!apps_image_create(twin_toplevel_box(toplevel))) { twin_toplevel_destroy(toplevel); return false; } diff --git a/include/twin.h b/include/twin.h index 067241c3..5a796208 100644 --- a/include/twin.h +++ b/include/twin.h @@ -839,6 +839,19 @@ void twin_widget_set_shape(twin_widget_t *widget, twin_shape_t shape); /** Make @widget request the same preferred size as @source */ void twin_widget_match_size(twin_widget_t *widget, twin_widget_t *source); +/** + * How much of its box's spare room @widget takes in each direction + * + * A box divides the room by the sum of its children's stretches, so 0 keeps + * @widget at its preferred size (widgets start at 1). The last widget in a + * box also takes what rounding leaves, and all of the room when no sibling + * stretches. On a box, this replaces the stretch it adds up from its + * children. + */ +void twin_widget_set_stretch(twin_widget_t *widget, + twin_stretch_t width, + twin_stretch_t height); + /* * Custom widget support - allows creating widgets without accessing internals * @@ -1231,10 +1244,12 @@ twin_time_t twin_now(void); /** * Render a TinyVG file (CONFIG_LOADER_TVG) * @filepath : File to load - * @w, @h : Pixmap size, the drawing scaled to fit; 0: the document's own + * @w, @h : Largest pixmap size, the drawing scaled to fit; 0: the + * document's own * - * The pixmap shrinks further, keeping its aspect, to fit - * CONFIG_TVG_MEMORY_BUDGET when that is set. + * The pixmap takes the drawing's aspect, so one side may come out shorter + * than asked, and shrinks further to fit CONFIG_TVG_MEMORY_BUDGET when that + * is set: read its size rather than assume @w x @h. */ twin_pixmap_t *twin_tvg_to_pixmap(const char *filepath, twin_coord_t w, diff --git a/src/box.c b/src/box.c index fd90ed22..4cf0c8c0 100644 --- a/src/box.c +++ b/src/box.c @@ -58,6 +58,11 @@ static twin_dispatch_result_t _twin_box_query_geometry(twin_box_t *box) preferred.stretch_width = child->preferred.stretch_width; } } + /* A stretch set explicitly outlasts what the children add up to */ + if (box->widget.fixed_stretch) { + preferred.stretch_width = box->widget.preferred.stretch_width; + preferred.stretch_height = box->widget.preferred.stretch_height; + } box->widget.preferred = preferred; return TwinDispatchContinue; } @@ -75,14 +80,17 @@ static twin_dispatch_result_t _twin_box_configure(twin_box_t *box) twin_widget_t *child; if (box->dir == TwinBoxHorz) { - stretch = box->widget.preferred.stretch_width; actual = width; pref = box->widget.preferred.width; } else { - stretch = box->widget.preferred.stretch_height; actual = height; pref = box->widget.preferred.height; } + /* The children share the room by their own stretches: a stretch set on + * the box itself is for its parent */ + for (child = box->children; child; child = child->next) + stretch += box->dir == TwinBoxHorz ? child->preferred.stretch_width + : child->preferred.stretch_height; if (!stretch) stretch = 1; delta = delta_remain = actual - pref; diff --git a/src/image-tvg.c b/src/image-tvg.c index 83a43df7..2c267dce 100644 --- a/src/image-tvg.c +++ b/src/image-tvg.c @@ -22,6 +22,7 @@ (((uint32_t) (a) << 24) | ((uint32_t) (r) << 16) | ((uint32_t) (g) << 8) | \ (b)) #define MIN(A, B) ((A) < (B) ? (A) : (B)) +#define MAX(A, B) ((A) > (B) ? (A) : (B)) enum { /* end of document This command determines the end of file. */ @@ -1185,8 +1186,9 @@ static bool tvg_fit_budget(twin_format_t fmt, return best_w && best_h; } -/* Render @filepath into a @w x @h pixmap, the drawing scaled to fit (0 x 0: - * the document's own size), shrunk further to CONFIG_TVG_MEMORY_BUDGET */ +/* Render @filepath scaled to fit @w x @h (0 x 0: the document's own size) + * into a pixmap of the drawing's aspect, shrunk further to + * CONFIG_TVG_MEMORY_BUDGET */ static twin_pixmap_t *tvg_load(const char *filepath, twin_format_t fmt, uint32_t w, @@ -1215,6 +1217,11 @@ static twin_pixmap_t *tvg_load(const char *filepath, /* Pixmaps are indexed with twin_coord_t */ w = MIN(w, INT16_MAX); h = MIN(h, INT16_MAX); + /* Take the drawing's aspect, so no margin is stored around it */ + if ((uint64_t) w * height <= (uint64_t) h * width) + h = MAX((uint32_t) ((uint64_t) height * w / width), 1); + else + w = MAX((uint32_t) ((uint64_t) width * h / height), 1); #if defined(CONFIG_TVG_MEMORY_BUDGET) && CONFIG_TVG_MEMORY_BUDGET > 0 if (!tvg_fit_budget(fmt, &w, &h, (size_t) CONFIG_TVG_MEMORY_BUDGET * 1024)) goto bail; diff --git a/src/twin_private.h b/src/twin_private.h index 18e84801..24ce5649 100644 --- a/src/twin_private.h +++ b/src/twin_private.h @@ -360,8 +360,9 @@ struct _twin_widget { twin_widget_t *copy_geom; /**< Geometry source widget (optional) */ /* Widget state */ - bool paint; /**< Needs painting */ - bool layout; /**< Needs layout */ + bool paint; /**< Needs painting */ + bool layout; /**< Needs layout */ + bool fixed_stretch; /**< Stretch set explicitly: a box keeps it */ /* Widget appearance */ twin_argb32_t background; /**< Background color */ diff --git a/src/widget.c b/src/widget.c index b4146f1b..3863058f 100644 --- a/src/widget.c +++ b/src/widget.c @@ -112,6 +112,7 @@ void _twin_widget_init(twin_widget_t *widget, widget->copy_geom = NULL; widget->paint = true; widget->layout = true; + widget->fixed_stretch = false; widget->background = 0x00000000; widget->extents.left = widget->extents.top = 0; widget->extents.right = widget->extents.bottom = 0; @@ -234,6 +235,16 @@ void twin_widget_match_size(twin_widget_t *widget, twin_widget_t *source) _twin_widget_queue_layout(widget); } +void twin_widget_set_stretch(twin_widget_t *widget, + twin_stretch_t width, + twin_stretch_t height) +{ + widget->preferred.stretch_width = width; + widget->preferred.stretch_height = height; + widget->fixed_stretch = true; + _twin_widget_queue_layout(widget); +} + static twin_coord_t twin_widget_width(twin_widget_t *widget) { return _twin_widget_width(widget); diff --git a/tests/test-core.c b/tests/test-core.c index 8fca1450..fe5316d1 100644 --- a/tests/test-core.c +++ b/tests/test-core.c @@ -1347,6 +1347,44 @@ static void test_toplevel_background(void) /* Boxes paint their own background in the gaps their children leave, a * background or shape change repaints everything over it, and a shaped * root leaves the rest of the window transparent */ +/* A stretch set on a box outlasts the next layout: a nested box held at + * stretch 0 keeps its children's height while a flexible sibling takes the + * rest of the window */ +static void test_box_stretch(void) +{ + th_t *t = th_create(W, H); + twin_toplevel_t *top = twin_toplevel_create(t->screen, 0, 0, W, H, "t"); + twin_box_t *row = twin_box_create(twin_toplevel_box(top), TwinBoxHorz); + twin_custom_widget_t *cell = + twin_custom_widget_create(row, 0, 0, 10, NULL, 0); + twin_custom_widget_t *rest = + twin_custom_widget_create(twin_toplevel_box(top), 0, 0, 0, NULL, 0); + twin_widget_set_stretch(twin_box_widget(row), 1, 0); + twin_toplevel_show(top); + th_frame(t); + CHECK_EQ(twin_custom_widget_height(cell), 10, + "a box held at stretch 0 keeps its height"); + CHECK(twin_custom_widget_height(rest) > 10, + "the flexible sibling takes the room (%d)", + twin_custom_widget_height(rest)); + twin_toplevel_destroy(top); + + /* Inside a box, its children share the room by their own stretches, + * whatever stretch the box shows its parent */ + top = twin_toplevel_create(t->screen, 0, 0, W, H, "t"); + twin_box_t *col = twin_box_create(twin_toplevel_box(top), TwinBoxVert); + twin_custom_widget_t *a = twin_custom_widget_create(col, 0, 0, 0, NULL, 0); + twin_custom_widget_t *b = twin_custom_widget_create(col, 0, 0, 0, NULL, 0); + twin_widget_set_stretch(twin_box_widget(col), 1, 1); + twin_toplevel_show(top); + th_frame(t); + int ha = twin_custom_widget_height(a), hb = twin_custom_widget_height(b); + CHECK(ha > 0 && hb > 0 && ha - hb <= 1 && hb - ha <= 1, + "two equal children split a stretched box evenly (%d, %d)", ha, hb); + twin_toplevel_destroy(top); + th_destroy(t); +} + static void test_box_background(void) { th_t *t = th_create(W, H); @@ -1518,6 +1556,18 @@ static void test_tvg_even_odd(void) CHECK_EQ(*twin_pixmap_pointer(p, 16, 16).argb32, 0, "the inner square is a hole"); twin_pixmap_destroy(p); + + /* The pixmap takes the drawing's aspect: a square asked for 64x32 + * comes back 32x32, with no margin stored beside it */ + char wide_path[] = "/tmp/mado-tvg-XXXXXX"; + if (!write_fixture(wide_path, doc, sizeof(doc))) + return; + p = twin_tvg_to_pixmap(wide_path, 64, 32); + unlink(wide_path); + CHECK(p && twin_pixmap_width(p) == 32 && twin_pixmap_height(p) == 32, + "TVG pixmap keeps the drawing's aspect"); + if (p) + twin_pixmap_destroy(p); } #endif @@ -1769,6 +1819,7 @@ int main(void) test_widget_teardown(); test_toplevel_background(); test_box_background(); + test_box_stretch(); #endif #if defined(CONFIG_LOADER_GIF) test_gif_malformed(); From a55f90cde14e042843df435a7f0be5bb1f780013 Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Tue, 20 Oct 2026 09:10:35 +0800 Subject: [PATCH 06/13] Make the multi-window demo desktop only The demo shows overlapping windows: under the mobile policy each one covers the last, and the phone listed a launcher icon that opened six stacked screens. It now depends on the desktop window manager, and its comments no longer describe the other policies. --- apps/multi.c | 10 ++++------ configs/Kconfig | 7 +++---- tests/phone-demos.c | 2 +- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/apps/multi.c b/apps/multi.c index 4aed1fc4..e029ecd7 100644 --- a/apps/multi.c +++ b/apps/multi.c @@ -170,10 +170,9 @@ static bool apps_multiline_start(twin_screen_t *screen, } /* - * The windows below paint from draw callbacks at whatever size the window - * manager gives them: a desktop keeps the requested size, a phone or the - * 'none' policy hands out the whole work area, and the mobile policy may - * free a covered window's pixels and ask for them again. + * The windows below paint from draw callbacks at whatever size their + * client area has, so a resize repaints them to fit. Desktop only: the + * demo is about overlapping windows. */ /* Client size of the window being drawn */ @@ -215,8 +214,7 @@ static bool start_window(twin_screen_t *screen, twin_window_set_name(window, name); twin_window_set_client_data(window, data); twin_window_set_callbacks(window, draw, NULL, destroy); - /* Painted before the next redisplay, and only if still shown: on a - * phone each window covers the one opened before it */ + /* Painted before the next redisplay, and only if still shown */ twin_rect_t c = twin_window_client(window); twin_window_damage(window, c.left, c.top, c.right, c.bottom); twin_window_show(window); diff --git a/configs/Kconfig b/configs/Kconfig index 20dc33fc..34ef945c 100644 --- a/configs/Kconfig +++ b/configs/Kconfig @@ -520,12 +520,11 @@ config DEMO_CELLPHONE_RGB16 config DEMO_MULTI bool "Build multi demo" default y - depends on DEMO_APPLICATIONS && WIDGETS + depends on DEMO_APPLICATIONS && WIDGETS && WM_DESKTOP help Comprehensive demo with multiple windows and widgets. - Demonstrates window management and event handling: the desktop - policy staggers its windows, the mobile one stacks them and Back - closes one at a time. + Demonstrates window management and event handling on the desktop + policy, which staggers its overlapping windows. config DEMO_CLOCK bool "Build clock demo" diff --git a/tests/phone-demos.c b/tests/phone-demos.c index a288fbd4..aec5ba9f 100644 --- a/tests/phone-demos.c +++ b/tests/phone-demos.c @@ -74,7 +74,7 @@ void test_demos(void) * default set */ #if defined(CONFIG_DEMO_CLOCK) && defined(CONFIG_DEMO_CALCULATOR) && \ defined(CONFIG_DEMO_SPLINE) && defined(CONFIG_DEMO_ANIMATION) && \ - defined(CONFIG_DEMO_IMAGE) && defined(CONFIG_DEMO_MULTI) + defined(CONFIG_DEMO_IMAGE) ph_shot("home-demos"); #else ph_check("home-demos"); From 7e379dde8a67c391a07a56188ed09812c4526d17 Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Tue, 20 Oct 2026 09:10:36 +0800 Subject: [PATCH 07/13] Share one calculator between desktop and phone Two calculators did the same job: the desktop one stacked integers in ellipse buttons whose rectangular bevels left wedges between rows, and the phone kept its own decimal engine and keypad. apps/calc.c is now the only one. Both draw it into a twin window, so the keypad, the arithmetic, painting and input are shared; a front end supplies only the window and a palette. The desktop gets flat round keys colored by role, a fitted display and keyboard input; the phone keeps its theme. The keypad gains an erase key, also on Backspace and Delete, which drops the last character typed. The phone lists the shared app once: its launcher row names the desktop entry it replaces. --- Makefile | 1 - apps/apps.c | 4 +- apps/calc.c | 905 +++++++++++++++++++++++++++++++---------- apps/cellphone/calc.c | 425 ------------------- apps/cellphone/phone.h | 7 +- apps/cellphone/shell.c | 40 +- configs/Kconfig | 10 +- tests/test-cellphone.c | 10 + 8 files changed, 743 insertions(+), 659 deletions(-) delete mode 100644 apps/cellphone/calc.c diff --git a/Makefile b/Makefile index d6abc736..2c1b087f 100644 --- a/Makefile +++ b/Makefile @@ -182,7 +182,6 @@ libapps.a_files-$(CONFIG_DEMO_CELLPHONE) += \ apps/cellphone/home.c \ apps/cellphone/lock.c \ apps/cellphone/data.c \ - apps/cellphone/calc.c \ apps/cellphone/dialer.c \ apps/cellphone/contacts.c \ apps/cellphone/sms.c \ diff --git a/apps/apps.c b/apps/apps.c index 796f0c86..8c8be0e1 100644 --- a/apps/apps.c +++ b/apps/apps.c @@ -25,8 +25,8 @@ static const apps_entry_t apps[] = { {"Clock", TWIN_SYM_CLOCK, 10, 10, 200, 200, apps_clock_start, NULL, NULL}, #endif #if defined(CONFIG_DEMO_CALCULATOR) - {"Calc", TWIN_SYM_CALCULATOR, 100, 100, 200, 200, apps_calc_start, NULL, - NULL}, + {"Calculator", TWIN_SYM_CALCULATOR, 100, 100, 220, 300, apps_calc_start, + NULL, NULL}, #endif #if defined(CONFIG_DEMO_SPLINE) {"Spline", TWIN_SYM_SHUFFLE, 20, 20, 400, 400, apps_spline_start, NULL, diff --git a/apps/calc.c b/apps/calc.c index 4cab58ef..ed9f5526 100644 --- a/apps/calc.c +++ b/apps/calc.c @@ -1,212 +1,643 @@ /* * Twin - A Tiny Window System * Copyright (c) 2004 Keith Packard + * Copyright (c) 2026 National Cheng Kung University, Taiwan * All rights reserved. */ +/* + * Calculator: four functions on 64-bit integers scaled by 10^6. + * + * One source serves the desktop demo and the phone app. Both draw into a + * twin window, so the keypad, the arithmetic, the painting and the input + * handling are shared; a front end only supplies the window and a palette. + */ + #include #include +#include #include "apps.h" -#define APPS_CALC_STACK 5 - -enum { -#define _(x) APPS_CALC_##x - _(ZERO), - _(ONE), - _(TWO), - _(THREE), - _(FOUR), - _(FIVE), - _(SIX), - _(SEVEN), - _(EIGHT), - _(NINE), - _(PLUS), - _(MINUS), - _(TIMES), - _(DIVIDE), - _(EQUAL), - _(CLEAR), - _(N_BUTTONS), -#undef _ +#if defined(CONFIG_DEMO_CELLPHONE) +#include "phone.h" +#endif + +#define COLS 4 +#define ROWS 5 +#define SCALE 1000000LL +#define BUF_LEN 24 + +#if defined(CONFIG_FONT_MONTSERRAT) +#define FONT (&twin_font_montserrat) +#else +#define FONT NULL /* the built-in font */ +#endif + +/* The erase key shows the backspace symbol where the icon font is built */ +#if defined(CONFIG_FONT_ICONS) +#define ERASE TWIN_SYM_BACKSPACE +#define ERASE_FONT (&twin_font_icons) +#else +#define ERASE "<" +#define ERASE_FONT FONT +#endif + +typedef enum { DIGIT, FUNCTION, OPERATOR, EQUALS, N_KINDS } calc_kind_t; + +typedef struct { + const char *label; + char col, row, span; + calc_kind_t kind; +} calc_key_t; + +static const calc_key_t keys[] = { + {"C", 0, 0, 1, FUNCTION}, {ERASE, 1, 0, 1, FUNCTION}, + {"%", 2, 0, 1, FUNCTION}, {"/", 3, 0, 1, OPERATOR}, + {"7", 0, 1, 1, DIGIT}, {"8", 1, 1, 1, DIGIT}, + {"9", 2, 1, 1, DIGIT}, {"x", 3, 1, 1, OPERATOR}, + {"4", 0, 2, 1, DIGIT}, {"5", 1, 2, 1, DIGIT}, + {"6", 2, 2, 1, DIGIT}, {"-", 3, 2, 1, OPERATOR}, + {"1", 0, 3, 1, DIGIT}, {"2", 1, 3, 1, DIGIT}, + {"3", 2, 3, 1, DIGIT}, {"+", 3, 3, 1, OPERATOR}, + {"+/-", 0, 4, 1, FUNCTION}, {"0", 1, 4, 1, DIGIT}, + {".", 2, 4, 1, DIGIT}, {"=", 3, 4, 1, EQUALS}, }; +#define N_KEYS ((int) (sizeof(keys) / sizeof(keys[0]))) + +typedef struct { + twin_argb32_t bg, text, accent; /* accent: the pending operator */ + twin_argb32_t key[N_KINDS], key_text[N_KINDS]; + twin_argb32_t down, down_text; +} calc_palette_t; + +typedef struct { + twin_window_t *window; + twin_path_t *path; /* reused by every paint; NULL draws square keys */ + char display[BUF_LEN]; + int64_t accumulator; /* left operand, scaled */ + int64_t operand; /* value being entered, scaled */ + char pending; /* '+', '-', '*', '/' or 0 */ + bool new_input; /* next digit starts a new number */ + int32_t frac_div; /* 0: integer part; 10, 100, ...: after the dot */ + bool negative; + bool fresh; /* an operator was just pressed: no operand typed yet */ + bool error; + int pressed; /* key index, -1 when none */ +} calc_t; + /* - * Layout: - * - * display - * - * 7 8 9 + - * 4 5 6 - - * 1 2 3 * - * 0 clr = ÷ + * Arithmetic and entry */ -#define APPS_CALC_COLS 4 -#define APPS_CALC_ROWS 4 - -static const int calc_layout[APPS_CALC_ROWS][APPS_CALC_COLS] = { -#define _(x) APPS_CALC_##x - {_(SEVEN), _(EIGHT), _(NINE), _(PLUS)}, - {_(FOUR), _(FIVE), _(SIX), _(MINUS)}, - {_(ONE), _(TWO), _(THREE), _(TIMES)}, - {_(ZERO), _(CLEAR), _(EQUAL), _(DIVIDE)}, -#undef _ -}; -typedef struct _twin_calc { - int stack[APPS_CALC_STACK]; - int pending_op; - bool pending_delete; - twin_window_t *window; - twin_toplevel_t *toplevel; - twin_box_t *keys; - twin_box_t *cols[4]; - twin_label_t *display; - twin_button_t *buttons[APPS_CALC_N_BUTTONS]; -} apps_calc_t; - -static const char *apps_calc_labels[] = { - "0", "1", "2", "3", "4", "5", "6", "7", - "8", "9", "+", "-", "*", "/", "=", "CLR", -}; +static void format_scaled(calc_t *c, int64_t val) +{ + char *p = c->display; + int64_t a = val < 0 ? -val : val; + int pos = snprintf(p, BUF_LEN, "%s%lld", val < 0 ? "-" : "", + (long long) (a / SCALE)); + int64_t frac = a % SCALE; + if (frac && pos < BUF_LEN - 2) { + p[pos++] = '.'; + for (int64_t div = SCALE / 10; div && frac && pos < BUF_LEN - 1; + div /= 10) { + p[pos++] = (char) ('0' + frac / div); + frac %= div; + } + while (p[pos - 1] == '0') + pos--; + } + p[pos] = 0; +} -#define APPS_CALC_VALUE_SIZE twin_int_to_fixed(29) -#define APPS_CALC_VALUE_STYLE TwinStyleRoman -#define APPS_CALC_VALUE_FG 0xff000000 -/* The display and keys are translucent white: keep a black panel under - * them */ -#define APPS_CALC_BG 0xff000000 -#define APPS_CALC_VALUE_BG 0x80808080 -#define APPS_CALC_BUTTON_SIZE twin_int_to_fixed(15) -#define APPS_CALC_BUTTON_STYLE TwinStyleBold -#define APPS_CALC_BUTTON_FG 0xff000000 -#define APPS_CALC_BUTTON_BG 0xc0808080 - -static int _apps_calc_button_to_id(apps_calc_t *calc, twin_button_t *button) -{ - for (int i = 0; i < APPS_CALC_N_BUTTONS; i++) - if (calc->buttons[i] == button) +/* + * (a * b) / SCALE without a 128-bit type (32-bit targets lack one): + * split both operands into integer and fractional parts. + */ +static bool scaled_mul(int64_t a, int64_t b, int64_t *res) +{ + int64_t ai = a / SCALE, af = a % SCALE, bi = b / SCALE, bf = b % SCALE; + int64_t t0, t1, t2, sum; + if (__builtin_mul_overflow(ai, bi, &t0) || + __builtin_mul_overflow(t0, SCALE, &t0) || + __builtin_mul_overflow(ai, bf, &t1) || + __builtin_mul_overflow(af, bi, &t2) || + __builtin_add_overflow(t0, t1, &sum) || + __builtin_add_overflow(sum, t2, &sum) || + __builtin_add_overflow(sum, af * bf / SCALE, &sum)) + return false; + *res = sum; + return true; +} + +/* + * (a * SCALE) / b, truncated toward zero, without forming the product: + * integer quotient first, then the six fractional digits by long division + * of the remainder. Fails only when the result itself does not fit. + */ +static bool scaled_div(int64_t a, int64_t b, int64_t *res) +{ + if (!b) + return false; + bool neg = (a < 0) != (b < 0); + uint64_t ua = a < 0 ? -(uint64_t) a : (uint64_t) a; + uint64_t ub = b < 0 ? -(uint64_t) b : (uint64_t) b; + uint64_t q = ua / ub, r = ua % ub; + if (q > (uint64_t) INT64_MAX / SCALE) + return false; + q *= SCALE; + uint64_t frac = 0; + for (int64_t div = SCALE / 10; div; div /= 10) { + /* Next digit of r / ub: 10 r = d ub + r', with r, r' < ub, so the + * running sum stays below 2 ub and never wraps */ + uint64_t t = 0; + int d = 0; + for (int k = 0; k < 10; k++) { + t += r; + if (t >= ub) { + t -= ub; + d++; + } + } + r = t; + frac += (uint64_t) d * (uint64_t) div; + } + if (q + frac > (uint64_t) INT64_MAX) + return false; + *res = neg ? -(int64_t) (q + frac) : (int64_t) (q + frac); + return true; +} + +static bool execute(calc_t *c) +{ + switch (c->pending) { + case '+': + if (__builtin_add_overflow(c->accumulator, c->operand, &c->accumulator)) + goto fail; + break; + case '-': + if (__builtin_sub_overflow(c->accumulator, c->operand, &c->accumulator)) + goto fail; + break; + case '*': + if (!scaled_mul(c->accumulator, c->operand, &c->accumulator)) + goto fail; + break; + case '/': + if (!scaled_div(c->accumulator, c->operand, &c->accumulator)) + goto fail; + break; + default: + c->accumulator = c->operand; + break; + } + /* INT64_MIN has no positive counterpart: +/- and the display negate */ + if (c->accumulator == INT64_MIN) + goto fail; + return true; +fail: + c->error = true; + return false; +} + +static void reset(calc_t *c) +{ + c->accumulator = c->operand = 0; + c->pending = 0; + c->new_input = true; + c->frac_div = 0; + c->negative = c->fresh = c->error = false; + strcpy(c->display, "0"); +} + +static void append_digit(calc_t *c, char digit) +{ + if (c->new_input) { + c->display[0] = digit; + c->display[1] = 0; + c->new_input = false; + c->frac_div = 0; + c->negative = false; + c->operand = 0; + } else { + if (c->frac_div > SCALE) + return; + size_t len = strlen(c->display); + /* A lone "0" or "-0" is replaced, not extended */ + if (!c->frac_div && !c->operand && c->display[len - 1] == '0') + len--; + size_t digits = len - (c->display[0] == '-'); + if (len >= BUF_LEN - 2 || (!c->frac_div && digits >= 12)) + return; + c->display[len] = digit; + c->display[len + 1] = 0; + } + int64_t d = digit - '0'; + int64_t step = c->frac_div ? d * (SCALE / c->frac_div) : 0; + if (c->frac_div) { + c->operand += c->negative ? -step : step; + c->frac_div *= 10; + } else { + c->operand = c->operand * 10 + (c->negative ? -d : d) * SCALE; + } +} + +static void append_dot(calc_t *c) +{ + if (c->frac_div) + return; + c->frac_div = 10; + if (c->new_input) { + strcpy(c->display, "0."); + c->new_input = false; + c->operand = 0; + c->negative = false; + } else if (strlen(c->display) < BUF_LEN - 2) { + strcat(c->display, "."); + } +} + +/* Operand, sign and fraction position from the digits on display, as + * append_digit() would have built them */ +static void parse_display(calc_t *c) +{ + const char *p = c->display; + int64_t v = 0; + c->negative = *p == '-'; + c->frac_div = 0; + for (p += c->negative; *p; p++) { + if (*p == '.') { + c->frac_div = 10; + } else if (c->frac_div) { + v += (*p - '0') * (SCALE / c->frac_div); + c->frac_div *= 10; + } else { + v = v * 10 + (*p - '0') * SCALE; + } + } + c->operand = c->negative ? -v : v; +} + +/* Drop the last character typed; a result or an operand not yet begun has + * none */ +static void erase(calc_t *c) +{ + if (c->new_input) + return; + size_t len = strlen(c->display) - 1; + c->display[len] = 0; + if (!len || !strcmp(c->display, "-")) + strcpy(c->display, "0"); + parse_display(c); +} + +static void press_key(calc_t *c, int i) +{ + const char *t = keys[i].label; + if (!strcmp(t, "C")) { + reset(c); + return; + } + if (c->error) + return; + if (!strcmp(t, ERASE)) { + erase(c); + } else if (!strcmp(t, "+/-")) { + if (c->new_input && (c->fresh || !c->operand)) { + /* Nothing typed yet: sign the number about to be entered */ + c->new_input = c->fresh = false; + c->frac_div = 0; + c->operand = 0; + c->negative = true; + strcpy(c->display, "-0"); + return; + } + if (c->operand == INT64_MIN) { + c->error = true; + return; + } + /* Flip the sign on display too: re-formatting would drop a trailing + * dot or show "0" for a negative entry still being typed */ + c->operand = -c->operand; + c->negative = !c->negative; + size_t len = strlen(c->display); + if (c->display[0] == '-') { + memmove(c->display, c->display + 1, len); + } else if (len < BUF_LEN - 1) { + memmove(c->display + 1, c->display, len + 1); + c->display[0] = '-'; + } + } else if (!strcmp(t, "%")) { + c->operand /= 100; + format_scaled(c, c->operand); + c->new_input = true; + c->fresh = false; + c->frac_div = 0; + } else if (keys[i].kind == DIGIT) { + if (t[0] == '.') + append_dot(c); + else + append_digit(c, t[0]); + } else if (keys[i].kind == EQUALS) { + if (!c->pending) + return; + if (execute(c)) { + c->operand = c->accumulator; + c->pending = 0; + format_scaled(c, c->operand); + c->new_input = true; + c->fresh = false; + c->frac_div = 0; + } + } else { + char op = t[0] == 'x' ? '*' : t[0]; + if (c->pending && !c->new_input) { + if (!execute(c)) + return; + c->operand = c->accumulator; + format_scaled(c, c->operand); + } else { + c->accumulator = c->operand; + } + c->pending = op; + c->new_input = c->fresh = true; + c->frac_div = 0; + } +} + +static int key_named(const char *label) +{ + for (int i = 0; i < N_KEYS; i++) + if (!strcmp(keys[i].label, label)) return i; return -1; } -static void _apps_calc_update_value(apps_calc_t *calc) +/* Key for a typed character: digits, + - / . = %, x or * multiplies, + * Enter is '=', Backspace and Delete erase, Escape and 'c' clear */ +static int key_for(int ch) { - char v[20]; + if (ch <= 0 || ch >= 0x80) + return -1; + const char *want = (char[2]) {(char) ch, 0}; + if (ch == '*') + want = "x"; + else if (ch == '\r' || ch == '\n') + want = "="; + else if (ch == 8 || ch == 127) + want = ERASE; + else if (ch == 27 || ch == 'c') + want = "C"; + return key_named(want); +} - sprintf(v, "%d", calc->stack[0]); - twin_label_set(calc->display, v); +static const char *calc_text(const calc_t *c) +{ + return c->error ? "Error" : c->display; } -static void _apps_calc_push(apps_calc_t *calc) +/* + * Layout: the display takes the top sixth of the client, the keypad the + * rest. Everything scales with the window. + */ + +static twin_rect_t client_rect(const calc_t *c) +{ + twin_rect_t r = twin_window_client(c->window); + return (twin_rect_t) {0, r.right - r.left, 0, r.bottom - r.top}; +} + +static twin_rect_t display_rect(const calc_t *c) { - for (int i = 0; i < APPS_CALC_STACK - 1; i++) - calc->stack[i + 1] = calc->stack[i]; - calc->pending_delete = true; + twin_rect_t r = client_rect(c); + r.bottom = r.bottom / 6; + return r; } -static int _apps_calc_pop(apps_calc_t *calc) +static int key_gap(twin_coord_t kw, twin_coord_t kh) { - int v = calc->stack[0]; - for (int i = 0; i < APPS_CALC_STACK - 1; i++) - calc->stack[i] = calc->stack[i + 1]; - return v; + return (kw < kh ? kw : kh) / 16 + 1; } -static void _apps_calc_digit(apps_calc_t *calc, int digit) +static twin_rect_t key_rect(const calc_t *c, int i) { - if (calc->pending_delete) { - calc->stack[0] = 0; - calc->pending_delete = false; + twin_rect_t r = client_rect(c); + twin_coord_t top = display_rect(c).bottom; + twin_coord_t kw = r.right / COLS, kh = (r.bottom - top) / ROWS; + twin_coord_t gap = key_gap(kw, kh); + twin_coord_t left = keys[i].col * kw + gap; + top += keys[i].row * kh + gap; + return (twin_rect_t) {left, left + keys[i].span * kw - 2 * gap, top, + top + kh - 2 * gap}; +} + +static int key_at(const calc_t *c, twin_coord_t x, twin_coord_t y) +{ + for (int i = 0; i < N_KEYS; i++) { + twin_rect_t r = key_rect(c, i); + if (x >= r.left && x < r.right && y >= r.top && y < r.bottom) + return i; } + return -1; +} - /* When the length reaches 9 digits, pressing the digit button will not - * increase the number of digits. */ - if (calc->stack[0] > 99999999) - return; +static void damage(const calc_t *c, twin_rect_t r) +{ + twin_rect_t o = twin_window_client(c->window); + twin_window_damage(c->window, r.left + o.left, r.top + o.top, + r.right + o.left, r.bottom + o.top); +} + +/* + * Painting + */ - calc->stack[0] = calc->stack[0] * 10 + digit; - _apps_calc_update_value(calc); +static bool overlaps(twin_rect_t a, twin_rect_t b) +{ + return a.left < b.right && b.left < a.right && a.top < b.bottom && + b.top < a.bottom; } -static twin_dispatch_result_t _apps_calc_button_clicked(twin_widget_t *widget, - twin_event_t *event, - void *data) +/* @s in @font at the largest size up to @size that fits @r, baseline + * centered; @align is -1 left, 0 centered, 1 right */ +static void draw_text(twin_pixmap_t *px, + twin_font_t *font, + twin_rect_t r, + int size, + int align, + twin_argb32_t color, + const char *s) { - if (event->kind != TwinEventButtonSignalUp) - return TwinDispatchContinue; + twin_fixed_t room = twin_int_to_fixed(r.right - r.left); + twin_fixed_t w = twin_text_width(font, twin_int_to_fixed(size), s); + /* Widths scale with the size: jump near the fit, then step down */ + if (w > room) + size = (int) ((int64_t) size * room / w); + if (size < 8) + size = 8; + while ((w = twin_text_width(font, twin_int_to_fixed(size), s)) > room && + size > 8) + size--; + twin_fixed_t x = twin_int_to_fixed(r.left); + if (align == 0) + x += (room - w) / 2; + else if (align > 0) + x += room - w; + /* Center the cap height (~0.7 em) rather than the full line box */ + twin_coord_t y = r.top + (r.bottom - r.top + size * 7 / 10 + 1) / 2; + twin_text_draw(px, font, twin_int_to_fixed(size), x, twin_int_to_fixed(y), + color, s); +} - apps_calc_t *calc = data; - twin_button_t *button = (twin_button_t *) widget; - int a, b; +static void calc_paint(const calc_t *c, + twin_pixmap_t *px, + const calc_palette_t *pal) +{ + twin_rect_t all = client_rect(c), clip = twin_pixmap_get_clip(px); + twin_fill(px, pal->bg, TWIN_SOURCE, all.left, all.top, all.right, + all.bottom); - int i = _apps_calc_button_to_id(calc, button); - if (i < 0) - return TwinDispatchContinue; + twin_rect_t d = display_rect(c); + if (overlaps(d, clip)) { + twin_coord_t pad = (d.bottom - d.top) / 6 + 2; + twin_rect_t text = {d.left + pad, d.right - pad, d.top, d.bottom}; + draw_text(px, FONT, text, (d.bottom - d.top) * 3 / 5, 1, pal->text, + calc_text(c)); + if (c->pending) { + char op[2] = {c->pending == '*' ? 'x' : c->pending, 0}; + text.bottom = d.top + (d.bottom - d.top) / 2; + draw_text(px, FONT, text, (d.bottom - d.top) / 3, -1, pal->accent, + op); + } + } - switch (i) { -#define _(x) APPS_CALC_##x - case _(PLUS): - case _(MINUS): - case _(TIMES): - case _(DIVIDE): - calc->pending_op = i; - _apps_calc_push(calc); - break; - case _(EQUAL): - if (calc->pending_op != 0) { - b = _apps_calc_pop(calc); - a = _apps_calc_pop(calc); - switch (calc->pending_op) { - case _(PLUS): - a = a + b; - break; - case _(MINUS): - a = a - b; - break; - case _(TIMES): - a = a * b; - break; - case _(DIVIDE): - if (!b) - a = 0; - else - a = a / b; - break; - } - _apps_calc_push(calc); - calc->stack[0] = a; - _apps_calc_update_value(calc); - calc->pending_op = 0; + twin_path_t *path = c->path; + for (int i = 0; i < N_KEYS; i++) { + twin_rect_t r = key_rect(c, i); + if (!overlaps(r, clip)) + continue; + bool down = i == c->pressed; + twin_argb32_t bg = down ? pal->down : pal->key[keys[i].kind]; + twin_argb32_t fg = down ? pal->down_text : pal->key_text[keys[i].kind]; + twin_coord_t w = r.right - r.left, h = r.bottom - r.top; + /* Round keys, pill-shaped where a key spans two cells */ + twin_fixed_t radius = twin_int_to_fixed((w < h ? w : h) / 2); + if (path) { + twin_path_rounded_rectangle( + path, twin_int_to_fixed(r.left), twin_int_to_fixed(r.top), + twin_int_to_fixed(w), twin_int_to_fixed(h), radius, radius); + twin_paint_path(px, bg, path); + twin_path_empty(path); + } else { + twin_fill(px, bg, TWIN_SOURCE, r.left, r.top, r.right, r.bottom); } - calc->pending_delete = true; - break; - case _(CLEAR): - for (i = 0; i < APPS_CALC_STACK; i++) - calc->stack[i] = 0; - calc->pending_op = 0; - calc->pending_delete = true; - _apps_calc_update_value(calc); - break; -#undef _ + bool erase_key = !strcmp(keys[i].label, ERASE); + draw_text(px, erase_key ? ERASE_FONT : FONT, r, h * 2 / 5, 0, fg, + keys[i].label); + } +} + +/* + * Input, the same for mouse and touch + */ + +static void set_pressed(calc_t *c, int k) +{ + if (c->pressed == k) + return; + if (c->pressed >= 0) + damage(c, key_rect(c, c->pressed)); + c->pressed = k; + if (k >= 0) + damage(c, key_rect(c, k)); +} + +static void press(calc_t *c, int k) +{ + press_key(c, k); + damage(c, display_rect(c)); +} + +/* Finger or button released at (@x, @y): press the key under it */ +static void release(calc_t *c, twin_coord_t x, twin_coord_t y) +{ + int k = key_at(c, x, y); + set_pressed(c, -1); + if (k >= 0) + press(c, k); +} + +static bool type(calc_t *c, int ch) +{ + int k = key_for(ch); + if (k < 0) + return false; + press(c, k); + return true; +} + +static void calc_init(calc_t *c, twin_window_t *window) +{ + c->window = window; + c->path = twin_path_create(); + c->pressed = -1; + reset(c); +} + +static void calc_fini(calc_t *c) +{ + if (c->path) + twin_path_destroy(c->path); +} + +/* + * Desktop window + */ + +/* clang-format off */ +static const calc_palette_t desktop_palette = { + .bg = 0xff1c1c1e, .text = 0xffffffff, .accent = 0xffff9f0a, + .key = {[DIGIT] = 0xff3a3a3c, [FUNCTION] = 0xffa5a5a5, + [OPERATOR] = 0xffff9f0a, [EQUALS] = 0xffff9f0a}, + .key_text = {[DIGIT] = 0xffffffff, [FUNCTION] = 0xff1c1c1e, + [OPERATOR] = 0xffffffff, [EQUALS] = 0xffffffff}, + .down = 0xff737376, .down_text = 0xffffffff, +}; +/* clang-format on */ + +static void desktop_draw(twin_window_t *window) +{ + calc_paint(twin_window_client_data(window), twin_window_pixmap(window), + &desktop_palette); +} + +static bool desktop_event(twin_window_t *window, twin_event_t *ev) +{ + calc_t *c = twin_window_client_data(window); + switch (ev->kind) { + case TwinEventButtonDown: + set_pressed(c, key_at(c, ev->u.pointer.x, ev->u.pointer.y)); + return true; + case TwinEventMotion: + if (c->pressed >= 0) + set_pressed(c, key_at(c, ev->u.pointer.x, ev->u.pointer.y)); + return true; + case TwinEventButtonUp: + release(c, ev->u.pointer.x, ev->u.pointer.y); + return true; + case TwinEventUcs4: + return type(c, (int) ev->u.ucs4.ucs4); default: - _apps_calc_digit(calc, i); - break; + return false; } - return TwinDispatchDone; } -/* The display is destroyed with the window: release the calculator then */ -static twin_dispatch_result_t _apps_calc_display_event(twin_widget_t *widget, - twin_event_t *event, - void *data) +static void desktop_destroy(twin_window_t *window) { - (void) widget; - if (event->kind == TwinEventDestroy) - free(data); - return TwinDispatchContinue; + calc_t *c = twin_window_client_data(window); + calc_fini(c); + free(c); } bool apps_calc_start(twin_screen_t *screen, @@ -216,64 +647,108 @@ bool apps_calc_start(twin_screen_t *screen, int w, int h) { - apps_calc_t *calc = malloc(sizeof(apps_calc_t)); - if (!calc) + calc_t *c = malloc(sizeof(*c)); + if (!c) return false; - - calc->toplevel = twin_toplevel_create(screen, x, y, w, h, name); - if (!calc->toplevel) { - free(calc); + twin_window_t *window = twin_window_create( + screen, TWIN_ARGB32, TwinWindowApplication, x, y, w, h); + if (!window) { + free(c); return false; } - twin_box_t *top = twin_toplevel_box(calc->toplevel); - twin_widget_set(twin_box_widget(top), APPS_CALC_BG); - calc->display = - twin_label_create(top, "0", APPS_CALC_VALUE_FG, APPS_CALC_VALUE_SIZE, - APPS_CALC_VALUE_STYLE); - if (!calc->display) - goto fail; - twin_widget_t *display = twin_label_widget(calc->display); - twin_widget_set(display, APPS_CALC_VALUE_BG); - twin_label_set_align(calc->display, TwinAlignRight); - twin_widget_set_shape(display, TwinShapeLozenge); - calc->keys = twin_box_create(top, TwinBoxHorz); - if (!calc->keys) - goto fail; - for (int i = 0; i < APPS_CALC_COLS; i++) { - calc->cols[i] = twin_box_create(calc->keys, TwinBoxVert); - if (!calc->cols[i]) - goto fail; - for (int j = 0; j < APPS_CALC_ROWS; j++) { - int b = calc_layout[j][i]; - calc->buttons[b] = twin_button_create( - calc->cols[i], apps_calc_labels[b], APPS_CALC_BUTTON_FG, - APPS_CALC_BUTTON_SIZE, APPS_CALC_BUTTON_STYLE); - if (!calc->buttons[b]) - goto fail; - twin_widget_t *key = twin_button_widget(calc->buttons[b]); - twin_widget_set(key, APPS_CALC_BUTTON_BG); - if (!twin_widget_set_callback(key, _apps_calc_button_clicked, calc)) - goto fail; - twin_widget_set_shape(key, TwinShapeEllipse); - if (i || j) - twin_widget_match_size( - key, twin_button_widget(calc->buttons[calc_layout[0][0]])); - } + calc_init(c, window); + twin_window_set_opaque(window, true); + twin_window_set_name(window, name); + twin_window_set_client_data(window, c); + twin_window_set_callbacks(window, desktop_draw, desktop_event, + desktop_destroy); + damage(c, client_rect(c)); + twin_window_show(window); + return true; +} + +/* + * Phone view: the same calculator in the UI kit's theme + */ + +#if defined(CONFIG_DEMO_CELLPHONE) +typedef struct { + ui_view_t view; + calc_t calc; +} phone_calc_t; + +static void phone_paint(ui_view_t *v, twin_pixmap_t *px) +{ + const phone_theme_t *th = phone_theme(); + twin_argb32_t op = ui_mix(th->indicator, th->primary, 90); + const calc_palette_t pal = { + .bg = th->card, + .text = th->text, + .accent = th->primary, + .key = {[DIGIT] = th->indicator, + [FUNCTION] = th->indicator, + [OPERATOR] = op, + [EQUALS] = th->primary}, + .key_text = {[DIGIT] = th->text, + [FUNCTION] = th->text, + [OPERATOR] = th->text, + [EQUALS] = th->on_primary}, + .down = th->primary_dk, + .down_text = th->on_primary, + }; + calc_paint(&((phone_calc_t *) v)->calc, px, &pal); +} + +static bool phone_event(ui_view_t *v, const ui_event_t *e) +{ + calc_t *c = &((phone_calc_t *) v)->calc; + switch (e->kind) { + case UI_PRESS: + case UI_MOVE: + set_pressed(c, key_at(c, e->x, e->y)); + return true; + case UI_TAP: + release(c, e->x, e->y); + return true; + case UI_KEY: { + int ch = ui_key_ascii(e); + return ch >= 0 && type(c, ch); + } + default: + set_pressed(c, -1); + return false; } +} - /* Last: from here on the display frees the calculator with the window */ - if (!twin_widget_set_callback(display, _apps_calc_display_event, calc)) - goto fail; +static void phone_destroy(ui_view_t *v) +{ + calc_fini(&((phone_calc_t *) v)->calc); +} - for (int i = 0; i < APPS_CALC_STACK; i++) - calc->stack[i] = 0; - calc->pending_delete = true; - calc->pending_op = 0; - twin_toplevel_show(calc->toplevel); - return true; +void app_calc_open(void) +{ + phone_calc_t *p = ui_view_new(sizeof(*p), "Calculator", phone_paint, + phone_event, phone_destroy); + if (!p) + return; + calc_init(&p->calc, p->view.window); + ui_view_show(&p->view); +} -fail: - twin_toplevel_destroy(calc->toplevel); - free(calc); - return false; +/* Test hook: feed typed keys ("12+3=") to the calculator on top; '~' + * stands for the sign key, which no character types */ +const char *app_calc_eval(const char *typed) +{ + ui_view_t *v = phone_top(); + if (!v || v->paint != phone_paint) + return NULL; + calc_t *c = &((phone_calc_t *) v)->calc; + for (const char *p = typed; *p; p++) { + if (*p == '~') + press(c, key_named("+/-")); + else + type(c, (unsigned char) *p); + } + return calc_text(c); } +#endif diff --git a/apps/cellphone/calc.c b/apps/cellphone/calc.c deleted file mode 100644 index eb64160e..00000000 --- a/apps/cellphone/calc.c +++ /dev/null @@ -1,425 +0,0 @@ -/* - * Twin - A Tiny Window System - * Copyright (c) 2026 National Cheng Kung University, Taiwan - * All rights reserved. - */ - -/* - * Calculator: four functions on 64-bit integers scaled by 10^6. - * - * Reference app for the UI kit: all state lives in the view struct, the - * keypad is a static table, and a key press repaints only that key and the - * display. - */ - -#include -#include - -#include "phone.h" - -#define DISPLAY_H 48 -#define COLS 4 -#define ROWS 5 -#define SCALE 1000000LL -#define BUF_LEN 24 - -typedef struct { - const char *label; - char col, row, span; - char kind; /* 'd' digit, 'o' operator, 'f' function, '=' equals */ -} calc_key_t; - -static const calc_key_t keys[] = { - {"C", 0, 0, 1, 'f'}, {"+/-", 1, 0, 1, 'f'}, {"%", 2, 0, 1, 'f'}, - {"/", 3, 0, 1, 'o'}, {"7", 0, 1, 1, 'd'}, {"8", 1, 1, 1, 'd'}, - {"9", 2, 1, 1, 'd'}, {"x", 3, 1, 1, 'o'}, {"4", 0, 2, 1, 'd'}, - {"5", 1, 2, 1, 'd'}, {"6", 2, 2, 1, 'd'}, {"-", 3, 2, 1, 'o'}, - {"1", 0, 3, 1, 'd'}, {"2", 1, 3, 1, 'd'}, {"3", 2, 3, 1, 'd'}, - {"+", 3, 3, 1, 'o'}, {"0", 0, 4, 2, 'd'}, {".", 2, 4, 1, 'd'}, - {"=", 3, 4, 1, '='}, -}; - -#define N_KEYS ((int) (sizeof(keys) / sizeof(keys[0]))) - -typedef struct { - ui_view_t view; - char display[BUF_LEN]; - int64_t accumulator; /* left operand, scaled */ - int64_t operand; /* value being entered, scaled */ - char pending; /* '+', '-', '*', '/' or 0 */ - bool new_input; /* next digit starts a new number */ - int32_t frac_div; /* 0: integer part; 10, 100, ...: after the dot */ - bool negative; - bool fresh; /* an operator was just pressed: no operand typed yet */ - bool error; - int pressed; /* key index, -1 when none */ -} calc_t; - -static ui_rect_t key_rect(ui_view_t *v, int i) -{ - twin_coord_t kw = PHONE_W / COLS; - twin_coord_t kh = (ui_view_height(v) - DISPLAY_H) / ROWS; - return ui_rect(keys[i].col * kw + 2, DISPLAY_H + keys[i].row * kh + 2, - keys[i].span * kw - 4, kh - 4); -} - -static ui_rect_t display_rect(void) -{ - return ui_rect(0, 0, PHONE_W, DISPLAY_H); -} - -static void format_scaled(calc_t *c, int64_t val) -{ - char *p = c->display; - int64_t a = val < 0 ? -val : val; - int pos = snprintf(p, BUF_LEN, "%s%lld", val < 0 ? "-" : "", - (long long) (a / SCALE)); - int64_t frac = a % SCALE; - if (frac && pos < BUF_LEN - 2) { - p[pos++] = '.'; - for (int64_t div = SCALE / 10; div && frac && pos < BUF_LEN - 1; - div /= 10) { - p[pos++] = (char) ('0' + frac / div); - frac %= div; - } - while (p[pos - 1] == '0') - pos--; - } - p[pos] = 0; -} - -/* - * (a * b) / SCALE without a 128-bit type (32-bit targets lack one): - * split both operands into integer and fractional parts. - */ -static bool scaled_mul(int64_t a, int64_t b, int64_t *res) -{ - int64_t ai = a / SCALE, af = a % SCALE, bi = b / SCALE, bf = b % SCALE; - int64_t t0, t1, t2, sum; - if (__builtin_mul_overflow(ai, bi, &t0) || - __builtin_mul_overflow(t0, SCALE, &t0) || - __builtin_mul_overflow(ai, bf, &t1) || - __builtin_mul_overflow(af, bi, &t2) || - __builtin_add_overflow(t0, t1, &sum) || - __builtin_add_overflow(sum, t2, &sum) || - __builtin_add_overflow(sum, af * bf / SCALE, &sum)) - return false; - *res = sum; - return true; -} - -/* - * (a * SCALE) / b, truncated toward zero, without forming the product: - * integer quotient first, then the six fractional digits by long division - * of the remainder. Fails only when the result itself does not fit. - */ -static bool scaled_div(int64_t a, int64_t b, int64_t *res) -{ - if (!b) - return false; - bool neg = (a < 0) != (b < 0); - uint64_t ua = a < 0 ? -(uint64_t) a : (uint64_t) a; - uint64_t ub = b < 0 ? -(uint64_t) b : (uint64_t) b; - uint64_t q = ua / ub, r = ua % ub; - if (q > (uint64_t) INT64_MAX / SCALE) - return false; - q *= SCALE; - uint64_t frac = 0; - for (int64_t div = SCALE / 10; div; div /= 10) { - /* Next digit of r / ub: 10 r = d ub + r', with r, r' < ub, so the - * running sum stays below 2 ub and never wraps */ - uint64_t t = 0; - int d = 0; - for (int k = 0; k < 10; k++) { - t += r; - if (t >= ub) { - t -= ub; - d++; - } - } - r = t; - frac += (uint64_t) d * (uint64_t) div; - } - if (q + frac > (uint64_t) INT64_MAX) - return false; - *res = neg ? -(int64_t) (q + frac) : (int64_t) (q + frac); - return true; -} - -static bool execute(calc_t *c) -{ - switch (c->pending) { - case '+': - if (__builtin_add_overflow(c->accumulator, c->operand, &c->accumulator)) - goto fail; - break; - case '-': - if (__builtin_sub_overflow(c->accumulator, c->operand, &c->accumulator)) - goto fail; - break; - case '*': - if (!scaled_mul(c->accumulator, c->operand, &c->accumulator)) - goto fail; - break; - case '/': - if (!scaled_div(c->accumulator, c->operand, &c->accumulator)) - goto fail; - break; - default: - c->accumulator = c->operand; - break; - } - /* INT64_MIN has no positive counterpart: +/- and the display negate */ - if (c->accumulator == INT64_MIN) - goto fail; - return true; -fail: - c->error = true; - return false; -} - -static void reset(calc_t *c) -{ - c->accumulator = c->operand = 0; - c->pending = 0; - c->new_input = true; - c->frac_div = 0; - c->negative = c->fresh = c->error = false; - strcpy(c->display, "0"); -} - -static void append_digit(calc_t *c, char digit) -{ - if (c->new_input) { - c->display[0] = digit; - c->display[1] = 0; - c->new_input = false; - c->frac_div = 0; - c->negative = false; - c->operand = 0; - } else { - if (c->frac_div > SCALE) - return; - size_t len = strlen(c->display); - /* A lone "0" or "-0" is replaced, not extended */ - if (!c->frac_div && !c->operand && c->display[len - 1] == '0') - len--; - size_t digits = len - (c->display[0] == '-'); - if (len >= BUF_LEN - 2 || (!c->frac_div && digits >= 12)) - return; - c->display[len] = digit; - c->display[len + 1] = 0; - } - int64_t d = digit - '0'; - int64_t step = c->frac_div ? d * (SCALE / c->frac_div) : 0; - if (c->frac_div) { - c->operand += c->negative ? -step : step; - c->frac_div *= 10; - } else { - c->operand = c->operand * 10 + (c->negative ? -d : d) * SCALE; - } -} - -static void append_dot(calc_t *c) -{ - if (c->frac_div) - return; - c->frac_div = 10; - if (c->new_input) { - strcpy(c->display, "0."); - c->new_input = false; - c->operand = 0; - c->negative = false; - } else if (strlen(c->display) < BUF_LEN - 2) { - strcat(c->display, "."); - } -} - -static void press_key(calc_t *c, int i) -{ - const char *t = keys[i].label; - if (!strcmp(t, "C")) { - reset(c); - return; - } - if (c->error) - return; - if (!strcmp(t, "+/-")) { - if (c->new_input && (c->fresh || !c->operand)) { - /* Nothing typed yet: sign the number about to be entered */ - c->new_input = c->fresh = false; - c->frac_div = 0; - c->operand = 0; - c->negative = true; - strcpy(c->display, "-0"); - return; - } - if (c->operand == INT64_MIN) { - c->error = true; - return; - } - c->operand = -c->operand; - c->negative = !c->negative; - format_scaled(c, c->operand); - } else if (!strcmp(t, "%")) { - c->operand /= 100; - format_scaled(c, c->operand); - c->new_input = true; - c->fresh = false; - c->frac_div = 0; - } else if (keys[i].kind == 'd') { - if (t[0] == '.') - append_dot(c); - else - append_digit(c, t[0]); - } else if (keys[i].kind == '=') { - if (!c->pending) - return; - if (execute(c)) { - c->operand = c->accumulator; - c->pending = 0; - format_scaled(c, c->operand); - c->new_input = true; - c->fresh = false; - c->frac_div = 0; - } - } else { - char op = t[0] == 'x' ? '*' : t[0]; - if (c->pending && !c->new_input) { - if (!execute(c)) - return; - c->operand = c->accumulator; - format_scaled(c, c->operand); - } else { - c->accumulator = c->operand; - } - c->pending = op; - c->new_input = c->fresh = true; - c->frac_div = 0; - } -} - -static void calc_paint(ui_view_t *v, twin_pixmap_t *px) -{ - calc_t *c = (calc_t *) v; - const phone_theme_t *th = phone_theme(); - ui_rect_t clip = ui_view_clip(v); - - if (ui_intersects(clip, display_rect())) { - ui_vgrad(px, display_rect(), th->card, ui_mix(th->card, th->bg, 128)); - ui_text_fit(px, FONT_LARGE, ui_rect(0, 4, PHONE_W - 6, DISPLAY_H - 8), - UI_RIGHT, th->text, c->error ? "Error" : c->display); - if (c->pending) { - char op[2] = {c->pending == '*' ? 'x' : c->pending, 0}; - ui_text(px, FONT_SM, 8, 18, UI_LEFT, th->primary, op); - } - } - ui_fill(px, ui_rect(0, DISPLAY_H, PHONE_W, ui_view_height(v) - DISPLAY_H), - th->card); - for (int i = 0; i < N_KEYS; i++) { - ui_rect_t r = key_rect(v, i); - if (!ui_intersects(r, clip)) - continue; - bool down = i == c->pressed; - twin_argb32_t bg = th->indicator, fg = th->text; - if (keys[i].kind == 'o' || keys[i].kind == '=') { - bg = keys[i].kind == '=' ? th->primary - : ui_mix(th->indicator, th->primary, 90); - fg = keys[i].kind == '=' ? th->on_primary : th->text; - } - if (down) { - bg = th->primary_dk; - fg = th->on_primary; - } - ui_rrect(px, r, 6, bg); - ui_text_in(px, FONT_HEADING, r, UI_CENTER, fg, keys[i].label); - } -} - -/* Key for a typed character: digits, + - / . = %, x or * multiplies, - * Enter is '=', Escape and 'c' clear */ -static int key_for(char ch) -{ - if (ch == '*') - ch = 'x'; - else if (ch == UI_KEY_ENTER) - ch = '='; - else if (ch == 27 || ch == 'c') - ch = 'C'; - for (int i = 0; i < N_KEYS; i++) - if (keys[i].label[0] == ch && !keys[i].label[1]) - return i; - return -1; -} - -static int key_at(ui_view_t *v, twin_coord_t x, twin_coord_t y) -{ - for (int i = 0; i < N_KEYS; i++) - if (ui_hit(key_rect(v, i), x, y)) - return i; - return -1; -} - -static void set_pressed(calc_t *c, int k) -{ - ui_view_set_pressed(&c->view, &c->pressed, k, key_rect); -} - -static bool calc_event(ui_view_t *v, const ui_event_t *e) -{ - calc_t *c = (calc_t *) v; - switch (e->kind) { - case UI_PRESS: - case UI_MOVE: - set_pressed(c, key_at(v, e->x, e->y)); - return true; - case UI_TAP: { - int k = key_at(v, e->x, e->y); - set_pressed(c, -1); - if (k >= 0) { - press_key(c, k); - ui_view_invalidate(v, display_rect()); - } - return true; - } - case UI_KEY: { - int ch = ui_key_ascii(e); - int k = ch < 0 ? -1 : key_for((char) ch); - if (k < 0) - return false; - press_key(c, k); - ui_view_invalidate(v, display_rect()); - return true; - } - default: - set_pressed(c, -1); - return false; - } -} - -void app_calc_open(void) -{ - calc_t *c = - ui_view_new(sizeof(*c), "Calculator", calc_paint, calc_event, NULL); - if (!c) - return; - reset(c); - c->pressed = -1; - ui_view_show(&c->view); -} - -/* Test hook: feed typed keys ("12+3=") to the calculator on top */ -const char *app_calc_eval(const char *typed) -{ - ui_view_t *v = phone_top(); - if (!v || v->paint != calc_paint) - return NULL; - calc_t *c = (calc_t *) v; - for (const char *p = typed; *p; p++) { - int k = key_for(*p); - if (k >= 0) - press_key(c, k); - } - ui_view_invalidate(v, display_rect()); - return c->error ? "Error" : c->display; -} diff --git a/apps/cellphone/phone.h b/apps/cellphone/phone.h index 6dc5fdfb..354f4a39 100644 --- a/apps/cellphone/phone.h +++ b/apps/cellphone/phone.h @@ -51,6 +51,8 @@ #include #include +#include "apps.h" + /* * Geometry: portrait QVGA */ @@ -684,7 +686,10 @@ typedef struct { void (*open)(void); /* An app from the shared registry (apps.h) that opens its own windows * rather than a view; NULL for the phone's apps */ - const struct apps_entry *demo; + const apps_entry_t *demo; + /* The registry app this one is the phone-themed form of, so the + * launcher lists it once; NULL for none */ + apps_start_fn *replaces; } phone_app_t; int phone_app_count(void); diff --git a/apps/cellphone/shell.c b/apps/cellphone/shell.c index dea789a4..312beec2 100644 --- a/apps/cellphone/shell.c +++ b/apps/cellphone/shell.c @@ -284,31 +284,49 @@ static void glyph_pong(twin_pixmap_t *px, ui_rect_t p, twin_argb32_t fg); static void glyph_tetris(twin_pixmap_t *px, ui_rect_t p, twin_argb32_t fg); static const phone_app_t apps[] = { - {"Phone", TWIN_SYM_PHONE, NULL, UI_RGB(0x5b8c45), app_dialer_open, NULL}, + {"Phone", TWIN_SYM_PHONE, NULL, UI_RGB(0x5b8c45), app_dialer_open, NULL, + NULL}, {"Contacts", TWIN_SYM_CONTACTS, NULL, UI_RGB(0x5a7896), app_contacts_open, + NULL, NULL}, + {"Messages", TWIN_SYM_MESSAGE, NULL, UI_RGB(0x4f8c7c), app_sms_open, NULL, NULL}, - {"Messages", TWIN_SYM_MESSAGE, NULL, UI_RGB(0x4f8c7c), app_sms_open, NULL}, +#if defined(CONFIG_DEMO_CALCULATOR) /* the shared calculator, phone themed */ {"Calculator", TWIN_SYM_CALCULATOR, NULL, UI_RGB(0xc08a3e), app_calc_open, + NULL, apps_calc_start}, +#endif + {"Music", TWIN_SYM_MUSIC, NULL, UI_RGB(0xa95c72), app_music_open, NULL, NULL}, - {"Music", TWIN_SYM_MUSIC, NULL, UI_RGB(0xa95c72), app_music_open, NULL}, - {"Photos", TWIN_SYM_IMAGE, NULL, UI_RGB(0xbb6e3e), app_photos_open, NULL}, - {"Camera", TWIN_SYM_CAMERA, NULL, UI_RGB(0x6c7064), app_camera_open, NULL}, - {"Settings", TWIN_SYM_SETTINGS, NULL, UI_RGB(0x767b6c), app_settings_open, + {"Photos", TWIN_SYM_IMAGE, NULL, UI_RGB(0xbb6e3e), app_photos_open, NULL, NULL}, + {"Camera", TWIN_SYM_CAMERA, NULL, UI_RGB(0x6c7064), app_camera_open, NULL, + NULL}, + {"Settings", TWIN_SYM_SETTINGS, NULL, UI_RGB(0x767b6c), app_settings_open, + NULL, NULL}, {"Call Log", TWIN_SYM_HISTORY, NULL, UI_RGB(0x7a6a96), app_calllog_open, + NULL, NULL}, + {"Snake", NULL, glyph_snake, UI_RGB(0x4d8f86), app_snake_open, NULL, NULL}, + {"Pong", NULL, glyph_pong, UI_RGB(0x646c9a), app_pong_open, NULL, NULL}, + {"Tetris", NULL, glyph_tetris, UI_RGB(0xad5348), app_tetris_open, NULL, NULL}, - {"Snake", NULL, glyph_snake, UI_RGB(0x4d8f86), app_snake_open, NULL}, - {"Pong", NULL, glyph_pong, UI_RGB(0x646c9a), app_pong_open, NULL}, - {"Tetris", NULL, glyph_tetris, UI_RGB(0xad5348), app_tetris_open, NULL}, }; #define N_APPS ((int) (sizeof(apps) / sizeof(apps[0]))) /* Then every other app of the shared registry (apps.c), in registry order: - * a mobile build compiles in only apps that run under it. Listed at boot. */ + * a mobile build compiles in only apps that run under it. A registry app + * the phone also offers natively, in its theme, is listed once, as the + * native one. Listed at boot. */ static phone_app_t demos[16]; static int n_demos; +static bool _replaced(const apps_entry_t *e) +{ + for (int i = 0; i < N_APPS; i++) + if (apps[i].replaces && apps[i].replaces == e->start) + return true; + return false; +} + static void _list_demos(void) { static const twin_argb32_t accents[] = { @@ -318,7 +336,7 @@ static void _list_demos(void) n_demos = 0; for (int i = 0; i < apps_count(); i++) { const apps_entry_t *e = apps_at(i); - if (e->boot) + if (e->boot || _replaced(e)) continue; if (n_demos == (int) (sizeof(demos) / sizeof(demos[0]))) break; diff --git a/configs/Kconfig b/configs/Kconfig index 34ef945c..87610d4d 100644 --- a/configs/Kconfig +++ b/configs/Kconfig @@ -538,11 +538,13 @@ config DEMO_CLOCK config DEMO_CALCULATOR bool "Build calculator demo" default y - depends on DEMO_APPLICATIONS && WIDGETS + depends on DEMO_APPLICATIONS help - Interactive calculator application. - Demonstrates button widgets and event handling. - Shows layout management and user input processing. + Four-function calculator with decimal fixed-point arithmetic. + One source serves the desktop window and, with the cellphone demo, + the phone's Calculator app in the current phone theme. + Demonstrates a window drawn with paths and text, and mouse, touch + and keyboard input. config DEMO_SPLINE bool "Build spline demo" diff --git a/tests/test-cellphone.c b/tests/test-cellphone.c index abe26e8c..c3bf45fb 100644 --- a/tests/test-cellphone.c +++ b/tests/test-cellphone.c @@ -138,6 +138,7 @@ static void test_wrap(void) "overlong word still terminates"); } +#if defined(CONFIG_DEMO_CALCULATOR) static void test_calculator(void) { /* Calculator is the fourth icon: row 1, column 0 */ @@ -150,6 +151,12 @@ static void test_calculator(void) CHECK_STR(app_calc_eval("C1.5x4="), "6", "1.5x4"); CHECK_STR(app_calc_eval("C7/0="), "Error", "divide by zero"); CHECK_STR(app_calc_eval("C10/4="), "2.5", "10/4"); + CHECK_STR(app_calc_eval("C12.5\b\b+1="), "13", "erase digits and dot"); + CHECK_STR(app_calc_eval("C7\b"), "0", "erase the last digit"); + CHECK_STR(app_calc_eval("C5-\b"), "5", "nothing typed after the operator"); + CHECK_STR(app_calc_eval("C3+4=\b"), "7", "a result is not erased"); + CHECK_STR(app_calc_eval("C0~3"), "-3", "sign typed before the digits"); + CHECK_STR(app_calc_eval("C5.~3"), "-5.3", "sign keeps a trailing dot"); /* Ten 12-digit terms overflow the 64-bit fixed-point accumulator */ char big[160]; for (int sign = 0; sign < 2; sign++) { @@ -175,6 +182,9 @@ static void test_calculator(void) th_settle(t, 2000); CHECK_EQ(phone_depth(), 0, "back to home"); } +#else +static void test_calculator(void) {} +#endif /* * Budgets for the whole phone: memory returns to baseline after visiting From 103a8a5ba4c4f5ec48adac28da2a9eea7edd6a21 Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Tue, 20 Oct 2026 09:10:36 +0800 Subject: [PATCH 08/13] Pick the phone apps in Kconfig The phone built every app it has. A "Phone apps" menu now selects them one by one; call log needs the dialer, and contacts needs the dialer and messages, which it opens. Settings stays, as it owns the theme and wallpaper. The tests follow the selection: each suite has an empty stand-in when its app is not built, the calculator is tapped wherever its icon lands, and the golden file records the launcher list, so frames recorded for one selection are not compared against another. --- Makefile | 14 +++++----- apps/cellphone/shell.c | 26 +++++++++++++++--- configs/Kconfig | 61 ++++++++++++++++++++++++++++++++++++++---- tests/harness.c | 13 ++++++--- tests/harness.h | 5 ++++ tests/phone-contacts.c | 5 ++++ tests/phone-demos.c | 8 ------ tests/phone-dialer.c | 10 +++++++ tests/phone-music.c | 5 ++++ tests/phone-photos.c | 14 +++++++--- tests/phone-regress.c | 23 ++++++++++++++++ tests/phone-sms.c | 5 ++++ tests/test-cellphone.c | 18 ++++++++++--- 13 files changed, 174 insertions(+), 33 deletions(-) diff --git a/Makefile b/Makefile index 2c1b087f..222043e0 100644 --- a/Makefile +++ b/Makefile @@ -182,18 +182,18 @@ libapps.a_files-$(CONFIG_DEMO_CELLPHONE) += \ apps/cellphone/home.c \ apps/cellphone/lock.c \ apps/cellphone/data.c \ - apps/cellphone/dialer.c \ - apps/cellphone/contacts.c \ - apps/cellphone/sms.c \ - apps/cellphone/music.c \ - apps/cellphone/photos.c \ - apps/cellphone/camera.c \ apps/cellphone/settings.c \ - apps/cellphone/calllog.c \ apps/cellphone/games.c \ apps/cellphone/snake.c \ apps/cellphone/pong.c \ apps/cellphone/tetris.c +libapps.a_files-$(CONFIG_DEMO_PHONE_DIALER) += apps/cellphone/dialer.c +libapps.a_files-$(CONFIG_DEMO_PHONE_CALLLOG) += apps/cellphone/calllog.c +libapps.a_files-$(CONFIG_DEMO_PHONE_MESSAGES) += apps/cellphone/sms.c +libapps.a_files-$(CONFIG_DEMO_PHONE_CONTACTS) += apps/cellphone/contacts.c +libapps.a_files-$(CONFIG_DEMO_PHONE_MUSIC) += apps/cellphone/music.c +libapps.a_files-$(CONFIG_DEMO_PHONE_PHOTOS) += apps/cellphone/photos.c +libapps.a_files-$(CONFIG_DEMO_PHONE_CAMERA) += apps/cellphone/camera.c libapps.a_includes-y := include apps apps/cellphone # Emscripten size optimization diff --git a/apps/cellphone/shell.c b/apps/cellphone/shell.c index 312beec2..b4a4857f 100644 --- a/apps/cellphone/shell.c +++ b/apps/cellphone/shell.c @@ -283,27 +283,42 @@ static void glyph_snake(twin_pixmap_t *px, ui_rect_t p, twin_argb32_t fg); static void glyph_pong(twin_pixmap_t *px, ui_rect_t p, twin_argb32_t fg); static void glyph_tetris(twin_pixmap_t *px, ui_rect_t p, twin_argb32_t fg); +/* The apps picked in Kconfig, in launcher order */ static const phone_app_t apps[] = { +#if defined(CONFIG_DEMO_PHONE_DIALER) {"Phone", TWIN_SYM_PHONE, NULL, UI_RGB(0x5b8c45), app_dialer_open, NULL, NULL}, +#endif +#if defined(CONFIG_DEMO_PHONE_CONTACTS) {"Contacts", TWIN_SYM_CONTACTS, NULL, UI_RGB(0x5a7896), app_contacts_open, NULL, NULL}, +#endif +#if defined(CONFIG_DEMO_PHONE_MESSAGES) {"Messages", TWIN_SYM_MESSAGE, NULL, UI_RGB(0x4f8c7c), app_sms_open, NULL, NULL}, +#endif #if defined(CONFIG_DEMO_CALCULATOR) /* the shared calculator, phone themed */ {"Calculator", TWIN_SYM_CALCULATOR, NULL, UI_RGB(0xc08a3e), app_calc_open, NULL, apps_calc_start}, #endif +#if defined(CONFIG_DEMO_PHONE_MUSIC) {"Music", TWIN_SYM_MUSIC, NULL, UI_RGB(0xa95c72), app_music_open, NULL, NULL}, +#endif +#if defined(CONFIG_DEMO_PHONE_PHOTOS) {"Photos", TWIN_SYM_IMAGE, NULL, UI_RGB(0xbb6e3e), app_photos_open, NULL, NULL}, +#endif +#if defined(CONFIG_DEMO_PHONE_CAMERA) {"Camera", TWIN_SYM_CAMERA, NULL, UI_RGB(0x6c7064), app_camera_open, NULL, NULL}, +#endif {"Settings", TWIN_SYM_SETTINGS, NULL, UI_RGB(0x767b6c), app_settings_open, NULL, NULL}, +#if defined(CONFIG_DEMO_PHONE_CALLLOG) {"Call Log", TWIN_SYM_HISTORY, NULL, UI_RGB(0x7a6a96), app_calllog_open, NULL, NULL}, +#endif {"Snake", NULL, glyph_snake, UI_RGB(0x4d8f86), app_snake_open, NULL, NULL}, {"Pong", NULL, glyph_pong, UI_RGB(0x646c9a), app_pong_open, NULL, NULL}, {"Tetris", NULL, glyph_tetris, UI_RGB(0xad5348), app_tetris_open, NULL, @@ -485,9 +500,12 @@ static twin_time_t status_tick(twin_time_t now, void *closure) static int nav_active(void) { ui_view_t *top = phone_top(); - if (top && top->app == phone_app_find("Phone")) + /* An app left out of the build finds no slot (-1), which views opened + * outside the launcher carry too: only a real slot may match */ + int app = top ? top->app : -1; + if (app >= 0 && app == phone_app_find("Phone")) return 0; - if (top && top->app == phone_app_find("Contacts")) + if (app >= 0 && app == phone_app_find("Contacts")) return 2; return phone_depth() > 0 ? -1 : 1; } @@ -571,8 +589,8 @@ static void nav_go_root(const char *name) { int app = phone_app_find(name); ui_view_t *top = phone_top(); - if (top && top->app == app) - return; + if (app < 0 || (top && top->app == app)) + return; /* not built, or already there */ /* Tabs are destinations, not history: reset to home, then open */ _drop_apps(NULL); phone_open(app); diff --git a/configs/Kconfig b/configs/Kconfig index 87610d4d..9bfd6ec3 100644 --- a/configs/Kconfig +++ b/configs/Kconfig @@ -502,11 +502,12 @@ config DEMO_CELLPHONE depends on DEMO_APPLICATIONS && FONT_MONTSERRAT && FONT_ICONS depends on WM_MOBILE help - Phone user interface: lock screen, home screen, dialer, contacts, - messages, calculator, music, photos, camera, settings, call log and - games on the mobile window manager, which it needs. The other demos - selected below get launcher icons too and run as stacked apps; Back - closes their windows one at a time. + Phone user interface on the mobile window manager, which it needs: + lock screen, home screen and settings, plus the phone apps picked + below. The other demos selected in this menu get launcher icons + too and run as stacked apps; Back closes their windows one at a + time. A demo the phone also offers natively, such as the + calculator, appears once, drawn in the phone theme. config DEMO_CELLPHONE_RGB16 bool "Use RGB565 surfaces for phone screens" @@ -517,6 +518,56 @@ config DEMO_CELLPHONE_RGB16 surfaces. RGB565 has no alpha channel, so screens are always opaque, at the cost of some banding in gradients. +menu "Phone apps" + depends on DEMO_CELLPHONE + +config DEMO_PHONE_DIALER + bool "Phone" + default y + help + Dialer keypad and the in-call screen. + +config DEMO_PHONE_CALLLOG + bool "Call log" + default y + depends on DEMO_PHONE_DIALER + help + Recent calls; tapping one calls it back through the dialer. + +config DEMO_PHONE_MESSAGES + bool "Messages" + default y + help + Text message conversation with an on-screen keyboard. + +config DEMO_PHONE_CONTACTS + bool "Contacts" + default y + depends on DEMO_PHONE_DIALER && DEMO_PHONE_MESSAGES + help + Contact list, details and an add form. A contact can be called or + messaged, so this needs the dialer and messages. + +config DEMO_PHONE_MUSIC + bool "Music" + default y + help + Track list and a now-playing screen. + +config DEMO_PHONE_PHOTOS + bool "Photos" + default y + help + Thumbnail grid and a swipeable photo viewer. + +config DEMO_PHONE_CAMERA + bool "Camera" + default y + help + Viewfinder with shutter, flash and mode controls. + +endmenu + config DEMO_MULTI bool "Build multi demo" default y diff --git a/tests/harness.c b/tests/harness.c index d4ead50d..13990f94 100644 --- a/tests/harness.c +++ b/tests/harness.c @@ -344,6 +344,13 @@ size_t th_heap_net(const th_t *t) * when it does not match. Options that must not change pixels, such as the * coverage cache, are deliberately left out: those builds have to match. */ +static uint32_t golden_salt; + +void th_golden_salt(const char *salt) +{ + golden_salt = th_crc_bytes(salt, strlen(salt)); +} + static uint32_t config_fingerprint(void) { char buf[256]; @@ -391,7 +398,7 @@ static uint32_t config_fingerprint(void) 0 #endif ); - return th_crc_bytes(buf, strlen(buf)); + return th_crc_bytes(buf, strlen(buf)) ^ golden_salt; } #define MAX_GOLDEN 256 @@ -516,8 +523,8 @@ int th_report(const char *suite) } if (golden_skipped) printf( - "%s: %d golden comparisons skipped (build options differ " - "from the recorded configuration)\n", + "%s: %d golden comparisons skipped (build options or the " + "suite's salt differ from the recorded configuration)\n", suite, golden_skipped); printf("%s: %d checks, %d failed\n", suite, th_checks, th_failures); return th_failures ? 1 : 0; diff --git a/tests/harness.h b/tests/harness.h index 62883819..c8d55141 100644 --- a/tests/harness.h +++ b/tests/harness.h @@ -134,4 +134,9 @@ void th_golden(const char *suite, uint32_t crc, const th_t *t); +/* Mix @salt into the build fingerprint the golden file records, for state + * a suite's frames depend on beyond the build options (the phone's + * launcher list). Comparisons are skipped when it differs. */ +void th_golden_salt(const char *salt); + #endif /* _TWIN_TEST_HARNESS_H_ */ diff --git a/tests/phone-contacts.c b/tests/phone-contacts.c index f2c045db..0c856562 100644 --- a/tests/phone-contacts.c +++ b/tests/phone-contacts.c @@ -14,6 +14,8 @@ #include "data.h" #include "phone-test.h" +#if defined(CONFIG_DEMO_PHONE_CONTACTS) + int app_contacts_test_offset(void); const char *app_contacts_test_detail(void); const char *app_contacts_test_form_error(void); @@ -204,3 +206,6 @@ void test_contacts(void) ph_home(); phone_data_reset(); } +#else +void test_contacts(void) {} +#endif diff --git a/tests/phone-demos.c b/tests/phone-demos.c index aec5ba9f..30d27556 100644 --- a/tests/phone-demos.c +++ b/tests/phone-demos.c @@ -70,15 +70,7 @@ void test_demos(void) th_release(phone_th, r.left + 3, CY(r.bottom + 40)); ph_wait(300); } - /* The page lists whichever apps are built: the golden frame holds the - * default set */ -#if defined(CONFIG_DEMO_CLOCK) && defined(CONFIG_DEMO_CALCULATOR) && \ - defined(CONFIG_DEMO_SPLINE) && defined(CONFIG_DEMO_ANIMATION) && \ - defined(CONFIG_DEMO_IMAGE) ph_shot("home-demos"); -#else - ph_check("home-demos"); -#endif size_t base = 0; for (int pass = 0; pass < 2; pass++) { diff --git a/tests/phone-dialer.c b/tests/phone-dialer.c index 48d1c3e6..f77b5343 100644 --- a/tests/phone-dialer.c +++ b/tests/phone-dialer.c @@ -10,6 +10,8 @@ #include "phone-test.h" +#if defined(CONFIG_DEMO_PHONE_DIALER) + const char *app_dialer_test_number(void); const char *app_dialer_test_name(void); int app_dialer_test_seconds(void); @@ -137,6 +139,7 @@ void test_dialer(void) ph_home(); } +#if defined(CONFIG_DEMO_PHONE_CALLLOG) void test_calllog(void) { ph_open("Call Log"); @@ -177,3 +180,10 @@ void test_calllog(void) CHECK_EQ(phone_depth(), 1, "hung up"); ph_home(); } +#else +void test_calllog(void) {} +#endif +#else +void test_dialer(void) {} +void test_calllog(void) {} +#endif diff --git a/tests/phone-music.c b/tests/phone-music.c index 79f72613..a72fe9ae 100644 --- a/tests/phone-music.c +++ b/tests/phone-music.c @@ -14,6 +14,8 @@ #include "data.h" #include "phone-test.h" +#if defined(CONFIG_DEMO_PHONE_MUSIC) + int app_music_test_elapsed(void); int app_music_test_track(void); bool app_music_test_playing(void); @@ -186,3 +188,6 @@ void test_music(void) CHECK(!app_music_test_playing(), "paused before leaving"); ph_home(); } +#else +void test_music(void) {} +#endif diff --git a/tests/phone-photos.c b/tests/phone-photos.c index 09178e12..2e323453 100644 --- a/tests/phone-photos.c +++ b/tests/phone-photos.c @@ -5,9 +5,9 @@ */ /* - * Photos and Camera scenarios. Both apps behave the same with or without - * a JPEG loader (procedural art stands in for missing pictures), so these - * checks hold in every build configuration. + * Photos and Camera scenarios, each run when its app is built. Both apps + * behave the same with or without a JPEG loader (procedural art stands in + * for missing pictures), so the checks hold with any loader set. */ #include "phone-test.h" @@ -23,6 +23,7 @@ int app_camera_test_flash(void); #define THUMB_X(i) (5 + ((i) % 3) * 78 + 37) #define THUMB_Y(i) (56 + ((i) / 3) * 78 + 30) +#if defined(CONFIG_DEMO_PHONE_PHOTOS) void test_photos(void) { ph_open("Photos"); @@ -112,7 +113,11 @@ void test_photos(void) CHECK(th_heap_net(phone_th) <= heap0, "photos freed everything (%zu > %zu)", th_heap_net(phone_th), heap0); } +#else +void test_photos(void) {} +#endif +#if defined(CONFIG_DEMO_PHONE_CAMERA) void test_camera(void) { ph_open("Camera"); @@ -165,3 +170,6 @@ void test_camera(void) CHECK(th_heap_net(phone_th) <= heap0, "camera freed everything (%zu > %zu)", th_heap_net(phone_th), heap0); } +#else +void test_camera(void) {} +#endif diff --git a/tests/phone-regress.c b/tests/phone-regress.c index ac88b778..0ab22c05 100644 --- a/tests/phone-regress.c +++ b/tests/phone-regress.c @@ -8,6 +8,7 @@ * Kit regressions that need no app on screen. */ +#include #include #include "phone-test.h" @@ -88,7 +89,29 @@ static void test_text_fit_utf8(void) w, "every width"); } +static int color_distance(twin_argb32_t a, twin_argb32_t b) +{ + int d = 0; + for (int shift = 0; shift < 24; shift += 8) + d += abs((int) ((a >> shift) & 0xff) - (int) ((b >> shift) & 0xff)); + return d; +} + +/* At home the Home tab is the current one, whichever apps are built: a + * view without a launcher slot must not light the Dialer tab */ +static void test_nav_at_home(void) +{ + CHECK_EQ(phone_depth(), 0, "at home"); + twin_argb32_t accent = phone_theme()->primary; + twin_coord_t y = PHONE_H - PHONE_NAV_H + PHONE_NAV_H / 2; + twin_argb32_t dialer = th_pixel(phone_th, 9, y); + twin_argb32_t home = th_pixel(phone_th, PHONE_W / 3 + 9, y); + CHECK(color_distance(home, accent) < color_distance(dialer, accent), + "home tab lit at home (home %08x, dialer %08x)", home, dialer); +} + void test_regress(void) { test_text_fit_utf8(); + test_nav_at_home(); } diff --git a/tests/phone-sms.c b/tests/phone-sms.c index 7f47d9b8..b6b7919c 100644 --- a/tests/phone-sms.c +++ b/tests/phone-sms.c @@ -13,6 +13,8 @@ #include "data.h" #include "phone-test.h" +#if defined(CONFIG_DEMO_PHONE_MESSAGES) + int app_sms_test_offset(void); int app_sms_test_max_offset(void); int app_sms_test_messages(void); @@ -124,3 +126,6 @@ void test_sms(void) ph_home(); } +#else +void test_sms(void) {} +#endif diff --git a/tests/test-cellphone.c b/tests/test-cellphone.c index c3bf45fb..dc784c66 100644 --- a/tests/test-cellphone.c +++ b/tests/test-cellphone.c @@ -29,6 +29,7 @@ static void fixed_clock(phone_clock_t *c) } const char *app_calc_eval(const char *typed); +bool app_home_icon_rect(int app, ui_rect_t *out); th_t *phone_th; static th_t *t; @@ -141,9 +142,13 @@ static void test_wrap(void) #if defined(CONFIG_DEMO_CALCULATOR) static void test_calculator(void) { - /* Calculator is the fourth icon: row 1, column 0 */ - th_tap(t, 40, PHONE_STATUS_H + 6 + 74 + 28); - th_settle(t, 2000); + /* Opened from its icon, wherever the apps built put it */ + ui_rect_t r; + bool shown = app_home_icon_rect(phone_app_find("Calculator"), &r); + CHECK(shown, "calculator icon on the first page"); + if (!shown) + return; + ph_tap((r.left + r.right) / 2, (r.top + r.bottom) / 2); CHECK_EQ(phone_depth(), 1, "calculator opened"); ph_shot("calculator"); @@ -262,6 +267,13 @@ int main(void) t = phone_th = th_create(PHONE_W, PHONE_H); phone_boot(t->screen); th_settle(t, 2000); + /* The frames show whichever apps the launcher lists */ + char apps[512] = ""; + for (int i = 0; i < phone_app_count(); i++) { + size_t n = strlen(apps); + snprintf(apps + n, sizeof(apps) - n, "%s,", phone_app_at(i)->name); + } + th_golden_salt(apps); test_boot_and_unlock(); test_wrap(); From c1e25d553b5622abd67952944d862f908e97bfee Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Tue, 20 Oct 2026 09:10:37 +0800 Subject: [PATCH 09/13] Run the arcade games under any window manager Snake, Pong and Tetris were phone apps only, although nothing in them needs the phone shell. They move to apps/ as their own demos, each a kit view in a window of its own on the desktop or under the plain policies, and still the themed launcher apps on the phone. Without the shell every app starts at boot, so these windows open paused until tapped rather than playing unasked. The UI kit no longer reaches into the shell: views open on a screen the shell attaches or on one given to ui_view_new_at(), the theme table lives in the kit, and a covered view is judged by the window manager's policy. A view wider than the phone layout, as a maximizing policy makes it, shows the theme background beside it. Each game owns its ops table, opener and launcher icon, so games.c no longer knows them. --- Makefile | 16 +- apps/apps.c | 10 + apps/apps.h | 3 +- apps/cellphone/phone.h | 37 ++++ apps/cellphone/shell.c | 211 ++----------------- apps/cellphone/ui-paint.c | 117 ++++++++++ apps/cellphone/ui.c | 113 ++++++++-- apps/{cellphone => }/games.c | 99 +++++---- apps/{cellphone/games-internal.h => games.h} | 37 ++-- apps/{cellphone => }/pong.c | 34 ++- apps/{cellphone => }/snake.c | 43 +++- apps/{cellphone => }/tetris.c | 36 +++- configs/Kconfig | 32 +++ tests/phone-games.c | 56 +++-- 14 files changed, 538 insertions(+), 306 deletions(-) rename apps/{cellphone => }/games.c (73%) rename apps/{cellphone/games-internal.h => games.h} (85%) rename apps/{cellphone => }/pong.c (87%) rename apps/{cellphone => }/snake.c (80%) rename apps/{cellphone => }/tetris.c (89%) diff --git a/Makefile b/Makefile index 222043e0..88ea367c 100644 --- a/Makefile +++ b/Makefile @@ -173,20 +173,22 @@ libapps.a_files-$(CONFIG_DEMO_CALCULATOR) += apps/calc.c libapps.a_files-$(CONFIG_DEMO_SPLINE) += apps/spline.c libapps.a_files-$(CONFIG_DEMO_ANIMATION) += apps/animation.c libapps.a_files-$(CONFIG_DEMO_IMAGE) += apps/image.c +libapps.a_files-$(CONFIG_DEMO_SNAKE) += apps/snake.c +libapps.a_files-$(CONFIG_DEMO_PONG) += apps/pong.c +libapps.a_files-$(CONFIG_DEMO_TETRIS) += apps/tetris.c +libapps.a_files-$(CONFIG_DEMO_GAMES) += apps/games.c -libapps.a_files-$(CONFIG_DEMO_CELLPHONE) += \ +libapps.a_files-$(CONFIG_DEMO_UIKIT) += \ apps/cellphone/ui.c \ apps/cellphone/ui-paint.c \ - apps/cellphone/ui-input.c \ + apps/cellphone/ui-input.c + +libapps.a_files-$(CONFIG_DEMO_CELLPHONE) += \ apps/cellphone/shell.c \ apps/cellphone/home.c \ apps/cellphone/lock.c \ apps/cellphone/data.c \ - apps/cellphone/settings.c \ - apps/cellphone/games.c \ - apps/cellphone/snake.c \ - apps/cellphone/pong.c \ - apps/cellphone/tetris.c + apps/cellphone/settings.c libapps.a_files-$(CONFIG_DEMO_PHONE_DIALER) += apps/cellphone/dialer.c libapps.a_files-$(CONFIG_DEMO_PHONE_CALLLOG) += apps/cellphone/calllog.c libapps.a_files-$(CONFIG_DEMO_PHONE_MESSAGES) += apps/cellphone/sms.c diff --git a/apps/apps.c b/apps/apps.c index 8c8be0e1..cbc88968 100644 --- a/apps/apps.c +++ b/apps/apps.c @@ -38,6 +38,16 @@ static const apps_entry_t apps[] = { #endif #if defined(CONFIG_DEMO_IMAGE) {"Vectors", TWIN_SYM_GLOBE, 20, 20, 0, 0, apps_image_start, NULL, NULL}, +#endif +/* Kit views in plain windows; the phone lists its own, themed rows */ +#if defined(CONFIG_DEMO_SNAKE) + {"Snake", TWIN_SYM_PLAY, 60, 40, 0, 0, apps_snake_start, NULL, NULL}, +#endif +#if defined(CONFIG_DEMO_PONG) + {"Pong", TWIN_SYM_PLAY, 90, 60, 0, 0, apps_pong_start, NULL, NULL}, +#endif +#if defined(CONFIG_DEMO_TETRIS) + {"Tetris", TWIN_SYM_PLAY, 120, 80, 0, 0, apps_tetris_start, NULL, NULL}, #endif {0}, /* keeps the table valid when nothing is built */ }; diff --git a/apps/apps.h b/apps/apps.h index 9e004743..74b13615 100644 --- a/apps/apps.h +++ b/apps/apps.h @@ -48,6 +48,7 @@ const apps_entry_t *apps_at(int idx); #define ASSET_PATH "assets/" apps_start_fn apps_animation_start, apps_calc_start, apps_clock_start, - apps_image_start, apps_multi_start, apps_spline_start; + apps_image_start, apps_multi_start, apps_spline_start, apps_snake_start, + apps_pong_start, apps_tetris_start; #endif /* _APPS_H_ */ diff --git a/apps/cellphone/phone.h b/apps/cellphone/phone.h index 354f4a39..4ef26efd 100644 --- a/apps/cellphone/phone.h +++ b/apps/cellphone/phone.h @@ -112,6 +112,8 @@ const phone_theme_t *phone_theme(void); int phone_theme_count(void); int phone_theme_index(void); const phone_theme_t *phone_theme_at(int idx); +/* Make theme @idx current without repainting; false if out of range */ +bool ui_theme_select(int idx); /* Switch theme and repaint every view */ void phone_theme_set(int idx); @@ -329,6 +331,18 @@ void ui_vgrad(twin_pixmap_t *px, bool ui_init(void); void ui_fini(void); +/* ui_view_new() opens views on @screen from now on; @changed (may be + * NULL) runs whenever the view stack changes. The phone shell attaches + * itself. */ +void ui_attach(twin_screen_t *screen, void (*changed)(void)); + +/* An app window, mapped and not a bar, so it takes part in the stack: the + * phone's views and the windows of registry apps alike */ +bool ui_is_app_window(twin_window_t *w); + +/* Topmost mapped app window on the attached screen, NULL if none */ +twin_window_t *ui_top_window(void); + twin_path_t *ui_path(void); void ui_path_done(twin_path_t *path); @@ -408,6 +422,17 @@ void *ui_view_new(size_t size, ui_handler_t event, ui_destroy_t destroy); +/* Same, in a window of its own at @x,@y on @screen, for running kit views + * under any window manager without the phone shell */ +void *ui_view_new_at(twin_screen_t *screen, + twin_coord_t x, + twin_coord_t y, + size_t size, + const char *title, + ui_paint_t paint, + ui_handler_t event, + ui_destroy_t destroy); + /* Same, covering the whole screen above the bars (lock screen) */ void *ui_view_new_fullscreen(size_t size, const char *title, @@ -421,6 +446,11 @@ void ui_view_show(ui_view_t *view); /* Close the view (slides out); state is freed immediately */ void ui_view_close(ui_view_t *view); +/* True while @view is hidden: below an app or fullscreen window under the + * mobile policy, unmapped under one that overlaps windows (there a view + * behind others still counts as shown) */ +bool ui_view_covered(const ui_view_t *view); + /* * Geometry that depends on the view's size (scroll areas and the like) is * computed by @layout: now, and again whenever the view is resized. @@ -431,6 +461,9 @@ void ui_view_set_layout(ui_view_t *view, ui_layout_t layout); int ui_key_ascii(const ui_event_t *event); ui_rect_t ui_view_bounds(const ui_view_t *view); +/* True when the window manager draws a title bar for @view, so a header + * inside it need not name the view again */ +bool ui_view_titled(const ui_view_t *view); twin_coord_t ui_view_width(const ui_view_t *view); twin_coord_t ui_view_height(const ui_view_t *view); @@ -714,6 +747,10 @@ void app_calllog_open(void); void app_snake_open(void); void app_pong_open(void); void app_tetris_open(void); +/* Their launcher icons (phone_glyph_t) */ +void app_snake_glyph(twin_pixmap_t *px, ui_rect_t p, twin_argb32_t fg); +void app_pong_glyph(twin_pixmap_t *px, ui_rect_t p, twin_argb32_t fg); +void app_tetris_glyph(twin_pixmap_t *px, ui_rect_t p, twin_argb32_t fg); /* Home and lock screens (shell.c) */ ui_view_t *home_create(void); diff --git a/apps/cellphone/shell.c b/apps/cellphone/shell.c index b4a4857f..ef6291d0 100644 --- a/apps/cellphone/shell.c +++ b/apps/cellphone/shell.c @@ -21,115 +21,6 @@ #include "apps.h" #include "phone.h" -/* - * Themes - */ -static const phone_theme_t themes[] = { - { - /* Sage: warm white pages, sage green groups, deep olive bars */ - .name = "Sage", - .primary = UI_RGB(0x667a3e), - .primary_dk = UI_RGB(0x4f5f30), - .statusbar = UI_RGB(0x4e5540), - .statusbar_grad = UI_RGB(0x646b54), - .header = UI_RGB(0x7d8568), - .header_grad = UI_RGB(0x6a7257), - .on_bar = UI_RGB(0xf7f5ec), - .navbar = UI_RGB(0xd3d9c4), - .navbar_grad = UI_RGB(0xe0e5d3), - .bg = UI_RGB(0xf4f2e9), - .bg_grad = UI_RGB(0xedeadf), - .card = UI_RGB(0xfbfaf4), - .group = UI_RGB(0xe2e6d6), - .text = UI_RGB(0x26301f), - .text_sec = UI_RGB(0x5c6652), - .separator = UI_RGB(0xdadccc), - .call_green = UI_RGB(0x55873a), - .call_red = UI_RGB(0xb0473e), - .missed_red = UI_RGB(0xa63c34), - .sms_sent = UI_RGB(0xdde5c6), - .sms_recv = UI_RGB(0xfbfaf4), - .indicator = UI_RGB(0xbcc2ab), - .on_primary = UI_RGB(0xffffff), - }, - { - .name = "Dark", - .primary = UI_RGB(0x8fb06a), - .primary_dk = UI_RGB(0x6f8f4c), - .statusbar = UI_RGB(0x000000), - .statusbar_grad = UI_RGB(0x0c0c0c), - .header = UI_RGB(0x262923), - .header_grad = UI_RGB(0x1e201c), - .on_bar = UI_RGB(0xe4e6df), - .navbar = UI_RGB(0x1a1b18), - .navbar_grad = UI_RGB(0x22241f), - .bg = UI_RGB(0x121311), - .bg_grad = UI_RGB(0x121311), - .card = UI_RGB(0x1c1d1a), - .group = UI_RGB(0x2a2c27), - .text = UI_RGB(0xe4e6df), - .text_sec = UI_RGB(0xa0a498), - .separator = UI_RGB(0x34372f), - .call_green = UI_RGB(0x6fbf5a), - .call_red = UI_RGB(0xe0574d), - .missed_red = UI_RGB(0xe0574d), - .sms_sent = UI_RGB(0x34452a), - .sms_recv = UI_RGB(0x2a2c27), - .indicator = UI_RGB(0x50544a), - .on_primary = UI_RGB(0x10140c), - }, - { - .name = "Ocean", - .primary = UI_RGB(0x1e78b4), - .primary_dk = UI_RGB(0x165e8e), - .statusbar = UI_RGB(0x14496e), - .statusbar_grad = UI_RGB(0x1d5f8c), - .header = UI_RGB(0x2b78a8), - .header_grad = UI_RGB(0x226690), - .on_bar = UI_RGB(0xffffff), - .navbar = UI_RGB(0xcfe0ec), - .navbar_grad = UI_RGB(0xdeeaf3), - .bg = UI_RGB(0xf2f7fa), - .bg_grad = UI_RGB(0xe8f0f6), - .card = UI_RGB(0xffffff), - .group = UI_RGB(0xdde9f2), - .text = UI_RGB(0x1c2833), - .text_sec = UI_RGB(0x55687a), - .separator = UI_RGB(0xd5dee5), - .call_green = UI_RGB(0x2e9a58), - .call_red = UI_RGB(0xc8403f), - .missed_red = UI_RGB(0xc03030), - .sms_sent = UI_RGB(0xcfe5f6), - .sms_recv = UI_RGB(0xffffff), - .indicator = UI_RGB(0xa9c3d6), - .on_primary = UI_RGB(0xffffff), - }, -}; - -#define N_THEMES ((int) (sizeof(themes) / sizeof(themes[0]))) - -static int theme_idx; - -const phone_theme_t *phone_theme(void) -{ - return &themes[theme_idx]; -} - -int phone_theme_count(void) -{ - return N_THEMES; -} - -int phone_theme_index(void) -{ - return theme_idx; -} - -const phone_theme_t *phone_theme_at(int idx) -{ - return (idx >= 0 && idx < N_THEMES) ? &themes[idx] : NULL; -} - /* * Shell state */ @@ -160,7 +51,7 @@ static void _repaint_window(twin_window_t *w) static int nav_state(void); /* The view stack changed: repaint the nav bar if its tabs look different */ -void phone_view_changed(void) +static void phone_view_changed(void) { if (nav_state() != g_nav_drawn) _repaint_window(g_nav); @@ -168,9 +59,8 @@ void phone_view_changed(void) void phone_theme_set(int idx) { - if (idx < 0 || idx >= N_THEMES || !g_screen) + if (!g_screen || !ui_theme_select(idx)) return; - theme_idx = idx; for (twin_window_t *w = twin_screen_first_window(g_screen); w; w = twin_window_next(w)) _repaint_window(w); @@ -279,9 +169,6 @@ void phone_format_date(char *buf, size_t len) /* * App registry and icons */ -static void glyph_snake(twin_pixmap_t *px, ui_rect_t p, twin_argb32_t fg); -static void glyph_pong(twin_pixmap_t *px, ui_rect_t p, twin_argb32_t fg); -static void glyph_tetris(twin_pixmap_t *px, ui_rect_t p, twin_argb32_t fg); /* The apps picked in Kconfig, in launcher order */ static const phone_app_t apps[] = { @@ -319,10 +206,18 @@ static const phone_app_t apps[] = { {"Call Log", TWIN_SYM_HISTORY, NULL, UI_RGB(0x7a6a96), app_calllog_open, NULL, NULL}, #endif - {"Snake", NULL, glyph_snake, UI_RGB(0x4d8f86), app_snake_open, NULL, NULL}, - {"Pong", NULL, glyph_pong, UI_RGB(0x646c9a), app_pong_open, NULL, NULL}, - {"Tetris", NULL, glyph_tetris, UI_RGB(0xad5348), app_tetris_open, NULL, - NULL}, +#if defined(CONFIG_DEMO_SNAKE) + {"Snake", NULL, app_snake_glyph, UI_RGB(0x4d8f86), app_snake_open, NULL, + apps_snake_start}, +#endif +#if defined(CONFIG_DEMO_PONG) + {"Pong", NULL, app_pong_glyph, UI_RGB(0x646c9a), app_pong_open, NULL, + apps_pong_start}, +#endif +#if defined(CONFIG_DEMO_TETRIS) + {"Tetris", NULL, app_tetris_glyph, UI_RGB(0xad5348), app_tetris_open, NULL, + apps_tetris_start}, +#endif }; #define N_APPS ((int) (sizeof(apps) / sizeof(apps[0]))) @@ -386,50 +281,6 @@ int phone_app_find(const char *name) return -1; } -static void glyph_snake(twin_pixmap_t *px, ui_rect_t p, twin_argb32_t fg) -{ - int u = ui_w(p) / 8; - twin_coord_t x = p.left + 2 * u, y = p.top + 2 * u; - twin_path_t *path = ui_path(); - twin_path_move(path, twin_int_to_fixed(x), twin_int_to_fixed(y + 4 * u)); - twin_path_draw(path, twin_int_to_fixed(x), twin_int_to_fixed(y)); - twin_path_draw(path, twin_int_to_fixed(x + 4 * u), twin_int_to_fixed(y)); - twin_path_draw(path, twin_int_to_fixed(x + 4 * u), - twin_int_to_fixed(y + 2 * u)); - twin_path_draw(path, twin_int_to_fixed(x + 2 * u), - twin_int_to_fixed(y + 2 * u)); - twin_path_draw(path, twin_int_to_fixed(x + 2 * u), - twin_int_to_fixed(y + 4 * u)); - twin_path_set_cap_style(path, TwinCapRound); - twin_paint_stroke(px, fg, path, twin_int_to_fixed(u + u / 2)); - ui_path_done(path); - ui_circle(px, x + 4 * u, y + 4 * u, u / 2 + 1, ui_alpha(fg, 0xc0)); -} - -static void glyph_pong(twin_pixmap_t *px, ui_rect_t p, twin_argb32_t fg) -{ - int u = ui_w(p) / 8; - ui_rrect(px, ui_rect(p.left + u + u / 2, p.top + 2 * u, u, 3 * u), u / 2, - fg); - ui_rrect(px, ui_rect(p.right - 2 * u - u / 2, p.top + 3 * u, u, 3 * u), - u / 2, fg); - ui_circle(px, (p.left + p.right) / 2, p.top + 3 * u + u / 2, u * 2 / 3 + 1, - fg); -} - -static void glyph_tetris(twin_pixmap_t *px, ui_rect_t p, twin_argb32_t fg) -{ - int u = ui_w(p) / 7; - twin_coord_t x = p.left + (ui_w(p) - 3 * u) / 2; - twin_coord_t y = p.top + (ui_h(p) - 2 * u) / 2; - static const int cells[4][2] = {{0, 0}, {1, 0}, {2, 0}, {1, 1}}; - for (int i = 0; i < 4; i++) - ui_rrect(px, - ui_rect(x + cells[i][0] * u + 1, y + cells[i][1] * u + 1, - u - 2, u - 2), - 2, i == 3 ? ui_alpha(fg, 0xb0) : fg); -} - void phone_app_icon(twin_pixmap_t *px, int app, ui_rect_t plate, bool pressed) { const phone_app_t *a = phone_app_at(app); @@ -562,13 +413,6 @@ static int nav_hit(twin_coord_t x, twin_coord_t y) return -1; } -/* The stack holds phone views and the windows of registry apps alike */ -static bool is_app_window(twin_window_t *w) -{ - return twin_window_is_mapped(w) && - twin_window_style(w) == TwinWindowApplication; -} - static bool is_home(twin_window_t *w) { return g_home && w == g_home->window; @@ -579,7 +423,7 @@ static void _drop_apps(twin_window_t *keep) { for (twin_window_t *w = twin_screen_first_window(g_screen); w;) { twin_window_t *next = twin_window_next(w); - if (w != keep && is_app_window(w) && !is_home(w)) + if (w != keep && ui_is_app_window(w) && !is_home(w)) twin_window_destroy(w); w = next; } @@ -628,22 +472,9 @@ static bool nav_event(twin_window_t *w, twin_event_t *ev) /* * Navigation */ -static twin_window_t *_top_app(void) -{ - twin_window_t *top = NULL; - if (!g_screen) - return NULL; - for (twin_window_t *w = twin_screen_first_window(g_screen); w; - w = twin_window_next(w)) - if (is_app_window(w)) - top = w; - return top; -} - ui_view_t *phone_top(void) { - twin_window_t *w = _top_app(); - return w ? ui_view_of(w) : NULL; + return ui_view_of(ui_top_window()); } int phone_depth(void) @@ -653,7 +484,7 @@ int phone_depth(void) return 0; for (twin_window_t *w = twin_screen_first_window(g_screen); w; w = twin_window_next(w)) - if (is_app_window(w) && !is_home(w)) + if (ui_is_app_window(w) && !is_home(w)) n++; return n; } @@ -682,14 +513,14 @@ void phone_open(int app) void phone_back(void) { - twin_window_t *top = _top_app(); + twin_window_t *top = ui_top_window(); if (top && !is_home(top)) twin_window_back(top); } void phone_home(void) { - twin_window_t *top = _top_app(); + twin_window_t *top = ui_top_window(); if (!top || is_home(top)) return; /* Hidden screens go first (no animation), then the visible one pops */ @@ -767,6 +598,7 @@ bool phone_boot(twin_screen_t *screen) if (!ui_init()) return false; g_screen = screen; + ui_attach(screen, phone_view_changed); _list_demos(); g_status = bar_create(0, PHONE_STATUS_H, status_draw, NULL); @@ -814,9 +646,10 @@ void phone_shutdown(void) g_status = g_nav = NULL; g_home = NULL; g_screen = NULL; + ui_attach(NULL, NULL); g_status_minute = -1; g_nav_pressed = -1; - theme_idx = 0; + ui_theme_select(0); g_wallpaper = 0; ui_fini(); } diff --git a/apps/cellphone/ui-paint.c b/apps/cellphone/ui-paint.c index a8a8dba7..5300faa3 100644 --- a/apps/cellphone/ui-paint.c +++ b/apps/cellphone/ui-paint.c @@ -72,6 +72,123 @@ static bool _text_culled(twin_pixmap_t *px, int size, twin_coord_t y) return y + size + 2 <= clip.top || y - 2 * size - 2 >= clip.bottom; } +/* + * Themes + */ +static const phone_theme_t themes[] = { + { + /* Sage: warm white pages, sage green groups, deep olive bars */ + .name = "Sage", + .primary = UI_RGB(0x667a3e), + .primary_dk = UI_RGB(0x4f5f30), + .statusbar = UI_RGB(0x4e5540), + .statusbar_grad = UI_RGB(0x646b54), + .header = UI_RGB(0x7d8568), + .header_grad = UI_RGB(0x6a7257), + .on_bar = UI_RGB(0xf7f5ec), + .navbar = UI_RGB(0xd3d9c4), + .navbar_grad = UI_RGB(0xe0e5d3), + .bg = UI_RGB(0xf4f2e9), + .bg_grad = UI_RGB(0xedeadf), + .card = UI_RGB(0xfbfaf4), + .group = UI_RGB(0xe2e6d6), + .text = UI_RGB(0x26301f), + .text_sec = UI_RGB(0x5c6652), + .separator = UI_RGB(0xdadccc), + .call_green = UI_RGB(0x55873a), + .call_red = UI_RGB(0xb0473e), + .missed_red = UI_RGB(0xa63c34), + .sms_sent = UI_RGB(0xdde5c6), + .sms_recv = UI_RGB(0xfbfaf4), + .indicator = UI_RGB(0xbcc2ab), + .on_primary = UI_RGB(0xffffff), + }, + { + .name = "Dark", + .primary = UI_RGB(0x8fb06a), + .primary_dk = UI_RGB(0x6f8f4c), + .statusbar = UI_RGB(0x000000), + .statusbar_grad = UI_RGB(0x0c0c0c), + .header = UI_RGB(0x262923), + .header_grad = UI_RGB(0x1e201c), + .on_bar = UI_RGB(0xe4e6df), + .navbar = UI_RGB(0x1a1b18), + .navbar_grad = UI_RGB(0x22241f), + .bg = UI_RGB(0x121311), + .bg_grad = UI_RGB(0x121311), + .card = UI_RGB(0x1c1d1a), + .group = UI_RGB(0x2a2c27), + .text = UI_RGB(0xe4e6df), + .text_sec = UI_RGB(0xa0a498), + .separator = UI_RGB(0x34372f), + .call_green = UI_RGB(0x6fbf5a), + .call_red = UI_RGB(0xe0574d), + .missed_red = UI_RGB(0xe0574d), + .sms_sent = UI_RGB(0x34452a), + .sms_recv = UI_RGB(0x2a2c27), + .indicator = UI_RGB(0x50544a), + .on_primary = UI_RGB(0x10140c), + }, + { + .name = "Ocean", + .primary = UI_RGB(0x1e78b4), + .primary_dk = UI_RGB(0x165e8e), + .statusbar = UI_RGB(0x14496e), + .statusbar_grad = UI_RGB(0x1d5f8c), + .header = UI_RGB(0x2b78a8), + .header_grad = UI_RGB(0x226690), + .on_bar = UI_RGB(0xffffff), + .navbar = UI_RGB(0xcfe0ec), + .navbar_grad = UI_RGB(0xdeeaf3), + .bg = UI_RGB(0xf2f7fa), + .bg_grad = UI_RGB(0xe8f0f6), + .card = UI_RGB(0xffffff), + .group = UI_RGB(0xdde9f2), + .text = UI_RGB(0x1c2833), + .text_sec = UI_RGB(0x55687a), + .separator = UI_RGB(0xd5dee5), + .call_green = UI_RGB(0x2e9a58), + .call_red = UI_RGB(0xc8403f), + .missed_red = UI_RGB(0xc03030), + .sms_sent = UI_RGB(0xcfe5f6), + .sms_recv = UI_RGB(0xffffff), + .indicator = UI_RGB(0xa9c3d6), + .on_primary = UI_RGB(0xffffff), + }, +}; + +#define N_THEMES ((int) (sizeof(themes) / sizeof(themes[0]))) + +static int theme_idx; + +const phone_theme_t *phone_theme(void) +{ + return &themes[theme_idx]; +} + +int phone_theme_count(void) +{ + return N_THEMES; +} + +int phone_theme_index(void) +{ + return theme_idx; +} + +const phone_theme_t *phone_theme_at(int idx) +{ + return (idx >= 0 && idx < N_THEMES) ? &themes[idx] : NULL; +} + +bool ui_theme_select(int idx) +{ + if (idx < 0 || idx >= N_THEMES) + return false; + theme_idx = idx; + return true; +} + /* * Typography */ diff --git a/apps/cellphone/ui.c b/apps/cellphone/ui.c index 0c181e8d..328c12cf 100644 --- a/apps/cellphone/ui.c +++ b/apps/cellphone/ui.c @@ -22,32 +22,81 @@ static void _ui_draw(twin_window_t *window); static bool _ui_event(twin_window_t *window, twin_event_t *ev); static void _ui_destroy(twin_window_t *window); -void phone_view_changed(void); - /* Every live view, so ui_view_of() can tell kit windows from the others */ static ui_view_t *g_views; -static void *_ui_view_new(size_t size, +/* Where views open, and who hears that the view stack changed */ +static twin_screen_t *g_screen; +static void (*g_changed)(void); + +void ui_attach(twin_screen_t *screen, void (*changed)(void)) +{ + g_screen = screen; + g_changed = changed; +} + +static void _changed(void) +{ + if (g_changed) + g_changed(); +} + +bool ui_is_app_window(twin_window_t *w) +{ + return twin_window_is_mapped(w) && + twin_window_style(w) == TwinWindowApplication; +} + +twin_window_t *ui_top_window(void) +{ + twin_window_t *top = NULL; + if (!g_screen) + return NULL; + for (twin_window_t *w = twin_screen_first_window(g_screen); w; + w = twin_window_next(w)) + if (ui_is_app_window(w)) + top = w; + return top; +} + +bool ui_view_covered(const ui_view_t *view) +{ +#if defined(CONFIG_WM_MOBILE) + /* The mobile policy gives every app window the whole work area, and a + * fullscreen one (the lock screen) more: either above hides this one */ + for (twin_window_t *w = twin_window_next(view->window); w; + w = twin_window_next(w)) + if (ui_is_app_window(w) || + (twin_window_is_mapped(w) && + twin_window_style(w) == TwinWindowFullscreen)) + return true; + return false; +#else + /* Overlapping windows: a view below another still shows */ + return !twin_window_is_mapped(view->window); +#endif +} + +/* A PHONE_W wide view of @h rows at @x,@y on @screen */ +static void *_ui_view_new(twin_screen_t *screen, twin_window_style_t style, + twin_coord_t x, + twin_coord_t y, + twin_coord_t h, + size_t size, const char *title, ui_paint_t paint, ui_handler_t event, ui_destroy_t destroy) { - twin_screen_t *screen = phone_screen(); if (!screen || size < sizeof(ui_view_t)) return NULL; ui_view_t *v = calloc(1, size); if (!v) return NULL; - twin_window_t *w; - if (style == TwinWindowFullscreen) - w = twin_window_create(screen, PHONE_FORMAT, style, 0, 0, PHONE_W, - PHONE_H); - else - w = twin_window_create(screen, PHONE_FORMAT, style, 0, PHONE_STATUS_H, - PHONE_W, PHONE_CONTENT_H); + twin_window_t *w = + twin_window_create(screen, PHONE_FORMAT, style, x, y, PHONE_W, h); if (!w) { free(v); return NULL; @@ -73,8 +122,21 @@ void *ui_view_new(size_t size, ui_handler_t event, ui_destroy_t destroy) { - return _ui_view_new(size, TwinWindowApplication, title, paint, event, - destroy); + return _ui_view_new(g_screen, TwinWindowApplication, 0, PHONE_STATUS_H, + PHONE_CONTENT_H, size, title, paint, event, destroy); +} + +void *ui_view_new_at(twin_screen_t *screen, + twin_coord_t x, + twin_coord_t y, + size_t size, + const char *title, + ui_paint_t paint, + ui_handler_t event, + ui_destroy_t destroy) +{ + return _ui_view_new(screen, TwinWindowApplication, x, y, PHONE_CONTENT_H, + size, title, paint, event, destroy); } void *ui_view_new_fullscreen(size_t size, @@ -83,8 +145,8 @@ void *ui_view_new_fullscreen(size_t size, ui_handler_t event, ui_destroy_t destroy) { - return _ui_view_new(size, TwinWindowFullscreen, title, paint, event, - destroy); + return _ui_view_new(g_screen, TwinWindowFullscreen, 0, 0, PHONE_H, size, + title, paint, event, destroy); } void ui_view_show(ui_view_t *view) @@ -92,7 +154,7 @@ void ui_view_show(ui_view_t *view) /* Paint first so the slide-in shows real content */ twin_window_draw(view->window); twin_window_show(view->window); - phone_view_changed(); + _changed(); } void ui_view_close(ui_view_t *view) @@ -126,6 +188,12 @@ ui_rect_t ui_view_bounds(const ui_view_t *view) return ui_rect(0, 0, ui_view_width(view), ui_view_height(view)); } +bool ui_view_titled(const ui_view_t *view) +{ + /* Decorations sit above the client: a title bar pushes it down */ + return twin_window_client(view->window).top > 0; +} + twin_coord_t ui_view_width(const ui_view_t *view) { return ui_w(twin_window_client(view->window)); @@ -177,8 +245,15 @@ void ui_view_scroll(ui_view_t *view, ui_rect_t r, int dx, int dy) static void _ui_draw(twin_window_t *window) { ui_view_t *v = twin_window_client_data(window); + twin_pixmap_t *px = twin_window_pixmap(window); + /* Views are laid out PHONE_W wide: a policy that maximizes windows + * shows the theme background beside them */ + twin_coord_t w = ui_view_width(v); + if (w > PHONE_W) + ui_fill(px, ui_rect(PHONE_W, 0, w - PHONE_W, ui_view_height(v)), + phone_theme()->bg); if (v->paint) - v->paint(v, twin_window_pixmap(window)); + v->paint(v, px); } /* Deliver to the handler; never touch @v afterwards (it may be gone) */ @@ -255,7 +330,7 @@ static bool _ui_event(twin_window_t *window, twin_event_t *ev) _deliver(v, &e); /* A view revealed by a closing demo window: the stack changed */ if (ev->kind == TwinEventActivate) - phone_view_changed(); + _changed(); return true; case TwinEventClose: return false; /* let the window close */ @@ -298,7 +373,7 @@ static void _ui_destroy(twin_window_t *window) if (v->destroy) v->destroy(v); free(v); - phone_view_changed(); + _changed(); } /* diff --git a/apps/cellphone/games.c b/apps/games.c similarity index 73% rename from apps/cellphone/games.c rename to apps/games.c index d4dcc604..36559110 100644 --- a/apps/cellphone/games.c +++ b/apps/games.c @@ -9,11 +9,11 @@ * * One view type (this file) serves all three games; everything game * specific sits in a const ops table (geometry, init, tick, paint) defined - * by each game's own file, so adding a game is one file and one row below. The - * board lives in small fixed arrays in the view, and a tick damages only what - * it changed: the snake's head and tail cells, the ball and paddle strips, the - * falling piece's old and new cells. The whole board is repainted only on a - * line clear or a restart. + * by each game's own file, which also opens it, so adding a game is one file + * and a launcher row. The board lives in small fixed arrays in the view, + * and a tick damages only what it changed: the snake's head and tail cells, + * the ball and paddle strips, the falling piece's old and new cells. The + * whole board is repainted only on a line clear or a restart. * * Window damage is a single bounding box, so two far-apart cells (snake head * and tail) would coalesce into a large rectangle. game_mark() batches nearby @@ -27,13 +27,7 @@ #include #include -#include "games-internal.h" - -static const game_ops_t *const ops[GAME_COUNT] = { - [GAME_SNAKE] = &game_snake_ops, - [GAME_PONG] = &game_pong_ops, - [GAME_TETRIS] = &game_tetris_ops, -}; +#include "games.h" /* * Helpers @@ -69,6 +63,13 @@ static ui_rect_t score_rect(void) return ui_rect(PHONE_W / 2 - 24, 0, PHONE_W / 2 + 24, HEADER_H - 1); } +/* The hint under the panel */ +static ui_rect_t footer_rect(const ui_view_t *v) +{ + twin_coord_t top = game_panel_rect().bottom; + return ui_rect(0, top, PHONE_W, ui_view_height(v) - top); +} + static ui_rect_t overlay_rect(void) { ui_rect_t p = game_panel_rect(); @@ -114,14 +115,14 @@ void game_mark(game_t *g, ui_rect_t r) ui_rect_t game_cell_rect(const game_t *g, int c, int r) { - const game_ops_t *o = ops[g->kind]; + const game_ops_t *o = g->ops; return ui_rect(o->ox + c * o->cell, o->oy + r * o->cell, o->cell, o->cell); } /* Cells-changed hook shared by the grid games */ void game_mark_cell(game_t *g, int c, int r) { - const game_ops_t *o = ops[g->kind]; + const game_ops_t *o = g->ops; if (c >= 0 && r >= 0 && c < o->cols && r < o->rows) game_mark(g, game_cell_rect(g, c, r)); } @@ -134,7 +135,7 @@ void game_cell_span(const game_t *g, int *r0, int *r1) { - const game_ops_t *o = ops[g->kind]; + const game_ops_t *o = g->ops; int a = (clip.left - o->ox) / o->cell, b = (clip.right - o->ox - 1); int c = (clip.top - o->oy) / o->cell, d = (clip.bottom - o->oy - 1); b = b < 0 ? -1 : b / o->cell; @@ -159,7 +160,7 @@ void game_set_score(game_t *g, int score) void game_restart(game_t *g) { game_set_score(g, 0); - ops[g->kind]->init(g); + g->ops->init(g); g->full++; game_mark(g, game_inner_rect()); } @@ -180,7 +181,7 @@ static void game_paint(ui_view_t *v, twin_pixmap_t *px) snprintf(buf, sizeof(buf), "Score %d \xc2\xb7 Best %d", g->score, g->best); ui_header(px, ui_rect(0, 0, PHONE_W, HEADER_H), NULL, - ops[g->kind]->name, buf); + ui_view_titled(v) ? NULL : g->ops->name, buf); } /* Ticks damage cells inside the panel: fill just the clip there */ if (ui_rect_contains(in, clip)) { @@ -190,12 +191,11 @@ static void game_paint(ui_view_t *v, twin_pixmap_t *px) th->bg); ui_rrect(px, panel, PANEL_R, C_PANEL); ui_rrect_stroke(px, panel, PANEL_R, 1, C_BORDER); - ui_text(px, FONT_SM, PHONE_W / 2, - ui_baseline(FONT_SM, panel.bottom, - ui_view_height(v) - panel.bottom), - UI_CENTER, th->text_sec, "Auto-play \xc2\xb7 tap to pause"); + ui_text_in(px, FONT_SM, footer_rect(v), UI_CENTER, th->text_sec, + g->paused ? "Paused \xc2\xb7 tap to play" + : "Auto-play \xc2\xb7 tap to pause"); } - ops[g->kind]->paint(g, px, clip); + g->ops->paint(g, px, clip); ui_rect_t o = overlay_rect(); if (g->paused && ui_intersects(clip, o)) { @@ -210,9 +210,9 @@ static void game_paint(ui_view_t *v, twin_pixmap_t *px) static twin_time_t game_tick(ui_view_t *v) { game_t *g = (game_t *) v; - const game_ops_t *o = ops[g->kind]; - /* Freeze while another view covers this one */ - if (g->paused || phone_top() != v) + const game_ops_t *o = g->ops; + /* Freeze while another window covers this one */ + if (g->paused || ui_view_covered(v)) return o->period; o->tick(g); g->steps++; @@ -226,11 +226,12 @@ static void set_paused(game_t *g, bool paused) return; g->paused = paused; ui_view_invalidate(&g->view, overlay_rect()); + ui_view_invalidate(&g->view, footer_rect(&g->view)); /* No timer while paused: nothing to wake up for */ if (paused) ui_view_stop_timers(&g->view); else - ui_view_every(&g->view, ops[g->kind]->period, game_tick); + ui_view_every(&g->view, g->ops->period, game_tick); } static bool game_event(ui_view_t *v, const ui_event_t *e) @@ -244,36 +245,44 @@ static bool game_event(ui_view_t *v, const ui_event_t *e) return false; } -static void game_open(game_kind_t kind) +/* Set up @g, a fresh view (NULL when its allocation failed), and show it; + * a @paused game waits for a tap before its first step */ +static bool game_show(game_t *g, const game_ops_t *ops, bool paused) { - game_t *g = - ui_view_new(sizeof(*g), ops[kind]->name, game_paint, game_event, NULL); if (!g) - return; - g->kind = kind; - g->rng = 0x2545f491u * (uint32_t) (kind + 1); - ops[kind]->init(g); + return false; + g->ops = ops; + g->rng = 0x2545f491u * ops->seed; + ops->init(g); g->pend = (ui_rect_t) {0, 0, 0, 0}; /* the first paint covers it all */ - if (!ui_view_every(&g->view, ops[kind]->period, game_tick)) { + g->paused = paused; + if (!paused && !ui_view_every(&g->view, ops->period, game_tick)) { ui_view_close(&g->view); - return; + return false; } ui_view_show(&g->view); + return true; } -void app_snake_open(void) -{ - game_open(GAME_SNAKE); -} - -void app_pong_open(void) +void game_open(const game_ops_t *ops) { - game_open(GAME_PONG); + game_show( + ui_view_new(sizeof(game_t), ops->name, game_paint, game_event, NULL), + ops, false); } -void app_tetris_open(void) +/* Opens paused: without the phone shell every registry app starts at boot, + * and three games playing unasked would only burn cycles */ +bool game_start(const game_ops_t *ops, + twin_screen_t *screen, + const char *name, + int x, + int y) { - game_open(GAME_TETRIS); + return ui_init() && + game_show(ui_view_new_at(screen, x, y, sizeof(game_t), name, + game_paint, game_event, NULL), + ops, true); } /* @@ -281,7 +290,7 @@ void app_tetris_open(void) */ static game_t *top_game(void) { - ui_view_t *v = phone_top(); + ui_view_t *v = ui_view_of(ui_top_window()); return v && v->paint == game_paint ? (game_t *) v : NULL; } diff --git a/apps/cellphone/games-internal.h b/apps/games.h similarity index 85% rename from apps/cellphone/games-internal.h rename to apps/games.h index 917b9ccc..d7f3fba7 100644 --- a/apps/cellphone/games-internal.h +++ b/apps/games.h @@ -9,9 +9,10 @@ * per game (snake.c, pong.c, tetris.c). */ -#ifndef _GAMES_INTERNAL_H_ -#define _GAMES_INTERNAL_H_ +#ifndef _APPS_GAMES_H_ +#define _APPS_GAMES_H_ +#include "apps.h" #include "phone.h" /* Layout, client coordinates */ @@ -59,13 +60,6 @@ #define TT_SIDE_X (TT_X + TT_COLS * TT_CELL) #define TT_PREV_CELL 9 -typedef enum { - GAME_SNAKE, - GAME_PONG, - GAME_TETRIS, - GAME_COUNT, -} game_kind_t; - typedef struct { uint8_t occ[SN_ROWS][SN_COLS]; /* 1 where the body is */ uint8_t bx[SN_N], by[SN_N]; /* ring buffer, head at index head */ @@ -89,9 +83,11 @@ typedef struct { int target_x, target_rot; } tetris_t; +typedef struct game_ops game_ops_t; + typedef struct { ui_view_t view; - game_kind_t kind; + const game_ops_t *ops; uint32_t rng; bool paused; int score, best; @@ -106,8 +102,9 @@ typedef struct { } u; } game_t; -typedef struct { +struct game_ops { const char *name; + uint32_t seed; /* the PRNG's start, so every open replays alike */ twin_time_t period; /* ms per step */ /* Cell grid for mark_cell(); zero for games drawn in pixels */ int cols, rows, cell; @@ -115,14 +112,18 @@ typedef struct { void (*init)(game_t *g); /* new round; the caller repaints the board */ void (*tick)(game_t *g); /* advance one step, mark() what changed */ void (*paint)(game_t *g, twin_pixmap_t *px, ui_rect_t clip); -} game_ops_t; - -/* Per-game tables, defined next to each game */ -extern const game_ops_t game_snake_ops; -extern const game_ops_t game_pong_ops; -extern const game_ops_t game_tetris_ops; +}; /* Helpers (games.c) */ +/* Open @ops as a phone view, pushed on the shell's stack */ +void game_open(const game_ops_t *ops); +/* Open @ops paused in a window of its own on @screen at @x,@y (any window + * manager, no phone shell); false when it could not start */ +bool game_start(const game_ops_t *ops, + twin_screen_t *screen, + const char *name, + int x, + int y); uint32_t game_rnd(game_t *g); int game_rnd_below(game_t *g, int n); ui_rect_t game_panel_rect(void); @@ -143,4 +144,4 @@ void game_set_score(game_t *g, int score); /* Start a new round and repaint the board */ void game_restart(game_t *g); -#endif /* _GAMES_INTERNAL_H_ */ +#endif /* _APPS_GAMES_H_ */ diff --git a/apps/cellphone/pong.c b/apps/pong.c similarity index 87% rename from apps/cellphone/pong.c rename to apps/pong.c index 9bdc566f..ed4a1b3a 100644 --- a/apps/cellphone/pong.c +++ b/apps/pong.c @@ -8,7 +8,7 @@ #include -#include "games-internal.h" +#include "games.h" /* * Pong: the ball moves in subpixels and speeds up on every hit; paddles @@ -204,6 +204,34 @@ static void pong_paint(game_t *g, twin_pixmap_t *px, ui_rect_t clip) /* Pong steps faster: the ball moves continuously and a step damages only * two small strips and the ball */ -const game_ops_t game_pong_ops = { - "Pong", 50, 0, 0, 0, 0, 0, pong_init, pong_tick, pong_paint, +static const game_ops_t game_pong_ops = { + "Pong", 2, 50, 0, 0, 0, 0, 0, pong_init, pong_tick, pong_paint, }; + +/* Launcher icon */ +void app_pong_glyph(twin_pixmap_t *px, ui_rect_t p, twin_argb32_t fg) +{ + int u = ui_w(p) / 8; + ui_rrect(px, ui_rect(p.left + u + u / 2, p.top + 2 * u, u, 3 * u), u / 2, + fg); + ui_rrect(px, ui_rect(p.right - 2 * u - u / 2, p.top + 3 * u, u, 3 * u), + u / 2, fg); + ui_circle(px, (p.left + p.right) / 2, p.top + 3 * u + u / 2, u * 2 / 3 + 1, + fg); +} + +void app_pong_open(void) +{ + game_open(&game_pong_ops); +} + +bool apps_pong_start(twin_screen_t *screen, + const char *name, + int x, + int y, + int w, + int h) +{ + (void) w, (void) h; /* the board has a fixed size */ + return game_start(&game_pong_ops, screen, name, x, y); +} diff --git a/apps/cellphone/snake.c b/apps/snake.c similarity index 80% rename from apps/cellphone/snake.c rename to apps/snake.c index dc300459..7c585e76 100644 --- a/apps/cellphone/snake.c +++ b/apps/snake.c @@ -8,7 +8,7 @@ #include -#include "games-internal.h" +#include "games.h" /* * Snake: steer by breadth-first search to the fruit, but only along moves @@ -184,7 +184,44 @@ static void snake_paint(game_t *g, twin_pixmap_t *px, ui_rect_t clip) } } -const game_ops_t game_snake_ops = { - "Snake", 130, SN_COLS, SN_ROWS, SN_CELL, +static const game_ops_t game_snake_ops = { + "Snake", 1, 130, SN_COLS, SN_ROWS, SN_CELL, INNER_X, INNER_Y + 1, snake_init, snake_tick, snake_paint, }; + +/* Launcher icon */ +void app_snake_glyph(twin_pixmap_t *px, ui_rect_t p, twin_argb32_t fg) +{ + int u = ui_w(p) / 8; + twin_coord_t x = p.left + 2 * u, y = p.top + 2 * u; + twin_path_t *path = ui_path(); + twin_path_move(path, twin_int_to_fixed(x), twin_int_to_fixed(y + 4 * u)); + twin_path_draw(path, twin_int_to_fixed(x), twin_int_to_fixed(y)); + twin_path_draw(path, twin_int_to_fixed(x + 4 * u), twin_int_to_fixed(y)); + twin_path_draw(path, twin_int_to_fixed(x + 4 * u), + twin_int_to_fixed(y + 2 * u)); + twin_path_draw(path, twin_int_to_fixed(x + 2 * u), + twin_int_to_fixed(y + 2 * u)); + twin_path_draw(path, twin_int_to_fixed(x + 2 * u), + twin_int_to_fixed(y + 4 * u)); + twin_path_set_cap_style(path, TwinCapRound); + twin_paint_stroke(px, fg, path, twin_int_to_fixed(u + u / 2)); + ui_path_done(path); + ui_circle(px, x + 4 * u, y + 4 * u, u / 2 + 1, ui_alpha(fg, 0xc0)); +} + +void app_snake_open(void) +{ + game_open(&game_snake_ops); +} + +bool apps_snake_start(twin_screen_t *screen, + const char *name, + int x, + int y, + int w, + int h) +{ + (void) w, (void) h; /* the board has a fixed size */ + return game_start(&game_snake_ops, screen, name, x, y); +} diff --git a/apps/cellphone/tetris.c b/apps/tetris.c similarity index 89% rename from apps/cellphone/tetris.c rename to apps/tetris.c index 2879fabf..c64e5910 100644 --- a/apps/cellphone/tetris.c +++ b/apps/tetris.c @@ -8,7 +8,7 @@ #include -#include "games-internal.h" +#include "games.h" /* * Tetris: on spawn, score every rotation and column by the resting @@ -273,7 +273,37 @@ static void tetris_paint(game_t *g, twin_pixmap_t *px, ui_rect_t clip) } } -const game_ops_t game_tetris_ops = { - "Tetris", 110, TT_COLS, TT_ROWS, TT_CELL, +static const game_ops_t game_tetris_ops = { + "Tetris", 3, 110, TT_COLS, TT_ROWS, TT_CELL, TT_X, TT_Y, tetris_init, tetris_tick, tetris_paint, }; + +/* Launcher icon */ +void app_tetris_glyph(twin_pixmap_t *px, ui_rect_t p, twin_argb32_t fg) +{ + int u = ui_w(p) / 7; + twin_coord_t x = p.left + (ui_w(p) - 3 * u) / 2; + twin_coord_t y = p.top + (ui_h(p) - 2 * u) / 2; + static const int cells[4][2] = {{0, 0}, {1, 0}, {2, 0}, {1, 1}}; + for (int i = 0; i < 4; i++) + ui_rrect(px, + ui_rect(x + cells[i][0] * u + 1, y + cells[i][1] * u + 1, + u - 2, u - 2), + 2, i == 3 ? ui_alpha(fg, 0xb0) : fg); +} + +void app_tetris_open(void) +{ + game_open(&game_tetris_ops); +} + +bool apps_tetris_start(twin_screen_t *screen, + const char *name, + int x, + int y, + int w, + int h) +{ + (void) w, (void) h; /* the board has a fixed size */ + return game_start(&game_tetris_ops, screen, name, x, y); +} diff --git a/configs/Kconfig b/configs/Kconfig index 9bfd6ec3..a7c33e63 100644 --- a/configs/Kconfig +++ b/configs/Kconfig @@ -597,6 +597,38 @@ config DEMO_CALCULATOR Demonstrates a window drawn with paths and text, and mouse, touch and keyboard input. +config DEMO_SNAKE + bool "Build Snake demo" + default y + depends on DEMO_APPLICATIONS && FONT_MONTSERRAT && FONT_ICONS + help + Self-playing Snake. Snake, Pong and Tetris share one arcade view + from the phone's UI kit: on the phone they are launcher apps in the + phone theme, under any other window manager each opens a window, + paused until tapped. + +config DEMO_PONG + bool "Build Pong demo" + default y + depends on DEMO_APPLICATIONS && FONT_MONTSERRAT && FONT_ICONS + help + Self-playing Pong, in the same arcade view as Snake. + +config DEMO_TETRIS + bool "Build Tetris demo" + default y + depends on DEMO_APPLICATIONS && FONT_MONTSERRAT && FONT_ICONS + help + Self-playing Tetris, in the same arcade view as Snake. + +# Built when any game is: the arcade view they share +config DEMO_GAMES + def_bool DEMO_SNAKE || DEMO_PONG || DEMO_TETRIS + +# The UI kit the phone and the games draw with +config DEMO_UIKIT + def_bool DEMO_CELLPHONE || DEMO_GAMES + config DEMO_SPLINE bool "Build spline demo" default y diff --git a/tests/phone-games.c b/tests/phone-games.c index 85b5256a..54fdadc0 100644 --- a/tests/phone-games.c +++ b/tests/phone-games.c @@ -14,6 +14,8 @@ #include "phone-test.h" +#if defined(CONFIG_DEMO_GAMES) + int app_games_test_steps(void); int app_games_test_score(void); int app_games_test_best(void); @@ -99,34 +101,52 @@ static void play(const char *name, const char *shot) b.steps, a.score, b.score, a.events, b.events); } +/* Each game is picked on its own in Kconfig */ +static bool built(const char *name) +{ + return phone_app_find(name) >= 0; +} + void test_games(void) { - play("Snake", "games-snake"); - ph_wait(15000); - CHECK(app_games_test_events() > 0, "snake ate"); - CHECK(app_games_test_best() > 0, "snake scored"); - ph_home(); + if (built("Snake")) { + play("Snake", "games-snake"); + ph_wait(15000); + CHECK(app_games_test_events() > 0, "snake ate"); + CHECK(app_games_test_best() > 0, "snake scored"); + ph_home(); + } - play("Pong", "games-pong"); - ph_wait(5000); - CHECK(app_games_test_events() > 0, "pong paddles hit the ball"); - ph_home(); + if (built("Pong")) { + play("Pong", "games-pong"); + ph_wait(5000); + CHECK(app_games_test_events() > 0, "pong paddles hit the ball"); + ph_home(); + } + if (!built("Tetris")) + return; play("Tetris", "games-tetris"); ph_wait(30000); CHECK(app_games_test_events() > 10, "tetris placed pieces"); CHECK(app_games_test_best() > 0, "tetris cleared lines"); ph_shot("games-tetris-late"); - /* A covering view freezes the game; revealing it repaints from state */ - int steps = app_games_test_steps(); - ph_open("Snake"); - CHECK_EQ(phone_depth(), 2, "snake over tetris"); - phone_back(); - ph_wait(500); - CHECK_EQ(phone_depth(), 1, "back to tetris"); - CHECK(app_games_test_steps() >= steps, "tetris state kept"); - ph_shot("games-tetris-revealed"); + if (built("Snake")) { + /* A covering view freezes the game; revealing it repaints from + * state */ + int steps = app_games_test_steps(); + ph_open("Snake"); + CHECK_EQ(phone_depth(), 2, "snake over tetris"); + phone_back(); + ph_wait(500); + CHECK_EQ(phone_depth(), 1, "back to tetris"); + CHECK(app_games_test_steps() >= steps, "tetris state kept"); + ph_shot("games-tetris-revealed"); + } ph_home(); } +#else +void test_games(void) {} +#endif From d00b66544b58a8a092437b7a240549013f403b79 Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Tue, 20 Oct 2026 09:28:25 +0800 Subject: [PATCH 10/13] Sort polygon edges deterministically Paths of more than 48 edges are ordered with qsort, compared by top row alone. qsort need not be stable, and the order of edges starting on one row decides the coverage of coincident spans, so glibc and the BSD libc of macOS rasterized a few glyphs and icons differently. Ties now keep their place in the edge array, the order the insertion sort for short paths already gives, so a frame is the same on every host. --- src/poly.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/poly.c b/src/poly.c index dea4f2d2..59b94607 100644 --- a/src/poly.c +++ b/src/poly.c @@ -33,12 +33,18 @@ typedef struct _twin_edge { #define TWIN_POLY_CEIL(c) (((c) + (TWIN_POLY_STEP - 1)) & ~(TWIN_POLY_STEP - 1)) #define TWIN_POLY_COL(x) (((x) >> TWIN_POLY_FIXED_SHIFT) & TWIN_POLY_MASK) +/* By top, then by place in the edge array: qsort need not be stable, and + * the order of edges starting on one row decides coincident spans, so ties + * keep their original order as the insertion sort below does. Without it + * glibc and BSD libc rasterize some glyphs differently. */ static int _edge_compare_y(const void *a, const void *b) { const twin_edge_t *ae = *(twin_edge_t *const *) a; const twin_edge_t *be = *(twin_edge_t *const *) b; - return (int) (ae->top - be->top); + if (ae->top != be->top) + return ae->top < be->top ? -1 : 1; + return (ae > be) - (ae < be); } /* Order @order (pointers into the edge array) by top. Outlines arrive in From 94833f740e558b7ec3912ba2bc958ec2be080841 Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Tue, 20 Oct 2026 09:28:25 +0800 Subject: [PATCH 11/13] Refresh the cellphone golden frames The recorded frames carried a build fingerprint that no longer matched any configuration, so every cellphone comparison was skipped. Record them again for the default mobile build, with the shared calculator and the launcher list the phone now shows. With the edge sort fixed they hold on Linux and macOS alike. --- tests/golden/cellphone.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/golden/cellphone.txt b/tests/golden/cellphone.txt index 2e643c10..aff4fdcb 100644 --- a/tests/golden/cellphone.txt +++ b/tests/golden/cellphone.txt @@ -1,9 +1,9 @@ # Golden frame checksums for the cellphone suite. # Regenerate with: make check-update -# config 6969dfe0 +# config 1cdbad9f lock 4c07fb5b home c7c79dd3 -calculator de05315a +calculator 61636526 dialer-empty 05eab18b dialer-number a5813994 dialer-long 511ed576 @@ -51,10 +51,10 @@ settings-display-dark a9e82e99 settings-home-dark fa8cca66 settings-sound-changed b5a3df17 settings-wallpaper-dusk f9b149d2 -games-paused 71d75a7d +games-paused a0b87d1e games-snake 8dfbdbe9 games-pong 117eba72 games-tetris c8e30d17 games-tetris-late 0e8303fa games-tetris-revealed b458380d -home-demos 93c579a9 +home-demos 8ee55c71 From dc38d8d40b3110d6fafbbe98f9cf9e5fd8196c92 Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Tue, 20 Oct 2026 15:15:59 +0800 Subject: [PATCH 12/13] Freeze time for the headless determinism check CI compares two fresh demo-headless runs captured about 3 s in and allowed 100 differing pixels, yet the Animation demo's GIF showed through a gap between windows and its frame depended on when each run started, so the check failed about one run in three. MADO_FROZEN_TIME=1 now stops twin's clock at startup in the headless backend: no timeout comes due, so every animation keeps its first frame, while the control tool is still served by the poll loop. The Clock reads wall time on its own and shows a fixed time under the same variable. The CI step sets it and requires identical frames. --- .github/workflows/headless-test.yml | 8 +++++--- apps/clock.c | 15 ++++++++++++--- backend/headless.c | 14 ++++++++++++++ 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/.github/workflows/headless-test.yml b/.github/workflows/headless-test.yml index 89ccfcf1..088cc8cf 100644 --- a/.github/workflows/headless-test.yml +++ b/.github/workflows/headless-test.yml @@ -80,6 +80,8 @@ jobs: echo "Screenshot verification passed" - name: Test deterministic rendering + env: + MADO_FROZEN_TIME: "1" run: | # Run twice and compare outputs for determinism echo "First run..." @@ -106,7 +108,7 @@ jobs: fi done - # Compare images (allowing minor differences) + # Compare images exactly # compare writes the metric to stderr, stdout gets the diff image DIFF=$(compare -metric AE run1.png run2.png null: 2>&1) || true # Extract just the numeric value (AE metric is an integer) @@ -117,8 +119,8 @@ jobs: fi echo "Pixel difference: $DIFF" - if [ "$DIFF" -gt "100" ]; then - echo "ERROR: Renderings differ by more than 100 pixels" + if [ "$DIFF" -ne "0" ]; then + echo "ERROR: Renderings differ" exit 1 fi echo "Deterministic rendering test passed" diff --git a/apps/clock.c b/apps/clock.c index 17b2fc16..e4d82277 100644 --- a/apps/clock.c +++ b/apps/clock.c @@ -6,6 +6,7 @@ #include #include +#include #include #include @@ -37,6 +38,7 @@ static inline twin_pixmap_t *_apps_clock_pixmap(twin_custom_widget_t *clock) typedef struct { twin_timeout_t *timeout; + bool frozen; /* MADO_FROZEN_TIME=1: show a fixed time, for screenshots */ } apps_clock_data_t; static void apps_clock_set_transform(twin_custom_widget_t *clock, @@ -175,9 +177,14 @@ static void _apps_clock_paint(twin_custom_widget_t *clock) twin_angle_t second_angle, minute_angle, hour_angle; struct tm t; - gettimeofday(&tv, NULL); - - localtime_r(&tv.tv_sec, &t); + const apps_clock_data_t *data = twin_custom_widget_data(clock); + if (data->frozen) { + tv = (struct timeval) {0}; + gmtime_r(&tv.tv_sec, &t); + } else { + gettimeofday(&tv, NULL); + localtime_r(&tv.tv_sec, &t); + } _apps_clock_face(clock); @@ -234,6 +241,8 @@ static twin_custom_widget_t *_apps_clock_init(twin_box_t *parent) apps_clock_data_t *data = (apps_clock_data_t *) twin_custom_widget_data(custom); + const char *frozen = getenv("MADO_FROZEN_TIME"); + data->frozen = frozen && !strcmp(frozen, "1"); data->timeout = twin_set_timeout(_apps_clock_timeout, _apps_clock_interval(), custom); diff --git a/backend/headless.c b/backend/headless.c index 96a3c21c..65a9e89b 100644 --- a/backend/headless.c +++ b/backend/headless.c @@ -329,8 +329,22 @@ static bool _twin_headless_shm_live(void) return live; } +/* MADO_FROZEN_TIME=1 stops twin's clock at startup: no timeout comes due, + * so every animation keeps its first frame and screenshots repeat exactly. + * The control tool still gets served, as polling does not use timeouts. + * Anything timed reads the same instant, so two clicks always count as a + * double-click (a title bar maximizes): meant for screenshots, not input. */ +static twin_time_t _twin_headless_frozen_now(void) +{ + return 0; +} + twin_context_t *twin_headless_init(int width, int height) { + const char *frozen = getenv("MADO_FROZEN_TIME"); + if (frozen && !strcmp(frozen, "1")) + _twin_set_time_source(_twin_headless_frozen_now); + twin_context_t *ctx = calloc(1, sizeof(twin_context_t)); if (!ctx) return NULL; From 875a4a69202eecc6685d65140df33b5de181f7d8 Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Tue, 20 Oct 2026 23:17:35 +0800 Subject: [PATCH 13/13] Pick each application on its own in Kconfig "Build demo applications" gated every application behind one switch, and the menu still called them demos although they are the applications Mado ships. The menu is now "Applications": each one is chosen on its own, and APPS, true when any is picked, builds the application library, the launcher and its tests. The phone shell is a menuconfig holding its own options and apps, the games have their own group, and every help text says what the application does. The DEMO_* symbols become APP_*. A configuration written before this change no longer sets them, so its applications fall back to the defaults until they are picked again. --- .github/workflows/headless-test.yml | 2 +- .github/workflows/main.yml | 2 +- Makefile | 52 +++---- README.md | 4 +- apps/apps.c | 22 +-- apps/apps.h | 8 +- apps/calc.c | 4 +- apps/cellphone/phone.h | 2 +- apps/cellphone/shell.c | 22 +-- configs/Kconfig | 228 ++++++++++++++-------------- configs/defconfig | 11 +- docs/phone.md | 6 +- tests/harness.c | 2 +- tests/phone-contacts.c | 2 +- tests/phone-dialer.c | 4 +- tests/phone-games.c | 2 +- tests/phone-music.c | 2 +- tests/phone-photos.c | 4 +- tests/phone-sms.c | 2 +- tests/test-cellphone.c | 2 +- 20 files changed, 188 insertions(+), 195 deletions(-) diff --git a/.github/workflows/headless-test.yml b/.github/workflows/headless-test.yml index 088cc8cf..3d5fb99a 100644 --- a/.github/workflows/headless-test.yml +++ b/.github/workflows/headless-test.yml @@ -21,7 +21,7 @@ jobs: BACKEND_HEADLESS=y \ TOOLS=y \ TOOL_HEADLESS_CTL=y \ - DEMO_MULTI=y + APP_MULTI=y - name: Build with headless backend run: | diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9fa52dd1..55d4722b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -122,7 +122,7 @@ jobs: - name: no widget toolkit options: WM_MOBILE=y WIDGETS=n - name: RGB565 phone surfaces - options: WM_MOBILE=y DEMO_CELLPHONE_RGB16=y + options: WM_MOBILE=y APP_CELLPHONE_RGB16=y - name: TLSF heap options: WM_MOBILE=y MEM_TLSF=y MEM_TLSF_POOL_SIZE=4194304 - name: Pixman renderer diff --git a/Makefile b/Makefile index 88ea367c..ccd4343f 100644 --- a/Makefile +++ b/Makefile @@ -33,7 +33,7 @@ target.o-y := TARGET_LIBS := target.a-y := -target.a-$(CONFIG_DEMO_APPLICATIONS) += libapps.a +target.a-$(CONFIG_APPS) += libapps.a target.a-y += libtwin.a # All translation units must share the arithmetic policy and target ABI. @@ -166,36 +166,36 @@ endif # Applications libapps.a_files-y := apps/dummy.c -libapps.a_files-$(CONFIG_DEMO_APPLICATIONS) += apps/apps.c -libapps.a_files-$(CONFIG_DEMO_MULTI) += apps/multi.c -libapps.a_files-$(CONFIG_DEMO_CLOCK) += apps/clock.c -libapps.a_files-$(CONFIG_DEMO_CALCULATOR) += apps/calc.c -libapps.a_files-$(CONFIG_DEMO_SPLINE) += apps/spline.c -libapps.a_files-$(CONFIG_DEMO_ANIMATION) += apps/animation.c -libapps.a_files-$(CONFIG_DEMO_IMAGE) += apps/image.c -libapps.a_files-$(CONFIG_DEMO_SNAKE) += apps/snake.c -libapps.a_files-$(CONFIG_DEMO_PONG) += apps/pong.c -libapps.a_files-$(CONFIG_DEMO_TETRIS) += apps/tetris.c -libapps.a_files-$(CONFIG_DEMO_GAMES) += apps/games.c - -libapps.a_files-$(CONFIG_DEMO_UIKIT) += \ +libapps.a_files-$(CONFIG_APPS) += apps/apps.c +libapps.a_files-$(CONFIG_APP_MULTI) += apps/multi.c +libapps.a_files-$(CONFIG_APP_CLOCK) += apps/clock.c +libapps.a_files-$(CONFIG_APP_CALCULATOR) += apps/calc.c +libapps.a_files-$(CONFIG_APP_SPLINE) += apps/spline.c +libapps.a_files-$(CONFIG_APP_ANIMATION) += apps/animation.c +libapps.a_files-$(CONFIG_APP_IMAGE) += apps/image.c +libapps.a_files-$(CONFIG_APP_SNAKE) += apps/snake.c +libapps.a_files-$(CONFIG_APP_PONG) += apps/pong.c +libapps.a_files-$(CONFIG_APP_TETRIS) += apps/tetris.c +libapps.a_files-$(CONFIG_APP_GAMES) += apps/games.c + +libapps.a_files-$(CONFIG_APP_UIKIT) += \ apps/cellphone/ui.c \ apps/cellphone/ui-paint.c \ apps/cellphone/ui-input.c -libapps.a_files-$(CONFIG_DEMO_CELLPHONE) += \ +libapps.a_files-$(CONFIG_APP_CELLPHONE) += \ apps/cellphone/shell.c \ apps/cellphone/home.c \ apps/cellphone/lock.c \ apps/cellphone/data.c \ apps/cellphone/settings.c -libapps.a_files-$(CONFIG_DEMO_PHONE_DIALER) += apps/cellphone/dialer.c -libapps.a_files-$(CONFIG_DEMO_PHONE_CALLLOG) += apps/cellphone/calllog.c -libapps.a_files-$(CONFIG_DEMO_PHONE_MESSAGES) += apps/cellphone/sms.c -libapps.a_files-$(CONFIG_DEMO_PHONE_CONTACTS) += apps/cellphone/contacts.c -libapps.a_files-$(CONFIG_DEMO_PHONE_MUSIC) += apps/cellphone/music.c -libapps.a_files-$(CONFIG_DEMO_PHONE_PHOTOS) += apps/cellphone/photos.c -libapps.a_files-$(CONFIG_DEMO_PHONE_CAMERA) += apps/cellphone/camera.c +libapps.a_files-$(CONFIG_APP_PHONE_DIALER) += apps/cellphone/dialer.c +libapps.a_files-$(CONFIG_APP_PHONE_CALLLOG) += apps/cellphone/calllog.c +libapps.a_files-$(CONFIG_APP_PHONE_MESSAGES) += apps/cellphone/sms.c +libapps.a_files-$(CONFIG_APP_PHONE_CONTACTS) += apps/cellphone/contacts.c +libapps.a_files-$(CONFIG_APP_PHONE_MUSIC) += apps/cellphone/music.c +libapps.a_files-$(CONFIG_APP_PHONE_PHOTOS) += apps/cellphone/photos.c +libapps.a_files-$(CONFIG_APP_PHONE_CAMERA) += apps/cellphone/camera.c libapps.a_includes-y := include apps apps/cellphone # Emscripten size optimization @@ -260,7 +260,7 @@ endif # Standalone application -ifeq ($(CONFIG_DEMO_APPLICATIONS), y) +ifeq ($(CONFIG_APPS), y) target-y += demo-$(BACKEND) demo-$(BACKEND)_depends-y += $(target.a-y) demo-$(BACKEND)_files-y = apps/main.c @@ -301,7 +301,7 @@ demo-$(BACKEND)_ldflags-y += \ -flto endif endif -endif # CONFIG_DEMO_APPLICATIONS +endif # CONFIG_APPS # Font editor tool # Tools should not be built for WebAssembly @@ -333,10 +333,10 @@ endif # Headless tests: built only for 'make check' / 'make check-update' # (and listed for 'make clean' so their binaries are removed) TESTS := test-core test-wm test-lifetime -ifeq ($(CONFIG_DEMO_APPLICATIONS), y) +ifeq ($(CONFIG_APPS), y) TESTS += test-demos endif -ifeq ($(CONFIG_DEMO_CELLPHONE), y) +ifeq ($(CONFIG_APP_CELLPHONE), y) TESTS += test-cellphone endif diff --git a/README.md b/README.md index 5b7de9ee..802222c8 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,7 @@ $ ./demo-sdl ``` The default build uses the desktop window manager: overlapping, decorated -windows running the demo apps. The window manager is a build-time +windows running the applications. The window manager is a build-time choice, and the desktop and mobile policies are mutually exclusive. Build the phone instead, a lock screen (drag the knob to unlock), a home screen and a dozen apps with vector-drawn interfaces (Photos shows JPEG @@ -142,7 +142,7 @@ $ ./demo-sdl # phone; drag from the left edge to go back `WM_NONE=y` builds with no window manager at all, showing a single app. The window manager is fixed for the life of the build: nothing switches -or checks it at run time. The demo apps in `apps/apps.c` run under all +or checks it at run time. The applications in `apps/apps.c` run under all three; the phone shell depends on `WM_MOBILE` and is built only there. See [docs/phone.md](docs/phone.md) for the design of the window manager, compositor and caches. diff --git a/apps/apps.c b/apps/apps.c index cbc88968..7afe3f29 100644 --- a/apps/apps.c +++ b/apps/apps.c @@ -6,47 +6,47 @@ #include "apps.h" -#if defined(CONFIG_DEMO_CELLPHONE) +#if defined(CONFIG_APP_CELLPHONE) #include "phone.h" #endif /* Every app below the phone runs under all three window managers */ static const apps_entry_t apps[] = { -#if defined(CONFIG_DEMO_CELLPHONE) && defined(CONFIG_WM_MOBILE) +#if defined(CONFIG_APP_CELLPHONE) && defined(CONFIG_WM_MOBILE) /* Mobile only: the phone shell lists the other apps on its home * screen */ {"Phone", TWIN_SYM_MOBILE, 0, 0, PHONE_W, PHONE_H, NULL, phone_boot, phone_shutdown}, #endif -#if defined(CONFIG_DEMO_MULTI) +#if defined(CONFIG_APP_MULTI) {"Demo", TWIN_SYM_LIST, 100, 100, 400, 400, apps_multi_start, NULL, NULL}, #endif -#if defined(CONFIG_DEMO_CLOCK) +#if defined(CONFIG_APP_CLOCK) {"Clock", TWIN_SYM_CLOCK, 10, 10, 200, 200, apps_clock_start, NULL, NULL}, #endif -#if defined(CONFIG_DEMO_CALCULATOR) +#if defined(CONFIG_APP_CALCULATOR) {"Calculator", TWIN_SYM_CALCULATOR, 100, 100, 220, 300, apps_calc_start, NULL, NULL}, #endif -#if defined(CONFIG_DEMO_SPLINE) +#if defined(CONFIG_APP_SPLINE) {"Spline", TWIN_SYM_SHUFFLE, 20, 20, 400, 400, apps_spline_start, NULL, NULL}, #endif -#if defined(CONFIG_DEMO_ANIMATION) +#if defined(CONFIG_APP_ANIMATION) {"Animation", TWIN_SYM_PLAY, 20, 20, 0, 0, apps_animation_start, NULL, NULL}, #endif -#if defined(CONFIG_DEMO_IMAGE) +#if defined(CONFIG_APP_IMAGE) {"Vectors", TWIN_SYM_GLOBE, 20, 20, 0, 0, apps_image_start, NULL, NULL}, #endif /* Kit views in plain windows; the phone lists its own, themed rows */ -#if defined(CONFIG_DEMO_SNAKE) +#if defined(CONFIG_APP_SNAKE) {"Snake", TWIN_SYM_PLAY, 60, 40, 0, 0, apps_snake_start, NULL, NULL}, #endif -#if defined(CONFIG_DEMO_PONG) +#if defined(CONFIG_APP_PONG) {"Pong", TWIN_SYM_PLAY, 90, 60, 0, 0, apps_pong_start, NULL, NULL}, #endif -#if defined(CONFIG_DEMO_TETRIS) +#if defined(CONFIG_APP_TETRIS) {"Tetris", TWIN_SYM_PLAY, 120, 80, 0, 0, apps_tetris_start, NULL, NULL}, #endif {0}, /* keeps the table valid when nothing is built */ diff --git a/apps/apps.h b/apps/apps.h index 74b13615..e5b0d05d 100644 --- a/apps/apps.h +++ b/apps/apps.h @@ -5,13 +5,13 @@ */ /* - * Registry of the demo applications that are built. The demo launcher - * (main.c), the phone's home screen and the tests all read it, so a new app - * is listed everywhere by adding one entry. + * Registry of the applications that are built. The launcher (main.c), the + * phone's home screen and the tests all read it, so a new app is listed + * everywhere by adding one entry. * * Which window managers an app runs under is settled at build time, like * the window manager itself: an app runs under all of them unless its - * Kconfig option depends on one (DEMO_CELLPHONE depends on WM_MOBILE), and + * Kconfig option depends on one (APP_CELLPHONE depends on WM_MOBILE), and * its entry is compiled in only when it can run. */ diff --git a/apps/calc.c b/apps/calc.c index ed9f5526..312e42f9 100644 --- a/apps/calc.c +++ b/apps/calc.c @@ -19,7 +19,7 @@ #include "apps.h" -#if defined(CONFIG_DEMO_CELLPHONE) +#if defined(CONFIG_APP_CELLPHONE) #include "phone.h" #endif @@ -671,7 +671,7 @@ bool apps_calc_start(twin_screen_t *screen, * Phone view: the same calculator in the UI kit's theme */ -#if defined(CONFIG_DEMO_CELLPHONE) +#if defined(CONFIG_APP_CELLPHONE) typedef struct { ui_view_t view; calc_t calc; diff --git a/apps/cellphone/phone.h b/apps/cellphone/phone.h index 4ef26efd..e843356d 100644 --- a/apps/cellphone/phone.h +++ b/apps/cellphone/phone.h @@ -69,7 +69,7 @@ */ /* Pixel format of every phone surface */ -#if defined(CONFIG_DEMO_CELLPHONE_RGB16) +#if defined(CONFIG_APP_CELLPHONE_RGB16) #define PHONE_FORMAT TWIN_RGB16 #else #define PHONE_FORMAT TWIN_ARGB32 diff --git a/apps/cellphone/shell.c b/apps/cellphone/shell.c index ef6291d0..78c52306 100644 --- a/apps/cellphone/shell.c +++ b/apps/cellphone/shell.c @@ -172,49 +172,49 @@ void phone_format_date(char *buf, size_t len) /* The apps picked in Kconfig, in launcher order */ static const phone_app_t apps[] = { -#if defined(CONFIG_DEMO_PHONE_DIALER) +#if defined(CONFIG_APP_PHONE_DIALER) {"Phone", TWIN_SYM_PHONE, NULL, UI_RGB(0x5b8c45), app_dialer_open, NULL, NULL}, #endif -#if defined(CONFIG_DEMO_PHONE_CONTACTS) +#if defined(CONFIG_APP_PHONE_CONTACTS) {"Contacts", TWIN_SYM_CONTACTS, NULL, UI_RGB(0x5a7896), app_contacts_open, NULL, NULL}, #endif -#if defined(CONFIG_DEMO_PHONE_MESSAGES) +#if defined(CONFIG_APP_PHONE_MESSAGES) {"Messages", TWIN_SYM_MESSAGE, NULL, UI_RGB(0x4f8c7c), app_sms_open, NULL, NULL}, #endif -#if defined(CONFIG_DEMO_CALCULATOR) /* the shared calculator, phone themed */ +#if defined(CONFIG_APP_CALCULATOR) /* the shared calculator, phone themed */ {"Calculator", TWIN_SYM_CALCULATOR, NULL, UI_RGB(0xc08a3e), app_calc_open, NULL, apps_calc_start}, #endif -#if defined(CONFIG_DEMO_PHONE_MUSIC) +#if defined(CONFIG_APP_PHONE_MUSIC) {"Music", TWIN_SYM_MUSIC, NULL, UI_RGB(0xa95c72), app_music_open, NULL, NULL}, #endif -#if defined(CONFIG_DEMO_PHONE_PHOTOS) +#if defined(CONFIG_APP_PHONE_PHOTOS) {"Photos", TWIN_SYM_IMAGE, NULL, UI_RGB(0xbb6e3e), app_photos_open, NULL, NULL}, #endif -#if defined(CONFIG_DEMO_PHONE_CAMERA) +#if defined(CONFIG_APP_PHONE_CAMERA) {"Camera", TWIN_SYM_CAMERA, NULL, UI_RGB(0x6c7064), app_camera_open, NULL, NULL}, #endif {"Settings", TWIN_SYM_SETTINGS, NULL, UI_RGB(0x767b6c), app_settings_open, NULL, NULL}, -#if defined(CONFIG_DEMO_PHONE_CALLLOG) +#if defined(CONFIG_APP_PHONE_CALLLOG) {"Call Log", TWIN_SYM_HISTORY, NULL, UI_RGB(0x7a6a96), app_calllog_open, NULL, NULL}, #endif -#if defined(CONFIG_DEMO_SNAKE) +#if defined(CONFIG_APP_SNAKE) {"Snake", NULL, app_snake_glyph, UI_RGB(0x4d8f86), app_snake_open, NULL, apps_snake_start}, #endif -#if defined(CONFIG_DEMO_PONG) +#if defined(CONFIG_APP_PONG) {"Pong", NULL, app_pong_glyph, UI_RGB(0x646c9a), app_pong_open, NULL, apps_pong_start}, #endif -#if defined(CONFIG_DEMO_TETRIS) +#if defined(CONFIG_APP_TETRIS) {"Tetris", NULL, app_tetris_glyph, UI_RGB(0xad5348), app_tetris_open, NULL, apps_tetris_start}, #endif diff --git a/configs/Kconfig b/configs/Kconfig index a7c33e63..5de75d07 100644 --- a/configs/Kconfig +++ b/configs/Kconfig @@ -188,7 +188,7 @@ config WM_NONE help Minimal, no window manager: no decorations, every application window covers the screen, and the keyboard goes to the newest or the last pressed - window. The demo launcher starts only the first demo. + window. The launcher starts only the first application. endchoice @@ -367,7 +367,7 @@ config MEM_TLSF_POOL_SIZE Size of the static memory pool backing the TLSF allocator. Must be large enough for all runtime allocations including pixmap buffers. A 640x480 ARGB32 screen pixmap alone needs - ~1.2 MB; the SDL demo with multiple windows peaks at ~8 MB. + ~1.2 MB; the SDL build running every application peaks at ~8 MB. Embedded targets with small displays need far less (e.g. 320x240 RGB565 = ~150 KB framebuffer + widget overhead). Requests exceeding pool capacity return NULL. @@ -421,8 +421,8 @@ config WIDGETS default y help Boxes, labels, buttons and toplevel windows with automatic layout, - used by the desktop demos. Applications that draw their own UI - (such as the cellphone demo) do not need it. + used by most desktop applications. Applications that draw their + own UI (the calculator, the games and the phone) do not need it. config FONT_MONTSERRAT bool "Montserrat Medium font" @@ -487,176 +487,170 @@ config TVG_MEMORY_BUDGET endmenu -menu "Demo Applications" +menu "Applications" -config DEMO_APPLICATIONS - bool "Build demo applications" +comment "Pick any set: the launcher is built when at least one is chosen" + +config APP_CALCULATOR + bool "Calculator" default y help - Build demonstration applications showcasing Mado features. - Provides example code and testing targets. + Four-function calculator with decimal fixed-point arithmetic and an + erase key, driven by mouse, touch or keyboard. One source serves + a desktop window and, with the phone, its Calculator app drawn in + the phone theme. -config DEMO_CELLPHONE - bool "Build cellphone demo" +config APP_CLOCK + bool "Clock" default y - depends on DEMO_APPLICATIONS && FONT_MONTSERRAT && FONT_ICONS - depends on WM_MOBILE + depends on WIDGETS + help + Analog clock that redraws once a second, its hands placed with + fixed-point trigonometry. + +config APP_SPLINE + bool "Spline editor" + default y + depends on WIDGETS + help + Bezier curve with draggable control points, cubic or quadratic at + the press of a button, stroked by the path renderer as it moves. + +config APP_ANIMATION + bool "Animation player" + default y + depends on WIDGETS + help + Plays an animated GIF, decoding each frame on demand and advancing + on timeouts. + +config APP_IMAGE + bool "Vector image viewer" + default y + depends on WIDGETS + select LOADER_TVG + help + TinyVG drawings rendered to fit the window at any size; a button + steps through them. Selects the TinyVG loader. + +config APP_MULTI + bool "Window showcase" + default y + depends on WIDGETS && WM_DESKTOP + help + Six overlapping windows on the desktop window manager: text on a + circle, draggable thick lines, a smiley, the glyph table, a flower + of elliptical arcs and a blurred picture. + +comment "Games" + +config APP_SNAKE + bool "Snake" + default y + depends on FONT_MONTSERRAT && FONT_ICONS + help + Self-playing Snake. Snake, Pong and Tetris share one arcade view + from the phone's UI kit: on the phone they are launcher apps in the + phone theme; under any other window manager each opens a window, + paused until tapped. + +config APP_PONG + bool "Pong" + default y + depends on FONT_MONTSERRAT && FONT_ICONS + help + Self-playing Pong, in the same arcade view as Snake. + +config APP_TETRIS + bool "Tetris" + default y + depends on FONT_MONTSERRAT && FONT_ICONS + help + Self-playing Tetris, in the same arcade view as Snake. + +comment "Phone" + +menuconfig APP_CELLPHONE + bool "Phone shell" + default y + depends on WM_MOBILE && FONT_MONTSERRAT && FONT_ICONS help Phone user interface on the mobile window manager, which it needs: lock screen, home screen and settings, plus the phone apps picked - below. The other demos selected in this menu get launcher icons - too and run as stacked apps; Back closes their windows one at a - time. A demo the phone also offers natively, such as the - calculator, appears once, drawn in the phone theme. + in this submenu. The other applications selected above get + launcher icons too and run as stacked windows; Back closes them one + at a time. An application the phone also offers natively, such as + the calculator, appears once, drawn in the phone theme. + +if APP_CELLPHONE -config DEMO_CELLPHONE_RGB16 +config APP_CELLPHONE_RGB16 bool "Use RGB565 surfaces for phone screens" default n - depends on DEMO_CELLPHONE help Halve the memory of every phone screen by rendering into 16-bit surfaces. RGB565 has no alpha channel, so screens are always opaque, at the cost of some banding in gradients. -menu "Phone apps" - depends on DEMO_CELLPHONE - -config DEMO_PHONE_DIALER +config APP_PHONE_DIALER bool "Phone" default y help Dialer keypad and the in-call screen. -config DEMO_PHONE_CALLLOG +config APP_PHONE_CALLLOG bool "Call log" default y - depends on DEMO_PHONE_DIALER + depends on APP_PHONE_DIALER help Recent calls; tapping one calls it back through the dialer. -config DEMO_PHONE_MESSAGES +config APP_PHONE_MESSAGES bool "Messages" default y help Text message conversation with an on-screen keyboard. -config DEMO_PHONE_CONTACTS +config APP_PHONE_CONTACTS bool "Contacts" default y - depends on DEMO_PHONE_DIALER && DEMO_PHONE_MESSAGES + depends on APP_PHONE_DIALER && APP_PHONE_MESSAGES help Contact list, details and an add form. A contact can be called or messaged, so this needs the dialer and messages. -config DEMO_PHONE_MUSIC +config APP_PHONE_MUSIC bool "Music" default y help Track list and a now-playing screen. -config DEMO_PHONE_PHOTOS +config APP_PHONE_PHOTOS bool "Photos" default y help Thumbnail grid and a swipeable photo viewer. -config DEMO_PHONE_CAMERA +config APP_PHONE_CAMERA bool "Camera" default y help Viewfinder with shutter, flash and mode controls. -endmenu - -config DEMO_MULTI - bool "Build multi demo" - default y - depends on DEMO_APPLICATIONS && WIDGETS && WM_DESKTOP - help - Comprehensive demo with multiple windows and widgets. - Demonstrates window management and event handling on the desktop - policy, which staggers its overlapping windows. - -config DEMO_CLOCK - bool "Build clock demo" - default y - depends on DEMO_APPLICATIONS && WIDGETS - help - Analog clock demonstration with real-time updates. - Showcases CORDIC trigonometry functions. - Demonstrates timeout callbacks and redraw logic. - -config DEMO_CALCULATOR - bool "Build calculator demo" - default y - depends on DEMO_APPLICATIONS - help - Four-function calculator with decimal fixed-point arithmetic. - One source serves the desktop window and, with the cellphone demo, - the phone's Calculator app in the current phone theme. - Demonstrates a window drawn with paths and text, and mouse, touch - and keyboard input. - -config DEMO_SNAKE - bool "Build Snake demo" - default y - depends on DEMO_APPLICATIONS && FONT_MONTSERRAT && FONT_ICONS - help - Self-playing Snake. Snake, Pong and Tetris share one arcade view - from the phone's UI kit: on the phone they are launcher apps in the - phone theme, under any other window manager each opens a window, - paused until tapped. - -config DEMO_PONG - bool "Build Pong demo" - default y - depends on DEMO_APPLICATIONS && FONT_MONTSERRAT && FONT_ICONS - help - Self-playing Pong, in the same arcade view as Snake. - -config DEMO_TETRIS - bool "Build Tetris demo" - default y - depends on DEMO_APPLICATIONS && FONT_MONTSERRAT && FONT_ICONS - help - Self-playing Tetris, in the same arcade view as Snake. +endif # Built when any game is: the arcade view they share -config DEMO_GAMES - def_bool DEMO_SNAKE || DEMO_PONG || DEMO_TETRIS +config APP_GAMES + def_bool APP_SNAKE || APP_PONG || APP_TETRIS # The UI kit the phone and the games draw with -config DEMO_UIKIT - def_bool DEMO_CELLPHONE || DEMO_GAMES +config APP_UIKIT + def_bool APP_CELLPHONE || APP_GAMES -config DEMO_SPLINE - bool "Build spline demo" - default y - depends on DEMO_APPLICATIONS && WIDGETS - help - Bezier spline curve demonstration. - Shows advanced path rendering capabilities. - Demonstrates curve smoothing algorithms. - -config DEMO_ANIMATION - bool "Build animation demo" - default y - depends on DEMO_APPLICATIONS && WIDGETS - help - Animation demonstration with moving objects. - Shows frame-based animation techniques. - Demonstrates work queue and timeout systems. - -config DEMO_IMAGE - bool "Build scalable image demo" - select LOADER_TVG - default y - depends on DEMO_APPLICATIONS && WIDGETS - help - Scalable vector graphics demonstration. - Shows TinyVG format rendering capabilities. - Automatically enables TVG loader. - Demonstrates resolution-independent graphics. +# The launcher and the application library, when anything is picked +config APPS + def_bool APP_CALCULATOR || APP_CLOCK || APP_SPLINE || APP_ANIMATION || \ + APP_IMAGE || APP_MULTI || APP_GAMES || APP_CELLPHONE endmenu diff --git a/configs/defconfig b/configs/defconfig index c344b394..9cf13379 100644 --- a/configs/defconfig +++ b/configs/defconfig @@ -2,10 +2,9 @@ CONFIG_BACKEND_SDL=y CONFIG_LOADER_PNG=y CONFIG_LOADER_JPEG=y CONFIG_LOADER_GIF=y -CONFIG_DEMO_APPLICATIONS=y -CONFIG_DEMO_MULTI=y -CONFIG_DEMO_CLOCK=y -CONFIG_DEMO_CALCULATOR=y -CONFIG_DEMO_SPLINE=y -CONFIG_DEMO_ANIMATION=y +CONFIG_APP_MULTI=y +CONFIG_APP_CLOCK=y +CONFIG_APP_CALCULATOR=y +CONFIG_APP_SPLINE=y +CONFIG_APP_ANIMATION=y CONFIG_MEMORY_STATS=y diff --git a/docs/phone.md b/docs/phone.md index e45aff85..f9735332 100644 --- a/docs/phone.md +++ b/docs/phone.md @@ -2,7 +2,7 @@ This document describes how Mado runs a phone user interface or a classic desktop from the same core, and why the core is shaped the way it is. The -cellphone demo under `apps/cellphone/` is the reference workload: a lock +phone shell under `apps/cellphone/` is the reference workload: a lock screen, a home screen and twelve apps on a 240x320 display, drawn with anti-aliased vector graphics (plus JPEG photo fixtures in Photos) on a CPU without graphics acceleration. @@ -44,7 +44,7 @@ geometry: `TwinWindowPlain` (placed exactly as requested, never decorated), `TwinWindowApplication` (a managed app window) and `TwinWindowFullscreen` (above everything, e.g. a lock screen). The same app code therefore runs decorated on a desktop, stacked on a phone, or full screen with no window -manager at all. The demo apps are the proof. `apps/apps.c` registers +manager at all. The applications are the proof. `apps/apps.c` registers every app that is built, and which policies an app runs under is settled at build time too: ordinary apps run under all three, while the phone shell, which owns the screen, depends on `WM_MOBILE` in Kconfig and is @@ -123,7 +123,7 @@ is repainted from application state when revealed. An opaque full-screen window (the lock screen) likewise releases the app and the bars under it. Steady-state memory is one work-area surface plus the bars, independent of how many screens are stacked. Phone screens can also render into RGB565 -surfaces (`DEMO_CELLPHONE_RGB16`) to halve that again. +surfaces (`APP_CELLPHONE_RGB16`) to halve that again. Heap measured by the `test-cellphone` budget gate: everything allocated through `twin_malloc` except the bounded reuse pools (the 24 KiB coverage diff --git a/tests/harness.c b/tests/harness.c index 13990f94..ca1ca189 100644 --- a/tests/harness.c +++ b/tests/harness.c @@ -380,7 +380,7 @@ static uint32_t config_fingerprint(void) #else 0, #endif -#if defined(CONFIG_DEMO_CELLPHONE_RGB16) +#if defined(CONFIG_APP_CELLPHONE_RGB16) 1, #else 0, diff --git a/tests/phone-contacts.c b/tests/phone-contacts.c index 0c856562..9c293dff 100644 --- a/tests/phone-contacts.c +++ b/tests/phone-contacts.c @@ -14,7 +14,7 @@ #include "data.h" #include "phone-test.h" -#if defined(CONFIG_DEMO_PHONE_CONTACTS) +#if defined(CONFIG_APP_PHONE_CONTACTS) int app_contacts_test_offset(void); const char *app_contacts_test_detail(void); diff --git a/tests/phone-dialer.c b/tests/phone-dialer.c index f77b5343..918ea593 100644 --- a/tests/phone-dialer.c +++ b/tests/phone-dialer.c @@ -10,7 +10,7 @@ #include "phone-test.h" -#if defined(CONFIG_DEMO_PHONE_DIALER) +#if defined(CONFIG_APP_PHONE_DIALER) const char *app_dialer_test_number(void); const char *app_dialer_test_name(void); @@ -139,7 +139,7 @@ void test_dialer(void) ph_home(); } -#if defined(CONFIG_DEMO_PHONE_CALLLOG) +#if defined(CONFIG_APP_PHONE_CALLLOG) void test_calllog(void) { ph_open("Call Log"); diff --git a/tests/phone-games.c b/tests/phone-games.c index 54fdadc0..02170240 100644 --- a/tests/phone-games.c +++ b/tests/phone-games.c @@ -14,7 +14,7 @@ #include "phone-test.h" -#if defined(CONFIG_DEMO_GAMES) +#if defined(CONFIG_APP_GAMES) int app_games_test_steps(void); int app_games_test_score(void); diff --git a/tests/phone-music.c b/tests/phone-music.c index a72fe9ae..f8f11ecf 100644 --- a/tests/phone-music.c +++ b/tests/phone-music.c @@ -14,7 +14,7 @@ #include "data.h" #include "phone-test.h" -#if defined(CONFIG_DEMO_PHONE_MUSIC) +#if defined(CONFIG_APP_PHONE_MUSIC) int app_music_test_elapsed(void); int app_music_test_track(void); diff --git a/tests/phone-photos.c b/tests/phone-photos.c index 2e323453..ee1f3e89 100644 --- a/tests/phone-photos.c +++ b/tests/phone-photos.c @@ -23,7 +23,7 @@ int app_camera_test_flash(void); #define THUMB_X(i) (5 + ((i) % 3) * 78 + 37) #define THUMB_Y(i) (56 + ((i) / 3) * 78 + 30) -#if defined(CONFIG_DEMO_PHONE_PHOTOS) +#if defined(CONFIG_APP_PHONE_PHOTOS) void test_photos(void) { ph_open("Photos"); @@ -117,7 +117,7 @@ void test_photos(void) void test_photos(void) {} #endif -#if defined(CONFIG_DEMO_PHONE_CAMERA) +#if defined(CONFIG_APP_PHONE_CAMERA) void test_camera(void) { ph_open("Camera"); diff --git a/tests/phone-sms.c b/tests/phone-sms.c index b6b7919c..1e428c83 100644 --- a/tests/phone-sms.c +++ b/tests/phone-sms.c @@ -13,7 +13,7 @@ #include "data.h" #include "phone-test.h" -#if defined(CONFIG_DEMO_PHONE_MESSAGES) +#if defined(CONFIG_APP_PHONE_MESSAGES) int app_sms_test_offset(void); int app_sms_test_max_offset(void); diff --git a/tests/test-cellphone.c b/tests/test-cellphone.c index dc784c66..9053b6e9 100644 --- a/tests/test-cellphone.c +++ b/tests/test-cellphone.c @@ -139,7 +139,7 @@ static void test_wrap(void) "overlong word still terminates"); } -#if defined(CONFIG_DEMO_CALCULATOR) +#if defined(CONFIG_APP_CALCULATOR) static void test_calculator(void) { /* Opened from its icon, wherever the apps built put it */