diff --git a/.github/workflows/headless-test.yml b/.github/workflows/headless-test.yml index 89ccfcf..3d5fb99 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: | @@ -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/.github/workflows/main.yml b/.github/workflows/main.yml index 9fa52dd..55d4722 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 d6abc73..ccd4343 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,35 +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_CELLPHONE) += \ +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 \ + apps/cellphone/ui-input.c + +libapps.a_files-$(CONFIG_APP_CELLPHONE) += \ apps/cellphone/shell.c \ 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 \ - 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 + apps/cellphone/settings.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 @@ -259,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 @@ -300,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 @@ -332,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 5b7de9e..802222c 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/animation.c b/apps/animation.c index 2d3ff46..f161066 100644 --- a/apps/animation.c +++ b/apps/animation.c @@ -25,16 +25,13 @@ 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 - * 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); @@ -47,7 +44,9 @@ 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 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); } 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); @@ -90,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; @@ -135,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/apps/apps.c b/apps/apps.c index 796f0c8..7afe3f2 100644 --- a/apps/apps.c +++ b/apps/apps.c @@ -6,38 +6,48 @@ #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) - {"Calc", TWIN_SYM_CALCULATOR, 100, 100, 200, 200, apps_calc_start, NULL, - NULL}, +#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_APP_SNAKE) + {"Snake", TWIN_SYM_PLAY, 60, 40, 0, 0, apps_snake_start, NULL, NULL}, +#endif +#if defined(CONFIG_APP_PONG) + {"Pong", TWIN_SYM_PLAY, 90, 60, 0, 0, apps_pong_start, NULL, NULL}, +#endif +#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 9e00474..e5b0d05 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. */ @@ -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/calc.c b/apps/calc.c index 4cab58e..312e42f 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_APP_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_APP_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 eb64160..0000000 --- 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 6dc5fdf..e843356 100644 --- a/apps/cellphone/phone.h +++ b/apps/cellphone/phone.h @@ -51,6 +51,8 @@ #include #include +#include "apps.h" + /* * Geometry: portrait QVGA */ @@ -67,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 @@ -110,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); @@ -327,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); @@ -406,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, @@ -419,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. @@ -429,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); @@ -684,7 +719,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); @@ -709,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 dea789a..78c5230 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,36 +169,74 @@ 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[] = { - {"Phone", TWIN_SYM_PHONE, NULL, UI_RGB(0x5b8c45), app_dialer_open, NULL}, +#if defined(CONFIG_APP_PHONE_DIALER) + {"Phone", TWIN_SYM_PHONE, NULL, UI_RGB(0x5b8c45), app_dialer_open, NULL, + NULL}, +#endif +#if defined(CONFIG_APP_PHONE_CONTACTS) {"Contacts", TWIN_SYM_CONTACTS, NULL, UI_RGB(0x5a7896), app_contacts_open, + NULL, NULL}, +#endif +#if defined(CONFIG_APP_PHONE_MESSAGES) + {"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}, +#endif +#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_APP_PHONE_MUSIC) + {"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, +#endif +#if defined(CONFIG_APP_PHONE_PHOTOS) + {"Photos", TWIN_SYM_IMAGE, NULL, UI_RGB(0xbb6e3e), app_photos_open, NULL, NULL}, - {"Call Log", TWIN_SYM_HISTORY, NULL, UI_RGB(0x7a6a96), app_calllog_open, +#endif +#if defined(CONFIG_APP_PHONE_CAMERA) + {"Camera", TWIN_SYM_CAMERA, NULL, UI_RGB(0x6c7064), app_camera_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}, +#endif + {"Settings", TWIN_SYM_SETTINGS, NULL, UI_RGB(0x767b6c), app_settings_open, + NULL, NULL}, +#if defined(CONFIG_APP_PHONE_CALLLOG) + {"Call Log", TWIN_SYM_HISTORY, NULL, UI_RGB(0x7a6a96), app_calllog_open, + NULL, NULL}, +#endif +#if defined(CONFIG_APP_SNAKE) + {"Snake", NULL, app_snake_glyph, UI_RGB(0x4d8f86), app_snake_open, NULL, + apps_snake_start}, +#endif +#if defined(CONFIG_APP_PONG) + {"Pong", NULL, app_pong_glyph, UI_RGB(0x646c9a), app_pong_open, NULL, + apps_pong_start}, +#endif +#if defined(CONFIG_APP_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]))) /* 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 +246,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; @@ -353,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); @@ -467,9 +351,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; } @@ -526,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; @@ -543,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; } @@ -553,8 +433,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); @@ -592,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) @@ -617,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; } @@ -646,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 */ @@ -731,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); @@ -778,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 a8a8dba..5300faa 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 0c181e8..328c12c 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/clock.c b/apps/clock.c index 17b2fc1..e4d8227 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/apps/cellphone/games.c b/apps/games.c similarity index 73% rename from apps/cellphone/games.c rename to apps/games.c index d4dcc60..3655911 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 917b9cc..d7f3fba 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/image.c b/apps/image.c index 4693535..d9c6a43 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/apps/multi.c b/apps/multi.c index 4aed1fc..e029ecd 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/apps/cellphone/pong.c b/apps/pong.c similarity index 87% rename from apps/cellphone/pong.c rename to apps/pong.c index 9bdc566..ed4a1b3 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 dc30045..7c585e7 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 2879fab..c64e591 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/backend/headless.c b/backend/headless.c index 96a3c21..65a9e89 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; diff --git a/configs/Kconfig b/configs/Kconfig index 02880f8..5de75d0 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 launcher starts only the first application. + +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 @@ -278,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. @@ -294,96 +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 6 - range 1 10 - depends on DROP_SHADOW - help - Horizontal shadow offset in pixels. - Default matches a CSS-style soft shadow (โ‰ˆ6 px offset). - Larger values push shadow further right. - -config VERTICAL_OFFSET - int "Vertical offset" - default 6 - range 1 10 - depends on DROP_SHADOW - help - Vertical shadow offset in pixels. - Default matches a CSS-style soft shadow (โ‰ˆ6 px offset). - Larger values push shadow further down. - -config SHADOW_BLUR - int "Shadow blur radius" - default 12 - range 1 15 - depends on DROP_SHADOW - 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 - is computed while scanning out, so the radius does not change - its cost per pixel. - -endmenu - menu "Rendering" config SCRATCH_SIZE @@ -422,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" @@ -488,92 +487,170 @@ config TVG_MEMORY_BUDGET endmenu -menu "Demo Applications" +menu "Applications" + +comment "Pick any set: the launcher is built when at least one is chosen" -config DEMO_APPLICATIONS - bool "Build demo applications" +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 - 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. + 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 DEMO_CELLPHONE_RGB16 +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 + 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 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. -config DEMO_MULTI - bool "Build multi demo" +config APP_PHONE_DIALER + bool "Phone" default y - depends on DEMO_APPLICATIONS && WIDGETS 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. + Dialer keypad and the in-call screen. -config DEMO_CLOCK - bool "Build clock demo" +config APP_PHONE_CALLLOG + bool "Call log" default y - depends on DEMO_APPLICATIONS && WIDGETS + depends on APP_PHONE_DIALER help - Analog clock demonstration with real-time updates. - Showcases CORDIC trigonometry functions. - Demonstrates timeout callbacks and redraw logic. + Recent calls; tapping one calls it back through the dialer. -config DEMO_CALCULATOR - bool "Build calculator demo" +config APP_PHONE_MESSAGES + bool "Messages" default y - depends on DEMO_APPLICATIONS && WIDGETS help - Interactive calculator application. - Demonstrates button widgets and event handling. - Shows layout management and user input processing. + Text message conversation with an on-screen keyboard. -config DEMO_SPLINE - bool "Build spline demo" +config APP_PHONE_CONTACTS + bool "Contacts" default y - depends on DEMO_APPLICATIONS && WIDGETS + depends on APP_PHONE_DIALER && APP_PHONE_MESSAGES help - Bezier spline curve demonstration. - Shows advanced path rendering capabilities. - Demonstrates curve smoothing algorithms. + Contact list, details and an add form. A contact can be called or + messaged, so this needs the dialer and messages. -config DEMO_ANIMATION - bool "Build animation demo" +config APP_PHONE_MUSIC + bool "Music" default y - depends on DEMO_APPLICATIONS && WIDGETS help - Animation demonstration with moving objects. - Shows frame-based animation techniques. - Demonstrates work queue and timeout systems. + Track list and a now-playing screen. -config DEMO_IMAGE - bool "Build scalable image demo" - select LOADER_TVG +config APP_PHONE_PHOTOS + bool "Photos" 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. + Thumbnail grid and a swipeable photo viewer. + +config APP_PHONE_CAMERA + bool "Camera" + default y + help + Viewfinder with shutter, flash and mode controls. + +endif + +# Built when any game is: the arcade view they share +config APP_GAMES + def_bool APP_SNAKE || APP_PONG || APP_TETRIS + +# The UI kit the phone and the games draw with +config APP_UIKIT + def_bool APP_CELLPHONE || APP_GAMES + +# 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 c344b39..9cf1337 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 e45aff8..f973533 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/include/twin.h b/include/twin.h index c88feec..5a79620 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; /** @@ -553,8 +562,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. */ @@ -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); @@ -827,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 * @@ -1219,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/animation.c b/src/animation.c index 1fe3995..4701cb0 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/box.c b/src/box.c index fd90ed2..4cf0c8c 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-gif.c b/src/image-gif.c index e8b1e7c..5d0b03c 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 16a3373..5cba46f 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 b81c04f..91512e1 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 8363b79..2c267dc 100644 --- a/src/image-tvg.c +++ b/src/image-tvg.c @@ -22,32 +22,7 @@ (((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; -} +#define MAX(A, B) ((A) > (B) ? (A) : (B)) enum { /* end of document This command determines the end of file. */ @@ -432,11 +407,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 +474,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 +493,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 +516,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 +535,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 +826,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 +839,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 +880,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 +1094,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 +1115,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"); @@ -1178,6 +1131,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: @@ -1207,7 +1164,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 +1172,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; @@ -1228,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, @@ -1258,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/path.c b/src/path.c index 78a6913..98e139c 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/pixmap.c b/src/pixmap.c index f359d5a..fc81e71 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; } @@ -57,31 +50,43 @@ 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}; 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) @@ -257,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, @@ -267,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/poly.c b/src/poly.c index 6b9e15f..59b9460 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 @@ -323,6 +329,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 +356,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 +447,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/src/screen.c b/src/screen.c index f6477c0..dee0b73 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 f6f3cec..24ce564 100644 --- a/src/twin_private.h +++ b/src/twin_private.h @@ -60,45 +60,39 @@ 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); }; +/* 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) */ @@ -366,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 */ @@ -1320,6 +1315,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 +1551,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 { @@ -1976,6 +1982,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/widget.c b/src/widget.c index b4146f1..3863058 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/src/window.c b/src/window.c index 6079ce1..1d847c2 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 347f1d4..77d6494 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/cellphone.txt b/tests/golden/cellphone.txt index 2e643c1..aff4fdc 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 diff --git a/tests/golden/core.txt b/tests/golden/core.txt index b118002..7c120a8 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 f27581a..ca1ca18 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]; @@ -355,9 +362,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 @@ -371,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, @@ -389,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 @@ -514,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 6288381..c8d5514 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 f2c045d..9c293df 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_APP_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 a288fbd..30d2755 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) && defined(CONFIG_DEMO_MULTI) 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 48d1c3e..918ea59 100644 --- a/tests/phone-dialer.c +++ b/tests/phone-dialer.c @@ -10,6 +10,8 @@ #include "phone-test.h" +#if defined(CONFIG_APP_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_APP_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-games.c b/tests/phone-games.c index 85b5256..0217024 100644 --- a/tests/phone-games.c +++ b/tests/phone-games.c @@ -14,6 +14,8 @@ #include "phone-test.h" +#if defined(CONFIG_APP_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 diff --git a/tests/phone-music.c b/tests/phone-music.c index 79f7261..f8f11ec 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_APP_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 09178e1..ee1f3e8 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_APP_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_APP_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 ac88b77..0ab22c0 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 7f47d9b..1e428c8 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_APP_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 abe26e8..9053b6e 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; @@ -138,11 +139,16 @@ static void test_wrap(void) "overlong word still terminates"); } +#if defined(CONFIG_APP_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"); @@ -150,6 +156,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 +187,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 @@ -252,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(); diff --git a/tests/test-core.c b/tests/test-core.c index 5b61533..fe5316d 100644 --- a/tests/test-core.c +++ b/tests/test-core.c @@ -29,6 +29,65 @@ 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) { th_t *t = th_create(W, H); @@ -1132,12 +1191,26 @@ 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, "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 && @@ -1274,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); @@ -1333,7 +1444,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 +1494,88 @@ 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); +} + +/* 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); + + /* 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 -#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 +1583,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 */ @@ -1439,6 +1795,7 @@ int main(void) test_row_fallback(); #if defined(CONFIG_DROP_SHADOW) test_shadow(); + test_shadow_mask(); #endif test_rgb16_and_opacity(); test_stub(); @@ -1462,12 +1819,19 @@ int main(void) test_widget_teardown(); test_toplevel_background(); test_box_background(); + test_box_stretch(); #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(); + test_tvg_even_odd(); +#endif +#if defined(CONFIG_LOADER_PNG) + test_png_16bit(); #endif test_leaks(); twin_cache_flush(); diff --git a/tests/test-wm.c b/tests/test-wm.c index cd2a1cc..cc4fe3e 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");